/* AC power meter with log V1.0 20200614PZEM-004Logger 測定器はPZEM-004T v3.0。表示は 0.96 インチ OLED (SSD1306, 128x64) 画面の焼き付き防止のためのスクリーンセーバー機能付き スリープ条件:パワー変動がX%以内がY秒以上。スリープ解除条件:パワー変化X%以上 or ボタン押し 2020/06/14 radiopench http://radiopench.blog96.fc2.com/ */ #include #include // PZEM004Tv3 library #include #include #include // 0.96inch OLED #include #define VCal 1.006 // 電圧感度補正係数 voltage calibration coefficient(dafault=1.0) #define ICal 0.9205 // 電流感度補正係数 current calibration coefficient(dafault=1.0) #define SelPin 2 // セレクト ボタン select button #define EntPin 3 // Enter ボタン enter button #define PzemRX 11 // recieve data (connect to PZEM TX) #define PzemTX 12 // send data (connect to PZEM RX) Adafruit_SSD1306 oled(128, 64, &Wire, -1); // OLED setup PZEM004Tv30 pzem(PzemRX, PzemTX); // PZEM004T setup 接続ピンPin11 to TX, Pin12 to RX unsigned int rangeTable[9] = {1, 2, 5, 10, 30, 60, 120, 300, 600}; // 記録時間間隔(秒)time Inerval Table unsigned int sleepT[9] = {5, 10, 20, 60, 120, 300, 600, 1200, 3600}; // スクリーンセーバー 起動時間 char buff[8]; // 文字列操作バッファ character format buffer int rangeOfLog; // ログ周期のレンジ番号 int timeToLog; // ログ出力までの残り時間 remaining time to output log int rangeOfSleep; // スリープ開始めでの時間のテーブルの番号 int timeToSleep; // スリープ開始までの時間 int enterCount = 0; // enterボタンが押された回数 int dispTime = 0; // 表示の経過時間 unsigned int logInterval; // 記録周期 log interval int deltaPow; // スリープから復旧するパワー変化率(%) boolean dispOn = true; // 画面表示フラグ(trueで画面表示) volatile boolean t2Flag = false; // タイミング同期フラグ timing sync variable volatile boolean buttonPushed = false; // セレクトボタンが押された(割り込み) float Vx, VAx, Wx, kWhx, PFx, Hzx, VARx; // 測定結果 measured value float Ax = 0.0; float lastAx = 0.0; // 前回の電流値 void setup() { oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); // set OLED I2C Address 0x3C Serial.begin(115200); pinMode(EntPin, INPUT_PULLUP); // Enterボタン enter button pinMode(SelPin, INPUT_PULLUP); // セレクトボタン select button uuPinOutputLow(0b10011111110000, 0b001111); // 未使用ピンをLOWで出力に設定 oled.setTextColor(WHITE); EEPROMread(); // EEPROMから設定値を読み出す if (digitalRead(EntPin) == LOW) { // 開始時にEnterボタンが押されていたら paramSet(); // 動作パラメーター設定 } startScreen(); // 開始画面 show start screen Serial.println(); // シリアルに開始メッセージを流す start message Serial.println(F(" , PZEM-004V3 Power meter start V1.0")); Serial.println(F(" , sec, V(rms), I(rms), VA, W, PF, kWh, Hz")); Serial.flush(); timeToLog = logInterval; // ログ周期をセットset interval measure(); // PZEM-004Tの測定結果を読み取り get value from PZEM-004Tv3 logPrint(); // 結果をシリアルに流す data out serial port dispInf(); // 結果をOLEDに表示 display data on OLED MsTimer2::set(1000, timer2IRQ); // MSタイマーで1秒間隔で割り込み IRQ interval 1sec MsTimer2::start(); attachInterrupt(0, pin2IRQ, FALLING); // ピン2(selectボタン)割り込み attachInterrupt(1, pin3IRQ, FALLING); // ピン3(Enterボタン)割り込み } void loop() { while (t2Flag == false) { // 1秒タイマー割り込み待ち wait MsTimer2 IRQ } t2Flag = false; dispTime ++; // スクリーンセーバー用に累積表示時間を更新 if (dispTime >= timeToSleep) { // スリープ待ち時間を超えていたら dispTime = timeToSleep; // 値はそこで止めておく dispOn = false; // 表示フラグOFF(画面表示しない) } if (buttonPushed == true) { // 待機中にどちらかのボタンが押された記録が残っていたら、 buttonPushed = false; // その記録をクリアして、 dispOn = true; // 表示フラグON(画面表示) dispTime = 0; // スリープ監視タイマーをリセット } if (digitalRead(EntPin) == LOW) { // 今でもenterボタンが押し続けられていたら enterCount++; // カウントアップ if (enterCount >= 3) { // 3回(3秒)以上押されていたら enterCount = 0; resetKWh(); // 積算電力の値をクリア } } else { // Entボタンが押し続けられていなかったら enterCount = 0; // enterカウントをクリア } lastAx = Ax; // 前回の電流値を保存 measure(); // 電力測定 get value from PZEM-004Tv3 if (((abs(Ax - lastAx) / Ax) > (float)deltaPow / 100.0) || // 電流が前回よりX%以上変化していたら、あるいは ((nan) のままなら下記2行は不要) ((lastAx > 0.001) && (Ax < 0.00001)) || // 前回がゼロ以上で、今回がゼロなら、あるいは ((Ax < 0.00001) && (lastAx > 0.001))) { // 前回がゼロで、今回がゼロ以上なら dispOn = true; // スリープ中止(表示をON) dispTime = 0; // スリープタイマーリセット } dispInf(); // 現在値をOLEDに表示 display present data to OLED timeToLog --; // ログタイマー減算 decrement log timer if (timeToLog == 0 ) { // タイムアップしてたら if log interval time is up, logPrint(); // シリアルに出力 output log data to serial port timeToLog = logInterval; // タイマーの値をセット set value for next cycle } } // end of loop void EEPROMread() { rangeOfLog = EEPROM.read(0); // EEPROMからレンジを読み出し if (rangeOfLog > 8) { // 異常値だったら、abnormal range number will be rangeOfLog = 5; // set to 5(60秒) } logInterval = rangeTable[rangeOfLog]; rangeOfSleep = EEPROM.read(1); // EEPROMからレンジを読み出し if (rangeOfSleep > 8) { // 異常値だったら、abnormal range number will be rangeOfSleep = 5; // set to 5 (10分) } timeToSleep = sleepT[rangeOfSleep]; deltaPow = EEPROM.read(2); // EEPROMからパワー変化率を読み出し if (deltaPow > 30) { // 異常値だったら、abnormal range number will be deltaPow = 10; // set to 10% } } void measure() { // PZEM-004T v3 から測定結果を受信 Vx = VCal * pzem.voltage(); // voltage(apply calibration correction) Ax = ICal * pzem.current(); // current(apply calibration correction) if (isnan(Ax) == true) { // 電流の値が nan (not a number)なら Ax = 0.0; // ゼロにする(nanのままでも大丈夫だったけど念のために処置) } VAx = Vx * Ax; // 皮相電力 calculate apparent power Wx = VCal * ICal * pzem.power(); // 有効電力 effective power(Use the value after calibration correction) PFx = pzem.pf(); // 力率 power factor kWhx = VCal * ICal * pzem.energy(); // 積算電力 sum of energy(Use the value after calibration correction) Hzx = pzem.frequency(); // 周波数 line frequency } void logPrint() { // シリアルに測定データーを流すserial out for log static long t = 0; Serial.print(F(" , ")); // this for the separator for terminal software timestamp text Serial.print(t); Serial.print(F(", ")); // time in second Serial.print(Vx, 1); Serial.print(F(", ")); // voltage Serial.print(Ax, 4); Serial.print(F(", ")); // current amps Serial.print(VAx, 3); Serial.print(F(", ")); // VA value Serial.print(Wx, 2); Serial.print(F(", ")); // wattage Serial.print(PFx, 2); Serial.print(F(", ")); // powewr factor Serial.print(kWhx, 3); Serial.print(F(", ")); // totall energy Serial.print(Hzx, 1); // frequency Serial.println(); t += logInterval; // increment accumurate time } void dispInf() { // 状態をOLEDに表示 display information on OLED oled.clearDisplay(); // clear display buffer if (dispOn == true) { oled.setTextSize(2); // 1行目は2倍角文字で表示 double size character if (Wx >= 100.0) { // 100W以上は dtostrf(Wx, 7, 1, buff); // 1234.5W } else { // 100W以下は dtostrf(Wx, 7, 2, buff); // 99.99W } oled.setCursor(0, 0); oled.print(buff); oled.print(F("W")); // 電力を2倍角で表示 display power value oled.setTextSize(1); // standerd size character oled.setCursor(110, 8); sprintf(buff, "%3d", timeToLog); // format decimal 3-digit oled.print(buff); // ログまでの時間を表示 remainig time display if (timeToLog == 0) { // if time up oled.setCursor(110, 0); oled.print(F("Log")); // display "Log" at top right } dtostrf(Vx, 5, 1, buff); oled.setCursor(0, 16); oled.print(F("V:")); oled.print(buff); oled.print(F("V")); // 電圧(V) voltage nnn.n dtostrf(Ax, 6, 3, buff); oled.setCursor(56, 16); oled.print(F("I: ")); oled.print(buff); oled.print(F("A")); // 電流(A) amps nn.nnn dtostrf(VAx, 7, 2, buff); oled.setCursor(0, 26); oled.print(F("Pappa:")); oled.print(buff); oled.print(F(" VA")); // 皮相電力(VA) volt amps nnnn.nn dtostrf(kWhx, 7, 3, buff); oled.setCursor(0, 36); oled.print(F("Energ:")); oled.print(buff); oled.print(F(" kWh")); // 積算電力値(kWh) watt hours nnn.nnn dtostrf(Hzx, 4, 1, buff); oled.setCursor(0, 46); oled.print(F("Freq : ")); oled.print(buff); oled.print(F(" Hz")); // 周波数(Hz) fequency nn.n dtostrf(PFx * 100.0, 3, 0, buff); oled.setCursor(0, 56); oled.print(F("PF:")); oled.print(buff); oled.print(F("% Y Y Y")); // 力率(%) power factor nnn oled.fillRect(50, 61, (60 * PFx) + 1, 3, WHITE); // 力率のバーグラフ Power factor bar } oled.display(); // actual display will be done at here } void paramSet() { // 運転パラメーター設定 Log interval time setting by button swith while (digitalRead(EntPin) == LOW) { // Enter ボタンが離されるまで待つ wait till the enter button is released oled.clearDisplay(); oled.setCursor(0, 0); oled.setTextSize(2); oled.print(F("Setting")); // 開始メッセージstart message oled.display(); } delay(30); while (digitalRead(EntPin) == HIGH) { // 次にEnterボタンが押されるまでの間に、インターバル設定 oled.clearDisplay(); oled.setCursor(0, 0); oled.println(F("Log Int.")); // 記録インターバル設定 display set interaval value oled.setCursor(20, 30); oled.print(logInterval); oled.print(F("sec")); // display the value oled.display(); if (digitalRead(SelPin) == LOW) { // セレクトボタンが押されたら rangeOfLog ++; // レンジ番号をひとつ進め increment range number if (rangeOfLog > 8) { // 上限超えてたらif beyond upper limit rangeOfLog = 0; // 下限に戻す(サーキュレート)circulate the number } logInterval = rangeTable[rangeOfLog]; // ボタンが離されたので値を保存 } while (digitalRead(SelPin) == LOW) { // ボタンが離させるまで待つ wait select botton relesed } delay(30); } // Enterが押されたら次に進む while (digitalRead(EntPin) == LOW) { // Enterが離されるまで待つ } delay(30); while (digitalRead(EntPin) == HIGH) { // 次にEnterボタンが押されるまでの間に、スリープ開始時間の設定 oled.clearDisplay(); oled.setCursor(0, 0); oled.println(F("Sleep aft")); // スリープ時間の設定画面を表示 display set interaval value oled.setCursor(20, 30); oled.print(timeToSleep); oled.print(F("sec")); // 現在値を表示 display the value oled.display(); if (digitalRead(SelPin) == LOW) { // セレクトボタンが押されたら rangeOfSleep ++; // レンジ番号をひとつ進め increment range number if (rangeOfSleep > 8) { // 上限超えてたら if beyond upper limit rangeOfSleep = 0; // 下限に戻す(サーキュレート)circulate the number } timeToSleep = sleepT[rangeOfSleep]; // スリープ時間をセット set interval time from table } while (digitalRead(SelPin) == LOW) { // ボタンが離させるまで待つ wait select botton relesed } delay(30); } while (digitalRead(EntPin) == LOW) { // Enterが離されるまで待つ } delay(30); while (digitalRead(EntPin) == HIGH) { // 次にEnterボタンが押されるまでの間に、スリープから復旧するパワー変化率の設定 oled.clearDisplay(); oled.setCursor(0, 0); oled.println(F("Wake up")); // スリープ時間の設定画面を表示 display set interaval value oled.setCursor(20, 30); oled.print(deltaPow); oled.print(F("%")); // 現在値を表示 display the value oled.display(); if (digitalRead(SelPin) == LOW) { // セレクトボタンが押されたら deltaPow += 5; // パワーを5% UP increment power value if (deltaPow > 30) { // 上限超えてたら if beyond upper limit deltaPow = 5; // 下限に戻す(サーキュレート)circulate the number } } while (digitalRead(SelPin) == LOW) { // ボタンが離させるまで待つ wait select botton relesed } delay(30); } while (digitalRead(EntPin) == LOW) { // Enterが離されるまで待つ } delay(30); EEPROM.write(0, rangeOfLog); // 設定値をEEPROMに保存 save value to EEPROM EEPROM.write(1, rangeOfSleep); // EEPROM.write(2, deltaPow); // } void startScreen() { // 開始画面 start message oled.clearDisplay(); oled.setTextSize(2); // 2倍角文字で、 double size character oled.setTextColor(WHITE); oled.setCursor(0, 0); oled.println(F("PowMon 1.0")); oled.print(F("Int: ")); oled.print(logInterval); oled.println(F("s")); // ログ周期表示 display log interval oled.print(F("Slp: ")); oled.print(timeToSleep); oled.println(F("s")); // スリープに入る時間を表示 display log interval oled.print(F("Wake:")); oled.print(deltaPow); oled.println(F("%")); // パワー変化検知レベルを表示 display log interval oled.display(); delay(2000); } void resetKWh() { // 積算電流の値をクリア clear accumulated energy value Serial.println(F("Reset kWh")); // ログにも記録 write message on the log pzem.resetEnergy(); // reset value oled.clearDisplay(); // 画面を一旦消してコマンドが実行されたことを表示 erase disp. to show command accepted oled.display(); delay(50); } void uuPinOutputLow(unsigned int d, unsigned int a) { // 指定ピンを出力/LOWに設定(un used Pin set to Output Low) // ビットパターンで該当ピンを指定(1で有効)。d:D13-D0, a:A5-A0 // PORTx=0, DDRx=1 unsigned int x; uint8_t oldSREG = SREG; // ステータスレジスタ保存(割込状態保存)*1 cli(); // 割込み禁止*1 x = d & 0x00FF ; PORTD &= ~x; DDRD |= x; // D0-7 x = (d >> 8) & 0x3F; PORTB &= ~x; DDRB |= x; // D8-13 x = a & 0x003F ; PORTC &= ~x; DDRC |= x; // A0-5 SREG = oldSREG; // ステータスレジスタ復元(割込状態復元)*1 } void timer2IRQ() { // MsTimer2 IRQ t2Flag = true; // set flag for loop } void pin2IRQ() { // ピン2割り込み(int0) buttonPushed = true; // フラグセット } void pin3IRQ() { // ピン3割り込み(int1) buttonPushed = true; // フラグセット }