title: Crestron SIMPL# Pro — C# Programming for 4-Series description: Deep reference for Crestron SIMPL# Pro: C# architecture, Crestron API, hardware device classes, threading model, TCP/IP and HTTP clients, JSON parsing, and Crestron Studio integration. tags: [control-systems, crestron, simpl-sharp, csharp, programming, 4-series, api, tcp, http, json] created: 2026-05-05 status: current review_by: 2027-05-05
Crestron SIMPL# Pro — C# Programming for 4-Series
This note covers SIMPL# Pro C# development. For the graphical SIMPL Windows environment, see control-systems/crestron-simpl-programming. For the Crestron platform overview, see control-systems/crestron-basics.
SIMPL# Pro is Crestron's object-oriented programming environment for 4-Series control processors. Unlike SIMPL Windows, which builds control logic graphically by connecting signal-flow symbols, SIMPL# Pro programs are written in C# using standard .NET development practices — classes, interfaces, inheritance, async/await, and third-party libraries — combined with Crestron's hardware abstraction API. SIMPL# Pro is the right tool for projects that exceed what graphical SIMPL can express cleanly: REST API integrations, complex state machines, data parsing, dynamic room configuration, and programs where maintainability over years is a priority.
Architecture Overview
A SIMPL# Pro program is a C# class library compiled to a .cpz file that runs on the 4-Series processor's Linux-based runtime. The program communicates with the physical world through Crestron's hardware API classes, which abstract the processor's I/O into C# objects. The program communicates with touchpanels through BasicTriList objects representing each panel.
Every SIMPL# Pro program inherits from CrestronControlSystem:
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.CrestronThread;
public class ControlSystem : CrestronControlSystem
{
public ControlSystem() : base()
{
// Constructor — hardware not yet initialized here
}
public override void InitializeSystem()
{
// Called after hardware is ready; start program logic here
CrestronConsole.PrintLine("System initializing...");
}
}
InitializeSystem() is the entry point — hardware devices are registered, event handlers are wired, and startup sequences begin here.
Hardware Device Classes
The Crestron API models every hardware device as a C# class. Devices are instantiated with their IP ID (for IP-connected devices) or Cresnet ID.
Touchpanels
Tsw1070 panel;
public override void InitializeSystem()
{
panel = new Tsw1070(0x03, this); // IP ID 0x03
panel.SigChange += Panel_SigChange;
panel.OnlineStatusChange += Panel_OnlineStatusChange;
if (panel.Register() != eDeviceRegistrationUnRegistrationResponse.Success)
CrestronConsole.PrintLine("Panel registration failed");
}
void Panel_SigChange(BasicTriList device, SigEventArgs args)
{
switch (args.Sig.Type)
{
case eSigType.Bool:
if (args.Sig.Number == 101 && args.Sig.BoolValue) // Join 101 press
DisplayPowerOn();
break;
case eSigType.UShort:
if (args.Sig.Number == 1) // Analog join 1 (volume slider)
SetVolume(args.Sig.UShortValue);
break;
case eSigType.String:
// Serial join received from panel
break;
}
}
Feedback to panels — Drive joins back to the panel using BooleanInput, UShortInput, and StringInput collections:
panel.BooleanInput[101].BoolValue = true; // Turn on LED for join 101
panel.UShortInput[1].UShortValue = 32767; // Set analog join 1 to 50%
panel.StringInput[1].StringValue = "HDMI 1"; // Set text join 1
RS-232 Ports
ComPort displayPort;
public override void InitializeSystem()
{
displayPort = ControllerComPortSlots[1].ComPort; // COM1
displayPort.SetComPortSpec(ComPort.eComBaudRates.ComspecBaudRate9600,
ComPort.eComDataBits.ComspecDataBits8,
ComPort.eComParityType.ComspecParityNone,
ComPort.eComStopBits.ComspecStopBits1,
ComPort.eComProtocolType.ComspecProtocolRS232,
ComPort.eComHardwareHandshakeType.ComspecHardwareHandshakeNone,
ComPort.eComSoftwareHandshakeType.ComspecSoftwareHandshakeNone,
false);
displayPort.SerialDataReceived += DisplayPort_DataReceived;
}
void DisplayPort_DataReceived(ComPort port, ComPortSerialDataEventArgs args)
{
_rxBuffer += args.SerialData;
ParseBuffer();
}
void ParseBuffer()
{
int end = _rxBuffer.IndexOf("\r\n");
while (end >= 0)
{
string line = _rxBuffer.Substring(0, end);
ProcessResponse(line);
_rxBuffer = _rxBuffer.Substring(end + 2);
end = _rxBuffer.IndexOf("\r\n");
}
}
Relays and Versiports
// Relay: close (true) / open (false)
ControllerRelaySlots[1].Relay.State = true;
// Versiport as digital input
ControllerVersiportSlots[1].VersiPort.SetVersiportConfiguration(eVersiportConfiguration.DigitalInput);
ControllerVersiportSlots[1].VersiPort.VersiportChange += VersiPort_Change;
// Versiport as digital output
ControllerVersiportSlots[1].VersiPort.SetVersiportConfiguration(eVersiportConfiguration.DigitalOutput);
ControllerVersiportSlots[1].VersiPort.DigitalOut = true;
TCP/IP Client Communication
SIMPL# Pro provides TCPClient and SecureTCPClient classes for IP-controlled devices.
using Crestron.SimplSharp.CrestronSockets;
TCPClient _tcpClient;
string _ipAddress = "192.168.1.50";
int _port = 4999;
string _rxBuffer = string.Empty;
void ConnectToDevice()
{
_tcpClient = new TCPClient(_ipAddress, _port, 4096);
_tcpClient.SocketStatusChange += Client_SocketStatusChange;
_tcpClient.ReceiveDataAsync(Client_DataReceived);
_tcpClient.ConnectToServerAsync(Client_ConnectCallback);
}
void Client_ConnectCallback(TCPClient client)
{
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
CrestronConsole.PrintLine("Connected to device");
SendCommand("POLL\r\n");
}
else
{
// Reconnect after delay
CrestronEnvironment.Sleep(5000);
_tcpClient.ConnectToServerAsync(Client_ConnectCallback);
}
}
void Client_SocketStatusChange(TCPClient client, SocketStatus status)
{
if (status != SocketStatus.SOCKET_STATUS_CONNECTED)
{
CrestronConsole.PrintLine("Disconnected — scheduling reconnect");
CrestronEnvironment.Sleep(5000);
_tcpClient.ConnectToServerAsync(Client_ConnectCallback);
}
}
void Client_DataReceived(TCPClient client, int bytesReceived)
{
byte[] data = client.IncomingDataBuffer;
_rxBuffer += Encoding.ASCII.GetString(data, 0, bytesReceived);
ParseBuffer();
client.ReceiveDataAsync(Client_DataReceived); // Re-arm receive
}
void SendCommand(string command)
{
if (_tcpClient.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
byte[] bytes = Encoding.ASCII.GetBytes(command);
_tcpClient.SendData(bytes, bytes.Length);
}
}
Important: Always re-arm ReceiveDataAsync at the end of the receive callback. The callback fires once per received data block; without re-arming, subsequent data is silently discarded.
HTTP Client — REST API Integration
SIMPL# Pro's HttpClient handles REST API calls. This is the primary mechanism for integrating room booking systems, building management, and cloud services.
using Crestron.SimplSharp.Net.Http;
void GetRoomBooking(string roomId)
{
HttpClient client = new HttpClient();
client.Accept = "application/json";
client.AllowAutoRedirect = true;
HttpClientRequest request = new HttpClientRequest();
request.Url.Parse($"https://api.roombooking.com/rooms/{roomId}/current");
request.RequestType = RequestType.Get;
request.Header.SetHeaderValue("Authorization", "Bearer " + _apiToken);
HttpClientResponse response = client.Dispatch(request);
if (response.Code == 200)
ParseBookingResponse(response.ContentString);
else
CrestronConsole.PrintLine($"Booking API error: {response.Code}");
}
void ParseBookingResponse(string json)
{
// Crestron's runtime includes Newtonsoft.Json
var booking = Newtonsoft.Json.JsonConvert.DeserializeObject<BookingResponse>(json);
if (booking != null && booking.IsBooked)
{
panel.StringInput[10].StringValue = booking.OrganizerName;
panel.StringInput[11].StringValue = booking.Title;
panel.BooleanInput[50].BoolValue = true; // "Room is booked" indicator
}
}
Async HTTP: For non-blocking HTTP (essential on the main thread), use the async overload:
client.DispatchAsync(request, (response, error) =>
{
if (error == HTTP_CALLBACK_ERROR.COMPLETED)
ParseBookingResponse(response.ContentString);
});
JSON Handling
Crestron's 4-Series runtime includes Newtonsoft.Json (Json.NET). Define matching C# classes and use JsonConvert.DeserializeObject<T>:
public class BookingResponse
{
[JsonProperty("isBooked")]
public bool IsBooked { get; set; }
[JsonProperty("organizer")]
public string OrganizerName { get; set; }
[JsonProperty("subject")]
public string Title { get; set; }
[JsonProperty("endTime")]
public DateTime EndTime { get; set; }
}
For dynamic JSON where the structure is not known at compile time, use JObject:
var obj = JObject.Parse(json);
string name = (string)obj["organizer"]["name"];
Threading Model
The 4-Series processor runs SIMPL# Pro on a Linux multi-threaded runtime. Understanding threading prevents subtle bugs.
Event handlers run on separate threads. SigChange, SerialDataReceived, SocketStatusChange, and similar callbacks fire on background threads — not the main program thread. Accessing shared state from multiple event handlers without synchronization causes race conditions.
Use CMonitor for synchronization (Crestron's threading primitive, analogous to Monitor/lock):
readonly CCriticalSection _lock = new CCriticalSection();
void Panel_SigChange(BasicTriList device, SigEventArgs args)
{
_lock.Enter();
try
{
// Access shared state safely
_currentSource = args.Sig.Number;
}
finally
{
_lock.Leave();
}
}
Never block event handlers. Long operations (HTTP requests, file I/O, complex calculations) on an event handler thread can starve other events. Use CrestronThread or CrestronInvoke to dispatch long operations to a worker thread:
void Panel_SigChange(BasicTriList device, SigEventArgs args)
{
if (args.Sig.Number == 200 && args.Sig.BoolValue)
{
// Start async operation — don't block the event handler
CrestronInvoke.BeginInvoke((_) => FetchRoomBookingFromServer(), null);
}
}
CrestronEnvironment.Sleep(ms) — Suspends the current thread. Safe in worker threads; dangerous in event handlers.
Timers
CTimer _pollTimer;
CTimer _startupDelay;
public override void InitializeSystem()
{
// One-shot timer: fire once after 2 seconds
_startupDelay = new CTimer(StartupCallback, null, 2000);
// Repeating timer: fire every 30 seconds
_pollTimer = new CTimer(PollCallback, null, 0, 30000);
}
void PollCallback(object obj)
{
SendCommand("STATUS\r\n");
}
void StartupCallback(object obj)
{
InitializeDevices();
}
Dispose timers when no longer needed: _pollTimer.Stop(); _pollTimer.Dispose();
Crestron Studio Integration
Crestron Studio is the 4-Series project environment that wraps SIMPL# Pro. Studio provides:
- Certified device modules — Pre-built modules for common AV devices; each module is a SIMPL# Pro class with a defined interface (signal inputs/outputs) that Studio exposes graphically
- Module wiring canvas — Connect module I/O pins visually; Studio generates SIMPL# Pro glue code
- UI Builder — Touchpanel UI design integrated into the Studio project
- One-click deployment — Compile and upload from Studio directly
Studio is appropriate for projects using primarily certified modules with minimal custom logic. For projects requiring substantial custom code, developing in SIMPL# Pro directly (in Visual Studio with the Crestron SDK) gives better control over code organization and testing.
Visual Studio + Crestron SDK: Professional SIMPL# Pro development uses Visual Studio (Community or Professional) with the Crestron SDK installed. The SDK provides IntelliSense, full debugging capability via Crestron Toolbox's remote debugger, and access to the complete Crestron API. The compiled .cpz file is uploaded to the processor via Toolbox.
Crestron Console Commands for SIMPL# Pro Debugging
Access via Crestron Toolbox Text Console:
PROGREGISTER # List all registered programs and their status
PROGRESET 1 # Reset program slot 1
APPSTAT # Show application status (memory, CPU)
APPDEBUG 1 ON # Enable debug output from program slot 1
ERRCLOG # Show error log (catches unhandled exceptions)
Unhandled exceptions in SIMPL# Pro crash the program thread and are logged to the error log. Always wrap event handlers in try/catch during development:
void Panel_SigChange(BasicTriList device, SigEventArgs args)
{
try
{
// logic
}
catch (Exception e)
{
CrestronConsole.PrintLine($"SigChange exception: {e.Message}");
}
}
Common Pitfalls
-
Forgetting to re-arm
ReceiveDataAsync. TCP receive callbacks in Crestron's API are one-shot: after the callback fires, no further data is received untilReceiveDataAsyncis called again. Missing this re-arm call results in receiving the first response but silently dropping everything after. Always putclient.ReceiveDataAsync(callback)as the last line of the receive callback. -
Accessing panel joins before registration. Attempting to set
panel.BooleanInput[1].BoolValuebeforepanel.Register()succeeds (or beforeOnlineStatusChangefires with connected status) throws a null reference exception or silently fails. Queue all initial state pushes to fire inPanel_OnlineStatusChangewhen the panel comes online. -
Blocking the event handler with synchronous HTTP. Calling
client.Dispatch(request)(synchronous HTTP) inside aSigChangeevent handler blocks the event thread for the duration of the HTTP request — typically 200ms–2s. During that time, no other panel events are processed; the panel appears to freeze. UseDispatchAsyncor dispatch to aCrestronInvokeworker thread. -
Race condition on shared state without locking. Two event handlers (RS-232 receive and panel button press) both read and write
_currentSourcewithout aCCriticalSection. Intermittent wrong-state bugs result. Lock all shared mutable state accessed from multiple event handler contexts. -
Newtonsoft.Json version conflict. If a third-party NuGet package references a different version of Newtonsoft.Json than the one bundled in the Crestron runtime, assembly binding failures occur. Avoid adding NuGet packages that depend on Newtonsoft.Json; use the Crestron-bundled version directly.
-
CTimer not disposed before program reload. Active
CTimerobjects that fire after a program reload can call into an uninitialized object graph, causing exceptions or unpredictable behavior. Dispose all timers in the program'sDispose()method.