Wednesday, June 13, 2018

(#30) Solar Charge Controller Communication


The last post covered off on the hardware and computer connection for creating a communication link with the EPSolar LandStar LS1024B controller.

Here we'll briefly cover off on the basic software approach to communicating with the controller.

As mentioned last time, the communication protocol used by the controller is an archaic one called Modbus.

With our USB/RS485 Adapter connected and plugged into a Linux machine we can start coding up the software.

There are Python libraries for modbus, but I chose to use a 'C' library for initial exploration.  I used libmodbus V3.1.4 from libmodbus.org on Ubuntu 16.04 LTS.

[ November 2018 Update: complete code for my solution is available in my GitHub Repository ]


Modbus and Libmodbus

Fortunately the API exposed by libmodbus is small; there's not a lot to master. And the documentation is good. Unfortunately, the terminology is modbus specific and I've already taken the stance that I have no desire to master an interface protocol for industrial devices from the late 70's.

So rather than dig into why a register isn't a coil and why there are three or four ways to actually read data, it was easier to just throw some code and it and see what works.


The C code


Right off the bat, include the right header files:

#include <errno.h>
#include <modbus/modbus.h>


#define LANDSTAR_1024B_ID       0x01

Then try to open the port to the controller. The settings are 15200 Baud, 8N1. My USB/RS485 adapter shows up as /dev/ttyUSB0:

int main (int argc, char* argv[]) 
{
    modbus_t    *ctx;
    
    puts( "Opening ttyUSB0, 115200 8N1" );
    ctx = modbus_new_rtu( "/dev/ttyUSB0", 115200, 'N', 8, 1 );
    if (ctx == NULL) {
        fprintf( stderr, "Error opening\n" );
        return -1;
    }

    printf( "Setting slave ID to %X\n", LANDSTAR_1024B_ID );

    modbus_set_slave( ctx, LANDSTAR_1024B_ID );
    <...>

If the port opens successfully, then try to connect:

    puts( "Connecting" );
    if (modbus_connect( ctx ) == -1) {
        fprintf( stderr, "Connection failed: %s\n", 
                 modbus_strerror( errno ) );
        modbus_free( ctx );
        return -1;

    }

Once the port is open, you need to consult the vendor documentation to see what 'modbus registers' hold the data we're interested in. 


Here's a snippet that shows how to pull what EPSolar calls the "real time" data:

static 
void    getRealTimeData (modbus_t *ctx)
{
    int         registerAddress = 0x3100;

    int         numBytes = 0x13;  // 0x14 and up gives 
                                  // 'illegal data address'
    uint16_t    buffer[ 32 ];

    // zero everything out to start!    
    memset( buffer, '\0', sizeof buffer );
    
    // call 'read-input-registers', other read calls error
    if (modbus_read_input_registers( ctx, 
            registerAddress, numBytes, buffer ) == -1) {
        fprintf(stderr, "Read failed: %s\n", 
                modbus_strerror( errno ));
        return;
    }
    
    // ---------------------------------------------
    //  Photo Voltaic Values - Volts, Amps and Watts
    float pvArrayVoltage =  ((float) buffer[ 0x00 ]) / 100.0;
    float pvArrayCurrent =  ((float) buffer[ 0x01 ]) / 100.0;
    
    //
    // Assemble the Power (watts) value from two words
    long    temp = buffer[ 0x03 ] << 16;
    temp |= buffer[ 0x02 ];
    float pvArrayPower   =  (float) temp / 100.0;
    <..snip..>


A couple of notes...
There are a handful of 'read' calls in the modbus library. Again, rather that learn modbus details, I just whacked at the code until one of the calls worked.

But the 'read-input-registers' call did not always work. More on this later.




Note the 'register-address' variable that's set to 0x3100. Again, this value comes from the vendor documentation.  And the amount of data read, numBytes, can vary from 1 to MAX (where MAX seems to depend). 

This number of bytes values sometimes fell short of what the documentation said I could expect. For example, there are three additional fields documented:

float batterySOC = ((float) buffer[ 0x1A ]) / 100.0;
float remoteBatteryTemp = ((float) buffer[ 0x1B ]) / 100.0;

float systemRatedVoltage = ((float) buffer[ 0x1D ]) / 100.0;

But it didn't work for my controller - setting numBytes higher that 0x13 gave an error.


I mentioned that 'read-input-registers' didn't always work. For the Settings stored at 0x9000, I had to use the 'read-registers' call.  Looking at little into the Modbus specifics, I can see that one call uses Modbus function call 0x04 and the other uses 0x03.   It's most likely because the settings at 0x9000 are both read and write. 

At some point in the future, I'll try the function write-registers (modbus function 0x10) to see if I can update the settings. 

No comments :

Post a Comment