diff --git a/src/Ri5devBrowser.ts b/src/Ri5devBrowser.ts index 49010ec..80df193 100644 --- a/src/Ri5devBrowser.ts +++ b/src/Ri5devBrowser.ts @@ -11,10 +11,10 @@ import { decodeBase64, formatFilesize } from './utils'; export class DeviceTreeItem extends vscode.TreeItem { constructor() { - const { name } = getDeviceInfo(); + const { name, firmwareVersion } = getDeviceInfo(); super(name, vscode.TreeItemCollapsibleState.Collapsed); this.tooltip = name; - this.description = 'No version available'; + this.description = firmwareVersion || 'No version available'; this.contextValue = 'device'; this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; } diff --git a/src/device.ts b/src/device.ts index 229b579..106c5cd 100644 --- a/src/device.ts +++ b/src/device.ts @@ -81,6 +81,21 @@ class Device extends EventEmitter { }); } + async retrieveHubInfo() { + const info = (await APIRequest(this.serialPort!, 'get_hub_info', {})) as { + firmware: { + version: number[]; + checksum: string; + }; + }; + const [major, minor, patch, build] = info.firmware.version; + + this.firmwareVersion = `${major}.${minor}.${String(patch).padStart( + 2, + '0', + )}.${String(build).padStart(4, '0')}-${info.firmware.checksum}`; + } + getSlots() { return this.storageStatus?.slots; } @@ -113,14 +128,12 @@ class Device extends EventEmitter { old_slotid: fromSlotId, new_slotid: toSlotId, }); - await this.refreshStorageStatus(); - this.emit('change'); + return this.refreshStorageStatus(); } async removeProgram(slotId: number) { await this.executeSlotSpecificCommand('remove_project', slotId); - await this.refreshStorageStatus(); - this.emit('change'); + return this.refreshStorageStatus(); } async uploadProgram(prgName: string, prgText: string, slotId: number) { @@ -182,7 +195,7 @@ class Device extends EventEmitter { } }, ); - }); + }).then(() => this.retrieveHubInfo()); } disconnect() { @@ -243,16 +256,19 @@ export async function runProgramOnDevice(slotId: number) { return device?.runProgram(slotId); } -export async function removeProgramFromDevice(slotId: number) { - return device?.removeProgram(slotId); +export async function stopProgramOnDevice() { + return device?.stopProgram(); } -export function stopProgramOnDevice() { - device?.stopProgram(); +export async function moveProgramOnDevice( + fromSlotId: number, + toSlotId: number, +) { + return device?.moveProgram(fromSlotId, toSlotId); } -export function moveProgramOnDevice(fromSlotId: number, toSlotId: number) { - device?.moveProgram(fromSlotId, toSlotId); +export async function removeProgramFromDevice(slotId: number) { + return device?.removeProgram(slotId); } export function uploadProgramToDevice( diff --git a/src/test/device.test.ts b/src/test/device.test.ts index 553f29e..206d994 100644 --- a/src/test/device.test.ts +++ b/src/test/device.test.ts @@ -7,6 +7,11 @@ import { removeProgramFromDevice, uploadProgramToDevice, getDeviceSlots, + stopProgramOnDevice, + moveProgramOnDevice, + addDeviceOnChangeCallbak, + getDeviceInfo, + removeDeviceAllListeners, } from '../device'; import { SerialPortMock } from 'serialport'; import * as chai from 'chai'; @@ -74,25 +79,48 @@ describe('Device', () => { }; }; + const basicMockAPIRequestStub = () => { + APIRequestStub.callsFake((port, method) => { + if (method === 'get_storage_status') { + return Promise.resolve(getStorageInfo()); + } else if (method === 'get_hub_info') { + return Promise.resolve({ + firmware: { + version: [1, 2, 3, 0], + checksum: '1e60', + }, + }); + } else { + return Promise.resolve({}); + } + }); + }; + describe('connectDevice', function () { it('should connect to the device and refresh storage status', async function () { createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); expect(isDeviceConnected()).to.be.true; - expect(APIRequestStub).to.have.been.calledOnce; + expect(APIRequestStub).to.have.been.calledTwice; expect(APIRequestStub).to.have.been.calledWith( sinon.match.any, 'get_storage_status', {}, ); + expect(APIRequestStub).to.have.been.calledWith( + sinon.match.any, + 'get_hub_info', + {}, + ); }); it('should throw an error if the device is already connected', async function () { createPortMock(''); + basicMockAPIRequestStub(); await connectDevice(deviceName); @@ -139,7 +167,7 @@ describe('Device', () => { describe('disconnectDevice', function () { it('should disconnect the device', async function () { createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); @@ -160,7 +188,7 @@ describe('Device', () => { describe('runProgramOnDevice', function () { it('should run a program on a device slot', async function () { createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); @@ -179,7 +207,7 @@ describe('Device', () => { it('should throw an error if the slot is not valid', async function () { createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); @@ -199,29 +227,90 @@ describe('Device', () => { }); }); + describe('stopProgramOnDevice', function () { + it('should stop a running program on a device slot', async function () { + createPortMock(''); + basicMockAPIRequestStub(); + + await connectDevice(deviceName); + + APIRequestStub.reset(); + APIRequestStub.resolves({}); + + stopProgramOnDevice(); + + expect(APIRequestStub).to.have.been.calledOnce; + expect(APIRequestStub).to.have.been.calledWith( + sinon.match.any, + 'program_terminate', + {}, + sinon.match.any, + ); + }); + }); + + describe('moveProgramOnDevice', function () { + it('should move a program from one slot to another', async function () { + createPortMock(''); + basicMockAPIRequestStub(); + + await connectDevice(deviceName); + + APIRequestStub.reset(); + APIRequestStub.resolves({}); + const onChangeFn = sinon.spy(); + + addDeviceOnChangeCallbak(onChangeFn); + + await moveProgramOnDevice(12, 15); + + expect(APIRequestStub).to.have.been.calledTwice; + expect(APIRequestStub).to.have.been.calledWith( + sinon.match.any, + 'move_project', + { old_slotid: 12, new_slotid: 15 }, + ); + expect(APIRequestStub).to.have.been.calledWith( + sinon.match.any, + 'get_storage_status', + {}, + ); + expect(onChangeFn).to.have.been.calledOnce; + }); + }); + describe('removeProgramFromDevice', function () { it('should remove a program from a device slot', async function () { createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); APIRequestStub.reset(); APIRequestStub.resolves({}); + const onChangeFn = sinon.spy(); - removeProgramFromDevice(12); + addDeviceOnChangeCallbak(onChangeFn); - expect(APIRequestStub).to.have.been.calledOnce; + await removeProgramFromDevice(12); + + expect(APIRequestStub).to.have.been.calledTwice; expect(APIRequestStub).to.have.been.calledWith( sinon.match.any, 'remove_project', { slotid: 12 }, ); + expect(APIRequestStub).to.have.been.calledWith( + sinon.match.any, + 'get_storage_status', + {}, + ); + expect(onChangeFn).to.have.been.calledOnce; }); it('should throw an error if the slot is not valid', async function () { createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); @@ -255,7 +344,7 @@ describe('Device', () => { }, }; createPortMock(''); - APIRequestStub.resolves(getStorageInfo()); + basicMockAPIRequestStub(); await connectDevice(deviceName); @@ -275,10 +364,13 @@ describe('Device', () => { print("...but it's a good start!") `; const size = prgText.length; + const onChangeFn = sinon.spy(); + + addDeviceOnChangeCallbak(onChangeFn); await uploadProgramToDevice('Program 3', prgText, 12); - expect(APIRequestStub.callCount).to.equal(14); + expect(APIRequestStub.callCount).to.equal(15); expect(APIRequestStub).to.have.been.calledWith( sinon.match.any, 'start_write_program', @@ -294,6 +386,11 @@ describe('Device', () => { }, }, ); + expect(APIRequestStub).to.have.been.calledWith( + sinon.match.any, + 'get_hub_info', + {}, + ); expect(APIRequestStub).to.have.been.calledWith( sinon.match.any, 'write_package', @@ -313,6 +410,58 @@ describe('Device', () => { ...defaultSlots, ...additionalSlots, }); + expect(onChangeFn).to.have.been.calledOnce; + }); + }); + + describe('getDeviceInfo', function () { + it('should return the device info', async function () { + createPortMock(''); + basicMockAPIRequestStub(); + + await connectDevice(deviceName); + + const info = getDeviceInfo(); + + expect(info).to.deep.equal({ + name: '/dev/mock', + firmwareVersion: '1.2.03.0000-1e60', + }); + }); + }); + + describe('getDeviceSlots', function () { + it('should return the device slots', async function () { + createPortMock(''); + basicMockAPIRequestStub(); + + await connectDevice(deviceName); + + const slots = await getDeviceSlots(); + + expect(slots).to.deep.equal(defaultSlots); + }); + }); + + describe('removeDeviceAllListeners', function () { + it('should remove all listeners', async function () { + createPortMock(''); + basicMockAPIRequestStub(); + + await connectDevice(deviceName); + + const onChangeFn = sinon.spy(); + addDeviceOnChangeCallbak(onChangeFn); + + await removeProgramFromDevice(12); + + expect(onChangeFn).to.have.been.calledOnce; + + removeDeviceAllListeners(); + + await removeProgramFromDevice(12); + + expect(onChangeFn).to.have.been.calledOnce; }); }); });