//##################################################################
//# iNoWa - the indoor NorthWall #
//##################################################################
// NANO-BLE Firmware für die
// Steuerung des LED Systems
//
// by Constantin Hagen, 2021
// Open Source! GNU GPL v3.0
//
// Version 1.1.6
// 20.02.2022
//
// Version 1.2.0 by Thomas Raddatz ([email protected])
// 23.12.2023
// Changed: Changed program to run on Arduino Nano 33 BLE
//##################################################################
// App Command:
// The command sent by the Android app has the following format:
// <led_num>:<color>/<brightness>!<effects><eol>
//
// led_num - LED number, starting at 1.
// Range: 1 - NUM_LEDS
// color - Color.
// Possible values:
// r=red, g=green, b=blue or l=purple.
// brightness - Brightness.
// Must be between 0 (min) and 255 (max).
// effects - Optional. Special effects.
// Possible values:
// P=Pulse (fading) or S=Sparkle (glitter).
// eol - End of line.
// Possible values:
// #=Boulder or *=Snake game
//
// Buttons:
// Red button - Reset.
// Green button - Mode switch. Modes:
// 0=Off. All LEDs are switched off.
// 1=Red. All LEDs are set to RED.
// 2=Green. All LEDs are set to GREEN.
// 3=Blue. All LEDs are set to BLUE.
// 4=Rainbow. All LEDs display rainbow colors.
// 5=Boulder. LEDs are set according to the last app command.
//
// Limitations:
// The maximum number of boulder moves are defined by I_MAX_MOVES.
//
// Error Handling:
// In case of an error, the application sets all LEDs of the strip
// to RED and flashes the whole strip 3 times.
// Then the first LED is set to RED.
// The error number is displayed by n yellow LEDs.
//
// Questions:
// 1. What is the '.' used for in the incoming app command?
// 2. Why does enabling "signalErrorState()" break the application
// if executed in the WOKWI simulator?
//##################################################################
// External libraries
#include <EasyButton.h>
#include <Adafruit_NeoPixel.h>
#include <Random16.h>
// #include <ArduinoBLE.h>
Random16 rnd16;
#define random16 rnd16.get
// Hardware settings.
#define READY_LED 2 // Pin of "Power-On" LED.
#define LED_STRIPE_PIN 13 // Pin of 'WS2811' LED stripe.
#define TEST_BUTTON_PIN 11 // Test Button.
#define SPEAKER_PIN 12 // Loudspeaker (buzzer).
#define NUM_LEDS 80 // Number of LEDs of the LED stripe. (WOKWI matrix is: 8 x 10 = 80)
Adafruit_NeoPixel leds(NUM_LEDS, LED_STRIPE_PIN, NEO_GRB + NEO_KHZ800);
// Argument 1 = Number of pixels in NeoPixel leds
// Argument 2 = Arduino pin number (most are valid)
// Argument 3 = Pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
// NEO_RGBW Pixels are wired for RGBW bitstream (NeoPixel RGBW products)
EasyButton testButton(TEST_BUTTON_PIN, 50, true, true); // pullup & invert -> true; debounce: 50ms
// NeoPixel LED colors
#define NP_OFF leds.Color(0, 0, 0)
#define NP_RED leds.Color(255, 0, 0)
#define NP_GREEN leds.Color(0, 255, 0)
#define NP_BLUE leds.Color(0, 0, 255)
#define NP_WHITE leds.Color(255, 255, 255)
#define NP_PURPLE leds.Color(194, 66, 245)
#define NP_YELLOW leds.Color(255, 228, 0)
// App command markers
#define COLOR_SEPARATOR ':' // Number separator. The following byte is a character.
#define HOLD_SEPARATOR '/' // Character separator. The following byte is a number.
#define PULSE_MARKER 'P' // Enabled fading if set to 'P'.
#define SPARKLE_MARKER 'S' // Enabled glittering if set to 'S'.
#define BRIGHTNESS_MARKER '!' // Starts the brightness value.
#define EOL_SNAKE_GAME '*' // End of transmission of Snake game.
#define EOL_BOULDER '#' // End of transmission of boulder.
#define RED "r" // Red
#define BLUE "b" // Blue
#define GREEN "g" // Green
#define PURPLE "l" // Purple
// Configuration values
#define BRIGHTNESS_TEST 127 // Brightness for startup blink test.
#define BRIGHTNESS_DEFAULT 200 // Brightness, if fading is disabled.
#define BRIGHTNESS_MIN 100 // Minimum brightness, if fading is enabled.
#define BRIGHTNESS_MAX 255 // Maximum brightness, if fading is enabled.
#define FADE_AMOUNT 5 // Amount of fading. Original value: 5
#define FADE_SPEED 20 // Fading speed. Low values = fast, hight values = slow. Original value: 9
#define RAINBOW_FIRST_HUE 0 // Hue of first pixel, 0-65535, representing one full cycle of the color wheel.
#define RAINBOW_REPS 7 // Number of cycles of the color wheel over the length of the strip.
#define RAINBOW_SATURATION 255 // Saturation (optional)
#define RAINBOW_BRIGHTNESS 255 // Brightness (optional)
#define RAINBOW_GAMMIFY true// If true (default), apply gamma correction to colors for better appearance.
// Error status
#define E_ERROR_FLAG_LED 0 // Error indicator LED.
#define E_ERROR_OFFS_LED 2 // Offset error indicator LEDs.
#define E_TOO_MANY_MOVES 1 // Error: Too many moves.
#define E_INTERVALL 500 // Error flag flashing interval in milliseconds.
#define NP_E_ERROR NP_RED // Color of the error LED at position 0.
#define NP_E_ERRNUM NP_YELLOW // Color of the error number LED at position 2-n.
// Application Status Variables.
bool isEventLoopMessage = false; // Ensures message is sent only once.
bool glitterEnabled = false; // Enable glitter effect.
bool fadingEnabled = false; // Enable fadingEnabled effect.
int fadeDirection = -1; // Fade down.
int brightness = BRIGHTNESS_MAX; // Set max. brightness for fadingEnabled effect.
int testButton_counter = 0; // Test button counter for switching mode.
bool isErrorState = false; // Error indicator. Stops processing commands.
// Variables for reading data from the Bluetooth interface.
#define I_LED 0 // Index of LED number property.
#define I_COLOR 1 // Index of COLOR code property.
#define I_MAX_MOVES 25 // Maximum number of boulder moves.
#define I_MAX_LED_PROPS 2 // Maximum number of LED configuration properties.
String inValue = ""; // Incoming value of Android app command.
int inMoveNum = 0; // Number of boulder move of Android app command.
String inLEDConfig
[I_MAX_MOVES]
[I_MAX_LED_PROPS]; // Array to store a LED configuration properties of Android app command.
char inPulseMarker = ' '; // Incoming marker for fading effect.
char inSparkleMarker = ' '; // Incoming marker for glitter effect.
int inBrightness = 0; // Incoming brightness value.
// Sample command for debugging purposes. The command is red
// by procedure WokwiSerial_read().
// led:r/led:b/led:g/brightness!SP[*|#]
#define SIZE_CMD_BOULDER 64
#define END_OF_DBG_CMD '@'
int cmdCounter = 0;
char cmdBoulder[SIZE_CMD_BOULDER] =
{
'6', '1', // LED #61
COLOR_SEPARATOR,
'g',
HOLD_SEPARATOR,
'6', '3', // LED #63
COLOR_SEPARATOR,
'g',
HOLD_SEPARATOR,
'2','7', // LED #27
COLOR_SEPARATOR,
'b',
HOLD_SEPARATOR,
'3','5', // LED #35
COLOR_SEPARATOR,
'l',
HOLD_SEPARATOR,
'4','2', // LED #42
COLOR_SEPARATOR,
'b',
HOLD_SEPARATOR,
'9', // LED #9
COLOR_SEPARATOR,
'r',
HOLD_SEPARATOR,
'2','5','5', // Max. brightness
BRIGHTNESS_MARKER,
// SPARKLE_MARKER, // Glitter effect.
// PULSE_MARKER, // Fading effect.
EOL_BOULDER, // EOL bloulder.
END_OF_DBG_CMD // End of debug command.
};
// =================================================================
// Standard Arduino application methods.
// =================================================================
/**
* Setup procedure. Executed once on application start.
*/
void setup() {
// Enable debugging
Serial.begin(115200); // Any baud rate should work
while (!Serial);
log("Starting setup...");
// Initialize LED strip.
leds_initialize();
// Prepare status LED.
pinMode(READY_LED, OUTPUT);
// Test all lEDs at startup.
startupLEDsTest();
// Play welcome melody.
playWelcomeMelody();
// Register listener for "Mode" button.
testButton.begin();
testButton.onPressed(testButton_pressed);
testButton_counter = 0;
log("...finished setup.");
}
/**
* Main event loop.
*/
void loop() {
// Signal "Power-On".
digitalWrite(READY_LED, HIGH);
if (!isEventLoopMessage) {
log("Starting event loop.");
isEventLoopMessage = true;
}
// Read the TEST button.
testButton.read();
// Receive data from Android app.
// Stop execution, if application is in error state.
receiveAppCommand();
// Optionally add some glitter effect.
if (glitterEnabled == true){
addGlitterEffect(30);
}
// fadingEnabled signal for ambience light.
if (fadingEnabled == true){
addFadingEffect();
}
}
// =================================================================
// Methods for handling Android app commands.
// =================================================================
/**
* Receives and processes a command send by the Android app.
*/
void receiveAppCommand() {
if (isErrorState) {
return;
}
if (WokwiSerial_available()) { // WOKWI simulator.
delay(20); // just wait a little bit for more characters to arrive
// Reset input buffer.
in_reset();
while (WokwiSerial_available()) { // WOKWI simulator.
//TODO: receive data from serial or Bluetooth
// inByte = Serial.read();
char inByte = WokwiSerial_read(); // WOKWI simulator.
// Consume data bytes for collecting inValue bytes.
if (!isControlByte(inByte) && inByte != END_OF_DBG_CMD) {
inValue.concat(inByte);
}
// Store numeric LED number.
if (inByte == COLOR_SEPARATOR) {
inLEDConfig[inMoveNum][I_LED] = inValue;
log(".. LED: #" + String(inLEDConfig[inMoveNum][I_LED]));
inValue = "";
}
// Store alpha-numeric color indicator: 'r', 'g' or 'b'.
if (inByte == HOLD_SEPARATOR) {
inLEDConfig[inMoveNum][I_COLOR] = inValue;
log(".. Color: " + String(inLEDConfig[inMoveNum][I_COLOR]));
inValue = "";
inMoveNum++;
if (inMoveNum > I_MAX_MOVES) {
// signalErrorState(E_TOO_MANY_MOVES); // TODO: breaks simulator if enabled
return;
}
}
// 'P': pulsate marker for ambience light mode (fading).
if (inByte == PULSE_MARKER) {
inPulseMarker = inByte;
log(".. Pulse marker: " + String(inPulseMarker));
inValue = "";
}
// 'S': sparkle marker for glitter mode.
if (inByte == SPARKLE_MARKER) {
inSparkleMarker = inByte; // 'S'
log(".. Sparkle marker: " + String(inSparkleMarker));
inValue = "";
}
// Store brightness value.
if (inByte == BRIGHTNESS_MARKER) {
inBrightness = inValue.toInt();
log(".. Brightness: " + String(brightness));
inValue = "";
}
// End of App command.
if (isEOL(inByte)) {
configureLEDs();
if (inByte == EOL_BOULDER) {
playBoulderMelody();
} else {
playSnakeMelody();
}
}
}
}
}
/**
* Configures the LEDs according to the app command last received.
*/
void configureLEDs() {
// Turn off all LEDs.
log("Turning all LEDs off.");
leds_off();
// Set LED colors
log("Configuring " + String(inMoveNum) + " LEDs.");
for (int m = 0; m < inMoveNum; m++) {
int led = inLEDConfig[m][I_LED].toInt() - 1;
String color = inLEDConfig[m][I_COLOR];
log("Set LED #" + String(led+1) + " to color: " + color);
if (color == RED) {
leds_setPixelColor(led, NP_RED);
} else if (color == GREEN) {
leds_setPixelColor(led, NP_GREEN);
} else if (color == BLUE) {
leds_setPixelColor(led, NP_BLUE);
} else if (color == PURPLE) {
leds_setPixelColor(led, NP_PURPLE);
}
}
// Enable fading.
if (inPulseMarker == PULSE_MARKER) {
log("Fading: enabled");
fadingEnabled = true;
} else {
log("Fading: disabled");
fadingEnabled = false;
}
// Enable glitter.
if (inSparkleMarker == SPARKLE_MARKER) {
log("Glitter: enabled");
glitterEnabled = true;
} else {
log("Glitter: disabled");
glitterEnabled = false;
}
// Set brightness.
brightness = inBrightness;
log("Brightness: " + String(brightness));
leds_brightness(brightness);
// Turn LEDs on.
log("Displaying boulder moves.");
leds_show();
}
/**
* Add a glitter effect.
*/
void addGlitterEffect( int aChanceOfGlitter) {
if(random(0, 256) < aChanceOfGlitter) {
leds_setPixelColor(random16(NUM_LEDS), NP_WHITE);
leds_show();
}
}
/**
* Add fading effect for ambience light.
*/
void addFadingEffect() {
log("Fading brightness: " + String(brightness));
leds_brightness(brightness);
leds_show();
brightness = brightness + (FADE_AMOUNT * fadeDirection);
// Reverse the direction of the fading at the ends of the fade:
if(brightness <= BRIGHTNESS_MIN || brightness >= BRIGHTNESS_MAX){
fadeDirection = fadeDirection * -1;
}
delay(FADE_SPEED);
}
/**
* Tests an incoming byte.
* Returns 'true', if the incoming byte is a control byte, else 'false'.
*/
bool isControlByte(char anInByte) {
if (isEOL(anInByte)
|| anInByte == HOLD_SEPARATOR
|| anInByte == COLOR_SEPARATOR
|| anInByte == BRIGHTNESS_MARKER
|| anInByte == SPARKLE_MARKER
|| anInByte == PULSE_MARKER
|| anInByte == '.') { //TODO: Dot is actually not used. Indented usage: unknown
return true;
}
return false;
}
/**
* Tests an incoming byte.
* Returns 'true', if the incoming byte indicates the end of the line (command).
*/
bool isEOL(char anInByte) {
if (anInByte == EOL_BOULDER || anInByte == EOL_SNAKE_GAME) {
return true;
}
return false;
}
/**
* Resets the variables for reading data from the Bluetooth interface.
*/
void in_reset() {
inValue = "";
inMoveNum = 0;
inPulseMarker = ' ';
inSparkleMarker = ' ';
inBrightness = 0;
for (int m=0; m < I_MAX_MOVES; m++) {
for (int p=0; m < I_MAX_LED_PROPS; m++) {
inLEDConfig[m][p] = "";
}
}
}
// =================================================================
// Methods for handling user input.
// =================================================================
/**
* Listener: Fired on pressing the "green" TEST button.
*/
void testButton_pressed() {
testButton_counter++;
log(String("testButton_counter is: ") += String(testButton_counter));
if (testButton_counter >= 6) {
testButton_counter = 0;
leds_off();
}
else if (testButton_counter == 1) {
leds_on(NP_RED);
signalMode();
}
else if (testButton_counter == 2) {
leds_on(NP_GREEN);
signalMode();
}
else if (testButton_counter == 3) {
leds_on(NP_BLUE);
signalMode();
}
else if (testButton_counter == 4) {
leds_rainbow();
signalMode();
}
else if (testButton_counter == 5) {
configureLEDs();
signalMode();
}
}
// =================================================================
// Methods for testing the LED stripe.
// =================================================================
/**
* Test LEDs on startup.
*/
void startupLEDsTest() {
log("Starting LED test...");
leds_brightness(BRIGHTNESS_TEST);
// Start blink test to confirm LEDs are working.
leds_on(NP_RED);
delay(1000);
leds_on(NP_GREEN);
delay(1000);
leds_on(NP_BLUE);
delay(1000);
// Rainbow.
leds_rainbow();
delay(2000);
leds_brightness(BRIGHTNESS_DEFAULT);
leds_off();
log("...finished LED test.");
}
// =================================================================
// Methods for audio output. Melodies and beeps.
// =================================================================
/**
* Plays a short welcome melody at startup.
*/
void playWelcomeMelody() {
log("Start playing welcome melody...");
tone(SPEAKER_PIN, 264);
delay(300);
tone(SPEAKER_PIN, 330);
delay(300);
tone(SPEAKER_PIN, 396);
delay(450);
noTone(SPEAKER_PIN);
log("... finished playing welcome melody.");
}
/**
* Plays a short boulder melody.
*/
void playBoulderMelody() {
log("Start playing boulder melody...");
tone(SPEAKER_PIN, 1000);
delay(100);
tone(SPEAKER_PIN, 2000);
delay(100);
noTone(SPEAKER_PIN);
log("... finished playing boulder melody.");
}
/**
* Plays a short melody fpr the Snake game.
*/
void playSnakeMelody() {
tone(SPEAKER_PIN, 432);
delay(100);
noTone(SPEAKER_PIN);
log("Snake Game");
}
/**
* Produces a short keystroke beep.
*/
void signalMode() {
tone(SPEAKER_PIN, 864);
delay(10);
noTone(SPEAKER_PIN);
log("Buzzer");
}
/**
* Signal an application error.
*/
void signalErrorState(int errorNumber) {
log("Start signaling error: " + String(errorNumber) + "...");
isErrorState = true;
in_reset();
tone(SPEAKER_PIN, 50);
delay(500);
noTone(SPEAKER_PIN);
// Blink all LEDs to show error status.
leds_off();
for (int i=0; i < 3; i++) {
leds_on(NP_E_ERROR);
delay(E_INTERVALL);
leds_off();
delay(E_INTERVALL);
}
// Show error number:
leds_off();
leds_setPixelColor(E_ERROR_FLAG_LED, NP_E_ERROR);
leds_brightness(BRIGHTNESS_MAX);
leds_show();
// displayErrorNumber(errorNumber);
log("...finished signaling error.");
}
/**
* Displays the current error number with n yellow LEDs.
*/
void displayErrorNumber(int anErrorNumber) {
log("Displaying error number...");
for (int i=0; i < anErrorNumber; i++) {
int led = E_ERROR_OFFS_LED + i;
log("Testing LED #:" + String(led));
if (leds.getPixelColor(led) == NP_OFF) {
log("Switching 'ON' LED#: " + String(led));
leds_setPixelColor(led, NP_E_ERRNUM);
} else {
leds_setPixelColor(led, NP_OFF);
log("Switching 'OFF' LED#: " + String(led));
}
leds_brightness(BRIGHTNESS_MAX);
leds_show();
}
log("...finished displayint error number.");
}
// =================================================================
// Methods for encapsulating the calls to the NeoPixel library.
// =================================================================
/**
* Initializes the NeoPixel library.
* Sets the number of LEDs and turns all LEDs off.
*/
void leds_initialize() {
log("Initializing NeoPixel library...");
leds.begin();
// leds.setMaxPowerInVoltsAndMilliamps(12,9240); // we have 154 12V pixels with max power consumption of 60mA per pixel (white)
leds_off();
log("...finished initializing NeoPixel library.");
}
/**
* Sets all LEDs to the same color.
*/
void leds_on(uint32_t aColor) {
leds.fill(aColor, 0, NUM_LEDS);
leds_show();
}
/**
* Sets the color of the LED identified by
* the specified index starting at 0.
*/
void leds_setPixelColor(unsigned int anIndex, uint32_t aColor) {
leds.setPixelColor(anIndex, aColor);
}
/**
* Sets all LEDs to rainbow colors.
*/
void leds_rainbow() {
leds.rainbow(RAINBOW_FIRST_HUE, RAINBOW_REPS, RAINBOW_SATURATION, RAINBOW_BRIGHTNESS, RAINBOW_GAMMIFY);
leds_show();
}
/**
* Sets the brightness of the LEDs.
*/
void leds_brightness(uint8_t aBrightness) {
leds.setBrightness(aBrightness);
}
/**
* Turns all LEDs off.
*/
void leds_off() {
leds.clear();
leds_show();
}
/**
* Send LED configurations to LED stripe.
*/
void leds_show() {
leds.show();
}
// =================================================================
// Methods for debugging the application with the WOKWI simulator.
// See: https://wokwi.com/
// =================================================================
/*
* Simulates testing for data availability on a Serial interface.
*/
bool WokwiSerial_available() {
if (cmdCounter < SIZE_CMD_BOULDER) {
char tmpInByte = cmdBoulder[cmdCounter];
if (tmpInByte != END_OF_DBG_CMD) {
return true;
}
}
return false;
}
/*
* Simulates reading data from a Serial interface.
*/
char WokwiSerial_read() {
char tmpInByte = cmdBoulder[cmdCounter];
if (cmdCounter < SIZE_CMD_BOULDER && tmpInByte != END_OF_DBG_CMD) {
log(tmpInByte);
cmdCounter++;
return tmpInByte;
}
return END_OF_DBG_CMD;
}
/**
* Appends a message to the console log.
*/
void log(String aMessage) {
Serial.println(aMessage);
}
/**
* Appends a char to the console log.
*/
void log(char aChar) {
Serial.println(aChar);
}