diff --git a/README.md b/README.md index 2471186..d320ac9 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,20 @@ Under the hood it uses [Spinel](https://github.com/matz/spinel), a Ruby AOT comp -## Example - -IMG_0197 +## Hello, Blink ```ruby -pin_mode(ArduinoUNO::LED_BUILTIN, ArduinoUNO::OUTPUT) +pin_mode(ArduinoUno::LED_BUILTIN, ArduinoUno::OUTPUT) loop do - digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::HIGH) - delay_ms(100) - digital_write(ArduinoUNO::LED_BUILTIN, ArduinoUNO::LOW) - delay_ms(100) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::HIGH) + sleep_ms(1000) + digital_write(ArduinoUno::LED_BUILTIN, ArduinoUno::LOW) + sleep_ms(1000) end ``` -Not bad, huh? +IMG_0197 ## Installation @@ -57,15 +55,107 @@ sudo dnf install avr-gcc avr-libc avrdude ## Usage ```bash -rubyduino examples/hello.rb +rubyduino examples/builtin/01_basics/blink.rb ``` Pass a serial port explicitly when auto-detection is not enough: ```bash -rubyduino -p /dev/cu.usbmodem11401 examples/hello.rb +rubyduino -p /dev/cu.usbmodem11401 examples/builtin/01_basics/blink.rb ``` +## Two API styles + +Rubyduino exposes the same functionality two ways. Pick whichever reads better — both compile to identical AVR code. + +**Arduino-compatible (porting `.ino` sketches)** + +```ruby +pin_mode(13, ArduinoUno::OUTPUT) +digital_write(13, ArduinoUno::HIGH) +delay_ms(500) + +serial_begin(9600) +serial_println("hello") +serial_println(value, ArduinoUno::HEX) +``` + +**Ruby-style (modules, predicates, idiomatic naming)** + +```ruby +Pin.mode(13, ArduinoUno::OUTPUT) +Pin.high(13) +sleep_ms(500) + +Serial.begin(9600) +Serial.println_str("hello") +Serial.println_hex(value) + +Eeprom.write(0, 0xAB); v = Eeprom.read(0) +Spi.begin; reply = Spi.transfer(0x9F) +Wire.begin; Wire.transmit(0x68); Wire.write(0x6B); Wire.end_transmission +Servo.attach(9); Servo.angle = 90 +``` + +Other Ruby-idiomatic helpers: `clamp`, `square`, `sleep_ms`, `sleep_us`, `srand`, `stop_tone`, predicate aliases like `alpha?`, `digit?`, `hex_digit?`, `interrupt_fired?`, `servo_attached?`, plus `without_interrupts { ... }` for critical sections. + +## What's covered + +The full Arduino UNO core API plus most of the bundled libraries — see [`plans/arduino_api_coverage.md`](plans/arduino_api_coverage.md) for the precise checklist. + +**Core API:** `pin_mode`, `digital_read/write`, `analog_read/write`, `analog_reference`, `delay_ms`, `delay_us`, `millis`, `micros`, `pulse_in`, `pulse_in_long`, `shift_in/out`, `tone`, `no_tone`, `attach_interrupt`, `detach_interrupt`, all bit/byte macros, `map_value`, `constrain`, `sq`, all 13 character classifiers (with `?` aliases), full Serial including `parse_int`, `parse_float`, `find?`, formatted print (HEX/BIN/OCT/float), `random_seed` + `rand(low..high)` codegen. + +**Bundled libraries:** EEPROM, SPI, Wire (I²C master), Servo (single-servo on Timer1). + +**Common third-party hardware**, all bit-banged in `lib/rubyduino/sp_runtime.h`: + +| Driver | Helpers | Wiring | +|----------------|-----------------------------------------------------------------|-----------------------| +| WS2812 / NeoPixel | `neopixel_begin`, `neopixel_set_pixel`, `neopixel_show`, `neopixel_clear` | data pin only | +| DHT11 / DHT22 | `dht_read?`, `dht_temperature_x10`, `dht_humidity_x10` | 1 wire + pull-up | +| 1-Wire / DS18B20 | `onewire_*`, `ds18b20_request_temperature`, `ds18b20_read_temperature_x10` | 1 wire + 4.7k pull-up | +| HD44780 LCD (4-bit) | `lcd_begin`, `lcd_print_str`, `lcd_print_int`, `lcd_set_cursor`, `lcd_clear` | RS, EN, D4–D7 | +| Stepper | `stepper_begin`, `stepper_set_speed`, `stepper_step` | 4 GPIO | +| SoftwareSerial | `soft_serial_begin`, `soft_serial_write`, `soft_serial_print_str`, `soft_serial_read` | RX + TX pins | +| IR remote (NEC) | `ir_receive?`, `ir_command` | TSOP38xx demodulator | + +## Examples + +Every Arduino built-in example for an UNO ships as a Ruby port: + +- [`examples/builtin/01_basics`](examples/builtin/01_basics) — Blink, Fade, AnalogReadSerial, etc. +- [`examples/builtin/02_digital`](examples/builtin/02_digital) — BlinkWithoutDelay, Debounce, tone melodies, … +- [`examples/builtin/03_analog`](examples/builtin/03_analog) — AnalogInOutSerial, Calibration, Smoothing, … +- [`examples/builtin/04_communication`](examples/builtin/04_communication) — ASCIITable, Dimmer, Midi, … +- [`examples/builtin/05_control`](examples/builtin/05_control) — Arrays, switchCase, while, … +- [`examples/builtin/06_sensors`](examples/builtin/06_sensors) — ADXL3xx, Knock, Memsic2125, Ping +- [`examples/builtin/07_display`](examples/builtin/07_display) — barGraph, RowColumnScanning +- [`examples/builtin/08_strings`](examples/builtin/08_strings) — character_analysis (the rest depend on the Arduino `String` class) + +Hardware-driver examples live in [`examples/hardware`](examples/hardware): + +- `neopixel_rainbow.rb` — 8-pixel WS2812 rainbow chase +- `dht22_serial.rb` — temperature/humidity over Serial +- `ds18b20_serial.rb` — DS18B20 temperature read every second +- `lcd_hello.rb` — 16x2 HD44780 hello-world +- `stepper_loop.rb` — 4-wire stepper full forward/back rotation +- `software_serial_passthrough.rb` — bridge between hardware and software UART +- `ir_remote_decode.rb` — print decoded NEC IR frames + +## Tests + +```bash +bundle exec rake test +``` + +The test suite has three layers: + +1. **Codegen tests** — exercise the Ruby → C compiler hooks. +2. **Native logic tests** — extract pure-logic helpers and compile/run them with `clang` on the host. +3. **AVR compile tests** — run the full pipeline through `avr-gcc` and inspect ELF output for ISR vectors and symbols. + +Bulk-compile coverage walks every sketch in `examples/builtin/**` and `examples/hardware/**` and verifies it links cleanly for `atmega328p`. + ## License MIT diff --git a/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