tshannon
Posts: 2
Joined: Wed Sep 02, 2015 1:30 pm

Read signal of an RV Tank Sensor

Wed Sep 02, 2015 2:25 pm

My wife and I are in the middle of renovating an airstream travel trailer (more info here if you're curious: http://rumblestripramblings.com/), and one of the features I'd love to add during the reno would be to read our Fresh, Grey, and Black Water tanks with a Pi so I can display it on our TV in the trailer, or throw it up on a web page and display it on my phone, etc.

The sensor I'm planning on using is this: https://www.rvupgradestore.com/Seelevel ... /710es.htm.

It states that there is a micro-controller in the sensor, and it'll put out digital codes based on the sensed level. I don't know the voltage of the digital signal currently but I assume I can add resistors to get it to the right level for the GPIO.

Based on the admittedly, limited information I've provided, do you think this would be possible? Or are there some glaring inaccuracies in my assumptions?

Thanks,
Tim

scotty101
Posts: 3958
Joined: Fri Jun 08, 2012 6:03 pm

Re: Read signal of an RV Tank Sensor

Wed Sep 02, 2015 3:24 pm

Looks feasible but difficult to tell given that the protocol used by the sensors isn't mentioned. It looks like it might a some kind of one-wire protocol since it only has ground and signal connections.
You should ask the manufacturer for more details, I suspect that they only want you to connect this device to the level indicators they they manufacture.
Electronic and Computer Engineer
Pi Interests: Home Automation, IOT, Python and Tkinter

tshannon
Posts: 2
Joined: Wed Sep 02, 2015 1:30 pm

Re: Read signal of an RV Tank Sensor

Wed Sep 02, 2015 3:51 pm

Yeah, I figure since it's single wire, and the protocol is described as a "set of codes", I may possibly be able to reverse engineer it. But that's a few big "ifs". Worse case scenario I can buy their panel, but a $150 panel that does one thing with data seems like a sad bargain compared to a Pi which will let me put the data wherever I want.
Tim

digiital
Posts: 4
Joined: Tue Oct 27, 2015 11:44 pm

Re: Read signal of an RV Tank Sensor

Wed Oct 28, 2015 12:18 am

That sensor reminds me of the following setup.
https://www.adafruit.com/products/1602

The 710ES has 8 points, the adafruit board has 8 points as well.

seu_dcmilan
Posts: 7
Joined: Tue Nov 03, 2015 5:18 am

Re: Read signal of an RV Tank Sensor

Thu Nov 05, 2015 5:16 am

In my experience, the signal may be connect to a rs-232. You just receive the bytes passively, maybe every 1sec or 10sec.

alfista2600
Posts: 34
Joined: Wed Jan 22, 2014 1:19 am

Re: Read signal of an RV Tank Sensor

Mon Apr 04, 2016 5:00 am

Scotty - did you ever end up doing anything with this? I'm thinking of doing the same thing. I saw another thread with someone planning on using a voltage meter running off the sensor to an arduino.

alfista2600
Posts: 34
Joined: Wed Jan 22, 2014 1:19 am

Re: Read signal of an RV Tank Sensor

Mon Apr 04, 2016 5:21 am

looking further into this, the sensors look much more complicated. I know nothing about reverse engineering whatever protocol. Too bad... have much of my automation wrapped around a pi and don't want to invest in this controller.

GiantLifeTinyRV
Posts: 1
Joined: Fri Mar 02, 2018 7:56 pm

Re: Read signal of an RV Tank Sensor

Fri Mar 02, 2018 8:00 pm

Has any one made progress on this. I'd also like to do this and tie it into my Google Home. For example, "Hey Google... What's the black tank level?". Has anyone contacted Garnet?

davidv851
Posts: 1
Joined: Wed Aug 22, 2018 1:15 am

Re: Read signal of an RV Tank Sensor

Wed Aug 22, 2018 1:21 am

I haven't done this _yet_ but there's a module that will allow you to interface up to 6 sensors via RS232 serial connection (https://www.garnetinstruments.com/produ ... interface/).

mydog8it
Posts: 1
Joined: Wed Sep 04, 2019 11:17 pm

Re: Read signal of an RV Tank Sensor

Wed Sep 04, 2019 11:31 pm

I was looking for a solution like this myself. I want to collect the data from the sensors and display it in a Splunk dashboard. I want to create alerts for high or low levels (depending on the tank type). I was looking at the See Level solution but don't think it will allow me to send the data outside of the system.

I took a look at the documentation on See Level's website and it looks like they are using Geotab/RV-C to "talk" to the sensors. Between the Geotab SDK/docs and the See Level docs this seems doable to me, am I missing something?

limeandcoconut
Posts: 1
Joined: Wed Mar 04, 2020 10:58 pm

Re: Read signal of an RV Tank Sensor

Wed Mar 04, 2020 10:59 pm

Has anyone made progress on this? I'm looking to start working on my tank sensors and this sounds like it would be a great solution.

jgrant911
Posts: 2
Joined: Thu Jun 04, 2020 6:02 pm

Re: Read signal of SeeLevel RV Tank Sensor

Thu Jun 04, 2020 6:50 pm

I just completed a monitor/control project in my RV using the SeeLevel sensors. My project was done using Arduino but he results are transportable.

I spent a fair amount of time reverse engineering the protocol and the electronics to be able to directly and reliably read the 3 tank sensors that I installed in my Sunseeker 2300. I have attached some of the documentation that I produced for my own records. Have a look and see if it makes sense. I would have added more but the size limitation on attachments made it difficult.

For anyone wanting to do a project like this I am willing to share details such as the code that I wrote to properly read the sensors including the checksum calculations that are used in the data stream. FYI, I also wrote code to access DS18B20 temperature sensors, DS3231 Real Time Clock, TriMetric Battery Monitor and EpEver Tracer Solar Charge Controller.

Jim G.
Attachments
Panel Front Layout3_LowRes.jpg
Panel Front Layout3_LowRes.jpg (146.72 KiB) Viewed 394 times
SeeLevel Data Detail.jpg
SeeLevel Data Detail.jpg (244.92 KiB) Viewed 394 times
SeeLevel Circuit.jpg
SeeLevel Circuit.jpg (240.73 KiB) Viewed 394 times

dgorman
Posts: 1
Joined: Fri Jun 12, 2020 5:21 am

Re: Read signal of an RV Tank Sensor

Fri Jun 12, 2020 5:24 am

Hi

I am very interested in the SeeLevel post above. I’ve instrumented Victron devices and my Weatherflow station and just need tank levels and have been trying to figure this out for a while! I have a Pi in my RV and it collects the data and I have my own dashboards. I’d love to see how this was implemented, the binary code signals (PWM?) isn’t something I’ve done with a Pi. Any additional info would be awesome!

jgrant911
Posts: 2
Joined: Thu Jun 04, 2020 6:02 pm

Re: Read signal of an RV Tank Sensor

Sat Jun 13, 2020 1:26 am

OK, glad someone is interested! It took me quite a while to reverse engineer the SeeLevel protocol and electronics so I hope others can use the results.

I have "played" with an RPi 3B+ that I picked up last year but as I mentioned before, all my work on this RV Info Panel was Arduino based because of the much more user friendly IDE and the auto restart functionality of the Arduinos (a real benefit in an RV when the power tends to be on and off quite a bit).

Also, remember that programming isn't my profession its just one of my hobbies. The only coding I did at university was FORTRAN so all of this "object oriented" stuff and anything to do with "array pointer de-referencing" I tend to avoid!

I have included 2 more pieces that are important to understanding the decoding of the data and how it is gathered, converted and displayed by my Arduino based system:

The first file is a Notepad++ excerpt of the code that actually reads the tank levels (this is from the Arduino Nano that I have doing temperature and tank level monitoring). I can't promise that I copied all the declarations into the file but you will find out if you get as far as running it! The file components are :

- Title blurb
- Declarations
- Main code segment that manages reading the level of 3 tanks and then converts the data to percent readings
- Subroutine readLevel() is passed a tank number and sends out 1-3 pulses to select a tank then waits for stores, and returns a 12 byte reply
- Subroutine readByte() reads and stores 8 bits and returns it as a single properly coded byte

If you review this info along with the previous pages showing the data format and the electrical interface you should get a good idea of how it all comes together. The major part of the Main code segment is spent figuring out what is currently the top non-zero segment of each tank tankFillSeg(), and then assigning fill percentages to it and all the lower segments then totaling them.

My setup is additionally complicated by the fact that I have shortened 2 of the sensors to properly fit my waste tanks which are long and flat. This is a design feature of the SeeLevel sensor to allow use on shorter tanks (you can also stack sensors to read taller tanks). So you can see 2 arrays that store calculated fill segment percentages for 8 segment (tankSegVol8[ ]) and 5 segment (tankSegVol5[ ]) tanks that are used in the calculations. My plan, when I have my RV in a location I can run the test, is to fill each tank at a known rate while collecting time-stamped fill data from the Nano. This will allow a more accurate determination of the tankSeg Fill numbers.

The checksum verification had me stumped for quite a while. The Checksum is basically a calculation of (MOD(byteSum, 256) -2). This works except when MOD(byteSum, 256) is 0 or 1. which results in an error (negative) when using byte math. The designer apparently uses byte 0 as some sort of rollover counter to keep track of each time the sum of the tank level segments (bytes 2 to 9) exceeds 255 and somehow incorporates this into the checksum verification. Someone with more smarts than me could probably look at the data and come up with an elegant solution to this. In order to avoid the checksum failure that occurs whenever (MOD(byteSum, 256) is 0 or 1 I just added 2 special cases to the verification logic to handle this.

The second is a transcript of data read from the Seelevel ES710 sensor on my test bench using a logic analyzer. I taped the sensor to a big bucket and used the SeeLevel 709 display panel to read the percent fill while gradually adding water to the bucket. You can get a good feel for how the first segment of the sensor "sees" the water rising then when the level gets to around 128 (9%) the second segment starts to register. This continues segment by segment until the sensor reads 100%.

Ok, that's about it for now. I am happy to answer questions but remember, being an old engineer I tend to get cranky when I get the sense that the people asking the questions haven't put much thought into the problem.

Cheers!

Jim

Code: Select all

// The part of my monitor program that reads and decodes the data from the SeeLevel sensors to provide a Fill Percent
// 
// Byte 0: read counter, byte 1: checksum, bytes 2-9: fill data, bytes 10-11: all zeros
// Bytes 2-9 are 8 bit representations of the "fill density" of the 8 sensor segments
//
// 3 tanks x 12 bytes:  
byte SeeLevels[3][12];  //Seelevel raw data storage:
byte lowLimit[3][8];    //May require zero/empty offset for SeeLevel calibration.
byte highLimit[3][8];   //May require zero/empty offset for SeeLevel calibration.
float tankLev[] = {0, 0, 0};      //Initialize tank levels to zero
byte tankArray[] = {90, 35, 25, 0};  //Test values: Fresh, Grey, Black; percent full with last byte reserved for crc
byte tankBaseSeg[] = {9, 6, 6};  //Base segment of bytes 11 (LSB) to 4 (MSB) of sensor strip (Black/Grey are cut down for shallow tank)

// It's important to note that a "full" segment does not mean a reading of 255. Usually a full segment reading only reaches around 125 
// before the next segment starts to register.
// Tank sensors were bench tested to determine variations in the sensor readings along the sensor strips and the following
// parameters were defined to help improve the accuracy of the calculations used to determine the measured tank level
// Use of each is explained further in the related code segments
byte tankFillSeg[] = {0, 0, 0};   //Current top segment of each tank with nonzero reading
float tankSegVol8[] = {18, 13, 11, 15, 11, 11, 11, 10};  //Measured approx. percent fill for each of 8 segments (top to bottom)
float tankSegVol5[] = {22, 21, 20, 19, 18};   //Measured percent fill for each of 5 segments on shallow tanks (top to bottom)
float baseSum;
int byteSum, checkSum;  //SeeLevel data integrity checks
float segVal, fltTFS;

// Main code segment that reads the 3 tank levels. (Lots of left-over print statements for debug of calculations)
    readLevel(tankNum);                // Read Tank levels 0, 1, 2 sequentially. Each Read returns 12 bytes.
    
	for (i = 0; i < 12; i++) {
      Serial.print(SeeLevels[tankNum][i]);  //Store 12 bytes of data in array
      Serial.print(' ');
    }
    Serial.println("----------");
	// Bytes 2 to 9 are an 8-bit code that represents  the fill in each segment of the tank, from bottom (9) to top (2)
    // Fresh tank uses all 8 segments of sensor strip, Grey and Black in my RV use only 5 segs, so decode must be adapted
    // Validate checksum (A checksum is contained in  byte1 (rollover count in byte0 is not used in calculations)
    byteSum = 0;                    
    for (i = 2; i <= 9; i++) {      
      byteSum = byteSum + SeeLevels[tankNum][i];  // Add all 8 data bytes together then ((remainder of byteSum/256) - 2) = checkSum.
    }
    // Verifying the checksum that comes with the SeeLevel data.
    // Remainder of (sum of all 8 data bytes)/256 - (checkSum + 2) should be zero
    checkSum = SeeLevels[tankNum][1];        //Get checksum from 2nd byte of read data
    Serial.print("byteSum % 256 - 2 = ");    
    Serial.print((byteSum % 256) - 2);
    Serial.print(" checkSum = ");
    Serial.println(checkSum);
    // Verify checkSum. Special cases to avoid negative result: if byteSum = 0 or 1, checksum will be 254 or 255 respectively
    if ((byteSum == 0 && checkSum == 254) || (byteSum == 1 && checkSum == 255) ||
        (byteSum % 256) > 0 and (byteSum % 256) == (checkSum + 2)) {                   
    // If data OK:
	// Calculation of total fill is kinda convoluted but it works:
      baseSum = 0;
      fltTFS = 0;
      segVal = 0;
      if (SeeLevels[tankNum][tankBaseSeg[tankNum]] == 0) {        // tankBaseSeg[] is 6 for tank 1 and 2 and 9 for tank 0
        tankFillSeg[tankNum] = tankBaseSeg[tankNum];              // If bottom (base) segment fill = 0, tank is empty so...
        tankLev[tankNum] = 0;
      }
      else {
        i = tankBaseSeg[tankNum];                                 // Data bytes are 2 to 9 or 2 to 6 depending on tank (MSB -> LSB)
        while (SeeLevels[tankNum][i] != 0 && i >= 2) {
          tankFillSeg[tankNum] = (i);                             // tankFillSeg[tankNum] will point to the top non-empty segment of tank[tankNum]
          --i;
        }
        if (tankNum == 0)  {                                      // Fresh water tank (tank 0) has 8 segment sensor
          for (i = 7; i > (tankFillSeg[tankNum] - 2); i--) {      // Add together the percent fill of each segment but the top one
            baseSum = baseSum + tankSegVol8[i];
          }
        }
        else {                                                    // Gray and Black tanks (tanks 1 and 2) have 5 segment sensorS
          for (i = 4; i > (tankFillSeg[tankNum] - 2); i--) {      // Add together the percent fill of each segment but the top one
            baseSum = baseSum + tankSegVol5[i];
          }
        }                                                          // Tank 0 (Fresh)has 8 segment sensor
        if (tankNum == 0) {                                        // Calculate the percent fill of the top non-zero segment(Fresh tank)
          fltTFS = SeeLevels[tankNum][tankFillSeg[tankNum]];       // convert partial tankFill to float to compare with typicsl "full" value
          segVal = (fltTFS / 125) * tankSegVol8[tankFillSeg[tankNum] - 2];    // 125 chosen to give close match to actual SeeLevel gauge
        }                                                          // Tanks 1 and 2(Grey and Black)have 5 segment sensors
        else {                                                     // Calculate the percent fill of the top non-zero segment(gray & black tanks)
          fltTFS = SeeLevels[tankNum][tankFillSeg[tankNum]];       // convert to float
          segVal = (fltTFS / 125) * tankSegVol5[tankFillSeg[tankNum] - 2];
        }
        tankLev[tankNum] = baseSum + segVal;                       // Add base fill and topseg fill together for total calculated tank fill
      }
	        //Serial.println(" ");
      Serial.print("BaseSeg = ");
      Serial.print(tankBaseSeg[tankNum]);
      Serial.print(" FillSeg = ");
      Serial.print(tankFillSeg[tankNum]);
      Serial.print(" FillSeg Level = ");
      Serial.print(SeeLevels[tankNum][tankFillSeg[tankNum]]);
      if (tankNum == 0) {
        Serial.print(" tankSegVol8 = ");
        Serial.println(tankSegVol8[tankFillSeg[tankNum] - 2]);
        //Serial.print(" tankSegVol8 = ");
        //Serial.println(tankSegVol8[tankFillSeg[tankNum] - 2]);
      }
      else {
        Serial.print(" tankSegVol5 = ");
        Serial.println(tankSegVol5[tankFillSeg[tankNum] - 2]);
      }
      Serial.print("baseSum = ");
      Serial.print(baseSum);
      Serial.print(" fltTFS = ");
      Serial.print(fltTFS);
      Serial.print(" segVal = ");
      Serial.print(segVal);

      tankLev[tankNum] = round(tankLev[tankNum]);
      if (tankLev[tankNum] > 100) tankLev[tankNum] = 100;  //Don't let tankLev exceed 100%
      Serial.print(" tankLev = ");
      Serial.println(tankLev[tankNum]);
      Serial.println("--------");

      //    Save limit values of 8 data bytes per sample for transfer to Mega
      //    Serial.print(SeeLevels[tankNum][0]);
      //    Serial.print(" ");
      //    Serial.print(SeeLevels[tankNum][1]);
      //Serial.println(" ");
      for (i = 2; i < 10; i++) {                     // Save and Print read values for verification
        if (SeeLevels[tankNum][i] > highLimit[tankNum][i - 2]) {
          highLimit[tankNum][i - 2] = SeeLevels[tankNum][i];
        }
        if (SeeLevels[tankNum][i] < lowLimit[tankNum][i - 2]) {
          lowLimit[tankNum][i - 2] = SeeLevels[tankNum][i];
        }
      }
	  
	  //Potential use of a zero/empty offset to provide better accuracy if tanks don't read empty when they are in fact empty
      Serial.println("Low-High Limit Check: ");
      for (i = 0; i < 8; i++) {
        Serial.print(lowLimit[tankNum][i]);
        Serial.print(" ");
        Serial.print(SeeLevels[tankNum][i + 2], HEX);
        Serial.print(" ");
        Serial.println(highLimit[tankNum][i]);
      }

      Serial.println(" ");

      
     
    }
    else {
      tankErr[tankNum] = ++tankErr[tankNum];      // increment error count for this tank
      dataUpdate = true;
    }
    if (tankNum != 2) {               // increment or reset tank number
      ++tankNum;
    }
    else {
      tankNum = 0;
    }
    tankPass = 1;
    //delay(200);

  


// Read individual tank levels
void readLevel(int t) {              // passed variable (t) is 0, 1 or 2          //Could use time check instead iof delay?
  digitalWrite(pinSLout, HIGH);     // Power the sensor line for 2.4 ms so tank levels can be read
  delayMicroseconds(2450);
  for (i = 0; i <= t; i++) {        // 1, 2 or 3 low pulses to select Fresh, Grey, Black tank
    digitalWrite(pinSLout, LOW);    // See RV Panel data file for protocol details
    delayMicroseconds(85);          // These settings give the 85 down/300 up (microsecond) pulse
    digitalWrite(pinSLout, HIGH);
    delayMicroseconds(290);
  }
  for (byteLoop = 0; byteLoop < 12; byteLoop++) {
    SeeLevels[t][byteLoop] = ~readByte();  // Populate 12 bytes with bitwise inverted readings (FF > 00)
  }
  delay(20);                    // Leave power on long enough to allow sensor to transmit data stream
  digitalWrite(pinSLout, LOW);  //Turn power off until next poll
}

byte readByte() {               //Function to read individual bytes from SeeLevel sensor
  result = 0;
  for (bitLoop = 0; bitLoop < 8; bitLoop++) {   // We need to populate byte from right to left,
    result = result << 1;                       // so shift right for each incoming bit
    pulseTime = (pulseIn(pinSLin, LOW));        // If needed, pulseIn() has optional 3rd arg: "timeout" in microseconds
    if (pulseTime  <= 26) {                     // "0" bit is low for about 43 microseconds, a "1" for about 13 microseconds
      thisBit = 1;                              // so 26 is the close to the decision point (tune for error improvement)
    }
    else  {
      thisBit = 0;
    }
    result |= thisBit;                          // compound bitwise OR succesive reads into shifted single byte
  }
  return result;
}

 
Attachments
SeeLevel Table P1.JPG
SeeLevel Table P1.JPG (167.42 KiB) Viewed 254 times
SeeLevel Table P2.JPG
SeeLevel Table P2.JPG (185.58 KiB) Viewed 254 times

Return to “Advanced users”