diff --git a/.github/workflows/compile-all-batteries.yml b/.github/workflows/compile-all-batteries.yml index 653de40f..1c163126 100644 --- a/.github/workflows/compile-all-batteries.yml +++ b/.github/workflows/compile-all-batteries.yml @@ -41,7 +41,8 @@ jobs: - NISSAN_LEAF_BATTERY - PYLON_BATTERY - RENAULT_KANGOO_BATTERY - - RENAULT_ZOE_BATTERY + - RENAULT_ZOE_GEN1_BATTERY + - RENAULT_ZOE_GEN2_BATTERY - TESLA_MODEL_3_BATTERY - VOLVO_SPA_BATTERY - TEST_FAKE_BATTERY diff --git a/.github/workflows/compile-all-combinations.yml b/.github/workflows/compile-all-combinations.yml index 3a930fd1..356f35cb 100644 --- a/.github/workflows/compile-all-combinations.yml +++ b/.github/workflows/compile-all-combinations.yml @@ -44,7 +44,8 @@ jobs: - NISSAN_LEAF_BATTERY - PYLON_BATTERY - RENAULT_KANGOO_BATTERY - - RENAULT_ZOE_BATTERY + - RENAULT_ZOE_GEN1_BATTERY + - RENAULT_ZOE_GEN2_BATTERY - TESLA_MODEL_3_BATTERY - VOLVO_SPA_BATTERY - TEST_FAKE_BATTERY diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index c2f64fd8..af19d5dd 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -18,7 +18,8 @@ //#define NISSAN_LEAF_BATTERY //#define PYLON_BATTERY //#define RENAULT_KANGOO_BATTERY -//#define RENAULT_ZOE_BATTERY +//#define RENAULT_ZOE_GEN1_BATTERY +//#define RENAULT_ZOE_GEN2_BATTERY //#define SANTA_FE_PHEV_BATTERY //#define TESLA_MODEL_3_BATTERY //#define VOLVO_SPA_BATTERY diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index 80c7b82c..daf47608 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -43,8 +43,12 @@ #include "RENAULT-KANGOO-BATTERY.h" #endif -#ifdef RENAULT_ZOE_BATTERY -#include "RENAULT-ZOE-BATTERY.h" +#ifdef RENAULT_ZOE_GEN1_BATTERY +#include "RENAULT-ZOE-GEN1-BATTERY.h" +#endif + +#ifdef RENAULT_ZOE_GEN2_BATTERY +#include "RENAULT-ZOE-GEN2-BATTERY.h" #endif #ifdef SANTA_FE_PHEV_BATTERY diff --git a/Software/src/battery/RENAULT-ZOE-BATTERY.cpp b/Software/src/battery/RENAULT-ZOE-BATTERY.cpp deleted file mode 100644 index dff3f2cd..00000000 --- a/Software/src/battery/RENAULT-ZOE-BATTERY.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "../include.h" -#ifdef RENAULT_ZOE_BATTERY -#include "../datalayer/datalayer.h" -#include "../devboard/utils/events.h" -#include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" -#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" -#include "RENAULT-ZOE-BATTERY.h" - -/* Do not change code below unless you are sure what you are doing */ -static uint8_t errorCode = 0; //stores if we have an error code active from battery control logic -static uint16_t LB_SOC = 50; -static uint16_t LB_SOH = 99; -static int16_t LB_MIN_TEMPERATURE = 0; -static int16_t LB_MAX_TEMPERATURE = 0; -static uint16_t LB_Discharge_Power_Limit = 0; -static uint32_t LB_Discharge_Power_Limit_Watts = 0; -static uint16_t LB_Charge_Power_Limit = 0; -static uint32_t LB_Charge_Power_Limit_Watts = 0; -static int32_t LB_Current = 0; -static uint16_t LB_kWh_Remaining = 0; -static uint16_t LB_Cell_Max_Voltage = 3700; -static uint16_t LB_Cell_Min_Voltage = 3700; -static uint32_t LB_Battery_Voltage = 3700; -static uint8_t LB_Discharge_Power_Limit_Byte1 = 0; - -CAN_frame_t ZOE_423 = {.FIR = {.B = - { - .DLC = 8, - .FF = CAN_frame_std, - }}, - .MsgID = 0x423, - .data = {0x33, 0x00, 0xFF, 0xFF, 0x00, 0xE0, 0x00, 0x00}}; - -static unsigned long previousMillis10 = 0; // will store last time a 10ms CAN Message was sent -static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was sent -static unsigned long previousMillis1000 = 0; // will store last time a 1000ms CAN Message was sent -static unsigned long GVL_pause = 0; - -void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus - datalayer.battery.status.soh_pptt = (LB_SOH * 100); //Increase range from 99% -> 99.00% - - datalayer.battery.status.real_soc = (LB_SOC * 10); //increase LB_SOC range from 0-100.0 -> 100.00 - - datalayer.battery.status.voltage_dV = LB_Battery_Voltage; - - datalayer.battery.status.current_dA = LB_Current; - - //Calculate the remaining Wh amount from SOC% and max Wh value. - datalayer.battery.status.remaining_capacity_Wh = static_cast( - (static_cast(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); - - datalayer.battery.status.max_discharge_power_W; - - datalayer.battery.status.max_charge_power_W; - - datalayer.battery.status.active_power_W; - - datalayer.battery.status.temperature_min_dC; - - datalayer.battery.status.temperature_max_dC; - - datalayer.battery.status.cell_min_voltage_mV; - - datalayer.battery.status.cell_max_voltage_mV; - - if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) { - set_event(EVENT_CELL_OVER_VOLTAGE, 0); - } - if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) { - set_event(EVENT_CELL_UNDER_VOLTAGE, 0); - } - -#ifdef DEBUG_VIA_USB - Serial.println("Values going to inverter:"); - Serial.print("SOH%: "); - Serial.print(datalayer.battery.status.soh_pptt); - Serial.print(", SOC% scaled: "); - Serial.print(datalayer.battery.status.reported_soc); - Serial.print(", Voltage: "); - Serial.print(datalayer.battery.status.voltage_dV); - Serial.print(", Max discharge power: "); - Serial.print(datalayer.battery.status.max_discharge_power_W); - Serial.print(", Max charge power: "); - Serial.print(datalayer.battery.status.max_charge_power_W); - Serial.print(", Max temp: "); - Serial.print(datalayer.battery.status.temperature_max_dC); - Serial.print(", Min temp: "); - Serial.print(datalayer.battery.status.temperature_min_dC); - Serial.print(", BMS Status (3=OK): "); - Serial.print(datalayer.battery.status.bms_status); - - Serial.println("Battery values: "); - Serial.print("Real SOC: "); - Serial.print(LB_SOC); - Serial.print(", Current: "); - Serial.print(LB_Current); - Serial.print(", kWh remain: "); - Serial.print(LB_kWh_Remaining); - Serial.print(", max mV: "); - Serial.print(LB_Cell_Max_Voltage); - Serial.print(", min mV: "); - Serial.print(LB_Cell_Min_Voltage); - -#endif -} - -void receive_can_battery(CAN_frame_t rx_frame) { - datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; - switch (rx_frame.MsgID) { - case 0x42E: //HV SOC & Battery Temp & Charging Power - break; - case 0x430: //HVBatteryCoolingState & HVBatteryEvapTemp & HVBatteryEvapSetpoint - break; - case 0x432: //BatVEShutDownAlert & HVBatCondPriorityLevel & HVBatteryLevelAlert & HVBatCondPriorityLevel & HVBatteryConditioningMode - break; - default: - break; - } -} - -void send_can_battery() { - unsigned long currentMillis = millis(); - // Send 100ms CAN Message - if (currentMillis - previousMillis100 >= INTERVAL_100_MS) { - // Check if sending of CAN messages has been delayed too much. - if ((currentMillis - previousMillis100 >= INTERVAL_100_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) { - set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis100)); - } - previousMillis100 = currentMillis; - //ESP32Can.CANWriteFrame(&ZOE_423); - } - // 1000ms CAN handling - if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { - previousMillis1000 = currentMillis; - //ESP32Can.CANWriteFrame(&ZOE_423); - } -} - -void setup_battery(void) { // Performs one time setup at startup -#ifdef DEBUG_VIA_USB - Serial.println("Renault Zoe battery selected"); -#endif - - datalayer.battery.info.max_design_voltage_dV = - 4040; // 404.0V, over this, charging is not possible (goes into forced discharge) - datalayer.battery.info.min_design_voltage_dV = 3100; // 310.0V under this, discharging further is disabled -} - -#endif diff --git a/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.cpp b/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.cpp new file mode 100644 index 00000000..d991ff5c --- /dev/null +++ b/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.cpp @@ -0,0 +1,144 @@ +#include "../include.h" +#ifdef RENAULT_ZOE_GEN1_BATTERY +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" +#include "RENAULT-ZOE-GEN1-BATTERY.h" + +/* Information in this file is based of the OVMS V3 vehicle_renaultzoe.cpp component +https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/blob/master/vehicle/OVMS.V3/components/vehicle_renaultzoe/src/vehicle_renaultzoe.cpp +/* + +/* Do not change code below unless you are sure what you are doing */ +static uint16_t LB_SOC = 50; +static uint16_t LB_SOH = 99; +static int16_t LB_Average_Temperature = 0; +static uint32_t LB_Charge_Power_W = 0; +static int32_t LB_Current = 0; +static uint16_t LB_kWh_Remaining = 0; +static uint16_t LB_Cell_Max_Voltage = 3700; +static uint16_t LB_Cell_Min_Voltage = 3700; +static uint16_t LB_Battery_Voltage = 3700; + +CAN_frame_t ZOE_423 = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x423, + .data = {0x07, 0x1d, 0x00, 0x02, 0x5d, 0x80, 0x5d, 0xc8}}; +CAN_frame_t ZOE_POLL_79B = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x79B, //0x41 = cell bat module 1-62 , 0x42 = cell bat module 63-96 + .data = {0x02, 0x21, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00}}; + +static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was sent +static unsigned long previousMillis5000 = 0; // will store last time a 1000ms CAN Message was sent +static uint8_t counter_423 = 0; + +void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus + datalayer.battery.status.soh_pptt = (LB_SOH * 100); // Increase range from 99% -> 99.00% + + datalayer.battery.status.real_soc = (LB_SOC * 10); // Increase LB_SOC range from 0-100.0 -> 100.00 + + datalayer.battery.status.voltage_dV = LB_Battery_Voltage; + + datalayer.battery.status.current_dA = LB_Current; //TODO: Take from CAN + + //Calculate the remaining Wh amount from SOC% and max Wh value. + datalayer.battery.status.remaining_capacity_Wh = static_cast( + (static_cast(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); + + datalayer.battery.status.max_discharge_power_W = 5000; //TODO: Take from CAN + + datalayer.battery.status.max_charge_power_W = LB_Charge_Power_W; + + datalayer.battery.status.active_power_W; + + datalayer.battery.status.temperature_min_dC = LB_Average_Temperature; + + datalayer.battery.status.temperature_max_dC = LB_Average_Temperature; + + datalayer.battery.status.cell_min_voltage_mV; + + datalayer.battery.status.cell_max_voltage_mV; + + if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) { + set_event(EVENT_CELL_OVER_VOLTAGE, 0); + } + if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) { + set_event(EVENT_CELL_UNDER_VOLTAGE, 0); + } + +#ifdef DEBUG_VIA_USB + +#endif +} + +void receive_can_battery(CAN_frame_t rx_frame) { + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + switch (rx_frame.MsgID) { + case 0x427: + LB_Charge_Power_W = rx_frame.data.u8[5] * 300; + LB_kWh_Remaining = (((((rx_frame.data.u8[6] << 8) | (rx_frame.data.u8[7])) >> 6) & 0x3ff) * 0.1); + break; + case 0x42E: //HV SOC & Battery Temp & Charging Power + LB_Battery_Voltage = (((((rx_frame.data.u8[3] << 8) | (rx_frame.data.u8[4])) >> 5) & 0x3ff) * 0.5); //0.5V/bit + LB_Average_Temperature = (((((rx_frame.data.u8[5] << 8) | (rx_frame.data.u8[6])) >> 5) & 0x7F) - 40); + break; + case 0x654: //SOC + LB_SOC = rx_frame.data.u8[3]; + break; + case 0x658: //SOH + LB_SOH = (rx_frame.data.u8[4] & 0x7F); + break; + case 0x7BB: //Reply from active polling + // TODO: Handle the cellvoltages + + break; + default: + break; + } +} + +void send_can_battery() { + unsigned long currentMillis = millis(); + // Send 100ms CAN Message + if (currentMillis - previousMillis100 >= INTERVAL_100_MS) { + // Check if sending of CAN messages has been delayed too much. + if ((currentMillis - previousMillis100 >= INTERVAL_100_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) { + set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis100)); + } + previousMillis100 = currentMillis; + ESP32Can.CANWriteFrame(&ZOE_423); + + if ((counter_423 / 5) % 2 == 0) { // Alternate every 5 messages between these two + ZOE_423.data.u8[4] = 0xB2; + ZOE_423.data.u8[6] = 0xB2; + } else { + ZOE_423.data.u8[4] = 0x5D; + ZOE_423.data.u8[6] = 0x5D; + } + counter_423 = (counter_423 + 1) % 10; + } + // 5000ms CAN handling + if (currentMillis - previousMillis5000 >= INTERVAL_5_S) { + previousMillis5000 = currentMillis; + ESP32Can.CANWriteFrame(&ZOE_POLL_79B); + } +} + +void setup_battery(void) { // Performs one time setup at startup +#ifdef DEBUG_VIA_USB + Serial.println("Renault Zoe 22/40kWh battery selected"); +#endif + + datalayer.battery.info.max_design_voltage_dV = 4040; + datalayer.battery.info.min_design_voltage_dV = 2700; +} + +#endif diff --git a/Software/src/battery/RENAULT-ZOE-BATTERY.h b/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h similarity index 78% rename from Software/src/battery/RENAULT-ZOE-BATTERY.h rename to Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h index 129925fc..0d53dfa6 100644 --- a/Software/src/battery/RENAULT-ZOE-BATTERY.h +++ b/Software/src/battery/RENAULT-ZOE-GEN1-BATTERY.h @@ -1,5 +1,5 @@ -#ifndef RENAULT_ZOE_BATTERY_H -#define RENAULT_ZOE_BATTERY_H +#ifndef RENAULT_ZOE_GEN1_BATTERY_H +#define RENAULT_ZOE_GEN1_BATTERY_H #include "../include.h" #include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" diff --git a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp new file mode 100644 index 00000000..9ddc82c4 --- /dev/null +++ b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.cpp @@ -0,0 +1,113 @@ +#include "../include.h" +#ifdef RENAULT_ZOE_GEN2_BATTERY +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" +#include "RENAULT-ZOE-GEN2-BATTERY.h" + +/* Information in this file is based of the OVMS V3 vehicle_renaultzoe.cpp component +https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/blob/master/vehicle/OVMS.V3/components/vehicle_renaultzoe_ph2_obd/src/vehicle_renaultzoe_ph2_obd.cpp +/* + +/* Do not change code below unless you are sure what you are doing */ +static uint16_t LB_SOC = 50; +static uint16_t LB_SOH = 99; +static int16_t LB_Average_Temperature = 0; +static uint32_t LB_Charge_Power_W = 0; +static int32_t LB_Current = 0; +static uint16_t LB_kWh_Remaining = 0; +static uint16_t LB_Cell_Max_Voltage = 3700; +static uint16_t LB_Cell_Min_Voltage = 3700; +static uint16_t LB_Battery_Voltage = 3700; + +CAN_frame_t ZOE_373 = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x373, + .data = {0xC1, 0x80, 0x5D, 0x5D, 0x00, 0x00, 0xff, 0xcb}}; +CAN_frame_t ZOE_POLL_18DADBF1 = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_ext, + }}, + .MsgID = 0x18DADBF1, + .data = {0x03, 0x22, 0x90, 0x00, 0xff, 0xff, 0xff, 0xff}}; + +static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was sent + +void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus + datalayer.battery.status.soh_pptt = (LB_SOH * 100); //Increase range from 99% -> 99.00% + + datalayer.battery.status.real_soc = (LB_SOC * 10); //increase LB_SOC range from 0-100.0 -> 100.00 + + datalayer.battery.status.voltage_dV = LB_Battery_Voltage; + + datalayer.battery.status.current_dA = LB_Current; + + //Calculate the remaining Wh amount from SOC% and max Wh value. + datalayer.battery.status.remaining_capacity_Wh = static_cast( + (static_cast(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); + + datalayer.battery.status.max_discharge_power_W; + + datalayer.battery.status.max_charge_power_W; + + datalayer.battery.status.active_power_W; + + datalayer.battery.status.temperature_min_dC; + + datalayer.battery.status.temperature_max_dC; + + datalayer.battery.status.cell_min_voltage_mV; + + datalayer.battery.status.cell_max_voltage_mV; + + if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) { + set_event(EVENT_CELL_OVER_VOLTAGE, 0); + } + if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) { + set_event(EVENT_CELL_UNDER_VOLTAGE, 0); + } + +#ifdef DEBUG_VIA_USB + +#endif +} + +void receive_can_battery(CAN_frame_t rx_frame) { + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + switch (rx_frame.MsgID) { + case 0x18daf1db: // LBC Reply from active polling + break; + default: + break; + } +} + +void send_can_battery() { + unsigned long currentMillis = millis(); + // Send 200ms CAN Message + if (currentMillis - previousMillis200 >= INTERVAL_200_MS) { + // Check if sending of CAN messages has been delayed too much. + if ((currentMillis - previousMillis200 >= INTERVAL_200_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) { + set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis200)); + } + previousMillis200 = currentMillis; + ESP32Can.CANWriteFrame(&ZOE_373); + ESP32Can.CANWriteFrame(&ZOE_POLL_18DADBF1); + } +} + +void setup_battery(void) { // Performs one time setup at startup +#ifdef DEBUG_VIA_USB + Serial.println("Renault Zoe 50kWh battery selected"); +#endif + + datalayer.battery.info.max_design_voltage_dV = 4040; + datalayer.battery.info.min_design_voltage_dV = 3100; +} + +#endif diff --git a/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h new file mode 100644 index 00000000..a0fb08fb --- /dev/null +++ b/Software/src/battery/RENAULT-ZOE-GEN2-BATTERY.h @@ -0,0 +1,14 @@ +#ifndef RENAULT_ZOE_GEN2_BATTERY_H +#define RENAULT_ZOE_GEN2_BATTERY_H +#include "../include.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" + +#define BATTERY_SELECTED + +#define ABSOLUTE_CELL_MAX_VOLTAGE 4100 +#define ABSOLUTE_CELL_MIN_VOLTAGE 3000 +#define MAX_CELL_DEVIATION_MV 500 + +void setup_battery(void); + +#endif diff --git a/Software/src/devboard/utils/types.h b/Software/src/devboard/utils/types.h index 7d37dff8..e46c3f23 100644 --- a/Software/src/devboard/utils/types.h +++ b/Software/src/devboard/utils/types.h @@ -27,6 +27,7 @@ enum led_color { GREEN, YELLOW, RED, BLUE, RGB }; #define INTERVAL_30_MS_DELAYED 40 #define INTERVAL_50_MS_DELAYED 65 #define INTERVAL_100_MS_DELAYED 120 +#define INTERVAL_200_MS_DELAYED 240 #define INTERVAL_500_MS_DELAYED 550 #define CAN_STILL_ALIVE \ diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index dcebd583..827a236b 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -469,8 +469,11 @@ String processor(const String& var) { #ifdef RENAULT_KANGOO_BATTERY content += "Renault Kangoo"; #endif -#ifdef RENAULT_ZOE_BATTERY - content += "Renault Zoe"; +#ifdef RENAULT_ZOE_GEN1_BATTERY + content += "Renault Zoe Gen1 22/40"; +#endif +#ifdef RENAULT_ZOE_GEN2_BATTERY + content += "Renault Zoe Gen2 50"; #endif #ifdef SERIAL_LINK_RECEIVER content += "Serial link to another LilyGo board";