#include "FspTimer.h"
#include <Wire.h>

// ----- 各種定数 -----
// 使用する端子
const int DUST_SENSOR = A0; // ほこりセンサを模擬した可変抵抗
const int MOTION_SENSOR = D3; // 人感センサを模擬したスイッチ
const int ONOFF_SW = D2; // 電源スイッチ
const int ENV_HUMIDIFIER = D9; // 加湿装置を模擬したLED
const int FAN_MOTOR = D10; // ファンのモータドライバ
const int ONOFF_LED = D13; // 電源LED
// 他にSCL、SDAはI2C通信で使用する

// メインループの待ち時間
const int ON_INTERVAL = 1*1000; // 電源ON時の待ち時間
const int OFF_INTERVAL = ON_INTERVAL; // 電源OFF時の待ち時間

// ほこりセンサ関係
const int DUST_DATA_NUM = 3; // ほこりセンサ平均の履歴数

// 人感センサ関係
const int MOTION_DATA_NUM = 3; // 人感センサデータの履歴数

// 温湿度関係
const float HUMIDIFY_THRESHOLD = 60.0; // 加湿装置を動かすしきい値

// ファン関係
const int FAN_HIGH = 200; // 回転高
const int FAN_MID = 100; // 回転中
const int FAN_LOW = 50; // 回転低
const int FAN_THRESHOLD_LOW = 300; // 回転低と中の間のしきい値
const int FAN_THRESHOLD_HIGH = 600; // 回転中と高の間のしきい値

// 電源スイッチ関係
const int ONOFF_HOLD_COUNT_MAX = 10; // 長押しの時間（この値×100ミリ秒）
const float ONOFF_CHECK_INTERVAL = 10.0; // 1/この値 の間隔で周期割り込み発生

// ----- 各種外部変数 -----

// ほこりセンサ関係
int dust_index; // 次に保存する先
int dust_data[DUST_DATA_NUM]; // 履歴の保存場所

// 人感センサ関係
int motion_count; // 人感センサの検出数
int motion_index; // 次に保存する先
int motion_data[MOTION_DATA_NUM]; // 履歴の保存場所

// 電源スイッチ関係
FspTimer onoff_timer; // 電源スイッチの周期タイマ
bool onoff_power; // 電源状態（trueで電源ON）

// ----- プログラム本体 -----
void setup() {
  dust_init(); // ほこりセンサ初期化
  motion_init(); // 人感センサ初期化
  env_init(); // 温湿度センサと液晶表示器と加湿装置の初期化
  fan_init(); // ファンの初期化
  onoff_init(); // 電源スイッチとLEDの初期化
}

void loop() {
  // 電源ON/OFF切り替え時に必要な処理を行うための直前の状態情報
  enum prev_exec_t {FIRST, ON, OFF};
  static prev_exec_t prev_exec = FIRST; // 初期値はONでもOFFでもない

  if (onoff_power_nointr()) { // 電源状態を参照し、電源ONならtrue
    if (prev_exec != ON) { // 直前の電源状態がONでなければ
      loop_init2nd(); // 電源ON以外からONに移行するための処理を実行
      prev_exec = ON; // 直前の状態を電源ONに設定
    }
    main_control(); // センサの値からファンを制御する
    humidify_control(); // センサの値から加湿装置を制御する
    delay(ON_INTERVAL);
  } else { // 電源OFFなら
    if (prev_exec != OFF) { // 直前の状態が電源OFFでなければ
      loop_deinit2nd(); // 電源OFF以外からOFFに移行するための処理を実行
      prev_exec = OFF; // 直前の状態を電源OFFに設定
    }
    delay(OFF_INTERVAL);
  }
}

// 電源ONに移行したときの処理
void loop_init2nd() {
  dust_init2nd();
  motion_init2nd();
  env_init2nd();
  fan_init2nd();
  onoff_init2nd();
}

// 電源OFFに移行したときの処理
void loop_deinit2nd() {
  dust_deinit2nd();
  motion_deinit2nd();
  env_deinit2nd();
  fan_deinit2nd();
  onoff_deinit2nd();
}

// センサの値からファンを制御する
void main_control() {
  int dust, pwm;

  if (motion_read() < 1) { // 人がいない状態
    pwm = FAN_LOW; // ファンを弱にする
  } else {
    dust = dust_read(); // ほこりセンサを読み取る
    if (dust < FAN_THRESHOLD_LOW) { // ホコリが少ない場合
      pwm = FAN_LOW; // ファンを弱にする
    } else if (dust > FAN_THRESHOLD_HIGH) { // ホコリが多い場合
      pwm = FAN_HIGH; // ファンを強にする
    } else { // それ以外の場合
      pwm = FAN_MID; // ファンを中にする
    }
  }
  fan_control(pwm); // ファンを動かす
}

// センサの値から温湿度を表示し、加湿装置を制御する
void humidify_control() {
  float temp, humi;

  env_read(temp, humi); // 温湿度読み取り
  env_display(temp, humi); // 温湿度表示

  // 湿度によって加湿装置をON/OFFする
  if (humi < HUMIDIFY_THRESHOLD) {
    env_humidify(true);
  } else {
    env_humidify(false);
  }
}

// ほこりセンサ処理
void dust_init() {
  // することなし
}

void dust_init2nd() {
  // 履歴保存場所をクリア
  for (int i = 0; i < DUST_DATA_NUM; i++) {
    dust_data[i] = 0;
  }
  dust_index = 0;
}

void dust_deinit2nd() {
  // することなし
}

int dust_read() {
  // ほこりセンサを読み取ってdust_data配列に格納
  // 格納先はdust_indexが示す
  dust_data[dust_index] = analogRead(DUST_SENSOR);
  ++dust_index;
  if (dust_index >= DUST_DATA_NUM) {
    dust_index = 0;
  }

  // 平均計算
  int dust_sum = 0;
  for (int i = 0; i < DUST_DATA_NUM; i++) {
    dust_sum += dust_data[i];
  }
  return dust_sum / DUST_DATA_NUM;
}

// 人感センサ処理
void motion_init() {
  // 人感センサから外部割り込みが発生するように設定
  pinMode(MOTION_SENSOR, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(MOTION_SENSOR), motion_trigger, FALLING);
}

void motion_init2nd() {
  // 履歴保存場所をクリア
  for (int i = 0; i < MOTION_DATA_NUM; i++) {
    motion_data[i] = 0;
  }
  motion_index = 0;

  // 割り込み禁止にして排他制御して変数にアクセスする
  noInterrupts();
  motion_count = 0;
  interrupts();
}

void motion_deinit2nd() {
  // することなし
}

// 人感センサ割り込み処理ルーチン
void motion_trigger() {
  // 電源ON状態なら検出カウント+1
  if (onoff_power) ++motion_count;
}

// 人感センサの測定値を保存して、一定期間の検出総数を返す
int motion_read() {
  // 割り込み禁止にして排他制御して変数にアクセスする
  noInterrupts();
  motion_data[motion_index] = motion_count; // 検出数を保存
  motion_count = 0; // 次の測定に向けて検出数をクリア
  interrupts();

  // 次に保存する先を更新
  ++motion_index;
  if (motion_index >= MOTION_DATA_NUM) {
    motion_index = 0;
  }

  // 検出総数計算
  int motion_sum = 0;
  for (int i = 0; i < MOTION_DATA_NUM; i++) {
    motion_sum += motion_data[i];
  }
  return motion_sum;
}

// ファン関連
void fan_init() {
  analogWrite(FAN_MOTOR, 0); // ファン停止
}

void fan_init2nd() {
  // することなし
}

void fan_deinit2nd() {
  analogWrite(FAN_MOTOR, 0); // ファン停止
}

void fan_control(int pwm) {
  analogWrite(FAN_MOTOR, pwm); // 指定値でファンを駆動
}

// 電源スイッチ処理
// 電源スイッチ初期化では、周期タイマ割り込みを設定する
void onoff_init() {
  pinMode(ONOFF_SW, INPUT_PULLUP);
  pinMode(ONOFF_LED, OUTPUT);
  digitalWrite(ONOFF_LED, LOW);

  uint8_t timer_type;
  int8_t timer_channel = FspTimer::get_available_timer(timer_type);
  onoff_timer.begin(TIMER_MODE_PERIODIC, timer_type, timer_channel, ONOFF_CHECK_INTERVAL, 50.0, onoff_isr, nullptr);
  onoff_timer.setup_overflow_irq();
  onoff_timer.open();
  onoff_timer.start();
}

void onoff_init2nd() {
  // することなし
}

void onoff_deinit2nd() {
  // することなし
}

// タイマ割り込み処理
// 状態遷移で処理を進める
void onoff_isr(timer_callback_args_t *arg)
{
  enum onoff_state_t {OFF, OFF2ON, ON, HOLD, ON2OFF};
  static onoff_state_t onoff_state = OFF;
  static int hold_count;

  int sw = digitalRead(ONOFF_SW); // 電源スイッチを読み取る

  switch (onoff_state) { // 現在の状態によって分岐
    case OFF: // OFF状態の場合
      if (sw == LOW) { // 電源スイッチが押されているなら
        onoff_state = OFF2ON; // OFF2ON状態に遷移
        digitalWrite(ONOFF_LED, HIGH); // ここで電源LEDを点灯
        onoff_power = true; // 電源ON状態にする（次のloop関数呼び出しで処理が進む）
      } else {
        // 電源スイッチが押されなければ何も起こらない
      }
      break;

    case OFF2ON: // OFF2ON状態（電源スイッチが離されるのを待つ）
      if (sw == LOW) { // 電源スイッチが押されているなら
        // 電源スイッチが離されるまで次に進まない
      } else { // 電源スイッチが離されたら
        onoff_state = ON; // 次の状態に進む
      }
      break;
      
    case ON: // ON状態
      if (sw == LOW) { // 電源スイッチが押されているなら
        onoff_state = HOLD; // HOLD（長押し判定）状態に進む
        hold_count = ONOFF_HOLD_COUNT_MAX; // 長押し判定回数を設定
      } else {
        // 電源スイッチが押されなければ何も起こらない
      }
      break;
      
    case HOLD: // HOLD（長押し判定）状態
      if (sw == LOW) { // 電源スイッチが押されているなら
        --hold_count; // 長押し判定回数を-1
        if (hold_count > 0) { // まだ長押しとは言えない
          // ここでは何もせず、長押し待ちを続ける
        } else { // 十分な時間長押しした
          onoff_state = ON2OFF; // 次の状態に進む
          digitalWrite(ONOFF_LED, LOW); // 電源LEDを消灯
          onoff_power = false; // 電源OFF状態にする（次のloop関数呼び出しで処理が進む）
        }
      } else { // 電源スイッチが離されたら
        onoff_state = ON; // 長押し失敗、ON状態に戻る
      }
      break;
      
    case ON2OFF: // ON2OFF状態（電源スイッチが離されるのを待つ）
      if (sw == LOW) { // 電源スイッチが押されているなら
        // 電源スイッチが離されるまで次に進まない
      } else { // 電源スイッチが離されたら
        onoff_state = OFF; // 次の状態に進む
      }
      break;

    default:
      break;
  }
}

// 排他制御を行って電源状態を読み取る
bool onoff_power_nointr() {
  noInterrupts();
  bool r = onoff_power;
  interrupts();
  return r;
}

// 温湿度センサ関係
#define SHT31_ADDR 0x45
#define SHT31_RESET_H 0x30
#define SHT31_RESET_L 0xA2
#define SHT31_CLEAR_STATUS_H 0x30
#define SHT31_CLEAR_STATUS_L 0x41
#define SHT31_SINGLE_HIGH_H 0x24
#define SHT31_SINGLE_HIGH_L 0x00
#define SHT31_DATA_LENGTH 6
#define SHT31_TEMP_H 0
#define SHT31_TEMP_L 1
#define SHT31_HUMI_H 3
#define SHT31_HUMI_L 4

#define LCD_ADDR 0x3e
#define LCD_CONTRAST 25  // コントラスト(0～63)

// 温湿度センサと液晶表示器と加湿装置の初期化 
void env_init() {
  Wire.begin();

  // センサをリセット
  Wire.beginTransmission(SHT31_ADDR);
  Wire.write(SHT31_RESET_H);
  Wire.write(SHT31_RESET_L);
  Wire.endTransmission();
  delay(10);

  // センサの状態レジスタをクリア
  Wire.beginTransmission(SHT31_ADDR);
  Wire.write(SHT31_CLEAR_STATUS_H);
  Wire.write(SHT31_CLEAR_STATUS_L);
  Wire.endTransmission();
  delay(10);

  // 液晶表示器の初期化
  lcd_init();

  // 加湿装置の停止
  pinMode(ENV_HUMIDIFIER, OUTPUT);
  env_humidify(false);
}

// 電源OFFからONに移行したときは液晶表示器をONにして表示クリア
void env_init2nd() {
  lcd_display_on();
  lcd_clear_display();
  delay(2);
}

// 電源ONからOFFに移行したときは液晶表示器をOFF
void env_deinit2nd() {
  lcd_display_off();
  delay(2);

  env_humidify(false);
}

// 温湿度センサ読み出し
void env_read(float& temp, float& humi) {
  unsigned int dac[SHT31_DATA_LENGTH];

  // 測定実行（ワンショット、高精度）
  Wire.beginTransmission(SHT31_ADDR);
  Wire.write(SHT31_SINGLE_HIGH_H);
  Wire.write(SHT31_SINGLE_HIGH_L);
  Wire.endTransmission();
  delay(150);

  // 測定データ読み出し
  Wire.requestFrom(SHT31_ADDR, SHT31_DATA_LENGTH);
  while (Wire.available() < SHT31_DATA_LENGTH) /*データが準備できるのを待つ*/;
  for (int i = 0; i < SHT31_DATA_LENGTH; i++) {
    dac[i] = Wire.read();
  }

  // 温湿度計算 計算式はセンサの仕様書を参照
  temp = (float)((dac[SHT31_TEMP_H] << 8) | dac[SHT31_TEMP_L]) * 175 / 65535.0 - 45.0;
  humi = (float)((dac[SHT31_HUMI_H] << 8) | dac[SHT31_HUMI_L]) * 100.0 / 65535.0;
}

// 温湿度表示
void env_display(float temp, float humi) {
  char val2str[8];
  char temp_str[17];
  char humi_str[17];

  // 表示文字列を作成
  dtostrf((double)temp, 5, 1, val2str);
  sprintf(temp_str, "T:%sC", val2str);
  dtostrf((double)humi, 5, 1, val2str);
  sprintf(humi_str, "H:%s%%", val2str);

  // 液晶に出力
  // 1行目
  lcd_setCursor(0, 0);
  lcd_printStr(temp_str);
  // 2行目
  lcd_setCursor(0, 1);
  lcd_printStr(humi_str);
}

// 加湿装置の制御
void env_humidify(bool onoff) {
  if (onoff) {
    digitalWrite(ENV_HUMIDIFIER, HIGH);
  } else {
    digitalWrite(ENV_HUMIDIFIER, LOW);
  }
}

// --- ここから液晶表示器の制御処理 ---
void lcd_init() {
  lcd_cmd(0b00111000); // function set
  lcd_cmd(0b00111001); // function set
  lcd_cmd(0b00000100); // EntryModeSet
  lcd_cmd(0b00010100); // interval osc
  lcd_cmd(0b01110000 | (LCD_CONTRAST & 0xF)); // contrast Low
  lcd_cmd(0b01011100 | ((LCD_CONTRAST >> 4) & 0x3)); // contast High/icon/power
  lcd_cmd(0b01101100); // follower control
  delay(200);
  lcd_cmd(0b00111000); // function set
  lcd_cmd(0b00001100); // Display On
  lcd_cmd(0b00000001); // Clear Display
  delay(2);
}

void lcd_cmd(byte x) {
  Wire.beginTransmission(LCD_ADDR);
  Wire.write(0b00000000); // CO = 0,RS = 0
  Wire.write(x);
  Wire.endTransmission();
}
 
void lcd_contdata(byte x) {
  Wire.write(0b11000000); // CO = 1, RS = 1
  Wire.write(x);
}
 
void lcd_lastdata(byte x) {
  Wire.write(0b01000000); // CO = 0, RS = 1
  Wire.write(x);
}
 
// 文字の表示
void lcd_printStr(const char *s) {
  Wire.beginTransmission(LCD_ADDR);
  while (*s) {
    if (*(s + 1)) {
      lcd_contdata(*s);
    } else {
      lcd_lastdata(*s);
    }
    s++;
  }
  Wire.endTransmission();
}
 
// 表示位置の指定
void lcd_setCursor(byte x, byte y) {
  lcd_cmd(0x80 | (y * 0x40 + x));
}

void lcd_clear_display() {
  lcd_cmd(0b00000001); // Clear Display
  delay(2);
}

void lcd_display_off() {
  lcd_cmd(0b00001000); // Display Off
  delay(2);
}
void lcd_display_on() {
  lcd_cmd(0b00001100); // Display On
  delay(2);
}
// --- ここまで液晶表示器の制御処理 ---
