A library for creating time-based animations for Arduino R4 WiFi...for one hour.
Image by Rizwan Syed
- Introduction
- Installing the Library
- Real Time Clock (RTC)
- Servo Motors
- Notifier Animation System
- Example Projects
- Using the LED Matrix Display
This library introduces methods for Project 3 to assist with:
- Reading and using information from the Real Time Clock
- Controlling servos and other actuators as outputs
- Creating keyframe-based animations for smooth motion control
- Using the LED Matrix for debugging and visualization
The library combines time-based triggers with keyframe animation capabilities, allowing you to create complex kinetic projects.
Current version: 1.1.6
- Open the Arduino Library Manager
- Search for "YouveBeenNotified" or "DIGF"
- Press the Install Button (Note: If you have previously installed the library, press Update)
The Arduino R4 WiFi features a built-in Real Time Clock (RTC) that allows your projects to track time even when disconnected from the internet. When working with time-based projects that need to run for extended periods (like our 1-hour requirement), the RTC provides reliable timing for triggering animations.
For a full overview of the RTC on the Arduino R4 WiFi, see the official documentation.
This class handles time storage and representation:
// Create a time object (day, month, year, hours, minutes, seconds, day of week, DST)
RTCTime myTime(14, Month::MARCH, 2025, 12, 30, 0, DayOfWeek::FRIDAY, SaveLight::SAVING_TIME_ACTIVE);
// Get values from a time object
int currentHour = myTime.getHour();
int currentMinute = myTime.getMinutes();
int currentSecond = myTime.getSeconds();void setup() {
// Initialize the RTC
RTC.begin();
// Set the initial time (important for alarm functionality)
RTCTime initialTime(14, Month::MARCH, 2025, 12, 0, 0, DayOfWeek::FRIDAY, SaveLight::SAVING_TIME_ACTIVE);
RTC.setTime(initialTime);
}void loop() {
// Create a variable to store the current time
RTCTime currentTime;
// Get the current time from the RTC
RTC.getTime(currentTime);
// Use time components
int hour = currentTime.getHour();
int minute = currentTime.getMinutes();
int second = currentTime.getSeconds();
// Display or use the time...
}Servo motors provide precise angular control for your projects and are ideal for creating physical notifications and interactions.
- Positional (Hobby) Servos: Can rotate to specific angles (typically 0-180°). These are the type we're using in this project.
- Continuous Rotation Servos: Modified to rotate continuously like a normal motor, but with speed control rather than position control.
The SG90 Micro Servo used in this project has these key specifications:
- Operating voltage: 4.8V to 6V
- Operating speed: 0.1 sec/60° at 4.8V
- Stall torque: 1.6 kg-cm at 4.8V
- Weight: 9g
- Dimensions: 22.2 x 11.8 x 31 mm
- Angular range: 0-180 degrees
- Standard servo range is 0-180 degrees
- Servo horns (attachments) can modify the effective range of movement when connected to mechanisms
- We're using micro servos in this project, which are small and lightweight
- Each servo has a speed (how fast can it move) and a torque (how much mass can it move) rating. Consult it's documentation
A standard servo has three wires:
- Brown/Black: Ground (GND)
- Red: Power (+5V)
- Yellow/Orange: Control/Signal (connect to any Arduino pin)
Typical servo wiring diagram showing the three essential connections
For complete wiring diagrams for both single and dual servo configurations, see the Wiring Diagrams guide.
The Servo library comes pre-installed with the Arduino IDE, so you don't need to install it through the Library Manager. Just include it in your sketch and you're ready to go.
Key Servo Commands:
attach(pin): Connects the servo object to the specified pin. Also accepts optional min/max pulse width parameters to calibrate the servo range.detach(): Disconnects the servo from its pin. This disengages the motor, but keeps a connection to arduino. Can be re-connected by calling attach() againwrite(angle): Moves the servo to the specified angle (0-180 degrees).read(): Returns the last angle written to the servo (0-180).
Here's a simple example that moves a servo between two positions without using delay():
#include <Servo.h>
Servo myServo; // Create a servo object
int angle = 0; // Current servo position
unsigned long prevTime; // For tracking elapsed time
int interval = 1000; // Time between position changes (ms)
void setup() {
myServo.attach(9); // Attach servo to pin 9
prevTime = millis(); // Initialize timing
}
void loop() {
// Non-blocking timing approach
unsigned long currentTime = millis();
if (currentTime - prevTime >= interval) {
prevTime = currentTime; // Reset the timer
// Toggle between 0 and 90 degrees
angle = (angle == 0) ? 90 : 0;
// Move the servo
myServo.write(angle);
}
// Other Things
}Servos can be connected to various mechanisms to create different types of movement:
Mechanical
- Linear Actuator: Converts rotational motion to linear (straight line) movement. See Linear Actuator Guide for build instructions.
- Kinetic Wearables Toolkit: Collection of ready-made wearable servo mechanisms and attachments optimized for attaching to the body.
- Thingiverse Micro Servo Mechanisms: Collection of 3D printable mechanisms and attachments designed for micro servos
Other Materials
- Paper and Card Stock: Create fluttering wings, fans, or flexible sculptures
- Fabric and Textiles: Rippling banners, stretchy materials, or suspended cloth that drapes
- Natural Materials: Feathers, leaves, or lightweight branches for organic movement
- Wire and Mesh: Bendable sculptures that maintain shape while moving
- Lightweight Plastics: Mylar sheets, cellophane, or thin flexible acrylic for fluid motion
- String/Thread: Suspended elements that swing, bounce, or create pendulum effects
The You've Been Notified library includes a keyframe animation system that makes it easy to create complex servo movements.
A sequence of positions and timing information that defines a motion pattern.
// Create an animation named "wave"
KeyframeAnimation waveMotion("wave");
// Add keyframes: (position, time in milliseconds)
waveMotion.addKeyFrame(0, 0); // Start at 0 degrees
waveMotion.addKeyFrame(90, 1000); // Move to 90 degrees over 1 second
waveMotion.addKeyFrame(30, 2000); // Move to 30 degrees over 1 secondManages animations for servo motors, handling all the timing and interpolation.
// Create a servo notifier
ServoNotifier notifier(myServo); // Pass in your Servo object
// Add the animation to the notifier
notifier.addAnimation(waveMotion);
// Play the animation
notifier.playAnimation("wave", LOOP); // Name and playback modeisPlaying(): Returns true if an animation is currently activegetCurrentAnimationName(): Returns the name of the current animationgetPlaybackMode(): Returns the current playback mode (ONCE, LOOP, BOOMERANG)getElapsedTime(): Returns milliseconds since animation startedgetTotalDuration(): Returns total length of current animation in milliseconds
getValue(): Returns the current calculated position valuehasChanged(): Returns true if the value has updated since last checkgetStartValue(): Returns the first keyframe value of current animationgetEndValue(): Returns the last keyframe value of current animation
hasAnimation(name): Checks if an animation with given name existsgetAnimationCount(): Returns total number of stored animationsgetAnimationNames(): Returns array of all animation namesgetCurrentSpeed(): Returns current global speed multiplier
- ONCE: Plays the animation once and stops
- LOOP: Repeats the animation continuously
- BOOMERANG: Plays forward, then backward, then repeats
Each animation is identified by a unique name string:
- Names must be unique within a notifier
- Used to play, stop, or manage specific animations
- Case-sensitive (e.g., "wave" and "Wave" are different)
// Creating animations with names
KeyframeAnimation upDown("upDown"); // Name: "upDown"
KeyframeAnimation wave("wave"); // Name: "wave"
// Playing by name
notifier.playAnimation("upDown", ONCE);
notifier.stopAnimation("wave");
// Check if exists
if (notifier.hasAnimation("upDown")) {
// Animation exists
}- Create the servo and notifier objects
Servo myServo;
ServoNotifier notifier(myServo);- Create and name an animation
KeyframeAnimation wave("wave");- Attach servo to pin
myServo.attach(9); // Attach to pin 9 (or your chosen pin)- Add keyframes (position, time in ms)
wave.addKeyFrame(0, 0); // Start position
wave.addKeyFrame(90, 1000); // Move to 90° over 1 second
wave.addKeyFrame(45, 2000); // Move to 45° over next second
wave.addKeyFrame(0, 3000); // Return to start over final second- Add animation to notifier
notifier.addAnimation(wave);- Start the animation
notifier.playAnimation("wave", LOOP);- Update the notifier and servo
notifier.update(); // Calculate new positions
if (notifier.hasChanged()) {
myServo.write(notifier.getValue());
}- Create servo and notifier objects
Servo myServo;
ServoNotifier notifier(myServo);
KeyframeAnimation wave("wave");
KeyframeAnimation pulse("pulse");- Attach servo
myServo.attach(9); // Attach to pin 9 (or your chosen pin)- Add keyframes to both animations
// Wave animation
wave.addKeyFrame(0, 0);
wave.addKeyFrame(90, 1000);
wave.addKeyFrame(0, 2000);
// Pulse animation
pulse.addKeyFrame(45, 0);
pulse.addKeyFrame(60, 500);
pulse.addKeyFrame(45, 1000);- Add both animations to notifier
notifier.addAnimation(wave);
notifier.addAnimation(pulse);- Start with one animation
notifier.playAnimation("wave", LOOP);- Update animations
notifier.update(); // Calculate new positions
if (notifier.hasChanged()) {
myServo.write(notifier.getValue());
}#include <Servo.h>
#include "YouveBeenNotified.h"
// Create servo and notifier
Servo myServo;
ServoNotifier notifier(myServo);
void setup() {
// Setup servo
myServo.attach(9);
// Create animation
KeyframeAnimation waveMotion("wave");
waveMotion.addKeyFrame(0, 0); // Start at 0 degrees
waveMotion.addKeyFrame(90, 1000); // Move to 90 degrees over 1 second
waveMotion.addKeyFrame(30, 2000); // Move to 30 degrees over 1 second
// Add animation to notifier
notifier.addAnimation(waveMotion);
// Start playing in loop mode
notifier.playAnimation("wave", LOOP);
}
void loop() {
// Update animation calculations
notifier.update();
// If the servo value has changed, update the servo
if (notifier.hasChanged()) {
myServo.write(notifier.getValue());
}
}// Example: ServoAnimation_01_Sweep
// Shows how to create a simple servo sweep using
// a single keyframe and boomerang playback mode
#include <Servo.h>
#include "YouveBeenNotified.h"
// Create servo and notifier objects
Servo myServo;
ServoNotifier notifier(myServo);
void setup() {
// Attach servo to pin 9
myServo.attach(9);
// Create a sweep animation
KeyframeAnimation sweep("sweep");
// Add single keyframe to move from 0 to 180 over 1 second
sweep.addKeyFrame(0, 0); // Start position
sweep.addKeyFrame(180, 1000); // End position
// Add animation to notifier
notifier.addAnimation(sweep);
// Play animation in boomerang mode (goes forward, then reverse)
notifier.playAnimation("sweep", BOOMERANG);
}
void loop() {
// Update animation calculations
notifier.update();
// If value changed, update servo position
if (notifier.hasChanged()) {
myServo.write(notifier.getValue());
}
}Create and switch between different animation patterns:
// Create multiple animations
KeyframeAnimation wave("wave");
KeyframeAnimation pulse("pulse");
// Add both to notifier
notifier.addAnimation(wave);
notifier.addAnimation(pulse);
// Switch between them
notifier.playAnimation("wave", LOOP);
// Later...
notifier.playAnimation("pulse", ONCE);Adjust playback speed dynamically without modifying keyframes:
// Play at double speed
notifier.setGlobalSpeed(2.0);
// Half speed
notifier.setGlobalSpeed(0.5);
// Return to normal speed
notifier.setGlobalSpeed(1.0);Smoothly transition between animations for continuous motion:
// Blend from current animation to "pulse" over 500ms
notifier.crossfadeTo("pulse", 500, LOOP);
// You can also specify the playback mode for the target animation
notifier.crossfadeTo("wave", 1000, BOOMERANG);The library provides several methods to control animation playback:
Starts an animation with the specified playback mode:
// Play by animation name
notifier.playAnimation("wave", LOOP); // Name and playback mode
// Play by animation reference
notifier.playAnimation(waveMotion, ONCE);Temporarily pause an animation while preserving its current position:
// Pause the current animation
notifier.pause();
// Resume a paused animation
notifier.resume();When paused, the animation will:
- Maintain its current position
- Keep track of how long it's been paused
- Continue from exact same position when resumed
- Adjust timing to account for the pause duration
Completely stops the current animation:
// Stop any currently playing animation
notifier.stop();When stopped, the animation will:
- Reset to the IDLE state
- Clear all animation references
- Need to be started again with playAnimation()
- Not maintain its position or timing information
Here's a complete example showing how to implement and use play, pause, and stop controls with button inputs:
#include <Servo.h>
#include "YouveBeenNotified.h"
// Create servo and notifier
Servo myServo;
ServoNotifier notifier(myServo);
// Pin assignments
const int servoPin = 9;
const int playButtonPin = 2;
const int pauseButtonPin = 3;
const int stopButtonPin = 4;
// Button state tracking
bool playButtonState = false;
bool pauseButtonState = false;
bool stopButtonState = false;
bool lastPlayButtonState = false;
bool lastPauseButtonState = false;
bool lastStopButtonState = false;
void setup() {
// Set up Serial for debugging
Serial.begin(9600);
// Initialize button pins
pinMode(playButtonPin, INPUT_PULLUP);
pinMode(pauseButtonPin, INPUT_PULLUP);
pinMode(stopButtonPin, INPUT_PULLUP);
// Attach servo to pin
myServo.attach(servoPin);
// Set up a wave animation
KeyframeAnimation wave("wave");
wave.addKeyFrame(0, 0); // Start at 0 degrees
wave.addKeyFrame(90, 1000); // Move to 90 degrees over 1 second
wave.addKeyFrame(45, 2000); // Move to 45 degrees over 1 second
wave.addKeyFrame(120, 3000); // Move to 120 degrees over 1 second
wave.addKeyFrame(0, 4000); // Return to 0 degrees over 1 second
// Add animation to notifier
notifier.addAnimation(wave);
Serial.println("Animation setup complete");
Serial.println("Press buttons to control: Play, Pause, or Stop");
}
void loop() {
// Update animation calculations
notifier.update();
// If the servo value has changed, update the servo position
if (notifier.hasChanged()) {
myServo.write(notifier.getValue());
}
// Read button states (inverted because of INPUT_PULLUP)
playButtonState = !digitalRead(playButtonPin);
pauseButtonState = !digitalRead(pauseButtonPin);
stopButtonState = !digitalRead(stopButtonPin);
// Check for Play button press
if (playButtonState && !lastPlayButtonState) {
// Start animation in LOOP mode
notifier.playAnimation("wave", LOOP);
Serial.println("Animation started");
}
// Check for Pause/Resume button press
if (pauseButtonState && !lastPauseButtonState) {
if (notifier.isPaused()) {
// Resume if currently paused
notifier.resume();
Serial.println("Animation resumed");
} else if (notifier.isPlaying()) {
// Pause if currently playing
notifier.pause();
Serial.println("Animation paused");
}
}
// Check for Stop button press
if (stopButtonState && !lastStopButtonState) {
// Stop the animation
notifier.stop();
Serial.println("Animation stopped");
}
// Update button state history
lastPlayButtonState = playButtonState;
lastPauseButtonState = pauseButtonState;
lastStopButtonState = stopButtonState;
// Print animation status when it changes
static AnimationState lastState = IDLE;
if (notifier.getState() != lastState) {
lastState = notifier.getState();
Serial.print("Animation state: ");
switch (lastState) {
case IDLE:
Serial.println("IDLE");
break;
case PLAYING:
Serial.println("PLAYING");
break;
case PAUSED:
Serial.println("PAUSED");
break;
case COMPLETED:
Serial.println("COMPLETED");
break;
}
}
}The animation system uses the following state machine for managing animations:
- IDLE → No animation playing (initial state)
- PLAYING → Animation is actively running
- PAUSED → Animation is temporarily frozen
- COMPLETED → Animation (ONCE mode only) has finished
Possible state transitions:
- IDLE → PLAYING: Call
playAnimation() - PLAYING → PAUSED: Call
pause() - PAUSED → PLAYING: Call
resume() - PLAYING → COMPLETED: Automatic when a ONCE animation finishes
- PLAYING → IDLE: Call
stop() - PAUSED → IDLE: Call
stop() - COMPLETED → IDLE: Call
stop()orplayAnimation()
You can check the current state with methods like isPlaying(), isPaused(), isCompleted(), or the more general getState() method.
The library includes several example sketches to demonstrate key concepts:
| Category | Example | Description |
|---|---|---|
| RTC Basics | RTC_01_CountMinutes | Basic minute counter displayed on LED matrix |
| RTC_02_CycleShapes | Cycle through different shapes on the minute | |
| RTC_03_IncrementLEDs | Sequentially fill LEDs as time passes | |
| RTC_04_TimeCue | Trigger special events at specific times | |
| Servo Control | RTCservo_01_1Servo_AngleList | Introduction to servo control methods |
| RTCservo_02_2Servos_AngleList | Move servo based on current time | |
| RTCservo_03_1Servo_RandomAngles | Control servo with random positions | |
| RTCservo_04_2Servos_RandomAngles | Control two servos with random positions | |
| RTC Servo Animations | RTC_ServoAnimation_01_1ServoSimple | Simple example combining RTC with servo animations |
| RTC_ServoAnimation_02_2ServosSimple | Control two servo animations with RTC | |
| RTC_ServoAnimation_03_1Servo_TimeCues | Trigger animations at specific minute cues | |
| RTC_ServoAnimation_04_2Servos_1Switch1Constant | Advanced dual servo animation control |
For detailed instructions and code walkthroughs:
The Arduino R4 WiFi's 12x8 LED matrix is useful for displaying information and debug data. For detailed documentation on the ArduinoGraphics library for the LED matrix, see the Arduino Graphics Guide.
#include "ArduinoGraphics.h" //must be defined before Arduino_LED_Matrix.h
#include "Arduino_LED_Matrix.h"
ArduinoLEDMatrix matrix;
void setup() {
if (!matrix.begin()) {
Serial.println("Matrix initialization failed!");
while (1);
}
}Here are some common display patterns you can use:
void displayNumber(int value) {
matrix.beginDraw();
matrix.clear();
// Configure text properties
matrix.stroke(0xFFFFFFFF);
matrix.textFont(Font_5x7);
// Center the number
String numStr = String(value);
int xPos = (matrix.width() - (numStr.length() * 5)) / 2;
matrix.text(numStr, xPos, 1);
matrix.endDraw();
}void showProgress(int percentage) {
matrix.beginDraw();
matrix.clear();
// Map percentage to matrix width
int fillWidth = map(percentage, 0, 100, 0, matrix.width());
// Draw progress bar
for(int x = 0; x < fillWidth; x++) {
matrix.point(x, 3);
matrix.point(x, 4);
}
matrix.endDraw();
}-
Drawing Operations
- Always wrap drawing code between
beginDraw()andendDraw() - Clear the display with
clear()before drawing new content - Use
stroke()to set the LED state (0xFFFFFFFF for on)
- Always wrap drawing code between
-
Text Display
- Use
Font_5x7for numbers and large text - Use
Font_4x6for scrolling messages - Center text by calculating position based on string length
- Use
-
Animation
- Use
millis()for timing instead ofdelay() - Keep scrolling messages brief
- Update display only when content changes
- Use
