Edit

Deck App

A Deck App is a type of application designed to interact with decks and buttons created in SAMMI Core. It’s a great way to control SAMMI from a different device, such as a tablet or a phone.

Spotify Deck in Deck Hopper
Spotify Deck in Deck Hopper

SAMMI Panel and SAMMI Deck #

SAMMI Panel is a free, open-source web-based Deck App. Development was previously paused but has resumed. It is not prioritized over other efforts, so updates may be infrequent.

The latest version of SAMMI Panel can be accessed at https://panel.sammi.solutions/ and supports the full SAMMI Deck App protocol including:

  • Triggering and releasing buttons
  • Real-time button modifications and ongoing-button tracking
  • Deck switching (specific deck, next, previous)
  • Enabling and disabling decks (ChangeDeckStatus)
  • Stopping buttons (StopButton)
  • Reinitialising button variables (ReinitButtonVars)
  • Sending custom messages to SAMMI triggers (CustomMessage)
  • Responding to Deck App: Wait for Input, Wait for Choice, and Wait for Multi Choice commands
  • Receiving custom JSON payloads via Deck App: Send JSON (dispatched as a SAMMISendJSON DOM event for developers building on top of the panel)

SAMMI Deck is now outdated and incompatible with the latest SAMMI Core versions.

Deck Hopper #

We highly recommend using Deck Hopper, a solution officially endorsed by the SAMMI team.

Deck Hopper is available for Desktop (Windows, macOS, Linux), Android and iOS.

It comes with a free trial, and a PRO version that removes ads and includes all advanced features. (not applicable to the iOS version, which is Pro by default)

Setup #

Deck Hopper #

Deck Hopper setup file is included in all SAMMI 2024.1.0 or newer downloads by default.

Go to SAMMI Core - Deck App and select Open Deck Hopper. If Deck Hopper isn’t installed, you’ll be prompted to install it. After installation, you can launch it from the same menu.

Follow the setup instructions on the Deck Hopper Setup Page to connect it with your SAMMI Core.

Deck Hopper
Deck Hopper

SAMMI Panel #

Visit the SAMMI Panel website at https://panel.sammi.solutions/.

Upon first use, you’ll see an agreement page. After agreeing to the SAMMI Panel EULA, the Panel will start. You’ll then see the connection settings for the Panel.

The default IP address for using the Panel on the same PC as SAMMI Core is 127.0.0.1 with the port 9470.

For accessing SAMMI Panel from a different device, use the IP address of the PC running SAMMI Core. This can be found in SAMMI Core → Deck App → Get LAN IP. Enter this IP address in the Panel’s IP address field.

SAMMI Panel Connection Settings
SAMMI Panel Connection Settings

Choose a deck from the dropdown and click Select Deck. You can now interact with it by pressing its buttons.

What’s supported in SAMMI Panel:

  • Full button control (trigger, release, stop, block/unblock)
  • Real-time button modifications and state sync
  • Deck switching (specific, next, previous) via Deck App: Switch Deck
  • Enable/disable decks programmatically
  • Deck App: Wait for Input, Wait for Choice, and Wait for Multi Choice — shows a pop-up inside the panel
  • Deck App: Send JSON — dispatched as a SAMMISendJSON DOM event (useful for developers building on top of SAMMI Panel)

Other Alternatives #

Deck Mate Control, developed by SAMMI Community member Flipstream, is an Android-exclusive alternative to SAMMI Panel.


For Deck App Developers #

This section documents the WebSocket protocol that SAMMI Core exposes for Deck App connections. Any app that speaks this protocol can act as a fully featured Deck App.

SAMMI Core listens on port 9470 by default (configurable in Settings). Deck Apps connect via a standard WebSocket (ws://).

Recommended Library #

The easiest way to implement a JavaScript/browser-based Deck App is to use the SAMMI WebSocket library, which wraps the protocol and exposes a promise-based API. SAMMI Panel is built on top of this library. For other platforms, implement the protocol described below directly.

Operation Codes #

Every message is a JSON object with a top-level op field that identifies the message type.

OP Name Direction Purpose
1 Heartbeat Both Keep-alive ping — send periodically to maintain the connection
2 Identify Client → Server Authentication request sent immediately after connecting
3 Identified Server → Client Confirms successful authentication
4 Request Client → Server A request to SAMMI (fetch data, trigger a button, etc.)
5 Response Server → Client SAMMI’s reply to a Request
6 Event Server → Client Unsolicited event broadcast by SAMMI (button state changes, deck updates, etc.)
7 Close Both Signals that the connection is being closed

Authentication #

After the WebSocket connection is established, the client must send an Identify message:

{
  "op": 2,
  "id": "optional-request-id",
  "data": {
    "clientName": "My Deck App",
    "authentication": "<password-hash>"
  }
}
  • clientName — a human-readable name for the connection. SAMMI uses this to target events at a specific panel via the panelName field on Switch Deck commands.
  • authentication — the password hash. If SAMMI has no panel password set, send an empty string. The hash algorithm matches the one used by SAMMI Bridge.

On success SAMMI replies with Identified (op: 3). On failure the connection is closed.

Requests (Client → SAMMI) #

All requests use op: 4. Supply an id to match the response.

{
  "op": 4,
  "id": "my-request-id",
  "data": {
    "requestName": "RequestName",
    "requestData": { }
  }
}

SAMMI replies with op: 5:

{
  "op": 5,
  "id": "my-request-id",
  "data": {
    "requestName": "RequestName",
    "requestSuccess": true,
    "responseData": { }
  }
}

On failure, requestSuccess is false and responseData contains { "error": { "errorCode": 106, "errorMessage": "..." } }.

GetDeckList #

Returns a flat list of all decks and their current status.

{ "requestName": "GetDeckList" }

responseData:

{
  "deckList": [
    { "deckName": "Main Deck", "deckId": "20230101120000000000001", "crc": "abc123", "status": true },
    { "deckName": "Hidden Deck", "deckId": "20230101120000000000002", "crc": "def456", "status": false }
  ]
}

status: true = enabled, status: false = disabled/hidden.

GetDeck #

Returns the full data for a single deck including all button properties. Accepts either deckId or deckName.

{
  "requestName": "GetDeck",
  "requestData": { "deckId": "20230101120000000000001" }
}

responseData:

{
  "deckData": {
    "deckId": "20230101120000000000001",
    "deckName": "Main Deck",
    "crc": "abc123",
    "status": true,
    "background_color": 4210752,
    "background_image": "my_bg.png",
    "background_image_crc": "xyz789",
    "grid_x": 10,
    "grid_y": 10,
    "snap_grid": true,
    "adaptive_resizing": true,
    "stretch": false,
    "sammi_version": "2024.1.0",
    "button_image_crcs": {
      "MyButton": "def456"
    },
    "button_list": [
      {
        "button_id": "MyButton",
        "text": "Play",
        "color": 3093194,
        "font_color": 16777215,
        "font_shadow": true,
        "border_color": 0,
        "border": 2,
        "x": 0.05,
        "y": 0.1,
        "width": 0.15,
        "height": 0.2,
        "image": "play.png",
        "stretch": false,
        "is_transparent": false,
        "overlappable": false,
        "persistent": true,
        "queueable": false,
        "payload": true,
        "press_type": 0,
        "group_id": "",
        "init_variable": "",
        "button_duration": 0,
        "release_duration": 0,
        "functions": 64,
        "triggers": []
      }
    ]
  }
}
  • Colors (color, font_color, border_color, background_color) are BGR decimal integers.
  • x, y, width, height are fractions of the deck’s display area (0.0–1.0). Multiply by the device’s screen size to get pixel coordinates.
  • command_list and release_list are stripped and never sent to Deck Apps.
  • background_image and button image are filenames only (no path). Fetch the actual image data with GetImage.

GetImage #

Returns a button or background image as a base64-encoded string.

{
  "requestName": "GetImage",
  "requestData": { "fileName": "my_button.png" }
}

responseData: { "fileName": "my_button.png", "imageData": "<base64>" }

Use the per-button CRC values from GetDeck to determine whether you need to re-fetch an image.

GetModifications #

Returns all currently active button modifications (temporary appearance overrides applied by “Modify Button” commands).

{ "requestName": "GetModifications" }

responseData:

{
  "modifications": {
    "MyButton": {
      "text": "LIVE",
      "color": 255,
      "font_color": 16777215,
      "border_color": 0,
      "border": 3,
      "font_shadow": true,
      "image": "live.png"
    }
  }
}

Only the fields that were actually overridden are present — unmodified properties are omitted.

GetOngoingButtons #

Returns all buttons that are currently executing (held down / still running their command list).

{ "requestName": "GetOngoingButtons" }

responseData:

{
  "buttons": [
    {
      "buttonId": "MyButton",
      "groupId": "",
      "overlappable": false,
      "releaseType": false,
      "duration": 0,
      "elapsedTime": 1240
    }
  ]
}
  • releaseType: true means this is a release-command execution (not the main press).
  • duration is the configured button duration in ms (0 = no fixed duration).
  • elapsedTime is milliseconds since the button started executing.

TriggerButton #

Triggers a button as if it were pressed.

{
  "requestName": "TriggerButton",
  "requestData": { "buttonId": "MyButton" }
}

ReleaseButton #

Sends the release signal to a held button.

{
  "requestName": "ReleaseButton",
  "requestData": { "buttonId": "MyButton" }
}

StopButton #

Force-stops a button that is currently executing.

{
  "requestName": "StopButton",
  "requestData": {
    "buttonId": "MyButton",
    "stopOngoing": true,
    "stopQueued": false,
    "stopAll": false
  }
}

Set stopAll: true to stop every running button regardless of buttonId.

BlockUnblockButton #

Toggles the block state of a button (blocked buttons ignore trigger events).

{
  "requestName": "BlockUnblockButton",
  "requestData": { "buttonId": "MyButton" }
}

ReinitButtonVars #

Resets a button’s local variables back to their initial values.

{
  "requestName": "ReinitButtonVars",
  "requestData": { "buttonId": "MyButton" }
}

GetDeckStatus #

Returns whether a specific deck is currently enabled or disabled.

{
  "requestName": "GetDeckStatus",
  "requestData": { "deckId": "20230101120000000000001" }
}

responseData: { "status": true }

ChangeDeckStatus #

Enables or disables a deck.

{
  "requestName": "ChangeDeckStatus",
  "requestData": { "deckId": "20230101120000000000001", "status": true }
}

status: true enables the deck, status: false disables it.

InputRequestReply #

Responds to a WaitForInput event (see Events below). The button that issued the wait command will unblock and receive this value.

{
  "requestName": "InputRequestReply",
  "requestData": {
    "requestId": 42,
    "input": "user typed value",
    "type": "waitForInput"
  }
}

type must be one of "waitForInput", "waitForChoice", or "waitForMultiChoice". Echo requestId exactly as received. It may arrive as a string or number depending on the client JSON parser. For waitForChoice, send the zero-based index of the selected choice as input, not true or false. For waitForMultiChoice, send a comma-separated string of selected values as input.

CustomMessage #

Sends a custom payload to SAMMI that fires a Deck App trigger. Buttons with a Deck App trigger type can listen for these.

{
  "requestName": "CustomMessage",
  "requestData": {
    "triggerName": "my_custom_event",
    "data": { "foo": "bar", "count": 3 }
  }
}

EditButton #

Opens the button command editor in SAMMI Core for a specific button.

{
  "requestName": "EditButton",
  "requestData": { "deckId": "20230101120000000000001", "buttonId": "MyButton" }
}

Events (SAMMI → Client) #

All events use op: 6:

{
  "op": 6,
  "data": {
    "eventType": "EventName",
    "eventData": { }
  }
}

Events Sent by SAMMI Commands #

These SAMMI Core commands send events to Deck App clients. If the command’s Client Name field is empty, SAMMI sends the event to every connected Deck App client; otherwise it sends only to the client whose Identify clientName matches. For broadcast SwitchDeck events, eventData.panelName is also an empty string; treat that as “for all clients” instead of filtering it out.

SAMMI command Event type Event data
Deck App: Switch Deck SwitchDeck panelName, deckID
Deck App: Switch Deck with Next Deck selected SwitchDeckNext panelName
Deck App: Switch Deck with Previous Deck selected SwitchDeckPrevious panelName
Deck App: Wait for Input WaitForInput commandName: "waitForInput", requestId, instanceId, buttonId, variableName, timeoutAfter, message, choices: "[]", defaultInput
Deck App: Wait for Choice WaitForInput commandName: "waitForChoice", requestId, instanceId, buttonId, variableName, timeoutAfter, message, choices: "[]", defaultInput: ""
Deck App: Wait for Multi Choice WaitForInput commandName: "waitForMultiChoice", requestId, instanceId, buttonId, variableName, timeoutAfter, message, choices, defaultInput
Deck App: Send JSON SendJSON event, json

For wait commands, target a single Client Name when possible. If a wait command is sent to all clients, every client receives the same requestId, but SAMMI only needs one InputRequestReply.

timeoutAfter is always intended to be a number of milliseconds. Use 0 for no timeout. A TriggerButton response with requestSuccess: true only confirms that SAMMI accepted the trigger request; the Deck App should still wait for the separate op: 6 event before showing a prompt.

Deck App: Switch Deck #

Sent when the command targets a specific deck:

{
  "eventType": "SwitchDeck",
  "eventData": {
    "panelName": "My Deck App",
    "deckID": "20230101120000000000001"
  }
}

Sent when the command targets the next or previous deck:

{ "eventType": "SwitchDeckNext", "eventData": { "panelName": "My Deck App" } }
{ "eventType": "SwitchDeckPrevious", "eventData": { "panelName": "My Deck App" } }

Expected from the Deck App: navigate to the requested deck, or to the next/previous enabled deck. No reply is expected. If the command’s Enable Deck option is checked, SAMMI Core may enable the deck before sending SwitchDeck.

Deck App: Wait for Input #

Sent to the Deck App:

{
  "eventType": "WaitForInput",
  "eventData": {
    "commandName": "waitForInput",
    "requestId": 42,
    "instanceId": 100123,
    "buttonId": "MyButton",
    "variableName": "inputResult",
    "message": "Enter a value",
    "choices": "[]",
    "defaultInput": "default text",
    "timeoutAfter": 15000
  }
}

Expected from the Deck App: show a text input prompt, then reply with the entered string. On cancel or timeout, the built-in Deck App replies with defaultInput.

{
  "op": 4,
  "data": {
    "requestName": "InputRequestReply",
    "requestData": {
      "requestId": 42,
      "input": "user typed value",
      "type": "waitForInput"
    }
  }
}
Deck App: Wait for Choice #

Sent to the Deck App:

{
  "eventType": "WaitForInput",
  "eventData": {
    "commandName": "waitForChoice",
    "requestId": 42,
    "instanceId": 100123,
    "buttonId": "MyButton",
    "variableName": "choiceResult",
    "message": "Continue?",
    "choices": "[]",
    "defaultInput": "",
    "timeoutAfter": 15000
  }
}

Expected from the Deck App: show a single-choice prompt. The built-in Deck App treats empty choices as Yes / No. Reply with the zero-based selected index as a number (0 for the first choice, 1 for the second), not a boolean. On cancel or timeout, the built-in Deck App replies with 0.

{
  "op": 4,
  "data": {
    "requestName": "InputRequestReply",
    "requestData": {
      "requestId": 42,
      "input": 0,
      "type": "waitForChoice"
    }
  }
}
Deck App: Wait for Multi Choice #

Sent to the Deck App:

{
  "eventType": "WaitForInput",
  "eventData": {
    "commandName": "waitForMultiChoice",
    "requestId": 42,
    "instanceId": 100123,
    "buttonId": "MyButton",
    "variableName": "choicesResult",
    "message": "Pick one or more colors",
    "choices": "[\"Red\",\"Green\",\"Blue\"]",
    "defaultInput": "Green",
    "timeoutAfter": 15000
  }
}

Expected from the Deck App: parse choices as a JSON array, show a multi-select prompt, and preselect defaultInput if it matches one of the choices. Reply with the selected values as a comma-separated string. On cancel or timeout, the built-in Deck App replies with an empty string.

SAMMI Core only sends this event after the command’s Choice Array Name resolves to a valid SAMMI array/list. If the array is missing or invalid, the command errors in SAMMI and no WaitForInput event is sent.

{
  "op": 4,
  "data": {
    "requestName": "InputRequestReply",
    "requestData": {
      "requestId": 42,
      "input": "Red,Blue",
      "type": "waitForMultiChoice"
    }
  }
}
Deck App: Send JSON #

Sent to the Deck App:

{
  "eventType": "SendJSON",
  "eventData": {
    "event": "score_update",
    "json": "{\"score\":42}"
  }
}

Expected from the Deck App: handle the custom event. No reply is expected. event is the Event Name box value, and json is the JSON Payload box value as a string. Parse json on the client if you need an object. The built-in Deck App also dispatches this as a SAMMISendJSON DOM CustomEvent on document, with this same eventData as event.detail.

ButtonTriggered #

Fired when a button starts executing.

{
  "eventType": "ButtonTriggered",
  "eventData": { "buttonId": "MyButton", "groupId": "", "overlappable": false, "duration": 0 }
}

ButtonEnded #

Fired when a button finishes executing.

{
  "eventType": "ButtonEnded",
  "eventData": { "buttonId": "MyButton", "groupId": "", "overlappable": false }
}

ReleaseEnded #

Fired when the release command list of a button finishes executing.

{
  "eventType": "ReleaseEnded",
  "eventData": { "buttonId": "MyButton", "groupId": "", "overlappable": false }
}

ButtonModified #

Fired when a button’s visual appearance is temporarily overridden (e.g. by a “Modify Button” command). Also fired when modifications are cleared (SAMMIReset).

{
  "eventType": "ButtonModified",
  "eventData": {
    "buttonId": "MyButton",
    "modifications": {
      "text": "LIVE",
      "color": 255,
      "font_color": 16777215,
      "border_color": 0,
      "border": 3,
      "font_shadow": true,
      "image": "live.png"
    }
  }
}

Only the fields that were actually overridden are present. An empty modifications object means all overrides have been cleared for that button.

DeckStatusChanged #

Fired when a deck is enabled or disabled.

{
  "eventType": "DeckStatusChanged",
  "eventData": { "deckId": "20230101120000000000001", "status": true }
}

DeckUpdated #

Fired when a deck’s configuration changes (e.g. after editing a button in SAMMI Core). The full updated deck structure is included so the app can refresh its view.

{
  "eventType": "DeckUpdated",
  "eventData": {
    "deckData": { }
  }
}

eventData.deckData has the same full structure as responseData.deckData from GetDeck.

SwitchDeck #

SAMMI Core is instructing this Deck App to navigate to a different deck. This is sent by the Deck App: Switch Deck command when a specific deck is selected. panelName is the command’s Client Name value and deckID is the selected deck’s unique ID.

{
  "eventType": "SwitchDeck",
  "eventData": { "panelName": "My Deck App", "deckID": "20230101120000000000001" }
}

SwitchDeckNext / SwitchDeckPrevious #

SAMMI Core is instructing the Deck App to navigate to the next or previous deck in the enabled deck list. These are sent by the Deck App: Switch Deck command when Next Deck or Previous Deck is selected.

{ "eventType": "SwitchDeckNext",     "eventData": { "panelName": "My Deck App" } }
{ "eventType": "SwitchDeckPrevious", "eventData": { "panelName": "My Deck App" } }

SAMMIReset #

Fired when SAMMI Core resets all button modifications. The Deck App should clear all stored overrides and redraw buttons in their default state.

{ "eventType": "SAMMIReset", "eventData": {} }

WaitForInput #

Fired by the Deck App: Wait for Input, Wait for Choice, or Wait for Multi Choice commands. The Deck App must show a UI prompt and reply with InputRequestReply to unblock the waiting button.

{
  "eventType": "WaitForInput",
  "eventData": {
    "commandName": "waitForChoice",
    "requestId": 42,
    "instanceId": 100123,
    "buttonId": "MyButton",
    "variableName": "choiceResult",
    "message": "Pick a colour",
    "choices": "[\"Red\",\"Green\",\"Blue\"]",
    "defaultInput": "",
    "timeoutAfter": 15000
  }
}
  • commandName"waitForInput", "waitForChoice", or "waitForMultiChoice"
  • requestId — echo this back in InputRequestReply
  • instanceId — the running button instance that is waiting for the reply
  • buttonId — the button that issued the wait command
  • variableName — the Save Variable As target in SAMMI
  • choices — JSON-encoded array of strings; sent as "[]" when no choice array is supplied
  • defaultInput — Default Text for waitForInput, Default Choice for waitForMultiChoice, or an empty string for waitForChoice
  • timeoutAfter — milliseconds before SAMMI times out and uses the default value (0 = no timeout)

If the user cancels or the timeout fires without a reply, the Deck App should still send InputRequestReply so the button can continue. The built-in Deck App replies with defaultInput for waitForInput, 0 for waitForChoice, and an empty string for waitForMultiChoice.

SendJSON #

Fired by the Deck App: Send JSON command. Delivers the JSON Payload box content to the Deck App.

{
  "eventType": "SendJSON",
  "eventData": { "event": "score_update", "json": "{\"score\":42}" }
}

json is sent as a string. SAMMI Panel dispatches this as a SAMMISendJSON DOM CustomEvent on document so third-party scripts embedded in the panel can react to it.