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
}
}
}
}
}