Project Detroit: Lighting System
- Posted: May 21, 2012 at 9:00 AM
- 56,797 Views
- 4 Comments
Something went wrong getting user information from Channel 9
Something went wrong getting user information from MSDN
Something went wrong getting the Visual Studio Achievements
In this article, we give a detailed look into the external lighting system of Project Detroit, the Microsoft-West Coast Custom Mustang creation. If you're not already familiar with this project, you can find more information here.
The external lighting system for Project Detroit is run on a web server on a Netduino Plus, which uses the .NET Micro Framework (NETMF). To get an accelerated start, we leveraged code from the CodePlex project Netduino Helpers to control the AdaFruit LPD8806 LED strip, and the web server base code from the Netduino forums. They have been tweaked slightly for our project.
We wanted the NETMF web server to have the exact same routes as the full-blown REST service layer in Detroit. With compiler conditionals, this allowed the same code to use the same routes on both the full .NET stack and NETMF. Since we couldn't use the ASP.NET MVC framework here, we had to make something close to it.
Controllers implement an interface with a method named ExecuteAction. When GetController is called, we return the correct controller but cast as the interface IController. This lets us figure out what controller should be executing the request and continue to work generically in the context of the request connection thread. If new functionality is needed, we create a new controller that inherits from IController and update GetController:
private static void RequestReceived(Request request) { // url comes in as /foo/bar/12345?style=255 // // /CONTROLLER/ACTION/ // // LIGHTING SAMPLE URL: /0/1/?r=255&g=255&b=255&zone=EnumAsInt // /0/... 0 = NetduinoControllerType.Lighting var validResponse = false; try { Logger.WriteLine("Start: " + request.Url + " at " + DateTime.Now); var urlInParts = request.Url.TrimStart(UriPathSeparator).Split(new[] {'?'}, 2); string[] uriSubParts = null; string[] queryStringSubParts = null; if (urlInParts.Length > 0) uriSubParts = urlInParts[0].Split(UriPathSeparator); if (urlInParts.Length > 1) queryStringSubParts = urlInParts[1].Split(QueryStringSeparator); if (uriSubParts != null && uriSubParts.Length > 1) { var targetController = GetController(uriSubParts[0]); if (targetController != null) { var action = (uriSubParts.Length >= 2) ? uriSubParts[1] : string.Empty; var result = targetController.ExecuteAction(action, queryStringSubParts); validResponse = true; request.SendResponse(result.ToString()); Logger.WriteLine("Result: " + result + " from " + request.Url + " at " + DateTime.Now); } } if (!validResponse) { SendIndexHtml(request); } } catch(Exception ex0) { Logger.WriteLine(ex0.ToString()); } }
NETMF can't parse an enum from a string, which is something that can be done in the full .NET Framework. But what does this actually mean? It means the routes for the NETMF web server are a bit unreadable. Here is the same route but with the parsing issue exposed:
Detroit's REST service route with ASP.NET MVC:
http://…/Lighting/Solid/?zone=all&r=255&g=0&b=0
Detroit's NETMF route:
http://…/0/3/?zone=0&r=255&g=0&b=0
This issue made direct device debugging a bit harder.
Parsing the route is broken into two parts. The first is determines what controller to target. For Project Detroit, that meant the lighting system or the rear glass system. Once we know the controller, we still need to parse the query string and action into something more useful:
#if NETMF private static readonly char[] QueryStringParamSeparator = new[] { '=' }; public static LightingData ParseQueryStringValues(string action, params string[] queryStringParameters) #else public static LightingData ParseQueryStringValues(string action, Dictionary<string, object> queryStringParameters) #endif { var data = new LightingData {LightingZone = LightingZoneType.All, LightingAction = ParseLightingActionType(action)}; #if NETMF if (queryStringParameters != null) { foreach (var queryString in queryStringParameters) { var valueKey = queryString.Split(QueryStringParamSeparator); if (valueKey.Length != 2) continue; var key = valueKey[0]; var value = valueKey[1]; #else foreach(var key in queryStringParameters.Keys) { var value = queryStringParameters[key].ToString(); #endif switch (key.ToLower()) { case ZoneHuman: case ZoneComputer: data.LightingZone = ParseLightingZoneType(value); break; // more stuff } } #if NETMF } #endif // more stuff return data; }
Individually addressable LED strips allow us to tell each individual LED what color to be. To adjust the colors of the LED lights, we loop through all the LEDs, set their color value, and then call LedRefresh() on our LED strip. There's no need to set all of them, only the ones being updated:
private static void SolidAnimationWorker() { var leds = GetLedsToIlluminate(); var dataCopy = _data; for (var i = 0; i < leds.Length; i++) SetLed(leds[i], dataCopy.Red, dataCopy.Green, dataCopy.Blue); LedRefresh(); }
A solid color is boring, but we can use procedural animations to add some flair. The current code supports the following animations:
Since we know the LED strip is a single strand, we can be clever with offsets and create groupings. By having a dedicated animation thread in each zone, we can have the grill of the car in Police Mode while the bottom fades to red and the vents fill to blue at different refresh rates.
To run an animation, we need to first kill the old animation thread, spin up a new thread, mark LEDs in the "All LED" zone as do not touch, and execute the new pattern. Without a way to mark the LEDs as "bad" in the "All" zone, the two animation threads would compete for setting the color and result in a flickering effect. Other zones don't have to worry about this since there is zero overlap of LEDs:
public bool ExecuteAction(string action, params string[] queryStringParameters) { var data = LightingDataHelper.ParseQueryStringValues(action, queryStringParameters); SignalToEndAnimationThread(data.LightingZone); int counter = 0; while (counter++ < 5 && IsThreadAlive(data.LightingZone)) Thread.Sleep(10); if (IsThreadAlive(data.LightingZone)) AbortAnimationThread(data.LightingZone); // moving data bucket into public so new threads can leverage it _data = data; if (_data.LightingAction == LightingActionType.Police || _data.LightingAction == LightingActionType.Sweep) { _data.LightingZone = LightingZoneType.Grill; if (IsThreadAlive(_data.LightingZone)) AbortAnimationThread(_data.LightingZone); } // cleaning up just incase if (data.LightingZone != LightingZoneType.All) { for (var i = 0; i < _zoneAllAnimation.Length; i++) { var leds = GetLedsToIlluminate(); for (var z = 0; z < leds.Length; z++) { // flagging item as bad in animation lib if (_zoneAllAnimation[i] == leds[z]) _zoneAllAnimation[i] = -1; } } } switch (_data.LightingAction) { case LightingActionType.Solid: Debug.Print("Solid"); ExecuteThread(SolidAnimationWorker); break; // more patterns } return true; }
We can now do something more interesting, like Police Mode:
private static void PoliceModeAnimationWorker() { var leds = GetLedsToIlluminate(); var dataCopy = _data; var length = leds.Length; var half = length / 2; var index = 0; while (IsThreadSignaledToBeAlive(dataCopy.LightingZone)) { int redIntensity = (index == 0) ? 255 : 0; int blueIntensity = (index == 0) ? 0 : 255; for (var i = 0; i < half; i++) SetLed(leds[i], redIntensity, 0, 0); for (var i = half; i < length; i++) SetLed(leds[i], 0, 0, blueIntensity); LedRefresh(); index++; index %= 2; Thread.Sleep(100); } }
Hindsight is 20/20. Nothing is ever perfect and there are of course items we'd love to change. At the start, the goal was to make this fairly generic and not Project Detroit "aware." As you can see by looking at the source, that wasn't accomplished due to time constraints.
We would use a different LED system for two reasons: data transfer speed and power. The longer the LED strip is, the longer it takes to address an item further down the line. Project Detroit, having 15 meters of lighting in it, was impacted by this issue. It works well, just not as well as we hoped. The voltage requirements of the LEDs are another consideration. They are 5V, while a car uses 12V. Had we used 12V LEDs, our wiring would have been far simpler and wouldn't have required multiple transformers to get the required amperage needed at the correct voltage.
We think the lighting solution, coupled with the time and material constraints, worked out pretty well. Project Detroit was a ton of fun to build out and working with West Coast Customs was the chance of a .............
Хуя себе я тоже так хочу программировать научиться !!!
good job
nice work .
netduino zindabaad(long live)
Remove this comment
Remove this threadclose