Skip to content

Instantly share code, notes, and snippets.

@arduinka55055
Created January 18, 2026 16:27
Show Gist options
  • Select an option

  • Save arduinka55055/38a4f7d846d67f93bd25ff202a4df454 to your computer and use it in GitHub Desktop.

Select an option

Save arduinka55055/38a4f7d846d67f93bd25ff202a4df454 to your computer and use it in GitHub Desktop.
Dyness (pylon mode) RS485 battery communication parser
#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
// --- WIFI CONFIGURATION ---
const char* ssid = "ssid"; // <--- CHANGE THIS
const char* password = "password"; // <--- CHANGE THIS
const char* udpAddress = "192.168.0.100";
const int udpPort = 8323;
WiFiUDP udp;
// --- RS485 Configuration ---
// Using Hardware Serial (Serial) for RS485 RX-only
// Connect: RS485 TX (from module) -> ESP8266 RX (D0/GPIO3)
// No TX pin needed since we're only receiving
// No enable pin needed for receive-only
// ST7735 Display Pins
// ST7735 Display Pins
#define TFT_CS 15//20
#define TFT_RST 2//21
#define TFT_DC 16//22
#define TFT_MOSI 13//23
#define TFT_SCLK 14//15
#define RS485_EN 4
// Initialize Display
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
// No SoftwareSerial needed - using Hardware Serial
// --- DATA STRUCTURES ---
struct PylonStats {
float voltage = 0.0;
float current = 0.0;
int soc = 0;
int soh = 0; // Leave as zero (not provided in protocol)
float temp = 0.0;
bool updated = false;
};
// Packed struct for UDP transmission
struct __attribute__((packed)) PylonPacket {
float voltage;
float current;
int32_t soc;
int32_t soh;
float temp;
bool updated;
};
PylonStats batteryData;
unsigned long lastUpdate = 0;
unsigned long lastRS485Activity = 0;
String rxBuffer = "";
// Function Prototypes
bool parseRS485Message(String message);
bool isAllZeros(const uint8_t *data, uint8_t len);
void updateDisplay();
void updateHeader();
void drawLabels();
void sendToUDP();
// Helper function to convert hex string to int (signed)
int16_t hexToInt(String hexStr, bool signedVal = true) {
if (hexStr.length() == 0) return 0;
uint16_t val = (uint16_t)strtol(hexStr.c_str(), NULL, 16);
if (signedVal && val > 0x7FFF) {
val -= 0x10000;
}
return (int16_t)val;
}
uint16_t hexStringToUInt16(String hexStr) {
uint16_t result = 0;
for (int i = 0; i < hexStr.length(); i++) {
char c = hexStr.charAt(i);
result <<= 4;
if (c >= '0' && c <= '9') {
result |= (c - '0');
} else if (c >= 'A' && c <= 'F') {
result |= (c - 'A' + 10);
} else if (c >= 'a' && c <= 'f') {
result |= (c - 'a' + 10);
}
}
return result;
}
void setup() {
// IMPORTANT: For hardware serial receive-only setup:
// 1. Serial.begin(115200) for debugging (uses GPIO1/TX, GPIO3/RX)
// 2. But we want to use GPIO3/RX for RS485
// 3. We can't use Serial for both debugging AND RS485 at the same time
// Option 1: Use Serial for debugging during development
// Then switch to RS485 for production
// Option 2: Use Serial for RS485 and SoftwareSerial for debugging
// Let's use Serial for debugging first to test WiFi/display
Serial.begin(9600);
delay(1000);
Serial.println("\n\nESP8266 Battery Monitor Starting...");
Serial.println("Using Hardware Serial for RS485 (RX-only)");
// 0. Setup WiFi
WiFi.hostname("esp8266batt");
ArduinoOTA.setHostname("esp8266batt");
ArduinoOTA.setPassword("esp-flashing");
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 20) {
delay(250);
Serial.print(".");
retry++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi Connected!");
Serial.print("IP: "); Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi Failed (continuing offline)");
}
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH) {
type = "sketch";
} else { // U_FS
type = "filesystem";
}
// NOTE: if updating FS this would be the place to unmount FS using FS.end()
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) {
Serial.println("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
Serial.println("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
Serial.println("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
Serial.println("Receive Failed");
} else if (error == OTA_END_ERROR) {
Serial.println("End Failed");
}
});
ArduinoOTA.begin();
// 1. Initialize Display
Serial.println("Initializing Display...");
SPI.begin();
tft.initR(INITR_144GREENTAB);
tft.setRotation(1);
tft.fillScreen(ST7735_BLACK);
drawLabels();
updateHeader();
// 2. Setup Hardware Serial for RS485
// IMPORTANT: We need to reconfigure Serial for RS485
// Serial already began at 115200 for debugging
// Reinitialize Serial at 9600 for RS485
pinMode(RS485_EN, OUTPUT);
digitalWrite(RS485_EN, 0);
Serial.flush();
delay(100);
// Switch Serial to RS485 mode (9600 baud, RX on GPIO3)
//Serial.begin(9600, SERIAL_8N1, SERIAL_RX_ONLY);
// Note: SERIAL_RX_ONLY disables TX, freeing GPIO1 for other uses
Serial.println("RS485 Initialized at 9600 baud (RX-only)");
Serial.println("Serial monitor won't work now - use another method for debugging");
// Alternative: If you want to keep Serial for debugging,
// use SoftwareSerial for debugging instead:
// SoftwareSerial debugSerial(14, 12); // RX, TX (D5, D6)
// debugSerial.begin(115200);
// Then use debugSerial.println() instead of Serial.println()
// Clear receive buffer
while (Serial.available()) {
char c = Serial.read();
Serial.print("Startup Read: ");
Serial.println(c, HEX);
}
Serial.println("Setup Complete!");
}
void loop() {
ArduinoOTA.handle();
// Read RS485 messages from Hardware Serial
if (Serial.available()) {
lastRS485Activity = millis();
char c = Serial.read();
// Echo to debug if needed (but Serial is now RS485)
// For debugging, you'd need SoftwareSerial
// Check for start of frame
if (c == '~') {
rxBuffer = "~";
}
// Check for end of frame (carriage return)
else if (c == '\r') {
if (rxBuffer.length() > 0 && rxBuffer.startsWith("~")) {
// Parse the message
if (parseRS485Message(rxBuffer)) {
batteryData.updated = true;
}
}
rxBuffer = "";
}
// Accumulate characters
else if (rxBuffer.length() > 0 && isprint(c)) {
rxBuffer += c;
}
}
// Debug: Show RS485 activity (would need SoftwareSerial for output)
if (millis() - lastRS485Activity > 5000 && lastRS485Activity > 0) {
// Can't Serial.println here since Serial is RS485
lastRS485Activity = millis();
}
// Update Screen and Send UDP every 1 second
if (millis() - lastUpdate >= 1000) {
updateHeader();
if (batteryData.updated) {
updateDisplay();
sendToUDP();
batteryData.updated = false;
}
lastUpdate = millis();
}
}
void sendToUDP() {
if (WiFi.status() == WL_CONNECTED) {
PylonPacket packet;
packet.voltage = batteryData.voltage;
packet.current = batteryData.current;
packet.soc = (int32_t)batteryData.soc;
packet.soh = (int32_t)batteryData.soh; // Will be 0
packet.temp = batteryData.temp;
udp.beginPacket(udpAddress, udpPort);
udp.write((uint8_t*)&packet, sizeof(packet));
udp.endPacket();
}
}
void updateHeader() {
tft.fillRect(0, 0, 128, 9, ST7735_BLACK);
tft.setTextSize(1);
tft.setCursor(2, 0);
if (WiFi.status() == WL_CONNECTED) {
tft.setTextColor(ST7735_GREEN);
tft.print(WiFi.localIP());
} else {
tft.setTextColor(ST7735_RED);
tft.print("OFFLINE");
}
}
void drawLabels() {
tft.setTextSize(1);
tft.setTextColor(ST7735_CYAN);
tft.drawLine(0, 10, 128, 10, ST7735_BLUE);
tft.setTextColor(ST7735_WHITE);
int x_label = 0;
int y_start = 20;
int y_step = 12;
tft.setCursor(x_label, y_start); tft.print("VOLT:");
tft.setCursor(x_label, y_start + y_step); tft.print("AMP :");
tft.setCursor(x_label, y_start + y_step * 2); tft.print("TEMP:");
tft.setCursor(x_label, y_start + y_step * 3); tft.print("SOH :");
tft.drawLine(0, 75, 128, 75, ST7735_BLUE);
}
void updateDisplay() {
int x_val = 40;
int y_start = 20;
int y_step = 12;
// 1. Voltage
tft.setTextSize(1);
tft.setCursor(x_val, y_start);
tft.setTextColor(ST7735_GREEN, ST7735_BLACK);
tft.print(batteryData.voltage, 2);
tft.print("V");
// 2. Current
tft.setCursor(x_val, y_start + y_step);
if (batteryData.current < 0) tft.setTextColor(ST7735_RED, ST7735_BLACK);
else tft.setTextColor(ST7735_GREEN, ST7735_BLACK);
tft.print(batteryData.current, 1);
tft.print("A ");
// 3. Temperature
tft.setCursor(x_val, y_start + y_step * 2);
tft.setTextColor(ST7735_ORANGE, ST7735_BLACK);
tft.print(batteryData.temp, 1);
tft.print("C");
// 4. SOH (will be 0)
tft.setCursor(x_val, y_start + y_step * 3);
tft.setTextColor(ST7735_MAGENTA, ST7735_BLACK);
tft.print(batteryData.soh);
tft.print("% ");
// --- BOTTOM SECTION (SOC & WATTS) ---
float power = batteryData.voltage * batteryData.current;
// A. SOC (Big Font)
tft.setTextSize(2);
tft.setCursor(1, 85);
if (batteryData.soc < 20) tft.setTextColor(ST7735_RED, ST7735_BLACK);
else if (batteryData.soc < 50) tft.setTextColor(ST7735_YELLOW, ST7735_BLACK);
else tft.setTextColor(ST7735_GREEN, ST7735_BLACK);
if (batteryData.soc < 100) tft.print(" ");
if (batteryData.soc < 10) tft.print(" ");
tft.print(batteryData.soc);
tft.setTextSize(1);
tft.print("%");
// B. Watts
tft.setCursor(60, 85);
tft.setTextSize(1);
tft.setTextColor(ST7735_WHITE, ST7735_BLACK);
tft.print("POWER:");
tft.setTextSize(2);
tft.setCursor(60, 98);
if (power < 0) tft.setTextColor(ST7735_RED, ST7735_BLACK);
else tft.setTextColor(ST7735_GREEN, ST7735_BLACK);
tft.print((int)power);
tft.print("W ");
}
bool parseRS485Message(String message) {
if (message.length() < 17) {
return false;
}
// Check for valid start
if (message.charAt(0) != '~') {
return false;
}
// Get CID2 (bytes 7-8, 0-indexed)
String cid2 = message.substring(7, 9);
// Get LENGTH field (last 3 chars of 4-char field at position 10-13)
String lengthStr = message.substring(10, 13);
int infoLength = 0;
// Convert hex length to decimal
char hexStr[4];
lengthStr.toCharArray(hexStr, 4);
infoLength = (int)strtol(hexStr, NULL, 16);
// Check if we have enough data for analog info (needs at least 44 chars)
if (infoLength < 44 || message.length() < (13 + infoLength + 4)) {
return false;
}
// Only parse if CID2 is "00" (Response Frame)
if (cid2 != "00") {
return false;
}
// Get INFO payload
String infoPayload = message.substring(13, 13 + infoLength);
try {
// 1. Total Average Voltage (4 chars) -> Unit: 0.001V
String voltageHex = infoPayload.substring(0, 4);
uint16_t voltageRaw = hexStringToUInt16(voltageHex);
batteryData.voltage = voltageRaw / 1000.0;
// 2. Total Current (4 chars) -> Unit: 0.01A
String currentHex = infoPayload.substring(4, 8);
batteryData.current = hexToInt(currentHex, true) / 100.0;
// 3. SOC (2 chars) -> Unit: 1%
String socHex = infoPayload.substring(8, 10);
batteryData.soc = hexToInt(socHex, false);
// 12. Average Temperature (4 chars) - offset 40-44
if (infoPayload.length() >= 44) {
String tempHex = infoPayload.substring(40, 44);
// Swap bytes as in Python code
String swappedTemp = tempHex.substring(2, 4) + tempHex.substring(0, 2);
int16_t tempRaw = hexToInt(swappedTemp, false);
// Convert from Kelvin (as per Python code)
if (tempRaw > 0) {
batteryData.temp = (tempRaw - 2731) / 10.0;
} else {
batteryData.temp = 0.0;
}
} else {
batteryData.temp = 0.0;
}
// SOH is not provided in this protocol, leave as 0
batteryData.soh = 0;
return true;
} catch (...) {
return false;
}
}
bool isAllZeros(const uint8_t *data, uint8_t len) {
for (uint8_t i = 0; i < len; i++) {
if (data[i] != 0) return false;
}
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment