Products for USB Sensing and Control
Products for USB Sensing and Control

Brewing with Phidgets #1 - Brew Kettle

Using Phidgets to control and monitor an electric brew kettle


by Patrick

Source Code

Download source code written in C for Linux.

Introduction

In this series, we will build up an all electric home brewery. Part 1 covers the brew kettle, and part 2 will cover the fermentation chamber.

I live in Calgary, where we have snow and subzero temperatures for a large chunk of the year. I wanted to be able to brew year-round, so that meant brewing inside. Propane inside is a really bad idea, so that meant going all-electric. Phidgets has almost everything you need to build a control box for an electric brew kettle.

There is some good information online about building an electric kettle, but a lot of it glosses over the control portion, focussing instead on kettle conversion. Even when control is discussed, it's generally done with industrial PID controllers. I wanted to write my own software to control the process. This article focuses on the control panel, which is built around a PhidgetSBC4, with software written in C.

I run an EBIAB (electric brew-in-a-bag) setup, so I'm only controlling / monitoring a single kettle, but it would be easy to scale up to multiple inputs/outputs.

Hardware

These are the parts for the control panel. Kettle, exhaust, etc. are not listed.

Phidgets Boards, Hardware

Other Parts

Step 1 - Kettle conversion

You probably already have or will purchase a brew kettle that is meant for propane heat. The first step is to convert this to an electric kettle by adding an electric heating element.

For this step I simply followed along with The Electric Brewery Probably the easiest method would be to buy their heating element kit - in which case you just need to make a hole in your kettle and bolt it on. I decided to build everything up from scratch, and it wasn't too difficult. In Canada, I was able to get everything I needed from Ontario Beer Kegs and the local Home Depot.

Safety

Safety is very important here because we have high voltage mixing with water. You will need to have a qualified electrician install a GFCI protected 240V 30A outlet for the kettle. If the outlet isn't GFCI protected and something goes wrong, you risk death. I had a GFCI breaker installed in my main panel which is dedicated to the kettle. These are pricey but really necessary.

Step 2 - Ventilation

Ventilation becomes really important when brewing indoors. My setup boils off 2 gallons/hour. If you don't have proper ventilation, this ends up as brown streaks running down the walls.

for this step, I again mostly followed along with The Electric Brewery. The most important thing is to buy a good fan. A cheap fan will be noisy, and the wrong type of fan won't move enough air.

Since I do BIAB, I mounted the exhaust hood on a TV wall mount, so I can move it out of the way and pulley up the bag after mashing. I mounted the fan outlet onto a piece of plywood which fits into the window frame, so I could avoid having to drill a 6" hole in the foundation.

Step 3 - Control Panel

The control panel is used for controlling the power to the heating element. It would technically be possible to just wire the element up to house power directly - but in this case there would be no way of preventing boil over. So, we need some way to reduce power to the element. This is achieved by PWMing an SSR (details below). We also need some way to change/view the power level - in this case achieved with a rotation sensor and LCD. The whole thing is managed by a PhidgetSBC4 and software written in C.

Control Panel interior.

Enclosure

The control panel needs to be housed in a grounded metal electrical box for safety reasons. I just picked up a box that seemed to be a good size at the local Home Depot. It has a removable front cover which makes it easy to access the interior.

Wiring and Power

The control is powered from a 240V 30A outlet. Because we are dealing with a giant pot full of water, this outlet must be GFCI protected, a mentioned earlier. You will probably have to have an electrician install an outlet - consult your local regulations about this part - I only know what is required in Canada.

I chose to use a standard dryer outlet, and a dryer cord kit to bring power into the enclosure. For powering the kettle, I picked up a watertight cable that I found at the local Home Depot. My kettle sits right next to the sink, so I didn't need to be able to unplug it from the control panel for cleaning, but normally you would want to install a female outlet in the control panel for the kettle.

Cable management inside the panel is done with DIN rail and DIN terminal blocks. Wires have had ferrules attached to ensure terminal block connections are sound. Have a look at the DIN Rail Primer for more information on DIN and Ferrules.

On the right we have the 240V AC power lines. Power here is 240V split phase, with 2 120V lines (red, black) and a neutral (white). I'm running the heating element off the 2 hot lines at 240V, with 1 line being switched via the SSR.

In the middle, we have a DIN mount 24V DC power supply. This is used for powering the Phidgets. I'm powering This with 120V AC by using one hot line and the neutral. I think it could also run off 240V directly.

On the right side, we have 24V DC power, which will then be distributed out to the Phidget boards.

SSR

This kettle has a 5500 Watt heating element. Using the Power formula, I = P / V, I = 5500W / 240V = 22.9A. So, we need to choose an SSR which can switch AC at 240V, 23A. Phidgets sells an SSR that's rated for 25A. Since we are going to be running this near it's limit, it's necessary to use a heatsink. Have a look at the Solid State Relay Primer for more information about SSRs and how to choose them.

The SSR is mounted to the heatsink, and the heatsink is mounted to the enclosure. We are using an REL1100 to control the relay by switching the 24V power. The REL1100 is attached to a VINT Port on the SBC4. I would have just used the VINT Hub port in digital output mode, but the SSR has a minimum switching voltage of 4V, and the VINT Ports output 3.3V.

Phidgets

Next, we mount the Phidgets. I've mounted the Phidgets using Velcro tape so they can easily be removed if needed.

PhidgetSBC

PhidgetSBC4 is the brain of the control panel. All other Phidget's are attached to the SBC.

The SBC is powered from with 24V. Since the SBC4 has a barrel jack connector, I cut the cable off a broken wall power supply, and crimped ferrules onto the wires to power the SBC4 from the terminal blocks.

I've added a WiFi adapter, which is connected via a USB extension, and placed outside the enclosure, because reception inside of a metal box would not be very reliable. This let's me code/debug the control panel software over WiFi.

Control / Display

Input is very simple at this point - consisting of a single 1109 - Rotation Sensor. This is an analog sensor, and is attached to one of the VINT ports on the SBC4.

Display is achieved with a 1204 - PhidgetTextLCD Adapter and

  • 3654 - LCD Screen 4x20 - LCM2004D. The 1204 is attached to the SBC4 with a USB cable.

    The 1109 and 3654 are mounted to the faceplate of the enclosure, and allow control of the heating element power and display of the power and measured temperature.

  • Temperature Monitoring

    Temperature is monitored with an RTD. In the future, I would like to expand to PID temperature control using the RTD - this should just be a matter of software.

    The RTD needs to be un-plugable from the control panel, so I used CBL4403 - 4-Pin Circular Cable Connector (for Enclosures) and CBL4402 - 4-Pin Circular Cable Connector (Male) along with some wire and solder to attach it.

    The RTD is mounted in the kettle by removing the existing analog thermometer in the front the kettle, and re-using that hole. Using HDW4100 - 1/8″ Mounting Nut for Probe Thermocouples and a 1/2" to 1/8" NPT reducer from Home Depot, I was able to mount the RTD.

    Step 4 - Software

    The software running the control panel is a simple program written in C. It's primary purpose is to PWM the SSR, while also monitoring the control dial, and the temperature, and displaying results to the LCD.

    I uses the PhidgetSBC projects tab in the web interface to create a new project for brewing. This starts up the brew software when the SBC boots, so it's ready to go when I turn the power on.

    Download the source package above to follow along.

    Brewing.

    Configuration

    In order to avoid hard coding serial numbers, server namer, port numbers, etc. into the source, a config file is used. I decided to use the pconf format, which is the same format used by the phidget network server. The pconf API is part of libphidget22extra, and the header can be installed with the libphidget22extra-dev package.

    The program will create an empty config file if none exists, and otherwise read in all of the open parameters for all of the channels that are used. The pconf API is not officially supported or documented, but you can get some idea of it's use from this code.

                    
    #include <phidget22.h>
    #include <phidget22extra/phidgetconfig.h>
    
    ...
    
    static int readConfig(const char *file, pconf_t **pc) {
    	PhidgetReturnCode res;
    	char errbuf[128];
    	
    	res = pconf_parsepc_locked(pc, errbuf, sizeof (errbuf), "%s", file);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "failed to parse config file '%s' (%s)\n", file, errbuf);
    		return 1;
    	}
    	
    	return 0;
    }
    
    #define BUFSIZE	(128 * 1024)
    static void createEmptyConfig(const char *file) {
    	PhidgetReturnCode res;
    	pconf_t *pc;
    	char *buf;
    	int fd;
    	
    	pconf_create(&pc);
    	pconf_setcreatemissing(pc, 1);
    	
    	pconf_addi(pc, 0, "settings.channel.element.channel");
    	pconf_addi(pc, 0, "settings.channel.element.hubport");
    	pconf_addbool(pc, 0, "settings.channel.element.ishubport");
    	pconf_addi(pc, 12345, "settings.channel.element.serial");
    	
    	pconf_addi(pc, 0, "settings.channel.dial.channel");
    	pconf_addi(pc, 0, "settings.channel.dial.hubport");
    	pconf_addbool(pc, 0, "settings.channel.dial.ishubport");
    	pconf_addi(pc, 12345, "settings.channel.dial.serial");
    	
    	pconf_addi(pc, 0, "settings.channel.rtd.channel");
    	pconf_addi(pc, 0, "settings.channel.rtd.hubport");
    	pconf_addbool(pc, 0, "settings.channel.rtd.ishubport");
    	pconf_addi(pc, 12345, "settings.channel.rtd.serial");
    	
    	pconf_addi(pc, 0, "settings.channel.lcd.channel");
    	pconf_addi(pc, 0, "settings.channel.lcd.hubport");
    	pconf_addbool(pc, 0, "settings.channel.lcd.ishubport");
    	pconf_addi(pc, 12345, "settings.channel.lcd.serial");
    	
    	buf = malloc(BUFSIZE);
    	
    	res = pconf_renderpc(pc, buf, BUFSIZE);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "failed to render config file.\n");
    		return;
    	}
    	
    	fd = open(file, O_WRONLY | O_CREAT);
    	if (fd == -1) {
    		fprintf(stderr, "failed to open file.\n");
    		return;
    	}
    	
    	if (write(fd, buf, strlen(buf)) == -1) {
    		fprintf(stderr, "failed to write file.\n");
    		return;
    	}
    	
    	close(fd);
    	free(buf);
    }
    
    ...
    
    int main(int argc, char **argv) {
    	PhidgetReturnCode res;
    	pconf_t *pc;
    	
    	if (argc != 2) {
    		fprintf(stderr, "Pass is config file as first argument.\n");
    		exit(1);
    	}
    	
    	if (readConfig(argv[1], &pc)) {
    		char tmp[128];
    		fprintf(stderr, "Config file error. Creating %s.sample. Fill this in and pass as 1st argument\n", argv[1]);
    		snprintf(tmp, 128, "%s.sample", argv[1]);
    		createEmptyConfig(tmp);
    		exit(1);
    	}
    	
    	...
    }
                    
                    

    Initialization

    Next, we create and initialize the Phidget handles, and then open them. The serial number, hub port, channel, etc. parameters to open are read from the config file.

    Channels are configured in their attach events, so that the system can recover in the case of an unexpected detach. openWaitForAttachment is used so that we know all channels are ready once openHandles() returns.

                    
    PhidgetDigitalOutputHandle element;
    PhidgetVoltageRatioInputHandle dial;
    PhidgetTemperatureSensorHandle rtd;
    PhidgetLCDHandle lcd;
    
    static void CCONV dialAttach(PhidgetHandle phid, void *ctx) {
    	PhidgetVoltageRatioInputHandle dial;
    	
    	dial = (PhidgetVoltageRatioInputHandle)phid;
    	
    	PhidgetVoltageRatioInput_setSensorType(dial, SENSOR_TYPE_1109);
    	PhidgetVoltageRatioInput_setDataInterval(dial, 100);
    }
    
    static void CCONV rtdAttach(PhidgetHandle phid, void *ctx) {
    	PhidgetTemperatureSensorHandle rtd;
    	
    	rtd = (PhidgetTemperatureSensorHandle)phid;
    	
    	PhidgetTemperatureSensor_setRTDType(rtd, RTD_TYPE_PT1000_3850);
    	PhidgetTemperatureSensor_setRTDWireSetup(rtd, RTD_WIRE_SETUP_4WIRE);
    }
    
    static void CCONV lcdAttach(PhidgetHandle phid, void *ctx) {
    	PhidgetLCDHandle lcd;
    	
    	lcd = (PhidgetLCDHandle)phid;
    	
    	PhidgetLCD_setScreenSize(lcd, SCREEN_SIZE_4x20);
    	PhidgetLCD_initialize(lcd);
    	PhidgetLCD_setBacklight(lcd, 1);
    	
    	PhidgetLCD_writeText(lcd, FONT_5x8, 2, 0, "Kalfalfa Brewing");
    	PhidgetLCD_flush(lcd);
    }
    
    static int initHandles(pconf_t *pc) {
    	
    	PhidgetDigitalOutput_create(&element);
    	PhidgetVoltageRatioInput_create(&dial);
    	PhidgetTemperatureSensor_create(&rtd);
    	PhidgetLCD_create(&lcd);
    	
    	Phidget_setOnAttachHandler((PhidgetHandle)element, elementAttach, NULL);
    	Phidget_setOnDetachHandler((PhidgetHandle)element, elementDetach, NULL);
    	Phidget_setOnErrorHandler((PhidgetHandle)element, errorHandler, NULL);
    	
    	Phidget_setOnAttachHandler((PhidgetHandle)dial, dialAttach, NULL);
    	Phidget_setOnDetachHandler((PhidgetHandle)dial, dialDetach, NULL);
    	Phidget_setOnErrorHandler((PhidgetHandle)dial, errorHandler, NULL);
    	PhidgetVoltageRatioInput_setOnSensorChangeHandler(dial, dialChange, NULL);
    	
    	Phidget_setOnAttachHandler((PhidgetHandle)rtd, rtdAttach, NULL);
    	Phidget_setOnDetachHandler((PhidgetHandle)rtd, rtdDetach, NULL);
    	Phidget_setOnErrorHandler((PhidgetHandle)rtd, errorHandler, NULL);
    	PhidgetTemperatureSensor_setOnTemperatureChangeHandler(rtd, temperatureChange, NULL);
    	
    	Phidget_setOnAttachHandler((PhidgetHandle)lcd, lcdAttach, NULL);
    	Phidget_setOnDetachHandler((PhidgetHandle)lcd, lcdDetach, NULL);
        Phidget_setOnErrorHandler((PhidgetHandle)lcd, errorHandler, NULL);
    	
    	Phidget_setDeviceSerialNumber((PhidgetHandle)element, pconf_get32(pc, 0, "settings.channel.element.serial"));
    	Phidget_setHubPort((PhidgetHandle)element, pconf_get32(pc, 0, "settings.channel.element.hubport"));
    	Phidget_setIsHubPortDevice((PhidgetHandle)element, pconf_getbool(pc, 0, "settings.channel.element.ishubport"));
    	Phidget_setChannel((PhidgetHandle)element, pconf_get32(pc, 0, "settings.channel.element.channel"));
    	
    	Phidget_setDeviceSerialNumber((PhidgetHandle)dial, pconf_get32(pc, 0, "settings.channel.dial.serial"));
    	Phidget_setHubPort((PhidgetHandle)dial, pconf_get32(pc, 0, "settings.channel.dial.hubport"));
    	Phidget_setIsHubPortDevice((PhidgetHandle)dial, pconf_getbool(pc, 0, "settings.channel.dial.ishubport"));
    	Phidget_setChannel((PhidgetHandle)dial, pconf_get32(pc, 0, "settings.channel.dial.channel"));
    	
    	Phidget_setDeviceSerialNumber((PhidgetHandle)rtd, pconf_get32(pc, 0, "settings.channel.rtd.serial"));
    	Phidget_setHubPort((PhidgetHandle)rtd, pconf_get32(pc, 0, "settings.channel.rtd.hubport"));
    	Phidget_setIsHubPortDevice((PhidgetHandle)rtd, pconf_getbool(pc, 0, "settings.channel.rtd.ishubport"));
    	Phidget_setChannel((PhidgetHandle)rtd, pconf_get32(pc, 0, "settings.channel.rtd.channel"));
    	
    	Phidget_setDeviceSerialNumber((PhidgetHandle)lcd, pconf_get32(pc, 0, "settings.channel.lcd.serial"));
    	Phidget_setHubPort((PhidgetHandle)lcd, pconf_get32(pc, 0, "settings.channel.lcd.hubport"));
    	Phidget_setIsHubPortDevice((PhidgetHandle)lcd, pconf_getbool(pc, 0, "settings.channel.lcd.ishubport"));
    	Phidget_setChannel((PhidgetHandle)lcd, pconf_get32(pc, 0, "settings.channel.lcd.channel"));
    	
    	return (0);
    }
    
    static int openHandles() {
    	PhidgetReturnCode res;
    	
    	res = Phidget_openWaitForAttachment((PhidgetHandle)element, PHIDGET_TIMEOUT_DEFAULT);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "Error opening element: 0x%08x\n", res);
    		return (1);
    	}
    	
    	res = Phidget_openWaitForAttachment((PhidgetHandle)dial, PHIDGET_TIMEOUT_DEFAULT);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "Error opening dial: 0x%08x\n", res);
    		return (1);
    	}
    	
    	res = Phidget_openWaitForAttachment((PhidgetHandle)rtd, PHIDGET_TIMEOUT_DEFAULT);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "Error opening rtd: 0x%08x\n", res);
    		return (1);
    	}
    	
    	res = Phidget_openWaitForAttachment((PhidgetHandle)lcd, PHIDGET_TIMEOUT_DEFAULT);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "Error opening lcd: 0x%08x\n", res);
    		return (1);
    	}
    	
    	return (0);
    }
    
    int main(int argc, char **argv) {
    
        ...
    	
    	if (initHandles(pc))
    		goto error;
    	
    	if (openHandles())
            goto error;
            
        ...
    }
                    
                    

    Main Logic

    Once we have initialized and opened up the channels, the main logic starts. I decided to spawn a thread to run the PWM rather then just running it in main() because I would like to extend the program to include a PID control loop as well in the future.

    PWM Control

    The output power of the heating element is controlled by PWMing the SSR. The REL1100 outputs are actually capable of PWM output natively, so at first I thought I could just use the PhidgetDigitalOutput_setDutyCycle() API. However, the REL1100 PWM frequency is 16 kHz - and this is too fast for a big AC SSR.

    The 3959_0 AC SSR has a max turn-on time on 2ms - however the turn-off time is up to 1/2 AC cycle, which is ~8ms for 60Hz AC. This means that the minimum pulse width I could consider outputting should be 8ms. I want to have 100 levels of control (0-100%), so at a minimum, my period should be 800ms.

    I decided to go with a 2 second period, because this is well within the capabilities of this SSR, and because the heating load from the water is so large, it's unlikely that I would be able to tell the difference between 1 second, 2 second, or even 5 second PWM periods.

    The PWM duty cycle is controlled via the rotation sensor, with it's value being read in the VoltageRatioInput SensorChangeHandler event.

    Signal Handlers

    It's really important in a program like this to handle termination signals properly, and ensure that the SSR is turned off. Otherwise, the program could exit leaving the SSR turned on, and there would be no way to turn the element off other then cutting power directly.

    I register handlers for SIGINT and SIGTERM signals - these correspond to Ctrl-C and the kill command, and allow the main loop to break and continue, closing the Phidget handles properly and cleaning up anything else before exiting.

    Display

    The PWN rate and the measured temperature are displayed on the TextLCD directly from their respective data event handlers. The Phidget library is fully thread safe, so we don't have to worry about synchronization issues.

                    
    #include <signal.h>
    #include <pthread.h>
    
    int running = 1;
    const int pwnPeriod_us = 2000000; //2 seconds
    double pwm = 0.0;
    
    pthread_t pwmThread;
    
    static void CCONV dialChange(PhidgetVoltageRatioInputHandle ch, void *ctx, double sensorValue, Phidget_UnitInfo *sensorUnit) {
    	double correctedVal;
    	char val[21];
    	
    	correctedVal = 1.0 - sensorValue;
    	
    	if (correctedVal < 0.005) {
    		pwm = 0.0;
    	} else if (correctedVal > 0.995) {
    		pwm = 1.0;
    	} else {
    		pwm = (correctedVal - 0.005) / 0.99;
    	}
    	
    	snprintf(val, 21, "Power: %3d%%", (int)(pwm * 100));
    	PhidgetLCD_writeText(lcd, FONT_5x8, 3, 2, val);
    	PhidgetLCD_flush(lcd);
    }
    
    static void CCONV temperatureChange(PhidgetTemperatureSensorHandle ch, void *ctx, double temperature) {
    	char val[21];
    	
    	snprintf(val, 21, "Temperature: %5.1lf\xdf""C", temperature);
    	PhidgetLCD_writeText(lcd, FONT_5x8, 0, 3, val);
    	PhidgetLCD_flush(lcd);
    }
    
    static void sig_handler(int signo) {
    	
    	running = 0;
    }
    
    static void *pwmThreadFunction(void *param) {
    	PhidgetReturnCode res;
    	int pwm_on;
    	int pwm_off;
    	
    	while (running) {
    		
    		pwm_on = pwm * pwnPeriod_us;
    		pwm_off = pwnPeriod_us - pwm_on;
    		
    		if (pwm_on) {
    			res = PhidgetDigitalOutput_setState(element, 1);
    			usleep(pwm_on);
    		}
    		if (pwm_off) {
    			res = PhidgetDigitalOutput_setState(element, 0);
    			usleep(pwm_off);
    		}
    		
    		PhidgetLCD_flush(lcd);
    	}
    }
    
    int main(int argc, char **argv) {
        
        ...
    	
    	signal(SIGINT, sig_handler);
    	signal(SIGTERM, sig_handler);
    	
    	if(pthread_create(&pwmThread, NULL, pwmThreadFunction, NULL))
    		goto error;
    	
    	while (running)
    		usleep(100000);
    	
    	printf("\nShutting down...\n");
    	
    	pthread_join(pwmThread, NULL);
    	
    	...
    }
                    
                    

    Cleanup / Shutdown

    Once a signal is recieved, we clean up the Phidget handles, and exit.

    Closing a Phidget handle will reset the phisical device to defaults, so we don't have to worry about manually disabling the SSR, or clearing the LCD.

                    
    static void closeHandles() {
    	
    	Phidget_close((PhidgetHandle)element);
    	PhidgetDigitalOutput_delete(&element);
    	
    	Phidget_close((PhidgetHandle)dial);
    	PhidgetVoltageRatioInput_delete(&dial);
    	
    	Phidget_close((PhidgetHandle)rtd);
    	PhidgetTemperatureSensor_delete(&rtd);
    	
    	Phidget_close((PhidgetHandle)lcd);
    	PhidgetLCD_delete(&lcd);
    }
    
    int main(int argc, char **argv) {
        
        ...
    	
    	closeHandles();
    	printf("Done.\n\n");
    	exit(0);
    	
    error:
    	closeHandles();
    	printf("Exiting with error.\n\n");
    	exit(1);
    }
                    
                    

    Webpage

    I created a simple webpage so I could monitor temperature remotely from my phone.

    I added a new dictionary in the Phidget Network Server config page on the SBC4, then restarted. Now, this dictionary is available on the local Phidget Server for whichever keys I'd like to add. The program writes pwm and temperature keys to the dictionary from the data change events. There is a simple index.html which connects to the same dictionary and just displays those keys as the events come in. This index.html was placed under the Phidget Server htdocs path so it could be served by the builtin webserver.

    In the future, I'd like to also allow control via the webpage.

                    
    PhidgetDictionaryHandle dict;
    
    static void CCONV dialChange(PhidgetVoltageRatioInputHandle ch, void *ctx, double sensorValue, Phidget_UnitInfo *sensorUnit) {
    	...
    	
    	snprintf(val, 21, "%1.2lf", pwm);
    	PhidgetDictionary_set(dict, "pwm", val);
    	
    	...
    }
    
    static void CCONV temperatureChange(PhidgetTemperatureSensorHandle ch, void *ctx, double temperature) {
    	...
    	
    	snprintf(val, 21, "%.2lf", temperature);
    	PhidgetDictionary_set(dict, "rtd", val);
    	
    	...
    }
    
    static int initHandles(pconf_t *pc) {
        ...
        
    	PhidgetDictionary_create(&dict);
    	Phidget_setDeviceLabel((PhidgetHandle)dict, pconf_getstr(pc, "", "settings.channel.dictionary.label"));
    	
    	return (0);
    }
    
    static int openHandles() {
    	PhidgetReturnCode res;
    	
    	...
    	
    	// Give some extra time in case Network server is slow starting up.
    	res = Phidget_openWaitForAttachment((PhidgetHandle)dict, 10000);
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "Error opening dictionary: 0x%08x\n", res);
    		return (1);
    	}
    	
    	return (0);
    }
    
    static void closeHandles() {
    	
    	...
    	
    	Phidget_close((PhidgetHandle)dict);
    	PhidgetDictionary_delete(&dict);
    }
    
    
    int main(int argc, char **argv) {
    	...
    	
    	res = PhidgetNet_addServer(
    			pconf_getstr(pc, "local", "settings.server.name"),
    			pconf_getstr(pc, "localhost", "settings.server.host"),
    			pconf_get32(pc, 5661, "settings.server.port"),
    			pconf_getstr(pc, "", "settings.server.pass"), 0);
    	
    	if (res != EPHIDGET_OK) {
    		fprintf(stderr, "Error adding server: 0x%08x\n", res);
    		exit(1);
        }
        
    	...
    }
                    
                    
                    
                        
    <!DOCTYPE html>
    <meta charset="UTF-8">
    <html>
    <head>
    	<title>Brewery</title>
    	<script src="jquery-2.1.4.min.js"></script>
    	<script src="/sha256.js"></script>
    	<script src="/phidget22.min.js"></script>
    	<script>
    	
    	function go() {
    		var d = new phidget22.Dictionary();
    		
    		d.setDeviceLabel('brewer');
    		
    		d.onAttach = function(ch) {
    			$('#dictattachdetach').text('Attached');
    		};
    
    		d.onDetach = function(ch) {
    			$('#dictattachdetach').text('Detached');
    		};
    		
    		var addupdate = function(key, value) {
    			if (key === 'rtd')
    				$('#rtd').text(value);
    			else if (key === 'pwm')
    				$('#pwm').text(value);
    		}
    
    		d.onAdd = addupdate;
    		d.onUpdate = addupdate;
    
    		d.onRemove = function (key) {
    			console.log('removed:' + key);
    		};
    
    		d.open().then(function (ch) {
    			$('#dictstatus').text('Opened');
    		}).catch(function (err) {
    			$('#dictstatus').text('Failed to open dictionary:' + err);
    		});
    	}
    	
    	$(document).ready(function() {
    		let server = window.location.host;
    		var conn = new phidget22.Connection({
    			hostname: window.location.hostname,
    			port: window.location.port,
    			name: window.location.host,
    			onError: function(code, msg) { $('#serverstatus').text('Connection Error:' + msg); },
    			onConnect: function(code, msg) { $('#serverstatus').text('Connected'); },
    			onDisconnect: function(code, msg) { $('#serverstatus').text('Connected'); }
    		});
    		conn.connect().then(function(res) {
    			go();
    		}).catch(function(err) {
    			conn.delete();
    			$('#serverstatus').text('Failed to connect to server:' + err);
    		});
    	});
    	</script>
    </head>
    
    <body>
    	Server: <span id="serverstatus">Waiting for connection...</span><br>
    	Dictionary: <span id="dictstatus">Waiting for open...</span> <span id="dictattachdetach">Waiting for attach...</span><br>
    	Temperature: <span id="rtd"></span>°C<br>
    	PWM: <span id="pwm"></span>
    </body>
    </html>
    
    
                    
                    

    The Future

    The system is working well, but there are some further enhancement that would be nice to implement.

    Temperature Control

    Since we have access to the temperature and the SSR, the obvious next step is PWM temperature control. This would be really nice for the heating to mash temperature step, because I generally target a temperature with +-0.2 degree accuracy, and I often overshoot when not paying attention.

    With the addition of a pump I could go even further and implement programmed step mashes, and ditch the thermal jacket.

    For the actual boil stage, it's probably best to stick with pure power control, in order to guarantee no boil-overs.

    Temperature control would be implemented as a second thread running a PID algorithm. I would also have to add a second control input, or maybe switch out the simple rotation sensor for something like HIN1101_0 (Dial Phidget), which offers continuous rotation, and a pushbutton to cycle between modes.

    Master Switch

    It would be nice to have a big switch so I could cut power to the heating element. As is, I always fill the kettle before switching the breaker on, because I don't want a coding error or crash to turn the element on in an empty kettle while I'm developing the code.

    Logging

    Once temperature control is implemented, it would be nice to log the temperature over time. This could be usefull for repeatability in brew sessions, but I also just really like collecting data just for interests sake. The data could also be graphed in realtime on the webpage.

    Sous-Vide

    Not really related to brewing - but once we have the temperature control implemented, the brew kettle could be used for Sous-Vide cooking, and being a giant pot, it could be used to cook some really big things - such as a full turkey.

    Conclusion

    We have used Phidgets to create a safe and resilient electric brewing control panel, demonstrating proper coding practices for real-world usage of Phidgets.

    In the next article, we build a fermentation chamber to turn the newly created wort into delicious beer.