diff --git a/software/VSC.cpp/VSC.cpp b/software/VSC.cpp/VSC.cpp new file mode 100644 index 0000000..32f0d35 --- /dev/null +++ b/software/VSC.cpp/VSC.cpp @@ -0,0 +1,230 @@ +/* Routines to +- access Victron Device using Bluetooth Low Energy (BLE) to retrieve the advertised data, +- decrypt & disassemble the values in the bit fields of the advertised data */ + +#include "ZZ.h" +#include "VSC.h" +#include + +#if defined(NO_AES) or !defined(WOLFSSL_AES_COUNTER) or !defined(WOLFSSL_AES_128) +#error "Missing AES, WOLFSSL_AES_COUNTER or WOLFSSL_AES_128" +#endif + +Aes aesEnc; +Aes aesDec; + +byte BIGarray[26] = {0}; // for all manufacturer data including encypted data +byte encKey[16]; // for nominated encryption key +byte iv[blkSize]; // initialisation vector +byte cipher[blkSize]; // encrypted data +byte output[blkSize]; // decrypted result + + + +// fwd decs +//id decryptAesCtr(bool VERBOSE); +void loadKey(); +//id reportSCvalues(); +void reportDeviceState(); +void reportControllerError(); +float parseBattVolts(); +float parseBattAmps(); +float parseKWHtoday(); +float parsePVpower(); +float parseLoadAmps(); +bool checkForbadArgs(); +void printBIGarray(); +void printByteArray(byte byteArray[16]); +void printBins(); + +// ---------------------------------------------------------------------- +BLEScan *pBLEScan = BLEDevice::getScan(); + +int scan_secs = 2; + +// -------------------------------------------------------------------------------- +// Scan for BLE servers for the advertising service we seek. Called for each advertising server +void AdDataCallback::onResult(BLEAdvertisedDevice advertisedDevice) { + if (advertisedDevice.getAddress().toString() == VICTRON_ADDRESS){ // select target device + String manufData = advertisedDevice.getManufacturerData(); + unsigned int len = manufData.length(); + if (len >= 11 && manufData[0] == 0xE1 && manufData[1] == 0x02 && manufData[2] == 0x10) { + manufData.getBytes(BIGarray, std::min(len, sizeof(BIGarray))); + BLEDevice::getScan()->stop(); + } + } +} +// -------------------------------------------------------------------------------- +// encryption routine not required here (covered in AES_CTR_enc_dec.ino) +// decrypt cipher -> outputs +void decryptAesCtr(bool VERBOSE){ + memcpy(encKey,key_SC,sizeof(key_SC)); // key_SC -> encKey[] + //loadKey(); // replaced by line above. enable loadKey() for multiple SC + iv[0] = BIGarray[7]; // copy LSB into iv + iv[1] = BIGarray[8]; // copy MSB into iv + memcpy(cipher, BIGarray + 10, 16); // BIGarray[11:26] -> cipher[1:16] + memset(&aesDec,0,sizeof(Aes)); // Init stack variables + if (VERBOSE) { + Serial << "key : "; printByteArray(encKey); Serial << '\n'; + Serial << "salt : "; printByteArray(iv); Serial << '\n'; + Serial << "cipher: "; printByteArray(cipher); Serial << '\n'; + } + wc_AesInit (&aesDec, NULL, INVALID_DEVID); // init aesDec + wc_AesSetKey (&aesDec, encKey, blkSize, iv, AES_ENCRYPTION); // load dec key + wc_AesCtrEncrypt(&aesDec, output, cipher, sizeof(cipher)/sizeof(byte)); // do decryption + if (checkForbadArgs()) Serial << F("**FAIL** bad args detected!"); // + wc_AesFree(&aesDec); // free up resources +} + +/* Activate for use on multiple controllers +void loadKey(){ + if (VICTRON_NAME == "My_Solar_Controller") memcpy(encKey,key_SC,sizeof(key_SC)); // key_SC -> encKey[] + else if (VICTRON_NAME == "My_SmartShunt_1") memcpy(encKey,key_SS,sizeof(key_SS)); // key_SS -> encKey[] + else if (VICTRON_NAME == "My_BMV712_P1") memcpy(encKey,key_P1,sizeof(key_P1)); // key_P1 -> encKey[] + else if (VICTRON_NAME == "My_BMV712_P2") memcpy(encKey,key_P2,sizeof(key_P2)); // key_P2 -> encKey[] + else if (VICTRON_NAME == "My_BMV712_P3") memcpy(encKey,key_P3,sizeof(key_P3)); // key_P3 -> encKey[] + else { + Serial << "\n\n *** Program HALTED: encryption key not set!\n"; + while(1); + } +} +*/ + +bool na_batV = false; +bool na_batA = false; +bool na_kWh = false; +bool na_pvW = false; +bool na_lodA = false; + +/* ------------------------------------------------------------------------ +Extract & decode bytes received and report current values. +NB: multiple bytes are little-endian (i.e order of double/triple bytes reversed) +signed ints use 2's complement, so the first mask excises the sign bit */ +void reportSCvalues(){ + float battV = parseBattVolts(); // -327.68 -> 327.66 V + float battA = parseBattAmps(); // -3276.8 -> 3276.6 A + float kWh = parseKWHtoday(); // 0 -> 655.34 kWh + float PV_W = parsePVpower(); // 0 -> 65534 W + float loadA = parseLoadAmps(); // 0 -> 51.0 A + //Serial << '\t'; + reportDeviceState(); Serial << " "; + reportControllerError(); Serial << " "; + if (na_batV) Serial << "n/a-"; else Serial << _FLOAT(battV,2); Serial << "V "; + if (na_batA) Serial << "n/a-"; else Serial << _FLOAT(battA,1); Serial << "A| "; + if (na_kWh) Serial << "n/a-"; else Serial << _FLOAT(kWh ,2); Serial << "kWh "; + if (na_pvW) Serial << "n/a-"; else Serial << _FLOAT(PV_W ,0); Serial << "W "; + if (na_lodA) Serial << "n/a-"; else Serial << _FLOAT(loadA,1); Serial << "A\n"; +} + +void reportDeviceState(){ + if (output[0] == 0) Serial << "_OFF_"; + else if (output[0] == 1) Serial << "Low_P"; + else if (output[0] == 2) Serial << "Fault"; + else if (output[0] == 3) Serial << "Bulk "; + else if (output[0] == 4) Serial << "Absor"; + else if (output[0] == 5) Serial << "Float"; + else if (output[0] == 6) Serial << "Store"; + else if (output[0] == 7) Serial << "Equal"; + else Serial << "*" << _WIDTHZ(_HEX(output[0]),2) << "*"; +} + +void reportControllerError(){ + if (output[1] == 0) Serial << "no_err"; + else if (output[1] == 1) Serial << "BATHOT"; + else if (output[1] == 2) Serial << "VOLTHI"; + else if (output[1] == 3) Serial << "REMC_A"; + else if (output[1] == 4) Serial << "REMC_B"; + else if (output[1] == 5) Serial << "REMC_C"; + else if (output[1] == 6) Serial << "REMB_A"; + else if (output[1] == 7) Serial << "REMB_B"; + else if (output[1] == 8) Serial << "REMB_C"; + else Serial << "*" << _WIDTHZ(_HEX(output[1]),2) << "*"; +} + +// Some of the routines below use static_cast to convert integers +// to floats while avoiding dud fractions from integer division. + +// Note: SC & BM use same bytes (2,3) for battery volts +float parseBattVolts(){ + bool neg = (output[3] & 0x80) >> 7; // extract sign bit + int16_t batt_mV10 = ((output[3] & 0x7F) << 8) + output[2]; // exclude sign bit from byte 3 + if (batt_mV10 == 0x7FFF) na_batV = true; + if (neg) batt_mV10 = batt_mV10 - 32768; // 2's complement = val - 2^(b-1) b = bit# = 16 + return (static_cast(batt_mV10)/100); // integer units 10mV converted to V as float +} + +// Battery Current (signed) 16 bits = sign bit + 15 bits +float parseBattAmps(){ + bool neg = ((output[5] & 0x80) >> 7); // extract sign bit + int16_t ma100 = ((output[5] & 0x7F) << 8) + output[4]; // exclude sign bit from byte 5 + if (ma100 == 0x7FFF) na_batA = true; + if (neg) ma100 = ma100 - 32768; // 2's complement = val - 2^(b-1) b = bit# = 16 + return (static_cast(ma100)/10); // convert mA100 to float A +} + +// Today's Yield 16bits (unsigned int) units 0.01kWh (10Wh). +float parseKWHtoday(){ + uint16_t Wh10 = (output[7] << 8) + output[6]; // NB little endian: byte[7] <-> byte[6] + if (Wh10 == 0xFFFF) na_kWh = true; + return (static_cast(Wh10)/100); // convert integer in 10Wh units to kWh as float +} + +// PV panel power in Watts +float parsePVpower(){ + uint16_t pvW = (output[9] << 8) + output[8]; // NB little endian: byte[9] <-> byte[8] + if (pvW == 0xFFFF) na_pvW = true; + return (static_cast(pvW)); // convert integer Watts to float +} + +// Load current? (Possibly irrelevant as VictronConnect doesn't even display this) +float parseLoadAmps(){ + uint16_t PVma100 = ((output[11] & 0x01) << 8) + output[10]; // NB little endian: byte[11] <-> byte[10] + if (PVma100 == 0x1FF) na_lodA = true; + return (static_cast(PVma100)/10); // convert integer in 100mA units to Amps as float +} + +// --------------------------------- Shared routines --------------------------------------------- +bool checkForbadArgs(){ + int x = wc_AesCtrEncrypt( NULL, output, cipher, sizeof(cipher)/sizeof(byte)); + int y = wc_AesCtrEncrypt(&aesDec, NULL, cipher, sizeof(cipher)/sizeof(byte)); + int z = wc_AesCtrEncrypt(&aesDec, output, NULL, sizeof(cipher)/sizeof(byte)); + if (x == WC_NO_ERR_TRACE(BAD_FUNC_ARG) && + y == WC_NO_ERR_TRACE(BAD_FUNC_ARG) && + z == WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + return false; + else return true; +} + +// print all bytes, before decryption +void printBIGarray(){ + Serial << "["; + int sz = sizeof(BIGarray); + for (int i=0; i + +extern void encryptAesCtr(); +extern void decryptAesCtr(bool quiet); + +const word32 blkSize = AES_BLOCK_SIZE * 1; + +extern byte inputs[blkSize]; +extern byte cipher[blkSize]; +extern byte output[blkSize]; + +extern void reportSCvalues(); +extern float parseBattVolts(); + +extern void printBIGarray(); +extern void printByteArray(byte byteArray[16]); +extern void printBins(byte byteArray[16]); + +extern float parseBattAmps(); + +extern float parseKWHtoday(); +extern float parsePVpower(); +extern float parseLoadAmps(); diff --git a/software/VSC.cpp/ZZ.cpp b/software/VSC.cpp/ZZ.cpp new file mode 100644 index 0000000..45c56df --- /dev/null +++ b/software/VSC.cpp/ZZ.cpp @@ -0,0 +1,24 @@ +// Global definitions + +#include "ZZ.h" + +#include // provides toupper() function + +bool VERBOSE = false; // true = verbose, false = quiet mode + +const char dashes[] PROGMEM = " ------------------- "; +const char line[] PROGMEM = "..........................................................\n"; + +// NB: Beware - Serial Monitor must be set with no line ending, else will be CR/LF detected here +void processSerialCommands() { + if(Serial.available() > 0) { + char readChar = Serial.read(); + if (readChar >= 42 && readChar <= 122) { // ignore if not between '*' and 'z' in ASCII table + readChar = toupper(readChar); // convert lower case characters to upper case + switch(readChar){ + case 'V': if (VERBOSE) {VERBOSE = false; Serial << F("\nVERBOSE - off\n\n") ;} + else {VERBOSE = true; Serial << F("\nVERBOSE - ON\n") ;} break; + } + } + } +} diff --git a/software/VSC.cpp/ZZ.h b/software/VSC.cpp/ZZ.h new file mode 100644 index 0000000..1b09e5e --- /dev/null +++ b/software/VSC.cpp/ZZ.h @@ -0,0 +1,25 @@ +#pragma once + +const char CJ_V[] = ".u"; +#define LINUX 0 // 0:compiling in Windows 1:Ubuntu + +#include + +extern const char dashes[]; // in PROGMEM +extern const char line[]; + +extern void processSerialCommands(); + +extern bool VERBOSE; + +// ----------------------------------------------------------------------------------------------- + +#define CF(x) ((const __FlashStringHelper *)x) // to stream a const char[] + +// "__FILE__" is predefined by the IDE as the full sketch folder location + file name. +// "FILENAME" is extracted here as the file name only, ignoring the folder location. +#if (LINUX) + #define FILENAME (strrchr(__FILE__,'/') ? strrchr(__FILE__,'/')+1 : __FILE__) // sketch file name - Linux +#else + #define FILENAME (strrchr(__FILE__,'\\') ? strrchr(__FILE__,'\\')+1 : __FILE__) // sketch file name - Windows +#endif \ No newline at end of file diff --git a/software/VSC.cpp/vgarden1.ino b/software/VSC.cpp/vgarden1.ino new file mode 100644 index 0000000..1573582 --- /dev/null +++ b/software/VSC.cpp/vgarden1.ino @@ -0,0 +1,241 @@ +/* + * This sketch sends a message to a TCP server + * + */ + +#include +#include +#include + +#include "ZZ.h" +#include "VSC.h" // Victron Solar Controller + +#include "wifi.h" + +WiFiMulti WiFiMulti; + +WiFiClient client; + +HTTPClient http; + +#define SERVER_IP "192.168.1.221:8086" + +int count = 0; +int pump_on = 1; +int pump_off = 0; + +int led = 15; +int pump_con = 6; + +int delay_ms = 500; + +void update_server(int count) { + Serial.print("Connecting to "); + Serial.println(SERVER_IP); + + // wait for WiFi connection + if ((WiFi.status() == WL_CONNECTED)) { + + Serial.print("[HTTP] begin...\n"); + // configure traged server and url + http.begin(client, "http://" SERVER_IP "/write?db=garagedb"); //HTTP + http.setTimeout(5000); + http.addHeader("Content-Type", "application/json"); + + //Serial.print("[HTTP] POST...\n"); + // start connection and send HTTP header and body + String count_string = String(count); + String start_string = "vstate,host=vgarden value="; + String post_string = String(start_string + count_string); + Serial.println(post_string); + int httpCode = http.POST(post_string.c_str()); + + // httpCode will be negative on error + if (httpCode > 0) { + // HTTP header has been send and Server response header has been handled + Serial.printf("[HTTP] POST... code: %d\n", httpCode); + + // file found at server + if (httpCode == HTTP_CODE_OK) { + const String& payload = http.getString(); + Serial.println("received payload:\n<<"); + Serial.println(payload); + Serial.println(">>"); + } + } else { + Serial.printf("[HTTP] POST... failed, error: %s\n", http.errorToString(httpCode).c_str()); + } + + http.end(); + } + else { + Serial.println("Reconnecting to WiFi..."); + WiFi.disconnect(); + WiFi.reconnect(); + } +} + +void send_sensor_data(String sensor_name, float sensor_value) { + Serial.print("Connecting to "); + Serial.println(SERVER_IP); + + // wait for WiFi connection + if ((WiFi.status() == WL_CONNECTED)) { + + Serial.print("[HTTP] begin...\n"); + // configure traged server and url + http.begin(client, "http://" SERVER_IP "/write?db=garagedb"); //HTTP + http.setTimeout(5000); + http.addHeader("Content-Type", "application/json"); + + //Serial.print("[HTTP] POST...\n"); + // start connection and send HTTP header and body + String count_string = String(sensor_value); + String start_string = sensor_name + ",host=vgarden_test value="; + String post_string = String(start_string + count_string); + Serial.println(post_string); + int httpCode = http.POST(post_string.c_str()); + + // httpCode will be negative on error + if (httpCode > 0) { + // HTTP header has been send and Server response header has been handled + Serial.printf("[HTTP] POST... code: %d\n", httpCode); + + // file found at server + if (httpCode == HTTP_CODE_OK) { + const String& payload = http.getString(); + Serial.println("received payload:\n<<"); + Serial.println(payload); + Serial.println(">>"); + } + } else { + Serial.printf("[HTTP] POST... failed, error: %s\n", http.errorToString(httpCode).c_str()); + } + + http.end(); + } + else { + Serial.println("Reconnecting to WiFi..."); + WiFi.disconnect(); + WiFi.reconnect(); + } +} + +void get_victron_data(){ + float battV = 0; + float battA = 0; + float kWh = 0; + float PV_W = 0; + float loadA = 0; + + pBLEScan->start(scan_secs, false); + delay(delay_ms); + + decryptAesCtr(VERBOSE); + + battV = parseBattVolts(); // -327.68 -> 327.66 V + Serial.print("battV: "); Serial.println(battV); + if (battV > 0.0 && battV < 20.0) { + send_sensor_data("battery_v", battV); + } + + battA = parseBattAmps(); // -3276.8 -> 3276.6 A + Serial.print("battA: "); Serial.println(battA); + if (battA > -2000.0 && battA < 2000.0) { + send_sensor_data("battery_a", battA); + } + + kWh = parseKWHtoday(); // 0 -> 655.34 kWh + Serial.print("kWh: "); Serial.println(kWh); + if (kWh < 10.0) { + send_sensor_data("yield_today", kWh); + } + + PV_W = parsePVpower(); // 0 -> 65534 W + Serial.print("PV: "); Serial.println(PV_W); + if (PV_W < 100.0) { + send_sensor_data("panel_p", PV_W); + } + + loadA = parseLoadAmps(); // 0 -> 51.0 A + Serial.print("loadA: "); Serial.println(loadA); + if (loadA < 25.0) { + send_sensor_data("load_p", loadA); + } +} + +void get_temp_data(){ + float temp_celsius = temperatureRead(); + Serial.print(temp_celsius); + Serial.println(" °C"); + + send_sensor_data("temperature", temp_celsius); +} + +void setup() { + + pinMode(led, OUTPUT); + pinMode(pump_con, OUTPUT); + + digitalWrite(led,LOW); + digitalWrite(pump_con,LOW); + + Serial.begin(115200); + + delay(10); + + // We start by connecting to a WiFi network + WiFiMulti.addAP(SSID_NAME, PASSWD); + + Serial.println(); + Serial.println(); + Serial.print("Waiting for WiFi... "); + + while (WiFiMulti.run() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + + Serial.println(""); + Serial.println("WiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + + delay(500); + + Serial.println("* init BLE ...\n"); + BLEDevice::init(""); + Serial.println("* setup scan ...\n"); + pBLEScan->setAdvertisedDeviceCallbacks(new AdDataCallback()); + pBLEScan->setActiveScan(true); // uses more power, but get results faster +} + +void loop() { + + if (count < 1){ + Serial.println("Pump On"); + digitalWrite(led,HIGH); + digitalWrite(pump_con,HIGH); + update_server(pump_on); + } + else { + Serial.println("Pump Off"); + digitalWrite(led,LOW); + digitalWrite(pump_con,LOW); + update_server(pump_off); + } + + get_temp_data(); + + get_victron_data(); + + if (count >= 9){ + count = 0; + } + else { + count++; + } + + delay(60000); // Sleep 1 minute + +}