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
-
-
+## 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?
+
## 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/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/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
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
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
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/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
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
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
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/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
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 df028e8..82c56a3 100644
--- a/lib/rubyduino/arduino_uno.rb
+++ b/lib/rubyduino/arduino_uno.rb
@@ -17,6 +17,83 @@ module ArduinoUNO
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
+
+# 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
@@ -40,6 +117,116 @@ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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)
@@ -70,6 +257,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
@@ -117,3 +312,724 @@ def interrupts
def no_interrupts
ArduinoUNO.no_interrupts
end
+
+def without_interrupts
+ ArduinoUNO.no_interrupts
+ yield
+ ArduinoUNO.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
+
+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
+
+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
+
+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
+
+def is_graph(c)
+ ArduinoUNO.is_graph(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 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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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
+
+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.
+#
+# 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/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/sp_runtime.h b/lib/rubyduino/sp_runtime.h
index 2769cab..21417d3 100644
--- a/lib/rubyduino/sp_runtime.h
+++ b/lib/rubyduino/sp_runtime.h
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -28,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)
@@ -270,6 +284,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 +302,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);
@@ -443,104 +464,1813 @@ 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;
}
-void serial_write(uint8_t value) {
+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))) {
}
- UDR0 = value;
+ while (!(UCSR0A & (uint8_t)(1 << TXC0))) {
+ }
+ /* Clear the TXC0 flag (write 1 to it). */
+ UCSR0A |= (uint8_t)(1 << TXC0);
}
-void serial_print_str(const char *value) {
- while (*value) {
- serial_write((uint8_t)*value);
- value++;
+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;
+}
+
+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;
+ }
}
}
-void serial_print_int(int value) {
- char buf[12];
- char *p = &buf[11];
- unsigned int n;
+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;
+ }
+ }
+}
- *p = '\0';
- if (value < 0) {
- serial_write((uint8_t)'-');
- n = (unsigned int)(-value);
- } else {
- n = (unsigned int)value;
+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);
+}
+
+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--;
- *p = (char)('0' + (n % 10));
- n /= 10;
- } while (n > 0);
+ 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_println_str(const char *value) {
- serial_print_str(value);
+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_int(int value) {
- serial_print_int(value);
+void serial_println_bin(uint32_t value) {
+ rd_uno_print_unsigned_base(value, 2);
serial_write((uint8_t)'\r');
serial_write((uint8_t)'\n');
}
-uint8_t shift_in(uint8_t data_pin, uint8_t clock_pin, uint8_t bit_order) {
- uint8_t value = 0;
+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--;
+ }
+ }
+}
- for (i = 0; i < 8; i++) {
- digital_write(clock_pin, 1);
- if (bit_order == 0) {
- value |= (uint8_t)(digital_read(data_pin) << i);
- } else {
- value |= (uint8_t)(digital_read(data_pin) << (7 - i));
+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');
+}
+
+#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);
+}
+
+#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);
+}
+
+#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;
}
- digital_write(clock_pin, 0);
}
+ return 1;
+}
- return value;
+static uint8_t rd_wire_status(void) {
+ return (uint8_t)(TWSR & 0xF8);
}
-void shift_out(uint8_t data_pin, uint8_t clock_pin, uint8_t bit_order, uint8_t value) {
+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;
- uint8_t bit;
- for (i = 0; i < 8; i++) {
- if (bit_order == 0) {
- bit = (uint8_t)((value >> i) & 1);
- } else {
- bit = (uint8_t)((value >> (7 - i)) & 1);
+ 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;
}
+ }
- digital_write(data_pin, bit);
- digital_write(clock_pin, 1);
- digital_write(clock_pin, 0);
+ if (stop) {
+ rd_wire_stop();
}
-}
-void interrupts(void) {
- sei();
+ rd_wire_tx_len = 0;
+ return 0;
}
-void no_interrupts(void) {
- cli();
+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++];
+}
+
+#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 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. */
+}
+
+/* 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))) {
+ }
+ UDR0 = value;
+}
+
+void serial_print_str(const char *value) {
+ while (*value) {
+ serial_write((uint8_t)*value);
+ value++;
+ }
+}
+
+void serial_print_int(int value) {
+ char buf[12];
+ char *p = &buf[11];
+ unsigned int n;
+
+ *p = '\0';
+ if (value < 0) {
+ serial_write((uint8_t)'-');
+ n = (unsigned int)(-value);
+ } else {
+ n = (unsigned int)value;
+ }
+
+ do {
+ p--;
+ *p = (char)('0' + (n % 10));
+ n /= 10;
+ } while (n > 0);
+
+ serial_print_str(p);
+}
+
+void serial_println_str(const char *value) {
+ serial_print_str(value);
+ serial_write((uint8_t)'\r');
+ serial_write((uint8_t)'\n');
+}
+
+void serial_println_int(int value) {
+ serial_print_int(value);
+ serial_write((uint8_t)'\r');
+ serial_write((uint8_t)'\n');
+}
+
+uint8_t shift_in(uint8_t data_pin, uint8_t clock_pin, uint8_t bit_order) {
+ uint8_t value = 0;
+ uint8_t i;
+
+ for (i = 0; i < 8; i++) {
+ digital_write(clock_pin, 1);
+ if (bit_order == 0) {
+ value |= (uint8_t)(digital_read(data_pin) << i);
+ } else {
+ value |= (uint8_t)(digital_read(data_pin) << (7 - i));
+ }
+ digital_write(clock_pin, 0);
+ }
+
+ return value;
+}
+
+void shift_out(uint8_t data_pin, uint8_t clock_pin, uint8_t bit_order, uint8_t value) {
+ uint8_t i;
+ uint8_t bit;
+
+ for (i = 0; i < 8; i++) {
+ if (bit_order == 0) {
+ bit = (uint8_t)((value >> i) & 1);
+ } else {
+ bit = (uint8_t)((value >> (7 - i)) & 1);
+ }
+
+ digital_write(data_pin, bit);
+ digital_write(clock_pin, 1);
+ digital_write(clock_pin, 0);
+ }
+}
+
+void interrupts(void) {
+ sei();
+}
+
+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);
+}
+
+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;
+}
+
+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;
+}
+
+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;
+ }
+ 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);
+}
+
+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();
+}
+
+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)
diff --git a/lib/rubyduino/spinel_arduino_codegen.rb b/lib/rubyduino/spinel_arduino_codegen.rb
index 940cf04..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)
@@ -53,21 +58,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
@@ -80,15 +116,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/plans/arduino_api_coverage.md b/plans/arduino_api_coverage.md
new file mode 100644
index 0000000..6b95871
--- /dev/null
+++ b/plans/arduino_api_coverage.md
@@ -0,0 +1,128 @@
+# 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`
+- [x] `analogReference` (as `analog_reference`)
+
+## 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] `pulseInLong` (as `pulse_in_long`)
+- [x] `shiftIn`
+- [x] `shiftOut`
+- [x] `tone` (with optional duration)
+- [x] `noTone` (as `no_tone`)
+
+## Interrupts
+
+- [x] `interrupts`
+- [x] `noInterrupts` (as `no_interrupts`)
+- [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
+
+- [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)
+- [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.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)
+- [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
+
+- [x] `bit` / `bitRead` / `bitWrite` / `bitSet` / `bitClear` / `highByte` / `lowByte` (snake_case)
+
+## Math / Utility Helpers
+
+- [x] `map` (as `map_value`)
+- [x] `constrain`
+- [x] `sq`
+- [x] `abs`, `min`, `max`, `pow`, `sqrt` — provided by Ruby itself, not the gem
+
+## Character Classification
+
+- [x] `isAlpha`/`isDigit`/`isAlphaNumeric`/`isSpace`/`isWhitespace`/`isUpperCase`/`isLowerCase`/`isAscii`/`isControl`/`isPrintable`/`isPunct`/`isHexadecimalDigit` (snake_case + `?` predicate variants)
+
+## Other Core
+
+- [x] `yield` (as `arduino_yield` — Ruby keyword conflict requires the prefix)
+- [ ] Arduino `String` class
+- [ ] `PROGMEM` / `pgm_read_*`
+
+## Bundled Libraries
+
+- [x] `Wire` (I²C master mode)
+- [x] `SPI`
+- [x] `EEPROM`
+- [x] `Servo` (single-servo, Timer1)
+- [ ] `SoftwareSerial`
+- [ ] `Stepper`
+- [ ] `LiquidCrystal`
+- [ ] `SD`
+
+## Constants Provided
+
+- [x] `LOW`, `HIGH`
+- [x] `INPUT`, `OUTPUT`, `INPUT_PULLUP`
+- [x] `A0`–`A5`
+- [x] `LED_BUILTIN`
+- [x] `LSBFIRST`, `MSBFIRST`
+- [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)
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.
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_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
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_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
diff --git a/test/test_builtin_examples.rb b/test/test_builtin_examples.rb
new file mode 100644
index 0000000..796c999
--- /dev/null
+++ b/test/test_builtin_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "support/compile_helper"
+
+class TestBuiltinExamples < Minitest::Test
+ ROOT_EXAMPLES = File.expand_path("../examples", __dir__)
+
+ # 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
+ 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_dirs_have_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
diff --git a/test/test_char_classification.rb b/test/test_char_classification.rb
new file mode 100644
index 0000000..2423f88
--- /dev/null
+++ b/test/test_char_classification.rb
@@ -0,0 +1,108 @@
+# 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 is_graph
+ ].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
+
+ 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_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
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_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
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
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_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
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_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_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
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
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
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
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
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
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
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_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
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
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
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
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
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