Monday, May 25, 2020

RV Solar Tech: Using the data from a Bogart Trimetric TM-2030 to use excess solar power to power a refrigerator

PURPOSE

I have a 2015 Winnebago 26A Brave, which is a 26-foot Class A Recreational Vehicle (RV). The RV has been fitted with 300 Watts of solar panels connected to a Bogart Engineering SC-2030 PWM solar charge controller to charge the house battery, and a Bogart TM-2030 system monitor which reports net current, state of charge, and voltage among other things. The house battery comprises a pair of Trojan T-105 6 V batteries wired in series.

When we camp, the batteries are usually fully charged by about noon, and we don't use much electricity during the day. Therefore, the solar energy the panels generate in the afternoon used to be wasted. The RV is equipped with a small refrigerator that can run off either electricity or propane. Normally, when not connected to "shore" power (i.e. 120 Volt main power), the fridge runs on propane. This Arduino project utilizes the formerly wasted solar power after the batteries are charged to run the fridge, thus saving propane. It does this by turning on a relay when there is excess power, and the relay energizes a 120 VAC outlet where the fridge is plugged in. When the outlet is energized, the fridge automatically switches from propane to AC.

I have a cheap 700 W non-sine inverter from Radio Shack (R.I.P.) that powers this outlet, and the inverter is on continually when using the RV, and with no load, it draws about 0.5 A at 12 VDC, (6 W) which is acceptable. I could put the relay in the DC supply to the inverter, but to match the capacity of the inverter, it would need to be a 700/12 = 60 Amp relay, while cheap relays such as I use here only drive up to 10 amps.

When the fridge is running on AC, it draws about 270 Watts, so only in ideal conditions will the 300 Watt solar panels "keep up" with the added load continuously, so the relay cycles on and off when in use.

Solar is only one way the house batteries are charged: the batteries also charge when the engine is running or when plugged in, or if the built-in generator is running. A side benefit to this project is that the fridge runs off electricity when the coach engine is running (at least, once the house batteries are charged), so turning off the propane to increase safety on the road, as many RV operators do, won't disable the fridge.

HOW IT WORKS

The TM-2030 has a TTL-level serial port that continually sends out charging data at 2400 baud. Based on what I've seen on the internet from EorEquis and others, it appears that the data is not completely consistent from one unit to another, but the main data needed for my purposes are Voltage and State of Charge (SOC).

The format of the data is par1=val1,par2=val2,par3=val3 where parN is "parameter" N, and valN is value N. For example V=12.4,%=98,A=1.0 which means that voltage is 12.4, SOC is 98, and we have a net charge (charge current – load current) of 1 Amp. There is no white space, and every field is delimited by a comma.

Voltage while charging is typically over 13 volts, so, even if the batteries are full, I don't want to run the system when the batteries are not being charged, or when the SOC is less than 98%. The relay turns on until the SOC drops below 95%, then shuts off and lets the batteries recover to 98%. These limits may change as I get more experience with the system.

I am worried that there could be a condition where the relay is turning on and off repeatedly, and wanted to avoid that. Therefore, I put in some timing logic so that after the relay turns off, it won't turn on again for at least 10 minutes. This shouldn't be needed. As a side effect of this logic, the relay won't activate until the controller has been powered up at least 10 minutes.

I saw Gordon Boulton's (aka EorEquis, https://TristarObservatory.Space) blog post and code to do a similar task on Github, which used a basic Arduino (Uno?) and grabbed the serial data using the SoftwareSerial library. However, based on a comment in his code, it appears his code doesn't actually work. I started out using a Pololu A-Star Micro which has a 32U4 chip, and was unable to read the serial data using the SoftwareSerial library. I wanted to try a hardware serial port, and I don't know how to use the one port on the 32U4 for user needs, (I think it's possible, but didn't want to deal with it) so I switched to a Teensy 3.2, which has 4 serial ports, 3 of which are available for the user. I suspect an Arduino Mega would work too. Using a hardware port, collecting the data was easy-peasy.

The moral of that story is, if you have to collect serial data, use a serial port, imho. I think SoftwareSerial might be ok for sending data, but for collecting a continuous stream of data even at 2400 baud, forget it.

The main "loop" checks if there is anything waiting at the serial port, and, if so, puts the character into a buffer to be parsed later. When a comma is detected, the program attempts to parse the contents of the buffer, determining the parameter and the value. If it's something I want to process, I store it in a structure, and check the criteria as described in the preceding paragraph, and see if I need to change the status of the relay, which is driven by a single digital output. I wrote the logic in such a way that the program discards things that are erroneous, that it doesn't recognize, or that it doesn't care about, which should make it reliable – I hope.

If you dig into my code, you should know that I've done quite a bit of C programming, so the code uses some cool features of C that, for me, increase clarity of code function. This is only my second Arduino project though, so some of the Arduino stuff might not be the best way to do things.

HARDWARE

I did take some care in building the relay box, as I don't want anyone to get shocked, and I also don't want any fires. I used two steel electric utility boxes from Home Depot bolted together back-to-back. Side-to-side would work fine too. One box has the outlet, and the other has a metal cover which I drilled to accommodate standoffs. I attached the relay to the cover because I wanted to be able to remove the cover and get to the terminals easily. A standard 3-prong plug is plugged into the inverter, and leads into the boxes. The ground and neutral are led directly to the plug, while the "hot" lead goes to the common terminal of the relay. The "normally open" side of the relay then goes to the hot side of the socket.

Be super careful messing with 120 VAC. Make super sure that everything is wired correctly before plugging into the wall, and I recommend making the connections inside a relatively fireproof container, which is the reason I used the steel utility boxes. Mistakes with AC power hurt and can cause fires.

The serial port on the back of the TM-2030 uses an RJ-11 socket with a phone extension cable that runs down to the SC-2030. I put an RJ-11 two-line splitter from Ace Hardware into that socket. Note that not all such splitters are created equal -- I got one that didn't give me all the connections in the same place. I use an RJ-11 wall socket with easy-to-access terminals to get to the individual wires. I don't have the patience or skill to strip and solder phone wires, and I didn't have to. The wall socket has 4 connections, which are 5v, ground, data, and something else, I don't know what. Based on wire tracing, the wires I thought would carry the data were wrong, so I used an oscilliscope to find which pair was the correct pair. Am not sure whether Bogart's documentation is wrong, or, more likely, it's hard to trace RJ-ll wires. Again, my patience and skill for dealing with tiny wires is lacking.

The relay is powered from the same 5 Volt USB power supply which powers the Teensy. The USB power supply is plugged into a second outlet powered by the same 700 W inverter. The grounds of the signal line from the SC-2030, the Teensy, and the relay are all wired together. The relay is active when the signal line is pulled LOW, because if the wire breaks or the Teensy dies or whatever, I want the relay to turn OFF. An open circuit should give a HIGH signal.

The Teensy's digital lines are only 3.3 Volt, while the relay I used is 5 volt. The 3.3 volt signal is sufficient to operate the relay.

CONCLUSION

The program parses more data than it needs, and it prints it out to the Serial port occasionally. A future project may be to put these values on a display, and maybe even a time history graph.

I'm not sure how much propane will be saved by this project, but I really like the idea of wasting less of my excess solar energy. The project was fun and educational and maybe helps save a few bucks in the long run.


Here's the Arduino code:

//  *************************************************************************************************************
//  *  Excess Solar Power Utilization Project
//  *  Feb  2019
//  *  Works on Teensy 3.2
//  *  Chris Hill
//  *  Collects, uses and displays data continuously from TM-2030
//  *  Turns on a relay to add load to a solar system that is fully charged, e.g. a propane/electric refrigerator
//  *************************************************************************************************************

  // **********************************************************************************************************
  //
  // The comments documenting the data stream are from Gordon Boulton, but the code below that is all my mess.
  // 
  // **********************************************************************************************************
  //  V = Volts (Batt 1)  *Both
  //  FV = Filtered Volts (Batt 1)  *Both
  //  V2 = Volts (Batt 2) *Both
  //  A = Amps  * Both
  //  FA = Filtered Amps (Batt 1) *Both
  //  PW = Charge controller pulse width (First number is charge state, then is hex number defineing pulse width from 0-FF hex.)  *2030
          // Charge State Values
          // 0 - Discharging, < 98% Full : See section 4.5.3 of http://www.bogartengineering.com/sites/default/files/docs/SC2030-9-10-15-UsersInstruc.pdf
          // 1 - Bulk Charge
          // 2 - Absorb
          // 3 - Float
          // 4 - Finish
          // 5 - Max Voltage Finish
          // 6 - Was in float, now discharging, still > 98% Full 
  //  AH = Amp Hours From Full  * Both
  //  T% = State of charge  * Both << CJH  FOR ME, I only see a % symbol for State of Charge, not T%
  //  W = Watts * Both
  //  DSC = Days Since Charged  * Both
  //  DSE = Days Since Equalized  * Both
  //  PW = (Why is PW here twice??  No effing clue) *2030
  //  r% = Replaced Percentage Data - Replaced percentage of used AH since last full charge   *2030 
  //  pD = Lowest previous discharge Amp Hours.  Total used, remaining capacity?  I dunno yet.    *2030
  //  X = Shows up occasionally ??  CJH

// Here's a list of the data I'm actually collecting, some of it just for the heck of it
#define VOLTS       1
#define FILTVOLTS   2
#define AMPS        3
#define FILTAMPS    4
#define SOC         5
#define WATTS       6
#define PREVDIS     7
#define INVALID     0  // bad value

#define BUFSIZE  32

int   RELAY = 22 ; //  Pin for the relay
int   LED   = 21 ; //  Pin for the LED

boolean processBuffer(char *, int) ;
boolean relayOn ;

struct {
    float          parameterValue ;
    unsigned long  collectionTime ;
} measurement[20] ;  // leave a little room for expansion

void setup(){
  Serial.println("In setup ... ") ;
  Serial.begin(38400);     // UART Serial for comm w/ PC over USB
  Serial3.begin(2400) ;
  delay(1000) ;
  Serial.println(" Setup Finished ") ;
  printHeader() ;
  initRelay() ;
  pinMode(LED, OUTPUT) ;    // Set up LED

}   // End Setup

void loop()
{
    // continuously collects data from serial port = TM-2030
    static char buffer[BUFSIZE];
    static unsigned long lastEvalTime=0, lastBannerTime=0, lastOffTime = 0 ;
    static unsigned long lastLEDTime = 0 ;
    static int ii = 0 ;
    unsigned long currentTime = 0 ;   // in mSec since board powered up
    static boolean LED_On = 0 ; 

    // get the time
    currentTime = millis() ;

    // Go to the serial port and get the data from the TM-2030
    if (Serial3.available()) {
      char c = Serial3.read();
      if(c == ',') {      // commas delimit the data fields, so when we get one, we discard it and process what we have

        processBuffer(buffer) ;
       
        for(ii = 0 ; ii < BUFSIZE ; ii++) buffer[ii] = (char)0 ;           // clean out the buffer.  (I'm sure there's a better way)
        ii = 0 ;
        
        //
        //  check criteria for changing state
        //  consider whether to turn the relay OFF
        //
        if(relayOn) {        
          if(measurement[SOC].parameterValue < 95.0 ||
             measurement[SOC].collectionTime + 10000 < currentTime)  {
                lastOffTime = currentTime ;
                turnRelayOff() ;
          }
        }
        // now consider whether we want to turn the relay ON
        else if(measurement[VOLTS].parameterValue > 13.0 &&       // relay is OFF, do we want to turn it on?
              measurement[SOC].parameterValue > 98.0 &&
              measurement[VOLTS].collectionTime + 10000 > currentTime &&
              measurement[SOC].collectionTime + 10000 > currentTime &&
              currentTime > lastOffTime + 600000)  {   // wait at least 10 minutes before turning on. unfortunately, this means won't turn on immediately on power up.
                // consider putting in anti-chatter logic, e.g., don't turn on if you've been on more than 4 (?) times in last hour
                turnRelayOn() ;
        }
      }
      else {
        if(ii      } 
    }

    //
    //  LED is on for 1 second, then off for 1 second
    //
    if(currentTime > lastLEDTime + 1000) {
      lastLEDTime = currentTime ;
      if(LED_On) {
        LED_On = 0 ;
        digitalWrite(LED, LOW) ;
      }
      else {
        LED_On = 1 ;
        digitalWrite(LED, HIGH) ;
      }
    }
   
    //
    //  Print out the data and headers in the Serial monitor
    //
    if(currentTime > lastEvalTime + 10000) {  // 10 seconds since last evaluation
      lastEvalTime = currentTime ;
      Serial.print("  ") ;      Serial.print(currentTime / 60000) ; // minutes of run time
      Serial.print("  ") ;      Serial.print(measurement[VOLTS].parameterValue) ;
      if(currentTime > measurement[VOLTS].collectionTime+2000) Serial.print("*") ;
      Serial.print("  ") ;      Serial.print(measurement[FILTVOLTS].parameterValue) ;
      if(currentTime > measurement[FILTVOLTS].collectionTime+2000) Serial.print("*") ;
      Serial.print("  ") ;      Serial.print(measurement[AMPS].parameterValue) ;
      if(currentTime > measurement[AMPS].collectionTime+2000) Serial.print("*") ;
      Serial.print("  ") ;      Serial.print(measurement[FILTAMPS].parameterValue) ;
      if(currentTime > measurement[FILTAMPS].collectionTime+2000) Serial.print("*") ;
      Serial.print("  ") ;      Serial.print(measurement[WATTS].parameterValue) ;
      if(currentTime > measurement[WATTS].collectionTime+2000) Serial.print("*") ;
      Serial.print("  ") ;      Serial.print(measurement[SOC].parameterValue) ;
      if(currentTime > measurement[SOC].collectionTime+2000) Serial.print("*") ;
      Serial.print("    ") ;    Serial.print(measurement[PREVDIS].parameterValue) ;
      if(currentTime > measurement[PREVDIS].collectionTime+2000) Serial.print("*") ;
      Serial.print("       ") ;
      Serial.print(relayOn ? "ON" : "OFF") ;
      Serial.println() ;
   
      if(currentTime > lastBannerTime + 100000) { // 100 seconds have elapsed since last banner
        lastBannerTime = currentTime ;
        printHeader() ;
      }
    }
}

void printHeader() {
      Serial.println("Mins   >Volts FiltVolts Amps  FiltAmps  Watts   >SOC  PrevDischarge  Relay On?") ;
}

//
//  processBuffer takes the buffer, hopefully containing a parameter e.g. Volts, Watts, SOC (state of charge)
//  and a value, e.g. V=12.5, in which the "parameter" is Volts, and finds the parameter and the value
//  I would like to see the Solar Amps in this data, but that is not available.
//

boolean processBuffer(char buffer[])
{
  int eqLoc ;
  String s=buffer ;
  int parameter ; // was global, but didn't need to be, i hope.

  eqLoc = s.indexOf('=') ;  // is there an = sign
  if(eqLoc == 0) {          // no good -- set values and bail
    parameter = INVALID ;
    return 0 ;
  }
  String sParmID = s.substring(0, eqLoc) ; // just the parm ID
  String sParmVal = s.substring(eqLoc+1) ; // just the value as a string
 
  if(sParmID == "V") {       parameter = VOLTS ;   }
  else if(sParmID == "FV")  {    parameter = FILTVOLTS ;   }
  else if(sParmID == "A")   {    parameter = AMPS ;   }
  else if(sParmID == "FA")  {    parameter = FILTAMPS ;   }
  else if(sParmID == "W")   {    parameter = WATTS ;  }
  else if(sParmID == "pD")  {    parameter = PREVDIS ;   }
  else if(sParmID == "%" || sParmID == "T%") {    parameter = SOC ;   } // my TM-2030 just says %, but Gordon Boulton's says T%
  else {   
    parameter = INVALID ; 
    return 0 ; 
  }
  measurement[parameter].parameterValue = sParmVal.toFloat() ;  // place data in structure so can use it
  measurement[parameter].collectionTime = millis() ; 

  return 1 ; // valid result
}

void initRelay() {
  relayOn = 0 ;
  pinMode(RELAY, OUTPUT) ;
//  delay(200) ;
  digitalWrite(RELAY, HIGH) ;   // Note: Relay is set up to be HIGH=OFF
}

void turnRelayOn() {
  relayOn = 1 ;
  digitalWrite(RELAY, LOW) ;
}

void turnRelayOff() {
  relayOn = 0 ;
  digitalWrite(RELAY, HIGH) ;
}

No comments: