What are Essentials Plugins?
- YouTube Video - Loading Plugins in PepperDash Essentials
- YouTube Video - Build Your Own Plugin, Part 1
- YouTube Video - Build Your Own Plugin, Part 2
Plugins are SIMPL# Pro (.NET 8) libraries that reference the Essentials Framework and can be loaded into an Essentials Application at runtime to extend functionality beyond what the Essentials Framework provides on its own.
Why Use Plugins?
Plugins are a way to extend or add new functionality to the Essentials Application without having to modify the actual Framework. In most cases, a plugin can be written to support a new device or behavior. Using plugins also limits the scope of understanding needed to work within the Essentials Framework.
Should I use a Plugin?
Essentials is meant to be a lightweight framework and an extensible basis for development. While some devices are included in the framework, mostly for the purposes of providing examples and developing and prototyping new device types, the bulk of new development is intended to take place in Plugins. Once a plugin adds new functionality that may be of benefit if shared across multiple plugins, it may make sense to port that common logic (base classes and/or interfaces) back into the framework to make it available to others. The thrust of future Essentials development is targeted towards building a library of plugins.
How do Plugins Work?
One or more plugins can be loaded to the plugins folder as .dlls or .cplz packages. On a 4-series processor this is /user/programX/plugins; on VC-4 it is the room's User/plugins folder. When the Essentials Application starts, if the plugins folder exists, it moves any .dll files from it into plugins/loadedAssemblies, unzips any .cplz files it finds anywhere under the user folder (/user/programX or User) into the same place, and then loads the .dll assemblies in loadedAssemblies. Once the plugin assemblies are loaded the Essentials Application will then attempt to load a configuration file and construct items as defined in the file. Those items can be defined in either the Essentials Framework or in any of the loaded plugin assemblies.
flowchart TD
Start(["PluginLoader.LoadPlugins()"]) --> Exists{"plugins folder<br/>exists?"}
Exists -- No --> Config(["Continue: read the config file"])
Exists -- Yes --> Move["Move .dll files from plugins<br/>into plugins/loadedAssemblies<br/>(skipping assemblies already loaded)"]
Move --> Unzip["Unzip every .cplz under the user folder<br/>into plugins/loadedAssemblies"]
Unzip --> EachDll["For each .dll in loadedAssemblies"]
EachDll --> Compatible{"Built for .NET 8, and its<br/>Essentials references are<br/>available in this version?"}
Compatible -- No --> Issue1["Record a plugin load issue<br/>and skip the file"]
Compatible -- Yes --> LoadAsm["Load the assembly"]
LoadAsm --> EachType["For each type in each loaded plugin assembly"]
EachType --> IsFactory{"Implements<br/>IPluginDeviceFactory?"}
IsFactory -- No --> EachType
IsFactory -- Yes --> MinVersion{"Running Essentials version is at least<br/>MinimumEssentialsFrameworkVersion?"}
MinVersion -- No --> Issue2["Record a plugin load issue<br/>and skip the factory"]
MinVersion -- Yes --> Register["Register each of the factory's TypeNames<br/>with DeviceFactory"]
Register --> Report["Set each plugin's load status, and warn if there were<br/>issues (see pluginissues)"]
Issue1 --> Report
Issue2 --> Report
Report --> Config
What Must be Implemented in a Plugin for it to Work?
All plugin assemblies must contain a class which inherits from EssentialsPluginDeviceFactory<T> (or otherwise implements IPluginDeviceFactory), where <T> is a class which inherits from PepperDash.Essentials.Core.EssentialsDevice. The device class <T> must have a [Description("...")] attribute (PepperDash.Essentials.Core.DescriptionAttribute); the loader reads it when registering each type name, and the factory fails to load without it.
Within this class, we will define some metadata for the plugin and define which constructor to use for instantiating each class as defined by type.
Note that multiple types can be loaded from the same plugin.
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
using PepperDash.Core;
using System.Collections.Generic;
namespace MyPlugin
{
public class SomeDeviceFactory : EssentialsPluginDeviceFactory<SomeDevice>
{
// This method defines metadata for the devices in the plugin
public SomeDeviceFactory()
{
// This string is used to define the minimum version of the
// Essentials Framework required for this plugin
MinimumEssentialsFrameworkVersion = "3.0.0";
// This string is used to define all valid type names for
// this plugin. This string is case insensitive
TypeNames = new List<string> { "SomeDevice" , "ThisNewDevice" };
}
// This method instantiates new devices for the defined type
// within the plugin
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
// Deserialize the json properties for the loaded type to a new object
var props = dc.Properties.ToObject<SomeDeviceProperties>();
// Return a newly instantiated device using the desired constructor
// If no valid property data exists, return null with console print
// describing the failure.
if (props == null)
{
Debug.LogError("No valid property data for 'SomeDevice' - Verify Configuration.");
Debug.LogError("{properties}", dc.Properties.ToString());
return null;
}
return new SomeDevice(dc.Key, dc.Name, props);
}
}
public class OtherDeviceFactory : EssentialsPluginDeviceFactory<OtherDevice>
{
// This method defines metadata for the devices in the plugin
public OtherDeviceFactory()
{
// This string is used to define the minimum version of the
// Essentials Framework required for this plugin
MinimumEssentialsFrameworkVersion = "3.0.0";
// This string is used to define all valid type names for
// this plugin. This string is case insensitive
TypeNames = new List<string> { "OtherDevice", "ThisOtherDevice" };
}
// This method instantiates new devices for the defined type
// within the plugin
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
// Deserialize the json properties for the loaded type to a new object
var props = dc.Properties.ToObject<OtherDeviceProperties>();
// Return a newly instantiated device using the desired constructor
// If no valid property data exists, return null with console print
// describing the failure.
if (props == null)
{
Debug.LogError("No valid property data for 'OtherDevice' - Verify Configuration.");
Debug.LogError("{properties}", dc.Properties.ToString());
return null;
}
return new OtherDevice(dc.Key, dc.Name, props);
}
}
}
Plugin Load Diagnostics
Before Essentials loads a plugin, it reads the plugin's metadata to see what the plugin was built against, and compares that with what is running. Everything that goes wrong while loading plugins is recorded, logged, and available afterwards from the console and the Web API.
What is checked
- Essentials version. The
PepperDashEssentialsandPepperDash.Essentials.CoreNuGet packages add anAssemblyMetadataattribute to every project that references them, recording the exact package version, for examplePepperDash.Essentials.BuiltAgainst:PepperDashEssentials = 3.0.0-rc.16. Plugins get this automatically when they are rebuilt; no changes are needed. If the plugin was built against a newer Essentials than the one running, a warning is recorded. - Plugins built before the stamp existed. Essentials falls back to the version of the Essentials assemblies the plugin references. Those carry only
major.minor.patch, so a difference between two prereleases of the same version cannot be seen. - Shared dependencies, including the Crestron SDK. If the plugin references a newer version of an assembly that is already present, such as
Crestron.SimplSharpPro.UI 2.21.274.0when Essentials was built against2.21.128.0, a warning is recorded. Essentials and its plugins should be built against the same Crestron SDK version.
These checks only warn. A plugin with a version mismatch is still loaded, as it always has been.
Local developer builds (-local), 0.x versions, and prereleases with different labels (a feature-branch build against an rc) are not compared.
To leave the stamp out of a project, set:
<PepperDashEssentialsSkipVersionStamp>true</PepperDashEssentialsSkipVersionStamp>
What is recorded
Each problem is logged when it happens, at Warning or Error, and kept in memory with the plugin file it belongs to. Besides the version checks above, that includes plugins that use APIs missing on .NET 8, assemblies that fail to load, missing dependencies, types that cannot be read, plugin factories that throw, factories skipped because MinimumEssentialsFrameworkVersion is not met, development factories whose DevelopmentEssentialsFrameworkVersions list does not include the running build, duplicate assemblies (a copy already loaded from another file, or an Essentials assembly bundled in a plugin that the program already provides), and .cplz archives that cannot be extracted. A .cplz that cannot be extracted no longer stops the remaining plugins from loading.
Each plugin file also gets a status:
| Status | Meaning |
|---|---|
Loaded |
The assembly loaded and at least one plugin factory was registered. |
LoadedAsLibrary |
The assembly loaded and has no plugin factory. It is a library another plugin uses. |
Skipped |
The file was not used: none of its plugin factories were registered, or it duplicates an assembly the program already provides. |
Failed |
The assembly could not be loaded. |
Reading the results
At the console:
pluginissues
pluginissues verbose
verbose adds the detail for each issue, such as the exception or how to fix it. reportversions also prints the issue count.
From the Web API:
GET /cws/app01/api/pluginLoadIssues
GET /cws/app01/api/pluginLoadIssues?all=true
On Virtual Control the base path is /cws/api. The response lists the plugin files with issues; add ?all=true to include the ones that loaded cleanly:
{
"EssentialsVersion": "3.0.0-rc.11",
"IssueCount": 2,
"ErrorCount": 0,
"Plugins": [
{
"FileName": "MyPlugin.dll",
"AssemblyName": "MyPlugin",
"Version": "1.0.0",
"BuiltAgainst": { "PepperDashEssentials": "3.0.0-rc.16" },
"Status": "Loaded",
"Issues": [
{
"Severity": "Warning",
"Category": "BuiltAgainstNewerEssentials",
"Message": "Built against PepperDashEssentials 3.0.0-rc.16, but Essentials 3.0.0-rc.11 is running.",
"Detail": "...",
"TriggeredBy": "Direct load",
"TimestampUtc": "2026-10-20T14:02:11Z"
}
]
}
]
}
The Essentials web config app shows the same information on its Plugin Load Issues page.
SIMPL Bridging
Optionally, if your plugin device needs to be able to bridge to a SIMPL program over EISC, and there isn't already an existing bridge class in the Essentials Framework, you can write a new bridge class in your plugin. However, in order for the Essentials Application to be able to us that new bridge, the device must implement the IBridgeAdvanced interface and its LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) method.
If you are writing code for a bridgeable device, you should be inheriting from EssentialsBridgeableDevice, which inherits from the already-required EssentialsDevice and implements IBridgeAdvanced.
Often though, you may find that a bridge class already exists in the Essentials Framework that you can leverage. For example, if you were writing a plugin to support a new display model that isn't already in the Essentials Framework, you would define a class in your plugin that inherits from PepperDash.Essentials.Devices.Common.Displays.DisplayBase (or TwoWayDisplayBase). If you're only implementing the standard display control functions such as power/input/volume control, then your LinkToApi can call the existing LinkDisplayToApi method on DisplayBase, which uses the DisplayControllerJoinMap. If you needed to add additional functions to the bridge, then you would need to write your own bridge in the plugin.
For additional info see the SIMPL-Bridging article.
Template Essentials Plugin Repository
Fork this repository when starting a new plugin. Plugins reference Essentials through the PepperDashEssentials NuGet package, so set the package version to the Essentials build that contains any dependencies that your plugin may rely on.
Note
The template currently targets Essentials 2.x (net472, PepperDashEssentials 2.x); its migration to v3 is in progress. To use it for a v3 plugin, change the project's TargetFramework to net8 and its PepperDashEssentials package reference to a 3.x version, then fix any API changes the build reports.