lincolnstein
Published © GPL3+

Magic Picture of Storm at Sea

This "magic picture" features an animated sailing boat caught in a squall, rocking and pitching as lightning flashes and thunder rumbles.

BeginnerShowcase (no instructions)1,224
Magic Picture of Storm at Sea

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
Dual H-Bridge motor drivers L298
SparkFun Dual H-Bridge motor drivers L298
×1
Shift Register- Serial to Parallel
Texas Instruments Shift Register- Serial to Parallel
×1
MicroSD Card Module, SPI interface, ESP8266 ESP32
×1
High Accuracy Pi RTC (DS3231)
Seeed Studio High Accuracy Pi RTC (DS3231)
×1
EK1236 audio amplifier module (based on LM386 chip)
×1
Speaker: 0.25W, 8 ohms
Speaker: 0.25W, 8 ohms
×1
WS2812 Addressable LED Strip
Digilent WS2812 Addressable LED Strip
Any WS2812-based addressable LED strip will do - I got mine from NewEgg
×1
High Brightness LED, White
High Brightness LED, White
For sides of box
×2
5 mm LED: Red
5 mm LED: Red
For mast
×1
Tilt sensor SW-520D
Generic. Any ball-type tilt sensor will do.
×1
DC 12V worm gear box reduction electric motor, 20 RPM
I tried several motors. This one is quiet and strong.
×1
12V DC switching power supply, 5 A
Was in my box of surplus computer equipment. Probably used for a laptop. MInimum 5A is needed to light up LED strip fully.
×1
Pushbutton Switch, Momentary
Pushbutton Switch, Momentary
Reset button
×1
Rocker Switch, Non Illuminated
Rocker Switch, Non Illuminated
Power on/off
×1
Male and female barrel jack for power, 2.5 mm
×1
Female/Female Jumper Wires
Female/Female Jumper Wires
×1

Hand tools and fabrication machines

Jigsaw or coping saw
For cutting out ship and waves
Table saw or handheld saw
For making the box
Hot glue gun (generic)
Hot glue gun (generic)

Story

Read more

Schematics

Shadowbox circuit diagram - storm at sea

This is a schematic of the shadowbox wiring.

Code

StormScript-v2.ino

C/C++
This is the main loop
/* Copyright 2021 Lincoln D. Stein <lincoln.stein@gmail.com> */

#include <pcmConfig.h>
#include <TMRpcm.h>
#include <pcmRF.h>
#include <FastLED.h>

#include <SPI.h>
#include <SD.h>
#include <L298N.h>

// Date and time functions using a DS3231 RTC connected via I2C and Wire lib
#include <RTClib.h>

#include "script.h"
#include "ship_pins.h"

// this controls the granularity of lighting transitions - higher is more gradual
#define GRADIENT_SIZE 20

// Create one motor instance
L298N motor(M_EN, M_IN1, M_IN2);

// real time clock
RTC_DS3231 Clock;

// uncomment this to set the clock at compile time
// #define SET_CLOCK 1

// This is an array of leds.  One item for each led in your strip.
CRGB argb[NUM_LEDS];

// Audio
TMRpcm audio;

// state variables
byte Leds           = 0;
byte Sleeping       = 0;
byte MastLightState = 0;
unsigned long int  StormTime = 0;
unsigned long int  GullTime  = 0;
unsigned long int  RTCTime   = 0;
unsigned long int  MastBlinkTime = 0;

// tilt sensor state
byte last_button_state = LOW;
byte ButtonState       = LOW;
unsigned long int last_debounce_time = 0;

void setup() {
  char wavFile[15];
  Wire.begin();

  FastLED.addLeds<WS2812B, LED_STRIP_PIN, GRB>(argb, NUM_LEDS);

  pinMode(SD_Pin, OUTPUT);
  digitalWrite(SD_Pin, HIGH);

  pinMode(latchPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
  pinMode(clockPin, OUTPUT);

  // use internal button pullup for tilt sensor
  pinMode(TILT_PIN, INPUT_PULLUP);

  Serial.begin(9600);

  if (!SD.begin(SD_Pin)) {
    Serial.println(F("SD fail"));
    Serial.flush();
    abort();
  }

  if (!Clock.begin()) {
    Serial.println(F("Couldn't find RTC module"));
    Serial.flush();
    abort();
  }

  if (Clock.lostPower()) {
    Serial.println(F("Clock lost power. Resetting time"));
    Clock.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
#ifdef SET_CLOCK
  Clock.adjust(DateTime(F(__DATE__), F(__TIME__)));
#endif

  Serial.println();

  audio.speakerPin = Speaker_Pin;
  audio.quality(1);
  audio.loop(0);
  audio.setVolume(3);
  audio.play(strcpy_P(wavFile, (char*)pgm_read_word(&(Bells[1]))));

  // initial lighting
  Leds = 0;
  updateShiftRegister();
  change_lighting(DAYLIGHT);

  StormTime = INITIAL_STORM_PAUSE - TIME_BETWEEN_STORMS; // this forces the storm to start 30 seconds after turning on
  GullTime = millis();

#if DEBUG
  Serial.print(F("StormTime = "));
  Serial.print(StormTime);
  Serial.print(F(" millis() = "));
  Serial.println(millis());
#endif

  randomSeed(analogRead(A1)); // analogRead on unconnected pin to give a random seed

}

void loop() {
  char wavFile[15];

  byte sleeping = is_sleeping();

  if (millis() - MastBlinkTime > MAST_BLINK_INTERVAL) {
    MastLightState = !MastLightState;
    set_mast_light(MastLightState);
    MastBlinkTime = millis();
  }

  if (!sleeping) {
    if (millis() - StormTime > TIME_BETWEEN_STORMS) {
#if DEBUG
      Serial.println(F("Storm starting"));
#endif
      do_storm();
      StormTime = millis();
    }

    if (millis() - GullTime > random(AVERAGE_GULL_INTERVAL - GULL_INTERVAL_RANGE, AVERAGE_GULL_INTERVAL + GULL_INTERVAL_RANGE)) {
#if DEBUG
      Serial.println(F("Gulls squawking"));
#endif
      audio.setVolume(4);
      audio.play(strcpy_P(wavFile,
                          (char*)pgm_read_word(&(Gulls[random(0, sizeof(Gulls) / sizeof(Gulls[0]))]
                                                ))));
      GullTime = millis();
    }
  }
}

void do_storm() {
  char wavFile[15];
  byte EventCounter = 0;
  byte done         = 0;
  long int now = millis();

  while (!done) {
    long int time_elapsed = millis() - now;
    byte tilt = tilt_changed(); // have to keep monitoring this in order to update ButtonState
#if DEBUG
    if (tilt) {
      Serial.print(F("Tilt state changed to "));
      Serial.println(ButtonState, DEC);
    }
#endif

    if (StormScript[EventCounter].millis <= time_elapsed) { // time to do an event!
#if DEBUG
      Serial.print(F("** Event "));
      Serial.print(EventCounter);
      Serial.println(F(" **"));
      Serial.print(F("Relative Time = "));
      Serial.print(time_elapsed);
      Serial.print(F("; Script time = "));
      Serial.println(StormScript[EventCounter].millis);
      Serial.print(F("Action = "));
      Serial.println(StormScript[EventCounter].action);
      Serial.print(F("Value = "));
      Serial.println(StormScript[EventCounter].value, DEC);
#endif

      int value = StormScript[EventCounter].value;
      switch (StormScript[EventCounter].action) {

        case stormsound :
          while (audio.isPlaying()) {
            delay(10);
          }
          audio.setVolume(5);
          audio.play(strcpy_P(wavFile, stormfile));
          break;

        case light_transition :
          change_lighting(value);
          break;

        case lightning :
          flash_lightning((Lightning)value);
          break;

        case motor_speed :
          set_motor_speed(value);
          break;

        case mast_light_transition :
          set_mast_light(value);
          break;

        case ending :
          while (audio.isPlaying()) {
            delay(10);
          }
          done++;
          break;
      }
      EventCounter++;
    }
  }
}

void set_motor_speed (int value) {
  if (value == 0) {
    // motor.stop();
    stop_when_horizontal(motor);
  } else {
    motor.setSpeed(value);
    motor.forward();
  }
}

void change_lighting (int v) {
  byte arrSize = sizeof(LightingEffects) / sizeof(LightingEffects[0]);
  if (v >= arrSize) {
#if DEBUG
    Serial.print(F("BUG: change_lighting called with value of "));
    Serial.print(v, DEC);
    Serial.print(F(". But end of array is at "));
    Serial.println(arrSize - 1, DEC);
#endif
    return;
  }
  CRGB color = LightingEffects[v];
  fade_to_color(color);
}

void flash_lightning (Lightning v) {
  int led, rgb;
  int previous_color = argb[0];

  if (v == center) {
    big_flash();
    return;
  }

  if (v == left) {
    led = LEFT_LED;
    rgb = 0;
  } else if (v == right) {
    led = RIGHT_LED;
    rgb = NUM_LEDS - 1;
  } else {
    return;
  }

  for (uint8_t i = 0; i < 10; i++) {
    bitSet(Leds, led);
    updateShiftRegister();
    argb[rgb] = LIGHTNING_COLOR;
    FastLED.show();

    delay(random(0, 10));

    bitClear(Leds, led);
    updateShiftRegister();
    argb[rgb] = previous_color;
    FastLED.show();

    delay(random(0, 50));
  }
}

void big_flash() {
  CRGB current = argb[1];
  for (uint8_t i = 0; i < 10; i++) {
    fill_solid(argb, NUM_LEDS, LIGHTNING_COLOR);
    FastLED.show();
    delay(random(0, 10));
    fill_solid(argb, NUM_LEDS, current);
    FastLED.show();
    delay(random(0, 50));
  }
}


void set_mast_light(int on) {
  if (on) {
    bitSet(Leds, MAST_LED);
  } else {
    bitClear(Leds, MAST_LED);
  }
  updateShiftRegister();
}

void updateShiftRegister()
{
  digitalWrite(latchPin, LOW);
  shiftOut(dataPin, clockPin, MSBFIRST, Leds);
  digitalWrite(latchPin, HIGH);
}

void fade_to_color(CRGB destination) {
  CRGB CurrentSky = argb[1];
  CRGB gradient[GRADIENT_COUNT];
  int pause_time = FADE_DURATION / GRADIENT_COUNT;
  fill_gradient_RGB(gradient, 0, CurrentSky, GRADIENT_COUNT - 1, destination);
  for (int i = 0; i < GRADIENT_COUNT; i++) {
    fill_solid(argb, NUM_LEDS, gradient[i]);
    FastLED.show();
    delay(pause_time);
  }
  CurrentSky = destination;
}

// check for bedtime
byte is_sleeping() {
  char wavFile[15];

  if ((RTCTime == 0) || (millis() - RTCTime > CHECK_RTC_INTERVAL)) {
    RTCTime = millis();
    byte hr = get_hour();
#if DEBUG
    Serial.print(F("Checking clock...hr="));
    Serial.println(hr, DEC);
#endif

    if ((hr >= SLEEP_START) || (hr <= SLEEP_END)) {
#if DEBUG
      Serial.println(F("In sleep range"));
#endif
      if (!Sleeping) {
#if DEBUG
        Serial.println(F("It's late. Nighty night!"));
#endif
        audio.play(strcpy_P(wavFile, (char*)pgm_read_word(&(Bells[0]))));
        change_lighting(NIGHTLIGHT);
        Sleeping = TRUE;
      }
    }

    else { // wake up!
      if (Sleeping) {
#if DEBUG
        Serial.println(F("Good morning! Waking up"));
#endif
        change_lighting(DAYLIGHT);
        audio.play(strcpy_P(wavFile, (char*)pgm_read_word(&(Bells[1]))));
        Sleeping = FALSE;
        StormTime = millis();
        GullTime = millis();
      }
    }
  }

  return Sleeping;
}

/* method for calculating DST offset copied from here: https://forum.arduino.cc/t/ds3231-rtc-daylight-savings-time/410699/4 */
int get_hour () {
  DateTime now = Clock.now();
  boolean DST  = 0;

  // ********************* Calculate offset for Sunday *********************
  byte y    = now.year();           // DS3231 uses two digit year (required here)
  byte x = (y + y / 4 + 2) % 7;     // remainder will identify which day of month DST starts on

  byte hour  = now.hour();
  byte month = now.month();
  byte dom   = now.day();

  // LS: this code is inelegant, but it purportedly works, so I'm not going to mess with it
  // is Sunday by subtracting x from the one
  // or two week window. First two weeks for March
  // and first week for November
  // *********** Test DST: BEGINS on 2nd Sunday of March @ 2:00 AM *********
  if (month == 3 && dom == (14 - x) && hour >= 2)   {
    DST = 1;
  }
  if ((month == 3 && dom > (14 - x)) || month > 3)  {
    DST = 1;
  }
  // ************* Test DST: ENDS on 1st Sunday of Nov @ 2:00 AM ************
  if (month == 11 && dom == (7 - x) && hour >= 2)  {
    DST = 0; // daylight savings time is FALSE (Standard time)
  }
  if ((month == 11 && dom > (7 - x)) || month > 11 || month < 3)  {
    DST = 0;
  }

  if (DST) // Test DST and add one hour if = 1 (TRUE)
    return hour + 1;
  else
    return hour;
}

/* Tilt sensor routines */

void stop_when_horizontal (L298N m, int timeout) {
  unsigned long timestart = millis();
  boolean  timedout = false;

#if DEBUG
  Serial.println(F("Waiting for horizontal before stopping"));
#endif
  while (!timedout) {
    if (tilt_changed()) {
      if (ButtonState == LOW)
        break;
    }
    timedout = millis() - timestart > timeout;
  }
  if (timedout)
    Serial.println(F("Timed out waiting for horizontal"));
  else
    Serial.println(F("Stopped normally"));
  m.stop();
}

boolean tilt_changed() {
  byte reading = digitalRead(TILT_PIN);
  boolean changed = false;

  if (reading != last_button_state) {
    last_debounce_time = millis();
  }

  if (millis() - last_debounce_time > DEBOUNCE_DELAY) {
    if (reading != ButtonState) {
      changed    = true;
      ButtonState = reading;
    }
  }
  last_button_state = reading;
  return changed;
}

script.h

C/C++
C++ include file containing storm script data structures and defines
/* Copyright 2021 Lincoln D. Stein <lincoln.stein@gmail.com> */

#ifndef SCRIPT_H
#define SCRIPT_H

#define DEBUG 0

static const char stormfile[] PROGMEM = "storm4.wav"; // was storm3.wav
static const char bell1[]     PROGMEM = "bell1.wav";
static const char bell2[]     PROGMEM = "bells2-4.wav";
static const char gull1[]     PROGMEM = "gull1.wav";
static const char gull2[]     PROGMEM = "gull2.wav";
static const char gull3[]     PROGMEM = "gull3.wav";

PGM_P const Gulls[]    PROGMEM = {gull1,gull2,gull3};
PGM_P const Bells[]    PROGMEM = {bell1,bell2};

// These are the lighting states
// They are indexes into the LightingEffect[] array
#define DAYLIGHT   0
#define TWILIGHT   1
#define STORMLIGHT 2
#define NIGHTLIGHT  3

#define OFF        0
#define ON         1

#define FALSE      0
#define TRUE       1

#define DAYLIGHT_COLOR   CRGB(150,140,70)
// #define DAYLIGHT_COLOR   CRGB(200,200,150)
#define TWILIGHT_COLOR   CRGB(40,30,90)
#define STORMLIGHT_COLOR CRGB(15,10,30)
#define LIGHTNING_COLOR  CRGB(250,250,250)
#define NIGHTLIGHT_COLOR CRGB(0,0,10)

// the gradient count defines the number of light transition steps between one
// lighting effect and the next. The higher the count, the smoother the transition
// will be
#define GRADIENT_COUNT 40
#define FADE_DURATION  3000

// one hour between storms = 60 x 60 x 1000
#define TIME_BETWEEN_STORMS  3600000
// 10 minutes between storms
// #define TIME_BETWEEN_STORMS 600000
// after poweron, storm will start in 20s
#define INITIAL_STORM_PAUSE  20000

// gulls randomly call every 10 minutes, roughly
#define AVERAGE_GULL_INTERVAL 600000
// 3 minutes for testing
//#define AVERAGE_GULL_INTERVAL 180000
#define GULL_INTERVAL_RANGE   AVERAGE_GULL_INTERVAL/2

// Using the 24h clock, these define the intervals when the storm and other effects are muted
// (i.e. bedtime!)
#define SLEEP_START 22
#define SLEEP_END   07

// Check the RTC for sleep time every minute
#define CHECK_RTC_INTERVAL 60000
// Frequency of mast blink
#define MAST_BLINK_INTERVAL 1000

// The debounce delay for the tilt sensor
#define DEBOUNCE_DELAY 10

// prototype for the stop_when_horizontal() function
void stop_when_horizontal (L298N m, int timeout = 25000);

enum Lightning : byte {
  left   = 0,
  right  = 1,
  center = 2
};

enum Action : byte {
  lightning,
  mast_light_transition,
  motor_speed,
  light_transition,
  ending,
  stormsound
};

struct Script {
  long int    millis;
  Action      action;
  byte        value;
};

// note, times must be sorted in ascending order
Script StormScript[] = {
  {500,  stormsound, 0},
  {5000, light_transition, TWILIGHT},
  {5000, lightning, left},
  {8000, mast_light_transition, ON},
  {9000, lightning, left},
  {15000, motor_speed, 60},
  {17250, lightning, left},
  {20000, light_transition, STORMLIGHT},
  {30105, lightning, left},
  {30105, lightning, center},
  {32742, motor_speed, 80},
  {42000, lightning, center},
  {43000, lightning, right},
  {46000, lightning, center},
  {61211, lightning, left},
  {61211, lightning, center},
  {61211, lightning, right},
  {61211, motor_speed, 120},
  {70539, lightning, center},
  {70539, lightning, right},
  {61211, motor_speed, 80},
  {80000, light_transition, TWILIGHT},
  {80000, motor_speed, 60},
  {90000, motor_speed, 0},
  {97238, lightning, right},
  {99545, light_transition, DAYLIGHT},
  {99545, mast_light_transition, OFF},
  {120000, ending, 0}
};

CRGB LightingEffects[] = {DAYLIGHT_COLOR, TWILIGHT_COLOR, STORMLIGHT_COLOR, NIGHTLIGHT_COLOR};

#endif // SCRIPT_H

ship_pins.h

C/C++
This is the include file that defines which Arduino pins are connected where
/* Copyright 2021 Lincoln D. Stein <lincoln.stein@gmail.com> */

#ifndef PINS_H
#define PINS_H

// location of the pin connected to the SD card
#define SD_Pin      10

// Pin the tilt sensor is attached to
#define TILT_PIN   A0

// location of the PWM pin connected to the speaker amp
#define Speaker_Pin 9

// Definitions for connections to the 74HC595 shift register IC
#define latchPin    5
#define clockPin    6
#define dataPin     4

// L298N motor pins
#define M_EN       3
#define M_IN1      7
#define M_IN2      8

// bit positions for independent LEDs driven by 74HCT595
#define MAST_LED   1
#define LEFT_LED   2
#define RIGHT_LED  3

// definitions for ARGB LED light strip
#define LED_STRIP_PIN  2
#define NUM_LEDS      29



#endif

Credits

lincolnstein

lincolnstein

0 projects • 0 followers

Comments