-
-
Save janpom/9bd20d4c0887847d628b18a05e2fd7eb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define DEV_MODE 1 | |
#include <TFT_22_ILI9225.h> | |
#include <SoftwareSerial.h> | |
#include "tft_util.h" | |
#if defined(ESP8266) | |
#define TFT_RST 4 // D2 | |
#define TFT_RS 5 // D1 | |
#define TFT_CLK 14 // D5 SCK | |
#define TFT_SDI 13 // D7 MOSI | |
#define TFT_CS 15 // D8 SS | |
#else | |
#define TFT_RST 12 | |
#define TFT_RS 9 | |
#define TFT_CS 10 // SS | |
#define TFT_SDI 11 // MOSI | |
#define TFT_CLK 13 // SCK | |
#endif | |
#define TFT_LED 0 | |
#if defined(ESP8266) | |
#include <ESP8266WiFi.h> | |
#include <BlynkSimpleEsp8266.h> | |
char auth[] = "<auth>"; | |
char ssid[] = "<ssid>"; | |
char pass[] = "<pass>"; | |
BlynkTimer timer; | |
WidgetLED led_charge_disabled(V11); | |
#endif | |
#define SCREEN_ORIENTATION 0 | |
#define UPDATE_DELAY 100 | |
#define MAX_INPUT_BYTES 50 | |
#define MIN_BATTERY_CELLS 9 | |
#define MAX_BATTERY_CELLS 15 | |
#define LEN(X) (sizeof(X) / sizeof(X[0])) | |
uint8_t REQUEST_CELL_VOLTAGES_PACKET[] = {0xDD, 0xA5, 0x04, 0x00, 0xFF, 0xFC, 0x77}; | |
uint8_t REQUEST_BASIC_INFO_PACKET[] = {0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77}; | |
uint8_t REQUEST_HARDWARE_VERSION_PACKET[] = {0xDD, 0xA5, 0x05, 0x00, 0xFF, 0xFB, 0x77}; | |
const float discharge_ticks[] = { | |
3.5 , | |
3.67, | |
3.73, | |
3.76, | |
3.80, | |
3.83, | |
3.86, | |
3.90, | |
4.0 , | |
4.2 , | |
}; | |
TFT_22_ILI9225 tft = TFT_22_ILI9225(TFT_RST, TFT_RS, TFT_CS, TFT_LED, 200); | |
//SoftwareSerial BMS_UART(5, 6); | |
HardwareSerial &BMS_UART = Serial; | |
typedef struct { | |
uint16_t current_mA; | |
uint8_t battery_cells; | |
bool charging_enabled; | |
} t_basic_info; | |
uint8_t INPUT_BYTES[MAX_INPUT_BYTES]; | |
uint16_t VOLTAGES[MAX_BATTERY_CELLS]; | |
uint16_t AVG_VOLTAGE; | |
t_basic_info BASIC_INFO; | |
float voltage_to_percent(float voltage) { | |
if (voltage < discharge_ticks[0]) | |
return 0.0; | |
for (int i = 1; i < LEN(discharge_ticks); i++) { | |
float cur_voltage = discharge_ticks[i]; | |
if (voltage < cur_voltage) { | |
float prev_voltage = discharge_ticks[i - 1]; | |
float interval_perc = (voltage - prev_voltage) / (cur_voltage - prev_voltage); | |
float low = 1.0 * (i - 1) / (LEN(discharge_ticks) - 1); | |
float high = 1.0 * i / (LEN(discharge_ticks) - 1); | |
return low + (high - low) * interval_perc; | |
} | |
} | |
return 1.0; | |
} | |
int fetch_input() { | |
int bytes_read = 0; | |
while (BMS_UART.available()) { | |
if (bytes_read >= MAX_INPUT_BYTES) | |
return -1; | |
INPUT_BYTES[bytes_read] = BMS_UART.read(); | |
bytes_read++; | |
} | |
// TODO: CRC check | |
return bytes_read; | |
} | |
bool fetch_basic_info() { | |
BMS_UART.write(REQUEST_BASIC_INFO_PACKET, sizeof(REQUEST_BASIC_INFO_PACKET)); | |
delay(100); | |
if (fetch_input() < 26) | |
return false; | |
BASIC_INFO.current_mA = ((INPUT_BYTES[6]<<8) | INPUT_BYTES[7]) * 10; | |
BASIC_INFO.battery_cells = INPUT_BYTES[25]; | |
uint8_t mosfet_status = INPUT_BYTES[24]; | |
BASIC_INFO.charging_enabled = !!(mosfet_status & (1<<0)); | |
return true; | |
} | |
bool fetch_voltages(uint8_t battery_cells) { | |
BMS_UART.write(REQUEST_CELL_VOLTAGES_PACKET, sizeof(REQUEST_CELL_VOLTAGES_PACKET)); | |
delay(100); | |
int expected_bytes = 7 + 2 * battery_cells; | |
if (fetch_input() != expected_bytes) | |
return false; | |
for (int i=0; i<battery_cells; i++) { | |
VOLTAGES[i] = (INPUT_BYTES[4+i*2]<<8) | (INPUT_BYTES[4+i*2+1]); | |
} | |
return true; | |
} | |
void heartbeat(bool alive) { | |
uint16_t color = alive ? tft.setColor(0, 100, 0) : tft.setColor(100, 0, 0); | |
tft.fillRectangle(0, 204, 3, 207, color); | |
delay(UPDATE_DELAY / 2); | |
tft.fillRectangle(0, 204, 3, 207, COLOR_BLACK); | |
} | |
int min(int a, int b) { | |
return a < b ? a : b; | |
} | |
void sync_blynk() { | |
Blynk.virtualWrite(V0, AVG_VOLTAGE); | |
Blynk.virtualWrite(V1, VOLTAGES[0]); | |
Blynk.virtualWrite(V2, VOLTAGES[1]); | |
Blynk.virtualWrite(V3, VOLTAGES[2]); | |
Blynk.virtualWrite(V4, VOLTAGES[3]); | |
Blynk.virtualWrite(V5, VOLTAGES[4]); | |
Blynk.virtualWrite(V6, VOLTAGES[5]); | |
Blynk.virtualWrite(V7, VOLTAGES[6]); | |
Blynk.virtualWrite(V8, VOLTAGES[7]); | |
Blynk.virtualWrite(V9, VOLTAGES[8]); | |
Blynk.virtualWrite(V10, VOLTAGES[9]); | |
// FIXME | |
if (AVG_VOLTAGE >= 4000 && AVG_VOLTAGE <= 4002) { | |
Blynk.notify(String("AVG voltage = ") + String(AVG_VOLTAGE)); | |
} | |
if (BASIC_INFO.charging_enabled) | |
led_charge_disabled.off(); | |
else | |
led_charge_disabled.on(); | |
Blynk.virtualWrite(V12, BASIC_INFO.current_mA / 1000.0); | |
} | |
void setup() { | |
// Serial.begin(115200); | |
Blynk.begin(auth, ssid, pass); | |
timer.setInterval(1000, sync_blynk); | |
BMS_UART.begin(9600); | |
tft.begin(); | |
tft.setOrientation(SCREEN_ORIENTATION); | |
tft.setBackgroundColor(COLOR_BLACK); | |
} | |
void loop() { | |
Blynk.run(); | |
timer.run(); | |
delay(UPDATE_DELAY / 2); | |
#ifdef DEV_MODE | |
BASIC_INFO.battery_cells = 10; | |
BASIC_INFO.charging_enabled = (AVG_VOLTAGE < 4200); | |
BASIC_INFO.current_mA = (BASIC_INFO.charging_enabled ? 1250 : 0); | |
uint16_t voltages[] = {3824, 3825, 3830, 3835, 3810, 3826, 3830, 3825, 3840, 3835, 3835, 3835}; | |
for (int i=0; i<BASIC_INFO.battery_cells; i++) | |
VOLTAGES[i] = voltages[i] + (millis() / 1000); | |
heartbeat(true); | |
#else | |
if (!fetch_basic_info()) { | |
heartbeat(false); | |
return; | |
} | |
// sanity check | |
if (BASIC_INFO.battery_cells < MIN_BATTERY_CELLS || BASIC_INFO.battery_cells > MAX_BATTERY_CELLS) { | |
heartbeat(false); | |
return; | |
} | |
if (!fetch_voltages(BASIC_INFO.battery_cells)) { | |
heartbeat(false); | |
return; | |
} | |
#endif | |
// TODO: constants, better naming | |
float max_diff = 20; // mV | |
uint8_t max_len = 48; | |
uint8_t x_mid = 128; | |
uint8_t line_height; | |
if (BASIC_INFO.battery_cells <= 10) | |
line_height = 20; | |
else if (BASIC_INFO.battery_cells == 11) | |
line_height = 18; | |
else | |
line_height = 17; | |
char fmt[20]; | |
uint16_t sum_voltage = 0; | |
for (int i=0; i<BASIC_INFO.battery_cells; i++) { | |
sum_voltage += VOLTAGES[i]; | |
} | |
AVG_VOLTAGE = sum_voltage / BASIC_INFO.battery_cells; | |
tft.setFont(Terminal6x8); | |
for (int i=0; i<BASIC_INFO.battery_cells; i++) { | |
// cell number | |
dtostrf((i+1), 2, 0, fmt); | |
if (fmt[0] == ' ') | |
fmt[0] = '0'; | |
tft.drawText(0, i * line_height + 7, fmt, tft.setColor(128, 128, 128)); | |
// voltage | |
uint16_t voltage = VOLTAGES[i]; | |
dtostrf(voltage / 1000.0, 5, 3, fmt); | |
tft_util_draw_number(&tft, fmt, 21, i * line_height, COLOR_WHITE, COLOR_BLACK, 1, 3); | |
tft.drawText(65, i * line_height + 8, "V", COLOR_WHITE); | |
// voltage diff indicator | |
int voltage_diff = voltage - AVG_VOLTAGE; | |
int abs_voltage_diff = min(abs(voltage_diff), max_diff); | |
int indicator_len = round(1.0 * abs_voltage_diff / max_diff * max_len); | |
if (indicator_len != 0) { | |
int y1 = i * line_height; | |
int y2 = (i + 1) * line_height - 3; | |
if (voltage_diff > 0) { | |
tft.fillRectangle(x_mid - max_len, y1, x_mid - 1, y2, COLOR_BLACK); | |
tft.fillRectangle(x_mid + 1, y1, x_mid + indicator_len, y2, tft.setColor(0, 100, 0)); | |
tft.fillRectangle(x_mid + indicator_len + 1, y1, x_mid + max_len, y2, COLOR_BLACK); | |
} | |
else { | |
tft.fillRectangle(x_mid - max_len, y1, x_mid - indicator_len - 1, y2, COLOR_BLACK); | |
tft.fillRectangle(x_mid - indicator_len, y1, x_mid - 1, y2, tft.setColor(100, 0, 0)); | |
tft.fillRectangle(x_mid + 1, y1, x_mid + max_len, y2, COLOR_BLACK); | |
} | |
} | |
} | |
// voltage diff indicator middle line | |
tft.fillRectangle(x_mid, 0, x_mid, BASIC_INFO.battery_cells * line_height - 3, tft.setColor(32, 32, 32)); | |
// bottom bar: Arithmetic Mean Voltage | |
tft.drawText(0, 204 + 7, "AM", tft.setColor(128, 128, 128)); | |
dtostrf(AVG_VOLTAGE / 1000.0, 5, 3, fmt); | |
tft_util_draw_number(&tft, fmt, 21, 204, tft.setColor(55, 200, 255), COLOR_BLACK, 1, 3); | |
tft.drawText(65, 204 + 8, "V", COLOR_WHITE); | |
// bottom bar: Battery Percent | |
float soc = voltage_to_percent(AVG_VOLTAGE / 1000.0); | |
dtostrf(min(soc * 100.0, 99.9), 4, 1, fmt); | |
uint16_t color = tft.setColor(255 * (1 - soc), 255 * soc, 0); | |
tft_util_draw_number(&tft, fmt, 86, 204, color, COLOR_BLACK, 1, 3); | |
tft.drawText(120, 204 + 8, "%", COLOR_WHITE); | |
// bottom bar: Current | |
dtostrf(BASIC_INFO.current_mA / 1000.0, 4, 2, fmt); | |
uint16_t current_color = BASIC_INFO.charging_enabled ? tft.setColor(55, 200, 255) : tft.setColor(100, 0, 0); | |
tft_util_draw_number(&tft, fmt, 136, 204, current_color, COLOR_BLACK, 1, 3); | |
tft.drawText(170, 204 + 8, "A", COLOR_WHITE); | |
heartbeat(true); | |
} | |
/* | |
void write_line(String *text, int lineno, uint16_t color = COLOR_WHITE) { | |
const int max_line_length = 30; | |
char line_buffer[max_line_length + 1]; | |
String s = String("") + *text; | |
while (s.length() < max_line_length) | |
s.concat(String(" ")); | |
s.toCharArray(line_buffer, sizeof(line_buffer)); | |
int y = lineno * 12 + 5; | |
tft.setFont(Terminal6x8); | |
tft.drawText(5, y, line_buffer, color); | |
} | |
uint8_t input[MAX_BATTERY_CELLS]; | |
void test_fetch_voltages() { | |
BMS_UART.write(REQUEST_CELL_VOLTAGES_PACKET, sizeof(REQUEST_CELL_VOLTAGES_PACKET)); | |
delay(100); | |
int bytes_read = 0; | |
while (BMS_UART.available()) { | |
input[bytes_read] = BMS_UART.read(); | |
bytes_read++; | |
} | |
uint8_t line = 2; | |
String text = String("bytes read: ") + String(bytes_read); | |
write_line(&text, line++); | |
if (bytes_read == 27) { | |
for (int i=0; i<10; i++) { | |
uint16_t voltage = (input[4+i*2]<<8) | (input[4+i*2+1]); | |
text = (i < 10 ? "0" : "") + String(i) + String(": ") + String(voltage) + String(" mV"); | |
write_line(&text, line++); | |
} | |
} | |
} | |
void test_fetch_hardware_version() { | |
BMS_UART.write(REQUEST_HARDWARE_VERSION_PACKET, sizeof(REQUEST_HARDWARE_VERSION_PACKET)); | |
delay(100); | |
int bytes_read = 0; | |
while (BMS_UART.available()) { | |
input[bytes_read] = BMS_UART.read(); | |
bytes_read++; | |
} | |
Serial.println(String("bytes read: ") + String(bytes_read)); | |
for (int i=0; i<bytes_read; i++) { | |
Serial.print(char(input[i])); | |
Serial.println(); | |
} | |
} | |
void test_fetch_basic_info() { | |
BMS_UART.write(REQUEST_BASIC_INFO_PACKET, sizeof(REQUEST_BASIC_INFO_PACKET)); | |
delay(100); | |
int bytes_read = 0; | |
while (BMS_UART.available()) { | |
input[bytes_read] = BMS_UART.read(); | |
bytes_read++; | |
} | |
uint8_t header_size = 4; | |
uint8_t data_size = input[3]; | |
Serial.println(String("bytes read: ") + String(bytes_read)); | |
Serial.println(String("data size: ") + String(data_size)); | |
uint16_t total_voltage_mV = ((input[4]<<8) | input[5]) * 10; | |
Serial.println(String("total voltage: ") + String(total_voltage_mV) + String(" mV")); | |
uint16_t current_mA = ((input[6]<<8) | input[7]) * 10; | |
Serial.println(String("current: ") + String(current_mA) + String(" mA")); | |
uint16_t remaining_capacity_mAh = ((input[8]<<8) | input[9]) * 10; | |
Serial.println(String("remaining capacity: ") + String(remaining_capacity_mAh) + String(" mAh")); | |
uint16_t capacity_mAh = ((input[10]<<8) | input[11]) * 10; | |
Serial.println(String("capacity: ") + String(capacity_mAh) + String(" mAh")); | |
uint16_t charge_cycles = ((input[12]<<8) | input[13]) * 10; | |
Serial.println(String("charge cycles: ") + String(charge_cycles)); | |
// 14+15 manufactured date? | |
// 16+17 balancing on/off for cells 1-16 | |
// 18+19 balancing on/off for cells 17-32 | |
uint16_t protection_status = (input[20]<<8) | input[21]; | |
Serial.println(String("single cell overvoltage: ") + String(protection_status & (1<<0))); | |
Serial.println(String("single cell undervoltage: ") + String(protection_status & (1<<1))); | |
Serial.println(String("battery pack overvoltage: ") + String(protection_status & (1<<2))); | |
Serial.println(String("battery pack undervoltage: ") + String(protection_status & (1<<3))); | |
Serial.println(String("high temperature for charging: ") + String(protection_status & (1<<4))); | |
Serial.println(String("low temperature for charging: ") + String(protection_status & (1<<5))); | |
Serial.println(String("high temperature for discharging: ") + String(protection_status & (1<<6))); | |
Serial.println(String("low temperature for discharging: ") + String(protection_status & (1<<7))); | |
Serial.println(String("charge overcurrent: ") + String(protection_status & (1<<8))); | |
Serial.println(String("discharge overcurrent: ") + String(protection_status & (1<<9))); | |
Serial.println(String("short circuit: ") + String(protection_status & (1<<10))); | |
Serial.println(String("IC inspection error: ") + String(protection_status & (1<<11))); | |
Serial.println(String("MOSFET software lock: ") + String(protection_status & (1<<12))); | |
uint8_t firmware_version = input[22]; | |
Serial.println(String("FW version: ") + String(firmware_version)); | |
uint8_t SOC_percent = input[23]; | |
Serial.println(String("SOC: ") + String(SOC_percent) + String("%")); | |
uint8_t mosfet_status = input[24]; | |
Serial.println(String("charging enabled: ") + String(!!(mosfet_status & (1<<0)))); | |
Serial.println(String("discharging enabled: ") + String(!!(mosfet_status & (1<<1)))); | |
uint8_t battery_cells = input[25]; | |
Serial.println(String("battery cells: ") + String(battery_cells)); | |
uint8_t ntc_count = input[26]; | |
Serial.println(String("NTC count: ") + String(ntc_count)); | |
for (int ntc_i=0; ntc_i<ntc_count; ntc_i++) { | |
uint16_t temp_raw = (input[27+ntc_i*2]<<8) | input[27+ntc_i*2+1]; | |
float temp_celsius = (temp_raw - 2731) / 10.0; | |
Serial.println(String("NTC ") + String(ntc_i) + String(": ") + String(temp_celsius) + String(" Celsius")); | |
} | |
Serial.println(); | |
} | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment