diff --git a/.github/workflows/compile-examples.yml b/.github/workflows/compile-examples.yml index e3e6757..30f8514 100644 --- a/.github/workflows/compile-examples.yml +++ b/.github/workflows/compile-examples.yml @@ -57,6 +57,8 @@ jobs: - name: Adafruit SSD1306 - name: Adafruit GFX Library - name: Adafruit BusIO + # SD is needed by the FileUtils example (ListSdFiles). + - name: SD sketch-paths: | - examples enable-warnings-report: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0519b9f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +All notable changes to the MakeabilityLab Arduino library are documented here. +This project adheres to [Semantic Versioning](https://semver.org/). + +## [0.3.0] + +### Added +- **`SimpleLineGraph`** — the simplest, non-scrolling line graph: it fills the + screen left to right, then clears and restarts at x=0. Keeps no history + buffer (the OLED framebuffer is the "memory"). +- New example sketches for previously uncovered classes: `SimpleLineGraphA0`, + `ScrollingLineGraphA0`, `ScrollingLineGraphTwoInputs`, `ButtonBasic`, + `SmoothAnalogInput`, `ClosestColorMatch`, and `ListSdFiles`. +- `Button`: `wasReleased()` and `wasJustPressed()` edge detectors — clearer + companions to the legacy `wasPressed()` (which, despite its name, fires on + release and is now documented as such). Added `timeSinceLastRead()` and + `lastReadTimestamp()`. + +### Changed (breaking) +- The four public enums are now **scoped (`enum class`)**: `ShapeType`, + `PointSymbol`, `JoystickYDirection`, and `GraphPrimitive`. Enumerators must + now be qualified — e.g. `JoystickYDirection::RIGHT`, `PointSymbol::CIRCLE`. + This fixes a latent name collision (`CIRCLE`/`TRIANGLE` were defined in both + `ShapeType` and `PointSymbol`) that broke compilation when `Shape.hpp` and a + graph header were included together. +- **Removed `LineGraph.hpp` / `MultiValueScrollingLineGraph`** — a superseded, + unused draft whose API is a strict subset of `ScrollingLineGraphMultiValue`. + +### Fixed +- `ScrollingLineGraphMultiValue` now deletes its copy constructor/assignment + (it owns heap memory), preventing a double-free on copy. +- `MovingAverageFilter` guards against a non-positive window size. +- `ScrollingLineGraph` guards against a non-positive buffer size. +- `Button` no longer emits a `-Wreorder` warning (constructor initializer-list + order now matches declaration order). + +### Internal / housekeeping +- Standardized on `bool` (over Arduino's `boolean`) and `nullptr` (over `NULL`). +- `Shape::distance()` now uses integer multiplication instead of `pow(x, 2)` + (faster and exact on AVR). +- Removed dead code and commented-out debug prints; filled in doc stubs. +- Example headers updated to the shared author footer. + +## [0.2.0] +- Added Library Manager badge; first release published to the Arduino Library + Manager. + +## [0.1.0] +- Initial release, extracted from the makeabilitylab/arduino repository. + +[0.3.0]: https://github.com/makeabilitylab/makelab-arduino-lib +[0.2.0]: https://github.com/makeabilitylab/makelab-arduino-lib +[0.1.0]: https://github.com/makeabilitylab/makelab-arduino-lib diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..84bbad4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,114 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +The **MakeabilityLab** Arduino library — shared classes for physical computing +courses (joystick input, OLED shapes/graphics & collision, scrolling graphs, +signal filtering, color matching, SD file utilities). It is a proper, installable +Arduino library, recently extracted from the main `arduino` repo (commit +`11fddc0`, "Moved code from .../arduino"). + +This work is part of the **v2.0 textbook modernization** (summer 2026): improving +code quality, comments, clarity, and correctness across the lab's teaching code. +This library is teaching material — optimize for a student reading it. + +## Cross-repo context (important) + +This library does not live alone. A session here should know its neighbors (all +cloned locally; other Claude Code sessions may be active in them): + +- **`arduino` repo** (`d:\git\arduino`, github.com/makeabilitylab/arduino) — the + main consumer. ~30 sketches `#include` these `.hpp` headers. On the dev machine + the Arduino `libraries/MakeabilityLab` folder is a **directory junction** → + this repo, so edits here are immediately live for those sketches and their + compile checks. **Changing a public class/method API here can break sketches + there** — grep the arduino repo before renaming/removing anything public. +- **`physcomp` textbook** (`d:\git\physcomp`, makeabilitylab.github.io/physcomp) — + lessons reference these classes; class/header docs link back to specific lesson + URLs. Renaming/moving a lesson breaks those inbound links (and vice versa). +- **`signals`** (`d:\git\signals`) and **`p5js`** (`d:\git\p5js`) — sibling + teaching repos that share the **author footer** convention (below). + +The **canonical header/footer convention** is defined in the arduino repo at +`docs/sketch-header-template.md`. That doc governs `.ino` *sketch* headers; this +library uses Doxygen-style *file* headers (see Conventions), but the **author +footer is shared** across all repos. + +## Structure + +- `src/*.hpp` — the library. **Header-only** (classes implemented inline in `.hpp`). + Classes: `Shape`/`Rectangle`/`RoundRect`/`Triangle`/`Circle`/`Ball` (`Shape.hpp`), + `Button`, `ParallaxJoystick`, `MovingAverageFilter`, `SimpleLineGraph` + (non-scrolling), `ScrollingLineGraph`, `ScrollingLineGraphMultiValue`, + `GraphLine` (building block for the multi-value graph), `ColorName`, `FileUtils`. + NOTE: `LineGraph.hpp`/`MultiValueScrollingLineGraph` was removed — it was a + superseded, unused draft of `ScrollingLineGraphMultiValue`. +- `examples/` — games/shapes: `BallBounceObjectOriented`, `BallBounceShapes`, + `CollisionTest`, `FlappyBird`, `MoveBallJoystickTest`, `Pong`. Graphs: + `SimpleLineGraphA0`, `ScrollingLineGraphA0`, `ScrollingLineGraphTwoInputs`. + Standalone classes: `ButtonBasic`, `SmoothAnalogInput`, `ClosestColorMatch`, + `ListSdFiles`. +- `library.properties` — name `MakeabilityLab`, version `0.3.0`, + `depends=Adafruit SSD1306, Adafruit GFX Library` (installing the lib pulls these). +- `keywords.txt` — Arduino IDE syntax highlighting; **keep in sync** when adding, + renaming, or removing public classes/methods. +- `README.md` — has the class table and install instructions (Library Manager or clone/symlink). + +## Build / test + +There is **no CI yet** (a v2.0 opportunity — see below). `arduino-cli` is installed +on this machine at `C:\Program Files\Arduino CLI\arduino-cli.exe` (may not be on +PATH; call by full path if needed). Compile an example to verify changes: + +```bash +arduino-cli compile --fqbn arduino:avr:uno examples/Pong +``` + +Verified compiling on `arduino:avr:uno`: `examples/Pong`, `examples/FlappyBird`. +Examples that target OLED/joystick hardware still compile on Uno. Installed cores +on this machine: `arduino:avr`, `arduino:renesas_uno`, `esp32:esp32`, +`adafruit:samd`, `adafruit:nrf52`. + +**Testing opportunities for v2.0** (discuss before building): +- **Compile the examples in CI** on every PR (the floor — mirrors what the arduino + repo is setting up). Use the `arduino/compile-sketches` action. +- **Unit-test the pure-logic classes off-device** — `MovingAverageFilter`, + `Shape` collision math, `ColorName` matching are testable without hardware + (host build + a framework like AUnit / ArduinoFake, or desktop GoogleTest). + This is where deeper testing pays off and naturally belongs in *this* repo + rather than the arduino repo. + +## Conventions + +- **`.hpp` extension**, header-only, `#pragma once`. +- **Doxygen-style file/class headers**: `/** @file … @brief … @section usage + @code … @endcode @author … */`. Keep this style for library headers. +- **GOTCHA — never nest `*/` inside a `/** … */` doc block.** Inline block + comments like `/* held down */` inside an `@code` example prematurely close the + doc comment, turning the rest into stray code (compile error "stray '@' in + program"). This exact bug was just fixed in `Button.hpp` — use `//` line + comments inside `@code` examples. +- **Author footer.** Headers currently end with the older + `@author Jon E. Froehlich` / `@jonfroehlich` / URL style. The arduino repo's + *sketch* convention recently moved to: + ``` + By Professor Jon E. Froehlich + Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + ``` + **Open question for this repo:** whether to align the library's `.hpp` headers + to that footer. Worth deciding deliberately rather than drifting. +- **Public API stability:** before renaming/removing anything public, grep the + arduino repo (`d:\git\arduino`) for usages — sketches there compile against this + via the junction. +- Update `keywords.txt` and `library.properties` (`version`, `depends`) alongside + code changes that affect them. + +## Recent context + +- `Tone32.hpp` was intentionally **removed** (commit `3d8faae`) — ESP32 now + supports native `tone()`. A few sketches in the arduino repo still `#include + ` and are being migrated there. +- Library is pre-1.0 (`0.1.0`); just extracted from the arduino repo. diff --git a/README.md b/README.md index 7567f1f..db19b70 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Officially available in the [Arduino Library Manager](https://www.arduino.cc/ref | `ParallaxJoystick` | `ParallaxJoystick.hpp` | Read input from a [Parallax 2-Axis Joystick](https://www.parallax.com/product/2-axis-joystick/), with support for different orientations | | `Button` | `Button.hpp` | Debounced digital button with press/release detection | | `Shape`, `Rectangle`, `RoundRect`, `Triangle`, `Circle`, `Ball` | `Shape.hpp` | Drawing primitives with collision detection, velocity, and boundary checking for monochrome OLEDs (Adafruit SSD1306) | +| `SimpleLineGraph` | `SimpleLineGraph.hpp` | Simplest non-scrolling line graph: fills left to right, then restarts at x=0 | | `ScrollingLineGraph` | `ScrollingLineGraph.hpp` | Single-value scrolling line graph for OLEDs | | `ScrollingLineGraphMultiValue` | `ScrollingLineGraphMultiValue.hpp` | Multi-value scrolling line graph with per-line symbols | | `MovingAverageFilter` | `MovingAverageFilter.hpp` | Sliding-window moving average for smoothing sensor input | @@ -69,6 +70,13 @@ These example sketches are included with the library (**File → Examples → Ma | Pong | Two-player input (joystick + buttons), ball-paddle collision, scoring | | MoveBallJoystickTest | ParallaxJoystick controlling a Ball on the OLED | | CollisionTest | Polymorphism with Shape pointers, mixed Ball/Rectangle collision | +| SimpleLineGraphA0 | Non-scrolling graph of an analog input with `SimpleLineGraph` | +| ScrollingLineGraphA0 | Scrolling, auto-scaling graph of an analog input with `ScrollingLineGraph` | +| ScrollingLineGraphTwoInputs | Two-series scrolling graph (A0/A1) with `ScrollingLineGraphMultiValue` | +| ButtonBasic | Debounced press/release detection with `Button` | +| SmoothAnalogInput | Smoothing a noisy analog input with `MovingAverageFilter` | +| ClosestColorMatch | Nearest named-color matching with `ColorName` | +| ListSdFiles | Counting and listing SD-card files with `FileUtils` | ## Dependencies @@ -170,6 +178,20 @@ void loop() { } ``` +### SimpleLineGraph + +The simplest line graph: non-scrolling. It fills the screen left to right, then clears and restarts at x=0. It keeps no history buffer, so your loop does **not** call `display.clearDisplay()` — the drawing persists until it wraps. + +```cpp +SimpleLineGraph graph(128, 64); // width, height in pixels + +void loop() { + graph.addValue(display, analogRead(A0)); // draws one bar, advances the pen + display.display(); // note: no clearDisplay()! + delay(5); +} +``` + ### ScrollingLineGraph Real-time scrolling graph for visualizing sensor data on an OLED. diff --git a/docs/HARDWARE_TEST_PLAN.md b/docs/HARDWARE_TEST_PLAN.md new file mode 100644 index 0000000..1e0382a --- /dev/null +++ b/docs/HARDWARE_TEST_PLAN.md @@ -0,0 +1,66 @@ +# Hardware Test Plan — v0.3.0 + +A hands-on checklist for validating the library on real hardware after the +0.3.0 changes (new `SimpleLineGraph`, the `enum class` migration, new examples, +and cleanup). All examples are confirmed to **compile** on `arduino:avr:uno`; +this plan verifies **runtime behavior**. + +## Gear + +- Arduino Uno (or compatible) +- SSD1306 128×64 I²C OLED +- Parallax 2-axis joystick +- 3–4 momentary push buttons +- 2 potentiometers +- SD card module + a card with a few files (incl. at least one `.txt`) +- Breadboard + jumper wires +- Serial Monitor / Serial Plotter at **9600 baud** + +## Read first — common gotchas + +- **OLED address:** every sketch uses **`0x3D`**. If the screen stays black, + your module is probably **`0x3C`** — edit the + `_display.begin(SSD1306_SWITCHCAPVCC, 0x3D)` line. +- **OLED wiring (Uno):** SDA→A4, SCL→A5, VCC→3.3V/5V, GND→GND. +- **Joystick orientation:** examples assume `JoystickYDirection::RIGHT`. If an + axis is swapped or inverted, that constant is the knob to turn. +- The `enum class` change is **invisible at runtime** — the shape/game tests in + Part B are pure regression and should behave exactly as before. + +## Part A — New & changed (priority) + +- [ ] **SimpleLineGraphA0** *(new class)* — pot wiper → A0. Expect white + **vertical bars** marching **left→right**; pot changes bar height live; at + the right edge the graph **clears and restarts at x=0** (does **not** + scroll). Check both pot extremes (flat-low, full-height). +- [ ] **ScrollingLineGraphA0** — same A0 pot. Expect a line that **scrolls + right→left**, auto-scaling Y-axis, and an "A0: ####" label. +- [ ] **ScrollingLineGraphTwoInputs** *(validates `enum class PointSymbol`)* — + pots on A0 and A1. Expect **two series**, one drawn with **circles** and + one with **squares**, both scrolling with a shared auto-scale. +- [ ] **ButtonBasic** *(no OLED)* — button pin 2 → GND. Serial prints + **"Button pressed"** on press and **"Button released"** on release, once + each, with no bounce/repeat. +- [ ] **SmoothAnalogInput** *(no OLED)* — pot → A0. Serial Plotter shows two + traces: raw (jumpy) and smoothed (laggy/clean). Flicking the pot makes the + smoothed trace visibly lag. +- [ ] **ClosestColorMatch** *(no hardware)* — Serial prints + `Closest color to (240, 20, 30): Red`. +- [ ] **ListSdFiles** *(SD module, CS → pin 10)* — Serial prints the `.txt` + count and a directory listing. "init failed" → check CS pin/wiring. + +## Part B — Regression (confirm the enum migration broke nothing) + +- [ ] **BallBounceObjectOriented** — ball bounces off all four edges, stays on + screen. +- [ ] **BallBounceShapes** *(best test of `enum class ShapeType` at runtime)* — + ball bounces among a Rectangle/RoundRect/Triangle/Circle; shapes fill + white on contact. +- [ ] **CollisionTest** — joystick moves the player ball; targets fill when + overlapped; hold reset to reshuffle. +- [ ] **MoveBallJoystickTest** — joystick moves the ball with correct axis + directions (flip orientation if not). +- [ ] **Pong** — joystick (left paddle) + buttons on pins 6/7 (right paddle) + + serve button on pin 5; ball-paddle bounce, scoring, game-over/restart. +- [ ] **FlappyBird** — flap button (pin 4) lifts the bird; pipes scroll; + collision ends the game; flap to restart. diff --git a/examples/BallBounceObjectOriented/BallBounceObjectOriented.ino b/examples/BallBounceObjectOriented/BallBounceObjectOriented.ino index e1597d7..6dea8c7 100644 --- a/examples/BallBounceObjectOriented/BallBounceObjectOriented.ino +++ b/examples/BallBounceObjectOriented/BallBounceObjectOriented.ino @@ -19,10 +19,9 @@ * Makeability Lab OLED lesson: * https://makeabilitylab.github.io/physcomp/advancedio/oled.html * - * By Jon E. Froehlich - * @jonfroehlich - * https://jonfroehlich.github.io/ - * https://makeabilitylab.github.io/physcomp/ + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ */ #include @@ -50,7 +49,7 @@ unsigned long _frameCount = 0; unsigned long _fpsStartTimeStamp = 0; // status bar -const boolean _drawStatusBar = true; // change to show/hide status bar +const bool _drawStatusBar = true; // change to show/hide status bar void setup() { Serial.begin(9600); diff --git a/examples/BallBounceShapes/BallBounceShapes.ino b/examples/BallBounceShapes/BallBounceShapes.ino index d149619..f3de31f 100644 --- a/examples/BallBounceShapes/BallBounceShapes.ino +++ b/examples/BallBounceShapes/BallBounceShapes.ino @@ -14,10 +14,9 @@ * - Makeability Lab library (Shape.hpp) * - Adafruit SSD1306 and GFX libraries * - * By Jon E. Froehlich - * @jonfroehlich - * https://jonfroehlich.github.io/ - * https://makeabilitylab.github.io/physcomp/ + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ */ #include @@ -54,7 +53,7 @@ float _fps = 0; unsigned long _frameCount = 0; unsigned long _fpsStartTimeStamp = 0; -const boolean _drawStatusBar = true; +const bool _drawStatusBar = true; // Forward declarations void createObstacles(); @@ -102,7 +101,7 @@ void createObstacles() { bool placed = false; for (int attempt = 0; attempt < MAX_PLACEMENT_ATTEMPTS && !placed; attempt++) { - Shape* candidate = NULL; + Shape* candidate = nullptr; switch (i) { case 0: // Rectangle diff --git a/examples/ButtonBasic/ButtonBasic.ino b/examples/ButtonBasic/ButtonBasic.ino new file mode 100644 index 0000000..d4a84c9 --- /dev/null +++ b/examples/ButtonBasic/ButtonBasic.ino @@ -0,0 +1,47 @@ +/** + * Reads a debounced push button with the Button class and prints + * press / release events to the Serial Monitor. + * + * Demonstrates: + * - Button with internal pull-up (active-low wiring) + * - Calling read() once per loop(), then querying isPressed() / didStateChange() + * + * Requires: + * - Makeability Lab library (Button.hpp) + * - A momentary push button from pin 2 to GND + * + * Wiring: one leg of the button to pin 2, the other leg to GND. The internal + * pull-up (on by default) makes the pin read HIGH when released and LOW when + * pressed; the Button class normalizes that so "pressed" always means pressed. + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include + +const int BUTTON_PIN = 2; + +// 25 ms debounce, internal pull-up, active-low (all defaults) +Button _button(BUTTON_PIN); + +void setup() { + Serial.begin(9600); + _button.begin(); // must call before read() + Serial.println("Press the button..."); +} + +void loop() { + // read() samples the pin and updates the debounced state. Call it once + // per loop, then use the query methods (isPressed/didStateChange) below. + _button.read(); + + // Only print on a transition (press or release), not every loop + if (_button.didStateChange()) { + if (_button.isPressed()) { + Serial.println("Button pressed"); + } else { + Serial.println("Button released"); + } + } +} diff --git a/examples/ClosestColorMatch/ClosestColorMatch.ino b/examples/ClosestColorMatch/ClosestColorMatch.ino new file mode 100644 index 0000000..48871f8 --- /dev/null +++ b/examples/ClosestColorMatch/ClosestColorMatch.ino @@ -0,0 +1,49 @@ +/** + * Matches an RGB value to the nearest named color in a palette using + * ColorName and mean squared error. + * + * Demonstrates: + * - Building a palette of ColorName entries + * - ColorName::getClosestColorName() to map an RGB reading to a name + * + * Requires: + * - Makeability Lab library (ColorName.hpp) + * + * This sketch uses a hard-coded RGB value to stand in for a sensor reading. + * With a color sensor (e.g., a TCS34725), you would pass the sensor's red, + * green, and blue values into getClosestColorName() instead. + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include + +ColorName _palette[] = { + ColorName("Red", 255, 0, 0), + ColorName("Green", 0, 255, 0), + ColorName("Blue", 0, 0, 255), + ColorName("Yellow", 255, 255, 0), + ColorName("White", 255, 255, 255), + ColorName("Black", 0, 0, 0), +}; +const int NUM_COLORS = sizeof(_palette) / sizeof(_palette[0]); + +void setup() { + Serial.begin(9600); + + // Pretend this is a reading from a color sensor + byte red = 240, green = 20, blue = 30; + + ColorName* match = ColorName::getClosestColorName(_palette, NUM_COLORS, + red, green, blue); + + Serial.print("Closest color to ("); + Serial.print(red); Serial.print(", "); + Serial.print(green); Serial.print(", "); + Serial.print(blue); Serial.print("): "); + Serial.println(match->getName()); // expect "Red" +} + +void loop() { +} diff --git a/examples/CollisionTest/CollisionTest.ino b/examples/CollisionTest/CollisionTest.ino index a476c6a..f507cc5 100644 --- a/examples/CollisionTest/CollisionTest.ino +++ b/examples/CollisionTest/CollisionTest.ino @@ -20,10 +20,9 @@ * Makeability Lab OLED lesson: * https://makeabilitylab.github.io/physcomp/advancedio/oled.html * - * By Jon E. Froehlich - * @jonfroehlich - * https://jonfroehlich.github.io/ - * https://makeabilitylab.github.io/physcomp/ + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ */ #include @@ -48,7 +47,7 @@ const char STR_LOADSCREEN_CREATOR[] = "Makeability Lab"; const char STR_LOADSCREEN_APP_NAME[] = "Collision Test!"; const int MAX_ANALOG_VAL = 1023; -const enum JoystickYDirection JOYSTICK_Y_DIR = RIGHT; +const JoystickYDirection JOYSTICK_Y_DIR = JoystickYDirection::RIGHT; // Analog joystick ParallaxJoystick _analogJoystick(JOYSTICK_UPDOWN_PIN, JOYSTICK_LEFTRIGHT_PIN, MAX_ANALOG_VAL, JOYSTICK_Y_DIR); @@ -84,7 +83,7 @@ unsigned long _frameCount = 0; unsigned long _fpsStartTimeStamp = 0; // status bar -const boolean _drawStatusBar = true; // change to show/hide status bar +const bool _drawStatusBar = true; // change to show/hide status bar const int DELAY_LOOP_MS = 5; const int LOAD_SCREEN_SHOW_MS = 500; @@ -101,7 +100,12 @@ void setup() { // In order for polymorphism to work, we need to use pointers // See: https://stackoverflow.com/a/45176572 - // So, we dynamically create our shapes here + // So, we dynamically create our shapes here. + // + // Note: these shapes (and the array) live for the entire run of the sketch, + // so we never delete them. If you allocated shapes that came and went during + // gameplay, you'd want a matching `delete` for each `new` to avoid leaking + // the Arduino's limited SRAM. _targetShapes = new Shape*[NUM_TARGET_SHAPES]; for (int i = 0; i < NUM_TARGET_SHAPES; i++) { diff --git a/examples/FlappyBird/FlappyBird.ino b/examples/FlappyBird/FlappyBird.ino index cb83069..db8cd23 100644 --- a/examples/FlappyBird/FlappyBird.ino +++ b/examples/FlappyBird/FlappyBird.ino @@ -24,10 +24,9 @@ * Makeability Lab OLED lesson: * https://makeabilitylab.github.io/physcomp/advancedio/oled.html * - * By Jon E. Froehlich - * @jonfroehlich - * https://jonfroehlich.github.io/ - * https://makeabilitylab.github.io/physcomp/ + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ */ #include @@ -58,7 +57,7 @@ float _fps = 0; unsigned long _fpsStartTimeStamp = 0; // status bar -const boolean _drawFrameCount = false; // change to show/hide frame count +const bool _drawFrameCount = false; // change to show/hide frame count const int DELAY_LOOP_MS = 5; const int LOAD_SCREEN_SHOW_MS = 750; diff --git a/examples/ListSdFiles/ListSdFiles.ino b/examples/ListSdFiles/ListSdFiles.ino new file mode 100644 index 0000000..25224f0 --- /dev/null +++ b/examples/ListSdFiles/ListSdFiles.ino @@ -0,0 +1,40 @@ +/** + * Counts and lists files on an SD card using FileUtils. + * + * Demonstrates: + * - FileUtils::countMatchingFiles() to count files by extension + * - FileUtils::printDirectory() to print a directory tree to Serial + * + * Requires: + * - Makeability Lab library (FileUtils.hpp) + * - The SD library (bundled with the Arduino IDE) + * - An SD card module wired over SPI; set SD_CS_PIN to your card's CS pin + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include +#include + +const int SD_CS_PIN = 10; // chip-select pin for your SD module + +void setup() { + Serial.begin(9600); + while (!Serial) { ; } // wait for Serial on boards that need it + + if (!SD.begin(SD_CS_PIN)) { + Serial.println("SD card initialization failed!"); + return; + } + + int count = FileUtils::countMatchingFiles("/", ".txt"); + Serial.print("Number of .txt files in root: "); + Serial.println(count); + + Serial.println("Full directory listing:"); + FileUtils::printDirectory("/"); +} + +void loop() { +} diff --git a/examples/MoveBallJoystickTest/MoveBallJoystickTest.ino b/examples/MoveBallJoystickTest/MoveBallJoystickTest.ino index d22bf4c..34621b0 100644 --- a/examples/MoveBallJoystickTest/MoveBallJoystickTest.ino +++ b/examples/MoveBallJoystickTest/MoveBallJoystickTest.ino @@ -24,10 +24,9 @@ * Adafruit OLED tutorials: * https://learn.adafruit.com/monochrome-oled-breakouts * - * By Jon E. Froehlich - * @jonfroehlich - * https://jonfroehlich.github.io/ - * https://makeabilitylab.github.io/physcomp/ + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ */ #include @@ -52,7 +51,7 @@ const int JOYSTICK_LEFTRIGHT_PIN = A0; // Change to 4095 for 12-bit ADCs like ESP32 and 1023 for 10-bit ADCs like Arduino Uno/Leonardo const int MAX_ANALOG_VAL = 1023; -const enum JoystickYDirection JOYSTICK_Y_DIR = RIGHT; +const JoystickYDirection JOYSTICK_Y_DIR = JoystickYDirection::RIGHT; ParallaxJoystick _joystick(JOYSTICK_UPDOWN_PIN, JOYSTICK_LEFTRIGHT_PIN, MAX_ANALOG_VAL, JOYSTICK_Y_DIR); Ball _ball(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 5); @@ -63,7 +62,7 @@ unsigned long _frameCount = 0; unsigned long _fpsStartTimeStamp = 0; // status bar -const boolean _drawStatusBar = true; // change to show/hide status bar +const bool _drawStatusBar = true; // change to show/hide status bar void setup() { Serial.begin(9600); diff --git a/examples/Pong/Pong.ino b/examples/Pong/Pong.ino index 92c2e8d..33d5678 100644 --- a/examples/Pong/Pong.ino +++ b/examples/Pong/Pong.ino @@ -23,10 +23,9 @@ * Adafruit OLED tutorials: * https://learn.adafruit.com/monochrome-oled-breakouts * - * By Jon E. Froehlich - * @jonfroehlich - * https://jonfroehlich.github.io/ - * https://makeabilitylab.github.io/physcomp/ + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ */ #include #include @@ -49,7 +48,7 @@ const int JOYSTICK_LEFTRIGHT_PIN = A0; const int SERVE_BUTTON_INPUT_PIN = 5; const int MAX_ANALOG_VAL = 1023; -const enum JoystickYDirection JOYSTICK_Y_DIR = RIGHT; +const JoystickYDirection JOYSTICK_Y_DIR = JoystickYDirection::RIGHT; // Analog joystick for left player ParallaxJoystick _analogJoystick(JOYSTICK_UPDOWN_PIN, JOYSTICK_LEFTRIGHT_PIN, MAX_ANALOG_VAL, JOYSTICK_Y_DIR); @@ -101,7 +100,7 @@ enum Players _gameWinner = LEFT_PLAYER; float _fps = 0; unsigned long _frameCount = 0; unsigned long _fpsStartTimeStamp = 0; -const boolean _drawFps = false; // change to show/hide fps display +const bool _drawFps = false; // change to show/hide fps display void setup() { Serial.begin(9600); diff --git a/examples/ScrollingLineGraphA0/ScrollingLineGraphA0.ino b/examples/ScrollingLineGraphA0/ScrollingLineGraphA0.ino new file mode 100644 index 0000000..218ee4e --- /dev/null +++ b/examples/ScrollingLineGraphA0/ScrollingLineGraphA0.ino @@ -0,0 +1,65 @@ +/** + * Plots a single analog input (A0) as a real-time scrolling line graph + * on an OLED using ScrollingLineGraph. + * + * Demonstrates: + * - ScrollingLineGraph with an auto-scaling Y-axis and a data label + * - The redraw-every-frame pattern (clearDisplay -> draw -> display) + * + * Requires: + * - Makeability Lab library (ScrollingLineGraph.hpp) + * - Adafruit SSD1306 and GFX libraries + * - A 128x64 SSD1306 OLED on I2C, and a sensor/potentiometer on A0 + * + * For the simplest non-scrolling version, see the SimpleLineGraph example. + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include +#include +#include + +#include +#include + +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels + +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +Adafruit_SSD1306 _display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +const int ANALOG_INPUT_PIN = A0; +const int DELAY_LOOP_MS = 5; + +ScrollingLineGraph _graph(SCREEN_WIDTH, SCREEN_HEIGHT); + +void setup() { + Serial.begin(9600); + + // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally + if (!_display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3D for 128x64 + Serial.println(F("SSD1306 allocation failed")); + for (;;); // Don't proceed, loop forever + } + _display.clearDisplay(); + + // The label is shown in the on-screen legend next to the latest value + _graph.setDataLabel("A0"); +} + +void loop() { + // Feed the graph one new sample per frame; it keeps a rolling history + // and scrolls older values off the left edge. + _graph.addData(analogRead(ANALOG_INPUT_PIN)); + + // Standard OLED redraw cycle: clear the in-memory buffer, draw the whole + // graph into it, then push the buffer to the screen in one shot. A scrolling + // graph must redraw every frame (unlike SimpleLineGraph, which persists). + _display.clearDisplay(); + _graph.draw(_display); + _display.display(); + + delay(DELAY_LOOP_MS); // pace the scroll so it's readable +} diff --git a/examples/ScrollingLineGraphTwoInputs/ScrollingLineGraphTwoInputs.ino b/examples/ScrollingLineGraphTwoInputs/ScrollingLineGraphTwoInputs.ino new file mode 100644 index 0000000..dff971f --- /dev/null +++ b/examples/ScrollingLineGraphTwoInputs/ScrollingLineGraphTwoInputs.ino @@ -0,0 +1,62 @@ +/** + * Plots two analog inputs (A0 and A1) as a scrolling multi-series graph on an + * OLED using ScrollingLineGraphMultiValue. Each series uses its own symbol. + * + * Demonstrates: + * - ScrollingLineGraphMultiValue with two data series + * - Per-series point symbols (PointSymbol::CIRCLE, PointSymbol::SQUARE) + * - Auto-scaling Y-axis across both series + * + * Requires: + * - Makeability Lab library (ScrollingLineGraphMultiValue.hpp) + * - Adafruit SSD1306 and GFX libraries + * - A 128x64 SSD1306 OLED on I2C, and sensors/potentiometers on A0 and A1 + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include +#include +#include + +#include +#include + +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels + +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +Adafruit_SSD1306 _display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +const int DELAY_LOOP_MS = 5; + +// One symbol per data series (scoped because PointSymbol is an enum class) +PointSymbol _symbols[] = { PointSymbol::CIRCLE, PointSymbol::SQUARE }; +ScrollingLineGraphMultiValue _graph(2, _symbols); + +void setup() { + Serial.begin(9600); + + // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally + if (!_display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3D for 128x64 + Serial.println(F("SSD1306 allocation failed")); + for (;;); // Don't proceed, loop forever + } + _display.clearDisplay(); +} + +void loop() { + // Feed each series its own new sample per frame (index 0 and index 1). + // The graph auto-scales its Y-axis to fit whichever series is largest. + _graph.addData(0, analogRead(A0)); // series 0 -> circles + _graph.addData(1, analogRead(A1)); // series 1 -> squares + + // Standard OLED redraw cycle: clear the buffer, draw both series into it, + // then push the whole frame to the screen at once. + _display.clearDisplay(); + _graph.draw(_display); + _display.display(); + + delay(DELAY_LOOP_MS); +} diff --git a/examples/SimpleLineGraphA0/SimpleLineGraphA0.ino b/examples/SimpleLineGraphA0/SimpleLineGraphA0.ino new file mode 100644 index 0000000..734e3b7 --- /dev/null +++ b/examples/SimpleLineGraphA0/SimpleLineGraphA0.ino @@ -0,0 +1,66 @@ +/** + * Graphs a single analog input (A0) on an OLED using SimpleLineGraph. + * + * SimpleLineGraph is the simplest, non-scrolling graph: it fills the screen + * left to right, then clears and restarts at x=0. Because the drawing persists + * on screen between frames, this sketch does NOT call display.clearDisplay() + * in loop() — doing so would erase the graph. + * + * Demonstrates: + * - SimpleLineGraph (non-scrolling, "sweep" style) + * - Mapping an analog reading onto the graph height + * + * Requires: + * - Makeability Lab library (SimpleLineGraph.hpp) + * - Adafruit SSD1306 and GFX libraries + * - A 128x64 SSD1306 OLED on I2C, and a sensor/potentiometer on A0 + * + * For a scrolling version, see the ScrollingLineGraph class. + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include +#include +#include + +#include +#include + +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels + +// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +Adafruit_SSD1306 _display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +const int ANALOG_INPUT_PIN = A0; +const int DELAY_LOOP_MS = 5; // slow down how often we read and plot + +SimpleLineGraph _graph(SCREEN_WIDTH, SCREEN_HEIGHT); + +void setup() { + Serial.begin(9600); + + // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally + if (!_display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3D for 128x64 + Serial.println(F("SSD1306 allocation failed")); + for (;;); // Don't proceed, loop forever + } + + _display.clearDisplay(); + _display.display(); + + // analogRead() returns 0–1023 on an Uno, which is SimpleLineGraph's default + // range, so we don't need to call setMinMaxValue() here. +} + +void loop() { + // Draw one bar for the current reading and advance the pen. + // Note: no clearDisplay() — the graph persists until it wraps at the edge. + _graph.addValue(_display, analogRead(ANALOG_INPUT_PIN)); + _display.display(); + + delay(DELAY_LOOP_MS); +} diff --git a/examples/SmoothAnalogInput/SmoothAnalogInput.ino b/examples/SmoothAnalogInput/SmoothAnalogInput.ino new file mode 100644 index 0000000..e3eff4e --- /dev/null +++ b/examples/SmoothAnalogInput/SmoothAnalogInput.ino @@ -0,0 +1,41 @@ +/** + * Smooths a noisy analog input with MovingAverageFilter and prints the raw + * and smoothed values to the Serial Monitor (and Serial Plotter). + * + * Demonstrates: + * - MovingAverageFilter with a fixed window size + * - Comparing raw vs. smoothed readings + * + * Requires: + * - Makeability Lab library (MovingAverageFilter.hpp) + * - A sensor or potentiometer on A0 + * + * Tip: open Tools -> Serial Plotter to see the two traces side by side. + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + */ +#include + +const int ANALOG_INPUT_PIN = A0; + +MovingAverageFilter _filter(10); // 10-sample window + +void setup() { + Serial.begin(9600); +} + +void loop() { + int raw = analogRead(ANALOG_INPUT_PIN); + + // Add the new reading to the rolling window; getAverage() now reflects it + _filter.add(raw); + + // Print "raw smoothed" — two traces for the Serial Plotter + Serial.print(raw); + Serial.print('\t'); + Serial.println(_filter.getAverage()); + + delay(20); +} diff --git a/keywords.txt b/keywords.txt index 6165ff3..a1c8de8 100644 --- a/keywords.txt +++ b/keywords.txt @@ -11,9 +11,9 @@ Ball KEYWORD1 Button KEYWORD1 ParallaxJoystick KEYWORD1 MovingAverageFilter KEYWORD1 +SimpleLineGraph KEYWORD1 ScrollingLineGraph KEYWORD1 ScrollingLineGraphMultiValue KEYWORD1 -MultiValueScrollingLineGraph KEYWORD1 GraphLine KEYWORD1 ColorName KEYWORD1 FileUtils KEYWORD1 @@ -50,12 +50,16 @@ getShapeType KEYWORD2 getName KEYWORD2 isPressed KEYWORD2 wasPressed KEYWORD2 +wasReleased KEYWORD2 +wasJustPressed KEYWORD2 didStateChange KEYWORD2 add KEYWORD2 getAverage KEYWORD2 getAverageAsFloat KEYWORD2 isWindowFull KEYWORD2 addData KEYWORD2 +addValue KEYWORD2 +setMinMaxValue KEYWORD2 setDataLabel KEYWORD2 setAutoYAxis KEYWORD2 setMinMaxY KEYWORD2 diff --git a/library.properties b/library.properties index f32f33b..0be8128 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=MakeabilityLab -version=0.2.0 +version=0.3.0 author=Jon E. Froehlich maintainer=Jon E. Froehlich sentence=Joystick input, OLED graphics, signal filtering, and game primitives for physical computing courses. diff --git a/src/Button.hpp b/src/Button.hpp index 4538e77..8c1f442 100644 --- a/src/Button.hpp +++ b/src/Button.hpp @@ -26,8 +26,9 @@ * * void loop() { * myButton.read(); - * if (myButton.isPressed()) { } // held down - * if (myButton.wasPressed()) { } // just released + * if (myButton.isPressed()) { } // currently held down + * if (myButton.wasJustPressed()) { } // just pressed (press edge) + * if (myButton.wasReleased()) { } // just released (release edge) * } * @endcode * @@ -65,15 +66,18 @@ class Button { */ Button(uint8_t buttonPin, unsigned long debounceTime = 25, bool internalPullUpEnabled = true, bool activeLow = true) + // NOTE: members are initialized in declaration order (not the order + // listed here), so this list is kept in declaration order to avoid a + // -Wreorder warning. : _pin(buttonPin), - _debounceTime(debounceTime), _isInternalPullUpEnabled(internalPullUpEnabled), _isActiveLow(activeLow), _stateChange(false), _curState(false), _prevState(false), _lastReadTimestamp(0), - _lastChangeTimestamp(0) + _lastChangeTimestamp(0), + _debounceTime(debounceTime) {} /** @@ -84,7 +88,7 @@ class Button { pinMode(_pin, _isInternalPullUpEnabled ? INPUT_PULLUP : INPUT); unsigned long curTimestampMs = millis(); - _curState = digitalRead(_pin); + _curState = (digitalRead(_pin) == HIGH); // If active low, invert the state so that _curState == true means "pressed" // (regardless of the wiring). This normalization makes all the query methods @@ -104,13 +108,19 @@ class Button { * @brief Read the pin and update the debounced button state. * * Call this once per loop() iteration. After calling, use isPressed(), - * wasPressed(), and didStateChange() to query the result. + * wasJustPressed(), wasReleased(), and didStateChange() to query the result. + * + * Debouncing uses a "lockout" strategy: the first edge is accepted + * immediately, then further changes are ignored for the debounce window. + * This is responsive and works well for pushbuttons with INPUT_PULLUP; in + * very noisy or long-wire setups, a "wait until the level is stable" + * strategy would reject electrical spikes more aggressively. * * @return true if the button is currently pressed (after debouncing) */ bool read() { unsigned long curTimestampMs = millis(); - bool pinVal = digitalRead(_pin); + bool pinVal = (digitalRead(_pin) == HIGH); if (_isActiveLow) { pinVal = !pinVal; @@ -142,12 +152,35 @@ class Button { } /** - * @brief Returns true if the button was just pressed but is now released. + * @brief Returns true on the read() cycle when the button is released + * (a release "edge"). Does not call read(). + * @return true the first read() after the button goes from pressed to released + */ + bool wasReleased() const { + return _stateChange && !_curState; + } + + /** + * @brief Returns true on the read() cycle when the button is pressed + * (a press "edge"). Does not call read(). + * @return true the first read() after the button goes from released to pressed + */ + bool wasJustPressed() const { + return _stateChange && _curState; + } + + /** + * @brief Returns true the read() cycle immediately after the button is released. * Does not call read(). + * + * @deprecated Despite its name, this fires on RELEASE, not press. It is kept + * for backwards compatibility; new code should use wasReleased() (identical + * behavior, clearer name), or wasJustPressed() to detect the press edge. + * * @return true on the read() cycle immediately after the button is released */ bool wasPressed() const { - return _prevState && !_curState && _stateChange; + return wasReleased(); } /** @@ -173,4 +206,20 @@ class Button { unsigned long lastStateChangeTimestamp() const { return _lastChangeTimestamp; } + + /** + * @brief Returns the time in milliseconds since the most recent read(). + * @return Milliseconds since the last call to read() (or begin()) + */ + unsigned long timeSinceLastRead() const { + return millis() - _lastReadTimestamp; + } + + /** + * @brief The time in milliseconds (from millis()) of the most recent read(). + * @return Timestamp in milliseconds of the last read() (or begin()) + */ + unsigned long lastReadTimestamp() const { + return _lastReadTimestamp; + } }; diff --git a/src/ColorName.hpp b/src/ColorName.hpp index 05d3272..6e47d66 100644 --- a/src/ColorName.hpp +++ b/src/ColorName.hpp @@ -116,7 +116,7 @@ class ColorName { byte red, byte green, byte blue, bool debug = false) { if (numColors == 0) { - return NULL; + return nullptr; } if (debug) { @@ -124,11 +124,11 @@ class ColorName { red + ", " + green + ", " + blue + ")"); } - ColorName* _bestMatchColorName = NULL; + ColorName* _bestMatchColorName = nullptr; long bestMatchDistance = 65535; for (unsigned int i = 0; i < numColors; i++) { ColorName* curColorName = &colorNames[i]; - if (_bestMatchColorName == NULL) { + if (_bestMatchColorName == nullptr) { _bestMatchColorName = &colorNames[0]; } diff --git a/src/FileUtils.hpp b/src/FileUtils.hpp index 748be8d..8445d8d 100644 --- a/src/FileUtils.hpp +++ b/src/FileUtils.hpp @@ -58,7 +58,7 @@ class FileUtils { * @return Number of matching files */ static int countMatchingFiles(const String& path, String fileExt, - boolean includeSubdirs = false) { + bool includeSubdirs = false) { File file = SD.open(path); int cntMatchingFiles = countMatchingFiles(file, fileExt, includeSubdirs); file.close(); @@ -74,7 +74,7 @@ class FileUtils { * @return Number of matching files */ static int countMatchingFiles(File dir, String fileExt, - boolean includeSubdirs = false) { + bool includeSubdirs = false) { // Force lower case for fileExt, which is done "in place" // https://www.arduino.cc/reference/en/language/variables/data-types/string/functions/tolowercase/ fileExt.toLowerCase(); @@ -93,9 +93,6 @@ class FileUtils { entryFilename.toLowerCase(); if (entryFilename.endsWith(fileExt)) { cntMatchingFiles++; - //Serial.println(entryFilename + " ends with " + fileExt); - } else { - //Serial.println(entryFilename + " does NOT end with " + fileExt); } } fileEntry.close(); @@ -134,7 +131,7 @@ class FileUtils { * @param path The current path to search * @param fileExt The file extension to search for; case-insensitive * @param arraySize [out] Set to the number of matching files found - * @return String* An array of matching String paths, or NULL if none found + * @return String* An array of matching String paths, or nullptr if none found */ static String* getFilePaths(String path, String fileExt, int& arraySize) { @@ -150,11 +147,11 @@ class FileUtils { int numMatchingFiles = countMatchingFiles(fileDir, fileExt); - // If no matching files, set arraySize to zero and return NULL + // If no matching files, set arraySize to zero and return nullptr if (numMatchingFiles <= 0) { arraySize = 0; fileDir.close(); - return NULL; + return nullptr; } // Caller must delete[] this array when done @@ -168,7 +165,6 @@ class FileUtils { String entryFilename = String(fileEntry.name()); entryFilename.toLowerCase(); if (entryFilename.endsWith(fileExt)) { - // Serial.println("Adding " + entryFilename + " at index " + (String)matchingFileIndex); String filenameWithPath = path + fileEntry.name(); matchingFiles[matchingFileIndex++] = filenameWithPath; } @@ -191,7 +187,7 @@ class FileUtils { * @param includeSubdirs If true, recurse into subdirectories (default true) */ static void printDirectory(const String& path, int numTabs = 0, - boolean includeSubdirs = true) { + bool includeSubdirs = true) { Serial.println(path); File file = SD.open(path); printDirectory(file, numTabs, includeSubdirs); @@ -206,13 +202,12 @@ class FileUtils { * @param includeSubdirs If true, recurse into subdirectories (default true) */ static void printDirectory(File dir, int numTabs = 0, - boolean includeSubdirs = true) { + bool includeSubdirs = true) { while (true) { File entry = dir.openNextFile(); if (!entry) { // no more files - //Serial.println("**nomorefiles**"); break; } diff --git a/src/GraphLine.hpp b/src/GraphLine.hpp index 5e33b05..07f4882 100644 --- a/src/GraphLine.hpp +++ b/src/GraphLine.hpp @@ -2,8 +2,8 @@ * @file GraphLine.hpp * @brief A single data series for plotting on a monochrome OLED. * - * Provides the PointSymbol enum and GraphLine class used by both - * LineGraph.hpp and ScrollingLineGraphMultiValue.hpp. Each GraphLine + * Provides the PointSymbol enum and GraphLine class used by + * ScrollingLineGraphMultiValue.hpp. Each GraphLine * stores a circular buffer of integer values and draws them using * a configurable point symbol (circle, square, triangle, or pixel). * @@ -27,7 +27,7 @@ #include /** @brief Symbol shapes used to plot data points on a graph line. */ -enum PointSymbol { +enum class PointSymbol { CIRCLE, ///< Open circle SQUARE, ///< Open square TRIANGLE, ///< Open triangle @@ -43,12 +43,11 @@ enum PointSymbol { */ class GraphLine { protected: - int* _circularBuffer = NULL; ///< Circular buffer of data values - int _bufferSize = -1; ///< Number of slots in the buffer - long _sampleTotal = 0; ///< Running sum (preserved from original, unused currently) - int _curWriteIndex = 0; ///< Next write position in the buffer - bool _isBufferFull = false; ///< True once the buffer has wrapped at least once - enum PointSymbol _symbol = CIRCLE; ///< Symbol used to render this line + int* _circularBuffer = nullptr; ///< Circular buffer of data values + int _bufferSize = -1; ///< Number of slots in the buffer + int _curWriteIndex = 0; ///< Next write position in the buffer + bool _isBufferFull = false; ///< True once the buffer has wrapped at least once + PointSymbol _symbol = PointSymbol::CIRCLE; ///< Symbol used to render this line int _ptSize = DEFAULT_SYMBOL_SIZE; ///< Size of each symbol in pixels (width and height) public: @@ -74,9 +73,9 @@ class GraphLine { } ~GraphLine() { - if (_circularBuffer != NULL) { + if (_circularBuffer != nullptr) { delete[] _circularBuffer; - _circularBuffer = NULL; + _circularBuffer = nullptr; _bufferSize = -1; } } @@ -193,19 +192,19 @@ class GraphLine { int halfSize = size / 2; switch (symbol) { - case CIRCLE: + case PointSymbol::CIRCLE: { disp.drawCircle(x, y, halfSize, SSD1306_WHITE); break; } - case SQUARE: + case PointSymbol::SQUARE: { int topLeftX = x - halfSize; int topLeftY = y - halfSize; disp.drawRect(topLeftX, topLeftY, size, size, SSD1306_WHITE); break; } - case TRIANGLE: + case PointSymbol::TRIANGLE: { int x0 = x - halfSize; int y0 = y + halfSize; @@ -219,7 +218,7 @@ class GraphLine { disp.drawTriangle(x0, y0, x1, y1, x2, y2, SSD1306_WHITE); break; } - case POINT: + case PointSymbol::POINT: { disp.drawPixel(x, y, SSD1306_WHITE); break; diff --git a/src/LineGraph.hpp b/src/LineGraph.hpp deleted file mode 100644 index 257fad8..0000000 --- a/src/LineGraph.hpp +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @file LineGraph.hpp - * @brief Multi-value scrolling line graph using point symbols on SSD1306 OLEDs. - * - * This file provides `MultiValueScrollingLineGraph`, which manages multiple - * `GraphLine` instances (defined in GraphLine.hpp) and renders them together - * on a monochrome OLED display. - * - * @note For a single-value scrolling graph with line rendering (not symbols), - * see ScrollingLineGraph.hpp. - * - * @see GraphLine.hpp - * @see ScrollingLineGraphMultiValue.hpp (newer version with more features) - * - * @author Jon E. Froehlich - * - * Learn more in the Physical Computing textbook: - * - OLED displays: https://makeabilitylab.github.io/physcomp/advancedio/oled.html - * - * By Professor Jon E. Froehlich - * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ - * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ - * - * SPDX-License-Identifier: MIT - */ -#pragma once -#include -#include "GraphLine.hpp" - -/** - * @brief A multi-line scrolling graph rendered with point symbols. - * - * Each line uses a different PointSymbol (CIRCLE, SQUARE, TRIANGLE, POINT) - * so they can be distinguished on a monochrome display. - * - * @section usage Usage Example - * @code - * PointSymbol symbols[] = { CIRCLE, SQUARE }; - * MultiValueScrollingLineGraph graph(2, symbols); - * - * void loop() { - * graph.addData(0, analogRead(A0)); - * graph.addData(1, analogRead(A1)); - * display.clearDisplay(); - * graph.draw(display); - * display.display(); - * } - * @endcode - */ -class MultiValueScrollingLineGraph { - protected: - GraphLine** _graphLines = NULL; ///< Array of pointers to GraphLine objects - int _numLines = -1; ///< Number of graph lines - - int _minY = 0; ///< Minimum data value (Y-axis floor) - int _maxY = 1023; ///< Maximum data value (Y-axis ceiling) - - int _xGraph = 0; ///< X origin of graph area in pixels - int _yGraph = 0; ///< Y origin of graph area in pixels - int _widthGraph = 128; ///< Width of graph area in pixels - int _heightGraph = 64; ///< Height of graph area in pixels - - public: - MultiValueScrollingLineGraph(int numLines, PointSymbol symbols[]) - : MultiValueScrollingLineGraph(0, 0, 128, 64, numLines, symbols) { - // purposefully empty - } - - MultiValueScrollingLineGraph(int xGraph, int yGraph, int graphWidth, - int graphHeight, int numLines, - PointSymbol symbols[]) { - _xGraph = xGraph; - _yGraph = yGraph; - _widthGraph = graphWidth; - _heightGraph = graphHeight; - - _graphLines = new GraphLine*[numLines]; - _numLines = numLines; - - for (int i = 0; i < numLines; i++) { - _graphLines[i] = new GraphLine(graphWidth, symbols[i]); - } - } - - ~MultiValueScrollingLineGraph() { - if (_graphLines != NULL) { - // Each _graphLines[i] was allocated with `new` (not `new[]`), - // so we use `delete` (not `delete[]`) - // See: https://stackoverflow.com/a/4194228/388117 - for (int i = 0; i < _numLines; i++) { - delete _graphLines[i]; - } - - delete[] _graphLines; - _graphLines = NULL; - _numLines = -1; - } - } - - MultiValueScrollingLineGraph(const MultiValueScrollingLineGraph&) = delete; - MultiValueScrollingLineGraph& operator=(const MultiValueScrollingLineGraph&) = delete; - - int getMinY() const { return _minY; } - - int getMaxY() const { return _maxY; } - - void addData(int graphLineIndex, int newYPos) { - // Guard: valid indices are 0 to _numLines - 1 - if (graphLineIndex < 0 || graphLineIndex >= _numLines) { - return; - } - - _graphLines[graphLineIndex]->addData(newYPos); - } - - void draw(Adafruit_SSD1306& disp) const { - for (int i = 0; i < _numLines; i++) { - _graphLines[i]->draw(disp, _yGraph, _heightGraph, _minY, _maxY); - } - } - - void drawLegend(Adafruit_SSD1306& disp, int x, int y) const { - int xCurPos = -1; - int ySymbolPos = -1; - int16_t xText, yText; - uint16_t textWidth, textHeight; - int symbolSize = 6; - - for (int i = 0; i < _numLines; i++) { - PointSymbol ptSymbol = _graphLines[i]->getSymbol(); - //int symbolSize = _graphLines[i]->getSymbolSize(); - - if (xCurPos == -1) { - xCurPos = x + symbolSize / 2; - } - - if (ySymbolPos == -1) { - ySymbolPos = y + symbolSize / 2; - } - - GraphLine::drawSymbol(disp, xCurPos, ySymbolPos, symbolSize, ptSymbol); - xCurPos += symbolSize; - - String strVal = (String)_graphLines[i]->getCurrentValue(); - disp.getTextBounds(strVal, 0, 0, &xText, &yText, &textWidth, &textHeight); - disp.setCursor(xCurPos, 0); - disp.print(strVal); - xCurPos += textWidth + 7; - } - } - - /** - * @brief Get the width of the graph - * @return int Width of the shape in pixels - */ - int getWidth() const { return _widthGraph; } - - /** - * @brief Get the height of the graph - * @return int Height of the graph in pixels - */ - int getHeight() const { return _heightGraph; } - - /** - * @brief Get the left (x) location of the graph - * @return int The left location of the shape - */ - int getLeft() const { return _xGraph; } - - /** - * @brief Get the right (x) location of the graph - * @return int The right location (x) of the graph - */ - int getRight() const { return _xGraph + _widthGraph; } - - /** - * @brief Get the bottom of the graph (y) - * @return int - */ - int getBottom() const { return _yGraph + _heightGraph; } - - /** - * @brief Get the top of the graph - * @return int - */ - int getTop() const { return _yGraph; } -}; diff --git a/src/MovingAverageFilter.hpp b/src/MovingAverageFilter.hpp index cf86eb7..21bf038 100644 --- a/src/MovingAverageFilter.hpp +++ b/src/MovingAverageFilter.hpp @@ -36,7 +36,7 @@ class MovingAverageFilter { protected: - int* _samples = NULL; ///< Circular buffer of sample values + int* _samples = nullptr; ///< Circular buffer of sample values int _windowSize = -1; ///< Number of samples in the window long _sampleTotal = 0; ///< Running sum of all samples in the buffer int _curReadIndex = 0; ///< Next write position in the circular buffer @@ -49,6 +49,11 @@ class MovingAverageFilter { * @param windowSize The window size of the moving average filter (must be > 0) */ MovingAverageFilter(int windowSize) { + // Guard against a non-positive window: a size < 1 would mean new int[0] + // (or a negative size) and a divide-by-zero in getAverage(). Clamp to 1. + if (windowSize < 1) { + windowSize = 1; + } _samples = new int[windowSize]; _windowSize = windowSize; @@ -60,9 +65,9 @@ class MovingAverageFilter { /** @brief Destructor. Frees the sample buffer. */ ~MovingAverageFilter() { - if (_samples != NULL) { + if (_samples != nullptr) { delete[] _samples; - _samples = NULL; + _samples = nullptr; _windowSize = -1; } } diff --git a/src/ParallaxJoystick.hpp b/src/ParallaxJoystick.hpp index 1749cd6..bca6816 100644 --- a/src/ParallaxJoystick.hpp +++ b/src/ParallaxJoystick.hpp @@ -13,7 +13,7 @@ * ParallaxJoystick joystick(A0, A1); * * // With orientation and 12-bit ADC (e.g., ESP32) - * ParallaxJoystick joystick(A0, A1, 4095, RIGHT); + * ParallaxJoystick joystick(A0, A1, 4095, JoystickYDirection::RIGHT); * * void loop() { * joystick.read(); @@ -36,7 +36,7 @@ // The joystick's orientation with respect to the user. // We need this because sometimes we have to place a joystick // upside down, etc. in our designs -enum JoystickYDirection { +enum class JoystickYDirection { UP, ///< Y-axis label faces up (default, no remapping) RIGHT, ///< Y-axis label faces right DOWN, ///< Y-axis label faces down (both axes inverted) @@ -53,7 +53,7 @@ class ParallaxJoystick { int _maxAnalogValue = DEFAULT_MAX_ANALOG_VALUE; int _joyStickCenterValue = int(DEFAULT_MAX_ANALOG_VALUE / 2); - enum JoystickYDirection _joystickYDir = UP; + JoystickYDirection _joystickYDir = JoystickYDirection::UP; int _upDownVal = 0; ///< Most recent up/down reading (after orientation mapping) int _leftRightVal = 0; ///< Most recent left/right reading (after orientation mapping) @@ -142,14 +142,14 @@ class ParallaxJoystick { int joystickLeftRightVal = analogRead(_leftRightInputPin); // Remap axes based on how the joystick is physically oriented - if (_joystickYDir == RIGHT) { + if (_joystickYDir == JoystickYDirection::RIGHT) { int tmpX = joystickLeftRightVal; _leftRightVal = _maxAnalogValue - joystickUpDownVal; _upDownVal = tmpX; - } else if (_joystickYDir == DOWN) { + } else if (_joystickYDir == JoystickYDirection::DOWN) { _upDownVal = _maxAnalogValue - joystickUpDownVal; _leftRightVal = _maxAnalogValue - joystickLeftRightVal; - } else if (_joystickYDir == LEFT) { + } else if (_joystickYDir == JoystickYDirection::LEFT) { int tmpX = joystickLeftRightVal; _leftRightVal = joystickUpDownVal; _upDownVal = _maxAnalogValue - tmpX; diff --git a/src/ScrollingLineGraph.hpp b/src/ScrollingLineGraph.hpp index 221f21e..7fb1d73 100644 --- a/src/ScrollingLineGraph.hpp +++ b/src/ScrollingLineGraph.hpp @@ -11,7 +11,7 @@ * https://youtu.be/MJt9kSNlsU4 * * See also: - * LineGraph.hpp + * SimpleLineGraph.hpp (the simplest, non-scrolling graph) * ScrollingLineGraphMultiValue.hpp * * @section usage Usage Example @@ -44,7 +44,7 @@ #include /** @brief Drawing primitive for the graph: connected lines or individual points. */ -enum GraphPrimitive { +enum class GraphPrimitive { POINTS, ///< Draw each sample as an individual pixel LINES ///< Draw connected line segments between samples }; @@ -54,7 +54,7 @@ class ScrollingLineGraph { int _yMin = DEFAULT_MIN_Y; int _yMax = DEFAULT_MAX_Y; - boolean _isAutoYAxis = true; + bool _isAutoYAxis = true; int _xGraph = 0; // in pixels int _yGraph = 0; // in pixels @@ -63,14 +63,14 @@ class ScrollingLineGraph { int* _circularBuffer; int _curWriteIndex = 0; - boolean _isBufferFull = false; + bool _isBufferFull = false; int _bufferSize = -1; String _dataLabel = ""; - GraphPrimitive _drawGraphPrimitive = LINES; + GraphPrimitive _drawGraphPrimitive = GraphPrimitive::LINES; - boolean _drawAxis = true; - boolean _drawLegend = true; + bool _drawAxis = true; + bool _drawLegend = true; public: static const int DEFAULT_MIN_Y = 0; @@ -87,7 +87,13 @@ class ScrollingLineGraph { _widthGraph = graphWidth; _heightGraph = graphHeight; + // The buffer holds one sample per horizontal pixel of the drawable area. + // Guard against a non-positive size (e.g., if xGraph >= width) so we + // never call new int[<= 0]. _bufferSize = _widthGraph - _xGraph; + if (_bufferSize < 1) { + _bufferSize = 1; + } _circularBuffer = new int[_bufferSize]; // initialize all the readings to 0: @@ -97,9 +103,9 @@ class ScrollingLineGraph { } ~ScrollingLineGraph() { - if (_circularBuffer != NULL) { + if (_circularBuffer != nullptr) { delete[] _circularBuffer; - _circularBuffer = NULL; + _circularBuffer = nullptr; _bufferSize = -1; } } @@ -107,11 +113,11 @@ class ScrollingLineGraph { ScrollingLineGraph(const ScrollingLineGraph&) = delete; ScrollingLineGraph& operator=(const ScrollingLineGraph&) = delete; - void setDrawLegend(boolean drawLegendOn) { _drawLegend = drawLegendOn; } + void setDrawLegend(bool drawLegendOn) { _drawLegend = drawLegendOn; } - void setDrawAxis(boolean drawAxisOn) { _drawAxis = drawAxisOn; } + void setDrawAxis(bool drawAxisOn) { _drawAxis = drawAxisOn; } - void setAutoYAxis(boolean autoYAxisOn) { _isAutoYAxis = autoYAxisOn; } + void setAutoYAxis(bool autoYAxisOn) { _isAutoYAxis = autoYAxisOn; } void setMinMaxY(int minY, int maxY) { _yMin = min(minY, maxY); @@ -214,7 +220,7 @@ class ScrollingLineGraph { int sensorVal = _circularBuffer[i]; // draw the line (or point) - if (_drawGraphPrimitive == POINTS) { + if (_drawGraphPrimitive == GraphPrimitive::POINTS) { drawPoint(disp, xPos, sensorVal); } else { if (xPos > _xGraph) { @@ -231,7 +237,7 @@ class ScrollingLineGraph { int sensorVal = _circularBuffer[i]; //draw the line (or point) - if (_drawGraphPrimitive == POINTS) { + if (_drawGraphPrimitive == GraphPrimitive::POINTS) { drawPoint(disp, xPos, sensorVal); } else { // prevSensorVal carries across the buffer boundary correctly diff --git a/src/ScrollingLineGraphMultiValue.hpp b/src/ScrollingLineGraphMultiValue.hpp index bf7fbe7..636cd7b 100644 --- a/src/ScrollingLineGraphMultiValue.hpp +++ b/src/ScrollingLineGraphMultiValue.hpp @@ -6,20 +6,20 @@ * Supports multiple data series, each rendered with a different PointSymbol. * Includes auto-scaling Y-axis, optional axis labels, and a legend. * - * This is the more feature-rich successor to the MultiValueScrollingLineGraph - * class in LineGraph.hpp. If you only need one data series rendered as lines - * (not symbols), see ScrollingLineGraph.hpp instead. + * If you only need one data series rendered as lines (not symbols), see + * ScrollingLineGraph.hpp. For the simplest, non-scrolling graph, see + * SimpleLineGraph.hpp. * * Video demo: * https://youtu.be/EJDewh8Y1TY * * See also: - * LineGraph.hpp + * SimpleLineGraph.hpp * ScrollingLineGraph.hpp * * @section usage Usage Example * @code - * PointSymbol symbols[] = { CIRCLE, SQUARE }; + * PointSymbol symbols[] = { PointSymbol::CIRCLE, PointSymbol::SQUARE }; * ScrollingLineGraphMultiValue graph(2, symbols); * graph.setAutoYAxis(true); * @@ -49,7 +49,7 @@ class ScrollingLineGraphMultiValue { protected: - GraphLine** _graphLines = NULL; + GraphLine** _graphLines = nullptr; int _numLines = -1; int _yMin = 0; @@ -60,9 +60,9 @@ class ScrollingLineGraphMultiValue { int _widthGraph = 128; // in pixels int _heightGraph = 64; // in pixels - boolean _drawAxis = true; - boolean _isAutoYAxis = true; - boolean _drawLegend = true; + bool _drawAxis = true; + bool _isAutoYAxis = true; + bool _drawLegend = true; public: static const int DEFAULT_SYMBOL_SIZE = 4; @@ -94,7 +94,7 @@ class ScrollingLineGraphMultiValue { } ~ScrollingLineGraphMultiValue() { - if (_graphLines != NULL) { + if (_graphLines != nullptr) { // Each _graphLines[i] was allocated with `new` (not `new[]`), // so we use `delete` (not `delete[]`) // From https://stackoverflow.com/a/4194228/388117 @@ -103,20 +103,26 @@ class ScrollingLineGraphMultiValue { } delete[] _graphLines; - _graphLines = NULL; + _graphLines = nullptr; _numLines = -1; } } - void setDrawLegend(boolean drawLegendOn) { + // Prevent copying — this class owns dynamically allocated GraphLine + // objects, and a shallow copy would cause double-free crashes when both + // copies are destroyed. (Matches GraphLine, ScrollingLineGraph, etc.) + ScrollingLineGraphMultiValue(const ScrollingLineGraphMultiValue&) = delete; + ScrollingLineGraphMultiValue& operator=(const ScrollingLineGraphMultiValue&) = delete; + + void setDrawLegend(bool drawLegendOn) { _drawLegend = drawLegendOn; } - void setDrawAxis(boolean drawAxisOn) { + void setDrawAxis(bool drawAxisOn) { _drawAxis = drawAxisOn; } - void setAutoYAxis(boolean autoYAxisOn) { + void setAutoYAxis(bool autoYAxisOn) { _isAutoYAxis = autoYAxisOn; } diff --git a/src/Shape.hpp b/src/Shape.hpp index ead565a..30febb9 100644 --- a/src/Shape.hpp +++ b/src/Shape.hpp @@ -12,7 +12,7 @@ * https://youtu.be/-dLtvL1bOn8 * * See also: - * LineGraph.hpp + * SimpleLineGraph.hpp * ScrollingLineGraph.hpp * * @section usage Usage Example @@ -65,7 +65,7 @@ * fragmentation that Arduino String comparisons would cause on memory-constrained * boards like the Uno (2KB SRAM). */ -enum ShapeType { +enum class ShapeType { SHAPE, RECTANGLE, ROUND_RECT, @@ -401,8 +401,6 @@ class Shape { * @return false If the shape does not overlap */ virtual bool overlaps(const Shape& shape) const { - //Serial.println("We are in overlaps shape!"); - // based on https://stackoverflow.com/a/4098512 return !(getRight() < shape._x || getBottom() < shape._y || @@ -437,7 +435,7 @@ class Shape { * @return ShapeType */ virtual ShapeType getShapeType() const { - return SHAPE; + return ShapeType::SHAPE; } /** @@ -470,16 +468,21 @@ class Shape { } /** - * @brief Static function that calculates the Euclidean distance between two points + * @brief Static function that calculates the Euclidean distance between two points. * - * @param x1 - * @param y1 - * @param x2 - * @param y2 - * @return float + * @param x1 X coordinate of the first point + * @param y1 Y coordinate of the first point + * @param x2 X coordinate of the second point + * @param y2 Y coordinate of the second point + * @return float The straight-line distance between (x1,y1) and (x2,y2) */ static float distance(int x1, int y1, int x2, int y2) { - return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2)); + // Use multiplication rather than pow(..., 2): on AVR, pow() is a slow + // floating-point call, while dx*dx is fast and exact. The long cast + // avoids int overflow before the values are widened for sqrt(). + long dx = x2 - x1; + long dy = y2 - y1; + return sqrt((float)(dx * dx + dy * dy)); } }; @@ -521,7 +524,7 @@ class Rectangle : public Shape { } ShapeType getShapeType() const override { - return RECTANGLE; + return ShapeType::RECTANGLE; } const char* getName() const override { @@ -604,7 +607,7 @@ class RoundRect : public Rectangle { } ShapeType getShapeType() const override { - return ROUND_RECT; + return ShapeType::ROUND_RECT; } const char* getName() const override { @@ -764,7 +767,7 @@ class Triangle : public Shape { int getY2() const { return _y2; } ShapeType getShapeType() const override { - return TRIANGLE; + return ShapeType::TRIANGLE; } const char* getName() const override { @@ -824,9 +827,8 @@ class Circle : public Shape { * Uses distance between center points compared to sum of radii, * which is more precise than bounding-box overlap for circles. * - * @param circle - * @return true - * @return false + * @param circle The other circle to test against + * @return true if the two circles overlap, false otherwise */ bool overlaps(const Circle& circle) const { float distanceFromCenterPoints = Shape::distance(getCenterX(), getCenterY(), @@ -840,16 +842,15 @@ class Circle : public Shape { * uses the more precise radius-based overlap. Otherwise falls back * to the parent bounding-box overlap function. * - * @param shape - * @return true - * @return false + * @param shape The other shape to test against + * @return true if the shapes overlap, false otherwise */ bool overlaps(const Shape& shape) const override { // Use ShapeType enum instead of string comparison to avoid // heap-allocating Arduino Strings on every collision check. // Ball returns CIRCLE for its ShapeType, so Ball-to-Circle and // Ball-to-Ball collisions correctly use radial overlap. - if (shape.getShapeType() == CIRCLE) { + if (shape.getShapeType() == ShapeType::CIRCLE) { return this->overlaps((const Circle&)shape); } @@ -902,7 +903,7 @@ class Circle : public Shape { } ShapeType getShapeType() const override { - return CIRCLE; + return ShapeType::CIRCLE; } /** @@ -945,7 +946,7 @@ class Ball : public Circle { * @brief Check if the ball has reached a vertical boundary. * @deprecated Use isOutOfBoundsY() instead, which is available on all shapes. */ - boolean checkYBounce(int yMin, int yMax) const { + bool checkYBounce(int yMin, int yMax) const { return isOutOfBoundsY(yMin, yMax); } @@ -953,7 +954,7 @@ class Ball : public Circle { * @brief Check if the ball has reached a horizontal boundary. * @deprecated Use isOutOfBoundsX() instead, which is available on all shapes. */ - boolean checkXBounce(int xMin, int xMax) const { + bool checkXBounce(int xMin, int xMax) const { return isOutOfBoundsX(xMin, xMax); } @@ -961,7 +962,7 @@ class Ball : public Circle { // correctly uses radial collision for Ball-to-Circle and // Ball-to-Ball overlap checks ShapeType getShapeType() const override { - return CIRCLE; + return ShapeType::CIRCLE; } const char* getName() const override { diff --git a/src/SimpleLineGraph.hpp b/src/SimpleLineGraph.hpp new file mode 100644 index 0000000..fd46e9e --- /dev/null +++ b/src/SimpleLineGraph.hpp @@ -0,0 +1,177 @@ +/** + * @file SimpleLineGraph.hpp + * @brief The simplest possible line graph for monochrome OLEDs: a + * non-scrolling graph that fills left to right and restarts at x=0. + * + * Unlike ScrollingLineGraph, this class keeps NO history buffer. Each value + * is drawn immediately as a vertical bar, and the pixels stay on the screen + * until the pen reaches the right edge — at which point the graph area is + * cleared and drawing restarts from the left. The OLED's framebuffer itself + * is the "memory," so the class needs almost no state of its own. + * + * This makes it a great first graph for students: there is no circular buffer, + * no auto-scaling, and no per-frame redraw to reason about. + * + * @note Because the graph persists on screen between frames, you must NOT call + * display.clearDisplay() in your loop() — that would erase the graph. + * This is the opposite of ScrollingLineGraph, which redraws every frame. + * (See the usage example below.) + * + * @note For a graph that scrolls instead of restarting, see ScrollingLineGraph.hpp. + * + * @section usage Usage Example + * @code + * SimpleLineGraph graph(128, 64); // width, height in pixels + * + * void loop() { + * // Draws one bar and advances the pen. No clearDisplay() here! + * graph.addValue(display, analogRead(A0)); + * display.display(); + * delay(5); + * } + * @endcode + * + * @author Jon E. Froehlich + * + * Learn more in the Physical Computing textbook: + * - OLED displays: https://makeabilitylab.github.io/physcomp/advancedio/oled.html + * + * By Professor Jon E. Froehlich + * Director, Makeability Lab: https://makeabilitylab.cs.washington.edu/ + * Physical Computing Textbook: https://makeabilitylab.github.io/physcomp/ + * + * SPDX-License-Identifier: MIT + */ +#pragma once +#include +#include +#include + +class SimpleLineGraph { + protected: + int _xGraph; ///< X origin of the graph area in pixels + int _yGraph; ///< Y origin of the graph area in pixels + int _widthGraph; ///< Width of the graph area in pixels + int _heightGraph; ///< Height of the graph area in pixels + + int _xPos; ///< Current horizontal pen position (the only "state") + + int _minValue = 0; ///< Input value that maps to the bottom of the graph + int _maxValue = 1023; ///< Input value that maps to the top of the graph + + public: + /** + * @brief Construct a full-area graph at the top-left of the display. + * @param graphWidth Width of the graph in pixels (e.g., 128) + * @param graphHeight Height of the graph in pixels (e.g., 64) + */ + SimpleLineGraph(int graphWidth, int graphHeight) + : SimpleLineGraph(0, 0, graphWidth, graphHeight) { + // purposefully empty + } + + /** + * @brief Construct a graph at a given location and size. + * @param xGraph X origin (top-left) in pixels + * @param yGraph Y origin (top-left) in pixels + * @param graphWidth Width of the graph in pixels + * @param graphHeight Height of the graph in pixels + */ + SimpleLineGraph(int xGraph, int yGraph, int graphWidth, int graphHeight) { + _xGraph = xGraph; + _yGraph = yGraph; + _widthGraph = graphWidth; + _heightGraph = graphHeight; + _xPos = xGraph; + } + + /** + * @brief Set the range of input values that map onto the graph height. + * + * Values are clamped to this range by Arduino's map(). Defaults to + * 0–1023 (a 10-bit ADC, like analogRead() on an Arduino Uno). + * + * @param minValue Value drawn at the bottom of the graph + * @param maxValue Value drawn at the top of the graph + */ + void setMinMaxValue(int minValue, int maxValue) { + _minValue = min(minValue, maxValue); + _maxValue = max(minValue, maxValue); + } + + int getMinValue() const { return _minValue; } + + int getMaxValue() const { return _maxValue; } + + /** + * @brief Plot one value as a vertical bar and advance the pen. + * + * Draws immediately to the display — there is no separate draw() call. + * When the pen reaches the right edge, the graph area is cleared and + * drawing restarts from the left (a non-scrolling, "sweep" graph). + * + * @param disp The Adafruit_SSD1306 display to draw on + * @param value The value to plot (clamped to [getMinValue(), getMaxValue()]) + */ + void addValue(Adafruit_SSD1306& disp, int value) { + // If the pen has reached the right edge, clear the graph area and + // start over at the left. We clear only our own rectangle (not the + // whole display) so an optional status bar or label elsewhere survives. + if (_xPos >= _xGraph + _widthGraph) { + disp.fillRect(_xGraph, _yGraph, _widthGraph, _heightGraph, SSD1306_BLACK); + _xPos = _xGraph; + } + + // Map the value to a bar height, then draw the bar from the baseline up. + int barHeight = map(value, _minValue, _maxValue, 0, _heightGraph); + int yTop = _yGraph + _heightGraph - barHeight; + + // drawFastVLine is the optimized vertical-line primitive (no angular math). + // https://learn.adafruit.com/adafruit-gfx-graphics-library/graphics-primitives#drawing-lines-2002778-4 + disp.drawFastVLine(_xPos, yTop, barHeight, SSD1306_WHITE); + + _xPos++; + } + + /** + * @brief Get the current horizontal pen position (next column to draw). + * @return int The current x position in pixels + */ + int getCurrentX() const { return _xPos; } + + /** + * @brief Get the width of the graph + * @return int Width of the graph in pixels + */ + int getWidth() const { return _widthGraph; } + + /** + * @brief Get the height of the graph + * @return int Height of the graph in pixels + */ + int getHeight() const { return _heightGraph; } + + /** + * @brief Get the left (x) location of the graph + * @return int The left location of the graph + */ + int getLeft() const { return _xGraph; } + + /** + * @brief Get the right (x) location of the graph + * @return int The right location (x) of the graph + */ + int getRight() const { return _xGraph + _widthGraph; } + + /** + * @brief Get the bottom of the graph (y) + * @return int + */ + int getBottom() const { return _yGraph + _heightGraph; } + + /** + * @brief Get the top of the graph + * @return int + */ + int getTop() const { return _yGraph; } +};