References:
I used the given Arduino code as above and adjusted the code for a Heltec WiFi Kit V3 board with an OLED display.
The code below display the received Victron EasySolar Charger Bluetooth advertised data on te OLED screen.
In addition, I adjusted the code to communicate the variables to my Thingspeek channel. You can see the picture accordingly.
Here is the code, BUT TAKE NOTE THERE ARE ALSO ADDED FILES VCC, and ZZ you need to adjust with your own secret keys and MAC address (see the reference link above):
#define HELTEC_BOARD true
#define SLOW_CLK_TYPE 1
#include "ZZ.h"
#include "VSC.h" // Victron Solar Controller
#include <user_settings.h>
#include <wolfssl.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <heltec.h>
#include "HT_SSD1306Wire.h"
#define OLED_ADDR 0x3C
// OLED setup for Heltec WiFi Kit V3 (SSD1306 128x64)
SSD1306Wire display(OLED_ADDR, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED);
// WiFi credentials
const char* ssid = ".........."; // WiFi SSID
const char* password = ".............."; // WiFi password
// Thingspeak API details
const char* thingspeakServer = "api.thingspeak.com";
const String apiKey = "................."; // Your Thingspeak API write key
const String channelID = "............."; // Your Thingspeak channel ID
// Digital output pin for voltage control
const int outputPin = 46; // GPIO 46, safe for Heltec V3 (not used by OLED or Vext)
const int ledPin = 37; // Built-in LED, GPIO 37 on Heltec V3 (GPIO 35 suggested, verify if needed)
int delay_ms = 500;
unsigned long lastThingspeakUpdate = 0;
const unsigned long thingspeakInterval = 60000; // Update every 60 seconds
// Function to control external power (Vext)
void VextON() {
pinMode(21, OUTPUT);
digitalWrite(21, LOW);
}
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 2000) ; // wait for serial, up to 1 sec
Serial << "\n\n======== Solar Controller ========\n";
Serial << "* wolfssl : V" << LIBWOLFSSL_VERSION_STRING << '\n';
Serial << "* Target : " << VICTRON_NAME << '\n';
Serial << "* Enter V to toggle VERBOSE mode ON/OFF\n";
// Initialize digital output pin
pinMode(outputPin, OUTPUT);
digitalWrite(outputPin, HIGH); // Start with pin HIGH
Serial << "* Digital output pin initialized: GPIO " << outputPin << '\n';
// Initialize built-in LED
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW); // Start with LED OFF (outputPin is HIGH)
Serial << "* Built-in LED initialized: GPIO " << ledPin << '\n';
// Initialize OLED
VextON(); // Power on OLED
display.init();
display.clear();
display.setFont(ArialMT_Plain_10); // 10-pixel font
display.drawString(0, 0, "Initializing...");
display.display();
Serial << "* OLED initialized\n";
// Initialize WiFi
Serial << "* Connecting to WiFi: " << ssid << '\n';
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial << ".";
}
Serial << "\n* WiFi connected, IP address: " << WiFi.localIP() << '\n';
// Initialize BLE
Serial << "* init BLE ...\n";
BLEDevice::init("");
Serial << "* setup scan ...\n";
pBLEScan->setAdvertisedDeviceCallbacks(new AdDataCallback());
pBLEScan->setActiveScan(true); // uses more power, but get results faster
//pBLEScan->setInterval(100); // time between consecutive scan starts
//pBLEScan->setWindow(99); // duration of each scan, less or equal setInterval value
Serial << "* scan for devices every " << delay_ms << " ms (and up to " << scan_secs << " secs/scan)\n";
Serial << CF(dashes) << "setup done" << CF(dashes) << '\n';
displayHeadings();
}
uint32_t loopCount = 0;
void loop() {
loopCount++;
processSerialCommands();
pBLEScan->start(scan_secs, false);
delay(delay_ms);
if (VERBOSE) Serial << '\n';
printLoopCount();
if (VERBOSE) {
Serial << CF(line);
Serial << "data : "; printBIGarray(); Serial << '\n';
}
decryptAesCtr(VERBOSE);
if (VERBOSE) {
Serial << "output: "; printByteArray(output); Serial << '\n';
//Serial << "binary:\n"; printBins(output); Serial << '\n';
Serial << "values: ";
}
reportSCvalues();
// Control digital output and LED
controlDigitalOutput();
// Update OLED display
updateOledDisplay();
// Send data to Thingspeak every 60 seconds
if (millis() - lastThingspeakUpdate >= thingspeakInterval) {
sendToThingspeak();
lastThingspeakUpdate = millis();
}
} // loop
void displayHeadings() {
Serial << '\n';
Serial << "\tstate " << "error " << "battV " << "battA " << " kWh " << "Watts " << "loadA\n";
Serial << "\t----- " << "----- " << "----- " << "----- " << "------- " << "----- " << "-----\n";
}
void printLoopCount() {
if (loopCount < 100) Serial << "0";
if (loopCount < 10) Serial << "0";
Serial << loopCount << ":\t";
}
void controlDigitalOutput() {
static bool pinState = false; // Track current state (false = HIGH, true = LOW)
float battV = parseBattVolts();
if (!na_batV) { // Only update if voltage is valid
if (battV < 12.3 && !pinState) {
digitalWrite(outputPin, LOW);
pinState = true;
digitalWrite(ledPin, HIGH); // LED ON when outputPin is LOW
Serial << "* GPIO " << outputPin << " set LOW (battV = " << _FLOAT(battV, 2) << "V < 12.3V), LED ON\n";
} else if (battV > 13.3 && pinState) {
digitalWrite(outputPin, HIGH);
pinState = false;
digitalWrite(ledPin, LOW); // LED OFF when outputPin is HIGH
Serial << "* GPIO " << outputPin << " set HIGH (battV = " << _FLOAT(battV, 2) << "V > 13.3V), LED OFF\n";
}
}
}
void updateOledDisplay() {
// Get the values to display (same as sent to Thingspeak)
String status = getDeviceStateString();
float battV = parseBattVolts();
float battA = parseBattAmps();
float kWh = parseKWHtoday();
float pvW = parsePVpower();
// Prepare strings, handle NaN cases
String voltStr = na_batV ? "NaN" : String(battV, 2) + "V";
String ampStr = na_batA ? "NaN" : String(battA, 1) + "A";
String kwhStr = na_kWh ? "NaN" : String(kWh, 2) + "kWh";
String wattStr = na_pvW ? "NaN" : String(pvW, 0) + "W";
String statusStr = (status == "1") ? "BULK" : "OFF"; // Match Thingspeak digital values
// Update OLED
display.clear();
display.setFont(ArialMT_Plain_10); // 10-pixel font
display.drawString(0, 0, "Status: " + statusStr);
display.drawString(0, 10, "Volts: " + voltStr);
display.drawString(0, 20, "Amps: " + ampStr);
display.drawString(0, 30, "kWh: " + kwhStr);
display.drawString(0, 40, "Watts: " + wattStr);
display.display();
}
void sendToThingspeak() {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
// Get the values to send
String status = getDeviceStateString();
float battV = parseBattVolts();
float battA = parseBattAmps();
float kWh = parseKWHtoday();
float pvW = parsePVpower();
// float loadA = parseLoadAmps(); // Excluded as per channel setup (Status, Volts, Amperes, kWh, Watts)
// Construct the Thingspeak URL
String url = "https://" + String(thingspeakServer) + "/update?api_key=" + apiKey;
url += "&field1=" + (na_batV ? "NaN" : String(battV, 2));
url += "&field2=" + (na_batA ? "NaN" : String(battA, 1));
url += "&field3=" + (na_kWh ? "NaN" : String(kWh, 2));
url += "&field4=" + (na_pvW ? "NaN" : String(pvW, 0));
url += "&field5=" + status; // Status as field5 to match channel order
// Start the HTTP request
http.begin(url);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
Serial << "* Data sent to Thingspeak successfully\n";
} else {
Serial << "* Failed to send to Thingspeak, HTTP code: " << httpCode << '\n';
}
http.end();
} else {
Serial << "* WiFi disconnected, attempting to reconnect...\n";
WiFi.reconnect();
}
}
String getDeviceStateString() {
// Convert device state to digital value: 1 for BULK, 2 for ABSORPTION, 3 for FLOAT, 0 for OFF or other states
switch (output[0]) {
case 3: return "1"; // BULK
case 4: return "2"; // ABSORPTION
case 5: return "3"; // FLOAT
default: return "0"; // OFF or any other state
}
}



©Chemcool Electronics Privacy Policy