EDN Admin
Well-known member
Our home is equipped with a relatively old gas heater, built in 1996. It still works great, has been serviced regularly since it was installed and there is no good reason to replace it yet. However, it isn’t as energy efficient as more recent models. The other aspect of this gas water heater is that it keeps the water hot 24/7, 365 days a year whether we need it or not. In my home, we generally only need hot water in the morning between 7 and 9 AM and in the evening, from 5 to 9 PM. On weekends, our schedule is a bit whacky and we need hot water from 8 AM to 9PM. So, 30 hours for week days + 26 hours for weekends, that’s 56 hours / week where hot water is actually needed, as opposed to 168 hours / week when the heater is just left alone. In order words, in our house, we only really need a third of the water heater energy that we normally consume. To make matters worse, the water heater was installed in the garage by the builder and it gets pretty cold during the winter time. Considering that http://www.energysavers.gov/your_home/water_heating/index.cfm/mytopic=12760 ~25% of our heating bill goes into heating water , I felt compelled to stop this senseless waste. The idea that I came up with was to design a scheduler, configured to follow our weekly hot water usage pattern and capable of lowering the water heater temperature down to a minimum during off-hours. I wanted it to be cheap to build with easy-to-find parts and very reliable as my wife does not appreciate cold showers: I decided to use a http://www.netduino.com/netduinomini/specs.htm netduino-mini micro-controller , an AdaFruit http://www.adafruit.com/index.php?main_page=product_info&cPath=42&products_id=264 DS1307 real-time clock and a servo to adjust the temperature of the water heater. Here’s what the end result looks like in action: http://www.youtube.com/watch?v=QEmMS5qdOcI&feature=player_embedded Startup sequence and http://www.youtube.com/watch?v=ntumJIyvUzw&feature=player_embedded Manual override sequence The slow moving speed of the servo is intentional in the application in order to minimize wear and tear on the servo’s gears and the overall assembly. Application Overview Every 60 seconds, the application checks the current date and time against a weekly schedule tracking daily timeframes when the gas water heater should be turned ON and when it should be turned OFF. When it is time to turn the heater ON, the program first applies power the servo actuating the thermostats dial and then tells the servo to turn the dial until it reaches the High Heat position. The servo power is then removed. Conversely, when the time to turn the heater OFF comes, the same steps occur but this time, turning the dial all the way in the opposite direction. The configuration of the schedule and the clock is done through a serial interface The user can override the schedule by pressing a button which immediately turns the water heat ON. Pressing the button again resumes the normal schedule tracking process. Architecture The application is built on the open source http://netduinohelpers.codeplex.com netduino.helpers library which provides a set of hardware drivers written in C# targeting the netduino family of .Net Micro Framework micro-controllers. The drivers used by this application are: DS1307.cs: This class implements a complete driver for the http://pdfserv.maxim-ic.com/en/ds/DS1307.pdf Dallas Semiconductors / Maxim DS1307 I2C real-time clock . The clock has 56 bytes of battery-backed memory which the application uses to serialize and deserialize the weekly schedule data. HS6635HBServo.cs: This class implements the driver for the http://www.hitecrcd.com/products/digital/digital-sport/hs-6635hb.html HiTech HS-6635HB servo . While this class has only been tested with this particular servo, the implementation is generic enough and can be applied to many other brands of servos. SerialUserInterface.cs: This class provides building blocks needed to display and receive data over a serial communication port. User input is handled in a interrupt-driven manner and results in a callback when the Enter key is pressed. It also provides input value storage, basic input validation suitable for menu options and state management to track input sequences. PushButton.cs: This class provides a wrapper around the InterruptPort class of the .Net Micro Framework and makes it easy to use momentary switch in an application without polling inputs. Application Details Walking through the application, there are some details worth noting: The beginning of the application starts with a conditional compilation definition based on the hardware that youre targeting. Because the netduino boards have different format factors and on-board devices, pin assignments will vary. By default, the application is targeting the mini: <pre class="brush: csharp #define NETDUINO_MINI[/code]. Any other definition would target the regular netduino. I was unable to test with the netduino Plus, so theres no provision for it in the code yet. <pre class="brush: csharp #define NETDUINO_MINI
using System;
using System.Threading;
using System.Collections;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using netduino.helpers.Hardware;
using netduino.helpers.SerialUI;
using netduino.helpers.Servo;
using System.IO.Ports;
[/code] You must remember to include the proper reference to the Secret Labs assembly for the platform as well. If you dont your code might run, but expect very weird GPIO behavior <img src=http://ecn.channel9.msdn.com/o9/content/images/emoticons/emotion-1.gif?v=c9 alt=Smiley /> <pre class="brush: csharp #if NETDUINO_MINI
// You must ensure that you also have the reference set to SecretLabs.NETMF.Hardware.NetduinoMini in the project
// You must also remove the SecretLabs.NETMF.Hardware.Netduino if it was there.
using SecretLabs.NETMF.Hardware.NetduinoMini;
#else
// You must ensure that you also have the reference set to SecretLabs.NETMF.Hardware.Netduino in the project
// You must also remove the SecretLabs.NETMF.Hardware.NetduinoMini if it was there.
using SecretLabs.NETMF.Hardware.Netduino;
#endif
[/code] Most of the core objects in the application last for the lifetime of the application, except for the SerialUserInterface object which may be recycled if a communication failure occurs. <pre class="brush: csharp
namespace WaterHeaterController {
public class Program {
#if NETDUINO_MINI
private static readonly OutputPort _servoPowerEnable = new OutputPort(Pins.GPIO_PIN_16, false);
private static readonly PushButton _pushButton = new PushButton(Pin: Pins.GPIO_PIN_17, Target: PushButtonHandler);
private static readonly OutputPort _ledOverride = new OutputPort(Pins.GPIO_PIN_13, false);
private static readonly OutputPort _ledServoPowerEnable = new OutputPort(Pins.GPIO_PIN_15, false);
private static readonly PWM _ledLowHeat = new PWM(Pins.GPIO_PIN_18);
private static readonly PWM _ledHighHeat = new PWM(Pins.GPIO_PIN_19);
private static readonly HS6635HBServo _servo = new HS6635HBServo(Pins.GPIO_PIN_20,minPulse: 700, centerPulse: 1600);
private static SerialUserInterface _serialUI = new SerialUserInterface(Serial.COM2);
#else
private static readonly OutputPort _servoPowerEnable = new OutputPort(Pins.GPIO_PIN_D3, false);
private static readonly PushButton _pushButton = new PushButton(Pin: Pins.ONBOARD_SW1, Target: PushButtonHandler);
private static readonly OutputPort _ledOverride = new OutputPort(Pins.GPIO_PIN_D2, false);
private static readonly OutputPort _ledServoPowerEnable = new OutputPort(Pins.GPIO_PIN_D4, false);
private static readonly PWM _ledLowHeat = new PWM(Pins.GPIO_PIN_D5);
private static readonly PWM _ledHighHeat = new PWM(Pins.GPIO_PIN_D6);
private static readonly HS6635HBServo _servo = new HS6635HBServo(Pins.GPIO_PIN_D9,minPulse: 700, centerPulse: 1600);
private static SerialUserInterface _serialUI = new SerialUserInterface();
#endif
private static readonly Schedule _schedule = new Schedule();
private static readonly Status _status = new Status();
private static readonly DS1307 _clock = new DS1307();
[/code] The following constants reference absolute servo positions expressed in degrees. Depending on the physical configuration of the servo installation (vertical, horizontal, left or right of the thermostat), you may need to change these values so that the servo moves in the correct direction. The values below correspond to an upside down servo (arm pointing downwards) placed on the left of the gas valve. <pre class="brush: csharp private const uint LowHeat = 180;
private const uint Center = 100;
private const uint HighHeat = 0;[/code] When the application starts, it attempts to detect if the real-time clock was ever configured properly. If an exception occurs, caused by a faulty initialization of the DateTime object normally returned by _clock.Get(), a default date time is assigned to the clock and the schedule held in the user-backed memory of the clock is filled with zeros and the internal oscillator of the clock is finally started. <pre class="brush: csharp private static void InitializeClock() {
try {
_clock.Get();
} catch (Exception e) {
Debug.Print("Initializating the clock with default values due to: " + e);
byte[] ram = new byte[DS1307.DS1307_RAM_SIZE];
_clock.Set(new DateTime(2011, 1, 1, 12, 0, 0));
_clock.Halt(false);
_clock.SetRAM(ram);
}
}[/code] The next step initializes the position of the servo: in an attempt to avoid sudden or harsh movements of the thermostat dial, the application expects the user to have positioned the arm of the servo manually at a 90 degree angle before powering ON the controller: with the servo arms position centered, the call to _servo.Center() will hardly result in any motion at all. <pre class="brush: csharp InitializeClock();
Log("rnWater Heater Controller v1.0rn");
Log("Initializing...");
LoadSchedule();
PowerServo(true);
Log("Centering servo");
_servo.Center();
Log("Setting heater on high heat by default");
_servo.Move(Center, currentHeat);
Log("Running...");
PowerServo(false);
[/code] Its important to note that all calls to move the servo are prefixed with a PowerServo() call: the controller board was designed to control the 5 volt power supply to the servo through a transistor which gets activated / deactivated through that call. At the same time, an LED is turned ON / OFF indicating when the servo is moving. To ensure that the motion of the servo is always nice and slow, preventing wear and tear on the gas valve as much as possible, _servo.Move() operates the servo with one degree increments at a time, with a short pause in between steps: <pre class="brush: csharp // Slowly moves the servo from a position to another
public void Move(uint startDegree, uint endDegree, int delay = 80) {
if (delay <= 1) {
delay = 10;
}
if (startDegree < endDegree) {
for (var degree = startDegree; degree <= endDegree; degree++) {
Degree = degree;
Thread.Sleep(delay);
}
} else {
for (var degree = startDegree; degree > endDegree; degree--) {
Degree = degree;
Thread.Sleep(delay);
}
}
Release();
}[/code] The Degree property converts a degree value from 0 to 180 into servo pulses ranging from 900 to 2100, based on the HS-6635HB specs, using a mapping function described here: http://rosettacode.org/wiki/Map_range#C http://rosettacode.org/wiki/Map_range#C . Note that the servos spec will not always match the actual capabilities of the servo and you may need to tweak these min / max pulse values after testing your specific servo like this: <pre class="brush: csharp public static void Main() {
using (var servo = new HS6635HBServo(Pins.GPIO_PIN_D9))
{
servo.Center();
servo.Move(90, 0, 25);
while (true)
{
servo.Move(0, 180, 25);
servo.Move(180, 0, 25);
}
}
}[/code] In my case, I discovered that degree 0 corresponds to 700 instead of 900 called by the specs. Once the initialization completes, the main loop of the application runs indefinitely or until a command to shutdown is provided. The main job if the loop is to check time against the water heater schedule and to respond to user input events. User interactions take place over a serial port on the netduino with all the heavy lifting performed by the SerialInterface.cs class: it makes it easy to build simple menus and gather user-input in an event-driven fashion to avoid polling the serial port. When a menu option is recognized or a CR/LF was provided on a free-form field, a callback takes place to a method with the context required to process the user input. The main menu of the application shows the flow: <pre class="brush: csharp public static void MainMenu(SerialInputItem item) {
var SystemStatus = new ArrayList();
_status.Display(SystemStatus, _clock, _schedule);
_serialUI.Stop();
_serialUI.AddDisplayItem(Divider);
_serialUI.AddDisplayItem(SystemStatus);
_serialUI.AddDisplayItem("rn");
_serialUI.AddDisplayItem("Main Menu:rn");
_serialUI.AddInputItem(new SerialInputItem { Option = "1", Label = ": Show Schedule", Callback = ShowSchedule });
_serialUI.AddInputItem(new SerialInputItem { Option = "2", Label = ": Set Schedule", Callback = SetSchedule });
_serialUI.AddInputItem(new SerialInputItem { Option = "3", Label = ": Set Clock", Callback = SetClock, Context = 0 });
_serialUI.AddInputItem(new SerialInputItem { Option = "4", Label = ": Swith Heater ON / Resume Schedule", Callback = SwitchHeaterOn });
_serialUI.AddInputItem(new SerialInputItem { Option = "X", Label = ": Shutdown", Callback = Shutdown });
_serialUI.AddInputItem(new SerialInputItem { Callback = RefreshMainMenu });
_serialUI.Go();
}[/code] Managing the user interface happens between bookend calls to _serialUI.Stop() which disables the serial port interrupts and clear up any previously defined UI and and _serialUI.Go() which re-enables serial port interrupts and reset the state of the input buffer. The display of text to the user is handled which successive calls to _serialUI.AddDisplayItem() which will display the text in the order where it was provided. Defining user-input is done in the same manner, with successive calls to _serialUI.AddInputItem() which takes the following parameters: Option: defines an entry in a list of one or more options that the user can pick from. This is an optional parameter. When omitted, the user input will be free form, in which case, there must only be one input item presented to the user at a time. Label: a text label shown next to the Option field. This is an optional parameter. If the Option field is omitted, the Label should be provided to let the user know what kind of free-form input is format is expected. For example: <pre class="brush: csharp _serialUI.AddInputItem(new SerialInputItem { Label = "End Hour (00-23)?", Callback = SetSchedule, Context = 4, StoreKey = "sched.endHour" });
[/code] Callback: a mandatory delegate method to be called when the user hits the Enter key. If one or more Option parameter was specified, the user will be shown a error message if he did not provide a valid input instead of calling the delegate method. Providing a Callback parameter alone allows defining a catch all handler. Context: an optional integer value used to manage state transitions when multiple successive user input steps are needed to complete a form. For example: <pre class="brush: csharp switch (item.Context) {
case 0:
_serialUI.Store.Clear();
_serialUI.AddDisplayItem(Divider);
_serialUI.AddDisplayItem("Set Clock:rn");
_serialUI.AddInputItem(new SerialInputItem { Label = "Year (YYYY)?", Callback = SetClock, Context = 1, StoreKey = "clock.YYYY" });
break;
case 1:
_serialUI.AddInputItem(new SerialInputItem { Label = "Month (01-12)?", Callback = SetClock, Context = 2, StoreKey = "clock.MM" });
break;
case 2:
_serialUI.AddInputItem(new SerialInputItem { Label = "Day (01-31)?", Callback = SetClock, Context = 3, StoreKey = "clock.DD" });
break;
case 3:...[/code] StoreKey: optional parameter used to preserve the users input in a dictionary where the StoreKey is the name of the value provided by the user. In the above example, the call to _serialUI.Store.Clear(); ensures that the dictionary is empty before gathering the users input. Other things that are worth mentioning about the code: the .Net Micro Framework on the netduino is Spartan by design so that it can fit well within the constraints of the Atmel ARM 7 chip. As a result, things that many programmers may take for granted arent always there or may only be partially supported, such as: Generics, Reflection, Serialization, dynamic AppDomains creation, etc. Therefore, one has to expect making trade-offs and accept that the code will not always be as pure as it could be otherwise. This is one of the reasons for creating the netduino.helpers library which aims to provide re-usable building blocks relevant to the netduino platform, attempting to lower the barrier to entry to embedded hardware development for folks coming from a pure software development background. Building the Gas Water Heater Controller board Electronic components This Gas Water Heater controller board is built around the following: 1 http://www.netduino.com/netduinomini/specs.htm netduino-mini 1 http://www.adafruit.com/index.php?main_page=product_info&cPath=42&products_id=264 DS1307 real-time clock 1 http://www.servocity.com/html/hs-6635hb_stan__hi-torque.html HiTech HS-6635HB servo as well as the following components: 1 1N4001 rectifier diode used on the ground pin of the 9V power supply 1 2N2222 transistor switching the power to the servo 1 1N4001 rectifier diode connected to the base of the transistor 1 300 Ohm resistor connected in series with the base of the transistor 4 LEDs of different colors 4 ~220 Ohm resistors for the LEDs 1 momentary switch 1 100 Ohm resistor (to be used with the switch) 1 10K resistor (to be used with the switch) 1 9 volt / 1A power wall-wart power supply 1 generic proto board straight and angled pin headers 1 barrel jack connector for the power supply Simple Wiring Diagram <img src="http://fabienroyer.files.wordpress.com/2011/02/wiring-diagram.png?w=300&h=149" alt=" To further increase the life of the servo, I decided to control the power supply to the servo through a 2N2222A transistor. It works well because the servo doesn’t need to hold the water heater knob into place all the time: once positioned, the knob stays where it is and the servo no longer needs power to maintain its position. The base of the transistor is connected to the ‘Servo Power Enable’ (pin 16 of the netduino mini) through a 300 Ohm resistor in series with a 1N4001 rectifier diode. The diode is there to eliminate 0.7 volts present on the pin even when it is turned off. Here’s http://forums.netduino.com/index.php?/topic/1403-there-is-always-07-volts-on-digital-pins-even-when-theyre-not-active/ the thread on the netduino forums discussing the 0.7 volts issue which seems specific to the netduino mini. To be on the safe side, I also added a 1N4001 rectifier diode on pin 23 (power ground) of the netduino mini to prevent any potential damage to the micro-controller if the power were connected backwards. I did not see such protection on the http://netduino.com/netduinomini/schematic.pdf schematics of the mini . Building the board: http://fabienroyer.files.wordpress.com/2011/02/b-build-2of5.jpg <img class="size-medium wp-image-228 aligncenter" title="B-Build-2of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-2of5.jpg?w=200&h=300" alt="" width="200" height="300 http://fabienroyer.files.wordpress.com/2011/02/b-build-3of5.jpg <img class="size-medium wp-image-229 aligncenter" title="B-Build-3of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-3of5.jpg?w=200&h=300" alt="" width="200" height="300 The final board: http://fabienroyer.files.wordpress.com/2011/02/c-whc-controller.jpg <img class="size-medium wp-image-230 aligncenter" title="C-WHC-Controller" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-controller.jpg?w=300&h=173" alt="" width="300" height="173 Mechanical Parts 2 popsicle sticks A spool of thin metal wire A thin but sturdy brass rod A section of rubber gasket long enough to fit around the circumference of the water heater knob. Make sure that the width of the rubber gasket doesn’t exceed the width of the knob’s hand-grip An adjustable metal ring Hand-craft a bracket the length of the popsicle stick from a thin strip of brass 10 zip ties 1 small wood board for mounting the servo Mounting the servo on the water heater I chose to anchor the servo to the gas pipe of the water heater, using the section of the pipe to the left of the gas valve. To do so, I cut a piece of wood from left-over hardwood flooring material to fit the lower left area of the pipe. I drilled holes on the top and left sides of the board so that it could be secured to the gas pipe using zip ties. I also drilled 4 holes to secure the servo to the board with long thin screws and locking nuts. http://fabienroyer.files.wordpress.com/2011/02/c-whc-servo.jpg <img class="size-medium wp-image-224 aligncenter" title="C-WHC-Servo" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-servo.jpg?w=300&h=200" alt="" width="300" height="200 Connecting the servo to the control knob Drill two holes near the end of the popsicle sticks. Make sure that the thin brass rod fits easily through the holes but doesn’t have wiggle room either. Secure one of the popsicle sticks to the brass bracket with some tape then with the metal wire wrapped tightly around it. Insert the brass bracket between the rubber gasket strip and the metal ring Tighten the metal ring around the water heater knob. The final assembly should feel tight and strong while turning the knob. http://fabienroyer.files.wordpress.com/2011/02/c-whc-dial.jpg <img class="aligncenter size-medium wp-image-225" title="C-WHC-Dial" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-dial.jpg?w=300&h=200" alt="" width="300" height="200 To secure the other popsicle stick to the arm of the servo, drill a few holes into the stick, matching the holes in the servo’s arm. Then weave the thin metal wire through the holes, then wrap the metal wire tightly around the stick and the servo’s arm. The final assembly should also be tight and strong while turning the servo’s arm. Connect the popsicle sticks together with the brass rod and secure it by bending it carefully around the ends of the sticks. It’s easier to do this while the servo’s arm is not attached to the servo. Finally, re-attach the arm to the servo and test the assembly by moving the servo’s arm slowly. The knob should turn in sync with the servo with ease: http://fabienroyer.files.wordpress.com/2011/02/c-whc-heater.jpg <img class="aligncenter size-medium wp-image-226" title="C-WHC-Heater" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-heater.jpg?w=300&h=200" alt="" width="300" height="200 Operation Connect a dumb-terminal to COM2 on the netduino mini (or to COM1 on the regular netduino). http://fabienroyer.files.wordpress.com/2011/02/b-build-5of5.jpg <img class="size-medium wp-image-232 aligncenter" title="B-Build-5of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-5of5.jpg?w=300&h=200" alt="" width="300" height="200 http://fabienroyer.files.wordpress.com/2011/02/b-build-4of5.jpg <img class="size-medium wp-image-233 aligncenter" title="B-Build-4of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-4of5.jpg?w=300&h=200" alt="" width="300" height="200 Using the serial interface: Set the clock’s date and time and define a schedule when the heater should turn ON. The schedule tracks 7 days, with 4 timeslots for each day. Each timeslot has a begin time defining when the heater should turn itself ON and an end time, defining when the heater should turn itself OFF. Setting a timeslot to 0 resets the timeslot and the heater stays OFF. Keep in mind that the heater timeslots and the clock expect to work on a 24 hour schedule. Sample output:
Meaning of the LEDs High heat LED: ON indicates that the water heater is set to high heat. Low heat LED: ON indicates that the water heater is set to low heat. Servo Active LED: ON indicates that the servo is changing position. High heat override LED: ON indicates that the push button was used to override the schedule. Starting the water heater controller the first time Before you apply power to the board, make sure that the arm of the servo is centered (vertical position). This will ensure that the startup sequence is smooth. From there, the controller will slowly set the water heater knob on high heat before tracking to the schedule. Cheers, -Fabien. <img src="http://m.webtrends.com/dcs1wotjh10000w0irc493s0e_6x1g/njs.gif?dcssip=channel9.msdn.com&dcsuri=http://channel9.msdn.com/Feeds/RSS&WT.dl=0&WT.entryid=Entry:RSSView:d2b430175afe457f833e9e9300268089
View the full article
using System;
using System.Threading;
using System.Collections;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using netduino.helpers.Hardware;
using netduino.helpers.SerialUI;
using netduino.helpers.Servo;
using System.IO.Ports;
[/code] You must remember to include the proper reference to the Secret Labs assembly for the platform as well. If you dont your code might run, but expect very weird GPIO behavior <img src=http://ecn.channel9.msdn.com/o9/content/images/emoticons/emotion-1.gif?v=c9 alt=Smiley /> <pre class="brush: csharp #if NETDUINO_MINI
// You must ensure that you also have the reference set to SecretLabs.NETMF.Hardware.NetduinoMini in the project
// You must also remove the SecretLabs.NETMF.Hardware.Netduino if it was there.
using SecretLabs.NETMF.Hardware.NetduinoMini;
#else
// You must ensure that you also have the reference set to SecretLabs.NETMF.Hardware.Netduino in the project
// You must also remove the SecretLabs.NETMF.Hardware.NetduinoMini if it was there.
using SecretLabs.NETMF.Hardware.Netduino;
#endif
[/code] Most of the core objects in the application last for the lifetime of the application, except for the SerialUserInterface object which may be recycled if a communication failure occurs. <pre class="brush: csharp
namespace WaterHeaterController {
public class Program {
#if NETDUINO_MINI
private static readonly OutputPort _servoPowerEnable = new OutputPort(Pins.GPIO_PIN_16, false);
private static readonly PushButton _pushButton = new PushButton(Pin: Pins.GPIO_PIN_17, Target: PushButtonHandler);
private static readonly OutputPort _ledOverride = new OutputPort(Pins.GPIO_PIN_13, false);
private static readonly OutputPort _ledServoPowerEnable = new OutputPort(Pins.GPIO_PIN_15, false);
private static readonly PWM _ledLowHeat = new PWM(Pins.GPIO_PIN_18);
private static readonly PWM _ledHighHeat = new PWM(Pins.GPIO_PIN_19);
private static readonly HS6635HBServo _servo = new HS6635HBServo(Pins.GPIO_PIN_20,minPulse: 700, centerPulse: 1600);
private static SerialUserInterface _serialUI = new SerialUserInterface(Serial.COM2);
#else
private static readonly OutputPort _servoPowerEnable = new OutputPort(Pins.GPIO_PIN_D3, false);
private static readonly PushButton _pushButton = new PushButton(Pin: Pins.ONBOARD_SW1, Target: PushButtonHandler);
private static readonly OutputPort _ledOverride = new OutputPort(Pins.GPIO_PIN_D2, false);
private static readonly OutputPort _ledServoPowerEnable = new OutputPort(Pins.GPIO_PIN_D4, false);
private static readonly PWM _ledLowHeat = new PWM(Pins.GPIO_PIN_D5);
private static readonly PWM _ledHighHeat = new PWM(Pins.GPIO_PIN_D6);
private static readonly HS6635HBServo _servo = new HS6635HBServo(Pins.GPIO_PIN_D9,minPulse: 700, centerPulse: 1600);
private static SerialUserInterface _serialUI = new SerialUserInterface();
#endif
private static readonly Schedule _schedule = new Schedule();
private static readonly Status _status = new Status();
private static readonly DS1307 _clock = new DS1307();
[/code] The following constants reference absolute servo positions expressed in degrees. Depending on the physical configuration of the servo installation (vertical, horizontal, left or right of the thermostat), you may need to change these values so that the servo moves in the correct direction. The values below correspond to an upside down servo (arm pointing downwards) placed on the left of the gas valve. <pre class="brush: csharp private const uint LowHeat = 180;
private const uint Center = 100;
private const uint HighHeat = 0;[/code] When the application starts, it attempts to detect if the real-time clock was ever configured properly. If an exception occurs, caused by a faulty initialization of the DateTime object normally returned by _clock.Get(), a default date time is assigned to the clock and the schedule held in the user-backed memory of the clock is filled with zeros and the internal oscillator of the clock is finally started. <pre class="brush: csharp private static void InitializeClock() {
try {
_clock.Get();
} catch (Exception e) {
Debug.Print("Initializating the clock with default values due to: " + e);
byte[] ram = new byte[DS1307.DS1307_RAM_SIZE];
_clock.Set(new DateTime(2011, 1, 1, 12, 0, 0));
_clock.Halt(false);
_clock.SetRAM(ram);
}
}[/code] The next step initializes the position of the servo: in an attempt to avoid sudden or harsh movements of the thermostat dial, the application expects the user to have positioned the arm of the servo manually at a 90 degree angle before powering ON the controller: with the servo arms position centered, the call to _servo.Center() will hardly result in any motion at all. <pre class="brush: csharp InitializeClock();
Log("rnWater Heater Controller v1.0rn");
Log("Initializing...");
LoadSchedule();
PowerServo(true);
Log("Centering servo");
_servo.Center();
Log("Setting heater on high heat by default");
_servo.Move(Center, currentHeat);
Log("Running...");
PowerServo(false);
[/code] Its important to note that all calls to move the servo are prefixed with a PowerServo() call: the controller board was designed to control the 5 volt power supply to the servo through a transistor which gets activated / deactivated through that call. At the same time, an LED is turned ON / OFF indicating when the servo is moving. To ensure that the motion of the servo is always nice and slow, preventing wear and tear on the gas valve as much as possible, _servo.Move() operates the servo with one degree increments at a time, with a short pause in between steps: <pre class="brush: csharp // Slowly moves the servo from a position to another
public void Move(uint startDegree, uint endDegree, int delay = 80) {
if (delay <= 1) {
delay = 10;
}
if (startDegree < endDegree) {
for (var degree = startDegree; degree <= endDegree; degree++) {
Degree = degree;
Thread.Sleep(delay);
}
} else {
for (var degree = startDegree; degree > endDegree; degree--) {
Degree = degree;
Thread.Sleep(delay);
}
}
Release();
}[/code] The Degree property converts a degree value from 0 to 180 into servo pulses ranging from 900 to 2100, based on the HS-6635HB specs, using a mapping function described here: http://rosettacode.org/wiki/Map_range#C http://rosettacode.org/wiki/Map_range#C . Note that the servos spec will not always match the actual capabilities of the servo and you may need to tweak these min / max pulse values after testing your specific servo like this: <pre class="brush: csharp public static void Main() {
using (var servo = new HS6635HBServo(Pins.GPIO_PIN_D9))
{
servo.Center();
servo.Move(90, 0, 25);
while (true)
{
servo.Move(0, 180, 25);
servo.Move(180, 0, 25);
}
}
}[/code] In my case, I discovered that degree 0 corresponds to 700 instead of 900 called by the specs. Once the initialization completes, the main loop of the application runs indefinitely or until a command to shutdown is provided. The main job if the loop is to check time against the water heater schedule and to respond to user input events. User interactions take place over a serial port on the netduino with all the heavy lifting performed by the SerialInterface.cs class: it makes it easy to build simple menus and gather user-input in an event-driven fashion to avoid polling the serial port. When a menu option is recognized or a CR/LF was provided on a free-form field, a callback takes place to a method with the context required to process the user input. The main menu of the application shows the flow: <pre class="brush: csharp public static void MainMenu(SerialInputItem item) {
var SystemStatus = new ArrayList();
_status.Display(SystemStatus, _clock, _schedule);
_serialUI.Stop();
_serialUI.AddDisplayItem(Divider);
_serialUI.AddDisplayItem(SystemStatus);
_serialUI.AddDisplayItem("rn");
_serialUI.AddDisplayItem("Main Menu:rn");
_serialUI.AddInputItem(new SerialInputItem { Option = "1", Label = ": Show Schedule", Callback = ShowSchedule });
_serialUI.AddInputItem(new SerialInputItem { Option = "2", Label = ": Set Schedule", Callback = SetSchedule });
_serialUI.AddInputItem(new SerialInputItem { Option = "3", Label = ": Set Clock", Callback = SetClock, Context = 0 });
_serialUI.AddInputItem(new SerialInputItem { Option = "4", Label = ": Swith Heater ON / Resume Schedule", Callback = SwitchHeaterOn });
_serialUI.AddInputItem(new SerialInputItem { Option = "X", Label = ": Shutdown", Callback = Shutdown });
_serialUI.AddInputItem(new SerialInputItem { Callback = RefreshMainMenu });
_serialUI.Go();
}[/code] Managing the user interface happens between bookend calls to _serialUI.Stop() which disables the serial port interrupts and clear up any previously defined UI and and _serialUI.Go() which re-enables serial port interrupts and reset the state of the input buffer. The display of text to the user is handled which successive calls to _serialUI.AddDisplayItem() which will display the text in the order where it was provided. Defining user-input is done in the same manner, with successive calls to _serialUI.AddInputItem() which takes the following parameters: Option: defines an entry in a list of one or more options that the user can pick from. This is an optional parameter. When omitted, the user input will be free form, in which case, there must only be one input item presented to the user at a time. Label: a text label shown next to the Option field. This is an optional parameter. If the Option field is omitted, the Label should be provided to let the user know what kind of free-form input is format is expected. For example: <pre class="brush: csharp _serialUI.AddInputItem(new SerialInputItem { Label = "End Hour (00-23)?", Callback = SetSchedule, Context = 4, StoreKey = "sched.endHour" });
[/code] Callback: a mandatory delegate method to be called when the user hits the Enter key. If one or more Option parameter was specified, the user will be shown a error message if he did not provide a valid input instead of calling the delegate method. Providing a Callback parameter alone allows defining a catch all handler. Context: an optional integer value used to manage state transitions when multiple successive user input steps are needed to complete a form. For example: <pre class="brush: csharp switch (item.Context) {
case 0:
_serialUI.Store.Clear();
_serialUI.AddDisplayItem(Divider);
_serialUI.AddDisplayItem("Set Clock:rn");
_serialUI.AddInputItem(new SerialInputItem { Label = "Year (YYYY)?", Callback = SetClock, Context = 1, StoreKey = "clock.YYYY" });
break;
case 1:
_serialUI.AddInputItem(new SerialInputItem { Label = "Month (01-12)?", Callback = SetClock, Context = 2, StoreKey = "clock.MM" });
break;
case 2:
_serialUI.AddInputItem(new SerialInputItem { Label = "Day (01-31)?", Callback = SetClock, Context = 3, StoreKey = "clock.DD" });
break;
case 3:...[/code] StoreKey: optional parameter used to preserve the users input in a dictionary where the StoreKey is the name of the value provided by the user. In the above example, the call to _serialUI.Store.Clear(); ensures that the dictionary is empty before gathering the users input. Other things that are worth mentioning about the code: the .Net Micro Framework on the netduino is Spartan by design so that it can fit well within the constraints of the Atmel ARM 7 chip. As a result, things that many programmers may take for granted arent always there or may only be partially supported, such as: Generics, Reflection, Serialization, dynamic AppDomains creation, etc. Therefore, one has to expect making trade-offs and accept that the code will not always be as pure as it could be otherwise. This is one of the reasons for creating the netduino.helpers library which aims to provide re-usable building blocks relevant to the netduino platform, attempting to lower the barrier to entry to embedded hardware development for folks coming from a pure software development background. Building the Gas Water Heater Controller board Electronic components This Gas Water Heater controller board is built around the following: 1 http://www.netduino.com/netduinomini/specs.htm netduino-mini 1 http://www.adafruit.com/index.php?main_page=product_info&cPath=42&products_id=264 DS1307 real-time clock 1 http://www.servocity.com/html/hs-6635hb_stan__hi-torque.html HiTech HS-6635HB servo as well as the following components: 1 1N4001 rectifier diode used on the ground pin of the 9V power supply 1 2N2222 transistor switching the power to the servo 1 1N4001 rectifier diode connected to the base of the transistor 1 300 Ohm resistor connected in series with the base of the transistor 4 LEDs of different colors 4 ~220 Ohm resistors for the LEDs 1 momentary switch 1 100 Ohm resistor (to be used with the switch) 1 10K resistor (to be used with the switch) 1 9 volt / 1A power wall-wart power supply 1 generic proto board straight and angled pin headers 1 barrel jack connector for the power supply Simple Wiring Diagram <img src="http://fabienroyer.files.wordpress.com/2011/02/wiring-diagram.png?w=300&h=149" alt=" To further increase the life of the servo, I decided to control the power supply to the servo through a 2N2222A transistor. It works well because the servo doesn’t need to hold the water heater knob into place all the time: once positioned, the knob stays where it is and the servo no longer needs power to maintain its position. The base of the transistor is connected to the ‘Servo Power Enable’ (pin 16 of the netduino mini) through a 300 Ohm resistor in series with a 1N4001 rectifier diode. The diode is there to eliminate 0.7 volts present on the pin even when it is turned off. Here’s http://forums.netduino.com/index.php?/topic/1403-there-is-always-07-volts-on-digital-pins-even-when-theyre-not-active/ the thread on the netduino forums discussing the 0.7 volts issue which seems specific to the netduino mini. To be on the safe side, I also added a 1N4001 rectifier diode on pin 23 (power ground) of the netduino mini to prevent any potential damage to the micro-controller if the power were connected backwards. I did not see such protection on the http://netduino.com/netduinomini/schematic.pdf schematics of the mini . Building the board: http://fabienroyer.files.wordpress.com/2011/02/b-build-2of5.jpg <img class="size-medium wp-image-228 aligncenter" title="B-Build-2of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-2of5.jpg?w=200&h=300" alt="" width="200" height="300 http://fabienroyer.files.wordpress.com/2011/02/b-build-3of5.jpg <img class="size-medium wp-image-229 aligncenter" title="B-Build-3of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-3of5.jpg?w=200&h=300" alt="" width="200" height="300 The final board: http://fabienroyer.files.wordpress.com/2011/02/c-whc-controller.jpg <img class="size-medium wp-image-230 aligncenter" title="C-WHC-Controller" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-controller.jpg?w=300&h=173" alt="" width="300" height="173 Mechanical Parts 2 popsicle sticks A spool of thin metal wire A thin but sturdy brass rod A section of rubber gasket long enough to fit around the circumference of the water heater knob. Make sure that the width of the rubber gasket doesn’t exceed the width of the knob’s hand-grip An adjustable metal ring Hand-craft a bracket the length of the popsicle stick from a thin strip of brass 10 zip ties 1 small wood board for mounting the servo Mounting the servo on the water heater I chose to anchor the servo to the gas pipe of the water heater, using the section of the pipe to the left of the gas valve. To do so, I cut a piece of wood from left-over hardwood flooring material to fit the lower left area of the pipe. I drilled holes on the top and left sides of the board so that it could be secured to the gas pipe using zip ties. I also drilled 4 holes to secure the servo to the board with long thin screws and locking nuts. http://fabienroyer.files.wordpress.com/2011/02/c-whc-servo.jpg <img class="size-medium wp-image-224 aligncenter" title="C-WHC-Servo" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-servo.jpg?w=300&h=200" alt="" width="300" height="200 Connecting the servo to the control knob Drill two holes near the end of the popsicle sticks. Make sure that the thin brass rod fits easily through the holes but doesn’t have wiggle room either. Secure one of the popsicle sticks to the brass bracket with some tape then with the metal wire wrapped tightly around it. Insert the brass bracket between the rubber gasket strip and the metal ring Tighten the metal ring around the water heater knob. The final assembly should feel tight and strong while turning the knob. http://fabienroyer.files.wordpress.com/2011/02/c-whc-dial.jpg <img class="aligncenter size-medium wp-image-225" title="C-WHC-Dial" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-dial.jpg?w=300&h=200" alt="" width="300" height="200 To secure the other popsicle stick to the arm of the servo, drill a few holes into the stick, matching the holes in the servo’s arm. Then weave the thin metal wire through the holes, then wrap the metal wire tightly around the stick and the servo’s arm. The final assembly should also be tight and strong while turning the servo’s arm. Connect the popsicle sticks together with the brass rod and secure it by bending it carefully around the ends of the sticks. It’s easier to do this while the servo’s arm is not attached to the servo. Finally, re-attach the arm to the servo and test the assembly by moving the servo’s arm slowly. The knob should turn in sync with the servo with ease: http://fabienroyer.files.wordpress.com/2011/02/c-whc-heater.jpg <img class="aligncenter size-medium wp-image-226" title="C-WHC-Heater" src="http://fabienroyer.files.wordpress.com/2011/02/c-whc-heater.jpg?w=300&h=200" alt="" width="300" height="200 Operation Connect a dumb-terminal to COM2 on the netduino mini (or to COM1 on the regular netduino). http://fabienroyer.files.wordpress.com/2011/02/b-build-5of5.jpg <img class="size-medium wp-image-232 aligncenter" title="B-Build-5of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-5of5.jpg?w=300&h=200" alt="" width="300" height="200 http://fabienroyer.files.wordpress.com/2011/02/b-build-4of5.jpg <img class="size-medium wp-image-233 aligncenter" title="B-Build-4of5" src="http://fabienroyer.files.wordpress.com/2011/02/b-build-4of5.jpg?w=300&h=200" alt="" width="300" height="200 Using the serial interface: Set the clock’s date and time and define a schedule when the heater should turn ON. The schedule tracks 7 days, with 4 timeslots for each day. Each timeslot has a begin time defining when the heater should turn itself ON and an end time, defining when the heater should turn itself OFF. Setting a timeslot to 0 resets the timeslot and the heater stays OFF. Keep in mind that the heater timeslots and the clock expect to work on a 24 hour schedule. Sample output:
Code:
[02/19/2011 19:29:40]
Water Heater Controller v1.0
Code:
[02/19/2011 19:29:40] Initializing...
[02/19/2011 19:29:40] Loading schedule
[02/19/2011 19:29:40] Centering servo
[02/19/2011 19:29:41] Setting heater on high heat by default
[02/19/2011 19:29:51] Running...
-------------------------------------------------------------
Time: Saturday, 19 February 2011 19:29:51
Heater Schedule Today: Sat [8-21] [0-0] [0-0] [0-0]
Heater Status: ON [scheduled]
Code:
Main Menu:
1 : Show Schedule
2 : Set Schedule
3 : Set Clock
4 : Swith Heater ON / Resume Schedule
X : Shutdown
Code:
[02/19/2011 19:29:52] Heater state change
[02/19/2011 19:29:52] Setting heater on high
1
-------------------------------------------------------------
Heater Weekly Schedule:
Code:
Sun [8-21] [0-0] [0-0] [0-0]
Mon [6-9] [17-21] [0-0] [0-0]
Tue [6-9] [17-21] [0-0] [0-0]
Wed [6-9] [17-21] [0-0] [0-0]
Thu [6-9] [17-21] [0-0] [0-0]
Fri [6-9] [17-21] [0-0] [0-0]
Sat [8-21] [0-0] [0-0] [0-0]
View the full article