using System; using System.Linq; using System.Collections.Generic; using System.IO.Ports; using System.Threading; using Nintendo.SDSG; namespace CafeX { /// /// Provides a set of operations for interacting with the serial port, supporting CAT-R communication. /// public interface ISerialDevice : ISerialStartable, ISerialRunnable { /// /// Gets or sets whether to display the serial output. /// bool DisplayOutput { get; set; } } /// /// Provides a set of operations that are available after the serial device has successfully started. /// public interface ISerialRunnable { /// /// Waits for a message to come through the serial stream. This will use the default timeout (1 min). /// /// Message to look for. ISerialRunnable WaitForMessage(string message); /// /// Waits for a message to come through the serial stream. /// /// Message to look for. /// Number of milliseconds to wait for the message. ISerialRunnable WaitForMessage(string message, int timeout); /// /// Signals to launch the title. If you want to launch the title after the System Config Tool has launched, /// call first to ensure progress is that far. /// /// Title Id of the game to launch. ISerialRunnable LaunchTitle(string titleId); /// /// Signals to reboot the CAT-R and launch the title. /// /// Title Id of the game to launch. ISerialRunnable LaunchTitleSlow(string titleId); /// /// Closes the serial port. /// ISerialStartable Stop(); /// /// Simply performs an action. Used only to keep the fluent API intact. /// /// Action to perform. ISerialRunnable PerformAction(Action action); } /// /// Provides a set of base operations for starting up and configuring the serial port. /// public interface ISerialStartable { /// /// Opens the serial port and begins listening to incoming data. /// /// COM port number. ISerialRunnable Start(string portNumber); } /// /// Provides a set of helper methods and shortcuts for working with the serial device. /// public static class SerialExtensions { /// /// Waits for the System Config Tool to load. /// public static ISerialRunnable WaitForSystemConfigTool(this ISerialRunnable serialRunnable) { return serialRunnable.WaitForMessage(Serial.Messages.SysConfigEntry); } /// /// Simply prints a message to the console window. Used to keep the fluent API intact. /// /// Message to print. public static ISerialRunnable PrintMessage(this ISerialRunnable serialRunnable, string message) { return serialRunnable.PerformAction(() => { if(!string.IsNullOrEmpty(message)) { Console.WriteLine(message); } }); } } /// /// Provides a set of operations for working with the serial port out from a devkit. /// public class Serial : ISerialDevice { /// /// Defines a set of known messages that are output from the devkit. /// public static class Messages { /// /// Output string from the devkit that indicates the System Config Tool has started. /// public const string SysConfigEntry = "sysconf: enter"; /// /// Message "ACP: initialized". /// public const string AcpInitialized = "ACP: initialized"; /// /// Message "VPAD Init end". /// public static readonly string VpadInitEnd = CatToucan.Terms.VPAD_INIT_END; } /// /// Cafe new line in serial is \r; not \r\n and not \n. /// private const string CafeNewLine = "\r"; /// /// Amount of time to wait a message (1 min). /// private const int DefaultMessageTimeout = 1000 * 60; /// /// 5 second read timeout. Used in the event the serial stream gets cut and does not send . /// private const int ReadTimeout = 1000 * 5; private const int DevKitBaudRate = 57600; private const Parity DevKitParity = Parity.None; private const int DevKitDataBits = 8; private const StopBits DevKitStopBits = StopBits.One; private SerialPort _serialPort; private IDictionary _messages; private readonly object _messageLock = new object(); private readonly object _readLock = new object(); /// /// Gets or sets whether to display the serial output. /// public bool DisplayOutput { get; set; } /// /// Gets an instance to the serial device. /// public static ISerialDevice Device { get; private set; } static Serial() { Device = new Serial(); } private Serial() { } /// /// Opens the serial port and begins listening to incoming data. /// /// COM port number. public ISerialRunnable Start(string portNumber) { if(_serialPort != null) { return this; } _serialPort = new SerialPort(string.Format("COM{0}", portNumber), DevKitBaudRate, DevKitParity, DevKitDataBits, DevKitStopBits); _serialPort.NewLine = CafeNewLine; _serialPort.ReadTimeout = ReadTimeout; //crucial because default is Infinite, could create a deadlock _serialPort.DataReceived += DataReceivedHandler; _messages = new Dictionary(); _serialPort.Open(); Console.WriteLine(string.Format("Serial communication started on COM{0}", portNumber)); return this; } /// /// Closes the serial port. /// public ISerialStartable Stop() { if(_serialPort == null) { return this; } lock (_readLock) { _serialPort.DataReceived -= DataReceivedHandler; //this .NET bug can cause Close/Dispose to hang without throwing an exception. //for now, the serial port should not be started and stopped more than one //we won't explicitly close and let Windows handle freeing up the device on exit //https://connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port //_serialPort.Close(); //_serialPort.Dispose(); _serialPort = null; foreach(var message in _messages) { message.Value.Set(); message.Value.Close(); } _messages.Clear(); _messages = null; } Console.WriteLine("Serial communication stopped."); return this; } /// /// Signals to launch the title. If you want to launch the title after the System Config Tool has launched, /// call first to ensure progress is that far. /// /// Title Id of the game to launch. public ISerialRunnable LaunchTitle(string titleId) { if (string.IsNullOrEmpty(titleId)) { throw new ArgumentNullException(titleId); } ValidateSerialPort(); Console.WriteLine(string.Format("Launching title {0}", titleId)); _serialPort.WriteLine(string.Format("cos launch 0x{0} 0x{1}", titleId.Substring(0, 8), titleId.Substring(8))); return this; } /// /// Signals to reboot the CAT-R and launch the title. /// /// Title Id of the game to launch. public ISerialRunnable LaunchTitleSlow(string titleId) { if (string.IsNullOrEmpty(titleId)) { throw new ArgumentNullException(titleId); } ValidateSerialPort(); Console.WriteLine(string.Format("Launching title {0}", titleId)); _serialPort.WriteLine(string.Format("cos slowlaunch 0x{0}", titleId)); return this; } /// /// Waits for a message to come through the serial stream. /// /// Message to look for. public ISerialRunnable WaitForMessage(string message) { return WaitForMessage(message, DefaultMessageTimeout); } /// /// Waits for a message to come through the serial stream. /// /// Message to look for. /// Number of milliseconds to wait for the message. public ISerialRunnable WaitForMessage(string message, int timeout) { ValidateSerialPort(); var messageEvent = new ManualResetEvent(false); lock(_messageLock) { _messages.Add(message, messageEvent); } if(!messageEvent.WaitOne(timeout)) { Console.WriteLine(string.Format("Could not find message '{0}'", message)); } return this; } /// /// Simply performs an action. Used only to keep the fluent API intact. /// /// Action to perform. public ISerialRunnable PerformAction(Action action) { if(action != null) { action(); } return this; } /// /// Checks that the serial port is open and working, otherwise throws an exception. /// private void ValidateSerialPort() { if (_serialPort == null || !_serialPort.IsOpen) { throw new Exception("Serial communication has not been started."); } } /// /// Incoming data handler. Listens for incoming cues to perform certain actions. /// /// Serial Port. /// Data received. private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { lock (_readLock) { //the lock and subsequent check for serialport is to ensure that the port //is not taken down during a read, which will result in an exception. This //can happen because the two operations happen on separate threads. if(_serialPort == null) { return; } try { var line = _serialPort.ReadLine(); if (DisplayOutput) { Console.WriteLine(line); } lock(_messageLock) { var message = _messages.Keys.FirstOrDefault(m => line.Contains(m)); if(message != null) { _messages[message].Set(); _messages.Remove(message); } } } catch (TimeoutException) { //do nothing, this could happen if the serial stream was cut and doesn't send a NewLine //like when a title is launched } } } } }