/* 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).
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
|
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 |
object m_lock = new object();
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, |
AutoResetEvent m_wakeWorkerThread = new AutoResetEvent(false);
ManualResetEvent m_idleEvent = new ManualResetEvent(true);
Cm11.m_idle Field
True while no commands are being processed. |
bool m_idle = true;
bool m_quitting;
string m_serialPortName;
Cm11.m_serialPort Field
The serial port used to communicate with the CM11 device. |
DnSerialPort m_serialPort;
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
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; } }
[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:
|
[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:
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
Remarks
|
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; } }
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.
|
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
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".
|
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".
|
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%.
|
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%.
|
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
Exceptions
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
|
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); } }