diff --git a/src/codes.ts b/src/codes.ts index c207028..7244629 100644 --- a/src/codes.ts +++ b/src/codes.ts @@ -2,46 +2,49 @@ export type ResponseCode = ErrorCode | SynchronousCode | AsynchronousCode export enum ErrorCode { - SyntaxError = 100, - UnsupportedParameter = 101, - InvalidValue = 102, - Unsupported = 103, - DiskFull = 104, - NoDisk = 105, - DiskError = 106, - TimelineEmpty = 107, - InternalError = 108, - OutOfRange = 109, - NoInput = 110, - RemoteControlDisabled = 111, - ConnectionRejected = 120, - InvalidState = 150, - InvalidCodec = 151, - InvalidFormat = 160, - InvalidToken = 161, - FormatNotPrepared = 162, + SyntaxError = 100, + UnsupportedParameter = 101, + InvalidValue = 102, + Unsupported = 103, + DiskFull = 104, + NoDisk = 105, + DiskError = 106, + TimelineEmpty = 107, + InternalError = 108, + OutOfRange = 109, + NoInput = 110, + RemoteControlDisabled = 111, + ConnectionRejected = 120, + InvalidState = 150, + InvalidCodec = 151, + InvalidFormat = 160, + InvalidToken = 161, + FormatNotPrepared = 162 } export enum SynchronousCode { - OK = 200, - Notify = 209, + OK = 200, + DeviceInfo = 204, + TransportInfo = 208, + Notify = 209 } export enum AsynchronousCode { - ConnectionInfo = 500, + ConnectionInfo = 500, + TransportInfo = 508 } export enum ResponseCodeType { - Unknown, - Error, - Synchronous, - Asynchronous, + Unknown, + Error, + Synchronous, + Asynchronous } export function GetResponseCodeType (val: ResponseCode): ResponseCodeType { - if (val >= 100 && val <= 199) return ResponseCodeType.Error - if (val >= 200 && val <= 299) return ResponseCodeType.Synchronous - if (val >= 500 && val <= 599) return ResponseCodeType.Asynchronous + if (val >= 100 && val <= 199) return ResponseCodeType.Error + if (val >= 200 && val <= 299) return ResponseCodeType.Synchronous + if (val >= 500 && val <= 599) return ResponseCodeType.Asynchronous - return ResponseCodeType.Unknown + return ResponseCodeType.Unknown } diff --git a/src/commands/abstractCommand.ts b/src/commands/abstractCommand.ts index da710ef..fba065c 100644 --- a/src/commands/abstractCommand.ts +++ b/src/commands/abstractCommand.ts @@ -8,68 +8,68 @@ export interface ErrorResponse extends ResponseMessage { // } export interface AbstractCommand { - expectedResponseCode: ResponseCode + expectedResponseCode: ResponseCode - handle(msg: ResponseMessage) + handle (msg: ResponseMessage) - //deserialize(msg: ResponseMessage): IResponse - serialize(): NamedMessage | null + // deserialize(msg: ResponseMessage): IResponse + serialize (): NamedMessage | null - markSent() + markSent () } export abstract class AbstractCommandBase implements Promise, AbstractCommand { - abstract expectedResponseCode: ResponseCode - - handle(msg: ResponseMessage) { - if (msg.Code === this.expectedResponseCode) { - this.resolve(this.deserialize(msg)) - } else { - this.reject(msg) - } - } - - abstract deserialize(msg: ResponseMessage): T - abstract serialize(): NamedMessage | null - - private _promise: Promise - protected resolve: (res: T) => void - protected reject: (res: ErrorResponse) => void - - constructor() { - // TODO - can this be done any cleaner? - this._promise = new Promise((resolve, reject) => { - this.resolve = resolve - this.reject = reject - }) - } - - then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { - return this._promise.then(onfulfilled, onrejected) - } - - catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { - return this._promise.catch(onrejected) - } - - [Symbol.toStringTag] // TODO what is this?? - - markSent() { - // TODO - track time sent - } + abstract expectedResponseCode: ResponseCode + + handle (msg: ResponseMessage) { + if (msg.Code === this.expectedResponseCode) { + this.resolve(this.deserialize(msg)) +} else { + this.reject(msg) +} +} + + abstract deserialize (msg: ResponseMessage): T + abstract serialize (): NamedMessage | null + + private _promise: Promise + protected resolve: (res: T) => void + protected reject: (res: ErrorResponse) => void + + constructor () { + // TODO - can this be done any cleaner? + this._promise = new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject +}) } -export abstract class AbstractCommandBaseNoResponse extends AbstractCommandBase{ - expectedResponseCode = SynchronousCode.OK + then (onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { + return this._promise.then(onfulfilled, onrejected) +} - // markSent() { - // super.markSent() - // // No response will be received, so resolve the promise now - // this.resolve(true) - // } + catch (onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise { + return this._promise.catch(onrejected) +} - deserialize(msg: ResponseMessage) { - msg.Code - return true - } + [Symbol.toStringTag] // TODO what is this?? + + markSent () { + // TODO - track time sent +} +} + +export abstract class AbstractCommandBaseNoResponse extends AbstractCommandBase { + expectedResponseCode = SynchronousCode.OK + + // markSent() { + // super.markSent() + // // No response will be received, so resolve the promise now + // this.resolve(true) + // } + + deserialize (msg: ResponseMessage) { + msg.Code + return true +} } diff --git a/src/commands/connect.ts b/src/commands/connect.ts index 7a7febc..7efa66b 100644 --- a/src/commands/connect.ts +++ b/src/commands/connect.ts @@ -3,23 +3,23 @@ import { ResponseMessage } from '../message' import { AbstractCommandBase } from './abstractCommand' export interface ConnectionInfoResponse { - ProtocolVersion: number - Model: string + ProtocolVersion: number + Model: string } // Purpose of this is to emit the connect event with the connectionInfo export class DummyConnectCommand extends AbstractCommandBase { - expectedResponseCode = AsynchronousCode.ConnectionInfo + expectedResponseCode = AsynchronousCode.ConnectionInfo - deserialize (msg: ResponseMessage) { - const res: ConnectionInfoResponse = { - ProtocolVersion: parseFloat(msg.Params['protocol version']), - Model: msg.Params['model'], - } - return res - } - serialize () { - // Nothing to send - return null - } -} \ No newline at end of file + deserialize (msg: ResponseMessage) { + const res: ConnectionInfoResponse = { + ProtocolVersion: parseFloat(msg.Params['protocol version']), + Model: msg.Params['model'] +} + return res +} + serialize () { + // Nothing to send + return null +} +} diff --git a/src/commands/deviceInfo.ts b/src/commands/deviceInfo.ts index 057fd12..0eda7e2 100644 --- a/src/commands/deviceInfo.ts +++ b/src/commands/deviceInfo.ts @@ -3,28 +3,28 @@ import { ResponseMessage, NamedMessage } from '../message' import { AbstractCommandBase } from './abstractCommand' export interface DeviceInfoCommandResponse { - ProtocolVersion: number - Model: string - UniqueId: string + ProtocolVersion: number + Model: string + UniqueId: string } export class DeviceInfoCommand extends AbstractCommandBase { - expectedResponseCode = SynchronousCode.Notify + expectedResponseCode = SynchronousCode.DeviceInfo - deserialize (msg: ResponseMessage) { - const res: DeviceInfoCommandResponse = { - ProtocolVersion: parseFloat(msg.Params['protocol version']), - Model: msg.Params['model'], - UniqueId: msg.Params['unique id'], - } - return res - } - serialize () { - const res: NamedMessage = { - Name: 'device info', - Params: {} - } + deserialize (msg: ResponseMessage) { + const res: DeviceInfoCommandResponse = { + ProtocolVersion: parseFloat(msg.Params['protocol version']), + Model: msg.Params['model'], + UniqueId: msg.Params['unique id'] +} + return res +} + serialize () { + const res: NamedMessage = { + Name: 'device info', + Params: {} +} - return res - } + return res +} } diff --git a/src/commands/index.ts b/src/commands/index.ts index a6c29e9..2d61a20 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -5,3 +5,4 @@ export * from './deviceInfo' export * from './notify' export * from './record' export * from './stop' +export * from './transportInfo' diff --git a/src/commands/record.ts b/src/commands/record.ts index cfcb464..7cec58f 100644 --- a/src/commands/record.ts +++ b/src/commands/record.ts @@ -10,7 +10,7 @@ export class RecordCommand extends AbstractCommandBaseNoResponse { Params: {} } - if (this.Filename) res.Params.Name = this.Filename + if (this.Filename) res.Params.name = this.Filename return res } diff --git a/src/commands/transportInfo.ts b/src/commands/transportInfo.ts new file mode 100644 index 0000000..1829eee --- /dev/null +++ b/src/commands/transportInfo.ts @@ -0,0 +1,74 @@ +import { SynchronousCode } from '../codes' +import { TransportStatus, SlotId, VideoFormat } from '../enums' +import { ResponseMessage, NamedMessage } from '../message' +import { AbstractCommandBase } from './abstractCommand' +import { parseIdOrNone, parseIntIfDefined, parseBool } from './util' + +export interface TransportInfoCommandResponse { + Status: TransportStatus + Speed: number + SlotId: SlotId | null + ClipId: number | null + SingleClip: boolean + DisplayTimecode: string + Timecode: string + VideoFormat: VideoFormat + Loop: boolean +} + +export class TransportInfoCommand extends AbstractCommandBase { + expectedResponseCode = SynchronousCode.TransportInfo + + deserialize (msg: ResponseMessage) { + const res: TransportInfoCommandResponse = { + Status: msg.Params['status'] as TransportStatus, + Speed: parseInt(msg.Params['speed']), + SlotId: parseIdOrNone(msg.Params['slot id']) || null, + ClipId: parseIdOrNone(msg.Params['clip id']) || null, + SingleClip: parseBool(msg.Params['single clip']) || false, + DisplayTimecode: msg.Params['display timecode'], + Timecode: msg.Params['timecode'], + VideoFormat: msg.Params['video format'] as VideoFormat, + Loop: parseBool(msg.Params['loop']) || false, + } + return res + } + serialize () { + const res: NamedMessage = { + Name: 'transport info', + Params: {} + } + + return res + } +} + +export interface TransportInfoChangeResponse { + Status: TransportStatus | undefined + Speed: number | undefined + SlotId: SlotId | null | undefined + ClipId: number | null | undefined + SingleClip: boolean | undefined + DisplayTimecode: string | undefined + Timecode: string | undefined + VideoFormat: VideoFormat | undefined + Loop: boolean | undefined +} + +export class TransportInfoChange { + + deserialize (msg: ResponseMessage) { + const res: TransportInfoChangeResponse = { + Status: msg.Params['status'] as TransportStatus, + Speed: parseIntIfDefined(msg.Params['speed']), + SlotId: parseIdOrNone(msg.Params['slot id']), + ClipId: parseIdOrNone(msg.Params['clip id']), + SingleClip: parseBool(msg.Params['single clip']), + DisplayTimecode: msg.Params['display timecode'], + Timecode: msg.Params['timecode'], + VideoFormat: msg.Params['video format'] as VideoFormat, + Loop: parseBool(msg.Params['loop']), + } + return res + } +} diff --git a/src/commands/util.ts b/src/commands/util.ts index 16cee38..dafe470 100644 --- a/src/commands/util.ts +++ b/src/commands/util.ts @@ -1,9 +1,25 @@ import { NamedMessage } from '../message' export function SetBoolIfDefined(msg: NamedMessage, name: string, val: boolean | undefined) { - if (val !== undefined) { - msg.Params[name] = val ? 'true' : 'false' - } + if (val !== undefined) { + msg.Params[name] = val ? 'true' : 'false' +} +} + +export function literal (o: T) { return o } + +export function parseIdOrNone (str: string): number | null | undefined { + if (str === undefined) return undefined + if (str === 'none') return null + return parseInt(str) } -export function literal (o: T) { return o } \ No newline at end of file +export function parseIntIfDefined(str: string | undefined): number | undefined { + if (str === undefined) return undefined + return parseInt(str) +} + +export function parseBool(str: string | undefined): boolean | undefined { + if (str === undefined) return undefined + return str === 'true' +} diff --git a/src/enums.ts b/src/enums.ts new file mode 100644 index 0000000..cf7a6f7 --- /dev/null +++ b/src/enums.ts @@ -0,0 +1,15 @@ +export enum TransportStatus { + Preview = 'preview', + Record = 'record' + // TODO +} + +export enum SlotId { + SlotOne = 1, + SlotTwo = 2 +} + +export enum VideoFormat { + _1080i50 = '1080i50' + // TODO +} diff --git a/src/hyperdeck.ts b/src/hyperdeck.ts index 4714a53..1a7516a 100644 --- a/src/hyperdeck.ts +++ b/src/hyperdeck.ts @@ -2,8 +2,8 @@ import { EventEmitter } from 'events' import { Socket } from 'net' import * as _ from 'underscore' -import { ResponseCodeType, GetResponseCodeType } from './codes' -import { AbstractCommand } from './commands' +import { ResponseCodeType, GetResponseCodeType, AsynchronousCode } from './codes' +import { AbstractCommand, TransportInfoChange } from './commands' import { ResponseMessage, NamedMessage } from './message' import { DummyConnectCommand } from './commands/connect' @@ -197,7 +197,14 @@ export class Hyperdeck extends EventEmitter { return } + if (this.DEBUG) this._log('res', resMsg) + const codeIsAsync = codeType === ResponseCodeType.Asynchronous + if (codeIsAsync) { + this._handleAsyncResponse(resMsg) + // leave it to fall through in case the queued command is waiting for an async response + } + if (this._commandQueue.length > 0 && (!codeIsAsync || this._commandQueue[0].expectedResponseCode === code)) { // this belongs to the command, so handle it const cmd = this._commandQueue[0] @@ -205,13 +212,22 @@ export class Hyperdeck extends EventEmitter { cmd.handle(resMsg) this._sendQueuedCommand() - return } + } - - - // TODO - this._log('res', resMsg) - + private _handleAsyncResponse(msg: ResponseMessage) { + // TODO - refactor to pick the handler dynamically + switch (msg.Code) { + case AsynchronousCode.TransportInfo: + { + const handler = new TransportInfoChange() + const r = handler.deserialize(msg) + this.emit('transportInfo', r) + } + break + default: + this._log('unknown async response:', msg) + break + } } } diff --git a/src/message.ts b/src/message.ts index 4df78b0..a6a7ecc 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,9 +1,9 @@ import { ResponseCode } from './codes' export interface NamedMessage { - Name: string - Params: { [key: string]: string } + Name: string + Params: { [key: string]: string } } export interface ResponseMessage extends NamedMessage { - Code: ResponseCode -} \ No newline at end of file + Code: ResponseCode +}