Arduino CLI build system with HAL-based test architecture
Build and upload tool (arduino-build.sh): - Compile, upload, and monitor via arduino-cli - Device discovery with USB ID identification (--devices) - Persistent reconnecting serial monitor (--watch) - Split compile/upload workflow (--verify, --upload-only) - First-time setup wizard (--setup) - Comprehensive --help with troubleshooting and RedBoard specs Testable application architecture: - Hardware abstraction layer (lib/hal/) decouples logic from Arduino API - Google Mock HAL for unit tests (exact call verification) - Simulated HAL for system tests (GPIO state, virtual clock, I2C devices) - Example I2C temperature sensor simulator (TMP102) - Host-side test suite via CMake + Google Test (21 tests) Example sketch: - blink/ -- LED blink with button-controlled speed, wired through HAL
This commit is contained in:
14
.clang-format
Normal file
14
.clang-format
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 100
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
BreakBeforeBraces: Attach
|
||||
PointerAlignment: Left
|
||||
SpaceAfterCStyleCast: false
|
||||
IncludeBlocks: Preserve
|
||||
SortIncludes: false
|
||||
---
|
||||
24
.editorconfig
Normal file
24
.editorconfig
Normal file
@@ -0,0 +1,24 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.ino]
|
||||
indent_size = 4
|
||||
|
||||
[*.{c,cpp,h,hpp}]
|
||||
indent_size = 4
|
||||
|
||||
[*.sh]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
29
.gitattributes
vendored
Normal file
29
.gitattributes
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Auto-detect text files and normalize line endings
|
||||
* text=auto
|
||||
|
||||
# Source code
|
||||
*.ino text
|
||||
*.cpp text
|
||||
*.c text
|
||||
*.h text
|
||||
*.hpp text
|
||||
*.S text
|
||||
|
||||
# Scripts
|
||||
*.sh text eol=lf
|
||||
|
||||
# Config
|
||||
*.json text
|
||||
*.yaml text
|
||||
*.yml text
|
||||
*.md text
|
||||
*.txt text
|
||||
|
||||
# Binary / generated -- do not diff
|
||||
*.hex binary
|
||||
*.elf binary
|
||||
*.bin binary
|
||||
*.eep binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.pdf binary
|
||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Build artifacts (arduino-cli output)
|
||||
/tmp/arduino-build/
|
||||
*.elf
|
||||
*.hex
|
||||
*.bin
|
||||
*.map
|
||||
*.eep
|
||||
|
||||
# Compiled object files
|
||||
*.o
|
||||
*.d
|
||||
|
||||
# Arduino IDE (if someone opens it there)
|
||||
build/
|
||||
*.ino.cpp
|
||||
|
||||
# Editor files
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/ipch/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.idea/
|
||||
*.sublime-workspace
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
36
.vscode/settings.json
vendored
Normal file
36
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.ino": "cpp"
|
||||
},
|
||||
"C_Cpp.default.compilerPath": "",
|
||||
"C_Cpp.default.defines": [
|
||||
"ARDUINO=10816",
|
||||
"ARDUINO_AVR_UNO",
|
||||
"ARDUINO_ARCH_AVR",
|
||||
"__AVR_ATmega328P__",
|
||||
"F_CPU=16000000L"
|
||||
],
|
||||
"C_Cpp.default.includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"~/.arduino15/packages/arduino/hardware/avr/*/cores/arduino",
|
||||
"~/.arduino15/packages/arduino/hardware/avr/*/variants/standard",
|
||||
"~/.arduino15/packages/arduino/hardware/avr/*/libraries/**",
|
||||
"~/Arduino/libraries/**"
|
||||
],
|
||||
"C_Cpp.errorSquiggles": "enabled",
|
||||
"C_Cpp.default.cStandard": "c11",
|
||||
"C_Cpp.default.cppStandard": "c++11",
|
||||
"editor.formatOnSave": false,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.exclude": {
|
||||
"**/*.o": true,
|
||||
"**/*.d": true,
|
||||
"**/*.elf": true,
|
||||
"**/*.hex": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/build": true,
|
||||
"/tmp/arduino-build": true
|
||||
}
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Nexus Workshops LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# SparkFun RedBoard Projects
|
||||
|
||||
Arduino sketches for SparkFun RedBoard, built and uploaded via `arduino-build.sh`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [arduino-cli](https://arduino.github.io/arduino-cli/)
|
||||
- Run `./arduino-build.sh --setup` once after installing
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
./arduino-build.sh --devices # find your board
|
||||
./arduino-build.sh --monitor ./blink # build, upload, serial console
|
||||
```
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
sparkfun/
|
||||
arduino-build.sh Build/upload/monitor tool (run --help)
|
||||
blink/
|
||||
blink.ino LED blink + serial hello world
|
||||
```
|
||||
|
||||
## Two-Terminal Workflow
|
||||
|
||||
```bash
|
||||
# Terminal 1: persistent serial monitor
|
||||
./arduino-build.sh --watch
|
||||
|
||||
# Terminal 2: edit, build, upload (monitor reconnects automatically)
|
||||
./arduino-build.sh ./blink
|
||||
```
|
||||
|
||||
## Adding a New Sketch
|
||||
|
||||
```bash
|
||||
mkdir my_sketch
|
||||
# Create my_sketch/my_sketch.ino (filename must match directory)
|
||||
./arduino-build.sh --verify ./my_sketch # compile check
|
||||
./arduino-build.sh ./my_sketch # build + upload
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Ubuntu: Board not detected (CH340 / RedBoard)
|
||||
|
||||
On Ubuntu, `brltty` (a braille display daemon) may claim the CH340 USB-serial
|
||||
interface before the `ch341` kernel driver can bind to it. The board will appear
|
||||
in `lsusb` but no `/dev/ttyUSB*` device is created.
|
||||
|
||||
Symptoms in `dmesg`:
|
||||
|
||||
```
|
||||
usb 1-3.1.3: usbfs: interface 0 claimed by ch341 while 'brltty' sets config #1
|
||||
ch341-uart ttyUSB0: ch341-uart converter now disconnected from ttyUSB0
|
||||
```
|
||||
|
||||
If you are not using a braille display, remove `brltty` entirely:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop brltty-udev
|
||||
sudo systemctl disable brltty-udev
|
||||
sudo systemctl stop brltty
|
||||
sudo systemctl disable brltty
|
||||
sudo apt remove brltty
|
||||
```
|
||||
|
||||
Alternatively, to keep `brltty` installed but block it from claiming CH340
|
||||
devices, add a udev rule:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/udev/rules.d/99-ch340-no-brltty.rules << 'EOF'
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ENV{BRLTTY_BRAILLE_DRIVER}="none"
|
||||
EOF
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
```
|
||||
999
arduino-build.sh
Executable file
999
arduino-build.sh
Executable file
@@ -0,0 +1,999 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# arduino-build.sh -- Build, upload, and monitor Arduino sketches
|
||||
# for SparkFun RedBoard (and other Uno-compatible boards)
|
||||
#
|
||||
# Run with --help for full usage information.
|
||||
# Run with --setup for first-time toolchain installation.
|
||||
# Run with --devices to find connected boards.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# -- Configuration (override via env vars if needed) -------------------------
|
||||
FQBN="${ARDUINO_FQBN:-arduino:avr:uno}"
|
||||
BAUD="${ARDUINO_BAUD:-115200}"
|
||||
BUILD_DIR="${ARDUINO_BUILD_DIR:-/tmp/arduino-build}"
|
||||
ARDUINO_CLI="${ARDUINO_CLI_BIN:-arduino-cli}"
|
||||
VERSION="1.1.0"
|
||||
|
||||
# -- Color output (disabled if stdout is not a terminal) ---------------------
|
||||
if [[ -t 1 ]]; then
|
||||
RED=$'\033[0;31m'; GRN=$'\033[0;32m'; YEL=$'\033[0;33m'
|
||||
CYN=$'\033[0;36m'; BLD=$'\033[1m'; DIM=$'\033[2m'; RST=$'\033[0m'
|
||||
else
|
||||
RED=''; GRN=''; YEL=''; CYN=''; BLD=''; DIM=''; RST=''
|
||||
fi
|
||||
|
||||
info() { echo -e "${CYN}[INFO]${RST} $*"; }
|
||||
ok() { echo -e "${GRN}[ OK ]${RST} $*"; }
|
||||
warn() { echo -e "${YEL}[WARN]${RST} $*"; }
|
||||
die() { echo -e "${RED}[ERR]${RST} $*" >&2; exit 1; }
|
||||
|
||||
# ============================================================================
|
||||
# HELP
|
||||
# ============================================================================
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}NAME${RST}
|
||||
arduino-build.sh - Build, upload, and monitor Arduino sketches
|
||||
|
||||
${BLD}VERSION${RST}
|
||||
${VERSION}
|
||||
|
||||
${BLD}SYNOPSIS${RST}
|
||||
./arduino-build.sh ${BLD}--setup${RST}
|
||||
./arduino-build.sh ${BLD}--devices${RST}
|
||||
./arduino-build.sh [OPTIONS] <sketch_dir>
|
||||
|
||||
${BLD}DESCRIPTION${RST}
|
||||
A self-contained script that replaces the Arduino IDE for command-line
|
||||
workflows. Compiles sketches with arduino-cli, uploads to the board,
|
||||
and optionally opens a serial monitor -- all in one command.
|
||||
|
||||
Designed for the SparkFun RedBoard but works with any Uno-compatible
|
||||
board. Just plug in, run --devices to find your port, and go.
|
||||
|
||||
$(_help_section_commands)
|
||||
$(_help_section_build_options)
|
||||
$(_help_section_config_options)
|
||||
$(_help_section_env_vars)
|
||||
$(_help_section_examples)
|
||||
$(_help_section_cheatsheet)
|
||||
$(_help_section_sketch_layout)
|
||||
$(_help_section_redboard)
|
||||
$(_help_section_port_permissions)
|
||||
$(_help_section_first_time)
|
||||
$(_help_section_troubleshooting)
|
||||
$(_help_section_usb_ids)
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_commands() {
|
||||
cat <<EOF
|
||||
${BLD}STANDALONE COMMANDS${RST}
|
||||
These run independently. No sketch directory needed.
|
||||
|
||||
${BLD}-h, --help${RST}
|
||||
Show this help and exit. You are reading it now.
|
||||
|
||||
${BLD}--version${RST}
|
||||
Print version string and exit.
|
||||
|
||||
${BLD}--setup${RST}
|
||||
First-time setup wizard. Installs the arduino:avr core, checks
|
||||
that your user is in the dialout group (serial port access), and
|
||||
verifies the toolchain. Run once after installing arduino-cli.
|
||||
|
||||
${BLD}--devices${RST}
|
||||
Scan for connected boards. For each /dev/ttyUSB* and /dev/ttyACM*
|
||||
port found, shows:
|
||||
- USB vendor and product strings
|
||||
- USB vendor:product ID (hex)
|
||||
- Kernel driver in use (ch341-uart, cdc_acm, etc.)
|
||||
- Best guess at what board it is
|
||||
- Whether you have write permission to the port
|
||||
Also runs 'arduino-cli board list' for cross-reference.
|
||||
|
||||
${BLD}--watch${RST}
|
||||
Open a persistent serial monitor that automatically reconnects
|
||||
when the port disappears and reappears (e.g., during upload from
|
||||
another terminal). Run in one terminal, build/upload in another.
|
||||
|
||||
Survives: upload resets, board power cycles, USB re-plugs.
|
||||
Exit with Ctrl+C.
|
||||
|
||||
Combine with -p and -b:
|
||||
./arduino-build.sh --watch -p /dev/ttyUSB0 -b 9600
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_build_options() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}BUILD & UPLOAD OPTIONS${RST}
|
||||
|
||||
${BLD}--verify${RST}
|
||||
Compile only -- do not upload. Good for syntax checking, CI
|
||||
pipelines, or when the board is not plugged in. Still reports
|
||||
binary size so you can track flash usage.
|
||||
|
||||
${BLD}--upload${RST}
|
||||
Compile and upload. This is the default when you pass a sketch
|
||||
directory, so you almost never need to type it explicitly.
|
||||
|
||||
${BLD}--upload-only${RST}
|
||||
Upload previously compiled artifacts without recompiling. Requires
|
||||
a prior --verify or build to have cached the .hex file. Useful for
|
||||
separating build and flash steps:
|
||||
|
||||
Terminal 1: ./arduino-build.sh --verify ./blink (compile)
|
||||
Terminal 2: ./arduino-build.sh --upload-only ./blink (flash)
|
||||
|
||||
Fails if no cached build exists for the sketch.
|
||||
|
||||
${BLD}--monitor${RST}
|
||||
After a successful upload, automatically open the serial monitor
|
||||
at the configured baud rate. Ctrl+C exits the monitor. Combines
|
||||
the build-upload-monitor cycle into a single command.
|
||||
|
||||
${BLD}--clean${RST}
|
||||
Delete cached build artifacts for this sketch before compiling.
|
||||
Forces a full recompile from scratch. Use when you suspect stale
|
||||
object files or after changing compiler flags.
|
||||
|
||||
${BLD}--verbose${RST}
|
||||
Show full compiler and avrdude output. Helpful for debugging
|
||||
linker errors, upload failures, or library conflicts.
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_config_options() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}CONFIGURATION OPTIONS${RST}
|
||||
|
||||
${BLD}-p, --port PORT${RST}
|
||||
Use this serial port instead of auto-detecting.
|
||||
Example: -p /dev/ttyUSB0
|
||||
|
||||
${BLD}-b, --baud RATE${RST}
|
||||
Serial monitor baud rate (default: 115200). Only affects the
|
||||
--monitor feature; upload speed is set by the bootloader.
|
||||
Common values: 9600, 19200, 38400, 57600, 115200
|
||||
|
||||
${BLD}--fqbn FQBN${RST}
|
||||
Override the Fully Qualified Board Name. Default: arduino:avr:uno
|
||||
|
||||
Common FQBNs:
|
||||
arduino:avr:uno Uno / RedBoard
|
||||
arduino:avr:mega:cpu=atmega2560 Mega 2560
|
||||
arduino:avr:nano:cpu=atmega328 Nano (old bootloader)
|
||||
arduino:avr:nano:cpu=atmega328old Nano (new bootloader)
|
||||
esp32:esp32:esp32 ESP32 DevKit
|
||||
SparkFun:avr:RedBoard SparkFun board package
|
||||
|
||||
To list all installed FQBNs:
|
||||
arduino-cli board listall
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_env_vars() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}ENVIRONMENT VARIABLES${RST}
|
||||
Override defaults without editing the script or passing flags.
|
||||
|
||||
Variable Default What it does
|
||||
-------- ------- ------------
|
||||
ARDUINO_FQBN arduino:avr:uno Board identifier
|
||||
ARDUINO_BAUD 115200 Serial monitor baud rate
|
||||
ARDUINO_BUILD_DIR /tmp/arduino-build Where .o/.elf/.hex go
|
||||
ARDUINO_CLI_BIN arduino-cli Path to arduino-cli binary
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_examples() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}EXAMPLES${RST}
|
||||
|
||||
${DIM}# --- Getting started (do these once) ---${RST}
|
||||
|
||||
./arduino-build.sh --setup
|
||||
Install the AVR toolchain and verify permissions.
|
||||
|
||||
./arduino-build.sh --devices
|
||||
See what boards are plugged in and which port to use.
|
||||
|
||||
${DIM}# --- Day-to-day usage ---${RST}
|
||||
|
||||
./arduino-build.sh ./blink
|
||||
Compile and upload blink sketch. Port auto-detected.
|
||||
|
||||
./arduino-build.sh --verify ./blink
|
||||
Compile only. Good for checking code on a laptop with no board.
|
||||
|
||||
./arduino-build.sh -p /dev/ttyUSB0 ./blink
|
||||
Upload to a specific port (skip auto-detection).
|
||||
|
||||
./arduino-build.sh --monitor ./blink
|
||||
Build, upload, and open serial monitor in one shot.
|
||||
|
||||
./arduino-build.sh --clean --verbose ./blink
|
||||
Nuke the cache and rebuild with full compiler output.
|
||||
|
||||
${DIM}# --- Two-terminal workflow (monitor + build separately) ---${RST}
|
||||
|
||||
${DIM}# Terminal 1: persistent monitor that reconnects after uploads${RST}
|
||||
./arduino-build.sh --watch
|
||||
|
||||
${DIM}# Terminal 2: build and upload (monitor in T1 reconnects)${RST}
|
||||
./arduino-build.sh ./blink
|
||||
|
||||
${DIM}# --- Split compile and upload (CI / unit test workflow) ---${RST}
|
||||
|
||||
./arduino-build.sh --verify ./blink
|
||||
Compile and cache artifacts. No board needed.
|
||||
|
||||
./arduino-build.sh --upload-only ./blink
|
||||
Flash cached artifacts. No recompile.
|
||||
|
||||
${DIM}# --- Different boards ---${RST}
|
||||
|
||||
./arduino-build.sh --fqbn arduino:avr:mega:cpu=atmega2560 ./my_sketch
|
||||
Upload to an Arduino Mega instead of a RedBoard.
|
||||
|
||||
ARDUINO_FQBN=esp32:esp32:esp32 ./arduino-build.sh ./my_esp_sketch
|
||||
Upload to an ESP32 board via environment variable.
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_cheatsheet() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}QUICK REFERENCE (tape this to your monitor)${RST}
|
||||
|
||||
Command What it does
|
||||
------- ------------
|
||||
--help You are here
|
||||
--setup One-time toolchain install
|
||||
--devices What's plugged in?
|
||||
--verify ./sketch Does it compile?
|
||||
./sketch Build + upload (auto port)
|
||||
--upload-only ./sketch Flash cached build (no compile)
|
||||
-p /dev/ttyUSB0 ./sketch Build + upload (manual port)
|
||||
--monitor ./sketch Build + upload + serial console
|
||||
--watch Persistent monitor (reconnects)
|
||||
--clean ./sketch Full rebuild (no cache)
|
||||
--verbose ./sketch Show all compiler output
|
||||
|
||||
${BLD}Two-terminal workflow:${RST}
|
||||
Terminal 1: ./arduino-build.sh --watch
|
||||
Terminal 2: ./arduino-build.sh ./blink (watch reconnects)
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_sketch_layout() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}SKETCH DIRECTORY LAYOUT${RST}
|
||||
Arduino requires the .ino filename to match the directory name:
|
||||
|
||||
my_sketch/ <-- pass this directory to the script
|
||||
my_sketch.ino <-- main sketch file (name must match dir)
|
||||
config.h <-- optional extra files
|
||||
utils.cpp <-- optional extra files
|
||||
|
||||
Quick way to create a new sketch:
|
||||
|
||||
mkdir blink
|
||||
cat > blink/blink.ino << 'SKETCH'
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
}
|
||||
void loop() {
|
||||
digitalWrite(LED_BUILTIN, HIGH);
|
||||
Serial.println("ON");
|
||||
delay(500);
|
||||
digitalWrite(LED_BUILTIN, LOW);
|
||||
Serial.println("OFF");
|
||||
delay(500);
|
||||
}
|
||||
SKETCH
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_redboard() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}SPARKFUN REDBOARD SPECS${RST}
|
||||
|
||||
MCU: ATmega328P at 16 MHz
|
||||
Flash: 32 KB total (31.5 KB usable; 512 B bootloader)
|
||||
SRAM: 2 KB (runtime variables and stack)
|
||||
EEPROM: 1 KB (persistent storage)
|
||||
Digital I/O: 14 pins (6 with PWM on pins 3, 5, 6, 9, 10, 11)
|
||||
Analog inputs: 6 pins (A0-A5, 10-bit ADC)
|
||||
USB chip: CH340G (shows up as /dev/ttyUSB*)
|
||||
Voltage: 5V logic, 7-15V barrel jack input
|
||||
FQBN: arduino:avr:uno (same as Arduino Uno)
|
||||
|
||||
The RedBoard is electrically identical to an Arduino Uno. The only
|
||||
real difference is the CH340 USB-to-serial chip (Uno uses ATmega16U2).
|
||||
The CH340 shows up as /dev/ttyUSB* while the 16U2 shows as /dev/ttyACM*.
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_port_permissions() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}SERIAL PORT PERMISSIONS${RST}
|
||||
If you get "Permission denied" on /dev/ttyUSB0 or /dev/ttyACM0:
|
||||
|
||||
sudo usermod -aG dialout \$USER
|
||||
|
||||
Then ${BLD}log out and back in${RST} (or reboot). Verify with:
|
||||
|
||||
groups | grep dialout
|
||||
|
||||
Alternatively, for a quick one-off test:
|
||||
|
||||
sudo chmod 666 /dev/ttyUSB0 (resets on reboot/replug)
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_first_time() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}FIRST-TIME INSTALL (step by step)${RST}
|
||||
|
||||
1. Install arduino-cli:
|
||||
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
||||
sudo mv bin/arduino-cli /usr/local/bin/
|
||||
|
||||
2. Run first-time setup:
|
||||
./arduino-build.sh --setup
|
||||
|
||||
3. Plug in your RedBoard and verify detection:
|
||||
./arduino-build.sh --devices
|
||||
|
||||
4. Create a test sketch:
|
||||
mkdir blink && cat > blink/blink.ino << 'SKETCH'
|
||||
void setup() { pinMode(LED_BUILTIN, OUTPUT); }
|
||||
void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(500);
|
||||
digitalWrite(LED_BUILTIN, LOW); delay(500); }
|
||||
SKETCH
|
||||
|
||||
5. Build and upload:
|
||||
./arduino-build.sh ./blink
|
||||
|
||||
6. Verify the LED on the board is blinking. Done!
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_troubleshooting() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}TROUBLESHOOTING${RST}
|
||||
|
||||
${BLD}Board not detected (--devices shows nothing)${RST}
|
||||
- Try a different USB cable. Many cables are charge-only with no
|
||||
data lines. If your phone does not offer file transfer with the
|
||||
cable, it is probably charge-only.
|
||||
- Check the USB bus directly:
|
||||
lsusb | grep -i -E 'ch34|arduino|1a86|2341'
|
||||
- Check kernel log for attach/detach events:
|
||||
dmesg | tail -20
|
||||
- Try a different USB port (front panel ports can be flaky).
|
||||
|
||||
${BLD}Upload fails: "avrdude: stk500_getsync(): not in sync"${RST}
|
||||
- Most common cause: wrong port. Run --devices to double-check.
|
||||
- Close any other program using the port (serial monitors, other
|
||||
IDE instances, screen sessions, etc.)
|
||||
- Try pressing the board reset button just before upload starts.
|
||||
- Bad or loose USB cable.
|
||||
|
||||
${BLD}"Sketch uses N bytes ... maximum is 32256 bytes"${RST}
|
||||
- The ATmega328P has 32,256 bytes for program storage.
|
||||
- Use --verify to check size without uploading.
|
||||
- Tips to reduce size: use F() macro for string literals,
|
||||
avoid the String class (use char arrays), remove unused libraries.
|
||||
- If you genuinely need more space, use a Mega 2560 (256 KB).
|
||||
|
||||
${BLD}Compilation errors about missing libraries${RST}
|
||||
- Search: arduino-cli lib search "Servo"
|
||||
- Install: arduino-cli lib install "Servo"
|
||||
- List: arduino-cli lib list
|
||||
|
||||
${BLD}Serial monitor shows garbage characters${RST}
|
||||
- Baud rate mismatch. Make sure -b/--baud matches the
|
||||
Serial.begin() value in your sketch. Default here is 115200.
|
||||
|
||||
${BLD}CH340 driver not loading (Linux)${RST}
|
||||
- Most kernels since 4.x include ch341 out of the box.
|
||||
- Verify: lsmod | grep ch341
|
||||
- If missing: sudo modprobe ch341
|
||||
- Still gone: sudo apt install linux-modules-extra-\$(uname -r)
|
||||
EOF
|
||||
}
|
||||
|
||||
_help_section_usb_ids() {
|
||||
cat <<EOF
|
||||
|
||||
${BLD}KNOWN USB VENDOR:PRODUCT IDS${RST}
|
||||
Used by --devices to identify boards.
|
||||
|
||||
ID Board / Chip
|
||||
---- ------------
|
||||
1a86:7523 CH340 (SparkFun RedBoard, most Arduino clones)
|
||||
1a86:55d4 CH340C (RedBoard Qwiic, newer clones)
|
||||
2341:0043 Arduino Uno R3 (ATmega16U2)
|
||||
2341:0001 Arduino Uno (older revision)
|
||||
2341:0010 Arduino Mega 2560
|
||||
2341:0042 Arduino Mega 2560 R3
|
||||
2341:003d Arduino Due (Programming Port)
|
||||
2341:003e Arduino Due (Native USB)
|
||||
2341:8036 Arduino Leonardo
|
||||
2341:8037 Arduino Micro
|
||||
0403:6001 FTDI FT232R (older RedBoard, FTDI clones)
|
||||
0403:6015 FTDI FT231X (SparkFun FTDI breakout)
|
||||
10c4:ea60 CP2102 (NodeMCU, some ESP32 boards)
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# DEVICE SCANNER
|
||||
# ============================================================================
|
||||
scan_devices() {
|
||||
echo ""
|
||||
echo -e "${BLD}=== Connected Serial Devices ===${RST}"
|
||||
echo ""
|
||||
|
||||
local found=0
|
||||
|
||||
for port in /dev/ttyUSB* /dev/ttyACM*; do
|
||||
[[ -e "$port" ]] || continue
|
||||
found=1
|
||||
|
||||
echo -e " ${BLD}${GRN}$port${RST}"
|
||||
|
||||
local sysfs_path="" usb_dev_path=""
|
||||
|
||||
# Find the kernel driver for this tty
|
||||
local driver=""
|
||||
local tty_device="/sys/class/tty/$(basename "$port")/device/driver"
|
||||
if [[ -L "$tty_device" ]]; then
|
||||
driver="$(basename "$(readlink -f "$tty_device")")"
|
||||
fi
|
||||
|
||||
# Walk up from the tty device node to find the USB device node.
|
||||
# The tty sits under a USB interface node; idVendor/idProduct
|
||||
# live on the USB device node which is one or more levels above.
|
||||
if [[ -e "/sys/class/tty/$(basename "$port")/device" ]]; then
|
||||
sysfs_path="$(readlink -f "/sys/class/tty/$(basename "$port")/device")"
|
||||
usb_dev_path="$sysfs_path"
|
||||
# Walk up until we find idVendor (max 5 levels to avoid runaway)
|
||||
local walk=0
|
||||
while [[ $walk -lt 5 ]] && [[ "$usb_dev_path" != "/" ]]; do
|
||||
if [[ -f "$usb_dev_path/idVendor" ]]; then
|
||||
break
|
||||
fi
|
||||
usb_dev_path="$(dirname "$usb_dev_path")"
|
||||
walk=$((walk + 1))
|
||||
done
|
||||
# If we didn't find idVendor, clear the path
|
||||
if [[ ! -f "$usb_dev_path/idVendor" ]]; then
|
||||
usb_dev_path=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$usb_dev_path" ]]; then
|
||||
local vendor="" product="" vid="" pid="" serial=""
|
||||
|
||||
[[ -f "$usb_dev_path/manufacturer" ]] && vendor="$(cat "$usb_dev_path/manufacturer" 2>/dev/null)"
|
||||
[[ -f "$usb_dev_path/product" ]] && product="$(cat "$usb_dev_path/product" 2>/dev/null)"
|
||||
[[ -f "$usb_dev_path/idVendor" ]] && vid="$(cat "$usb_dev_path/idVendor" 2>/dev/null)"
|
||||
[[ -f "$usb_dev_path/idProduct" ]] && pid="$(cat "$usb_dev_path/idProduct" 2>/dev/null)"
|
||||
[[ -f "$usb_dev_path/serial" ]] && serial="$(cat "$usb_dev_path/serial" 2>/dev/null)"
|
||||
|
||||
[[ -n "$vendor" ]] && echo " Manufacturer: $vendor"
|
||||
[[ -n "$product" ]] && echo " Product: $product"
|
||||
[[ -n "$vid" ]] && [[ -n "$pid" ]] && echo " USB ID: ${vid}:${pid}"
|
||||
[[ -n "$serial" ]] && echo " Serial: $serial"
|
||||
[[ -n "$driver" ]] && echo " Driver: $driver"
|
||||
|
||||
echo -n " Likely board: "
|
||||
case "${vid}:${pid}" in
|
||||
1a86:7523) echo "SparkFun RedBoard / CH340 clone" ;;
|
||||
1a86:55d4) echo "SparkFun RedBoard Qwiic / CH340C" ;;
|
||||
2341:0043) echo "Arduino Uno R3 (ATmega16U2)" ;;
|
||||
2341:0001) echo "Arduino Uno (older revision)" ;;
|
||||
2341:0010) echo "Arduino Mega 2560" ;;
|
||||
2341:0042) echo "Arduino Mega 2560 R3" ;;
|
||||
2341:003d) echo "Arduino Due (Programming Port)" ;;
|
||||
2341:003e) echo "Arduino Due (Native USB)" ;;
|
||||
2341:8036) echo "Arduino Leonardo" ;;
|
||||
2341:8037) echo "Arduino Micro" ;;
|
||||
0403:6001) echo "FTDI FT232R (older RedBoard / clone)" ;;
|
||||
0403:6015) echo "FTDI FT231X (SparkFun breakout)" ;;
|
||||
10c4:ea60) echo "CP2102 (NodeMCU / some ESP32 boards)" ;;
|
||||
1a86:*) echo "CH340 variant (likely Arduino clone)" ;;
|
||||
2341:*) echo "Arduino (unknown model -- USB ID ${vid}:${pid})" ;;
|
||||
*) echo "Unknown device -- USB ID ${vid}:${pid}" ;;
|
||||
esac
|
||||
else
|
||||
# Could not find USB device node, but we may still have the driver
|
||||
[[ -n "$driver" ]] && echo " Driver: $driver"
|
||||
echo " USB info: Could not read (sysfs walk failed)"
|
||||
fi
|
||||
|
||||
if [[ -w "$port" ]]; then
|
||||
echo -e " Permissions: ${GRN}OK (writable)${RST}"
|
||||
else
|
||||
echo -e " Permissions: ${RED}NOT writable${RST} -- run: sudo usermod -aG dialout \$USER"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
if [[ $found -eq 0 ]]; then
|
||||
echo -e " ${YEL}No serial devices found.${RST}"
|
||||
echo ""
|
||||
echo " Checklist:"
|
||||
echo " 1. Is the board plugged in via USB?"
|
||||
echo " 2. Is the USB cable a data cable (not charge-only)?"
|
||||
echo " 3. Check kernel log: dmesg | tail -20"
|
||||
echo " 4. Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341|0403'"
|
||||
echo " 5. Try a different USB port or cable"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if command -v "$ARDUINO_CLI" &>/dev/null; then
|
||||
echo -e "${BLD}=== arduino-cli Board Detection ===${RST}"
|
||||
echo ""
|
||||
$ARDUINO_CLI board list 2>/dev/null || warn "arduino-cli board list failed (is the core installed?)"
|
||||
echo ""
|
||||
else
|
||||
warn "arduino-cli not found -- run --setup first"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# FIRST-TIME SETUP
|
||||
# ============================================================================
|
||||
run_setup() {
|
||||
echo ""
|
||||
echo -e "${BLD}=== Arduino CLI First-Time Setup ===${RST}"
|
||||
echo ""
|
||||
|
||||
if ! command -v "$ARDUINO_CLI" &>/dev/null; then
|
||||
die "arduino-cli not found in PATH.
|
||||
|
||||
Install it:
|
||||
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
||||
sudo mv bin/arduino-cli /usr/local/bin/
|
||||
|
||||
Or via package manager:
|
||||
sudo apt install arduino-cli (Debian/Ubuntu)
|
||||
brew install arduino-cli (macOS)
|
||||
yay -S arduino-cli (Arch)
|
||||
|
||||
Then re-run: ./arduino-build.sh --setup"
|
||||
fi
|
||||
ok "arduino-cli found: $($ARDUINO_CLI version 2>/dev/null | head -1)"
|
||||
|
||||
info "Updating board index..."
|
||||
$ARDUINO_CLI core update-index
|
||||
ok "Board index updated."
|
||||
|
||||
if $ARDUINO_CLI core list 2>/dev/null | grep -q "arduino:avr"; then
|
||||
ok "arduino:avr core already installed."
|
||||
else
|
||||
info "Installing arduino:avr core (this may take a minute)..."
|
||||
$ARDUINO_CLI core install arduino:avr
|
||||
ok "arduino:avr core installed."
|
||||
fi
|
||||
|
||||
if command -v avr-size &>/dev/null; then
|
||||
ok "avr-size found (binary size reporting enabled)."
|
||||
else
|
||||
warn "avr-size not found. Install for binary size details:"
|
||||
warn " sudo apt install gcc-avr"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if groups 2>/dev/null | grep -q dialout; then
|
||||
ok "User '$(whoami)' is in the 'dialout' group."
|
||||
else
|
||||
warn "User '$(whoami)' is NOT in the 'dialout' group."
|
||||
warn "Fix with: sudo usermod -aG dialout \$USER"
|
||||
warn "Then log out and back in (or reboot)."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
scan_devices
|
||||
|
||||
echo -e "${BLD}Setup complete!${RST}"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Plug in your RedBoard"
|
||||
echo " 2. ./arduino-build.sh --devices"
|
||||
echo " 3. ./arduino-build.sh ./your_sketch"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# AUTO-DETECT SERIAL PORT
|
||||
# ============================================================================
|
||||
auto_detect_port() {
|
||||
local candidates=()
|
||||
for p in /dev/ttyUSB* /dev/ttyACM*; do
|
||||
[[ -e "$p" ]] && candidates+=("$p")
|
||||
done
|
||||
|
||||
if [[ ${#candidates[@]} -eq 0 ]]; then
|
||||
die "No serial ports found. Is the board plugged in?
|
||||
Run: ./arduino-build.sh --devices"
|
||||
fi
|
||||
|
||||
if [[ ${#candidates[@]} -eq 1 ]]; then
|
||||
echo "${candidates[0]}"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "Multiple serial ports detected:"
|
||||
for p in "${candidates[@]}"; do
|
||||
warn " $p"
|
||||
done
|
||||
|
||||
# Prefer /dev/ttyUSB* (CH340 on RedBoard) over /dev/ttyACM*
|
||||
for p in "${candidates[@]}"; do
|
||||
if [[ "$p" == /dev/ttyUSB* ]]; then
|
||||
warn "Auto-selected $p (CH340 -- likely RedBoard)"
|
||||
warn "Use -p to override if this is wrong."
|
||||
echo "$p"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
warn "Auto-selected ${candidates[0]}. Use -p to override."
|
||||
echo "${candidates[0]}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PERSISTENT SERIAL MONITOR (--watch)
|
||||
# ============================================================================
|
||||
run_watch_monitor() {
|
||||
local port="$1"
|
||||
local baud="$2"
|
||||
|
||||
# If no port specified, auto-detect once to get a starting point
|
||||
if [[ -z "$port" ]]; then
|
||||
# Don't die if no port yet -- we'll wait for it
|
||||
for p in /dev/ttyUSB* /dev/ttyACM*; do
|
||||
if [[ -e "$p" ]]; then
|
||||
port="$p"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "$port" ]]; then
|
||||
# Default to the most common RedBoard port
|
||||
port="/dev/ttyUSB0"
|
||||
info "No port detected yet. Waiting for $port to appear..."
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Persistent monitor on ${BLD}$port${RST} at ${baud} baud"
|
||||
info "Reconnects automatically after upload / reset / replug."
|
||||
info "Press Ctrl+C to exit."
|
||||
echo ""
|
||||
|
||||
trap 'echo ""; info "Monitor stopped."; exit 0' INT TERM
|
||||
|
||||
while true; do
|
||||
# Wait for port to exist
|
||||
if [[ ! -e "$port" ]]; then
|
||||
echo -e "${DIM}--- Waiting for $port ...${RST}"
|
||||
while [[ ! -e "$port" ]]; do
|
||||
sleep 0.5
|
||||
done
|
||||
# Brief settle time after port appears (CH340 needs a moment)
|
||||
sleep 1.0
|
||||
echo -e "${GRN}--- $port connected ---${RST}"
|
||||
fi
|
||||
|
||||
# Open the monitor (will exit when port disappears)
|
||||
$ARDUINO_CLI monitor -p "$port" -c baudrate="$baud" 2>/dev/null || true
|
||||
|
||||
# If we get here, the monitor exited (port vanished during upload,
|
||||
# board reset, USB replug, etc.). Brief pause then retry.
|
||||
echo -e "${YEL}--- $port disconnected ---${RST}"
|
||||
sleep 0.5
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# ARGUMENT PARSING
|
||||
# ============================================================================
|
||||
SKETCH_DIR=""
|
||||
PORT=""
|
||||
VERIFY_ONLY=0
|
||||
UPLOAD_ONLY=0
|
||||
DO_MONITOR=0
|
||||
DO_CLEAN=0
|
||||
DO_VERBOSE=0
|
||||
ACTION=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
--version)
|
||||
echo "arduino-build.sh v${VERSION}"
|
||||
exit 0
|
||||
;;
|
||||
--setup)
|
||||
ACTION="setup"
|
||||
shift
|
||||
;;
|
||||
--devices)
|
||||
ACTION="devices"
|
||||
shift
|
||||
;;
|
||||
--watch)
|
||||
ACTION="watch"
|
||||
shift
|
||||
;;
|
||||
--verify)
|
||||
VERIFY_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--upload)
|
||||
shift # default behavior, accepted for explicitness
|
||||
;;
|
||||
--upload-only)
|
||||
UPLOAD_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--monitor)
|
||||
DO_MONITOR=1
|
||||
shift
|
||||
;;
|
||||
--clean)
|
||||
DO_CLEAN=1
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
DO_VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
-p|--port)
|
||||
[[ -n "${2:-}" ]] || die "--port requires a value (e.g., -p /dev/ttyUSB0)"
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--baud)
|
||||
[[ -n "${2:-}" ]] || die "--baud requires a value (e.g., -b 115200)"
|
||||
BAUD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--fqbn)
|
||||
[[ -n "${2:-}" ]] || die "--fqbn requires a value (e.g., --fqbn arduino:avr:uno)"
|
||||
FQBN="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
die "Unknown option: $1
|
||||
Run with --help for usage."
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$SKETCH_DIR" ]]; then
|
||||
SKETCH_DIR="$1"
|
||||
else
|
||||
die "Unexpected argument: $1
|
||||
Sketch directory already set to: $SKETCH_DIR"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ============================================================================
|
||||
# DISPATCH STANDALONE COMMANDS
|
||||
# ============================================================================
|
||||
case "${ACTION:-}" in
|
||||
setup) run_setup; exit 0 ;;
|
||||
devices) scan_devices; exit 0 ;;
|
||||
watch) run_watch_monitor "$PORT" "$BAUD" ;;
|
||||
esac
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATE SKETCH
|
||||
# ============================================================================
|
||||
if [[ $UPLOAD_ONLY -eq 1 ]] && [[ $VERIFY_ONLY -eq 1 ]]; then
|
||||
die "--upload-only and --verify are contradictory.
|
||||
--verify means compile only, --upload-only means upload only."
|
||||
fi
|
||||
|
||||
if [[ $UPLOAD_ONLY -eq 1 ]] && [[ $DO_CLEAN -eq 1 ]]; then
|
||||
die "--upload-only and --clean are contradictory.
|
||||
--clean would delete the cached build that --upload-only needs."
|
||||
fi
|
||||
|
||||
if [[ -z "$SKETCH_DIR" ]]; then
|
||||
echo ""
|
||||
echo -e "${RED}Error: No sketch directory specified.${RST}"
|
||||
echo ""
|
||||
echo " Usage: ./arduino-build.sh [OPTIONS] <sketch_dir>"
|
||||
echo ""
|
||||
echo " Quick start:"
|
||||
echo " --help Full help with examples and troubleshooting"
|
||||
echo " --setup First-time toolchain installation"
|
||||
echo " --devices Find connected boards"
|
||||
echo " ./my_sketch Build and upload a sketch"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SKETCH_DIR="$(realpath "$SKETCH_DIR")"
|
||||
[[ -d "$SKETCH_DIR" ]] || die "Not a directory: $SKETCH_DIR"
|
||||
|
||||
SKETCH_NAME="$(basename "$SKETCH_DIR")"
|
||||
INO_FILE="$SKETCH_DIR/$SKETCH_NAME.ino"
|
||||
|
||||
if [[ ! -f "$INO_FILE" ]]; then
|
||||
ALT_INO="$(find "$SKETCH_DIR" -maxdepth 1 -name '*.ino' | head -1)"
|
||||
if [[ -n "$ALT_INO" ]]; then
|
||||
warn "Expected ${SKETCH_NAME}.ino but found $(basename "$ALT_INO")"
|
||||
warn "Arduino convention: .ino filename must match directory name."
|
||||
INO_FILE="$ALT_INO"
|
||||
SKETCH_NAME="$(basename "$INO_FILE" .ino)"
|
||||
else
|
||||
die "No .ino file found in $SKETCH_DIR
|
||||
Expected: $SKETCH_DIR/$SKETCH_NAME.ino
|
||||
Create one: touch $SKETCH_DIR/$SKETCH_NAME.ino"
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Sketch: ${BLD}$SKETCH_NAME${RST}"
|
||||
info "Board: $FQBN"
|
||||
|
||||
# ============================================================================
|
||||
# PRE-FLIGHT CHECKS
|
||||
# ============================================================================
|
||||
command -v "$ARDUINO_CLI" &>/dev/null \
|
||||
|| die "arduino-cli not found. Run: ./arduino-build.sh --setup"
|
||||
|
||||
if ! $ARDUINO_CLI core list 2>/dev/null | grep -q "arduino:avr"; then
|
||||
die "arduino:avr core not installed. Run: ./arduino-build.sh --setup"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# CLEAN (if requested)
|
||||
# ============================================================================
|
||||
SKETCH_BUILD_DIR="$BUILD_DIR/$SKETCH_NAME"
|
||||
|
||||
if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$SKETCH_BUILD_DIR" ]]; then
|
||||
info "Cleaning build cache..."
|
||||
rm -rf "$SKETCH_BUILD_DIR"
|
||||
ok "Cache cleared."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# COMPILE
|
||||
# ============================================================================
|
||||
# ============================================================================
|
||||
# COMPILE (skipped with --upload-only)
|
||||
# ============================================================================
|
||||
if [[ $UPLOAD_ONLY -eq 1 ]]; then
|
||||
# Verify cached build artifacts exist
|
||||
if [[ ! -d "$SKETCH_BUILD_DIR" ]]; then
|
||||
die "No cached build found for '$SKETCH_NAME'.
|
||||
Run a compile first: ./arduino-build.sh --verify ./$SKETCH_NAME
|
||||
Build cache expected at: $SKETCH_BUILD_DIR"
|
||||
fi
|
||||
|
||||
HEX_FILE="$SKETCH_BUILD_DIR/$SKETCH_NAME.ino.hex"
|
||||
if [[ ! -f "$HEX_FILE" ]]; then
|
||||
die "Build cache exists but no .hex file found.
|
||||
Try a clean rebuild: ./arduino-build.sh --clean ./$SKETCH_NAME"
|
||||
fi
|
||||
|
||||
ok "Using cached build."
|
||||
info "Hex: $HEX_FILE"
|
||||
|
||||
# Report binary size if possible
|
||||
ELF_FILE="$SKETCH_BUILD_DIR/$SKETCH_NAME.ino.elf"
|
||||
if command -v avr-size &>/dev/null && [[ -f "$ELF_FILE" ]]; then
|
||||
echo ""
|
||||
avr-size --mcu=atmega328p -C "$ELF_FILE" 2>/dev/null || avr-size "$ELF_FILE"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
info "Compiling..."
|
||||
mkdir -p "$SKETCH_BUILD_DIR"
|
||||
|
||||
COMPILE_ARGS=(
|
||||
--fqbn "$FQBN"
|
||||
--build-path "$SKETCH_BUILD_DIR"
|
||||
--warnings all
|
||||
)
|
||||
[[ $DO_VERBOSE -eq 1 ]] && COMPILE_ARGS+=(--verbose)
|
||||
|
||||
if ! $ARDUINO_CLI compile "${COMPILE_ARGS[@]}" "$SKETCH_DIR"; then
|
||||
die "Compilation failed."
|
||||
fi
|
||||
|
||||
ok "Compile succeeded."
|
||||
|
||||
# Report binary size
|
||||
ELF_FILE="$SKETCH_BUILD_DIR/$SKETCH_NAME.ino.elf"
|
||||
if command -v avr-size &>/dev/null && [[ -f "$ELF_FILE" ]]; then
|
||||
echo ""
|
||||
avr-size --mcu=atmega328p -C "$ELF_FILE" 2>/dev/null || avr-size "$ELF_FILE"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# VERIFY-ONLY EXIT
|
||||
# ============================================================================
|
||||
if [[ $VERIFY_ONLY -eq 1 ]]; then
|
||||
ok "Verify-only mode. Done."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# UPLOAD
|
||||
# ============================================================================
|
||||
if [[ -z "$PORT" ]]; then
|
||||
PORT="$(auto_detect_port)"
|
||||
fi
|
||||
|
||||
info "Uploading to ${BLD}$PORT${RST} ..."
|
||||
|
||||
if [[ ! -e "$PORT" ]]; then
|
||||
die "Port $PORT does not exist. Run --devices to check."
|
||||
fi
|
||||
|
||||
if [[ ! -w "$PORT" ]]; then
|
||||
warn "No write access to $PORT"
|
||||
warn "Fix: sudo usermod -aG dialout \$USER (then log out/in)"
|
||||
fi
|
||||
|
||||
UPLOAD_ARGS=(
|
||||
--fqbn "$FQBN"
|
||||
--port "$PORT"
|
||||
--input-dir "$SKETCH_BUILD_DIR"
|
||||
)
|
||||
[[ $DO_VERBOSE -eq 1 ]] && UPLOAD_ARGS+=(--verbose)
|
||||
|
||||
if ! $ARDUINO_CLI upload "${UPLOAD_ARGS[@]}"; then
|
||||
die "Upload failed. Run with --verbose for details.
|
||||
Also try: ./arduino-build.sh --devices"
|
||||
fi
|
||||
|
||||
ok "Upload complete!"
|
||||
|
||||
# ============================================================================
|
||||
# SERIAL MONITOR
|
||||
# ============================================================================
|
||||
if [[ $DO_MONITOR -eq 1 ]]; then
|
||||
echo ""
|
||||
info "Opening serial monitor on $PORT at ${BAUD} baud..."
|
||||
info "Press Ctrl+C to exit."
|
||||
echo ""
|
||||
$ARDUINO_CLI monitor -p "$PORT" -c baudrate="$BAUD"
|
||||
else
|
||||
echo ""
|
||||
info "To open serial monitor:"
|
||||
echo " ./arduino-build.sh --monitor -p $PORT ./your_sketch"
|
||||
echo " arduino-cli monitor -p $PORT -c baudrate=$BAUD"
|
||||
echo " screen $PORT $BAUD"
|
||||
fi
|
||||
32
blink/blink.ino
Normal file
32
blink/blink.ino
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* blink.ino -- LED blink with button-controlled speed
|
||||
*
|
||||
* This .ino file is just the entry point. All logic lives in
|
||||
* BlinkApp (lib/app/blink_app.h) which depends on the HAL
|
||||
* interface (lib/hal/hal.h), making it testable on the host.
|
||||
*
|
||||
* The build script auto-discovers lib/ subdirectories and passes
|
||||
* them as --library flags to arduino-cli, so angle-bracket includes
|
||||
* work for project-local libraries.
|
||||
*
|
||||
* Wiring:
|
||||
* Pin 13 (LED_BUILTIN) -- onboard LED (no wiring needed)
|
||||
* Pin 2 -- momentary button to GND (uses INPUT_PULLUP)
|
||||
*
|
||||
* Serial: 115200 baud
|
||||
* Prints "FAST" or "SLOW" on button press.
|
||||
*/
|
||||
|
||||
#include <hal_arduino.h>
|
||||
#include <blink_app.h>
|
||||
|
||||
static ArduinoHal hw;
|
||||
static BlinkApp app(&hw);
|
||||
|
||||
void setup() {
|
||||
app.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
app.update();
|
||||
}
|
||||
88
lib/app/blink_app.h
Normal file
88
lib/app/blink_app.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef BLINK_APP_H
|
||||
#define BLINK_APP_H
|
||||
|
||||
#include <hal.h>
|
||||
|
||||
/*
|
||||
* BlinkApp -- Testable blink logic, decoupled from hardware.
|
||||
*
|
||||
* Blinks an LED and reads a button. When the button is pressed,
|
||||
* the blink rate doubles (toggles between normal and fast mode).
|
||||
*
|
||||
* All hardware access goes through the injected Hal pointer. This
|
||||
* class has no dependency on Arduino.h and compiles on any host.
|
||||
*/
|
||||
class BlinkApp {
|
||||
public:
|
||||
static constexpr uint8_t DEFAULT_LED_PIN = LED_BUILTIN; // pin 13
|
||||
static constexpr uint8_t DEFAULT_BUTTON_PIN = 2;
|
||||
static constexpr unsigned long SLOW_INTERVAL_MS = 500;
|
||||
static constexpr unsigned long FAST_INTERVAL_MS = 125;
|
||||
|
||||
BlinkApp(Hal* hal,
|
||||
uint8_t led_pin = DEFAULT_LED_PIN,
|
||||
uint8_t button_pin = DEFAULT_BUTTON_PIN)
|
||||
: hal_(hal)
|
||||
, led_pin_(led_pin)
|
||||
, button_pin_(button_pin)
|
||||
, led_state_(LOW)
|
||||
, fast_mode_(false)
|
||||
, last_toggle_ms_(0)
|
||||
, last_button_state_(HIGH) // pulled up, so HIGH = not pressed
|
||||
{}
|
||||
|
||||
// Call once from setup()
|
||||
void begin() {
|
||||
hal_->pinMode(led_pin_, OUTPUT);
|
||||
hal_->pinMode(button_pin_, INPUT_PULLUP);
|
||||
hal_->serialBegin(115200);
|
||||
hal_->serialPrintln("BlinkApp started");
|
||||
last_toggle_ms_ = hal_->millis();
|
||||
}
|
||||
|
||||
// Call repeatedly from loop()
|
||||
void update() {
|
||||
handleButton();
|
||||
handleBlink();
|
||||
}
|
||||
|
||||
// -- Accessors for testing ----------------------------------------------
|
||||
bool ledState() const { return led_state_ == HIGH; }
|
||||
bool fastMode() const { return fast_mode_; }
|
||||
unsigned long interval() const {
|
||||
return fast_mode_ ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS;
|
||||
}
|
||||
|
||||
private:
|
||||
void handleButton() {
|
||||
uint8_t reading = hal_->digitalRead(button_pin_);
|
||||
|
||||
// Detect falling edge (HIGH -> LOW = button press with INPUT_PULLUP)
|
||||
if (last_button_state_ == HIGH && reading == LOW) {
|
||||
fast_mode_ = !fast_mode_;
|
||||
hal_->serialPrintln(fast_mode_ ? "FAST" : "SLOW");
|
||||
}
|
||||
last_button_state_ = reading;
|
||||
}
|
||||
|
||||
void handleBlink() {
|
||||
unsigned long now = hal_->millis();
|
||||
unsigned long target = fast_mode_ ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS;
|
||||
|
||||
if (now - last_toggle_ms_ >= target) {
|
||||
led_state_ = (led_state_ == HIGH) ? LOW : HIGH;
|
||||
hal_->digitalWrite(led_pin_, led_state_);
|
||||
last_toggle_ms_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
Hal* hal_;
|
||||
uint8_t led_pin_;
|
||||
uint8_t button_pin_;
|
||||
uint8_t led_state_;
|
||||
bool fast_mode_;
|
||||
unsigned long last_toggle_ms_;
|
||||
uint8_t last_button_state_;
|
||||
};
|
||||
|
||||
#endif // BLINK_APP_H
|
||||
67
lib/hal/hal.h
Normal file
67
lib/hal/hal.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef HAL_H
|
||||
#define HAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Pin modes (match Arduino constants)
|
||||
#ifndef INPUT
|
||||
#define INPUT 0x0
|
||||
#define OUTPUT 0x1
|
||||
#define INPUT_PULLUP 0x2
|
||||
#endif
|
||||
|
||||
#ifndef LOW
|
||||
#define LOW 0x0
|
||||
#define HIGH 0x1
|
||||
#endif
|
||||
|
||||
// LED_BUILTIN for host builds
|
||||
#ifndef LED_BUILTIN
|
||||
#define LED_BUILTIN 13
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Hardware Abstraction Layer
|
||||
*
|
||||
* Abstract interface over GPIO, timing, serial, and I2C. Sketch logic
|
||||
* depends on this interface only -- never on Arduino.h directly.
|
||||
*
|
||||
* Two implementations:
|
||||
* hal_arduino.h -- real hardware (included by .ino files)
|
||||
* mock_hal.h -- Google Mock (included by test files)
|
||||
*/
|
||||
class Hal {
|
||||
public:
|
||||
virtual ~Hal() = default;
|
||||
|
||||
// -- GPIO ---------------------------------------------------------------
|
||||
virtual void pinMode(uint8_t pin, uint8_t mode) = 0;
|
||||
virtual void digitalWrite(uint8_t pin, uint8_t value) = 0;
|
||||
virtual uint8_t digitalRead(uint8_t pin) = 0;
|
||||
virtual int analogRead(uint8_t pin) = 0;
|
||||
virtual void analogWrite(uint8_t pin, int value) = 0;
|
||||
|
||||
// -- Timing -------------------------------------------------------------
|
||||
virtual unsigned long millis() = 0;
|
||||
virtual unsigned long micros() = 0;
|
||||
virtual void delay(unsigned long ms) = 0;
|
||||
virtual void delayMicroseconds(unsigned long us) = 0;
|
||||
|
||||
// -- Serial -------------------------------------------------------------
|
||||
virtual void serialBegin(unsigned long baud) = 0;
|
||||
virtual void serialPrint(const char* msg) = 0;
|
||||
virtual void serialPrintln(const char* msg) = 0;
|
||||
virtual int serialAvailable() = 0;
|
||||
virtual int serialRead() = 0;
|
||||
|
||||
// -- I2C ----------------------------------------------------------------
|
||||
virtual void i2cBegin() = 0;
|
||||
virtual void i2cBeginTransmission(uint8_t addr) = 0;
|
||||
virtual size_t i2cWrite(uint8_t data) = 0;
|
||||
virtual uint8_t i2cEndTransmission() = 0;
|
||||
virtual uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) = 0;
|
||||
virtual int i2cAvailable() = 0;
|
||||
virtual int i2cRead() = 0;
|
||||
};
|
||||
|
||||
#endif // HAL_H
|
||||
93
lib/hal/hal_arduino.h
Normal file
93
lib/hal/hal_arduino.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef HAL_ARDUINO_H
|
||||
#define HAL_ARDUINO_H
|
||||
|
||||
/*
|
||||
* Real hardware implementation of the HAL.
|
||||
*
|
||||
* This file includes Arduino.h and Wire.h, so it can only be compiled
|
||||
* by avr-gcc (via arduino-cli). It is included by .ino files only.
|
||||
*
|
||||
* Every method is a trivial passthrough to the real Arduino function.
|
||||
* The point is not to add logic here -- it is to keep Arduino.h out
|
||||
* of your application code so that code can compile on the host.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <hal.h>
|
||||
|
||||
class ArduinoHal : public Hal {
|
||||
public:
|
||||
// -- GPIO ---------------------------------------------------------------
|
||||
void pinMode(uint8_t pin, uint8_t mode) override {
|
||||
::pinMode(pin, mode);
|
||||
}
|
||||
void digitalWrite(uint8_t pin, uint8_t value) override {
|
||||
::digitalWrite(pin, value);
|
||||
}
|
||||
uint8_t digitalRead(uint8_t pin) override {
|
||||
return ::digitalRead(pin);
|
||||
}
|
||||
int analogRead(uint8_t pin) override {
|
||||
return ::analogRead(pin);
|
||||
}
|
||||
void analogWrite(uint8_t pin, int value) override {
|
||||
::analogWrite(pin, value);
|
||||
}
|
||||
|
||||
// -- Timing -------------------------------------------------------------
|
||||
unsigned long millis() override {
|
||||
return ::millis();
|
||||
}
|
||||
unsigned long micros() override {
|
||||
return ::micros();
|
||||
}
|
||||
void delay(unsigned long ms) override {
|
||||
::delay(ms);
|
||||
}
|
||||
void delayMicroseconds(unsigned long us) override {
|
||||
::delayMicroseconds(us);
|
||||
}
|
||||
|
||||
// -- Serial -------------------------------------------------------------
|
||||
void serialBegin(unsigned long baud) override {
|
||||
Serial.begin(baud);
|
||||
}
|
||||
void serialPrint(const char* msg) override {
|
||||
Serial.print(msg);
|
||||
}
|
||||
void serialPrintln(const char* msg) override {
|
||||
Serial.println(msg);
|
||||
}
|
||||
int serialAvailable() override {
|
||||
return Serial.available();
|
||||
}
|
||||
int serialRead() override {
|
||||
return Serial.read();
|
||||
}
|
||||
|
||||
// -- I2C ----------------------------------------------------------------
|
||||
void i2cBegin() override {
|
||||
Wire.begin();
|
||||
}
|
||||
void i2cBeginTransmission(uint8_t addr) override {
|
||||
Wire.beginTransmission(addr);
|
||||
}
|
||||
size_t i2cWrite(uint8_t data) override {
|
||||
return Wire.write(data);
|
||||
}
|
||||
uint8_t i2cEndTransmission() override {
|
||||
return Wire.endTransmission();
|
||||
}
|
||||
uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) override {
|
||||
return Wire.requestFrom(addr, count);
|
||||
}
|
||||
int i2cAvailable() override {
|
||||
return Wire.available();
|
||||
}
|
||||
int i2cRead() override {
|
||||
return Wire.read();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAL_ARDUINO_H
|
||||
70
test/CMakeLists.txt
Normal file
70
test/CMakeLists.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(sparkfun_tests LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Google Test (fetched automatically)
|
||||
# --------------------------------------------------------------------------
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG v1.14.0
|
||||
)
|
||||
# Prevent gtest from overriding compiler/linker options on Windows
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
enable_testing()
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Include paths -- same headers used by Arduino, but compiled on host
|
||||
# --------------------------------------------------------------------------
|
||||
set(LIB_DIR ${CMAKE_SOURCE_DIR}/../lib)
|
||||
|
||||
include_directories(
|
||||
${LIB_DIR}/hal # hal.h
|
||||
${LIB_DIR}/app # blink_app.h
|
||||
${CMAKE_SOURCE_DIR}/mocks # mock_hal.h, sim_hal.h
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Unit tests (Google Mock)
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_blink_unit
|
||||
test_blink_unit.cpp
|
||||
)
|
||||
target_link_libraries(test_blink_unit
|
||||
GTest::gtest_main
|
||||
GTest::gmock
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# System tests (SimHal)
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_blink_system
|
||||
test_blink_system.cpp
|
||||
)
|
||||
target_link_libraries(test_blink_system
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# I2C example tests
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_i2c_example
|
||||
test_i2c_example.cpp
|
||||
)
|
||||
target_link_libraries(test_i2c_example
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Register with CTest
|
||||
# --------------------------------------------------------------------------
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(test_blink_unit)
|
||||
gtest_discover_tests(test_blink_system)
|
||||
gtest_discover_tests(test_i2c_example)
|
||||
45
test/mocks/mock_hal.h
Normal file
45
test/mocks/mock_hal.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef MOCK_HAL_H
|
||||
#define MOCK_HAL_H
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include "hal.h"
|
||||
|
||||
/*
|
||||
* StrictMock-friendly HAL mock for unit tests.
|
||||
*
|
||||
* Use this when you want to verify exact call sequences:
|
||||
* EXPECT_CALL(mock, digitalWrite(13, HIGH)).Times(1);
|
||||
*/
|
||||
class MockHal : public Hal {
|
||||
public:
|
||||
// GPIO
|
||||
MOCK_METHOD(void, pinMode, (uint8_t pin, uint8_t mode), (override));
|
||||
MOCK_METHOD(void, digitalWrite, (uint8_t pin, uint8_t value), (override));
|
||||
MOCK_METHOD(uint8_t, digitalRead, (uint8_t pin), (override));
|
||||
MOCK_METHOD(int, analogRead, (uint8_t pin), (override));
|
||||
MOCK_METHOD(void, analogWrite, (uint8_t pin, int value), (override));
|
||||
|
||||
// Timing
|
||||
MOCK_METHOD(unsigned long, millis, (), (override));
|
||||
MOCK_METHOD(unsigned long, micros, (), (override));
|
||||
MOCK_METHOD(void, delay, (unsigned long ms), (override));
|
||||
MOCK_METHOD(void, delayMicroseconds, (unsigned long us), (override));
|
||||
|
||||
// Serial
|
||||
MOCK_METHOD(void, serialBegin, (unsigned long baud), (override));
|
||||
MOCK_METHOD(void, serialPrint, (const char* msg), (override));
|
||||
MOCK_METHOD(void, serialPrintln, (const char* msg), (override));
|
||||
MOCK_METHOD(int, serialAvailable, (), (override));
|
||||
MOCK_METHOD(int, serialRead, (), (override));
|
||||
|
||||
// I2C
|
||||
MOCK_METHOD(void, i2cBegin, (), (override));
|
||||
MOCK_METHOD(void, i2cBeginTransmission, (uint8_t addr), (override));
|
||||
MOCK_METHOD(size_t, i2cWrite, (uint8_t data), (override));
|
||||
MOCK_METHOD(uint8_t, i2cEndTransmission, (), (override));
|
||||
MOCK_METHOD(uint8_t, i2cRequestFrom, (uint8_t addr, uint8_t count), (override));
|
||||
MOCK_METHOD(int, i2cAvailable, (), (override));
|
||||
MOCK_METHOD(int, i2cRead, (), (override));
|
||||
};
|
||||
|
||||
#endif // MOCK_HAL_H
|
||||
256
test/mocks/sim_hal.h
Normal file
256
test/mocks/sim_hal.h
Normal file
@@ -0,0 +1,256 @@
|
||||
#ifndef SIM_HAL_H
|
||||
#define SIM_HAL_H
|
||||
|
||||
#include "hal.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/*
|
||||
* Simulated HAL for system tests.
|
||||
*
|
||||
* Unlike MockHal (which verifies call expectations), SimHal actually
|
||||
* maintains state: pin values, a virtual clock, serial output capture,
|
||||
* and pluggable I2C device simulators.
|
||||
*
|
||||
* This lets you write system tests that exercise full application logic
|
||||
* against simulated hardware:
|
||||
*
|
||||
* SimHal sim;
|
||||
* BlinkApp app(&sim);
|
||||
* app.begin();
|
||||
*
|
||||
* sim.setPin(2, LOW); // simulate button press
|
||||
* sim.advanceMillis(600); // advance clock
|
||||
* app.update();
|
||||
*
|
||||
* EXPECT_EQ(sim.getPin(13), HIGH); // check LED state
|
||||
*/
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// I2C device simulator interface
|
||||
// --------------------------------------------------------------------
|
||||
class I2cDeviceSim {
|
||||
public:
|
||||
virtual ~I2cDeviceSim() = default;
|
||||
|
||||
// Called when master writes bytes to this device
|
||||
virtual void onReceive(const uint8_t* data, size_t len) = 0;
|
||||
|
||||
// Called when master requests bytes; fill response buffer
|
||||
virtual size_t onRequest(uint8_t* buf, size_t max_len) = 0;
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Simulated HAL
|
||||
// --------------------------------------------------------------------
|
||||
class SimHal : public Hal {
|
||||
public:
|
||||
static const int NUM_PINS = 20; // D0-D13 + A0-A5
|
||||
|
||||
SimHal() : clock_ms_(0), clock_us_(0) {
|
||||
memset(pin_modes_, 0, sizeof(pin_modes_));
|
||||
memset(pin_values_, 0, sizeof(pin_values_));
|
||||
}
|
||||
|
||||
// -- GPIO ---------------------------------------------------------------
|
||||
void pinMode(uint8_t pin, uint8_t mode) override {
|
||||
if (pin < NUM_PINS) {
|
||||
pin_modes_[pin] = mode;
|
||||
// INPUT_PULLUP defaults to HIGH
|
||||
if (mode == INPUT_PULLUP) {
|
||||
pin_values_[pin] = HIGH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void digitalWrite(uint8_t pin, uint8_t value) override {
|
||||
if (pin < NUM_PINS) {
|
||||
pin_values_[pin] = value;
|
||||
gpio_log_.push_back({clock_ms_, pin, value});
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t digitalRead(uint8_t pin) override {
|
||||
if (pin < NUM_PINS) return pin_values_[pin];
|
||||
return LOW;
|
||||
}
|
||||
|
||||
int analogRead(uint8_t pin) override {
|
||||
if (pin < NUM_PINS) return analog_values_[pin];
|
||||
return 0;
|
||||
}
|
||||
|
||||
void analogWrite(uint8_t pin, int value) override {
|
||||
if (pin < NUM_PINS) pin_values_[pin] = (value > 0) ? HIGH : LOW;
|
||||
}
|
||||
|
||||
// -- Timing -------------------------------------------------------------
|
||||
unsigned long millis() override { return clock_ms_; }
|
||||
unsigned long micros() override { return clock_us_; }
|
||||
void delay(unsigned long ms) override { advanceMillis(ms); }
|
||||
void delayMicroseconds(unsigned long us) override { clock_us_ += us; }
|
||||
|
||||
// -- Serial -------------------------------------------------------------
|
||||
void serialBegin(unsigned long baud) override { (void)baud; }
|
||||
void serialPrint(const char* msg) override {
|
||||
serial_output_ += msg;
|
||||
}
|
||||
void serialPrintln(const char* msg) override {
|
||||
serial_output_ += msg;
|
||||
serial_output_ += "\n";
|
||||
}
|
||||
int serialAvailable() override {
|
||||
return static_cast<int>(serial_input_.size());
|
||||
}
|
||||
int serialRead() override {
|
||||
if (serial_input_.empty()) return -1;
|
||||
int c = serial_input_.front();
|
||||
serial_input_.erase(serial_input_.begin());
|
||||
return c;
|
||||
}
|
||||
|
||||
// -- I2C ----------------------------------------------------------------
|
||||
void i2cBegin() override {}
|
||||
|
||||
void i2cBeginTransmission(uint8_t addr) override {
|
||||
i2c_addr_ = addr;
|
||||
i2c_tx_buf_.clear();
|
||||
}
|
||||
|
||||
size_t i2cWrite(uint8_t data) override {
|
||||
i2c_tx_buf_.push_back(data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t i2cEndTransmission() override {
|
||||
auto it = i2c_devices_.find(i2c_addr_);
|
||||
if (it == i2c_devices_.end()) return 2; // NACK on address
|
||||
it->second->onReceive(i2c_tx_buf_.data(), i2c_tx_buf_.size());
|
||||
return 0; // success
|
||||
}
|
||||
|
||||
uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) override {
|
||||
i2c_rx_buf_.clear();
|
||||
auto it = i2c_devices_.find(addr);
|
||||
if (it == i2c_devices_.end()) return 0;
|
||||
uint8_t tmp[256];
|
||||
size_t n = it->second->onRequest(tmp, count);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
i2c_rx_buf_.push_back(tmp[i]);
|
||||
}
|
||||
return static_cast<uint8_t>(n);
|
||||
}
|
||||
|
||||
int i2cAvailable() override {
|
||||
return static_cast<int>(i2c_rx_buf_.size());
|
||||
}
|
||||
|
||||
int i2cRead() override {
|
||||
if (i2c_rx_buf_.empty()) return -1;
|
||||
int val = i2c_rx_buf_.front();
|
||||
i2c_rx_buf_.erase(i2c_rx_buf_.begin());
|
||||
return val;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test control API (not part of Hal interface)
|
||||
// ====================================================================
|
||||
|
||||
// -- Clock control ------------------------------------------------------
|
||||
void advanceMillis(unsigned long ms) {
|
||||
clock_ms_ += ms;
|
||||
clock_us_ += ms * 1000;
|
||||
}
|
||||
|
||||
void setMillis(unsigned long ms) {
|
||||
clock_ms_ = ms;
|
||||
clock_us_ = ms * 1000;
|
||||
}
|
||||
|
||||
// -- GPIO control -------------------------------------------------------
|
||||
void setPin(uint8_t pin, uint8_t value) {
|
||||
if (pin < NUM_PINS) pin_values_[pin] = value;
|
||||
}
|
||||
|
||||
uint8_t getPin(uint8_t pin) const {
|
||||
if (pin < NUM_PINS) return pin_values_[pin];
|
||||
return LOW;
|
||||
}
|
||||
|
||||
uint8_t getPinMode(uint8_t pin) const {
|
||||
if (pin < NUM_PINS) return pin_modes_[pin];
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setAnalog(uint8_t pin, int value) {
|
||||
analog_values_[pin] = value;
|
||||
}
|
||||
|
||||
// -- GPIO history -------------------------------------------------------
|
||||
struct GpioEvent {
|
||||
unsigned long timestamp_ms;
|
||||
uint8_t pin;
|
||||
uint8_t value;
|
||||
};
|
||||
|
||||
const std::vector<GpioEvent>& gpioLog() const { return gpio_log_; }
|
||||
|
||||
void clearGpioLog() { gpio_log_.clear(); }
|
||||
|
||||
// Count how many times a pin was set to a specific value
|
||||
int countWrites(uint8_t pin, uint8_t value) const {
|
||||
int count = 0;
|
||||
for (const auto& e : gpio_log_) {
|
||||
if (e.pin == pin && e.value == value) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// -- Serial control -----------------------------------------------------
|
||||
const std::string& serialOutput() const { return serial_output_; }
|
||||
void clearSerialOutput() { serial_output_.clear(); }
|
||||
|
||||
void injectSerialInput(const std::string& data) {
|
||||
for (char c : data) {
|
||||
serial_input_.push_back(static_cast<uint8_t>(c));
|
||||
}
|
||||
}
|
||||
|
||||
// -- I2C device registration --------------------------------------------
|
||||
void attachI2cDevice(uint8_t addr, I2cDeviceSim* device) {
|
||||
i2c_devices_[addr] = device;
|
||||
}
|
||||
|
||||
void detachI2cDevice(uint8_t addr) {
|
||||
i2c_devices_.erase(addr);
|
||||
}
|
||||
|
||||
private:
|
||||
// GPIO
|
||||
uint8_t pin_modes_[NUM_PINS];
|
||||
uint8_t pin_values_[NUM_PINS];
|
||||
std::map<uint8_t, int> analog_values_;
|
||||
std::vector<GpioEvent> gpio_log_;
|
||||
|
||||
// Timing
|
||||
unsigned long clock_ms_;
|
||||
unsigned long clock_us_;
|
||||
|
||||
// Serial
|
||||
std::string serial_output_;
|
||||
std::vector<uint8_t> serial_input_;
|
||||
|
||||
// I2C
|
||||
uint8_t i2c_addr_ = 0;
|
||||
std::vector<uint8_t> i2c_tx_buf_;
|
||||
std::vector<uint8_t> i2c_rx_buf_;
|
||||
std::map<uint8_t, I2cDeviceSim*> i2c_devices_;
|
||||
};
|
||||
|
||||
#endif // SIM_HAL_H
|
||||
84
test/run_tests.sh
Executable file
84
test/run_tests.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# run_tests.sh -- Build and run host-side unit and system tests
|
||||
#
|
||||
# Usage:
|
||||
# ./test/run_tests.sh Build and run all tests
|
||||
# ./test/run_tests.sh --clean Clean rebuild
|
||||
# ./test/run_tests.sh --verbose Verbose test output
|
||||
# ./test/run_tests.sh --filter X Run only tests matching X
|
||||
#
|
||||
# Prerequisites:
|
||||
# cmake >= 3.14, g++ or clang++, git (for fetching gtest)
|
||||
#
|
||||
# First run will download Google Test (~30 seconds).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
|
||||
# Color output
|
||||
if [[ -t 1 ]]; then
|
||||
RED=$'\033[0;31m'; GRN=$'\033[0;32m'; CYN=$'\033[0;36m'
|
||||
BLD=$'\033[1m'; RST=$'\033[0m'
|
||||
else
|
||||
RED=''; GRN=''; CYN=''; BLD=''; RST=''
|
||||
fi
|
||||
|
||||
info() { echo -e "${CYN}[TEST]${RST} $*"; }
|
||||
ok() { echo -e "${GRN}[PASS]${RST} $*"; }
|
||||
die() { echo -e "${RED}[FAIL]${RST} $*" >&2; exit 1; }
|
||||
|
||||
# Parse args
|
||||
DO_CLEAN=0
|
||||
VERBOSE=""
|
||||
FILTER=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--clean) DO_CLEAN=1 ;;
|
||||
--verbose) VERBOSE="--verbose" ;;
|
||||
--filter) : ;; # next arg is the pattern
|
||||
-*) die "Unknown option: $arg" ;;
|
||||
*) FILTER="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check prerequisites
|
||||
command -v cmake &>/dev/null || die "cmake not found. Install: sudo apt install cmake"
|
||||
command -v g++ &>/dev/null || command -v clang++ &>/dev/null || die "No C++ compiler found"
|
||||
command -v git &>/dev/null || die "git not found (needed to fetch Google Test)"
|
||||
|
||||
# Clean if requested
|
||||
if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$BUILD_DIR" ]]; then
|
||||
info "Cleaning build directory..."
|
||||
rm -rf "$BUILD_DIR"
|
||||
fi
|
||||
|
||||
# Configure
|
||||
if [[ ! -f "$BUILD_DIR/CMakeCache.txt" ]]; then
|
||||
info "Configuring (first run will fetch Google Test)..."
|
||||
cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug
|
||||
fi
|
||||
|
||||
# Build
|
||||
info "Building tests..."
|
||||
cmake --build "$BUILD_DIR" --parallel
|
||||
|
||||
# Run
|
||||
echo ""
|
||||
info "${BLD}Running tests...${RST}"
|
||||
echo ""
|
||||
|
||||
CTEST_ARGS=("--test-dir" "$BUILD_DIR" "--output-on-failure")
|
||||
[[ -n "$VERBOSE" ]] && CTEST_ARGS+=("--verbose")
|
||||
[[ -n "$FILTER" ]] && CTEST_ARGS+=("-R" "$FILTER")
|
||||
|
||||
if ctest "${CTEST_ARGS[@]}"; then
|
||||
echo ""
|
||||
ok "${BLD}All tests passed.${RST}"
|
||||
else
|
||||
echo ""
|
||||
die "Some tests failed."
|
||||
fi
|
||||
201
test/test_blink_system.cpp
Normal file
201
test/test_blink_system.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "sim_hal.h"
|
||||
#include "blink_app.h"
|
||||
|
||||
// ============================================================================
|
||||
// System Tests -- exercise full app behavior against simulated hardware
|
||||
// ============================================================================
|
||||
|
||||
class BlinkAppSystemTest : public ::testing::Test {
|
||||
protected:
|
||||
SimHal sim_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Basic blink behavior
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, LedBlinksAtSlowRate) {
|
||||
BlinkApp app(&sim_);
|
||||
app.begin();
|
||||
|
||||
// Run for 2 seconds, calling update every 10ms (like a real loop)
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
// At 500ms intervals over 2000ms, we expect ~4 toggles
|
||||
// (0->500: toggle, 500->1000: toggle, 1000->1500: toggle, 1500->2000: toggle)
|
||||
int highs = sim_.countWrites(LED_BUILTIN, HIGH);
|
||||
int lows = sim_.countWrites(LED_BUILTIN, LOW);
|
||||
EXPECT_GE(highs, 2);
|
||||
EXPECT_GE(lows, 2);
|
||||
EXPECT_LE(highs + lows, 6); // should not over-toggle
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, LedBlinksAtFastRateAfterButtonPress) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
// Button starts HIGH (INPUT_PULLUP)
|
||||
EXPECT_EQ(sim_.getPin(2), HIGH);
|
||||
|
||||
// Run 1 second in slow mode
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
int slow_toggles = sim_.gpioLog().size();
|
||||
|
||||
// Press button
|
||||
sim_.setPin(2, LOW);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Release button
|
||||
sim_.setPin(2, HIGH);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
|
||||
// Clear log and run another second in fast mode
|
||||
sim_.clearGpioLog();
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
int fast_toggles = sim_.gpioLog().size();
|
||||
|
||||
// Fast mode (125ms) should produce roughly 4x more toggles than slow (500ms)
|
||||
EXPECT_GT(fast_toggles, slow_toggles * 2);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Button edge detection
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, ButtonDebounceOnlyTriggersOnFallingEdge) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
// Rapid button noise: HIGH-LOW-HIGH-LOW-HIGH
|
||||
uint8_t sequence[] = {HIGH, LOW, HIGH, LOW, HIGH};
|
||||
int mode_changes = 0;
|
||||
bool was_fast = false;
|
||||
|
||||
for (uint8_t val : sequence) {
|
||||
sim_.setPin(2, val);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
if (app.fastMode() != was_fast) {
|
||||
mode_changes++;
|
||||
was_fast = app.fastMode();
|
||||
}
|
||||
}
|
||||
|
||||
// Each HIGH->LOW transition is a falling edge, so 2 edges
|
||||
// (positions 0->1 and 2->3), toggling fast->slow->fast or slow->fast->slow
|
||||
EXPECT_EQ(mode_changes, 2);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, ButtonHeldDoesNotRepeatToggle) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
// Press and hold for 50 update cycles
|
||||
sim_.setPin(2, LOW);
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Should have toggled exactly once (to fast mode)
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Release and hold released for 50 cycles
|
||||
sim_.setPin(2, HIGH);
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Still fast -- release alone should not toggle
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Serial output verification
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, PrintsStartupMessage) {
|
||||
BlinkApp app(&sim_);
|
||||
app.begin();
|
||||
|
||||
EXPECT_NE(sim_.serialOutput().find("BlinkApp started"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, PrintsModeChangeMessages) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
sim_.clearSerialOutput();
|
||||
|
||||
// Press button
|
||||
sim_.setPin(2, LOW);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
|
||||
EXPECT_NE(sim_.serialOutput().find("FAST"), std::string::npos);
|
||||
|
||||
sim_.setPin(2, HIGH);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
sim_.clearSerialOutput();
|
||||
|
||||
// Press again
|
||||
sim_.setPin(2, LOW);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
|
||||
EXPECT_NE(sim_.serialOutput().find("SLOW"), std::string::npos);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GPIO log / timing verification
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, LedToggleTimingIsCorrect) {
|
||||
BlinkApp app(&sim_);
|
||||
app.begin();
|
||||
|
||||
// Run for 3 seconds at 1ms resolution
|
||||
for (int i = 0; i < 3000; ++i) {
|
||||
sim_.advanceMillis(1);
|
||||
app.update();
|
||||
}
|
||||
|
||||
const auto& log = sim_.gpioLog();
|
||||
ASSERT_GE(log.size(), 4u);
|
||||
|
||||
// Check intervals between consecutive toggles on pin 13
|
||||
for (size_t i = 1; i < log.size(); ++i) {
|
||||
if (log[i].pin == LED_BUILTIN && log[i - 1].pin == LED_BUILTIN) {
|
||||
unsigned long delta = log[i].timestamp_ms - log[i - 1].timestamp_ms;
|
||||
// Should be approximately 500ms (+/- 10ms for loop granularity)
|
||||
EXPECT_NEAR(delta, 500, 10)
|
||||
<< "Toggle " << i << " at t=" << log[i].timestamp_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, PinModesAreConfiguredCorrectly) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
EXPECT_EQ(sim_.getPinMode(LED_BUILTIN), OUTPUT);
|
||||
EXPECT_EQ(sim_.getPinMode(2), INPUT_PULLUP);
|
||||
}
|
||||
132
test/test_blink_unit.cpp
Normal file
132
test/test_blink_unit.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "mock_hal.h"
|
||||
#include "blink_app.h"
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::AnyNumber;
|
||||
using ::testing::Return;
|
||||
using ::testing::InSequence;
|
||||
using ::testing::HasSubstr;
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests -- verify exact HAL interactions
|
||||
// ============================================================================
|
||||
|
||||
class BlinkAppUnitTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Allow timing and serial calls by default so tests can focus
|
||||
// on the specific interactions they care about.
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(_)).WillByDefault(Return(HIGH));
|
||||
EXPECT_CALL(mock_, serialBegin(_)).Times(AnyNumber());
|
||||
EXPECT_CALL(mock_, serialPrintln(_)).Times(AnyNumber());
|
||||
EXPECT_CALL(mock_, millis()).Times(AnyNumber());
|
||||
}
|
||||
|
||||
::testing::NiceMock<MockHal> mock_;
|
||||
};
|
||||
|
||||
TEST_F(BlinkAppUnitTest, BeginConfiguresPins) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
EXPECT_CALL(mock_, pinMode(13, OUTPUT)).Times(1);
|
||||
EXPECT_CALL(mock_, pinMode(2, INPUT_PULLUP)).Times(1);
|
||||
EXPECT_CALL(mock_, serialBegin(115200)).Times(1);
|
||||
|
||||
app.begin();
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, StartsInSlowMode) {
|
||||
BlinkApp app(&mock_);
|
||||
app.begin();
|
||||
|
||||
EXPECT_FALSE(app.fastMode());
|
||||
EXPECT_EQ(app.interval(), BlinkApp::SLOW_INTERVAL_MS);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, TogglesLedAfterInterval) {
|
||||
BlinkApp app(&mock_);
|
||||
|
||||
// begin() at t=0
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
app.begin();
|
||||
|
||||
// update() at t=500 should toggle LED
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(500));
|
||||
EXPECT_CALL(mock_, digitalWrite(13, _)).Times(1);
|
||||
app.update();
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, DoesNotToggleBeforeInterval) {
|
||||
BlinkApp app(&mock_);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
app.begin();
|
||||
|
||||
// update() at t=499 -- not enough time, no toggle
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(499));
|
||||
EXPECT_CALL(mock_, digitalWrite(_, _)).Times(0);
|
||||
app.update();
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, ButtonPressSwitchesToFastMode) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.begin();
|
||||
|
||||
// Simulate button press (falling edge: HIGH -> LOW)
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
EXPECT_CALL(mock_, serialPrintln(HasSubstr("FAST"))).Times(1);
|
||||
app.update();
|
||||
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
EXPECT_EQ(app.interval(), BlinkApp::FAST_INTERVAL_MS);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, SecondButtonPressReturnsToSlowMode) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.begin();
|
||||
|
||||
// First press: fast mode
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Release
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.update();
|
||||
|
||||
// Second press: back to slow
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
EXPECT_CALL(mock_, serialPrintln(HasSubstr("SLOW"))).Times(1);
|
||||
app.update();
|
||||
|
||||
EXPECT_FALSE(app.fastMode());
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, HoldingButtonDoesNotRepeatToggle) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.begin();
|
||||
|
||||
// Press and hold
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Still held -- should NOT toggle again
|
||||
app.update();
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode()); // still fast, not toggled back
|
||||
}
|
||||
140
test/test_i2c_example.cpp
Normal file
140
test/test_i2c_example.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
// ============================================================================
|
||||
// Example: I2C Temperature Sensor Simulator
|
||||
//
|
||||
// Demonstrates how to mock an I2C device. This simulates a simple
|
||||
// temperature sensor at address 0x48 (like a TMP102 or LM75).
|
||||
//
|
||||
// Protocol (simplified):
|
||||
// Write register pointer (1 byte), then read 2 bytes back.
|
||||
// Register 0x00 = temperature (MSB:LSB, 12-bit, 0.0625 deg/LSB)
|
||||
// Register 0x01 = config register
|
||||
// ============================================================================
|
||||
|
||||
class TempSensorSim : public I2cDeviceSim {
|
||||
public:
|
||||
static const uint8_t ADDR = 0x48;
|
||||
static const uint8_t REG_TEMP = 0x00;
|
||||
static const uint8_t REG_CONFIG = 0x01;
|
||||
|
||||
TempSensorSim() : reg_pointer_(0), temp_raw_(0) {}
|
||||
|
||||
// Set temperature in degrees C (converted to 12-bit raw)
|
||||
void setTemperature(float deg_c) {
|
||||
temp_raw_ = static_cast<int16_t>(deg_c / 0.0625f);
|
||||
}
|
||||
|
||||
// I2cDeviceSim interface
|
||||
void onReceive(const uint8_t* data, size_t len) override {
|
||||
if (len >= 1) {
|
||||
reg_pointer_ = data[0];
|
||||
}
|
||||
}
|
||||
|
||||
size_t onRequest(uint8_t* buf, size_t max_len) override {
|
||||
if (max_len < 2) return 0;
|
||||
|
||||
if (reg_pointer_ == REG_TEMP) {
|
||||
// 12-bit left-aligned in 16 bits
|
||||
int16_t raw = temp_raw_ << 4;
|
||||
buf[0] = static_cast<uint8_t>((raw >> 8) & 0xFF);
|
||||
buf[1] = static_cast<uint8_t>(raw & 0xFF);
|
||||
return 2;
|
||||
}
|
||||
if (reg_pointer_ == REG_CONFIG) {
|
||||
buf[0] = 0x60; // default config
|
||||
buf[1] = 0xA0;
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t reg_pointer_;
|
||||
int16_t temp_raw_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helper: read temperature the way real Arduino code would
|
||||
// -----------------------------------------------------------------------
|
||||
static float readTemperature(Hal* hal, uint8_t addr) {
|
||||
// Set register pointer to 0x00 (temperature)
|
||||
hal->i2cBeginTransmission(addr);
|
||||
hal->i2cWrite(TempSensorSim::REG_TEMP);
|
||||
hal->i2cEndTransmission();
|
||||
|
||||
// Read 2 bytes
|
||||
uint8_t count = hal->i2cRequestFrom(addr, 2);
|
||||
if (count < 2) return -999.0f;
|
||||
|
||||
uint8_t msb = static_cast<uint8_t>(hal->i2cRead());
|
||||
uint8_t lsb = static_cast<uint8_t>(hal->i2cRead());
|
||||
|
||||
// Convert 12-bit left-aligned to temperature
|
||||
int16_t raw = (static_cast<int16_t>(msb) << 8) | lsb;
|
||||
raw >>= 4; // right-align the 12 bits
|
||||
return raw * 0.0625f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
class I2cTempSensorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
sensor_.setTemperature(25.0f);
|
||||
sim_.attachI2cDevice(TempSensorSim::ADDR, &sensor_);
|
||||
sim_.i2cBegin();
|
||||
}
|
||||
|
||||
SimHal sim_;
|
||||
TempSensorSim sensor_;
|
||||
};
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsRoomTemperature) {
|
||||
sensor_.setTemperature(25.0f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, 25.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsFreezing) {
|
||||
sensor_.setTemperature(0.0f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsNegativeTemperature) {
|
||||
sensor_.setTemperature(-10.5f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, -10.5f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsHighTemperature) {
|
||||
sensor_.setTemperature(85.0f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, 85.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, UnregisteredDeviceReturnsNack) {
|
||||
// Try to talk to a device that does not exist
|
||||
sim_.i2cBeginTransmission(0x50);
|
||||
sim_.i2cWrite(0x00);
|
||||
uint8_t result = sim_.i2cEndTransmission();
|
||||
EXPECT_NE(result, 0); // non-zero = error
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, TemperatureUpdatesLive) {
|
||||
sensor_.setTemperature(20.0f);
|
||||
float t1 = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
|
||||
sensor_.setTemperature(30.0f);
|
||||
float t2 = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
|
||||
EXPECT_NEAR(t1, 20.0f, 0.1f);
|
||||
EXPECT_NEAR(t2, 30.0f, 0.1f);
|
||||
}
|
||||
Reference in New Issue
Block a user