The D0 is there just to provide the 3.3v needed, but configurable. (In practice I could have just connected to 3.3v pin, since I never turn it off.)
The sensor based on a voltage divider, with a known resistance between Vsense and ground, and the resistance of water/air between 3.3v and Vsense. When there is just air, the resistance is high, so Vsense is low. When there is water between 3.3v and Vsense, resistance is much lower so Vsense becomes higher.
I use two sense wires at different positions to provide a hysteresis band.
Normally level sensors need to know which the high and low sensor. Here, I just count how many sensors are covered in water, and count a bubble when there is a transition from 0 to 2.
Splitting hairs, I should probably add a very high resistance between each sensor and 3.3v - when the sensor is in air then the pin is just floating. I sense values around 200-300. With the large pullup resistor I imagine this would be closer to 0.
Here’s the code
void logDebug(const char* s, ...)
{
serialInit();
va_list args;
va_start (args, s );
vsnprintf(buf, 256, s, args);
va_end (args);
Serial.println(buf);
}
namespace ls
{
typedef uint16_t pin_t;
typedef uint16_t read_t;
class LevelSensor
{
pin_t vin;
pin_t vlow;
pin_t vhigh;
public:
LevelSensor(pin_t vin, pin_t vlow, pin_t vhigh)
: vin(vin), vlow(vlow), vhigh(vhigh)
{
}
void init()
{
initReadPin(vhigh);
initReadPin(vlow);
// provide the v-in for the voltage dividers
pinMode(vin, OUTPUT);
digitalWrite(vin, HIGH);
}
void initReadPin(pin_t pin)
{
setInputOutput(pin, pin_t(-1));
}
void setInputOutput(pin_t pin, pin_t wantedInputPin)
{
if (pin==wantedInputPin)
{
pinMode(pin, INPUT);
}
else {
pinMode(pin, OUTPUT);
digitalWrite(pin, 0);
}
}
read_t readPin(pin_t pin)
{
// make other pin high impedance
setInputOutput(vlow, vlow);
setInputOutput(vhigh, vhigh);
read_t value = analogRead(pin);
return value;
}
void readBoth(read_t& low, read_t& high)
{
low = readPin(vlow);
high = readPin(vhigh);
}
read_t maxValue() { return 4096; }
read_t minValue() { return 0; }
read_t constrainValue(read_t value) {
return constrain(value, minValue(), maxValue());
}
};
/**
* Interprets the values from the LevelSensor to determine how many of the two
* electrodes conduct (and assumed in water.)
* Because a count is used, we don't need to specifically distinguish high and low.
*/
class LevelPresence
{
private:
LevelSensor sensor;
/**
* Threshold for sensing no liquid. Values lower than this are considered
*/
read_t lower;
read_t upper;
public:
LevelPresence(LevelSensor sensor, read_t upper=800, read_t sc=4000)
: sensor(sensor)
{
setConnectedThreshold(upper);
}
void init()
{
sensor.init();
}
void setConnectedThreshold(read_t connected)
{
this->upper = sensor.constrainValue(connected);
}
/**
* Determines the number of lines that are conducting.
*/
uint8_t count_connected()
{
read_t a, b;
sensor.readBoth(a,b);
return countConnected(a,b);
}
uint8_t countConnected(read_t a, read_t b)
{
uint8_t count = 0;
count += connected(a);
count += connected(b);
return count;
}
bool isConnected(read_t value)
{
return value>upper;
}
uint8_t connected(read_t value)
{
return isConnected(value) ? 1 : 0;
}
};
/**
* Tracks the levels to provide notification of transitions.
*/
class LevelTracker
{
private:
LevelPresence& presence;
uint8_t prev;
uint8_t readConnected()
{
return presence.count_connected();
}
public:
LevelTracker(LevelPresence& presence)
: presence(presence)
{
}
void init()
{
presence.init();
prev = readConnected();
if (prev==1) // if it's intermittent then just force a state
prev = 0;
}
uint8_t lastReadConnected() const
{
return prev;
}
/**
*
* @return 0 for no change. -1 for transition from all conducting to none,
* and +1 for transition from none conducting to all.
*/
int8_t readTransition()
{
uint8_t next = readConnected();
uint8_t result = 0;
if (next!=prev && next!=1)
{
result = next ? 1 : -1;
prev = next;
}
return result;
}
};
class BubbleCounter
{
private:
LevelTracker level;
uint32_t lastTime;
uint32_t count;
public:
BubbleCounter(LevelTracker& level)
: level(level)
{
}
void init()
{
level.init();
fetchCount();
}
/**
* This would be a good candidate for interrupt-driven calling, say every 10 ms.
* Returns true if a bubble was detected.
*/
bool read()
{
bool bubble = level.readTransition()>0;
if (bubble)
count++;
return bubble;
}
uint32_t fetchCount(uint32_t* duration=NULL)
{
uint32_t now = ::millis();
uint32_t result = count;
count = 0;
if (duration!=NULL)
*duration = (now-lastTime);
lastTime = now;
return result;
}
};
}
using namespace ls;
LevelSensor sensor(D0,A0,A1);
LevelPresence presence(sensor);
LevelTracker tracker(presence);
BubbleCounter counter(tracker);
uint32_t last;
void setup()
{
serialInit();
counter.init();
last = millis();
RGB.control(true);
RGB.color(0,0,64);
}
void loop()
{
// continually poll
bool bubble = counter.read();
if (bubble)
{
RGB.color(127,127,64);
logDebug("Bubble!");
Delay(100);
RGB.color(0,0,64);
}
if ((millis()-last)>5000) {
uint32_t duration;
uint32_t count = counter.fetchCount(&duration);
double bps = count*1000.0*60/duration;
logDebug("BPM: %f", bps);
last = millis();
}
}