/*       Copyright (c) Eric Ledoux.  All rights reserved.       */
/* See http://www.dwell.net/terms for code sharing information. */

// Cm11.cs
//
// Implements the DwellNet.Cm11 class.
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO.Ports;
using System.Threading;
using System.Text;
using System.Text.RegularExpressions;
using DwellNet;
using DwellNet.Properties;

namespace DwellNet
{

Cm11 Class

Controls a CM11 device (or an equivalent, such as CM11A) connected through a serial port.

Remarks

See Dwell.Net CM11 Introduction for information about how to use the Cm11 class.

Example

The following example turns on the X10 device with address "A1" (i.e. house code "A", device code "1"), and then waits until the command completes (successfully or not).

C#
using (Cm11 cm11 = new Cm11())
{
    cm11.Open("COM1");
    cm11.Execute("A1 On");
    cm11.WaitUntilIdle();
}
The following code is equivalent -- it uses the higher-level TurnOnDevice method.
C#
using (Cm11 cm11 = new Cm11())
{
    cm11.Open("COM1");
    cm11.TurnOnDevice("A1");
    cm11.WaitUntilIdle();
}

See Also
[ToolboxBitmap(typeof(ResourceFinder), "DwellNet.Properties.16x16w.ico"),
 Description("Controls X10 devices using CM11 hardware.")]
public class Cm11 : Component, IDisposable
{
    //////////////////////////////////////////////////////////////////////////
    // Private Constants and Statics
    //

    
Cm11.s_codesToNibbles Field

Used to convert a house code ('A' through 'P') or a device code (1 through 15) to a nibble value used in the X10 protocol.

    static byte[] s_codesToNibbles = new byte[]
    {
        0x6,    // binary 0110: house code "A" or device code "1"
        0xE,    // binary 1110: house code "B" or device code "2"
        0x2,    // binary 0010: house code "C" or device code "3"
        0xA,    // binary 1010: house code "D" or device code "4"
        0x1,    // binary 0001: house code "E" or device code "5"
        0x9,    // binary 1001: house code "F" or device code "6"
        0x5,    // binary 0101: house code "G" or device code "7"
        0xD,    // binary 1101: house code "H" or device code "8"
        0x7,    // binary 0111: house code "I" or device code "9"
        0xF,    // binary 1111: house code "J" or device code "10"
        0x3,    // binary 0011: house code "K" or device code "11"
        0xB,    // binary 1011: house code "L" or device code "12"
        0x0,    // binary 0000: house code "M" or device code "13"
        0x8,    // binary 1000: house code "N" or device code "14"
        0x4,    // binary 0100: house code "O" or device code "15"
        0xC,    // binary 1100: house code "P" or device code "16"
    };

    
Cm11.s_nibblesToCodes Field

Used to convert a nibble value used in the X10 protocol to a house code ('A' through 'P') or a device code (1 through 15). Maps a nibble value to the a zero-based index

    static byte[] s_nibblesToCodes = new byte[]
    {
        12, // 0x0 binary 0000: house code "M" or device code "13"
        4,  // 0x1 binary 0001: house code "E" or device code "5"
        2,  // 0x2 binary 0010: house code "C" or device code "3"
        10, // 0x3 binary 0011: house code "K" or device code "11"
        14, // 0x4 binary 0100: house code "O" or device code "15"
        6,  // 0x5 binary 0101: house code "G" or device code "7"
        0,  // 0x6 binary 0110: house code "A" or device code "1"
        8,  // 0x7 binary 0111: house code "I" or device code "9"
        13, // 0x8 binary 1000: house code "N" or device code "14"
        5,  // 0x9 binary 1001: house code "F" or device code "6"
        3,  // 0xA binary 1010: house code "D" or device code "4"
        11, // 0xB binary 1011: house code "L" or device code "12"
        15, // 0xC binary 1100: house code "P" or device code "16"
        7,  // 0xD binary 1101: house code "H" or device code "8"
        1,  // 0xE binary 1110: house code "B" or device code "2"
        9,  // 0xF binary 1111: house code "J" or device code "10"
    };

    
Cm11.X10Function Enumeration

X10 function code nibble values.

Members
Name Description
AllLightsOff

0110 = All Lights Off

AllLightsOn

0001 = All Lights On

AllOff

0000 = All Units Off

Brighten

0101 = Brighten

Dim

0100 = Dim

ExtCode

0111 = Extended Code

ExtDataXfer

1100 = Extended Data Transfer

HailAck

1001 = Hail Acknowledge

HailReq

1000 = Hail Request

Off

0011 = Off

On

0010 = On

PresetDim1

1010 = Preset Dim 1

PresetDim2

1011 = Preset Dim 2

StatusOff

1110 = Status Off

StatusOn

1101 = Status On

StatusReq

1111 = Status Request

    enum X10Function
    {
        
Cm11.X10Function.AllOff Enumeration Value

0000 = All Units Off

        AllOff = 0,

        
Cm11.X10Function.AllLightsOn Enumeration Value

0001 = All Lights On

        AllLightsOn = 1,

        
Cm11.X10Function.On Enumeration Value

0010 = On

        On = 2,

        
Cm11.X10Function.Off Enumeration Value

0011 = Off

        Off = 3,

        
Cm11.X10Function.Dim Enumeration Value

0100 = Dim

        Dim = 4,

        
Cm11.X10Function.Brighten Enumeration Value

0101 = Brighten

        Brighten = 5,

        
Cm11.X10Function.AllLightsOff Enumeration Value

0110 = All Lights Off

        AllLightsOff = 6,

        
Cm11.X10Function.ExtCode Enumeration Value

0111 = Extended Code

        ExtCode = 7,

        
Cm11.X10Function.HailReq Enumeration Value

1000 = Hail Request

        HailReq = 8,

        
Cm11.X10Function.HailAck Enumeration Value

1001 = Hail Acknowledge

        HailAck = 9,

        
Cm11.X10Function.PresetDim1 Enumeration Value

1010 = Preset Dim 1

        PresetDim1 = 10,

        
Cm11.X10Function.PresetDim2 Enumeration Value

1011 = Preset Dim 2

        PresetDim2 = 11,

        
Cm11.X10Function.ExtDataXfer Enumeration Value

1100 = Extended Data Transfer

        ExtDataXfer = 12,

        
Cm11.X10Function.StatusOn Enumeration Value

1101 = Status On

        StatusOn = 13,

        
Cm11.X10Function.StatusOff Enumeration Value

1110 = Status Off

        StatusOff = 14,

        
Cm11.X10Function.StatusReq Enumeration Value

1111 = Status Request

        StatusReq = 15
    };


    
Cm11.s_addressRegex Field

A regular expression that matches an X10 address or *address command*, e.g. "A1" or "P16". Note that some invalid address, such as "A99", are also matched.

    Regex s_addressRegex = new Regex(@"^([A-P])(\d{1,2})$");

    
Cm11.s_dimRegex Field

A regular expression that matches a command of the form "Dim<percent>", with an optional house code prefix; for example, "A.Dim0", "Dim50", and "Dim100". Note that some invalid commands, such as "Dim999", are also matched.

    Regex s_dimRegex = new Regex(@"^(?:([A-P])\.)?Dim(\d{0,4})$");

    
Cm11.s_brightenRegex Field

A regular expression that matches a command of the form "Brighten<percent>", with an optional house code prefix; for example, "A.Brighten0", "Brighten50", and "Brighten100". Note that some invalid commands, such as "Brighten999", are also matched.

    Regex s_brightenRegex = new Regex(@"^(?:([A-P])\.)?Brighten(\d{0,4})$");

    
Cm11.s_functionRegex Field

A regular expression that matches a *function command* with an optional house code prefi; for example, "A.On" or "On". However, this is very loose match; "Abcde" is also matched, for example.

    Regex s_functionRegex = new Regex(@"^(?:([A-P])\.)?([A-Za-z0-9]+)$");

    
Cm11.s_hexRegex Field

A regular expression that matches a command of the form "Hex<hex-digits>"; for example, "Hex046E". Note that some invalid commands, such as "Hex1A2" (odd number of hexadecimal digits), are also matched.

    Regex s_hexRegex = new Regex(@"^Hex([0-9a-fA-F]*)$");

    
Cm11.SERIAL_TIMEOUT Field

The maximum number of milliseconds to wait for a response from the CM11 device.

    const int SERIAL_TIMEOUT = 5000;

    //////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    
Cm11.m_lock Field

m_lock is locked while a thread accesses any state of this object, to serialize access to the object. Exception: Access to the command queue (m_commandQueue) is serialized using lock (m_commandQueue) so that the application isn't blocked from queuing new commands while an existing message is being transmitted.

    object m_lock = new object();

    
Cm11.m_isOpen Field

Holds the value of the IsOpen property.

    bool m_isOpen;

    
Cm11.m_workerThreadId Field

The managed thread ID of the worker thread.

    int m_workerThreadId;

    
Cm11.m_workerThreadWaitHandle Field

The WaitHandle of the worker thread. This WaitHandle is set when the WorkerThread method exits.

    WaitHandle m_workerThreadWaitHandle;

    
Cm11.m_wakeWorkerThread Field

Signaled when it's time to "wake up" the worker thread (if it's sleeping).

Remarks

For example, m_wakeWorkerThread.Set is called when serial data is received from the CM11, or a new command is added to m_commandQueue, or Close is called. Also, when the worker thread is initially created, m_wakeWorkerThread is used in the opposite way: the application temporarily blocks on m_wakeWorkerThread until the worker thread begins running, so that we can be sure that m_workerThreadId is set before Open returns.

    AutoResetEvent m_wakeWorkerThread = new AutoResetEvent(false);

    
Cm11.m_idleEvent Field

Signaled when m_commandQueue becomes empty.

    ManualResetEvent m_idleEvent = new ManualResetEvent(true);

    
Cm11.m_idle Field

True while no commands are being processed.

    bool m_idle = true;

    
Cm11.m_quitting Field

Set to true when it's time to dispose of this Cm11 object. Once set to true, further calls to Execute will silently fail, and messages already in the message queue won't be sent.

    bool m_quitting;

    
Cm11.m_serialPortName Field

Holds the value of the SerialPortName property.

    string m_serialPortName;

    
Cm11.m_serialPort Field

The serial port used to communicate with the CM11 device.

    DnSerialPort m_serialPort;

    
Cm11.m_commandQueue Field

The queue of DwellNet.Cm11 commands to execute.

    Queue<string> m_commandQueue = new Queue<string>(20);

    
Cm11.m_commandQueueChangeCount Field

Incremented each time the command queue is modified. Used to track whether the command queue may have changed between two points in time.

    long m_commandQueueChangeCount = 0;

    
Cm11.m_addressTracker Field

Tracks which X10 devices are currently addressed. (See Cm11.doc and AddressTracker for more information.)

    AddressTracker m_addressTracker = new AddressTracker(false);

#if DEBUG
    /// <summary>
    /// Tracks time from when this object was created, for debugging purposes.
    /// </summary>
    Stopwatch m_traceStopwatch = Stopwatch.StartNew();
#endif

    
Cm11.m_invokeEventsUsing Field

Holds the value of the InvokeEventsUsing property.

    ISynchronizeInvoke m_invokeEventsUsing;

    //////////////////////////////////////////////////////////////////////////
    // Public Properties
    //

    
Cm11.SerialPortName Property

Gets the serial port that the CM11 is connected to; for example, "COM1".

    [Browsable(false)]
    public string SerialPortName
    {
        get
        {
            return m_serialPortName;
        }
    }

    
Cm11.IsOpen Property

Gets a value indicating the open or closed status of the Cm11 object.

    [Browsable(false)]
    public bool IsOpen
    {
        get
        {
            return m_isOpen;
        }
    }

    
Cm11.InvokeEventsUsing Property

Gets or sets an application-provided ISynchronizeInvoke instance to use to invoke methods. Provides a way to invoke events on an application-provided thread, for situations in which firing an event on the internal worker thread would cause cross-thread errors.

Remarks

If InvokeEventsUsing is set to an application-provided ISynchronizeInvoke instance, then Cm11 will call ISynchronizeInvoke.Invoke to fire events, provided that ISynchronizeInvoke.InvokeRequired is true. Otherwise, Cm11 will invoke event delegates directly.

If you're writing a Windows Forms application that uses Cm11 and you get a "Cross-thread operation not valid" error when Cm11 fires an event, you can often solve this problem by setting the InvokeEventsUsing to the Form instance; for example:

C#
cm11.InvokeEventsUsing = Form1;

    [Description("Runs events on the thread provided by this object.  For Windows Forms applications, set this to the form.")]
    public ISynchronizeInvoke InvokeEventsUsing
    {
        get
        {
            return m_invokeEventsUsing;
        }
        set
        {
            m_invokeEventsUsing = value;
        }
    }

    //////////////////////////////////////////////////////////////////////////
    // Public Events
    //

    
Cm11.OnReceived Event

Fired when an "On" command is transmitted by a controller or device on the X10 network.

Remarks

This event is fired once for each device that the "On" command applies to.

    [Description("Occurs when an \"On\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11DeviceNotificationEventDelegate OnReceived;

    
Cm11.OffReceived Event

Fired when an "Off" command is transmitted by a controller or device on the X10 network.

Remarks

This event is fired once for each device that the "Off" command applies to.

    [Description("Occurs when an \"Off\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11DeviceNotificationEventDelegate OffReceived;

    
Cm11.BrightenReceived Event

Fired when a "Brighten" command is transmitted by a controller or device on the X10 network.

Remarks

This event is fired once for each device that the "Brighten" command applies to.

    [Description("Occurs when a \"Brighten\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11BrightenOrDimNotificationEventDelegate BrightenReceived;

    
Cm11.DimReceived Event

Fired when a "Dim" command is transmitted by a controller or device on the X10 network.

Remarks

This event is fired once for each device that the "Dim" command applies to.

    [Description("Occurs when a \"Dim\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11BrightenOrDimNotificationEventDelegate DimReceived;

    
Cm11.AllLightsOnReceived Event

Fired when an "AllLightsOn" command is transmitted by a controller or device on the X10 network.

    [Description("Occurs when an \"AllLightsOn\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11HouseNotificationEventDelegate AllLightsOnReceived;

    
Cm11.AllLightsOffReceived Event

Fired when an "AllLightsOff" command is transmitted by a controller or device on the X10 network.

    [Description("Occurs when an \"AllLightsOff\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11HouseNotificationEventDelegate AllLightsOffReceived;

    
Cm11.AllOffReceived Event

Fired when an "AllOff" command is transmitted by a controller or device on the X10 network.

    [Description("Occurs when an \"AllOff\" command is transmitted by a controller or device on the X10 network.")]
    public event Cm11HouseNotificationEventDelegate AllOffReceived;

    
Cm11.Notification Event

Fired when a notification of an event on the X10 network is received from the CM11 hardware.

Remarks

This is a lower-level event than events such as OnReceived and OffReceived. For example, if the "Unit 1 On" button is pressed on an X10 controller set to house code "A", three events are fired: (1) a "Notification" event with command name "A1"; a "Notification" event with a command name "On"; (3) a "OnReceived" event with address "A1". Applications can choose to handle the low-level events, the high-level events, both, or neither; handling low-level events requires that the application keep track of which X10 devices are currently addressed.

This event is fired on a thread other than the thread which created the Cm11 object, unless InvokeEventsUsing is used.

    [Description("Occurs when a notification of an event on the X10 network is received from the CM11 hardware.")]
    public event Cm11LowLevelNotificationEventDelegate Notification;

    
Cm11.IdleStateChange Event

Fired when the Cm11 object changes from processing commands to being idle, or vice versa.

Remarks

This event is fired on a thread other than the thread which created the Cm11 object, unless InvokeEventsUsing is used.

    [Description("Occurs when the Cm11 object changes from processing commands to being idle, or vice versa.")]
    public event Cm11IdleStateChangeEventDelegate IdleStateChange;

    
Cm11.Error Event

Fired when communication with the CM11 hardware fails, or the hardware itself fails.

Remarks

This event is fired on a thread other than the thread which created the Cm11 object, unless InvokeEventsUsing is used.

    [Description("Occurs when communication with the CM11 hardware fails, or the hardware itself fails.")]
    public event Cm11ErrorEventDelegate Error;

    
Cm11.LogMessage Event

Fired when the Cm11 object has information to provide to the application that may be useful for later review by the user.

Remarks

Typically the application handles this event in order to log the messages provided by this event into a file or event log that can be reviewed later by a user. The messages sent by this event include:

  • Messages prefixed by "<--" that indicate bytes sent to the CM11 hardware, including a decoding of those bytes.
  • Messages prefixed by "-->" that indicate bytes received from the CM11 hardware, including a decoding of those bytes.
  • Other messages indication error conditions.
This information may help the user debug problems with the CM11 hardware or the X10 network.

This event is fired on a thread other than the thread which created the Cm11 object, unless InvokeEventsUsing is used.

    [Description("Occurs when the Cm11 object has information to provide to the application that may be useful for later review by the user.")]
    public event Cm11LogMessageEventDelegate LogMessage;

    //////////////////////////////////////////////////////////////////////////
    // Public Methods
    //

    
Cm11.Open Method

Begins communication with the CM11 device.

Parameters

serialPortName

The name of the serial port that the CM11 device is connected to; for example, "COM1".

Exceptions
Exception type Condition
InvalidOperationException

Thrown if Open was already called.

Remarks

This method throws an InvalidOperationException if this Cm11 instance is already open. closed.

This method cannot be called while a Cm11 event handler is executing.

    public void Open(string serialPortName)
    {
        TraceInfo("Open");

        lock (m_lock)
        {
            // make sure we're not already open
            if (m_isOpen)
                throw new InvalidOperationException(Resources.AlreadyOpen);

            // update state
            m_serialPortName = serialPortName;

            // open the serial port, if it wasn't opened yet
            OpenSerialPort();

            // start the worker thread, if it wasn't started yet
            if (m_workerThreadWaitHandle == null)
            {
                // start the thread
                VoidDelegate workerThreadDelegate = WorkerThread;
                IAsyncResult ar = workerThreadDelegate.BeginInvoke(null, null);

                // initialize <m_workerThreadHandle>
                m_workerThreadWaitHandle = ar.AsyncWaitHandle;

                // wait until the worker thread has started to ensure that
                // <m_workerThreadId> is initialized before Open() returns;
                // note that this is the opposite of the normal usage of
                // <m_wakeWorkerThread>, since in this case the application
                // thread is using it to wait on the worker thread rather than
                // vice versa
                m_wakeWorkerThread.WaitOne();
            }

            // update state
            m_isOpen = true;
        }
    }

    
Cm11.Close Method

Closes the serial port and frees resources used by this object. No further access to the CM11 device by this object is possible until Open is called again.

Remarks

This method does nothing if this Cm11 instance is already closed.

    public void Close()
    {
        TraceInfo("Close");

        // update state; don't rely on <isOpen> to tell us if the object is
        // open or closed, since if an exception occurred inside Open() then
        // <isOpen> will be false but some resources may still need to be
        // cleaned up
        m_isOpen = false;
        m_serialPortName = null;

        // do nothing further if we're already closed
        if (m_workerThreadWaitHandle == null)
            return;

        // tell the worker thread that it's quitting time
        m_quitting = true;
        m_wakeWorkerThread.Set();

        // Wait for the worker thread to quit -- unless we're currently
        // running in the worker thread.
        //
        // Consider the following two scenarios:
        //
        // Scenario 1:  The worker thread fires an event.  While the event is
        // executing on the worker thread, the user closes the application,
        // which causes Close() to be called on the application's UI thread.
        // No problem: Close() sets <m_quitting> to true and waits on
        // <m_workerThreadWaitHandle> and eventually the event handler returns
        // and the worker thread notices that <m_quitting> is true and quits.
        //
        // Scenario 2:  The worker thread fires event; in application's event
        // handler the application calls Close().  (Note that this time we're
        // executing Close() in the worker thread.)  Close() sets <m_quitting>
        // to true and waits on <m_workerThreadWaitHandle>.  The result is a
        // deadlock: the current thread will never exit because it's waiting
        // for itself to exit.
        //
        // Moral of the story: don't wait on <m_workerThreadWaitHandle> if the
        // current thread is the worker thread.
        //
        if (m_workerThreadId != Thread.CurrentThread.ManagedThreadId)
            m_workerThreadWaitHandle.WaitOne();
    }

    
Cm11.Execute Method

Sends one or more DwellNet.Cm11 commands to the CM11 device. Commands are translated to the CM11 binary protocol.

Parameters

commands

The series of DwellNet.Cm11 commands to execute, separated by spaces. See Cm11Help.htm for a list of valid DwellNet.Cm11 commands.

Remarks

This method starts sending commands to the CM11 device. This method returns immediately -- it doesn't wait for the commands to complete. You can call WaitUntilIdle after calling Execute if you'd like to wait until all queued commands have been executed.

Commands are separated by spaces; each command must not contain spaces within it. For example, if commands equals "A1 A2 Dim70", that specifies three commands, "A1", "A2", and "Dim70". Commands are case-sensitive; you can't specify "a1" or "dim70".

Open must be called before calling this method.

Example

The following code turns on lamps A1, A2, and A3, then dims them by 25%, then turns off lamp B1.

C#
cm11.Execute("A1 A2 A3 On Dim25 B1 Off");

    public void Execute(string commands)
    {
        TraceInfo("Execute: queue: {0}", commands);

        // "lock (m_lock)" is not called here because we don't want to block
        // unnecessarily -- we're just adding commands to the queue, and
        // <m_commandQueue> has its own lock

        // make sure Open() was called
        if (!m_isOpen)
            throw new InvalidOperationException(Resources.NotOpen);

        // split <commands> into an array of individual commands, one per
        // command per array element
        string[] commandArray = commands.Split(
            new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

        // perform two passes: in the first pass, check the syntax of each
        // command; in the second, add the command to the command queue;
        // the purpose of this two-pass approach is to reduce the chance of
        // having invalid commands in the queue...

        // pass one:
        AddressTracker addressTracker = new AddressTracker(true);
        foreach (string command in commandArray)
            ProcessCommand(command, addressTracker, false);

        // pass two:
        foreach (string command in commandArray)
        {
            lock (m_commandQueue)
            {
                m_commandQueue.Enqueue(command);
                m_commandQueueChangeCount++;
                SetIdleState(false);
            }
        }

        // wake up the worker thread so it can process the commands
        m_wakeWorkerThread.Set();
    }

    
Cm11.Clear Method

Removes all DwellNet.Cm11 commands from the Cm11 command queue.

Exceptions
Exception type Condition
InvalidOperationException

Thrown if Open was not called before this method was called.

Remarks

Open must be called before calling this method.

    public void Clear()
    {
        // make sure Open() was called
        if (!m_isOpen)
            throw new InvalidOperationException(Resources.NotOpen);

        // clear the command queue
        lock (m_commandQueue)
        {
            m_commandQueue.Clear();
            m_commandQueueChangeCount++;
            SetIdleState(true);
        }
    }

    
Cm11.TurnOnDevice Method

Turns on an X10 device.

Parameters

address

The X10 address of the device to control; for example, "A1". An address consists of a house code, "A" through "P" inclusive, followed by a device code, "1" through "16" inclusive.

Remarks

Open must be called before calling this method.

Example

The following code turns on the X10 device at address "A1", assuming a CM11 (or compatible) device is plugged into serial port "COM1". cm11 is a variable of type Cm11 or, in the case of a Windows Forms application, a Cm11 component that was dragged onto the form.

C#
cm11.Open("COM1");
cm11.TurnOnDevice("A1");

    public void TurnOnDevice(string address)
    {
        // prevalidate <address>: make sure the string contains only a single
        // command; beyond that, Execute() will complete validaton of <address>
        if (address.IndexOf(' ') >= 0)
        {
            throw new Cm11InvalidCommandException(Resources.InvalidAddress,
                    address);
        }

        // execute the necessary commands
        Execute(String.Format("{0} On", address));
    }

    
Cm11.TurnOffDevice Method

Turns off an X10 device.

Parameters

address

The X10 address of the device to control; for example, "A1". An address consists of a house code, "A" through "P" inclusive, followed by a device code, "1" through "16" inclusive.

Remarks

Open must be called before calling this method.

Example

The following code turns off the X10 device at address "A1", assuming a CM11 (or compatible) device is plugged into serial port "COM1". cm11 is a variable of type Cm11 or, in the case of a Windows Forms application, a Cm11 component that was dragged onto the form.

C#
cm11.Open("COM1");
cm11.TurnOffDevice("A1");

    public void TurnOffDevice(string address)
    {
        // prevalidate <address>: make sure the string contains only a single
        // command; beyond that, Execute() will complete validaton of <address>
        if (address.IndexOf(' ') >= 0)
        {
            throw new Cm11InvalidCommandException(Resources.InvalidAddress,
                    address);
        }

        // execute the necessary commands
        Execute(String.Format("{0} Off", address));
    }

    
Cm11.BrightenLamp Method

Brightens an X10 lamp device.

Parameters

address

The X10 address of the device to control; for example, "A1". An address consists of a house code, "A" through "P" inclusive, followed by a device code, "1" through "16" inclusive.

percent

The amount to brighten the lamp by, measured as a percentage value from 0 to 100.

Remarks

Open must be called before calling this method.

Example

Assuming the X10 device at address "A1" is a lamp module, and assuming a CM11 (or compatible) device is plugged into serial port "COM1", the following code brightens the lamp by 25%. cm11 is a variable of type Cm11 or, in the case of a Windows Forms application, a Cm11 component that was dragged onto the form.

C#
cm11.Open("COM1");
cm11.BrightenLamp("A1", 33);

    public void BrightenLamp(string address, int percent)
    {
        // prevalidate <address>: make sure the string contains only a single
        // command; beyond that, Execute() will complete validaton of <address>
        if (address.IndexOf(' ') >= 0)
        {
            throw new Cm11InvalidCommandException(Resources.InvalidAddress,
                    address);
        }

        // execute the necessary commands
        Execute(String.Format("{0} Brighten{1}", address, percent));
    }

    
Cm11.DimLamp Method

Dims an X10 lamp device.

Parameters

address

The X10 address of the device to control; for example, "A1". An address consists of a house code, "A" through "P" inclusive, followed by a device code, "1" through "16" inclusive.

percent

The amount to dim the lamp by, measured as a percentage value from 0 to 100.

Remarks

Open must be called before calling this method.

Example

Assuming the X10 device at address "A1" is a lamp module, and assuming a CM11 (or compatible) device is plugged into serial port "COM1", the following code dims the lamp by 25%. cm11 is a variable of type Cm11 or, in the case of a Windows Forms application, a Cm11 component that was dragged onto the form.

C#
cm11.Open("COM1");
cm11.DimLamp("A1", 33);

    public void DimLamp(string address, int percent)
    {
        // prevalidate <address>: make sure the string contains only a single
        // command; beyond that, Execute() will complete validaton of <address>
        if (address.IndexOf(' ') >= 0)
        {
            throw new Cm11InvalidCommandException(Resources.InvalidAddress,
                    address);
        }

        // execute the necessary commands
        Execute(String.Format("{0} Dim{1}", address, percent));
    }

    
Cm11.WaitUntilIdle Method

Returns after all Cm11 commands queued by Execute have completed.

Exceptions
Exception type Condition
InvalidOperationException

Thrown if Open was not called before this method was called.

Remarks

This method blocks the caller until the internal command queue is empty, either because the commands were successfully executed or because there was an error that caused the commands to be discarded.

This method cannot be called while a Cm11 event handler is executing.

Open must be called before calling this method.

    public void WaitUntilIdle()
    {
        // make sure Open() was called
        if (!m_isOpen)
            throw new InvalidOperationException(Resources.NotOpen);

        // "lock (m_lock)" is not needed here
        m_idleEvent.WaitOne();
    }

    //////////////////////////////////////////////////////////////////////////
    // Event Handlers for <m_serialPort>
    //

    void m_serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        // if we received bytes, notify the worker thread
        TraceInfo("Serial port: DataReceived ({0})", e.EventType);
        if (e.EventType == SerialData.Chars)
            m_wakeWorkerThread.Set();
    }

    void m_serialPort_ErrorReceived(object sender,
        SerialErrorReceivedEventArgs e)
    {
        // a serial port communication error occurred -- log informatin to the
        // application and resync
        FireLogMessage(Resources.SerialPortError, e.EventType);
        HardResync();
    }

    //////////////////////////////////////////////////////////////////////////
    // IDisposable Implemention
    //

    
Cm11.IDisposable.Dispose Method

Disposes of resources used by this object.

    void IDisposable.Dispose()
    {
        Close();
    }

    //////////////////////////////////////////////////////////////////////////
    // Private Methods
    //
    // NOTE: These private properties and methods are generally NOT
    // thread-safe -- call these methods within "lock (m_lock)" or an
    // equivalent.
    //

    
Cm11.OpenSerialPort Method

Opens the serial port, if it's not open yet.

    void OpenSerialPort()
    {
        // do nothing if OpenSerialPort() was already called
        if (m_serialPort != null)
            return;

        // create and initialize <m_serialPort>
        m_serialPort = new DnSerialPort(new DnSerialPortStringResources(),
            m_serialPortName, 4800, Parity.None, 8, StopBits.One);
        m_serialPort.DataReceived += new SerialDataReceivedEventHandler(
            m_serialPort_DataReceived);
        m_serialPort.ErrorReceived += new SerialErrorReceivedEventHandler(
            m_serialPort_ErrorReceived);
        m_serialPort.Open();
    }

    
Cm11.CloseSerialPort Method

Closes the serial port, if it's open.

    void CloseSerialPort()
    {
        if (m_serialPort != null)
        {
            m_serialPort.Purge();
            m_serialPort.Close();
            m_serialPort = null;
        }
    }

    
Cm11.WorkerThread Method

The code of the worker thread which services the message queue.

    void WorkerThread()
    {
        TraceInfo("worker thread started");
        // initialize <m_workerThreadId>
        m_workerThreadId = Thread.CurrentThread.ManagedThreadId;

        // allow Open() to complete, now that <m_workerThreadId> has been
        // initialized; note that this is the opposite of the normal usage of
        // <m_wakeWorkerThread>, since in this case the application thread is
        // using it to wait on the worker thread rather than vice versa
        m_wakeWorkerThread.Set();

        TraceInfo("worker thread initialized");

        // process commands in the command queue until we're told to quit;
        // NOTE: we don't "lock (m_lock)" until absolutely necessary, since
        // doing so restricts concurrent access
        try
        {
            while (true)
            {
                try
                {
                    // wait for one of the following events:
                    //   (a) serial port data the CM11;
                    //   (b) a command from the application (from Execute());
                    //   (c) Close() called -- this causes a QuittingException
                    //       to be thrown
                    WaitResult wr =
                        WaitForSerialInputOrCommand(DateTime.MaxValue, true);
                    if (wr == WaitResult.SerialInput)
                        ProcessNotification();
                    else
                    if (wr == WaitResult.CommandQueued)
                        ProcessQueuedCommands();
                }
                catch (TimeoutException ex)
                {
                    // the CM11 took too long to reply during an exchange with
                    // the PC -- pause to allow the CM11 to return to a
                    // "normal" state (waiting for a command or sending a
                    // notification)
                    TraceException(ex);
                    FireLogMessage(Resources.Cm11ReplyTimeout);
                    SoftResync();
                }
                catch (HardResyncException ex)
                {
                    // a serious serial port error occurred -- close and reopen
                    // the serial port, and discard any queued commands
                    TraceException(ex, "HardResyncException");
                    HardResync();
                }
                catch (DnSerialPortException ex)
                {
                    // a serious serial port error occurred -- close and reopen
                    // the serial port, and discard any queued commands
                    TraceException(ex);
                    FireLogMessage(Resources.CommunicationError,
                        GetExceptionFullMessage(null, ex));
                    HardResync();
                }
            }
        }
        catch (QuittingException ex)
        {
            // stop worker thread by exiting this method
            TraceException(ex);
        }

        TraceInfo("worker thread exiting");

        // clear state
        m_workerThreadId = 0;
        m_workerThreadWaitHandle = null;

        // close the serial port, if it's open
        lock (m_lock)
            CloseSerialPort();

        TraceInfo("worker thread exited");
    }

    
Cm11.ProcessNotification Method

Called when we receive one or more bytes from the CM11. This method reads the bytes and processes them as a notification.

    void ProcessNotification()
    {
        // set <b1> to the first byte of the notification; return if none
        byte? b1 = m_serialPort.ReadByte();
        if (!b1.HasValue)
            return;

        // process the notification
        if (b1 == 0xA5)
        {
            // this is a *clock set request*, i.e. the CM11 is requesting that
            // we set its internal clock...

            // feedback to application
            FireLogMessage(Resources.Cm11Notification, b1,
                Resources.Cm11TimeRequestNotification);

            // set <reply> to a *clock set reply* to send to the CM11...
            DateTime now = DateTime.Now;
            int minutes = now.Hour * 60 + now.Minute;
            DateTime startOfYear = new DateTime(now.Year, 1, 1);
            int dayOfYear = (int) (now - startOfYear).TotalDays;
            byte[] reply = new byte[]
            {
                // set-time header:
                0x9B,
                // seconds component of current time
                (byte) now.Second,
                // (minute-of-day component of current time) % 120
                (byte) (minutes % 120), 
                // (minute-of-day component of current time) / 120
                (byte) (minutes / 120),
                // low order 8 bits of day of the year
                (byte) (dayOfYear & 0xFF),
                // high order bit of day of the year in MSB,
                // day of week specified by setting bit 0 (Sunday)
                // through 7 (Saturday)
                (byte) (((dayOfYear & 0x100) >> 1) |
                        (1 << (int) now.DayOfWeek)),
                // monitored house code in upper 4 bits; bit 3 is
                // reserved; bit 2 is battery timer clear flag;
                // bit 1 is monitored status clear flag; bit 0 is
                // timer purge flag
                (byte) (s_codesToNibbles[0] << 4)
            };

            // write <reply> to the CM11
            WriteToSerialPort(reply, Resources.SettingCm11Clock);

            // the CM11 will send one 0xA5 request every second after power-up,
            // so it's not unusual for a series of these to be present in the
            // serial input buffer; for efficiency, delete all remaining 0xA5
            // bytes in the input buffer
            while (m_serialPort.PeekByte() == 0xA5)
                m_serialPort.ReadByte();
        }
        else
        if (b1 == 0x5A)
        {
            // this is a *device notification*...

            // feedback to application
            FireLogMessage(Resources.Cm11Notification, b1,
                Resources.Cm11X10DeviceNotification);

            // tell the CM11 we received the first byte of the notification
            WriteToSerialPort(new byte[] { 0xC3 }, Resources.AckNotification);

            // wait for the CM11 to send a count of data bytes; ignore all
            // 0x5A bytes we receive, because those are likely just extra
            // 0x5A's that got queued before we got around to responding
            // (since the CM11 sends one 0x5A per second until we respond)
            int dataByteCount;
            while (true)
            {
                if ((dataByteCount = ReadSerialPortByte(SERIAL_TIMEOUT))
                    != 0x5A)
                    break;
            }
            FireLogMessage(Resources.Cm11NotificationDataLength, dataByteCount);
            if ((dataByteCount < 2/*note below*/) || (dataByteCount > 9))
            {
                // invalid data byte count -- ignore this notification;
                // note that there must be at least 2 data bytes: one for the
                // data byte specifier and at least one following data byte
                FireLogMessage(Resources.InvalidReply);
                SoftResync();
                return;
            }

            // read the specified number of bytes from the CM11
            byte[] dataBytes = ReadSerialPortBytes(dataByteCount,
                SERIAL_TIMEOUT);
            FireLogMessage("CM11 --> {0} ({1})", FormatBytes(dataBytes),
                Resources.DeviceNotificationData);

            // parse and process the notification bytes
            NotificationDataParser parser =
                new NotificationDataParser(dataBytes);
            while (!parser.AtEndOfDataBytes)
            {
                bool isFunctionCode;
                byte dataByte = parser.GetNextDataByte(out isFunctionCode);
                if (!isFunctionCode)
                {
                    // this is an *address code*
                    ProcessDeviceNotification(false, dataByte, 0);
                }
                else
                {
                    // this is a *function code* -- check to see if there is
                    // a following *function parameter* byte and set
                    // <parameterByte> to it if so, otherwise set
                    // <parameterByte> to zero
                    byte parameterByte = 0;
                    if (((dataByte & 0xF) == (int) X10Function.Dim) ||
                        ((dataByte & 0xF) == (int) X10Function.Brighten))
                    {
                        // this is a *function code* that requires a parameter
                        if (!parser.AtEndOfDataBytes)
                        {
                            bool isFunctionCode2;
                            byte b = parser.GetNextDataByte(
                                out isFunctionCode2);
                            if (!isFunctionCode2)
                                parameterByte = b;
                            else
                                parser.UngetDataByte();
                        }
                    }
                    ProcessDeviceNotification(true, dataByte, parameterByte);
                }
            }
        }
        else
        {
            // unknown notification (ignored)
            FireLogMessage(Resources.Cm11Notification, b1,
                Resources.Cm11UnknownNotification);
        }
    }

    
Cm11.ProcessDeviceNotification Method

Called when we receive a device notification from the CM11. This method is called once for each address code or function code contained within each device notification.

Parameters

isFunctionCode

true if this is a function code, false if it's an address code.

dataByte

The address code or function code byte.

parameterByte

If this is a function code, and the function code nibble is is X10Function.Dim or X10Function.Brighten, parameterByte contains the additional parameter byte specifying the relative brighten-by or dim-by amount, from 0 (meaning 0%) to 210 (meaning 100%).

    void ProcessDeviceNotification(bool isFunctionCode, byte dataByte,
        byte parameterByte)
    {
        // set <commandName> to the string version of the notification,
        // e.g. "On" or "Dim"; set <commandParameter> to the parameter value,
        // i.e. a brighten-by or dim-by value in the range 0 to 100 inclusive
        // in the case of "Dim" and "Brighten" commands, -1 otherwise
        string commandName;
        int commandParameter;
        if (isFunctionCode)
        {
            // this is a *function code*
            X10Function x10Function = (X10Function) (dataByte & 0xF);
            byte highNibble = (byte) ((dataByte >> 4) & 0xF);
            if ((x10Function == X10Function.Dim) ||
                (x10Function == X10Function.Brighten))
            {
                // this is a "Dim" or "Brighten" notification
                commandName = String.Format("{0}.{1}",
                    HouseCodeNibbleToChar(highNibble), x10Function);
                commandParameter = (parameterByte * 100 + 105) / 210;

                // keep track of which devices are *currently addressed*
                m_addressTracker.RegisterFunctionCommand(highNibble);
            }
            else
            if ((x10Function == X10Function.PresetDim1) ||
                (x10Function == X10Function.PresetDim2))
            {
                // this is a *device level notification* -- see Cm11.doc
                byte appended5Bits = (byte) ((highNibble << 1) |
                    ((x10Function == X10Function.PresetDim2) ? 1 : 0));
                byte reversed5Bits = (byte)
                    (((appended5Bits & 0x10) >> 4) |
                     ((appended5Bits & 0x08) >> 2) |
                     (appended5Bits & 0x04) |
                     ((appended5Bits & 0x02) << 2) |
                     ((appended5Bits & 0x01) << 4));
                commandName = "Level";
                commandParameter = ((reversed5Bits + 1) * 100 + 16) / 32;
            }
            else
            {
                // this is some other *function code* notification
                commandName = String.Format("{0}.{1}",
                    HouseCodeNibbleToChar(highNibble), x10Function);
                commandParameter = -1;

                // keep track of which devices are *currently addressed*
                m_addressTracker.RegisterFunctionCommand(highNibble);
            }
        }
        else
        {
            // this is an *address code* notification
            commandName = AddressByteToString(dataByte);
            commandParameter = -1;

            // keep track of which devices are *currently addressed*
            m_addressTracker.RegisterAddressCommand(dataByte);
        }

        // fire a low-level notification event
        FireNotification(commandName, commandParameter);

        // fire a high-level notification event, if appropriate
        int percent;
        if (isFunctionCode)
        {
            X10Function x10Function = (X10Function) (dataByte & 0xF);
            byte houseCodeNibble = (byte) ((dataByte >> 4) & 0xF);
            switch (x10Function)
            {

            case X10Function.On:

                foreach (string address in
                        m_addressTracker.GetAddressedDevices(houseCodeNibble))
                    FireOnReceived(address);
                break;

            case X10Function.Off:

                foreach (string address in
                        m_addressTracker.GetAddressedDevices(houseCodeNibble))
                    FireOffReceived(address);
                break;

            case X10Function.Brighten:

                percent = (parameterByte * 100 + 105) / 210;
                foreach (string address in
                        m_addressTracker.GetAddressedDevices(houseCodeNibble))
                    FireBrightenReceived(address, percent);
                break;

            case X10Function.Dim:

                percent = (parameterByte * 100 + 105) / 210;
                foreach (string address in
                        m_addressTracker.GetAddressedDevices(houseCodeNibble))
                    FireDimReceived(address, percent);
                break;

            case X10Function.AllLightsOn:

                FireAllLightsOnReceived(
                    HouseCodeNibbleToChar(houseCodeNibble));
                break;

            case X10Function.AllLightsOff:

                FireAllLightsOffReceived(
                    HouseCodeNibbleToChar(houseCodeNibble));
                break;

            case X10Function.AllOff:

                FireAllOffReceived(HouseCodeNibbleToChar(houseCodeNibble));
                break;
            }
        }
    }

    
Cm11.ProcessQueuedCommands Method

Called when we one or more commands were queued in m_commandQueue. This method reads and executes queued commands until Close is called, serial input is received from the CM11, or there are no more queued commands to execute, whichever happens first.

    void ProcessQueuedCommands()
    {
        // process
        while (true)
        {
            // return if Close() was called or serial input is received from
            // the CM11; in the latter case is due to the fact that we need to
            // process C11 notifications before executing commands, because the
            // CM11 ignores commands while it's in notification mode
            WaitResult wr = WaitForSerialInputOrCommand(null, true);
            if (wr != WaitResult.CommandQueued)
                return;

            // set <command> to the next command in the queue, but don't remove
            // it from the queue yet
            string command;
            lock (m_commandQueue)
            {
                // check the command queue again in case it was emptied by
                // another thread
                if (m_commandQueue.Count == 0)
                    return;
                command = m_commandQueue.Peek();
            }

            // execute <command> -- don't "lock <m_commandQueue>" here because
            // ProcessCommand() could take quite a while and we don't want to
            // lock the command queue for the entire time
            if (ProcessCommand(command, m_addressTracker, true))
            {
                // the command was successfully executed -- remove it from the
                // queue
                lock (m_commandQueue)
                {
                    // check the command queue again in case it was emptied by
                    // another thread (with possibly another command queued --
                    // we don't want to delete the wrong command)
                    if ((m_commandQueue.Count > 0) &&
                        (command == m_commandQueue.Peek()))
                    {
                        m_commandQueue.Dequeue();
                        m_commandQueueChangeCount++;
                    }
                    if (m_commandQueue.Count == 0)
                        SetIdleState(true);
                }
            }
            else
            {
                // the command could not be executed, presumably because a
                // notification arrived from the CM11 -- leave the command in
                // the queue for now, and return so the notification can be
                // processed
                return;
            }
        }
    }

    
Cm11.SoftResync Method

Performs a "soft resynchronization" of serial port communications. This process attempts to correct a serial port communication problem without losing queued commands.

Remarks

This method informs the application that the CM11 is behaving unexpectedly (for example, an invalid reply), then pauses for a short time to allow the CM11 to reset its internal state and thereby (hopefully) resync communications with the PC.

During most back-and-forth communication exchanges between the CM11 and the PC, if the CM11 doesn't hear back from the PC within a second or so it appears to cancel the exchange. This method exploits that characteristic as a way to get the CM11 back to a normal state (waiting for a command for the PC or sending a new notification to the PC).

    void SoftResync()
    {
        try
        {
            FireLogMessage(Resources.Pausing, SERIAL_TIMEOUT);
            Wait(SERIAL_TIMEOUT);
        }
        catch (HardResyncException ex)
        {
            // a serious serial port error occurred -- close and reopen
            // the serial port, and discard any queued commands
            TraceException(ex, "HardResyncException during SoftResync");
            HardResync();
        }
        catch (DnSerialPortException ex)
        {
            // a serious serial port error occurred -- close and reopen
            // the serial port, and discard any queued commands
            TraceException(ex, "DnSerialPortException during SoftResync");
            HardResync();
        }
    }

    
Cm11.HardResync Method

Performs a "hard resynchronization" of serial port communications. This process attempts to correct a serious serial port communication problem by disconnecting and reconnecting the serial port. Any queued commands are discarded, to prevent commands from building up indefinitely during a period of poor communication with the CM11.

    void HardResync()
    {
        // loop until the serial port is successfully reopened
        for (int attemptNumber = 1; ; attemptNumber++)
        {
            TraceInfo("HardResync: attempt #{0}", attemptNumber);
            try
            {
                // purge the serial port input and output buffers, then close
                // the serial port
                CloseSerialPort();

                // discard any queued commands; set <discardedMessageCount> to
                // the number of discarded messages
                int discardedMessagesCount;
                lock (m_commandQueue)
                {
                    discardedMessagesCount = m_commandQueue.Count;
                    Clear();
                }

                // notify the application of the situation
                FireError(Resources.HardResyncOccurred,
                        discardedMessagesCount);

                // give the CM11 time to reset its state; this also prevents
                // rapid close/open/close/open/etc. of the serial port if
                // reopening the serial port (below) fails continually
                Wait(SERIAL_TIMEOUT);

                // reopen the serial port and purge any collected serial port
                // input
                OpenSerialPort();
                m_serialPort.Purge();

                // done with no exceptions
                FireLogMessage(Resources.HardResyncComplete);
                break;
            }
            catch (HardResyncException ex)
            {
                TraceException(ex,
                    "HardResyncException during HardResync");
            }
            catch (DnSerialPortException ex)
            {
                TraceException(ex, "DnSerialPortException during HardResync");
            }
        }
    }

    
Cm11.Wait Method

Waits for a specified number of milliseconds.

Parameters

timeoutMsec

The number of milliseconds to wait.

Exceptions
Exception type Condition
QuittingException

Thrown if Close was called before or during this method call.

    void Wait(int timeoutMsec)
    {
        DateTime timeoutTime = DateTime.Now.AddMilliseconds(timeoutMsec);
        bool timeout = false;
        while (true)
        {
            // if Close() was called, it's time to quit
            if (m_quitting)
                throw new QuittingException();

            // see if it's time to return
            if (timeout)
                return;

            // sleep until m_wakeWorkerThread.Set() is called or we reach
            // <timeoutTime> (if not null), whichever happens first; set
            // <timeout> to true if the latter happens first, false if the
            // former happens first
            TimeSpan sleepTime = timeoutTime - DateTime.Now;
            if (sleepTime <= TimeSpan.Zero)
                timeout = true;
            else
                timeout = !m_wakeWorkerThread.WaitOne(sleepTime, false);
        }
    }