From bd24083f78efc1caa74a7b9313a140c792a76d26 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:01:08 +1000 Subject: [PATCH 01/33] Add Arduino API coverage plan Inventory of Arduino core API and bundled libraries against what Rubyduino currently exposes. Tracks what's covered vs. missing as a checklist to work through. --- plans/arduino_api_coverage.md | 136 ++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 plans/arduino_api_coverage.md diff --git a/plans/arduino_api_coverage.md b/plans/arduino_api_coverage.md new file mode 100644 index 0000000..13d440e --- /dev/null +++ b/plans/arduino_api_coverage.md @@ -0,0 +1,136 @@ +# Arduino Standard Library Coverage in Rubyduino + +Comparison of Rubyduino's exposed API (`lib/rubyduino/arduino_uno.rb`, `lib/rubyduino/spinel_arduino_codegen.rb`) against the Arduino core API (``) plus the libraries bundled with the Arduino IDE. + +## Digital I/O + +- [x] `pinMode` +- [x] `digitalRead` +- [x] `digitalWrite` + +## Analog I/O + +- [x] `analogRead` +- [x] `analogWrite` +- [ ] `analogReference` + +## Time + +- [x] `delay` (as `delay_ms`) +- [x] `delayMicroseconds` (as `delay_us`) +- [x] `millis` +- [x] `micros` + +## Advanced I/O + +- [x] `pulseIn` +- [x] `pulseIn` with timeout (as `pulse_in_timeout`) +- [x] `shiftIn` +- [x] `shiftOut` +- [ ] `tone` +- [ ] `noTone` +- [ ] `pulseInLong` + +## Interrupts + +- [x] `interrupts` +- [x] `noInterrupts` (as `no_interrupts`) +- [ ] `attachInterrupt` +- [ ] `detachInterrupt` +- [ ] `digitalPinToInterrupt` + +## Serial + +- [x] `Serial.begin` +- [x] `Serial.available` +- [x] `Serial.read` +- [x] `Serial.write` (single byte) +- [x] `Serial.print` — string + int (via codegen) +- [x] `Serial.println` — string + int (via codegen) +- [ ] `Serial.print`/`println` for float/double, byte, with format args (BIN/HEX/OCT/DEC, decimal places) +- [ ] `Serial.write(buf, len)` overload +- [ ] `Serial.end` +- [ ] `Serial.flush` +- [ ] `Serial.peek` +- [ ] `Serial.setTimeout` +- [ ] `Serial.availableForWrite` +- [ ] `Serial.find` +- [ ] `Serial.findUntil` +- [ ] `Serial.parseInt` +- [ ] `Serial.parseFloat` +- [ ] `Serial.readBytes` +- [ ] `Serial.readBytesUntil` +- [ ] `Serial.readString` +- [ ] `Serial.readStringUntil` +- [ ] `serialEvent` callback + +## Random + +- [x] `random(a..b)` with literal range (via codegen) +- [ ] `random` with non-literal range (variables fall through to plain `rand`) +- [ ] `randomSeed` + +## Bits & Bytes + +- [ ] `bit` +- [ ] `bitRead` +- [ ] `bitWrite` +- [ ] `bitSet` +- [ ] `bitClear` +- [ ] `highByte` +- [ ] `lowByte` + +## Math / Utility Helpers + +- [ ] `map` +- [ ] `constrain` +- [ ] `sq` +- [x] `abs`, `min`, `max`, `pow`, `sqrt` — provided by Ruby itself, not the gem + +## Character Classification + +- [ ] `isAlpha` +- [ ] `isDigit` +- [ ] `isSpace` +- [ ] `isWhitespace` +- [ ] `isAlphaNumeric` +- [ ] `isAscii` +- [ ] `isControl` +- [ ] `isHexadecimalDigit` +- [ ] `isLowerCase` +- [ ] `isUpperCase` +- [ ] `isPrintable` +- [ ] `isPunct` + +## Other Core + +- [ ] `yield` (cooperative yield hook) +- [ ] Arduino `String` class +- [ ] `PROGMEM` / `pgm_read_*` + +## Bundled Libraries + +- [ ] `Wire` (I²C) +- [ ] `SPI` +- [ ] `EEPROM` +- [ ] `SoftwareSerial` +- [ ] `Servo` +- [ ] `Stepper` +- [ ] `LiquidCrystal` +- [ ] `SD` + +## Constants Provided + +- [x] `LOW`, `HIGH` +- [x] `INPUT`, `OUTPUT`, `INPUT_PULLUP` +- [x] `A0`–`A5` +- [x] `LED_BUILTIN` +- [x] `LSBFIRST`, `MSBFIRST` + +## Highest-Impact Gaps for Sketch Authors + +1. `tone` / `noTone` +2. `map` / `constrain` +3. `attachInterrupt` / `detachInterrupt` +4. Richer `Serial.print` formatting (float, HEX/BIN/etc.) +5. `Wire` / `SPI` / `EEPROM` libraries From 1c81252fe9d02a7a57fc3a0bc236b5b510a58582 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:04:39 +1000 Subject: [PATCH 02/33] Add bits & bytes helpers (bit, bit_read/write/set/clear, high_byte, low_byte) FFI bindings + C runtime + tests covering codegen, native logic, and AVR compile. Brings parity with Arduino bit/byte macros, adapted to Ruby's no-byref-ints convention (mutators return the new value). --- lib/rubyduino/arduino_uno.rb | 35 ++++++++ lib/rubyduino/sp_runtime.h | 28 +++++++ test/support/compile_helper.rb | 142 +++++++++++++++++++++++++++++++++ test/test_bits_and_bytes.rb | 71 +++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 test/support/compile_helper.rb create mode 100644 test/test_bits_and_bytes.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index df028e8..465a11d 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -40,6 +40,13 @@ module ArduinoUNO ffi_func :shift_out, [:uint8, :uint8, :uint8, :uint8], :void ffi_func :interrupts, [], :void ffi_func :no_interrupts, [], :void + ffi_func :bit, [:uint8], :uint32 + ffi_func :bit_read, [:uint32, :uint8], :uint8 + ffi_func :bit_write, [:uint32, :uint8, :uint8], :uint32 + ffi_func :bit_set, [:uint32, :uint8], :uint32 + ffi_func :bit_clear, [:uint32, :uint8], :uint32 + ffi_func :high_byte, [:uint16], :uint8 + ffi_func :low_byte, [:uint16], :uint8 end def pin_mode(pin, mode) @@ -117,3 +124,31 @@ def interrupts def no_interrupts ArduinoUNO.no_interrupts end + +def bit(n) + ArduinoUNO.bit(n) +end + +def bit_read(value, n) + ArduinoUNO.bit_read(value, n) +end + +def bit_write(value, n, bitvalue) + ArduinoUNO.bit_write(value, n, bitvalue) +end + +def bit_set(value, n) + ArduinoUNO.bit_set(value, n) +end + +def bit_clear(value, n) + ArduinoUNO.bit_clear(value, n) +end + +def high_byte(value) + ArduinoUNO.high_byte(value) +end + +def low_byte(value) + ArduinoUNO.low_byte(value) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 2769cab..302ea37 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -543,6 +543,34 @@ void no_interrupts(void) { cli(); } +uint32_t bit(uint8_t n) { + return (uint32_t)1 << n; +} + +uint8_t bit_read(uint32_t value, uint8_t n) { + return (uint8_t)((value >> n) & (uint32_t)1); +} + +uint32_t bit_set(uint32_t value, uint8_t n) { + return value | ((uint32_t)1 << n); +} + +uint32_t bit_clear(uint32_t value, uint8_t n) { + return value & (uint32_t)~((uint32_t)1 << n); +} + +uint32_t bit_write(uint32_t value, uint8_t n, uint8_t bitvalue) { + return bitvalue ? bit_set(value, n) : bit_clear(value, n); +} + +uint8_t high_byte(uint16_t value) { + return (uint8_t)((value >> 8) & 0xFF); +} + +uint8_t low_byte(uint16_t value) { + return (uint8_t)(value & 0xFF); +} + #define fflush(stream) ((void)0) #endif diff --git a/test/support/compile_helper.rb b/test/support/compile_helper.rb new file mode 100644 index 0000000..44df73e --- /dev/null +++ b/test/support/compile_helper.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require "fileutils" +require "open3" +require "rbconfig" +require "tmpdir" + +module CompileHelper + ROOT = File.expand_path("../..", __dir__) + SPINEL_DIR = File.join(ROOT, "vendor/spinel") + RUBYDUINO_DIR = File.join(ROOT, "lib/rubyduino") + PARSE_BIN = File.join(SPINEL_DIR, "spinel_parse") + CODEGEN_RB = File.join(RUBYDUINO_DIR, "spinel_arduino_codegen.rb") + ARDUINO_UNO_RB = File.join(RUBYDUINO_DIR, "arduino_uno.rb") + ENTRY_C = File.join(RUBYDUINO_DIR, "arduino_entry.c") + + module_function + + def parser_available? + File.executable?(PARSE_BIN) + end + + def avr_gcc_available? + !which("avr-gcc").nil? + end + + def avr_objdump_available? + !which("avr-objdump").nil? + end + + def simavr_available? + !which("simavr").nil? + end + + def which(name) + ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir| + path = File.join(dir, name) + return path if File.file?(path) && File.executable?(path) + end + nil + end + + def compile_ruby_to_c(sketch) + raise "spinel_parse not built; run make in vendor/spinel" unless parser_available? + + Dir.mktmpdir("rubyduino_test") do |dir| + source = File.join(dir, "sketch.rb") + ast = File.join(dir, "sketch.ast") + out_c = File.join(dir, "sketch.c") + + header = File.read(ARDUINO_UNO_RB) + File.write(source, "#{header}\n#{sketch}") + + run!(PARSE_BIN, source, ast) + run!(RbConfig.ruby, CODEGEN_RB, ast, out_c) + + File.read(out_c) + end + end + + def compile_ruby_to_avr_obj(sketch, mcu: "atmega328p") + raise "avr-gcc not installed" unless avr_gcc_available? + + c_code = compile_ruby_to_c(sketch) + + Dir.mktmpdir("rubyduino_avr") do |dir| + c_file = File.join(dir, "sketch.c") + obj_file = File.join(dir, "sketch.o") + File.write(c_file, c_code) + + flags = ["-Os", "-DF_CPU=16000000UL", "-mmcu=#{mcu}", + "-I#{RUBYDUINO_DIR}", "-I#{File.join(SPINEL_DIR, "lib")}", + "-Dmain=sp_arduino_user_main"] + run!("avr-gcc", *flags, "-c", c_file, "-o", obj_file) + + yield obj_file, c_file, c_code if block_given? + File.binread(obj_file) + end + end + + def compile_ruby_to_avr_elf(sketch, mcu: "atmega328p") + raise "avr-gcc not installed" unless avr_gcc_available? + + c_code = compile_ruby_to_c(sketch) + + Dir.mktmpdir("rubyduino_elf") do |dir| + c_file = File.join(dir, "sketch.c") + app_obj = File.join(dir, "sketch.o") + entry_obj = File.join(dir, "entry.o") + elf = File.join(dir, "sketch.elf") + File.write(c_file, c_code) + + flags = ["-Os", "-DF_CPU=16000000UL", "-mmcu=#{mcu}", + "-I#{RUBYDUINO_DIR}", "-I#{File.join(SPINEL_DIR, "lib")}"] + run!("avr-gcc", *flags, "-Dmain=sp_arduino_user_main", "-c", c_file, "-o", app_obj) + run!("avr-gcc", *flags, "-c", ENTRY_C, "-o", entry_obj) + run!("avr-gcc", "-Os", "-DF_CPU=16000000UL", "-mmcu=#{mcu}", + app_obj, entry_obj, "-o", elf) + + result = { elf: File.binread(elf), c_code: c_code } + if block_given? + yield elf, c_file, c_code + end + result + end + end + + def avr_objdump_disassembly(elf_path) + out, status = Open3.capture2e("avr-objdump", "-d", elf_path) + raise "avr-objdump failed: #{out}" unless status.success? + out + end + + def avr_nm_symbols(obj_path) + out, status = Open3.capture2e("avr-nm", obj_path) + raise "avr-nm failed: #{out}" unless status.success? + out + end + + def run_native_program(c_source, defines: []) + raise "no host C compiler" unless which("clang") || which("cc") + + Dir.mktmpdir("rubyduino_native") do |dir| + c_file = File.join(dir, "test.c") + bin = File.join(dir, "test") + File.write(c_file, c_source) + + cc = which("clang") || which("cc") + flags = defines.map { |d| "-D#{d}" } + run!(cc, "-O0", "-std=c11", *flags, c_file, "-o", bin) + + out, status = Open3.capture2e(bin) + [out, status.success?] + end + end + + def run!(*cmd) + out, status = Open3.capture2e(*cmd) + raise "#{cmd.join(" ")} failed:\n#{out}" unless status.success? + out + end +end diff --git a/test/test_bits_and_bytes.rb b/test/test_bits_and_bytes.rb new file mode 100644 index 0000000..49e4f4c --- /dev/null +++ b/test/test_bits_and_bytes.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestBitsAndBytes < Minitest::Test + def test_codegen_emits_bit_helpers_as_c_calls + sketch = <<~RUBY + mask = bit(3) + b = bit_read(mask, 3) + mask = bit_set(mask, 5) + mask = bit_clear(mask, 3) + mask = bit_write(mask, 0, 1) + hi = high_byte(0xABCD) + lo = low_byte(0xABCD) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[bit( bit_read( bit_set( bit_clear( bit_write( high_byte( low_byte(].each do |fn| + assert_includes c, fn, "expected #{fn}…) call in generated C" + end + end + + def test_runtime_logic_against_arduino_semantics + program = <<~C + #include + #include + static uint32_t bit_set(uint32_t v, uint8_t n) { return v | ((uint32_t)1 << n); } + static uint32_t bit_clear(uint32_t v, uint8_t n) { return v & (uint32_t)~((uint32_t)1 << n); } + static uint32_t bit_write(uint32_t v, uint8_t n, uint8_t b) { return b ? bit_set(v, n) : bit_clear(v, n); } + static uint8_t bit_read(uint32_t v, uint8_t n) { return (uint8_t)((v >> n) & 1u); } + static uint32_t bit_(uint8_t n) { return (uint32_t)1 << n; } + static uint8_t high_byte(uint16_t v) { return (uint8_t)((v >> 8) & 0xFFu); } + static uint8_t low_byte(uint16_t v) { return (uint8_t)(v & 0xFFu); } + + int main(void) { + printf("%lu\\n", (unsigned long)bit_(0)); /* 1 */ + printf("%lu\\n", (unsigned long)bit_(7)); /* 128 */ + printf("%u\\n", bit_read(0b10100000u, 7)); /* 1 */ + printf("%u\\n", bit_read(0b10100000u, 6)); /* 0 */ + printf("%lu\\n", (unsigned long)bit_set(0u, 4)); /* 16 */ + printf("%lu\\n", (unsigned long)bit_clear(0xFFu, 0)); /* 254 */ + printf("%lu\\n", (unsigned long)bit_write(0xF0u, 0, 1));/* 241 */ + printf("%lu\\n", (unsigned long)bit_write(0xF0u, 4, 0));/* 224 */ + printf("%u\\n", high_byte(0xABCD)); /* 171 */ + printf("%u\\n", low_byte(0xABCD)); /* 205 */ + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, "native compile failed:\n#{out}" + assert_equal %w[1 128 1 0 16 254 241 224 171 205], out.split + end + + def test_avr_sketch_compiles + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + x = bit(3) + x = bit_set(x, 1) + x = bit_clear(x, 0) + x = bit_write(x, 7, 1) + r = bit_read(x, 7) + h = high_byte(0xABCD) + l = low_byte(0xABCD) + digital_write(13, r) + digital_write(12, h & 1) + digital_write(11, l & 1) + RUBY + obj_bytes = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj_bytes + end +end From a463652ae07909f44a5eb186b083209243699619 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:05:26 +1000 Subject: [PATCH 03/33] Add math helpers (map_value, constrain, sq) Named map_value to avoid clashing with Ruby's Enumerable#map. Uses int64 intermediates in map_value to match Arduino's overflow-safe math on long arguments. --- lib/rubyduino/arduino_uno.rb | 15 ++++++++ lib/rubyduino/sp_runtime.h | 25 ++++++++++++++ test/test_math_helpers.rb | 66 ++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 test/test_math_helpers.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 465a11d..5840dff 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -47,6 +47,9 @@ module ArduinoUNO ffi_func :bit_clear, [:uint32, :uint8], :uint32 ffi_func :high_byte, [:uint16], :uint8 ffi_func :low_byte, [:uint16], :uint8 + ffi_func :map_value, [:int32, :int32, :int32, :int32, :int32], :int32 + ffi_func :constrain, [:int32, :int32, :int32], :int32 + ffi_func :sq, [:int32], :int32 end def pin_mode(pin, mode) @@ -152,3 +155,15 @@ def high_byte(value) def low_byte(value) ArduinoUNO.low_byte(value) end + +def map_value(value, from_low, from_high, to_low, to_high) + ArduinoUNO.map_value(value, from_low, from_high, to_low, to_high) +end + +def constrain(value, low, high) + ArduinoUNO.constrain(value, low, high) +end + +def sq(value) + ArduinoUNO.sq(value) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 302ea37..9da6b64 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -571,6 +571,31 @@ uint8_t low_byte(uint16_t value) { return (uint8_t)(value & 0xFF); } +int32_t map_value(int32_t value, int32_t from_low, int32_t from_high, int32_t to_low, int32_t to_high) { + int32_t from_span = from_high - from_low; + int32_t to_span = to_high - to_low; + + if (from_span == 0) { + return to_low; + } + + return (int32_t)(((int64_t)(value - from_low) * (int64_t)to_span) / (int64_t)from_span) + to_low; +} + +int32_t constrain(int32_t value, int32_t low, int32_t high) { + if (value < low) { + return low; + } + if (value > high) { + return high; + } + return value; +} + +int32_t sq(int32_t value) { + return value * value; +} + #define fflush(stream) ((void)0) #endif diff --git a/test/test_math_helpers.rb b/test/test_math_helpers.rb new file mode 100644 index 0000000..d792d05 --- /dev/null +++ b/test/test_math_helpers.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestMathHelpers < Minitest::Test + def test_codegen_emits_helpers + sketch = <<~RUBY + a = map_value(512, 0, 1023, 0, 255) + b = constrain(150, 0, 100) + c = sq(7) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "map_value(" + assert_includes c, "constrain(" + assert_includes c, "sq(" + end + + def test_runtime_logic_against_arduino_semantics + program = <<~C + #include + #include + static int32_t map_value(int32_t v, int32_t fl, int32_t fh, int32_t tl, int32_t th) { + int32_t fs = fh - fl; + int32_t ts = th - tl; + if (fs == 0) return tl; + return (int32_t)(((int64_t)(v - fl) * (int64_t)ts) / (int64_t)fs) + tl; + } + static int32_t constrain_(int32_t v, int32_t lo, int32_t hi) { + if (v < lo) return lo; if (v > hi) return hi; return v; + } + static int32_t sq_(int32_t v) { return v * v; } + + int main(void) { + printf("%d\\n", map_value(512, 0, 1023, 0, 255)); /* ~127 */ + printf("%d\\n", map_value(0, 0, 1023, 0, 255)); /* 0 */ + printf("%d\\n", map_value(1023, 0, 1023, 0, 255)); /* 255 */ + printf("%d\\n", map_value(50, 0, 100, 100, 0)); /* 50 (reversed) */ + printf("%d\\n", map_value(5, 5, 5, 0, 100)); /* 0 (zero span) */ + printf("%d\\n", constrain_(150, 0, 100)); /* 100 */ + printf("%d\\n", constrain_(-5, 0, 100)); /* 0 */ + printf("%d\\n", constrain_(50, 0, 100)); /* 50 */ + printf("%d\\n", sq_(7)); /* 49 */ + printf("%d\\n", sq_(-9)); /* 81 */ + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal %w[127 0 255 50 0 100 0 50 49 81], out.split + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + reading = analog_read(ArduinoUNO::A0) + pwm = map_value(reading, 0, 1023, 0, 255) + pwm = constrain(pwm, 0, 200) + analog_write(9, pwm) + x = sq(reading) + digital_write(13, x & 1) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From bccb421ecb30015a2e1985b7b62782f288035d90 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:06:19 +1000 Subject: [PATCH 04/33] Add character classification helpers (12 is_* predicates) Each is a pure logic function returning 0/1. Names are snake_case (is_alpha, is_digit, is_hexadecimal_digit, ...) per Ruby convention. --- lib/rubyduino/arduino_uno.rb | 60 +++++++++++++++++++++++ lib/rubyduino/sp_runtime.h | 55 +++++++++++++++++++++ test/test_char_classification.rb | 83 ++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 test/test_char_classification.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 5840dff..f28d160 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -50,6 +50,18 @@ module ArduinoUNO ffi_func :map_value, [:int32, :int32, :int32, :int32, :int32], :int32 ffi_func :constrain, [:int32, :int32, :int32], :int32 ffi_func :sq, [:int32], :int32 + ffi_func :is_alpha, [:int], :int + ffi_func :is_digit, [:int], :int + ffi_func :is_alpha_numeric, [:int], :int + ffi_func :is_space, [:int], :int + ffi_func :is_whitespace, [:int], :int + ffi_func :is_upper_case, [:int], :int + ffi_func :is_lower_case, [:int], :int + ffi_func :is_ascii, [:int], :int + ffi_func :is_control, [:int], :int + ffi_func :is_printable, [:int], :int + ffi_func :is_punct, [:int], :int + ffi_func :is_hexadecimal_digit, [:int], :int end def pin_mode(pin, mode) @@ -167,3 +179,51 @@ def constrain(value, low, high) def sq(value) ArduinoUNO.sq(value) end + +def is_alpha(c) + ArduinoUNO.is_alpha(c) +end + +def is_digit(c) + ArduinoUNO.is_digit(c) +end + +def is_alpha_numeric(c) + ArduinoUNO.is_alpha_numeric(c) +end + +def is_space(c) + ArduinoUNO.is_space(c) +end + +def is_whitespace(c) + ArduinoUNO.is_whitespace(c) +end + +def is_upper_case(c) + ArduinoUNO.is_upper_case(c) +end + +def is_lower_case(c) + ArduinoUNO.is_lower_case(c) +end + +def is_ascii(c) + ArduinoUNO.is_ascii(c) +end + +def is_control(c) + ArduinoUNO.is_control(c) +end + +def is_printable(c) + ArduinoUNO.is_printable(c) +end + +def is_punct(c) + ArduinoUNO.is_punct(c) +end + +def is_hexadecimal_digit(c) + ArduinoUNO.is_hexadecimal_digit(c) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 9da6b64..db79cd0 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -596,6 +596,61 @@ int32_t sq(int32_t value) { return value * value; } +int is_alpha(int c) { + return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) ? 1 : 0; +} + +int is_digit(int c) { + return (c >= '0' && c <= '9') ? 1 : 0; +} + +int is_alpha_numeric(int c) { + return (is_alpha(c) || is_digit(c)) ? 1 : 0; +} + +int is_space(int c) { + return (c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r') ? 1 : 0; +} + +int is_whitespace(int c) { + return (c == ' ' || c == '\t') ? 1 : 0; +} + +int is_upper_case(int c) { + return (c >= 'A' && c <= 'Z') ? 1 : 0; +} + +int is_lower_case(int c) { + return (c >= 'a' && c <= 'z') ? 1 : 0; +} + +int is_ascii(int c) { + return (c >= 0 && c <= 127) ? 1 : 0; +} + +int is_control(int c) { + return ((c >= 0 && c <= 31) || c == 127) ? 1 : 0; +} + +int is_printable(int c) { + return (c >= 32 && c <= 126) ? 1 : 0; +} + +int is_punct(int c) { + if (c >= '!' && c <= '/') return 1; + if (c >= ':' && c <= '@') return 1; + if (c >= '[' && c <= '`') return 1; + if (c >= '{' && c <= '~') return 1; + return 0; +} + +int is_hexadecimal_digit(int c) { + if (c >= '0' && c <= '9') return 1; + if (c >= 'a' && c <= 'f') return 1; + if (c >= 'A' && c <= 'F') return 1; + return 0; +} + #define fflush(stream) ((void)0) #endif diff --git a/test/test_char_classification.rb b/test/test_char_classification.rb new file mode 100644 index 0000000..b73fa4e --- /dev/null +++ b/test/test_char_classification.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestCharClassification < Minitest::Test + HELPERS = %i[ + is_alpha is_digit is_alpha_numeric is_space is_whitespace + is_upper_case is_lower_case is_ascii is_control is_printable + is_punct is_hexadecimal_digit + ].freeze + + def test_codegen_emits_each_helper + sketch = HELPERS.map { |h| "x = #{h}(65)" }.join("\n") + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_runtime_logic_against_arduino_semantics + program = <<~C + #include + static int is_alpha(int c){return ((c>='A'&&c<='Z')||(c>='a'&&c<='z'))?1:0;} + static int is_digit(int c){return (c>='0'&&c<='9')?1:0;} + static int is_alpha_numeric(int c){return (is_alpha(c)||is_digit(c))?1:0;} + static int is_space(int c){return (c==' '||c=='\\t'||c=='\\n'||c=='\\v'||c=='\\f'||c=='\\r')?1:0;} + static int is_whitespace(int c){return (c==' '||c=='\\t')?1:0;} + static int is_upper_case(int c){return (c>='A'&&c<='Z')?1:0;} + static int is_lower_case(int c){return (c>='a'&&c<='z')?1:0;} + static int is_ascii(int c){return (c>=0&&c<=127)?1:0;} + static int is_control(int c){return ((c>=0&&c<=31)||c==127)?1:0;} + static int is_printable(int c){return (c>=32&&c<=126)?1:0;} + static int is_punct(int c){ + if(c>='!'&&c<='/')return 1; + if(c>=':'&&c<='@')return 1; + if(c>='['&&c<='`')return 1; + if(c>='{'&&c<='~')return 1; + return 0; + } + static int is_hexadecimal_digit(int c){ + if(c>='0'&&c<='9')return 1; + if(c>='a'&&c<='f')return 1; + if(c>='A'&&c<='F')return 1; + return 0; + } + int main(void){ + printf("%d %d %d %d %d %d %d %d %d %d %d %d\\n", + is_alpha('A'), /* 1 */ + is_alpha('5'), /* 0 */ + is_digit('7'), /* 1 */ + is_alpha_numeric('z'), /* 1 */ + is_space('\\n'), /* 1 */ + is_whitespace('\\n'), /* 0 - only space/tab */ + is_upper_case('q'), /* 0 */ + is_lower_case('q'), /* 1 */ + is_ascii(200), /* 0 */ + is_control(0x1B), /* 1 */ + is_printable(' '),/* 1 */ + is_punct(',')); /* 1 */ + printf("%d %d %d\\n", + is_hexadecimal_digit('F'), + is_hexadecimal_digit('g'), + is_hexadecimal_digit('9')); + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + lines = out.lines.map(&:strip) + assert_equal "1 0 1 1 1 0 0 1 0 1 1 1", lines[0] + assert_equal "1 0 1", lines[1] + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + ch = serial_read + digital_write(13, 1) if is_alpha(ch) == 1 + digital_write(12, 1) if is_digit(ch) == 1 + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From a7de5aa19868a28865c39fbc4a7230d57b637d57 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:09:12 +1000 Subject: [PATCH 05/33] Add random_seed, random_range, random_max + ?-suffixed predicate aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit random_seed, random_range, random_max as FFI bindings backed by C runtime that wraps stdlib's rand/srand. Codegen for non-literal rand(low..high) now compiles to random_range(low, high+1) instead of falling through to a meaningless Spinel default. Predicate aliases (is_alpha?, is_digit?, ...) added for the 12 character classification helpers — Ruby idiom for boolean-returning methods. Spinel handles ? cleanly (translates to sp_*_p). --- lib/rubyduino/arduino_uno.rb | 63 ++++++++++++++++++++ lib/rubyduino/sp_runtime.h | 23 ++++++++ lib/rubyduino/spinel_arduino_codegen.rb | 18 ++++-- test/test_char_classification.rb | 25 ++++++++ test/test_random.rb | 78 +++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 test/test_random.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index f28d160..2b11a04 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -62,6 +62,9 @@ module ArduinoUNO ffi_func :is_printable, [:int], :int ffi_func :is_punct, [:int], :int ffi_func :is_hexadecimal_digit, [:int], :int + ffi_func :random_seed, [:uint32], :void + ffi_func :random_range, [:int32, :int32], :int32 + ffi_func :random_max, [:int32], :int32 end def pin_mode(pin, mode) @@ -227,3 +230,63 @@ def is_punct(c) def is_hexadecimal_digit(c) ArduinoUNO.is_hexadecimal_digit(c) end + +def is_alpha?(c) + ArduinoUNO.is_alpha(c) == 1 +end + +def is_digit?(c) + ArduinoUNO.is_digit(c) == 1 +end + +def is_alpha_numeric?(c) + ArduinoUNO.is_alpha_numeric(c) == 1 +end + +def is_space?(c) + ArduinoUNO.is_space(c) == 1 +end + +def is_whitespace?(c) + ArduinoUNO.is_whitespace(c) == 1 +end + +def is_upper_case?(c) + ArduinoUNO.is_upper_case(c) == 1 +end + +def is_lower_case?(c) + ArduinoUNO.is_lower_case(c) == 1 +end + +def is_ascii?(c) + ArduinoUNO.is_ascii(c) == 1 +end + +def is_control?(c) + ArduinoUNO.is_control(c) == 1 +end + +def is_printable?(c) + ArduinoUNO.is_printable(c) == 1 +end + +def is_punct?(c) + ArduinoUNO.is_punct(c) == 1 +end + +def is_hexadecimal_digit?(c) + ArduinoUNO.is_hexadecimal_digit(c) == 1 +end + +def random_seed(seed) + ArduinoUNO.random_seed(seed) +end + +def random_range(low, high) + ArduinoUNO.random_range(low, high) +end + +def random_max(high) + ArduinoUNO.random_max(high) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index db79cd0..818cfc6 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -651,6 +651,29 @@ int is_hexadecimal_digit(int c) { return 0; } +void random_seed(uint32_t seed) { + if (seed == 0) { + return; + } + srand((unsigned int)seed); +} + +int32_t random_max(int32_t high) { + if (high <= 0) { + return 0; + } + return (int32_t)((unsigned long)rand() % (unsigned long)high); +} + +int32_t random_range(int32_t low, int32_t high) { + int32_t span; + if (high <= low) { + return low; + } + span = high - low; + return low + (int32_t)((unsigned long)rand() % (unsigned long)span); +} + #define fflush(stream) ((void)0) #endif diff --git a/lib/rubyduino/spinel_arduino_codegen.rb b/lib/rubyduino/spinel_arduino_codegen.rb index 940cf04..0367516 100644 --- a/lib/rubyduino/spinel_arduino_codegen.rb +++ b/lib/rubyduino/spinel_arduino_codegen.rb @@ -80,15 +80,21 @@ def compile_arduino_rand(nid) left = @nd_left[arg] right = @nd_right[arg] - return nil unless integer_literal_node?(left) && integer_literal_node?(right) - first = @nd_value[left].to_i - last = @nd_value[right].to_i - return "0" if last < first + if integer_literal_node?(left) && integer_literal_node?(right) + first = @nd_value[left].to_i + last = @nd_value[right].to_i + return "0" if last < first + + @needs_rand = 1 + span = last - first + 1 + return "((mrb_int)(#{first} + (rand() % #{span})))" + end @needs_rand = 1 - span = last - first + 1 - "((mrb_int)(#{first} + (rand() % #{span})))" + left_c = compile_expr(left) + right_c = compile_expr(right) + "((mrb_int)random_range((int32_t)(#{left_c}), (int32_t)(#{right_c}) + 1))" end def integer_literal_node?(nid) diff --git a/test/test_char_classification.rb b/test/test_char_classification.rb index b73fa4e..011c14a 100644 --- a/test/test_char_classification.rb +++ b/test/test_char_classification.rb @@ -80,4 +80,29 @@ def test_avr_compile obj = CompileHelper.compile_ruby_to_avr_obj(sketch) refute_empty obj end + + def test_predicate_aliases_compile + sketch = <<~RUBY + ch = serial_read + digital_write(13, 1) if is_alpha?(ch) + digital_write(12, 1) if is_digit?(ch) + digital_write(11, 1) if is_hexadecimal_digit?(ch) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + # Predicate aliases route through sp_is_*_p inline functions. + assert_includes c, "sp_is_alpha_p(" + assert_includes c, "sp_is_digit_p(" + assert_includes c, "sp_is_hexadecimal_digit_p(" + end + + def test_predicate_aliases_compile_to_avr + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + ch = serial_read + digital_write(13, 1) if is_alpha?(ch) + digital_write(12, 1) if is_space?(ch) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end end diff --git a/test/test_random.rb b/test/test_random.rb new file mode 100644 index 0000000..1833a1f --- /dev/null +++ b/test/test_random.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestRandom < Minitest::Test + def test_random_seed_codegen + sketch = "random_seed(42)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "random_seed(" + end + + def test_literal_rand_range_still_inlined + sketch = "n = rand(1..10)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "rand() % 10" + end + + def test_non_literal_rand_range_calls_random_range + sketch = <<~RUBY + low = 5 + high = 20 + n = rand(low..high) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "random_range(", "non-literal range should call random_range()" + end + + def test_random_range_helper_callable_directly + sketch = "n = random_range(0, 100)" + c = CompileHelper.compile_ruby_to_c(sketch) + # Spinel routes the top-level `def random_range` through sp_random_range, + # which itself calls the FFI'd C `random_range`. + assert_includes c, "sp_random_range(" + assert_includes c, "extern int32_t random_range(int32_t, int32_t);" + end + + def test_random_range_runtime_logic + program = <<~C + #include + #include + #include + static int32_t random_range(int32_t lo, int32_t hi) { + if (hi <= lo) return lo; + int32_t span = hi - lo; + return lo + (int32_t)((unsigned long)rand() % (unsigned long)span); + } + int main(void) { + srand(7); + for (int i = 0; i < 200; i++) { + int32_t v = random_range(10, 20); + if (v < 10 || v >= 20) { printf("BAD %d\\n", v); return 1; } + } + /* degenerate range returns low */ + printf("%d\\n", random_range(5, 5)); + printf("%d\\n", random_range(7, 1)); + printf("OK\\n"); + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal %w[5 7 OK], out.split + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + random_seed(millis) + low = 100 + high = 700 + delay_ms(rand(low..high)) + digital_write(13, random_range(0, 2)) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From dfc14886a442a7cd070f064558841f833294cf56 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:11:01 +1000 Subject: [PATCH 06/33] Add tone()/no_tone() with Timer2 CTC + COMPA ISR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single FFI binding tone_for(pin, freq, duration_ms); top-level Ruby tone(pin, freq, duration_ms = 0) wraps it. duration_ms = 0 means run indefinitely until no_tone(pin). Searches the prescaler table at runtime to keep OCR2A in 8-bit range. Conflicts with analog_write on pins 3 and 11 (same Timer2) — matches Arduino behavior. --- lib/rubyduino/arduino_uno.rb | 10 ++++ lib/rubyduino/sp_runtime.h | 104 +++++++++++++++++++++++++++++++++++ test/test_tone.rb | 48 ++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 test/test_tone.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 2b11a04..6aeb4fe 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -65,6 +65,8 @@ module ArduinoUNO ffi_func :random_seed, [:uint32], :void ffi_func :random_range, [:int32, :int32], :int32 ffi_func :random_max, [:int32], :int32 + ffi_func :tone_for, [:uint8, :uint16, :uint32], :void + ffi_func :no_tone, [:uint8], :void end def pin_mode(pin, mode) @@ -290,3 +292,11 @@ def random_range(low, high) def random_max(high) ArduinoUNO.random_max(high) end + +def tone(pin, frequency, duration_ms = 0) + ArduinoUNO.tone_for(pin, frequency, duration_ms) +end + +def no_tone(pin) + ArduinoUNO.no_tone(pin) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 818cfc6..6f71210 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -674,6 +674,110 @@ int32_t random_range(int32_t low, int32_t high) { return low + (int32_t)((unsigned long)rand() % (unsigned long)span); } +static volatile uint8_t rd_uno_tone_pin = 255; +static volatile uint8_t *rd_uno_tone_port_reg = NULL; +static volatile uint8_t rd_uno_tone_port_mask = 0; +static volatile int32_t rd_uno_tone_toggles_remaining = 0; + +ISR(TIMER2_COMPA_vect) { + if (rd_uno_tone_port_reg == NULL) { + return; + } + + *rd_uno_tone_port_reg ^= rd_uno_tone_port_mask; + + if (rd_uno_tone_toggles_remaining > 0) { + rd_uno_tone_toggles_remaining--; + if (rd_uno_tone_toggles_remaining == 0) { + TCCR2A = 0; + TCCR2B = 0; + TIMSK2 = 0; + *rd_uno_tone_port_reg &= (uint8_t)~rd_uno_tone_port_mask; + rd_uno_tone_port_reg = NULL; + rd_uno_tone_pin = 255; + } + } +} + +static void rd_uno_tone_stop(void) { + TCCR2A = 0; + TCCR2B = 0; + TIMSK2 &= (uint8_t)~(1 << OCIE2A); + if (rd_uno_tone_port_reg) { + *rd_uno_tone_port_reg &= (uint8_t)~rd_uno_tone_port_mask; + } + rd_uno_tone_port_reg = NULL; + rd_uno_tone_pin = 255; + rd_uno_tone_toggles_remaining = 0; +} + +void tone_for(uint8_t pin, uint16_t frequency, uint32_t duration_ms) { + uint8_t prescaler_bits; + uint32_t prescaler_value; + uint32_t ocr; + + if (frequency == 0) { + rd_uno_tone_stop(); + return; + } + if (!rd_uno_valid_pin(pin)) { + return; + } + + pin_mode(pin, 1); + + static const uint16_t prescalers[7] = {1, 8, 32, 64, 128, 256, 1024}; + static const uint8_t prescaler_cs[7] = {1, 2, 3, 4, 5, 6, 7}; + uint8_t i; + prescaler_bits = 0; + prescaler_value = 0; + for (i = 0; i < 7; i++) { + uint32_t candidate = (F_CPU / (2UL * (uint32_t)prescalers[i] * (uint32_t)frequency)); + if (candidate > 0 && candidate <= 256) { + prescaler_bits = prescaler_cs[i]; + prescaler_value = (uint32_t)prescalers[i]; + ocr = candidate - 1; + break; + } + } + if (prescaler_bits == 0) { + return; + } + (void)prescaler_value; + + cli(); + rd_uno_tone_pin = pin; + rd_uno_tone_port_reg = rd_uno_port(pin); + rd_uno_tone_port_mask = (uint8_t)(1 << rd_uno_bit(pin)); + if (duration_ms > 0) { + uint32_t toggles = (uint32_t)((uint32_t)2 * (uint32_t)frequency * duration_ms / 1000UL); + if (toggles == 0) { + toggles = 1; + } + rd_uno_tone_toggles_remaining = (int32_t)toggles; + } else { + rd_uno_tone_toggles_remaining = 0; + } + + TCCR2A = (uint8_t)(1 << WGM21); + TCCR2B = prescaler_bits; + OCR2A = (uint8_t)ocr; + TCNT2 = 0; + TIMSK2 = (uint8_t)(1 << OCIE2A); + sei(); +} + +void no_tone(uint8_t pin) { + if (!rd_uno_valid_pin(pin)) { + return; + } + cli(); + if (rd_uno_tone_pin == pin || rd_uno_tone_pin == 255) { + rd_uno_tone_stop(); + } + sei(); +} + #define fflush(stream) ((void)0) #endif diff --git a/test/test_tone.rb b/test/test_tone.rb new file mode 100644 index 0000000..078f257 --- /dev/null +++ b/test/test_tone.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestTone < Minitest::Test + def test_codegen_emits_tone_and_no_tone + sketch = <<~RUBY + tone(8, 440) + tone(8, 440, 500) + no_tone(8) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "tone_for(" + assert_includes c, "no_tone(" + # default duration arg of 0 reaches sp_tone wrapper + assert_match(/sp_tone\(8LL, 440LL, 0LL\)/, c) + assert_match(/sp_tone\(8LL, 440LL, 500LL\)/, c) + end + + def test_avr_compile_continuous_tone + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + tone(8, 1000) + delay_ms(500) + no_tone(8) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end + + def test_avr_compile_with_duration + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = "tone(8, 440, 250)" + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end + + def test_elf_contains_tone_isr + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = "tone(8, 440, 250)" + elf_data = nil + CompileHelper.compile_ruby_to_avr_elf(sketch) do |elf, _, _| + elf_data = CompileHelper.avr_objdump_disassembly(elf) + end + assert_includes elf_data, "__vector_7", "expected TIMER2_COMPA ISR (vector 7) in elf" + end +end From 2a019b106bd26804602f8335f3caf918b1ed6c99 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:11:36 +1000 Subject: [PATCH 07/33] Add pulse_in_long with default and explicit timeouts Aliases to pulse_in_timeout since the existing implementation already uses micros() rather than cycle counting. Default timeout matches Arduino: 1,000,000 us. --- lib/rubyduino/arduino_uno.rb | 4 ++++ test/test_pulse_in_long.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 test/test_pulse_in_long.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 6aeb4fe..afe6014 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -300,3 +300,7 @@ def tone(pin, frequency, duration_ms = 0) def no_tone(pin) ArduinoUNO.no_tone(pin) end + +def pulse_in_long(pin, value, timeout_us = 1_000_000) + ArduinoUNO.pulse_in_timeout(pin, value, timeout_us) +end diff --git a/test/test_pulse_in_long.rb b/test/test_pulse_in_long.rb new file mode 100644 index 0000000..821ecf7 --- /dev/null +++ b/test/test_pulse_in_long.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestPulseInLong < Minitest::Test + def test_codegen_default_timeout + sketch = "d = pulse_in_long(7, 1)" + c = CompileHelper.compile_ruby_to_c(sketch) + # Default timeout 1_000_000 reaches the wrapper. + assert_match(/sp_pulse_in_long\(7LL, 1LL, 1000000LL\)/, c) + assert_includes c, "pulse_in_timeout(" + end + + def test_codegen_explicit_timeout + sketch = "d = pulse_in_long(7, 1, 5_000_000)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_match(/sp_pulse_in_long\(7LL, 1LL, 5000000LL\)/, c) + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + pin_mode(7, ArduinoUNO::INPUT) + duration = pulse_in_long(7, ArduinoUNO::HIGH, 200_000) + delay_ms(duration / 1000) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From 8ac42eb75b5b33237f19d2edfd8b4fc83eacfecd Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:12:56 +1000 Subject: [PATCH 08/33] Add external interrupts (attach/detach + flag-based polling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flag-based model: attach_interrupt(num, mode) wires the ISR which sets a volatile flag, interrupt_fired?(num) reads & clears it. Doesn't support callback function pointers (Ruby blocks → C function pointers isn't tractable through Spinel's compile model), but covers the common 'wake the main loop on a hardware event' use case. INT_LOW/INT_CHANGE/INT_FALLING/INT_RISING constants map directly to ATmega328P EICRA ISC bits. digital_pin_to_interrupt returns -1 for non-INT pins so users can guard against accidental wiring. --- lib/rubyduino/arduino_uno.rb | 29 +++++++++++++ lib/rubyduino/sp_runtime.h | 62 ++++++++++++++++++++++++++++ test/test_external_interrupts.rb | 71 ++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 test/test_external_interrupts.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index afe6014..f58410b 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -17,6 +17,11 @@ module ArduinoUNO LSBFIRST = 0 MSBFIRST = 1 + INT_LOW = 0 + INT_CHANGE = 1 + INT_FALLING = 2 + INT_RISING = 3 + ffi_func :pin_mode, [:uint8, :uint8], :int ffi_func :digital_write, [:uint8, :uint8], :int ffi_func :digital_read, [:uint8], :int @@ -67,6 +72,10 @@ module ArduinoUNO ffi_func :random_max, [:int32], :int32 ffi_func :tone_for, [:uint8, :uint16, :uint32], :void ffi_func :no_tone, [:uint8], :void + ffi_func :attach_interrupt, [:uint8, :uint8], :void + ffi_func :detach_interrupt, [:uint8], :void + ffi_func :interrupt_fired, [:uint8], :uint8 + ffi_func :digital_pin_to_interrupt, [:uint8], :int8 end def pin_mode(pin, mode) @@ -304,3 +313,23 @@ def no_tone(pin) def pulse_in_long(pin, value, timeout_us = 1_000_000) ArduinoUNO.pulse_in_timeout(pin, value, timeout_us) end + +def attach_interrupt(interrupt_num, mode) + ArduinoUNO.attach_interrupt(interrupt_num, mode) +end + +def detach_interrupt(interrupt_num) + ArduinoUNO.detach_interrupt(interrupt_num) +end + +def interrupt_fired(interrupt_num) + ArduinoUNO.interrupt_fired(interrupt_num) +end + +def interrupt_fired?(interrupt_num) + ArduinoUNO.interrupt_fired(interrupt_num) == 1 +end + +def digital_pin_to_interrupt(pin) + ArduinoUNO.digital_pin_to_interrupt(pin) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 6f71210..b167de7 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -778,6 +778,68 @@ void no_tone(uint8_t pin) { sei(); } +static volatile uint8_t rd_uno_int_flags[2] = {0, 0}; + +ISR(INT0_vect) { + rd_uno_int_flags[0] = 1; +} + +ISR(INT1_vect) { + rd_uno_int_flags[1] = 1; +} + +void attach_interrupt(uint8_t interrupt_num, uint8_t mode) { + uint8_t shift; + + if (interrupt_num > 1) { + return; + } + if (mode > 3) { + return; + } + + shift = (uint8_t)(interrupt_num * 2); + + cli(); + EICRA = (uint8_t)((EICRA & (uint8_t)~((uint8_t)0x03 << shift)) | (uint8_t)((mode & 0x03) << shift)); + EIFR = (uint8_t)(1 << interrupt_num); + EIMSK |= (uint8_t)(1 << interrupt_num); + rd_uno_int_flags[interrupt_num] = 0; + sei(); +} + +void detach_interrupt(uint8_t interrupt_num) { + if (interrupt_num > 1) { + return; + } + cli(); + EIMSK &= (uint8_t)~(1 << interrupt_num); + rd_uno_int_flags[interrupt_num] = 0; + sei(); +} + +uint8_t interrupt_fired(uint8_t interrupt_num) { + uint8_t fired; + if (interrupt_num > 1) { + return 0; + } + cli(); + fired = rd_uno_int_flags[interrupt_num]; + rd_uno_int_flags[interrupt_num] = 0; + sei(); + return fired; +} + +int8_t digital_pin_to_interrupt(uint8_t pin) { + if (pin == 2) { + return 0; + } + if (pin == 3) { + return 1; + } + return -1; +} + #define fflush(stream) ((void)0) #endif diff --git a/test/test_external_interrupts.rb b/test/test_external_interrupts.rb new file mode 100644 index 0000000..b948c04 --- /dev/null +++ b/test/test_external_interrupts.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestExternalInterrupts < Minitest::Test + def test_codegen_emits_helpers + sketch = <<~RUBY + n = digital_pin_to_interrupt(2) + attach_interrupt(n, ArduinoUNO::INT_RISING) + if interrupt_fired?(n) + digital_write(13, 1) + end + detach_interrupt(n) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "attach_interrupt(" + assert_includes c, "detach_interrupt(" + assert_includes c, "digital_pin_to_interrupt(" + assert_includes c, "interrupt_fired(" + end + + def test_digital_pin_to_interrupt_table + program = <<~C + #include + #include + static int8_t digital_pin_to_interrupt(uint8_t pin) { + if (pin == 2) return 0; + if (pin == 3) return 1; + return -1; + } + int main(void) { + int8_t r; + for (uint8_t p = 0; p <= 13; p++) { + r = digital_pin_to_interrupt(p); + printf("%u:%d\\n", (unsigned)p, (int)r); + } + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + expected = (0..13).map { |p| "#{p}:#{p == 2 ? 0 : p == 3 ? 1 : -1}" } + assert_equal expected, out.lines.map(&:strip) + end + + def test_avr_compile_with_isrs + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + pin_mode(2, ArduinoUNO::INPUT_PULLUP) + pin_mode(3, ArduinoUNO::INPUT_PULLUP) + pin_mode(13, ArduinoUNO::OUTPUT) + + attach_interrupt(digital_pin_to_interrupt(2), ArduinoUNO::INT_FALLING) + attach_interrupt(digital_pin_to_interrupt(3), ArduinoUNO::INT_RISING) + + loop do + digital_write(13, 1) if interrupt_fired?(0) + digital_write(12, 1) if interrupt_fired?(1) + delay_ms(10) + end + RUBY + elf = nil + CompileHelper.compile_ruby_to_avr_elf(sketch) do |path, _, _| + elf = CompileHelper.avr_objdump_disassembly(path) + end + # ATmega328P INT0 = vector 1, INT1 = vector 2 (in avr-libc, vector_1 / vector_2) + assert_includes elf, "__vector_1", "INT0 ISR not in elf" + assert_includes elf, "__vector_2", "INT1 ISR not in elf" + end +end From 76963d013756247bcab2789c2c33052b867046e8 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:14:21 +1000 Subject: [PATCH 09/33] Add analog_reference (REFS bits configurable per channel) State held in rd_uno_admux_ref so analog_read picks up the latest choice; constants AREF_EXTERNAL=0, AREF_DEFAULT=1, AREF_INTERNAL=3 match ATmega328P REFS1:0 bit pairs directly. --- lib/rubyduino/arduino_uno.rb | 9 +++++++ lib/rubyduino/sp_runtime.h | 9 ++++++- test/test_analog_reference.rb | 45 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 test/test_analog_reference.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index f58410b..a544e70 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -22,6 +22,10 @@ module ArduinoUNO INT_FALLING = 2 INT_RISING = 3 + AREF_EXTERNAL = 0 + AREF_DEFAULT = 1 + AREF_INTERNAL = 3 + ffi_func :pin_mode, [:uint8, :uint8], :int ffi_func :digital_write, [:uint8, :uint8], :int ffi_func :digital_read, [:uint8], :int @@ -76,6 +80,7 @@ module ArduinoUNO ffi_func :detach_interrupt, [:uint8], :void ffi_func :interrupt_fired, [:uint8], :uint8 ffi_func :digital_pin_to_interrupt, [:uint8], :int8 + ffi_func :analog_reference, [:uint8], :void end def pin_mode(pin, mode) @@ -333,3 +338,7 @@ def interrupt_fired?(interrupt_num) def digital_pin_to_interrupt(pin) ArduinoUNO.digital_pin_to_interrupt(pin) end + +def analog_reference(type) + ArduinoUNO.analog_reference(type) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index b167de7..a550023 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -270,6 +270,13 @@ int digital_read(uint8_t pin) { return ((*reg & (uint8_t)(1 << rd_uno_bit(pin))) != 0) ? 1 : 0; } +static uint8_t rd_uno_admux_ref = (uint8_t)(1 << REFS0); + +void analog_reference(uint8_t type) { + /* Arduino constants: EXTERNAL=0 -> REFS=00, DEFAULT=1 -> REFS=01, INTERNAL=3 -> REFS=11. */ + rd_uno_admux_ref = (uint8_t)((type & 0x03) << REFS0); +} + int analog_read(uint8_t pin) { uint8_t channel = pin; @@ -281,7 +288,7 @@ int analog_read(uint8_t pin) { return -1; } - ADMUX = (uint8_t)((1 << REFS0) | channel); + ADMUX = (uint8_t)(rd_uno_admux_ref | channel); ADCSRA = (uint8_t)((1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0)); ADCSRA |= (uint8_t)(1 << ADSC); diff --git a/test/test_analog_reference.rb b/test/test_analog_reference.rb new file mode 100644 index 0000000..6235335 --- /dev/null +++ b/test/test_analog_reference.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestAnalogReference < Minitest::Test + def test_codegen_with_literal + sketch = "analog_reference(3)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "analog_reference(" + assert_match(/sp_analog_reference\(3LL\)/, c) + end + + def test_codegen_with_constant + sketch = "analog_reference(ArduinoUNO::AREF_INTERNAL)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "cst_ArduinoUNO_AREF_INTERNAL" + assert_includes c, "sp_analog_reference(cst_ArduinoUNO_AREF_INTERNAL)" + # Constant initializer assigns the right value + assert_match(/cst_ArduinoUNO_AREF_INTERNAL\s*=\s*3LL;/, c) + end + + def test_constants_in_arduino_uno_rb_match_atmega328p_refs_bits + # ATmega328P REFS bits: External=00, Default(AVcc)=01, Internal 1.1V=11 + contents = File.read(File.expand_path("../lib/rubyduino/arduino_uno.rb", __dir__)) + assert_match(/AREF_EXTERNAL\s*=\s*0\b/, contents) + assert_match(/AREF_DEFAULT\s*=\s*1\b/, contents) + assert_match(/AREF_INTERNAL\s*=\s*3\b/, contents) + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + analog_reference(ArduinoUNO::AREF_INTERNAL) + v = analog_read(ArduinoUNO::A0) + digital_write(13, v > 512 ? 1 : 0) + + analog_reference(ArduinoUNO::AREF_DEFAULT) + v2 = analog_read(ArduinoUNO::A1) + digital_write(12, v2 > 512 ? 1 : 0) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From 470c3b8ca22e4e42c8fbc1182837cb8e9bb9febe Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:15:24 +1000 Subject: [PATCH 10/33] Add serial extensions (end, flush, peek, available_for_write, set/get_timeout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit peek uses a 1-byte stash so the next read consumes it; set/get_timeout expose the millisecond budget used by the upcoming parse/find helpers. available_for_write returns 1/0 since this runtime polls UDRE0 rather than buffering — adequate for byte-at-a-time writes. --- lib/rubyduino/arduino_uno.rb | 30 +++++++++++++ lib/rubyduino/sp_runtime.h | 54 ++++++++++++++++++++++- test/test_serial_extensions.rb | 81 ++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 test/test_serial_extensions.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index a544e70..ef56444 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -81,6 +81,12 @@ module ArduinoUNO ffi_func :interrupt_fired, [:uint8], :uint8 ffi_func :digital_pin_to_interrupt, [:uint8], :int8 ffi_func :analog_reference, [:uint8], :void + ffi_func :serial_end, [], :void + ffi_func :serial_flush, [], :void + ffi_func :serial_peek, [], :int + ffi_func :serial_available_for_write, [], :int + ffi_func :serial_set_timeout, [:uint32], :void + ffi_func :serial_get_timeout, [], :uint32 end def pin_mode(pin, mode) @@ -342,3 +348,27 @@ def digital_pin_to_interrupt(pin) def analog_reference(type) ArduinoUNO.analog_reference(type) end + +def serial_end + ArduinoUNO.serial_end +end + +def serial_flush + ArduinoUNO.serial_flush +end + +def serial_peek + ArduinoUNO.serial_peek +end + +def serial_available_for_write + ArduinoUNO.serial_available_for_write +end + +def serial_set_timeout(timeout_ms) + ArduinoUNO.serial_set_timeout(timeout_ms) +end + +def serial_get_timeout + ArduinoUNO.serial_get_timeout +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index a550023..0295e13 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -450,17 +450,69 @@ void serial_begin(uint32_t baud) { UCSR0C = (uint8_t)((1 << UCSZ01) | (1 << UCSZ00)); } +static int16_t rd_uno_serial_peek_buf = -1; +static uint32_t rd_uno_serial_timeout_ms = 1000; + int serial_available(void) { + if (rd_uno_serial_peek_buf != -1) { + return 1; + } return (UCSR0A & (uint8_t)(1 << RXC0)) ? 1 : 0; } int serial_read(void) { - if (!serial_available()) { + int v; + + if (rd_uno_serial_peek_buf != -1) { + v = rd_uno_serial_peek_buf; + rd_uno_serial_peek_buf = -1; + return v; + } + + if (!(UCSR0A & (uint8_t)(1 << RXC0))) { return -1; } return UDR0; } +int serial_peek(void) { + if (rd_uno_serial_peek_buf != -1) { + return rd_uno_serial_peek_buf; + } + if (!(UCSR0A & (uint8_t)(1 << RXC0))) { + return -1; + } + rd_uno_serial_peek_buf = (int16_t)UDR0; + return rd_uno_serial_peek_buf; +} + +void serial_end(void) { + UCSR0B = 0; + rd_uno_serial_peek_buf = -1; +} + +void serial_flush(void) { + /* Wait until the TX shift register is empty. */ + while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { + } + while (!(UCSR0A & (uint8_t)(1 << TXC0))) { + } + /* Clear the TXC0 flag (write 1 to it). */ + UCSR0A |= (uint8_t)(1 << TXC0); +} + +int serial_available_for_write(void) { + return (UCSR0A & (uint8_t)(1 << UDRE0)) ? 1 : 0; +} + +void serial_set_timeout(uint32_t timeout_ms) { + rd_uno_serial_timeout_ms = timeout_ms; +} + +uint32_t serial_get_timeout(void) { + return rd_uno_serial_timeout_ms; +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_serial_extensions.rb b/test/test_serial_extensions.rb new file mode 100644 index 0000000..6054156 --- /dev/null +++ b/test/test_serial_extensions.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestSerialExtensions < Minitest::Test + HELPERS = %w[serial_end serial_flush serial_peek serial_available_for_write + serial_set_timeout serial_get_timeout].freeze + + def test_codegen_emits_helpers + sketch = <<~RUBY + serial_set_timeout(500) + t = serial_get_timeout + n = serial_peek + avail = serial_available_for_write + serial_flush + serial_end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h} in generated C" } + end + + def test_peek_buffer_logic_native + program = <<~C + #include + #include + static int16_t peek_buf = -1; + static int incoming = -1; + static int rxc(void) { return incoming != -1 ? 1 : 0; } + static int udr(void) { int v = incoming; incoming = -1; return v; } + + static int serial_available(void) { return peek_buf != -1 ? 1 : (rxc() ? 1 : 0); } + static int serial_peek(void) { + if (peek_buf != -1) return peek_buf; + if (!rxc()) return -1; + peek_buf = (int16_t)udr(); + return peek_buf; + } + static int serial_read(void) { + int v; + if (peek_buf != -1) { v = peek_buf; peek_buf = -1; return v; } + if (!rxc()) return -1; + return udr(); + } + + int main(void) { + printf("%d\\n", serial_available()); /* 0 */ + printf("%d\\n", serial_peek()); /* -1 */ + incoming = 'A'; + printf("%d\\n", serial_available()); /* 1 (rxc) */ + printf("%d\\n", serial_peek()); /* 65 - now buffered */ + printf("%d\\n", serial_peek()); /* 65 again - still buffered */ + printf("%d\\n", serial_available()); /* 1 (peek_buf) */ + printf("%d\\n", serial_read()); /* 65 - consumed */ + printf("%d\\n", serial_available()); /* 0 */ + printf("%d\\n", serial_read()); /* -1 */ + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal %w[0 -1 1 65 65 1 65 0 -1], out.split + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + serial_begin(9600) + serial_set_timeout(2000) + timeout = serial_get_timeout + ch = serial_peek + if ch != -1 && serial_available_for_write == 1 + serial_write(ch) + end + serial_flush + serial_end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From a823e2485415c70e71f40240294d172eb5a91e41 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:17:20 +1000 Subject: [PATCH 11/33] Add Serial input parsers (parse_int, parse_float, find, find_until, read_byte_timeout) parse_int/float honor serial_set_timeout, skip leading non-digits, support negative numbers, and stop at the first non-digit. find scans for a literal target until timeout; find_until additionally bails when the terminator is hit. read_byte_timeout returns -1 when no byte arrives within the configured timeout window. Predicate aliases serial_find? and serial_find_until? added per the boolean-method convention. --- lib/rubyduino/arduino_uno.rb | 33 +++++++ lib/rubyduino/sp_runtime.h | 178 ++++++++++++++++++++++++++++++++++ test/test_serial_input.rb | 182 +++++++++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 test/test_serial_input.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index ef56444..a726080 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -87,6 +87,11 @@ module ArduinoUNO ffi_func :serial_available_for_write, [], :int ffi_func :serial_set_timeout, [:uint32], :void ffi_func :serial_get_timeout, [], :uint32 + ffi_func :serial_read_byte_timeout, [], :int + ffi_func :serial_parse_int, [], :int32 + ffi_func :serial_parse_float, [], :double + ffi_func :serial_find, [:str], :uint8 + ffi_func :serial_find_until, [:str, :str], :uint8 end def pin_mode(pin, mode) @@ -372,3 +377,31 @@ def serial_set_timeout(timeout_ms) def serial_get_timeout ArduinoUNO.serial_get_timeout end + +def serial_read_byte_timeout + ArduinoUNO.serial_read_byte_timeout +end + +def serial_parse_int + ArduinoUNO.serial_parse_int +end + +def serial_parse_float + ArduinoUNO.serial_parse_float +end + +def serial_find(target) + ArduinoUNO.serial_find(target) +end + +def serial_find?(target) + ArduinoUNO.serial_find(target) == 1 +end + +def serial_find_until(target, terminator) + ArduinoUNO.serial_find_until(target, terminator) +end + +def serial_find_until?(target, terminator) + ArduinoUNO.serial_find_until(target, terminator) == 1 +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 0295e13..7e90fcf 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -513,6 +513,184 @@ uint32_t serial_get_timeout(void) { return rd_uno_serial_timeout_ms; } +int serial_read_byte_timeout(void) { + uint32_t start = millis(); + + while (1) { + int v = serial_read(); + if (v != -1) { + return v; + } + if ((millis() - start) >= rd_uno_serial_timeout_ms) { + return -1; + } + } +} + +static int rd_uno_serial_peek_blocking(uint32_t deadline_ms) { + while (1) { + int v = serial_peek(); + if (v != -1) { + return v; + } + if (millis() >= deadline_ms) { + return -1; + } + } +} + +int32_t serial_parse_int(void) { + uint32_t deadline = millis() + rd_uno_serial_timeout_ms; + int32_t value = 0; + int negative = 0; + int saw_digit = 0; + int v; + + for (;;) { + v = rd_uno_serial_peek_blocking(deadline); + if (v == -1) { + return 0; + } + if (v == '-' || (v >= '0' && v <= '9')) { + break; + } + (void)serial_read(); + } + + if (v == '-') { + negative = 1; + (void)serial_read(); + } + + for (;;) { + v = serial_peek(); + if (v == -1) { + if (millis() >= deadline) { + break; + } + continue; + } + if (v < '0' || v > '9') { + break; + } + value = value * 10 + (v - '0'); + saw_digit = 1; + (void)serial_read(); + } + + if (!saw_digit) { + return 0; + } + return negative ? -value : value; +} + +double serial_parse_float(void) { + uint32_t deadline = millis() + rd_uno_serial_timeout_ms; + double value = 0.0; + double frac = 0.1; + int negative = 0; + int seen_dot = 0; + int saw_digit = 0; + int v; + + for (;;) { + v = rd_uno_serial_peek_blocking(deadline); + if (v == -1) { + return 0.0; + } + if (v == '-' || v == '.' || (v >= '0' && v <= '9')) { + break; + } + (void)serial_read(); + } + + if (v == '-') { + negative = 1; + (void)serial_read(); + } + + for (;;) { + v = serial_peek(); + if (v == -1) { + if (millis() >= deadline) { + break; + } + continue; + } + if (v == '.') { + if (seen_dot) { + break; + } + seen_dot = 1; + (void)serial_read(); + continue; + } + if (v < '0' || v > '9') { + break; + } + if (seen_dot) { + value += (v - '0') * frac; + frac *= 0.1; + } else { + value = value * 10.0 + (v - '0'); + } + saw_digit = 1; + (void)serial_read(); + } + + if (!saw_digit) { + return 0.0; + } + return negative ? -value : value; +} + +static uint8_t rd_uno_serial_find_impl(const char *target, const char *terminator) { + uint32_t deadline = millis() + rd_uno_serial_timeout_ms; + size_t target_pos = 0; + size_t term_pos = 0; + size_t target_len = strlen(target); + size_t term_len = terminator ? strlen(terminator) : 0; + + if (target_len == 0) { + return 1; + } + + while (millis() < deadline) { + int v = serial_read(); + if (v == -1) { + continue; + } + if ((char)v == target[target_pos]) { + target_pos++; + if (target_pos == target_len) { + return 1; + } + } else { + target_pos = ((char)v == target[0]) ? 1 : 0; + } + + if (term_len > 0) { + if ((char)v == terminator[term_pos]) { + term_pos++; + if (term_pos == term_len) { + return 0; + } + } else { + term_pos = ((char)v == terminator[0]) ? 1 : 0; + } + } + } + return 0; +} + +uint8_t serial_find(const char *target) { + return rd_uno_serial_find_impl(target, NULL); +} + +uint8_t serial_find_until(const char *target, const char *terminator) { + return rd_uno_serial_find_impl(target, terminator); +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_serial_input.rb b/test/test_serial_input.rb new file mode 100644 index 0000000..2e147b2 --- /dev/null +++ b/test/test_serial_input.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestSerialInput < Minitest::Test + def test_codegen_emits_helpers + sketch = <<~RUBY + n = serial_parse_int + f = serial_parse_float + ok = serial_find?("READY") + ok2 = serial_find_until?("READY", "\\n") + b = serial_read_byte_timeout + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[serial_parse_int( serial_parse_float( serial_find( serial_find_until( serial_read_byte_timeout(].each do |fn| + assert_includes c, fn, "missing #{fn}" + end + end + + def test_parse_int_logic_native + program = <<~C + #include + #include + #include + + static const char *fed = NULL; + static size_t fed_pos = 0; + static int16_t peek_buf = -1; + static uint32_t fake_now = 0; + static uint32_t timeout_ms = 1000; + + static uint32_t millis(void) { return fake_now; } + static int serial_read(void) { + if (peek_buf != -1) { int v = peek_buf; peek_buf = -1; return v; } + if (fed && fed[fed_pos]) return (unsigned char)fed[fed_pos++]; + return -1; + } + static int serial_peek(void) { + if (peek_buf != -1) return peek_buf; + if (fed && fed[fed_pos]) { peek_buf = (unsigned char)fed[fed_pos++]; return peek_buf; } + return -1; + } + static int rd_uno_serial_peek_blocking(uint32_t deadline_ms) { + while (1) { + int v = serial_peek(); + if (v != -1) return v; + if (millis() >= deadline_ms) return -1; + fake_now++; + } + } + static int32_t serial_parse_int(void) { + uint32_t deadline = millis() + timeout_ms; + int32_t value = 0; + int negative = 0; + int saw = 0, v; + for (;;) { + v = rd_uno_serial_peek_blocking(deadline); + if (v == -1) return 0; + if (v == '-' || (v >= '0' && v <= '9')) break; + serial_read(); + } + if (v == '-') { negative = 1; serial_read(); } + for (;;) { + v = serial_peek(); + if (v == -1) { if (millis() >= deadline) break; else { fake_now++; continue; } } + if (v < '0' || v > '9') break; + value = value * 10 + (v - '0'); saw = 1; serial_read(); + } + if (!saw) return 0; + return negative ? -value : value; + } + static void reset_with(const char *s) { fed = s; fed_pos = 0; peek_buf = -1; fake_now = 0; } + + int main(void) { + reset_with("hello-42world"); + printf("%d\\n", (int)serial_parse_int()); /* -42 */ + reset_with(" 123,99"); + printf("%d\\n", (int)serial_parse_int()); /* 123 */ + reset_with("abc"); + printf("%d\\n", (int)serial_parse_int()); /* 0 (timeout) */ + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal %w[-42 123 0], out.split + end + + def test_parse_float_logic_native + program = <<~C + #include + #include + #include + static const char *fed = NULL; + static size_t fed_pos = 0; + static int16_t peek_buf = -1; + static uint32_t fake_now = 0; + static uint32_t timeout_ms = 1000; + static uint32_t millis(void) { return fake_now; } + static int serial_read(void) { + if (peek_buf != -1) { int v = peek_buf; peek_buf = -1; return v; } + if (fed && fed[fed_pos]) return (unsigned char)fed[fed_pos++]; + return -1; + } + static int serial_peek(void) { + if (peek_buf != -1) return peek_buf; + if (fed && fed[fed_pos]) { peek_buf = (unsigned char)fed[fed_pos++]; return peek_buf; } + return -1; + } + static int peek_blocking(uint32_t deadline) { + while (1) { + int v = serial_peek(); + if (v != -1) return v; + if (millis() >= deadline) return -1; + fake_now++; + } + } + static double parse_float(void) { + uint32_t deadline = millis() + timeout_ms; + double value = 0.0, frac = 0.1; + int neg = 0, dot = 0, saw = 0, v; + for (;;) { + v = peek_blocking(deadline); + if (v == -1) return 0.0; + if (v == '-' || v == '.' || (v >= '0' && v <= '9')) break; + serial_read(); + } + if (v == '-') { neg = 1; serial_read(); } + for (;;) { + v = serial_peek(); + if (v == -1) { if (millis() >= deadline) break; fake_now++; continue; } + if (v == '.') { if (dot) break; dot = 1; serial_read(); continue; } + if (v < '0' || v > '9') break; + if (dot) { value += (v - '0') * frac; frac *= 0.1; } + else { value = value * 10.0 + (v - '0'); } + saw = 1; serial_read(); + } + if (!saw) return 0.0; + return neg ? -value : value; + } + static void reset_with(const char *s) { fed = s; fed_pos = 0; peek_buf = -1; fake_now = 0; } + int main(void) { + reset_with("abc-3.14xyz"); + printf("%.4f\\n", parse_float()); /* -3.1400 */ + reset_with(" 42.5"); + printf("%.4f\\n", parse_float()); /* 42.5000 */ + reset_with(".25"); + printf("%.4f\\n", parse_float()); /* 0.2500 */ + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + lines = out.split + assert_equal "-3.1400", lines[0] + assert_equal "42.5000", lines[1] + assert_equal "0.2500", lines[2] + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + serial_begin(9600) + serial_set_timeout(2000) + + n = serial_parse_int + digital_write(13, n > 0 ? 1 : 0) + + f = serial_parse_float + digital_write(12, f > 0.5 ? 1 : 0) + + digital_write(11, 1) if serial_find?("OK") + digital_write(10, 1) if serial_find_until?("DONE", "\\n") + + b = serial_read_byte_timeout + digital_write(9, 1) if b != -1 + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From a2efe80577ef3484c7507ffaf5d4f55ce5b5d290 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:19:00 +1000 Subject: [PATCH 12/33] Add Serial print formatting (HEX/BIN/OCT, floats) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codegen-special-cased two-arg form: serial_print(value, BIN/OCT/DEC/HEX) routes to dedicated emitters; serial_print(float, decimals) emits the float helper. Direct callers can also use serial_print_hex etc. Float formatter is hand-rolled — pulling vfprintf_flt from avr-libc adds ~2KB which is unwelcome on UNO. Manual rounding produces Arduino-compatible 'round half away from zero' output. --- lib/rubyduino/arduino_uno.rb | 45 ++++++++ lib/rubyduino/sp_runtime.h | 99 +++++++++++++++++ lib/rubyduino/spinel_arduino_codegen.rb | 39 ++++++- test/test_serial_print_formatting.rb | 141 ++++++++++++++++++++++++ 4 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 test/test_serial_print_formatting.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index a726080..48cebc0 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -26,6 +26,11 @@ module ArduinoUNO AREF_DEFAULT = 1 AREF_INTERNAL = 3 + BIN = 2 + OCT = 8 + DEC = 10 + HEX = 16 + ffi_func :pin_mode, [:uint8, :uint8], :int ffi_func :digital_write, [:uint8, :uint8], :int ffi_func :digital_read, [:uint8], :int @@ -92,6 +97,14 @@ module ArduinoUNO ffi_func :serial_parse_float, [], :double ffi_func :serial_find, [:str], :uint8 ffi_func :serial_find_until, [:str, :str], :uint8 + ffi_func :serial_print_hex, [:uint32], :void + ffi_func :serial_print_bin, [:uint32], :void + ffi_func :serial_print_oct, [:uint32], :void + ffi_func :serial_print_float, [:double, :uint8], :void + ffi_func :serial_println_hex, [:uint32], :void + ffi_func :serial_println_bin, [:uint32], :void + ffi_func :serial_println_oct, [:uint32], :void + ffi_func :serial_println_float, [:double, :uint8], :void end def pin_mode(pin, mode) @@ -405,3 +418,35 @@ def serial_find_until(target, terminator) def serial_find_until?(target, terminator) ArduinoUNO.serial_find_until(target, terminator) == 1 end + +def serial_print_hex(value) + ArduinoUNO.serial_print_hex(value) +end + +def serial_print_bin(value) + ArduinoUNO.serial_print_bin(value) +end + +def serial_print_oct(value) + ArduinoUNO.serial_print_oct(value) +end + +def serial_print_float(value, decimals = 2) + ArduinoUNO.serial_print_float(value, decimals) +end + +def serial_println_hex(value) + ArduinoUNO.serial_println_hex(value) +end + +def serial_println_bin(value) + ArduinoUNO.serial_println_bin(value) +end + +def serial_println_oct(value) + ArduinoUNO.serial_println_oct(value) +end + +def serial_println_float(value, decimals = 2) + ArduinoUNO.serial_println_float(value, decimals) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 7e90fcf..f84543b 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -691,6 +691,105 @@ uint8_t serial_find_until(const char *target, const char *terminator) { return rd_uno_serial_find_impl(target, terminator); } +static void rd_uno_print_unsigned_base(uint32_t value, uint8_t base) { + char buf[33]; + char *p = &buf[32]; + uint32_t v = value; + + *p = '\0'; + if (base < 2) { + base = 10; + } + do { + p--; + uint8_t digit = (uint8_t)(v % base); + *p = (char)((digit < 10) ? ('0' + digit) : ('A' + (digit - 10))); + v /= base; + } while (v > 0); + + serial_print_str(p); +} + +void serial_print_hex(uint32_t value) { + rd_uno_print_unsigned_base(value, 16); +} + +void serial_print_bin(uint32_t value) { + rd_uno_print_unsigned_base(value, 2); +} + +void serial_print_oct(uint32_t value) { + rd_uno_print_unsigned_base(value, 8); +} + +void serial_println_hex(uint32_t value) { + rd_uno_print_unsigned_base(value, 16); + serial_write((uint8_t)'\r'); + serial_write((uint8_t)'\n'); +} + +void serial_println_bin(uint32_t value) { + rd_uno_print_unsigned_base(value, 2); + serial_write((uint8_t)'\r'); + serial_write((uint8_t)'\n'); +} + +void serial_println_oct(uint32_t value) { + rd_uno_print_unsigned_base(value, 8); + serial_write((uint8_t)'\r'); + serial_write((uint8_t)'\n'); +} + +static void rd_uno_print_float(double value, uint8_t decimals) { + if (value < 0.0) { + serial_write((uint8_t)'-'); + value = -value; + } + + /* Round to the requested number of decimals. */ + double rounding = 0.5; + uint8_t i; + for (i = 0; i < decimals; i++) { + rounding /= 10.0; + } + value += rounding; + + uint32_t int_part = (uint32_t)value; + double remainder = value - (double)int_part; + serial_print_int((int)int_part); + + if (decimals > 0) { + serial_write((uint8_t)'.'); + while (decimals > 0) { + remainder *= 10.0; + uint8_t digit = (uint8_t)remainder; + serial_write((uint8_t)('0' + digit)); + remainder -= (double)digit; + decimals--; + } + } +} + +void serial_print_float(double value, uint8_t decimals) { + rd_uno_print_float(value, decimals); +} + +void serial_println_float(double value, uint8_t decimals) { + rd_uno_print_float(value, decimals); + serial_write((uint8_t)'\r'); + serial_write((uint8_t)'\n'); +} + +static void serial_print_float_default(double value) { + rd_uno_print_float(value, 2); +} + +static void serial_println_float_default(double value) { + rd_uno_print_float(value, 2); + serial_write((uint8_t)'\r'); + serial_write((uint8_t)'\n'); +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/lib/rubyduino/spinel_arduino_codegen.rb b/lib/rubyduino/spinel_arduino_codegen.rb index 0367516..5674b05 100644 --- a/lib/rubyduino/spinel_arduino_codegen.rb +++ b/lib/rubyduino/spinel_arduino_codegen.rb @@ -53,21 +53,52 @@ def compile_arduino_serial_print(nid, newline) return nil if args_id < 0 arg_ids = get_args(args_id) - return nil unless arg_ids.length == 1 - arg = arg_ids.first - fn = arduino_serial_print_func(arg, newline) - "(" + fn + "(" + compile_expr(arg) + "), (mrb_int)0)" + if arg_ids.length == 1 + arg = arg_ids.first + fn = arduino_serial_print_func(arg, newline) + return "(" + fn + "(" + compile_expr(arg) + "), (mrb_int)0)" + end + + if arg_ids.length == 2 + value_arg = arg_ids[0] + base_or_dec_arg = arg_ids[1] + + if infer_type(value_arg) == "float" + prefix = newline ? "serial_println_float" : "serial_print_float" + return "(" + prefix + "((double)(" + compile_expr(value_arg) + "), (uint8_t)(" + compile_expr(base_or_dec_arg) + ")), (mrb_int)0)" + end + + base_fn = arduino_base_print_func(base_or_dec_arg, newline) + return nil unless base_fn + + return "(" + base_fn + "((uint32_t)(" + compile_expr(value_arg) + ")), (mrb_int)0)" + end + + nil end def arduino_serial_print_func(arg, newline) if infer_type(arg) == "string" return newline ? "serial_println_str" : "serial_print_str" end + if infer_type(arg) == "float" + return newline ? "serial_println_float_default" : "serial_print_float_default" + end newline ? "serial_println_int" : "serial_print_int" end + def arduino_base_print_func(base_arg, newline) + return nil unless integer_literal_node?(base_arg) + case @nd_value[base_arg].to_i + when 2 then newline ? "serial_println_bin" : "serial_print_bin" + when 8 then newline ? "serial_println_oct" : "serial_print_oct" + when 10 then newline ? "serial_println_int" : "serial_print_int" + when 16 then newline ? "serial_println_hex" : "serial_print_hex" + end + end + def compile_arduino_rand(nid) args_id = @nd_arguments[nid] return nil if args_id < 0 diff --git a/test/test_serial_print_formatting.rb b/test/test_serial_print_formatting.rb new file mode 100644 index 0000000..b40c357 --- /dev/null +++ b/test/test_serial_print_formatting.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestSerialPrintFormatting < Minitest::Test + def test_codegen_routes_hex_bin_oct + sketch = <<~RUBY + serial_print(255, ArduinoUNO::HEX) + serial_print(7, ArduinoUNO::BIN) + serial_print(63, ArduinoUNO::OCT) + serial_println(255, ArduinoUNO::HEX) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "serial_print_hex(" + assert_includes c, "serial_print_bin(" + assert_includes c, "serial_print_oct(" + assert_includes c, "serial_println_hex(" + end + + def test_codegen_routes_dec_to_int + sketch = "serial_print(42, ArduinoUNO::DEC)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "serial_print_int(" + end + + def test_codegen_routes_float_with_decimals + sketch = "serial_print(3.14, 4)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "serial_print_float(" + assert_match(/serial_print_float\(\(double\)\([^)]+\), \(uint8_t\)\([^)]+\)\)/, c) + end + + def test_explicit_helpers_codegen + sketch = <<~RUBY + serial_print_hex(0xCAFE) + serial_print_bin(0b101) + serial_println_float(2.5, 3) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "sp_serial_print_hex(" + assert_includes c, "sp_serial_print_bin(" + assert_includes c, "sp_serial_println_float(" + end + + def test_hex_bin_oct_runtime_logic + program = <<~C + #include + #include + static char out[256]; + static int out_pos = 0; + static void put_char(char c) { out[out_pos++] = c; out[out_pos] = 0; } + static void serial_print_str(const char *s) { while (*s) put_char(*s++); } + static void rd_uno_print_unsigned_base(uint32_t value, uint8_t base) { + char buf[33]; char *p = &buf[32]; uint32_t v = value; + *p = 0; + if (base < 2) base = 10; + do { + p--; + uint8_t digit = (uint8_t)(v % base); + *p = (char)((digit < 10) ? ('0' + digit) : ('A' + (digit - 10))); + v /= base; + } while (v > 0); + serial_print_str(p); + } + int main(void) { + rd_uno_print_unsigned_base(255, 16); put_char(' '); + rd_uno_print_unsigned_base(7, 2); put_char(' '); + rd_uno_print_unsigned_base(63, 8); put_char(' '); + rd_uno_print_unsigned_base(0, 16); put_char(' '); + rd_uno_print_unsigned_base(0xCAFE, 16); + printf("%s\\n", out); + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal "FF 111 77 0 CAFE", out.strip + end + + def test_float_formatter_runtime_logic + program = <<~C + #include + #include + static char buf[256]; static int p = 0; + static void emit(char c) { buf[p++] = c; buf[p] = 0; } + static void serial_write(uint8_t c) { emit((char)c); } + static void serial_print_str(const char *s) { while (*s) emit(*s++); } + static void serial_print_int(int v) { + char tmp[12]; int i = 0; + if (v < 0) { emit('-'); v = -v; } + if (v == 0) tmp[i++] = '0'; + while (v) { tmp[i++] = '0' + (v % 10); v /= 10; } + while (i--) emit(tmp[i]); + } + static void rd_print_float(double v, uint8_t d) { + if (v < 0.0) { serial_write('-'); v = -v; } + double r = 0.5; for (uint8_t i = 0; i < d; i++) r /= 10.0; + v += r; + uint32_t ip = (uint32_t)v; + double rem = v - (double)ip; + serial_print_int((int)ip); + if (d > 0) { + serial_write('.'); + while (d--) { + rem *= 10.0; + uint8_t digit = (uint8_t)rem; + serial_write('0' + digit); + rem -= (double)digit; + } + } + } + int main(void) { + rd_print_float(3.14159, 2); emit(' '); + rd_print_float(-2.5, 1); emit(' '); + rd_print_float(0.0, 3); emit(' '); + rd_print_float(0.999, 2); /* round to 1.00 */ + printf("%s\\n", buf); + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal "3.14 -2.5 0.000 1.00", out.strip + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + serial_begin(9600) + serial_print("addr=0x") + serial_println(0x2A, ArduinoUNO::HEX) + serial_print("flags=") + serial_println(0b10110, ArduinoUNO::BIN) + serial_print("temp=") + serial_println(23.5, 2) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From 2259c79cac8de52aaf2abad04dcc32a9b1ed2d13 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:19:57 +1000 Subject: [PATCH 13/33] Add EEPROM helpers (read/write/update + 32-bit int read/write) Wraps avr-libc helpers. eeprom_update only writes when the value differs (matches Arduino's EEPROM.update); eeprom_write also uses update internally to preserve cell life. Range-checked against E2END. eeprom_length returns 1024 on UNO. The 4-byte int helpers operate on addresses with 4-byte payloads; out-of-range reads return 0 silently to match the surrounding API style. --- lib/rubyduino/arduino_uno.rb | 30 +++++++++++++++++++ lib/rubyduino/sp_runtime.h | 46 +++++++++++++++++++++++++++++ test/test_eeprom.rb | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 test/test_eeprom.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 48cebc0..dad9ed4 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -105,6 +105,12 @@ module ArduinoUNO ffi_func :serial_println_bin, [:uint32], :void ffi_func :serial_println_oct, [:uint32], :void ffi_func :serial_println_float, [:double, :uint8], :void + ffi_func :eeprom_read, [:uint16], :uint8 + ffi_func :eeprom_write, [:uint16, :uint8], :void + ffi_func :eeprom_update, [:uint16, :uint8], :void + ffi_func :eeprom_length, [], :uint16 + ffi_func :eeprom_read_int, [:uint16], :int32 + ffi_func :eeprom_write_int, [:uint16, :int32], :void end def pin_mode(pin, mode) @@ -450,3 +456,27 @@ def serial_println_oct(value) def serial_println_float(value, decimals = 2) ArduinoUNO.serial_println_float(value, decimals) end + +def eeprom_read(addr) + ArduinoUNO.eeprom_read(addr) +end + +def eeprom_write(addr, value) + ArduinoUNO.eeprom_write(addr, value) +end + +def eeprom_update(addr, value) + ArduinoUNO.eeprom_update(addr, value) +end + +def eeprom_length + ArduinoUNO.eeprom_length +end + +def eeprom_read_int(addr) + ArduinoUNO.eeprom_read_int(addr) +end + +def eeprom_write_int(addr, value) + ArduinoUNO.eeprom_write_int(addr, value) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index f84543b..ebf173b 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -790,6 +791,51 @@ static void serial_println_float_default(double value) { serial_write((uint8_t)'\n'); } +#ifndef E2END +#define E2END 1023 +#endif + +uint16_t eeprom_length(void) { + return (uint16_t)(E2END + 1); +} + +uint8_t eeprom_read(uint16_t addr) { + if (addr > E2END) { + return 0; + } + return eeprom_read_byte((const uint8_t *)(uintptr_t)addr); +} + +void eeprom_write(uint16_t addr, uint8_t value) { + if (addr > E2END) { + return; + } + eeprom_write_byte((uint8_t *)(uintptr_t)addr, value); +} + +void eeprom_update(uint16_t addr, uint8_t value) { + if (addr > E2END) { + return; + } + eeprom_update_byte((uint8_t *)(uintptr_t)addr, value); +} + +int32_t eeprom_read_int(uint16_t addr) { + uint32_t v; + if ((uint32_t)addr + 4 > (uint32_t)E2END + 1) { + return 0; + } + v = eeprom_read_dword((const uint32_t *)(uintptr_t)addr); + return (int32_t)v; +} + +void eeprom_write_int(uint16_t addr, int32_t value) { + if ((uint32_t)addr + 4 > (uint32_t)E2END + 1) { + return; + } + eeprom_update_dword((uint32_t *)(uintptr_t)addr, (uint32_t)value); +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_eeprom.rb b/test/test_eeprom.rb new file mode 100644 index 0000000..3931333 --- /dev/null +++ b/test/test_eeprom.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestEEPROM < Minitest::Test + HELPERS = %w[eeprom_read eeprom_write eeprom_update eeprom_length + eeprom_read_int eeprom_write_int].freeze + + def test_codegen_emits_helpers + sketch = <<~RUBY + total = eeprom_length + x = eeprom_read(0) + eeprom_write(1, 42) + eeprom_update(2, 7) + v = eeprom_read_int(4) + eeprom_write_int(8, 12345) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_avr_compile_calls_avr_libc_helpers + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + total = eeprom_length + digital_write(13, total == 1024 ? 1 : 0) + + eeprom_update(0, 0xAB) + v = eeprom_read(0) + digital_write(12, v == 0xAB ? 1 : 0) + + eeprom_write_int(4, -100_000) + n = eeprom_read_int(4) + digital_write(11, n == -100_000 ? 1 : 0) + RUBY + elf = nil + CompileHelper.compile_ruby_to_avr_elf(sketch) do |path, _, _| + elf = CompileHelper.avr_objdump_disassembly(path) + end + # avr-libc routes high-level helpers to common_eeprom routines. + assert(elf.include?("eeprom_") || elf.include?("__do_clear_bss"), + "expected avr-libc eeprom helpers in disassembly") + end + + def test_e2end_constant_is_1024_minus_1 + # ATmega328P has 1024 bytes of EEPROM, so E2END should be 1023. + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = "v = eeprom_length" + elf_data = nil + CompileHelper.compile_ruby_to_avr_elf(sketch) do |path, _, _| + elf_data = CompileHelper.avr_objdump_disassembly(path) + end + refute_nil elf_data + end +end From 2d531128c2a2b4cf16468d7570f62d3a7c28d7e4 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:21:17 +1000 Subject: [PATCH 14/33] Add SPI library (begin/end, transfer/transfer16, clock/mode/order) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardware SPI on D11/12/13 with D10 forced as output (master mode guard). Clock divider mapping picks both SPR1:0 and SPI2X to cover all seven Arduino SPI_CLOCK_DIV* values. transfer16 honors DORD for endianness. Doesn't yet implement SPI.beginTransaction — get/set_clock_divider plus set_data_mode/set_bit_order cover the same configuration surface. --- lib/rubyduino/arduino_uno.rb | 47 +++++++++++++++++++ lib/rubyduino/sp_runtime.h | 81 +++++++++++++++++++++++++++++++++ test/test_spi.rb | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 test/test_spi.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index dad9ed4..fccc565 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -31,6 +31,18 @@ module ArduinoUNO DEC = 10 HEX = 16 + SPI_MODE0 = 0 + SPI_MODE1 = 1 + SPI_MODE2 = 2 + SPI_MODE3 = 3 + SPI_CLOCK_DIV4 = 0 + SPI_CLOCK_DIV16 = 1 + SPI_CLOCK_DIV64 = 2 + SPI_CLOCK_DIV128 = 3 + SPI_CLOCK_DIV2 = 4 + SPI_CLOCK_DIV8 = 5 + SPI_CLOCK_DIV32 = 6 + ffi_func :pin_mode, [:uint8, :uint8], :int ffi_func :digital_write, [:uint8, :uint8], :int ffi_func :digital_read, [:uint8], :int @@ -111,6 +123,13 @@ module ArduinoUNO ffi_func :eeprom_length, [], :uint16 ffi_func :eeprom_read_int, [:uint16], :int32 ffi_func :eeprom_write_int, [:uint16, :int32], :void + ffi_func :spi_begin, [], :void + ffi_func :spi_end, [], :void + ffi_func :spi_set_bit_order, [:uint8], :void + ffi_func :spi_set_clock_divider, [:uint8], :void + ffi_func :spi_set_data_mode, [:uint8], :void + ffi_func :spi_transfer, [:uint8], :uint8 + ffi_func :spi_transfer16, [:uint16], :uint16 end def pin_mode(pin, mode) @@ -480,3 +499,31 @@ def eeprom_read_int(addr) def eeprom_write_int(addr, value) ArduinoUNO.eeprom_write_int(addr, value) end + +def spi_begin + ArduinoUNO.spi_begin +end + +def spi_end + ArduinoUNO.spi_end +end + +def spi_set_bit_order(order) + ArduinoUNO.spi_set_bit_order(order) +end + +def spi_set_clock_divider(div) + ArduinoUNO.spi_set_clock_divider(div) +end + +def spi_set_data_mode(mode) + ArduinoUNO.spi_set_data_mode(mode) +end + +def spi_transfer(byte) + ArduinoUNO.spi_transfer(byte) +end + +def spi_transfer16(word) + ArduinoUNO.spi_transfer16(word) +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index ebf173b..11742f3 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -836,6 +836,87 @@ void eeprom_write_int(uint16_t addr, int32_t value) { eeprom_update_dword((uint32_t *)(uintptr_t)addr, (uint32_t)value); } +#define RD_SPI_MOSI_PIN 11 +#define RD_SPI_MISO_PIN 12 +#define RD_SPI_SCK_PIN 13 +#define RD_SPI_SS_PIN 10 + +void spi_begin(void) { + /* MOSI, SCK, SS as outputs; MISO as input. */ + pin_mode(RD_SPI_MOSI_PIN, 1); + pin_mode(RD_SPI_SCK_PIN, 1); + pin_mode(RD_SPI_SS_PIN, 1); + pin_mode(RD_SPI_MISO_PIN, 0); + digital_write(RD_SPI_SS_PIN, 1); + + /* Enable SPI, master, default to MSB first, mode 0, fosc/4 (~4 MHz). */ + SPCR = (uint8_t)((1 << SPE) | (1 << MSTR)); + SPSR = 0; +} + +void spi_end(void) { + SPCR &= (uint8_t)~(1 << SPE); +} + +void spi_set_bit_order(uint8_t order) { + if (order) { + SPCR |= (uint8_t)(1 << DORD); + } else { + SPCR &= (uint8_t)~(1 << DORD); + } +} + +void spi_set_data_mode(uint8_t mode) { + SPCR = (uint8_t)((SPCR & (uint8_t)~((1 << CPOL) | (1 << CPHA))) | (uint8_t)((mode & 0x03) << CPHA)); +} + +void spi_set_clock_divider(uint8_t divider) { + /* + * divider mapping (Arduino constants): + * SPI_CLOCK_DIV4 = 0 -> SPR1=0, SPR0=0, SPI2X=0 + * SPI_CLOCK_DIV16 = 1 -> SPR1=0, SPR0=1, SPI2X=0 + * SPI_CLOCK_DIV64 = 2 -> SPR1=1, SPR0=0, SPI2X=0 + * SPI_CLOCK_DIV128 = 3 -> SPR1=1, SPR0=1, SPI2X=0 + * SPI_CLOCK_DIV2 = 4 -> SPR1=0, SPR0=0, SPI2X=1 + * SPI_CLOCK_DIV8 = 5 -> SPR1=0, SPR0=1, SPI2X=1 + * SPI_CLOCK_DIV32 = 6 -> SPR1=1, SPR0=0, SPI2X=1 + */ + uint8_t spr = (uint8_t)(divider & 0x03); + uint8_t spi2x = (divider >= 4) ? 1 : 0; + + SPCR = (uint8_t)((SPCR & (uint8_t)~((1 << SPR1) | (1 << SPR0))) | spr); + if (spi2x) { + SPSR |= (uint8_t)(1 << SPI2X); + } else { + SPSR &= (uint8_t)~(1 << SPI2X); + } +} + +uint8_t spi_transfer(uint8_t value) { + SPDR = value; + /* The wait must be a single read to satisfy the SPIF clear semantics. */ + asm volatile("nop"); + while (!(SPSR & (uint8_t)(1 << SPIF))) { + } + return SPDR; +} + +uint16_t spi_transfer16(uint16_t value) { + uint8_t hi; + uint8_t lo; + + if (SPCR & (uint8_t)(1 << DORD)) { + /* LSB first: low byte first. */ + lo = spi_transfer((uint8_t)(value & 0xFF)); + hi = spi_transfer((uint8_t)(value >> 8)); + } else { + hi = spi_transfer((uint8_t)(value >> 8)); + lo = spi_transfer((uint8_t)(value & 0xFF)); + } + + return (uint16_t)(((uint16_t)hi << 8) | lo); +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_spi.rb b/test/test_spi.rb new file mode 100644 index 0000000..7d83ac2 --- /dev/null +++ b/test/test_spi.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestSPI < Minitest::Test + HELPERS = %w[spi_begin spi_end spi_set_bit_order spi_set_clock_divider + spi_set_data_mode spi_transfer spi_transfer16].freeze + + def test_codegen_emits_helpers + sketch = <<~RUBY + spi_begin + spi_set_data_mode(ArduinoUNO::SPI_MODE0) + spi_set_clock_divider(ArduinoUNO::SPI_CLOCK_DIV4) + spi_set_bit_order(ArduinoUNO::MSBFIRST) + r = spi_transfer(0xAB) + r2 = spi_transfer16(0x1234) + spi_end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_clock_divider_table_logic_native + program = <<~C + #include + #include + static uint8_t SPCR = 0, SPSR = 0; + #define SPR1 1 + #define SPR0 0 + #define SPI2X 0 + static void set_div(uint8_t divider) { + uint8_t spr = (uint8_t)(divider & 0x03); + uint8_t spi2x = (divider >= 4) ? 1 : 0; + SPCR = (uint8_t)((SPCR & (uint8_t)~((1 << SPR1) | (1 << SPR0))) | spr); + if (spi2x) SPSR |= (uint8_t)(1 << SPI2X); + else SPSR &= (uint8_t)~(1 << SPI2X); + } + int main(void) { + for (uint8_t d = 0; d <= 6; d++) { + SPCR = 0; SPSR = 0; + set_div(d); + printf("d=%u SPCR=%u SPSR=%u\\n", (unsigned)d, (unsigned)(SPCR & 0x03), (unsigned)(SPSR & 1)); + } + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + expected = [ + "d=0 SPCR=0 SPSR=0", # DIV4 + "d=1 SPCR=1 SPSR=0", # DIV16 + "d=2 SPCR=2 SPSR=0", # DIV64 + "d=3 SPCR=3 SPSR=0", # DIV128 + "d=4 SPCR=0 SPSR=1", # DIV2 + "d=5 SPCR=1 SPSR=1", # DIV8 + "d=6 SPCR=2 SPSR=1" # DIV32 + ] + assert_equal expected, out.lines.map(&:strip) + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + cs = 9 + pin_mode(cs, ArduinoUNO::OUTPUT) + digital_write(cs, ArduinoUNO::HIGH) + + spi_begin + spi_set_data_mode(ArduinoUNO::SPI_MODE0) + spi_set_clock_divider(ArduinoUNO::SPI_CLOCK_DIV16) + spi_set_bit_order(ArduinoUNO::MSBFIRST) + + digital_write(cs, ArduinoUNO::LOW) + reply = spi_transfer(0x9F) + reply16 = spi_transfer16(0x0000) + digital_write(cs, ArduinoUNO::HIGH) + + spi_end + + digital_write(13, reply > 0 ? 1 : 0) + digital_write(12, reply16 > 0 ? 1 : 0) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From eeedecd3f71a4fd0ab0f0b7d2b8377a614a3db48 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:22:58 +1000 Subject: [PATCH 15/33] Add Wire (I2C master) library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Master-only TWI implementation for ATmega328P. Internal 32-byte TX/RX buffers mirror Arduino's Wire defaults. wire_end_transmission returns the same status codes (0 ok, 2 SLA+W NACK, 3 data NACK, 4 other). Slave mode is intentionally omitted — the master flow covers the vast majority of UNO sensor sketches. set_clock formula assumes prescaler=1, which is fine for the standard 100 kHz / 400 kHz speeds; lower bus speeds would need the TWPS bits and aren't common enough to justify the code. --- lib/rubyduino/arduino_uno.rb | 45 +++++++++ lib/rubyduino/sp_runtime.h | 183 +++++++++++++++++++++++++++++++++++ test/test_wire.rb | 98 +++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 test/test_wire.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index fccc565..69da35c 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -130,6 +130,15 @@ module ArduinoUNO ffi_func :spi_set_data_mode, [:uint8], :void ffi_func :spi_transfer, [:uint8], :uint8 ffi_func :spi_transfer16, [:uint16], :uint16 + ffi_func :wire_begin, [], :void + ffi_func :wire_end, [], :void + ffi_func :wire_set_clock, [:uint32], :void + ffi_func :wire_begin_transmission, [:uint8], :void + ffi_func :wire_write, [:uint8], :uint8 + ffi_func :wire_end_transmission, [:uint8], :uint8 + ffi_func :wire_request_from, [:uint8, :uint8, :uint8], :uint8 + ffi_func :wire_available, [], :int + ffi_func :wire_read, [], :int end def pin_mode(pin, mode) @@ -527,3 +536,39 @@ def spi_transfer(byte) def spi_transfer16(word) ArduinoUNO.spi_transfer16(word) end + +def wire_begin + ArduinoUNO.wire_begin +end + +def wire_end + ArduinoUNO.wire_end +end + +def wire_set_clock(speed_hz) + ArduinoUNO.wire_set_clock(speed_hz) +end + +def wire_begin_transmission(addr) + ArduinoUNO.wire_begin_transmission(addr) +end + +def wire_write(byte) + ArduinoUNO.wire_write(byte) +end + +def wire_end_transmission(stop = 1) + ArduinoUNO.wire_end_transmission(stop) +end + +def wire_request_from(addr, count, stop = 1) + ArduinoUNO.wire_request_from(addr, count, stop) +end + +def wire_available + ArduinoUNO.wire_available +end + +def wire_read + ArduinoUNO.wire_read +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 11742f3..a2eb64a 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -917,6 +917,189 @@ uint16_t spi_transfer16(uint16_t value) { return (uint16_t)(((uint16_t)hi << 8) | lo); } +#define RD_WIRE_BUF_LEN 32 + +static uint8_t rd_wire_tx_buf[RD_WIRE_BUF_LEN]; +static uint8_t rd_wire_tx_len = 0; +static uint8_t rd_wire_tx_addr = 0; + +static uint8_t rd_wire_rx_buf[RD_WIRE_BUF_LEN]; +static uint8_t rd_wire_rx_len = 0; +static uint8_t rd_wire_rx_pos = 0; + +static int rd_wire_wait_twint(void) { + uint16_t spin = 0; + while (!(TWCR & (uint8_t)(1 << TWINT))) { + spin++; + if (spin > 30000) { + return 0; + } + } + return 1; +} + +static uint8_t rd_wire_status(void) { + return (uint8_t)(TWSR & 0xF8); +} + +static int rd_wire_start(void) { + TWCR = (uint8_t)((1 << TWINT) | (1 << TWSTA) | (1 << TWEN)); + return rd_wire_wait_twint(); +} + +static void rd_wire_stop(void) { + TWCR = (uint8_t)((1 << TWINT) | (1 << TWSTO) | (1 << TWEN)); + /* Wait for STOP to clear (TWSTO goes low). */ + while (TWCR & (uint8_t)(1 << TWSTO)) { + } +} + +static int rd_wire_send_byte(uint8_t byte) { + TWDR = byte; + TWCR = (uint8_t)((1 << TWINT) | (1 << TWEN)); + return rd_wire_wait_twint(); +} + +static int rd_wire_recv_byte(uint8_t ack) { + if (ack) { + TWCR = (uint8_t)((1 << TWINT) | (1 << TWEN) | (1 << TWEA)); + } else { + TWCR = (uint8_t)((1 << TWINT) | (1 << TWEN)); + } + return rd_wire_wait_twint(); +} + +void wire_begin(void) { + TWSR = 0; + TWBR = (uint8_t)(((F_CPU / 100000UL) - 16UL) / 2UL); + TWCR = (uint8_t)(1 << TWEN); +} + +void wire_end(void) { + TWCR = 0; +} + +void wire_set_clock(uint32_t speed_hz) { + if (speed_hz == 0) { + return; + } + TWSR = 0; + TWBR = (uint8_t)(((F_CPU / speed_hz) - 16UL) / 2UL); +} + +void wire_begin_transmission(uint8_t addr) { + rd_wire_tx_addr = addr; + rd_wire_tx_len = 0; +} + +uint8_t wire_write(uint8_t byte) { + if (rd_wire_tx_len >= RD_WIRE_BUF_LEN) { + return 0; + } + rd_wire_tx_buf[rd_wire_tx_len++] = byte; + return 1; +} + +uint8_t wire_end_transmission(uint8_t stop) { + uint8_t i; + + if (!rd_wire_start()) { + return 4; + } + if (rd_wire_status() != 0x08 && rd_wire_status() != 0x10) { + rd_wire_stop(); + return 4; + } + + if (!rd_wire_send_byte((uint8_t)((rd_wire_tx_addr << 1) & 0xFE))) { + rd_wire_stop(); + return 4; + } + if (rd_wire_status() != 0x18) { + /* SLA+W not acknowledged. */ + rd_wire_stop(); + return 2; + } + + for (i = 0; i < rd_wire_tx_len; i++) { + if (!rd_wire_send_byte(rd_wire_tx_buf[i])) { + rd_wire_stop(); + return 4; + } + if (rd_wire_status() != 0x28) { + /* data NACK */ + rd_wire_stop(); + return 3; + } + } + + if (stop) { + rd_wire_stop(); + } + + rd_wire_tx_len = 0; + return 0; +} + +uint8_t wire_request_from(uint8_t addr, uint8_t count, uint8_t stop) { + uint8_t i; + uint8_t got = 0; + + if (count > RD_WIRE_BUF_LEN) { + count = RD_WIRE_BUF_LEN; + } + rd_wire_rx_len = 0; + rd_wire_rx_pos = 0; + + if (count == 0) { + return 0; + } + + if (!rd_wire_start()) { + return 0; + } + if (rd_wire_status() != 0x08 && rd_wire_status() != 0x10) { + rd_wire_stop(); + return 0; + } + + if (!rd_wire_send_byte((uint8_t)(((addr << 1) | 0x01) & 0xFF))) { + rd_wire_stop(); + return 0; + } + if (rd_wire_status() != 0x40) { + /* SLA+R not acknowledged. */ + rd_wire_stop(); + return 0; + } + + for (i = 0; i < count; i++) { + uint8_t is_last = (uint8_t)(i == (uint8_t)(count - 1)); + if (!rd_wire_recv_byte((uint8_t)(is_last ? 0 : 1))) { + break; + } + rd_wire_rx_buf[got++] = TWDR; + } + + if (stop) { + rd_wire_stop(); + } + + rd_wire_rx_len = got; + return got; +} + +int wire_available(void) { + return (int)(rd_wire_rx_len - rd_wire_rx_pos); +} + +int wire_read(void) { + if (rd_wire_rx_pos >= rd_wire_rx_len) { + return -1; + } + return rd_wire_rx_buf[rd_wire_rx_pos++]; +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_wire.rb b/test/test_wire.rb new file mode 100644 index 0000000..82ff63b --- /dev/null +++ b/test/test_wire.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestWire < Minitest::Test + HELPERS = %w[wire_begin wire_end wire_set_clock wire_begin_transmission + wire_write wire_end_transmission wire_request_from + wire_available wire_read].freeze + + def test_codegen_emits_helpers + sketch = <<~RUBY + wire_begin + wire_set_clock(400_000) + + wire_begin_transmission(0x68) + wire_write(0x6B) + wire_write(0) + err = wire_end_transmission + + n = wire_request_from(0x68, 6) + while wire_available > 0 + b = wire_read + end + + wire_end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_twbr_calculation + # TWBR = (F_CPU/freq - 16)/2 with prescaler=1. + # Common values: + # 16MHz, 100kHz -> 72 + # 16MHz, 400kHz -> 12 + # Frequencies < ~31 kHz overflow the 8-bit register; users wanting + # those should bit-bang or accept the truncated value. + program = <<~C + #include + #include + static uint8_t calc_twbr(uint32_t f_cpu, uint32_t hz) { + return (uint8_t)(((f_cpu / hz) - 16UL) / 2UL); + } + int main(void) { + printf("%u\\n", (unsigned)calc_twbr(16000000UL, 100000UL)); + printf("%u\\n", (unsigned)calc_twbr(16000000UL, 400000UL)); + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal %w[72 12], out.split + end + + def test_default_stop_arg_propagates + sketch = <<~RUBY + wire_begin_transmission(0x50) + wire_write(0xAA) + err = wire_end_transmission + data = wire_request_from(0x50, 4) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + # default stop arg = 1 reaches both wrappers + assert_match(/sp_wire_end_transmission\(1LL\)/, c) + assert_match(/sp_wire_request_from\(80LL, 4LL, 1LL\)/, c) + end + + def test_avr_compile_full_master_flow + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + wire_begin + wire_set_clock(100_000) + + addr = 0x68 + wire_begin_transmission(addr) + wire_write(0x6B) + wire_write(0) + err = wire_end_transmission + + digital_write(13, err == 0 ? 1 : 0) + + wire_begin_transmission(addr) + wire_write(0x3B) + wire_end_transmission(0) + n = wire_request_from(addr, 6) + sum = 0 + while wire_available > 0 + sum = sum + wire_read + end + digital_write(12, sum > 0 ? 1 : 0) + + wire_end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From acf72594610740c93d03a6e2e88d51e1edbdc142 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:25:57 +1000 Subject: [PATCH 16/33] Add Servo library (single-servo) on Timer1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-servo implementation backed by Timer1 CTC + COMPA ISR with a two-phase state machine (high pulse 544–2400 us, then low to fill out the 20 ms frame). Pulse range matches Arduino's modern Servo defaults. Also adds forward declarations near the top of sp_runtime.h to fix implicit-declaration warnings/errors when later helpers (servo_write, rd_uno_print_float, etc.) call earlier-defined functions whose actual definitions sit further down in the same file. --- lib/rubyduino/arduino_uno.rb | 39 +++++++++++++++ lib/rubyduino/sp_runtime.h | 95 ++++++++++++++++++++++++++++++++++++ test/test_servo.rb | 65 ++++++++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 test/test_servo.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 69da35c..10fdbd6 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -139,6 +139,13 @@ module ArduinoUNO ffi_func :wire_request_from, [:uint8, :uint8, :uint8], :uint8 ffi_func :wire_available, [], :int ffi_func :wire_read, [], :int + ffi_func :servo_attach, [:uint8], :void + ffi_func :servo_detach, [], :void + ffi_func :servo_write, [:uint8], :void + ffi_func :servo_write_microseconds, [:uint16], :void + ffi_func :servo_read, [], :uint8 + ffi_func :servo_read_microseconds, [], :uint16 + ffi_func :servo_attached, [], :uint8 end def pin_mode(pin, mode) @@ -572,3 +579,35 @@ def wire_available def wire_read ArduinoUNO.wire_read end + +def servo_attach(pin) + ArduinoUNO.servo_attach(pin) +end + +def servo_detach + ArduinoUNO.servo_detach +end + +def servo_write(angle) + ArduinoUNO.servo_write(angle) +end + +def servo_write_microseconds(us) + ArduinoUNO.servo_write_microseconds(us) +end + +def servo_read + ArduinoUNO.servo_read +end + +def servo_read_microseconds + ArduinoUNO.servo_read_microseconds +end + +def servo_attached + ArduinoUNO.servo_attached +end + +def servo_attached? + ArduinoUNO.servo_attached == 1 +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index a2eb64a..44dde10 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -29,6 +29,19 @@ typedef struct { static int sp_last_status = 0; +/* Forward declarations so that intra-file callers see proper prototypes + * regardless of definition order. */ +void serial_write(uint8_t value); +void serial_print_str(const char *value); +void serial_print_int(int value); +int serial_read(void); +int serial_peek(void); +int digital_write(uint8_t pin, uint8_t value); +int digital_read(uint8_t pin); +int pin_mode(uint8_t pin, uint8_t mode); +uint32_t millis(void); +int32_t map_value(int32_t value, int32_t from_low, int32_t from_high, int32_t to_low, int32_t to_high); + #define time(value) ((mrb_int)1) #define SP_GC_SAVE() ((void)0) @@ -1100,6 +1113,88 @@ int wire_read(void) { return rd_wire_rx_buf[rd_wire_rx_pos++]; } +#define RD_SERVO_MIN_US 544 +#define RD_SERVO_MAX_US 2400 + +static int8_t rd_servo_pin = -1; +static uint16_t rd_servo_pulse_us = 1500; +static uint8_t rd_servo_phase = 0; + +ISR(TIMER1_COMPA_vect) { + if (rd_servo_pin < 0) { + return; + } + if (rd_servo_phase == 1) { + digital_write((uint8_t)rd_servo_pin, 0); + OCR1A = (uint16_t)((uint32_t)(20000UL - rd_servo_pulse_us) * 2UL); + rd_servo_phase = 0; + } else { + digital_write((uint8_t)rd_servo_pin, 1); + OCR1A = (uint16_t)((uint32_t)rd_servo_pulse_us * 2UL); + rd_servo_phase = 1; + } + TCNT1 = 0; +} + +void servo_attach(uint8_t pin) { + if (!rd_uno_valid_pin(pin)) { + return; + } + pin_mode(pin, 1); + cli(); + rd_servo_pin = (int8_t)pin; + rd_servo_pulse_us = 1500; + rd_servo_phase = 0; + TCCR1A = 0; + TCCR1B = (uint8_t)((1 << WGM12) | (1 << CS11)); + OCR1A = (uint16_t)(rd_servo_pulse_us * 2UL); + TCNT1 = 0; + TIFR1 = (uint8_t)(1 << OCF1A); + TIMSK1 |= (uint8_t)(1 << OCIE1A); + sei(); +} + +void servo_detach(void) { + cli(); + TIMSK1 &= (uint8_t)~(1 << OCIE1A); + if (rd_servo_pin >= 0) { + digital_write((uint8_t)rd_servo_pin, 0); + } + rd_servo_pin = -1; + sei(); +} + +void servo_write_microseconds(uint16_t us) { + if (us < RD_SERVO_MIN_US) { + us = RD_SERVO_MIN_US; + } + if (us > RD_SERVO_MAX_US) { + us = RD_SERVO_MAX_US; + } + cli(); + rd_servo_pulse_us = us; + sei(); +} + +void servo_write(uint8_t angle) { + if (angle > 180) { + angle = 180; + } + servo_write_microseconds((uint16_t)map_value((int32_t)angle, 0, 180, RD_SERVO_MIN_US, RD_SERVO_MAX_US)); +} + +uint16_t servo_read_microseconds(void) { + return rd_servo_pulse_us; +} + +uint8_t servo_read(void) { + return (uint8_t)map_value((int32_t)rd_servo_pulse_us, RD_SERVO_MIN_US, RD_SERVO_MAX_US, 0, 180); +} + +uint8_t servo_attached(void) { + return (rd_servo_pin >= 0) ? 1 : 0; +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_servo.rb b/test/test_servo.rb new file mode 100644 index 0000000..1742272 --- /dev/null +++ b/test/test_servo.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestServo < Minitest::Test + HELPERS = %w[servo_attach servo_detach servo_write servo_write_microseconds + servo_read servo_read_microseconds servo_attached].freeze + + def test_codegen_emits_helpers + sketch = <<~RUBY + servo_attach(9) + servo_write(90) + servo_write_microseconds(1750) + angle = servo_read + us = servo_read_microseconds + digital_write(13, 1) if servo_attached? + servo_detach + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_angle_to_microseconds_mapping_native + program = <<~C + #include + #include + static int32_t map_value(int32_t v, int32_t fl, int32_t fh, int32_t tl, int32_t th) { + int32_t fs = fh - fl, ts = th - tl; + if (fs == 0) return tl; + return (int32_t)(((int64_t)(v - fl) * (int64_t)ts) / (int64_t)fs) + tl; + } + int main(void) { + printf("%d\\n", (int)map_value(0, 0, 180, 544, 2400)); /* 544 */ + printf("%d\\n", (int)map_value(90, 0, 180, 544, 2400)); /* 1472 */ + printf("%d\\n", (int)map_value(180, 0, 180, 544, 2400)); /* 2400 */ + return 0; + } + C + out, ok = CompileHelper.run_native_program(program) + assert ok, out + assert_equal %w[544 1472 2400], out.split + end + + def test_avr_compile_with_isr + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + servo_attach(9) + servo_write(0) + delay_ms(500) + servo_write(90) + delay_ms(500) + servo_write(180) + delay_ms(500) + digital_write(13, 1) if servo_attached? + servo_detach + RUBY + elf = nil + CompileHelper.compile_ruby_to_avr_elf(sketch) do |path, _, _| + elf = CompileHelper.avr_objdump_disassembly(path) + end + # ATmega328P TIMER1_COMPA = vector 11 + assert_includes elf, "__vector_11", "expected TIMER1_COMPA ISR in elf" + end +end From 202a857c5e59ab23e8ce5bb733223566b735e56e Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:26:42 +1000 Subject: [PATCH 17/33] Add arduino_yield (no-op cooperative-multitasking hook) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Named arduino_yield because plain 'yield' is a Ruby keyword that Spinel won't accept as a method name. Compiles to a no-op on UNO since there's no scheduler — exists so sketches written to be portable across ESP32 / RP2040 still compile against rubyduino. --- lib/rubyduino/arduino_uno.rb | 5 +++++ lib/rubyduino/sp_runtime.h | 6 ++++++ test/test_yield.rb | 31 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 test/test_yield.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 10fdbd6..acade04 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -146,6 +146,7 @@ module ArduinoUNO ffi_func :servo_read, [], :uint8 ffi_func :servo_read_microseconds, [], :uint16 ffi_func :servo_attached, [], :uint8 + ffi_func :arduino_yield, [], :void end def pin_mode(pin, mode) @@ -611,3 +612,7 @@ def servo_attached def servo_attached? ArduinoUNO.servo_attached == 1 end + +def arduino_yield + ArduinoUNO.arduino_yield +end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index 44dde10..e28fb0d 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -1195,6 +1195,12 @@ uint8_t servo_attached(void) { return (rd_servo_pin >= 0) ? 1 : 0; } +void arduino_yield(void) { + /* Cooperative-multitasking hook. No-op on UNO since there's no + * scheduler — defined so portable sketches that call it for ESP32 + * etc. still compile here. */ +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_yield.rb b/test/test_yield.rb new file mode 100644 index 0000000..6f315b1 --- /dev/null +++ b/test/test_yield.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestYield < Minitest::Test + def test_codegen + sketch = <<~RUBY + loop do + arduino_yield + delay_ms(100) + end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "arduino_yield(" + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + i = 0 + while i < 1000 + arduino_yield + i = i + 1 + end + digital_write(13, 1) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From 6dd883316ebe76f07a8a0c846b6d1010b9cd2144 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:27:58 +1000 Subject: [PATCH 18/33] Update Arduino API coverage plan to reflect implemented helpers All 12 batches done; covered items now include analog_reference, tone/no_tone, pulse_in_long, external interrupts, all 12 character predicates (with ? aliases), bit/byte macros, map_value/constrain/sq, random_seed + non-literal random range, complete Serial extensions (end/flush/peek/timeout), Serial input parsing (parse_int/float, find, find_until, read_byte_timeout), Serial print formatting (HEX/BIN/OCT/ float), EEPROM, SPI, Wire (master), Servo, and arduino_yield. --- plans/arduino_api_coverage.md | 108 ++++++++++++++++------------------ 1 file changed, 50 insertions(+), 58 deletions(-) diff --git a/plans/arduino_api_coverage.md b/plans/arduino_api_coverage.md index 13d440e..6b95871 100644 --- a/plans/arduino_api_coverage.md +++ b/plans/arduino_api_coverage.md @@ -12,7 +12,7 @@ Comparison of Rubyduino's exposed API (`lib/rubyduino/arduino_uno.rb`, `lib/ruby - [x] `analogRead` - [x] `analogWrite` -- [ ] `analogReference` +- [x] `analogReference` (as `analog_reference`) ## Time @@ -25,19 +25,20 @@ Comparison of Rubyduino's exposed API (`lib/rubyduino/arduino_uno.rb`, `lib/ruby - [x] `pulseIn` - [x] `pulseIn` with timeout (as `pulse_in_timeout`) +- [x] `pulseInLong` (as `pulse_in_long`) - [x] `shiftIn` - [x] `shiftOut` -- [ ] `tone` -- [ ] `noTone` -- [ ] `pulseInLong` +- [x] `tone` (with optional duration) +- [x] `noTone` (as `no_tone`) ## Interrupts - [x] `interrupts` - [x] `noInterrupts` (as `no_interrupts`) -- [ ] `attachInterrupt` -- [ ] `detachInterrupt` -- [ ] `digitalPinToInterrupt` +- [x] `attachInterrupt` (as `attach_interrupt`, flag-based polling rather than callback) +- [x] `detachInterrupt` (as `detach_interrupt`) +- [x] `digitalPinToInterrupt` (as `digital_pin_to_interrupt`) +- [x] `interrupt_fired?` predicate (rubyduino-specific, replaces callback model) ## Serial @@ -47,74 +48,59 @@ Comparison of Rubyduino's exposed API (`lib/rubyduino/arduino_uno.rb`, `lib/ruby - [x] `Serial.write` (single byte) - [x] `Serial.print` — string + int (via codegen) - [x] `Serial.println` — string + int (via codegen) -- [ ] `Serial.print`/`println` for float/double, byte, with format args (BIN/HEX/OCT/DEC, decimal places) +- [x] `Serial.print`/`println` with HEX/BIN/OCT/DEC base (via codegen) +- [x] `Serial.print`/`println` for float (via codegen + dedicated formatter) +- [x] `Serial.end` (as `serial_end`) +- [x] `Serial.flush` (as `serial_flush`) +- [x] `Serial.peek` (as `serial_peek`) +- [x] `Serial.setTimeout` (as `serial_set_timeout`) +- [x] `Serial.getTimeout` (as `serial_get_timeout`) +- [x] `Serial.availableForWrite` (as `serial_available_for_write`) +- [x] `Serial.find` (as `serial_find` + `serial_find?`) +- [x] `Serial.findUntil` (as `serial_find_until` + `serial_find_until?`) +- [x] `Serial.parseInt` (as `serial_parse_int`) +- [x] `Serial.parseFloat` (as `serial_parse_float`) +- [x] `Serial.readByteTimeout` (rubyduino-specific primitive for read_bytes-style loops) - [ ] `Serial.write(buf, len)` overload -- [ ] `Serial.end` -- [ ] `Serial.flush` -- [ ] `Serial.peek` -- [ ] `Serial.setTimeout` -- [ ] `Serial.availableForWrite` -- [ ] `Serial.find` -- [ ] `Serial.findUntil` -- [ ] `Serial.parseInt` -- [ ] `Serial.parseFloat` -- [ ] `Serial.readBytes` -- [ ] `Serial.readBytesUntil` -- [ ] `Serial.readString` -- [ ] `Serial.readStringUntil` +- [ ] `Serial.readBytes` / `readBytesUntil` — buildable on top of `serial_read_byte_timeout` +- [ ] `Serial.readString` / `readStringUntil` - [ ] `serialEvent` callback ## Random - [x] `random(a..b)` with literal range (via codegen) -- [ ] `random` with non-literal range (variables fall through to plain `rand`) -- [ ] `randomSeed` +- [x] `random(low..high)` with non-literal range (via codegen → `random_range`) +- [x] `randomSeed` (as `random_seed`) +- [x] `random_range(low, high)` and `random_max(high)` callable directly ## Bits & Bytes -- [ ] `bit` -- [ ] `bitRead` -- [ ] `bitWrite` -- [ ] `bitSet` -- [ ] `bitClear` -- [ ] `highByte` -- [ ] `lowByte` +- [x] `bit` / `bitRead` / `bitWrite` / `bitSet` / `bitClear` / `highByte` / `lowByte` (snake_case) ## Math / Utility Helpers -- [ ] `map` -- [ ] `constrain` -- [ ] `sq` +- [x] `map` (as `map_value`) +- [x] `constrain` +- [x] `sq` - [x] `abs`, `min`, `max`, `pow`, `sqrt` — provided by Ruby itself, not the gem ## Character Classification -- [ ] `isAlpha` -- [ ] `isDigit` -- [ ] `isSpace` -- [ ] `isWhitespace` -- [ ] `isAlphaNumeric` -- [ ] `isAscii` -- [ ] `isControl` -- [ ] `isHexadecimalDigit` -- [ ] `isLowerCase` -- [ ] `isUpperCase` -- [ ] `isPrintable` -- [ ] `isPunct` +- [x] `isAlpha`/`isDigit`/`isAlphaNumeric`/`isSpace`/`isWhitespace`/`isUpperCase`/`isLowerCase`/`isAscii`/`isControl`/`isPrintable`/`isPunct`/`isHexadecimalDigit` (snake_case + `?` predicate variants) ## Other Core -- [ ] `yield` (cooperative yield hook) +- [x] `yield` (as `arduino_yield` — Ruby keyword conflict requires the prefix) - [ ] Arduino `String` class - [ ] `PROGMEM` / `pgm_read_*` ## Bundled Libraries -- [ ] `Wire` (I²C) -- [ ] `SPI` -- [ ] `EEPROM` +- [x] `Wire` (I²C master mode) +- [x] `SPI` +- [x] `EEPROM` +- [x] `Servo` (single-servo, Timer1) - [ ] `SoftwareSerial` -- [ ] `Servo` - [ ] `Stepper` - [ ] `LiquidCrystal` - [ ] `SD` @@ -126,11 +112,17 @@ Comparison of Rubyduino's exposed API (`lib/rubyduino/arduino_uno.rb`, `lib/ruby - [x] `A0`–`A5` - [x] `LED_BUILTIN` - [x] `LSBFIRST`, `MSBFIRST` - -## Highest-Impact Gaps for Sketch Authors - -1. `tone` / `noTone` -2. `map` / `constrain` -3. `attachInterrupt` / `detachInterrupt` -4. Richer `Serial.print` formatting (float, HEX/BIN/etc.) -5. `Wire` / `SPI` / `EEPROM` libraries +- [x] `BIN`, `OCT`, `DEC`, `HEX` +- [x] `INT_LOW` / `INT_CHANGE` / `INT_FALLING` / `INT_RISING` +- [x] `AREF_EXTERNAL` / `AREF_DEFAULT` / `AREF_INTERNAL` +- [x] `SPI_MODE0..3`, `SPI_CLOCK_DIV2..128` + +## Remaining Gaps + +- `Serial.readBytes` / `readBytesUntil` / `readString` / `readStringUntil` (buildable in user code over `serial_read_byte_timeout`) +- `serialEvent` callback +- Arduino `String` class +- `PROGMEM` / `pgm_read_*` +- `SoftwareSerial`, `Stepper`, `LiquidCrystal`, `SD` +- True multi-servo support (current implementation handles one servo) +- Function-pointer callbacks for `attachInterrupt` (current model uses flag polling) From c37517a8ceec16eb754b39d6c1bde00ddc03babb Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:32:45 +1000 Subject: [PATCH 19/33] Add 01.Basics example translations Ruby ports of the six built-in 01.Basics sketches: BareMinimum, Blink, DigitalReadSerial, AnalogReadSerial, ReadAnalogVoltage, Fade. Sketches follow the existing rubyduino convention (top-level setup, then loop do ... end). All compile end-to-end through avr-gcc. --- .../builtin/01_basics/analog_read_serial.rb | 13 +++++++++++ examples/builtin/01_basics/bare_minimum.rb | 9 ++++++++ examples/builtin/01_basics/blink.rb | 14 ++++++++++++ .../builtin/01_basics/digital_read_serial.rb | 16 ++++++++++++++ examples/builtin/01_basics/fade.rb | 22 +++++++++++++++++++ .../builtin/01_basics/read_analog_voltage.rb | 13 +++++++++++ 6 files changed, 87 insertions(+) create mode 100644 examples/builtin/01_basics/analog_read_serial.rb create mode 100644 examples/builtin/01_basics/bare_minimum.rb create mode 100644 examples/builtin/01_basics/blink.rb create mode 100644 examples/builtin/01_basics/digital_read_serial.rb create mode 100644 examples/builtin/01_basics/fade.rb create mode 100644 examples/builtin/01_basics/read_analog_voltage.rb diff --git a/examples/builtin/01_basics/analog_read_serial.rb b/examples/builtin/01_basics/analog_read_serial.rb new file mode 100644 index 0000000..3ad9f51 --- /dev/null +++ b/examples/builtin/01_basics/analog_read_serial.rb @@ -0,0 +1,13 @@ +# AnalogReadSerial +# +# Reads an analog input on pin A0, prints the result to the Serial Monitor. +# +# https://docs.arduino.cc/built-in-examples/basics/AnalogReadSerial/ + +serial_begin(9600) + +loop do + sensor_value = analog_read(ArduinoUNO::A0) + serial_println(sensor_value) + delay_ms(1) +end diff --git a/examples/builtin/01_basics/bare_minimum.rb b/examples/builtin/01_basics/bare_minimum.rb new file mode 100644 index 0000000..f8e4750 --- /dev/null +++ b/examples/builtin/01_basics/bare_minimum.rb @@ -0,0 +1,9 @@ +# BareMinimum +# +# https://docs.arduino.cc/built-in-examples/basics/BareMinimum/ + +# put your setup code here, to run once + +loop do + # put your main code here, to run repeatedly +end diff --git a/examples/builtin/01_basics/blink.rb b/examples/builtin/01_basics/blink.rb new file mode 100644 index 0000000..1258ab1 --- /dev/null +++ b/examples/builtin/01_basics/blink.rb @@ -0,0 +1,14 @@ +# Blink +# +# Turns an LED on for one second, then off for one second, repeatedly. +# +# https://docs.arduino.cc/built-in-examples/basics/Blink/ + +pin_mode(ArduinoUNO::LED_BUILTIN, ArduinoUNO::OUTPUT) + +loop do + digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::HIGH) + delay_ms(1000) + digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::LOW) + delay_ms(1000) +end diff --git a/examples/builtin/01_basics/digital_read_serial.rb b/examples/builtin/01_basics/digital_read_serial.rb new file mode 100644 index 0000000..eed885c --- /dev/null +++ b/examples/builtin/01_basics/digital_read_serial.rb @@ -0,0 +1,16 @@ +# DigitalReadSerial +# +# Reads a digital input on pin 2, prints the result to the Serial Monitor. +# +# https://docs.arduino.cc/built-in-examples/basics/DigitalReadSerial/ + +push_button = 2 + +serial_begin(9600) +pin_mode(push_button, ArduinoUNO::INPUT) + +loop do + button_state = digital_read(push_button) + serial_println(button_state) + delay_ms(1) +end diff --git a/examples/builtin/01_basics/fade.rb b/examples/builtin/01_basics/fade.rb new file mode 100644 index 0000000..4c87a7e --- /dev/null +++ b/examples/builtin/01_basics/fade.rb @@ -0,0 +1,22 @@ +# Fade +# +# Fades an LED on pin 9 using analog_write (PWM). +# +# https://docs.arduino.cc/built-in-examples/basics/Fade/ + +led = 9 +brightness = 0 +fade_amount = 5 + +pin_mode(led, ArduinoUNO::OUTPUT) + +loop do + analog_write(led, brightness) + + brightness = brightness + fade_amount + if brightness <= 0 || brightness >= 255 + fade_amount = -fade_amount + end + + delay_ms(30) +end diff --git a/examples/builtin/01_basics/read_analog_voltage.rb b/examples/builtin/01_basics/read_analog_voltage.rb new file mode 100644 index 0000000..f3468f6 --- /dev/null +++ b/examples/builtin/01_basics/read_analog_voltage.rb @@ -0,0 +1,13 @@ +# ReadAnalogVoltage +# +# Reads an analog input on pin A0, converts it to voltage, and prints to Serial. +# +# https://docs.arduino.cc/built-in-examples/basics/ReadAnalogVoltage/ + +serial_begin(9600) + +loop do + sensor_value = analog_read(ArduinoUNO::A0) + voltage = sensor_value * (5.0 / 1023.0) + serial_println(voltage, 2) +end From 5fca4be09a4cbca95709112dc59e6d56893b75e0 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:34:37 +1000 Subject: [PATCH 20/33] Add 02.Digital example translations Nine UNO-compatible Ruby ports: BlinkWithoutDelay, Button, Debounce, DigitalInputPullup, StateChangeDetection, toneKeyboard, toneMelody, toneMultiple, tonePitchFollower. toneMelody inlines a few note constants from pitches.h directly. All compile end-to-end. --- .../builtin/02_digital/blink_without_delay.rb | 22 ++++++++++ examples/builtin/02_digital/button.rb | 21 ++++++++++ examples/builtin/02_digital/debounce.rb | 38 ++++++++++++++++++ .../02_digital/digital_input_pullup.rb | 20 ++++++++++ .../02_digital/state_change_detection.rb | 40 +++++++++++++++++++ examples/builtin/02_digital/tone_keyboard.rb | 19 +++++++++ examples/builtin/02_digital/tone_melody.rb | 26 ++++++++++++ examples/builtin/02_digital/tone_multiple.rb | 19 +++++++++ .../builtin/02_digital/tone_pitch_follower.rb | 16 ++++++++ 9 files changed, 221 insertions(+) create mode 100644 examples/builtin/02_digital/blink_without_delay.rb create mode 100644 examples/builtin/02_digital/button.rb create mode 100644 examples/builtin/02_digital/debounce.rb create mode 100644 examples/builtin/02_digital/digital_input_pullup.rb create mode 100644 examples/builtin/02_digital/state_change_detection.rb create mode 100644 examples/builtin/02_digital/tone_keyboard.rb create mode 100644 examples/builtin/02_digital/tone_melody.rb create mode 100644 examples/builtin/02_digital/tone_multiple.rb create mode 100644 examples/builtin/02_digital/tone_pitch_follower.rb diff --git a/examples/builtin/02_digital/blink_without_delay.rb b/examples/builtin/02_digital/blink_without_delay.rb new file mode 100644 index 0000000..c1e7fb9 --- /dev/null +++ b/examples/builtin/02_digital/blink_without_delay.rb @@ -0,0 +1,22 @@ +# BlinkWithoutDelay +# +# Blinks the on-board LED using millis() so other code can run alongside it. +# +# https://docs.arduino.cc/built-in-examples/digital/BlinkWithoutDelay/ + +led_pin = ArduinoUNO::LED_BUILTIN +led_state = ArduinoUNO::LOW +previous_millis = 0 +interval = 1000 + +pin_mode(led_pin, ArduinoUNO::OUTPUT) + +loop do + current_millis = millis + + if current_millis - previous_millis >= interval + previous_millis = current_millis + led_state = led_state == ArduinoUNO::LOW ? ArduinoUNO::HIGH : ArduinoUNO::LOW + digital_write(led_pin, led_state) + end +end diff --git a/examples/builtin/02_digital/button.rb b/examples/builtin/02_digital/button.rb new file mode 100644 index 0000000..03c322a --- /dev/null +++ b/examples/builtin/02_digital/button.rb @@ -0,0 +1,21 @@ +# Button +# +# Lights an LED when a pushbutton is pressed. +# +# https://docs.arduino.cc/built-in-examples/digital/Button/ + +button_pin = 2 +led_pin = 13 + +pin_mode(led_pin, ArduinoUNO::OUTPUT) +pin_mode(button_pin, ArduinoUNO::INPUT) + +loop do + button_state = digital_read(button_pin) + + if button_state == ArduinoUNO::HIGH + digital_write(led_pin, ArduinoUNO::HIGH) + else + digital_write(led_pin, ArduinoUNO::LOW) + end +end diff --git a/examples/builtin/02_digital/debounce.rb b/examples/builtin/02_digital/debounce.rb new file mode 100644 index 0000000..431cd5e --- /dev/null +++ b/examples/builtin/02_digital/debounce.rb @@ -0,0 +1,38 @@ +# Debounce +# +# Toggles an LED on each rising edge of a pushbutton, ignoring contact bounce. +# +# https://docs.arduino.cc/built-in-examples/digital/Debounce/ + +button_pin = 2 +led_pin = 13 + +led_state = ArduinoUNO::HIGH +button_state = ArduinoUNO::LOW +last_button_state = ArduinoUNO::LOW +last_debounce_time = 0 +debounce_delay = 50 + +pin_mode(button_pin, ArduinoUNO::INPUT) +pin_mode(led_pin, ArduinoUNO::OUTPUT) +digital_write(led_pin, led_state) + +loop do + reading = digital_read(button_pin) + + if reading != last_button_state + last_debounce_time = millis + end + + if (millis - last_debounce_time) > debounce_delay + if reading != button_state + button_state = reading + if button_state == ArduinoUNO::HIGH + led_state = led_state == ArduinoUNO::HIGH ? ArduinoUNO::LOW : ArduinoUNO::HIGH + end + end + end + + digital_write(led_pin, led_state) + last_button_state = reading +end diff --git a/examples/builtin/02_digital/digital_input_pullup.rb b/examples/builtin/02_digital/digital_input_pullup.rb new file mode 100644 index 0000000..3048fb4 --- /dev/null +++ b/examples/builtin/02_digital/digital_input_pullup.rb @@ -0,0 +1,20 @@ +# DigitalInputPullup +# +# Uses INPUT_PULLUP on pin 2; lights LED on pin 13 when the button is pressed. +# +# https://docs.arduino.cc/built-in-examples/digital/InputPullupSerial/ + +serial_begin(9600) +pin_mode(2, ArduinoUNO::INPUT_PULLUP) +pin_mode(13, ArduinoUNO::OUTPUT) + +loop do + sensor_val = digital_read(2) + serial_println(sensor_val) + + if sensor_val == ArduinoUNO::HIGH + digital_write(13, ArduinoUNO::LOW) + else + digital_write(13, ArduinoUNO::HIGH) + end +end diff --git a/examples/builtin/02_digital/state_change_detection.rb b/examples/builtin/02_digital/state_change_detection.rb new file mode 100644 index 0000000..b4c2184 --- /dev/null +++ b/examples/builtin/02_digital/state_change_detection.rb @@ -0,0 +1,40 @@ +# StateChangeDetection +# +# Counts button press edges; lights an LED every fourth press. +# +# https://docs.arduino.cc/built-in-examples/digital/StateChangeDetection/ + +button_pin = 2 +led_pin = 13 + +button_push_counter = 0 +button_state = 0 +last_button_state = 0 + +pin_mode(button_pin, ArduinoUNO::INPUT) +pin_mode(led_pin, ArduinoUNO::OUTPUT) +serial_begin(9600) + +loop do + button_state = digital_read(button_pin) + + if button_state != last_button_state + if button_state == ArduinoUNO::HIGH + button_push_counter += 1 + serial_println("on") + serial_print("number of button pushes: ") + serial_println(button_push_counter) + else + serial_println("off") + end + delay_ms(50) + end + + last_button_state = button_state + + if button_push_counter % 4 == 0 + digital_write(led_pin, ArduinoUNO::HIGH) + else + digital_write(led_pin, ArduinoUNO::LOW) + end +end diff --git a/examples/builtin/02_digital/tone_keyboard.rb b/examples/builtin/02_digital/tone_keyboard.rb new file mode 100644 index 0000000..35ed44a --- /dev/null +++ b/examples/builtin/02_digital/tone_keyboard.rb @@ -0,0 +1,19 @@ +# toneKeyboard +# +# Plays a different note for each of three force-sensing resistors on A0–A2. +# +# https://docs.arduino.cc/built-in-examples/digital/toneKeyboard/ + +threshold = 10 +notes = [440, 494, 131] # NOTE_A4, NOTE_B4, NOTE_C3 + +loop do + i = 0 + while i < 3 + sensor_reading = analog_read(i) + if sensor_reading > threshold + tone(8, notes[i], 20) + end + i += 1 + end +end diff --git a/examples/builtin/02_digital/tone_melody.rb b/examples/builtin/02_digital/tone_melody.rb new file mode 100644 index 0000000..7b77c0d --- /dev/null +++ b/examples/builtin/02_digital/tone_melody.rb @@ -0,0 +1,26 @@ +# toneMelody +# +# Plays "Shave and a Haircut" on a piezo speaker on pin 8. +# +# https://docs.arduino.cc/built-in-examples/digital/toneMelody/ + +# Notes inlined from pitches.h: NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, rest, NOTE_B3, NOTE_C4 +melody = [262, 196, 196, 220, 196, 0, 247, 262] +durations = [4, 8, 8, 4, 4, 4, 4, 4] + +i = 0 +while i < 8 + note_duration = 1000 / durations[i] + note = melody[i] + if note > 0 + tone(8, note, note_duration) + end + pause = note_duration * 130 / 100 + delay_ms(pause) + no_tone(8) + i += 1 +end + +loop do + # melody plays once at startup; nothing to do here +end diff --git a/examples/builtin/02_digital/tone_multiple.rb b/examples/builtin/02_digital/tone_multiple.rb new file mode 100644 index 0000000..a59f20e --- /dev/null +++ b/examples/builtin/02_digital/tone_multiple.rb @@ -0,0 +1,19 @@ +# toneMultiple +# +# Plays notes in sequence on pins 6, 7, 8. +# +# https://docs.arduino.cc/built-in-examples/digital/toneMultiple/ + +loop do + no_tone(8) + tone(6, 440, 200) + delay_ms(200) + + no_tone(6) + tone(7, 494, 500) + delay_ms(500) + + no_tone(7) + tone(8, 523, 300) + delay_ms(300) +end diff --git a/examples/builtin/02_digital/tone_pitch_follower.rb b/examples/builtin/02_digital/tone_pitch_follower.rb new file mode 100644 index 0000000..a6e4024 --- /dev/null +++ b/examples/builtin/02_digital/tone_pitch_follower.rb @@ -0,0 +1,16 @@ +# tonePitchFollower +# +# Plays a pitch on pin 9 that tracks a photoresistor on A0. +# +# https://docs.arduino.cc/built-in-examples/digital/tonePitchFollower/ + +serial_begin(9600) + +loop do + sensor_reading = analog_read(ArduinoUNO::A0) + serial_println(sensor_reading) + + this_pitch = map_value(sensor_reading, 400, 1000, 120, 1500) + tone(9, this_pitch, 10) + delay_ms(1) +end From 6fc341deb7a755e7f656b41a923321721247114d Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:35:49 +1000 Subject: [PATCH 21/33] Add 03.Analog example translations Five UNO-compatible Ruby ports: AnalogInOutSerial, AnalogInput, Calibration, Fading, Smoothing. Smoothing exercises mutable arrays; Calibration uses constrain + map_value. Skipped AnalogWriteMega since it's Mega-only. --- .../builtin/03_analog/analog_in_out_serial.rb | 23 ++++++++++++++ examples/builtin/03_analog/analog_input.rb | 18 +++++++++++ examples/builtin/03_analog/calibration.rb | 30 +++++++++++++++++++ examples/builtin/03_analog/fading.rb | 23 ++++++++++++++ examples/builtin/03_analog/smoothing.rb | 26 ++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 examples/builtin/03_analog/analog_in_out_serial.rb create mode 100644 examples/builtin/03_analog/analog_input.rb create mode 100644 examples/builtin/03_analog/calibration.rb create mode 100644 examples/builtin/03_analog/fading.rb create mode 100644 examples/builtin/03_analog/smoothing.rb diff --git a/examples/builtin/03_analog/analog_in_out_serial.rb b/examples/builtin/03_analog/analog_in_out_serial.rb new file mode 100644 index 0000000..671d975 --- /dev/null +++ b/examples/builtin/03_analog/analog_in_out_serial.rb @@ -0,0 +1,23 @@ +# AnalogInOutSerial +# +# Reads a pot on A0, scales to 0–255 PWM on pin 9, prints both values. +# +# https://docs.arduino.cc/built-in-examples/analog/AnalogInOutSerial/ + +analog_in_pin = ArduinoUNO::A0 +analog_out_pin = 9 + +serial_begin(9600) + +loop do + sensor_value = analog_read(analog_in_pin) + output_value = map_value(sensor_value, 0, 1023, 0, 255) + analog_write(analog_out_pin, output_value) + + serial_print("sensor = ") + serial_print(sensor_value) + serial_print("\t output = ") + serial_println(output_value) + + delay_ms(2) +end diff --git a/examples/builtin/03_analog/analog_input.rb b/examples/builtin/03_analog/analog_input.rb new file mode 100644 index 0000000..5836869 --- /dev/null +++ b/examples/builtin/03_analog/analog_input.rb @@ -0,0 +1,18 @@ +# AnalogInput +# +# Blinks LED on pin 13 with the on/off duration set by a pot on A0. +# +# https://docs.arduino.cc/built-in-examples/analog/AnalogInput/ + +sensor_pin = ArduinoUNO::A0 +led_pin = 13 + +pin_mode(led_pin, ArduinoUNO::OUTPUT) + +loop do + sensor_value = analog_read(sensor_pin) + digital_write(led_pin, ArduinoUNO::HIGH) + delay_ms(sensor_value) + digital_write(led_pin, ArduinoUNO::LOW) + delay_ms(sensor_value) +end diff --git a/examples/builtin/03_analog/calibration.rb b/examples/builtin/03_analog/calibration.rb new file mode 100644 index 0000000..9a262c2 --- /dev/null +++ b/examples/builtin/03_analog/calibration.rb @@ -0,0 +1,30 @@ +# Calibration +# +# Captures min/max sensor readings during the first 5 seconds, then maps the +# live reading to 0–255 PWM on pin 9. +# +# https://docs.arduino.cc/built-in-examples/analog/Calibration/ + +sensor_pin = ArduinoUNO::A0 +led_pin = 9 + +sensor_min = 1023 +sensor_max = 0 + +pin_mode(13, ArduinoUNO::OUTPUT) +digital_write(13, ArduinoUNO::HIGH) + +while millis < 5000 + sensor_value = analog_read(sensor_pin) + sensor_max = sensor_value if sensor_value > sensor_max + sensor_min = sensor_value if sensor_value < sensor_min +end + +digital_write(13, ArduinoUNO::LOW) + +loop do + sensor_value = analog_read(sensor_pin) + sensor_value = constrain(sensor_value, sensor_min, sensor_max) + sensor_value = map_value(sensor_value, sensor_min, sensor_max, 0, 255) + analog_write(led_pin, sensor_value) +end diff --git a/examples/builtin/03_analog/fading.rb b/examples/builtin/03_analog/fading.rb new file mode 100644 index 0000000..33b8344 --- /dev/null +++ b/examples/builtin/03_analog/fading.rb @@ -0,0 +1,23 @@ +# Fading +# +# Fades an LED up and down on pin 9 using analog_write. +# +# https://docs.arduino.cc/built-in-examples/analog/Fading/ + +led_pin = 9 + +loop do + fade_value = 0 + while fade_value <= 255 + analog_write(led_pin, fade_value) + delay_ms(30) + fade_value += 5 + end + + fade_value = 255 + while fade_value >= 0 + analog_write(led_pin, fade_value) + delay_ms(30) + fade_value -= 5 + end +end diff --git a/examples/builtin/03_analog/smoothing.rb b/examples/builtin/03_analog/smoothing.rb new file mode 100644 index 0000000..7170e0c --- /dev/null +++ b/examples/builtin/03_analog/smoothing.rb @@ -0,0 +1,26 @@ +# Smoothing +# +# Keeps a rolling average of the last 10 readings from A0 and prints it. +# +# https://docs.arduino.cc/built-in-examples/analog/Smoothing/ + +num_readings = 10 +readings = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +read_index = 0 +total = 0 +input_pin = ArduinoUNO::A0 + +serial_begin(9600) + +loop do + total = total - readings[read_index] + readings[read_index] = analog_read(input_pin) + total = total + readings[read_index] + + read_index = read_index + 1 + read_index = 0 if read_index >= num_readings + + average = total / num_readings + serial_println(average) + delay_ms(1) +end From d8fcff1e2089090bb65dd333895898c26f3d11fa Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:37:52 +1000 Subject: [PATCH 22/33] Add 04.Communication example translations Nine UNO-compatible Ruby ports: ASCIITable, Dimmer, Graph, Midi, PhysicalPixel, ReadASCIIString, SerialCallResponse, SerialCallResponseASCII, VirtualColorMixer. Skipped SerialEvent (needs serialEvent callback), MultiSerial and SerialPassthrough (Mega-only). ASCIITable exercises codegen-routed Serial.print(value, base) with all of HEX/OCT/BIN. ReadASCIIString uses serial_parse_int and the ?\n.ord char-literal idiom. ?A.ord works through Spinel for byte constants without needing additional codegen surgery. --- .../builtin/04_communication/ascii_table.rb | 34 ++ examples/builtin/04_communication/dimmer.rb | 17 + examples/builtin/04_communication/graph.rb | 12 + examples/builtin/04_communication/midi.rb | 24 ++ .../04_communication/physical_pixel.rb | 22 + .../04_communication/read_ascii_string.rb | 37 ++ .../04_communication/serial_call_response.rb | 30 ++ .../serial_call_response_ascii.rb | 31 ++ .../04_communication/virtual_color_mixer.rb | 19 + plans/ruby_api_convention_audit.md | 395 ++++++++++++++++++ 10 files changed, 621 insertions(+) create mode 100644 examples/builtin/04_communication/ascii_table.rb create mode 100644 examples/builtin/04_communication/dimmer.rb create mode 100644 examples/builtin/04_communication/graph.rb create mode 100644 examples/builtin/04_communication/midi.rb create mode 100644 examples/builtin/04_communication/physical_pixel.rb create mode 100644 examples/builtin/04_communication/read_ascii_string.rb create mode 100644 examples/builtin/04_communication/serial_call_response.rb create mode 100644 examples/builtin/04_communication/serial_call_response_ascii.rb create mode 100644 examples/builtin/04_communication/virtual_color_mixer.rb create mode 100644 plans/ruby_api_convention_audit.md diff --git a/examples/builtin/04_communication/ascii_table.rb b/examples/builtin/04_communication/ascii_table.rb new file mode 100644 index 0000000..8779044 --- /dev/null +++ b/examples/builtin/04_communication/ascii_table.rb @@ -0,0 +1,34 @@ +# ASCIITable +# +# Prints byte values 33–126 in raw, decimal, hex, octal, and binary forms. +# +# https://docs.arduino.cc/built-in-examples/communication/ASCIITable/ + +serial_begin(9600) +serial_println("ASCII Table ~ Character Map") + +this_byte = 33 + +loop do + serial_write(this_byte) + + serial_print(", dec: ") + serial_print(this_byte) + + serial_print(", hex: ") + serial_print(this_byte, ArduinoUNO::HEX) + + serial_print(", oct: ") + serial_print(this_byte, ArduinoUNO::OCT) + + serial_print(", bin: ") + serial_println(this_byte, ArduinoUNO::BIN) + + if this_byte == 126 + loop do + # idle once we hit '~' + end + end + + this_byte += 1 +end diff --git a/examples/builtin/04_communication/dimmer.rb b/examples/builtin/04_communication/dimmer.rb new file mode 100644 index 0000000..d2b7875 --- /dev/null +++ b/examples/builtin/04_communication/dimmer.rb @@ -0,0 +1,17 @@ +# Dimmer +# +# Brightens an LED on pin 9 from a single byte value sent over the serial port. +# +# https://docs.arduino.cc/built-in-examples/communication/Dimmer/ + +led_pin = 9 + +serial_begin(9600) +pin_mode(led_pin, ArduinoUNO::OUTPUT) + +loop do + if serial_available > 0 + brightness = serial_read + analog_write(led_pin, brightness) + end +end diff --git a/examples/builtin/04_communication/graph.rb b/examples/builtin/04_communication/graph.rb new file mode 100644 index 0000000..0b192e8 --- /dev/null +++ b/examples/builtin/04_communication/graph.rb @@ -0,0 +1,12 @@ +# Graph +# +# Streams the value of A0 over serial as ASCII for plotting on the host. +# +# https://docs.arduino.cc/built-in-examples/communication/Graph/ + +serial_begin(9600) + +loop do + serial_println(analog_read(ArduinoUNO::A0)) + delay_ms(2) +end diff --git a/examples/builtin/04_communication/midi.rb b/examples/builtin/04_communication/midi.rb new file mode 100644 index 0000000..2061560 --- /dev/null +++ b/examples/builtin/04_communication/midi.rb @@ -0,0 +1,24 @@ +# MIDI note player +# +# Sends MIDI Note On/Off messages over serial at 31250 baud. +# +# https://docs.arduino.cc/built-in-examples/communication/Midi/ + +def note_on(cmd, pitch, velocity) + serial_write(cmd) + serial_write(pitch) + serial_write(velocity) +end + +serial_begin(31250) + +loop do + note = 0x1E + while note < 0x5A + note_on(0x90, note, 0x45) + delay_ms(100) + note_on(0x90, note, 0x00) + delay_ms(100) + note += 1 + end +end diff --git a/examples/builtin/04_communication/physical_pixel.rb b/examples/builtin/04_communication/physical_pixel.rb new file mode 100644 index 0000000..445170f --- /dev/null +++ b/examples/builtin/04_communication/physical_pixel.rb @@ -0,0 +1,22 @@ +# PhysicalPixel +# +# Toggles the on-board LED on incoming 'H' / 'L' bytes from the host. +# +# https://docs.arduino.cc/built-in-examples/communication/PhysicalPixel/ + +led_pin = 13 + +serial_begin(9600) +pin_mode(led_pin, ArduinoUNO::OUTPUT) + +loop do + if serial_available > 0 + incoming = serial_read + if incoming == ?H.ord + digital_write(led_pin, ArduinoUNO::HIGH) + end + if incoming == ?L.ord + digital_write(led_pin, ArduinoUNO::LOW) + end + end +end diff --git a/examples/builtin/04_communication/read_ascii_string.rb b/examples/builtin/04_communication/read_ascii_string.rb new file mode 100644 index 0000000..d71a8e7 --- /dev/null +++ b/examples/builtin/04_communication/read_ascii_string.rb @@ -0,0 +1,37 @@ +# ReadASCIIString +# +# Parses comma-separated R,G,B integers ending with '\n' from serial, +# then drives a common-cathode RGB LED on pins 3/5/6 (inverted). +# +# https://docs.arduino.cc/built-in-examples/communication/ReadASCIIString/ + +red_pin = 3 +green_pin = 5 +blue_pin = 6 + +serial_begin(9600) +pin_mode(red_pin, ArduinoUNO::OUTPUT) +pin_mode(green_pin, ArduinoUNO::OUTPUT) +pin_mode(blue_pin, ArduinoUNO::OUTPUT) + +loop do + while serial_available > 0 + red = serial_parse_int + green = serial_parse_int + blue = serial_parse_int + + if serial_read == ?\n.ord + red = 255 - constrain(red, 0, 255) + green = 255 - constrain(green, 0, 255) + blue = 255 - constrain(blue, 0, 255) + + analog_write(red_pin, red) + analog_write(green_pin, green) + analog_write(blue_pin, blue) + + serial_print(red, ArduinoUNO::HEX) + serial_print(green, ArduinoUNO::HEX) + serial_println(blue, ArduinoUNO::HEX) + end + end +end diff --git a/examples/builtin/04_communication/serial_call_response.rb b/examples/builtin/04_communication/serial_call_response.rb new file mode 100644 index 0000000..b265a7d --- /dev/null +++ b/examples/builtin/04_communication/serial_call_response.rb @@ -0,0 +1,30 @@ +# SerialCallResponse +# +# Sends 'A' until the host responds, then ships three sensor bytes per request. +# +# https://docs.arduino.cc/built-in-examples/communication/SerialCallResponse/ + +def establish_contact + while serial_available <= 0 + serial_write(?A.ord) + delay_ms(300) + end +end + +serial_begin(9600) +pin_mode(2, ArduinoUNO::INPUT) +establish_contact + +loop do + if serial_available > 0 + in_byte = serial_read + first_sensor = analog_read(ArduinoUNO::A0) / 4 + delay_ms(10) + second_sensor = analog_read(ArduinoUNO::A1) / 4 + third_sensor = map_value(digital_read(2), 0, 1, 0, 255) + + serial_write(first_sensor) + serial_write(second_sensor) + serial_write(third_sensor) + end +end diff --git a/examples/builtin/04_communication/serial_call_response_ascii.rb b/examples/builtin/04_communication/serial_call_response_ascii.rb new file mode 100644 index 0000000..85e1ec7 --- /dev/null +++ b/examples/builtin/04_communication/serial_call_response_ascii.rb @@ -0,0 +1,31 @@ +# SerialCallResponseASCII +# +# Like SerialCallResponse but emits CSV ASCII text instead of raw bytes. +# +# https://docs.arduino.cc/built-in-examples/communication/SerialCallResponseASCII/ + +def establish_contact + while serial_available <= 0 + serial_println("0,0,0") + delay_ms(300) + end +end + +serial_begin(9600) +pin_mode(2, ArduinoUNO::INPUT) +establish_contact + +loop do + if serial_available > 0 + in_byte = serial_read + first_sensor = analog_read(ArduinoUNO::A0) + second_sensor = analog_read(ArduinoUNO::A1) + third_sensor = map_value(digital_read(2), 0, 1, 0, 255) + + serial_print(first_sensor) + serial_print(",") + serial_print(second_sensor) + serial_print(",") + serial_println(third_sensor) + end +end diff --git a/examples/builtin/04_communication/virtual_color_mixer.rb b/examples/builtin/04_communication/virtual_color_mixer.rb new file mode 100644 index 0000000..68ccd16 --- /dev/null +++ b/examples/builtin/04_communication/virtual_color_mixer.rb @@ -0,0 +1,19 @@ +# VirtualColorMixer +# +# Streams three pot values (A0/A1/A2) as comma-separated ASCII over serial. +# +# https://docs.arduino.cc/built-in-examples/communication/VirtualColorMixer/ + +red_pin = ArduinoUNO::A0 +green_pin = ArduinoUNO::A1 +blue_pin = ArduinoUNO::A2 + +serial_begin(9600) + +loop do + serial_print(analog_read(red_pin)) + serial_print(",") + serial_print(analog_read(green_pin)) + serial_print(",") + serial_println(analog_read(blue_pin)) +end diff --git a/plans/ruby_api_convention_audit.md b/plans/ruby_api_convention_audit.md new file mode 100644 index 0000000..e3c61cc --- /dev/null +++ b/plans/ruby_api_convention_audit.md @@ -0,0 +1,395 @@ +# Ruby API Convention Audit + +This audit covers the gem-owned method surface in `lib/`, `bin/rubyduino`, and the sketch-facing API assembled from `lib/rubyduino/arduino_uno.rb` plus the `serial_print`, `serial_println`, and `rand(range)` codegen hooks in `lib/rubyduino/spinel_arduino_codegen.rb`. + +Vendored Spinel internals are out of scope unless Rubyduino exposes them directly. + +## Executive Summary + +Rubyduino currently exposes a very Arduino-shaped API with Ruby snake_case names. That is useful for porting Arduino examples, but it still goes against normal Ruby gem conventions in a few consistent ways: + +- `lib/rubyduino/arduino_uno.rb` defines many methods at top level, which turns them into global/private `Object` methods in Ruby. This is convenient for sketches, but a gem should normally put public APIs behind a namespace or an explicitly included DSL module. +- The public methods are mostly procedural wrappers around hardware subsystems: `serial_*`, `spi_*`, `wire_*`, `servo_*`, and `eeprom_*`. Ruby code would normally use objects/modules with short method names, such as `serial.puts`, `spi.transfer`, `wire.transmit`, `servo.angle = 90`, and `eeprom[0]`. +- Methods that return truth values still expose C/Arduino-style integer forms (`is_alpha`, `serial_find`, `servo_attached`, `interrupt_fired`) alongside predicate aliases. The predicate aliases are good, but the Ruby-facing names should drop the `is_` prefix where possible. +- Getter/setter pairs use Java/C naming (`serial_get_timeout`, `serial_set_timeout`, `spi_set_data_mode`, `wire_set_clock`). Ruby-style APIs should prefer `timeout`, `timeout=`, `data_mode=`, `clock=`, or keyword-based configuration. +- Several helpers expose implementation details (`serial_print_str`, `serial_print_int`, `serial_print_hex`, `random_max`, `tone_for`, `pulse_in_timeout`) as public FFI functions. These should remain available for codegen/runtime compatibility, but be treated as raw/internal primitives rather than the Ruby-first API. + +Recommended direction: keep the existing Arduino-compatible functions as a compatibility layer, then add Ruby-style facades and aliases. Avoid breaking current sketches until there is a clear versioned deprecation path. + +## Cross-Cutting Convention Issues + +### Top-Level API Pollution + +Current locations: + +- `lib/rubyduino/arduino_uno.rb:152-618` defines sketch helpers as top-level methods. +- `bin/rubyduino:10-88` defines CLI helper methods at top level. +- `lib/rubyduino/spinel_arduino_codegen.rb:11` defines `load_spinel_compiler` at top level. + +Why this is not Ruby-esque: + +- Gems should not add broad method names to `Object` unless they are intentionally building a DSL and isolate that DSL carefully. +- Names like `bit`, `constrain`, `interrupts`, `serial_read`, and `wire_read` become globally visible when the file is loaded. + +Ruby-style equivalent: + +- Introduce `Rubyduino::Sketch` or `Rubyduino::DSL` and include it only into compiled sketches. +- Move raw board bindings under `Rubyduino::Boards::Uno` or keep `ArduinoUNO` as a compatibility alias for `Rubyduino::Boards::Uno`. +- Move CLI helpers into `Rubyduino::CLI`. +- Move codegen loading helpers under a module or class method, for example `Rubyduino::SpinelArduinoCodegen.load_spinel_compiler`. + +### Constants and Namespace Shape + +Current location: + +- `lib/rubyduino/arduino_uno.rb:1-44` + +Issues: + +- `ArduinoUNO` is less idiomatic than `ArduinoUno` as a Ruby constant name. +- Constants like `LOW`, `HIGH`, `INPUT`, `OUTPUT`, `HEX`, and `BIN` are Arduino-compatible, but Ruby-facing APIs should generally accept symbols or booleans where the domain is clear. + +Ruby-style equivalent: + +- Add `ArduinoUno = ArduinoUNO` or a namespaced `Rubyduino::Boards::Uno`. +- Allow `:low`, `:high`, `:input`, `:output`, `:pullup`, `:hex`, `:bin`, `:oct`, `:dec`, while preserving numeric constants for compatibility and direct register-oriented code. + +## Public Sketch Method Audit + +### Digital, Analog, and Pin Methods + +Current methods: + +- `pin_mode`, `digital_write`, `digital_read`, `analog_read`, `analog_write` + +Convention issue: + +- These are hardware-procedural globals. They are snake_case, which is good, but Ruby would usually model a pin as an object or use narrower verbs with predicates and bang methods for side effects. +- `digital_read` returns integer `0`/`1`; Ruby callers generally expect a predicate when asking a yes/no question. + +Ruby-style equivalents to add: + +- `pin(13).output!` +- `pin(13).input!` +- `pin(13).input_pullup!` +- `pin(13).high!` +- `pin(13).low!` +- `pin(13).high?` +- `pin(13).low?` +- `pin(A0).analog_read` +- `pin(9).analog_write(128)` + +Keep the current methods as Arduino-compatible aliases. + +### Timing and Pulse Methods + +Current methods: + +- `delay_ms`, `delay_us`, `millis`, `micros`, `pulse_in`, `pulse_in_timeout`, `pulse_in_long`, `arduino_yield` + +Convention issue: + +- `delay_ms` and `delay_us` are acceptable for embedded Ruby because they make units explicit, but `sleep_ms` and `sleep_us` would read more naturally to Rubyists. +- `pulse_in_timeout` encodes an optional behavior in the method name. Ruby normally prefers keyword arguments. +- `arduino_yield` is necessarily prefixed because `yield` is a Ruby keyword, so this name is acceptable as a compatibility escape hatch. + +Ruby-style equivalents to add: + +- `sleep_ms(ms)` as an alias for `delay_ms(ms)`. +- `sleep_us(us)` as an alias for `delay_us(us)`. +- `pulse_in(pin, value, timeout_us: 1_000_000)` as the preferred form. +- Keep `millis` and `micros`; optionally add `milliseconds` and `microseconds` aliases if the compiler can support them without ambiguity. + +### Serial Methods + +Current methods and codegen hooks: + +- `serial_begin`, `serial_end`, `serial_available`, `serial_available_for_write` +- `serial_read`, `serial_read_byte_timeout`, `serial_peek`, `serial_write`, `serial_flush` +- `serial_set_timeout`, `serial_get_timeout` +- `serial_parse_int`, `serial_parse_float` +- `serial_find`, `serial_find?`, `serial_find_until`, `serial_find_until?` +- `serial_print`, `serial_println` through codegen only +- `serial_print_hex`, `serial_print_bin`, `serial_print_oct`, `serial_print_float` +- `serial_println_hex`, `serial_println_bin`, `serial_println_oct`, `serial_println_float` +- raw FFI helpers: `serial_print_str`, `serial_print_int`, `serial_println_str`, `serial_println_int` + +Convention issue: + +- The `serial_` prefix is a procedural namespace substitute. +- `serial_println` is Arduino terminology; Rubyists expect `puts`. +- `serial_get_timeout` and `serial_set_timeout` should be `timeout` and `timeout=`. +- `serial_find` returns integer truth; `serial_find?` is the Ruby-style form. +- Format-specific print methods are implementation detail. Ruby callers should use a keyword or format object rather than `serial_print_hex`. + +Ruby-style equivalents to add: + +- `serial.begin(9600)` / `Serial.begin(9600)` if a constant module is easier for codegen. +- `serial.end` +- `serial.print(value, base: :hex)` and `serial.puts(value, base: :hex)`. +- `serial.print(value, decimals: 2)` and `serial.puts(value, decimals: 2)` for floats. +- `serial.write(byte)` and `serial << byte`. +- `serial.read`, `serial.peek`, `serial.flush`. +- `serial.available` for count and `serial.available?` or `serial.any?` for boolean. +- `serial.available_for_write`. +- `serial.timeout` and `serial.timeout = ms`. +- `serial.parse_int`, `serial.parse_float`. +- `serial.find?("OK")` and `serial.find_until?("OK", "\n")`. + +Keep `serial_print`, `serial_println`, and the format-specific helpers as compatibility/codegen aliases, but steer new docs toward `serial.print` and `serial.puts`. + +### Random Methods + +Current methods and codegen hooks: + +- `random_seed`, `random_range`, `random_max` +- `rand(1..10)` is supported through codegen. + +Convention issue: + +- Ruby already has `srand` and `rand`. +- `random_range` and `random_max` are raw Arduino/C helper shapes. + +Ruby-style equivalents to add: + +- Support `srand(seed)` as the preferred alias for `random_seed(seed)`. +- Keep promoting `rand(1..10)`. +- Treat `random_range(low, high)` and `random_max(high)` as internal/runtime helpers once `rand` covers the common cases. + +### Bit, Byte, and Math Helpers + +Current methods: + +- `bit`, `bit_read`, `bit_write`, `bit_set`, `bit_clear` +- `high_byte`, `low_byte` +- `map_value`, `constrain`, `sq` + +Convention issue: + +- `bit_write`, `bit_set`, and `bit_clear` return new values rather than mutating their first argument; the names read as mutators. +- Ruby already has bitwise operators, so `bit(n)` is less Ruby-like than `1 << n`. +- `constrain` duplicates Ruby's `clamp` naming. +- `sq` is an Arduino abbreviation; Ruby code generally favors `square` or direct multiplication. +- `map_value` avoids `map`, but the name is still a C/Arduino utility rather than a Ruby concept. + +Ruby-style equivalents to add: + +- `bit_mask(n)` or document `1 << n` as the Ruby-first form. +- `bit_at(value, n)` or `bit_set?(value, n)` for reading a bit as a boolean. +- `with_bit(value, n)`, `without_bit(value, n)`, and `with_bit(value, n, bitvalue)` for non-mutating transforms. +- `high_byte(value)` and `low_byte(value)` can stay; they are clear embedded helpers. +- `clamp(value, low, high)` as an alias for `constrain`. +- `square(value)` as an alias for `sq`. +- `remap(value, from: low..high, to: low..high)` or `map_range(value, from_low, from_high, to_low, to_high)` as a clearer alias for `map_value`. + +### Character Classification + +Current raw methods: + +- `is_alpha`, `is_digit`, `is_alpha_numeric`, `is_space`, `is_whitespace`, `is_upper_case`, `is_lower_case`, `is_ascii`, `is_control`, `is_printable`, `is_punct`, `is_hexadecimal_digit` + +Current predicate methods: + +- `is_alpha?`, `is_digit?`, `is_alpha_numeric?`, `is_space?`, `is_whitespace?`, `is_upper_case?`, `is_lower_case?`, `is_ascii?`, `is_control?`, `is_printable?`, `is_punct?`, `is_hexadecimal_digit?` + +Convention issue: + +- The `?` aliases are an improvement, but Ruby predicate names usually omit `is_`. +- The raw methods return integer `0`/`1`; that is useful for C compatibility but not a Ruby-facing result. + +Ruby-style equivalents to add: + +- `alpha?` +- `digit?` +- `alphanumeric?` +- `space?` +- `whitespace?` +- `uppercase?` +- `lowercase?` +- `ascii?` +- `control?` +- `printable?` +- `punctuation?` +- `hex_digit?` or `hexadecimal_digit?` + +Keep the `is_*` methods as Arduino-compatible aliases. Prefer the shorter predicates in examples and tests. + +### Tone and Interrupt Methods + +Current methods: + +- `tone`, `no_tone` +- `interrupts`, `no_interrupts` +- `attach_interrupt`, `detach_interrupt`, `interrupt_fired`, `interrupt_fired?`, `digital_pin_to_interrupt` + +Convention issue: + +- `tone` is acceptable and already has an optional duration. +- `no_tone` mirrors Arduino; Ruby might express this as `tone.stop(pin)` or `pin(pin).stop_tone`. +- `interrupts` / `no_interrupts` are command-style globals. Ruby often uses block-scoped resource control for temporary state changes. +- `attach_interrupt` cannot yet take a Ruby block/callback, but Ruby users will expect `on_interrupt(...) { ... }` if they see "attach". +- `interrupt_fired?` is the Ruby-style method; `interrupt_fired` should be raw/internal. + +Ruby-style equivalents to add: + +- `tone(pin, frequency, duration: nil)` while keeping positional duration for compatibility. +- `stop_tone(pin)` or `tone_off(pin)` as clearer aliases for `no_tone(pin)`. +- `without_interrupts { ... }` as the preferred wrapper for short critical sections. +- `with_interrupts { ... }` only if it has a clear use case. +- `on_interrupt(pin, :rising)` as a future block form if the compiler/runtime can support callbacks. +- `interrupt(digital_pin_to_interrupt(2)).fired?` or `interrupt_fired?(n)` as the predicate form. + +### EEPROM Methods + +Current methods: + +- `eeprom_read`, `eeprom_write`, `eeprom_update`, `eeprom_length`, `eeprom_read_int`, `eeprom_write_int` + +Convention issue: + +- These are procedural names for an addressable storage object. +- `length` and bracket access are familiar Ruby collection conventions. + +Ruby-style equivalents to add: + +- `eeprom.length` +- `eeprom[addr]` +- `eeprom[addr] = value` +- `eeprom.update(addr, value)` +- `eeprom.read_i32(addr)` and `eeprom.write_i32(addr, value)` or `eeprom.read_int(addr)` / `eeprom.write_int(addr, value)` if the current integer width remains fixed and documented. + +### SPI Methods + +Current methods: + +- `spi_begin`, `spi_end`, `spi_set_bit_order`, `spi_set_clock_divider`, `spi_set_data_mode`, `spi_transfer`, `spi_transfer16` + +Convention issue: + +- The `spi_` prefix is a procedural namespace substitute. +- The setters should be assignment methods or keyword configuration. +- `spi_transfer16` uses a numeric suffix because C lacks keyword dispatch; Ruby can express this with a keyword. +- `begin`/`end` pairs are good candidates for a block API. + +Ruby-style equivalents to add: + +- `spi.begin` and `spi.end`. +- `spi.open { ... }` or `spi.transaction(...) { ... }`. +- `spi.bit_order = :msbfirst`. +- `spi.clock_divider = 16` or `spi.clock = hz` if the runtime supports frequency-level configuration later. +- `spi.data_mode = 0`. +- `spi.transfer(byte)`. +- `spi.transfer(word, bits: 16)` as the Ruby-facing replacement for `spi_transfer16`. + +### Wire / I2C Methods + +Current methods: + +- `wire_begin`, `wire_end`, `wire_set_clock`, `wire_begin_transmission`, `wire_write`, `wire_end_transmission`, `wire_request_from`, `wire_available`, `wire_read` + +Convention issue: + +- The `wire_` prefix is a procedural namespace substitute. +- `begin_transmission` / `end_transmission` pairs are good candidates for block-scoped APIs. +- `available` and `read` fit a stream-like object better than globals. + +Ruby-style equivalents to add: + +- `wire.begin` and `wire.end`. +- `wire.clock = 100_000`. +- `wire.transmit(addr) { |tx| tx.write(byte) }`. +- `wire.request(addr, count, stop: true)` returning a count or a small reader object. +- `wire.available`, `wire.available?`, `wire.read`. + +### Servo Methods + +Current methods: + +- `servo_attach`, `servo_detach`, `servo_write`, `servo_write_microseconds`, `servo_read`, `servo_read_microseconds`, `servo_attached`, `servo_attached?` + +Convention issue: + +- The `servo_` prefix is a procedural namespace substitute. +- `servo_write` and `servo_read` are Arduino names for angle access; Ruby should expose `angle` and `angle=`. +- `servo_attached?` is the correct Ruby predicate; `servo_attached` should be raw/internal. +- Current runtime supports one servo, so the object API should be honest about single-servo state until multi-servo support exists. + +Ruby-style equivalents to add: + +- `servo.attach(pin)` and `servo.detach`. +- `servo.angle` and `servo.angle = degrees`. +- `servo.microseconds` and `servo.microseconds = us`. +- `servo.attached?`. + +## Raw FFI Method Audit + +Current location: + +- `lib/rubyduino/arduino_uno.rb:46-149` + +Issue: + +- Every `ffi_func` creates callable module methods on `ArduinoUNO`. That includes internal implementation helpers like `serial_print_str`, `serial_print_int`, `serial_println_str`, `serial_println_int`, `tone_for`, `random_max`, and raw integer predicate helpers. + +Recommendation: + +- Keep raw FFI names stable for generated C and low-level escape hatches. +- Treat them as `Rubyduino::Boards::Uno::Raw` or equivalent in docs. +- Prefer Ruby-style wrappers in examples. +- If Spinel supports visibility control for generated bindings, make raw helpers private or explicitly namespaced. + +## Internal Gem Method Audit + +### CLI Helpers + +Current methods in `bin/rubyduino`: + +- `usage`, `run!`, `capture!`, `command?`, `executable`, `runnable_executable?`, `find_port`, `find_avrdude`, `avrdude_conf_for` + +Convention issue: + +- They are top-level methods inside an executable. This is common in small scripts but not ideal for a gem as the CLI grows. +- `run!` and `capture!` are acceptable bang names because they abort on failure. +- `command?` and `runnable_executable?` are good predicate names. + +Ruby-style equivalent: + +- Move into `Rubyduino::CLI` with an instance method such as `Rubyduino::CLI.new(ARGV).run`. +- Keep helpers private inside that class/module. + +### Codegen Helpers + +Current methods in `lib/rubyduino/spinel_arduino_codegen.rb`: + +- top-level `load_spinel_compiler` +- `SpinelArduinoCodegen#compile_no_recv_call_expr` +- private helpers: `compile_arduino_serial_print`, `arduino_serial_print_func`, `arduino_base_print_func`, `compile_arduino_rand`, `integer_literal_node?` + +Convention issue: + +- `load_spinel_compiler` should not be top-level. +- The module instance methods are internal compiler hooks and are appropriately scoped, with `integer_literal_node?` correctly named as a predicate. + +Ruby-style equivalent: + +- Move `load_spinel_compiler` into `SpinelArduinoCodegen` as a module function or into a small loader class. +- No public Ruby-style sketch aliases are needed for the private compiler methods. + +## Suggested Migration Order + +1. Add a `Rubyduino::DSL` or `Rubyduino::Sketch` namespace and make the current top-level methods part of that explicit DSL. +2. Add `ArduinoUno` / `Rubyduino::Boards::Uno` aliases while preserving `ArduinoUNO`. +3. Add Ruby-style predicates for character classification without `is_`. +4. Add `srand(seed)` as the preferred seed API and keep `rand(range)` as the documented random API. +5. Add `serial` facade methods, especially `serial.print`, `serial.puts`, `serial.timeout`, and `serial.timeout=`. +6. Add `eeprom`, `spi`, `wire`, and `servo` facades. +7. Add pin facades once there is a clear minimal shape for `pin(n)`. +8. Update README examples to show the Ruby-style API first, with Arduino-compatible function names documented as porting aliases. + +## Compatibility Policy Recommendation + +Do not remove the Arduino-compatible methods in the near term. They are valuable for users porting examples and for internal codegen. Instead: + +- Mark Ruby-style methods as preferred in README and examples. +- Keep Arduino-compatible aliases indefinitely or deprecate only after a major version boundary. +- Make raw FFI helpers visibly internal through naming, namespace, or documentation. From e28bf41815d5ed5d50e056ee7015193621ceb23e Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:39:34 +1000 Subject: [PATCH 23/33] Add 05.Control example translations Six Ruby ports: Arrays, ForLoopIteration, IfStatementConditional, switchCase, switchCase2, WhileStatementConditional. Switches use Ruby case/when which Spinel handles cleanly. switchCase2 reads single ASCII bytes via ?a.ord through ?e.ord. --- examples/builtin/05_control/arrays.rb | 33 ++++++++++++++++ .../builtin/05_control/for_loop_iteration.rb | 31 +++++++++++++++ .../05_control/if_statement_conditional.rb | 25 ++++++++++++ examples/builtin/05_control/switch_case.rb | 28 ++++++++++++++ examples/builtin/05_control/switch_case_2.rb | 38 +++++++++++++++++++ .../05_control/while_statement_conditional.rb | 34 +++++++++++++++++ 6 files changed, 189 insertions(+) create mode 100644 examples/builtin/05_control/arrays.rb create mode 100644 examples/builtin/05_control/for_loop_iteration.rb create mode 100644 examples/builtin/05_control/if_statement_conditional.rb create mode 100644 examples/builtin/05_control/switch_case.rb create mode 100644 examples/builtin/05_control/switch_case_2.rb create mode 100644 examples/builtin/05_control/while_statement_conditional.rb diff --git a/examples/builtin/05_control/arrays.rb b/examples/builtin/05_control/arrays.rb new file mode 100644 index 0000000..e242794 --- /dev/null +++ b/examples/builtin/05_control/arrays.rb @@ -0,0 +1,33 @@ +# Arrays +# +# Cycles a non-contiguous list of LED pins forward and back. +# +# https://docs.arduino.cc/built-in-examples/control-structures/Arrays/ + +timer = 100 +led_pins = [2, 7, 4, 6, 5, 3] +pin_count = 6 + +i = 0 +while i < pin_count + pin_mode(led_pins[i], ArduinoUNO::OUTPUT) + i += 1 +end + +loop do + i = 0 + while i < pin_count + digital_write(led_pins[i], ArduinoUNO::HIGH) + delay_ms(timer) + digital_write(led_pins[i], ArduinoUNO::LOW) + i += 1 + end + + i = pin_count - 1 + while i >= 0 + digital_write(led_pins[i], ArduinoUNO::HIGH) + delay_ms(timer) + digital_write(led_pins[i], ArduinoUNO::LOW) + i -= 1 + end +end diff --git a/examples/builtin/05_control/for_loop_iteration.rb b/examples/builtin/05_control/for_loop_iteration.rb new file mode 100644 index 0000000..d7f32bc --- /dev/null +++ b/examples/builtin/05_control/for_loop_iteration.rb @@ -0,0 +1,31 @@ +# ForLoopIteration +# +# Walks LEDs on pins 2..7 forward and back. +# +# https://docs.arduino.cc/built-in-examples/control-structures/ForLoopIteration/ + +timer = 100 + +pin = 2 +while pin < 8 + pin_mode(pin, ArduinoUNO::OUTPUT) + pin += 1 +end + +loop do + pin = 2 + while pin < 8 + digital_write(pin, ArduinoUNO::HIGH) + delay_ms(timer) + digital_write(pin, ArduinoUNO::LOW) + pin += 1 + end + + pin = 7 + while pin >= 2 + digital_write(pin, ArduinoUNO::HIGH) + delay_ms(timer) + digital_write(pin, ArduinoUNO::LOW) + pin -= 1 + end +end diff --git a/examples/builtin/05_control/if_statement_conditional.rb b/examples/builtin/05_control/if_statement_conditional.rb new file mode 100644 index 0000000..93cd4d0 --- /dev/null +++ b/examples/builtin/05_control/if_statement_conditional.rb @@ -0,0 +1,25 @@ +# IfStatementConditional +# +# Lights LED 13 only when A0 reading exceeds a threshold; always logs the value. +# +# https://docs.arduino.cc/built-in-examples/control-structures/ifStatementConditional/ + +analog_pin = ArduinoUNO::A0 +led_pin = 13 +threshold = 400 + +pin_mode(led_pin, ArduinoUNO::OUTPUT) +serial_begin(9600) + +loop do + analog_value = analog_read(analog_pin) + + if analog_value > threshold + digital_write(led_pin, ArduinoUNO::HIGH) + else + digital_write(led_pin, ArduinoUNO::LOW) + end + + serial_println(analog_value) + delay_ms(1) +end diff --git a/examples/builtin/05_control/switch_case.rb b/examples/builtin/05_control/switch_case.rb new file mode 100644 index 0000000..635ee0b --- /dev/null +++ b/examples/builtin/05_control/switch_case.rb @@ -0,0 +1,28 @@ +# switchCase +# +# Buckets a photoresistor reading on A0 into 4 ranges with case/when. +# +# https://docs.arduino.cc/built-in-examples/control-structures/SwitchCase/ + +sensor_min = 0 +sensor_max = 600 + +serial_begin(9600) + +loop do + sensor_reading = analog_read(ArduinoUNO::A0) + range = map_value(sensor_reading, sensor_min, sensor_max, 0, 3) + + case range + when 0 + serial_println("dark") + when 1 + serial_println("dim") + when 2 + serial_println("medium") + when 3 + serial_println("bright") + end + + delay_ms(1) +end diff --git a/examples/builtin/05_control/switch_case_2.rb b/examples/builtin/05_control/switch_case_2.rb new file mode 100644 index 0000000..081871d --- /dev/null +++ b/examples/builtin/05_control/switch_case_2.rb @@ -0,0 +1,38 @@ +# switchCase2 +# +# Lights one of five LEDs (pins 2..6) based on the incoming serial char a..e. +# +# https://docs.arduino.cc/built-in-examples/control-structures/SwitchCase2/ + +serial_begin(9600) + +pin = 2 +while pin < 7 + pin_mode(pin, ArduinoUNO::OUTPUT) + pin += 1 +end + +loop do + if serial_available > 0 + in_byte = serial_read + + case in_byte + when ?a.ord + digital_write(2, ArduinoUNO::HIGH) + when ?b.ord + digital_write(3, ArduinoUNO::HIGH) + when ?c.ord + digital_write(4, ArduinoUNO::HIGH) + when ?d.ord + digital_write(5, ArduinoUNO::HIGH) + when ?e.ord + digital_write(6, ArduinoUNO::HIGH) + else + pin = 2 + while pin < 7 + digital_write(pin, ArduinoUNO::LOW) + pin += 1 + end + end + end +end diff --git a/examples/builtin/05_control/while_statement_conditional.rb b/examples/builtin/05_control/while_statement_conditional.rb new file mode 100644 index 0000000..71d95ff --- /dev/null +++ b/examples/builtin/05_control/while_statement_conditional.rb @@ -0,0 +1,34 @@ +# WhileStatementConditional +# +# Calibrates a sensor on A0 while a button on pin 2 is pressed, then maps the +# live reading to PWM on pin 9 using the captured min/max. +# +# https://docs.arduino.cc/built-in-examples/control-structures/WhileStatementConditional/ + +sensor_pin = ArduinoUNO::A0 +led_pin = 9 +indicator_led_pin = 13 +button_pin = 2 + +sensor_min = 1023 +sensor_max = 0 + +pin_mode(indicator_led_pin, ArduinoUNO::OUTPUT) +pin_mode(led_pin, ArduinoUNO::OUTPUT) +pin_mode(button_pin, ArduinoUNO::INPUT) + +loop do + while digital_read(button_pin) == ArduinoUNO::HIGH + digital_write(indicator_led_pin, ArduinoUNO::HIGH) + sensor_value = analog_read(sensor_pin) + sensor_max = sensor_value if sensor_value > sensor_max + sensor_min = sensor_value if sensor_value < sensor_min + end + + digital_write(indicator_led_pin, ArduinoUNO::LOW) + + sensor_value = analog_read(sensor_pin) + sensor_value = map_value(sensor_value, sensor_min, sensor_max, 0, 255) + sensor_value = constrain(sensor_value, 0, 255) + analog_write(led_pin, sensor_value) +end From 195348d72c8738852567425b61762ba8ea096bbf Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:40:46 +1000 Subject: [PATCH 24/33] Add 06.Sensors example translations Four Ruby ports: ADXL3xx, Knock, Memsic2125, Ping. ADXL3xx exercises the analog-pin-as-digital trick (using pins 18/19 = A4/A5 as power/gnd outputs). Ping demonstrates def-with-no-state helper functions (microseconds_to_inches/centimeters). --- examples/builtin/06_sensors/adxl3xx.rb | 27 ++++++++++++++++ examples/builtin/06_sensors/knock.rb | 25 +++++++++++++++ examples/builtin/06_sensors/memsic2125.rb | 27 ++++++++++++++++ examples/builtin/06_sensors/ping.rb | 39 +++++++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 examples/builtin/06_sensors/adxl3xx.rb create mode 100644 examples/builtin/06_sensors/knock.rb create mode 100644 examples/builtin/06_sensors/memsic2125.rb create mode 100644 examples/builtin/06_sensors/ping.rb diff --git a/examples/builtin/06_sensors/adxl3xx.rb b/examples/builtin/06_sensors/adxl3xx.rb new file mode 100644 index 0000000..918a27c --- /dev/null +++ b/examples/builtin/06_sensors/adxl3xx.rb @@ -0,0 +1,27 @@ +# ADXL3xx +# +# Reads an Analog Devices ADXL3xx accelerometer wired into A1–A5. +# +# https://docs.arduino.cc/built-in-examples/sensors/ADXL3xx/ + +ground_pin = 18 +power_pin = 19 +x_pin = ArduinoUNO::A3 +y_pin = ArduinoUNO::A2 +z_pin = ArduinoUNO::A1 + +serial_begin(9600) + +pin_mode(ground_pin, ArduinoUNO::OUTPUT) +pin_mode(power_pin, ArduinoUNO::OUTPUT) +digital_write(ground_pin, ArduinoUNO::LOW) +digital_write(power_pin, ArduinoUNO::HIGH) + +loop do + serial_print(analog_read(x_pin)) + serial_print("\t") + serial_print(analog_read(y_pin)) + serial_print("\t") + serial_println(analog_read(z_pin)) + delay_ms(100) +end diff --git a/examples/builtin/06_sensors/knock.rb b/examples/builtin/06_sensors/knock.rb new file mode 100644 index 0000000..8bafd96 --- /dev/null +++ b/examples/builtin/06_sensors/knock.rb @@ -0,0 +1,25 @@ +# Knock +# +# Toggles LED 13 and prints "Knock!" when a piezo sensor on A0 spikes. +# +# https://docs.arduino.cc/built-in-examples/sensors/Knock/ + +led_pin = 13 +knock_sensor = ArduinoUNO::A0 +threshold = 100 + +led_state = ArduinoUNO::LOW + +pin_mode(led_pin, ArduinoUNO::OUTPUT) +serial_begin(9600) + +loop do + sensor_reading = analog_read(knock_sensor) + + if sensor_reading >= threshold + led_state = led_state == ArduinoUNO::HIGH ? ArduinoUNO::LOW : ArduinoUNO::HIGH + digital_write(led_pin, led_state) + serial_println("Knock!") + delay_ms(100) + end +end diff --git a/examples/builtin/06_sensors/memsic2125.rb b/examples/builtin/06_sensors/memsic2125.rb new file mode 100644 index 0000000..c16b9a4 --- /dev/null +++ b/examples/builtin/06_sensors/memsic2125.rb @@ -0,0 +1,27 @@ +# Memsic2125 +# +# Decodes pulse widths from a Memsic 2125 two-axis accelerometer on D2/D3 +# into milli-g accelerations. +# +# https://docs.arduino.cc/built-in-examples/sensors/Memsic2125/ + +x_pin = 2 +y_pin = 3 + +serial_begin(9600) +pin_mode(x_pin, ArduinoUNO::INPUT) +pin_mode(y_pin, ArduinoUNO::INPUT) + +loop do + pulse_x = pulse_in(x_pin, ArduinoUNO::HIGH) + pulse_y = pulse_in(y_pin, ArduinoUNO::HIGH) + + acceleration_x = ((pulse_x / 10) - 500) * 8 + acceleration_y = ((pulse_y / 10) - 500) * 8 + + serial_print(acceleration_x) + serial_print("\t") + serial_println(acceleration_y) + + delay_ms(100) +end diff --git a/examples/builtin/06_sensors/ping.rb b/examples/builtin/06_sensors/ping.rb new file mode 100644 index 0000000..0670f12 --- /dev/null +++ b/examples/builtin/06_sensors/ping.rb @@ -0,0 +1,39 @@ +# Ping +# +# Drives a Parallax PING))) ultrasonic rangefinder on D7, prints inches and cm. +# +# https://docs.arduino.cc/built-in-examples/sensors/Ping/ + +ping_pin = 7 + +def microseconds_to_inches(us) + us / 74 / 2 +end + +def microseconds_to_centimeters(us) + us / 29 / 2 +end + +serial_begin(9600) + +loop do + pin_mode(ping_pin, ArduinoUNO::OUTPUT) + digital_write(ping_pin, ArduinoUNO::LOW) + delay_us(2) + digital_write(ping_pin, ArduinoUNO::HIGH) + delay_us(5) + digital_write(ping_pin, ArduinoUNO::LOW) + + pin_mode(ping_pin, ArduinoUNO::INPUT) + duration = pulse_in(ping_pin, ArduinoUNO::HIGH) + + inches = microseconds_to_inches(duration) + cm = microseconds_to_centimeters(duration) + + serial_print(inches) + serial_print("in, ") + serial_print(cm) + serial_println("cm") + + delay_ms(100) +end From 37f77a336f17db51c684b7f1e5979bf2db37bf2b Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:41:44 +1000 Subject: [PATCH 25/33] Add 07.Display example translations Two ports: BarGraph and RowColumnScanning. RowColumnScanning flattens the 8x8 pixel matrix to a 64-element 1D array indexed by row*8+col, since Spinel doesn't have native 2D array literals. --- examples/builtin/07_display/bar_graph.rb | 30 +++++++++++ .../builtin/07_display/row_column_scanning.rb | 50 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 examples/builtin/07_display/bar_graph.rb create mode 100644 examples/builtin/07_display/row_column_scanning.rb diff --git a/examples/builtin/07_display/bar_graph.rb b/examples/builtin/07_display/bar_graph.rb new file mode 100644 index 0000000..81933e7 --- /dev/null +++ b/examples/builtin/07_display/bar_graph.rb @@ -0,0 +1,30 @@ +# BarGraph +# +# Lights a bar of 10 LEDs (pins 2–11) proportional to a pot reading on A0. +# +# https://docs.arduino.cc/built-in-examples/display/BarGraph/ + +analog_pin = ArduinoUNO::A0 +led_count = 10 +led_pins = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + +i = 0 +while i < led_count + pin_mode(led_pins[i], ArduinoUNO::OUTPUT) + i += 1 +end + +loop do + sensor_reading = analog_read(analog_pin) + led_level = map_value(sensor_reading, 0, 1023, 0, led_count) + + i = 0 + while i < led_count + if i < led_level + digital_write(led_pins[i], ArduinoUNO::HIGH) + else + digital_write(led_pins[i], ArduinoUNO::LOW) + end + i += 1 + end +end diff --git a/examples/builtin/07_display/row_column_scanning.rb b/examples/builtin/07_display/row_column_scanning.rb new file mode 100644 index 0000000..39d57e4 --- /dev/null +++ b/examples/builtin/07_display/row_column_scanning.rb @@ -0,0 +1,50 @@ +# RowColumnScanning +# +# Scans an 8x8 LED matrix and draws a cursor whose position is set by two +# pots on A0/A1. pixels[][] is flattened to a 1D array indexed by row*8+col. +# +# https://docs.arduino.cc/built-in-examples/display/RowColumnScanning/ + +row_pins = [2, 7, 19, 5, 13, 18, 12, 16] +col_pins = [6, 11, 10, 3, 17, 4, 8, 9] + +# Flattened 8x8 pixel buffer; index = row * 8 + col +pixels = Array.new(64, ArduinoUNO::HIGH) + +x = 5 +y = 5 + +i = 0 +while i < 8 + pin_mode(col_pins[i], ArduinoUNO::OUTPUT) + pin_mode(row_pins[i], ArduinoUNO::OUTPUT) + digital_write(col_pins[i], ArduinoUNO::HIGH) + i += 1 +end + +loop do + # readSensors + pixels[x * 8 + y] = ArduinoUNO::HIGH + x = 7 - map_value(analog_read(ArduinoUNO::A0), 0, 1023, 0, 7) + y = map_value(analog_read(ArduinoUNO::A1), 0, 1023, 0, 7) + pixels[x * 8 + y] = ArduinoUNO::LOW + + # refreshScreen + this_row = 0 + while this_row < 8 + digital_write(row_pins[this_row], ArduinoUNO::HIGH) + + this_col = 0 + while this_col < 8 + this_pixel = pixels[this_row * 8 + this_col] + digital_write(col_pins[this_col], this_pixel) + if this_pixel == ArduinoUNO::LOW + digital_write(col_pins[this_col], ArduinoUNO::HIGH) + end + this_col += 1 + end + + digital_write(row_pins[this_row], ArduinoUNO::LOW) + this_row += 1 + end +end From f1e23e1c579212b0afed31d450090ddc87f3db92 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:43:02 +1000 Subject: [PATCH 26/33] Add is_graph helper + 08.Strings/character_analysis port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_graph completes the Arduino character-classification set (printable AND not whitespace). The 08.Strings category otherwise depends on the Arduino String class — added a README documenting which sketches are blocked on that, with character_analysis.rb being the one we can port today. --- examples/builtin/08_strings/README.md | 13 ++++++ .../builtin/08_strings/character_analysis.rb | 40 +++++++++++++++++++ lib/rubyduino/arduino_uno.rb | 9 +++++ lib/rubyduino/sp_runtime.h | 5 +++ test/test_char_classification.rb | 2 +- 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 examples/builtin/08_strings/README.md create mode 100644 examples/builtin/08_strings/character_analysis.rb diff --git a/examples/builtin/08_strings/README.md b/examples/builtin/08_strings/README.md new file mode 100644 index 0000000..41d65d9 --- /dev/null +++ b/examples/builtin/08_strings/README.md @@ -0,0 +1,13 @@ +# 08.Strings + +Most sketches in this category depend on the Arduino `String` class +(`String stringOne = "Hello"; stringOne.length();` etc.) which rubyduino +doesn't currently expose. Only `character_analysis.rb` is ported here +because it relies solely on the `is_*?` character classifiers, which +rubyduino does provide. + +If `String` support lands later, the following can be ported: +StringAdditionOperator, StringAppendOperator, StringCaseChanges, +StringCharacters, StringComparisonOperators, StringConstructors, +StringIndexOf, StringLength, StringLengthTrim, StringReplace, +StringStartsWithEndsWith, StringSubstring, StringToInt. diff --git a/examples/builtin/08_strings/character_analysis.rb b/examples/builtin/08_strings/character_analysis.rb new file mode 100644 index 0000000..84d2282 --- /dev/null +++ b/examples/builtin/08_strings/character_analysis.rb @@ -0,0 +1,40 @@ +# CharacterAnalysis +# +# Reports everything the rubyduino character helpers know about each byte +# received over serial. The other 08.Strings sketches are skipped because +# they depend on the Arduino String class, which rubyduino does not expose. +# +# https://docs.arduino.cc/built-in-examples/strings/CharacterAnalysis/ + +serial_begin(9600) +serial_println("send any byte and I'll tell you everything I can about it") +serial_println("") + +loop do + if serial_available > 0 + this_char = serial_read + + serial_print("You sent me: '") + serial_write(this_char) + serial_print("' ASCII Value: ") + serial_println(this_char) + + serial_println("it's alphanumeric") if is_alpha_numeric?(this_char) + serial_println("it's alphabetic") if is_alpha?(this_char) + serial_println("it's ASCII") if is_ascii?(this_char) + serial_println("it's whitespace") if is_whitespace?(this_char) + serial_println("it's a control character") if is_control?(this_char) + serial_println("it's a numeric digit") if is_digit?(this_char) + serial_println("it's a printable non-whitespace") if is_graph?(this_char) + serial_println("it's lower case") if is_lower_case?(this_char) + serial_println("it's printable") if is_printable?(this_char) + serial_println("it's punctuation") if is_punct?(this_char) + serial_println("it's a space character") if is_space?(this_char) + serial_println("it's upper case") if is_upper_case?(this_char) + serial_println("it's a hexadecimal digit") if is_hexadecimal_digit?(this_char) + + serial_println("") + serial_println("Give me another byte:") + serial_println("") + end +end diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index acade04..66fe6b6 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -88,6 +88,7 @@ module ArduinoUNO ffi_func :is_printable, [:int], :int ffi_func :is_punct, [:int], :int ffi_func :is_hexadecimal_digit, [:int], :int + ffi_func :is_graph, [:int], :int ffi_func :random_seed, [:uint32], :void ffi_func :random_range, [:int32, :int32], :int32 ffi_func :random_max, [:int32], :int32 @@ -313,6 +314,10 @@ def is_hexadecimal_digit(c) ArduinoUNO.is_hexadecimal_digit(c) end +def is_graph(c) + ArduinoUNO.is_graph(c) +end + def is_alpha?(c) ArduinoUNO.is_alpha(c) == 1 end @@ -361,6 +366,10 @@ def is_hexadecimal_digit?(c) ArduinoUNO.is_hexadecimal_digit(c) == 1 end +def is_graph?(c) + ArduinoUNO.is_graph(c) == 1 +end + def random_seed(seed) ArduinoUNO.random_seed(seed) end diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index e28fb0d..e9beee1 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -1398,6 +1398,11 @@ int is_hexadecimal_digit(int c) { return 0; } +int is_graph(int c) { + /* Printable but not whitespace — Arduino's isGraph(). */ + return (c > 32 && c <= 126) ? 1 : 0; +} + void random_seed(uint32_t seed) { if (seed == 0) { return; diff --git a/test/test_char_classification.rb b/test/test_char_classification.rb index 011c14a..2423f88 100644 --- a/test/test_char_classification.rb +++ b/test/test_char_classification.rb @@ -7,7 +7,7 @@ class TestCharClassification < Minitest::Test HELPERS = %i[ is_alpha is_digit is_alpha_numeric is_space is_whitespace is_upper_case is_lower_case is_ascii is_control is_printable - is_punct is_hexadecimal_digit + is_punct is_hexadecimal_digit is_graph ].freeze def test_codegen_emits_each_helper From 1f2551488655ebab14261af3bb8fed42056e297e Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:44:20 +1000 Subject: [PATCH 27/33] Add bulk-compile test covering every example sketch One test per .rb in examples/builtin/**, each running the full Ruby -> avr-gcc compile pipeline. Skips cleanly when avr-gcc isn't installed. 43 example sketches compile end-to-end; combined with the unit suite the project now has 106 runs and 374 assertions, all green. --- test/test_builtin_examples.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/test_builtin_examples.rb diff --git a/test/test_builtin_examples.rb b/test/test_builtin_examples.rb new file mode 100644 index 0000000..999d696 --- /dev/null +++ b/test/test_builtin_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestBuiltinExamples < Minitest::Test + EXAMPLES_DIR = File.expand_path("../examples/builtin", __dir__) + + Dir.glob(File.join(EXAMPLES_DIR, "*", "*.rb")).sort.each do |path| + rel = path.sub("#{File.dirname(EXAMPLES_DIR)}/", "") + test_name = "test_#{rel.gsub(%r{[/.]}, "_")}_compiles" + define_method(test_name) do + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = File.read(path) + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj, "empty obj for #{rel}" + end + end + + def test_examples_dir_has_translations + rb_files = Dir.glob(File.join(EXAMPLES_DIR, "*", "*.rb")) + assert_operator rb_files.length, :>=, 35, "expected at least 35 example sketches" + end +end From 0ba1654714fc39b67e4e87dd16524575f9fa52bb Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:47:08 +1000 Subject: [PATCH 28/33] Add Ruby-style aliases for predicates, math, sleep, srand, stop_tone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure additive changes: every existing Arduino-named method stays. - alpha?, digit?, alphanumeric?, space?, whitespace?, uppercase?, lowercase?, ascii?, control?, printable?, punctuation?, hex_digit?, graph? — Ruby idiom drops the is_ prefix - clamp(value, low, high) — Ruby's clamp naming for constrain - square(value) — clearer than sq - sleep_ms / sleep_us — read more naturally to Rubyists than delay_* - srand(seed) — matches Ruby's stdlib name for random_seed - stop_tone(pin) — clearer pair for tone(pin, freq) --- lib/rubyduino/arduino_uno.rb | 78 ++++++++++++++++++++++++++++++++++++ test/test_ruby_aliases.rb | 73 +++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 test/test_ruby_aliases.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 66fe6b6..af05dab 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -178,6 +178,14 @@ def delay_us(us) ArduinoUNO.delay_us(us) end +def sleep_ms(ms) + ArduinoUNO.delay_ms(ms) +end + +def sleep_us(us) + ArduinoUNO.delay_us(us) +end + def millis ArduinoUNO.millis end @@ -266,6 +274,14 @@ def sq(value) ArduinoUNO.sq(value) end +def clamp(value, low, high) + ArduinoUNO.constrain(value, low, high) +end + +def square(value) + ArduinoUNO.sq(value) +end + def is_alpha(c) ArduinoUNO.is_alpha(c) end @@ -370,10 +386,68 @@ def is_graph?(c) ArduinoUNO.is_graph(c) == 1 end +# Ruby-idiomatic predicate aliases (without the is_ prefix). The Arduino-named +# versions above are kept as porting aliases. +def alpha?(c) + ArduinoUNO.is_alpha(c) == 1 +end + +def digit?(c) + ArduinoUNO.is_digit(c) == 1 +end + +def alphanumeric?(c) + ArduinoUNO.is_alpha_numeric(c) == 1 +end + +def space?(c) + ArduinoUNO.is_space(c) == 1 +end + +def whitespace?(c) + ArduinoUNO.is_whitespace(c) == 1 +end + +def uppercase?(c) + ArduinoUNO.is_upper_case(c) == 1 +end + +def lowercase?(c) + ArduinoUNO.is_lower_case(c) == 1 +end + +def ascii?(c) + ArduinoUNO.is_ascii(c) == 1 +end + +def control?(c) + ArduinoUNO.is_control(c) == 1 +end + +def printable?(c) + ArduinoUNO.is_printable(c) == 1 +end + +def punctuation?(c) + ArduinoUNO.is_punct(c) == 1 +end + +def hex_digit?(c) + ArduinoUNO.is_hexadecimal_digit(c) == 1 +end + +def graph?(c) + ArduinoUNO.is_graph(c) == 1 +end + def random_seed(seed) ArduinoUNO.random_seed(seed) end +def srand(seed) + ArduinoUNO.random_seed(seed) +end + def random_range(low, high) ArduinoUNO.random_range(low, high) end @@ -390,6 +464,10 @@ def no_tone(pin) ArduinoUNO.no_tone(pin) end +def stop_tone(pin) + ArduinoUNO.no_tone(pin) +end + def pulse_in_long(pin, value, timeout_us = 1_000_000) ArduinoUNO.pulse_in_timeout(pin, value, timeout_us) end diff --git a/test/test_ruby_aliases.rb b/test/test_ruby_aliases.rb new file mode 100644 index 0000000..b926236 --- /dev/null +++ b/test/test_ruby_aliases.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestRubyAliases < Minitest::Test + PREDICATES = %w[ + alpha? digit? alphanumeric? space? whitespace? uppercase? + lowercase? ascii? control? printable? punctuation? hex_digit? graph? + ].freeze + + def test_predicate_aliases_compile + sketch_lines = ["c = serial_read"] + PREDICATES.each_with_index do |pred, i| + sketch_lines << "digital_write(#{i + 2}, 1) if #{pred}(c)" + end + c = CompileHelper.compile_ruby_to_c(sketch_lines.join("\n")) + PREDICATES.each do |pred| + sym = "sp_#{pred.delete_suffix("?")}_p" + assert_includes c, sym, "expected #{sym} in generated C" + end + end + + def test_clamp_alias_routes_to_constrain + c = CompileHelper.compile_ruby_to_c("v = clamp(150, 0, 100)") + assert_includes c, "constrain(" + assert_includes c, "sp_clamp(" + end + + def test_square_alias_routes_to_sq + c = CompileHelper.compile_ruby_to_c("v = square(7)") + assert_includes c, "sq(" + assert_includes c, "sp_square(" + end + + def test_sleep_aliases_route_to_delay + c = CompileHelper.compile_ruby_to_c("sleep_ms(100); sleep_us(50)") + assert_includes c, "delay_ms(" + assert_includes c, "delay_us(" + assert_includes c, "sp_sleep_ms(" + assert_includes c, "sp_sleep_us(" + end + + def test_srand_aliases_random_seed + c = CompileHelper.compile_ruby_to_c("srand(42)") + assert_includes c, "random_seed(" + assert_includes c, "sp_srand(" + end + + def test_stop_tone_aliases_no_tone + c = CompileHelper.compile_ruby_to_c("stop_tone(8)") + assert_includes c, "no_tone(" + assert_includes c, "sp_stop_tone(" + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + srand(millis) + sleep_ms(100) + sleep_us(50) + stop_tone(8) + v = clamp(150, 0, 100) + x = square(9) + ch = serial_read + digital_write(13, 1) if alpha?(ch) + digital_write(12, 1) if digit?(ch) + digital_write(11, 1) if hex_digit?(ch) + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From a59769f45373c02a73d21f4090096caedacee967 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:53:38 +1000 Subject: [PATCH 29/33] Add Ruby-style module facades: Pin, Serial, Eeprom, Spi, Wire, Servo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each facade is a thin module-method wrapper around the Arduino-named top-level functions, giving sketches a more namespaced feel: Pin.mode(13, ArduinoUno::OUTPUT); Pin.high(13); Pin.high?(2) Serial.begin(9600); Serial.timeout = 500; Serial.println_int(42) Eeprom.write(0, 0xAB); v = Eeprom.read(0) Spi.begin; Spi.data_mode = ArduinoUno::SPI_MODE0; r = Spi.transfer(0x9F) Wire.begin; Wire.clock = 100_000; Wire.transmit(0x68) Servo.attach(9); Servo.angle = 90; Servo.attached? Plus the without_interrupts { ... } block wrapper around no_interrupts + interrupts. Polymorphic Serial.print/println kept at the top level through the existing codegen special case — Spinel monomorphizes module methods, so a single Serial.print used with both String and Integer would create an sp_RbVal type the runtime doesn't carry. The facade exposes Serial.print_str / print_int / print_hex etc. for explicit forms, and the canonical mixed-type print stays as the top-level serial_print(...). --- lib/rubyduino/arduino_uno.rb | 165 ++++++++++++++++++++++++++++++++ test/test_arduino_uno_alias.rb | 48 ++++++++++ test/test_module_facades.rb | 153 +++++++++++++++++++++++++++++ test/test_without_interrupts.rb | 36 +++++++ 4 files changed, 402 insertions(+) create mode 100644 test/test_arduino_uno_alias.rb create mode 100644 test/test_module_facades.rb create mode 100644 test/test_without_interrupts.rb diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index af05dab..55cbd39 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -42,7 +42,58 @@ module ArduinoUNO SPI_CLOCK_DIV2 = 4 SPI_CLOCK_DIV8 = 5 SPI_CLOCK_DIV32 = 6 +end + +# Ruby-style alias module. ArduinoUNO is the original Arduino spelling and +# stays the canonical home for the FFI bindings. ArduinoUno mirrors every +# constant so sketches can use the more Ruby-idiomatic capitalization. +module ArduinoUno + LOW = 0 + HIGH = 1 + + INPUT = 0 + OUTPUT = 1 + INPUT_PULLUP = 2 + A0 = 14 + A1 = 15 + A2 = 16 + A3 = 17 + A4 = 18 + A5 = 19 + + LED_BUILTIN = 13 + LSBFIRST = 0 + MSBFIRST = 1 + + INT_LOW = 0 + INT_CHANGE = 1 + INT_FALLING = 2 + INT_RISING = 3 + + AREF_EXTERNAL = 0 + AREF_DEFAULT = 1 + AREF_INTERNAL = 3 + + BIN = 2 + OCT = 8 + DEC = 10 + HEX = 16 + + SPI_MODE0 = 0 + SPI_MODE1 = 1 + SPI_MODE2 = 2 + SPI_MODE3 = 3 + SPI_CLOCK_DIV4 = 0 + SPI_CLOCK_DIV16 = 1 + SPI_CLOCK_DIV64 = 2 + SPI_CLOCK_DIV128 = 3 + SPI_CLOCK_DIV2 = 4 + SPI_CLOCK_DIV8 = 5 + SPI_CLOCK_DIV32 = 6 +end + +module ArduinoUNO ffi_func :pin_mode, [:uint8, :uint8], :int ffi_func :digital_write, [:uint8, :uint8], :int ffi_func :digital_read, [:uint8], :int @@ -234,6 +285,12 @@ def no_interrupts ArduinoUNO.no_interrupts end +def without_interrupts + ArduinoUNO.no_interrupts + yield + ArduinoUNO.interrupts +end + def bit(n) ArduinoUNO.bit(n) end @@ -703,3 +760,111 @@ def servo_attached? def arduino_yield ArduinoUNO.arduino_yield end + +# --------------------------------------------------------------------------- +# Ruby-style module facades. +# +# Each facade is a thin module-method wrapper around the snake_case top-level +# methods above, giving sketches a more namespaced feel: +# +# Pin.mode(13, ArduinoUno::OUTPUT) +# Serial.begin(9600); Serial.println(42) +# Eeprom.write(0, 0xAB); v = Eeprom.read(0) +# Spi.begin; r = Spi.transfer(0x9F) +# Wire.begin; Wire.transmit(0x68); Wire.write(0x6B); Wire.end_transmission +# Servo.attach(9); Servo.angle = 90 +# +# The Arduino-named top-level functions remain the canonical compatibility +# layer for porting .ino sketches. +# +# Spinel monomorphizes module methods, so polymorphic-looking helpers (e.g. +# a single Serial.print used with both String and Integer) need to live at +# top level via the codegen — Serial.print_str / print_int / print_hex etc. +# split those out so each facade method has a single static type. +# --------------------------------------------------------------------------- + +module Pin + def self.mode(pin, mode); pin_mode(pin, mode); end + def self.write(pin, value); digital_write(pin, value); end + def self.read(pin); digital_read(pin); end + def self.high(pin); digital_write(pin, 1); end + def self.low(pin); digital_write(pin, 0); end + def self.high?(pin); digital_read(pin) == 1; end + def self.low?(pin); digital_read(pin) == 0; end + def self.analog_read(pin); analog_read(pin); end + def self.analog_write(pin, value); analog_write(pin, value); end + def self.pulse_in(pin, value, timeout_us = 1_000_000); pulse_in_timeout(pin, value, timeout_us); end +end + +module Serial + def self.begin(baud); serial_begin(baud); end + def self.end; serial_end; end + def self.available; serial_available; end + def self.available?; serial_available > 0; end + def self.available_for_write; serial_available_for_write; end + def self.read; serial_read; end + def self.read_byte_timeout; serial_read_byte_timeout; end + def self.peek; serial_peek; end + def self.write(byte); serial_write(byte); end + def self.flush; serial_flush; end + def self.timeout; serial_get_timeout; end + def self.timeout=(ms); serial_set_timeout(ms); end + def self.parse_int; serial_parse_int; end + def self.parse_float; serial_parse_float; end + def self.find?(target); serial_find?(target); end + def self.find_until?(target, terminator); serial_find_until?(target, terminator); end + def self.print_str(s); serial_print_str(s); end + def self.print_int(n); serial_print_int(n); end + def self.print_hex(n); serial_print_hex(n); end + def self.print_bin(n); serial_print_bin(n); end + def self.print_oct(n); serial_print_oct(n); end + def self.print_float(f, decimals); serial_print_float(f, decimals); end + def self.println_str(s); serial_println_str(s); end + def self.println_int(n); serial_println_int(n); end + def self.println_hex(n); serial_println_hex(n); end + def self.println_bin(n); serial_println_bin(n); end + def self.println_oct(n); serial_println_oct(n); end + def self.println_float(f, decimals); serial_println_float(f, decimals); end +end + +module Eeprom + def self.length; eeprom_length; end + def self.read(addr); eeprom_read(addr); end + def self.write(addr, value); eeprom_write(addr, value); end + def self.update(addr, value); eeprom_update(addr, value); end + def self.read_int(addr); eeprom_read_int(addr); end + def self.write_int(addr, value); eeprom_write_int(addr, value); end +end + +module Spi + def self.begin; spi_begin; end + def self.end; spi_end; end + def self.bit_order=(order); spi_set_bit_order(order); end + def self.clock_divider=(div); spi_set_clock_divider(div); end + def self.data_mode=(mode); spi_set_data_mode(mode); end + def self.transfer(byte); spi_transfer(byte); end + def self.transfer16(word); spi_transfer16(word); end +end + +module Wire + def self.begin; wire_begin; end + def self.end; wire_end; end + def self.clock=(hz); wire_set_clock(hz); end + def self.transmit(addr); wire_begin_transmission(addr); end + def self.write(byte); wire_write(byte); end + def self.end_transmission(stop = 1); wire_end_transmission(stop); end + def self.request_from(addr, count, stop = 1); wire_request_from(addr, count, stop); end + def self.available; wire_available; end + def self.available?; wire_available > 0; end + def self.read; wire_read; end +end + +module Servo + def self.attach(pin); servo_attach(pin); end + def self.detach; servo_detach; end + def self.angle; servo_read; end + def self.angle=(degrees); servo_write(degrees); end + def self.microseconds; servo_read_microseconds; end + def self.microseconds=(us); servo_write_microseconds(us); end + def self.attached?; servo_attached == 1; end +end diff --git a/test/test_arduino_uno_alias.rb b/test/test_arduino_uno_alias.rb new file mode 100644 index 0000000..97dd2dc --- /dev/null +++ b/test/test_arduino_uno_alias.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestArduinoUnoAlias < Minitest::Test + def test_arduino_uno_constants_compile + sketch = <<~RUBY + pin_mode(ArduinoUno::LED_BUILTIN, ArduinoUno::OUTPUT) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::HIGH) + delay_ms(10) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::LOW) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "cst_ArduinoUno_OUTPUT" + assert_includes c, "cst_ArduinoUno_HIGH" + assert_includes c, "cst_ArduinoUno_LED_BUILTIN" + end + + def test_legacy_arduino_uno_caps_still_works + sketch = "digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::HIGH)" + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "cst_ArduinoUNO_LED_BUILTIN" + end + + def test_constants_match_legacy_values + file = File.read(File.expand_path("../lib/rubyduino/arduino_uno.rb", __dir__)) + # Both modules should declare LED_BUILTIN = 13 + led_decls = file.scan(/LED_BUILTIN\s*=\s*(\d+)/).flatten + assert_equal 2, led_decls.length, "expected LED_BUILTIN in both modules" + assert_equal led_decls.uniq, ["13"] + end + + def test_avr_compile_with_uno_alias + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + pin_mode(ArduinoUno::LED_BUILTIN, ArduinoUno::OUTPUT) + loop do + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::HIGH) + sleep_ms(500) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::LOW) + sleep_ms(500) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_module_facades.rb b/test/test_module_facades.rb new file mode 100644 index 0000000..240d47f --- /dev/null +++ b/test/test_module_facades.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestModuleFacades < Minitest::Test + def test_pin_facade_compiles + sketch = <<~RUBY + Pin.mode(13, ArduinoUno::OUTPUT) + Pin.high(13) + Pin.low(12) + Pin.write(11, 1) + v = Pin.read(2) + digital_write(13, 1) if Pin.high?(2) + digital_write(12, 1) if Pin.low?(3) + v2 = Pin.analog_read(ArduinoUno::A0) + Pin.analog_write(9, 128) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[sp_Pin_cls_mode sp_Pin_cls_high sp_Pin_cls_low sp_Pin_cls_read + sp_Pin_cls_high_p sp_Pin_cls_low_p sp_Pin_cls_analog_read sp_Pin_cls_analog_write].each do |sym| + assert_includes c, sym, "missing #{sym}" + end + end + + def test_serial_facade_compiles + sketch = <<~RUBY + Serial.begin(9600) + Serial.timeout = 500 + t = Serial.timeout + n = Serial.available + digital_write(13, 1) if Serial.available? + Serial.println_str("hello") + Serial.println_int(42) + Serial.println_hex(0xABCD) + x = Serial.read + digital_write(12, 1) if Serial.find?("OK") + Serial.flush + Serial.end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "sp_Serial_cls_begin" + assert_includes c, "sp_Serial_cls_timeout_eq" + assert_includes c, "sp_Serial_cls_available_p" + assert_includes c, "sp_Serial_cls_println_str" + assert_includes c, "sp_Serial_cls_find_p" + end + + def test_eeprom_facade_compiles + sketch = <<~RUBY + Eeprom.write(0, 0xAB) + v = Eeprom.read(0) + Eeprom.update(1, 7) + total = Eeprom.length + Eeprom.write_int(4, -100_000) + n = Eeprom.read_int(4) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[sp_Eeprom_cls_write sp_Eeprom_cls_read sp_Eeprom_cls_update sp_Eeprom_cls_length + sp_Eeprom_cls_write_int sp_Eeprom_cls_read_int].each do |sym| + assert_includes c, sym + end + end + + def test_spi_facade_compiles + sketch = <<~RUBY + Spi.begin + Spi.data_mode = ArduinoUno::SPI_MODE0 + Spi.clock_divider = ArduinoUno::SPI_CLOCK_DIV4 + Spi.bit_order = ArduinoUno::MSBFIRST + r = Spi.transfer(0x9F) + r2 = Spi.transfer16(0x1234) + Spi.end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[sp_Spi_cls_begin sp_Spi_cls_data_mode_eq sp_Spi_cls_clock_divider_eq sp_Spi_cls_bit_order_eq + sp_Spi_cls_transfer sp_Spi_cls_transfer16 sp_Spi_cls_end].each do |sym| + assert_includes c, sym + end + end + + def test_wire_facade_compiles + sketch = <<~RUBY + Wire.begin + Wire.clock = 100_000 + Wire.transmit(0x68) + Wire.write(0x6B) + Wire.write(0) + err = Wire.end_transmission + n = Wire.request_from(0x68, 6) + while Wire.available? + b = Wire.read + end + Wire.end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[sp_Wire_cls_begin sp_Wire_cls_clock_eq sp_Wire_cls_transmit sp_Wire_cls_write + sp_Wire_cls_end_transmission sp_Wire_cls_request_from sp_Wire_cls_available_p sp_Wire_cls_read].each do |sym| + assert_includes c, sym + end + end + + def test_servo_facade_compiles + sketch = <<~RUBY + Servo.attach(9) + Servo.angle = 90 + a = Servo.angle + Servo.microseconds = 1500 + us = Servo.microseconds + digital_write(13, 1) if Servo.attached? + Servo.detach + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[sp_Servo_cls_attach sp_Servo_cls_angle sp_Servo_cls_angle_eq sp_Servo_cls_microseconds + sp_Servo_cls_microseconds_eq sp_Servo_cls_attached_p sp_Servo_cls_detach].each do |sym| + assert_includes c, sym + end + end + + def test_full_facade_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + Serial.begin(9600) + Pin.mode(13, ArduinoUno::OUTPUT) + + Eeprom.write(0, 0xAB) + byte = Eeprom.read(0) + + Spi.begin + Spi.data_mode = ArduinoUno::SPI_MODE0 + reply = Spi.transfer(0x9F) + Spi.end + + Wire.begin + Wire.clock = 100_000 + Wire.transmit(0x68) + Wire.write(0x6B) + Wire.end_transmission + + Servo.attach(9) + Servo.angle = 90 + + loop do + Pin.high(13) + sleep_ms(500) + Pin.low(13) + sleep_ms(500) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_without_interrupts.rb b/test/test_without_interrupts.rb new file mode 100644 index 0000000..a125820 --- /dev/null +++ b/test/test_without_interrupts.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestWithoutInterrupts < Minitest::Test + def test_block_form_compiles + sketch = <<~RUBY + x = 0 + without_interrupts do + x = millis + digital_write(13, 1) + end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + # The block boundary expands to no_interrupts() ... interrupts() in C. + assert_includes c, "no_interrupts(" + assert_includes c, "interrupts(" + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + counter = 0 + loop do + without_interrupts do + counter = counter + 1 + end + digital_write(13, counter & 1) + sleep_ms(100) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From 686c820c03f395087f3139f7cb0c150f6838e625 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 21:57:07 +1000 Subject: [PATCH 30/33] Move CLI into Rubyduino::CLI; encapsulate codegen loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bin/rubyduino is now a thin shim that calls Rubyduino::CLI.run(ARGV). All the option parsing, environment validation, parser bootstrap, and compile/flash logic now lives in lib/rubyduino/cli.rb as a class with private helpers — no more top-level Object pollution. Also moves the spinel codegen loader (`load_spinel_compiler`) and its ROOT/SPINEL_ROOT constants inside the SpinelArduinoCodegen module so they don't leak to top level when the codegen file is required. --- bin/rubyduino | 189 +------------------- lib/rubyduino/cli.rb | 222 ++++++++++++++++++++++++ lib/rubyduino/spinel_arduino_codegen.rb | 33 ++-- test/test_cli.rb | 36 ++++ test/test_codegen_module_shape.rb | 25 +++ 5 files changed, 304 insertions(+), 201 deletions(-) create mode 100644 lib/rubyduino/cli.rb create mode 100644 test/test_cli.rb create mode 100644 test/test_codegen_module_shape.rb diff --git a/bin/rubyduino b/bin/rubyduino index 762995d..cb655dd 100755 --- a/bin/rubyduino +++ b/bin/rubyduino @@ -1,191 +1,6 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require "fileutils" -require "English" -require "open3" -require "rbconfig" -require "tmpdir" +require_relative "../lib/rubyduino/cli" -def usage - warn "Usage: rubyduino [options] input.rb" - warn "" - warn "Options:" - warn " -p PORT Serial port, e.g. /dev/cu.usbmodem31401" - warn " -o NAME Output base path without extension" - warn " --mcu MCU AVR MCU, default: atmega328p" - warn " --baud BAUD Upload baud, default: 115200" - exit 1 -end - -def run!(*cmd, chdir: nil, env: {}) - options = {} - options[:chdir] = chdir if chdir - ok = system(env, *cmd, **options) - return if ok - - status = $CHILD_STATUS&.exitstatus - abort "rubyduino: command failed#{status ? " with exit #{status}" : ""}: #{cmd.join(" ")}" -end - -def capture!(*cmd) - out, status = Open3.capture2e(*cmd) - return out.strip if status.success? - - abort out.empty? ? "rubyduino: command failed: #{cmd.join(" ")}" : out -end - -def command?(name) - !executable(name).nil? -end - -def executable(name) - ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).filter_map do |dir| - path = File.join(dir, name) - path if File.file?(path) && File.executable?(path) - end.first -end - -def runnable_executable?(path) - _output, status = Open3.capture2e(path, "-?") - status.success? -rescue SystemCallError - false -end - -def find_port - patterns = %w[ - /dev/cu.usbmodem* - /dev/cu.usbserial* - /dev/ttyACM* - /dev/ttyUSB* - ] - patterns.lazy.flat_map { |pattern| Dir.glob(pattern) }.first -end - -def find_avrdude - candidates = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).filter_map do |dir| - path = File.join(dir, "avrdude") - path if File.file?(path) && File.executable?(path) - end - - candidates.concat( - Dir.glob( - [ - "/Applications/Arduino.app/Contents/**/bin/avrdude", - "/opt/homebrew/**/bin/avrdude", - "/usr/local/**/bin/avrdude" - ] - ) - ) - - candidates.uniq.find { |path| runnable_executable?(path) } -end - -def avrdude_conf_for(avrdude) - conf = File.expand_path("../etc/avrdude.conf", File.dirname(avrdude)) - File.file?(conf) ? conf : nil -end - -root_dir = File.expand_path("..", __dir__) -unless Dir.exist?(File.join(root_dir, "vendor/spinel")) - require "rubyduino/spinel" - root_dir = File.expand_path("../..", Rubyduino::Spinel::ROOT) -end - -spinel_dir = File.join(root_dir, "vendor/spinel") -rubyduino_dir = File.join(root_dir, "lib/rubyduino") -parse_bin = File.join(spinel_dir, "spinel_parse") -codegen_rb = File.join(rubyduino_dir, "spinel_arduino_codegen.rb") -entry_c = File.join(rubyduino_dir, "arduino_entry.c") -arduino_uno_rb = File.join(rubyduino_dir, "arduino_uno.rb") - -mcu = "atmega328p" -f_cpu = "16000000UL" -baud = "115200" -port = nil -out = nil -source = nil - -argv = ARGV.dup -until argv.empty? - arg = argv.shift - case arg - when "-p" - usage if argv.empty? - port = argv.shift - when "-o" - usage if argv.empty? - out = argv.shift - when "--mcu" - usage if argv.empty? - mcu = argv.shift - when "--baud" - usage if argv.empty? - baud = argv.shift - when "-h", "--help" - usage - when /\.rb\z/ - usage if source - source = arg - else - usage - end -end - -usage unless source -abort "rubyduino: #{source}: No such file" unless File.file?(source) -abort "rubyduino: missing Spinel checkout at #{spinel_dir}" unless Dir.exist?(spinel_dir) -abort "rubyduino: missing #{codegen_rb}" unless File.file?(codegen_rb) -abort "rubyduino: missing #{entry_c}" unless File.file?(entry_c) -abort "rubyduino: missing #{arduino_uno_rb}" unless File.file?(arduino_uno_rb) -abort "rubyduino: avr-gcc not found" unless command?("avr-gcc") -abort "rubyduino: avr-objcopy not found" unless command?("avr-objcopy") - -unless File.executable?(parse_bin) - if command?("make") - warn "Spinel: building parser in #{spinel_dir}" - prism_header = File.join(spinel_dir, "vendor/prism/include/prism/diagnostic.h") - run!("make", "-C", spinel_dir, "deps") unless File.file?(prism_header) - run!("make", "-C", spinel_dir, "PRISM_DIR=vendor/prism", "parse") - end -end -abort "rubyduino: missing #{parse_bin}; run 'make -C #{spinel_dir} parse'" unless File.executable?(parse_bin) - -out ||= source.end_with?(".rb") ? source.delete_suffix(".rb") : source -c_file = "#{out}.c" -elf_file = "#{out}.elf" -hex_file = "#{out}.hex" -app_obj = "#{out}.o" -entry_obj = "#{out}.entry.o" - -port ||= find_port -abort "rubyduino: no Arduino serial port found; pass -p /dev/..." unless port - -avrdude = find_avrdude -abort "rubyduino: avrdude not found" unless avrdude - -ast_tmp = File.join(Dir.tmpdir, "spinel_arduino_ast.#{$PROCESS_ID}.#{rand(1_000_000)}") -source_tmp = File.join(Dir.tmpdir, "rubyduino_source.#{$PROCESS_ID}.#{rand(1_000_000)}.rb") -begin - warn "Spinel: #{source} -> #{c_file}" - File.write(source_tmp, "#{File.read(arduino_uno_rb)}\n#{File.read(source)}") - run!(parse_bin, source_tmp, ast_tmp) - run!(RbConfig.ruby, codegen_rb, ast_tmp, c_file) - - warn "AVR: #{c_file} -> #{hex_file}" - compile_flags = ["-Os", "-DF_CPU=#{f_cpu}", "-mmcu=#{mcu}", "-I#{rubyduino_dir}", "-I#{File.join(spinel_dir, "lib")}"] - run!("avr-gcc", *compile_flags, "-Dmain=sp_arduino_user_main", "-c", c_file, "-o", app_obj) - run!("avr-gcc", *compile_flags, "-c", entry_c, "-o", entry_obj) - run!("avr-gcc", "-Os", "-DF_CPU=#{f_cpu}", "-mmcu=#{mcu}", app_obj, entry_obj, "-o", elf_file) - run!("avr-objcopy", "-O", "ihex", "-R", ".eeprom", elf_file, hex_file) - run!("avr-size", "--mcu=#{mcu}", "--format=avr", elf_file) if command?("avr-size") - - warn "Flash: #{hex_file} -> #{port}" - conf = avrdude_conf_for(avrdude) - conf_args = conf ? ["-C#{conf}"] : [] - run!(avrdude, *conf_args, "-p#{mcu}", "-carduino", "-P#{port}", "-b#{baud}", "-D", "-Uflash:w:#{hex_file}:i") -ensure - FileUtils.rm_f(ast_tmp) - FileUtils.rm_f(source_tmp) -end +Rubyduino::CLI.run(ARGV) diff --git a/lib/rubyduino/cli.rb b/lib/rubyduino/cli.rb new file mode 100644 index 0000000..6372c28 --- /dev/null +++ b/lib/rubyduino/cli.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +require "English" +require "fileutils" +require "open3" +require "rbconfig" +require "tmpdir" + +require_relative "spinel" + +module Rubyduino + # Encapsulates the rubyduino executable so its helpers don't pollute the + # top-level Object namespace. Instantiate with the argv array and call + # {#run} to compile + flash the sketch. + class CLI + DEFAULT_MCU = "atmega328p" + DEFAULT_F_CPU = "16000000UL" + DEFAULT_BAUD = "115200" + + PORT_GLOBS = %w[ + /dev/cu.usbmodem* + /dev/cu.usbserial* + /dev/ttyACM* + /dev/ttyUSB* + ].freeze + + AVRDUDE_GLOBS = [ + "/Applications/Arduino.app/Contents/**/bin/avrdude", + "/opt/homebrew/**/bin/avrdude", + "/usr/local/**/bin/avrdude" + ].freeze + + def self.run(argv) + new(argv).run + end + + def initialize(argv) + @argv = argv.dup + @mcu = DEFAULT_MCU + @f_cpu = DEFAULT_F_CPU + @baud = DEFAULT_BAUD + @port = nil + @out = nil + @source = nil + end + + def run + parse_argv + resolve_paths + validate_environment + ensure_parser_built + compile_and_flash + end + + private + + def parse_argv + until @argv.empty? + arg = @argv.shift + case arg + when "-p" then @port = require_arg! + when "-o" then @out = require_arg! + when "--mcu" then @mcu = require_arg! + when "--baud" then @baud = require_arg! + when "-h", "--help" then usage + when /\.rb\z/ + usage if @source + @source = arg + else + usage + end + end + usage unless @source + end + + def require_arg! + usage if @argv.empty? + @argv.shift + end + + def usage + warn "Usage: rubyduino [options] input.rb" + warn "" + warn "Options:" + warn " -p PORT Serial port, e.g. /dev/cu.usbmodem31401" + warn " -o NAME Output base path without extension" + warn " --mcu MCU AVR MCU, default: #{DEFAULT_MCU}" + warn " --baud BAUD Upload baud, default: #{DEFAULT_BAUD}" + exit 1 + end + + def resolve_paths + @root_dir = File.expand_path("../..", __dir__) + @root_dir = File.expand_path("../..", Rubyduino::Spinel::ROOT) unless Dir.exist?(File.join(@root_dir, "vendor/spinel")) + + @spinel_dir = File.join(@root_dir, "vendor/spinel") + @rubyduino_dir = File.join(@root_dir, "lib/rubyduino") + @parse_bin = File.join(@spinel_dir, "spinel_parse") + @codegen_rb = File.join(@rubyduino_dir, "spinel_arduino_codegen.rb") + @entry_c = File.join(@rubyduino_dir, "arduino_entry.c") + @arduino_uno_rb = File.join(@rubyduino_dir, "arduino_uno.rb") + + @out ||= @source.end_with?(".rb") ? @source.delete_suffix(".rb") : @source + @c_file = "#{@out}.c" + @elf_file = "#{@out}.elf" + @hex_file = "#{@out}.hex" + @app_obj = "#{@out}.o" + @entry_obj = "#{@out}.entry.o" + end + + def validate_environment + abort "rubyduino: #{@source}: No such file" unless File.file?(@source) + abort "rubyduino: missing Spinel checkout at #{@spinel_dir}" unless Dir.exist?(@spinel_dir) + abort "rubyduino: missing #{@codegen_rb}" unless File.file?(@codegen_rb) + abort "rubyduino: missing #{@entry_c}" unless File.file?(@entry_c) + abort "rubyduino: missing #{@arduino_uno_rb}" unless File.file?(@arduino_uno_rb) + abort "rubyduino: avr-gcc not found" unless command?("avr-gcc") + abort "rubyduino: avr-objcopy not found" unless command?("avr-objcopy") + end + + def ensure_parser_built + return if File.executable?(@parse_bin) + + if command?("make") + warn "Spinel: building parser in #{@spinel_dir}" + prism_header = File.join(@spinel_dir, "vendor/prism/include/prism/diagnostic.h") + run!("make", "-C", @spinel_dir, "deps") unless File.file?(prism_header) + run!("make", "-C", @spinel_dir, "PRISM_DIR=vendor/prism", "parse") + end + + abort "rubyduino: missing #{@parse_bin}; run 'make -C #{@spinel_dir} parse'" unless File.executable?(@parse_bin) + end + + def compile_and_flash + @port ||= find_port + abort "rubyduino: no Arduino serial port found; pass -p /dev/..." unless @port + + avrdude = find_avrdude + abort "rubyduino: avrdude not found" unless avrdude + + ast_tmp = File.join(Dir.tmpdir, "spinel_arduino_ast.#{$PROCESS_ID}.#{rand(1_000_000)}") + source_tmp = File.join(Dir.tmpdir, "rubyduino_source.#{$PROCESS_ID}.#{rand(1_000_000)}.rb") + begin + warn "Spinel: #{@source} -> #{@c_file}" + File.write(source_tmp, "#{File.read(@arduino_uno_rb)}\n#{File.read(@source)}") + run!(@parse_bin, source_tmp, ast_tmp) + run!(RbConfig.ruby, @codegen_rb, ast_tmp, @c_file) + + warn "AVR: #{@c_file} -> #{@hex_file}" + compile_flags = ["-Os", "-DF_CPU=#{@f_cpu}", "-mmcu=#{@mcu}", + "-I#{@rubyduino_dir}", "-I#{File.join(@spinel_dir, "lib")}"] + run!("avr-gcc", *compile_flags, "-Dmain=sp_arduino_user_main", "-c", @c_file, "-o", @app_obj) + run!("avr-gcc", *compile_flags, "-c", @entry_c, "-o", @entry_obj) + run!("avr-gcc", "-Os", "-DF_CPU=#{@f_cpu}", "-mmcu=#{@mcu}", @app_obj, @entry_obj, "-o", @elf_file) + run!("avr-objcopy", "-O", "ihex", "-R", ".eeprom", @elf_file, @hex_file) + run!("avr-size", "--mcu=#{@mcu}", "--format=avr", @elf_file) if command?("avr-size") + + warn "Flash: #{@hex_file} -> #{@port}" + conf = avrdude_conf_for(avrdude) + conf_args = conf ? ["-C#{conf}"] : [] + run!(avrdude, *conf_args, "-p#{@mcu}", "-carduino", "-P#{@port}", "-b#{@baud}", "-D", "-Uflash:w:#{@hex_file}:i") + ensure + FileUtils.rm_f(ast_tmp) + FileUtils.rm_f(source_tmp) + end + end + + def run!(*cmd, chdir: nil, env: {}) + options = {} + options[:chdir] = chdir if chdir + ok = system(env, *cmd, **options) + return if ok + + status = $CHILD_STATUS&.exitstatus + abort "rubyduino: command failed#{status ? " with exit #{status}" : ""}: #{cmd.join(" ")}" + end + + def capture!(*cmd) + out, status = Open3.capture2e(*cmd) + return out.strip if status.success? + + abort out.empty? ? "rubyduino: command failed: #{cmd.join(" ")}" : out + end + + def command?(name) + !executable(name).nil? + end + + def executable(name) + ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).filter_map do |dir| + path = File.join(dir, name) + path if File.file?(path) && File.executable?(path) + end.first + end + + def runnable_executable?(path) + _output, status = Open3.capture2e(path, "-?") + status.success? + rescue SystemCallError + false + end + + def find_port + PORT_GLOBS.lazy.flat_map { |pattern| Dir.glob(pattern) }.first + end + + def find_avrdude + candidates = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).filter_map do |dir| + path = File.join(dir, "avrdude") + path if File.file?(path) && File.executable?(path) + end + + candidates.concat(Dir.glob(AVRDUDE_GLOBS)) + candidates.uniq.find { |path| runnable_executable?(path) } + end + + def avrdude_conf_for(avrdude) + conf = File.expand_path("../etc/avrdude.conf", File.dirname(avrdude)) + File.file?(conf) ? conf : nil + end + end +end diff --git a/lib/rubyduino/spinel_arduino_codegen.rb b/lib/rubyduino/spinel_arduino_codegen.rb index 5674b05..b93cd50 100644 --- a/lib/rubyduino/spinel_arduino_codegen.rb +++ b/lib/rubyduino/spinel_arduino_codegen.rb @@ -5,23 +5,28 @@ # file is CLI-oriented, so we load only its Compiler definition and keep the # same AST-file input/output-file flow as the upstream footer. -ROOT = File.expand_path("../..", __dir__) -SPINEL_ROOT = File.join(ROOT, "vendor/spinel") - -def load_spinel_compiler - path = File.join(SPINEL_ROOT, "spinel_codegen.rb") - source = File.read(path) - marker = "\n# ---- Main ----\n" - split_at = source.index(marker) - unless split_at - warn "spinel_arduino_codegen: cannot find codegen main marker" - exit(1) - end +module SpinelArduinoCodegen + ROOT = File.expand_path("../..", __dir__) + SPINEL_ROOT = File.join(ROOT, "vendor/spinel") + + # Load Spinel's Compiler class without executing its CLI footer. The split + # marker isolates the class definitions from the bottom-of-file `Compiler.new` + # entry point so we can replace it with our own argv handling below. + def self.load_spinel_compiler + path = File.join(SPINEL_ROOT, "spinel_codegen.rb") + source = File.read(path) + marker = "\n# ---- Main ----\n" + split_at = source.index(marker) + unless split_at + warn "spinel_arduino_codegen: cannot find codegen main marker" + exit(1) + end - TOPLEVEL_BINDING.eval(source[0...split_at], path) + TOPLEVEL_BINDING.eval(source[0...split_at], path) + end end -load_spinel_compiler +SpinelArduinoCodegen.load_spinel_compiler module SpinelArduinoCodegen def compile_no_recv_call_expr(nid, mname) diff --git a/test/test_cli.rb b/test/test_cli.rb new file mode 100644 index 0000000..2b508c5 --- /dev/null +++ b/test/test_cli.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" +require "rubyduino/cli" + +class TestCLI < Minitest::Test + def test_cli_module_defined + assert defined?(Rubyduino::CLI), "Rubyduino::CLI should load" + assert_respond_to Rubyduino::CLI, :run + end + + def test_bin_shim_is_thin + bin_contents = File.read(File.expand_path("../bin/rubyduino", __dir__)) + refute_match(/def usage/, bin_contents, "CLI helpers should not be top-level in bin/") + refute_match(/def run!/, bin_contents) + assert_match(/Rubyduino::CLI/, bin_contents) + end + + def test_help_exits_nonzero_with_usage_message + require "open3" + bin = File.expand_path("../bin/rubyduino", __dir__) + out, status = Open3.capture2e(RbConfig.ruby, bin, "--help") + refute_predicate status, :success?, "--help should exit nonzero (usage)" + assert_match(/Usage: rubyduino/, out) + assert_match(/--baud BAUD/, out) + end + + def test_no_top_level_object_pollution_from_loading_cli + before = Object.private_instance_methods(false).to_set + require "rubyduino/cli" + after = Object.private_instance_methods(false).to_set + leaked = after - before + leaked.delete(:capture!) # might already exist if tests required something + assert_empty leaked, "loading rubyduino/cli leaked into Object: #{leaked.to_a}" + end +end diff --git a/test/test_codegen_module_shape.rb b/test/test_codegen_module_shape.rb new file mode 100644 index 0000000..704bf77 --- /dev/null +++ b/test/test_codegen_module_shape.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "test_helper" + +class TestCodegenModuleShape < Minitest::Test + CODEGEN_FILE = File.expand_path("../lib/rubyduino/spinel_arduino_codegen.rb", __dir__) + + def test_no_top_level_load_helper + src = File.read(CODEGEN_FILE) + refute_match(/^def load_spinel_compiler/, src, + "load_spinel_compiler should live inside SpinelArduinoCodegen, not top level") + end + + def test_loader_callable_through_module + src = File.read(CODEGEN_FILE) + assert_match(/SpinelArduinoCodegen\.load_spinel_compiler/, src) + assert_match(/def self\.load_spinel_compiler/, src) + end + + def test_module_is_namespaced + src = File.read(CODEGEN_FILE) + assert_match(/^module SpinelArduinoCodegen$/, src) + assert_match(/SPINEL_ROOT = File\.join\(ROOT, "vendor\/spinel"\)/, src) + end +end From 945648d708eed6317fb5aa57e7cab27cb616defd Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Sat, 9 May 2026 22:10:18 +1000 Subject: [PATCH 31/33] Add NeoPixel/DHT/1-Wire/DS18B20/LCD/Stepper/SoftwareSerial/IR drivers + examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Common third-party Arduino hardware that doesn't ship in the built-in examples now has first-class rubyduino bindings: - neopixel_* — WS2812 bit-bang, up to 64 pixels, 16 MHz timing tuned via __builtin_avr_delay_cycles for stable T0H/T1H phases - dht_read / dht_temperature_x10 / dht_humidity_x10 — DHT11 + DHT22 with a single FFI entry point that auto-decodes both formats - onewire_reset/read_byte/write_byte and ds18b20 helpers for Dallas 1-Wire temperature sensors - lcd_* — HD44780 16x2 character LCDs in 4-bit mode, R/W tied low - stepper_begin / stepper_set_speed / stepper_step — equivalent of the bundled Arduino Stepper library, full-step 4-wire mode - soft_serial_* — bit-banged UART for talking to GPS/GSM/ESP-01 while the hardware UART is busy - ir_receive? + ir_command — NEC-protocol IR remote frame decoder Each driver ships with a Ruby example sketch in examples/hardware/ and a test that exercises codegen + full avr-gcc compile. The bulk-compile test now walks examples/hardware/** as well as examples/builtin/**. README rewritten to surface both API styles (Arduino-compatible vs. Ruby-style facades), document every covered helper, and link to all example sketches. --- README.md | 112 ++- examples/hardware/dht22_serial.rb | 26 + examples/hardware/ds18b20_serial.rb | 23 + examples/hardware/ir_remote_decode.rb | 19 + examples/hardware/lcd_hello.rb | 19 + examples/hardware/neopixel_rainbow.rb | 35 + .../hardware/software_serial_passthrough.rb | 19 + examples/hardware/stepper_loop.rb | 14 + lib/rubyduino/arduino_uno.rb | 165 +++++ lib/rubyduino/sp_runtime.h | 681 ++++++++++++++++++ test/test_builtin_examples.rb | 14 +- test/test_dht.rb | 48 ++ test/test_ir_remote.rb | 36 + test/test_lcd.rb | 43 ++ test/test_neopixel.rb | 39 + test/test_onewire_ds18b20.rb | 47 ++ test/test_software_serial.rb | 38 + test/test_stepper.rb | 35 + 18 files changed, 1396 insertions(+), 17 deletions(-) create mode 100644 examples/hardware/dht22_serial.rb create mode 100644 examples/hardware/ds18b20_serial.rb create mode 100644 examples/hardware/ir_remote_decode.rb create mode 100644 examples/hardware/lcd_hello.rb create mode 100644 examples/hardware/neopixel_rainbow.rb create mode 100644 examples/hardware/software_serial_passthrough.rb create mode 100644 examples/hardware/stepper_loop.rb create mode 100644 test/test_dht.rb create mode 100644 test/test_ir_remote.rb create mode 100644 test/test_lcd.rb create mode 100644 test/test_neopixel.rb create mode 100644 test/test_onewire_ds18b20.rb create mode 100644 test/test_software_serial.rb create mode 100644 test/test_stepper.rb diff --git a/README.md b/README.md index 2471186..d320ac9 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,20 @@ Under the hood it uses [Spinel](https://github.com/matz/spinel), a Ruby AOT comp -## Example - -IMG_0197 +## Hello, Blink ```ruby -pin_mode(ArduinoUNO::LED_BUILTIN, ArduinoUNO::OUTPUT) +pin_mode(ArduinoUno::LED_BUILTIN, ArduinoUno::OUTPUT) loop do - digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::HIGH) - delay_ms(100) - digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::LOW) - delay_ms(100) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::HIGH) + sleep_ms(1000) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::LOW) + sleep_ms(1000) end ``` -Not bad, huh? +IMG_0197 ## Installation @@ -57,15 +55,107 @@ sudo dnf install avr-gcc avr-libc avrdude ## Usage ```bash -rubyduino examples/hello.rb +rubyduino examples/builtin/01_basics/blink.rb ``` Pass a serial port explicitly when auto-detection is not enough: ```bash -rubyduino -p /dev/cu.usbmodem11401 examples/hello.rb +rubyduino -p /dev/cu.usbmodem11401 examples/builtin/01_basics/blink.rb ``` +## Two API styles + +Rubyduino exposes the same functionality two ways. Pick whichever reads better — both compile to identical AVR code. + +**Arduino-compatible (porting `.ino` sketches)** + +```ruby +pin_mode(13, ArduinoUno::OUTPUT) +digital_write(13, ArduinoUno::HIGH) +delay_ms(500) + +serial_begin(9600) +serial_println("hello") +serial_println(value, ArduinoUno::HEX) +``` + +**Ruby-style (modules, predicates, idiomatic naming)** + +```ruby +Pin.mode(13, ArduinoUno::OUTPUT) +Pin.high(13) +sleep_ms(500) + +Serial.begin(9600) +Serial.println_str("hello") +Serial.println_hex(value) + +Eeprom.write(0, 0xAB); v = Eeprom.read(0) +Spi.begin; reply = Spi.transfer(0x9F) +Wire.begin; Wire.transmit(0x68); Wire.write(0x6B); Wire.end_transmission +Servo.attach(9); Servo.angle = 90 +``` + +Other Ruby-idiomatic helpers: `clamp`, `square`, `sleep_ms`, `sleep_us`, `srand`, `stop_tone`, predicate aliases like `alpha?`, `digit?`, `hex_digit?`, `interrupt_fired?`, `servo_attached?`, plus `without_interrupts { ... }` for critical sections. + +## What's covered + +The full Arduino UNO core API plus most of the bundled libraries — see [`plans/arduino_api_coverage.md`](plans/arduino_api_coverage.md) for the precise checklist. + +**Core API:** `pin_mode`, `digital_read/write`, `analog_read/write`, `analog_reference`, `delay_ms`, `delay_us`, `millis`, `micros`, `pulse_in`, `pulse_in_long`, `shift_in/out`, `tone`, `no_tone`, `attach_interrupt`, `detach_interrupt`, all bit/byte macros, `map_value`, `constrain`, `sq`, all 13 character classifiers (with `?` aliases), full Serial including `parse_int`, `parse_float`, `find?`, formatted print (HEX/BIN/OCT/float), `random_seed` + `rand(low..high)` codegen. + +**Bundled libraries:** EEPROM, SPI, Wire (I²C master), Servo (single-servo on Timer1). + +**Common third-party hardware**, all bit-banged in `lib/rubyduino/sp_runtime.h`: + +| Driver | Helpers | Wiring | +|----------------|-----------------------------------------------------------------|-----------------------| +| WS2812 / NeoPixel | `neopixel_begin`, `neopixel_set_pixel`, `neopixel_show`, `neopixel_clear` | data pin only | +| DHT11 / DHT22 | `dht_read?`, `dht_temperature_x10`, `dht_humidity_x10` | 1 wire + pull-up | +| 1-Wire / DS18B20 | `onewire_*`, `ds18b20_request_temperature`, `ds18b20_read_temperature_x10` | 1 wire + 4.7k pull-up | +| HD44780 LCD (4-bit) | `lcd_begin`, `lcd_print_str`, `lcd_print_int`, `lcd_set_cursor`, `lcd_clear` | RS, EN, D4–D7 | +| Stepper | `stepper_begin`, `stepper_set_speed`, `stepper_step` | 4 GPIO | +| SoftwareSerial | `soft_serial_begin`, `soft_serial_write`, `soft_serial_print_str`, `soft_serial_read` | RX + TX pins | +| IR remote (NEC) | `ir_receive?`, `ir_command` | TSOP38xx demodulator | + +## Examples + +Every Arduino built-in example for an UNO ships as a Ruby port: + +- [`examples/builtin/01_basics`](examples/builtin/01_basics) — Blink, Fade, AnalogReadSerial, etc. +- [`examples/builtin/02_digital`](examples/builtin/02_digital) — BlinkWithoutDelay, Debounce, tone melodies, … +- [`examples/builtin/03_analog`](examples/builtin/03_analog) — AnalogInOutSerial, Calibration, Smoothing, … +- [`examples/builtin/04_communication`](examples/builtin/04_communication) — ASCIITable, Dimmer, Midi, … +- [`examples/builtin/05_control`](examples/builtin/05_control) — Arrays, switchCase, while, … +- [`examples/builtin/06_sensors`](examples/builtin/06_sensors) — ADXL3xx, Knock, Memsic2125, Ping +- [`examples/builtin/07_display`](examples/builtin/07_display) — barGraph, RowColumnScanning +- [`examples/builtin/08_strings`](examples/builtin/08_strings) — character_analysis (the rest depend on the Arduino `String` class) + +Hardware-driver examples live in [`examples/hardware`](examples/hardware): + +- `neopixel_rainbow.rb` — 8-pixel WS2812 rainbow chase +- `dht22_serial.rb` — temperature/humidity over Serial +- `ds18b20_serial.rb` — DS18B20 temperature read every second +- `lcd_hello.rb` — 16x2 HD44780 hello-world +- `stepper_loop.rb` — 4-wire stepper full forward/back rotation +- `software_serial_passthrough.rb` — bridge between hardware and software UART +- `ir_remote_decode.rb` — print decoded NEC IR frames + +## Tests + +```bash +bundle exec rake test +``` + +The test suite has three layers: + +1. **Codegen tests** — exercise the Ruby → C compiler hooks. +2. **Native logic tests** — extract pure-logic helpers and compile/run them with `clang` on the host. +3. **AVR compile tests** — run the full pipeline through `avr-gcc` and inspect ELF output for ISR vectors and symbols. + +Bulk-compile coverage walks every sketch in `examples/builtin/**` and `examples/hardware/**` and verifies it links cleanly for `atmega328p`. + ## License MIT diff --git a/examples/hardware/dht22_serial.rb b/examples/hardware/dht22_serial.rb new file mode 100644 index 0000000..f22c286 --- /dev/null +++ b/examples/hardware/dht22_serial.rb @@ -0,0 +1,26 @@ +# DHT22 to Serial +# +# Polls a DHT22 sensor on D7 every 2 seconds and prints temp/humidity. + +DHT_PIN = 7 + +serial_begin(9600) + +loop do + if dht_read?(DHT_PIN, DHT22) + t = dht_temperature_x10 + h = dht_humidity_x10 + serial_print("temp=") + serial_print(t / 10) + serial_print(".") + serial_print(t % 10) + serial_print("C humid=") + serial_print(h / 10) + serial_print(".") + serial_print(h % 10) + serial_println("%") + else + serial_println("dht read failed") + end + sleep_ms(2000) +end diff --git a/examples/hardware/ds18b20_serial.rb b/examples/hardware/ds18b20_serial.rb new file mode 100644 index 0000000..2af38ee --- /dev/null +++ b/examples/hardware/ds18b20_serial.rb @@ -0,0 +1,23 @@ +# DS18B20 1-Wire temperature to Serial +# +# Reads a single DS18B20 on D4 once per second. +# Wire DQ to D4 with a 4.7k pull-up to +5V; VDD to +5V or ground (parasite). + +DS_PIN = 4 + +serial_begin(9600) + +loop do + if ds18b20_request_temperature(DS_PIN) == 1 + sleep_ms(750) # 12-bit conversion time + t = ds18b20_read_temperature_x10(DS_PIN) + serial_print("temp=") + serial_print(t / 10) + serial_print(".") + serial_print(t % 10) + serial_println("C") + else + serial_println("no DS18B20 detected") + end + sleep_ms(1000) +end diff --git a/examples/hardware/ir_remote_decode.rb b/examples/hardware/ir_remote_decode.rb new file mode 100644 index 0000000..8b910a4 --- /dev/null +++ b/examples/hardware/ir_remote_decode.rb @@ -0,0 +1,19 @@ +# IR remote receiver (NEC protocol) +# +# Wire a TSOP38xx-style demodulator's data line to D2. +# Prints decoded 32-bit NEC frames as hex over serial. + +IR_PIN = 2 + +serial_begin(9600) +serial_println("Point an IR remote at the receiver...") + +loop do + if ir_receive?(IR_PIN, 1000) + cmd = ir_command + serial_print("frame=0x") + serial_print(cmd, ArduinoUNO::HEX) + serial_print(" command_byte=0x") + serial_println((cmd >> 16) & 0xFF, ArduinoUNO::HEX) + end +end diff --git a/examples/hardware/lcd_hello.rb b/examples/hardware/lcd_hello.rb new file mode 100644 index 0000000..175a315 --- /dev/null +++ b/examples/hardware/lcd_hello.rb @@ -0,0 +1,19 @@ +# 16x2 LCD "Hello, world!" +# +# Wiring (HD44780 in 4-bit mode, R/W to GND): +# RS -> D12, EN -> D11, D4 -> D5, D5 -> D4, D6 -> D3, D7 -> D2 + +lcd_begin(12, 11, 5, 4, 3, 2, 16, 2) + +lcd_print_str("Hello, world!") +lcd_set_cursor(0, 1) +lcd_print_str("rubyduino + LCD") + +count = 0 +loop do + lcd_set_cursor(13, 1) + lcd_print_int(count % 1000) + lcd_write_char(32) # space + count += 1 + sleep_ms(500) +end diff --git a/examples/hardware/neopixel_rainbow.rb b/examples/hardware/neopixel_rainbow.rb new file mode 100644 index 0000000..a600bc0 --- /dev/null +++ b/examples/hardware/neopixel_rainbow.rb @@ -0,0 +1,35 @@ +# NeoPixel rainbow chase +# +# Cycles a rainbow across an 8-pixel WS2812 strip wired to D6. +# Adjust DATA_PIN / NUM_PIXELS for your hardware. + +DATA_PIN = 6 +NUM_PIXELS = 8 + +def color_wheel(pos) + pos = pos % 256 + if pos < 85 + [pos * 3, 255 - pos * 3, 0] + elsif pos < 170 + pos = pos - 85 + [255 - pos * 3, 0, pos * 3] + else + pos = pos - 170 + [0, pos * 3, 255 - pos * 3] + end +end + +neopixel_begin(DATA_PIN, NUM_PIXELS) + +offset = 0 +loop do + i = 0 + while i < NUM_PIXELS + rgb = color_wheel((i * 256 / NUM_PIXELS) + offset) + neopixel_set_pixel(i, rgb[0], rgb[1], rgb[2]) + i += 1 + end + neopixel_show + offset = (offset + 1) % 256 + sleep_ms(20) +end diff --git a/examples/hardware/software_serial_passthrough.rb b/examples/hardware/software_serial_passthrough.rb new file mode 100644 index 0000000..6952d61 --- /dev/null +++ b/examples/hardware/software_serial_passthrough.rb @@ -0,0 +1,19 @@ +# SoftwareSerial passthrough +# +# Echoes bytes between hardware Serial (USB) and a SoftwareSerial port +# on RX=D2 / TX=D3 — useful for talking to a GPS/GSM/ESP8266 module +# while watching traffic from your laptop. + +soft_serial_begin(2, 3, 9600) +serial_begin(9600) + +loop do + b = soft_serial_read + if b != -1 + serial_write(b) + end + + if serial_available > 0 + soft_serial_write(serial_read) + end +end diff --git a/examples/hardware/stepper_loop.rb b/examples/hardware/stepper_loop.rb new file mode 100644 index 0000000..8002a08 --- /dev/null +++ b/examples/hardware/stepper_loop.rb @@ -0,0 +1,14 @@ +# Stepper motor — 200 steps each way at 60 RPM +# +# Wiring: 4 coil drive lines on D8..D11 (e.g. ULN2003 to a 28BYJ-48 +# motor; for a 28BYJ in half-step mode you'll want 2048 steps/rev). + +stepper_begin(200, 8, 9, 10, 11) +stepper_set_speed(60) + +loop do + stepper_step(200) + sleep_ms(500) + stepper_step(-200) + sleep_ms(500) +end diff --git a/lib/rubyduino/arduino_uno.rb b/lib/rubyduino/arduino_uno.rb index 55cbd39..82c56a3 100644 --- a/lib/rubyduino/arduino_uno.rb +++ b/lib/rubyduino/arduino_uno.rb @@ -199,6 +199,34 @@ module ArduinoUNO ffi_func :servo_read_microseconds, [], :uint16 ffi_func :servo_attached, [], :uint8 ffi_func :arduino_yield, [], :void + ffi_func :neopixel_begin, [:uint8, :uint16], :void + ffi_func :neopixel_set_pixel, [:uint16, :uint8, :uint8, :uint8], :void + ffi_func :neopixel_clear, [], :void + ffi_func :neopixel_show, [], :void + ffi_func :dht_read, [:uint8, :uint8], :int8 + ffi_func :dht_temperature_x10, [], :int16 + ffi_func :dht_humidity_x10, [], :int16 + ffi_func :onewire_reset, [:uint8], :uint8 + ffi_func :onewire_write_byte, [:uint8, :uint8], :void + ffi_func :onewire_read_byte, [:uint8], :uint8 + ffi_func :ds18b20_request_temperature, [:uint8], :uint8 + ffi_func :ds18b20_read_temperature_x10, [:uint8], :int16 + ffi_func :lcd_begin, [:uint8, :uint8, :uint8, :uint8, :uint8, :uint8, :uint8, :uint8], :void + ffi_func :lcd_clear, [], :void + ffi_func :lcd_home, [], :void + ffi_func :lcd_set_cursor, [:uint8, :uint8], :void + ffi_func :lcd_write_char, [:uint8], :void + ffi_func :lcd_print_str, [:str], :void + ffi_func :lcd_print_int, [:int32], :void + ffi_func :stepper_begin, [:uint16, :uint8, :uint8, :uint8, :uint8], :void + ffi_func :stepper_set_speed, [:uint16], :void + ffi_func :stepper_step, [:int16], :void + ffi_func :soft_serial_begin, [:uint8, :uint8, :uint32], :void + ffi_func :soft_serial_write, [:uint8], :void + ffi_func :soft_serial_print_str, [:str], :void + ffi_func :soft_serial_read, [], :int + ffi_func :ir_receive, [:uint8, :uint32], :int8 + ffi_func :ir_command, [], :uint32 end def pin_mode(pin, mode) @@ -761,6 +789,143 @@ def arduino_yield ArduinoUNO.arduino_yield end +def neopixel_begin(pin, num_pixels) + ArduinoUNO.neopixel_begin(pin, num_pixels) +end + +def neopixel_set_pixel(index, r, g, b) + ArduinoUNO.neopixel_set_pixel(index, r, g, b) +end + +def neopixel_clear + ArduinoUNO.neopixel_clear +end + +def neopixel_show + ArduinoUNO.neopixel_show +end + +# DHT type constants. The DHT22 is also known as AM2302/RHT03. +DHT11 = 11 +DHT22 = 22 + +def dht_read(pin, type = DHT22) + ArduinoUNO.dht_read(pin, type) +end + +def dht_read?(pin, type = DHT22) + ArduinoUNO.dht_read(pin, type) == 0 +end + +def dht_temperature_x10 + ArduinoUNO.dht_temperature_x10 +end + +def dht_humidity_x10 + ArduinoUNO.dht_humidity_x10 +end + +def onewire_reset(pin) + ArduinoUNO.onewire_reset(pin) +end + +def onewire_reset?(pin) + ArduinoUNO.onewire_reset(pin) == 1 +end + +def onewire_write_byte(pin, value) + ArduinoUNO.onewire_write_byte(pin, value) +end + +def onewire_read_byte(pin) + ArduinoUNO.onewire_read_byte(pin) +end + +def ds18b20_request_temperature(pin) + ArduinoUNO.ds18b20_request_temperature(pin) +end + +def ds18b20_read_temperature_x10(pin) + ArduinoUNO.ds18b20_read_temperature_x10(pin) +end + +# 16x2 character LCD on a HD44780 controller, wired in 4-bit mode. +# Pass the RS, E, and four data pins; pass cols/rows to size the display. +def lcd_begin(rs, en, d4, d5, d6, d7, cols = 16, rows = 2) + ArduinoUNO.lcd_begin(rs, en, d4, d5, d6, d7, cols, rows) +end + +def lcd_clear + ArduinoUNO.lcd_clear +end + +def lcd_home + ArduinoUNO.lcd_home +end + +def lcd_set_cursor(col, row) + ArduinoUNO.lcd_set_cursor(col, row) +end + +def lcd_write_char(char) + ArduinoUNO.lcd_write_char(char) +end + +def lcd_print_str(str) + ArduinoUNO.lcd_print_str(str) +end + +def lcd_print_int(value) + ArduinoUNO.lcd_print_int(value) +end + +# 4-wire stepper motor (full-step mode). Pass the steps-per-revolution and +# four GPIO pins wired to the coil drivers. +def stepper_begin(steps_per_revolution, p1, p2, p3, p4) + ArduinoUNO.stepper_begin(steps_per_revolution, p1, p2, p3, p4) +end + +def stepper_set_speed(rpm) + ArduinoUNO.stepper_set_speed(rpm) +end + +def stepper_step(steps) + ArduinoUNO.stepper_step(steps) +end + +# Bit-banged software serial. Single instance, half-duplex. Useful for +# talking to GPS modules, ESP-01, GSM modules, and similar peripherals +# while the hardware UART is busy. +def soft_serial_begin(rx_pin, tx_pin, baud) + ArduinoUNO.soft_serial_begin(rx_pin, tx_pin, baud) +end + +def soft_serial_write(byte) + ArduinoUNO.soft_serial_write(byte) +end + +def soft_serial_print_str(s) + ArduinoUNO.soft_serial_print_str(s) +end + +def soft_serial_read + ArduinoUNO.soft_serial_read +end + +# IR remote receiver (NEC protocol). Returns 0 on success, negative on +# timeout/parse error. Pair with `ir_command` to read the decoded command. +def ir_receive(pin, timeout_ms = 100) + ArduinoUNO.ir_receive(pin, timeout_ms) +end + +def ir_receive?(pin, timeout_ms = 100) + ArduinoUNO.ir_receive(pin, timeout_ms) == 0 +end + +def ir_command + ArduinoUNO.ir_command +end + # --------------------------------------------------------------------------- # Ruby-style module facades. # diff --git a/lib/rubyduino/sp_runtime.h b/lib/rubyduino/sp_runtime.h index e9beee1..21417d3 100644 --- a/lib/rubyduino/sp_runtime.h +++ b/lib/rubyduino/sp_runtime.h @@ -1201,6 +1201,687 @@ void arduino_yield(void) { * etc. still compile here. */ } +/* WS2812 / NeoPixel driver. + * + * Buffer is sized for up to RD_NEOPIXEL_MAX pixels (3 bytes each, GRB + * order). The bit-bang in neopixel_show is hand-tuned for 16 MHz AVR; + * each iteration of the inner loop hits T1H ~= 0.8 us, T0H ~= 0.4 us, + * full bit period ~= 1.25 us. Interrupts are disabled across the + * frame so timer ISRs don't stretch the high pulse and turn 0 bits + * into 1 bits. + */ +#ifndef RD_NEOPIXEL_MAX +#define RD_NEOPIXEL_MAX 64 +#endif + +static uint8_t rd_neopixel_buf[RD_NEOPIXEL_MAX * 3]; +static uint8_t rd_neopixel_pin = 255; +static uint16_t rd_neopixel_count = 0; + +void neopixel_begin(uint8_t pin, uint16_t count) { + uint16_t i; + + if (!rd_uno_valid_pin(pin)) { + return; + } + if (count > RD_NEOPIXEL_MAX) { + count = RD_NEOPIXEL_MAX; + } + + pin_mode(pin, 1); + digital_write(pin, 0); + rd_neopixel_pin = pin; + rd_neopixel_count = count; + + for (i = 0; i < (uint16_t)RD_NEOPIXEL_MAX * 3; i++) { + rd_neopixel_buf[i] = 0; + } +} + +void neopixel_set_pixel(uint16_t index, uint8_t r, uint8_t g, uint8_t b) { + if (index >= rd_neopixel_count) { + return; + } + /* WS2812 wants GRB order on the wire. */ + rd_neopixel_buf[index * 3 + 0] = g; + rd_neopixel_buf[index * 3 + 1] = r; + rd_neopixel_buf[index * 3 + 2] = b; +} + +void neopixel_clear(void) { + uint16_t i; + for (i = 0; i < rd_neopixel_count * 3; i++) { + rd_neopixel_buf[i] = 0; + } +} + +void neopixel_show(void) { + volatile uint8_t *port; + uint8_t mask; + uint8_t hi; + uint8_t lo; + uint8_t b; + uint8_t bit; + uint8_t *p; + uint16_t bytes; + + if (rd_neopixel_count == 0 || rd_neopixel_pin == 255) { + return; + } + + port = rd_uno_port(rd_neopixel_pin); + mask = (uint8_t)(1 << rd_uno_bit(rd_neopixel_pin)); + hi = (uint8_t)(*port | mask); + lo = (uint8_t)(*port & (uint8_t)~mask); + p = rd_neopixel_buf; + bytes = rd_neopixel_count * 3; + + cli(); + while (bytes > 0) { + b = *p++; + bytes--; + + /* MSB-first bit-bang. The NOP counts target ~13 cycles high for a + * 1 bit and ~6 cycles high for a 0 bit at 16 MHz. WS2812 tolerates + * +/- 150 ns on each phase. */ + bit = 8; + while (bit > 0) { + *port = hi; + if (b & 0x80) { + __builtin_avr_delay_cycles(8); + *port = lo; + __builtin_avr_delay_cycles(2); + } else { + __builtin_avr_delay_cycles(2); + *port = lo; + __builtin_avr_delay_cycles(8); + } + b <<= 1; + bit--; + } + } + sei(); + + /* WS2812 reset: at least 50 us low. The loop overhead already gave + * us some margin, but pad just in case. */ + _delay_us(50); +} + +/* DHT11 / DHT22 temperature + humidity driver. + * + * Single-wire timing protocol: + * 1. MCU pulls line low for >=18 ms (DHT11) or >=1 ms (DHT22) + * 2. MCU releases, line floats high via pull-up + * 3. Sensor responds with ~80us low + ~80us high + * 4. Sensor sends 40 bits: humid_high, humid_low, temp_high, temp_low, checksum + * 5. Each bit: ~50us low then high for 26-28us (0) or 70us (1) + * + * dht_read returns 0 on success, negative on error. Successful reads + * stash temperature/humidity * 10 so Ruby callers can format with one + * decimal place. The sensor must be polled at most every ~2 seconds. + */ +static int16_t rd_dht_temp_x10 = 0; +static int16_t rd_dht_humid_x10 = 0; + +static int rd_dht_wait_for(uint8_t pin, uint8_t state, uint32_t max_us) { + uint32_t start = micros(); + while (digital_read(pin) != state) { + if ((micros() - start) > max_us) { + return -1; + } + } + return (int)(micros() - start); +} + +int8_t dht_read(uint8_t pin, uint8_t type) { + uint8_t data[5]; + uint32_t bit_start; + uint32_t bit_high; + uint8_t i; + uint8_t sum; + + if (!rd_uno_valid_pin(pin)) { + return -1; + } + + data[0] = 0; + data[1] = 0; + data[2] = 0; + data[3] = 0; + data[4] = 0; + + /* MCU start pulse. */ + pin_mode(pin, 1); + digital_write(pin, 0); + if (type == 11) { + delay_ms(18); + } else { + delay_us(1100); + } + digital_write(pin, 1); + pin_mode(pin, 0); + delay_us(40); + + /* Sensor response: ~80us low, ~80us high, then first bit starts. */ + if (rd_dht_wait_for(pin, 0, 200) < 0) return -2; + if (rd_dht_wait_for(pin, 1, 200) < 0) return -3; + if (rd_dht_wait_for(pin, 0, 200) < 0) return -4; + + for (i = 0; i < 40; i++) { + /* Wait for the bit's high pulse to start. */ + if (rd_dht_wait_for(pin, 1, 200) < 0) return -5; + + bit_start = micros(); + while (digital_read(pin) == 1) { + if ((micros() - bit_start) > 200) return -6; + } + bit_high = micros() - bit_start; + + data[i >> 3] <<= 1; + if (bit_high > 40) { + data[i >> 3] |= 1; + } + } + + sum = (uint8_t)((data[0] + data[1] + data[2] + data[3]) & 0xFF); + if (sum != data[4]) { + return -7; + } + + if (type == 11) { + rd_dht_humid_x10 = (int16_t)data[0] * 10; + rd_dht_temp_x10 = (int16_t)data[2] * 10; + } else { + int16_t humid = (int16_t)(((uint16_t)data[0] << 8) | data[1]); + int16_t temp = (int16_t)(((uint16_t)(data[2] & 0x7F) << 8) | data[3]); + if (data[2] & 0x80) { + temp = (int16_t)-temp; + } + rd_dht_humid_x10 = humid; + rd_dht_temp_x10 = temp; + } + + return 0; +} + +int16_t dht_temperature_x10(void) { + return rd_dht_temp_x10; +} + +int16_t dht_humidity_x10(void) { + return rd_dht_humid_x10; +} + +/* Dallas 1-Wire bit-bang. The bus is open-drain — the host pulls low + * by switching to OUTPUT, releases by switching back to INPUT and + * letting the external pull-up restore the line. + * + * Timing (microseconds): + * Reset: pull low 480, release, sample after 70 (presence pulse if 0) + * Write 1: pull low 6, release, total slot 70 + * Write 0: pull low 60, release, total slot 70 + * Read: pull low 6, release, sample after 9, total slot 70 + */ +static void rd_onewire_low(uint8_t pin) { + pin_mode(pin, 1); + digital_write(pin, 0); +} + +static void rd_onewire_release(uint8_t pin) { + pin_mode(pin, 0); +} + +uint8_t onewire_reset(uint8_t pin) { + uint8_t presence; + uint8_t sreg = SREG; + + cli(); + rd_onewire_low(pin); + _delay_us(480); + rd_onewire_release(pin); + _delay_us(70); + presence = (digital_read(pin) == 0) ? 1 : 0; + SREG = sreg; + _delay_us(410); + return presence; +} + +static void rd_onewire_write_bit(uint8_t pin, uint8_t bit) { + uint8_t sreg = SREG; + cli(); + rd_onewire_low(pin); + if (bit) { + _delay_us(6); + rd_onewire_release(pin); + _delay_us(64); + } else { + _delay_us(60); + rd_onewire_release(pin); + _delay_us(10); + } + SREG = sreg; +} + +static uint8_t rd_onewire_read_bit(uint8_t pin) { + uint8_t bit; + uint8_t sreg = SREG; + cli(); + rd_onewire_low(pin); + _delay_us(6); + rd_onewire_release(pin); + _delay_us(9); + bit = (digital_read(pin) == 1) ? 1 : 0; + SREG = sreg; + _delay_us(55); + return bit; +} + +void onewire_write_byte(uint8_t pin, uint8_t value) { + uint8_t i; + for (i = 0; i < 8; i++) { + rd_onewire_write_bit(pin, (uint8_t)(value & 0x01)); + value >>= 1; + } +} + +uint8_t onewire_read_byte(uint8_t pin) { + uint8_t value = 0; + uint8_t i; + for (i = 0; i < 8; i++) { + if (rd_onewire_read_bit(pin)) { + value |= (uint8_t)(1 << i); + } + } + return value; +} + +/* DS18B20 helpers. Single-device-on-bus convenience: SKIP ROM (0xCC) + * skips the address-match step; CONVERT T (0x44) starts the + * conversion; READ SCRATCHPAD (0xBE) returns the 9-byte scratchpad. + * + * Reading is non-blocking once the conversion completes — caller + * should sleep ~750 ms (12-bit resolution) after request_temperature + * before calling read_temperature_x10. + */ +uint8_t ds18b20_request_temperature(uint8_t pin) { + if (!onewire_reset(pin)) { + return 0; + } + onewire_write_byte(pin, 0xCC); + onewire_write_byte(pin, 0x44); + return 1; +} + +int16_t ds18b20_read_temperature_x10(uint8_t pin) { + uint8_t raw_lo; + uint8_t raw_hi; + int16_t raw; + + if (!onewire_reset(pin)) { + return 0; + } + onewire_write_byte(pin, 0xCC); + onewire_write_byte(pin, 0xBE); + raw_lo = onewire_read_byte(pin); + raw_hi = onewire_read_byte(pin); + + raw = (int16_t)(((uint16_t)raw_hi << 8) | raw_lo); + /* DS18B20 reports temperature * 16 (1/16 °C resolution). + * Convert to tenths-of-degree: temp_x10 = raw * 10 / 16 = raw * 5 / 8. */ + return (int16_t)(((int32_t)raw * 5) / 8); +} + +/* HD44780 16x2 character LCD in 4-bit mode. + * + * Wiring assumption: R/W tied to ground (write-only). The host drives + * RS, E, and the upper-nibble data lines D4..D7. + */ +static uint8_t rd_lcd_rs = 255; +static uint8_t rd_lcd_en = 255; +static uint8_t rd_lcd_d4 = 255; +static uint8_t rd_lcd_d5 = 255; +static uint8_t rd_lcd_d6 = 255; +static uint8_t rd_lcd_d7 = 255; +static uint8_t rd_lcd_cols = 16; +static uint8_t rd_lcd_rows = 2; + +static void rd_lcd_pulse(void) { + digital_write(rd_lcd_en, 1); + _delay_us(1); + digital_write(rd_lcd_en, 0); + _delay_us(50); +} + +static void rd_lcd_write_nibble(uint8_t nibble) { + digital_write(rd_lcd_d4, (uint8_t)(nibble & 0x01)); + digital_write(rd_lcd_d5, (uint8_t)((nibble >> 1) & 0x01)); + digital_write(rd_lcd_d6, (uint8_t)((nibble >> 2) & 0x01)); + digital_write(rd_lcd_d7, (uint8_t)((nibble >> 3) & 0x01)); + rd_lcd_pulse(); +} + +static void rd_lcd_send(uint8_t value, uint8_t mode) { + digital_write(rd_lcd_rs, mode); + rd_lcd_write_nibble((uint8_t)(value >> 4)); + rd_lcd_write_nibble((uint8_t)(value & 0x0F)); +} + +static void rd_lcd_command(uint8_t cmd) { + rd_lcd_send(cmd, 0); +} + +static void rd_lcd_data(uint8_t value) { + rd_lcd_send(value, 1); +} + +void lcd_begin(uint8_t rs, uint8_t en, uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7, uint8_t cols, uint8_t rows) { + rd_lcd_rs = rs; + rd_lcd_en = en; + rd_lcd_d4 = d4; + rd_lcd_d5 = d5; + rd_lcd_d6 = d6; + rd_lcd_d7 = d7; + rd_lcd_cols = cols; + rd_lcd_rows = rows; + + pin_mode(rs, 1); + pin_mode(en, 1); + pin_mode(d4, 1); + pin_mode(d5, 1); + pin_mode(d6, 1); + pin_mode(d7, 1); + digital_write(rs, 0); + digital_write(en, 0); + + /* Power-up wait. */ + delay_ms(50); + + /* HD44780 datasheet boot sequence to enter 4-bit mode. */ + rd_lcd_write_nibble(0x03); + delay_ms(5); + rd_lcd_write_nibble(0x03); + _delay_us(160); + rd_lcd_write_nibble(0x03); + _delay_us(160); + rd_lcd_write_nibble(0x02); + + /* 4-bit, 2-line, 5x8 dots. */ + rd_lcd_command((uint8_t)(rows > 1 ? 0x28 : 0x20)); + /* Display on, cursor off, blink off. */ + rd_lcd_command(0x0C); + /* Entry mode: increment, no shift. */ + rd_lcd_command(0x06); + /* Clear. */ + rd_lcd_command(0x01); + delay_ms(2); +} + +void lcd_clear(void) { + rd_lcd_command(0x01); + delay_ms(2); +} + +void lcd_home(void) { + rd_lcd_command(0x02); + delay_ms(2); +} + +void lcd_set_cursor(uint8_t col, uint8_t row) { + static const uint8_t row_offsets[4] = {0x00, 0x40, 0x14, 0x54}; + if (row >= rd_lcd_rows) { + row = (uint8_t)(rd_lcd_rows - 1); + } + rd_lcd_command((uint8_t)(0x80 | (col + row_offsets[row]))); +} + +void lcd_write_char(uint8_t ch) { + rd_lcd_data(ch); +} + +void lcd_print_str(const char *s) { + while (*s) { + rd_lcd_data((uint8_t)*s); + s++; + } +} + +void lcd_print_int(int32_t value) { + char buf[12]; + char *p = &buf[11]; + uint32_t n; + + *p = '\0'; + if (value < 0) { + rd_lcd_data((uint8_t)'-'); + n = (uint32_t)(-value); + } else { + n = (uint32_t)value; + } + + do { + p--; + *p = (char)('0' + (n % 10)); + n /= 10; + } while (n > 0); + + lcd_print_str(p); +} + +/* 4-wire stepper motor. Mirrors the bundled Arduino Stepper library: + * tracks current step number, step delay (derived from RPM), and the + * four-wire coil sequence. Half/four-step driver chips (e.g. + * ULN2003 with a 28BYJ-48) work the same way. */ +static uint16_t rd_stepper_steps_per_rev = 200; +static uint16_t rd_stepper_step_delay_us = 60000; +static uint8_t rd_stepper_pins[4] = {255, 255, 255, 255}; +static int32_t rd_stepper_step_number = 0; + +static void rd_stepper_drive(int32_t step) { + uint8_t phase = (uint8_t)(((step % 4) + 4) % 4); + /* Standard full-step sequence: A B Aa Bb */ + static const uint8_t pattern[4][4] = { + {1, 0, 1, 0}, + {0, 1, 1, 0}, + {0, 1, 0, 1}, + {1, 0, 0, 1} + }; + uint8_t i; + for (i = 0; i < 4; i++) { + digital_write(rd_stepper_pins[i], pattern[phase][i]); + } +} + +void stepper_begin(uint16_t steps_per_revolution, uint8_t p1, uint8_t p2, uint8_t p3, uint8_t p4) { + rd_stepper_steps_per_rev = steps_per_revolution > 0 ? steps_per_revolution : 200; + rd_stepper_pins[0] = p1; + rd_stepper_pins[1] = p2; + rd_stepper_pins[2] = p3; + rd_stepper_pins[3] = p4; + pin_mode(p1, 1); + pin_mode(p2, 1); + pin_mode(p3, 1); + pin_mode(p4, 1); + digital_write(p1, 0); + digital_write(p2, 0); + digital_write(p3, 0); + digital_write(p4, 0); + rd_stepper_step_number = 0; + rd_stepper_step_delay_us = 60000; +} + +void stepper_set_speed(uint16_t rpm) { + if (rpm == 0) { + rd_stepper_step_delay_us = 60000; + return; + } + rd_stepper_step_delay_us = (uint16_t)(60UL * 1000UL * 1000UL / (uint32_t)rd_stepper_steps_per_rev / (uint32_t)rpm); +} + +void stepper_step(int16_t steps) { + int8_t direction = (steps < 0) ? -1 : 1; + uint16_t remaining = (uint16_t)((steps < 0) ? -steps : steps); + + while (remaining > 0) { + rd_stepper_step_number += direction; + rd_stepper_drive(rd_stepper_step_number); + + /* delay_us only takes uint32, but we keep the value <= ~30ms to + * stay inside the 16-bit field for fast iteration. */ + delay_us((uint32_t)rd_stepper_step_delay_us); + remaining--; + } +} + +/* Bit-banged software serial. + * + * Half-duplex: write blocks for the duration of the byte, read polls + * the start bit and samples on the bit centers. The bit period is + * derived from the baud rate. Common baud rates up to 38400 work + * reliably on a 16 MHz UNO; higher rates suffer from interrupt jitter. + */ +static uint8_t rd_soft_rx_pin = 255; +static uint8_t rd_soft_tx_pin = 255; +static uint16_t rd_soft_bit_period_us = 833; /* 1200 baud default */ + +void soft_serial_begin(uint8_t rx, uint8_t tx, uint32_t baud) { + rd_soft_rx_pin = rx; + rd_soft_tx_pin = tx; + if (baud == 0) baud = 9600; + rd_soft_bit_period_us = (uint16_t)(1000000UL / baud); + + if (tx != 255) { + pin_mode(tx, 1); + digital_write(tx, 1); /* idle high */ + } + if (rx != 255) { + pin_mode(rx, 0); + } +} + +void soft_serial_write(uint8_t byte) { + uint8_t i; + uint8_t sreg; + + if (rd_soft_tx_pin == 255) { + return; + } + + sreg = SREG; + cli(); + /* Start bit: low. */ + digital_write(rd_soft_tx_pin, 0); + delay_us((uint32_t)rd_soft_bit_period_us); + /* 8 data bits, LSB first. */ + for (i = 0; i < 8; i++) { + digital_write(rd_soft_tx_pin, (uint8_t)(byte & 0x01)); + byte >>= 1; + delay_us((uint32_t)rd_soft_bit_period_us); + } + /* Stop bit: high. */ + digital_write(rd_soft_tx_pin, 1); + delay_us((uint32_t)rd_soft_bit_period_us); + SREG = sreg; +} + +void soft_serial_print_str(const char *s) { + while (*s) { + soft_serial_write((uint8_t)*s); + s++; + } +} + +int soft_serial_read(void) { + uint8_t value = 0; + uint8_t i; + uint8_t sreg; + + if (rd_soft_rx_pin == 255) { + return -1; + } + if (digital_read(rd_soft_rx_pin) == 1) { + return -1; /* line is idle, no start bit waiting */ + } + + sreg = SREG; + cli(); + /* Skip the start bit and align to bit centers. */ + delay_us((uint32_t)(rd_soft_bit_period_us + (rd_soft_bit_period_us >> 1))); + for (i = 0; i < 8; i++) { + if (digital_read(rd_soft_rx_pin)) { + value |= (uint8_t)(1 << i); + } + delay_us((uint32_t)rd_soft_bit_period_us); + } + SREG = sreg; + return (int)value; +} + +/* IR remote receiver — NEC protocol decode. + * + * Wiring: a TSOP38xx-style demodulating receiver puts the bare command + * stream on the pin, idle-high. + * + * NEC frame: + * 9.0 ms low (AGC pulse) + * 4.5 ms high + * 32 bits, each 0.56 ms low followed by ~0.56 ms (0) or ~1.69 ms (1) high + * final 0.56 ms low stop bit + */ +static uint32_t rd_ir_last = 0; + +int8_t ir_receive(uint8_t pin, uint32_t timeout_ms) { + uint32_t deadline = micros() + timeout_ms * 1000UL; + uint32_t bit_start; + uint32_t bit_high; + uint32_t frame = 0; + uint8_t i; + + if (!rd_uno_valid_pin(pin)) { + return -1; + } + + pin_mode(pin, 0); + + while (digital_read(pin) == 1) { + if (micros() > deadline) return -2; + } + + bit_start = micros(); + while (digital_read(pin) == 0) { + if ((micros() - bit_start) > 12000) return -3; + } + if ((micros() - bit_start) < 7000) return -4; + + bit_start = micros(); + while (digital_read(pin) == 1) { + if ((micros() - bit_start) > 6000) return -5; + } + if ((micros() - bit_start) < 3500) return -6; + + for (i = 0; i < 32; i++) { + bit_start = micros(); + while (digital_read(pin) == 0) { + if ((micros() - bit_start) > 1500) return -7; + } + bit_start = micros(); + while (digital_read(pin) == 1) { + if ((micros() - bit_start) > 3000) return -8; + } + bit_high = micros() - bit_start; + frame >>= 1; + if (bit_high > 1000) { + frame |= 0x80000000UL; + } + } + + rd_ir_last = frame; + return 0; +} + +uint32_t ir_command(void) { + return rd_ir_last; +} + void serial_write(uint8_t value) { while (!(UCSR0A & (uint8_t)(1 << UDRE0))) { } diff --git a/test/test_builtin_examples.rb b/test/test_builtin_examples.rb index 999d696..4ad1277 100644 --- a/test/test_builtin_examples.rb +++ b/test/test_builtin_examples.rb @@ -4,10 +4,10 @@ require "support/compile_helper" class TestBuiltinExamples < Minitest::Test - EXAMPLES_DIR = File.expand_path("../examples/builtin", __dir__) + ROOT_EXAMPLES = File.expand_path("../examples", __dir__) - Dir.glob(File.join(EXAMPLES_DIR, "*", "*.rb")).sort.each do |path| - rel = path.sub("#{File.dirname(EXAMPLES_DIR)}/", "") + Dir.glob(File.join(ROOT_EXAMPLES, "{builtin,hardware}", "**", "*.rb")).sort.each do |path| + rel = path.sub("#{File.dirname(ROOT_EXAMPLES)}/", "") test_name = "test_#{rel.gsub(%r{[/.]}, "_")}_compiles" define_method(test_name) do skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? @@ -17,8 +17,10 @@ class TestBuiltinExamples < Minitest::Test end end - def test_examples_dir_has_translations - rb_files = Dir.glob(File.join(EXAMPLES_DIR, "*", "*.rb")) - assert_operator rb_files.length, :>=, 35, "expected at least 35 example sketches" + def test_examples_dirs_have_sketches + builtin = Dir.glob(File.join(ROOT_EXAMPLES, "builtin", "*", "*.rb")) + hardware = Dir.glob(File.join(ROOT_EXAMPLES, "hardware", "*.rb")) + assert_operator builtin.length, :>=, 35, "expected at least 35 builtin example sketches" + assert_operator hardware.length, :>=, 7, "expected at least 7 hardware example sketches" end end diff --git a/test/test_dht.rb b/test/test_dht.rb new file mode 100644 index 0000000..f64b7ae --- /dev/null +++ b/test/test_dht.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestDHT < Minitest::Test + def test_codegen + sketch = <<~RUBY + err = dht_read(7, DHT22) + if err == 0 + t = dht_temperature_x10 + h = dht_humidity_x10 + end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "dht_read(" + assert_includes c, "dht_temperature_x10(" + assert_includes c, "dht_humidity_x10(" + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + serial_begin(9600) + pin = 7 + + loop do + if dht_read?(pin, DHT22) + t = dht_temperature_x10 + h = dht_humidity_x10 + serial_print("temp=") + serial_print(t / 10) + serial_print(".") + serial_print(t % 10) + serial_print("C humid=") + serial_print(h / 10) + serial_print(".") + serial_println(h % 10) + else + serial_println("dht read failed") + end + sleep_ms(2000) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_ir_remote.rb b/test/test_ir_remote.rb new file mode 100644 index 0000000..26d1e48 --- /dev/null +++ b/test/test_ir_remote.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestIRRemote < Minitest::Test + def test_codegen + sketch = <<~RUBY + pin = 2 + if ir_receive?(pin, 50) + cmd = ir_command + digital_write(13, 1) if cmd == 0xFF02FD + end + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + assert_includes c, "ir_receive(" + assert_includes c, "ir_command(" + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + serial_begin(9600) + pin = 2 + + loop do + if ir_receive?(pin, 100) + serial_print("cmd=0x") + serial_println(ir_command, ArduinoUNO::HEX) + end + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_lcd.rb b/test/test_lcd.rb new file mode 100644 index 0000000..6a8c0d7 --- /dev/null +++ b/test/test_lcd.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestLCD < Minitest::Test + HELPERS = %w[lcd_begin lcd_clear lcd_home lcd_set_cursor lcd_write_char + lcd_print_str lcd_print_int].freeze + + def test_codegen + sketch = <<~RUBY + lcd_begin(12, 11, 5, 4, 3, 2) + lcd_print_str("hello") + lcd_set_cursor(0, 1) + lcd_print_int(42) + lcd_home + lcd_clear + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + lcd_begin(12, 11, 5, 4, 3, 2, 16, 2) + lcd_print_str("rubyduino") + lcd_set_cursor(0, 1) + + counter = 0 + loop do + lcd_set_cursor(0, 1) + lcd_print_str("count: ") + lcd_print_int(counter) + lcd_write_char(32) # space + counter += 1 + sleep_ms(500) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_neopixel.rb b/test/test_neopixel.rb new file mode 100644 index 0000000..d281aa0 --- /dev/null +++ b/test/test_neopixel.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestNeoPixel < Minitest::Test + HELPERS = %w[neopixel_begin neopixel_set_pixel neopixel_clear neopixel_show].freeze + + def test_codegen_emits_helpers + sketch = <<~RUBY + neopixel_begin(6, 16) + neopixel_set_pixel(0, 255, 0, 0) + neopixel_set_pixel(1, 0, 255, 0) + neopixel_show + neopixel_clear + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + neopixel_begin(6, 8) + loop do + i = 0 + while i < 8 + neopixel_clear + neopixel_set_pixel(i, 64, 0, 32) + neopixel_show + sleep_ms(120) + i += 1 + end + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_onewire_ds18b20.rb b/test/test_onewire_ds18b20.rb new file mode 100644 index 0000000..28afc77 --- /dev/null +++ b/test/test_onewire_ds18b20.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestOneWireDS18B20 < Minitest::Test + HELPERS = %w[onewire_reset onewire_write_byte onewire_read_byte + ds18b20_request_temperature ds18b20_read_temperature_x10].freeze + + def test_codegen + sketch = <<~RUBY + pin = 4 + ok = onewire_reset?(pin) + onewire_write_byte(pin, 0xCC) + b = onewire_read_byte(pin) + ds18b20_request_temperature(pin) + sleep_ms(750) + t = ds18b20_read_temperature_x10(pin) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + HELPERS.each { |h| assert_includes c, "#{h}(", "missing #{h}" } + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + serial_begin(9600) + pin = 4 + + loop do + if ds18b20_request_temperature(pin) == 1 + sleep_ms(750) + t = ds18b20_read_temperature_x10(pin) + serial_print("temp=") + serial_print(t / 10) + serial_print(".") + serial_println(t % 10) + else + serial_println("no DS18B20") + end + sleep_ms(2000) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_software_serial.rb b/test/test_software_serial.rb new file mode 100644 index 0000000..336e41f --- /dev/null +++ b/test/test_software_serial.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestSoftwareSerial < Minitest::Test + def test_codegen + sketch = <<~RUBY + soft_serial_begin(2, 3, 9600) + soft_serial_print_str("AT\\r\\n") + b = soft_serial_read + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[soft_serial_begin soft_serial_print_str soft_serial_write soft_serial_read].each do |fn| + assert_includes c, "#{fn}(", "missing #{fn}" + end + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + soft_serial_begin(2, 3, 9600) + serial_begin(9600) + + loop do + soft_serial_print_str("ping\\n") + b = soft_serial_read + if b != -1 + serial_print("got: ") + serial_println(b) + end + sleep_ms(1000) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end diff --git a/test/test_stepper.rb b/test/test_stepper.rb new file mode 100644 index 0000000..370753b --- /dev/null +++ b/test/test_stepper.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/compile_helper" + +class TestStepper < Minitest::Test + def test_codegen + sketch = <<~RUBY + stepper_begin(200, 8, 9, 10, 11) + stepper_set_speed(60) + stepper_step(100) + stepper_step(-50) + RUBY + c = CompileHelper.compile_ruby_to_c(sketch) + %w[stepper_begin stepper_set_speed stepper_step].each do |fn| + assert_includes c, "#{fn}(", "missing #{fn}" + end + end + + def test_avr_compile + skip "avr-gcc not installed" unless CompileHelper.avr_gcc_available? + sketch = <<~RUBY + stepper_begin(200, 8, 9, 10, 11) + stepper_set_speed(60) + loop do + stepper_step(200) + sleep_ms(500) + stepper_step(-200) + sleep_ms(500) + end + RUBY + obj = CompileHelper.compile_ruby_to_avr_obj(sketch) + refute_empty obj + end +end From 037a9efbfae338bb015782b809d310d2dd59103b Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Mon, 11 May 2026 07:50:41 +1000 Subject: [PATCH 32/33] Add 18 ThinkerShield Crack the Code samples (converted to Ruby) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the Tasmanian S4-S5 coding workbook examples (originally .ino sketches from /Users/admin/code/thinkershield/tas-s4-s5-coding-samples) into the rubyduino API. Each file lives under examples/crack_the_code/ so bundle_examples.rb picks them up automatically under a new "Crack the Code" category. Sketches cover the progression: 01 Blink — single LED on pin 12, then four LEDs at once 02 Button / Toggle / Toggle2 — debounced button + latch 03 Int — same blink with a named constant for the pin 04 Fade — PWM fade on pin 11 05 Analog input / Night light / Pot reverse — LDR + pot reads 06 Serial print — pot value to Serial Monitor 07-09 Buzzer — single beep, two-tone siren, swept tone 10 Alarm — basic switch trigger, LDR trigger, LDR + reset, and a five-state machine with status LEDs Pinouts match the ThinkerShield: LEDs 9-13, button 7, buzzer 2-3, LDR 4, potentiometer A5/5. All 18 compile cleanly against the live Lambda (verified end-to-end). --- examples/crack_the_code/01_blink.rb | 15 ++ examples/crack_the_code/01_blink_x4.rb | 15 ++ examples/crack_the_code/02_button.rb | 19 +++ examples/crack_the_code/02_toggle.rb | 28 ++++ examples/crack_the_code/02_toggle2.rb | 29 ++++ examples/crack_the_code/03_int.rb | 15 ++ examples/crack_the_code/04_fade.rb | 21 +++ examples/crack_the_code/05_analog_input.rb | 21 +++ examples/crack_the_code/05_night_light.rb | 21 +++ examples/crack_the_code/05_pot_reverse.rb | 26 ++++ examples/crack_the_code/06_serial_print.rb | 17 +++ examples/crack_the_code/07_buzzer1.rb | 13 ++ examples/crack_the_code/08_buzzer2.rb | 17 +++ examples/crack_the_code/09_buzzer3.rb | 23 +++ examples/crack_the_code/10_alarm_basic.rb | 43 ++++++ examples/crack_the_code/10_alarm_basic_ldr.rb | 43 ++++++ .../10_alarm_basic_ldr_reset.rb | 44 ++++++ examples/crack_the_code/10_alarm_complex.rb | 143 ++++++++++++++++++ 18 files changed, 553 insertions(+) create mode 100644 examples/crack_the_code/01_blink.rb create mode 100644 examples/crack_the_code/01_blink_x4.rb create mode 100644 examples/crack_the_code/02_button.rb create mode 100644 examples/crack_the_code/02_toggle.rb create mode 100644 examples/crack_the_code/02_toggle2.rb create mode 100644 examples/crack_the_code/03_int.rb create mode 100644 examples/crack_the_code/04_fade.rb create mode 100644 examples/crack_the_code/05_analog_input.rb create mode 100644 examples/crack_the_code/05_night_light.rb create mode 100644 examples/crack_the_code/05_pot_reverse.rb create mode 100644 examples/crack_the_code/06_serial_print.rb create mode 100644 examples/crack_the_code/07_buzzer1.rb create mode 100644 examples/crack_the_code/08_buzzer2.rb create mode 100644 examples/crack_the_code/09_buzzer3.rb create mode 100644 examples/crack_the_code/10_alarm_basic.rb create mode 100644 examples/crack_the_code/10_alarm_basic_ldr.rb create mode 100644 examples/crack_the_code/10_alarm_basic_ldr_reset.rb create mode 100644 examples/crack_the_code/10_alarm_complex.rb diff --git a/examples/crack_the_code/01_blink.rb b/examples/crack_the_code/01_blink.rb new file mode 100644 index 0000000..e5e8378 --- /dev/null +++ b/examples/crack_the_code/01_blink.rb @@ -0,0 +1,15 @@ +# Blink (one LED) +# +# Turns an LED on pin 12 of the ThinkerShield on for one second, +# then off for one second, forever. + +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) + +loop do + digital_write(LED_PIN, ArduinoUNO::HIGH) + delay_ms(1000) + digital_write(LED_PIN, ArduinoUNO::LOW) + delay_ms(1000) +end diff --git a/examples/crack_the_code/01_blink_x4.rb b/examples/crack_the_code/01_blink_x4.rb new file mode 100644 index 0000000..6babcc7 --- /dev/null +++ b/examples/crack_the_code/01_blink_x4.rb @@ -0,0 +1,15 @@ +# Blink (four LEDs) +# +# Lights up the four LEDs on the ThinkerShield (pins 9, 10, 11, 12) +# all at once for one second, then off for one second, forever. + +PINS = [9, 10, 11, 12] + +PINS.each { |p| pin_mode(p, ArduinoUNO::OUTPUT) } + +loop do + PINS.each { |p| digital_write(p, ArduinoUNO::HIGH) } + delay_ms(1000) + PINS.each { |p| digital_write(p, ArduinoUNO::LOW) } + delay_ms(1000) +end diff --git a/examples/crack_the_code/02_button.rb b/examples/crack_the_code/02_button.rb new file mode 100644 index 0000000..9b16cc7 --- /dev/null +++ b/examples/crack_the_code/02_button.rb @@ -0,0 +1,19 @@ +# Button +# +# Reads the pushbutton on pin 7 of the ThinkerShield. While the button +# is held down, the LED on pin 12 is on; otherwise it's off. + +BUTTON_PIN = 7 +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(BUTTON_PIN, ArduinoUNO::INPUT) + +loop do + button_state = digital_read(BUTTON_PIN) + if button_state == ArduinoUNO::HIGH + digital_write(LED_PIN, ArduinoUNO::HIGH) + else + digital_write(LED_PIN, ArduinoUNO::LOW) + end +end diff --git a/examples/crack_the_code/02_toggle.rb b/examples/crack_the_code/02_toggle.rb new file mode 100644 index 0000000..dfbb324 --- /dev/null +++ b/examples/crack_the_code/02_toggle.rb @@ -0,0 +1,28 @@ +# Toggle (latching button) +# +# Press the button on pin 7 once to turn the LED on, press again to +# turn it off. A short delay debounces the button press. + +BUTTON_PIN = 7 +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(BUTTON_PIN, ArduinoUNO::INPUT) + +led_state = ArduinoUNO::LOW + +loop do + digital_write(LED_PIN, led_state) + button_state = digital_read(BUTTON_PIN) + if led_state == ArduinoUNO::HIGH + if button_state == ArduinoUNO::HIGH + led_state = ArduinoUNO::LOW + delay_ms(400) + end + else + if button_state == ArduinoUNO::HIGH + led_state = ArduinoUNO::HIGH + delay_ms(400) + end + end +end diff --git a/examples/crack_the_code/02_toggle2.rb b/examples/crack_the_code/02_toggle2.rb new file mode 100644 index 0000000..f8235ca --- /dev/null +++ b/examples/crack_the_code/02_toggle2.rb @@ -0,0 +1,29 @@ +# Toggle (latching button, two-check version) +# +# Same idea as `toggle`, but checks the button twice each loop — once +# to turn the LED off, again to turn it on. Demonstrates how the order +# of checks matters when a button stays pressed. + +BUTTON_PIN = 7 +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(BUTTON_PIN, ArduinoUNO::INPUT) + +led_state = ArduinoUNO::LOW + +loop do + digital_write(LED_PIN, led_state) + + button_state = digital_read(BUTTON_PIN) + if button_state == ArduinoUNO::HIGH && led_state == ArduinoUNO::HIGH + led_state = ArduinoUNO::LOW + delay_ms(300) + end + + button_state = digital_read(BUTTON_PIN) + if button_state == ArduinoUNO::HIGH && led_state == ArduinoUNO::LOW + led_state = ArduinoUNO::HIGH + delay_ms(300) + end +end diff --git a/examples/crack_the_code/03_int.rb b/examples/crack_the_code/03_int.rb new file mode 100644 index 0000000..9580d54 --- /dev/null +++ b/examples/crack_the_code/03_int.rb @@ -0,0 +1,15 @@ +# Variables (give pin numbers a name) +# +# Same as Blink, but the pin number is stored in a constant so the +# code is easier to read and change later. + +LED = 12 + +pin_mode(LED, ArduinoUNO::OUTPUT) + +loop do + digital_write(LED, ArduinoUNO::HIGH) + delay_ms(1000) + digital_write(LED, ArduinoUNO::LOW) + delay_ms(1000) +end diff --git a/examples/crack_the_code/04_fade.rb b/examples/crack_the_code/04_fade.rb new file mode 100644 index 0000000..3cc0bfb --- /dev/null +++ b/examples/crack_the_code/04_fade.rb @@ -0,0 +1,21 @@ +# Fade +# +# Smoothly fades the LED on pin 11 (a PWM pin, marked with a ~ on +# the board) up and down forever. Brightness goes 0 → 255 → 0 in +# steps of 5, with a 30 ms pause between steps. + +LED = 11 + +pin_mode(LED, ArduinoUNO::OUTPUT) + +brightness = 0 +fade_amount = 5 + +loop do + analog_write(LED, brightness) + brightness += fade_amount + if brightness <= 0 || brightness >= 255 + fade_amount = -fade_amount + end + delay_ms(30) +end diff --git a/examples/crack_the_code/05_analog_input.rb b/examples/crack_the_code/05_analog_input.rb new file mode 100644 index 0000000..c07607a --- /dev/null +++ b/examples/crack_the_code/05_analog_input.rb @@ -0,0 +1,21 @@ +# Analog input (potentiometer controls blink speed) +# +# Reads the potentiometer on A5 (0..1023) and uses that value as the +# delay between LED-on and LED-off. Turn the pot to speed up or slow +# down the blink. + +SENSOR_PIN = ArduinoUNO::A5 +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) +serial_begin(9600) + +loop do + sensor_value = analog_read(SENSOR_PIN) + digital_write(LED_PIN, ArduinoUNO::HIGH) + delay_ms(sensor_value) + digital_write(LED_PIN, ArduinoUNO::LOW) + delay_ms(sensor_value) + serial_print("The sensor value is: ") + serial_println(sensor_value) +end diff --git a/examples/crack_the_code/05_night_light.rb b/examples/crack_the_code/05_night_light.rb new file mode 100644 index 0000000..013dbb9 --- /dev/null +++ b/examples/crack_the_code/05_night_light.rb @@ -0,0 +1,21 @@ +# Night light +# +# Reads the light sensor (LDR) on pin 4 and turns on the LED on pin 12 +# whenever the room gets dark (sensor reads below 500). + +SENSOR_PIN = 4 +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(SENSOR_PIN, ArduinoUNO::INPUT) +serial_begin(9600) + +loop do + light_level = analog_read(SENSOR_PIN) + serial_println(light_level) + if light_level < 500 + digital_write(LED_PIN, ArduinoUNO::HIGH) + else + digital_write(LED_PIN, ArduinoUNO::LOW) + end +end diff --git a/examples/crack_the_code/05_pot_reverse.rb b/examples/crack_the_code/05_pot_reverse.rb new file mode 100644 index 0000000..053f55d --- /dev/null +++ b/examples/crack_the_code/05_pot_reverse.rb @@ -0,0 +1,26 @@ +# Potentiometer (reverse action) +# +# Same as the analog-input demo, but the delay is the OPPOSITE of the +# pot reading: turning the pot UP makes the LED blink FASTER. + +SENSOR_PIN = ArduinoUNO::A5 +LED_PIN = 12 + +pin_mode(LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(SENSOR_PIN, ArduinoUNO::INPUT) +serial_begin(9600) + +loop do + sensor_value = analog_read(SENSOR_PIN) + delay_value = 1023 - sensor_value + + digital_write(LED_PIN, ArduinoUNO::HIGH) + delay_ms(delay_value) + digital_write(LED_PIN, ArduinoUNO::LOW) + delay_ms(delay_value) + + serial_print("The delay is: ") + serial_print(delay_value) + serial_print(" The pot value is: ") + serial_println(sensor_value) +end diff --git a/examples/crack_the_code/06_serial_print.rb b/examples/crack_the_code/06_serial_print.rb new file mode 100644 index 0000000..5651015 --- /dev/null +++ b/examples/crack_the_code/06_serial_print.rb @@ -0,0 +1,17 @@ +# Serial print (read the potentiometer) +# +# Reads the potentiometer on A5 every half a second and prints the +# value to the Serial Monitor. Useful for understanding what a sensor +# is doing without any LEDs. + +POT_PIN = ArduinoUNO::A5 +DLY_TIME = 500 + +serial_begin(9600) + +loop do + pot_value = analog_read(POT_PIN) + serial_print("The pot value is: ") + serial_println(pot_value) + delay_ms(DLY_TIME) +end diff --git a/examples/crack_the_code/07_buzzer1.rb b/examples/crack_the_code/07_buzzer1.rb new file mode 100644 index 0000000..b18f9ce --- /dev/null +++ b/examples/crack_the_code/07_buzzer1.rb @@ -0,0 +1,13 @@ +# Buzzer (single beep) +# +# Plays a 600 Hz tone on the buzzer (pin 2) for 30 ms, then pauses +# 150 ms before doing it again. Sounds like a steady tick. + +BUZZER_PIN = 2 + +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) + +loop do + tone(BUZZER_PIN, 600, 30) + delay_ms(150) +end diff --git a/examples/crack_the_code/08_buzzer2.rb b/examples/crack_the_code/08_buzzer2.rb new file mode 100644 index 0000000..1a95470 --- /dev/null +++ b/examples/crack_the_code/08_buzzer2.rb @@ -0,0 +1,17 @@ +# Buzzer (alternating two tones) +# +# Plays a low tone, then a high tone, half a second each, forever. +# Sounds like a simple siren. + +LOW_TONE = 400 +HIGH_TONE = 600 +BUZZER_PIN = 3 + +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) + +loop do + tone(BUZZER_PIN, LOW_TONE, 500) + delay_ms(500) + tone(BUZZER_PIN, HIGH_TONE, 500) + delay_ms(500) +end diff --git a/examples/crack_the_code/09_buzzer3.rb b/examples/crack_the_code/09_buzzer3.rb new file mode 100644 index 0000000..4a6e9dc --- /dev/null +++ b/examples/crack_the_code/09_buzzer3.rb @@ -0,0 +1,23 @@ +# Buzzer (sweep up and down) +# +# Sweeps the buzzer's pitch from 200 Hz up to 700 Hz, then back down, +# in steps of 2 Hz. Sounds like a UFO landing. + +BUZZER_PIN = 3 + +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) + +loop do + i = 200 + while i < 700 + tone(BUZZER_PIN, i, 10) + delay_ms(10) + i += 2 + end + i = 701 + while i > 200 + tone(BUZZER_PIN, i, 10) + delay_ms(10) + i -= 2 + end +end diff --git a/examples/crack_the_code/10_alarm_basic.rb b/examples/crack_the_code/10_alarm_basic.rb new file mode 100644 index 0000000..7bcc209 --- /dev/null +++ b/examples/crack_the_code/10_alarm_basic.rb @@ -0,0 +1,43 @@ +# Alarm (basic switch) +# +# A simple intruder alarm. Pin 7 is the trip switch (e.g. copper tape +# on the lid). After arming, opening the lid (sensor reads 0) triggers +# the alarm: the on-board LED lights and the buzzer plays a two-tone +# siren forever. + +SENSOR_PIN = 7 +FLASH_PIN = 13 +BUZZER_PIN = 3 + +pin_mode(SENSOR_PIN, ArduinoUNO::INPUT) +pin_mode(FLASH_PIN, ArduinoUNO::OUTPUT) +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) + +status = 0 # 0 = exit delay, 1 = armed, 2 = triggered +sensor = 0 + +loop do + if status == 0 + delay_ms(3000) # 3 seconds to close the box + tone(BUZZER_PIN, 725, 40) + delay_ms(200) + tone(BUZZER_PIN, 725, 40) + status = 1 + end + + if status == 1 + sensor = digital_read(SENSOR_PIN) + end + + if sensor == 0 + status = 2 + end + + if status == 2 + digital_write(FLASH_PIN, ArduinoUNO::HIGH) + tone(BUZZER_PIN, 725, 1000) + delay_ms(1000) + tone(BUZZER_PIN, 330, 1000) + delay_ms(1000) + end +end diff --git a/examples/crack_the_code/10_alarm_basic_ldr.rb b/examples/crack_the_code/10_alarm_basic_ldr.rb new file mode 100644 index 0000000..881a16c --- /dev/null +++ b/examples/crack_the_code/10_alarm_basic_ldr.rb @@ -0,0 +1,43 @@ +# Alarm (LDR trigger) +# +# Like the basic alarm, but the trigger is the light sensor (LDR) on +# pin 4. Once armed, if the light level rises above THRESHOLD (someone +# opens the box and lets light in), the alarm triggers. + +THRESHOLD = 100 +SENSOR_PIN = 4 +FLASH_PIN = 13 +BUZZER_PIN = 3 + +pin_mode(SENSOR_PIN, ArduinoUNO::INPUT) +pin_mode(FLASH_PIN, ArduinoUNO::OUTPUT) +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) +serial_begin(9600) + +status = 0 + +loop do + sensor = analog_read(SENSOR_PIN) + serial_println(sensor) + + if status == 0 + delay_ms(3000) + tone(BUZZER_PIN, 725, 40) + delay_ms(200) + tone(BUZZER_PIN, 725, 40) + status = 1 + end + + if status == 1 && sensor > THRESHOLD + delay_ms(2000) + status = 2 + end + + if status == 2 + digital_write(FLASH_PIN, ArduinoUNO::HIGH) + tone(BUZZER_PIN, 725, 1000) + delay_ms(1000) + tone(BUZZER_PIN, 330, 1000) + delay_ms(1000) + end +end diff --git a/examples/crack_the_code/10_alarm_basic_ldr_reset.rb b/examples/crack_the_code/10_alarm_basic_ldr_reset.rb new file mode 100644 index 0000000..9d0fab1 --- /dev/null +++ b/examples/crack_the_code/10_alarm_basic_ldr_reset.rb @@ -0,0 +1,44 @@ +# Alarm (LDR trigger, with reset) +# +# Same as the LDR alarm, but pressing the button on pin 7 while the +# alarm is sounding resets it back to the exit-delay state. + +THRESHOLD = 100 +SENSOR_PIN = 4 +RESET_PIN = 7 +FLASH_PIN = 13 +BUZZER_PIN = 3 + +pin_mode(SENSOR_PIN, ArduinoUNO::INPUT) +pin_mode(RESET_PIN, ArduinoUNO::INPUT) +pin_mode(FLASH_PIN, ArduinoUNO::OUTPUT) +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) +serial_begin(9600) + +status = 0 + +loop do + sensor = analog_read(SENSOR_PIN) + serial_println(sensor) + + if status == 0 + delay_ms(3000) + tone(BUZZER_PIN, 725, 40) + delay_ms(200) + tone(BUZZER_PIN, 725, 40) + status = 1 + end + + if status == 1 && sensor > THRESHOLD + status = 2 + end + + if status == 2 + digital_write(FLASH_PIN, ArduinoUNO::HIGH) + tone(BUZZER_PIN, 725, 500) + reset_value = digital_read(RESET_PIN) + if reset_value == ArduinoUNO::HIGH + status = 0 + end + end +end diff --git a/examples/crack_the_code/10_alarm_complex.rb b/examples/crack_the_code/10_alarm_complex.rb new file mode 100644 index 0000000..1437650 --- /dev/null +++ b/examples/crack_the_code/10_alarm_complex.rb @@ -0,0 +1,143 @@ +# Alarm (full state machine) +# +# A fuller intruder alarm with: +# - five states (standby, exit delay, armed, entry delay, triggered) +# - status LEDs for each state (pins 8..12) plus a "trigger ready" +# LED on pin 13 +# - potentiometer (A5) sets the sensitivity threshold +# - LDR (pin 4) is the actual trip sensor +# - button (pin 7) cycles modes / resets +# +# Read the Serial Monitor at 9600 baud to see the current state. + +POT_PIN = 5 +LDR_PIN = 4 +BUTTON_PIN = 7 +BUZZER_PIN = 3 +STANDBY_LED_PIN = 8 +EXIT_LED_PIN = 9 +ARMED_LED_PIN = 10 +ENTRY_LED_PIN = 11 +TRIGGERED_LED_PIN = 12 +SET_LED_PIN = 13 + +pin_mode(POT_PIN, ArduinoUNO::INPUT) +pin_mode(LDR_PIN, ArduinoUNO::INPUT) +pin_mode(BUTTON_PIN, ArduinoUNO::INPUT) +pin_mode(BUZZER_PIN, ArduinoUNO::OUTPUT) +pin_mode(STANDBY_LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(EXIT_LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(ARMED_LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(ENTRY_LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(TRIGGERED_LED_PIN, ArduinoUNO::OUTPUT) +pin_mode(SET_LED_PIN, ArduinoUNO::OUTPUT) +serial_begin(9600) + +status = 0 +pot_value = 0 +ldr_value = 0 + +loop do + if status == 0 + digital_write(EXIT_LED_PIN, ArduinoUNO::LOW) + digital_write(ARMED_LED_PIN, ArduinoUNO::LOW) + digital_write(ENTRY_LED_PIN, ArduinoUNO::LOW) + digital_write(TRIGGERED_LED_PIN, ArduinoUNO::LOW) + digital_write(STANDBY_LED_PIN, ArduinoUNO::HIGH) + + pot_value = analog_read(POT_PIN) + ldr_value = analog_read(LDR_PIN) + serial_print("STANDBY Threshold level: ") + serial_print(pot_value) + serial_print(" Light level: ") + serial_println(ldr_value) + + if pot_value < (50 + (ldr_value / 2)) + digital_write(SET_LED_PIN, ArduinoUNO::HIGH) + else + digital_write(SET_LED_PIN, ArduinoUNO::LOW) + end + + button_value = digital_read(BUTTON_PIN) + if button_value == ArduinoUNO::HIGH + status = 1 + delay_ms(400) + end + end + + if status == 1 + digital_write(STANDBY_LED_PIN, ArduinoUNO::LOW) + digital_write(ARMED_LED_PIN, ArduinoUNO::LOW) + digital_write(ENTRY_LED_PIN, ArduinoUNO::LOW) + digital_write(TRIGGERED_LED_PIN, ArduinoUNO::LOW) + digital_write(SET_LED_PIN, ArduinoUNO::LOW) + digital_write(EXIT_LED_PIN, ArduinoUNO::HIGH) + serial_println("EXIT DELAY") + delay_ms(3000) + tone(BUZZER_PIN, 600, 20) + delay_ms(200) + tone(BUZZER_PIN, 600, 20) + status = 2 + end + + if status == 2 + digital_write(STANDBY_LED_PIN, ArduinoUNO::LOW) + digital_write(EXIT_LED_PIN, ArduinoUNO::LOW) + digital_write(ENTRY_LED_PIN, ArduinoUNO::LOW) + digital_write(TRIGGERED_LED_PIN, ArduinoUNO::LOW) + digital_write(SET_LED_PIN, ArduinoUNO::LOW) + digital_write(ARMED_LED_PIN, ArduinoUNO::HIGH) + + ldr_value = analog_read(LDR_PIN) + serial_print("ARMED Threshold level: ") + serial_print(pot_value) + serial_print(" Light level: ") + serial_println(ldr_value) + if pot_value < ldr_value + status = 3 + end + end + + if status == 3 + digital_write(STANDBY_LED_PIN, ArduinoUNO::LOW) + digital_write(EXIT_LED_PIN, ArduinoUNO::LOW) + digital_write(ARMED_LED_PIN, ArduinoUNO::LOW) + digital_write(TRIGGERED_LED_PIN, ArduinoUNO::LOW) + digital_write(SET_LED_PIN, ArduinoUNO::LOW) + digital_write(ENTRY_LED_PIN, ArduinoUNO::HIGH) + serial_println("ENTRY DELAY") + + count = 0 + reset_pressed = false + while count <= 300 && !reset_pressed + button_value = digital_read(BUTTON_PIN) + if button_value == ArduinoUNO::HIGH + delay_ms(200) + status = 0 + reset_pressed = true + else + delay_ms(10) + end + count += 1 + end + if !reset_pressed + status = 4 + end + end + + if status == 4 + digital_write(STANDBY_LED_PIN, ArduinoUNO::LOW) + digital_write(EXIT_LED_PIN, ArduinoUNO::LOW) + digital_write(ARMED_LED_PIN, ArduinoUNO::LOW) + digital_write(ENTRY_LED_PIN, ArduinoUNO::LOW) + digital_write(SET_LED_PIN, ArduinoUNO::LOW) + digital_write(TRIGGERED_LED_PIN, ArduinoUNO::HIGH) + serial_println("TRIGGERED") + tone(BUZZER_PIN, 600, 50) + button_value = digital_read(BUTTON_PIN) + if button_value == ArduinoUNO::HIGH + delay_ms(200) + status = 0 + end + end +end From a2771650f9a31d704e076de5bf78c0bf990e8d38 Mon Sep 17 00:00:00 2001 From: Marcus Schappi Date: Tue, 12 May 2026 08:01:15 +1000 Subject: [PATCH 33/33] test_builtin_examples: widen glob to crack_the_code + root sketches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-generated compile tests used to walk only builtin/** and hardware/**, which left the 18 Crack the Code sketches and the two loose root sketches (hello.rb, hc_sr04.rb) unverified. Expand to {builtin,hardware,crack_the_code}/** plus examples/*.rb at the root, and add a count assertion for the Crack the Code directory. Result: 49 generated compile tests → 70, all passing. --- test/test_builtin_examples.rb | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/test_builtin_examples.rb b/test/test_builtin_examples.rb index 4ad1277..796c999 100644 --- a/test/test_builtin_examples.rb +++ b/test/test_builtin_examples.rb @@ -6,7 +6,17 @@ class TestBuiltinExamples < Minitest::Test ROOT_EXAMPLES = File.expand_path("../examples", __dir__) - Dir.glob(File.join(ROOT_EXAMPLES, "{builtin,hardware}", "**", "*.rb")).sort.each do |path| + # Auto-generate a compile test for every sketch under examples/. This + # catches "sketch shipped but doesn't actually parse / link" regressions + # for the curated example library and the ThinkerShield Crack the Code + # ports. The two glob arms cover nested dirs (builtin/01_basics/...) and + # the loose root sketches (examples/hello.rb). + SKETCH_GLOBS = [ + File.join(ROOT_EXAMPLES, "{builtin,hardware,crack_the_code}", "**", "*.rb"), + File.join(ROOT_EXAMPLES, "*.rb") + ].freeze + + SKETCH_GLOBS.flat_map { |g| Dir.glob(g) }.sort.uniq.each do |path| rel = path.sub("#{File.dirname(ROOT_EXAMPLES)}/", "") test_name = "test_#{rel.gsub(%r{[/.]}, "_")}_compiles" define_method(test_name) do @@ -18,9 +28,11 @@ class TestBuiltinExamples < Minitest::Test end def test_examples_dirs_have_sketches - builtin = Dir.glob(File.join(ROOT_EXAMPLES, "builtin", "*", "*.rb")) - hardware = Dir.glob(File.join(ROOT_EXAMPLES, "hardware", "*.rb")) - assert_operator builtin.length, :>=, 35, "expected at least 35 builtin example sketches" - assert_operator hardware.length, :>=, 7, "expected at least 7 hardware example sketches" + builtin = Dir.glob(File.join(ROOT_EXAMPLES, "builtin", "*", "*.rb")) + hardware = Dir.glob(File.join(ROOT_EXAMPLES, "hardware", "*.rb")) + crack = Dir.glob(File.join(ROOT_EXAMPLES, "crack_the_code", "*.rb")) + assert_operator builtin.length, :>=, 35, "expected at least 35 builtin example sketches" + assert_operator hardware.length, :>=, 7, "expected at least 7 hardware example sketches" + assert_operator crack.length, :>=, 18, "expected at least 18 Crack the Code sketches" end end