# Smarter fermentation
By Filip Strandmark (fs223ji)
This project utilizes the Heltec ESP32 to monitor the fluid density of beer during fermentation, which is related to the sugar content and is used to determine when the fermentation is done. A Discord message will be sent once the desired *final gravity* is measured.
## Background
Lately I have, together with a friend, developed an interest in beer brewing. When brewing beer the fermentation is what takes the longest time. First a measurement is taken of the *Starting Gravity* (SG) using a hydrometer. Using this value and the desired final alcohol content a *Final Gravity* is calculated. Then the fermentation is set to do its thing, while at times taking new measurements to see if its done yet/soon.
Taking these measurements manually requires the opening of the fermentation vessel, which shall be air-tight, and taking a sample. This wastes some beer, lets in contaminants and disturbs the fermentation by letting in oxygen.
## Objective
A device that continuously measured the fluid density with the capability of sending alerts at specific values. It wil be connected to wifi and wall power, since it is not supposed to be moved once started.
Utilizing Archimedes principle we can precisely measure the density by measuring the upward force applied on an object of known mass and volume that is completely submerged in the fluid.
To measure this force a load cell will be mounted to the lid inside the fermentation bucket, from which a weight will be hung with fishing line and the wires fed out througha hole that is then sealed.
Liquid density changes with its temperature and when brewing you may work with quite some temperature deltas. To account for this thermal expansion a thermometer will also be hung down in to the liquid and its reading used in calculating the density.
Once the desired value is measured an alert shall be sent on Discord.
## Parts
| Part | Price (SEK) | Picture | Link |
| ---------------------------------------------------- | -----------:| ------------------------------------ | ---------------------------------------------------- |
| Heltec ESP32 | 370 |  | [Link](https://www.amazon.se/gp/product/B08243JHMW/) |
| 5kg Loadcell with HX711 AD Converter | 110 |  | [Link](https://www.amazon.se/gp/product/B085B167XL/) |
| DS18B20 Thermometer | 93 (for 2) |  |[Link](https://www.amazon.se/gp/product/B01MZG48OE/) |
| Breadboard and jumper wires | 110 |  | [Link](https://www.amazon.se/gp/product/B01N4VCYUK/) |
| 4,7 kOhm resistor (variety pack of 300) | 35 |  | [Link](https://www.amazon.se/dp/B0881THY43) |
#### Additional parts/equipment
* Soldering iron
* Micro-USB cable (preferably a long one)
* USB powersupply (any old USB charger should work)
* Scale for calibration (e.g. kitchen scale, 1-5 kg)
* Phillips screwdriver
* Drill, 5mm or larger
* Hot glue, or other sealant
* Fishingline (or equivalent)
* Weight, to be suspended submerged in the liquid (e.g. a small water bottle with enough weight in it for it to sink in water), it's total volume must be measured
* M4 eyehook
## Computer setup
### Board
*These instructions assume you're running Windows 10*
* Connect the board to the PC with the micro-USB cable. Download the Windows hardware drivers from [here](https://www.silabs.com/documents/public/software/CP210x_Windows_Drivers.zip) and run the x64 installer. Opening Device Manager you should now under *Ports* see a port named *Silicon Labs*(...) folowed by a parenthesis **(COM\*x\*)** where \*x\* is a number, take note of this.
* Download [this file](https://github.com/H-Ryan/Heltec/blob/main/PyCom%20MicroPython/Heltec%20PyCom%20MicroPython.zip?raw=true), unzip it, and open [this website](https://espressif.github.io/esptool-js/) in your preferred [Chromium based browser](https://en.wikipedia.org/wiki/Chromium_(web_browser)#Active). Change the baudrate to 115200, click *Connect* then choose the previously noted com port in the pop-up window. Now begin with clicking *Erase Flash* then, once complete, *Choose File* and upload the Heltec.bin file. Change the flash address to **0x00** and click *Program*. Close the tab once done.
### IDE
Before installing the IDE Node<span/>.js must be installed. [Download the setup](https://nodejs.org/en/download/) and run the wizard to install.
For this project I used Atom and the Pymakr plugin, mainly since it was suggested for the course and I had used Atom once before. This proved useful when editing multiple files and for swiftly uploading new code to the board since it allows for simulatious editing of multiple files and a single click to upload the code.
[Installation instructions for Atom](https://flight-manual.atom.io/getting-started/sections/installing-atom/)
[Installation instructions for Pymakr](https://docs.pycom.io/gettingstarted/software/atom/)
## Build
*This project is estimated to take 2 to 4 hours depending on experience.*

### Thermometer
* Connect the power wires to the board, red to 3,3v and black to ground.
* The yellow data wire shall be connected to pin 21 on the Heltec and with a 4,7 kΩ resistor to 3,3v.
* Import the [Onewire library](https://github.com/pycom/pycom-libraries/blob/master/lib/onewire/onewire.py) as onewire<span/>.py.
* Copy the following code to main<span/>.py.
``` python=
import machine
import sys
import time
from onewire import DS18X20
from onewire import OneWire
ow = OneWire(machine.Pin('P12')) #Create OneWire object
temp = DS18X20(ow) #Create DS18X20 object from OneWire object
while True:
temp.start_conversion() #Start temperature conversion
time.sleep(1) #Wait before continuing, due to the conversion not being instantanious
t = round(temp.read_temp_async(), 1) #Read temperature and round value to one decimal
print(str(t) + " c \n")
```
* Verify the temperature readings.
### Assembling and calibrating the load cell
* Start with soldering pins to the underside of the Heltec and the non-sensor side of the HX711.
* Assemble the loadcell with the included acrylics to make a scale.
* Connect the hx711 to 3,3v and ground as well as pin DT to pin 12 on the Heltec and SCK to pin 13.
* Copy the following code to main<span/>.py
```python=
import machine
import sys
import utime
from hx711 import HX711
driver = HX711(d_out='P9', pd_sck='P10') #Create a hx711-object with the correct pins
while 1:
print(driver.read()) #Read the loadcell value and print it
time.sleep(5) #Wait before running the loop again
```
* Import the [HX711 driver](https://github.com/HowManyOliversAreThere/hx711_mpy-driver/blob/master/hx711.py) as hx711<span/>.py.
* Prepare an assortment of items of different weight, no more than 5kg, that each fit on the scales.
* Place the hx711-scale on the calibration scale.
* Run the code while placing different items on the scales and taking notes of the values reported by the loadcell and the calibration scale respectively.
* More items and greater variation in weight should give the best result.
* Sum all the values of each scale then divide the sum of the calibration scale by the sum of the load cell. The result is expected to be between 1 and 0, take careful note of it. This is our calibration offset for the loadcell.
* The acrylic scale can now be disassembled.
## Connectivity
### Adafruit IO
::: info
Adafruit IO was chosen as it hosted by a company I know, easy to use yet still very capable and completely free.
:::
* Sign up to [Adafruit IO](https://accounts.adafruit.com/users/sign_up).
* Go to *Feeds*, create a new feed and give it a name.
*The idea is to create a new/recreate your feed for every new fermentation.*
* Click the yellow key on the top right to find your *Active Key*, this will be used in the coming steps.
* Import the [MQTT library](https://github.com/pycom/pycom-libraries/blob/master/lib/mqtt/mqtt.py) as mqtt<span/>.py.
### Sending data over wifi
* Start by creating keys<span/>.py and pasting the following to it, replacing the placeholders with your credentials and values.
```python=
ssid = '[Your SSID]'
wifi_pass = '[Your wifi password]'
server = 'io.adafruit.com'
user = "[Your Adafriut username]"
passkey = '[Your Adafruit key]'
feed = "[Your Adafruit username]/feeds/[Your feed name]"
interval = 10
LCoffset = ["Your loadcell calibration offset"]
volume = ["The total volume of your object, in liter or dm^3"]
```
* The *interval* parameter is set to 10 for testing purposes, this is the interval in seconds between taking measurements. For practical use it is more reasonable to measure every half hour or hour.
* Create the file boot<span/>.py and paste the following.
```python=
import machine
from network import WLAN
import keys
wlan = WLAN(mode=WLAN.STA) #Create wlan ibject
wlan.connect(keys.ssid, auth=(wlan.WPA2, keys.wifi_pass), timeout=5000) #Connect to wlan using parameters from keys.py
while not wlan.isconnected(): #Wait while wlan connects
machine.idle()
print('WLAN connection succeeded!')
```
* Paste the following code to main<span/>.py and run it.
```python=
import pycom
import ubinascii
import machine
import micropython
import sys
import time
import keys
from network import WLAN
from mqtt import MQTTClient
def sub_cb(topic, msg): #Callback function for MQTT
print(msg)
wlan = WLAN()
while not wlan.isconnected(): #Verify wifi connectivity before continuing
print("WLAN not connected, waiting 5s...")
time.sleep(5)
client = MQTTClient("1", keys.server,user=keys.user, password=keys.passkey, port=1883) #Configure MQTT client with parameters
client.set_callback(sub_cb) #Set the MQTT callback function
print("MQTT client configured...")
time.sleep(1)
client.connect() #Connect to Adafruit IO
client.publish(topic=keys.feed, msg=str("1")) #Send data to the Adafruit IO feed
client.disconnect() #Disconnect from Adafruit IO to not leave open sessions
```
* Looking at you Adafruit IO feed, you should now see a new datapoint with the value 1.
### Discord alerts
* Open the Discord server settings for the server you want your alerts in, you must be an administrator for this.
* Under *Integrations* go to *Create Webhook*.
* Give it a name and select what channel it shall send messages in.
* Click *Copy Webhook URL*.
* Go to Adafruit IO and *Actions*.
* Create a new reactive action.
* Set it to **If** *[Your feed]* **Is** *greater than or equal to 2* **Then** *send a Webhook message to:* **To URL** *[Your Discord Webhook URL]* *[Your feed]* **value and time. With Template** *Discord Template*.
* In the textbox you can then edit the alert content like below.
```
{
"username": "{{feed_name}}",
"content": "Beer is now at {{value}}"
}
```
* Submit the action and go back to your feed.
* Click *Add Data* and enter the number 2. This should trigger a message in your Discord channel.
* When used in production change the value from 2 to your desired final gravity and consider adding another at some earlier value to get a heads up.
:::info
Note that there's a limit to how often you casn send webhooks from Adafruit IO to Discord. After too many messages your action will be automatically disabled.
:::
### Combining it all
* Paste the following to main<span/>.py.
```python=
import sys
import machine
import pycom
import micropython
import time
import ubinascii
import keys
from mqtt import MQTTClient
from network import WLAN
from hx711 import HX711
from onewire import DS18X20
from onewire import OneWire
cell = HX711() #Initialize the loadcell
print("Loadcell initialized...")
ow = OneWire(machine.Pin('P12')) #Create OneWire object
temp = DS18X20(ow) #Initialize thermometer
def sub_cb(topic, msg): #Callback function for MQTT
print(msg)
wlan = WLAN() #Verify wifi connectivity before continuing
while not wlan.isconnected():
print("WLAN not connected, waiting 5s...")
time.sleep(5)
client = MQTTClient("1", keys.server,user=keys.user, password=keys.passkey, port=1883) #Configure the MQTT client with parameters
client.set_callback(sub_cb) #Set the MQTT callback fuction
print("MQTT client configured...")
print("Taking first measurement in 30 seconds...")
time.sleep(30) #Waiting 30 seconds to allow time to set up the sensors
print("Taking measurements every " + str(keys.interval) + " seconds.")
try:
while True:
temp.start_conversion() #Start temperature conversion
time.sleep(1)
t = round(temp.read_temp_async(), 1) #Read temperature value and round it to 1 decimal
m = round(cell.read()*keys.LCoffset) #Read loadcell value and offset it by calculated multiplier
print(str(t) + " c \n" + str(m) + " g") #Print temperature and weight to log
d = (-m + 1 + 0.0002 * (t - 4))/keys.volume #Calculate the liquid density in grams/liter or kg/m^3, accounting for thermal expansion
sg = d/1000 #Calculating the liquid density in relation to water to get Specific Gravity, the unit used in beer brewing
print(str(sg) + " SG\n")
client.connect() #Connecting to Adafruit IO
client.publish(topic=keys.feed, msg=str(sg)) #Sending data to Adafruit IO feed
client.disconnect() #Disconnecting from Adafruit IO, to not leave open sessions
time.sleep(keys.interval -1) #Sleep before taking next measurement
except KeyboardInterrupt:
print("Program interrupted.")
```
* Mount the loadcell on the inside of your fermentation vessel and hang your weight from it so that it can hang free from the bottom but still be completely submerged.
* Hang the thermometer in a similar way without it touching the loadcell or weight.
* Run the wires out through a hole and seal it around the wires with hot glue.
* Put your breadboard with the Heltec on the lid and plug in the sensors.
* You should now be ready to test it.
### Data storage and presentation
Since the data is not indended to be stored long term, it is fine with Adafruit IO's 30 day storage-limit on datapoints. 30 days is more than enough of "historical" data for this implementation.
The data is presented on Adafruit IO only as a simple list of data and timestamps under a graph. The idea was not to monitor the data personally but instead to just wait for the Discord message, thus, I think this presentation of data is just fine if I want to do a status check.

## Results
### Testrun
For my own testing I did not have the time to do a real fermentation. So instead I decided to test it "in reverse" by increasing the sugar content (density) of a fluid. To do this I rigged up the system over a bucket of water and carefully dissolved granulated sugar into it.


For this test I used a short interval between measurements and a small bucket of water (to not waste excessive amounts of water or sugar). This meant that setting it up without disturbing the sensors when they're measuring was harder that meant to be, hence I got some spikes on my graph in the beginning. Still, this short test proved that the measurements are working as well as the notification.

### Thoughts
Currently I am excited to try it with a real fermentation and see how well it performs since it did ok in my testing. I have started exploring ideas of making the loadcell less prone to errors. As of now it will just take a measurement and upload it without any checking if it's a reasonable value. I would like to add some code that verifies that the value is within reason compared to the last one, and if it isn't it would wait just a few seconds before taking a new one. Issue is I don't know what magnitude of change I should allow or not and how to calculate it.
There are off the shelf products that fill the same function for cheaper, but this one I can customize to do things just how I want them. This system can be expanded to do many more things aswell, and if I don't need it anymore I can repurpose these parts to something else. The road to getting this working has been paved with error messages and late deliveries, but it with pride I now stand with a functional system that I can continue to improve. In the end this approach was still worth it.
