Bottle Clock Sketch

Definitions and Declarations

/*****************************************************************************************************

        BottleClock.ino for the 8 A/N red LEDs and a Spark Fun MiniPro Arduino 16 MHz

        It's in a glass vase and rotates for control

  The Wire library is used directly instead of RTLib because that does not have a methood

  to write individual bytes to the DS-1307.

  The display uses library SegDisp for the 2 Adafruit HK 533 'backpacks'.

  AdafruitLEDBackpack does not work with multilple backpacks.

  24 hour display option and no turn off at night may be selected at power on and persist

  There are 5 states: Time, Day-Date,Month-Year,Adjust Hour, and Adjust minute.

  For each state operations are determined only by 5 angles:

  Face forward pitch flat, roll up pitch flat (45 deg CW viewed from USB end)

  roll downp pitch flat (45 deg CCW viewed from USB end),

  Face forward, pitch up, USB down, and Face forward, pitch up, USB up

  In Time mode rotating the clock around its principal horizontal (roll) axis selects 3

  alternate display modes: HH.MM.SS, Day and Date, or Month and Year.

  In both adjust modes rolling the clock increments or decrements the value

  John Saunders 2/1/2020

 *****************************************************************************************************

*/


#include <Wire.h>                     // The RTC and the display use i2C

#include <SPI.h>                      // The LIS3DH uses SPI

#include <Adafruit_LIS3DH.h>

#include <Adafruit_Sensor.h>

#include "SegDisp.h"

#define sqwPort 8

#define LIS3DH_CS 9

#define lightPort A0

#define messageSpeed 1300

#define darkLimit 100

#define rtcAddr  0x68


Adafruit_LIS3DH lis = Adafruit_LIS3DH(LIS3DH_CS);   // This implies using hardware SPI on pins 11-13


const int numLabels = 5;


const String labels[] = {

  "        ", "MIN     ", "HOUR    ", "M M  M M", "H H  H H", "12 HOUR ", "24 HOUR ", "DARK OFF", "DARK ON "

};


const int welcomeMesgLen = 12;

const String welcomeMesg[] = {

  "My name ", "is Vicky", "Vasey I ", "was made", "by John ", "Saunders", "in 2020 ", "I belong", "at 5496 ", "Waring  ", "Road San", " Diego  "

};


typedef struct {            // a mix of variable, ane fixed members

  uint8_t data;             // Read from the DS1307

  int index;                // Into labels

  int addr;                 // in the DS1307

  int uv;                   // Maximum value

  int lv;                   // Minimum value

} info_t;



info_t timeDate[] {                    // stored in DS1307 RAM starting at 0

  //    sec               min                hour             day              date            month            year

  {0, 0, 0, 59, 0}, {36, 1, 1, 59, 0,}, {8, 2, 2, 23, 0}, {6, 0, 3, 7, 1}, {27, 0, 4, 31, 0}, {3, 0, 5, 12, 1}, {21, 0, 6, 99, 0},

  //    control               Hour Mode                Dark Mode

  {0x10, 0, 0, 0x10, 0x10}, {0, 1, 8, 1, 0,}, {0, 2, 9, 1, 0},

};

// Stores the output of the clock-calendar DS-1307

Setup and Loop

/*****************************************************************************************************

              User option selection is determined by the display angle when power is applied

   Thr default when the display is face foeward is 12-hour time and operayion continues in the dark

   24 hour tine is selected if the display is face up or face away

   The display goes off in the daek if the display is face up or face down

 ******************************************************************************************************

*/


bool initMode = true;


bool sqw;


void setup() {

  int attCode;

  Serial.begin(9600);

  SegDisp_init();

  lis.begin(0x18);

  lis.setRange(LIS3DH_RANGE_2_G);

  pinMode(sqwPort, INPUT_PULLUP);

  Wire.setClock(100000);        // Needed for the DS1307, but it is default anyway

  delay(300);

  rtcSet(0, 0);                       // Start RTC

  rtcSet(7, 10);                      // Enable square wave, just in case

  Wire.beginTransmission(rtcAddr);

  Wire.write(8);

  if (Wire.endTransmission() == 0) {

    Wire.requestFrom(rtcAddr, 2);

    timeDate[8].data = Wire.read();

    timeDate[9].data = Wire.read();

  }

  attCode = attitudeGet();

  displayNum( 0,  attCode, true);

  if (attCode == ANGLE_ROLL_UP) {            // hour mode, 1 = 24

    rtcSet(8, 1);

  }

  if (attCode == ANGLE_ROLL_DOWN) {            // dark mode 1 = on at night

    rtcSet(9, 1);

  }

  if (attCode == ANGLE_FACE_AWAY) {            // 12 hour, off at night

    rtcSet(8, 0);

    rtcSet(9, 0);

  }

  do {

  attCode = attitudeGet();

  }

  while (attCode != ANGLE_FLAT);

  initMode = true;

  while (sqw == digitalRead(sqwPort));

  loopCount = 0;

}


void loop() {

  static int countDown;

   int attCode;

  if ((loopCount > 150000) && (timeDate[2].data == 8)) {     //A nudge if its stolen

    initMode = true;

    loopCount = 0;

  }

  /*****************************************************************************************************

             These messages confirm the option selection at boot

   *******************************************************************************************************

  */

  if (initMode == true) {

    dispOptions();

    delay(messageSpeed);

    countDown = random(1000,20000);

    /*

      for(int i=0;i<8;i++) {

       rtcSet(timeDate[i].addr,timeDate[i].data);

      }

    */

    initMode = false;

    opMode == OP_MODE_NORMAL;

    sqw = (digitalRead(sqwPort));

    delay(300);

  }


  if ((digitalRead(sqwPort) != sqw) && (initMode == false)) {

    countDown--;

 Serial.println(countDown,DEC);


  if (countDown == 1) {

    dispWelcome();

    countDown = random(5000,30000);  }



    /*****************************************************************************************************

             I2C operations are synchronous with the clock at a 1 Hz rate

             Adjustments are done at a 45 deg pitch, at different display face angles

    *******************************************************************************************************

    */

    delay(10);

    byte rtcIn;


    attCode = attitudeGet();

    int oldAtt;

    oldAtt = attCode;

    Wire.beginTransmission(rtcAddr);

    Wire.write(0);

    if (Wire.endTransmission() == 0) {

      Wire.requestFrom(rtcAddr, 7);

      for (byte i = 0; i < 7; i++) {

        rtcIn = Wire.read();

        timeDate[i].data = (10 * (rtcIn / 16)) + (rtcIn & 0x0F);    //working in decimal, not BCD

      }

    }


    delay(3);

    if ((timeDate[9].data == 0) && (analogRead(A0) <  darkLimit)) {

      attCode = ANGLE_FACE_DOWN;                                    // Darkness overrides other modes

    }

    loopCount++;

    if (attCode != oldAtt) {                              //Any gesture extends timeout

      loopCount = 0;

      oldAtt = attCode;

    }

    if (opMode == OP_MODE_NORMAL) {                       // Modes may only be changed from time display

      selectOpMode(attCode);

    }

    if (opMode != oldMode) {                             //Any mode change extends timeout

      loopCount = 0;

      oldMode = opMode;

    }

    if ((opMode == OP_MODE_NORMAL) && (attCode == ANGLE_FLAT)) {            // time display

      if ((timeDate[8].data == 0) && (timeDate[2].data > 12)) {    // flat

        displayNum( 0,  (timeDate[2].data - 12), true);      // location, index, length - hours

      }

      else {

        displayNum( 0,  timeDate[2].data, true);      // location, index, length - hours

      }

      SegDisp_drawAscii( 2, 0x20, true);       // period

      displayNum( 3,  timeDate[1].data, true);      // location, index, length - minutes

      SegDisp_drawAscii( 5, 0x20, true);       // period

      displayNum( 6,  timeDate[0].data, false);     // location, index, length - seconds

    }

    if (opMode == OP_MODE_DAYDATE) {         // 45 deg clockwise roll, Day-Date display continues until timeout

      displayWeekday( 0);                    // location - Day of Week

      SegDisp_drawAscii( 3, 0x20, false);    // space

      SegDisp_drawAscii( 4, 0x20, false);    // space

      SegDisp_drawAscii( 5, 0x20, false);    // space

      displayNum( 6,  timeDate[4].data, true);    // location - Date

    }

    if (opMode == OP_MODE_MONYR) {             // 45 degcounter-clockwise roll. Month-Year display continues until timeout

      displayMonth( 0);                        // location - month

      SegDisp_drawAscii( 3, 0x20, false);      // space

      displayNum( 4, 20, false);               // location, index, length - 20

      displayNum( 6,  timeDate[6].data, false);     // location, index, length - year

    }

    if ((opMode == OP_MODE_ADJ_MIN) && (attCode == ANGLE_PITCH_UP)) {

      dispLabel(3);                                 // Shows mode has been acheived

    }

    if ((opMode == OP_MODE_ADJ_MIN) && (attCode == ANGLE_FLAT)) {   //return to flat to make adjustments

      dispLabel(1);

      displayNum( 6,  timeDate[1].data, true);                      //shows the minute value

    }

    if ((opMode == OP_MODE_ADJ_MIN) && (attCode ==  ANGLE_ROLL_UP)) {

      adjustClock(1, true);                                        // be nimble!

      dispLabel(1);

      displayNum( 6,  timeDate[1].data, true);

      delay(450);

    }

    if ((opMode == OP_MODE_ADJ_MIN) && (attCode ==  ANGLE_ROLL_DOWN)) {

      adjustClock(1, false);

      dispLabel(1);

      displayNum( 6,  timeDate[1].data, true);

      delay(450);

    }

    if ((opMode == OP_MODE_ADJ_HOUR) && (attCode == ANGLE_PITCH_DOWN)) {

      dispLabel(4);

    }

    if ((opMode == OP_MODE_ADJ_HOUR) && (attCode == ANGLE_FLAT)) {

      dispLabel(2);

      displayNum( 6,  timeDate[2].data, true);

    }

    if ((opMode == OP_MODE_ADJ_HOUR) && (attCode ==  ANGLE_ROLL_UP)) {

      adjustClock(2, true);

      dispLabel(2);

      displayNum( 6,  timeDate[2].data, true);

      delay(450);

    }

    if ((opMode == OP_MODE_ADJ_HOUR) && (attCode ==  ANGLE_ROLL_DOWN)) {

      adjustClock(2, false);

      dispLabel(2);

      displayNum( 6,  timeDate[2].data, true);

      delay(450);

    }

    if (attCode == ANGLE_FACE_DOWN) {

      dispLabel(0);

    }

    SegDisp_writeAlpha();

    if (loopCount > timeout) {

      opMode = OP_MODE_NORMAL;

    }

    sqw = digitalRead(sqwPort);

    delay(300);

  }

}

Functions

/*****************************************************************************************************

                              Display utilities

 ******************************************************************************************************

*/


void dispLabel(int index) {

  char labelBuffer[9];

  labels[index].toCharArray(labelBuffer, 9);

  for (int j = 0; j < 8; j++) {

    SegDisp_drawAscii( j, (labelBuffer[j]), false);

  }

  SegDisp_writeAlpha();

}


void dispWelcome(void) {                                 // At setup for personalisation

  char welcomeBuffer[9];

  for (int i = 0; i < welcomeMesgLen; i++) {

    welcomeMesg[i].toCharArray(welcomeBuffer, 9);

    for (int i = 0; i < 8; i++) {

      SegDisp_drawAscii( i, (welcomeBuffer[i]), false);

    }

    SegDisp_writeAlpha();

    delay(messageSpeed );

  }

}


void dispOptions(void) {                                // At setup

  if (timeDate[8].data == 0) {

    dispLabel(5);

  }

  else {

    dispLabel(6);

  }

  delay(messageSpeed);

  if (timeDate[9].data == 0) {

    dispLabel(7);

  }

  else {

    dispLabel(8);

  }

}




const char weekdays[] = {"   MONTUEWEDTHUFRISATSUN"};


void displayWeekday(uint8_t loc) {    // Value in timeDate is 3

  char c;

  uint8_t addr = loc;

  uint8_t val = timeDate[3].data;

  if (val > 7) {

    val = 0;

  }

  for (byte i = 0; i < 3; i++)  {

    c = weekdays[((3 * val) + i)];

    SegDisp_drawAscii(addr++, c, false);

  }

}


const char months[] = {"   JANFEBMARAPRMAYJUNJLYAUGSEPOCTNOVDEC"};


void displayMonth(uint8_t loc) {

  char c;

  uint8_t addr = loc;

  uint8_t val = timeDate[5].data;

  if (val > 12) {

    val = 0;

  }

  for (byte i = 0; i < 3; i++)  {

    c = months[((3 * val) + i)];

    SegDisp_drawAscii(addr++, c, false);

  }

}


void displayNum(uint8_t loc, int val, bool leadZero) {

  byte c, r = 0;

  uint8_t addr = loc;

  if (val >= 1000) {

    val %= 1000;

    SegDisp_drawAscii(addr++, (val % 1000), false);

  }

  if (val < 0) {

    SegDisp_drawAscii(addr++, 0x2D, false);

    val = abs(val);

  }

  if (val > 99) {

    c = (val / 100) + 0x30;

    if ((leadZero == true) && (c == 0x30)) {

      SegDisp_drawAscii(addr++, 0x20, false);

    }

    else {

      SegDisp_drawAscii(addr++, c, false);

    }

    addr++;

  }

  r = val % 100;

  c = (r / 10) + 0x30;

  if ((leadZero == true) && (c == 0x30) && (val < 100)) {

    SegDisp_drawAscii(addr++, 0x20, false);

  }

  else {

    SegDisp_drawAscii(addr++, c, false);

  }

  c = (val % 10) + 0x30;

  SegDisp_drawAscii(addr, c, false);

}


/*****************************************************************************************************

                             Minute and hour adjustments

 ******************************************************************************************************

*/


void rtcSet(uint8_t item, uint8_t val) {

  byte bcdVal;

  bcdVal = (16 * (val / 10)) + val % 10;

  Wire.beginTransmission(rtcAddr);

  Wire.write(item);

  Wire.write(bcdVal);

  Wire.endTransmission();

}


int adjustIndex = 2;


void adjustClock(int dIndex, bool way) {

  uint8_t adjData = timeDate[dIndex].data;

  int lIndex = timeDate[dIndex].index;

  int rtcReg = timeDate[dIndex].addr;

  int upperLimit = timeDate[dIndex].uv;

  int lowerLimit = timeDate[dIndex].lv;

  if (way == true) {

    adjData++;

    if (adjData >= upperLimit) {

      adjData = lowerLimit;

    }

  }

  else {

    if (adjData > lowerLimit) {

      adjData--;

    }

    else {

      adjData = upperLimit;

    }

  }

  rtcSet(rtcReg, adjData);

  timeDate[dIndex].data =  adjData;

  dispLabel(lIndex);

  displayNum( 6,  adjData, true);

}



/*****************************************************************************************************

                            Display angle measurement

  This reduces the 3 floats from the LIS#DH to one unsigned int, which indicates the 45 degree angles

 ******************************************************************************************************

*/


int attitudeGet(void) {

  float gestureLimit = 7500;

  int attCode = 0;

  lis.read();      // get X Y and Z data at once

  float attVal;

  attVal = lis.x;

  if  (attVal > gestureLimit) {

    attCode |= 0x04;

  }

  if ((attVal + gestureLimit) < 0.0) {

    attCode |= 0x08;

  }


  attVal = lis.y;

  if  (attVal > gestureLimit) {

    attCode |= 0x01;

  }

  if ((attVal + gestureLimit) < 0.0) {

    attCode |= 0x02;

  }


  attVal = lis.z;

  if  (attVal > gestureLimit) {

    attCode |= 0x10;

  }

  if ((attVal + gestureLimit) < 0.0) {

    attCode |= 0x20;

  }

  return attCode;

}

/*****************************************************************************************************

                     Oprational Mode transitions

      Only 8 of the possible combinations are used: the rest are no-op

 ******************************************************************************************************

*/

#define OP_MODE_NORMAL 0

#define OP_MODE_ADJ_MIN 1

#define OP_MODE_ADJ_HOUR 2

#define OP_MODE_DAYDATE 3

#define OP_MODE_MONYR 4

#define ANGLE_FLAT 1

#define ANGLE_FACE_AWAY 2

#define ANGLE_FACE_UP 32

#define ANGLE_ROLL_UP 33

#define ANGLE_FACE_DOWN 16

#define ANGLE_ROLL_DOWN 17

#define ANGLE_PITCH_UP 5            // connector down

#define ANGLE_PITCH_DOWN 9

#define TIMEOUT_BASIC 8             // the unit is 500 ms

#define TIMEOUT_ADJ 20


long loopCount, timeout;

int opMode = OP_MODE_NORMAL;

int oldMode;


void selectOpMode(int attCode) {           //The op mode may be changed only from the time mode

  switch (attCode) {

    case ANGLE_PITCH_UP:

      opMode = OP_MODE_ADJ_MIN;

      timeout = TIMEOUT_ADJ;

      break;

    case ANGLE_PITCH_DOWN:

      opMode = OP_MODE_ADJ_HOUR;

      timeout = TIMEOUT_ADJ;

      break;

    case ANGLE_ROLL_UP:

      opMode = OP_MODE_DAYDATE;

      timeout = TIMEOUT_BASIC;

      break;

    case ANGLE_ROLL_DOWN:

      opMode = OP_MODE_MONYR;

    default:

      timeout = TIMEOUT_BASIC;

      break;

  }

}