Hardware components | ||||||
| × | 7 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 5 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 3 | ||||
| × | 2 | ||||
| × | 1 | ||||
| × | 1 | ||||
Hand tools and fabrication machines | ||||||
| ||||||
| ||||||
| ||||||
|
Disclaimer: First off, this project no way promotes the use or misuse of alcohol, it is completely up to the users what beverages will make up the contents of this kegerator.
This project was born out of the desire to better manage the contents of a kegerator. A kegerator works on the basic principal of keeping a beverage cold as well as keeping the beverages carbonated at a certain PSI. In addition just by pouring your self a cold beverage you have no idea how much is left in the keg. It would be a shame to have folks over for a Sunday football game and run out of root beer half way through the game.
So the goals of this project are:
- Maintain a consistent temperature of the beverages, ensure the beverages do not get too warm or too cold and freeze
- Ensure that an acceptable amount of carbonation is applied to the keg to maintain optimal flavor
- Keep track of the amount of beverages in each keg and provide visual feedback to ensure plenty of beverages are on hand for the big game.
- Keep track of the amount of CO2 left in the tank used to carbonate the beverages
The basic electronics components and their use:
- A chest freezer is used for the cooling unit and to provide a frame to create a nice piece of furniture
- Raspberry PI 2 Running Windows 10 IoT core is used as the brains of operation
- Small postage scales are used to measure the weight of each keg as well as the CO2 tank, these postage scales have the electronics removed and a load cell amplifier and small Arduino built in to the scale. These scales will communicate with the Raspberry PI 2 via I2C (more on this later)
- There are 5 Digital Temperature sensors that are installed on the unit, one on the bottom of the chest freezer, one attached to the underside of the top, one each installed in the towers where the tap handles are (more on this later) and one installed on the outside of the unit to measure ambient temperature. These temperature sensors are connected to a small Arduino and also communicate with the Raspberry PI 2 via I2C
- A Honeywell pressure sensor is attached to the air lines that are used to provide carbonation to the kegs. Although the adjustment of PSI is manual (for now) this will provide an accurate gauge of how much CO2 is applied to the kegs.
- A 5V power supply is used to provide power to the Raspberry PI2. A larger version (providing up to 6 amps) was chosen so it could also power an addressable LED strip.
- A simple relay is placed in-line to the power for the compressor. Using this relay the power can be applied and removed from the compressor, the compressor will then in turn control the temperature of the kegerator (more on this later)
Cloud Connectivity
The Ultimate Kegerator contains a web server to allow for remote configuration via REST services as well as simple static view of the current status. This web site can be reached at http://slsys.homeip.net:9501 .
In addition the Ultimate Kegerator uploads it's vital statistics to a Windows Azure event Hub. You won't be able to use the standard Nuget package for talking to the event hub however, you the easy to implement library provided by fellow Windows Embedded MVP Paolo Patierno available at
https://www.nuget.org/packages/AzureSBLite/
For ultimate processing by Stream Analytics
Eventual plans for Stream Analytics would be to:
1) Monitor and notify if temperatures get too warm or too cold
2) Monitor and notify when the CO2 tank gets too low
3) Monitor and notify if there is a leak detected in the CO2 tank (gradual decrease in weight)
Here are some additional pictures of the assembly process:
-twb
Keg Class
C#using LagoVista.Common.Commanding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
namespace LagoVista.IoT.Common.Kegerator.Models
{
public class Keg : DeviceBase
{
int _idx;
TimeSpan _updateInterval;
private Scales.Scale _scale;
public Keg(int idx, Scales.Scale scale, TimeSpan updateInterval)
{
_idx = idx;
_updateInterval = UpdateInterval;
_scale = scale;
}
public override TimeSpan UpdateInterval
{
get { return _updateInterval; }
}
public override void Refresh()
{
LastUpdated = DateTime.Now;
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
ContentsWeight = Scale.Weight - ContainerWeightLb;
if (FullContentsWeightLb > 0)
PercentFull = Convert.ToInt32((ContentsWeight / (FullContentsWeightLb - ContainerWeightLb)) * 100);
else
PercentFull = 0;
PercentFull = Math.Min(PercentFull, 100);
if (GlassSizeOz > 0)
QtyRemaining = Convert.ToInt32((ContentsWeight * 16)/ GlassSizeOz);
else
QtyRemaining = 0;
RaisePropertyChanged("PercentFullHeight");
RaisePropertyChanged("PercentFullDisplay");
});
}
public Scales.Scale Scale
{
get { return _scale; }
}
#region Calculated Properties
private int _qtyRemaining;
public int QtyRemaining
{
get { return _qtyRemaining; }
set { Set(ref _qtyRemaining, value); }
}
private DateTime? _installDate;
public DateTime? InstallDate
{
get { return _installDate; }
set { Set(ref _installDate, value); }
}
private int _percentFull;
public int PercentFull
{
get { return _percentFull; }
set { Set(ref _percentFull, value); }
}
public String PercentFullDisplay
{
get { return String.Format("{0}%", Convert.ToInt32(PercentFull)); }
}
public double PercentFullHeight
{
get { return Convert.ToDouble(_percentFull * 2); }
}
public int KegIndex
{
get { return _idx; }
}
#endregion
#region Entered Properties
private bool _isEmpty;
public bool IsEmpty
{
get { return _isEmpty; }
set {
_isEmpty = value;
RaisePropertyChanged();
}
}
private double _glassSize;
public double GlassSizeOz
{
get { return _glassSize; }
set { Set(ref _glassSize, value); }
}
private DateTime? _bornDate;
public DateTime? BornDate
{
get { return _bornDate; }
set { Set(ref _bornDate, value); }
}
double _containerWeight;
public double ContainerWeightLb
{
get { return _containerWeight; }
set { Set(ref _containerWeight, value); }
}
double _contentsWeight;
public double ContentsWeight
{
get { return _contentsWeight; }
set { Set(ref _contentsWeight, value); }
}
double _fullContentsWeight;
public double FullContentsWeightLb
{
get { return _fullContentsWeight; }
set { Set(ref _fullContentsWeight, value); }
}
private String _contentsName;
public String ContentsName
{
get { return _contentsName; }
set { Set(ref _contentsName, value); }
}
#endregion
public void Save()
{
LagoVista.Common.PlatformSupport.Services.BindingHelper.RefreshBindings();
PutSetting(String.Format("KEG{0}_CONTENTS", _idx), ContentsName);
PutSetting(String.Format("KEG{0}_IS_EMPTY", _idx), IsEmpty.ToString());
PutSetting(String.Format("KEG{0}_CONTAINER_WEIGHT", _idx), String.Format("{0:0.00}", ContainerWeightLb));
PutSetting(String.Format("KEG{0}_GLASS_SIZE", _idx), String.Format("{0:0.00}", GlassSizeOz));
PutSetting(String.Format("KEG{0}_FULL_CONTENTS_WEIGHT", _idx), String.Format("{0:0.00}", FullContentsWeightLb));
if (BornDate.HasValue)
PutSetting(String.Format("KEG{0}_BORN_DATE", _idx), BornDate.Value.ToString());
else
RemoveSetting(String.Format("KEG{0}_BORN_DATE", _idx));
if(InstallDate.HasValue)
PutSetting(String.Format("KEG{0}_INSTALL_DATE", _idx), InstallDate.Value.ToString());
else
RemoveSetting(String.Format("KEG{0}_INSTALL_DATE", _idx));
}
public void Load()
{
ContentsName = GetSetting(String.Format("KEG{0}_CONTENTS", _idx), "?");
ContainerWeightLb = Convert.ToDouble(GetSetting(String.Format("KEG{0}_CONTAINER_WEIGHT", _idx), "10.0"));
GlassSizeOz = Convert.ToDouble(GetSetting(String.Format("KEG{0}_GLASS_SIZE", _idx), "12.0"));
FullContentsWeightLb = Convert.ToDouble(GetSetting(String.Format("KEG{0}_FULL_CONTENTS_WEIGHT", _idx), "0.0"));
IsEmpty = Convert.ToBoolean(GetSetting(String.Format("KEG{0}_IS_EMPTY", _idx), "True"));
var bornDate = GetSetting("KEG{0}_BORN_DATE", String.Empty);
if (!String.IsNullOrEmpty(bornDate))
BornDate = DateTime.Parse(bornDate);
else
BornDate = null;
var installDate = GetSetting("KEG{0}_INSTALL_DATE", String.Empty);
if (!String.IsNullOrEmpty(installDate))
InstallDate = DateTime.Parse(installDate);
else
InstallDate = null;
}
public async void SaveFullWeight()
{
FullContentsWeightLb = await Scale.GetAverageWeight();
Save();
}
public RelayCommand SaveFullWeightCommand
{
get { return new RelayCommand(() => SaveFullWeight()); }
}
}
}
Scale Class
C#using LagoVista.Common.Commanding;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.I2c;
namespace LagoVista.IoT.Common.Kegerator.Scales
{
public class Scale : DeviceBase
{
Windows.Devices.I2c.I2cDevice _scaleI2CChannel;
int _countOffset;
double? _calibrationFactor = null;
private TimeSpan _updateInterval;
byte _address;
public Scale(byte address)
{
_address = address;
}
private void WriteValue(byte address, int value)
{
if (!IsDemoMode)
{
var offsetBuffer = new byte[5];
offsetBuffer[0] = address;
offsetBuffer[1] = (byte)(value >> 24);
offsetBuffer[2] = (byte)(value >> 16);
offsetBuffer[3] = (byte)(value >> 8);
offsetBuffer[4] = (byte)(value);
_scaleI2CChannel.Write(offsetBuffer);
}
}
public async Task Init(String i2cDeviceId, TimeSpan updateInterval)
{
var settings = new I2cConnectionSettings(_address)
{
BusSpeed = I2cBusSpeed.StandardMode,
SharingMode = I2cSharingMode.Shared
};
_updateInterval = updateInterval;
IsDemoMode = String.IsNullOrEmpty(i2cDeviceId);
if (!IsDemoMode)
{
_scaleI2CChannel = await Windows.Devices.I2c.I2cDevice.FromIdAsync(i2cDeviceId, settings);
if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey(String.Format("{0:X}.OFFSET", _address)))
{
_countOffset = Convert.ToInt32(Windows.Storage.ApplicationData.Current.LocalSettings.Values[String.Format("{0:X}.OFFSET", _address)]);
try
{
WriteValue((byte)'O', _countOffset);
}
catch (Exception ex)
{
Debug.WriteLine("Scale offline ");
}
}
if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey(String.Format("{0:X}.CALIBRATION", _address)))
{
_calibrationFactor = Convert.ToDouble(Windows.Storage.ApplicationData.Current.LocalSettings.Values[String.Format("{0:X}.CALIBRATION", _address)]);
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
Status = "Ready";
});
}
}
else
{
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
Status = "Ready";
});
}
}
int? _lastRaw = null;
private int GetRaw()
{
try
{
var inbuffer = new byte[4];
_scaleI2CChannel.Write(new byte[] { (byte)0x11 });
_scaleI2CChannel.Read(inbuffer);
/* Note on the scale, this is a long (64 bit) here it's a int (64 bit) */
var thisRaw = (int)(inbuffer[0] << 24 | inbuffer[1] << 16 | inbuffer[2] << 8 | inbuffer[3]);
if (_lastRaw.HasValue)
{
if (Math.Abs(_lastRaw.Value - thisRaw) > 0xFFFF)
return _lastRaw.Value;
}
else
_lastRaw = thisRaw;
return thisRaw;
}
catch (Exception)
{
return -1;
}
}
public override void Refresh()
{
LastUpdated = DateTime.Now;
int rawResult = 0;
var isOnline = true;
try
{
var inbuffer = new byte[4];
var statusBuffer = new byte[1];
if (!IsDemoMode)
{
_scaleI2CChannel.Write(new byte[] { (byte)0x0A });
_scaleI2CChannel.Read(statusBuffer);
rawResult = GetRaw();
}
if (_calibrationFactor.HasValue)
{
Weight = (rawResult - _countOffset) * _calibrationFactor.Value;
Debug.WriteLine(String.Format("0x{0:X} WEIGHT VALUE => {1:0.00} lbs", _address, Weight));
}
else if (_countOffset > 0)
Debug.WriteLine(String.Format("0x{0:X} ZEROED VALUE => {1}", _address, rawResult - _countOffset));
else
Debug.WriteLine(String.Format("0x{0:X} RAW VALUE => 0x{1:X}", _address, rawResult));
}
catch (Exception ex)
{
rawResult = -1;
isOnline = false;
Debug.WriteLine(ex.Message);
}
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
Raw = rawResult;
IsOnline = isOnline;
if (!IsOnline)
{
Status = "Offline";
WeightDisplay = "?";
}
else
{
if (_calibrationFactor.HasValue)
{
Status = "Ready";
WeightDisplay = String.Format("{0}lb {1:00}oz", Math.Truncate(Weight), ((Weight % 1.0) * 16.0));
}
else
{
WeightDisplay = "?";
Status = "Not Calibrated";
}
}
RaisePropertyChanged("LastUpdateDisplay");
});
}
const int CALIBRATION_COUNT = 10;
public async void StoreOffset()
{
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
Status = "Zeroing";
});
Debug.WriteLine("Staritng Zero Process");
long zeroSum = 0;
for (var idx = 0; idx < CALIBRATION_COUNT; ++idx)
{
await Task.Delay(250);
zeroSum += GetRaw();
}
_countOffset = (int)(zeroSum / CALIBRATION_COUNT);
WriteValue((byte)'O', _countOffset);
Debug.WriteLine(String.Format("Finished Zero Process {0:X}", _countOffset));
if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey(String.Format("{0:X}.OFFSET", _address)))
Windows.Storage.ApplicationData.Current.LocalSettings.Values.Remove(String.Format("{0:X}.OFFSET", _address));
Windows.Storage.ApplicationData.Current.LocalSettings.Values.Add(String.Format("{0:X}.OFFSET", _address), _countOffset);
Debug.WriteLine("Finished Zero Process");
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
Status = "Zeroed";
});
}
public async void Calibrate()
{
Status = "Calibrating";
LagoVista.Common.PlatformSupport.Services.BindingHelper.RefreshBindings();
long countSum = 0;
for (var idx = 0; idx < CALIBRATION_COUNT; ++idx)
{
await Task.Delay(250);
countSum += GetRaw() - _countOffset;
}
_calibrationFactor = CalibrationWeight / (countSum / CALIBRATION_COUNT);
if (Windows.Storage.ApplicationData.Current.LocalSettings.Values.ContainsKey(String.Format("{0:X}.CALIBRATION", _address)))
Windows.Storage.ApplicationData.Current.LocalSettings.Values.Remove(String.Format("{0:X}.CALIBRATION", _address));
Windows.Storage.ApplicationData.Current.LocalSettings.Values[String.Format("{0:X}.CALIBRATION", _address)] = _calibrationFactor;
}
public async Task<double> GetAverageWeight(int pointCount = 5)
{
var weightSum = 0.0;
for(var idx = 0; idx < pointCount; ++idx)
{
weightSum += Weight;
await Task.Delay(UpdateInterval);
}
return weightSum / pointCount;
}
private int _fullRaw;
public int FullRaw
{
get { return _fullRaw; }
set { Set(ref _fullRaw, value); }
}
private double _weight;
public double Weight
{
get { return _weight; }
set { _weight = value; }
}
private String _weightDisplay;
public String WeightDisplay
{
get { return _weightDisplay; }
set { Set(ref _weightDisplay, value); }
}
private int _raw;
public int Raw
{
get { return _raw; }
set
{
RaisePropertyChanged("RawDisplay");
Set(ref _raw, value);
}
}
public String AddressDisplay
{
get { return String.Format("0x{0:X}", _address); }
}
public String RawDisplay
{
get { return String.Format("{0:X}", Raw); }
}
private int _tareRaw;
public int TareRaw
{
get { return _tareRaw; }
set { Set(ref _tareRaw, value); }
}
private int _beerCount;
public int BeerCount
{
get { return _beerCount; }
set { Set(ref _beerCount, value); }
}
private double _percentFull;
public double PercentFull
{
get { return _percentFull; }
set { Set(ref _percentFull, value); }
}
public override TimeSpan UpdateInterval
{
get { return _updateInterval; }
}
private double _calibrationWeight;
public double CalibrationWeight
{
get { return _calibrationWeight; }
set { _calibrationWeight = value; }
}
public RelayCommand ZeroCommand
{
get { return new RelayCommand(() => StoreOffset()); }
}
public RelayCommand CalibrationCommand
{
get { return new RelayCommand(() => Calibrate()); }
}
}
}
Kegerator Class
C#using LagoVista.Common.Commanding;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Enumeration;
using Windows.Devices.I2c;
namespace LagoVista.IoT.Common.Kegerator
{
public class Kegerator : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Models.Keg _keg1;
private Models.Keg _keg2;
private Models.Keg _keg3;
private Models.Keg _keg4;
private CO2.CO2Tank _co2Tank;
private Kegerator() { }
public List<DeviceBase> _devices = new List<DeviceBase>();
private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
private bool Set<T>(ref T storage, T value, string columnName = null, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
this.RaisePropertyChanged(propertyName);
return true;
}
byte[] _scalesAddresses = { 0x43, 0x41, 0x40, 0x42 };
private const string I2C_CONTROLLER_NAME = "I2C1";
private Thermo.Temperatures _temperatures;
private Thermo.Controller _tempController;
private Scales.Scale _co2Scale;
private Dictionary<int, Scales.Scale> _kegScales;
private CO2.PressureSensor _pressureSensor;
private LED.LEDManager _ledManager;
private REST.KegeratorServices _kegServices;
private static Kegerator _kegerator = new Kegerator();
public static Kegerator Instance { get { return _kegerator; } }
private CloudServices.EventHubClient _eventHubClient;
System.Threading.Timer _timer;
private bool _initialized = false;
public async Task Init()
{
if (!_initialized)
{
_initialized = true;
var selector = I2cDevice.GetDeviceSelector(I2C_CONTROLLER_NAME); /* Find the selector string for the I2C bus controller */
var deviceInfo = (await DeviceInformation.FindAllAsync(selector)).FirstOrDefault(); /* Find the I2C bus controller device with our selector string */
var deviceId = deviceInfo == null ? (string)null : deviceInfo.Id;
_temperatures = new Thermo.Temperatures(0x48);
await _temperatures.Init(deviceId);
_devices.Add(_temperatures);
_tempController = new Thermo.Controller();
_tempController.Init(_temperatures);
_devices.Add(_tempController);
_pressureSensor = new CO2.PressureSensor();
await _pressureSensor.Init(deviceId, TimeSpan.FromSeconds(1));
_devices.Add(_pressureSensor);
_co2Scale = new Scales.Scale(0x44);
await _co2Scale.Init(deviceId, TimeSpan.FromSeconds(1));
_devices.Add(_co2Scale);
_co2Tank = new CO2.CO2Tank(_co2Scale, TimeSpan.FromSeconds(2));
_co2Tank.Load();
_devices.Add(_co2Tank);
_kegScales = new Dictionary<int, Scales.Scale>();
_eventHubClient = new CloudServices.EventHubClient(this, TimeSpan.FromSeconds(2));
_devices.Add(_eventHubClient);
for (var idx = 0; idx < 4; ++idx)
{
var scale = new Scales.Scale(_scalesAddresses[idx]);
await scale.Init(deviceId, TimeSpan.FromMilliseconds(500));
_kegScales.Add(idx, scale);
_devices.Add(scale);
}
_keg1 = new Models.Keg(1, _kegScales[0], TimeSpan.FromMilliseconds(500));
_keg1.Load();
_devices.Add(_keg1);
_keg2 = new Models.Keg(2, _kegScales[1], TimeSpan.FromMilliseconds(500));
_keg2.Load();
_devices.Add(_keg2);
_keg3 = new Models.Keg(3, _kegScales[2], TimeSpan.FromMilliseconds(500));
_keg3.Load();
_devices.Add(_keg3);
_keg4 = new Models.Keg(4, _kegScales[3], TimeSpan.FromMilliseconds(500));
_keg4.Load();
_devices.Add(_keg4);
DateInitialized = DateTime.Now.ToString();
Web.WebServer.Instance.StartServer();
_kegServices = new REST.KegeratorServices() { Port = 9500 };
_kegServices.EventContent += _kegServices_EventContent;
_kegServices.StartServer();
_timer = new System.Threading.Timer((state) =>
{
Refresh();
}, null, 0, 250);
}
}
private void _kegServices_EventContent(object sender, string e)
{
var parts = e.Split('/');
if (parts.Count() > 0)
{
switch (parts[1])
{
case "zero":
{
var scaleIndex = Convert.ToInt32(parts[2]);
_kegScales[scaleIndex].StoreOffset();
}
break;
case "cal":
{
var scaleIndex = Convert.ToInt32(parts[2]);
_kegScales[scaleIndex].CalibrationWeight = Convert.ToDouble(parts[3]);
_kegScales[scaleIndex].Calibrate();
}
break;
}
}
}
public void Refresh()
{
foreach (var device in _devices)
{
if (DateTime.Now > (device.LastUpdated + device.UpdateInterval))
device.Refresh();
}
LagoVista.Common.PlatformSupport.Services.DispatcherServices.Invoke(() =>
{
CurrentTimeDisplay = DateTime.Now.ToString();
RaisePropertyChanged("CurrentTimeDisplay");
});
}
public Thermo.Temperatures Temperatures { get { return _temperatures; } }
public Thermo.Controller TemperatureController { get { return _tempController; } }
private String _statusMessage;
public String StatusMessage
{
get { return _statusMessage; }
set { Set(ref _statusMessage, value); }
}
public List<Scales.Scale> KegScales
{
get { return _kegScales.Values.ToList(); }
}
public void ToggleCompressor()
{
if (_tempController.IsCompressorOn)
_tempController.CompressorOff();
else
_tempController.CompressorOn();
}
public String DateInitialized
{
get;
set;
}
public String CurrentTimeDisplay
{
get;
set;
}
public Scales.Scale CO2Scale
{
get { return _co2Scale; }
}
public CO2.PressureSensor PressureSensor
{
get { return _pressureSensor; }
}
public Models.Keg Keg1 { get { return _keg1; } }
public Models.Keg Keg2 { get { return _keg2; } }
public Models.Keg Keg3 { get { return _keg3; } }
public Models.Keg Keg4 { get { return _keg4; } }
public CO2.CO2Tank CO2Tank { get { return _co2Tank; } }
public RelayCommand ToggleCompressorCommand { get { return new RelayCommand(ToggleCompressor); } }
}
}
Comments