scriptdeck.sdPlugin/Receive-StreamDeck.ps1
function Receive-StreamDeck { <# .Synopsis Receives messages from a StreamDeck .Description Receives websocket messages from a StreamDeck. This is used in StreamDeck plugin development. Unless -AsJob is passed, this function **This function will block until the StreamDeck webSocket closes.** Each message receive will be converted from JSON and outputted. Each message will also be transmitted as two events | SourceIdentifier | Example | |-----------------------|-------------------------| | StreamDeck.$EventName | StreamDeck.KeyDown | | $Action.$EventName | MyPlugin.KeyDown | These events can be handled using Register-EngineEvent, for example: Register-EngineEvent -SourceIdentifier MyPlugin.KeyDown -Action { "MyPlugin.KeyDown was pressed" | Out-Gridview } .Link Send-StreamDeck .Link https://developer.elgato.com/documentation/stream-deck/sdk/events-received/ #> param( # The registration event. This is used in plugin registration. [Parameter(ValueFromPipelineByPropertyName)] [string] $RegisterEvent, # The plugin UUID. This is used in plugin registration. [Parameter(ValueFromPipelineByPropertyName)] [string] $PluginUUID, # The port. [Parameter(ValueFromPipelineByPropertyName)] [int] $Port, # The web socket. # If not provided, the $GLOBAL:STREAMDECK_WEBSOCKET will be used. # If $GLOBAL:STREAMDECK_WEBSOCKET has not been set, one will be created using -Port. [Parameter(ValueFromPipelineByPropertyName)] [Net.WebSockets.ClientWebSocket] $Websocket, # The buffer size for received messages. By default: 16 kilobytes. [int]$BufferSize = 16kb, # The maximum amount of time to wait for a WebSocket to open. By default, 30 seconds. [Parameter(ValueFromPipelineByPropertyName)] [Timespan] $WaitFor = '00:00:30', # The interval to wait while receiving a message. By default, 17 milliseconds. [TimeSpan]$WaitInterval = '00:00:00.017', # The output type. Default is 'Data' Can be: # * Data (the JSON event data) # * Event (the PowerShell events) # * Message (the JSON message) # * None [ValidateSet('Data', 'Event','Message','None')] [string]$OutputType = 'Data', # If set, will watch the streamdeck in a background job. [switch] $AsJob ) begin { if (-not $Global:STREAMDECK_DEVICES) { $Global:STREAMDECK_DEVICES = @{} } if (-not $Global:STREAMDECK_BUTTONS) { $Global:STREAMDECK_BUTTONS = @{} } if (-not $Global:STREAMDECK_SETTINGS) { $Global:STREAMDECK_SETTINGS = @{} } } process { if ($AsJob) { $myParams = @{} + $PSBoundParameters $myParams.Remove('AsJob') $myDef = [ScriptBlock]::Create(@" param([Hashtable]`$params) function $($MyInvocation.InvocationName) { $($MyInvocation.MyCommand.ScriptBlock) } $($MyInvocation.InvocationName) @params "@) Start-Job -Name $MyInvocation.MyCommand.Name -ArgumentList $myParams -ScriptBlock $myDef return } if (-not $Websocket -and $Global:STREAMDECK_WEBSOCKET -and $Global:STREAMDECK_WEBSOCKET.State -ne 'Closed') { $Websocket = $Global:STREAMDECK_WEBSOCKET } if ($Websocket.State -like 'Close*' -or $Websocket.State -eq 'Aborted') { $Websocket = $null } if (-not $Websocket -and $Port) { $global:STREAMDECK_WEBSOCKET = $Websocket = [Net.WebSockets.ClientWebSocket]::new() $ConnectTask = $Websocket.ConnectAsync("ws://$([IPAddress]::Loopback):$port", [Threading.CancellationToken]::new($false)) $maxWaitTime = [DateTime]::Now + $WaitFor while (!$ConnectTask.IsCompleted -and [DateTime]::Now -lt $maxWaitTime) { Start-Sleep -Milliseconds $WaitInterval.TotalMilliseconds } } if (-not $Websocket) { Write-Error "Must provide a -WebSocket or -Port" -ErrorId WebSocket.Missing -Category ConnectionError return } $forwardedEvents = @{} Get-EventSubscriber | Where-Object ForwardEvent | ForEach-Object { $forwardedEvents[$_.SourceIdentifier] = $_ } if ($PluginUUID -and $RegisterEvent) { $PayloadJson = @{event=$RegisterEvent;uuid=$PluginUUID} | ConvertTo-Json -Depth 100 $SendSegment = [ArraySegment[Byte]]::new([Text.Encoding]::UTF8.GetBytes($PayloadJson)) $SendTask = $Websocket.SendAsync($SendSegment, 'Binary', $true, [Threading.CancellationToken]::new($false)) while (-not $SendTask.IsCompleted) { Start-Sleep -Milliseconds $WaitInterval.TotalMilliseconds } } try { while ($true) { # Create a buffer for the response $buffer = [byte[]]::new($BufferSize) $receiveSegment = [ArraySegment[byte]]::new($buffer) if (!($Websocket.State -eq 'Open')) { throw 'Websocket is not open anymore. {0}' -f $Websocket.State } # Receive the next response from the WebSocket, $receiveTask = $Websocket.ReceiveAsync($receiveSegment, [Threading.CancellationToken]::new($false)) # then wait for it to complete. while (-not $receiveTask.IsCompleted) { Start-Sleep -Milliseconds $WaitInterval.TotalMilliseconds } $streamDeckMsg = # Get the response and trim with extreme prejudice. [Text.Encoding]::UTF8.GetString($buffer, 0, $receiveTask.Result.Count).Trim() -replace '\s+$' if ($OutputType -eq 'Message') { $streamDeckMsg } # Convert the response from JSON $streamDeckData = try { $streamDeckMsg | ConvertFrom-Json -ErrorAction SilentlyContinue } catch { $null } if ($streamDeckData.event -eq 'deviceDidConnect') { # Add-Member -InputObject $streamDeckData.deviceInfo -Name DeviceID -Value $streamDeckData.device $Global:STREAMDECK_DEVICES[$streamDeckData.device] = [PSCustomObject][Ordered]@{ Name=$streamDeckData.deviceInfo.name DeviceID = $streamDeckData.device Size = [PSCustomObject][Ordered]@{ Columns = $streamDeckData.device.size.columns Rows = $streamDeckData.device.size.rows } Type = $streamDeckData.type } } elseif ($streamDeckData.event -eq 'deviceDidDisconnect') { $Global:STREAMDECK_DEVICES.Remove($streamDeckData.device) } if ($streamDeckData.event -eq 'WillAppear') { $coordinates = $streamDeckData.payload.coordinates $ctx = "$( $streamDeckData.device )@$($coordinates.Column),$($coordinates.Row)" $Global:STREAMDECK_BUTTONS[$ctx] = [PSCustomObject][Ordered]@{ Row = $streamDeckData.payload.coordinates.row Column = $streamDeckData.payload.coordinates.column Settings = $streamDeckData.payload.settings Key = $ctx Context = $streamDeckData.context State = $streamDeckData.payload.state IsInMultiAction = $streamDeckData.payload.isInMultiAction Device = $Global:STREAMDECK_DEVICES[$streamDeckData.device] } if ($VerbosePreference -ne 'silentlycontinue') { Write-Verbose ($Global:STREAMDECK_BUTTONS[$ctx] | Out-String) } } elseif ($streamDeckData.event -eq 'WillDisappear') { $coordinates = $streamDeckData.payload.coordinates $ctx = "$( $streamDeckData.device )@$($coordinates.Column),$($coordinates.Row)" $Global:STREAMDECK_BUTTONS.Remove($ctx) } if ($streamDeckData.Context) { $Global:STREAMDECK_CONTEXT = $streamDeckData.Context if ($streamDeckData.event -eq 'willAppear') { # WillAppear events tell us when a given button has been mapped. } } if ($OutputType -eq 'Data') { $streamDeckData} $streamDeckEvent = @( $sid = "StreamDeck.$($streamDeckData.event)" if (-not $forwardedEvents[$sid]) { $forwardedEvents[$sid] = Register-EngineEvent -SourceIdentifier $sid -Forward } New-Event -SourceIdentifier $sid -MessageData $streamDeckData if ($streamDeckData.action) { $actionSid = "$($streamDeckData.action).$($streamDeckData.event)" if (-not $forwardedEvents[$actionSid]) { $forwardedEvents[$sid] = Register-EngineEvent -SourceIdentifier $actionSid -Forward } $coordinates = $streamDeckData.payload.coordinates $ctx = "$($streamDeckData.device)@$($coordinates.Column),$($coordinates.Row)" New-Event -SourceIdentifier "$($streamDeckData.action).$($streamDeckData.event)" -MessageData $streamDeckData -Sender $Global:STREAMDECK_BUTTONS[$ctx] } ) if ($OutputType -eq 'Event') { $streamDeckEvent} $buffer.Clear() } } catch { Write-Error -Exception $_.Exception -Message "StreamDeck Exception: $($_ | Out-String)" -ErrorId "WebSocket.State.$($Websocket.State)" } } } |