I tested this plugin with the Ajazz AKP03 keypad, but it should work with other clones.
This is a fork from the original project which I used to reverse engineer how the StreamDock AKP03 plugins work.
I finally managed to create a plugin for the Olliter OL-Master software. The plugin is still in development and I'm working on it in my free time. Collaborations are welcome.
- Ajazz AKP03 (personally tested)
- Stream Deck MK.2 (reported by some users)
This plugin is used to control the OL-Master software with macros and useful features
- Main/sub receivers enable
- Main/sub receivers MOX
- Main/sub receivers volume (with both dials and buttons)
- Main/sub receivers tuning (with both dials and buttons)
- Customizable MQTT settings
- OL-Master software startup
- Assign icon to "Launch OL-Master" action
- Implement OL-Master software path
- Implement CW Keyer macros
- Implement audio macros
- Download the latest release from the releases page
- Double-click the
.streamDeckPluginfile to install it on your Stream Deck software - The plugin should appear in your actions list under the "IU2FRL MQTT" category
For other clones (like the Ajazz AKP03 keypad), a manual installation is required:
- Downlaod the latest release from the releases page
- Select the
-manual.zipfile
- Select the
- Unzip the file to the software's plugin folder:
%appdata%\HotSpot\StreamDock\Plugins\it.iu2frl.streamdock.olliter.sdPlugin\(manually create the plugin folder if needed) - Restart the AJAZZ Stream Dock software
This project is licensed under the MIT License - see the LICENSE file for details
Plugin details
As there's a very few documentation available for the StreamDeck plugin system, I had to reverse-engineer a lot of the functionality. Here are my findings and some notes
- MainPluginFolder.sdPlugin/
-- bin/
-- images/
-- properties_inspector/
--- property-inspector.html
--- js/
---- sdtools.common.js
-- manifest.json
- Every pugin has a folder which has some speficic naming convention, for example
it.iu2frl.streamdock.mqtt.sdPlugin. ThesdPluginat the end is always there, I'm guessing it is required. - The
binfolder contains the binaries that are executed when the plugin is loaded or the keys are pressed (the .NET build output in our case) - The
imagesfolder contains the images used in the plugin. These are the images shown when dropping buttons to the keys. - The
properties_inspectorfolder contains the HTML and JavaScript files for the property inspector UI. These are web pages used to configure the plugin's settings. - The
manifest.jsonfile contains the plugin metadata (see below).
The manifest file is used by the Ajazz app to kwnow how the plugin works, for example:
{
"SDKVersion": 2,
"Author": "DevAny",
"CodePath": "bin/release/StreamDock.Plugins.Payload",
"Description": "StreamDock plugin to send MQTT commands via WS.",
"Name": "IU2FRL MQTT over WS plugin",
"Icon": "images/IU2FRL",
"URL": "https://www.iu2frl.it/",
"Version": "1.0",
"Software": {
"MinimumVersion": "2.9"
},
"OS": [
{
"Platform": "windows",
"MinimumVersion": "10"
}
],
"Category": "Mqtt",
"CategoryIcon": "images/Mqtt",
"Actions": [
{
"PropertyInspectorPath": "property-inspector.html",
"Name": "Send MQTT Message",
"Tooltip": "Sends a message to the MQTT broker when button is pressed",
"Controllers": [
"Keypad"
],
"UUID": "it.iu2frl.streamdock.mqtt.mqttbutton",
"Icon": "images/Mqtt",
"SupportedInMultiActions": true
},
{
"PropertyInspectorPath": "property-inspector.html",
"Name": "Send MQTT Message",
"Tooltip": "Sends a message to the MQTT broker when knob is rotated",
"Controllers": [
"Knob"
],
"UUID": "it.iu2frl.streamdock.mqtt.mqttknob",
"Icon": "images/Mqtt",
"SupportedInMultiActions": true
},
{
"PropertyInspectorPath": "property-inspector.html",
"Name": "Keypad Debug",
"Tooltip": "This function only prints to the log",
"Controllers": [
"Keypad"
],
"UUID": "it.iu2frl.streamdock.keypaddebug",
"Icon": "images/Mqtt",
"SupportedInMultiActions": true
},
{
"PropertyInspectorPath": "property-inspector.html",
"Name": "Dial Debug",
"Tooltip": "This function only prints to the log",
"Controllers": [
"Knob"
],
"UUID": "it.iu2frl.streamdock.dialdebug",
"Icon": "images/Mqtt",
"SupportedInMultiActions": true
}
]
}As you can see, there's an Actions array which contains a mapping to the C# methods (see below.)
The full documentation can be found here: https://staging-docs.elgato.com/streamdeck/sdk/references/manifest/
I've found two controllers so far:
Keypad: which tells the plugin this action can be used on keysKnob: which tells the plugin this action can be applied to knobs
Then, for some reason I've also found:
Information: unknown (not documented in the plugins doc)
The script called build_manifest.py will read the plugin source code and build the manifest file automatically.
The plugin only builds the actions list, other customization should be done manually.
The base method implementation for an action (like a key press) is derived from the KeyPadBase from the StreamDeck package. I preferred to wrap this initializer to a custom class so I can create shorter code when defining actions.
namespace Common
{
public class BaseKeyItem : KeypadBase
{
public BaseKeyItem(ISDConnection connection, InitialPayload payload) : base(connection, payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: Something to do as soon as the element is initialized");
}
public override void KeyPressed(KeyPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: KeyPressed called");
}
public override void KeyReleased(KeyPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: KeyReleased called");
}
public override void OnTick()
{
// Not sure when this is called
}
public override void Dispose()
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: Something to do when the object is removed");
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: ReceivedSettings called");
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: ReceivedGlobalSettings called");
}
}
}Once we creat that wrapper, for each action we need to define a NameSpace similar to this one:
Please note: The comments above the method name are used by the Python script (called build_manifest.py) to build the manifest file automatically at every build event of the .NET solution. They are not normally needed.
namespace ToggleMox1
{
// Name: MOX Receiver 1
// Tooltip: Toggle receive/transmit RX 1
// Controllers: Keypad
[PluginActionId("it.iu2frl.streamdock.mqtt.togglemox1")]
public class ToggleRx1(ISDConnection connection, InitialPayload payload) : Common.BaseKeyItem(connection, payload)
{
public override void KeyPressed(KeyPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, "KeyPressed called");
}
}
}The base method implementation for an action (like a dial rotation) is derived from the EncoderBase from the StreamDeck package. I preferred to wrap this initializer to a custom class so I can create shorter code when defining actions.
public class BaseDialItem : EncoderBase
{
public BaseDialMqttItem(ISDConnection connection, InitialPayload payload) : base(connection, payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: Something to do as soon as the element is initialized");
}
public override void DialDown(DialPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: DialDown called");
}
public override void DialRotate(DialRotatePayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: DialRotate called");
}
public override void DialUp(DialPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: DialUp called");
}
public override void Dispose()
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: Dispose called");
}
public override void OnTick()
{
// Not sure when this is called
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: ReceivedGlobalSettings called");
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: ReceivedSettings called");
}
public override void TouchPress(TouchpadPressPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"{GetType().Name}: TouchPress called");
}
}Once we creat that wrapper, for each action we need to define a NameSpace similar to this one:
Please note: The comments above the method name are used by the Python script (called build_manifest.py) to build the manifest file automatically at every build event of the .NET solution. They are not normally needed.
namespace TuneRx1
{
// Name: Tune Receiver 1
// Tooltip: Frequency knob for RX 1
// Controllers: Knob
[PluginActionId("it.iu2frl.streamdock.mqtt.tunerx1")]
public class TuneRx1(ISDConnection connection, InitialPayload payload) : Common.BaseDialMqttItem(connection, payload)
{
public override void DialUp(DialPayload payload)
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial was rotated");
}
}
}This plugin supports a variety of actions, each with its own settings. The StreamDock.Plugins.Olliter plugin uses a two-tier settings system:
- Global Settings: Configuration shared across all plugin instances
- Local Settings: Configuration specific to each plugin instance
The settings system operates through a bidirectional communication between:
- The C# backend (
Common.cs) - The JavaScript frontend (
sdtools.common.js)
Each property-inspector.html file corresponds to a specific plugin instance/type and contains the UI elements for configuring that instance. The sdtools.common.js file handles the communication between the UI and the C# backend by reading the settings from the CS code to build the UI (like with the dropdown lists) and loading the default settings (or the previous ones).
Once some changes are detected by the UI with the setSettings() function, the updated settings are sent to the C# backend for processing as a JSON payload. These payloads trigger the ReceivedSettings or ReceivedGlobalSettings handlers in the plugin, which update the internal settings objects.
Global settings are stored in the GlobalPluginSettings class and include MQTT connection details:
- MQTT Host
- MQTT Port
- Authentication credentials
- WebSocket configuration
Local settings are stored in the PluginSettings class and include configuration specific to each plugin instance:
- Receiver index
- Sub-receiver selection
- Band selection
- Volume increments
- Frequency increments
- SDR mode
-
Initialization:
- When a plugin instance starts, it checks for existing settings in the payload
- If no settings are found, default settings are created
-
Settings Update:
- When settings change in the Property Inspector,
setSettings()insdtools.common.jscollects values from UI elements - Settings are sent to the plugin via WebSocket using the
setSettingsorsetGlobalSettingsevent - In the plugin,
ReceivedSettingsorReceivedGlobalSettingshandlers update the internal settings objects
- When settings change in the Property Inspector,
-
Settings Loading:
- The plugin sends settings to the Property Inspector via the
didReceiveSettingsordidReceiveGlobalSettingsevent loadConfiguration()insdtools.common.jspopulates UI elements with values
- The plugin sends settings to the Property Inspector via the
JSON property names defined with [JsonProperty] in the C# classes map directly to HTML element IDs in the Property Inspector:
This project is a fork from the original project created by DevAny, which can be found here: DevAnyKR/StreamDock-Plugin. There's actually nothing left from the original code, but I wanted to give credit to the original author.

