Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Saving energy with the .NET Micro Framework

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 ~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 netduino-mini micro-controller, an AdaFruit 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:

Startup sequence and 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 thermostat's 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 '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:

  • HS6635HBServo.cs: This class implements the driver for the 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 you're 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:  

#define NETDUINO_MINI
.

Any other definition would target the regular netduino. I was unable to test with the netduino Plus, so there's no provision for it in the code yet.

#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;

You must remember to include the proper reference to the Secret Labs assembly for the platform as well. If you don't your code might run, but expect very weird GPIO behavior Smiley

#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

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.


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();

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.

        private const uint LowHeat = 180;
private const uint Center = 100;
private const uint HighHeat = 0;

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.

        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);
            }
        }

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 arm's position centered, the call to _servo.Center() will hardly result in any motion at all.

                        InitializeClock();

            Log("\r\nWater Heater Controller v1.0\r\n"); 
            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);

It's 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:

        // 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();
        }

 

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.

Note that the servo's 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:

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);
                }
            }
        }

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:

 

        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("\r\n");
            _serialUI.AddDisplayItem("Main Menu:\r\n");
            _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();
        }

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:

_serialUI.AddInputItem(new SerialInputItem { Label = "End Hour (00-23)?", Callback = SetSchedule, Context = 4, StoreKey = "sched.endHour" });

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:

switch (item.Context) {
                case 0:
                    _serialUI.Store.Clear();
                    _serialUI.AddDisplayItem(Divider);
                    _serialUI.AddDisplayItem("Set Clock:\r\n");
                    _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:...

StoreKey: optional parameter used to preserve the user's 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 user's 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 aren't 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:

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

 

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 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 schematics of the mini.

Building the board:

 

The final board:

 

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.

 

 

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.

 

 

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:

 

 

 

Operation

Connect a dumb-terminal to COM2 on the netduino mini (or to COM1 on the regular netduino).

 

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:


[02/19/2011 19:29:40]
Water Heater Controller v1.0

[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]

Main Menu:
1 : Show Schedule
2 : Set Schedule
3 : Set Clock
4 : Swith Heater ON / Resume Schedule
X : Shutdown

[02/19/2011 19:29:52] Heater state change
[02/19/2011 19:29:52] Setting heater on high
1
-------------------------------------------------------------
Heater Weekly Schedule:

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]

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.

Follow the Discussion

  • The premise of the article - that the proportion of hours where hot water is needed is the same as the proportion of energy that is actually required - is false. Unless you have a terrible insulating system, a huge amount of energy isn't required to keep water hot. Most of your energy use should be in heating up cold water which is taken in to replace hot water used. You will not see a 66% reduction in energy costs; how much you will see depends on how good or bad your hot water insulation is, and the temperature differential between the hot water and the ambient temperature in its surroundings. Whatever that saving is, it needs to be compared with the opportunity costs of not having hot water whenever you want it, but rather having to stick to a schedule.

    That's not to take away from the cool DIY scheduled servo job, which looks neat.

  • Jeff BirtJeff Birt

    I like to see unique projects like this. We have an electric water heater that has this type of feature built in, have not see it on a gas unit though. The cut back does help a little but like barrkel said you have to consider the amount of energy needed to heat up a large volume of water between your two set points. The other area of concern is that the automation of the gas valve may lead to a dangerous situation. Since the position of the valve is not monitored a slip of the band clamp or glitch of the program has the potential to heat the water more than expected leading to possible scalding. Just be careful.

  • ijimekoijimeko

    @barrkel: you rock!

  • Clint RutkasClint I'm a "developer"

    @barrkel: Fabien is from Seattle, the days it drops below 45 I can count on one hand each year.  Different regions have different heating needs.  If you're in Alaska, you'd want to focus your energy savings somewhere else.

  • Fabien RoyerFabienRoyer Breaking Bad? Me?

    @barrkel: Hi Barry. Thanks for sharing your thoughts.

    Unfortunately for us, the gas water heater was installed in the garage by the builder when the house was built. In the winter months, it really gets cold there and the temperature differential between the hot water and the ambient temperature of the garage leads to waste. After installing this hack, I also wrapped the gas water heater in insulation to help reduce the loss. The perceived constraint of having to stick to a strict schedule is actually not that bad since the water still remains warm for quite some time after the heat is lowered.

    @Jeff Birt: the servo can only physically go from 0 to 180 degrees: the way the mechanical coupling works with the valve, calibrated to be on the HOT setting at the full extent of the servo's arm, it is impossible to get into a dangerous situation where the water would become too hot. The water never gets stone cold as the temperature is only lowered to a low heat setting, so it never takes a huge amount of energy to heat it back up.

     

    I plan on having a follow-up article on the actual savings in a few months: the proof will be in the pudding Wink

    Cheers,

    -Fabien.

  • SilvioSilvio

    Just curious as to what the "low" temperature is set to?  Not sure if you are aware, but if your water tank temperature drops below 120*F (50*C) Legionellosis will start to grow in your water tank (see Legionnaires disease.)  This is very serious.  There was a recent incident at the Playboy mansion where people had a run in with Legionnaires disease.
    http://en.wikipedia.org/wiki/Legionellosis

  • Fabien RoyerFabienRoyer Breaking Bad? Me?

    @Silvio: Hi Silvio. I am aware of this bacteria. When the water temperature is hot, it stays hot for long enough to kill any potential Legionellosis bacteria. Also, Legionellosis is more of a concern in cooling systems. This issue has been discussed a few times on the original post: http://fabienroyer.wordpress.com/2011/02/22/saving-energy-with-a-netduino/

    Cheers,

    -Fabien.

  • magicalclickmagicalclick C9 slogan, #dealwithit. WinPh8.1 IE empty tab crash and removable video control edition.

    Is it really necessary? The insulation would make lose of heat very marginal. Sure higher temperature difference will lose more heat, but, it shouldn't be too much. I like your direction, but, it would be better doing it at air condition kind of situation.

    I didn't know the full extent, but, please make sure you have a safty measure when that thing going beaserk. Just imagine Final Destination happens and you have some kind of fuse to stop it happening.

  • Kyle EppleyQuickC I'm a Parallel Programmer

    This is a good artical with great idea, great artical however,,,, I see a new hot water heater in your near future.  Both the servo and the Knob it's turning are likely only rated for 5000 to 10000 action before they fail.  In other words, the parts are not designed to be used daily.  Durabilty needs to be considered when the costs like a new water heater are large.

  • @QuickC: Going with your figures and his schedule it seems like he should get at least 6 years of use.

    (5000 lifespan uses / 2 daily uses) / 365 days = 6.8493150684931506849315068493151 years

    Or, since it's PI day....

    2.1802046998889772023134762105824 * PI

  • Jon UdellJon Udell

    I /love/ the popsicle sticks!

  • Vuosaaren ATK huolto- ja korjauspalvelut,Vuosaaren ATK Vuosaaren ATK

    Hi Barry. Thanks for your work!

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.