Solis Hybrid Inverter S6 (via modbus rs485)

Developed or developing a new product in PureBasic? Tell the world about it.
JCV
Enthusiast
Enthusiast
Posts: 580
Joined: Fri Jun 30, 2006 4:30 pm
Location: Philippines

Solis Hybrid Inverter S6 (via modbus rs485)

Post by JCV »

Here's my working code to read the Solis Hybrid Inverter S6 data.
I tested on S6-EH1P6K-L-PRO connected via modbus RS485 serial connection.

Code: Select all

; Supports Solis Hybrid Inverter S6
; Connecting to hybrid inverter via port RS485 modbus
; Not tested on other models but probably needs changing the registers only
; Compiler: Purebasic v6.20
; By: JCV

EnableExplicit

#SerialPort = 0
#ModBus_FunctionCode_ReadDiscreteInputs = 2
#ModBus_FunctionCode_ReadInputRegisters = 4

Structure RegisterInfo
  addr.i
  type.s
  scale.f
  unit.s
EndStructure

Structure DataValue
  value.s
  unit.s
EndStructure

Structure FaultCategory
  startAddr.i
  endAddr.i
  Map faults.s()
EndStructure

Global NewMap Registers.RegisterInfo()
Global NewMap InverterData.DataValue()
Global NewMap StatusCodes.s()
Global NewMap FaultCategories.FaultCategory()
Global Port.i, Address.i = 1
Global ThreadID.i, MutexID.i, Quit.i, ErrorMessage.s
Global ModBus_RTU_Echo.b 

Procedure DefineRegisters()
  Registers("model_number")\addr = 33000 : Registers("model_number")\type = "uint16" : Registers("model_number")\scale = 1 : Registers("model_number")\unit = "Hex"
  Registers("dsp_version")\addr = 33001 : Registers("dsp_version")\type = "uint16" : Registers("dsp_version")\scale = 1 : Registers("dsp_version")\unit = "Hex"
  Registers("hmi_version")\addr = 33002 : Registers("hmi_version")\type = "uint16" : Registers("hmi_version")\scale = 1 : Registers("hmi_version")\unit = "Hex"
  Registers("protocol_version")\addr = 33003 : Registers("protocol_version")\type = "uint16" : Registers("protocol_version")\scale = 1 : Registers("protocol_version")\unit = "Hex"
  Registers("year")\addr = 33022 : Registers("year")\type = "uint16" : Registers("year")\scale = 1 : Registers("year")\unit = "Years (0-99)"
  Registers("month")\addr = 33023 : Registers("month")\type = "uint16" : Registers("month")\scale = 1 : Registers("month")\unit = "Months"
  Registers("day")\addr = 33024 : Registers("day")\type = "uint16" : Registers("day")\scale = 1 : Registers("day")\unit = "Days"
  Registers("hour")\addr = 33025 : Registers("hour")\type = "uint16" : Registers("hour")\scale = 1 : Registers("hour")\unit = "Hours"
  Registers("minute")\addr = 33026 : Registers("minute")\type = "uint16" : Registers("minute")\scale = 1 : Registers("minute")\unit = "Minutes"
  Registers("second")\addr = 33027 : Registers("second")\type = "uint16" : Registers("second")\scale = 1 : Registers("second")\unit = "Seconds"
  Registers("energy_total")\addr = 33029 : Registers("energy_total")\type = "uint32" : Registers("energy_total")\scale = 1 : Registers("energy_total")\unit = "kWh"
  Registers("energy_this_month")\addr = 33031 : Registers("energy_this_month")\type = "uint32" : Registers("energy_this_month")\scale = 1 : Registers("energy_this_month")\unit = "kWh"
  Registers("energy_last_month")\addr = 33033 : Registers("energy_last_month")\type = "uint32" : Registers("energy_last_month")\scale = 1 : Registers("energy_last_month")\unit = "kWh"
  Registers("energy_today")\addr = 33035 : Registers("energy_today")\type = "uint16" : Registers("energy_today")\scale = 0.1 : Registers("energy_today")\unit = "kWh"
  Registers("energy_yesterday")\addr = 33036 : Registers("energy_yesterday")\type = "uint16" : Registers("energy_yesterday")\scale = 0.1 : Registers("energy_yesterday")\unit = "kWh"
  Registers("energy_this_year")\addr = 33037 : Registers("energy_this_year")\type = "uint32" : Registers("energy_this_year")\scale = 1 : Registers("energy_this_year")\unit = "kWh"
  Registers("energy_last_year")\addr = 33039 : Registers("energy_last_year")\type = "uint32" : Registers("energy_last_year")\scale = 1 : Registers("energy_last_year")\unit = "kWh"
  Registers("dc_voltage_1")\addr = 33049 : Registers("dc_voltage_1")\type = "uint16" : Registers("dc_voltage_1")\scale = 0.1 : Registers("dc_voltage_1")\unit = "V"
  Registers("dc_current_1")\addr = 33050 : Registers("dc_current_1")\type = "uint16" : Registers("dc_current_1")\scale = 0.1 : Registers("dc_current_1")\unit = "A"
  Registers("dc_voltage_2")\addr = 33051 : Registers("dc_voltage_2")\type = "uint16" : Registers("dc_voltage_2")\scale = 0.1 : Registers("dc_voltage_2")\unit = "V"
  Registers("dc_current_2")\addr = 33052 : Registers("dc_current_2")\type = "uint16" : Registers("dc_current_2")\scale = 0.1 : Registers("dc_current_2")\unit = "A"
  Registers("dc_voltage_3")\addr = 33053 : Registers("dc_voltage_3")\type = "uint16" : Registers("dc_voltage_3")\scale = 0.1 : Registers("dc_voltage_3")\unit = "V"
  Registers("dc_current_3")\addr = 33054 : Registers("dc_current_3")\type = "uint16" : Registers("dc_current_3")\scale = 0.1 : Registers("dc_current_3")\unit = "A"
  Registers("dc_voltage_4")\addr = 33055 : Registers("dc_voltage_4")\type = "uint16" : Registers("dc_voltage_4")\scale = 0.1 : Registers("dc_voltage_4")\unit = "V"
  Registers("dc_current_4")\addr = 33056 : Registers("dc_current_4")\type = "uint16" : Registers("dc_current_4")\scale = 0.1 : Registers("dc_current_4")\unit = "A"
  Registers("total_dc_power")\addr = 33057 : Registers("total_dc_power")\type = "uint32" : Registers("total_dc_power")\scale = 1 : Registers("total_dc_power")\unit = "W"
  Registers("dc_bus_voltage")\addr = 33071 : Registers("dc_bus_voltage")\type = "uint16" : Registers("dc_bus_voltage")\scale = 0.1 : Registers("dc_bus_voltage")\unit = "V"
  Registers("dc_bus_half_voltage")\addr = 33072 : Registers("dc_bus_half_voltage")\type = "uint16" : Registers("dc_bus_half_voltage")\scale = 0.1 : Registers("dc_bus_half_voltage")\unit = "V"
  Registers("grid_voltage_a")\addr = 33073 : Registers("grid_voltage_a")\type = "uint16" : Registers("grid_voltage_a")\scale = 0.1 : Registers("grid_voltage_a")\unit = "V"
  Registers("grid_voltage_b")\addr = 33074 : Registers("grid_voltage_b")\type = "uint16" : Registers("grid_voltage_b")\scale = 0.1 : Registers("grid_voltage_b")\unit = "V"
  Registers("grid_voltage_c")\addr = 33075 : Registers("grid_voltage_c")\type = "uint16" : Registers("grid_voltage_c")\scale = 0.1 : Registers("grid_voltage_c")\unit = "V"
  Registers("grid_current_a")\addr = 33076 : Registers("grid_current_a")\type = "uint16" : Registers("grid_current_a")\scale = 0.1 : Registers("grid_current_a")\unit = "A"
  Registers("grid_current_b")\addr = 33077 : Registers("grid_current_b")\type = "uint16" : Registers("grid_current_b")\scale = 0.1 : Registers("grid_current_b")\unit = "A"
  Registers("grid_current_c")\addr = 33078 : Registers("grid_current_c")\type = "uint16" : Registers("grid_current_c")\scale = 0.1 : Registers("grid_current_c")\unit = "A"
  Registers("active_power")\addr = 33079 : Registers("active_power")\type = "int32" : Registers("active_power")\scale = 1 : Registers("active_power")\unit = "W"
  Registers("reactive_power")\addr = 33081 : Registers("reactive_power")\type = "int32" : Registers("reactive_power")\scale = 1 : Registers("reactive_power")\unit = "Var"
  Registers("apparent_power")\addr = 33083 : Registers("apparent_power")\type = "int32" : Registers("apparent_power")\scale = 1 : Registers("apparent_power")\unit = "VA"
  Registers("working_mode")\addr = 33091 : Registers("working_mode")\type = "uint16" : Registers("working_mode")\scale = 1 : Registers("working_mode")\unit = "Code"
  Registers("grid_standard")\addr = 33092 : Registers("grid_standard")\type = "uint16" : Registers("grid_standard")\scale = 1 : Registers("grid_standard")\unit = "Code"
  Registers("inverter_temp")\addr = 33093 : Registers("inverter_temp")\type = "int16" : Registers("inverter_temp")\scale = 0.1 : Registers("inverter_temp")\unit = "°C"
  Registers("grid_frequency")\addr = 33094 : Registers("grid_frequency")\type = "uint16" : Registers("grid_frequency")\scale = 0.01 : Registers("grid_frequency")\unit = "Hz"
  Registers("current_status")\addr = 33095 : Registers("current_status")\type = "uint16" : Registers("current_status")\scale = 1 : Registers("current_status")\unit = "Code"
  Registers("lead_acid_temp")\addr = 33096 : Registers("lead_acid_temp")\type = "int16" : Registers("lead_acid_temp")\scale = 0.1 : Registers("lead_acid_temp")\unit = "°C"
  Registers("battery_voltage")\addr = 33133 : Registers("battery_voltage")\type = "uint16" : Registers("battery_voltage")\scale = 0.1 : Registers("battery_voltage")\unit = "V"
  Registers("battery_current")\addr = 33134 : Registers("battery_current")\type = "int16" : Registers("battery_current")\scale = 0.1 : Registers("battery_current")\unit = "A"
  Registers("battery_direction")\addr = 33135 : Registers("battery_direction")\type = "uint16" : Registers("battery_direction")\scale = 1 : Registers("battery_direction")\unit = "0=Charge, 1=Discharge"
  Registers("llc_bus_voltage")\addr = 33136 : Registers("llc_bus_voltage")\type = "uint16" : Registers("llc_bus_voltage")\scale = 0.1 : Registers("llc_bus_voltage")\unit = "V"
  Registers("backup_voltage_a")\addr = 33137 : Registers("backup_voltage_a")\type = "uint16" : Registers("backup_voltage_a")\scale = 0.1 : Registers("backup_voltage_a")\unit = "V"
  Registers("backup_current_a")\addr = 33138 : Registers("backup_current_a")\type = "uint16" : Registers("backup_current_a")\scale = 0.1 : Registers("backup_current_a")\unit = "A"
  Registers("battery_soc")\addr = 33139 : Registers("battery_soc")\type = "uint16" : Registers("battery_soc")\scale = 1 : Registers("battery_soc")\unit = "%"
  Registers("battery_soh")\addr = 33140 : Registers("battery_soh")\type = "uint16" : Registers("battery_soh")\scale = 1 : Registers("battery_soh")\unit = "%"
  Registers("bms_voltage")\addr = 33141 : Registers("bms_voltage")\type = "uint16" : Registers("bms_voltage")\scale = 0.01 : Registers("bms_voltage")\unit = "V"
  Registers("bms_current")\addr = 33142 : Registers("bms_current")\type = "int16" : Registers("bms_current")\scale = 0.1 : Registers("bms_current")\unit = "A"
  Registers("bms_charge_limit")\addr = 33143 : Registers("bms_charge_limit")\type = "uint16" : Registers("bms_charge_limit")\scale = 0.1 : Registers("bms_charge_limit")\unit = "A"
  Registers("bms_discharge_limit")\addr = 33144 : Registers("bms_discharge_limit")\type = "uint16" : Registers("bms_discharge_limit")\scale = 0.1 : Registers("bms_discharge_limit")\unit = "A"
  Registers("battery_power")\addr = 33149 : Registers("battery_power")\type = "int32" : Registers("battery_power")\scale = 1 : Registers("battery_power")\unit = "W"
  Registers("inverter_ac_grid_power")\addr = 33151 : Registers("inverter_ac_grid_power")\type = "int32" : Registers("inverter_ac_grid_power")\scale = 1 : Registers("inverter_ac_grid_power")\unit = "W"
  Registers("backup_voltage_b")\addr = 33153 : Registers("backup_voltage_b")\type = "uint16" : Registers("backup_voltage_b")\scale = 0.1 : Registers("backup_voltage_b")\unit = "V"
  Registers("backup_current_b")\addr = 33154 : Registers("backup_current_b")\type = "uint16" : Registers("backup_current_b")\scale = 0.1 : Registers("backup_current_b")\unit = "A"
  Registers("backup_voltage_c")\addr = 33155 : Registers("backup_voltage_c")\type = "uint16" : Registers("backup_voltage_c")\scale = 0.1 : Registers("backup_voltage_c")\unit = "V"
  Registers("backup_current_c")\addr = 33156 : Registers("backup_current_c")\type = "uint16" : Registers("backup_current_c")\scale = 0.1 : Registers("backup_current_c")\unit = "A"
  Registers("inverting_rectifying_power")\addr = 33157 : Registers("inverting_rectifying_power")\type = "int16" : Registers("inverting_rectifying_power")\scale = 10 : Registers("inverting_rectifying_power")\unit = "W"
  Registers("battery_detected")\addr = 33159 : Registers("battery_detected")\type = "uint16" : Registers("battery_detected")\scale = 1 : Registers("battery_detected")\unit = "0=Not Detected, 1=Detected"
  Registers("current_battery_model")\addr = 33160 : Registers("current_battery_model")\type = "uint16" : Registers("current_battery_model")\scale = 1 : Registers("current_battery_model")\unit = "Code"
  Registers("battery_charge_total")\addr = 33161 : Registers("battery_charge_total")\type = "uint32" : Registers("battery_charge_total")\scale = 1 : Registers("battery_charge_total")\unit = "kWh"
  Registers("battery_charge_today")\addr = 33163 : Registers("battery_charge_today")\type = "uint16" : Registers("battery_charge_today")\scale = 0.1 : Registers("battery_charge_today")\unit = "kWh"
  Registers("battery_discharge_today")\addr = 33167 : Registers("battery_discharge_today")\type = "uint16" : Registers("battery_discharge_today")\scale = 0.1 : Registers("battery_discharge_today")\unit = "kWh"
  Registers("battery_discharge_yesterday")\addr = 33168 : Registers("battery_discharge_yesterday")\type = "uint16" : Registers("battery_discharge_yesterday")\scale = 0.1 : Registers("battery_discharge_yesterday")\unit = "kWh"
  Registers("grid_import_total")\addr = 33169 : Registers("grid_import_total")\type = "uint32" : Registers("grid_import_total")\scale = 1 : Registers("grid_import_total")\unit = "kWh"
  Registers("grid_import_today")\addr = 33171 : Registers("grid_import_today")\type = "uint16" : Registers("grid_import_today")\scale = 0.1 : Registers("grid_import_today")\unit = "kWh"
  Registers("grid_export_total")\addr = 33173 : Registers("grid_export_total")\type = "uint32" : Registers("grid_export_total")\scale = 1 : Registers("grid_export_total")\unit = "kWh"
  Registers("grid_export_today")\addr = 33175 : Registers("grid_export_today")\type = "uint16" : Registers("grid_export_today")\scale = 0.1 : Registers("grid_export_today")\unit = "kWh"
  Registers("load_total_energy")\addr = 33177 : Registers("load_total_energy")\type = "uint32" : Registers("load_total_energy")\scale = 1 : Registers("load_total_energy")\unit = "kWh"
  Registers("load_today_energy")\addr = 33179 : Registers("load_today_energy")\type = "uint16" : Registers("load_today_energy")\scale = 0.1 : Registers("load_today_energy")\unit = "kWh"
  Registers("house_load_power")\addr = 33147 : Registers("house_load_power")\type = "uint16" : Registers("house_load_power")\scale = 1 : Registers("house_load_power")\unit = "W"
  Registers("backup_load_power")\addr = 33148 : Registers("backup_load_power")\type = "uint16" : Registers("backup_load_power")\scale = 1 : Registers("backup_load_power")\unit = "W"
  Registers("meter_voltage")\addr = 33128 : Registers("meter_voltage")\type = "uint16" : Registers("meter_voltage")\scale = 0.1 : Registers("meter_voltage")\unit = "V"
  Registers("meter_current")\addr = 33129 : Registers("meter_current")\type = "uint16" : Registers("meter_current")\scale = 0.1 : Registers("meter_current")\unit = "A"
  Registers("meter_active_power")\addr = 33130 : Registers("meter_active_power")\type = "int32" : Registers("meter_active_power")\scale = 1 : Registers("meter_active_power")\unit = "W"
  Registers("dc_power_1")\addr = 0 : Registers("dc_power_1")\type = "calculated" : Registers("dc_power_1")\scale = 1 : Registers("dc_power_1")\unit = "W"
  Registers("dc_power_2")\addr = 0 : Registers("dc_power_2")\type = "calculated" : Registers("dc_power_2")\scale = 1 : Registers("dc_power_2")\unit = "W"
  Registers("dc_power_3")\addr = 0 : Registers("dc_power_3")\type = "calculated" : Registers("dc_power_3")\scale = 1 : Registers("dc_power_3")\unit = "W"
  Registers("dc_power_4")\addr = 0 : Registers("dc_power_4")\type = "calculated" : Registers("dc_power_4")\scale = 1 : Registers("dc_power_4")\unit = "W"
  Registers("connection_status")\addr = 0 : Registers("connection_status")\type = "string" : Registers("connection_status")\scale = 1 : Registers("connection_status")\unit = ""
EndProcedure

Procedure DefineStatusCodes()
  StatusCodes("0") = "Waiting"
  StatusCodes("1") = "Open Operating"
  StatusCodes("2") = "Soft Run"
  StatusCodes("3") = "Generating"
  StatusCodes("4") = "Bypass Inverter Running"
  StatusCodes("5") = "Bypass Inverter Sync"
  StatusCodes("6") = "Bypass Grid Running"
  StatusCodes("15") = "Normal Running"
  StatusCodes("4100") = "Grid Off"
  StatusCodes("61456") = "Grid Surge"
  StatusCodes("61457") = "Fan Fault"
  StatusCodes("4112") = "Grid Overvoltage"
  StatusCodes("4113") = "Grid Undervoltage"
  StatusCodes("4114") = "Grid Overfrequency"
  StatusCodes("4115") = "Grid Underfrequency"
  StatusCodes("4116") = "Grid Reverse Current"
  StatusCodes("4117") = "No-Grid"
  StatusCodes("4118") = "Grid Unbalanced"
  StatusCodes("4119") = "Grid Frequency Fluctuation"
  StatusCodes("4120") = "Grid Overcurrent"
  StatusCodes("4121") = "Grid Current Sampling Error"
  StatusCodes("4128") = "DC Overvoltage"
  StatusCodes("4129") = "DC Bus Overvoltage"
  StatusCodes("4130") = "DC Bus Unbalanced"
  StatusCodes("4131") = "DC Bus Undervoltage"
  StatusCodes("4132") = "DC Bus Unbalanced 2"
  StatusCodes("4133") = "DC(Channel A) Overcurrent"
  StatusCodes("4134") = "DC(Channel B) Overcurrent"
  StatusCodes("4135") = "DC Interference"
  StatusCodes("4136") = "DC Reverse"
  StatusCodes("4137") = "PV Midpoint Grounding"
  StatusCodes("4144") = "Grid Interference Protection"
  StatusCodes("4145") = "DSP Inital Protection"
  StatusCodes("4146") = "Over Temperature Protection"
  StatusCodes("4147") = "PV Insulation Fault"
  StatusCodes("4148") = "Leakage Current Protection"
  StatusCodes("4149") = "Relay Check Protection"
  StatusCodes("4150") = "DSP_B Protection"
  StatusCodes("4151") = "DC Injection Protection"
  StatusCodes("4152") = "12V Undervoltage Faulty"
  StatusCodes("4153") = "Leakage Current Check Protection"
  StatusCodes("4154") = "Under Temperature Protection"
  StatusCodes("4160") = "AFCI Check Fault"
  StatusCodes("4161") = "AFCI Fault"
  StatusCodes("4162") = "DSP Chip SRAM Fault"
  StatusCodes("4163") = "DSP Chip FLASH Fault"
  StatusCodes("4164") = "DSP Chip PC Pointer Fault"
  StatusCodes("4165") = "DSP Chip Register Fault"
  StatusCodes("4166") = "Grid Interference Protection 02"
  StatusCodes("4167") = "Grid Current Sampling Error"
  StatusCodes("4168") = "IGBT Overcurrent"
  StatusCodes("4176") = "Grid Transient Overcurrent"
  StatusCodes("4177") = "Battery Hardware Overvoltage fault"
  StatusCodes("4178") = "LLC Hardware Overcurrent"
  StatusCodes("4179") = "Battery Overvoltage"
  StatusCodes("4180") = "Battery Undervoltage"
  StatusCodes("4181") = "Battery Not Connected"
  StatusCodes("4182") = "Backup Overvoltage"
  StatusCodes("4183") = "Backup Overload"
  StatusCodes("4184") = "DSP Selfcheck Error"
  StatusCodes("8208") = "Fail Safe"
  StatusCodes("8209") = "Meter COM Fail"
  StatusCodes("8210") = "Battery COM Fail"
  StatusCodes("8212") = "DSP COM Fail"
  StatusCodes("8213") = "BMS Alarm"
  StatusCodes("8214") = "BatName-FAIL"
  StatusCodes("8215") = "BMS Alarm 2"
  StatusCodes("8216") = "DRM Connect Fail"
  StatusCodes("8217") = "Meter Select Fail"
  StatusCodes("8224") = "Lead-acid Battery High Temp"
  StatusCodes("8225") = "Lead-acid Battery Low Temp"
EndProcedure

Procedure DefineFaultCategories()
  FaultCategories("grid_status")\startAddr = 12501
  FaultCategories("grid_status")\endAddr = 12516
  FaultCategories("grid_status")\faults("12501") = "no_grid"
  FaultCategories("grid_status")\faults("12502") = "grid_overvoltage"
  FaultCategories("grid_status")\faults("12503") = "grid_undervoltage"
  FaultCategories("grid_status")\faults("12504") = "grid_overfrequency"
  FaultCategories("grid_status")\faults("12505") = "grid_underfrequency"
  FaultCategories("grid_status")\faults("12506") = "grid_phase_failure"
  FaultCategories("grid_status")\faults("12507") = "grid_island"
  FaultCategories("grid_status")\faults("12508") = "grid_voltage_unbalance"
  FaultCategories("grid_status")\faults("12509") = "grid_overcurrent"
  FaultCategories("grid_status")\faults("12510") = "grid_reverse_power"
  FaultCategories("grid_status")\faults("12511") = "grid_phase_sequence_error"
  FaultCategories("grid_status")\faults("12512") = "grid_voltage_spike"
  FaultCategories("grid_status")\faults("12513") = "grid_current_spike"
  FaultCategories("grid_status")\faults("12514") = "grid_voltage_harmonic_exceeded"
  FaultCategories("grid_status")\faults("12515") = "grid_current_harmonic_exceeded"
  FaultCategories("grid_status")\faults("12516") = "grid_power_factor_exceeded"

  FaultCategories("load_status")\startAddr = 12517
  FaultCategories("load_status")\endAddr = 12532
  FaultCategories("load_status")\faults("12517") = "backup_overvoltage_fault"
  FaultCategories("load_status")\faults("12518") = "backup_undervoltage_fault"
  FaultCategories("load_status")\faults("12519") = "backup_overcurrent_fault"
  FaultCategories("load_status")\faults("12520") = "backup_short_circuit"
  FaultCategories("load_status")\faults("12521") = "backup_overload"
  FaultCategories("load_status")\faults("12522") = "backup_phase_failure"
  FaultCategories("load_status")\faults("12523") = "backup_voltage_unbalance"
  FaultCategories("load_status")\faults("12524") = "backup_current_unbalance"
  FaultCategories("load_status")\faults("12525") = "backup_power_factor_exceeded"
  FaultCategories("load_status")\faults("12526") = "backup_frequency_out_of_range"
  FaultCategories("load_status")\faults("12527") = "backup_voltage_spike"
  FaultCategories("load_status")\faults("12528") = "backup_current_spike"
  FaultCategories("load_status")\faults("12529") = "backup_voltage_harmonic_exceeded"
  FaultCategories("load_status")\faults("12530") = "backup_current_harmonic_exceeded"
  FaultCategories("load_status")\faults("12531") = "backup_temperature_exceeded"
  FaultCategories("load_status")\faults("12532") = "backup_fan_failure"

  FaultCategories("battery_status")\startAddr = 12533
  FaultCategories("battery_status")\endAddr = 12548
  FaultCategories("battery_status")\faults("12533") = "battery_not_connected"
  FaultCategories("battery_status")\faults("12534") = "battery_overvoltage"
  FaultCategories("battery_status")\faults("12535") = "battery_undervoltage"
  FaultCategories("battery_status")\faults("12536") = "battery_overcurrent"
  FaultCategories("battery_status")\faults("12537") = "battery_short_circuit"
  FaultCategories("battery_status")\faults("12538") = "battery_overtemperature"
  FaultCategories("battery_status")\faults("12539") = "battery_undertemperature"
  FaultCategories("battery_status")\faults("12540") = "battery_cell_imbalance"
  FaultCategories("battery_status")\faults("12541") = "battery_charge_limit_exceeded"
  FaultCategories("battery_status")\faults("12542") = "battery_discharge_limit_exceeded"
  FaultCategories("battery_status")\faults("12543") = "battery_communication_failure"
  FaultCategories("battery_status")\faults("12544") = "battery_bms_failure"
  FaultCategories("battery_status")\faults("12545") = "battery_voltage_spike"
  FaultCategories("battery_status")\faults("12546") = "battery_current_spike"
  FaultCategories("battery_status")\faults("12547") = "battery_soc_out_of_range"
  FaultCategories("battery_status")\faults("12548") = "battery_soh_degraded"

  FaultCategories("inverter_status")\startAddr = 12581
  FaultCategories("inverter_status")\endAddr = 12587
  FaultCategories("inverter_status")\faults("12581") = "normal"
  FaultCategories("inverter_status")\faults("12582") = "inverter_overvoltage"
  FaultCategories("inverter_status")\faults("12583") = "inverter_undervoltage"
  FaultCategories("inverter_status")\faults("12584") = "inverter_overcurrent"
  FaultCategories("inverter_status")\faults("12585") = "inverter_overtemperature"
  FaultCategories("inverter_status")\faults("12586") = "inverter_short_circuit"
  FaultCategories("inverter_status")\faults("12587") = "inverter_fan_failure"
EndProcedure

Procedure.u ModBus_CalcCRC(*Buffer, Length.i)
  Protected CRC.u = $FFFF, i.i, j.i
  If *Buffer And Length > 0
    For i = 0 To Length - 1
      CRC ! PeekA(*Buffer + i)
      For j = 0 To 7
        CRC = (CRC >> 1) ! ((CRC & 1) * $A001)
      Next
    Next
  EndIf
  ProcedureReturn CRC
EndProcedure

Procedure.i ModBus_CheckCRC(*Buffer, Length.i)
  If Length < 2 Or Not *Buffer
    ProcedureReturn #False
  EndIf
  Protected CalculatedCRC.u = ModBus_CalcCRC(*Buffer, Length - 2)
  Protected ReceivedCRC.u = PeekU(*Buffer + Length - 2)
  ProcedureReturn Bool(CalculatedCRC = ReceivedCRC)
EndProcedure

Procedure.u ModBus_BigEndian16(Value.u)
  ProcedureReturn ((Value & $FF) << 8) | (Value >> 8)
EndProcedure

Procedure.i ReconnectSerialPort()
  LockMutex(MutexID) ; Ensure thread safety
  If Port
    CloseSerialPort(Port)
    Port = 0
    Debug "Closed serial port due to CRC error"
  EndIf
  Delay(500)
  Port = OpenSerialPort(#PB_Any, "COM3", 9600, #PB_SerialPort_NoParity, 8, 1, #PB_SerialPort_NoHandshake, 1024, 1024)
  If Port
    Debug "Reconnected serial port successfully on COM3"
    UnlockMutex(MutexID)
    ProcedureReturn 1
  Else
    ErrorMessage = "Failed to reconnect serial port COM3"
    Debug ErrorMessage
    UnlockMutex(MutexID)
    ProcedureReturn 0
  EndIf
EndProcedure

Procedure.i ModBus_RTU_ReadDiscreteInputs(SerialPort.i, SlaveAddress.i, StartInput.i, InputCount.i, Array DiscreteBits.i(1))
  Protected ReturnCode.i = -1 ; 0: success, negative: error, positive: device error
  Protected *RequestBuffer, ResponseTimeout.i, ResponseSize.i, DataByteCount.i, CurrentByte.i
  Protected DebugOutput.s, ByteIndex.i, BitIndex.i
  
  If SerialPort = 0
    Debug "Serial port not open in ReadDiscreteInputs"
    ProcedureReturn -3
  EndIf
  
  *RequestBuffer = AllocateMemory(128)
  If Not *RequestBuffer
    Debug "Memory allocation failed in ReadDiscreteInputs"
    ProcedureReturn -1
  EndIf
  
  ; Build Modbus RTU request
  PokeA(*RequestBuffer, SlaveAddress)
  PokeA(*RequestBuffer + 1, #ModBus_FunctionCode_ReadDiscreteInputs)
  PokeU(*RequestBuffer + 2, ModBus_BigEndian16(StartInput))
  PokeU(*RequestBuffer + 4, ModBus_BigEndian16(InputCount))
  PokeU(*RequestBuffer + 6, ModBus_CalcCRC(*RequestBuffer, 6))
  
  DebugOutput = "Sending (Discrete Inputs): " + RSet(Hex(PeekL(*RequestBuffer) & $FFFFFF), 6, "0") + " " +
                RSet(Hex(PeekL(*RequestBuffer + 4) & $FFFF), 4, "0") + " " + Hex(PeekW(*RequestBuffer + 6), #PB_Word)
  Debug DebugOutput
  
  If Not WriteSerialPortData(SerialPort, *RequestBuffer, 8)
    Debug "Failed to write to serial port in ReadDiscreteInputs"
    FreeMemory(*RequestBuffer)
    ProcedureReturn -3
  EndIf
  
  ; Handle echo if enabled
  If ModBus_RTU_Echo
    Protected EchoTimeout.i = 100
    While AvailableSerialPortInput(SerialPort) < 8 And EchoTimeout > 0
      Delay(1) : EchoTimeout - 1
    Wend
    If EchoTimeout > 0
      ReadSerialPortData(SerialPort, *RequestBuffer, 8)
    EndIf
  EndIf
  
  ; Read response
  ResponseTimeout = 3000
  ResponseSize = 0
  While ResponseTimeout > 0
    If AvailableSerialPortInput(SerialPort)
      ReadSerialPortData(SerialPort, *RequestBuffer + ResponseSize, 1)
      ResponseSize + 1
      If ResponseSize >= 3 ; Check for function code and byte count
        If PeekA(*RequestBuffer + 1) & $7F = #ModBus_FunctionCode_ReadDiscreteInputs
          If PeekA(*RequestBuffer + 1) & $80 ; Error response
            Debug "Error response from device: " + Str(PeekA(*RequestBuffer + 2))
            FreeMemory(*RequestBuffer)
            ProcedureReturn PeekA(*RequestBuffer + 2) ; Return error code
          EndIf
          DataByteCount = PeekA(*RequestBuffer + 2)
          If ResponseSize = DataByteCount + 5 ; Complete response (address + function + byte count + data + CRC)
            DebugOutput = "Received (Discrete Inputs): "
            For ByteIndex = 0 To ResponseSize - 1
              DebugOutput + Hex(PeekB(*RequestBuffer + ByteIndex) & $FF, #PB_Byte) + " "
            Next
            Debug DebugOutput
            If ModBus_CheckCRC(*RequestBuffer, ResponseSize)
              ReDim DiscreteBits(InputCount - 1)
              For ByteIndex = 0 To DataByteCount - 1
                CurrentByte = PeekA(*RequestBuffer + 3 + ByteIndex)
                For BitIndex = 0 To 7
                  If ByteIndex * 8 + BitIndex < InputCount
                    DiscreteBits(ByteIndex * 8 + BitIndex) = (CurrentByte >> BitIndex) & 1
                  EndIf
                Next
              Next
              Debug "Parsed discrete bits: " + Str(DiscreteBits(0)) + " " + Str(DiscreteBits(1)) + "..."
              ReturnCode = 0
            Else
              Debug "CRC check failed in ReadDiscreteInputs"
              ReturnCode = -2
            EndIf
            Break
          EndIf
        EndIf
      EndIf
    Else
      Delay(1)
      ResponseTimeout - 1
    EndIf
  Wend
  
  If ResponseTimeout = 0
    Debug "Response timeout in ReadDiscreteInputs"
    ReturnCode = -10
  EndIf
  
  FreeMemory(*RequestBuffer)
  ProcedureReturn ReturnCode
EndProcedure

Procedure.i ModBus_RTU_ReadInputRegisters(SerialPort.i, SlaveAddress.i, StartRegister.i, RegisterCount.i, Array RegisterValues.i(1))
  Protected ReturnCode.i = -1 ; 0: success, negative: error, positive: device error
  Protected *RequestBuffer, ResponseTimeout.i, ResponseSize.i, DataByteCount.i
  Protected DebugOutput.s, RegisterIndex.i
  
  If SerialPort = 0
    Debug "Serial port not open in ReadInputRegisters"
    ProcedureReturn -3
  EndIf
  
  *RequestBuffer = AllocateMemory(128)
  If Not *RequestBuffer
    Debug "Memory allocation failed in ReadInputRegisters"
    ProcedureReturn -1
  EndIf
  
  ; Build Modbus RTU request
  PokeA(*RequestBuffer, SlaveAddress)
  PokeA(*RequestBuffer + 1, #ModBus_FunctionCode_ReadInputRegisters)
  PokeU(*RequestBuffer + 2, ModBus_BigEndian16(StartRegister))
  PokeU(*RequestBuffer + 4, ModBus_BigEndian16(RegisterCount))
  PokeU(*RequestBuffer + 6, ModBus_CalcCRC(*RequestBuffer, 6))
  
  DebugOutput = "Sending (Input Registers): " + RSet(Hex(PeekL(*RequestBuffer) & $FFFFFF), 6, "0") + " " +
                RSet(Hex(PeekL(*RequestBuffer + 4) & $FFFF), 4, "0") + " " + Hex(PeekW(*RequestBuffer + 6), #PB_Word)
  Debug DebugOutput
  
  If Not WriteSerialPortData(SerialPort, *RequestBuffer, 8)
    Debug "Failed to write to serial port in ReadInputRegisters"
    FreeMemory(*RequestBuffer)
    ProcedureReturn -3
  EndIf
  
  ; Handle echo if enabled
  If ModBus_RTU_Echo
    Protected EchoTimeout.i = 100
    While AvailableSerialPortInput(SerialPort) < 8 And EchoTimeout > 0
      Delay(1) : EchoTimeout - 1
    Wend
    If EchoTimeout > 0
      ReadSerialPortData(SerialPort, *RequestBuffer, 8)
    EndIf
  EndIf
  
  ; Read response
  ResponseTimeout = 3000
  ResponseSize = 0
  While ResponseTimeout > 0
    If AvailableSerialPortInput(SerialPort)
      ReadSerialPortData(SerialPort, *RequestBuffer + ResponseSize, 1)
      ResponseSize + 1
      If ResponseSize >= 3 ; Check for function code and byte count
        If PeekA(*RequestBuffer + 1) & $7F = #ModBus_FunctionCode_ReadInputRegisters
          If PeekA(*RequestBuffer + 1) & $80 ; Error response
            Debug "Error response from device: " + Str(PeekA(*RequestBuffer + 2))
            FreeMemory(*RequestBuffer)
            ProcedureReturn PeekA(*RequestBuffer + 2) ; Return error code
          EndIf
          DataByteCount = PeekA(*RequestBuffer + 2)
          If ResponseSize = DataByteCount + 5 ; Complete response (address + function + byte count + data + CRC)
            DebugOutput = "Received (Input Registers): "
            For RegisterIndex = 0 To ResponseSize - 1
              DebugOutput + Hex(PeekB(*RequestBuffer + RegisterIndex) & $FF, #PB_Byte) + " "
            Next
            Debug DebugOutput
            If ModBus_CheckCRC(*RequestBuffer, ResponseSize)
              ReDim RegisterValues(RegisterCount - 1)
              For RegisterIndex = 0 To RegisterCount - 1
                RegisterValues(RegisterIndex) = ModBus_BigEndian16(PeekU(*RequestBuffer + 3 + RegisterIndex * 2))
              Next
              Debug "Parsed register values: " + Str(RegisterValues(0))
              ReturnCode = 0
            Else
              Debug "CRC check failed in ReadInputRegisters"
              ReturnCode = -2
            EndIf
            Break
          EndIf
        EndIf
      EndIf
    Else
      Delay(1)
      ResponseTimeout - 1
    EndIf
  Wend
  
  If ResponseTimeout = 0
    Debug "Response timeout in ReadInputRegisters"
    ReturnCode = -10
  EndIf
  
  FreeMemory(*RequestBuffer)
  ProcedureReturn ReturnCode
EndProcedure

Procedure.s ReadRegister(SerialPort.i, SlaveAddress.i, *Register.RegisterInfo)
  Protected ReadResult.i, ScaledValue.d, RawValue.q, Dim RegisterData.i(1)
  Protected MaxRetries.i = 1, RetryAttempt.i, RegisterCount.i
  
  Debug "Reading register " + Str(*Register\addr) + " (" + *Register\type + ")"
  
  ; Determine register count based on type
  Select *Register\type
    Case "uint16", "int16"
      RegisterCount = 1
    Case "uint32", "int32"
      RegisterCount = 2
    Case "calculated", "string"
      ProcedureReturn "unknown"
    Default
      Debug "Unsupported register type: " + *Register\type
      ProcedureReturn "unknown"
  EndSelect
  
  ; Retry loop for Modbus read
  For RetryAttempt = 0 To MaxRetries
    ReadResult = ModBus_RTU_ReadInputRegisters(SerialPort, SlaveAddress, *Register\addr, RegisterCount, RegisterData())
    If ReadResult = -2 ; CRC error
      If RetryAttempt < MaxRetries
        Debug "CRC error on register " + Str(*Register\addr) + ", retrying (" + Str(RetryAttempt + 1) + "/" + Str(MaxRetries) + ")"
        Delay(100)
        Continue
      Else
        Debug "Persistent CRC error on register " + Str(*Register\addr) + ", attempting reconnect"
        If ReconnectSerialPort()
          ReadResult = ModBus_RTU_ReadInputRegisters(SerialPort, SlaveAddress, *Register\addr, RegisterCount, RegisterData())
        EndIf
      EndIf
    EndIf
    Break
  Next
  
  ; Process successful read
  If ReadResult = 0
    Select *Register\type
      Case "uint16"
        ScaledValue = RegisterData(0) * *Register\scale
        If MapKey(Registers()) = "current_status" And FindMapElement(StatusCodes(), Str(RegisterData(0)))
          ProcedureReturn StatusCodes(Str(RegisterData(0)))
        EndIf
        ProcedureReturn StrF(ScaledValue, 2)
        
      Case "int16"
        RawValue = RegisterData(0)
        If RawValue >= 32768
          RawValue - 65536 ; Convert to signed
        EndIf
        ScaledValue = RawValue * *Register\scale
        ProcedureReturn StrF(ScaledValue, 2)
        
      Case "uint32"
        RawValue = (RegisterData(0) << 16) + RegisterData(1)
        ScaledValue = RawValue * *Register\scale
        ProcedureReturn StrF(ScaledValue, 2)
        
      Case "int32"
        RawValue = (RegisterData(0) << 16) + RegisterData(1)
        If RawValue & $80000000
          RawValue - $100000000 ; Convert to signed
        EndIf
        ScaledValue = RawValue * *Register\scale
        ProcedureReturn StrF(ScaledValue, 2)
    EndSelect
  EndIf
  
  Debug "Failed to read register " + Str(*Register\addr) + ": Result = " + Str(ReadResult)
  ProcedureReturn "unknown"
EndProcedure

Procedure.s ReadFaultStatus(SerialPort.i, SlaveAddress.i, FaultCategory.s)
  Protected FaultCount.i = FaultCategories(FaultCategory)\endAddr - FaultCategories(FaultCategory)\startAddr + 1
  Protected Dim FaultBits.i(15) ; Assuming max 16 bits, adjust if needed
  Protected ReadResult.i, FaultIndex.i, FaultStatus.s = "normal"
  Protected MaxRetries.i = 1, RetryAttempt.i
  
  ; Validate inputs
  If SerialPort = 0
    ProcedureReturn "Error: Serial port not open"
  EndIf
  If Not FindMapElement(FaultCategories(), FaultCategory)
    ProcedureReturn "Error: Invalid fault category '" + FaultCategory + "'"
  EndIf
  
  ; Retry loop for reading discrete inputs
  For RetryAttempt = 0 To MaxRetries
    ReadResult = ModBus_RTU_ReadDiscreteInputs(SerialPort, SlaveAddress, FaultCategories(FaultCategory)\startAddr, FaultCount, FaultBits())
    If ReadResult = -2 ; CRC error
      If RetryAttempt < MaxRetries
        Debug "CRC error reading fault category '" + FaultCategory + "', retrying (" + Str(RetryAttempt + 1) + "/" + Str(MaxRetries) + ")"
        Delay(100)
        Continue
      Else
        Debug "Persistent CRC error on fault category '" + FaultCategory + "', attempting reconnect"
        If ReconnectSerialPort()
          ReadResult = ModBus_RTU_ReadDiscreteInputs(SerialPort, SlaveAddress, FaultCategories(FaultCategory)\startAddr, FaultCount, FaultBits())
        EndIf
      EndIf
    EndIf
    Break
  Next
  
  ; Process fault status
  If ReadResult = 0
    For FaultIndex = 0 To FaultCount - 1
      Protected RegisterAddress.s = Str(FaultCategories(FaultCategory)\startAddr + FaultIndex)
      If FaultBits(FaultIndex) = 1 And FindMapElement(FaultCategories(FaultCategory)\faults(), RegisterAddress)
        FaultStatus = FaultCategories(FaultCategory)\faults(RegisterAddress)
        Break
      EndIf
    Next
  Else
    FaultStatus = "Error reading faults (" + Str(ReadResult) + ")"
  EndIf
  
  ProcedureReturn FaultStatus
EndProcedure

Procedure PollDataThread(*Unused)
  Protected FaultCategoryKey.s, PVIndex.i, VoltageKey.s, CurrentKey.s, PowerKey.s
  Protected VoltageValue.f, CurrentValue.f
  
  While Quit = 0
    LockMutex(MutexID)
    
    If Port
      ; Update register data
      ForEach Registers()
        Protected RegisterKey.s = MapKey(Registers())
        InverterData(RegisterKey)\unit = Registers()\unit
        Select Registers()\type
          Case "calculated"
          Case "string"
            InverterData(RegisterKey)\value = "connected"
          Default
            InverterData(RegisterKey)\value = ReadRegister(Port, Address, @Registers())
        EndSelect
      Next
      
      ; Calculate DC power for PV1 to PV4
      For PVIndex = 1 To 4
        VoltageKey = "dc_voltage_" + Str(PVIndex)
        CurrentKey = "dc_current_" + Str(PVIndex)
        PowerKey = "dc_power_" + Str(PVIndex)
        
        If InverterData(VoltageKey)\value <> "unknown" And InverterData(CurrentKey)\value <> "unknown"
          VoltageValue = ValF(InverterData(VoltageKey)\value)
          CurrentValue = ValF(InverterData(CurrentKey)\value)
          InverterData(PowerKey)\value = StrF(VoltageValue * CurrentValue, 2)
        Else
          InverterData(PowerKey)\value = "unknown"
        EndIf
      Next
      
      ; Update fault statuses
      ForEach FaultCategories()
        FaultCategoryKey = MapKey(FaultCategories())
        InverterData("fault_" + FaultCategoryKey)\value = ReadFaultStatus(Port, Address, FaultCategoryKey)
      Next
    Else
      ErrorMessage = "Serial port not open, attempting reconnect"
      Debug ErrorMessage
      If ReconnectSerialPort()
        Debug "Reconnect successful"
      Else
        Debug "Reconnect failed"
      EndIf
    EndIf
    
    UnlockMutex(MutexID)
    Delay(1000) ; Poll every second
  Wend
EndProcedure

Procedure.s CalculateTimeRemaining()
  Protected soc.f, power.f, direction.s, capacity.f = 15360
  Protected timeLabel.s, timeRemaining.s, hours.i, minutes.i
  
  LockMutex(MutexID)
  direction = InverterData("battery_direction")\value
  If direction = "0.00"
    direction = "Charging"
    timeLabel = "Time to 100%"
  ElseIf direction = "1.00"
    direction = "Discharging"
    timeLabel = "Time to 20%"
  Else
    direction = "unknown"
    timeLabel = "Time Remaining"
    UnlockMutex(MutexID)
    ProcedureReturn timeLabel + Space(14-Len(timeLabel)) + "N/A"
  EndIf
  
  Protected energyRemaining.f, hoursRemaining.f, totalMinutes.i,energyToFull.f
  
  soc = ValF(StringField(InverterData("battery_soc")\value, 1, " "))
  power = ValF(StringField(InverterData("battery_power")\value, 1, " "))
  
  If InverterData("battery_soc")\value <> "unknown" And InverterData("battery_power")\value <> "unknown" And power <> 0
    If direction = "Discharging" And soc > 20
      energyRemaining.f = capacity * (soc - 20) / 100
      hoursRemaining.f = energyRemaining / power
      totalMinutes.i = Int(hoursRemaining * 60)
      hours = totalMinutes / 60
      minutes = totalMinutes % 60
      timeRemaining = Str(hours) + "h " + Str(minutes) + "m"
    ElseIf direction = "Charging" And soc < 100
      energyToFull.f = capacity * (100 - soc) / 100
      hoursRemaining.f = energyToFull / power
      totalMinutes.i = Int(hoursRemaining * 60)
      hours = totalMinutes / 60
      minutes = totalMinutes % 60
      timeRemaining = Str(hours) + "h " + Str(minutes) + "m"
    ElseIf direction = "Charging" And soc >= 100
      timeRemaining = "Full"
    ElseIf direction = "Discharging" And soc <= 20
      timeRemaining = "Below 20%"
    Else
      timeRemaining = "N/A"
    EndIf
  Else
    timeRemaining = "N/A"
  EndIf
  UnlockMutex(MutexID)
  
  ProcedureReturn timeLabel + Space(14-Len(timeLabel)) + timeRemaining
EndProcedure

Procedure DisplayDashboard()
  ConsoleColor(7, 0)
  ClearConsole()
  PrintN(StringField("", 80, "="))
  
  PrintN("Solis Inverter Dashboard - " + FormatDate("%yyyy-%mm-%dd %hh:%ii:%ss", Date()))
  If ErrorMessage <> ""
    PrintN("Error: " + ErrorMessage)
  EndIf
  PrintN(StringField("", 80, "="))
  
  Protected ColumnWidth.i = 40
  Protected LabelWidth.i = 14
  Protected ValueWidth.i = ColumnWidth - LabelWidth - 1
  
  Protected Dim LeftColumn.s(19)
  Protected Dim RightColumn.s(19)
  
  LockMutex(MutexID)
  
  ; Left column: Inverter and Battery
  LeftColumn(0) = "INVERTER"
  LeftColumn(1) = LSet("Status ", LabelWidth, " ") + LSet(InverterData("current_status")\value, ValueWidth, " ")
  LeftColumn(2) = LSet("Temperature ", LabelWidth, " ") + LSet(InverterData("inverter_temp")\value + "°C", ValueWidth, " ")
  LeftColumn(3) = LSet("Active Power ", LabelWidth, " ") + LSet(InverterData("active_power")\value + "W", ValueWidth, " ")
  LeftColumn(4) = LSet("DC Power ", LabelWidth, " ") + LSet(InverterData("total_dc_power")\value + "W", ValueWidth, " ")
  LeftColumn(5) = LSet("PV1 ", LabelWidth, " ") + LSet(InverterData("dc_voltage_1")\value + "V, " + InverterData("dc_power_1")\value + "W", ValueWidth, " ")
  LeftColumn(6) = LSet("PV2 ", LabelWidth, " ") + LSet(InverterData("dc_voltage_2")\value + "V, " + InverterData("dc_power_2")\value + "W", ValueWidth, " ")
  LeftColumn(7) = ""
  LeftColumn(8) = "BATTERY"
  LeftColumn(9)  = LSet("Voltage ", LabelWidth, " ") + LSet(InverterData("battery_voltage")\value + "V", ValueWidth, " ")
  LeftColumn(10) = LSet("Current ", LabelWidth, " ") + LSet(InverterData("battery_current")\value + "A", ValueWidth, " ")
  LeftColumn(11) = LSet("Power ", LabelWidth, " ") + LSet(InverterData("battery_power")\value + "W", ValueWidth, " ")
  LeftColumn(12) = LSet("SOC ", LabelWidth, " ") + LSet(InverterData("battery_soc")\value + "%", ValueWidth, " ")
  LeftColumn(13) = LSet("SOH ", LabelWidth, " ") + LSet(InverterData("battery_soh")\value + "%", ValueWidth, " ")
  
  Protected BatteryDirection.s = InverterData("battery_direction")\value
  Select BatteryDirection
    Case "0.00" : LeftColumn(14) = LSet("Direction ", LabelWidth, " ") + LSet("Charging", ValueWidth, " ")
    Case "1.00" : LeftColumn(14) = LSet("Direction ", LabelWidth, " ") + LSet("Discharging", ValueWidth, " ")
    Default     : LeftColumn(14) = LSet("Direction ", LabelWidth, " ") + LSet("unknown", ValueWidth, " ")
  EndSelect
  
  LeftColumn(15) = LSet("Time Remaining ", LabelWidth, " ") + LSet(CalculateTimeRemaining(), ValueWidth, " ") 
  LeftColumn(16) = ""
  LeftColumn(17) = "CONNECTIONS"
  LeftColumn(18) = LSet("Tuya ", LabelWidth, " ") + LSet("OFF (Removed)", ValueWidth, " ") ; Removed
  LeftColumn(19) = LSet("MQTT ", LabelWidth, " ") + LSet("Off (Removed)", ValueWidth, " ") ; Removed
  
  ; Right column: Grid and Totals
  RightColumn(0) = "GRID"
  RightColumn(1) = LSet("V(A/B/C) ", LabelWidth, " ") + LSet(InverterData("grid_voltage_a")\value + "/" + 
                                                             InverterData("grid_voltage_b")\value + "/" + 
                                                             InverterData("grid_voltage_c")\value + "V", ValueWidth, " ")
  RightColumn(2) = LSet("I(A/B/C) ", LabelWidth, " ") + LSet(InverterData("grid_current_a")\value + "/" + 
                                                             InverterData("grid_current_b")\value + "/" + 
                                                             InverterData("grid_current_c")\value + "A", ValueWidth, " ")
  RightColumn(3) = LSet("Frequency ", LabelWidth, " ") + LSet(InverterData("grid_frequency")\value + "Hz", ValueWidth, " ")
  RightColumn(4) = LSet("Power ", LabelWidth, " ") + LSet(InverterData("inverter_ac_grid_power")\value + "W", ValueWidth, " ")
  RightColumn(5) = ""
  RightColumn(6) = "DAILY TOTALS"
  RightColumn(7)  = LSet("Generation ", LabelWidth, " ") + LSet(InverterData("energy_today")\value + "kWh", ValueWidth, " ")
  RightColumn(8)  = LSet("Grid Import ", LabelWidth, " ") + LSet(InverterData("grid_import_today")\value + "kWh", ValueWidth, " ")
  RightColumn(9)  = LSet("Grid Export ", LabelWidth, " ") + LSet(InverterData("grid_export_today")\value + "kWh", ValueWidth, " ")
  RightColumn(10) = LSet("Batt Charged ", LabelWidth, " ") + LSet(InverterData("battery_charge_today")\value + "kWh", ValueWidth, " ")
  RightColumn(11) = LSet("Batt Discharged ", LabelWidth, " ") + LSet(InverterData("battery_discharge_today")\value + "kWh", ValueWidth, " ")
  RightColumn(12) = LSet("Load ", LabelWidth, " ") + LSet(InverterData("load_today_energy")\value + "kWh", ValueWidth, " ")
  RightColumn(13) = ""
  RightColumn(14) = "FAULTS"
  RightColumn(15) = LSet("Grid ", LabelWidth, " ") + LSet(InverterData("fault_grid_status")\value, ValueWidth, " ")
  RightColumn(16) = LSet("Load ", LabelWidth, " ") + LSet(InverterData("fault_load_status")\value, ValueWidth, " ")
  RightColumn(17) = LSet("Battery ", LabelWidth, " ") + LSet(InverterData("fault_battery_status")\value, ValueWidth, " ")
  RightColumn(18) = LSet("Inverter ", LabelWidth, " ") + LSet(InverterData("fault_inverter_status")\value, ValueWidth, " ")
  
  UnlockMutex(MutexID)
  
  ; Display columns
  Protected TotalLines.i = 20, LineIndex.i
  For LineIndex = 0 To TotalLines - 1
    Protected LeftText.s = LeftColumn(LineIndex)
    Protected RightText.s = RightColumn(LineIndex)
    If Len(LeftText) < ColumnWidth
      LeftText + Space(ColumnWidth - Len(LeftText))
    Else
      LeftText = Left(LeftText, ColumnWidth) ; Truncate if too long
    EndIf
    If Len(RightText) < ColumnWidth
      RightText + Space(ColumnWidth - Len(RightText))
    Else
      RightText = Left(RightText, ColumnWidth) ; Truncate if too long
    EndIf
    PrintN(LeftText + RightText)
  Next
  
  PrintN(StringField("", 80, "="))
  PrintN("Press Ctrl+C to exit")
  PrintN(StringField("", 80, "="))
  ConsoleCursor(0)
EndProcedure

Procedure Main()
  DefineRegisters()
  DefineStatusCodes()
  DefineFaultCategories()
  
  Port = OpenSerialPort(#PB_Any, "COM3", 9600, #PB_SerialPort_NoParity, 8, 1, #PB_SerialPort_NoHandshake, 1024, 1024)
  If Port
    Debug "Serial port opened successfully on COM3"
  Else
    ErrorMessage = "Failed to open serial port COM3"
    Debug ErrorMessage
  EndIf
  
  MutexID = CreateMutex()
  If MutexID
    ThreadID = CreateThread(@PollDataThread(), 0)
    If ThreadID
      Debug "Polling thread started"
    Else
      ErrorMessage = "Failed to start polling thread"
      Debug ErrorMessage
    EndIf
  Else
    ErrorMessage = "Failed to create mutex"
    Debug ErrorMessage
  EndIf
  
  If OpenConsole() = 0
    ErrorMessage = "Failed to open console"
    Debug ErrorMessage
    End
  EndIf
  
  Repeat
    DisplayDashboard()
    Delay(2000)
  Until Inkey() = Chr(3) Or Quit = 1 ; Allow thread to signal quit
  
  Quit = 1
  If ThreadID
    WaitThread(ThreadID)
  EndIf
  If MutexID
    FreeMutex(MutexID)
  EndIf
  If Port
    CloseSerialPort(Port)
  EndIf
  CloseConsole()
EndProcedure

Main()

Here's the output in Console.
You can add MQTT support to use on Home Assistant for custom dashboard.

Image

[Registered PB User since 2006]
[PureBasic 6.20][SpiderBasic 2.2]
[RP4 x64][Win 11 x64][Ubuntu x64]
User avatar
idle
Always Here
Always Here
Posts: 5887
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: Solis Hybrid Inverter S6 (via modbus rs485)

Post by idle »

That would have been some fun to write. Thanks for sharing :D
Post Reply