Skip to content

Commit

Permalink
fix: Allow (and silently discard) "z0 38 FF" Network Change Reply mes…
Browse files Browse the repository at this point in the history
…sages from (non-PTZOptics) cameras that send them. (#51)
  • Loading branch information
jswalden committed May 25, 2024
1 parent 931c29e commit 837401b
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/visca/__tests__/ack-without-pending-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ACK, FocusModeInquiryBytes } from './camera-interactions/bytes.js'
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
InquiryFailedFatally,
SendInquiry,
} from './camera-interactions/interactions.js'
Expand All @@ -15,6 +16,7 @@ describe('ACK without pending command', () => {
test('ACK with only inquiry pending', async () => {
return RunCameraInteractionTest(
[
CameraReplyNetworkChange([0xf0, 0x38, 0xff]), // not essential to this test: randomly added to tests
SendInquiry(FocusModeInquiry, 'focus-mode'),
CameraExpectIncomingBytes(FocusModeInquiryBytes), // focus-mode
CameraReplyBytes(ACK(1)), // focus-mode
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/bad-inquiry-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ExposureModeInquiryBytes } from './camera-interactions/bytes.js'
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
InquiryFailed,
SendInquiry,
} from './camera-interactions/interactions.js'
Expand Down Expand Up @@ -68,6 +69,7 @@ describe('inquiry response mismatch', () => {
[
SendInquiry(ZoomPositionInquiry, 'zoom-position'),
CameraExpectIncomingBytes(ZoomPositionInquiryBytes), // zoom-position
CameraReplyNetworkChange([0xc0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(MaskMismatchResponseBytes), // zoom-position
InquiryFailed(
[InquiryResponseIncompatibleMatcher, MatchVISCABytes(MaskMismatchResponseBytes), UserDefinedInquiryMatcher],
Expand Down
24 changes: 24 additions & 0 deletions src/visca/__tests__/camera-interactions/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,30 @@ export function CameraExpectIncomingBytes(bytes: readonly number[]): CameraIncom
return { type: 'camera-expect-incoming-bytes', bytes }
}

/**
* The lead byte of a network change reply: `z0` where `z` is a nibble whose
* high bit is set.
*/
type NetworkChangeFirstByte = 0x80 | 0x90 | 0xa0 | 0xb0 | 0xc0 | 0xd0 | 0xe0 | 0xf0

/**
* Make the "camera" send a Network Change Reply identifying as the desired
* camera.
*
* These messages aren't sent by PTZOptics cameras, but some non-PTZOptics
* cameras send them (despite that they serve no purpose with VISCA over IP).
* This reply can be sprinkled pretty much anywhere in the stream of camera
* replies, and the module should simply skip them. (Note that if you want to
* guarantee the skipping happens, you have to ensure that subsequent bytes sent
* by the camera are depended upon in some fashion.)
*
* @param bytes
* A network change reply sequence: `z0 38 FF` where `z = Device address + 8`.
*/
export function CameraReplyNetworkChange(bytes: readonly [NetworkChangeFirstByte, 0x38, 0xff]): CameraReply {
return { type: 'camera-reply', bytes: new Uint8Array(bytes) }
}

/** Make the "camera" reply with the given bytes. */
export function CameraReplyBytes(bytes: readonly number[]): CameraReply {
return { type: 'camera-reply', bytes: new Uint8Array(bytes) }
Expand Down
3 changes: 1 addition & 2 deletions src/visca/__tests__/camera-interactions/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export function CompletionInEmptySocketMatcher(y: number): RegExp {
return new RegExp(`^Received Completion for socket ${y}, but no command is executing in it`)
}

export const BadReturnStartByteMatcher =
/^Error in camera response: return message data doesn't start with 0x90 \(VISCA bytes \[[0-9A-Z]{2}(?: [0-9A-Z]{2})*\]\)$/
export const BadReturnStartByteMatcher = /^Camera sent return message data not starting with z0 \(where z=8 to F\)/

export const UserDefinedInquiryMatcher = /Double-check the syntax of your inquiry./
export const UserDefinedMessageMatcher = /Double-check the syntax of the message./
Expand Down
4 changes: 2 additions & 2 deletions src/visca/__tests__/camera-interactions/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,9 @@ async function verifyInteractions(
case 'camera-reply': {
const { bytes } = interaction
if (!(await camera).socket.write(bytes)) {
throw new Error(`Writing ${prettyBytes([...bytes])} failed, socket closed`)
throw new Error(`Writing ${prettyBytes(bytes)} failed, socket closed`)
}
LOG(`Wrote ${prettyBytes([...bytes])} to socket`)
LOG(`Wrote ${prettyBytes(bytes)} to socket`)
break
}
case 'command-succeeded': {
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/command-buffer-full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailed,
CommandSucceeded,
SendCommand,
Expand All @@ -24,6 +25,7 @@ describe('VISCA port command buffer full', () => {
SendCommand(OnScreenDisplayClose, 'close'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // close
CameraReplyBytes(CommandBufferFullBytes), // close
CameraReplyNetworkChange([0xa0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // close again
CameraReplyBytes(ACKCompletion(2)), // close again
CommandSucceeded('close'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailed,
CommandSucceeded,
SendCommand,
Expand All @@ -34,6 +35,7 @@ describe('completion in empty socket', () => {
test('completion in never-used socket', async () => {
return RunCameraInteractionTest(
[
CameraReplyNetworkChange([0x80, 0x38, 0xff]), // not essential to this test: randomly added to tests
SendCommand(OnScreenDisplayClose, 'osd-close-1'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // osd-close-1
SendCommand(PresetRecall, { val: '04' }, 'preset-recall-2'),
Expand All @@ -43,6 +45,7 @@ describe('completion in empty socket', () => {
SendCommand(OnScreenDisplayClose, 'osd-close-3'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // osd-close-3
SendCommand(PresetRecall, { val: '01' }, 'preset-recall-4'),
CameraReplyNetworkChange([0xf0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraExpectIncomingBytes(PresetRecallBytes(1)), // preset-recall-4
SendCommand(OnScreenDisplayClose, 'osd-close-5'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // osd-close-5
Expand All @@ -69,6 +72,7 @@ describe('completion in empty socket', () => {
SendCommand(FocusStop, 'focus-stop-12'),
CameraExpectIncomingBytes(FocusStopBytes), // focus-stop-12
SendCommand(FocusNearStandard, 'focus-near-standard-13'),
CameraReplyNetworkChange([0x90, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraExpectIncomingBytes(FocusNearStandardBytes), // focus-near-standard-13
SendCommand(FocusStop, 'focus-stop-14'),
CameraExpectIncomingBytes(FocusStopBytes), // focus-stop-14
Expand All @@ -86,6 +90,7 @@ describe('completion in empty socket', () => {
CommandFailed([CantBeExecutedNowMatcher, MatchVISCABytes(FocusStopBytes)], 'focus-stop-10'),
CameraReplyBytes(CommandNotExecutable(1)), // focus-far-standard-11
CommandFailed([CantBeExecutedNowMatcher, MatchVISCABytes(FocusFarStandardBytes)], 'focus-far-standard-11'),
CameraReplyNetworkChange([0xc0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(CommandNotExecutable(2)), // focus-stop-12
CommandFailed([CantBeExecutedNowMatcher, MatchVISCABytes(FocusStopBytes)], 'focus-stop-12'),
CameraReplyBytes(CommandNotExecutable(1)), // focus-near-standard-13
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/completion-in-empty-socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ACKCompletion, CameraPowerBytes, Completion, FocusLockBytes } from './c
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailedFatally,
CommandSucceeded,
SendCommand,
Expand All @@ -18,6 +19,7 @@ describe('completion in empty socket', () => {
[
SendCommand(CameraPower, { bool: 'on' }, 'camera-power'),
CameraExpectIncomingBytes(CameraPowerBytes), // camera-power
CameraReplyNetworkChange([0x90, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(Completion(1)), // camera-power
CommandFailedFatally([CompletionInEmptySocketMatcher(1), MatchVISCABytes(Completion(1))], 'camera-power'),
],
Expand Down
92 changes: 92 additions & 0 deletions src/visca/__tests__/network-change-reply.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { InstanceStatus } from '@companion-module/base'
import { describe, test } from '@jest/globals'
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandSucceeded,
InquirySucceeded,
InstanceStatusIs,
SendCommand,
SendInquiry,
} from './camera-interactions/interactions.js'
import { ACKCompletion, OnScreenDisplayCloseBytes, OnScreenDisplayInquiryBytes } from './camera-interactions/bytes.js'
import { RunCameraInteractionTest } from './camera-interactions/run-test.js'
import { OnScreenDisplayClose } from '../../camera/commands.js'
import { OnScreenDisplayInquiry } from '../../camera/inquiries.js'

describe('network change reply', () => {
test('network change reply followed by command', async () => {
return RunCameraInteractionTest(
[
InstanceStatusIs(InstanceStatus.Connecting),
CameraReplyNetworkChange([0x80, 0x38, 0xff]),
SendCommand(OnScreenDisplayClose, 'osd-close'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // osd-close
CameraReplyBytes(ACKCompletion(1)), // osd-close
CommandSucceeded('osd-close'),
],
InstanceStatus.Ok
)
})

test('network change reply followed by inquiry', async () => {
return RunCameraInteractionTest(
[
InstanceStatusIs(InstanceStatus.Connecting),
// 90 is specially worth testing because 90 ... FF is the format
// of basic camera responses and inquiry responses -- it's just
// that none of those responses are ever 90 38 FF.
CameraReplyNetworkChange([0x90, 0x38, 0xff]),
SendInquiry(OnScreenDisplayInquiry, 'osd-inquiry'),
CameraExpectIncomingBytes(OnScreenDisplayInquiryBytes), // osd-inquiry
CameraReplyBytes([0x90, 0x50, 0x02, 0xff]), // osd-inquiry
InquirySucceeded({ state: 'open' }, 'osd-inquiry'),
],
InstanceStatus.Ok
)
})

test('network change reply not at start, followed by command', async () => {
return RunCameraInteractionTest(
[
InstanceStatusIs(InstanceStatus.Connecting),
SendCommand(OnScreenDisplayClose, 'osd-close-before'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // osd-close-before
CameraReplyBytes(ACKCompletion(1)), // osd-close-before
CommandSucceeded('osd-close-before'),
CameraReplyNetworkChange([0xc0, 0x38, 0xff]),
SendCommand(OnScreenDisplayClose, 'osd-close-after'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // osd-close-after
CameraReplyBytes(ACKCompletion(2)), // osd-close-after
CommandSucceeded('osd-close-after'),
],
InstanceStatus.Ok
)
})

test('network change reply not at start, followed by inquiry', async () => {
return RunCameraInteractionTest(
[
InstanceStatusIs(InstanceStatus.Connecting),
SendInquiry(OnScreenDisplayInquiry, 'osd-inquiry-before'),
CameraExpectIncomingBytes(OnScreenDisplayInquiryBytes), // osd-inquiry-before
CameraReplyBytes([0x90, 0x50, 0x02, 0xff]), // osd-inquiry-before
InquirySucceeded({ state: 'open' }, 'osd-inquiry-before'),
CameraReplyNetworkChange([0xd0, 0x38, 0xff]),
SendInquiry(OnScreenDisplayInquiry, 'osd-inquiry-after'),
CameraExpectIncomingBytes(OnScreenDisplayInquiryBytes), // osd-inquiry-after
CameraReplyBytes([0x90, 0x50, 0x02, 0xff]), // osd-inquiry-after
InquirySucceeded({ state: 'open' }, 'osd-inquiry-after'),
],
InstanceStatus.Ok
)
})

// We could test a network change reply on its own with no other
// interactions, but 1) it's kind of pointless because it's not how people
// will want to use the module, 2) to properly test it we'd need to have
// VISCAPort expose that it's received and parsed through a reply that we're
// deliberately hiding, and 3) it's not relevant to PTZOptics cameras so
// it's hard to justify the special effort.
})
2 changes: 2 additions & 0 deletions src/visca/__tests__/port-closed-early.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ACK, ExposureModeInquiryBytes, FocusNearStandardBytes, FocusStopBytes }
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CloseVISCAPortEarly,
CommandFailedFatally,
InquiryFailedFatally,
Expand Down Expand Up @@ -41,6 +42,7 @@ describe('VISCA port closed early', () => {
SendInquiry(ExposureModeInquiry, 'exposure-mode'),
CameraExpectIncomingBytes(FocusNearStandardBytes), // focus-near
CameraExpectIncomingBytes(ExposureModeInquiryBytes), // exposure-mode
CameraReplyNetworkChange([0xe0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(ACK(2)), // focus-near
CloseVISCAPortEarly(),
InstanceStatusIs(InstanceStatus.Disconnected),
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/return-message-syntax-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailedFatally,
InquiryFailedFatally,
InstanceStatusIs,
Expand Down Expand Up @@ -85,6 +86,7 @@ describe('VISCA return message syntax errors', () => {
CameraExpectIncomingBytes(ExposureModeInquiryBytes), // exposure-mode
CameraExpectIncomingBytes(FocusModeInquiryBytes), // focus-mode
InstanceStatusIs(InstanceStatus.Ok),
CameraReplyNetworkChange([0xb0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(BadReply), // near
CommandFailedFatally([BadReturnStartByteMatcher, BadReplyMatcher], 'near'),
InstanceStatusIs(InstanceStatus.ConnectionFailure),
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/syntax-error-in-ack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FocusNearStandardBytes } from './camera-interactions/bytes.js'
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailedFatally,
InstanceStatusIs,
SendCommand,
Expand Down Expand Up @@ -35,6 +36,7 @@ describe('VISCA ACK syntax errors', () => {
[
SendCommand(FocusNearStandard, 'focus-near'),
CameraExpectIncomingBytes(FocusNearStandardBytes), // focus-near
CameraReplyNetworkChange([0xb0, 0x38, 0xff]), // not essential to this test: randomly added to tests
InstanceStatusIs(InstanceStatus.Ok),
CameraReplyBytes(BadStartByte), // focus-near
CommandFailedFatally([BadReturnStartByteMatcher, MatchVISCABytes(BadStartByte)], 'focus-near'),
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/unexpected-inquiry-response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ACK, FocusLockBytes, FocusModeInquiryBytes } from './camera-interaction
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailedFatally,
InquirySucceeded,
SendCommand,
Expand Down Expand Up @@ -52,6 +53,7 @@ describe('no pending inquiry', () => {
SendCommand(FocusLock, 'focus-lock'),
CameraExpectIncomingBytes(FocusLockBytes), // focus-lock
CameraReplyBytes(ACK(1)), // focus-lock
CameraReplyNetworkChange([0x90, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(InquiryResponse),
CommandFailedFatally([InquiryResponseWithoutInquiryMatcher, MatchVISCABytes(InquiryResponse)], 'focus-lock'),
],
Expand Down
2 changes: 2 additions & 0 deletions src/visca/__tests__/unrecognized-return-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OnScreenDisplayCloseBytes } from './camera-interactions/bytes.js'
import {
CameraExpectIncomingBytes,
CameraReplyBytes,
CameraReplyNetworkChange,
CommandFailedFatally,
SendCommand,
} from './camera-interactions/interactions.js'
Expand All @@ -31,6 +32,7 @@ describe('unrecognized return message', () => {
[
SendCommand(OnScreenDisplayClose, 'close'),
CameraExpectIncomingBytes(OnScreenDisplayCloseBytes), // close
CameraReplyNetworkChange([0xd0, 0x38, 0xff]), // not essential to this test: randomly added to tests
CameraReplyBytes(UnrecognizedError), // close
CommandFailedFatally([UnrecognizedErrorMatcher, MatchVISCABytes(UnrecognizedError)], 'close'),
],
Expand Down
Loading

0 comments on commit 837401b

Please # to comment.