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

What's Playing? Interfacing Your Media with an External LCD Panel using Visual Studio 2005 Express

  The demonstrates a .NET Framework-based application using Visual C# 2005 Express Edition that interfaces with a CrystalFontz XE634 4-line by 20-character or XE632 2-line by 16-character display. The application will determine the song currently playing in either Windows Media Player or iTunes and output the title, artist name, album and song length to the LCD panel.
Scott Hanselman's Computer Zen

Difficulty: Intermediate
Time Required: 6-10 hours
Cost: Free
Software: Visual Studio Express Editions
Hardware:
Download: Download

Summary: To kick off his new "Some Assembly Required" column, Scott Hanselman explains how to use Visual C# 2005 Express Edition and the .NET Framework 2.0 to control an LCD Display Panel and interface it with Windows Media Player or iTunes to show "What's Playing?"

Interfacing with the World

Welcome to my new MSDN Hobbyist column "Some Assembly Required." This column is all about using C# and the .NET Framework to interface with gadgets and hardware - from your cell phone to your MP3 player, from your ReplayTV to your Windows Media Center PC, from your COM ports to a GPS device.

I'm constantly surprised by how much can be accomplished with so little code as I wander the Web and the Blogosphere. We are truly standing on the shoulders of giants. I recall manually configuring jumpers to get my Sound Blaster off IRQ5, and now I can plug my 256 Meg USB Key directly into the hub on my flat screen LCD with zero configuration. Things have certainly changed.

What's Playing?

When I first read Duncan Mackenzie's article "Coding in the Blue Glow", I was totally digging his CrystalFontz LCD Panel. I knew one day I wanted to write an application to report whatever music I was currently playing. It seems that everyone who's blogging includes a little tagline telling you what they're listening to this instant, why shouldn't I have a backlight LCD informing my officemates?

Our goal is to write a .NET Framework-based application using Visual C# 2005 Express Edition that interfaces with a CrystalFontz XE634 4-line by 20-character or XE632 2-line by 16-character display. Both are USB displays with associated virtual COM ports. These virtual ports are very useful to us because there's no standard way to address a USB device, but programming to a COM port is an easily understood problem for us to solve.

The application will determine the song currently playing in either Windows Media Player or iTunes and output the title, artist name, album and song length to the LCD panel.


Figure 1. The CrystalFontz USB LCD displays have an associated “virtual” COM Port.

After installing the free drivers for the CrystalFontz LCD the device manager shows both the USB device and the virtual COM port. I take note that the virtual COM port is COM3 on my system. I'll want to make that a configurable part of the application.

The Magic that is the new System.IO.Ports Namespace

That column was written in 2003 and Duncan lamented the lack of Serial Port support in the .NET Framework 1.x. It was a major drag and folks, particularly hobbyists, have been complaining about it on the boards for years. Well, complain no more because System.IO.Ports is here in the 2.0 BCL and it's very intuitive to use!

Here is a subset of the methods and properties of the System.IO.Ports.SerialPort class that comes new with the .NET BCL 2.0:

Visual C#

public class SerialPort : System.ComponentModel.Component
{
public System.IO.Stream BaseStream {get; }
public int BaudRate {get; set; }
public void Close();
public int DataBits {get; set; }
public bool DsrHolding {get; }
public bool DtrEnable {get; set; }
public System.Text.Encoding Encoding {get; set; }
public static string[] GetPortNames();
public static int InfiniteTimeout;
public bool IsOpen {get; }
public void Open();
public System.IO.Ports.Parity Parity {get; set; }
public int Read(char[] buffer, int offset, int count);
public int Read(byte[] buffer, int offset, int count);
public int ReadBufferSize {get; set; }
public int ReadByte();
public int ReadChar();
public string ReadExisting();
public string ReadLine();
public string ReadTo(string value);
public int ReceivedBytesThreshold {get; set; }
public SerialPort(string portName, int baudRate,
System.IO.Ports.Parity parity, int dataBits,
System.IO.Ports.StopBits stopBits);
public SerialPort(string portName, int baudRate,
System.IO.Ports.Parity parity, int dataBits);
public SerialPort(string portName, int baudRate,
System.IO.Ports.Parity parity);
public SerialPort(string portName, int baudRate);
public SerialPort(string portName);
public System.IO.Ports.StopBits StopBits {get; set; }
public void Write(byte[] buffer, int offset, int count);
public void Write(char[] buffer, int offset, int count);
public void Write(string str);
public void WriteLine(string str);
}

Visual Basic

  Public Class SerialPort Inherits Component    
Public Sub New()
Public Sub New(ByVal container As IContainer)
Public Sub New(ByVal portName As String)
Public Sub New(ByVal portName As String,
ByVal baudRate As Integer)
Public Sub New(ByVal portName As String,
ByVal baudRate As Integer, ByVal parity As Parity)
Public Sub New(ByVal portName As String,
ByVal baudRate As Integer, ByVal parity As Parity,
ByVal dataBits As Integer)
Public Sub New(ByVal portName As String,
ByVal baudRate As Integer, ByVal parity As Parity,
ByVal dataBits As Integer, ByVal stopBits As StopBits)
Public Sub Close()
Public Sub DiscardInBuffer()
Public Sub DiscardOutBuffer()
Public Shared Function GetPortNames() As String()
Public Sub Open()
Public Function Read(ByVal buffer As Byte(),
ByVal offset As Integer, ByVal count As Integer)
As Integer
Public Function Read(ByVal buffer As Char(),
ByVal offset As Integer, ByVal count As Integer)
As Integer
Public Function ReadByte() As Integer
Public Function ReadChar() As Integer
Public Function ReadExisting() As String
Public Function ReadLine() As String
Public Function ReadTo(ByVal value As String)
As String
Public Sub Write(ByVal [text] As String)
Public Sub Write(ByVal buffer As Byte(),
ByVal offset As Integer, ByVal count As Integer)
Public Sub Write(ByVal buffer As Char(),
ByVal offset As Integer, ByVal count As Integer)
Public Sub WriteLine(ByVal [text] As String)
End Class

Since I'll be writing data (rather than reading it) to the LCD, I suspect I'll be using some of the Write() overloads, as well as Open() and IsOpen. I'll want to read the hardware specification for the LCD Display that I'm using to create an LCDPanel class that models the Display's capabilities. Note: The spec for the 634 I'm using corresponds with hardware v2.0 and firmware v2.0, but the concepts in this article can be applied to any LCD panel with a serial interface.

Reading a Hardware Spec

The CrystalFontz Hardware Specification has a section called "Explanation of Control Functions" that lists each of the commands that can be sent to the LCD Panel. Here are a few sentences that stand out and will drive the implementation:

The Crystalfontz intelligent serial displays will accept “plain ASCII” characters and display them on the screen at the current cursor position. For instance, if you send “Hello World”, the display shows “Hello World”.

That's useful, and it means we can perform a "Hello World" test with the following code.

Visual C#

SerialPort port = new SerialPort("COM3",19200);
port.Open();
port.Write("Hello World");
port.Close();

Visual Basic

Private port As SerialPort
port = New SerialPort("COM3",19200)
port.Open()
port.Write("Hello World")
port.Close()

My LCD Panel is on COM3 as seen in Figure 1, and the specification, as well as the LCD's own boot up screen, indicate that it's running at 19.2k or 19200 bits per second.

Now that I can write out a few things to the LCD Panel, I'll design a class that exposes the useful things about the Panel (what it can do) and hides a reasonable amount of the details (what's hard). After reading the spec, here are the definitions for the LCDPanel Class. Notice that our LCDPanel contains a SerialPort. When the LCDPanel is constructed, the developer can pass in the PortNumber and Baud Rate as well as an enum describing what model of panel it is. We'll open the SerialPort in the constructor and include not only a Close() method, but we'll also make the LCDPanel "IDisposable." This will allow use of the Using() keyword in both C# and VB that will automatically call Dispose (which will in turn call Close).

Visual C#

public class LCDPanel : IDisposable
{
private SerialPort port;
private LCDType type;
public LCDPanel(LCDType lcdType, int portNumber, int baud);
public void Close()
public void Write(string text)
public void Write(string[] lines)
public void Write(byte[] bytes)
public void Write(byte aByte)
public void Write(int anInt)
public void Write(Command aCommand)
public void Write(HorizontalGraphStyle aStyle)
public void WriteToRow(string text, int row)
public void WriteToRow(string text, int row,
TextAlignment alignment)
public void WriteToRow(string text, int row,
TextAlignment alignment, bool clearRow)
public int MaxWidth
public void SetContrast(int level)
public void SetBacklight(int level)
public void SetCursorPosition(int column, int row)
public void ShowHorizontalBarGraph(
HorizontalGraphStyle style, int startColumn,
int endColumn, int lengthInPixels, int row)
public void SetMarqueeString(string marquee)
public void StartMarquee(int row, int pixelShift,
int updateSpeed)
public void StopMarquee()
public void MoveUp()
public void MoveDown()
public void MoveRight()
public void MoveLeft()
}

Visual Basic

Public Class LCDPanel Implements IDisposable
Private port As SerialPort
Private type As LCDType
Public Sub New(ByVal lcdType As LCDType, _
ByVal portNumber As Integer, _
ByVal baud As Integer)
Public Sub Close()
Public Overloads Sub Write(ByVal [text] As String)
Public Overloads Sub Write(ByVal lines() As String)
Public Overloads Sub WriteToRow( _
ByVal [text] As String, ByVal row As Integer)
Public Overloads Sub WriteToRow( _
ByVal [text] As String, ByVal row As Integer, _
ByVal alignment As TextAlignment)
Public ReadOnly Property MaxWidth() As Integer
Public Overloads Sub WriteToRow( _
ByVal [text] As String, ByVal row As Integer, _
ByVal alignment As TextAlignment, _
ByVal clearRow As Boolean)
Public Overloads Sub Write(ByVal bytes() As Byte)
Public Overloads Sub Write(ByVal aByte As Byte)
Public Overloads Sub Write(ByVal anInt As Integer)
Public Overloads Sub Write( _
ByVal aCommand As Command)
Public Overloads Sub Write( _
ByVal aStyle As HorizontalGraphStyle)
Public Sub SetContrast(ByVal level As Integer)
Public Sub SetBacklight(ByVal level As Integer)
Public Sub SetCursorPosition(ByVal column As Integer, _
ByVal row As Integer)
Private Sub validateRowAndColumn( _
ByVal column As Integer, ByVal row As Integer)
Public Sub ShowHorizontalBarGraph( _
ByVal style As HorizontalGraphStyle, _
ByVal startColumn As Integer, _
ByVal endColumn As Integer, _
ByVal lengthInPixels As Integer, ByVal row As Integer)
Public Sub SetMarqueeString(ByVal marquee As String)
Public Sub StartMarquee(ByVal row As Integer, _
ByVal pixelShift As Integer, _
ByVal updateSpeed As Integer)
Public Sub StopMarquee()
Public Sub MoveUp()
Public Sub MoveDown()
Public Sub MoveRight()
Public Sub MoveLeft()
End Class

It might look complicated, but for now, just focus on the Write() method overloads. There are seven of them, and three for WriteToRow. The ones that take a string will write out the string to a port, just like we did with our Hello World test earlier. We'll also write out the same text to System.Diagnostics.Trace.Write for debugging purposes. 

Visual C#

public void Write(string text)
{
port.Write(text);
Trace.Write(text);
}

Visual Basic

Public Overloads Sub Write(ByVal 

[text] As String)
port.Write([text])
Trace.Write([text])
End Sub

In the "Control Functions" section of the CrystalFontz Hardware Specification it lists out each of the commands that the LCD panel understands.

In this manual, for "Binary" data the notation \xxx is used, where xxx is the decimal representation of the number. \000 to \255 cover all possible values for a character.

So, in this specification \000 means a byte "0" and \255 is byte "255." If we're going to be writing out bytes, it's fortunate that System.IO.Ports.SerialPort.Write() has an overload that takes a byte array. We can now add three implement Write() methods for our LCDPanel class, one that takes a byte array, which maps directly to the underlying SerialPort, one that takes a single byte and calls laterally to the byte array overload. A final class takes an int and down-casts it to a byte. Note that in these three functions we're adding a lot of flexibility to the programmer but there's only one call to port.Write in the code. Making smart and thoughtful use of overloads is a great way to add flexibility to a developer-focused API, but adds additional testing burden.

Visual C#

public void Write(byte[] bytes)
{
if (bytes == null)
throw new ArgumentNullException("bytes");
port.Write(bytes, 0, bytes.Length);
}

public void Write(byte aByte)
{
Write(new byte[] { aByte });
}

public void Write(int anInt)
{
Write((byte)anInt);
}

Visual Basic

Public Overloads Sub Write(ByVal bytes() As Byte)
If bytes Is Nothing Then
Throw New ArgumentNullException("bytes")
End If
port.Write(bytes, 0, bytes.Length)
End Sub

Public Overloads Sub Write(ByVal aByte As Byte)
Write(New Byte() {aByte})
End Sub

Public Overloads Sub Write(ByVal anInt As Integer)
Write(System.Convert.ToByte(anInt))
End Sub

The specification lists out a pile of Commands that can be sent to the LCD Panel. Here are a few from the spec:

\001 Cursor Home
\002 Hide Display
\003 Restore Display
\004 Hide Cursor
\005 Show Underline Cursor
\006 Show Block Cursor
\007 Show Inverting Block Cursor
etc...

Looks like it's time for an enum!

Visual C#

public enum Command : byte
{
CursorHome = 1,
HideDisplay = 2,
RestoreDisplay = 3,
HideCursor = 4,
ShowUnderlineCursor = 5,
ShowBlockCursor = 6,
ShowInvertingBlockCursor = 7,
//etc...

Visual Basic

Public Enum Command As Byte
CursorHome = 1
HideDisplay = 2
RestoreDisplay = 3
HideCursor = 4
ShowUnderlineCursor = 5
ShowBlockCursor = 6
ShowInvertingBlockCursor = 7
'etc...

Now we can send not only text, but also single byte commands to the LCD Panel. However, some commands are multi-byte, meaning that you send the command byte then a parameter. For example, from the Hardware Specification:

Backlight Control (\014 ; Control N)
Send "Control-N", followed by a byte from 0-100 for the backlight brightness. 0=OFF, 100=ON, intermediate values will vary the brightness. There are a total of 25 possible brightness levels.
Examples:
  \014\000
  \014\050
  \014\100

We could leave the LCDPanel class with just Write() methods, but for the class to be convenient enough to use often, it'd be nice to have methods to handle complex functions. We have a command enum and enough Write() methods to create a SetBacklight() method that encapsulates the command along with it's param. We'll also throw an ArgumentOutOfRangeException if the level isn't in line with the specification.

Visual C#

public void SetBacklight(int level)
{
if (level < 0 || level > 100)
throw new ArgumentOutOfRangeException(
"Backlight Level must be between: 0 = OFF and 100 = ON");
Write(Command.BacklightControl);
Write(level);
}

Visual Basic

Public Sub SetBacklight(ByVal level As Integer)
If level < 0 OrElse level > 100 Then
Throw New ArgumentOutOfRangeException( _
"Backlight Level must be between: 0 = OFF and 100 = ON")
End If
Write(Command.BacklightControl)
Write(level)
End Sub 'SetBacklight

Now, we'll repeat this thinking with each of the commands in the specification that you want to expose in the LCDPanel class.  I've included 95% of the commands that the CrystalFontz 634 supports in this article's sample code.  

Getting Data from Media Player (and iTunes)

Windows Media player includes support for Plug-ins and the one we'll be using is what they call the Windows Media Player 9 Series Fun Pack, specifically the Blogging Plug-in. This Plug-In works with Media Player 9 and 10. The Blogging Plug-in does two things, first it adds the artist, song, and album name to the Windows Media Player 9 Series title bar, but more importantly it also populates a specific key in the registry with this information. The idea for this Fun Pack came from the fact that bloggers like to announce what music they are listening to when they post to their blog, and the blogging software they use can check for this information and include it in their post.

Since we're not blogging, we're sending the information to an LCD Panel, the Blogger Plug-In might be better named the "Windows Media What's Playing Publisher Plug-In," but no one asks me when they name these things. Anyway, the What's Playing data is published to four registry values under HKCU\Software\Microsoft\MediaPlayer\CurrentMetadata.

We'll create an (EXE) executable that uses our LCDPanel assembly and does the retrieval. We've designed the LCDPanel class to be reusable, so we'll keep it separate from the code that gathers music data. We might want to make other programs that use the LCDPanel later. The Registry is easy to access with the Microsoft.Win32.RegistryKey class.

Visual C#

RegistryKey currentMetadata = 
Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\MediaPlayer\CurrentMetadata", false);
if (currentMetadata != null)
{
string newCurrentTitle = currentMetadata.GetValue(
"Title", "No Title") as string;
string newCurrentAlbum = currentMetadata.GetValue(
"Album", "No Album") as string;
string newCurrentAuthor = currentMetadata.GetValue(
"Author", "No Author") as string;
string newDurationString = currentMetadata.GetValue(
"durationString", "No Duration") as string;
//etc...

Visual Basic

Dim currentMetadata As RegistryKey _
= Registry.CurrentUser.OpenSubKey( _
"Software\Microsoft\MediaPlayer\CurrentMetadata", _
False)
If currentMetadata IsNot Nothing Then
Dim newCurrentTitle As String = _
currentMetadata.GetValue("Title", "No Title")
Dim newCurrentAlbum As String = _
currentMetadata.GetValue("Album", "No Album")
Dim newCurrentAuthor As String = _
currentMetadata.GetValue("Author", "No Author")
Dim newDurationString As String = _
currentMetadata.GetValue("durationString", "No Duration")
currentMetadata.Close()
'etc...

Registry.CurrentUser.OpenSubKey will return null if the key doesn't exist, so it's important to be proactive. GetValue() on the returned RegistryKey is called once for each of the four values. GetValue() is a little less forgiving and takes your preferred default value as its second parameter, so if "Title" isn't available "No Title" will be returned. We'll run this code in a timer, perhaps every two seconds. Since we'll be accessing the registry each time, we'll want to save the strings we retrieved previously and compare them to the new values. There's no need to update the LCD display if the song being played hasn't changed.

As an aside: Personally, I'm more of an iTunes person than a Windows Media person, at least so far. Anyway, one fellow has seen fit to create a Plug-In of his own for iTunes that populates these Windows Media registry keys with what's playing in iTunes! What does that mean for us? It means we won't have to change this What's Playing LCD application at all to work with iTunes. I just dropped hi iTunes Plug-In into Program Files/iTunes/Plug-Ins and was able to retrieve track information from the same registry keys.

Making the Connection

So far we've proven that we can retrieve data from the Registry using either the Windows Media Blogging Plug-In or the iTunes Plug-In, and we can can send text and commands to a CrystalFontz LCD Panel. Now let's tie them together. We'll create a WinForms application that will live in the Tray (that little row of icons down in the TaskBar near the clock.) 

Create a new Windows application in Microsoft Visual C# 2005 Express Edition (or VB if you like) and drag a ContextMenu control and a Timer control onto the form. We'll have the Timer fire every two seconds (2000 ms) to retrieve the data from the registry and write it out to the LCD panel.

Since we want the application to be in the Tray only, we will hide the main form as soon as possible and hook our context menu up to the NotifyIcon control. When someone right clicks on the icon in the Tray, the context menu will appear showing a single menu item "Close." In the Click handler of the Close menu item we'll exit the application.

The tick event handler of the timer control will be called every two seconds when we set it's interval to 2000. Its event handler is our opportunity to check the registry and write our findings out to the LCD panel.  Additionally we'll call the ShowBalloon method to pop a balloon up from the tray.

Conclusion

This application only reports "What's Playing" to the LCD, but the concepts in this article along with the features in Visual C# 2005 Express Edition enable you to create any number of applications to interface with your LCD Screen. Here are some ideas to get you started:

  • Check an RSS Feed and display the latest news.
    • (Some sample code to get you started is hidden in the comments of this application, and the RSS classes you need come with the Visual C# 2005 Express Edition's Screen Saver sample application.)
  • Check your email, either via POP3 or Outlook MAPI, and display today's email.
  • Call Web Service and display the Traffic or Ski Conditions.
  • Display the “Upcoming Recordings” for Media Center.
  • Use System.IO.DriveInfo to display free space on each of your drives.
  • Display the status of all the files being currently downloaded
  • Both CrystalFontz screens support horizontal bar graphs and I've included sample code.

Have fun and have no fear when faced with the words - Some Assembly Required!

Follow the Discussion

  • Astrid ZAstrid Z

    Will this application work with WMP version 11?

    I've read that WMP 11 won't work with the fun pack - perhaps there's a version of that plug-in that someone knows of?

    Thanks

  • Satjit SSatjit S

    Hi Scott,

    Hope this message reaches you in good spirits? Can you please help me with configuring your application such that I can use it to work with Windows Media Player? This might be a dumb request but any help with it would be really appreciated.

    Thanks in anticipation.

    Cheers!

    Satjit S

  • JonyJony

    I  can't find that registry key anywhere, maybe 'cause I don't have that plugin, and not thinking on installing it though..

    ¿Does someone know about finding the currently playing item by other means? Please Post It Here.

    Thanks.

  • ecoeco

    The code download link (to gotdotnet) is not longer valid -- is the software download available anywhere else?

    Thank you!

  • Clint RutkasClint I'm a "developer"

    @eco:  Thanks for the heads up, working on fixing this

  • Clint RutkasClint I'm a "developer"

    @eco:  I contacted Scott and he gave me the source code.  The link at the top is fixed now.

    Sorry about that and thanks for alerting me!

  • JTJT

    Many thanks for the C# class to handle the CF 634.  I'm working on a project that utilizes this display and you've saved me a TON of work!

  • Karen KenworthyKaren Kenworthy

    << I contacted Scott and he gave me the source code.  The link at the top is fixed now. >>

    It seems to be broken again. Sad

  • Clint RutkasClint I'm a "developer"

    @Karen Kenworthy:  Just checked, the download link seems to be working.

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.