/* 
 * Power Monitor
 * 
 * Records the power supply voltage every hour, logged in EEPROM.
 * When a button is pressed the last voltage is displayed in a large font.
 * A graph of the last five days readings is shown over the readout.
 * The graph's y axis range is from 0 to 6 volts.
 * 
 * When using Spence Konde's ATTinyCore https://github.com/SpenceKonde/ATTinyCore
 * and using a chip without Optiboot, with LTO enabled, this sketch takes 5782 bytes of flash.
 * 
 * The battery level is measured roughly every hour
 * and the measured voltages are stored in EEPROM
 * 
 * When a button is pressed, the display shows a graph of the latest readings.
 * 
 * Power on:
 * check eeprom is set up to record log of voltages
 * record current voltage
 * set up wake up interupts on button press and watch dog timer
 * set up the screen and display the graph and latest voltage
 * Go to sleep
 * 
 * Every 8 seconds the watch dog timer wakes up the device
 * If the number of wake-ups means an hour has passed,
 * then the current voltage is logged.
 * If the screen has been on for 2 wake-ups then turn off the screen.
 * Go to sleep
 * 
 * If a button is pressed, the screen is turned on,
 * and the device goes back to sleep.
 */

#include "TinyDebug.h"

#include <avr/sleep.h>
#include <EEPROM.h>
#include <Tiny4kOLED_bitbang.h>
#include "eeprom_contents.h"

// Large font containing only digits.
// TinyOLED-Fonts library needed.
// https://github.com/datacute/TinyOLED-Fonts
#include "Sansita_Swashed_Regular_57_Digits.h"

// Routines to set and clear bits (used in the sleep code)
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

static bool eepromOk = false;
static uint8_t currentContrast = BATTERY_DEFAULT_CONTRAST;

static uint8_t currentAddress = BATTERY_READINGS_ADDRESS;

#define MAIN_BUTTON 4
#define ENTER_BUTTON 3

// Variables for the Sleep/power down modes:
volatile boolean f_wdt = 0;
static int wdt_counter = 0;
static int graph_display_counter = 0;

// The watch dog timer wakes up the device every 8 seconds
// The device graphs the last 120 readings
// 10800 * 8 seconds = 1 day. 120 days = 4 months, mark every 7th or 30th
// 450 * 8 seconds = 1 hour. 120 hours = 5 days, mark every 24th
// 75 * 8 seconds = 10 minutes. 1200 minutes = 20 hours, mark every 6th
// 15 * 8 seconds = 2 minutes. 240 minutes = 4 hours, mark every 30th
#define REPORT_PERIOD 450
#define GRAPH_TICK_COUNT 24

// Watchdog Interrupt Service / is executed when watchdog timed out
ISR(WDT_vect) {
  f_wdt=1;  // set global flag
}

ISR(PCINT0_vect) {}

void setup() {
  Debug.begin();
  Debug.println(F("Tiny4kOLED Demo of a graph on top of text."));
  Debug.println(F("Limitations of wokwi:"));
  Debug.println(F("- The EEPROM doesn't remember its values"));
  Debug.println(F("- The measured voltage is always 5V, hence a straight line"));
  Debug.println(F("- Sleeping doesn't seem to work"));
  Debug.println(F("- The watchdog timer doesn't appear to be working"));
  Debug.println(F("- The oled can't be turned off"));

  setupADC();
  setupInputs();
  setupOLED();
  setupFromEEPROM();
  setupWDT();
  oled.setContrast(currentContrast);
  displayGraph();
}

static void setupADC() {
  ADMUX = 0b1100<<MUX0;        // Vcc Ref, Measure bandgap voltage (1.1)
  //ADCSRA = 1<<ADEN | 4<<ADPS0; // Enable, 62.5kHz ADC clock (16x prescalar)
  ADCSRA = 4<<ADPS0; // Disabled, 62.5kHz ADC clock (16x prescalar)
}

void setupInputs() {
  pinMode(MAIN_BUTTON, INPUT_PULLUP);
  pinMode(ENTER_BUTTON, INPUT_PULLUP);
  GIMSK = 1 << PCIE; // Enable pin-change interrupt
  PCMSK = (1 << MAIN_BUTTON) | (1 << ENTER_BUTTON);
}

void setupOLED(void) {
  oled.begin(128, 64, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);
  oled.clear();
  oled.setFont(FONTSANSITASWASHEDREGULAR57DIGITS);
}

static void setupFromEEPROM() {
  eepromOk = EEPROM.read(BATTERY_MAGIC_ADDRESS    ) == BATTERY_MAGIC0 &&
             EEPROM.read(BATTERY_MAGIC_ADDRESS + 1) == BATTERY_MAGIC1 &&
             EEPROM.read(BATTERY_MAGIC_ADDRESS + 2) == BATTERY_MAGIC2 &&
             EEPROM.read(BATTERY_MAGIC_ADDRESS + 3) == BATTERY_MAGIC3;
  if (eepromOk) {
    currentContrast = EEPROM.read(BATTERY_CONTRAST_ADDRESS);
    currentAddress = EEPROM.read(BATTERY_CURRENT_ADDRESS);
    recordVoltage(); // Write a new value at power-on so that
  } else {
    settingsResetAction();
  }
}

static void setupWDT(void) {
  int ii = 9;
  byte bb;
  if (ii > 9 ) ii=9;
  bb=ii & 7;
  if (ii > 7) bb|= (1<<5);
  //bb|= (1<<WDCE);
  MCUSR &= ~(1<<WDRF);
  // start timed sequence
  WDTCR |= (1<<WDCE) | (1<<WDE);
  // set new watchdog timeout value
  WDTCR = bb;
  WDTCR |= _BV(WDIE);
}

void loop() {
  if (!processButtonInputs()) {
    system_sleep();
  }
  if (f_wdt == 1) {
    Debug.println(F("This is never hit?"));
    f_wdt=0;
    wdt_counter++;
    if (wdt_counter == REPORT_PERIOD) {
      wdt_counter = 0;
      recordVoltage();
    }
    if (graph_display_counter > 0) {
      graph_display_counter--;
      if (graph_display_counter == 0) {
        oled.off();
      }
    }
  }
}

bool processButtonInputs(void) {
  bool mainButtonIsDown = digitalRead(MAIN_BUTTON) == LOW;
  if (mainButtonIsDown) {
    displayGraph();
  }
  bool enterButtonIsDown = digitalRead(ENTER_BUTTON) == LOW;
  if (enterButtonIsDown) {
    displayGraph();
  }
  return mainButtonIsDown || enterButtonIsDown;
}

static uint8_t ticks[] = {
  0x04, // 5.5V to 6.2V
  0x10, // 4.7V to 5.4V
  0x40, // 3.9V to 4.6V
  0x00, // 3.1V to 3.8V
  0x01, // 2.3V to 3.0V
  0x04, // 1.5V to 2.2V
  0x10, // 0.7V to 1.4V
  0x40  // bottom ticks line, 0.0 to 0.6 V
  };

static uint8_t overlayGraph(uint8_t x, uint8_t y, uint8_t b) {
  int itemAddress = currentAddress + x - 2;
  if (itemAddress >= 128) itemAddress -= (128 - BATTERY_READINGS_ADDRESS);
  uint8_t bottomBit = (7-y) << 3;
  uint8_t reading = EEPROM.read(itemAddress);
  uint8_t v = reading; // mask out pixel below graph
  if ((v >= bottomBit) && (v < (bottomBit + 8))) {
    b &= ~(1 << (7 - (v - bottomBit)));
  }
  v++; // Shift graph up 1 pixel above ticks line
  if ((v >= bottomBit) && (v < (bottomBit + 8))) {
    b |= 1 << (7 - (v - bottomBit));
  }
  v++;// mask out pixel above graph
  if ((v >= bottomBit) && (v < (bottomBit + 8))) {
    b &= ~(1 << (7 - (v - bottomBit)));
  }
  return b;
}

static void displayGraph() {
  uint8_t reading;
  for (uint8_t line = 0; line < 8; line++) {
    uint8_t bottomBit = (7-line) << 3;
    oled.setCursor(0,line);
    oled.startData();
    oled.sendData(ticks[line]);
    oled.sendData(0xFF);
    int itemAddress = currentAddress; // oldest reading
    uint8_t tick_column = 0;
    do {
      reading = EEPROM.read(itemAddress);
      uint8_t v = reading + 1; // Shift graph up 1 pixel above ticks line
      uint8_t b = 0;
      if ((v >= bottomBit) && (v < (bottomBit + 8))) {
        b = 1 << (7 - (v - bottomBit));
      }
      if (line == 7) {
        b |= 0x40;
        tick_column++;
        if (tick_column == GRAPH_TICK_COUNT) {
          b |= 0x80;
          tick_column = 0;
        }
      }
      oled.sendData(b);
      itemAddress++;
      if (itemAddress >= 128) itemAddress = BATTERY_READINGS_ADDRESS;
    } while (itemAddress != currentAddress);
    oled.sendData(0xFF);
    oled.sendData(ticks[line]);
    oled.endData();
  }
  // This can be made more efficient by not drawing the following large portion of the graph twice.
  oled.setCursor(27,1);
  oled.setCombineFunction(&overlayGraph);
  oled.print((float)reading/10.0, 1);
  oled.setCombineFunction(NULL);
  oled.on();
  graph_display_counter = 2;
}
/*
static void setContrast(void) {
  oled.setContrast(currentContrast);
  if (eepromOk) {
    EEPROM.write(BATTERY_CONTRAST_ADDRESS, currentContrast);
  }
}
*/
void settingsResetAction(void) {
  // EEPROM Header
  for (uint16_t offset = 0; offset < sizeof(header); offset++) {
    EEPROM.write(offset, pgm_read_byte(&header[offset]));
  }
  // Default all readings to current voltage value
  uint16_t result = readADC();
  //result = 1126400L / result; // Calculate Vcc (in mV); 1.1*1024*1000
  result = 11264L / result; // 10ths of a volt

  for (uint16_t address = sizeof(header); address < 128; address++) {
    EEPROM.write(address, (byte)result);
  }
  eepromOk = true;
  currentAddress = BATTERY_READINGS_ADDRESS;
  currentContrast = BATTERY_DEFAULT_CONTRAST;
}

static void recordVoltage(void) {
  uint16_t result = readADC();
  //result = 1126400L / result; // Calculate Vcc (in mV); 1.1*1024*1000
  result = 11264L / result; // 10ths of a volt
  EEPROM.write(currentAddress, (byte)result);
  currentAddress++;
  if (currentAddress >= 128) {
    currentAddress = BATTERY_READINGS_ADDRESS;
  }
  EEPROM.write(BATTERY_CURRENT_ADDRESS, currentAddress);
}

static uint16_t readADC() {
  uint8_t low,high;
  sbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter ON  
  ADCSRA = ADCSRA | 1<<ADSC;           // Start
  do {} while (ADCSRA & 1<<ADSC);      // Wait while conversion in progress
  low = ADCL;
  high = ADCH;
  cbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter OFF
  return ((uint16_t)high<<8 | low);
}

void system_sleep() {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  //sleep_enable();
  sleep_mode();                        // System actually sleeps here
  //sleep_disable();                     // System continues execution here when watchdog timed out 
}
ATTINY8520PU