RV-C connection for SeeLevel tank monitor

Ideas and discussion of what to do with the CAN Bus ( i.e. XMDirect, iPod, Carputer, etc... )
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

RV-C connection for SeeLevel tank monitor

Post by JimmyM »

I want to build a module to connect to the SeeLevel 709-RVC tank monitor. It's just going to be an alarm module to read tank levels and flash an LED when the levels pass a certain threshold.
Google hasn't helped me much in a search for arduino projects that interface with the RV-C CAN bus.
I have a passing familiarity with CAN. I built a CAN bus module for my Ford Expedition to read oil temperature and pressures values from additional sensors and make them available on the HS-CAN to be displayed by Torque Pro.
But I can't find much on the CAN messages for the SeeLevel RVC.

I saw that only linuxkidd has messed with the RV-C protocol.

Some help or links to resources would be much appreciated. Thanks.
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

Hey JimmyM,
I do indeed have lots of experience with RV-C ( I created coachproxy.com ). I also created an arduino project that used Fresh and Gray tank levels to control RO production in my coach. It's pretty old code ( and one day I'll get around to putting it on GitHub ), but here's the .ino contents:

Code: Select all

/*
   RV-C CAN_Bus_Monitor
     by: Michael J. Kidd <linuxkidd@gmail.com>
    Rev: 1.0
   Date: 2015-11-23
*/

#include <SPI.h>

#include "mcp_can.h"

boolean enable_monitor_output  = false;  // Should we output full can-bus feed?
boolean debug                  = true;  // Enable debug output

//const int CAN_SPEED            = CAN_250KBPS;    // For Uno  CAN-Bus Shield    (16MHz)
const int CAN_SPEED            = CAN_8M_250KBPS; // For nano CAN SPI interface  (8MHz)
const int SPI_CS_PIN           =   10;           // Which pin to use for SPI CS with CAN bus interface

boolean enable_pump_control    = true;  // Enable RO pump control ( 1 = enabled )
const int PUMP_PIN             =    9;  // RO pump relay control output pin
unsigned int req_full_count    =   60;  // Required count of full level before action is taken

const int HOTWATER_FLOW_PIN    =    8;  // Pin to detect hot water demand (connect to ground = demand)


/*
 * Variables below this point are used internally and should not be changed.
 */
unsigned int status_every     =    30;  // Request status of DC loads every 30 seconds
unsigned int last_status      =     0;  // millis() value of last status request
unsigned int now_time         =     0;

unsigned int pump_state       =   LOW;  // Default pump state - LOW = off

unsigned char len = 0;
unsigned char buf[8];

INT32U canId = 0x000;
char outbuf[8];
char outbuf2[32];
char hexDGN[6];

unsigned int bin2int(char * digits);
char* int2bin(INT32U d);
bool checkAquaHotStatus();
void checkAquaHotDemand();
void toggleAquaHot();
void parseTank();


MCP_CAN CAN(SPI_CS_PIN);         // Set CS pin

typedef struct {
  boolean state           = false;
  unsigned int full_count = 0;
} tank;

tank grey;
tank fresh;

typedef struct {
  boolean enable    = true;  // Enable control of AquaHot based on flow switch
  boolean prestate  = false;  // What is the normal state of the AquaHot system (without us requesting it be on)
  boolean desired   = false;  // Desired Status
  boolean state     = false;  // Last read state
  boolean firstread = true;   // Is this the first read of the aquahot state
} aquahot;

typedef struct {
  char prio[4];
  char dgnhi[10];
  char dgnlo[9];
  char srcAD[9];
} packetmeta;

aquahot ah;

void setup() {
  Serial.begin(38400);
  pinMode(PUMP_PIN, OUTPUT);
  pinMode(HOTWATER_FLOW_PIN, INPUT_PULLUP);
START_INIT:
  if (CAN_OK == CAN.begin(CAN_SPEED))  {
    CAN.init_Mask(0, 1, 0);
    CAN.init_Mask(1, 1, 0);
    for (int i = 0; i < 6; i++) {
      CAN.init_Filt(i, 1, 0);
    }

    if ( debug )
      Serial.println("Debugging Enabled...");
  }
  else
  {
    delay(100);
    goto START_INIT;
  }
}

void loop() {

  if (CAN_MSGAVAIL == CAN.checkReceive()) {
    CAN.readMsgBuf(&len, buf);
    canId = CAN.getCanId();
    char *binCanID=int2bin(canId);

    packetmeta p;

    // Separate the overloaded canID
    memcpy(p.prio,  &binCanID[0], 3);
    memcpy(p.dgnhi, &binCanID[4], 9);
    memcpy(p.dgnlo, &binCanID[13],8);
    memcpy(p.srcAD, &binCanID[21],8);

    // Terminate all the char strings
    p.prio[3]='\0';
    p.dgnhi[9]='\0';
    p.dgnlo[8]='\0';
    p.srcAD[8]='\0';

    free(binCanID);

    sprintf(hexDGN, "%03X%02X",                // Construct full hexDGN for other checking (RO pump control)
            bin2int(p.dgnhi),
            bin2int(p.dgnlo));

    if ( enable_monitor_output ) {
      sprintf(outbuf2, "%X,%03X%02X,%02X,%d,",
            bin2int(p.prio),
            bin2int(p.dgnhi),
            bin2int(p.dgnlo),
            bin2int(p.srcAD),
            len);

      Serial.print(millis()); Serial.print(",");
      Serial.print(outbuf2);
      for (int i = 0; i < len; i++) {
        sprintf(outbuf, "%02X", buf[i]);
        Serial.print(outbuf);
      }
      Serial.println();
    }
    if (strncmp(hexDGN,"1FFB7", 5) == 0 && enable_pump_control )
      parseTank();

    if(ah.enable && !ah.firstread && !ah.desired)
      checkAquaHotDemand();

    if (ah.enable && strncmp(hexDGN, "1FEDA", 5) == 0 && buf[0] == 24) { // Only enter here if we have AquaHot Diesel status, and need it
      ah.state = checkAquaHotStatus();
      if ( !ah.desired || ah.firstread ) {
        ah.prestate = ah.state;
        ah.firstread = false;
      }
      if ( debug ) {
        sprintf(outbuf2, "pre: %d, ah_desired: %d, ah_state: %d",ah.prestate, ah.desired, ah.state);
        Serial.print(millis()); Serial.print(","); Serial.println(outbuf2);
      }
    }
    if(ah.enable && ah.desired)
      checkAquaHotDemand();

    if(ah.enable && ah.firstread) {
      if (millis()>4000) {
        ah.firstread = false;
        if ( debug )
          Serial.println("4 seconds elapsed without AquaHot status... Clearing 'firstread'");
      }
    }
  }
}


/*
   unsigned int bin2int(char * digits)
     This function converts a string of binary bits to an int.
*/
unsigned int bin2int(char * digits) {
  unsigned int res = 0;
  int digits_len = strlen(digits);

  for (int i = 0; i < digits_len; i++) {
    if ((digits[digits_len - (i + 1)]) == '1')
      res += (1 << i);
  }
  return res;
}


/*
 * char* int2bin(INT32U d)
 *    This funciton converts an Unsigned 32 bit int into a CHAR string of binary
 */
char* int2bin(INT32U d) {
  const char* mybins = "01";
  int pos = 0;
  // unsigned long int b;
  char b[29];
  char *c = (char*)malloc(29);
  while(d > 0) {
    b[pos]=mybins[d % 2];
    d /= 2;
    pos++;
  }
  while(pos<29) {
    b[pos]=mybins[0];
    pos++;
  }
  b[pos]='\0';
  pos--;
  for(int i=pos;i>=0;i--) {
    c[pos-i]=b[i];
  }
  pos++;
  c[pos]='\0';
  return c;
}

/*
 * parseAquaHotStatus()
 *   This function logs the current status of the AquaHot system and checks flow sensor input state
 */

bool checkAquaHotStatus() {
  bool curstate=false;
  if (buf[2] == 200)
    curstate=true;
  return curstate;
}

void checkAquaHotDemand() {
  if (digitalRead(HOTWATER_FLOW_PIN) == LOW && !ah.desired) {
    ah.desired = true;
    toggleAquaHot();

    if ( debug ) {
      sprintf(outbuf2, "AH: TurnOn: pre: %d, ah_desired: %d, ah_state: %d",ah.prestate, ah.desired, ah.state);
      Serial.print(millis()); Serial.print(","); Serial.println(outbuf2);
    }
  }
  if (digitalRead(HOTWATER_FLOW_PIN) == HIGH && ah.desired ) {
    ah.desired = false;
    toggleAquaHot();
    
    if ( debug ) {
      sprintf(outbuf2, "AH: TurnOff: pre: %d, ah_desired: %d, ah_state: %d",ah.prestate, ah.desired, ah.state);
      Serial.print(millis()); Serial.print(","); Serial.println(outbuf2);
    }
  }
}

void toggleAquaHot() {
  unsigned char stmp[8] = {0x18, 0xFF, 0xC8, 0x02, 0xFF, 0x00, 0xFF, 0xFF};
  if( !ah.desired && !ah.prestate)
    stmp[3]=0x03;
  CAN.sendMsgBuf(0x19FEDB97, 1, 8, stmp);
  if( debug ) {
    Serial.print("Toggle AquaHot: ");
    Serial.println(ah.desired);
  }
}

/*
   parseTank()
    This function handles Reverse Osmosis Pump control.
    The RO Pump is turned ON if:
     - Fresh water tank is < full
     - Grey water tank is < 1 step below full
    Both parameters are used in their native form ( level / resolution ) which would normally be divided for percentage.
*/
void parseTank() {
  if (buf[0] == 0) { // Fresh water tank
    if (buf[1] < buf[2]) { // It's not full
      fresh.state = true;
      fresh.full_count=0;
    } else {
      if(fresh.full_count > req_full_count )
        fresh.state = false;
      else
        fresh.full_count++;
    }
  }
  if (buf[0] == 2) { // Grey water tank
    if (buf[1] < (buf[2] - 1)) { // Grey tank < 1 level below full
      grey.state = true;
      grey.full_count=0;
    } else {
      if(grey.full_count > req_full_count)
        grey.state = false;
      else
        grey.full_count++;
    }
  }
  if (fresh.state == 1 && grey.state == 1) {
    if ( pump_state != HIGH && debug )
      Serial.println("Toggle Pump: on");
    pump_state = HIGH;
  } else {
    if ( pump_state != LOW && debug )
      Serial.println("Toggle Pump: off");
    pump_state = LOW;
  }

  digitalWrite(PUMP_PIN, pump_state);
}

/*********************************************************************************************************
  END FILE
*********************************************************************************************************/
Note that in addition to the RO control based on tank level, it also uses a flow sensor to send an AquaHot turn-on/off command on my 2016 Tiffin Allegro Bus. You may or may not have need for that, but it's there just in case.. :)

Let me know if you have any questions on this ( poorly commented ) code.

Thanks,
LK
If you can read this, the light is still red.
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

Oh, and the CoachProxy code is now opensource on github as RVC-Proxy:
https://github.com/rvc-proxy/

And the entire RV-C protocol is available in a PDF:
http://www.rv-c.com/?q=node/75
If you can read this, the light is still red.
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

OH, one other thing I should point out.. I ended up modifying the mcp_can.h library to accommodate the 8Mhz chip on the cheaper CAN-bus interfaces.

In case you're using one of those.. and the mcp_can library hasn't been updated upstream, here's the changes needed:

Inside mcp_can.cpp, under the INT8U MCP_CAN::mcp2515_configRate(const INT8U canSpeed) function, add a section like:

Code: Select all

        case (CAN_8M_250KBPS):
        cfg1 = MCP_8MHz_250kBPS_CFG1;
        cfg2 = MCP_8MHz_250kBPS_CFG2;
        cfg3 = MCP_8MHz_250kBPS_CFG3;
        break;
Inside mcp_can_dfs.h, below the "#define CAN_1000KBPS 16" line, add:

Code: Select all

#define CAN_8M_250KBPS 17
Inside keywords.txt, add the following under the "Constants (LITERAL1)" header:

Code: Select all

CAN_8M_250KBPS  LITERAL1
That should get it functioning for you.
LK
If you can read this, the light is still red.
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

Re: RV-C connection for SeeLevel tank monitor

Post by JimmyM »

Wow. Thanks for this. I can't wait to dig into it.
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

Re: RV-C connection for SeeLevel tank monitor

Post by JimmyM »

LK,
Have you worked with the SeeLevel 709-RVC?
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

Yep, that's what comes stock on Tiffin Motorhomes. Thus, it's what my code was written for.
If you can read this, the light is still red.
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

Re: RV-C connection for SeeLevel tank monitor

Post by JimmyM »

Oh good. I'm going to get some hardware together see if I can isolate the message readings. I've found out that the 709-RVC broadcasts the readings every 5 seconds, so it should just be a matter of reading the coming message and parsing out the tank level. Right?
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

Ya, the RV-C spec provides details on how often each type of message should be broadcast. But since it is broadcast, you simply monitor the CAN-bus for the messages you're interested in, and process as needed. The arduino code show good examples of all that, specifically the tank level readings.
If you can read this, the light is still red.
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

Re: RV-C connection for SeeLevel tank monitor

Post by JimmyM »

Does the code in the parseTank() routine decode a tank level?
Is that fresh.full_count ?
Based on this from the manual
"The sensors use a default source address of 72 and SPN-ISB instances are 0 for fresh, 1 for black, 2 for grey and 18 for galley (depending how the display is equipped). The LPG sensor uses a default source address of 73 and a SPN-ISB instance of 3."

From your code:
if (buf[0] == 0) { // Fresh water tank
and
if (buf[0] == 2) { // Grey water tank

Then this would be used for the Black tank, Right?
if (buf[0] == 1) {
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

Re: RV-C connection for SeeLevel tank monitor

Post by JimmyM »

It doesn't look like the code actually takes message data and populates it in a variable like fresh.full_count. It only increments it up based on a buf[1] buf[2] comparison.
JimmyM
What's hacking?
Posts: 16
Joined: 2020 May 04 14:20

Re: RV-C connection for SeeLevel tank monitor

Post by JimmyM »

From the rv-c spec for tanks it looks like buf[0] is the instance (Black, Grey, Fresh, etc)
buf[1] is relative level and buf[2] is resolution.

Bear with me. I'm still figuring this rv-c stuff out.
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

Hi JimmyM,
Yes, buf[0] == 1 would be the black tank. and == 3 is the Propane level ( if used )

The tank level is provided as a function of how many steps are active, out of how many steps are possible.

For example, due to tank height, you may need to cut the sensor down from 20 segments to 18 segments.
So, if the tank fluid is activating 9 segments, the tank level would be expressed as buf[1] = 9, and buf[2] = 18. To get the percentage level, you'd divide buf[1] by buf[2] and multiply by 100, and drop any remainder.

( 9/18 ) x 100 == 50% full

You're doing great so far.. :)
LK
If you can read this, the light is still red.
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

Also... for my specific use ( turning off my RO water production ), I didn't want to stop at the first sign of 100% full on the Fresh tank. Thus, why I put the counter in place and only turn off the RO when there have been 60 consecutive full readings. This is because the levels are rarely triggered just once, and stay at that state until the tank hits the next level. There's some "grey" area where the reported level will flip back and forth between the lower and higher levels as the tank slowly fills.

LK
If you can read this, the light is still red.
User avatar
linuxkidd
Site Admin
Posts: 365
Joined: 2005 Jul 22 15:48
Location: Anywhere, USA
Contact:

Re: RV-C connection for SeeLevel tank monitor

Post by linuxkidd »

FYI -- for tank level, that is covered in section 6.28.2 of the PDF starting on page 177:
http://www.rv-c.com/sites/rv-c.com/file ... 0Layer.pdf
If you can read this, the light is still red.
Post Reply