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) ;
}
// * 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) ;
}