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 "ACP: initialized". 122 /// </summary> 123 public const string AcpInitialized = "ACP: initialized"; 124 125 /// <summary> 126 /// Message "VPAD Init end". 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