Brew heater controller. Programmed to mash grain and to boil wort

Dependencies:   mbed QEI UniGraphic

Files at this revision

API Documentation at this revision

Comitter:
dswood
Date:
Fri Jan 07 12:08:12 2022 +0000
Commit message:
First release. Works but is a little clunky.

Changed in this revision

Boil1.h Show annotated file Show diff for this revision Revisions of this file
Boil2.h Show annotated file Show diff for this revision Revisions of this file
Boil3.h Show annotated file Show diff for this revision Revisions of this file
MashProfile.h Show annotated file Show diff for this revision Revisions of this file
MashProfileFull.h Show annotated file Show diff for this revision Revisions of this file
Profiles.h Show annotated file Show diff for this revision Revisions of this file
QEI.lib Show annotated file Show diff for this revision Revisions of this file
UniGraphic.lib Show annotated file Show diff for this revision Revisions of this file
main.cpp Show annotated file Show diff for this revision Revisions of this file
mbed.bld Show annotated file Show diff for this revision Revisions of this file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Boil1.h	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,44 @@
+#ifndef PROFILES
+#define PROFILES
+#include "Profiles.h"
+#endif //Profiles
+
+
+struct Profile Boil1  {{
+/*Name*/                            "Boil 3 Hop adding  "},{{
+/*Profile.Element[0].GoalType*/     Boil,//Raise temp to Boil
+/*Profile.Element[0].MessageNo*/    0,
+/*Profile.Element[0].Temp*/         120.0,
+/*Profile.Element[0].Power*/        100,
+/*Profile.Element[0].Seconds*/      0},{  
+
+/*Profile.Element[1].GoalType*/     Time,
+/*Profile.Element[1].MessageNo*/    1,
+/*Profile.Element[1].Temp*/         120.0,
+/*Profile.Element[1].Power*/        35,
+/*Profile.Element[1].Seconds*/      300},{  
+
+/*Profile.Element[2].GoalType*/     Time, //55mins boil bittering hops
+/*Profile.Element[2].MessageNo*/    1,
+/*Profile.Element[2].Temp*/         120.0,
+/*Profile.Element[2].Power*/        35,
+/*Profile.Element[2].Seconds*/      3300},{ 
+
+/*Profile.Element[3].GoalType*/     Time, // Aroma +bitter hops
+/*Profile.Element[3].MessageNo*/    2,
+/*Profile.Element[3].Temp*/         120.0,
+/*Profile.Element[3].Power*/        35,
+/*Profile.Element[3].Seconds*/      900},{  
+
+/*Profile.Element[4].GoalType*/     Time, //Aroma hops
+/*Profile.Element[4].MessageNo*/    1,
+/*Profile.Element[4].Temp*/         120.0,
+/*Profile.Element[4].Power*/        35,
+/*Profile.Element[4].Seconds*/      300},{ 
+
+/*Profile.Element[5].GoalType*/     Time,//Aroma Hops
+/*Profile.Element[5].MessageNo*/    5,
+/*Profile.Element[5].Temp*/         120,
+/*Profile.Element[5].Power*/        35,
+/*Profile.Element[5].Seconds*/      600}}  
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Boil2.h	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,44 @@
+#ifndef PROFILES
+#define PROFILES
+#include "Profiles.h"
+#endif //Profiles
+
+
+struct Profile Boil2 {{
+/*Name*/                            "Boil 3 Hop adding++"},{{ //long version
+/*Profile.Element[0].GoalType*/     Boil,//Raise temp to Boil
+/*Profile.Element[0].MessageNo*/    0,
+/*Profile.Element[0].Temp*/         120.0,
+/*Profile.Element[0].Power*/        100,
+/*Profile.Element[0].Seconds*/      0},{  
+
+/*Profile.Element[1].GoalType*/     Time,
+/*Profile.Element[1].MessageNo*/    1,
+/*Profile.Element[1].Temp*/         120.0,
+/*Profile.Element[1].Power*/        35,
+/*Profile.Element[1].Seconds*/      300},{  
+
+/*Profile.Element[2].GoalType*/     Time, //90mins boil bittering hops
+/*Profile.Element[2].MessageNo*/    1,
+/*Profile.Element[2].Temp*/         120.0,
+/*Profile.Element[2].Power*/        35,
+/*Profile.Element[2].Seconds*/      5400},{ 
+
+/*Profile.Element[3].GoalType*/     Time, // Aroma +bitter hops
+/*Profile.Element[3].MessageNo*/    2,
+/*Profile.Element[3].Temp*/         120.0,
+/*Profile.Element[3].Power*/        35,
+/*Profile.Element[3].Seconds*/      900},{  
+
+/*Profile.Element[4].GoalType*/     Time, //Add Flocculant
+/*Profile.Element[4].MessageNo*/    1,
+/*Profile.Element[4].Temp*/         120.0,
+/*Profile.Element[4].Power*/        35,
+/*Profile.Element[4].Seconds*/      60},{ 
+
+/*Profile.Element[5].GoalType*/     Time,//Aroma Hops
+/*Profile.Element[5].MessageNo*/    5,
+/*Profile.Element[5].Temp*/         120,
+/*Profile.Element[5].Power*/        35,
+/*Profile.Element[5].Seconds*/      600}}  
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Boil3.h	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,44 @@
+#ifndef PROFILES
+#define PROFILES
+#include "Profiles.h"
+#endif //Profiles
+
+
+struct Profile Boil3 {{
+/*Name*/                            "Boil 3 Hop quick   "},{{ //long version
+/*Profile.Element[0].GoalType*/     Boil,//Raise temp to Boil
+/*Profile.Element[0].MessageNo*/    0,
+/*Profile.Element[0].Temp*/         120.0,
+/*Profile.Element[0].Power*/        100,
+/*Profile.Element[0].Seconds*/      0},{  
+
+/*Profile.Element[1].GoalType*/     Time,
+/*Profile.Element[1].MessageNo*/    1,
+/*Profile.Element[1].Temp*/         120.0,
+/*Profile.Element[1].Power*/        35,
+/*Profile.Element[1].Seconds*/      150},{  
+
+/*Profile.Element[2].GoalType*/     Time, //30mins boil bittering hops
+/*Profile.Element[2].MessageNo*/    1,
+/*Profile.Element[2].Temp*/         120.0,
+/*Profile.Element[2].Power*/        35,
+/*Profile.Element[2].Seconds*/      1800},{ 
+
+/*Profile.Element[3].GoalType*/     Time, // Aroma +bitter hops
+/*Profile.Element[3].MessageNo*/    2,
+/*Profile.Element[3].Temp*/         120.0,
+/*Profile.Element[3].Power*/        35,
+/*Profile.Element[3].Seconds*/      840},{  
+
+/*Profile.Element[4].GoalType*/     Time, //Aroma hops
+/*Profile.Element[4].MessageNo*/    1,
+/*Profile.Element[4].Temp*/         120.0,
+/*Profile.Element[4].Power*/        35,
+/*Profile.Element[4].Seconds*/      60},{ 
+
+/*Profile.Element[5].GoalType*/     Time,//Aroma Hops
+/*Profile.Element[5].MessageNo*/    5,
+/*Profile.Element[5].Temp*/         120,
+/*Profile.Element[5].Power*/        35,
+/*Profile.Element[5].Seconds*/      600}}  
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MashProfile.h	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,44 @@
+#ifndef PROFILES
+#define PROFILES
+#include "Profiles.h"
+#endif //Profiles
+
+
+struct Profile MashProfile {{
+/*Name*/                            "Mash Profile       "},{{
+/*Profile.Element[0].GoalType*/     Temp,//Raise temp to 50C
+/*Profile.Element[0].MessageNo*/    3,
+/*Profile.Element[0].Temp*/         50.0,
+/*Profile.Element[0].Power*/        100,
+/*Profile.Element[0].Seconds*/      0},{  
+
+/*Profile.Element[1].GoalType*/     Time,
+/*Profile.Element[1].MessageNo*/    6,
+/*Profile.Element[1].Temp*/         60.0,
+/*Profile.Element[1].Power*/        30,
+/*Profile.Element[1].Seconds*/      2200},{  
+
+/*Profile.Element[2].GoalType*/     Time,
+/*Profile.Element[2].MessageNo*/    6,
+/*Profile.Element[2].Temp*/         66.0,
+/*Profile.Element[2].Power*/        30,
+/*Profile.Element[2].Seconds*/      4600},{ 
+
+/*Profile.Element[3].GoalType*/     Time,
+/*Profile.Element[3].MessageNo*/    4,
+/*Profile.Element[3].Temp*/         70.0,
+/*Profile.Element[3].Power*/        35,
+/*Profile.Element[3].Seconds*/      2200},{  
+
+/*Profile.Element[4].GoalType*/     Time,
+/*Profile.Element[4].MessageNo*/    5,
+/*Profile.Element[4].Temp*/         50.0,
+/*Profile.Element[4].Power*/        1,
+/*Profile.Element[4].Seconds*/      600},{ 
+
+/*Profile.Element[5].GoalType*/     Temp,
+/*Profile.Element[5].MessageNo*/    0,
+/*Profile.Element[5].Temp*/         50.0,
+/*Profile.Element[5].Power*/        1,
+/*Profile.Element[5].Seconds*/      0} } 
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MashProfileFull.h	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,44 @@
+#ifndef PROFILES
+#define PROFILES
+#include "Profiles.h"
+#endif //Profiles
+
+
+struct Profile MashProfileFull {{
+/*Name*/                            "Mash Profile Full  "},{{
+/*Profile.Element[0].GoalType*/     Temp, //Raise temp to 50C
+/*Profile.Element[0].MessageNo*/    3,
+/*Profile.Element[0].Temp*/         50.0,
+/*Profile.Element[0].Power*/        100,
+/*Profile.Element[0].Seconds*/      0},{  
+
+/*Profile.Element[1].GoalType*/     Time, //Hold temp at 50C for 20 mins
+/*Profile.Element[1].MessageNo*/    6,
+/*Profile.Element[1].Temp*/         50.0,
+/*Profile.Element[1].Power*/        30,
+/*Profile.Element[1].Seconds*/      1200},{  
+
+/*Profile.Element[2].GoalType*/     Time, //Beta amylase
+/*Profile.Element[2].MessageNo*/    6,
+/*Profile.Element[2].Temp*/         60.0,
+/*Profile.Element[2].Power*/        30,
+/*Profile.Element[2].Seconds*/      2200},{ 
+
+/*Profile.Element[3].GoalType*/     Time, //both beta and alpha compramise
+/*Profile.Element[3].MessageNo*/    6,
+/*Profile.Element[3].Temp*/         66.0,
+/*Profile.Element[3].Power*/        30, //hest time is 100s per celsius
+/*Profile.Element[3].Seconds*/      3300},{  // 45 mins + 600s
+
+/*Profile.Element[4].GoalType*/     Time, //alpha amllase
+/*Profile.Element[4].MessageNo*/    4,
+/*Profile.Element[4].Temp*/         70.0,
+/*Profile.Element[4].Power*/        35,
+/*Profile.Element[4].Seconds*/      2100},{ 
+
+/*Profile.Element[5].GoalType*/     Temp,
+/*Profile.Element[5].MessageNo*/    5,
+/*Profile.Element[5].Temp*/         50.0,
+/*Profile.Element[5].Power*/        1,
+/*Profile.Element[5].Seconds*/      0}}  
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Profiles.h	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,17 @@
+
+enum goal { Null,Boil, Temp, Time};
+char *Message[7]={"","Add Hops","Add Flocculant","Add Grain","Sparge","Finished","Phase Done"};
+
+struct ProfileElement {
+    goal    GoalType;
+    uint8_t MessageNo;
+    float   Temp;
+    int8_t Power;
+    int     Seconds;
+};
+struct Profile {
+    char Name[20];
+    ProfileElement Element[6];
+};
+char *TopLevelMenu[3]={"Top Menu           ","Manual             ","Programmed         "};
+//                                                                  12345678901234567890  
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QEI.lib	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,1 @@
+https://os.mbed.com/users/aberk/code/QEI/#5c2ad81551aa
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/UniGraphic.lib	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,1 @@
+https://os.mbed.com/users/dswood/code/UniGraphic/#67b3634507da
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.cpp	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,771 @@
+/**
+
+ Spice modal
+ .SUBCKT NTCLE203E3103SB0 RN Rp  PARAMS: TOLR=0 TOLB=0
+X64 Rn Rp NTC_BASE Params:
++ w=-14.6571976
++ x=4798.842
++ y=-115334
++ z=-3730535
++ gth=0.0032 gth1 = 0.0000167
++ cth=0.032
++ a=-14.65719769
++ r25=10000
++ b=4798.842
++ c=-115334
++ d=-3730535
++ T0=273.15
++ TR={1+TOLR/100}
++ TB={1+TOLB/100}
+.ENDS
+
+ */
+
+
+/* Includes ------------------------------------------------------------------*/
+
+/* mbed specific header files. */
+/*
+GPIO_InitTypeDef GPIO_InitStruct;
+//Configure GPIO pin : PB14
+GPIO_InitStruct.Pin = GPIO_PIN_14;
+GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // digital Output
+GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
+HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+*/
+#include "mbed.h"
+#include "SSD1306.h"
+#include "string"
+
+#include "MashProfile.h"
+#include "MashProfileFull.h"
+#include "Boil1.h"
+#include "Boil2.h"
+#include "Boil3.h"
+#ifndef PROFILES
+#define PROFILES
+#include "Profiles.h"
+#endif //Profiles
+
+#define Height 32
+#define Width 132
+
+
+
+#include "QEI.h"
+
+//Firing delay in microseconds
+#define SyncToZeroCrossingDelayRise 115
+#define SyncToZeroCrossingDelayFall 115
+
+#define TriacPulseLength 250
+// Blinking rate in milliseconds
+#define BLINKING_RATE_MS  500
+#define Bias_Current 0.261  // Thermistor current in mA
+/*
+
+FIR filter designed with
+http://t-filter.appspot.com
+
+sampling frequency: 2000 Hz
+
+* 0 Hz - 100 Hz
+  gain = 1
+  desired ripple = 5 dB
+  actual ripple = 1.0675530446538501 dB
+
+* 600 Hz - 1000 Hz
+  gain = 0
+  desired attenuation = -60 dB
+  actual attenuation = -71.5928467318133 dB
+
+*/
+
+#define NumberOfTaps 9
+
+static double filter_taps[NumberOfTaps] = {
+    0.009246906411528302,
+    0.0484176527692072,
+    0.12764201806827455,
+    0.21665898750162413,
+    0.25663867508629506,
+    0.21665898750162413,
+    0.12764201806827455,
+    0.0484176527692072,
+    0.009246906411528302
+};
+//float PotBuf[NumberOfTaps];
+float TempBuf[NumberOfTaps];
+int CycleCount=0,StartT=0;
+bool FireTriac,Boiling,DisplayFlag;
+/*------- inputs ---------*/
+InterruptIn FallingEdge(D12);
+InterruptIn RisingEdge(D13);
+//AnalogIn Potentiometer(A0);
+// Thermistor (ntc) with a constant current flowing through it
+AnalogIn Thermistor(A1);
+//Rotary Encoder for entering values and navigation
+QEI wheel (D9, D8, NC, 20);
+DigitalIn Button(PC_9);
+InterruptIn ButtonPressed(PC_9);
+/*------- inputs -end-----*/
+
+/*------- outputs ---------*/
+//DigitalOut led(LED1);
+// Output to switch on the LED in the opto to fire the triac
+DigitalOut Triac(D10),OledReset(D6);
+// Test output to see timings and states on a scope
+DigitalOut Test(D15);
+// The screen
+SSD1306 MyOled(SPI_8, 10000000, D4, D5, D3, D2, D6, D7,"MyOled", Width, Height); // Spi 8bit, 10MHz, mosi, miso, sclk, cs, reset, dc
+// fixme clock may need to be 5MHz
+// 5000000 gives a clock of 3.125MHz (genius)
+// 10000000 gives 6.25
+// 12800000 gives 12.8Mhz still works
+// 20000000 gives 12.5Mhz
+// the nearest i can find to 8MHz is 10000000
+
+/*------- outputs -end-----*/
+
+
+char Buff[3][20];
+Timeout SyncDelay,TriacPulseTimeout,ReadTemperature;
+
+//float Pot;
+int FireCount;
+int Demand;
+float valT,LastTemp=0,LastError=0,Integrated=0;
+bool Rise,MainsOn;
+time_t now;
+Timer ElapsedTime;
+uint64_t LastTime,TempTime,DebounceTime,StartTime,DemandTime,CheckTime,KnobTime,CycleTime;
+struct Controls {
+    float Temperature;
+    int8_t PercentPower;
+    int16_t TimeSeconds;
+    int8_t ProfileNumber;
+    int8_t ElementNumber;
+    uint8_t MessageNo;
+    goal Goal;
+    char Message[20];
+
+};
+struct Controls MyControls= {25.0,50,0};
+struct Profile *ProfileArray[5]= { &MashProfile,&MashProfileFull,&Boil1,&Boil2,&Boil3};
+enum VariableType { Index, TempC, PwrW, TimeS };
+struct MenuType {
+    int8_t IndexVal;  //The current thing pointed to by the cursor
+    int8_t Number;
+    int8_t MaxIndex;
+    int8_t Offset;
+    VariableType CurrentType;
+    void EncoderHandler( bool Increment, VariableType ChangeType);
+    struct Controls *Cntrl; // points to the main loop controls
+    char *MenuText[8]; //8 lines of menus to start with
+    void (*NextMenu[8])();
+    bool Switch;
+};
+
+Serial Mypc(USBTX, USBRX, 115200);  //for debugging
+int64_t DebugValue;
+void FiringCounter(int Power);
+void PulseTriac();
+void SyncFall();
+void SyncRise();
+//char *Menu[2]={"Manual","Programmed"};
+
+int string_length(char* given_string)
+{
+    // variable to store the
+    // length of the string
+    int length = 0;
+    while (*given_string != '\0') {
+        length++;
+        given_string++;
+    }
+
+    return length;
+}
+void MenuType::EncoderHandler( bool Increment, VariableType ChangeType)
+{
+    int Multiplier=1,t;
+    t=ElapsedTime.read_ms()-KnobTime;
+    if (t<50) Multiplier=10;
+    else if (t<100) Multiplier=5;
+    else if (t<150) Multiplier=2;
+    KnobTime=ElapsedTime.read_ms();
+    switch (ChangeType) {
+        case Index:
+            if (Increment) IndexVal++;
+            else IndexVal--;
+            if (IndexVal>MaxIndex) IndexVal=0;
+            if (IndexVal<0) IndexVal=MaxIndex;
+            if ((IndexVal-Offset)>2) Offset=IndexVal-2;
+            if ((IndexVal-Offset)<0) Offset=IndexVal;
+            break;
+        case TempC:
+            if (Increment) Cntrl->Temperature=Cntrl->Temperature+(float)0.1*(float)Multiplier;
+            else Cntrl->Temperature=Cntrl->Temperature-(float)0.1*(float)Multiplier;
+            if (Cntrl->Temperature>120)Cntrl->Temperature=120;
+            if (Cntrl->Temperature<25)Cntrl->Temperature=25;
+            break;
+        case PwrW:
+            if (Increment) Cntrl->PercentPower+=Multiplier;
+            else Cntrl->PercentPower-=Multiplier;
+            if (Cntrl->PercentPower>100)Cntrl->PercentPower=100;
+            if (Cntrl->PercentPower<=0)Cntrl->PercentPower=1;
+            break;
+        case TimeS:
+            Multiplier=Multiplier*Multiplier;
+            if (Increment) Cntrl->TimeSeconds+=Multiplier;
+            else Cntrl->TimeSeconds-=Multiplier;
+            if (Cntrl->TimeSeconds>10800)Cntrl->TimeSeconds=10800;
+            if (Cntrl->TimeSeconds<0)Cntrl->TimeSeconds=0;
+            break;
+    }
+}
+
+struct MenuType MainMenu;
+void SetTopLevelMenu();
+void SetProgrammedMenu();
+void SetManualMenu();
+void ShowProgramMenu();
+void StartProfileMenu();
+void DoNothing();
+void AlterDataMenu();
+void LoadProfile(int Prof,int Ele);
+void SetProfile();
+void ProfileMenu();
+void GetTemperature();
+void DisplayNow();
+void DisplayMenu();
+void SetOutputSpeed();
+void PulseTriac()
+{
+    if (Triac==1) {
+        Triac=0;
+        Test=0; //fixme
+    } else {
+        Triac=1;// Fire the triac
+        Test=1;
+        TriacPulseTimeout.attach_us(&PulseTriac,TriacPulseLength);
+    }
+}
+void MenuSetup()
+{
+    MainMenu.Offset=0;
+    MainMenu.IndexVal=0;
+    MainMenu.Cntrl=&MyControls;
+    SetTopLevelMenu();
+}
+void SyncFall()
+{
+    int64_t t;
+    /* After many attempts to get this right t should be a uint64_t BUT
+    whan you do a comparison if(uint64_t> 99) it all goes to crap.
+    I think you end up with an overflow in the math used to evaluate it.
+    So only do comparicons with signed numbers int or float.
+    */
+    if (!Rise) return;
+    Rise=false;
+    t=ElapsedTime.read_us()-CycleTime;
+    DebugValue=t;
+    CycleTime=ElapsedTime.read_us();
+    if (DisplayFlag) return;
+    if ((t>20400)||(t<19600)) {
+        CycleCount=0;
+        MainsOn=false;
+    }
+    if (CycleCount<10) {
+        CycleCount++;
+        return;
+    }
+    MainsOn=true;
+    if (FireTriac) SyncDelay.attach_us(&PulseTriac,SyncToZeroCrossingDelayFall);
+    FiringCounter(Demand);
+    ReadTemperature.attach_us(&GetTemperature,9000); //read temp if mains on synced
+    // the noise generated on triac firing upsets the temperature measurment
+}
+void SyncRise()
+{
+    int64_t t;
+    Test=1;
+    if (Rise) return;
+    Rise=true;
+    t=ElapsedTime.read_us()-CycleTime;
+    if (DisplayFlag) return;
+    if ((t>10200)||(t<9800)) {
+        CycleCount=0;
+        MainsOn=false;
+    }
+    if (CycleCount<10) {
+        return;
+    }
+    Test=0;
+    if (FireTriac) SyncDelay.attach_us(&PulseTriac,SyncToZeroCrossingDelayRise);
+}
+
+float ReadResistance() // Read voltage across thermistor
+{
+
+    int i,j;
+    float tmp=0;
+    j=StartT;
+    TempBuf[StartT]=Thermistor;
+    for (i=0; i<NumberOfTaps; i++) {
+        if (j<0) j+=NumberOfTaps;
+        tmp+=TempBuf[j]*filter_taps[i];
+        j--;
+        if (j<0) j=NumberOfTaps-1;
+    }
+    StartT--;
+    if (StartT<0) StartT=NumberOfTaps-1;
+    /* tmp is the fraction 3.3V across thermistor.
+    Resistance = V/I
+    Resistance = (tmp*3.3V)/Bias_Current */
+    tmp=(tmp*(float)3.3)/(float)Bias_Current;  // Answer is in Kohm because current is in mA
+    return tmp;
+}
+float ResistanceToTemp(float Res)
+{
+    /* T0 = 25C = 298K
+    beta =3977
+    R0=10000 ohm     */
+    Res=Res+(float)0.0001;  //Res can't be zero
+    float temp;  // temp here is both temperary and temperature
+    temp=log(Res/(float)10)/(float)3977.0;  //beta from datasheet
+    temp=(float)1/(temp+(float)0.003354);  // 1/298.15
+    //temp=temp-(float)273.15 ;//kelvin to c conversion
+    /* ok so the temp is miles off due to things like the sensor being pressed
+    against the outside of the barrel.  Loosing heat to ambient etc.
+    So lets compensate for that with an offset and a scale factor. */
+    temp=(temp*(float)1.09)-(float)22;
+    temp=temp-(float)273.15 ;//kelvin to c conversion
+    return temp;
+}
+void GetTemperature()
+{
+    if (DisplayFlag) return; // The noise generated by the comms to the display causes 0.5C error
+    valT=ResistanceToTemp(ReadResistance());
+}
+void Pressed()
+{
+    int t;
+    t=ElapsedTime.read_ms()-DebounceTime;
+    DebounceTime=ElapsedTime.read_ms();
+    if (t<250) return;
+    //Mypc.printf("Index %d \n",MainMenu.IndexVal);
+    MainMenu.Number=MainMenu.IndexVal;
+    MainMenu.NextMenu[MainMenu.IndexVal]();
+}
+void setup()
+{
+    set_time(1256729737);  // Set RTC time to Wed, 28 Oct 2009 11:35:37
+    now=time(NULL);
+    // Mypc.printf("Setup");
+    Triac=0;  // Low to fire
+    FallingEdge.fall(&SyncFall); //falling mains voltage
+    RisingEdge.fall(&SyncRise);  //rising mains voltage
+    FireCount=0;
+    ButtonPressed.fall(&Pressed);
+    //SetOutputSpeed();
+    MyOled.BusEnable(true);//Chip select
+    MyOled.FastWindow(true);
+    MyOled.set_orientation(3);
+    MyOled.locate(0,0);
+    MyOled.set_font((unsigned char*) Terminal6x8,32,127,false);
+    MyOled.cls();
+    MyOled.BusEnable(false);//Chip select
+    ButtonPressed.fall(&Pressed);
+    ElapsedTime.start();
+    LastTime=ElapsedTime.read_ms();
+    DebounceTime=LastTime;
+    CycleTime=ElapsedTime.read_us();
+    TempTime=LastTime;
+    CheckTime=LastTime;
+    DemandTime=LastTime;
+    KnobTime=LastTime;
+    MenuSetup();
+}
+void SetOutputSpeed()
+{
+    GPIO_InitTypeDef GPIO_InitStruct;
+//Configure GPIO pin : PB_3 D3
+    GPIO_InitStruct.Pin = GPIO_PIN_3;
+    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // digital Output
+    GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
+    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+}
+
+void FiringCounter(int Power)
+{
+    int x;
+    x=Power;
+    if (x>100) x=100;
+    if (x<0) x=0;
+    if (FireCount<0) {
+        FireCount+=100-x;
+        FireTriac=true;
+    } else {
+        FireCount-=x;
+        FireTriac=false;
+    }
+
+}
+void SetTopLevelMenu()
+{
+
+    MainMenu.IndexVal=0;
+    MainMenu.MaxIndex=2;
+    MainMenu.Offset=0;
+    MainMenu.MenuText[0]=TopLevelMenu[0];
+    MainMenu.MenuText[1]=TopLevelMenu[1];
+    MainMenu.MenuText[2]=TopLevelMenu[2];
+    MainMenu.CurrentType=Index;
+    MainMenu.NextMenu[0]=&SetTopLevelMenu;
+    MainMenu.NextMenu[1]=&SetManualMenu;
+    MainMenu.NextMenu[2]=&SetProgrammedMenu;
+    MainMenu.NextMenu[3]=&SetTopLevelMenu;//Set it to something that is valid
+    MainMenu.NextMenu[4]=&SetTopLevelMenu;
+    MainMenu.NextMenu[5]=&SetTopLevelMenu;
+    MyControls.Temperature= 25.0;
+    MyControls.PercentPower=1;
+    MyControls.TimeSeconds=0;
+    MainMenu.Switch=true;
+}
+void SetProgrammedMenu()
+{
+    MainMenu.Offset=0;
+    MainMenu.IndexVal=0;
+    MainMenu.MaxIndex=5;//12345678901234567890
+    MainMenu.MenuText[0]="Previous Menu      ";
+    MainMenu.MenuText[1]=MashProfile.Name;
+    MainMenu.MenuText[2]=MashProfileFull.Name;
+    MainMenu.MenuText[3]=Boil1.Name;
+    MainMenu.MenuText[4]=Boil2.Name;
+    MainMenu.MenuText[5]=Boil3.Name;
+
+    MainMenu.NextMenu[0]=&SetTopLevelMenu;
+    MainMenu.NextMenu[1]=&SetProfile;
+    MainMenu.NextMenu[2]=&SetProfile;
+    MainMenu.NextMenu[3]=&SetProfile;
+    MainMenu.NextMenu[4]=&SetProfile;
+    MainMenu.NextMenu[5]=&SetProfile;
+
+    MainMenu.CurrentType=Index;
+    MainMenu.Switch=true;
+}
+void SetManualMenu()
+{
+    MainMenu.IndexVal=0;
+    MainMenu.MaxIndex=3;
+    MainMenu.Offset=0; // 1234567890123456789
+    MainMenu.MenuText[0]="Manual Menu        ";
+    MainMenu.MenuText[1]="Temperature";
+    MainMenu.MenuText[2]="Power Level";
+    MainMenu.MenuText[3]="Time       ";
+    MainMenu.NextMenu[0]=&SetTopLevelMenu;
+    MainMenu.NextMenu[1]=&AlterDataMenu;
+    MainMenu.NextMenu[2]=&AlterDataMenu;
+    MainMenu.NextMenu[3]=&AlterDataMenu;
+    MainMenu.NextMenu[4]=&DoNothing;
+    MainMenu.Switch=false;
+}
+void ShowProgramMenu()
+{
+    MainMenu.Offset=0;
+    MainMenu.IndexVal=0;
+    MainMenu.MaxIndex=2;
+    MainMenu.MenuText[0]="Previous Menu      ";
+    MainMenu.MenuText[1]=ProfileArray[MyControls.ProfileNumber]->Name;
+    MainMenu.MenuText[2]="Start              ";
+
+    if (MyControls.Goal==Null) {
+        MainMenu.MenuText[3]="";
+        MainMenu.NextMenu[3]=&SetProgrammedMenu;
+    } else {//profile loaded
+        MainMenu.MenuText[3]="Current Profile    ";
+        //                    1234567890123456789
+        MainMenu.NextMenu[3]=&ProfileMenu;
+        MainMenu.MaxIndex=3;
+    }
+    MainMenu.MenuText[4]="";
+    MainMenu.MenuText[5]="";
+    MainMenu.NextMenu[0]=&SetProgrammedMenu;
+    MainMenu.NextMenu[1]=&DoNothing;
+    MainMenu.NextMenu[2]=&StartProfileMenu;
+
+    MainMenu.NextMenu[4]=&SetProgrammedMenu;
+    MainMenu.NextMenu[5]=&SetProgrammedMenu;
+    MainMenu.CurrentType=Index;
+    MainMenu.Switch=true;
+
+}
+void StartProfileMenu()
+{
+    LoadProfile(MyControls.ProfileNumber,0); //load the first element of the selected profile
+    ProfileMenu();
+}
+void ProfileMenu()
+{
+    MainMenu.IndexVal=0;
+    MainMenu.MaxIndex=3;
+    MainMenu.Offset=0;                       //
+    MainMenu.MenuText[0]=MyControls.Message;
+    MainMenu.MenuText[1]="Temperature";
+    MainMenu.MenuText[2]="Power Level";
+    MainMenu.MenuText[3]="Time       ";
+    MainMenu.NextMenu[0]=&ShowProgramMenu;
+    MainMenu.NextMenu[1]=&AlterDataMenu;
+    MainMenu.NextMenu[2]=&AlterDataMenu;
+    MainMenu.NextMenu[3]=&AlterDataMenu;
+    MainMenu.NextMenu[4]=&DoNothing;
+    MainMenu.Switch=false;
+}
+void AlterDataMenu()
+{
+    if (MainMenu.CurrentType==Index) switch (MainMenu.IndexVal) {
+            case 1:
+                MainMenu.CurrentType=TempC;
+                break;
+            case 2:
+                MainMenu.CurrentType=PwrW;
+                break;
+            case 3:
+                MainMenu.CurrentType=TimeS;
+                break;
+            default:
+                MainMenu.CurrentType=Index;
+        } else MainMenu.CurrentType=Index;
+}
+void DoNothing() //The purpose of this function is assign a function pointer to it rather than something wrong or a crash
+{
+}
+void SetProfile()
+{
+    int i=MainMenu.Number-1;
+    if (i<0)i=0;
+    if (i>4)i=4;//sanitize
+    MyControls.ProfileNumber=i;
+    strncpy(MyControls.Message,ProfileArray[i]->Name,20);
+    MyControls.Goal=Null;
+    ShowProgramMenu();
+
+
+}
+void LoadProfile(int Prof,int Ele)
+{
+    MyControls.Temperature= ProfileArray[Prof]->Element[Ele].Temp;
+    MyControls.PercentPower=ProfileArray[Prof]->Element[Ele].Power;
+    MyControls.TimeSeconds=ProfileArray[Prof]->Element[Ele].Seconds;
+    MyControls.Goal=ProfileArray[Prof]->Element[Ele].GoalType;
+    MyControls.ProfileNumber=Prof;
+    MyControls.ElementNumber=Ele;
+    MyControls.MessageNo=ProfileArray[Prof]->Element[Ele].MessageNo;
+    StartTime=ElapsedTime.read();
+    /*Mypc.printf("T %.1f P %d Sec %d Prof %d Ele %d\n",
+               MyControls.Temperature,
+                MyControls.PercentPower,
+                MyControls.TimeSeconds,
+                MyControls.ProfileNumber,
+                MyControls.ElementNumber);
+    */
+}
+
+void DisplayMenu()
+{
+    DisplayFlag=true; //This flag is used to stop generating new interupts
+    wait_us(SyncToZeroCrossingDelayRise+TriacPulseLength+25); //wait for interupts to finish
+    MyOled.BusEnable(true);//Chip select
+    bool Highlight=false;
+    //We have a 32 pixecl height and 9 pixcel font.  3 lines 10 spacing
+    if (MainMenu.Switch) {
+        for (int i=0; i<3 ; i++) {
+            MyOled.locate(6,(i*10)+1);
+            if (MainMenu.IndexVal==(i+MainMenu.Offset)) {
+                MyOled.background(White);
+                MyOled.foreground(Black);
+            } else {
+                MyOled.background(Black);
+                MyOled.foreground(White);
+            }
+            MyOled.printf(MainMenu.MenuText[i+MainMenu.Offset]);
+            MyOled.background(Black);
+            MyOled.foreground(White);
+        }
+    } else  {
+        time_t t;
+        if (MainMenu.CurrentType==TempC)sprintf(Buff[0],"  %6.1f ",MyControls.Temperature);
+        else sprintf(Buff[0],"  %6.1f ",valT);
+        if (MainMenu.CurrentType==PwrW)sprintf(Buff[1],"  %5d%c ",MyControls.PercentPower,38);
+        else sprintf(Buff[1],"  %5d%c ",Demand,38);
+        if (MainMenu.CurrentType==TimeS) {
+            t=MyControls.TimeSeconds;
+            strftime(Buff[2],10,"%T ",localtime(&t));
+        } else {
+            t=(int)ElapsedTime.read()-StartTime;
+            strftime(Buff[2],10,"%T ",localtime(&t));
+        }
+        for (int i=0; i<3 ; i++) {
+            MyOled.locate(6,(i*10)+1);
+            if (MainMenu.IndexVal==(i+MainMenu.Offset)) {
+                Highlight=true;
+            } else {
+                Highlight=false;
+            }
+            if (Highlight&&MainMenu.CurrentType==Index) {
+                MyOled.background(White);
+                MyOled.foreground(Black);
+                MyOled.printf(MainMenu.MenuText[i+MainMenu.Offset]);
+            } else {
+                MyOled.background(Black);
+                MyOled.foreground(White);
+                MyOled.printf(MainMenu.MenuText[i+MainMenu.Offset]);
+            }
+            if (Highlight&&MainMenu.CurrentType!=Index) {
+                MyOled.background(White);
+                MyOled.foreground(Black);
+                if ((i+MainMenu.Offset>=1)&&(i+MainMenu.Offset<=3))MyOled.printf(Buff[i+MainMenu.Offset-1]);
+            } else {
+                MyOled.background(Black);
+                MyOled.foreground(White);
+                if ((i+MainMenu.Offset>=1)&&(i+MainMenu.Offset<=3))MyOled.printf(Buff[i+MainMenu.Offset-1]);
+            }
+        }
+    }
+    MyOled.BusEnable(false);//Chip select
+    DisplayFlag=false;
+}
+int CalcDemand() //return percent power
+{
+    float ErrorSignal, Differential;
+    int power;
+    ErrorSignal=MyControls.Temperature-valT;
+    if (abs(ErrorSignal)<1)Integrated+=ErrorSignal;
+    else Integrated=0;
+    if (Integrated>20)Integrated=(float)20;
+    if (Integrated<-1)Integrated=(float)-1;
+    Differential=ErrorSignal-LastError;
+    LastError=ErrorSignal;
+    power=(int)(((float)70*ErrorSignal)+((float)0.6*Integrated)+(45*Differential));
+    // Mypc.printf("Error %.2f Int %.2f Dif %.2f\n\r", ErrorSignal,Integrated,Differential);
+    MainsOn ? Mypc.printf("Mains On\n\r") : Mypc.printf("Mains Off\n\r");
+    Mypc.printf("DebugValue %d\n\r",DebugValue);
+    Mypc.printf("Elapsed time %d\n\r",ElapsedTime.read_us());
+
+    if (power<0)power=0;
+    if (power>MyControls.PercentPower)power=MyControls.PercentPower;
+    return power;
+}
+void DisplayMessage()
+{
+    int x,y,z;
+    z=MyControls.MessageNo; //convert to int before compare
+    if (z>7)MyControls.MessageNo=0;
+    if (z==0) {
+        strncpy(MyControls.Message,ProfileArray[MyControls.ProfileNumber]->Name,20);
+    } else {
+        y=string_length(Message[MyControls.MessageNo]) ;
+        for (x=0; x<18; x++) {
+            if (x<y)MainMenu.MenuText[0][x]=Message[MyControls.MessageNo][x];
+            else MainMenu.MenuText[0][x]=' ';
+        }
+        MainMenu.MenuText[0][19]='\0';
+    }
+}
+int main()
+{
+    Mypc.printf("Starting Main\n\r");
+    setup();
+    int Position=wheel.getPulses();
+    int UpdateTime=750;
+    uint64_t TempTime=0;
+    int64_t t;// time is uint64_t but int64_t used for comparison in if statement
+    while (true) { //main loop
+        if (Position!=wheel.getPulses()) {
+            MainMenu.EncoderHandler( Position<wheel.getPulses(), MainMenu.CurrentType);
+            Position=wheel.getPulses();
+            UpdateTime=250; //Quick display refresh when knob moved
+            //Mypc.printf("Pos %d \n",Position);
+            //DisplayMenu();
+        }
+        /*Had a problem where mains detection was dropping out here.
+        The sum ElapsedTime.read_ms()-CycleTime was sometimes =0.
+        But 0>75 is true. eg if ((ElapsedTime.read_ms()-CycleTime)>75) would be true
+        if the sum result was 0. Doing the sum first was ok.  Had the same problem with Debounce.
+        */
+        t=ElapsedTime.read_us()-CycleTime;
+        if (t>75000) { //mains not proven. Read temp.  When mains is on this is done synced to mains
+            CycleCount=0;
+            MainsOn=false;
+            GetTemperature();
+        }
+        t=ElapsedTime.read_ms()-LastTime;
+        if (t>UpdateTime) {
+            LastTime=ElapsedTime.read_ms();
+            UpdateTime=750;
+            DisplayMenu();
+        }
+        t=ElapsedTime.read_ms()-TempTime;
+        if (t>15000) { //upped to 15s which will give a smaller temp rise by averaging over a longer period
+            if (valT>99.0f) {
+                if (valT-LastTemp<0.15f)Boiling=true;
+                else    Boiling=false; // still rising
+            } else  Boiling=false;  //too cold
+            LastTemp=valT;
+            TempTime=ElapsedTime.read_ms();
+        }
+        t=ElapsedTime.read_ms()-DemandTime;
+        if (t>3000) {
+            Demand=CalcDemand();
+            DemandTime=ElapsedTime.read_ms();
+        }
+        t=ElapsedTime.read_ms()-CheckTime;
+        if (t>3000) {
+            CheckTime=ElapsedTime.read_ms();
+            switch (MyControls.Goal) {
+                //enum goal { Null,Boil, Temp, Time};
+                case Boil:
+                    if (Boiling ) {
+                        DisplayMessage();
+                        if (MyControls.ElementNumber<5) {
+                            MyControls.ElementNumber++;
+                            LoadProfile(MyControls.ProfileNumber,MyControls.ElementNumber);
+                        } else {
+                            MyControls.Temperature=25.0;
+                            MyControls.PercentPower=0;
+                            MyControls.TimeSeconds=0;
+                        }
+                    }
+                    break;
+                case Temp:
+                    if (abs(valT-MyControls.Temperature)<(float)0.25) {
+                        DisplayMessage();
+                        if  (MyControls.ElementNumber<5) {
+                            MyControls.ElementNumber++;
+                            LoadProfile(MyControls.ProfileNumber,MyControls.ElementNumber);
+                        } else {
+                            MyControls.Temperature=25.0;
+                            MyControls.PercentPower=0;
+                            MyControls.TimeSeconds=0;
+                        }
+                    }
+                    break;
+                case Time:
+                    t=ElapsedTime.read()-StartTime;
+                    if (t>MyControls.TimeSeconds) {
+                        DisplayMessage();
+                        if(MyControls.ElementNumber<5) {
+                            MyControls.ElementNumber++;
+                            LoadProfile(MyControls.ProfileNumber,MyControls.ElementNumber);
+                        } else {
+                            MyControls.Temperature=25.0;
+                            MyControls.PercentPower=0;
+                            MyControls.TimeSeconds=0;
+                        }
+                    }
+                    break;
+            }
+        }//goal test
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mbed.bld	Fri Jan 07 12:08:12 2022 +0000
@@ -0,0 +1,1 @@
+https://os.mbed.com/users/mbed_official/code/mbed/builds/65be27845400
\ No newline at end of file