Add core MCP server implementation and Ross Talk protocol handler: ross-talk.ts
This commit is contained in:
parent
b9f387b53f
commit
0bb71d63bb
1 changed files with 242 additions and 0 deletions
242
src/ross-talk.ts
Normal file
242
src/ross-talk.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import { parseString as parseXML } from 'xml2js';
|
||||||
|
import { RossUltrixState, RossTalkConfig, PanelStatus, InputStatus } from './types.js';
|
||||||
|
|
||||||
|
export class RossTalkClient {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private config: RossTalkConfig;
|
||||||
|
private isConnected = false;
|
||||||
|
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||||
|
private heartbeatTimer: NodeJS.Timeout | null = null;
|
||||||
|
private onStateChange?: (state: Partial<RossUltrixState>) => void;
|
||||||
|
|
||||||
|
constructor(config: RossTalkConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.onStateChange = config.onStateChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHost(): string {
|
||||||
|
return this.config.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPort(): number {
|
||||||
|
return this.config.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
if (this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wsUrl = `ws://${this.config.host}:${this.config.port}/rosstalk`;
|
||||||
|
console.error(`Connecting to Ross Talk at ${wsUrl}`);
|
||||||
|
|
||||||
|
this.ws = new WebSocket(wsUrl, {
|
||||||
|
headers: {
|
||||||
|
'Sec-WebSocket-Protocol': 'rosstalk'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('Connection timeout'));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
this.ws.on('open', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.isConnected = true;
|
||||||
|
console.error('Connected to Ross Ultrix via Ross Talk');
|
||||||
|
this.startHeartbeat();
|
||||||
|
this.onStateChange?.({ connected: true });
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('error', (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
console.error('Ross Talk connection error:', error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('close', () => {
|
||||||
|
this.isConnected = false;
|
||||||
|
this.onStateChange?.({ connected: false });
|
||||||
|
console.error('Ross Talk connection closed');
|
||||||
|
this.scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ws.on('message', (data) => {
|
||||||
|
this.handleMessage(data.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect(): Promise<void> {
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearTimeout(this.heartbeatTimer);
|
||||||
|
this.heartbeatTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnected = false;
|
||||||
|
this.onStateChange?.({ connected: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleReconnect(): void {
|
||||||
|
if (this.reconnectTimer) return;
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
if (!this.isConnected) {
|
||||||
|
console.error('Attempting to reconnect to Ross Talk...');
|
||||||
|
this.connect().catch(console.error);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startHeartbeat(): void {
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.sendCommand('<heartbeat/>');
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage(message: string): void {
|
||||||
|
console.error('Received message:', message);
|
||||||
|
|
||||||
|
// Parse XML message from Ross Talk
|
||||||
|
parseXML(message, { explicitArray: false }, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Failed to parse Ross Talk message:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processRossTalkData(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processRossTalkData(data: any): void {
|
||||||
|
// Process different types of Ross Talk messages
|
||||||
|
if (data.status) {
|
||||||
|
// System status update
|
||||||
|
console.error('System status update:', data.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.panel) {
|
||||||
|
// Panel state update
|
||||||
|
const panelData = data.panel;
|
||||||
|
const panelStatus: PanelStatus = {
|
||||||
|
id: panelData.$.id || panelData.id,
|
||||||
|
active: panelData.active === 'true',
|
||||||
|
mixEffect: panelData.mixEffect,
|
||||||
|
lastUpdate: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onStateChange?.({
|
||||||
|
panels: {
|
||||||
|
[panelStatus.id]: panelStatus
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.input || data.source) {
|
||||||
|
// Input/Source state update
|
||||||
|
const inputData = data.input || data.source;
|
||||||
|
const inputStatus: InputStatus = {
|
||||||
|
id: inputData.$.id || inputData.id,
|
||||||
|
name: inputData.name || inputData.$.name,
|
||||||
|
live: inputData.live === 'true',
|
||||||
|
preview: inputData.preview === 'true',
|
||||||
|
lastUpdate: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onStateChange?.({
|
||||||
|
inputs: {
|
||||||
|
[inputStatus.id]: inputStatus
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendCommand(command: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
reject(new Error('Not connected to Ross Ultrix'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Sending command:', command);
|
||||||
|
this.ws.send(command);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSystemStatus(): Promise<any> {
|
||||||
|
await this.sendCommand('<status/>');
|
||||||
|
// In a real implementation, you'd wait for the response
|
||||||
|
return {
|
||||||
|
connected: this.isConnected,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchPanel(panelId: string): Promise<void> {
|
||||||
|
const command = `<panel id="${panelId}" action="switch"/>`;
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async takeTransition(meId: string): Promise<void> {
|
||||||
|
const command = `<me id="${meId}" action="take"/>`;
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async autoTransition(meId: string, duration: number): Promise<void> {
|
||||||
|
const command = `<me id="${meId}" action="auto" duration="${duration}"/>`;
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPreview(meId: string, sourceId: string): Promise<void> {
|
||||||
|
const command = `<me id="${meId}" preview="${sourceId}"/>`;
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setProgram(meId: string, sourceId: string): Promise<void> {
|
||||||
|
const command = `<me id="${meId}" program="${sourceId}"/>`;
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSources(): Promise<any[]> {
|
||||||
|
await this.sendCommand('<sources/>');
|
||||||
|
// In a real implementation, you'd wait for the response and parse it
|
||||||
|
return [
|
||||||
|
{ id: '1', name: 'Camera 1', type: 'camera' },
|
||||||
|
{ id: '2', name: 'Camera 2', type: 'camera' },
|
||||||
|
{ id: '3', name: 'Graphics', type: 'cg' },
|
||||||
|
{ id: '4', name: 'Media Player', type: 'media' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPanels(): Promise<any[]> {
|
||||||
|
await this.sendCommand('<panels/>');
|
||||||
|
// In a real implementation, you'd wait for the response and parse it
|
||||||
|
return [
|
||||||
|
{ id: 'ME1', name: 'Mix Effect 1', active: true },
|
||||||
|
{ id: 'ME2', name: 'Mix Effect 2', active: false },
|
||||||
|
{ id: 'AUX1', name: 'Auxiliary 1', active: false }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async runMacro(macroId: string): Promise<void> {
|
||||||
|
const command = `<macro id="${macroId}" action="run"/>`;
|
||||||
|
await this.sendCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue