Web server for Classic using Arduino DUE

Started by dgd, March 12, 2015, 01:00:24 AM

Previous topic - Next topic

paul alting

Hi David,
The ACS758's are still sitting on the table unfortunately, while I am being kept busy helping my neighbour with his new LiFeYPO4 system and new PV array, as well as lagging with my own new install.
The new battery and control cabinet is almost ready for install and then mounting parts into, starting with LiFeYPO4 cells, then moving up to inverter and then the Arduino DUE and Cubieboard II.

My plan is still to use the technique I mentioned, as the ACS758 will have much better performance on a higher supply than 3v3 that the DUE uses.
Then by using an accurate resistor divider that maintains the necessary impedance suitable for the analog input of the DUE (< 10kΩ), it should be good.

I would get uni-directional sensors of these sort, whether you get the ACS712-xx or ACS758, depending on your measurement range, as you will get double the resolution right away.
With the more commonly used bi-directional sensors, the sensor output sits at Vcc ÷ 2, and swings direction based on current flow direction.
If you only have Vcc ÷ 2 to play with, you loose half of your available anlaog input range, unless you use an op-amp to off-set the signal from such a sensor.

I like that these sensors can be placed in any location of the circuit, not just on the low side as is done with current shunts, and they are fully isolated electrically.

Maybe you used these type already?
_____
Paul
6 x 200W PV into home-brew 6 stage MOSFET charge controller : Microhydro 220Vac 3 phase IMAG
8 x 400Ah LiFeYPO4 Winston : Latronics LS2412 inverter
QuadlogSCADA control and monitoring system : Tasmania, Australia : http://paulalting.com

dgd

Paul,
I have ordered several of those devices so another fun job pending  :)
dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

Here is the bigger web page text version of the simple web server on DUE.
Again not very sophisticated, instead of plain table with data columns it using SVG boxes and text elements.
Still need to rewrite to stop using math floats and instead move real number modbus registers to a formatted string - lots of faffing about to just dispose of that unwanted second decimal place zero - ie 123.4 instead of 123.40
Also still can't figure out where or how to extract the modbus register for remaining battery AH count.
Re posted the sketch in next message - now back to playing with meters and highcharts. Found a real nice highchart that looks almost identical to the chart used in the MyMidnite web page, for Arduino  too  :)

dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

..cleaned up the page before posting sketch.
Now the numbers display with only one digit after decimal point  ;D   had to move all usage of data type FLOAT and replace with some string manipulation, added benefit is program now runs significantly faster and compiles to smaller size.
dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

paul alting

David,
Looks great, I saw it the day you posted it and have had it in one of my many open tabs intending to reply sooner.
You have made nice use of SVG.

You mention floats and strings, my curiosity is wondering and nagging me to suggest not using any strings or floats, at least no string use in the Arduino.
Without strings, I am currently assisting someone to decode the data stream from a TriMetric battery monitor, it's quite a challenge :)
Then, to pull the data across to the Cubieboard via Modbus holding registers, I then need to have the data as uint16_t, and scaled to get at least one or two decimal places.
(fun times I'm sure you know well enough)
____
Paul
6 x 200W PV into home-brew 6 stage MOSFET charge controller : Microhydro 220Vac 3 phase IMAG
8 x 400Ah LiFeYPO4 Winston : Latronics LS2412 inverter
QuadlogSCADA control and monitoring system : Tasmania, Australia : http://paulalting.com

dgd

#35
Arduino DUE sketch that displays larger SVG text panels web page.
Top panel for battery info then one panel each for connected Classics
No floating point number processing now, replaced by manipulating char arrays, runs too fast now as modbus reading can't keep up

dgd



/*
  Web Server

A simple web server that shows the values of several modbus registers from Midnite
Classic. Up to 4 Classics conected via separate rs232 connection to Arduino Mega or DUE
Display using SVG text panels
David Dix  2 April 2015  dgd@kc.net.nz
*/

#include <SimpleModbusMaster.h> 
#include <Ethernet.h>
#include <SPI.h>

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network (just leave mac as it is now)
byte mac[] = {
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
IPAddress ip(192, 168, 1, 177);

// Initialize the Ethernet server library
// with the IP address and port
// (port 80 is default for HTTP):
EthernetServer server(80);

// setup modbus port information
#define baud 19200
#define timeout 1000
#define polling 200 // the scan rate
#define retry_count 10
#define IDLE 1

// millis interval between switching modbus ports for different Classics
#define CLTIME 15000

// used to toggle the receive/transmit pin - only for rs485, not used here by lib still expects it - ugh!
#define TxEnablePin 2

// up to 4 Classics connected the serial ports (Mega and DUE)
// could just leave this at 4 but would run slower if cant find all four Classics
#define CLASSICS 2

// modbus lib structures
enum
{
  PACKET1,
  PACKET2,
  PACKET3,
  PACKET4,
  PACKET5,
  TOTAL_NO_OF_PACKETS // leave this last entry
};

// Create an array of Packets to be configured
Packet packets[TOTAL_NO_OF_PACKETS];
packetPointer packet1 = &packets[PACKET1];  // pointers to access each packet
packetPointer packet2 = &packets[PACKET2];
packetPointer packet3 = &packets[PACKET3];
packetPointer packet4 = &packets[PACKET4];
packetPointer packet5 = &packets[PACKET5];

// Data read from the Classic will be stored in these arrays, using separate arrays to keep memory usage low
// each array is initialized to a packet.
// Note: serial port hardware buffer size means largest array of registers that can be obtained is 29
unsigned int readRegs[30];
unsigned int wbjrRegs[15];
unsigned int vRegs[2];
unsigned int idRegs[10];       // name 4209 to 4212, and time/date 4213 to 4216
unsigned int sernoRegs[2];

HardwareSerial* serialPort;


int current_classic = 0, prev_classic;

  char  invstr[CLASSICS][8], oastr[CLASSICS][8], kwhstr[CLASSICS][8];
  int   cstate[CLASSICS], watts[CLASSICS], wbjr_ahr, wbjr_soc, vers[CLASSICS], model[CLASSICS], remt;
  char  ftpstr[CLASSICS][8], ptpstr[CLASSICS][8];
  char  Classic_name[CLASSICS][10], bvstr[8], wbastr[8], btpstr[8], rtpstr[8];
  int   eq_hrs[CLASSICS], eq_mins[CLASSICS], eq_secs[CLASSICS];
  int   fl_hrs[CLASSICS], fl_mins[CLASSICS], fl_secs[CLASSICS];
  int   abs_hrs[CLASSICS], abs_mins[CLASSICS], abs_secs[CLASSICS];
  int   serno[CLASSICS];
  unsigned long start_count, timenow;

void setup()
{
  int  i,j, k;
 
  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
   
  modbus_construct(packet1, 10, READ_HOLDING_REGISTERS, 4114, 29, readRegs);  // at max 29
  modbus_construct(packet2, 10, READ_HOLDING_REGISTERS, 4360, 14, wbjrRegs);
  modbus_construct(packet3, 10, READ_HOLDING_REGISTERS, 4100, 1, vRegs);
  modbus_construct(packet4, 10, READ_HOLDING_REGISTERS, 4209, 9, idRegs);
  modbus_construct(packet5, 10, READ_HOLDING_REGISTERS, 0x7000, 2, sernoRegs);
 
   // start modbus input from port Serial1 on DUE/Mega
   start_count = millis();
   modbus_configure(&Serial1, baud, timeout, polling, retry_count, TxEnablePin, packets, TOTAL_NO_OF_PACKETS);
}

void loop()
{
  char  c;
  int   i,j,k, amps;

/*
   Use millis() time counting for changing between Classics being processed by the modbus state machine.
   Each change just pokes the harware serial parameter and re-executes modbus_configure/modbus_update.
   Not perfect but seems to work, may take an iteration or three to get the correct readings in each CLassic's
   display panel, modbus ugly timing is the problem.
   Proper solution is to rewrite less sophisticated modbus lib and execute one modbus manager for each
   serial port - using the DUE scheduler would work well for this method     
     
*/
   prev_classic = current_classic;

   // Move to next serial port for input if time counter used up ie swap to next Classic
   timenow = millis();
   if ((timenow - start_count) > (unsigned long)CLTIME)
   {
     start_count = timenow;
     current_classic++;
     if (current_classic >= CLASSICS) current_classic = 0;
     switch (current_classic)
     {
       case 0:                        // serial port order could be changed if desired
          serialPort = &Serial1;      // eg if there is only ever one Clssic on &Serial then make it first
          break;
       case 1:
          serialPort = &Serial2;
          break;
       case 2:
          serialPort = &Serial3;
          break;
       case 3:
          serialPort = &Serial;     // hardware serial 0
          break;
     }
     if (current_classic != prev_classic )  // if only one classic then don't reconfigure
     {
       modbus_configure(serialPort, baud, timeout, polling, retry_count, TxEnablePin, packets, TOTAL_NO_OF_PACKETS);
       modbus_update();  // to start the reconfigure
       delay(10);
     }
   }
 
   modbus_update();   // the modbus lib state machine :-)
   
   k = current_classic;
   
  // make Classic register values ready for web page
     model[k] = (unsigned int)lowByte(vRegs[0]);
     serno[k] = sernoRegs[1];
     Classic_name[k][0] = lowByte (idRegs[0]);
     Classic_name[k][1] = highByte(idRegs[0]);
     Classic_name[k][2] = lowByte (idRegs[1]);
     Classic_name[k][3] = highByte(idRegs[1]);
     Classic_name[k][4] = lowByte (idRegs[2]);
     Classic_name[k][5] = highByte(idRegs[2]);
     Classic_name[k][6] = lowByte (idRegs[3]);
     Classic_name[k][7] = highByte(idRegs[3]);
     Classic_name[k][8] = 0x00;

     itoa (readRegs[0], bvstr, 10);
     i=strlen(bvstr);
     bvstr[i] = bvstr[i-1];
     bvstr[i-1] = '.';
     bvstr[i+1] = '\0';
     
     itoa (readRegs[1], invstr[k], 10);   // input voltage
     i = strlen (invstr[k]);
     invstr[k][i] = invstr[k][i-1];
     invstr[k][i-1] = '.';
     invstr[k][i+1] = '\0';
     
     itoa (readRegs[2], oastr[k], 10);    // output amps
     i = strlen (oastr[k]);
     if ( i==1 )                          // insert leading zero if value < 1
     {
       oastr[k][1]=oastr[k][0];
       oastr[k][0] = '0';
       i++;
     }
     oastr[k][i] = oastr[k][i-1];
     oastr[k][i-1] = '.';
     oastr[k][i+1] = '\0';

     itoa (readRegs[3], kwhstr[k], 10);   // kwh
     i = strlen (kwhstr[k]);
     if (i==1)              // insert leading zero if < 0
     {     kwhstr[k][1] = kwhstr[k][0];
           kwhstr[k][0] = '0';
           i++;
     }
     kwhstr[k][i] = kwhstr[k][i-1];
     kwhstr[k][i-1] = '.';
     kwhstr[k][i+1] = '\0';
         
     watts[k] = readRegs[4];
     cstate[k] = (unsigned int)readRegs[5] >> 8;  // high byte contains charge state code 
   
     if (readRegs[17] > 100)
     {
         btpstr[0] = '2';   // set to default 25c
         btpstr[1] = '5';
         btpstr[2] = '\0'; 
     }
     else
     {
         itoa (readRegs[17], btpstr, 10);    // need to test if BTS present
         i = strlen(btpstr);
         btpstr[i] = btpstr[i-1];
         btpstr[i-1] = '.';
         btpstr[i+1] = '\0';
     }
     
     itoa (readRegs[18], ftpstr[k], 10);
     i = strlen (ftpstr[k]);
     ftpstr[k][i] = ftpstr[k][i-1];
     ftpstr[k][i-1] = '.';
     ftpstr[k][i+1] = '\0'; 
     itoa (readRegs[19], ptpstr[k], 10);
     i = strlen (ptpstr[k]);
     ptpstr[k][i] = ptpstr[k][i-1];
     ptpstr[k][i-1] = '.';
     ptpstr[k][i+1] = '\0';

     fl_secs[k] = readRegs[23];      //4137 floatTimeofdayseconds
     if (fl_secs[k] >= 3600)
     { fl_hrs[k] = (int)fl_secs[k]/3600;
       fl_secs[k] -= (int) fl_hrs[k]*3600;
     }
     else fl_hrs[k] = 0;
     if (fl_secs[k] >= 60 )
     { fl_mins[k] = (int)fl_secs[k]/60;
       fl_secs[k] -= (int) fl_mins[k]*60;
     }
     else fl_mins[k]=0;
     
     
     eq_secs[k] = readRegs[28];      //4142 EqualizeTime
     if (eq_secs[k] >= 3600)
     {
       eq_hrs[k] = (int)eq_secs[k]/3600;
       eq_secs[k] -= (int) eq_hrs[k]*3600;
     }
     else eq_hrs[k] = 0;
     if (eq_secs[k] >= 60 )
     { eq_mins[k] = (int)eq_secs[k]/60;
       eq_secs[k] -= (int) eq_mins[k]*60;
     }
     else eq_mins[k]=0;
     
     abs_secs[k] = readRegs[24];      //4138 AbsorbTime
     if (abs_secs[k] >= 3600)
     {
       abs_hrs[k] = (int)abs_secs[k]/3600;
       abs_secs[k] -= (int) abs_hrs[k]*3600;
     }
     else abs_hrs[k] = 0;
     if (abs_secs[k] >= 60 )
     {
       abs_mins[k] = (int)abs_secs[k]/60;
       abs_secs[k] -= (int) abs_mins[k]*60;
     }
     else abs_mins[k] = 0;

// WBjr registers   
     remt = (int) lowByte (wbjrRegs[11]);
     remt -=50;
     if  (remt != -50 )  // there is WBjr connected
     {
        itoa (remt, rtpstr, 10);
        amps = (signed int)wbjrRegs[10];   // cast doesn't always work
  // wbjrRegs is unsigned int so need to make signed int before itoa conversion
        if (amps > 60000) amps -=65535;
        itoa (amps, wbastr, 10);
        i = strlen(wbastr);
  // need to insert leading zero if <1 and > -1,  eg .3 becomes 0.3, -.2 becomes -0.2
 
        if (i==1 || (i==2 && wbastr[0] == '-') )
        { 
            wbastr[i] = wbastr[i-1];
            wbastr[i-1] = '0';
            i++;
        }   
        wbastr[i] = wbastr[i-1];   // now divide by 10 by inserting decimal point
        wbastr[i-1] = '.';
        wbastr[i+1] = '\0';
        wbjr_soc = wbjrRegs[12];          // 4372
        wbjr_ahr = wbjrRegs[8];           // this battery Ah capacity`` [
     }
 
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client)
  {
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected())
    {
      if (client.available())
      {
        char c = client.read();
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank)
        {
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close");  // the connection will be closed after completion of the response
          client.println("Refresh: 5");  // refresh the page automatically every 5 sec
          client.println();
          client.println("<!DOCTYPE HTML>");     // HTML 5 reponse

          client.println("<html><body>");

         // a bigger display for battery/WBjr
         // top panel is for Battery stuff, Voltage, SOC, Amps in/out etc..
         // then one panel for each Classic with ID details, state, in voltage, Watts out and Amps out etc..
         
          client.println ("<svg width=\"1260\" height=\"185\">");
          client.println ("<rect x=\"145\" y=\"20\" rx=\"20\" ry=\"20\" width=\"935\" height=\"150\" style=\"fill:yellow;stroke:black;stroke-width:5;opacity:0.5\" />");
          client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"175\" y=\"56\">Battery SOC</text> ");
          client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"175\" y=\"123\">");
          client.print (wbjr_soc);
          client.println ("</text> ");
          client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"360\" y=\"56\">WBjr Amps</text>");
          client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"360\" y=\"123\">");
          client.print (wbastr);
          client.println ("</text> ");

          client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"535\" y=\"56\">Battery Volts</text> ");
          client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"535\" y=\"123\">");
          client.print (bvstr);
          client.println ("%</text> ");
          client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"710\" y=\"56\">Amp Hours</text> ");
          client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"710\" y=\"123\">");
          client.print (wbjr_ahr);
          client.println ("</text> ");

          client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"885\" y=\"56\">Temps</text> ");
          client.print ("<text font-size=\"25\" font-family=\"Verdana\" x=\"885\" y=\"96\">Battery ");
          client.print (btpstr);
          client.println ( "</text> ");
          client.print ("<text font-size=\"25\" font-family=\"Verdana\" x=\"885\" y=\"136\">Remote  ");
          client.print (rtpstr);
          client.println ("</text>");

        client.println ("Sorry, your browser does not support inline SVG.");
        client.println ("</svg>");

        for (k=0; k< CLASSICS; k++ )
        {

       
       // a bigger display for Classic data stuff
            client.println ("<svg width=\"1260\" height=\"245\">");
            client.println ("<rect x=\"145\" y=\"20\" rx=\"20\" ry=\"20\" width=\"935\" height=\"200\" style=\"fill:blue;stroke:black;stroke-width:5;opacity:0.1\" />");

            client.print ("<text font-size=\"20\" font-family=\"Verdana\" x=\"175\" y=\"56\">");
            client.print (Classic_name[k]);
            client.print ("  C");
            client.print (model[k]);
            client.print ("  #");
            client.print (serno[k]);
            client.println ("</text>");

            client.print ("<text font-size=\"25\" font-family=\"Verdana\" x=\"500\" y=\"56\">");
            switch (cstate[k])
            {
            case 0:
              client.print ("RESTING");
              break;
            case 3:
              client.print ("ABSORB - time left ");
              client.print (abs_hrs[k]);
              client.print (":");
              client.print (abs_mins[k]);
              client.print (":");
              client.print (abs_secs[k]);
              break;
            case 4:
              client.print ("BULKMPPT");
              break;
            case 5:
              client.print ("FLOAT - total time ");
              client.print (fl_hrs[k]);
              client.print (":");
              client.print (fl_mins[k]);
              client.print (":");
              client.print (fl_secs[k]);
              break;
            case 6:
              client.print ("FLOAT MPPT");
              break;
            case 7:
              client.print ("EQUALISE - time left ");
              client.print (eq_hrs[k]);
              client.print (":");
              client.print (eq_mins[k]);
              client.print (":");
              client.print (eq_secs[k]);
              break; 
            case 10:
              client.print ("HYPERVOC");
              break;
            case 18:
              client.print ("EQ MPPT");
              break;
            }
            client.println ("</text>");

            client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"175\" y=\"106\">Input Volts</text>");
            client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"175\" y=\"163\">");
            client.print (invstr[k]);
            client.println ("</text> ");
           
            client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"360\" y=\"106\">Kw Hours</text>");
            client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"360\" y=\"163\">");
            client.print (kwhstr[k]);
            client.println ("</text> ");
           
            client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"535\" y=\"106\">Output Watts</text> ");
            client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"535\" y=\"163\">");
            client.print (watts[k]);
            client.println ("</text> ");
           
            client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"710\" y=\"106\">Output Amps</text> ");
            client.print ("<text font-size=\"45\" font-family=\"Verdana\" x=\"710\" y=\"163\">");
            client.print (oastr[k]);
            client.println ("</text> ");

            client.println ("<text font-size=\"20\" font-family=\"Verdana\" x=\"885\" y=\"106\">Temps</text> ");
            client.print ("<text font-size=\"25\" font-family=\"Verdana\" x=\"885\" y=\"141\">PCB ");
            client.print (ptpstr[k]);
            client.println ( "</text> ");
            client.print ("<text font-size=\"25\" font-family=\"Verdana\" x=\"885\" y=\"176\">FET  ");
            client.print (ftpstr[k]);
            client.println ("</text> ");

            client.println ("Sorry, your browser does not support inline SVG.");
            client.println ("</svg>");

       
          }  // for loop CLASSICS

          client.println("</body></html>");
          break;
        }
        if (c == '\n')
        {
          // starting a new line
          currentLineIsBlank = true; 
         
        }
        else
          if (c != '\r')
          {
          // character on the current line
          currentLineIsBlank = false;
          }
       }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
  }
}

Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

Westbranch

I just scanned through the documentation, 'cause I am not conversant in this code language, and I like your self talk about where it can be improved..  Good job mate! ;D ;)
tks
KID FW1811 560W >C&D 24V 900Ah AGM
CL150 29032 FW V.2126-NW2097-GP2133 175A E-Panel WBjr, 3Px4s 140W > 24V 900Ah AGM,
2 Cisco WRT54GL i/c DD-WRT Rtr, NetGr DS104Hub
Cotek ST1500 Inv  want a 24V  ROSIE Inverter
OmniCharge3024  Eu1/2/3000iGens
West Chilcotin 1680+W to come

dgd

A  programmer friend who looked at my code posted here said I should be @&%@ slapped for the comments I had inline in the  code.
She said no real self respecting programmer would include comments let alone any explanation of what the code is trying to achieve  :o
Thankfully I'm not one of those then  :-X

dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

Westbranch

That's Professional speak for
''don't tell anyone you don't know something" :o  or
''always say something that they have to come to you for an explanation, in billable hours'' :'(  or horrors " that is not what we wanted the program to do'' ::)
KID FW1811 560W >C&D 24V 900Ah AGM
CL150 29032 FW V.2126-NW2097-GP2133 175A E-Panel WBjr, 3Px4s 140W > 24V 900Ah AGM,
2 Cisco WRT54GL i/c DD-WRT Rtr, NetGr DS104Hub
Cotek ST1500 Inv  want a 24V  ROSIE Inverter
OmniCharge3024  Eu1/2/3000iGens
West Chilcotin 1680+W to come

dgd

#39
..and this is the web screen design  I'm currently testing. I have not sorted out the background image yet. This test was to get XML updates of the text box only working. Hence good values there but no XML for the meter values.
Also trying to javascript to switch meters to 12volt and 48volt versions and when the Classic model is retreived change  the amps out and the volts meters to appropriate versions. 
Nearly there and will post the full sketch for this plus the html pages to store on SD card (and various background images)

AFter this I next  want to look at a web page for configuring the Classic  8)
Where is the KID info on reading its serial port for data?

dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

Quote from: paul alting link=topic=2393.msg22951#msg22951date=1426982879
This is where the folks at Electhouse made a DUE with these pins brought out, called the Taijuino.
I was given some of these boards by Elechouse in the hope a native Ethernet library could be developed in conjunction with two other programmers.
Having such an Ethernet interface would not have the issues that the Wiznet gives, where the Wiznet is slow, it depends on SPI and has limited concurrent TCP connections sockets.
Unfortunately, to date, the DUE Ethernet library has not been completed.

Paul,
I managed to find one of these Taijiuino DUE boards complete with a programming interface card. The most interesting feature, for me, is the ethernet pins from the cpu being made available.
As you pointed out the Wiznet uses the spi interface, as does the SD card, and even with the latest IDE1.6.3 the drivers appear broken, each works well by itself but using both ethernet and SD card just does not work very well.
So question is - has there been any progress towards completing a DUE ethernet library for the Taijiuino?
The is so much program flash,512k, and ram 96k, that I can get my more advanced (analogue gauges) web interface for Classic into memory and not bother the SD storage. However the SD would be very useful for log file storage.
Or maybe I have reached the limits of the DUE and its time to get to the Cubie2  :)

dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

#41
I have almost completed the analogue meter web server for Classic and have also compiled the code on both the Arduino DUE and Arduino Mega2560. It appears that the Arduino IDE1.6.3 that I use for development now auto-sorts-out the modbus library calls so no hacking about of libs is necessary.
Still a few minor issues to tidy up but the web page works nicely, example from running web page below and both the Arduino sketch and web page to be saved on SD card are in next posting.
The Mega uses the standard rs232 shield that I used in the simple UNO web server and the lcd UNO in other message threads. This means no special soldering together of an RS232 shield as shown in an earlier posting in this thread. The special is still needed for use with the DUE because of its 3.3volt maximum interface pins The mega is 5v so is easy to use existing $12 shield.

This web server stores the web pages (html,javascript) on the SD card and the sketch on the Arduino reads the web pages and serves them to the connecting web browser.
This sketch uses AJAX to update the web page (gauge and info window) by serving XML data as requested by the browser running the web page.
This makes for a nice steady display page with no flickering as the page is not totally reloaded as data values change.
The only issue I need to work on is the abyssmal modbus throughput which means I can only refresh the gauges etc, about once every 2 seconds. I want 200ms as that is the default for the gauges.
Still no background images  :o
Image below has wrong Remaining Ah, WBjr amps greater then Output amps because there is another Classic also providing output amps

Also still impatiently waiting on the KID data extraction details, hopefully it will be less irksome than dealing with modbus RTU.
I also have this working with a Morningstar MPPT60.

Any questions, suggestions, free beer etc are very welcome  ::)

dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

#42
The web server sketch for Arduino Mega and DUE (won't fit in an UNO as it uses over 3k ram for variables, UNO=2k, Mega2560=8k, DUE=96k)
Best to use Arduino 1.6.3 IDE, I used this on Windows 8.1 ASUS laptop
SD card web page index.htm, node14.htm and node17.htm in later posts
This web server code expects to read from SD card (from micro SD card interface on Arduino ethernet shield), files named index.htm, node14.htm  node17.htm and sasa.jpg
The web server sketch is continued in next posting
This version of the sketch includes the in-memory log file processing

dgd


/*
  Web Server

A web server that shows the values of several modbus registers from Midnite
Classic. Uses the canv_gauge lib and svg to display data
Uses simplemodbusmaster library
David Dix  12 July 2015  dgd@kc.net.nz
*/
#include <SPI.h>
#include <SimpleModbusMaster.h> 
#include <Ethernet.h>
#include <SD.h>


// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network (just leave mac as it is now)
byte mac[] = {
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
IPAddress ip(192, 168, 1, 177);

// size of buffer used to capture HTTP requests
#define REQ_BUF_SZ   50

// Initialize the Ethernet server library
// with the IP address and port
// (port 80 is default for HTTP):
EthernetServer server(80);
File webFile;               // the web page file on the SD card
char HTTP_req[REQ_BUF_SZ] = {0}; // buffered HTTP request stored as null terminated string
char req_index = 0;              // index into HTTP_req buffer

// setup modbus port information
#define baud 19200
#define timeout 1000
#define polling 200 // the scan rate
#define retry_count 10
#define IDLE 1

// millis interval between switching modbus ports for different Classics
// #define CLTIME 15000

#define LOG_INTERVAL 594970    // millis time between saving 10 minute log entry 600,000 minus fine tuning

// maximum number of og records to store in memory
// keep small (48 to 96) for Mega and lots for DUE
// defaults to 10 minutely samples or a day for DUE, 12+ hours for Mega
// not yet writing these to SD file as its a bit slow
#define MAX_LOG_COUNT 48
// Daily log, keep deault 20 days in memory arrays
#define MAX_DLOG_COUNT 20

// used to toggle the receive/transmit pin - only for rs485, not used here by lib still expects it - ugh!
#define TxEnablePin 2

// up to 4 Classics connected the serial ports (Mega and DUE)
// could just leave this at 4 but would run slower if cant find all four Classics
#define CLASSICS 1

// modbus lib structures
enum
{
  PACKET1,
  PACKET2,
  PACKET3,
  PACKET4,
  PACKET5,
  TOTAL_NO_OF_PACKETS // leave this last entry
};

// Create an array of Packets to be configured
Packet packets[TOTAL_NO_OF_PACKETS];
packetPointer packet1 = &packets[PACKET1];  // pointers to access each packet
packetPointer packet2 = &packets[PACKET2];
packetPointer packet3 = &packets[PACKET3];
packetPointer packet4 = &packets[PACKET4];
packetPointer packet5 = &packets[PACKET5];

// Data read from the Classic will be stored in these arrays, using separate arrays to keep memory usage low
// each array is initialized to a packet.
// Note: serial port hardware buffer size means largest array of registers that can be obtained is 29
unsigned int readRegs[30];
unsigned int wbjrRegs[21];
unsigned int vRegs[2];
unsigned int idRegs[10];       // name 4209 to 4212, and time/date 4213 to 4216
unsigned int sernoRegs[2];

// HardwareSerial* serialPort;


int current_classic = 0, prev_classic;

  float batt_volts, batt_amps, endamps, lowvolts, highvolts, in_volts[CLASSICS], out_amps[CLASSICS];
  char  invstr[CLASSICS][8], oastr[CLASSICS][8], kwhstr[CLASSICS][6], statestr[CLASSICS][30];
  char  lastkwhr[6];
  int   cstate[CLASSICS], watts[CLASSICS], wbjr_ahr, wbjr_ra, wbjr_soc, vers[CLASSICS], model[CLASSICS], remt;
  int   high_watts[CLASSICS], ctime_secs[CLASSICS], ctime_mins[CLASSICS], ctime_hrs[CLASSICS], last_min;
  int   ctime_day[CLASSICS], ctime_mth[CLASSICS];
  int   last_day, last_month;
  int   ctime_weekday[CLASSICS], last_weekday[CLASSICS],highwatts[CLASSICS];
  char  ftpstr[CLASSICS][8], ptpstr[CLASSICS][8], peakpstr[CLASSICS][6];
  char  Classic_name[CLASSICS][10], bvstr[8], wbastr[8], btpstr[8], rtpstr[8], ctimestr[CLASSICS][10];
  int   eq_hrs[CLASSICS], eq_mins[CLASSICS], eq_secs[CLASSICS];
  int   fl_hrs[CLASSICS], fl_mins[CLASSICS], fl_secs[CLASSICS];
  int   abs_hrs[CLASSICS], abs_mins[CLASSICS], abs_secs[CLASSICS];
  int   serno[CLASSICS];
  unsigned long start_count, timenow;
  int   log_index=0, last_index=0, next_mins=0;
 
  // 10 minute log file arrays (should eventually write to SD card)
  int   log_mins[MAX_LOG_COUNT], log_hrs[MAX_LOG_COUNT];   // for record time stamp, no date yet
  float log_battvolts[MAX_LOG_COUNT], log_battamps[MAX_LOG_COUNT];     
  int   log_watts[MAX_LOG_COUNT], log_state[MAX_LOG_COUNT];       
  float log_involts[MAX_LOG_COUNT], log_outamps[MAX_LOG_COUNT];
  int   log_ahremain[MAX_LOG_COUNT];
  char  log_kwh[MAX_LOG_COUNT][6];
  int   log_ahr[MAX_LOG_COUNT], log_soc[MAX_LOG_COUNT];   // Soc to 100%
 
  int   dlog_index=0, dlast_index=0;
  // Day log file in arrays max 20 entries
  int   dlog_mth[MAX_DLOG_COUNT], dlog_day[MAX_DLOG_COUNT];
  char  dlog_kwh[MAX_DLOG_COUNT][6];
  int   dlog_abhr[MAX_DLOG_COUNT], dlog_abmin[MAX_DLOG_COUNT], dlog_flhr[MAX_DLOG_COUNT];
  int   dlog_flmin[MAX_DLOG_COUNT];
  float dlog_lbv[MAX_DLOG_COUNT], dlog_hbv[MAX_DLOG_COUNT], dlog_ea[MAX_DLOG_COUNT];
  int   dlog_hwts[MAX_DLOG_COUNT];
  int   dlog_hsoc[MAX_DLOG_COUNT], dlog_lsoc[MAX_DLOG_COUNT];
  int   dlog_ham[MAX_DLOG_COUNT], dlog_lam[MAX_DLOG_COUNT];
  int   dlog_hahr[MAX_DLOG_COUNT], dlog_lahr[MAX_DLOG_COUNT];
 
  int   high_soc=0, low_soc=0;       
  int   high_ahr=0, low_ahr=0;     
  int   high_ra=0, low_ra=0;
  int   float_hrs, float_mins, absorb_hrs, absorb_mins;
   
void setup()
{
  int  i,j, k;
    // disable Ethernet chip
    pinMode(10, OUTPUT);
    digitalWrite(10, HIGH);

//    Serial.begin(9600);       // for debugging
   
    // initialize SD card
//    Serial.println("Initializing SD card...");
    if (!SD.begin(4)) {
//      Serial.println("ERROR - SD card initialization failed!");
        return;    // init failed
    }
//    Serial.println("SUCCESS - SD card initialized.");
    // check for index.htm file
    if (!SD.exists("index.htm")) {
//        Serial.println("ERROR - Can't find index.htm file!");
        return;  // can't find index file
    }
//    Serial.println("SUCCESS - Found index.htm file.");
   
     // start the Ethernet connection and the server:
    Ethernet.begin(mac, ip);
    server.begin();

   
  modbus_construct(packet1, 10, READ_HOLDING_REGISTERS, 4114, 29, readRegs);  // at max 29
  modbus_construct(packet2, 10, READ_HOLDING_REGISTERS, 4360, 20, wbjrRegs);
  modbus_construct(packet3, 10, READ_HOLDING_REGISTERS, 4100, 1, vRegs);
  modbus_construct(packet4, 10, READ_HOLDING_REGISTERS, 4209, 9, idRegs);
  modbus_construct(packet5, 10, READ_HOLDING_REGISTERS, 0x7000, 2, sernoRegs);
 
   // start modbus input from port Serial1 on DUE/ Serial on Mega
   start_count = millis();
   modbus_configure(&Serial, baud, timeout, polling, retry_count, TxEnablePin, packets, TOTAL_NO_OF_PACKETS);
}


/*  This function reads the modbus registers from Classic using modbus_update/
    It then unpacks the register unsigned int values into appropriate global floats, ints and character
    arrays. In character arrays decimal points are inserted where needed and leading zero prepended on
    fraction numbers in preparation for displaying in html page.
*/
void read_registers()
{
     int  i,j,k, amps;
     char temp[7];
     
     modbus_update();   // the modbus lib state machine :-)
//   delay(10);
   
     k = current_classic;
   
  // make Classic register values ready for web page
     model[k] = (unsigned int)lowByte(vRegs[0]);
     serno[k] = sernoRegs[1];
     Classic_name[k][0] = lowByte (idRegs[0]);
     Classic_name[k][1] = highByte(idRegs[0]);
     Classic_name[k][2] = lowByte (idRegs[1]);
     Classic_name[k][3] = highByte(idRegs[1]);
     Classic_name[k][4] = lowByte (idRegs[2]);
     Classic_name[k][5] = highByte(idRegs[2]);
     Classic_name[k][6] = lowByte (idRegs[3]);
     Classic_name[k][7] = highByte(idRegs[3]);
     Classic_name[k][8] = 0x00;
     
   // time registers at address 4213,  secs and mins, 4214 hours and weekday
   // bit pattern 4213  ..mmmmmm..ssssss and 4214  .....ddd...hhhhh
   // casting to unsigned int necessary to prevent right shifting leftmost 1 results in negative number
     ctime_secs[k] = idRegs[4] << 10;
     ctime_secs[k] =  (unsigned int)ctime_secs[k] >> 10; 
     ctime_mins[k] = idRegs[4] << 2;
     ctime_mins[k] = (unsigned int) ctime_mins[k] >> 10;
     ctime_hrs[k] = idRegs[5] << 11;
     ctime_hrs[k] = (unsigned int) ctime_hrs[k] >> 11;
     ctime_weekday[k] = idRegs[5] << 5;
     ctime_weekday[k] = (unsigned int) ctime_weekday[k] >> 13;
     ctime_day[k] = idRegs[6] << 11;
     ctime_day[k] = (unsigned int)ctime_day[k] >> 11;
     ctime_mth[k] = idRegs[6] << 4;
     ctime_mth[k] = (unsigned int) ctime_mth[k] >> 12;
 
  // Battery volts in float and string   
     batt_volts = readRegs[0];
     batt_volts /= 10;
     
     // record high/low battery voltages for day log
     if (batt_volts > highvolts ) highvolts = batt_volts;
     if (lowvolts == 0 || batt_volts < lowvolts ) lowvolts = batt_volts;
     
     itoa (readRegs[0], bvstr, 10);
     i=strlen(bvstr);
     bvstr[i] = bvstr[i-1];
     bvstr[i-1] = '.';
     bvstr[i+1] = '\0';

  // Input volts in float and string
     in_volts[k] = readRegs[1];
     in_volts[k] /= 10;   
     itoa (readRegs[1], invstr[k], 10);   // input voltage
     i = strlen (invstr[k]);
     invstr[k][i] = invstr[k][i-1];
     invstr[k][i-1] = '.';
     invstr[k][i+1] = '\0';

  // Output amps in float and string (note NOT wbjr amps but amps from Classic)   
     out_amps[k] = readRegs[2];
     out_amps[k] /=10;
     itoa (readRegs[2], oastr[k], 10);    // output amps
     i = strlen (oastr[k]);
     if ( i==1 )                          // insert leading zero if value < 1
     {
       oastr[k][1]=oastr[k][0];
       oastr[k][0] = '0';
       i++;
     }
     oastr[k][i] = oastr[k][i-1];
     oastr[k][i-1] = '.';
     oastr[k][i+1] = '\0';

  // Kwh as string
     itoa (readRegs[3], kwhstr[k], 10);   // kwh
     i = strlen (kwhstr[k]);
     if (i==1)              // insert leading zero if < 0
     {     kwhstr[k][1] = kwhstr[k][0];
           kwhstr[k][0] = '0';
           i++;
     }
     kwhstr[k][i] = kwhstr[k][i-1];
     kwhstr[k][i-1] = '.';
     kwhstr[k][i+1] = '\0';
 
  // Create string output line with day's peak Watts reading and timestamp it.
     watts[k] = readRegs[4];
     if (highwatts[k] < watts[k] )
     {
        highwatts[k] = watts[k];
        itoa (highwatts[k], peakpstr[k], 10);    // peak wattage from Classic today
       
        itoa(ctime_hrs[k], temp, 10);            // save time of peak power
        strcpy ( ctimestr[k], temp );            // build string ctimestr by writing ctime hours then
                                                 // ctime minutes then ctime seconds separted by a :
        itoa (ctime_mins[k], temp, 10);          // if mins or secs re single digit then insert leading zero
        i = strlen(temp);                        // to get  hh:mm:ss or h:mm:ss (not fussed about leading
        if (i==1)                                // zero on hours part
        {
            temp[2] = temp[0];
            temp[0] = ':';
            temp[1] = '0';
         }
         else
         {
            temp[2] = temp[1];
            temp[1] = temp[0];
            temp[0] = ':';
          }
          temp[3] = '\0';
          strcat (ctimestr[k], temp);   
         
         itoa (ctime_secs[k], temp, 10);
         i = strlen(temp);
         if (i==1)
         {
            temp[2] = temp[0];
            temp[0] = ':';
            temp[1] = '0';
         }
         else
         {
            temp[2] = temp[1];
            temp[1] = temp[0];
            temp[0] = ':';
          }
          temp[3] = '\0';
          strcat (ctimestr[k], temp);     // now have string ctimestr for displaying

     }

  // Charge state   
     cstate[k] = (unsigned int)readRegs[5] >> 8;  // high byte contains charge state code
     
  // Battery temperature string in degrees C (need to consider degrees F option here)   
     if (readRegs[17] > 100) // if its a crazy over 100 deg C measurement then likely no BTS present
     {
         btpstr[0] = '2';   // set to default 25c
         btpstr[1] = '5';
         btpstr[2] = '\0'; 
     }
     else
     {
         itoa (readRegs[17], btpstr, 10);    // need to properly test if BTS present
         i = strlen(btpstr);
         btpstr[i] = btpstr[i-1];
         btpstr[i-1] = '.';
         btpstr[i+1] = '\0';
     }
 
  // FET temperature string   
     itoa (readRegs[18], ftpstr[k], 10);
     i = strlen (ftpstr[k]);
     ftpstr[k][i] = ftpstr[k][i-1];
     ftpstr[k][i-1] = '.';
     ftpstr[k][i+1] = '\0';
     
  // PCB temperature string
     itoa (readRegs[19], ptpstr[k], 10);
     i = strlen (ptpstr[k]);
     ptpstr[k][i] = ptpstr[k][i-1];
     ptpstr[k][i-1] = '.';
     ptpstr[k][i+1] = '\0';

  // string for total float time in hh:mm:ss or h:mm:ss  inserting leading zero in mm and ss if needed
     fl_secs[k] = readRegs[23];      //4137 floatTimeofdayseconds
     if (fl_secs[k] >= 3600)
     { fl_hrs[k] = (int)fl_secs[k]/3600;
       fl_secs[k] -= (int) fl_hrs[k]*3600;
     }
     else fl_hrs[k] = 0;
     if (fl_secs[k] >= 60 )
     { fl_mins[k] = (int)fl_secs[k]/60;
       fl_secs[k] -= (int) fl_mins[k]*60;
     }
     else fl_mins[k]=0;
 
   // string for total Equalise time hh:mm:ss or h:mm:ss  inserting leading zero in mm and ss if needed       
     eq_secs[k] = readRegs[28];      //4142 EqualizeTime
     if (eq_secs[k] >= 3600)
     {
       eq_hrs[k] = (int)eq_secs[k]/3600;
       eq_secs[k] -= (int) eq_hrs[k]*3600;
     }
     else eq_hrs[k] = 0;
     if (eq_secs[k] >= 60 )
     { eq_mins[k] = (int)eq_secs[k]/60;
       eq_secs[k] -= (int) eq_mins[k]*60;
     }
     else eq_mins[k]=0;
     
   // string for Absorb time left hh:mm:ss or h:mm:ss inserting leading zero in mm and ss if needed
     abs_secs[k] = readRegs[24];      //4138 AbsorbTime
     if (abs_secs[k] >= 3600)
     {
       abs_hrs[k] = (int)abs_secs[k]/3600;
       abs_secs[k] -= (int) abs_hrs[k]*3600;
     }
     else abs_hrs[k] = 0;
     if (abs_secs[k] >= 60 )
     {
       abs_mins[k] = (int)abs_secs[k]/60;
       abs_secs[k] -= (int) abs_mins[k]*60;
     }
     else abs_mins[k] = 0;
     
   // set up state string 
     switch (cstate[k])
     {
            case 0:
              strcpy( statestr[k], "RESTING");
              break;
            case 3:
              strcpy ( statestr[k], "ABSORB - time left ");
              itoa(abs_hrs[k], temp, 10);
              strcat ( statestr[k], temp );
                     
              itoa (abs_mins[k], temp, 10);
              i = strlen(temp);
              if (i==1)
              {
                temp[2] = temp[0];
                temp[0] = ':';
                temp[1] = '0';
              }
              else
              {
                temp[2] = temp[1];
                temp[1] = temp[0];
                temp[0] = ':';
              }
              temp[3] = '\0';
              strcat (statestr[k], temp);     
           
              itoa(abs_secs[k], temp, 10);
              i = strlen(temp);
              if (i==1)
              {
                temp[2] = temp[0];
                temp[0] = ':';
                temp[1] = '0';
              }
              else
              {
                temp[2] = temp[1];
                temp[1] = temp[0];
                temp[0] = ':';
              }
              temp[3] = '\0';

              strcat (statestr[k], temp);
              break;
            case 4:
              strcpy (statestr[k], "BULKMPPT");
              break;
            case 5:
              strcpy ( statestr[k], "FLOAT - Total time ");
              itoa(fl_hrs[k], temp, 10);
              strcat ( statestr[k], temp );
              itoa (fl_mins[k], temp, 10);
              i = strlen(temp);
              if (i==1)
              {
                temp[2] = temp[0];
                temp[0] = ':';
                temp[1] = '0';
              }
              else
              {
                temp[2] = temp[1];
                temp[1] = temp[0];
                temp[0] = ':';
              }
              temp[3] = '\0';
              strcat (statestr[k], temp);     
           
              itoa(fl_secs[k], temp, 10);
              i = strlen(temp);
              if (i==1)
              {
                temp[2] = temp[0];
                temp[0] = ':';
                temp[1] = '0';
              }
              else
              {
                temp[2] = temp[1];
                temp[1] = temp[0];
                temp[0] = ':';
              }
              temp[3] = '\0';
              strcat (statestr[k], temp);
              break;
 
            case 6:
              strcpy ( statestr[k], "FLOAT MPPT");
              break;
            case 7:
              strcpy ( statestr[k], "EQUALIZE - Time left ");
              itoa(eq_hrs[k], temp, 10);
              strcat ( statestr[k], temp );
                     
              itoa (eq_mins[k], temp, 10);
              i = strlen(temp);
              if (i==1)
              {
                temp[2] = temp[0];
                temp[0] = ':';
                temp[1] = '0';
              }
              else
              {
                temp[2] = temp[1];
                temp[1] = temp[0];
                temp[0] = ':';
              }
              temp[3] = '\0';
             
              strcat (statestr[k], temp);     
           
              itoa(eq_secs[k], temp, 10);
              i = strlen(temp);
              if (i==1)
              {
                temp[2] = temp[0];
                temp[0] = ':';
                temp[1] = '0';
              }
              else
              {
                temp[2] = temp[1];
                temp[1] = temp[0];
                temp[0] = ':';
              }
              temp[3] = '\0';

              strcat (statestr[k], temp);
              break;
             case 10:
              strcpy ( statestr[k],"HYPERVOC");
              break;
            case 18:
              strcpy ( statestr[k],"EQ MPPT");
              break;
     }

 
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

#43
Arduino sketch continued Part 2 (was too big for one posting), Part 3 is in later posting
All 3 parts need o be extracted and joined to make the complete Arduino sketch


   // WBjr registers

  // wbjr remote temperature
     remt = (int) lowByte (wbjrRegs[11]);
     remt -=50;
     if  (remt != -50 )  // there is WBjr connected
     {
        itoa (remt, rtpstr, 10);
       
    // wbjr battery amps minus value = discharging, positive = charging, save as float and string
        amps = (signed int)wbjrRegs[10];   // cast doesn't seem to work
        // wbjrRegs is unsigned int so need to make signed int before itoa conversion
        if (amps > 60000) amps -=65535;
        batt_amps = (float) amps/10;      // float  battery amps
        if (cstate[k] == 3 ) endamps = batt_amps;  // ABSORB so set end amps
        itoa (amps, wbastr, 10);          // string battery amps
        i = strlen(wbastr);
          // need to insert leading zero if <1 and > -1,  eg .3 becomes 0.3, -.2 becomes -0.2
        if (i==1 || (i==2 && wbastr[0] == '-') )
        { 
            wbastr[i] = wbastr[i-1];
            wbastr[i-1] = '0';
            i++;
        }   
        wbastr[i] = wbastr[i-1];   // now divide by 10 by inserting decimal point
        wbastr[i-1] = '.';     
        wbastr[i+1] = '\0';
       
      // wbjr battery state of charge %
        wbjr_soc = wbjrRegs[12];          // 4372
       
        // record high/low SOC % for day log
       if (wbjr_soc > high_soc ) high_soc = wbjr_soc;
       if (low_soc == 0 || wbjr_soc < low_soc ) low_soc = wbjr_soc;
       
      // wbjr net Amp hours charge
        wbjr_ahr = wbjrRegs[8];           // Net Ah
        // record high/low net amp  hours for day log
       if (wbjr_ahr > high_ahr ) high_ahr = wbjr_ahr;
       if (low_ahr == 0 || wbjr_ahr < low_ahr ) low_ahr = wbjr_ahr;
       
      // wbjr Amp hours left in battery
        wbjr_ra = wbjrRegs[16];           // Remining Ah
        // record high/low remaining amp hours for day log
        if (wbjr_ra > high_ra ) high_ra = wbjr_ra;
        if (low_ra == 0 || wbjr_ra < low_ra ) low_ra = wbjr_ra;

     }
   
     //  save a new 10 minute log file record in memory arrays 
     //  log records are stored in a set of memory arrays, enough for MAX_LOG_COUNT entries with time
     //  of 10 minutes between each  The arrays are cyclic and a log_index is
     //  kept of where the last log record is stored. They are eventually display starting at latest and
     //  going backwards down dispay screen 
     timenow = millis();
     // millis may have cycled back to zero after 50 days so reset start_count as well
     if (timenow < start_count) start_count = 0;  // so there may be one 10min+ time gap
     if ((timenow - start_count) > (unsigned long)LOG_INTERVAL)
     {
         start_count = timenow;
         if (log_index >= MAX_LOG_COUNT ) log_index=0;  // back to start of log array
         log_mins[log_index] = ctime_mins[k];         
         log_hrs[log_index] = ctime_hrs[k];
         
         // now store all current data items
         log_battvolts[log_index] = batt_volts;
         log_battamps[log_index] = batt_amps;     
         log_watts[log_index] = watts[k];
         log_state[log_index] = cstate[k];       
         log_involts[log_index] = in_volts[k];
         log_outamps[log_index] = out_amps[k];
         log_soc[log_index] = wbjr_soc;
         log_ahremain[log_index] = wbjr_ra;
         strcpy (log_kwh[log_index], kwhstr[k]);
         strcpy (lastkwhr, kwhstr[k]);  // for the day log report
         log_ahr[log_index] = wbjr_ahr;
         last_index = log_index;
         log_index++;
       // end of writing new log record
       // now save last KWHr string, day and month, for Day log record
       // (because Classic clears Kwh to zero at midnite and day/month moves forward
         if (last_day == 0 )last_day = ctime_day[k];
         if (last_month == 0) last_month = ctime_mth[k];
         if (ctime_hrs[k] > 0 ) // so as not to clear them when Classic reset times at midnie
         {                      // saved float/absorb times will reset here at 1am
             float_hrs = fl_hrs[k];        // save float time for day log           
             float_mins = fl_mins[k];
             absorb_hrs = 1 - abs_hrs[k];       // for 2 hour max time absorb           
             absorb_mins = 60 - abs_mins[k];    // need to revisit this for EA terminated absorb
             if (absorb_mins == 60)             // and different than 2hr abs time settings
             {
               absorb_mins=0;
               absorb_hrs++;
             }

         }

     // daylog time testing done within 10 minute log time logic to reduce unnecessary
     // testing in main loop     
     // save a day log entry for yesterday just after midnight and reset highwatts
     // value/timestamp.
         if ( ctime_hrs[k] == 0 && ctime_mins[k] < 10 )   // rolled over to next day
         {       
            dlog_day[dlog_index] = last_day;     // the day of the month 1-31   
            dlog_mth[dlog_index] = last_month;     // the month of the year 1-12   
         // now store day data items
            dlog_ea[dlog_index] = endamps;            // the EA vale of last ABSORB state
            strcpy (dlog_kwh[dlog_index], lastkwhr);   // day's kwh reading
            dlog_hwts[dlog_index] = highwatts[k];
            dlog_hbv[dlog_index] = highvolts;
            dlog_lbv[dlog_index] = lowvolts;
            dlog_hsoc[dlog_index] = high_soc;
            dlog_lsoc[dlog_index] = low_soc;       
            dlog_ham[dlog_index] = high_ra;
            dlog_lam[dlog_index] = low_ra;     
            dlog_hahr[dlog_index] = high_ahr;
            dlog_lahr[dlog_index] = low_ahr;
            dlog_flhr[dlog_index] = float_hrs;        // save float/absorb times for day log           
            dlog_flmin[dlog_index] = float_mins;
            dlog_abhr[dlog_index] = absorb_hrs;                 
            dlog_abmin[dlog_index] = absorb_mins;

         // so clear highwatts value and its datestamp
            last_weekday[k] = ctime_weekday[k];
            highwatts[k] = 0;
            ctimestr[k][0] = '\0';                  // clear by nulling the string 
         // move array pointer dlog_index forward, roll back to zero if too large and reset variables
            dlast_index = dlog_index;
            dlog_index++;                             // for next dlog entry in array
            if (dlog_index >= MAX_DLOG_COUNT ) dlog_index=0;  // loop back to start of day log array
            endamps = 0;
            lowvolts = 0;
            highvolts = 0;
            low_soc = high_soc = 0;       
            high_ahr = low_ahr = 0;     
            high_ra = low_ra = 0;
            last_day = last_month = 0;
         } // end of day logging
     }
     //end of 10min and day logging
     
} // thats all the modbus registers moved and log file entries completed


void loop()
{
  char  c;
  int   i,j,k, amps;

/*
   Use millis() time counting for changing between Classics being processed by the modbus state machine.
   Each change just pokes the harware serial parameter and re-executes modbus_configure/modbus_update.
   Not perfect but seems to work, may take an iteration or three to get the correct readings in each CLassic's
   display panel, modbus ugly timing is the problem.
   Proper solution is to rewrite less sophisticated modbus lib and execute one modbus manager for each
   serial port - using the DUE scheduler would work well for this method     
     
*/
   prev_classic = current_classic;

/* for Analogue gauge displays only one Classic is processed on Serial port 0 and CLASSICS=1 defined at top
   Code for processing up to 4 Classics/KIDS left here for future design (and its more bother to remove it)
*/
/*
   // Move to next serial port for input if time counter used up ie swap to next Classic
   timenow = millis();
   if ((timenow - start_count) > (unsigned long)CLTIME)
   {
     start_count = timenow;
     current_classic++;
     if (current_classic >= CLASSICS) current_classic = 0;
     switch (current_classic)
     {
       case 0:                        // serial port order could be changed if desired
          serialPort = &Serial;      // eg if there is only ever one Classic on &Serial then make it first
          break;
       case 1:
          serialPort = &Serial2;
          break;
       case 2:
          serialPort = &Serial3;
          break;
       case 3:
          serialPort = &Serial1;     // hardware serial 0
          break;
     }
     if (current_classic != prev_classic )  // if only one classic then don't reconfigure
     {
       modbus_configure(serialPort, baud, timeout, polling, retry_count, TxEnablePin, packets, TOTAL_NO_OF_PACKETS);
       modbus_update();  // to start the reconfigure
//       delay(10);
     }
   }
*/
  read_registers();  //does modbus_update, converts and stores register values
 
  // listen for incoming clients
      EthernetClient client = server.available();  // try to get client

    if (client)
    {  // got client?
        boolean currentLineIsBlank = true;
        while (client.connected())
        {
            read_registers();
            if (client.available())
            {   // client data available to read
                char c = client.read(); // read 1 byte (character) from client
                // buffer first part of HTTP request in HTTP_req array (string)
                // leave last element in array as 0 to null terminate string (REQ_BUF_SZ - 1)
                if (req_index < (REQ_BUF_SZ - 1)) {
                    HTTP_req[req_index] = c;          // save HTTP request character
                    req_index++;
                }
                // last line of client request is blank and ends with \n
                // respond to client only after last line received
                if (c == '\n' && currentLineIsBlank)
                {
                    // send a standard http response header
                    client.println(F("HTTP/1.1 200 OK"));
                    // remainder of header follows below, depending on if
                    // web page or XML page is requested
                    // Ajax request - send appropriate XML file
                    if (StrContains(HTTP_req, "ajax_inputs") || StrContains(HTTP_req, "log"))
                    {
                        // send rest of HTTP header
                        client.println(F("Content-Type: text/xml"));
                        client.println(F("Connection: keep-alive"));
                        client.println(F("<?xml version = \"1.0\" ?>"));
                        client.println();
                        if (StrContains(HTTP_req, "ajax_inputs"))
                           XML_response(client);  // 1sec data XML for Canv gauges
                        else
                        {
                          if (StrContains(HTTP_req, "CLlog")) // send 10min log XML file
                             XML_CLlog(client);
                          else  // send day log XML file
                             XML_Daylog(client);
                        }
                    }
                    else
                    {  // web page or image request
                           if (StrContains(HTTP_req, "GET / ")
                                 || StrContains(HTTP_req, "GET /index.htm")
                                 || StrContains(HTTP_req, "node14.htm")
                                 || StrContains(HTTP_req, "node17.htm"))
                           {
                             // send rest of HTTP header
                             client.println(F("Content-Type: text/html"));
                             client.println(F("Connection: keep-alive"));
                             client.println();
                            // send web page
                             if (StrContains(HTTP_req, "node14.htm"))
                                webFile = SD.open("node14.htm");
                             else
                             {  if (StrContains(HTTP_req, "node17.htm"))
                                   webFile = SD.open("node17.htm");
                                else
                                   webFile = SD.open("index.htm");        // open web page file
                             }
                             if (webFile)
                             {
                               while(webFile.available())
                               {
                                client.write(webFile.read()); // send web page to client
                               }
                              webFile.close();
                             }
                           }
                           else
                           {
                             if (StrContains(HTTP_req, "GET /sasa.jpg"))
                             {
                               client.println(F("Content-Type: image/jpeg"));
                               client.println();
                               webFile = SD.open("sasa.jpg");
                               if (webFile)
                               {
                                 byte clientBuf[64];
                                 int clientCount = 0;

                                 while(webFile.available())
                                 {
                                   clientBuf[clientCount] = webFile.read();
                                   clientCount++;
                                   if(clientCount > 63)
                                   {
                                     // Serial.println("Packet");
                                     client.write(clientBuf,64);
                                     clientCount = 0;
                                   }
                                 }  // webfile available
                               //final <64 byte cleanup packet
                                 if(clientCount > 0) client.write(clientBuf,clientCount);           
                               // close the file:
                                 webFile.close();
                               } // if webfile
                             } // if sasa.jpg
                           } //else
                       
                    }
                    // display received HTTP request on serial port (debugging only)
                    // reset buffer index and all buffer elements to 0
                    req_index = 0;
                    StrClear(HTTP_req, REQ_BUF_SZ);
                    break;
                }
                // every line of text received from the client ends with \r\n
                if (c == '\n')
                {
                    // last character on line of received text
                    // starting new line with next character read
                    currentLineIsBlank = true;
                }
                else if (c != '\r') {
                    // a text character was received from client
                    currentLineIsBlank = false;
                }
            } // end if (client.available())
        } // end while (client.connected())
        delay(1);      // give the web browser time to receive the data
        client.stop(); // close the connection
    } // end if (client)
}

Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand

dgd

#44
The next development of this gauges web page is to have the gauge setup automated where possible.
The input voltage range set to match the Classic model, the battery voltage range to match 12,24 or 48 battery bank voltage and dynamic adjusting of the WBjr amps range and the watts input range.
The index.htm web page listed above just defines the gauges in html code but a better option is in JavaScript api so that a complete gauge can be redrawn as needed.

Now also have some nice background graphics and for the KID version have used a nice semi transparent image of a black KID with a dimly glowing blue lcd and LEDs. These don't do anything as the gauges simply overlay the image although the Kid by Mario scripted text shows through nicely.
Too much fun messing with images, need to get to the Cubie2  8)

Dgd
Classic 250, 150,  20 140w, 6 250w PVs, 2Kw turbine, MN ac Clipper, Epanel/MNdc, Trace SW3024E (1997), Century 1050Ah 24V FLA (1999). Arduino power monitoring and web server.  Off grid since 4/2000
West Auckland, New Zealand