Education
Unknown
Created Review

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 until ReceiveDataAsync is called again. Missing this re-arm call results in receiving the first response but silently dropping everything after. Always put client.ReceiveDataAsync(callback) as the last line of the receive callback.

  • Accessing panel joins before registration. Attempting to set panel.BooleanInput[1].BoolValue before panel.Register() succeeds (or before OnlineStatusChange fires with connected status) throws a null reference exception or silently fails. Queue all initial state pushes to fire in Panel_OnlineStatusChange when the panel comes online.

  • Blocking the event handler with synchronous HTTP. Calling client.Dispatch(request) (synchronous HTTP) inside a SigChange event 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. Use DispatchAsync or dispatch to a CrestronInvoke worker thread.

  • Race condition on shared state without locking. Two event handlers (RS-232 receive and panel button press) both read and write _currentSource without a CCriticalSection. 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 CTimer objects that fire after a program reload can call into an uninitialized object graph, causing exceptions or unpredictable behavior. Dispose all timers in the program's Dispose() method.

We use optional analytics cookies to understand site usage and improve the experience. You can accept or reject.