BrewController

Fra KollundvejBryglaug

Skift til: Navigation, Søgning

Indholdsfortegnelse

Baggrund

Vi har erfaret at sommetider løber temperaturen af med os og mæsken bliver for varm. Ved samtaler med en af Klaus' gode kollegaer, som er kemiker, har vi fået åbenbaret noget om mæskeprocessen, som vi hidtil ikke har givet den store opmærksomhed. Når man spirer kornet og får malt ud af processen sker der det i kernerne at der dannes nogle enzymer, som kan omdanne stivelsen i kornet til sukkerarter. Og sukkerarter er det vi ønsker at fodre gæren med, for det giver alkohol og CO2, som resultat.

Årsagen

Men disse enzymer, hvoraf det især er beta-amylase og alfa-amylase vi er interesseret i, har nogle optimale temperaturområder, hvor de fungerer mest effektivt med at katalysere spaltningen af stivelsen. Og enzymerne denatureres når temperaturen i mæsken kommer for højt op. En denaturering er desværre irreversible, d.v.s. at selvom man sænker temperaturen i mæsken igen er enzymerne der ikke længere til at katalysere omdannelsen af stivelsen til de ønskede sukkerarter. Se nedenstående figur.

Fundet på [1]

beta-amylasen nedbryder stivelsen til maltose, som gæren direkte kan omsætte mens alfa-amylasen nedbryder stivelsen til nogle flerstrengede sukre, som beta-amylasen kan nedbryde til det enkeltstrengede maltose. Mæsker man ved lave temperaturer (i det normale mæskningsområde fra 60 til 68 grader C) er det beta-amylasen, som arbejder og man opnår "tørre" øl med kun lidt restsukker. Mange anbefaler mæskning ved omkring 65-67 grader, hvor både alfa- og beta-amylasen er i aktion. Men vi går ikke altid den slagne vej, vi eksperimenterer med mæskningsprocessen for at få nogle gode og komplekse øl. Se f.eks. den grønne "Fermentability" linie, i diagrammet ovenfor, viser at der skabes flere ikke forgærbare sukkerarter i mæsken ved højere temperaturer.

Tag f.eks. vores Tjekke. Den mæskes ved 40, 60, 70 og 76 grader jvnf. mæskeskemaet. Se opskriften her. Ved 40 grader arbejder vi ikke direkte med alfa- og beta-amylasen, men forbereder masken til mæskningen. Vi arbejder med proteinerne, som forbedrer masken ved at gøre den tykkere og derved lader beta-amylasen arbejde længere tid med nedbrydningen. Ved 60 grader er det stort set kun beta-amylasen, som arbejder med at omdanne stivelse til sukkerarter. Ved 70 grader er det til gengæld stort set kun alfa-amylasen, som arbejder. alfa-amylasens resultater er svært nedbrydelige sukkerarter og giver derfor øllen krop og kompleksitet. De afsluttende 76 grader er for at denaturere amylaserne fuldstændigt. Læs alle detaljerne her.

Yderligere har ph-værdien af vandet vi bruger en indflydelse på enzymernes optimale funktion. Vandet i Silkeborg skulle efter sigende være optimal for ølbrygning - vi har i hvert fald aldrig gjort noget for at justere ph-værdien. beta-amylase arbejder optimalt ved en ph mellem 5,4 og 5,6 mens alfa-amylase arbejder mest optimalt ved 5,6 og 5,8. Det leder frem til at den optimale ph-værdi for vandet der skal anvendes i mæskningen er 5,6.

Enzymerne nedbrydes under aktiviteten efter nedenstående kurver.

Hittet her

Som det ses af kurverne falder mængden af amylase over tid men mest ved høje temperatuer.

Det praktiske problem

Hidtil har vi målt temperaturen ved at stikker et termometer i masken og målt temperaturen hvor termometeret nu er havnet. Det kan være i midten af gryden. Se nedenstående billede, hvor termometeret er cirka i midten af gryden.

Vi pumper mæsken rundt fra bunden af gryden til toppen og det løber ud over masken i en ringformet rør med mange små huller. Det vil sige at mæsken kommer varm fra bunden af gryden op langs ydersiderne af gryden og den centrale del af masken er meget lang tid om at opnå den ønskede temperatur. Dette kan naturligvis sikres ved hyppige omrøringer i gryden med den store grydeske, som ses på billede stikke ud til højre. Men hvis temperaturen er for lav fyrer vi varmelegemet i bunden af gryden indtil den ønskede temperatur sådan cirka er nået.

Den lokale opvarmning med et 1.800 W varmelegeme er voldsom og den mæsk der bliver pumpet op har en relativ høj temperatur. Den høje temperatur i bunden af gryden denaturerer enzymerne, som vi har lært ovenfor.

Derfor er det ønske at kunne opvarme på en sådan måde at vi ikke overskrider denatureringstemperaturerne på noget tidspunkt i mæsken.

Løsningen

Der er to dele i løsningen af problemet.

  • Den første del er en overvågning af temperaturen i bunden af gryden.
  • Den anden del er styring af varmelegemet, så der ikke fyres så stor en mængde energi af i det, når opvarmning er ønsket.

Temperaturovervågning

Klaus have en flok studerende til at se på i et projekt om det var muligt at udvikle et system, som kunne overvåge temperaturen og styre opvarmningen. De kom med et inspirerende projekt, som dog ikke helt fungerede efter hensigten. De elektronikingeniørstuderende havde set på forskellige sensorer, som kunne placeres inde i gryden nær varmelegemet - og det er sikkert også det mest optimale løsning, men forslaget lider under at disse sensorer er uforholdsvis dyre at anskaffe, da de skal fremstilles af materialer der er fødevaregodkendte. Vi taler om flere hundrede kroner for en enkelt temperatursensor. I øvrigt vil de også komme i konflikt med indsatssien, som bærer masken under mæskningen og ved efterfølgende rengøring af gryden.

Som projektleder foreslog Klaus at de så på alternative måder at måle temperaturen på og de endte med nogle påklistrede sensorer på gryde siderne. Det var sikkert et godt forslag, såfremt de kunne have fået det til at fungere, men det virkede ikke rigtigt.

Efter projekt har Klaus hittet nogle DS18B20 One-Wire sensorer på eBay til omkring 20 kroner stykket. Sensorerne er indbygget i et stålhylster og er lige knap 6 mm i diameter. Det er et rigtigt godt mål, for vores slanger er 8 mm slanger i ydre diameter og ca. 6 mm i indre diameter. Så en sensor smutter lige ind i et stykke plastikslange når der er omviklet et tyndt lag teflontape omkring sensoren for at tætne. Henrik har hittet et T-stykke med 3/4" gevind, som passer ind i tappehanen i bunden af gryden. Med diverse fittings kan vi isætte en sensor i udløbet fra gryden og måle temperaturen når der pumpes mæsk rundt. Vi måler derfor en temperatur, som er ret tæt på den temperatur, som er inde ved varmelegemet. Nok en smule lavere, da mæsken der løber ned fra sien blander sig med den opvarmede mæsk i bunden af gryden.

Sensoren ses her stikkende ud af T-stykket

Klaus har programmeret en simpel løsning på en mbed mikrocomputer, som kontinuert udlæser temperaturen i mæsken der cirkulerer. Og to forsøgsbryg viser at vi får en langt bedre proces ud af arbejdet. Vi hævede vores mæskeeffektivitet fra omkring 72-74% til 88-89% - ganske flot. Men det tog også noget længere at gennemføre mæskningen, da vi kun tændte for varmelegemet i ganske korte perioder af gangen for på den måde at stille og roligt at tilnærme os den ønskede temperatur.

Den samlede temperaturovervågning med mbed computeren stående på en gærspand

Mikrocomputeren set lidt tættere på - det er stadig på prototypestadiet

Styring af varmelegemet

De studerende, som Klaus havde i sving, fandt et elektronisk relæ, som kan bære den store effekt som varmelegemet kan aftage. Det kan styres af mikrocomputeren ved hjælp at et simpelt driverkredsløb, som kan håndtere den lidt større styrestrøm, som relæet skal have, men som mikrocomputeren ikke selv kan levere.

Det er tanken at der skal indbygges en reguleringssløjfe i temperaturovervågningen som kan tænde ganske kortvarigt for varmelegemet, så det set over tid arbejder med en afsat effekt på fra 150W til de maksimale 1.800W. Dette er i skrivende stund ikke udviklet.

Det er tanken at temperaturen aldrig må overstige denatureringstemperaturerne for henholdsvis beta-amylase og alfa-amylase afhængig af i hvilket temperaturområde der arbejdes. Det vil sige at varmelegemet aldrig vil få tilført mere effekt og dermed blive så varm at denatureringen sker. Dette skal ske ved en PWM (Pulse Width Modulation) styring af det elektroniske relæ - vi tænder og slukker for varmelegemet i ganske korte intervaller så den samlede afsatte effekt er på et ønsket niveau. Dette kunne for eksempel være at der tændes for varmelegemet i et sekund for at det så hviler i 9 sekunder, så har vi afsat 1/10 del af de 1.800W svarende til 180W i varmelegemet. Om periode tiderne lige bliver som her beskrevet kommer an på nogle praktiske prøver - det kan sagtens ende med at vi måske kører i nogle millisekunder i stedet. Men taget i betragtning at vi arbejder med at tænde og slukke for en 50Hz svingning bliver tiden ikke alt for lav. Periodetiden for 50 Hz er 20 ms.

Når projektet - forhåbentlig - er færdig i løbet af 2015 vil det blive offentliggjort her. Andre må gerne snuppe kode og i øvrigt hente inspiration af vores løsning. Vi hører naturligvis også gerne fra andre om tilsvarende løsninger til inspiration.

Erfaringer

Ovenstående tests gav hen over vinteren 2014-15 gode erfaringer og hævede generelt vores mæskeeffektivitet ganske meget. Samtidig fik vi bedre styr på temperaturstyringen - ganske vist manuelt, men det viste sig at vi ved at kortvarigt at tænde og slukke for varmelegemet kunne køre hele mæske processen i gryden uden at skulle ud i den lille kasserolle og uden at vi fik påbrændinger.

Klaus har efterfølgende arbejdet videre med et system.

Løsning på BeagleBone Black

BeagleBone Black (BBB) er en populær Open Source platform. Der knokler en 1 GHz ARM processor i modsætning til mBed'ens 40 MHz. Der kan derfor klares mange forskellige opgaver på samme tid på BBB fremfor mBed løsningen, hvor der skulle en server ind over. Hele løsningen skal implementeres på BBB.

BBB version 0.1

Foreløbig er der implementeret en temperaturmåling, PID regulator med en PWM styring af varmelegemet. Desuden er opskrifter indarbejdet.

Det hele er udviklet i Bonescript, som er JavaScript afviklet på Node.js med lidt ekstra tilpasninger til BBB.

En opskrift

{"Recipe":[
{"Comment":"Tjekke", "temp":0, "time":0},
{"Comment":"Infusion", "temp":43, "time":10},
{"Comment":"Mash", "temp":40, "time":20},
{"Comment":"Mash", "temp":60, "time":30},
{"Comment":"Mash", "temp":70, "time":30},
{"Comment":"Mash", "temp":76, "time":20},
{"Comment":"Mash", "temp":78, "time":10},
{"Comment":"Hop Cascade", "temp": 100, "time":30},
{"Comment":"Hop Saaz", "temp":100, "time":25},
{"Comment":"Hop Saaz", "temp":100, "time":5},
{"Comment":"Off", "temp":0, "time":0} ] }

En opskrift - her en Tjekke.

recipe.js

Behandling af opskriften:

// 
// Interface for the recipes
// Author   : Klaus Kolle
// Date     : 2015 02 22
// Revision : 0.0.3
//
 
var b = require ('bonescript');
 
// A recipe is an array of objects (in JSON format)
var recipe = [{}];
 
// Setting first step  
var curStep = 1;
 
// True if recipe read-in
var recipeAvailable = false;
 
// Returns current step in the recipe
var getRecipeItem = function  ()
{
  return recipe.Recipe[curStep].temp;
}
exports.getRecipeItem = getRecipeItem;
 
// Increments to the next step
var nextStep = function ()
{
  curStep = curStep + 1;
}
exports.nextStep = nextStep;
 
// Set the current step
var setCurStep = function (step)
{
  curStep = step;
}
exports.setCurStep = setCurStep;
 
// Reads a recipe from a disk file
var readRecipe = function ()
{
  console.log("Reading recipe.");
  recipeAvailable = false;
  b.readTextFile('/var/lib/cloud9/BrewController/tjekke.recipe.json', loadRecipe);
};
exports.readRecipe = readRecipe;
 
/**
 When the file has been read we receive data in x.data and errors in x.err
 The format in JSON of a recipe is shown below 
{"Recipe":[
{"Comment":"Tjekke", "temp":0, "time":0},
{"Comment":"Infusion", "temp":43, "time":10},
{"Comment":"Mash", "temp":40, "time":20},
{"Comment":"Mash", "temp":60, "time":30},
{"Comment":"Mash", "temp":70, "time":30},
{"Comment":"Mash", "temp":76, "time":20},
{"Comment":"Mash", "temp":78, "time":10},
{"Comment":"Hop Cascade", "temp": 100, "time":30},
{"Comment":"Hop Saaz", "temp":100, "time":25},
{"Comment":"Hop Saaz", "temp":100, "time":5},
{"Comment":"Off", "temp":0, "time":0} ] }
**/
 
function loadRecipe(x)
{
  recipe = JSON.parse(x.data);
/**/
  console.log(recipe);
  console.log("This is a : " + recipe.Recipe[0].Comment + " recipe.");
/**/
}
 
 
/*
console.log("temp: " + getRecipeItem().temp + " time: " + getRecipeItem().time);
nextStep();
console.log("temp: " + getRecipeItem().temp + " time: " + getRecipeItem().time);
nextStep();
console.log("temp: " + getRecipeItem().temp + " time: " + getRecipeItem().time);
nextStep();
console.log("temp: " + getRecipeItem().temp + " time: " + getRecipeItem().time);
*/

temperature.js

Temperaturen opsamles og midles af dette modul

// 
// Interface for the thermometer
// Author   : Klaus Kolle
// Date     : 2015 02 22
// Revision : 0.0.3
//
 
var b = require('bonescript');
var f = require('fs');
var r = require('./recipe.js');
var p = require('./powerController.js');
 
//console.log('Hello, Thermostate.');
 
var maxSamples = 5;
// Holds average temperature
var avgTemp = 0;
var getAvgTemp = function ()
{
  return avgTemp;
} 
exports.getAvgTemp = getAvgTemp;
 
// Holds the current temperature
var curTemp;
 
// Holds the last fifteen temperature samples
var tempSamples = [];
 
// Read from the 1-wire thermometer
// The 28-00000nnnnnnn will change depending of the device connected
// 
var oneWireDir;
 
// This is the starting point for this module
// It will read the temperature every interval seconds
var interval = 1000;
 
var startTempControl = function()
{
  console.log("Starting temp control.");
  setInterval(readTemp, interval);
} 
exports.startTempControl = startTempControl;
 
// We first have to locate the thermometer
locateThermometer();
 
function locateThermometer()
{
  var initialDir = '/sys/bus/w1/devices/';
  var regExpr = '28-00000';
  var dir = [];
  var i;
  // Get all files and directories in the dir
  var dirs = f.readdirSync(initialDir);
  // Did we get anything - if not the cape manager is probably not initialised
  // with the dtbo compiled device tree
  if (dirs.length > 0)
  {
    for (i = 0; i < dirs.length; i++)
    {
      // Only select the directories matching the pattern
      if(dirs[i].match(regExpr))
      {
        dir.push (dirs[i]);
      }
    }
    // Currently the code only accepts one thermometer
    oneWireDir = initialDir + dir + "/w1_slave";
  }
}
 
function readTemp() 
{
  // Callback function for the timer
  b.readTextFile(oneWireDir, extractTemp);
}
 
// The 1-wire returs this when reading the device
// klaus@klaus-BBB:~$ cat /sys/bus/w1/devices/28-000005a7ce64/w1_slave 
// a5 01 4b 46 7f ff 0b 10 f7 : crc=f7 YES
// a5 01 4b 46 7f ff 0b 10 f7 t=26312
// Therefore a split is needed. We need the string after the second =
 
function extractTemp(x) 
{
  // We receive the data i x
  if (x.data != '')
  {
    var stringToSplit = x.data;
    // Split at = - three resulting strings are returned
    var arrayOfStrings = stringToSplit.split('=');
    // We are only interesd in the last
    curTemp = (arrayOfStrings[2]) / 1000;
    calcAvg ();
  }
}
 
// In order not to have one or a few temperature measurements to introduce 
// instability in the control of the power an average of "maxSamples" measurements 
// are collected and averaged.
// 
// The average temperature is used to loacte in which temperature range the
// system currently operates
//
 
function calcAvg()
{
  if (tempSamples.length > maxSamples)
  {
    // Remove the first sample
    tempSamples.shift();
  }
  // Push the current measurement to the end of the array
  tempSamples.push(curTemp);
  var t = 0;
  for (i = 0; i < tempSamples.length; i++)
  {
    t = t + tempSamples[i];
  }
  var prev = avgTemp;
  // Calculate the average temperature from the samples
  avgTemp = t / tempSamples.length;
}

powerController.js

Reguleringen sker her

// 
// Interface for the power relay
// Author   : Klaus Kolle
// Date     : 2015 02 23
// Revision : 0.0.1
//
//
// Using the PWM output it is possible to control the amount of power
// applied to the brewer. The output controls an electronic relay.
//
 
var b = require('bonescript');
var t = require('./temperature.js');
var r = require('./recipe.js');
 
// Maximum power to apply:
// When temperature is below 60 C no more than one third power must be applied
// Even better only one fourth, but we have to experiment with that setting
// When temperature is between 60 C and 65 C two third power can be applied 
// At temperatures above 65 C full power can be given
//
// The temperature ranges
var tempPwrRange = [
                    {tempBelow:60, maxPwr:0.333}, 
                    {tempBelow:65, maxPwr:0.666}, 
                    {tempBelow:120, maxPwr:1.0}
                   ];
 
// PWMpin is the pin where the PWM is routed to
var PWMpin = 'P9_14';
 
// Startpoint of this module
//
// interval sets the frequency with which to control the heater
var interval = 5000;
 
var startPowerControl = function()
{
  console.log("starting the power control");
  // Just in case reset the PWM
  b.analogWrite(PWMpin, 0, 100);
  setInterval (calcPwrLvl, interval);
}
exports.startPowerControl = startPowerControl;
 
// These are the PID gains
var pGain = 10;
var iGain = 0.05;
var dGain = 30;
// These are the calculated states in the PID controller
var prevErr;
var iState = 0;
var dState = 0;
 
// calcPwrLvl calculates the necessary power level to apply using a PID controller
// The gain of the P, I and D element can be adjusted above
function calcPwrLvl()
{
  for (i = 0; i < tempPwrRange.length; i++)
  {
    if (t.avgTemp > tempPwrRange[i].tempBelow)
    { // Try next temp level until we reach one that is below
      continue;
    }
    // PID controller implementation
    var processValue = t.getAvgTemp();
    console.log("processValue: " + processValue);
    var setPoint = r.getRecipeItem();
    console.log("setPoint: " + setPoint);
    var error = setPoint - processValue;
    var pTerm = pGain * error;
    console.log("pTerm: " + pTerm);
    iState = iState + error;
    var iTerm = iState * iGain;
    console.log("iTerm: " + iTerm);
    var dTerm = (dState - processValue) * dGain;
    console.log("dTerm: " + dTerm);
    dState = processValue;
    var pwr = pTerm + iTerm + dTerm;
    var calcPwr = pwr / 10;
    console.log("calcPwr: " + calcPwr);
    setPwrLvl(calcPwr, tempPwrRange[i].maxPwr);
    return;
  }
}
 
// The setPwrLvl function set the requested power level by adjusting the
// PWM frequency
function setPwrLvl(calcPwr, maxPwr)
{
  console.log("maxPwr:" + maxPwr);
  if (calcPwr < 0)
  {
    b.analogWrite(PWMpin, 0, 1);
    console.log ("No power needed");
    return;
  }
  if (calcPwr < maxPwr)
  {
    b.analogWrite(PWMpin, calcPwr, 1);
    console.log ("Applying calculated pwr: " + calcPwr);
    return;
  }
  else
  { // Limit the power to maximum.
    b.analogWrite(PWMpin, maxPwr, 1);
    console.log ("Applying limited pwr   : " + maxPwr);
    return;
  }
 
}

main.js

Endelig opstartes det hele fra main

/**
 * Main runtime to start all the rest
 * Author   : Klaus Kolle
 * Date     : 2015 03 28
 * Revision : 0.0.1
 */
 
var b = require('bonescript');
var async = require('async');
var f = require('fs');
var r = require('./recipe.js');
var p = require('./powerController.js');
var t = require('./temperature.js');
 
// By using the async series a deterministic startup sequence is ensured
async.series([
  r.readRecipe(),
  t.startTempControl(),
  p.startPowerControl(),
  ], function(error, results) 
  {
    console.log(results);
  }
);

Der er stadig en del debug console.log's spredt ud i koden. Disse kan anvendes til at forstå hvordan koden fungerer og selvfølgelig også til fejlfinding. |<

Version 1.0 i C++

Nå men efter at have verificeret at det kunne lade sig gøre v.h.a. JavsScript blev jeg af forskellige årsager mistroisk på om JavaScript kunne håndtere tingene i rimelig real-tid. Desuden ville jeg gerne have logget data i en database - og lidt andet, så projektet greb om sig.

Jeg skrev derfor hele molevitten fuldstændigt om i C++. Det er detaljeret beskrevet i denne artikel Fil:Linux-Journal-2016-02-KK-article.pdf, som jeg skrev til Linux Journal.

Der mangler dog lidt om web-interfacet, som er bygget på en skabelon jeg hittede på nettet og HighCharts, som er et fantastisk bibliotek af grafsoftware med meget mere.

Jeg er ind imellem undervejs med at omskrive programmet, da det bærer noget præg af at være en prototype. Blandt andet vil jeg omskrive protokollen mellem web-interfacet og web-socket-serveren til at anvende JSON frem for min primitive protokol. Men i skyndingen blev det nu sådan i første omgang.

Personlige værktøjer