Skip to content

Commit

Permalink
Prepare for multiple adapters. #72
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Oct 16, 2019
1 parent dffac7b commit bd7dfff
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 52 deletions.
43 changes: 42 additions & 1 deletion src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import * as TsType from './tstype';
import {ZclDataPayload} from './events';
import events from 'events';
import {ZclFrame} from '../zcl';
import Debug from "debug";
import {ZStackAdapter} from './z-stack/adapter';

const debug = Debug("zigbee-herdsman:adapter");

abstract class Adapter extends events.EventEmitter {
protected networkOptions: TsType.NetworkOptions;
protected serialPortOptions: TsType.SerialPortOptions;
protected backupPath: string;

public constructor(
protected constructor(
networkOptions: TsType.NetworkOptions, serialPortOptions: TsType.SerialPortOptions, backupPath: string)
{
super();
Expand All @@ -17,6 +21,43 @@ abstract class Adapter extends events.EventEmitter {
this.backupPath = backupPath;
}

public static async create(
networkOptions: TsType.NetworkOptions, serialPortOptions: TsType.SerialPortOptions, backupPath: string
): Promise<Adapter> {
const adapters: typeof ZStackAdapter[] = [ZStackAdapter];
let adapter: typeof ZStackAdapter = null;

if (!serialPortOptions.path) {
debug('No path provided, auto detecting path');
for (const candidate of adapters) {
const path = await candidate.autoDetectPath();
if (path) {
debug(`Auto detected path '${path}' from adapter '${candidate}'`);
serialPortOptions.path = path;
break;
}
}

if (!serialPortOptions.path) {
throw new Error("No path provided and failed to auto detect path");
}
}

// Determine adapter to use
for (const candidate of adapters) {
if (await candidate.isValidPath(serialPortOptions.path)) {
debug(`Path '${serialPortOptions.path}' is valid for '${candidate}'`);
adapter = candidate;
}
}

if (!adapter) {
adapter = adapters[0];
}

return new adapter(networkOptions, serialPortOptions, backupPath);
}

public abstract start(): Promise<TsType.StartResult>;

public abstract stop(): Promise<void>;
Expand Down
3 changes: 1 addition & 2 deletions src/adapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as TsType from './tstype';
import Adapter from './adapter';
import {ZStackAdapter} from './z-stack/adapter';
import * as Events from './events';

export {
TsType, Adapter, ZStackAdapter, Events,
TsType, Adapter, Events,
};
24 changes: 24 additions & 0 deletions src/adapter/serialPortUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import SerialPort from 'serialport';
import {EqualsPartial} from '../utils';

interface PortInfoMatch {
manufacturer: string;
vendorId: string;
productId: string;
}

async function find(matchers: PortInfoMatch[]): Promise<string[]> {
let devices = await SerialPort.list();
devices = devices.filter((device) => matchers.find((matcher) => EqualsPartial(device, matcher)) != null);
// @ts-ignore; not sure why this is needed as path exists (definition is wrong?)
return devices.map((device) => device.path);
}

async function is(path: string, matchers: PortInfoMatch[]): Promise<boolean> {
const devices = await SerialPort.list();
// @ts-ignore; not sure why this is needed as path exists (definition is wrong?)
const device = devices.find((device) => device.path === path);
return matchers.find((matcher) => EqualsPartial(device, matcher)) != null;
}

export default {is, find};
10 changes: 9 additions & 1 deletion src/adapter/z-stack/adapter/zStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as Constants from '../constants';
import Debug from "debug";
import {Backup} from './backup';

const debug = Debug("zigbee-herdsman:controller:zStack");
const debug = Debug("zigbee-herdsman:adapter:zStack");
const Subsystem = UnpiConstants.Subsystem;
const Type = UnpiConstants.Type;

Expand Down Expand Up @@ -86,6 +86,14 @@ class ZStackAdapter extends Adapter {
await this.znp.close();
}

public static async isValidPath(path: string): Promise<boolean> {
return Znp.isValidPath(path);
}

public static async autoDetectPath(): Promise<string> {
return Znp.autoDetectPath();
}

public async getCoordinator(): Promise<Coordinator> {
return this.queue.execute<Coordinator>(async () => {
const activeEpRsp = this.znp.waitFor(UnpiConstants.Type.AREQ, Subsystem.ZDO, 'activeEpRsp');
Expand Down
42 changes: 18 additions & 24 deletions src/adapter/z-stack/znp/znp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
Frame as UnpiFrame,
} from '../unpi';

import {Wait, Queue, Waitress, EqualsPartial} from '../../../utils';
import {Wait, Queue, Waitress} from '../../../utils';

import SerialPortUtils from '../../serialPortUtils';

import ZpiObject from './zpiObject';
import {ZpiObjectPayload} from './tstype';
Expand Down Expand Up @@ -121,29 +123,6 @@ class Znp extends events.EventEmitter {
public async open(): Promise<void> {
const options = {baudRate: this.baudRate, rtscts: this.rtscts, autoOpen: false};

if (!this.path) {
debug.log(`No path provided, auto detecting...`);
const devices = (await SerialPort.list()).filter((d) => {
return autoDetectDefinitions.find((definition) => EqualsPartial(d, definition)) != null;
});

// CC1352P_2 and CC26X2R1 lists as 2 USB devices with same manufacturer, productId and vendorId
// one is the actual chip interface, other is the XDS110.
// The chip is always exposed on the first one after alphabetical sorting.
// @ts-ignore; not sure why this is needed as path exists (definition is wrong?)
devices.sort((a, b) => (a.path < b.path) ? -1 : 1);

const device = devices.length > 0 ? devices[0] : null;

if (device) {
// @ts-ignore; not sure why this is needed as path exists (definition is wrong?)
this.path = device.path;
debug.log(`Auto detected path '${this.path}'`);
} else {
throw new Error(`Failed to auto detect path`);
}
}

debug.log(`Opening with ${this.path} and ${JSON.stringify(options)}`);
this.serialPort = new SerialPort(this.path, options);

Expand Down Expand Up @@ -185,6 +164,21 @@ class Znp extends events.EventEmitter {
await Wait(1000);
}

public static async isValidPath(path: string): Promise<boolean> {
return SerialPortUtils.is(path, autoDetectDefinitions);
}

public static async autoDetectPath(): Promise<string> {
const paths = await SerialPortUtils.find(autoDetectDefinitions);

// CC1352P_2 and CC26X2R1 lists as 2 USB devices with same manufacturer, productId and vendorId
// one is the actual chip interface, other is the XDS110.
// The chip is always exposed on the first one after alphabetical sorting.
paths.sort((a, b) => (a < b) ? -1 : 1);

return paths.length > 0 ? paths[0] : null;
}

public close(): Promise<void> {
return new Promise((resolve, reject): void => {
if (this.initialized) {
Expand Down
4 changes: 2 additions & 2 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import events from 'events';
import Database from './database';
import {TsType as AdapterTsType, ZStackAdapter, Adapter, Events as AdapterEvents} from '../adapter';
import {TsType as AdapterTsType, Adapter, Events as AdapterEvents} from '../adapter';
import {Entity, Device} from './model';
import {ZclFrameConverter} from './helpers';
import * as Events from './events';
Expand Down Expand Up @@ -67,7 +67,6 @@ class Controller extends events.EventEmitter {
public constructor(options: Options) {
super();
this.options = mixin(DefaultOptions, options);
this.adapter = new ZStackAdapter(this.options.network, this.options.serialPort, this.options.backupPath);

// Validate options
for (const channel of this.options.network.channelList) {
Expand All @@ -81,6 +80,7 @@ class Controller extends events.EventEmitter {
* Start the Herdsman controller
*/
public async start(): Promise<void> {
this.adapter = await Adapter.create(this.options.network, this.options.serialPort, this.options.backupPath);
debug.log(`Starting with options '${JSON.stringify(this.options)}'`);
this.database = Database.open(this.options.databasePath);
const startResult = await this.adapter.start();
Expand Down
18 changes: 16 additions & 2 deletions test/adapter/z-stack/zStackAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "regenerator-runtime/runtime";
import {Znp} from '../../../src/adapter/z-stack/znp';
import {ZStackAdapter} from '../../../src/adapter';
import {ZStackAdapter} from '../../../src/adapter/z-stack/adapter';
import {Constants as UnpiConstants} from '../../../src/adapter/z-stack/unpi';
import equals from 'fast-deep-equal';
import * as Constants from '../../../src/adapter/z-stack/constants';
Expand Down Expand Up @@ -189,6 +189,8 @@ const serialPortOptions = {
path: 'dummy',
};

Znp.isValidPath = jest.fn().mockReturnValue(true);
Znp.autoDetectPath = jest.fn().mockReturnValue("/dev/autodetected");

describe('zStackAdapter', () => {
let adapter;
Expand All @@ -204,6 +206,18 @@ describe('zStackAdapter', () => {
networkOptions.networkKeyDistribute = false;
});

it('Is valid path', async () => {
const result = await ZStackAdapter.isValidPath("/dev/autodetected");
expect(result).toBeTruthy();
expect(Znp.isValidPath).toHaveBeenCalledWith("/dev/autodetected");
});

it('Auto detect path', async () => {
const result = await ZStackAdapter.autoDetectPath();
expect(result).toBe("/dev/autodetected");
expect(Znp.autoDetectPath).toHaveBeenCalledTimes(1);
});

it('Call znp constructor', async () => {
expect(Znp).toBeCalledWith("dummy", 800, false);
});
Expand Down Expand Up @@ -1333,7 +1347,7 @@ describe('zStackAdapter', () => {

const result = await adapter.start();
const actualBackup = await adapter.backup();
delete backup.time;
delete backup['time'];
delete actualBackup.time;
expect(equals(backup, actualBackup)).toBeTruthy();
});
Expand Down
30 changes: 14 additions & 16 deletions test/adapter/z-stack/znp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,36 +98,34 @@ describe('ZNP', () => {
});

it('Open autodetect port', async () => {
znp = new Znp(null, 100, true);
mockSerialPortList.mockReturnValue([
{manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'},
{path: '/dev/tty.usbmodemL43001T22', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'},
{path: '/dev/tty.usbmodemL43001T24', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'},
{path: '/dev/tty.usbmodemL43001T21', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'},
]);
await znp.open();

expect(SerialPort).toHaveBeenCalledTimes(1);
expect(SerialPort).toHaveBeenCalledWith(
"/dev/tty.usbmodemL43001T21",
{"autoOpen": false, "baudRate": 100, "rtscts": true},
);
expect(await Znp.autoDetectPath()).toBe("/dev/tty.usbmodemL43001T21");
});

it('Open autodetect port error when there are not available devices', async () => {
znp = new Znp(null, 100, true);
it('Autodetect port error when there are not available devices', async () => {
mockSerialPortList.mockReturnValue([
{manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'},
])

let error;
try {
await znp.open();
} catch (e) {
error = e;
}
expect(await Znp.autoDetectPath()).toBeNull();
});

it('Check if path is valid', async () => {
mockSerialPortList.mockReturnValue([
{manufacturer: 'Not texas instruments', vendorId: '0451', productId: '16a8', path: '/dev/autodetected2'},
{path: '/dev/tty.usbmodemL43001T22', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'},
{path: '/dev/tty.usbmodemL43001T24', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'},
{path: '/dev/tty.usbmodemL43001T21', manufacturer: 'Texas Instruments', vendorId: '0451', productId: 'bef3'},
])

expect(error).toStrictEqual(new Error('Failed to auto detect path'))
expect(await Znp.isValidPath('/dev/tty.usbmodemL43001T21')).toBeTruthy();
expect(await Znp.isValidPath('/dev/autodetected2')).toBeFalsy();
});

it('Open with error', async () => {
Expand Down
Loading

0 comments on commit bd7dfff

Please sign in to comment.