Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions src/device/protocol/zc95/zc95DeviceFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import MessageResponseHandler from '../messageResponseHandler.js';
import EventEmitterFactory from '../../../factory/eventEmitterFactory.js';
import { logError } from '../../../util/error.js';
import { DeviceId } from '../../deviceId.js';
import KnownDevice from '../../../settings/knownDevice.js';

export default class Zc95DeviceFactory
{
Expand Down Expand Up @@ -60,12 +61,15 @@ export default class Zc95DeviceFactory
availablePatterns.map((pattern) => ({ key: Int.from(pattern.Id), value: pattern.Name }))
);

// Not relevant until https://github.com/CrashOverride85/zc95/issues/151 is resolved
// this.settings.addKnownDevice(knownDevice);
// We only receive serial no. info for ZC95 devices with fw >=2.0
const knownDevice = this.createKnownDevice(
versionDetails.SerialNo !== undefined ? DeviceId.create(versionDetails.SerialNo) : deviceId,
provider,
);

return new Zc95Device(
deviceId,
this.nameGenerator.generateName(),
const device = new Zc95Device(
knownDevice.id,
knownDevice.name,
provider,
this.dateFactory.now(),
versionDetails.ZC95,
Expand All @@ -79,6 +83,13 @@ export default class Zc95DeviceFactory
this.eventEmitterFactory.create(),
this.logger,
);

// Only store the known device if we have a deterministic device id based on serial no. info of the zc95 fw
if (versionDetails.SerialNo !== undefined) {
this.settings.addKnownDevice(knownDevice);
}

return device;
} catch (e) {
logError(this.logger, 'Could not retrieve pattern list', e);
throw e;
Expand All @@ -99,4 +110,22 @@ export default class Zc95DeviceFactory
patternStarted: patternStartedAttr,
};
}

private createKnownDevice(deviceId: DeviceId, provider: string): KnownDevice {
const knownDevice = this.settings.getKnownDeviceById(deviceId)

if (undefined !== knownDevice) {
// Return already existing device if already known (previously detected serial number)
this.logger.debug(`Device is already known: ${knownDevice.id}`);
return knownDevice;
}

// Create a new device and return if not yet known (new serial number)
return new KnownDevice(
deviceId,
this.nameGenerator.generateName(),
'zc95',
provider
);
}
}
1 change: 1 addition & 0 deletions src/device/protocol/zc95/zc95MessageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface VersionMsgResponse extends MsgResponse
ZC95: string;
WsMajor: number;
WsMinor: number;
SerialNo?: string;
}

interface PatternDetail
Expand Down
165 changes: 165 additions & 0 deletions tests/unit/device/protocol/zc95/zc95DeviceFactory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { mock, MockProxy } from 'vitest-mock-extended';
import Zc95DeviceFactory from '../../../../../src/device/protocol/zc95/zc95DeviceFactory.js';
import Settings from '../../../../../src/settings/settings.js';
import KnownDevice from '../../../../../src/settings/knownDevice.js';
import DeviceNameGenerator from '../../../../../src/device/deviceNameGenerator.js';
import DateFactory from '../../../../../src/factory/dateFactory.js';
import EventEmitterFactory from '../../../../../src/factory/eventEmitterFactory.js';
import Logger from '../../../../../src/logging/Logger.js';
import Zc95Protocol from '../../../../../src/device/protocol/zc95/zc95Protocol.js';
import DeviceBidirectionalTransport from '../../../../../src/device/transport/deviceBidirectionalTransport.js';
import MessageResponseHandler from '../../../../../src/device/protocol/messageResponseHandler.js';
import Zc95MessageFactory, {
GetPatternsMsg,
PatternsMsgResponse,
VersionMsgResponse,
} from '../../../../../src/device/protocol/zc95/zc95MessageFactory.js';
import { MsgAndResponseIdentifier } from '../../../../../src/device/protocol/zc95/zc95Protocol.js';
import { DeviceId } from '../../../../../src/device/deviceId.js';

describe('Zc95DeviceFactory', () => {
let settings: MockProxy<Settings>;
let nameGenerator: MockProxy<DeviceNameGenerator>;
let eventEmitterFactory: EventEmitterFactory;
let dateFactory: DateFactory;
let logger: MockProxy<Logger>;
let mockProtocol: MockProxy<Zc95Protocol>;
let mockTransport: MockProxy<DeviceBidirectionalTransport>;
let mockMsgFactory: MockProxy<Zc95MessageFactory>;
let mockMsgHandler: MockProxy<MessageResponseHandler<Zc95Protocol>>;

const fakeGetPatternsMsg = {} as MsgAndResponseIdentifier<GetPatternsMsg, PatternsMsgResponse>;
const patternsResponse: PatternsMsgResponse = {
Type: 'PatternList',
MsgId: 1,
Result: 'OK',
Patterns: [{ Type: 'PatternDetail', Id: 0, Name: 'Pattern A' }],
};

const transportDeviceId = DeviceId.create('transport-device-id');
const provider = 'usb';

function baseVersionDetails(overrides: Partial<VersionMsgResponse> = {}): VersionMsgResponse {
return {
Type: 'VersionDetails',
MsgId: 1,
Result: 'OK',
ZC95: '2.0.0',
WsMajor: 1,
WsMinor: 0,
...overrides,
};
}

function createFactory(): Zc95DeviceFactory {
return new Zc95DeviceFactory(dateFactory, eventEmitterFactory, settings, nameGenerator, logger);
}

beforeEach(() => {
settings = mock<Settings>();
nameGenerator = mock<DeviceNameGenerator>();
eventEmitterFactory = new EventEmitterFactory();
dateFactory = new DateFactory();
logger = mock<Logger>();
mockProtocol = mock<Zc95Protocol>();
mockTransport = mock<DeviceBidirectionalTransport>();
mockMsgFactory = mock<Zc95MessageFactory>();
mockMsgHandler = mock<MessageResponseHandler<Zc95Protocol>>();

mockMsgFactory.createGetPatterns.mockReturnValue(fakeGetPatternsMsg);
mockMsgHandler.send.mockResolvedValue(patternsResponse);
nameGenerator.generateName.mockReturnValue('Generated Name');
settings.getKnownDeviceById.mockReturnValue(undefined);
});

it('uses the transport device id and does not persist a known device when no SerialNo is provided (fw <2.0)', async () => {
const factory = createFactory();

const device = await factory.create(
transportDeviceId,
baseVersionDetails({ SerialNo: undefined }),
mockProtocol,
mockTransport,
mockMsgFactory,
mockMsgHandler,
provider,
);

expect(device.getDeviceId).toStrictEqual(transportDeviceId);
expect(device.getDeviceName).toStrictEqual('Generated Name');
Comment thread
heavyrubberslave marked this conversation as resolved.
expect(settings.addKnownDevice).not.toHaveBeenCalled();
});

it('derives a deterministic device id from SerialNo and persists it as a known device when SerialNo is provided', async () => {
const factory = createFactory();
const expectedDeviceId = DeviceId.create('ZC95-SERIAL-123');

const device = await factory.create(
transportDeviceId,
baseVersionDetails({ SerialNo: 'ZC95-SERIAL-123' }),
mockProtocol,
mockTransport,
mockMsgFactory,
mockMsgHandler,
provider,
);

expect(device.getDeviceId).toStrictEqual(expectedDeviceId);
expect(device.getDeviceId).not.toStrictEqual(transportDeviceId);
expect(device.getDeviceName).toStrictEqual('Generated Name');
expect(settings.addKnownDevice).toHaveBeenCalledTimes(1);

const persisted = settings.addKnownDevice.mock.calls[0][0];
expect(persisted.id).toStrictEqual(expectedDeviceId);
expect(persisted.type).toStrictEqual('zc95');
expect(persisted.source).toStrictEqual(provider);
});

it('reuses an already known device (id and name) when the derived serial-based id is already known', async () => {
const existingKnownDevice = new KnownDevice(
DeviceId.create('ZC95-SERIAL-123'),
'Existing Device Name',
'zc95',
provider,
);
settings.getKnownDeviceById.mockReturnValue(existingKnownDevice);

const factory = createFactory();

const device = await factory.create(
transportDeviceId,
baseVersionDetails({ SerialNo: 'ZC95-SERIAL-123' }),
mockProtocol,
mockTransport,
mockMsgFactory,
mockMsgHandler,
provider,
);

expect(device.getDeviceId).toStrictEqual(existingKnownDevice.id);
expect(device.getDeviceName).toStrictEqual('Existing Device Name');
expect(nameGenerator.generateName).not.toHaveBeenCalled();
expect(settings.addKnownDevice).toHaveBeenCalledWith(existingKnownDevice);
});

it('throws and does not create a device when retrieving the pattern list fails', async () => {
mockMsgHandler.send.mockRejectedValue(new Error('timeout'));

const factory = createFactory();

await expect(
factory.create(
transportDeviceId,
baseVersionDetails({ SerialNo: undefined }),
mockProtocol,
mockTransport,
mockMsgFactory,
mockMsgHandler,
provider,
),
).rejects.toThrow('timeout');

expect(settings.addKnownDevice).not.toHaveBeenCalled();
});
});
Loading