Kevin D Wolf
Published © MIT

Ultimate Kegerator

Use the power of a Windows 10 UWP app to control and monitor a kegerator to keep your beverages cold, fresh and always on tap.

ExpertFull instructions provided15,691
Ultimate Kegerator

Things used in this project

Hardware components

Arduino Pro Mini 328 - 3.3V/8MHz
SparkFun Arduino Pro Mini 328 - 3.3V/8MHz
×7
SparkFun Load Cell Amplifier - HX711
×1
Leviton 47603-12B 4x12 Telephone Distrobution Board
×1
Raspberry Pi 2 Model B
Raspberry Pi 2 Model B
×1
AC 100-120V 200-220V 5V 6A 30W Swich Power Supply
×1
IR Motion Sensor
×1
10.6 cu. ft. Chest Freezer
×1
Smart Weight Digital Postage Scale
×5
Rii K12 Ultra Slim 2.4 GHz Keyboard
×1
Rasbperry Pi WiFi Dongle
×1
Arduino DC 5V Relay Module
×1
DHT11 Temperature & Humidity Sensor
DHT11 Temperature & Humidity Sensor
×2
DHT22 Temperature Sensor
DHT22 Temperature Sensor
×3
HomeBrewStuff Stainless Steel Double Draft Beer Tower
×2
Beer Tower Cooler
×1
0-30 PSI I2C Pressure Sensor (3.3V) ABPMANN030PG2A3
×1

Hand tools and fabrication machines

Wood Working Tools
Tools to customize the outside of the freezer
Wire Cutter
Soldering iron (generic)
Soldering iron (generic)
Heat Shrink Tubing

Story

Read more

Schematics

System Connectivity

High Level System Component Diagram

Code

Keg Class

C#
Preview of Source Code prior to full source being released on GitHub. If you want early access or would like to help contribute please contact the author of this project
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#
Preview of Source Code prior to full source being released on GitHub. If you want early access or would like to help contribute please contact the author of this project
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#
Preview of Source Code prior to full source being released on GitHub. If you want early access or would like to help contribute please contact the author of this project
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); } }
    }
}

Credits

Kevin D Wolf

Kevin D Wolf

3 projects • 24 followers
Windows Embedded MVP Out of Tampa FL. Full time consultant.
Thanks to Paolo Patierno and Bayer White.

Comments