Crane's Never ending Brewery Upgrades

Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum

Help Support Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.
I bought several of these IFM SM6004 flow/temp sensors off of ebay for $40 a piece last year. Only problem is that the BSP adapters that you need pretty much double the cost of these. However, I have fought enough "npt" threads on cheap stainless from china that I decided to use a crap ton of teflon tape with some pipe dope and we can get a good seal with a BPT TC adapter. Sure enough they hold up to my garden hose water pressure for a leak test.

IMG_20191003_140828.jpg
 
I have also force myself to plant my ass at the desk most of today to work on updating/revising the brew space background image. Now I need to re-layout all the device elements over the new background image.

Brewery_layout_8.png
 
I bought several of these IFM SM6004 flow/temp sensors off of ebay for $40 a piece last year. Only problem is that the BSP adapters that you need pretty much double the cost of these. However, I have fought enough "npt" threads on cheap stainless from china that I decided to use a crap ton of teflon tape with some pipe dope and we can get a good seal with a BPT TC adapter. Sure enough they hold up to my garden hose water pressure for a leak test.

View attachment 673221

I looked at this sensor a lot... it is a really nice unit. Ultimately didn't go this route because it can't get a sufficient pulse rate to generate a reasonable resolution at ~1 lpm rate.

BTW I kinda like the green background best... not usually a green guy but I think its a good contrast. Nice work!!
 
I looked at this sensor a lot... it is a really nice unit. Ultimately didn't go this route because it can't get a sufficient pulse rate to generate a reasonable resolution at ~1 lpm rate.

BTW I kinda like the green background best... not usually a green guy but I think its a good contrast. Nice work!!

Not sure what you mean about pulse rate, these are 4-20mA outputs. The measuring range shows 0.1 lpm for the minimum on:

https://www.ifm.com/us/en/product/SM6004

Capture.PNG
 
I bought several of these IFM SM6004 flow/temp sensors off of ebay for $40 a piece last year. Only problem is that the BSP adapters that you need pretty much double the cost of these. However, I have fought enough "npt" threads on cheap stainless from china that I decided to use a crap ton of teflon tape with some pipe dope and we can get a good seal with a BPT TC adapter. Sure enough they hold up to my garden hose water pressure for a leak test.

View attachment 673221

Based their specs I have wondered about their ability to handle close to boiling temps. This has held me back from getting them myself. I suppose that a short time at a high temp will not break them all at once?
 
There are several different reasons why the upper or lower limits of a sensor are determined by the manufacturer. A lot of the time its not because the sensor will be destroyed and stop working. There is usually some sort of margin factor built in as well. With all that being said, there is a possibility that they will degrade over time from extended exposure above they max rating. I guess I will find out if that is the case down the road.

I did some testing before with these flow meters above their rated temp. However, I did some more testing with them yesterday to see where they max out at. In my system these will see a max of 170-180F. These sensors work up to 211.5 before going into "Over Load". However, they start to drift above 175F by about +/-2F up to about 185F. Above that the drift increased. The parts of the brewing process where I will be measuring temps above 175F are not super critical to have a precise temperature, so I can compensate for this drift.

You can adjust the output scaling for these, but the max (20mA) output is 176F. No worries, the sensor continues to output above 20mA. I measured 5.36V across a 250.4 Ohm resistor, so Ohm's law tells me that its outputting 21.4mA as its max at 211.5F. So if I use a 233 Ohm or lower resistor value I will not exceed the ADC input of the Mega. Time to place a Digikey order. I needed a few other things from there as well.

Setup measuring the voltage across a 250 Ohm resistor. Resistor is placed between the output of the sensor to GND to convert the 4-20mA output to 1-5V.
IMG_20200403_180112.jpg


Max output temp and voltage.
IMG_20200403_180136.jpg


Sensor goes into "Over Load" when it measures 212F or above. It wasn't actually boiling inside there.
IMG_20200403_180212.jpg
 
Last edited:
Amazing! You must really like process control. Have you thought about selling your services to full size breweries?

And an attempt to make this a not completely useless comment: I have seen good results epoxying permanent threaded connections instead of using PTFE tape.
 
Amazing! You must really like process control. Have you thought about selling your services to full size breweries?

And an attempt to make this a not completely useless comment: I have seen good results epoxying permanent threaded connections instead of using PTFE tape.

It's not so much just process control, but automation in general. I've implemented some sort of automation at all my "career" jobs. Most of it has been for testing of electronics/computers. Most recently I am working on automating our circuit board design flow.

Also, thanks for the epoxy suggestion. That is a good middle ground between PTFE and silver soldering.
 
More quarantine funtimes today. Got my 232 Ohm resistors from Digikey yesterday. Took 2 days longer than normal, so not bad considering. I snuck out to the garage in between work meetings today and soldered in the new resistors to the mega screw terminal board.

I wanted to have a way to validate the temperature output from the IFM sensor with the new 232 Ohm resistor, so I slapped this together. It has a thermistor in it, and I can connect it to my "Aux Temp" input on my control panel. I can now insert this temp probe any place in my plumbing where a 90 degree elbow is to measure loop temperature.

IMG_20200410_134716.jpg


In this case, I put it right after the IFM sensor.
IMG_20200410_173536.jpg

MVIMG_20200410_160621.jpg


IMG_20200410_160700.jpg


And after some minor fine tuning we are pretty much spot on. IFM sensor and thermistor are within 0.1F of each other. IFM BruControl Element matches IFM display. Let's drink a beer to that.
IMG_20200410_162126.jpg


IMG_20200410_162118.jpg


Might as well try it here as well.
IMG_20200410_174626.jpg
 
Worked on getting the Hops Boss fully integrated into the brew process yesterday. This was not completely straight forward as one would hope. I needed to parse all the hop additions, combine some, split some into multiple drops, and ignore others like dry hop additions. I also need to differentiate from boil and whirlpool additions. If my recipe calls to add 1oz each of 4 different hops at 20 min in the boil, this will show up as 4 separate line items in the recipe, but needs to be combined into a single motor shell drop. Additionally, when I do 10 gallon batches I will have times where I drop more than 8oz (motor shell capacity) at a time. Here 1 or more line items on the recipe needs to turn into multiple motor shell drops. Unfortunately, BruControl doesn't support arrays, array indexing, or looping structures. You could accomplish this in BruControl, but it would be ugly to code, and would take a lot more lines of code than doing it in javascript.

I already use node-red to read in my Beersmith recipe and upload that to BruControl. Since the parsing of this information has to happen when the recipe is read in, it was pretty straight forward to add some more code in node-red to accomplish this. Below is the main section of code that iterates through all of the individual hop additions and translates that into the required motor shell drop times and types (boil vs whirlpool). Additionally, I parse the whirlpool hop additions, to set the whirlpool rest time. As you can see I have while loops buried inside of if statements inside of a for loop.


parse_hops.PNG


 
You guys kill me! With your automatic stuff. I love it, but I have a hard time understanding gravity!!! :ghostly:
 
The second major revision of the plumbing is getting wrapped up. Here are the changes:

1. swapped out my paddle wheel flow meters with the IFMs.
2. added some valves to drain the MT and BK during CIP.
3. added the Hops Boss
4. added a path in the BK re-circulation loop to bypass the chiller.
5. swapped out my DC pumps for AC pumps and added proportional valves for control flow.

I was not happy with the performance and control-ability of DC pumps. For example, when I am sparging the wort from the MT flows into the bottom of my BK, not the re-circulation return port. At the beginning of sparging the height difference between the MT and BK results in too high of a flow rate with the pump turned off. So I needed something else to restrict the flow rate.

Additionally, I found that the DC pumps had minimum duty cycle limits for the PWM outputs. However, the duty cycle necessary to get the pump spinning again is quite a bit higher. Without restricting the output of the valve, the resulting minimum flow rates were higher than I wanted.

From the testing I have done so far, these issues have been addressed with the AC pumps and proportional valves. The deadband flow control scripts I have, are working very well at quickly responding to target flow changes, as well as accurately maintaining the target flow once its there.

IMG_20200418_091242.jpg


IMG_20200418_091255.jpg


IMG_20200405_095715.jpg


IMG_20200401_184849.jpg


Finally, I thought it was inconsequential to leave a gap in the boards on the brew stand. There are over a dozen TC gaskets chilling under there now, and will stay there until I move this beast.
 
Last edited:
You guys kill me! With your automatic stuff. I love it, but I have a hard time understanding gravity!!! :ghostly:

Isn't that whats to great about this hobby? We can all participate at the level we are comfortable with and still make great beer. There are so many different areas to go hog wild and geek out on; from automation, to hi-tech home yeast wrangling labs. However, none of that is necessary to make a good batch.
 


You may notice in this video that there is a moment where the 2 yellow/green LEDs turn gray. I didn't realize this at the time, but the Arduino Mega in my second panel was being reset because of the butterfly valve.

I soldered some resistors and capacitors I had on hand to create some RC snubbers for the valve's power lines, but that didn't completely take care of the problem. The Mega would still occasionally reset while the valve was moving. So something else is also going on here. I am using the feed back signals from the valve to know when its fully open/closed. I use these in my script to prevent the script from advancing too soon without having to put in arbitrary sleep statements.

Turns out the motors on these valves put out a significant amount of EMI/EMC. This EMC was being coupled to the open/close feedback signals going to the digital inputs on the Mega. I had some of these ferrite beads sitting around that I ripped out of old VGA computer monitor cables. I put one around the feedback signals and that took care of the Mega resetting while the motor was moving.

zmf-700a4.jpg
 
Which DC pumps were you running that you found you were not satisfied with their performance?

Some of the 24V tan guys. I also bought some March 24VDC pumps off Ebay a while back. They came without heads, so I bought some chugger heads for them.

IMG_20200418_125631.jpg


DC pumps definitely need closed loop feedback and a comfort knowing you can't start them at slow speeds.

Correct. These things can be accounted for and were with my deadband flow control scripts. However, given that I needed to add a way to restrict the flow during sparging (proportional valve), it made sense to switch to AC pumps. AC pumps also have a higher max flow rate which will help to reduce heat stratification in the HLT, and better whirlpool cone formation in the BK.
 
Nice suggestion. I think I might put this on repeat in the garage tomorrow while this system makes some wort for me.

Nice! I wanted as my ring tone but could not pull that off. Glad you could use!
 
I cut off one of the handles on the MT and cut the corners off the base plate of my crappy barley crusher/drill combo, so I can mill directly into the MT. Definitely cut down on the amount of grain dust in the air.

IMG_20200418_163238.jpg


I'm pretty sure I have a new best friend and its name is CIP.

IMG_20200419_122830.jpg


I brewed today... So my garage is cleaner than ever.
 
I cut off one of the handles on the MT and cut the corners off the base plate of my crappy barley crusher/drill combo, so I can mill directly into the MT. Definitely cut down on the amount of grain dust in the air.

View attachment 676486

I'm pretty sure I have a new best friend and its name is CIP.

View attachment 676488

I brewed today... So my garage is cleaner than ever.

You're letting me down with that drill attached to your mill, at least mount a maker motor to it o_O. Yeah CIP is the first thing I got up and running once I finished my brew day script, it makes life much better.
 
You're letting me down with that drill attached to your mill, at least mount a maker motor to it o_O. Yeah CIP is the first thing I got up and running once I finished my brew day script, it makes life much better.
A new mill and motor are on my list of the never ending upgrades...
 
I plan on using it to acidify strike and sparge water as well as dosing for the acid rinse at the end of the CIP routine. It uses an SNx4HC595 8-Bit Shift Register to select which pump to activate (0-3) and 4 ULN2003A darlington transistor arrrays to drive the pumps. You could write a script in BruControl to bit bang digital IOs, but I opted to use my nodeMCU data exchange solution here instead. I have 3 globals set in BruControl, DosingPumpSend, DosingPump, and DosingTime. DosingPumpSend is a boolean to tell it to start pumping. Once its finished pumping it will write false back to this global so BruControl can know when its done in case I dose multiple things back to back. It reads DosingPump to get the index of what pump to activate and DosingTime to know how long to keep the pump on for. To calibrate this, I will do the same as the original control board where I will time how long it takes to fill a known volume. I will have a script in BruControl that will convert a global for dosing volume to the DosingTime global.

Code:
/**********************************************************************************
* Dosing Pump BruControl nodeMCU http bridge
* Version: 1
*
* BruControl nodeMCU http bridge framework Version: 0.2
*
* This framework provides a bridge between a nodeMCU and BruControl using BruControl's
* http GET/PUT data exchange protocol.
*   Basic flow
*     1. Every checkInterval each BruControl global variable in the globalsToRead array is read
*        and its value is placed in the corresonding index in the globalReadValues array.
*     2. Every calcInterval user defined code is ran to:
*           process inputs
*           interface with devices
*           calculate return values
*     3. Every sendInterval each BruControl global variable in the globalsToWrite array's
*        corresponding value in the globalValuesToWrite array is sent back to BruControl.
*
* General use of this framework.
*     Place your user code within the @@@@@@@@@@@ sections
*     There should be no need to modify the code outside of these sections.
*
************************************************************************************/


/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
* Interfaces to Jebao DP-4 auto dosing pump.
*
* Polling BruControl for DosingPumpSend updates
*   DosingPumpSend is a boolean.  Transitioning from false to true triggers triggers
*   the following:
*     1. Reads DosingPump value (integer 0-3 for what pump to turn on)
*     2. Reads DosingTime value (integer representing how many milliseconds to turn
*        the pump on for.
*     3. Doses the pump for the time called for.
*     4. Writes to DosingPumpSend to indicate that the dosing is complete in case
*        multiple doses will be performed back to back.
*
* Jebao DP-4 Uses SNx4HC595 8-Bit Shift Register to select which pump to activate (0-3)
* and 4 ULN2003A darlington transistor arrrays to drive the pumps.
*
* This uses the shiftout function to write the bits into the shift register.
* Only one pump can be on at a time.
*
* Calibration from dispense volume to pump time will be accounted for in BruControl.
* Create a script to time how long it takes to fill a container to a desire volume.
* Use a button to capture the time once the calibration volume is reached.
*
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>


/**********************************************************************************
* BruControl Settings
*
* Modify the following code for the number of globals you want to read and update
* from/to BruControl, with they names of each global as it is named in BruControl.
*
* Set the server IP address and port as needed.
*
***********************************************************************************/
// The following are used to define what global variables in BruControl to monitor and update.
#define READSIZE 3  // Number of global variables to read
#define WRITESIZE 1  // Number of global variables to update
// Optional to define indices for variables to make it easier to use them
// Read Array Indices
#define DOSINGPUMPSENDINDEX 0
#define DOSINGPUMPINDEX 1
#define DOSINGTIMEINDEX 2
// Write Array Indices
#define DOSINGPUMPSENDINDEX 0
// Create arrays
String globalsToRead[READSIZE] = { "DosingPumpSend", "DosingPump", "DosingTime" }; // List of global variables to read as they are named in BruControl
String globalReadValues[READSIZE]; // Array to store read values in
String globalsToWrite[WRITESIZE] = { "DosingPumpSend" }; // List of global variables to update as they are named in BruControl
String globalValuesToWrite[WRITESIZE]; // Array to store write values in

// BruControl Server settings
char serverAddress[] = "xxx.xxx.xxx.xxx"; // BruControl Server IP Address
int port = 8000; // BruControl Server port

// Scheduling interval
unsigned long int delayInterval = 1000;  // milliseconds between each time the main loop runs
/**********************************************************************************
* End BruControl Settings
***********************************************************************************/

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
* Place user defined initialization code below
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

//Pin connected to latch pin (ST_CP) of 74HC595
#define LATCHPIN 12
//Pin connected to clock pin (SH_CP) of 74HC595
#define CLOCKPIN 14
////Pin connected to Data in (DS) of 74HC595
#define DATAPIN 13

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
*  End User defined/modifiable variables
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/


String readString; //String loaded with results from HTTP get requests
char temp_string[300]; // temp string used for sprintf's and other crap
ESP8266WiFiMulti WiFiMulti;

void setup() {
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
  * Place user defined setup code below
  *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/
  // moved user defined setup first so pumps don't turn on randomly when powering on.
  //set pins to output because they are addressed in the main loop
  pinMode(LATCHPIN, OUTPUT);
  pinMode(DATAPIN, OUTPUT);
  pinMode(CLOCKPIN, OUTPUT);
  registerWrite(8, HIGH); // clear register so no pumps are on

  /*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
  * End user defined setup code
  *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

  Serial.begin(115200);
  Serial.println();
  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP("SSID", "PASSWORD"); //Change this to your Wifi settings
  Serial.print("Connecting");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(WiFi.localIP().toString());


  delay(5000);
}


/**********************************************************************************
*  Main Loop to get inputs from BruControl, process them, calculate outputs,
*  and send updates back to BruControl
*
*  This allows you to use global variables in BruControl to interface with
*  unsupported devices.
*    
***********************************************************************************/
void loop() {
  Serial.println("### Starting Main Loop ###################################################");
  Serial.println("  ## Starting to read variables from BruControl #####");
  getGlobals(); // get updates from BruControl
  Serial.println("  ## Done reading variables from BruControl #####");
  Serial.println("  @@ Starting to Parse and Caculate user code @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@U@@@@@@@@@@@@");
  /*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
  * Place user function calls here
  *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

  if(globalReadValues[DOSINGPUMPSENDINDEX] == "True") {
    registerWrite(globalReadValues[DOSINGPUMPINDEX].toInt(), HIGH); // turn on pump
    delay(globalReadValues[DOSINGTIMEINDEX].toInt()); // wait correct amount of time
    registerWrite(8, HIGH);  // Turn off pump
   
    globalValuesToWrite[DOSINGPUMPSENDINDEX] = "False";  // Tell BruControl we are done
  }


  /*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
  * End user function calls
  *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/
  Serial.println("  @@ Done with user code @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
  Serial.println("  ## Starting to write outputs back to BruControl #####");
  sendGlobals();  // send updates to BruControl
  Serial.println("  ## Done writing outputs to BruControl #####");
  delay(delayInterval);  // set delayInterval to how long you want to wait between executing the main loop
}
/**********************************************************************************
*  End Main Loop
***********************************************************************************/


/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
*  Place user defined functions below for processing inputs, interfacing with
*  devices, and calculating return values.
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/



void registerWrite(int whichPin, int whichState) {
// the bits you want to send
  byte bitsToSend = 0;

  // turn off the output so the pins don't light up
  // while you're shifting bits:
  digitalWrite(LATCHPIN, LOW);

  // turn off clock before writing since we clock on rising edge.
  digitalWrite(CLOCKPIN, LOW);

  // turn on the next highest bit in bitsToSend:
  bitWrite(bitsToSend, whichPin, whichState);

  // shift the bits out:
  shiftOut(DATAPIN, CLOCKPIN, MSBFIRST, bitsToSend);

    // turn on the output so the LEDs can light up:
  digitalWrite(LATCHPIN, HIGH);
}



/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
*  End user defined functions
*
*  Start of framework functions
*    Don't modify the code below here!!!!
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/


/**********************************************************************************
* getGlobals()
*
* Framework function to iterate through the array of BruControl global variables
* to get current values.
*
* This function will read all global variables in the globalsToRead array
* and fill the globalReadValues arrays with their current values.
*
* Do not modify this function. Instead set the following at the top of this file:
*   READSIZE - number of global variables to update
*   globalsToRead - names of global variables as they are named in BruControl
*   globalReadValues - Values for each global variable
*
***********************************************************************************/
void getGlobals() {
  int i;
  for(i=0; i<READSIZE; i++) {
    globalReadValues[i] = getVariableValue(globalsToRead[i]);
    Serial.print("     readString Is: ");
    Serial.print(globalsToRead[i]);
    Serial.print(", ");
    Serial.println(globalReadValues[i]);
  }
}


/**********************************************************************************
* sendGlobals()
*
* Framework function to iterate through the array of BruControl global variables
* to send updates to.
*
* This function will send all global variables in the globalsToWrite array with
* corresponding values from the globalValuesToWrite array.
*
* Do not modify this function. Instead set the following at the top of this file:
*   WRITESIZE - number of global variables to update
*   globalsToWrite - names of global variables as they are named in BruControl
*   globalValuestoWrite - Values for each global variable
*
***********************************************************************************/
void sendGlobals() {
  int i;
  for(i=0; i<WRITESIZE; i++) {
    sendVariableValue(globalsToWrite[i], globalValuesToWrite[i]);
    Serial.print("     Write: ");
    Serial.print(globalsToWrite[i]);
    Serial.print(", ");
    Serial.println(globalValuesToWrite[i]);
  }
}


/**********************************************************************************
* getVariableValue()
*   globalVariable - name of global variable as it appear in BruControl.
*
* Framework function to get values of individual BruControl global variables.
* This function is called by the getGlobals() framework function.
* The user should not call this function directly
*
* returns value of global variable in String form
*
***********************************************************************************/
String getVariableValue(String globalVariable) {
  String payload = "";
  StaticJsonDocument<512> jsonDoc;
  char temp1[100];
  char temp2[100];

  // wait for WiFi connection
  if ((WiFiMulti.run() == WL_CONNECTED)) {
    WiFiClient client;
    HTTPClient http;
   
    globalVariable.toCharArray(temp1,100);
    sprintf(temp2, "/global/%s", temp1);
    if (http.begin(client, serverAddress, port, temp2)) {
      // start connection and send HTTP header
      int httpCode = http.GET();

      // httpCode will be negative on error
      if (httpCode > 0) {
        // HTTP header has been send and Server response header has been handled
        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
          payload = http.getString();
        }
      } else {
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
      }
      http.end();
    } else {
      Serial.printf("[HTTP} Unable to connect\n");
    }
  }  
  payload.toCharArray(temp1,300);
  DeserializationError error = deserializeJson(jsonDoc, temp1);
  if(error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.c_str());
    return "";
  }
  const char* returnValue = jsonDoc["Value"];
  return returnValue;
}


/**********************************************************************************
* sendVariableValue()
*   globalVariable - name of global variable as it appear in BruControl.
*   variableValue - value of variable in string form to send back to BruControl.
*
* Framework function to send updates to individual BruControl global variables.
* This function is called by the sendGlobals() framework function.
* The user should not call this function directly
*
***********************************************************************************/
void sendVariableValue(String globalVariable, String variableValue) {
  char temp1[100];
  char temp2[100];
  char temp3[100];

  if ((WiFiMulti.run() == WL_CONNECTED)) {
    WiFiClient client;
    HTTPClient http;

    globalVariable.toCharArray(temp1,100);
    variableValue.toCharArray(temp2,100);
    sprintf(temp3, "[{\"Name\":\"%s\",\"Value\":\"%s\"}]", temp1, temp2);
    String jsonToWrite = temp3;

    if (http.begin(client, serverAddress, port, "/globals")) {  // HTTP
      http.addHeader("Content-Type", "application/json");
      http.addHeader("Content-Length", String(jsonToWrite.length()));
      // start connection and send HTTP header
      int httpCode = http.PUT(jsonToWrite);
      // httpCode will be negative on error
      if (httpCode > 0) {
        // HTTP header has been send and Server response header has been handled
        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
          String payload = http.getString();
        }
      } else {
        Serial.printf("[HTTP] PUT... failed, error: %s\n", http.errorToString(httpCode).c_str());
      }
      http.end();
    } else {
      Serial.printf("[HTTP} Unable to connect\n");
    }
  }
}
 
I got the nodeMCU mounted into the case nice and clean by reusing the ribbon cable from the original control board. I added a USB connector so I can change the FW without having to open it up.

I decided to go a different route for calibrating it. In favor of laziness and increased accuracy, I filled a beaker for a fixed time and measured the volume transferred. This removed any delay between my eagle eyes and pressing a key making it more accurate and it didn't require any special calibration script.

IMG_20200718_171912.jpg


MVIMG_20200718_171950.jpg


IMG_20200718_172543.jpg


IMG_20200719_140630.jpg


IMG_20200719_141955.jpg
 
Here is the dosing pump ready for action.

stand.jpg


And the accompanying dashboard graphics update. I had a nice open space in the bottom right corner to add in the dosing pump. I do have to say my Paint 3D skills are becoming more proficient. Did not take much time at all to whip those renderings together.

dashboard.PNG


And finally the code in node-Red to parse the miscellaneous ingredient list from the BeerXml file to find the amounts for the strike and sparge acid additions. The parseFloat() function is great for stripping off the units as it converts from string to float. "9.6 mL"=> 9.6

nodeRed.PNG
 
I was never really thrilled about the first mini PC I had for running BruControl. I bought a minis forum pc for the remote display in the garage and was satisfied with it. I then bought another one with more horsepower. The first one I bought had the bios setting to automatically boot whenever AC power is applied, but the second one for running BruControl did not have this option in the bios. It also was somewhat sluggish when running BruControl. I ended up getting a thinkcenter mini PC. This now has all the features I need for my main BruControl PC.

On the first PC I used, I didn't have any issues with having multiple devices communicate back to BruControl using the data exchange. However, on this new one I wasn't able to get this to work out of the box. From a web browser on the BruControl PC I was able to see all my globals with http://localhost:8000/globals. But when I changed localhost to the IP address of the BruControl PC, I got a connection refused error. In the end I needed to setup inbound and outbound rules to the windows firewall to open port 8000.

Below is the original pc for running BruControl. One nice thing about that PC is that it came with a bracket to mount it off the side of the VESA mount on the back of the monitor. The new PC did not come with such a thing. Most of the VESA mounts don't allow you to mount a mini PC and use a monitor stand simultaneously.

IMG_20200314_085824.jpg


I found this mount on Amazon that allows you to sandwich the mini PC in between the monitor and the monitor stand.

IMG_20200906_181243.jpg


And this is the brew command center.

IMG_20200906_181329.jpg
 
A bunch of new things for the brewery have come in. I wasn't getting a very tight hop/trub cone from whirlpooling so I picked up the larger 65 watt MKII pump on a black Friday deal. Now I need to silver solder some TC fittings on and it will be ready to speed things up.

PXL_20201224_004401558.jpg


The plastic push fittings I was using for the acid dosing kept cracking and breaking. I found some stainless ones from automation direct that will hopefully hold up much better.

PXL_20201216_011609850.jpg


PXL_20201216_013053929.jpg


PXL_20201216_012811024.jpg
 
Last edited:
Back
Top