Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions scripts/Probe-WaveLink.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#Requires -Version 5.1
[CmdletBinding()]
param(
[string[]]$Methods = @('getMixes', 'getOutputDevices', 'getInputDevices', 'getChannels', 'getApplicationInfo'),
[int]$ResponseTimeoutMs = 4000
)

$ErrorActionPreference = 'Stop'

$wsInfoPath = Join-Path $env:LOCALAPPDATA 'Packages\Elgato.WaveLink_g54w8ztgkx496\LocalState\ws-info.json'
if (-not (Test-Path $wsInfoPath)) {
throw "ws-info.json not found at $wsInfoPath. Is Wave Link running?"
}

$port = (Get-Content $wsInfoPath -Raw | ConvertFrom-Json).port
Write-Verbose "Using port $port from $wsInfoPath"

$ws = [System.Net.WebSockets.ClientWebSocket]::new()
$ws.Options.SetRequestHeader('Origin', 'streamdeck://')
$cts = [System.Threading.CancellationTokenSource]::new(5000)
$ws.ConnectAsync([Uri]"ws://127.0.0.1:$port", $cts.Token).Wait()
Write-Verbose "Connected (state: $($ws.State))"

function Send-Json {
param($Ws, $Object)
$json = $Object | ConvertTo-Json -Depth 10 -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
$seg = [System.ArraySegment[byte]]::new($bytes)
$Ws.SendAsync($seg, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).Wait()
}

function Receive-Json {
param($Ws, [int]$TimeoutMs)
$deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
$sb = [System.Text.StringBuilder]::new()
$buf = [byte[]]::new(8192)
while ([DateTime]::UtcNow -lt $deadline) {
$remain = [int]($deadline - [DateTime]::UtcNow).TotalMilliseconds
if ($remain -le 0) { break }
$cts2 = [System.Threading.CancellationTokenSource]::new($remain)
try {
$task = $Ws.ReceiveAsync([System.ArraySegment[byte]]::new($buf), $cts2.Token)
$task.Wait()
$r = $task.Result
$null = $sb.Append([System.Text.Encoding]::UTF8.GetString($buf, 0, $r.Count))
if ($r.EndOfMessage) { return $sb.ToString() }
} catch {
return $null
}
}
return $null
}

$id = 1
$results = [ordered]@{}

# Drain any unsolicited push frames first
while ($null -ne (Receive-Json -Ws $ws -TimeoutMs 250)) {}

foreach ($method in $Methods) {
$req = @{ jsonrpc = '2.0'; method = $method; id = $id }
Write-Host "--> $method (id=$id)" -ForegroundColor Cyan
Send-Json -Ws $ws -Object $req

$resp = $null
$deadline = [DateTime]::UtcNow.AddMilliseconds($ResponseTimeoutMs)
while ([DateTime]::UtcNow -lt $deadline -and $null -eq $resp) {
$msg = Receive-Json -Ws $ws -TimeoutMs 1000
if ($null -eq $msg) { continue }
try {
$obj = $msg | ConvertFrom-Json
if ($obj.id -eq $id) { $resp = $obj; break }
} catch {
Write-Warning "Non-JSON frame: $($msg.Substring(0, [Math]::Min(200, $msg.Length)))"
}
}

if ($null -eq $resp) {
Write-Host " (no response in ${ResponseTimeoutMs}ms)" -ForegroundColor Yellow
} else {
$results[$method] = $resp
$resp | ConvertTo-Json -Depth 10
}
$id++
}

$ws.CloseOutputAsync('NormalClosure', 'done', [System.Threading.CancellationToken]::None).Wait()

Write-Host "`n=== Summary ===" -ForegroundColor Green
foreach ($m in $results.Keys) {
$r = $results[$m]
if ($r.error) {
Write-Host "$m -> ERROR $($r.error.code): $($r.error.message)" -ForegroundColor Red
} else {
Write-Host "$m -> ok" -ForegroundColor Green
}
}
3 changes: 3 additions & 0 deletions src/Earmark.App/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<converters:SatisfiedGlyphConverter x:Key="SatisfiedGlyphConverter" />
<converters:SatisfiedBrushConverter x:Key="SatisfiedBrushConverter" />
<converters:SatisfiedTooltipConverter x:Key="SatisfiedTooltipConverter" />
<converters:WaveLinkStateBrushConverter x:Key="WaveLinkStateBrushConverter" />
<converters:WaveLinkStateGlyphConverter x:Key="WaveLinkStateGlyphConverter" />
<converters:VolumeFloatToPercentConverter x:Key="VolumeFloatToPercentConverter" />

<!-- Fluent 2 spacing scale (4px grid). -->
<x:Double x:Key="SpacingXSmall">4</x:Double>
Expand Down
50 changes: 50 additions & 0 deletions src/Earmark.App/Converters/Converters.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Earmark.Core.WaveLink;

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
Expand Down Expand Up @@ -72,6 +74,54 @@ public object ConvertBack(object value, Type targetType, object parameter, strin
throw new NotSupportedException();
}

public sealed class WaveLinkStateBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var key = value is WaveLinkConnectionState s ? s switch
{
WaveLinkConnectionState.Connected => "SystemFillColorSuccessBrush",
WaveLinkConnectionState.Unavailable => "SystemFillColorCriticalBrush",
_ => "TextFillColorTertiaryBrush",
} : "TextFillColorTertiaryBrush";

if (Application.Current.Resources.TryGetValue(key, out var brush) && brush is Brush b)
{
return b;
}

return new SolidColorBrush(Microsoft.UI.Colors.Gray);
}

public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
}

public sealed class WaveLinkStateGlyphConverter : IValueConverter
{
// Segoe Fluent Icons: CheckMark E73E, Warning E7BA, StatusCircleBlock F140.
public object Convert(object value, Type targetType, object parameter, string language) =>
value is WaveLinkConnectionState s ? s switch
{
WaveLinkConnectionState.Connected => "",
WaveLinkConnectionState.Unavailable => "",
_ => "",
} : "";

public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
}

public sealed class VolumeFloatToPercentConverter : IValueConverter
{
// Slider values are double; the rule action stores Volume as float in [0,1].
public object Convert(object value, Type targetType, object parameter, string language) =>
value is float f ? Math.Round(f * 100.0, 0) : 0.0;

public object ConvertBack(object value, Type targetType, object parameter, string language) =>
value is double d ? (float)Math.Clamp(d / 100.0, 0.0, 1.0) : 0f;
}

public sealed class EnumToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language) =>
Expand Down
14 changes: 14 additions & 0 deletions src/Earmark.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,27 @@
<TitleBar.IconSource>
<ImageIconSource ImageSource="ms-appx:///Assets/logo.png" />
</TitleBar.IconSource>
<TitleBar.LeftHeader>
<Button x:Name="PaneToggleButton"
Click="OnPaneToggleClick"
Background="Transparent"
BorderBrush="Transparent"
Padding="10,6"
MinWidth="40"
Height="32"
VerticalAlignment="Center"
ToolTipService.ToolTip="Toggle navigation">
<FontIcon Glyph="&#xE700;" FontSize="14" />
</Button>
</TitleBar.LeftHeader>
</TitleBar>

<NavigationView
x:Name="NavView"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsSettingsVisible="False"
IsPaneToggleButtonVisible="False"
PaneDisplayMode="Auto"
OpenPaneLength="220"
CompactModeThresholdWidth="640"
Expand Down
5 changes: 5 additions & 0 deletions src/Earmark.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelec
}
}

private void OnPaneToggleClick(object sender, RoutedEventArgs e)
{
NavView.IsPaneOpen = !NavView.IsPaneOpen;
}

private void NavigateTo(NavigationViewItem item)
{
if (item.Tag is not string tag)
Expand Down
Loading
Loading