In the last part, we got a basic SSH connection working to our Cisco codec. Now we’re going to try sending and receiving some commands!
We need to add a ShellStream to our Device class:
public class Device
{
private SshClient _ssh;
private ShellStream _stream;
public string Host { get; set; }
Then in our Connect method, we can instantiate _stream
:
public void Connect()
{
// Handle interactive authentication (typing in password)
var auth = new KeyboardInteractiveAuthenticationMethod(User);
auth.AuthenticationPrompt += HandleAuthentication;
// Create connection info
var conn = new ConnectionInfo(Host, User, auth);
// Connect SSH session
_ssh = new SshClient(conn);
_ssh.ErrorOccurred += HandleError;
_ssh.Connect();
// Create I/O stream
_stream = _ssh.CreateShellStream("Terminal", 80, 24, 800, 600, 1024);
_stream.DataReceived += HandleDataReceived;
_stream.ErrorOccurred += HandleStreamError;
if (OnConnect != null)
{
OnConnect(this, new EventArgs());
}
}
CreateShellStream uses the _ssh
connection to setup an I/O stream for communication. The arguments are a little strange, but here they are referenced from the help file:
public ShellStream CreateShellStream(string terminalName,
uint columns,
uint rows,
uint width,
uint height,
int bufferSize)
Think of these arguments like you are creating a window on your screen that you plan to type commands into. We tell it how many columns and rows of text we want as well as the pixel dimensions for the window. We don’t care about either of these since we aren’t displaying a window anywhere, but give the method some reasonable values. I’m just using the same values from Fraser McLean’s SSH-Client module since he knows what he’s doing!
We registered a few event handlers that we still need to create so lets do that now:
private void HandleDataReceived(object sender,
ShellDataEventArgs args)
{
}
private void HandleStreamError(object sender,
EventArgs args)
{
}
Build the solution and there shouldn’t be any errors… but nothing exciting either. How are we going to get commands to the device and responses to our program? How about another event handler! Your typical event handler has a signature like:
private void OnEvent(object sender, EventArgs args) { }
The problem here in that EventArgs contains a single member named Empty. We need a subclass that contains something useful we can pass back to our program. We’ll create a new class named DataEventArgs that will do just that. We can define this just above the Device class in the same file:
namespace CiscoRoomKit
{
public class DataEventArgs : EventArgs
{
public string Message { get; set; }
}
public class Device
{
Now add a new event handler in our Device class:
public class Device
{
private SshClient _ssh;
private ShellStream _stream;
public string Host { get; set; }
public string User { get; set; }
public string Password { get; set; }
public event EventHandler OnConnect;
public event EventHandler OnDisconnect;
public event EventHandler<DataEventArgs> OnDataReceived;
public Device()
{
We can fill in the HandleDataReceived method and send the received data to the OnDataReceived event:
private void HandleDataReceived(object sender,
ShellDataEventArgs args)
{
var stream = (ShellStream)sender;
string data = "";
// Gather all the available data
while (stream.DataAvailable)
{
data += stream.Read();
}
if (data != "")
{
if (OnDataReceived != null)
{
OnDataReceived(this, new DataEventArgs() { Message = data });
}
}
}
What this method does is read all the data into a string (data
), then pass it to OnDataReceived using a new DataEventArgs object. We can subscribe to OnDataReceived, and grab the message that way.
And we should go ahead and fill in HandleStreamError as well. This one is easy, we’ll just close the connection:
private void HandleStreamError(object sender,
EventArgs args)
{
Disconnect();
}
And lets add some more clean up to the Disconnect method:
public void Disconnect()
{
if (_ssh != null)
{
if (_stream != null)
{
// Close and dispose the I/O stream
_stream.Close();
_stream.Dispose();
}
// Disconnect and dispose of SSH session
_ssh.Disconnect();
_ssh.Dispose();
_ssh = null;
if (OnDisconnect != null)
{
OnDisconnect(this, new EventArgs());
}
}
}
One last thing we need is a way to send commands through the stream. Let’s add a SendCommand method to handle this:
public void SendCommand(string cmd)
{
// Make sure we can actually send data
if (_ssh.IsConnected && _stream.CanWrite)
{
_stream.WriteLine(cmd);
}
}
This checks to make sure the SSH connection is active and we can write to the I/O stream. Then we just write the command out to the stream to send it to the device.
Build the solution again (no errors!) and copy the CLZ library to your SIMPL project directory.
SIMPL+
Open the Cisco Codec Wrapper.usp module and update it to pass commands to/from our library:
// --- Compiler Directives ---
#DEFAULT_VOLATILE
#ENABLE_STACK_CHECKING
#ENABLE_TRACE
#DEFINE_CONSTANT MAX_NAME_LEN 50
#DEFINE_CONSTANT MAX_TEXT_LEN 255
#USER_SIMPLSHARP_LIBRARY "CiscoRoomKit"
// --- Inputs ---
DIGITAL_INPUT Connect;
STRING_INPUT To_Device[MAX_TEXT_LEN];
// --- Outputs ---
DIGITAL_OUTPUT Connect_Fb;
DIGITAL_OUTPUT Error_Fb;
STRING_OUTPUT Error_Message;
STRING_OUTPUT From_Device;
// --- Parameters ---
STRING_PARAMETER Host[MAX_NAME_LEN];
STRING_PARAMETER User[MAX_NAME_LEN];
STRING_PARAMETER Password[MAX_NAME_LEN];
// --- Global Variables ---
Device Codec;
// --- Events ---
PUSH Connect
{
Try
{
Codec.Connect();
Error_Fb = 0;
Error_Message = "";
}
Catch
{
Error_Fb = 1;
Error_Message = GetExceptionMessage();
}
}
RELEASE Connect
{
Codec.Disconnect();
}
CHANGE To_Device
{
Try
{
Codec.SendCommand(To_Device);
}
Catch
{
Error_Fb = 1;
Error_Message = GetExceptionMessage();
}
}
EVENTHANDLER Codec_OnConnect (Device sender, EventArgs args)
{
Connect_Fb = 1;
}
EVENTHANDLER Codec_OnDisconnect (Device sender, EventArgs args)
{
Connect_Fb = 0;
}
EVENTHANDLER Codec_OnDataRx (Device sender, DataEventArgs args)
{
From_Device = args.Message;
}
// --- Main ---
Function Main()
{
Codec.Host = Host;
Codec.User = User;
Codec.Password = Password;
RegisterEvent(Codec, OnConnect, Codec_OnConnect);
RegisterEvent(Codec, OnDisconnect, Codec_OnDisconnect);
RegisterEvent(Codec, OnDataReceived, Codec_OnDataRx);
WaitForInitializationComplete();
}
We’ve created To_Device and From_Device to handle exchanging data through our I/O stream. We’ve also added some error handling in case our library throws an exception (very likely to happen when calling Connect or SendCommand). And we register OnDataReceived so we can pass those messages through the From_Device signal.
Save and compile, then head into SIMPL Windows to update our demo program.
SIMPL
Update the Cisco Codec Demo.smw program with our new module. You might have to delete out the old symbol and re-add it since the inputs and outputs have changed significantly:

Save and compile this. Load to your Crestron processor. Now when you send a string To_Device, you should get back a mess of text:

And there you go: we can send and receive commands from our codec. This is still very rough at this point. In the next part, we’ll work on creating a new class in our library to abstract away some of the details about talking to a Cisco codec. Then we can spend part four fleshing out what our module does and using it in a REAL program.
Thanks for reading! Code is available on GitHub.