1 using System;
2 using System.Linq;
3 using System.Collections.Generic;
4 using System.IO.Ports;
5 using System.Threading;
6 using Nintendo.SDSG;
7 
8 namespace CafeX
9 {
10     /// <summary>
11     /// Provides a set of operations for interacting with the serial port, supporting CAT-R communication.
12     /// </summary>
13     public interface ISerialDevice : ISerialStartable, ISerialRunnable
14     {
15         /// <summary>
16         /// Gets or sets whether to display the serial output.
17         /// </summary>
18         bool DisplayOutput { get; set; }
19     }
20 
21     /// <summary>
22     /// Provides a set of operations that are available after the serial device has successfully started.
23     /// </summary>
24     public interface ISerialRunnable
25     {
26         /// <summary>
27         /// Waits for a message to come through the serial stream.  This will use the default timeout (1 min).
28         /// </summary>
29         /// <param name="message">Message to look for.</param>
WaitForMessage(string message)30         ISerialRunnable WaitForMessage(string message);
31 
32         /// <summary>
33         /// Waits for a message to come through the serial stream.
34         /// </summary>
35         /// <param name="message">Message to look for.</param>
36         /// <param name="timeout">Number of milliseconds to wait for the message.</param>
WaitForMessage(string message, int timeout)37         ISerialRunnable WaitForMessage(string message, int timeout);
38 
39         /// <summary>
40         /// Signals to launch the title.  If you want to launch the title after the System Config Tool has launched,
41         /// call <see cref="WaitForSystemConfigTool"/> first to ensure progress is that far.
42         /// </summary>
43         /// <param name="titleId">Title Id of the game to launch.</param>
LaunchTitle(string titleId)44         ISerialRunnable LaunchTitle(string titleId);
45 
46         /// <summary>
47         /// Signals to reboot the CAT-R and launch the title.
48         /// </summary>
49         /// <param name="titleId">Title Id of the game to launch.</param>
LaunchTitleSlow(string titleId)50         ISerialRunnable LaunchTitleSlow(string titleId);
51 
52         /// <summary>
53         /// Closes the serial port.
54         /// </summary>
Stop()55         ISerialStartable Stop();
56 
57         /// <summary>
58         /// Simply performs an action.  Used only to keep the fluent API intact.
59         /// </summary>
60         /// <param name="message">Action to perform.</param>
PerformAction(Action action)61         ISerialRunnable PerformAction(Action action);
62     }
63 
64     /// <summary>
65     /// Provides a set of base operations for starting up and configuring the serial port.
66     /// </summary>
67     public interface ISerialStartable
68     {
69         /// <summary>
70         /// Opens the serial port and begins listening to incoming data.
71         /// </summary>
72         /// <param name="portNumber">COM port number.</param>
Start(string portNumber)73         ISerialRunnable Start(string portNumber);
74     }
75 
76     /// <summary>
77     /// Provides a set of helper methods and shortcuts for working with the serial device.
78     /// </summary>
79     public static class SerialExtensions
80     {
81         /// <summary>
82         /// Waits for the System Config Tool to load.
83         /// </summary>
WaitForSystemConfigTool(this ISerialRunnable serialRunnable)84         public static ISerialRunnable WaitForSystemConfigTool(this ISerialRunnable serialRunnable)
85         {
86             return serialRunnable.WaitForMessage(Serial.Messages.SysConfigEntry);
87         }
88 
89         /// <summary>
90         /// Simply prints a message to the console window.  Used to keep the fluent API intact.
91         /// </summary>
92         /// <param name="message">Message to print.</param>
PrintMessage(this ISerialRunnable serialRunnable, string message)93         public static ISerialRunnable PrintMessage(this ISerialRunnable serialRunnable, string message)
94         {
95             return serialRunnable.PerformAction(() => {
96                                                         if(!string.IsNullOrEmpty(message))
97                                                         {
98                                                             Console.WriteLine(message);
99                                                         }
100                                                       });
101         }
102     }
103 
104 
105     /// <summary>
106     /// Provides a set of operations for working with the serial port out from a devkit.
107     /// </summary>
108     public class Serial : ISerialDevice
109     {
110         /// <summary>
111         /// Defines a set of known messages that are output from the devkit.
112         /// </summary>
113         public static class Messages
114         {
115             /// <summary>
116             /// Output string from the devkit that indicates the System Config Tool has started.
117             /// </summary>
118             public const string SysConfigEntry = "sysconf: enter";
119 
120             /// <summary>
121             /// Message &quot;ACP: initialized&quot;.
122             /// </summary>
123             public const string AcpInitialized = "ACP: initialized";
124 
125             /// <summary>
126             /// Message &quot;VPAD Init end&quot;.
127             /// </summary>
128             public static readonly string VpadInitEnd = CatToucan.Terms.VPAD_INIT_END;
129 
130         }
131 
132         /// <summary>
133         /// Cafe new line in serial is \r; not \r\n and not \n.
134         /// </summary>
135         private const string CafeNewLine = "\r";
136 
137         /// <summary>
138         /// Amount of time to wait a message (1 min).
139         /// </summary>
140         private const int DefaultMessageTimeout = 1000 * 60;
141 
142         /// <summary>
143         /// 5 second read timeout.  Used in the event the serial stream gets cut and does not send <see cref="CafeNewLine"/>.
144         /// </summary>
145         private const int ReadTimeout = 1000 * 5;
146 
147         private const int DevKitBaudRate = 57600;
148         private const Parity DevKitParity = Parity.None;
149         private const int DevKitDataBits = 8;
150         private const StopBits DevKitStopBits = StopBits.One;
151 
152         private SerialPort _serialPort;
153 
154         private IDictionary<string, ManualResetEvent> _messages;
155 
156         private readonly object _messageLock = new object();
157         private readonly object _readLock = new object();
158 
159         /// <summary>
160         /// Gets or sets whether to display the serial output.
161         /// </summary>
162         public bool DisplayOutput { get; set; }
163 
164         /// <summary>
165         /// Gets an instance to the serial device.
166         /// </summary>
167         public static ISerialDevice Device { get; private set; }
168 
Serial()169         static Serial()
170         {
171             Device = new Serial();
172         }
173 
Serial()174         private Serial() { }
175 
176         /// <summary>
177         /// Opens the serial port and begins listening to incoming data.
178         /// </summary>
179         /// <param name="portNumber">COM port number.</param>
Start(string portNumber)180         public ISerialRunnable Start(string portNumber)
181         {
182             if(_serialPort != null)
183             {
184                 return this;
185             }
186 
187             _serialPort = new SerialPort(string.Format("COM{0}", portNumber),
188                                          DevKitBaudRate,
189                                          DevKitParity,
190                                          DevKitDataBits,
191                                          DevKitStopBits);
192             _serialPort.NewLine = CafeNewLine;
193             _serialPort.ReadTimeout = ReadTimeout; //crucial because default is Infinite, could create a deadlock
194             _serialPort.DataReceived += DataReceivedHandler;
195 
196             _messages = new Dictionary<string, ManualResetEvent>();
197 
198             _serialPort.Open();
199 
200             Console.WriteLine(string.Format("Serial communication started on COM{0}", portNumber));
201 
202             return this;
203         }
204 
205         /// <summary>
206         /// Closes the serial port.
207         /// </summary>
Stop()208         public ISerialStartable Stop()
209         {
210             if(_serialPort == null)
211             {
212                 return this;
213             }
214 
215             lock (_readLock)
216             {
217 
218                 _serialPort.DataReceived -= DataReceivedHandler;
219 
220                 //this .NET bug can cause Close/Dispose to hang without throwing an exception.
221                 //for now, the serial port should not be started and stopped more than one
222                 //we won't explicitly close and let Windows handle freeing up the device on exit
223                 //https://connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port
224                 //_serialPort.Close();
225                 //_serialPort.Dispose();
226                 _serialPort = null;
227 
228                 foreach(var message in _messages)
229                 {
230                     message.Value.Set();
231                     message.Value.Close();
232                 }
233 
234                 _messages.Clear();
235                 _messages = null;
236 
237             }
238 
239             Console.WriteLine("Serial communication stopped.");
240 
241             return this;
242         }
243 
244         /// <summary>
245         /// Signals to launch the title.  If you want to launch the title after the System Config Tool has launched,
246         /// call <see cref="WaitForSystemConfigTool"/> first to ensure progress is that far.
247         /// </summary>
248         /// <param name="titleId">Title Id of the game to launch.</param>
LaunchTitle(string titleId)249         public ISerialRunnable LaunchTitle(string titleId)
250         {
251             if (string.IsNullOrEmpty(titleId))
252             {
253                 throw new ArgumentNullException(titleId);
254             }
255 
256             ValidateSerialPort();
257 
258             Console.WriteLine(string.Format("Launching title {0}", titleId));
259             _serialPort.WriteLine(string.Format("cos launch 0x{0} 0x{1}", titleId.Substring(0, 8), titleId.Substring(8)));
260 
261             return this;
262         }
263 
264         /// <summary>
265         /// Signals to reboot the CAT-R and launch the title.
266         /// </summary>
267         /// <param name="titleId">Title Id of the game to launch.</param>
LaunchTitleSlow(string titleId)268         public ISerialRunnable LaunchTitleSlow(string titleId)
269         {
270             if (string.IsNullOrEmpty(titleId))
271             {
272                 throw new ArgumentNullException(titleId);
273             }
274 
275             ValidateSerialPort();
276 
277             Console.WriteLine(string.Format("Launching title {0}", titleId));
278             _serialPort.WriteLine(string.Format("cos slowlaunch 0x{0}", titleId));
279 
280             return this;
281         }
282 
283         /// <summary>
284         /// Waits for a message to come through the serial stream.
285         /// </summary>
286         /// <param name="message">Message to look for.</param>
WaitForMessage(string message)287         public ISerialRunnable WaitForMessage(string message)
288         {
289             return WaitForMessage(message, DefaultMessageTimeout);
290         }
291 
292         /// <summary>
293         /// Waits for a message to come through the serial stream.
294         /// </summary>
295         /// <param name="message">Message to look for.</param>
296         /// <param name="timeout">Number of milliseconds to wait for the message.</param>
WaitForMessage(string message, int timeout)297         public ISerialRunnable WaitForMessage(string message, int timeout)
298         {
299             ValidateSerialPort();
300 
301             var messageEvent = new ManualResetEvent(false);
302 
303             lock(_messageLock)
304             {
305                 _messages.Add(message, messageEvent);
306             }
307 
308             if(!messageEvent.WaitOne(timeout))
309             {
310                 Console.WriteLine(string.Format("Could not find message '{0}'", message));
311             }
312 
313             return this;
314         }
315 
316         /// <summary>
317         /// Simply performs an action.  Used only to keep the fluent API intact.
318         /// </summary>
319         /// <param name="message">Action to perform.</param>
PerformAction(Action action)320         public ISerialRunnable PerformAction(Action action)
321         {
322             if(action != null)
323             {
324                 action();
325             }
326 
327             return this;
328         }
329 
330         /// <summary>
331         /// Checks that the serial port is open and working, otherwise throws an exception.
332         /// </summary>
ValidateSerialPort()333         private void ValidateSerialPort()
334         {
335             if (_serialPort == null || !_serialPort.IsOpen)
336             {
337                 throw new Exception("Serial communication has not been started.");
338             }
339         }
340 
341         /// <summary>
342         /// Incoming data handler.  Listens for incoming cues to perform certain actions.
343         /// </summary>
344         /// <param name="sender">Serial Port.</param>
345         /// <param name="e">Data received.</param>
DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)346         private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
347         {
348             lock (_readLock)
349             {
350                 //the lock and subsequent check for serialport is to ensure that the port
351                 //is not taken down during a read, which will result in an exception.  This
352                 //can happen because the two operations happen on separate threads.
353                 if(_serialPort == null)
354                 {
355                     return;
356                 }
357 
358                 try
359                 {
360                     var line = _serialPort.ReadLine();
361 
362                     if (DisplayOutput)
363                     {
364                         Console.WriteLine(line);
365                     }
366 
367                     lock(_messageLock)
368                     {
369                         var message = _messages.Keys.FirstOrDefault(m => line.Contains(m));
370 
371                         if(message != null)
372                         {
373                             _messages[message].Set();
374                             _messages.Remove(message);
375                         }
376                     }
377                 }
378                 catch (TimeoutException)
379                 {
380                     //do nothing, this could happen if the serial stream was cut and doesn't send a NewLine
381                     //like when a title is launched
382                 }
383             }
384         }
385     }
386 }
387