diff --git a/src/actions/gpu-graph.ts b/src/actions/gpu-graph.ts index cb0ff5c..e56ea0a 100644 --- a/src/actions/gpu-graph.ts +++ b/src/actions/gpu-graph.ts @@ -1 +1,72 @@ -export {}; +import streamDeck, { + action, + type DidReceiveSettingsEvent, + type KeyAction, + type KeyDownEvent, + SingletonAction, + type WillAppearEvent, +} from "@elgato/streamdeck"; +import { type GpuMetricKind } from "../lib/metrics-parser"; +import { renderGpuGraph, svgDataUrl } from "../lib/render"; +import { runtime } from "../lib/runtime"; +import { cfgFromSettings, type CfgSettings } from "../lib/util"; + +type GpuSettings = CfgSettings & { + gpuId?: string; + metric?: GpuMetricKind; +}; + +@action({ UUID: "com.bryce.llamawatch.gpu" }) +export class GpuGraph extends SingletonAction { + private settings: GpuSettings = {}; + private action?: KeyAction; + private unsubscribe?: () => void; + private lastSvg?: string; + + override onWillAppear(ev: WillAppearEvent): void { + if (!ev.action.isKey()) return; + this.settings = ev.payload.settings; + this.action = ev.action; + runtime.ensureConnections(cfgFromSettings(this.settings)); + this.unsubscribe = runtime.subscribe(() => this.render()); + this.render(); + } + + override onWillDisappear(): void { + this.unsubscribe?.(); + this.unsubscribe = undefined; + this.action = undefined; + } + + override onDidReceiveSettings(ev: DidReceiveSettingsEvent): void { + if (!ev.action.isKey()) return; + this.settings = ev.payload.settings; + this.action = ev.action; + this.render(); + } + + override onKeyDown(ev: KeyDownEvent): void { + void streamDeck.system.openUrl(`${cfgFromSettings(ev.payload.settings).baseUrl}/ui`); + } + + private render(): void { + const action = this.action; + if (!action) return; + const poller = runtime.pollerInstance; + const gpuId = this.settings.gpuId ?? "all"; + const metric = this.settings.metric ?? "util_percent"; + const value = poller?.getValue(gpuId, metric); + const history = poller?.getHistory(gpuId, metric) ?? []; + const offline = !poller || poller.isOffline(); + const gpuName = gpuId === "all" ? "ALL GPUS" : this.gpuLabel(gpuId); + const svg = renderGpuGraph({ gpuName, metric, value, history, offline }); + if (svg === this.lastSvg) return; + this.lastSvg = svg; + void action.setImage(svgDataUrl(svg)); + } + + private gpuLabel(gpuId: string): string { + const gpu = runtime.pollerInstance?.gpus().find((g) => g.id === gpuId); + return gpu && gpu.name ? `GPU ${gpuId} ยท ${gpu.name}` : `GPU ${gpuId}`; + } +} diff --git a/src/actions/inflight-monitor.ts b/src/actions/inflight-monitor.ts index cb0ff5c..c39eb70 100644 --- a/src/actions/inflight-monitor.ts +++ b/src/actions/inflight-monitor.ts @@ -1 +1,87 @@ -export {}; +import streamDeck, { + action, + type DidReceiveSettingsEvent, + type KeyAction, + type KeyDownEvent, + SingletonAction, + type WillAppearEvent, +} from "@elgato/streamdeck"; +import { renderInflight, svgDataUrl } from "../lib/render"; +import { runtime } from "../lib/runtime"; +import { cfgFromSettings, type CfgSettings } from "../lib/util"; + +type InflightSettings = CfgSettings & { + modelId?: string; +}; + +@action({ UUID: "com.bryce.llamawatch.inflight" }) +export class InflightMonitor extends SingletonAction { + private settings: InflightSettings = {}; + private action?: KeyAction; + private unsubscribe?: () => void; + private pulseTimer?: ReturnType; + private pulse = false; + + override onWillAppear(ev: WillAppearEvent): void { + if (!ev.action.isKey()) return; + this.settings = ev.payload.settings; + this.action = ev.action; + runtime.ensureConnections(cfgFromSettings(this.settings)); + this.unsubscribe = runtime.subscribe(() => this.render()); + this.render(); + } + + override onWillDisappear(): void { + this.unsubscribe?.(); + this.unsubscribe = undefined; + this.action = undefined; + this.clearPulse(); + } + + override onDidReceiveSettings(ev: DidReceiveSettingsEvent): void { + if (!ev.action.isKey()) return; + this.settings = ev.payload.settings; + this.action = ev.action; + runtime.ensureConnections(cfgFromSettings(this.settings)); + this.render(); + } + + override onKeyDown(ev: KeyDownEvent): void { + void streamDeck.system.openUrl(`${cfgFromSettings(ev.payload.settings).baseUrl}/ui`); + } + + private render(): void { + const action = this.action; + if (!action) return; + const modelId = this.settings.modelId ?? ""; + const state = runtime.tracker.state(modelId); + const count = runtime.tracker.count(modelId); + const offline = runtime.offline || modelId.length === 0; + void action.setImage( + svgDataUrl( + renderInflight({ + modelName: modelId.length > 0 ? modelId : "unset", + state, + count, + offline, + pulse: this.pulse, + }), + ), + ); + + if (state === "ready" && count > 0 && !this.pulseTimer) { + this.pulseTimer = setInterval(() => { + this.pulse = !this.pulse; + this.render(); + }, 500); + } else if (!(state === "ready" && count > 0) && this.pulseTimer) { + this.clearPulse(); + } + } + + private clearPulse(): void { + if (this.pulseTimer) clearInterval(this.pulseTimer); + this.pulseTimer = undefined; + this.pulse = false; + } +}