Fermenting beer bubble counter

I’m a pretty avid homebrewer. One way to estimate the rate of fermentation is to look at how often a bubble is produced. Some folks have done this using a photosensor. I put together a simple bubble counter using some 3-core cable and a couple of resistors.

The video shows it in action. The beer is bubbling 2-3 bubbles in succession every 8 seconds or so. (If you have sound, you can see+hear the bubbles in time with the sparks response and the log.)

Now to just hook this up to the fermentation monitor brewpi

4 Likes

Very cool! Before I saw the video I was thinking a laser array over a cauldron of bubbling brew. Totally forgot about the airlock. So this is essentially a liquid detector? Seems like it works very well! Would you share the exact configuration of the resistors/wire? Kinda looks like you might be doing analog and digital together. Interrupts on digital and?

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();
    }
    
}
1 Like

Oh yeah, power… tee hee. Do you think this will need to be interrupt driven bubble sensing once you add more complexity to the main loop? Like already just having a Delay(100); when flashing the LED might cause you to miss a bubble.

1 Like

Yeah definitely should be on a timer interrupt to invoke the read method at least every 10ms (there’s a comment about that in the code.). I’ve not played with timers on the stm32 so just polled for proof of concept. Are there any high level functions for setting up a timer interrupt on the spark?

Fortunately it doesn’t have to be 100% accurate. A missed bubble is ok, it’s the overall picture that’s more important - is it going quickly or slowly. Airlock activity itself is already quite imprecise due to the affect temperature has on the gasses in the headspace, so it’s only a secondary measure - really I should be measuring the density of the beer, but that’s a lot harder! :smile:

Oh come on we’re engineering here… ALL BUBBLES MUST BE ACCOUNTED FOR!!! :smile:

If you can get the voltages right, this is the easiest way:
http://docs.spark.io/#/firmware/other-functions-interrupts

Get in, increment a bubbleCount++; and get out… then process the count slowly in your main loop.

3 Likes

Hehe, alright then for my enginneer’s pride I will go after every last bubble!

Getting the voltages right so that I can use level change interrupts is going to be tricky, or require some trim pots and calibration by the user. The reason is because the analog data represents conductivity of the water. 0 is zero conductivity, 4096 is lots. (keeping it technical!) The conductivity depends on the ions in the water, so it will vary from user to user.

My water is moderately soft, so in air I read 300 (and I assume that’s just because the pin is floating), and in water around 1200. Sure, users could add table salt to increase conductivity, but I think it will just be simpler overall to poll with a timer. The bubbles don’t move so fast that wouldn’t need more than 50 - 100 reads per second.

I’ll try the code in here - https://community.spark.io/t/creating-timer-based-interrupts/771/3

Sounds like you are intending this to become a kit, or for many people to use it. They may have to tweak those analog values anyway… and their analog inputs may float around a bit differently. And the water level in the airlock will also vary per user and over time (evaporation).

Just thinking out loud: If you have to dangle something into the airlock anyway, maybe a tiny cork with a piece of stainless steel wire/rod sticking out of the top of it, that floats in the water… and when a bubble comes it pushes it up. At the top of the airlock you could have a sensor that snaps to the top of the circular part that houses an IR make/break sensor. The SS wire/rod would push a strip of plastic up through the IR sensor… partly clear at the top (when the cork is down) and opaque near the bottom of the strip (so when the cork pushes up it breaks the IR beam). That’s almost fool proof for an digital interrupt, but a little work to get it all mechanically set up.

I know a few brewing buddies that would want to build the same thing, so I’m trying to keep it simple with minimal hardware required.

A float? Thanks, that’s an interesting angle - not thought of using a float. But I don’t have an IR sensor to hand.

Rather than using an IR sensor why not some conductive foil in the lid? The float rises to the top and completes the circuit. Although just getting the float in there is going to be tricky - the neck of the airlock is quite thin. Also evaporation could also affect this too.

I’ll evaluate the current design with regards to evaporation etc. I’d rather not make it more complex unless there’s good reason to. But thanks for bouncing these ideas, always nice to hear a different perspective!

I plan to place the wires on the bottom of the airlock, just where the air pushes against the U shape at the bottom. At that point, evaporation has little affect (unless the airlock is completely dry, and then there are no bubbles to measure.)

No problem, just throwing out ideas :smile:

I was thinking of a float that makes/breaks electrical contact at first… but then thought it would probably have issues pressing hard enough, and the contacts not corroding. So IR was the least amount of force and nothing to corrode. Based on how hard that “bubble” pushes the water up… I think even with some evaporation it would still kick a cork up the channel a bit.

Yes it would have to be a small cork to fit in there :wink:

Aha, just had another idea… put the cork in the air lock all by itself, and create an IR beam through the airlock itself. The cork could block the beam when no bubbles are present, and water/air would allow the beam to pass. Again, clamping the IR phototransistor and receiver would be the hardest part.

Fellow homebrewer here. The most important question is… What kind of beer are you fermenting?

Second… I’m a big fan of using a blow-off tube instead of an airlock. I wonder how you could make a bubble sensor for that configuration.

1 Like

I think it would be much easier to do the float idea with a blow-off tube configuration.

1 Like

Heh. That kind of defeats the purpose of a blow off tube. You’re going to get beer/sanitizer exploding all over the place.

Hmm, I think I see… the riser tube might need a hole on the side to allow the bubble’s pressure to escape once the cork gets high enough… instead of IR let’s change this to magnets:

Edison’s first 9999 light bulbs failed…

that might work as long as your holes are big enough. krausen from the beer can clog up your airlock, which will build up pressure and make a mess. that’s the purpose of a blow off tube. if the krausen gets down your blow off tube and into your bucket, and then up through your cork mechanism, you’ll have the same problem unless your holes are big enough to let the mess through without clogging up.

what about a flap of plastic (with some foil on it for conductivity) over a large hole (with an o-ring to get a good seal) that pops up with a little pressure and acts like a little switch that opens/closes a circuit?

I thought of the flap as well, but didn’t think there was enough motion to have enough hysteresis in the sensor.

Krausen sounds like a pain…

I was also thinking of using a microphone to measure the POP, POP, POP… but now you are getting into some serious analog design, and it’s kind of not worth it unless you are selling this as a product/kit.

Given all this complexity, the first version is looking pretty good right now!

Another fellow homebrewer here, too. I’ll ask @Hypnopompia’s question again - what kind of brew was that?

If you were using a blow off tube, you could put the level sensors right underneath the blow off tube - they’d stay wet most of the time until a bubble came, maybe? Or, if you had the tube on an angle, you could put them right above where the bubbles would come out and see them all that way?

1 Like

Hmm. You could possibly use a weak magnet to keep the flap closed until enough pressure has built up enough to open it up. then the amount of pressure would be pretty consistent in order to open up the valve. You could even use the magnet as one side of the contact switch.

And yes. it will get enough pressure to pop the valve. When these things get clogged, they can blow the lid off a 5 gallon bucket. :smile:

If you want to go that route, it would probable be easiest and most accurate to just install an electronic relief valve and a pressure sensor. Let the pressure build up to 5psi, and then open the valve until the pressure drops below 1psi. The rate at which you have to open the valve would be proportional to Bubbles Per Minute (BPM) number.

1 Like

@Kudos, thanks for the reminder about the beer question! The beer is a Blonde Ale. I needed something simple and quick fermenting to be ready in two weeks. With temp control, nutrients and O2, I usually can be serving beers in that timeframe.

And, yes it seems complexity is running away with this! I feel we need to reign it back in and look for a simpler approach.

In principle, the blowoff tube can function the same as the airlock. As air pressure builds in the tube, it pushes the water inside the blowoff tube downwards until the air can escape, releasing the pressure so water fills back to the level.

Placing +5v cable below the tube (in the main water reservoir) and the two level sensors inside the blowoff tube would give the same affect as we have now in the airlock. In fact, I imagine this would be the more reliable than the airlock since there’s less chance of “secondary” bubbles that you sometimes get with the rapid depressurization in a small U-bend airlock.