I am working on a Family Photos application that organizes digital pictures into a chronological structure. I had created thrown together an application like this years ago, but it was no longer going to cut it, especially with the new Windows Home Server in the mix. One key requirement for Family Photos is to very easily move pictures off of the SD card from the camera, queuing them up for organization, and allowing the SD card to be taken back and put back into the camera. Our old process involved the following:
- Unlock the screen for Jeff's computer
- Plug the SD card into the SD->USB adapter
- Double-click an icon on the desktop (minimizing windows if needed)
- Watch the console window and wait for it to print a message stating that the program has finished moving all files off the drive
- Take the SD card out and be on your way
While a little clumsy, it had a high WAF. Kelly knew how to do it, and it allowed her to be on her way in minutes, with a clear indication as to when she could take the card out. Family Photos needs to offer the same benefits while ideally simplifying the process even further.
The HP MediaSmart Windows Home Server adds an interesting twist on this problem too: it's headless. No monitor, keyboard or mouse. That means there's no icon on the desktop, no mouse to click with anyway, and no console window to offer status indicators. There's no user input and no output. So how can we tell the server, "Hey, I just put in a drive that has photos. Take the photos off of there as quickly as possible and let me know when you're done so that I can take the drive back. Then, after I'm gone, organize all of the pictures for me. Thanks!" I haven't done any recreational coding since we moved, but this problem has been stirring in the back of my head for months. I'm finally working on it, and it feels great.
It boils down to the two requirements: the server needs to accept input, and it needs to give output. The rest of this article will focus on the output aspect, and we'll leave the input for another post. We need to think hard about ways the home server can provide status indicators as its output. Let's consider some options, ignoring the user input problem.
- Require a remote desktop session into the server to perform the task so that a console window can show status, just like the old process
- Send an email or IM when completed
- Send a network message to the family laptop, triggering it to pop up a dialog
None of those is good. This is where I was stuck for a long time. But the other night, I thought of option 4, and it was the clear winner.
- Find a way to programmatically flash the LEDs on the MediaSmart server
The MediaSmart server console provides a slider that adjusts the brightness of the LEDs for the drives. The idea was to crank the brightness all the way up while the files were being moved over, and then dim the lights way back down when finished. I'd tell Kelly, "When the lights basically turn off, you can take the card back out." So, how can the brightness be controlled programmatically? Let's ask Google.
Shiver me timbers! An Easter Egg!? That lets you perform light shows on your MediaSmart Server? Go read this post and watch the video real quick to see what I’m talking about.
…
Now, that's just cool! And boy does that offer up some new possibilities! It turns out that the MediaSmart servers support an array of light themes, along with the variable brightness. Instead of just changing the brightness, I can alter the light theme to give clues as to what the server is doing.
Specifically, when the process is copying files up from the USB stick, I'll have bright, red lights ascend over the USB drive. When that's finished and the server begins doing some crazy processing of the photos, I'll have the server put on a holiday light show. The dramatic change in theme will clearly indicate that the server is finished copying the files and it's off doing work--take the card out whenever you'd like.
It only took a minute to find that the LED settings are stored in the registry under HKLM. And it was pretty straight-forward to determine the values of the themes and the brightness. With this information in hand, the HomeServer.Notifications.Lights class is born.
1: using System;
2: using Microsoft.Win32;
3:
4: namespace HomeServer.Notifications
5: {
6: /// <summary>
7: /// Manipulate the lights on an HP MediaSmart Windows Home Server
8: /// </summary>
9: public class Lights : IDisposable
10: {
11: // Registry LocalMachine key path where the values are stored (as DWORD values)
12: private const string _key = @"SOFTWARE\Hewlett-Packard\HP MediaSmart Server\LEDs";
13:
14: // The registry key that we'll work against
15: private RegistryKey _leds;
16:
17: // Whether or not the key is currently opened for writing
18: private bool _isWriting;
19:
20: /// <summary>
21: /// Gets whether the lights are available on the current system.
22: /// </summary>
23: /// <remarks>
24: /// When unavailable, any attempt to read or write values will
25: /// result in an InvalidOperationException.
26: /// </remarks>
27: public bool Available
28: {
29: get
30: {
31: // Try to open the registry key for reading
32: OpenForReading();
33:
34: // If we found the key, then indicate that the lights are available
35: return _leds != null;
36: }
37: }
38:
39: /// <summary>
40: /// Gets or Sets the native value for the brightness
41: /// </summary>
42: private object BrightnessValue
43: {
44: get
45: {
46: return Read("Brightness");
47: }
48: set
49: {
50: Write("Brightness", value);
51: }
52: }
53:
54: /// <summary>
55: /// Gets or Sets the native value for the theme
56: /// </summary>
57: private object ThemeValue
58: {
59: get
60: {
61: return Read("Theme");
62: }
63: set
64: {
65: Write("Theme", value);
66: }
67: }
68:
69: /// <summary>
70: /// The current brightness value: 0-9
71: /// </summary>
72: public Int32 CurrentBrightness
73: {
74: get { return Convert.ToInt32(BrightnessValue); }
75: set
76: {
77: if (value < 0 || value > 9)
78: {
79: throw new ArgumentOutOfRangeException("Brightness values must be 0-9");
80: }
81:
82: BrightnessValue = value;
83: }
84: }
85:
86: /// <summary>
87: /// The current Theme of the lights
88: /// </summary>
89: public Theme CurrentTheme
90: {
91: get { return (Theme)Convert.ToInt32(ThemeValue); }
92: set { ThemeValue = (Int32)value; }
93: }
94:
95: /// <summary>
96: /// Open the registry key for reading
97: /// </summary>
98: /// <remarks>
99: /// Store the open key in a member variable, and don't re-open
100: /// the key if it's already open
101: /// </remarks>
102: private void OpenForReading()
103: {
104: if (_leds == null)
105: {
106: _leds = Registry.LocalMachine.OpenSubKey(_key, false);
107: }
108: }
109:
110: /// <summary>
111: /// Read a native value from the registry key
112: /// </summary>
113: /// <remarks>
114: /// Will ensure the lights are available and then open the key
115: /// for reading in order to get the value
116: /// </remarks>
117: /// <param name="valueName"></param>
118: /// <returns></returns>
119: private object Read(string valueName)
120: {
121: EnsureAvailable();
122: OpenForReading();
123:
124: return _leds.GetValue(valueName);
125: }
126:
127: /// <summary>
128: /// Write a native key value
129: /// </summary>
130: /// <remarks>
131: /// Will ensure the lights are available and that the key
132: /// is presently opened for writing. If it was previously
133: /// opened for reading, it will be closed and then re-opened
134: /// for writing.
135: /// </remarks>
136: /// <param name="valueName"></param>
137: /// <param name="value"></param>
138: private void Write(string valueName, object value)
139: {
140: // Make sure the lights are available
141: EnsureAvailable();
142:
143: // If we already had the key open for reading, close it
144: if (_leds != null && !_isWriting)
145: {
146: _leds.Close();
147: _leds = null;
148: }
149:
150: // If we don't have the key open, open it for writing
151: if (_leds == null)
152: {
153: _leds = Registry.LocalMachine.OpenSubKey(_key, true);
154: _isWriting = true;
155: }
156:
157: // Set the value and flush to disk
158: _leds.SetValue(valueName, value);
159: _leds.Flush();
160: }
161:
162: /// <summary>
163: /// Ensure the lights are available. When unavailable, throw
164: /// InvalidOperationException.
165: /// </summary>
166: private void EnsureAvailable()
167: {
168: if (!Available)
169: {
170: throw new InvalidOperationException("Lights not available. Maybe this isn't a MediaSmart server.");
171: }
172: }
173:
174: /// <summary>
175: /// Implement IDisposable, properly disposing of resources
176: /// </summary>
177: public void Dispose()
178: {
179: // Close the registry key if still open
180: if (_leds != null)
181: {
182: _leds.Close();
183: _leds = null;
184: }
185: }
186:
187: /// <summary>
188: /// Offer some helper values for brightness, allowing for consistent usage
189: /// </summary>
190: /// <remarks>
191: /// This is a static class with these readonly properties instead of an enum
192: /// because an enum would have required constant casting.
193: /// </remarks>
194: public static class Brightness
195: {
196: public static readonly Int32 Off = 0;
197: public static readonly Int32 Low = 1;
198: public static readonly Int32 MediumLow = 3;
199: public static readonly Int32 Medium = 4;
200: public static readonly Int32 MediumHigh = 5;
201: public static readonly Int32 High = 7;
202: public static readonly Int32 Max = 9;
203: }
204:
205: /// <summary>
206: /// The list of themes supported by the server
207: /// </summary>
208: public enum Theme
209: {
210: Default = 0,
211: HolidayLights = 1,
212: DescendingChaserBlue = 2,
213: AscendingChaserBlue = 3,
214: DescendingChaserPurple = 4,
215: AscendingChaserPurple = 5,
216: DescendingChaserRed = 6,
217: AscendingChaserRed = 7,
218: PulsingColors = 8,
219: PulsingBlue = 9,
220: PulsingPurple = 10,
221: PulsingRed = 11,
222: PulsingSystemColors = 12,
223: NightRiderBlue = 13,
224: NightRiderPurple = 14,
225: NightRiderRed = 15,
226: MorseCodeCredits = 16
227: }
228: }
229: }
The usage is pretty straight-forward.
1: if (lights.Available)
2: {
3: lights.CurrentTheme = HomeServer.Notifications.Lights.Theme.AscendingChaserRed;
4: lights.CurrentBrightness = HomeServer.Notifications.Lights.Brightness.Max;
5: }
The pattern I'm finding myself using is recording the original brightness and theme at the beginning of a process, then manipulate the lights during the process, and restore the lights afterward. I will be adding a more task-based API to the class, to support methods such as:
1: lights.StoreSettings();
2: lights.Show(Theme.HolidayLights, Brightness.Max);
3: // Do work here
4: lights.RestoreSettings();
I foresee using this class for other Home Server processes I end up writing, so that the server can report its status, and I can glean it from the couch. It'll be a balancing act between informative and annoying. We'll have to see what the WAF factor is on this one!