// manyI2Cbuses.ino
//
// Version 1, 29 August 2021, by Koepel
//
// Project: A clock is shown that counts the minutes and seconds.
//          With 30 software I2C buses plus 1 hardware I2C bus.
//          Each segment of the 7-segment number is a LCD display.
//          Each LCD display has its own I2C bus, but they do share the SCL.
//          
// This Wokwi project:
//   https://wokwi.com/arduino/projects/307886580731740737?dark=1
//   (the ?dark=1 is added for a dark theme, to make the number more visible)
//
//
// When this project is made with real hardware, there are some things that need attention:
//   1. The backlight needs current and the 5V pin of the Arduino board has not
//      enough power for all of them. A separate power supply is required.
//   2. When 30 LCD displays have 10k pullup resistors, then the shared SCL
//      has a combined pullup resistor of 333 Ohm.
//      The SCL signal is not strong enough for that. A logic driver can be
//      used to make it stronger. The SCL can be changed into a strong push/pull signal,
//      because there are no I2C devices that require clock pulse stretching.
//      An option can be to change the library and set the SCL pin always
//      as a OUTPUT for a strong HIGH/LOW signal.
// 
// Starting project:
//   https://wokwi.com/arduino/projects/307841659182252608
//
// Evolved into (by sutaburosu):
//   https://wokwi.com/arduino/projects/307849131382014528
// 
// That evolved into (by arcostasi):
//   https://wokwi.com/arduino/projects/307853926099583552
//
// With help from urish:
//   https://wokwi.com/arduino/projects/307973374851678786
//
// And help from mattam:
//   https://wokwi.com/arduino/projects/307982556017459778?dark=1
//
//
// 7-segment order:
//      a
//    f   b
//      g
//    e   c
//      d
//

#include <SoftwareWire.h>
#include "LiquidCrystal_I2C_Soft.h"     // changed for using software I2C
#include "StatusDisplay.h"              // an extra OLED display for the status

// Pattern for the numbers 0...9 for 7-segment display
const byte pattern7seg[10] = 
{
  // gfedcba   the segments, 'a' is upper in bit 0, 'b' is upper-right in bit 1
  0b00111111,  // 0
  0b00000110,  // 1
  0b01011011,  // 2
  0b01001111,  // 3
  0b01100110,  // 4
  0b01101101,  // 5
  0b01111101,  // 6
  0b00000111,  // 7
  0b01111111,  // 8
  0b01101111,  // 9
};

// Table that connects the index of the displays to one of the 7 segments.
// There are 4 digits, each with 7 segments, order: abcdefg
// The two in the middle are 14 and 15 and are not in this table.
const int index7seg[4][7] =
{
  // a,  b,  c,  d,  e,  f,  g         the segments
  {  0,  5,  6,  4,  3,  1,  2 },  //  the indexes of the display for the segments
  {  7, 12, 13, 11, 10,  8,  9 },
  { 16, 21, 22, 20, 19, 17, 18 },
  { 23, 28, 29, 27, 26, 24, 25 },
};

#define MIDDLE_BAR_1 14            // the bar in the middle, upper one
#define MIDDLE_BAR_2 15            // the bar in the middle, lower one


const int sclPin = 23;             // common SCL pin for all software I2C buses
const int sdaFirstPin = 24;        // the first SDA pin

#define NUM_DISPLAYS 30

// 30 pointers to 30 objects for the software I2C buses
SoftwareWire * pWIRE[NUM_DISPLAYS];

// 30 pointers to 30 objects for each LCD display
LiquidCrystal_I2C_Soft * pLCD[NUM_DISPLAYS];

// Counting the seconds with a millis timer.
// The two bars in the middle blink within that millis timer
int seconds;                       // 99 minutes will fit in a integer
unsigned long previousMillis;
const unsigned long interval = 1000UL;
const unsigned long middleBarDelay = 300UL;
bool middleBarTimer;


void setup() 
{
  StatusDisplayInit();

  // Create the objects for the I2C bus and for the displays.
  // The display will hold a pointer to its own I2C bus inside its object.
  // These objects are created, but never released from memory.
  for( int i=0; i<NUM_DISPLAYS; i++)
  {
    pWIRE[i] = new SoftwareWire( sdaFirstPin + i, sclPin);
    pLCD[i] = new LiquidCrystal_I2C_Soft( pWIRE[i]);
  }

  for( int i=0; i<NUM_DISPLAYS; i++)
  {
    pLCD[i]->init();
    pLCD[i]->backlight();

    // fill all the rows with full blocks (character 255 is full block)
    for( int row=0; row<2; row++)
    {
      pLCD[i]->setCursor( 0, row);

      for( int column=0; column<16; column++)
      {
        pLCD[i]->write( 255);
      }
    }
  }

  // Show initial value of zero
  showDigit( 0, 0);
  showDigit( 1, 0);
  showDigit( 2, 0);
  showDigit( 3, 0);

  previousMillis = millis();     // the setup could take a long time, start at begin
}


void loop() 
{
  unsigned long currentMillis = millis();

  if( currentMillis - previousMillis >= interval)
  {
    previousMillis += interval;   // special millis timer to stay in sync with time

    seconds++;
    if( seconds >= (100 * 60))   // 99 minutes + 59 seconds is the last valid one
      seconds = 0;

    // these four lines are not fully tested yet.
    int digit0 = (seconds / 600) % 10;
    int digit1 = (seconds / 60) % 10;
    int digit2 = (seconds / 10) % 6;
    int digit3 = seconds % 10;

    showDigit( 0, digit0);
    showDigit( 1, digit1);
    showDigit( 2, digit2);
    showDigit( 3, digit3);

    // Turn on the two bars in the middle
    lcdOnOff( MIDDLE_BAR_1, true);
    lcdOnOff( MIDDLE_BAR_2, true);

    // Start a millis-delay to turn the bars off
    middleBarTimer = true;
  }

  // The millis-delay-timer uses the millis-timer of the clock.
  // It is just another delay within the millis-timer of the clock.
  if( middleBarTimer)                 // millis-timer-delay running ?
  {
    if( currentMillis - previousMillis >= middleBarDelay)
    {
      lcdOnOff( MIDDLE_BAR_1, false);
      lcdOnOff( MIDDLE_BAR_2, false);
      middleBarTimer = false;         // turn off own millis-timer-delay
    }
  }

  StatusDisplayUpdate( seconds);      // update the extra OLED display
}


// The digit is from 0...3. The left one is 0
// The number is 0...9, it is the number that will be displayed
void showDigit( int digit, int number)
{
  // Lay the number through the pattern conversion table on the display
  // A single bit indicates if a segment should be on or off.
  // The index of the LCD display for that segment is in a table.
  for( int segment=0; segment<7; segment++)
  {
    int bit = bitRead( pattern7seg[number], segment);  // bit is on or off ?
    bool active = (bit == 1) ? true : false;   // turn bit into bool
    int index = index7seg[digit][segment];  // the index of the display

    lcdOnOff( index, active);
  }
}


// Turn segement on (true) or off (false)
// The segment of the 7-segment is a LCD display.
// The first parameter is the index of the display.
//
// There are a number of posibilities:
//  a. by turning on and off the backlight.
//  b. by changing the text from spaces to full blocks.
//  c. by turning on and off the display
//  d. any combination of a, b and c.
void lcdOnOff( int i, bool on)
{
  if( on)
  {
    pLCD[i]->backlight();
    pLCD[i]->display();
  }
  else
  {
    pLCD[i]->noBacklight();
    pLCD[i]->noDisplay();
  }
}