diff --git a/src/visca/__tests__/ack-without-pending-command.test.ts b/src/visca/__tests__/ack-without-pending-command.test.ts index 715a901..22964f1 100644 --- a/src/visca/__tests__/ack-without-pending-command.test.ts +++ b/src/visca/__tests__/ack-without-pending-command.test.ts @@ -5,6 +5,7 @@ import { ACK, FocusModeInquiryBytes } from './camera-interactions/bytes.js' import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, InquiryFailedFatally, SendInquiry, } from './camera-interactions/interactions.js' @@ -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 diff --git a/src/visca/__tests__/bad-inquiry-response.test.ts b/src/visca/__tests__/bad-inquiry-response.test.ts index 965c0ba..930e58f 100644 --- a/src/visca/__tests__/bad-inquiry-response.test.ts +++ b/src/visca/__tests__/bad-inquiry-response.test.ts @@ -5,6 +5,7 @@ import { ExposureModeInquiryBytes } from './camera-interactions/bytes.js' import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, InquiryFailed, SendInquiry, } from './camera-interactions/interactions.js' @@ -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], diff --git a/src/visca/__tests__/camera-interactions/interactions.ts b/src/visca/__tests__/camera-interactions/interactions.ts index e158e8d..7e5ec4c 100644 --- a/src/visca/__tests__/camera-interactions/interactions.ts +++ b/src/visca/__tests__/camera-interactions/interactions.ts @@ -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) } diff --git a/src/visca/__tests__/camera-interactions/matchers.ts b/src/visca/__tests__/camera-interactions/matchers.ts index d2dd39e..e3cf369 100644 --- a/src/visca/__tests__/camera-interactions/matchers.ts +++ b/src/visca/__tests__/camera-interactions/matchers.ts @@ -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./ diff --git a/src/visca/__tests__/camera-interactions/run-test.ts b/src/visca/__tests__/camera-interactions/run-test.ts index 8e171c7..e60c876 100644 --- a/src/visca/__tests__/camera-interactions/run-test.ts +++ b/src/visca/__tests__/camera-interactions/run-test.ts @@ -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': { diff --git a/src/visca/__tests__/command-buffer-full.test.ts b/src/visca/__tests__/command-buffer-full.test.ts index 1b96c0c..bce10ac 100644 --- a/src/visca/__tests__/command-buffer-full.test.ts +++ b/src/visca/__tests__/command-buffer-full.test.ts @@ -10,6 +10,7 @@ import { import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailed, CommandSucceeded, SendCommand, @@ -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'), diff --git a/src/visca/__tests__/completion-in-empty-socket-real-world.test.ts b/src/visca/__tests__/completion-in-empty-socket-real-world.test.ts index d569a4d..97a850a 100644 --- a/src/visca/__tests__/completion-in-empty-socket-real-world.test.ts +++ b/src/visca/__tests__/completion-in-empty-socket-real-world.test.ts @@ -21,6 +21,7 @@ import { import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailed, CommandSucceeded, SendCommand, @@ -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'), @@ -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 @@ -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 @@ -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 diff --git a/src/visca/__tests__/completion-in-empty-socket.test.ts b/src/visca/__tests__/completion-in-empty-socket.test.ts index 04ffc21..8ca69e7 100644 --- a/src/visca/__tests__/completion-in-empty-socket.test.ts +++ b/src/visca/__tests__/completion-in-empty-socket.test.ts @@ -5,6 +5,7 @@ import { ACKCompletion, CameraPowerBytes, Completion, FocusLockBytes } from './c import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailedFatally, CommandSucceeded, SendCommand, @@ -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'), ], diff --git a/src/visca/__tests__/network-change-reply.test.ts b/src/visca/__tests__/network-change-reply.test.ts new file mode 100644 index 0000000..098e8c1 --- /dev/null +++ b/src/visca/__tests__/network-change-reply.test.ts @@ -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. +}) diff --git a/src/visca/__tests__/port-closed-early.test.ts b/src/visca/__tests__/port-closed-early.test.ts index 124898f..187a8fb 100644 --- a/src/visca/__tests__/port-closed-early.test.ts +++ b/src/visca/__tests__/port-closed-early.test.ts @@ -6,6 +6,7 @@ import { ACK, ExposureModeInquiryBytes, FocusNearStandardBytes, FocusStopBytes } import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CloseVISCAPortEarly, CommandFailedFatally, InquiryFailedFatally, @@ -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), diff --git a/src/visca/__tests__/return-message-syntax-errors.test.ts b/src/visca/__tests__/return-message-syntax-errors.test.ts index 5673c9a..a82890a 100644 --- a/src/visca/__tests__/return-message-syntax-errors.test.ts +++ b/src/visca/__tests__/return-message-syntax-errors.test.ts @@ -13,6 +13,7 @@ import { import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailedFatally, InquiryFailedFatally, InstanceStatusIs, @@ -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), diff --git a/src/visca/__tests__/syntax-error-in-ack.test.ts b/src/visca/__tests__/syntax-error-in-ack.test.ts index f7421f7..f662431 100644 --- a/src/visca/__tests__/syntax-error-in-ack.test.ts +++ b/src/visca/__tests__/syntax-error-in-ack.test.ts @@ -5,6 +5,7 @@ import { FocusNearStandardBytes } from './camera-interactions/bytes.js' import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailedFatally, InstanceStatusIs, SendCommand, @@ -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'), diff --git a/src/visca/__tests__/unexpected-inquiry-response.test.ts b/src/visca/__tests__/unexpected-inquiry-response.test.ts index d773ca2..78462c8 100644 --- a/src/visca/__tests__/unexpected-inquiry-response.test.ts +++ b/src/visca/__tests__/unexpected-inquiry-response.test.ts @@ -6,6 +6,7 @@ import { ACK, FocusLockBytes, FocusModeInquiryBytes } from './camera-interaction import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailedFatally, InquirySucceeded, SendCommand, @@ -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'), ], diff --git a/src/visca/__tests__/unrecognized-return-message.test.ts b/src/visca/__tests__/unrecognized-return-message.test.ts index b044357..1e4b305 100644 --- a/src/visca/__tests__/unrecognized-return-message.test.ts +++ b/src/visca/__tests__/unrecognized-return-message.test.ts @@ -5,6 +5,7 @@ import { OnScreenDisplayCloseBytes } from './camera-interactions/bytes.js' import { CameraExpectIncomingBytes, CameraReplyBytes, + CameraReplyNetworkChange, CommandFailedFatally, SendCommand, } from './camera-interactions/interactions.js' @@ -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'), ], diff --git a/src/visca/port.ts b/src/visca/port.ts index aa8e167..b3251cc 100644 --- a/src/visca/port.ts +++ b/src/visca/port.ts @@ -531,11 +531,29 @@ export class VISCAPort { await moreDataAvailable } - // PTZOptics VISCA over IP responses always begin with 0x90. - if (receivedData[0] !== 0x90) { + // PTZOptics VISCA over IP responses always begin with 0x90. But + // some non-PTZOptics cameras (at least some Sony[0] and Aver[1] + // models) periodically send a "Network Change Message" (z0 38 FF, + // z = device address + 8) to signal the device's address for + // daisy-chained RS-232 control (Sony models don't send it in VISCA + // over IP situations because this particular address is immaterial + // to VISCA over IP, where the IP address uniquely identifies the + // device and so messages only always apply to just one camera. But + // at least one report from the field shows there's a camera that + // will send `80 38 FF` to the module.) + // + // Because this message isn't meaningful for VISCA over IP, and + // because PTZOptics cameras don't send any `z0 xy FF` messages + // where `x=3`, we can safely accept a `z0` leading byte here, + // discard all received network change messages, then restrict all + // other replies to start with `90`. + // + // 0. https://www.sony.net/Products/CameraSystem/CA/BRC_X400_SRG_X400/Technical_Document/E042100111.pdf + // 1. https://communication.aver.com/DownloadFile.aspx?n=5174%7C0C0D934C-A84C-423C-A14F-42C5F322C8AD&t=ServiceDownload + if ((receivedData[0] & 0b1000_1111) !== 0b1000_0000) { const leadingBytes = receivedData.slice(0, 8) throw this.#errorWhileProcessingMessage( - "Error in camera response: return message data doesn't start with 0x90", + 'Camera sent return message data not starting with z0 (where z=8 to F)', leadingBytes ) } @@ -589,6 +607,8 @@ export class VISCAPort { // 90 60 02 FF // Inquiry response: // 90 50 ...one or more non-FF bytes... FF + // Network change response (some non-PTZOptics cameras only): + // z0 38 FF (z=8 to F) // // The second byte therefore dictates return message interpretation. // @@ -598,9 +618,31 @@ export class VISCAPort { // command.) const secondByte = returnMessage[1] - // ACK (and then later Completion or maybe an error): - // 90 4y FF 90 5y FF (ACK, Completion) - // 90 4y FF ........ (ACK, some Error) + // Network change response (some non-PTZOptics cameras only): + // z0 38 FF (z=8 to F) + if (secondByte === 0x38) { + if (returnMessage.length !== 3) { + throw this.#errorWhileProcessingMessage( + 'Received network change response of unexpected length', + returnMessage + ) + } + + // As discussed in `#readReturnMessages`, ignore this message. + continue + } + + // Some VISCA flavors allow a `z0` initial byte in all return + // messages, not just network changes. We choose to be conservative + // in what we accept until someone complains and permit it only in + // network change messages. + if (returnMessage[0] !== 0x90) { + throw this.#errorWhileProcessingMessage('Received return message not starting with 0x90', returnMessage) + } + + // ACK (and then later Completion or maybe an error): + // 90 4y FF 90 5y FF (ACK, Completion) + // 90 4y FF ........ (ACK, some Error) // // Move the command from the waiting-initial-response queue to the // waiting-for-completion queue with the socket (the `y` nibble) diff --git a/src/visca/utils.ts b/src/visca/utils.ts index 89376f4..11430c0 100644 --- a/src/visca/utils.ts +++ b/src/visca/utils.ts @@ -9,7 +9,7 @@ function prettyByte(byte: number): string { } /** Debug representation of a byte list. */ -export function prettyBytes(bytes: readonly number[] | Buffer): string { +export function prettyBytes(bytes: readonly number[] | Uint8Array): string { // Explicitly apply the Array map function so this works on both arrays and // typed arrays. return `[${Array.prototype.map.call(bytes, prettyByte).join(' ')}]`