diff --git a/__tests__/bytebuf.test.ts b/__tests__/bytebuf.test.ts new file mode 100644 index 0000000..cc7467c --- /dev/null +++ b/__tests__/bytebuf.test.ts @@ -0,0 +1,325 @@ +import { ByteBuf } from '../src/bytebuf.js'; + +let errorSpy: jest.SpyInstance +describe('ByteBuf Class Tests', () => { + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterAll(() => { + errorSpy.mockRestore(); + }); + + let byteBuf; + + beforeEach(() => { + errorSpy.mockClear(); + + // Initialize a buffer with 10 bytes for testing + byteBuf = new ByteBuf(new ArrayBuffer(10)); + }); + + test('from buffer', () => { + ByteBuf.from(new ArrayBuffer(10)); + ByteBuf.from(new ArrayBuffer(10), 2); + ByteBuf.from(new ArrayBuffer(10), 2, 5); + + ByteBuf.from(Buffer.from(Uint8Array.from([0x01, 0x02, 0x03, 0x04]).buffer)); + + }); + + // Resetting the buffer after reading or writing + test('should skip the buffer correctly', () => { + byteBuf.writeInt8(100); + byteBuf.skip(3); + byteBuf.writeInt8(120); + byteBuf.reset(); + expect(byteBuf.getInt8(0)).toBe(100); + expect(byteBuf.getInt8(4)).toBe(120); + + byteBuf.reset(); + expect(byteBuf.readInt8()).toBe(100); + byteBuf.skip(3); + expect(byteBuf.readInt8()).toBe(120); + + byteBuf.reset(); + expect(byteBuf.byteOffset).toBe(0); + byteBuf.skip(3); + expect(byteBuf.byteOffset).toBe(3); + byteBuf.readInt8(); + expect(byteBuf.byteOffset).toBe(4); + }); + + // Resetting the buffer after reading or writing + test('should reset the buffer correctly', () => { + byteBuf.writeInt8(100); + byteBuf.reset(); + expect(byteBuf.readInt8()).toBe(100); + + byteBuf.reset(); + byteBuf.writeInt8(200); // 200 - 256 = -56 cause goes to 127 + byteBuf.reset(); + expect(byteBuf.readInt8()).toBe(-56); + }); + + // Clear the buffer + test('should clear the buffer correctly', () => { + byteBuf.writeInt8(100); + byteBuf.clear(); + byteBuf.reset(); + expect(byteBuf.readInt8()).toBe(0); + }); + + // Boolean Read/Write Tests + test('should read and write boolean', () => { + byteBuf.writeBool(false); + byteBuf.reset(); + expect(byteBuf.readBool()).toBe(false); + + byteBuf.reset(); + byteBuf.writeBool(true); + byteBuf.reset(); + expect(byteBuf.readBool()).toBe(true); + }); + + test('should set and get boolean at specific byteOffset', () => { + byteBuf.writeBool(true); + byteBuf.setBool(3, false); + byteBuf.reset(); + expect(byteBuf.readBool(0)).toBe(true); + expect(byteBuf.readBool(3)).toBe(false); + }); + + // Boundary Check Tests + test('should handle writing beyond buffer size', () => { + byteBuf.writeInt8(100); + expect(() => byteBuf.writeInt8(200)) // .toThrowError('Tried to write 1 bytes past the end of a buffer at offset 0x1 of 0xa'); + }); + + // Integer Read/Write Tests + test('should read and write Int8', () => { + byteBuf.writeInt8(127); + byteBuf.reset(); + expect(byteBuf.readInt8()).toBe(127); + }); + + test('should read and write Uint8', () => { + byteBuf.writeUint8(255); + byteBuf.reset(); + expect(byteBuf.readUint8()).toBe(255); + }); + + test('should read and write Int16', () => { + byteBuf.writeInt16(32767); + byteBuf.reset(); + expect(byteBuf.readInt16()).toBe(32767); + }); + + test('should read and write Uint16', () => { + byteBuf.writeUint16(65535); + byteBuf.reset(); + expect(byteBuf.readUint16()).toBe(65535); + }); + + test('should read and write Int24', () => { + byteBuf.writeInt24(8388607); + byteBuf.reset(); + expect(byteBuf.readInt24()).toBe(8388607); + + byteBuf.reset(); + byteBuf.writeInt24(-8388607); + byteBuf.reset(); + expect(byteBuf.readInt24()).toBe(-8388607); + }); + + test('should read and write Uint24', () => { + byteBuf.writeUint24(16777215); + byteBuf.reset(); + expect(byteBuf.readUint24()).toBe(16777215); + + byteBuf.reset(); + byteBuf.writeUint24(16777215, true); + byteBuf.reset(); + expect(byteBuf.readUint24(true)).toBe(16777215); + + byteBuf.reset(); + byteBuf.setUint24(-1, 16777215); + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.mock.calls[0][0]).toBe('Tried to write to a negative offset'); + + byteBuf.reset(); + byteBuf.setUint24(byteBuf.byteLength, 16777215); + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.mock.calls[1][0]).toContain('Tried to write 3 bytes past the end of a buffer at offset 0x'); + }); + + test('should read and write Int32', () => { + byteBuf.writeInt32(2147483647); + byteBuf.reset(); + expect(byteBuf.readInt32()).toBe(2147483647); + }); + + test('should read and write Uint32', () => { + byteBuf.writeUint32(4294967295); + byteBuf.reset(); + expect(byteBuf.readUint32()).toBe(4294967295); + }); + + test('should read and write Float32', () => { + byteBuf.writeFloat32(3.14); + byteBuf.reset(); + expect(byteBuf.readFloat32()).toBeCloseTo(3.14, 5); + }); + + test('should read and write Float64', () => { + byteBuf.writeFloat64(3.14159265359); + byteBuf.reset(); + expect(byteBuf.readFloat64()).toBeCloseTo(3.14159265359, 10); + }); + + test('should read and write Int64', () => { + const bigInt = BigInt(9007199254740991); + byteBuf.writeInt64(bigInt); + byteBuf.reset(); + expect(byteBuf.readInt64().toString()).toBe(bigInt.toString()); + }); + + test('should read and write Uint64', () => { + const bigUint = BigInt(9007199254740991); + byteBuf.writeUint64(bigUint); + byteBuf.reset(); + expect(byteBuf.readUint64().toString()).toBe(bigUint.toString()); + }); + + test('should read and write BigInt64', () => { + const bigInt = BigInt(9007199254740991); + byteBuf.writeBigInt64(bigInt); + byteBuf.reset(); + expect(byteBuf.readBigInt64().toString()).toBe(bigInt.toString()); + }); + + test('should read and write BigUint64', () => { + const bigUint = BigInt(9007199254740991); + byteBuf.writeBigUint64(bigUint); + byteBuf.reset(); + expect(byteBuf.readBigUint64().toString()).toBe(bigUint.toString()); + }); + + // VarInt, VarUint, VarZint Tests + test('should read and write VarInt', () => { + byteBuf.writeVarInt(100); + byteBuf.reset(); + expect(byteBuf.readVarInt()).toBe(100); + + byteBuf.reset(); + expect(() => { + byteBuf.writeVarInt(300, 1); + }).toThrowError( + `VarInt must be between 1 and 1 bytes.` + ); + + byteBuf.reset(); + expect(() => { + byteBuf.readVarInt(1); + }).toThrowError( + `VarInt must be between 1 and 1 bytes.` + ); + }); + + test('should read and write VarUint', () => { + byteBuf.writeVarUint(100); + byteBuf.reset(); + expect(byteBuf.readVarUint()).toBe(100); + }); + + test('should read and write VarZint', () => { + byteBuf.writeVarZint(100); + byteBuf.reset(); + expect(byteBuf.readVarZint()).toBe(100); + }); + + // String Read/Write Tests + test('should read and write string', () => { + byteBuf.writeString('hello'); + byteBuf.reset(); + expect(byteBuf.readString(5)).toBe('hello'); + + byteBuf.reset(); + byteBuf.writeString('helloworld'); + byteBuf.reset(); + expect(byteBuf.readString()).toBe('helloworld'); + + byteBuf.reset(); + byteBuf.writeString(''); // written || 0 tesitng + + byteBuf.reset(); + expect(() => { + byteBuf.writeString('Hello', 'utf-16'); + }).toThrowError( + `String encoding 'utf-16' is not supported` + ); + }); + + test('should read and write VarString', () => { + byteBuf.writeVarString('hello'); + byteBuf.reset(); + expect(byteBuf.readVarString()).toBe('hello'); + + byteBuf.reset(); + const count = byteBuf.writeVarString('hello') - 1; + byteBuf.reset(); + expect(() => { + byteBuf.readVarString(count - 1); + }).toThrowError( + `VarString must be less than or equal to ${count - 1} bytes.` + ); + }); + + // Array Read/Write Tests + test('should read and write Uint8Array', () => { + const arr = new Uint8Array([1, 2, 3, 4, 5]); + byteBuf.writeUint8Array(arr); + byteBuf.reset(); + expect(byteBuf.readUint8Array(5)).toEqual(arr); + }); + + test('should read and write Uint16Array', () => { + const arr = new Uint16Array([1, 2, 3, 4, 5]); + byteBuf.writeUint16Array(arr); + byteBuf.reset(); + expect(byteBuf.readUint16Array(5 * 2)).toEqual(arr); + }); + + test('should correctly convert the buffer to a string in hex or decimal format', () => { + // Write some values into the buffer + byteBuf.writeUint8(10); // 0x0A + byteBuf.writeUint8(20); // 0x14 + byteBuf.writeUint8(30); // 0x1E + byteBuf.writeUint8(40); // 0x28 + byteBuf.writeUint8(50); // 0x32 + + // Convert to hex and check if the result matches the expected hex string + const hexString = byteBuf.toString('hex'); + expect(hexString).toBe('0a 14 1e 28 32 00 00 00 00 00'); + + // Convert to decimal and check if the result matches the expected decimal string + const decString = byteBuf.toString('dec'); + expect(decString).toBe('10 20 30 40 50 0 0 0 0 0'); + }); + + test('should throw error when reading beyond buffer size', () => { + expect(() => byteBuf.readBytes(11)).toThrowError('EOF'); + + byteBuf.reset(); + expect(byteBuf.readBytes(10)).toEqual(Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])); + + byteBuf.reset(); + byteBuf.writeUint8(10); // 0x0A + byteBuf.writeUint8(20); // 0x14 + byteBuf.writeUint8(30); // 0x1E + byteBuf.writeUint8(40); // 0x28 + byteBuf.writeUint8(50); // 0x32 + byteBuf.reset(); + expect(byteBuf.readBytes(5)).toEqual(Uint8Array.from([10, 20, 30, 40, 50])); + }); +}); diff --git a/__tests__/enums.test.ts b/__tests__/enums.test.ts new file mode 100644 index 0000000..a4371b6 --- /dev/null +++ b/__tests__/enums.test.ts @@ -0,0 +1,14 @@ +import { DjiDeviceModel, getDjiDeviceModelName } from '../src/enums.js'; + +test('check model names', () => { + expect(getDjiDeviceModelName(DjiDeviceModel.unknown)).toBe('Unknown'); + + expect(getDjiDeviceModelName(DjiDeviceModel.osmoAction3)).toBe('Osmo Action 3'); + + expect(getDjiDeviceModelName(DjiDeviceModel.osmoAction4)).toBe('Osmo Action 4'); + + expect(getDjiDeviceModelName(DjiDeviceModel.osmoAction5Pro)).toBe('Osmo Action 5 Pro'); + + expect(getDjiDeviceModelName(DjiDeviceModel.osmoPocket3)).toBe('Osmo Pocket 3'); +}); + diff --git a/__tests__/message.test.ts b/__tests__/message.test.ts new file mode 100644 index 0000000..530fc75 --- /dev/null +++ b/__tests__/message.test.ts @@ -0,0 +1,205 @@ +import { + DjiMessage, DjiMessageWithData, + DjiPairMessagePayload, + DjiPreparingToLivestreamMessagePayload, + DjiSetupWifiMessagePayload, + DjiStartStreamingMessagePayload, + DjiConfirmStartStreamingMessagePayload, + DjiStopStreamingMessagePayload, + DjiConfigureMessagePayload, +} from '../src/message.js'; +import { DjiDeviceResolution, DjiDeviceImageStabilization } from '../src/enums.js'; + +test('parse message', () => { + const message = new DjiMessage( + 513, + 6363, + 8389120, + Buffer.from(Uint8Array.from([0x01, 0x02, 0x80, 0x00, 0x01, 0x4d, 0xed, 0x00, 0x00, 0x21, 0xea, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbe, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]).buffer), + ); + expect(new DjiMessageWithData(Buffer.from(Uint8Array.from(message.encode()).buffer))).toEqual(message); + + expect(message.format()).toBe('DjiMessage(target=513, id=6363, type=8389120, payload=01028000014ded000021ea000000000000be150000000000000000000000000000460000010000000000000000000000000000000000000000010000)') +}); + +test('check throws', () => { + expect(() => + new DjiMessageWithData(Buffer.from(Uint8Array.from( + [0x53, 0x04, 0x04, 0x52] + ).buffer)) + ).toThrowError('Bad first byte'); + + expect(() => + new DjiMessageWithData(Buffer.from(Uint8Array.from( + [0x55, 0x05, 0x04, 0x52] + ).buffer)) + ).toThrowError('Bad length'); + + expect(() => + new DjiMessageWithData(Buffer.from(Uint8Array.from( + [0x55, 0x04, 0x03, 0x52] + ).buffer)) + ).toThrowError('Bad version'); + + expect(() => + new DjiMessageWithData(Buffer.from(Uint8Array.from( + [0x55, 0x04, 0x04, 0x52] + ).buffer)) + ).toThrowError(`Calculated CRC 129 does not match received CRC ${0x52}`); + + const message = new DjiMessage( + 1, + 1, + 1, + Buffer.from(Uint8Array.from([0x00]).buffer), + ); + const data = message.encode(); + + const calculated = data.readUint16LE(data.length - 2); + data[data.length - 2] = data[data.length - 2] - 1; + data[data.length - 1] = data[data.length - 1] - 1; + const crc = data.readUint16LE(data.length - 2); + expect(() => + new DjiMessageWithData(Buffer.from(Uint8Array.from( + data + ).buffer)) + ).toThrowError(`Calculated CRC ${calculated} does not match received CRC ${crc}`); +}); + + +describe('MessagePayload checks', () => { + let message; + + test('DjiPairMessagePayload works', () => { + message = new DjiPairMessagePayload('love'); + expect(message.encode()).toEqual(Buffer.from([ + 0x20, 0x32, 0x38, 0x34, 0x61, 0x65, 0x35, 0x62, 0x38, 0x64, 0x37, 0x36, + 0x62, 0x33, 0x33, 0x37, 0x35, 0x61, 0x30, 0x34, 0x61, 0x36, 0x34, 0x31, + 0x37, 0x61, 0x64, 0x37, 0x31, 0x62, 0x65, 0x61, 0x33, + + // length, L, O, V, E + 4, 108, 111, 118, 101 + ])); + }); + + test('DjiPreparingToLivestreamMessagePayload works', () => { + message = new DjiPreparingToLivestreamMessagePayload(); + expect(message.encode()).toEqual(Buffer.from([ + 0x1a + ])); + }); + + test('DjiSetupWifiMessagePayload works', () => { + message = new DjiSetupWifiMessagePayload('ssid', 'password'); + expect(message.encode()).toEqual(Buffer.from([ + 4, 115, 115, 105, 100, + 8, 112, 97, 115, 115, 119, 111, 114, 100 + ])); + }); + + test('DjiStartStreamingMessagePayload works', () => { + message = new DjiStartStreamingMessagePayload('rtmp://localhost:4700', DjiDeviceResolution.r480p, 25, 5000, false); + expect(message.encode()).toEqual(Buffer.from([ + 0x00, + 0x2e, // + 0x00, + 0x47, // 480p + 136, 19, // 5000 + 0x02, 0x00, + 0x02, // 25 fps + 0x00, 0x00, 0x00, + 21, 0, + 114, 116, 109, 112, 58, 47, 47, 108, 111, 99, 97, 108, 104, 111, 115, 116, 58, 52, 55, 48, 48, + ])); + + message = new DjiStartStreamingMessagePayload('rtmp://localhost:4700', DjiDeviceResolution.r720p, 30, 5000, false); + expect(message.encode()).toEqual(Buffer.from([ + 0x00, + 0x2e, // + 0x00, + 0x04, // 720p + 136, 19, // 5000 + 0x02, 0x00, + 0x03, // 30fps + 0x00, 0x00, 0x00, + 21, 0, + 114, 116, 109, 112, 58, 47, 47, 108, 111, 99, 97, 108, 104, 111, 115, 116, 58, 52, 55, 48, 48, + ])); + + message = new DjiStartStreamingMessagePayload('rtmp://localhost:4700', DjiDeviceResolution.r1080p, 60, 5000, true); + expect(message.encode()).toEqual(Buffer.from([ + 0x00, + 0x2a, // + 0x00, + 0x0a, // 1080p + 136, 19, // 5000 + 0x02, 0x00, + 0x00, // unknown fps + 0x00, 0x00, 0x00, + 21, 0, + 114, 116, 109, 112, 58, 47, 47, 108, 111, 99, 97, 108, 104, 111, 115, 116, 58, 52, 55, 48, 48, + ])); + + message = new DjiStartStreamingMessagePayload('rtmp://localhost:4700', DjiDeviceResolution.r1080p + 1 as DjiDeviceResolution, 30, 5000, false); + expect(() => message.encode()).toThrowError('Unknown resolution'); + }); + + test('DjiConfirmStartStreamingMessagePayload works', () => { + message = new DjiConfirmStartStreamingMessagePayload(); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, 0x1a, 0x00, 0x01, 0x01 + ])); + }); + + test('DjiStopStreamingMessagePayload works', () => { + message = new DjiStopStreamingMessagePayload(); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, 0x1a, 0x00, 0x01, 0x02 + ])); + }); + + test('DjiConfigureMessagePayload works', () => { + message = new DjiConfigureMessagePayload(DjiDeviceImageStabilization.Off, false); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, + 0x08, + 0x00, 0x01, + 0 + ])); + + message = new DjiConfigureMessagePayload(DjiDeviceImageStabilization.RockSteady, false); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, + 0x08, + 0x00, 0x01, + 1 + ])); + + message = new DjiConfigureMessagePayload(DjiDeviceImageStabilization.HorizonSteady, false); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, + 0x08, + 0x00, 0x01, + 2 + ])); + + message = new DjiConfigureMessagePayload(DjiDeviceImageStabilization.RockSteadyPlus, false); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, + 0x08, + 0x00, 0x01, + 3 + ])); + + message = new DjiConfigureMessagePayload(DjiDeviceImageStabilization.HorizonBalancing, true); + expect(message.encode()).toEqual(Buffer.from([ + 0x01, 0x01, + 0x1a, + 0x00, 0x01, + 4 + ])); + + message = new DjiConfigureMessagePayload(DjiDeviceImageStabilization.RockSteady + 1 as DjiDeviceImageStabilization, false); + expect(() => message.encode()).toThrowError('Unknown image stabilization'); + }); +}); diff --git a/__tests__/model.test.ts b/__tests__/model.test.ts new file mode 100644 index 0000000..be08159 --- /dev/null +++ b/__tests__/model.test.ts @@ -0,0 +1,39 @@ +import { djiModelFromManufacturerData, djiModelNameFromManufacturerData, isDjiDevice } from '../src/model.js'; +import { DjiDeviceModel, DjiDeviceModelName } from '../src/enums.js'; + +test('get model from manufacturer data', () => { + expect(djiModelFromManufacturerData(Buffer.from([]))).toBe(null); + expect(djiModelFromManufacturerData(Buffer.from([0xaa, 0x08, 0x00]))).toBe(null); + + expect(djiModelFromManufacturerData(Buffer.from([0xaa, 0x08, 0x00, 0x00]))).toBe(DjiDeviceModel.unknown); + + expect(djiModelFromManufacturerData(Buffer.from([0xaa, 0x08, 0x12, 0x00]))).toBe(DjiDeviceModel.osmoAction3); + + expect(djiModelFromManufacturerData(Buffer.from([0xaa, 0x08, 0x14, 0x00]))).toBe(DjiDeviceModel.osmoAction4); + + expect(djiModelFromManufacturerData(Buffer.from([0xaa, 0x08, 0x15, 0x00]))).toBe(DjiDeviceModel.osmoAction5Pro); + + expect(djiModelFromManufacturerData(Buffer.from([0xaa, 0x08, 0x20, 0x00]))).toBe(DjiDeviceModel.osmoPocket3); +}); + +test('get model name from manufacturer data', () => { + expect(djiModelNameFromManufacturerData(Buffer.from([]))).toBe(null); + expect(djiModelNameFromManufacturerData(Buffer.from([0xaa, 0x08, 0x00]))).toBe(null); + + expect(djiModelNameFromManufacturerData(Buffer.from([0xaa, 0x08, 0x00, 0x00]))).toBe(DjiDeviceModelName.unknown); + + expect(djiModelNameFromManufacturerData(Buffer.from([0xaa, 0x08, 0x12, 0x00]))).toBe(DjiDeviceModelName.osmoAction3); + + expect(djiModelNameFromManufacturerData(Buffer.from([0xaa, 0x08, 0x14, 0x00]))).toBe(DjiDeviceModelName.osmoAction4); + + expect(djiModelNameFromManufacturerData(Buffer.from([0xaa, 0x08, 0x15, 0x00]))).toBe(DjiDeviceModelName.osmoAction5Pro); + + expect(djiModelNameFromManufacturerData(Buffer.from([0xaa, 0x08, 0x20, 0x00]))).toBe(DjiDeviceModelName.osmoPocket3); +}); + +test('check if dji device', () => { + expect(isDjiDevice(Buffer.from([0xaa, 0x08]))).toBe(true); + + expect(isDjiDevice(Buffer.from([0xa0, 0x08]))).toBe(false); +}); + diff --git a/src/bytebuf.ts b/src/bytebuf.ts index ef59ad3..0c19576 100644 --- a/src/bytebuf.ts +++ b/src/bytebuf.ts @@ -119,7 +119,7 @@ class ByteBuf extends DataView { * Reads the next boolean. */ readBool(): boolean { - return this.getInt8(this.#byteOffset++) !== 0; + return this.getBool(this.#byteOffset++); } /** @@ -136,7 +136,7 @@ class ByteBuf extends DataView { * @param value The value. */ writeBool(value: boolean): void { - this.setInt8(this.#byteOffset++, value ? 1 : 0); + this.setBool(this.#byteOffset++, value); } /** diff --git a/tsconfig.json b/tsconfig.json index c47386e..b918151 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,5 +29,5 @@ // "Go to Definition" in VSCode "declarationMap": true }, - "include": ["./src/**/*", "__tests__/**/*"] + "include": ["./src/**/*"] }