/*** LoRa-Geraffel ***/ #include // For Testing/TestApplication, set to your Values!!! const char* devEui = "7788998877665544"; const char* appEui = "7788998877665544"; const char* appKey = "494136c46837032fd41a9cda30d51e33"; TTN_esp32 ttn ; /*** LoRa End ***/ /*** Display ***/ #include "heltec.h" #define __USE_DISPLAY 1 // 0 = Display Off, 1 = Display On, 2 = Display On only on first Boot after Restart or Reset /*** Display End ***/ /*** Batteriegeraffel ***/ #include #include #include #define MAXBATT 4200 // The default Lipo is 4200mv when the battery is fully charged. #define LIGHT_SLEEP_VOLTAGE 3750 // Point where start light sleep #define MINBATT 3300 // The default Lipo is 3200mv when the battery is empty...this WILL be low on the 3.3v rail specs!!! #define VOLTAGE_DIVIDER 3.20 // Lora has 220k/100k voltage divider so need to reverse that reduction via (220k+100k)/100k on vbat GPIO37 or ADC1_1 (early revs were GPIO13 or ADC2_4 but do NOT use with WiFi.begin()) #define DEFAULT_VREF 1100 // Default VREF use if no e-fuse calibration #define VBATT_SAMPLE 500 // Battery sample rate in ms #define VBATT_SMOOTH 50 // Number of averages in sample #define ADC_READ_STABILIZE 5 // in ms (delay from GPIO control and ADC connections times) #define LO_BATT_SLEEP_TIME 60*60*1000*1000 // How long when low batt to stay in sleep (us) = 1h #define VBATT_GPIO 21 // Heltec GPIO to toggle VBatt read connection ... WARNING!!! This also connects VEXT to VCC=3.3v so be careful what is on header. Also, take care NOT to have ADC read connection in OPEN DRAIN when GPIO goes HIGH #define __DEBUG 2 // 0 = OFF, 1 = AkkuDebug, 2 = ReceivedData uint16_t Sample(); uint16_t batt_voltage; void drawBattery(uint16_t, bool = false); esp_adc_cal_characteristics_t *adc_chars; /*** Batt End **/ // Serial2 für LED HardwareSerial IRLed(2); #define uS_TO_S_FACTOR 1000000 // Umberechnungsfaktor Mikro Sekunden auf Sekunden #define TIME_TO_SLEEP 10 // Dauer des Tiefschlafzustands in Sekunden #define RESTART_COUNTER 480 // nach wie vielen WakeUps soll ein Restart durchgeführt werden RTC_DATA_ATTR int bootCounter = 0; /********Zaehlerzeugs********/ #include "strings.h" #define MaxBuffer 300 // maximum buffer for received bytes #define polynom 0x1021 // x16 + x12 + x5 + 1 0x1021 #define Watt 0x1b #define Watt/h 0x1e #define var 0x1d #define var/h 0x20 #define Ampere 0x21 #define Voltage 0x23 #define Herz 0x2c char receivedByte[2]; char extractValue[14]; char dataDTZ[MaxBuffer]; int pointer = 0; uint16_t crc_ccitt = 0xffff; uint16_t crc = 0xffff; int pointerEOT = MaxBuffer-2; int ValueScale; uint32_t FromGridS = 0; uint32_t FromGridA = 0; uint32_t FromGridB = 0; uint32_t IntoGrid = 0; int U_L1 =0; int U_L2 =0; int U_L3 =0; int I_L1 =0; int I_L2 =0; int I_L3 =0; int Frequency = 0; int Consumtion = 0; int singleValue = 0; uint32_t value0 = 0; uint32_t value1 = 0; uint32_t value2 = 0; uint32_t valueD = 0; uint32_t meterID0=0; uint32_t meterID1=0; uint32_t meterID2=0; String MeterType = DTZ541; /****************************/ void setup() { if(bootCounter == RESTART_COUNTER) { ESP.restart(); } #if defined(__USE_DISPLAY) && (__USE_DISPLAY == 1 || __USE_DISPLAY == 2) if (__USE_DISPLAY == 1 || (__USE_DISPLAY == 2 && bootCounter == 0)) { Heltec.begin(true /*DisplayEnable Enable*/, false /*Heltec.LoRa Disable*/, false /*Serial Enable*/, false /*PABOOST Enable*/, NULL /*long BAND*/); Heltec.display->init(); //Heltec.display->flipScreenVertically(); Heltec.display->setFont(ArialMT_Plain_10); Heltec.display->clear(); } #endif Serial.begin(115000); while (!Serial); delay(20); ++bootCounter; Serial.println("BOOT-COUNTER: " + String(bootCounter)); ttn.begin(); // Join the network ttn.join(devEui, appEui, appKey); Serial.print("Joining TTN "); String punkt = ""; while (!ttn.isJoined()) { Serial.print("."); #if defined(__USE_DISPLAY) && (__USE_DISPLAY == 1 || __USE_DISPLAY == 2) if (__USE_DISPLAY == 1 || (__USE_DISPLAY == 2 && bootCounter == 1)) { Heltec.display->drawString(0, 0, "Start of LoRa"); Heltec.display->drawString(0, 10, "Try Joining"); punkt += "."; Heltec.display->drawString(0, 20, punkt); Heltec.display->display(); } #endif delay(500); } #if defined(__USE_DISPLAY) && (__USE_DISPLAY == 1 || __USE_DISPLAY == 2) if (__USE_DISPLAY == 1 || (__USE_DISPLAY == 2 && bootCounter == 1)) { Heltec.display->clear(); Heltec.display->drawString(0, 0, "Start of LoRa"); Heltec.display->drawString(0, 10, "Joined"); Heltec.display->display(); } #endif Serial.println("\njoined!"); // Make sure any pending transactions are handled first waitForTransactions(); //Setup IREmpfänger // Data-Line pinMode(22, OUTPUT); digitalWrite(22, LOW); // Stromversorung pinMode(23, OUTPUT); digitalWrite(23, HIGH); // BATT-Zeugs adc_chars = (esp_adc_cal_characteristics_t*)calloc(1, sizeof(esp_adc_cal_characteristics_t)); esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars); adc1_config_width(ADC_WIDTH_BIT_12); adc1_config_channel_atten(ADC1_CHANNEL_1,ADC_ATTEN_DB_6); #if defined(__DEBUG) && __DEBUG == 1 Serial.printf("ADC Calibration: "); if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { Serial.printf("eFuse Vref\n"); } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { Serial.printf("Two Point\n"); } else { Serial.printf("Default[%dmV]\n",DEFAULT_VREF); } #else if (val_type); // Suppress warning #endif // Prime the Sample register for (uint8_t i = 0;i < VBATT_SMOOTH;i++) { Sample(); } pinMode(VBATT_GPIO,OUTPUT); digitalWrite(VBATT_GPIO, LOW); // ESP32 Lora v2.1 reads on GPIO37 when GPIO21 is low delay(ADC_READ_STABILIZE); // let GPIO stabilize // BATT-Zeugs End Serial2.begin(9600, SERIAL_8N1, 22, -1); } int retryCounter = 0; void loop() { /**** BAT ZEUGS ****/ batt_voltage = Sample(); if (batt_voltage < MINBATT) { // Low Voltage cut off shut down to protect battery as long as possible Serial.printf(" !! Shutting down...low battery volotage: %dmV.\n",batt_voltage); delay(10); esp_sleep_enable_timer_wakeup(LO_BATT_SLEEP_TIME); esp_deep_sleep_start(); } /**** BAT ZEUGS ****/ /********************Zählergerümpel*****************/ if (Serial2.available()>0) { Serial2.readBytes(receivedByte,1); dataDTZ[pointer] = receivedByte[0]; crc = crc_one_byte(crc, dataDTZ[pointer], polynom); if (pointer > MaxBuffer-2){ pointer = 0;} //zur sicherheit, dass pointer nicht überläuft decodeDataSML(); } delay(ADC_READ_STABILIZE); if (pointer > MaxBuffer-10) { Serial2.flush(); Serial2.end(); #if defined(__USE_DISPLAY) && (__USE_DISPLAY == 1 || __USE_DISPLAY == 2) if (__USE_DISPLAY == 1 || (__USE_DISPLAY == 2 && bootCounter == 1)) { Heltec.display->clear(); drawBattery(batt_voltage, batt_voltage < LIGHT_SLEEP_VOLTAGE); Heltec.display->drawString(0,0, (String)Consumtion + " Watt"); Heltec.display->drawString(0,10, (String)FromGridS + " Wh"); Heltec.display->display(); } #endif if (Consumtion != 0) { showValues(); esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); esp_deep_sleep_start(); } else { pointer = 0; crc_ccitt = crc = 0xffff; #if defined(__USE_DISPLAY) && (__USE_DISPLAY == 1 || __USE_DISPLAY == 2) if (__USE_DISPLAY == 1 || (__USE_DISPLAY == 2 && bootCounter == 1)) { retryCounter++; Heltec.display->drawString(0, 20, "neue Runde: " + String(retryCounter)); Heltec.display->display(); } #endif if (retryCounter > 10) { ESP.restart(); } Serial2.begin(9600, SERIAL_8N1, 22, -1); delay(5); } } } // Poll the proper ADC for VBatt on Heltec Lora 32 with GPIO21 toggled uint16_t ReadVBatt() { uint16_t reading = 666; digitalWrite(VBATT_GPIO, LOW); // ESP32 Lora v2.1 reads on GPIO37 when GPIO21 is low delay(ADC_READ_STABILIZE); // let GPIO stabilize pinMode(ADC1_CHANNEL_1, OPEN_DRAIN); // ADC GPIO37 reading = adc1_get_raw(ADC1_CHANNEL_1); pinMode(ADC1_CHANNEL_1, INPUT); // Disconnect ADC before GPIO goes back high so we protect ADC from direct connect to VBATT (i.e. no divider) uint16_t voltage = esp_adc_cal_raw_to_voltage(reading, adc_chars); voltage*=VOLTAGE_DIVIDER; return voltage; } // Use a buffer to average/sample ADC uint16_t Sample() { static uint8_t i = 0; static uint16_t samp[VBATT_SMOOTH]; static int32_t t = 0; static bool f = true; if(f){ for(uint8_t c=0;c= VBATT_SMOOTH) {i=0;} uint16_t s = round(((float)t / (float)VBATT_SMOOTH)); #if defined(__DEBUG) && __DEBUG == 1 Serial.printf(" Smoothed of %d/%d = %d\n",t,VBATT_SMOOTH,s); #endif return s; } void drawBattery(uint16_t voltage, bool sleep) { uint16_t v = voltage; if (v < MINBATT) {v = MINBATT;} if (v > MAXBATT) {v = MAXBATT;} double pct = map(v,MINBATT,MAXBATT,0,100); uint8_t bars = round(pct / 10.0); #if defined(__USE_DISPLAY) && (__USE_DISPLAY == 1 || __USE_DISPLAY == 2) if (__USE_DISPLAY == 1 || (__USE_DISPLAY == 2 && bootCounter == 1)) { Heltec.display->drawString(0,40, String((int)round(pct))+"%"); Heltec.display->drawString(0,50, String(round(voltage/10.0)/100.0)+"V"); Heltec.display->display(); } #endif #if defined(__DEBUG) && __DEBUG == 1 static uint8_t c = 0; if ((c++ % 100) == 0) { c = 1; Serial.printf("VBAT: %dmV [%4.1f%%] %d bars\n", voltage, pct, bars); } #endif } //************ reflected uint16_t for CRC calculation ******************** // this function turns the bits -> MSB will be LSB from 0111001 to 1001110 // it's needed for counting crc reflected input and reflected output uint16_t refUint(uint16_t reflector, int bits){ // bits = how many bits should be reflected uint16_t shift=0; int i=0; do { shift <<= 1; if(reflector & 0x0001) shift |= 1; reflector >>= 1; i++; } while (i < bits); return shift; } //******************* calculate CRC16-CCITT ****************************** uint16_t crc_one_byte(uint16_t crcR, uint16_t oneByte, uint16_t poly){ oneByte = refUint(oneByte,8); // reflected input oneByte <<= 8; // shift left eight times for(int i=0; i<8; i++){ if((crcR ^ oneByte) & 0x8000) crcR =(crcR << 1)^ poly; else crcR <<= 1; oneByte <<= 1; } return crcR; } //*********************** check for beginning of SML-data-stream ************* //* bool checkBeginning(){ if (dataDTZ[pointer] == 0x01 && dataDTZ[pointer-1] == 0x01 && dataDTZ[pointer-2] == 0x01 && dataDTZ[pointer-3] == 0x01){ // check of start-code add fill-byte-check if(dataDTZ[pointer-4] == 0x1b && dataDTZ[pointer-5] == 0x1b && dataDTZ[pointer-6] == 0x1b && dataDTZ[pointer-7] == 0x1b){ // really is start-code #if defined(__DEBUG) && __DEBUG == 2 Serial.println("Startpunkt gefunden !!!!!!!!!!!!!!!!!!"); #endif return true; } else { return false; } } else { return false; } } //*********************** decode SML-data from holley DTZ541 or generetic SML ************* void decodeDataSML(){ #if defined(__DEBUG) && __DEBUG == 2 Serial.print("Pointer: " + (String)pointer + " -> "); Serial.println(dataDTZ[pointer], HEX); #endif if (pointer == 7) { if ( false == checkBeginning() ) { pointer = 0; return; } } if (pointer > MaxBuffer / 4) { if (dataDTZ[pointer-1] == 0x1a && dataDTZ[pointer-2] == 0x1b){ // check of end-code add fill-byte-check if(dataDTZ[pointer-3] == 0x1b && dataDTZ[pointer-4] == 0x1b){ // really is end-code pointerEOT = pointer; // save pointer of data-buffer-end #if defined(__DEBUG) && __DEBUG == 2 Serial.println("Endpunkt gefunden"); #endif } } if(pointerEOT == pointer) { crc_ccitt = refUint(crc,16)^0xffff; // reflected CRC-output and finish it with xor by 0xffff } } pointer ++; if(pointer > pointerEOT+2) { // protcol end and now arriveds the CRC-bytes only uint16_t check_crc = dataDTZ[pointerEOT+2]; // load the first value of crc check_crc <<= 8; // do it to a MSB check_crc |= dataDTZ[pointerEOT+1]; // add the lower part //**************** debug crc check ********************************** #if defined(__DEBUG) && __DEBUG == 2 Serial.print(check_crc); // debug Serial.print(":"); // debug Serial.println(crc_ccitt); // debug #endif //**************** debug end **************************************** if(check_crc == crc_ccitt){ // compare crc-strings readValuesOBIS(); //************** protokoll debug only ****************/ #if defined(__DEBUG) && __DEBUG == 2 for(int k=0;k<=600;k++ ){ Serial.print("0x"); Serial.print(dataDTZ[k], HEX); Serial.print(","); } #endif //************** protokoll debug end *****************/ } pointerEOT = MaxBuffer-2; pointer = 0; // clear for new begin of protocol crc = 0xFFFF; // set crc to begin by 0xffff } } //****************** read and decode values according OBIS-code ******************* void readValuesOBIS(){ Serial.println("START OF READ VALUES"); int scaler=1; DecodeOneValue(SearchOBIS(OBISoutGridS,7),4); //7 bytes check and drop 4 values until target Serial.println("singleValue--- GridS " + (String)singleValue); if(ValueScale < 0) scaler = ValueScale * -10; else scaler = ValueScale / 10; FromGridS = singleValue*scaler; scaler=1; DecodeOneValue(SearchOBIS(OBISoutGridA,7),4); // the return-value ValueScale contents potency *10*x / singleValue = value of OBIS Serial.println("singleValue--- GridA " + (String)singleValue); if(ValueScale < 0) scaler = ValueScale * -10; else scaler = ValueScale / 10; FromGridA = singleValue*scaler; scaler=1; DecodeOneValue(SearchOBIS(OBISoutGridB,7),4); // the return-value ValueScale contents potency *10*x / singleValue = value of OBIS if(ValueScale < 0) scaler = ValueScale * -10; else scaler = ValueScale / 10; FromGridB = singleValue*scaler; scaler=1; DecodeOneValue(SearchOBIS(OBISintoGrid,7),4); // the return-value ValueScale contents potency *10*x / singleValue = value of OBIS if(ValueScale < 0) scaler = ValueScale * -10; else scaler = ValueScale / 10; IntoGrid = singleValue*scaler; scaler=1; DecodeOneValue(SearchOBIS(OBIS_Watt,7),4); Serial.println("singleValue--- WATT " + (String)singleValue); if (singleValue == 0){ DecodeOneValue(SearchOBIS(OBIS_WattA,7),4); // the return-value ValueScale contents potency *10*x / singleValue = value of OBIS } Consumtion = singleValue / ValueScale; DecodeOneValue(SearchOBIS(OBIS_U_L1,7),4); U_L1 = singleValue; // the return-value ValueScale contents potency *10*x / singleValue = value of OBIS DecodeOneValue(SearchOBIS(OBIS_U_L2,7),4); U_L2 = singleValue; DecodeOneValue(SearchOBIS(OBIS_U_L3,7),4); U_L3 = singleValue; DecodeOneValue(SearchOBIS(OBIS_I_L1,7),4); I_L1 = singleValue; DecodeOneValue(SearchOBIS(OBIS_I_L2,7),4); I_L2 = singleValue; DecodeOneValue(SearchOBIS(OBIS_I_L3,7),4); I_L3 = singleValue; DecodeOneValue(SearchOBIS(OBIS_Freq,7),4); Frequency = singleValue; DecodeOneValue(SearchOBIS(OBIS_DTZ_ID,7),4); meterID0 = value0; meterID1 = value1; meterID2 = value2; Serial.println("meterID0---" + (String)meterID0); Serial.println("meterID1---" + (String)meterID1); Serial.println("meterID2---" + (String)meterID2); if (meterID0 == 0){ DecodeOneValue(SearchOBIS(OBIS_ServerID,7),4); meterID0 = value0; meterID1 = value1; meterID2 = value2; } DecodeOneValue(SearchOBIS(OBIS_Manuf,7),4); Serial.println("value0---" + (String)value0); if (value0 !=0 ){ MeterType = ""; int i3= value0 & 0x00ff; int i2 = (value0 >>8) & 0x00ff; int i1 = (value0 >>16) & 0x00ff; MeterType += (char)i1; MeterType += (char)i2; MeterType += (char)i3; } Serial.println("MeterType---" + (String)MeterType); } /****************************** compare search-string with buffer *****************************/ int CheckOBIS(char *checkCode, int i, int searchLength){ int result = 0; for (int y=0;y<=searchLength;y++){ if(*(checkCode+y) != dataDTZ[i+y]) return -1; // check chars if identical } return result; } //****************************** search OBIS-code signation *****************************/ int SearchOBIS(char *searchCode, int slength){ int x = 0; do { if (*searchCode == dataDTZ[x]){ if (CheckOBIS(searchCode, x, slength)==0)break; } x++; } while (x < MaxBuffer); if(x > MaxBuffer - 10)return 0; // nothing of OBIS founded else return x+slength+1; // pointer on the first value after OBIS-String } //****************** drop values until target value *******************/ int dropValues(int actPointer, int drops){ int d=0; int c=0; if(drops > 0){ for(int i=0; i 10) break; d=dataDTZ[actPointer]; // get value type and length c=d&0x70; if(c == 0x70){ // jump over list-elements too actPointer++; drops += d&0x0f; } else { d=d&0x0f; // extract lenght actPointer += d; // add length to drop value } } } return actPointer; } //************** get scale-factor for value *********************************************/ int getScalerActValue(int actPointer){ int actual_sc = dataDTZ[actPointer]; int scaler = 1; if(actual_sc == 0x52){ scaler = dataDTZ[actPointer+1]; switch (scaler){ case 253: scaler=1000; break; case 254: scaler=100; break; case 255: scaler=10; break; case 0: scaler=1; break; case 1: scaler=-10; break; case 2: scaler=-100; break; case 3: scaler=-1000; break; default: scaler=1; break; } } return scaler; } //************** decode one value from data / this days works till 32 bits only ********** int DecodeOneValue(int actPointer, int drops){ singleValue = 0; value2 = 0; value1 = 0; value0 = 0; if(actPointer == 0) return 0; actPointer = dropValues(actPointer,drops); if (dataDTZ[actPointer] == 01) return actPointer+1; // check if data not in used if (dataDTZ[actPointer] == 00) return actPointer; // check if data-block is end if (dataDTZ[actPointer] == 77) return actPointer; // check if new data-block begin ValueScale = getScalerActValue(actPointer-2); // get scaler of actual value char actual_TL = dataDTZ[actPointer]; int actual_Type = actual_TL & 0xf0; int actual_Length = actual_TL & 0x0f; if(actual_Length == 9 && actual_Type == 0x50 ){ // get only the last 4 bytes if eigth bytes delivered as integer actual_Length -= 4; actPointer +=4; } for (int i=1; i=8){ value2 = value2 << 8; valueD = value1; valueD = valueD >> 24; value2 += valueD; } if (i >= 4){ value1 = value1 << 8; valueD = value0; valueD = valueD >> 24; value1 += valueD; } value0 = value0 << 8; value0 += dataDTZ[actPointer+i]; } extractValue[i-1] = dataDTZ[actPointer+i]; extractValue[i]=0; } if(actual_Type == 0x50){ switch(actual_Length){ // handle negative values and set the rest of int32 case 4: if(bitRead(singleValue,13)){ singleValue |= 0xff000000;} break; case 3: if(bitRead(singleValue,15)){ singleValue |= 0xffff0000;} break; case 2: if(bitRead(singleValue, 7)){ singleValue |= 0xffffff00;} break; default: break; } } return actPointer+actual_Length; } /******************Fertsch Zaehler*****/ void showValues(){ #if defined(__DEBUG) && __DEBUG == 2 Serial.println("################"); Serial.println("Das fällt raus:"); Serial.println(""); Serial.println("FromGridS :" + (String)FromGridS); Serial.println("FromGridA :" + (String)FromGridA); Serial.println("FromGridB :" + (String)FromGridB); Serial.println("IntoGrid :" + (String)IntoGrid); Serial.println("U_L1 :" + (String)U_L1); Serial.println("U_L2 :" + (String)U_L2); Serial.println("U_L3 :" + (String)U_L3); Serial.println("I_L1 :" + (String)I_L1); Serial.println("I_L2 :" + (String)I_L2); Serial.println("I_L3 :" + (String)I_L3); Serial.println("Frequency :" + (String)Frequency); Serial.println("Consumtion :" + (String)Consumtion); Serial.println("MeterType: " + (String)MeterType); #endif String LoRaMessage = ""; LoRaMessage = String(batt_voltage); LoRaMessage += "/" + String(Consumtion); LoRaMessage += "/" + String(FromGridS); sprintf(extractValue,"%08X",meterID1); Serial.println(LoRaMessage); int streamLength = LoRaMessage.length() + 1; uint8_t rakBuffer[streamLength]; LoRaMessage.getBytes(rakBuffer, streamLength); ttn.sendBytes(rakBuffer, sizeof(rakBuffer)); waitForTransactions(); delay(50); } void waitForTransactions() { Serial.println("Waiting for pending transactions... "); Serial.println("Waiting took " + String(ttn.waitForPendingTransactions()) + "ms"); }