Skip to content

Commit

Permalink
✨ (core) [DSDK-218]: Polling on connected device (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
jiyuzhuang authored May 29, 2024
2 parents 7a29a06 + fa11c16 commit 0f797f0
Show file tree
Hide file tree
Showing 19 changed files with 458 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-eels-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-sdk-core": minor
---

Polling on connected device to get device status.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ enum FlagMasks {
ISSUE_TEMPERATURE = 0x00000020,
}

type BatteryStatusFlags = {
export type BatteryStatusFlags = {
charging: ChargingMode;
issueCharging: boolean;
issueTemperature: boolean;
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/api/command/os/GetOsVersionCommand.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Apdu } from "@api/apdu/model/Apdu";
import { ApduBuilder } from "@api/apdu/utils/ApduBuilder";
import { ApduBuilder, ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
import { Command } from "@api/command/Command";
import { InvalidStatusWordError } from "@api/command/Errors";
Expand Down Expand Up @@ -64,13 +64,15 @@ export type GetOsVersionResponse = {
* Command to get information about the device firmware.
*/
export class GetOsVersionCommand implements Command<GetOsVersionResponse> {
getApdu = (): Apdu =>
new ApduBuilder({
getApdu(): Apdu {
const getOsVersionApduArgs: ApduBuilderArgs = {
cla: 0xe0,
ins: 0x01,
p1: 0x00,
p2: 0x00,
}).build();
} as const;
return new ApduBuilder(getOsVersionApduArgs).build();
}

parseResponse(responseApdu: ApduResponse, deviceModelId: DeviceModelId) {
const parser = new ApduParser(responseApdu);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import { DeviceSessionService } from "@internal/device-session/service/DeviceSes
import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";

import {
SendCommandUseCase,
// SendCommandUseCaseArgs,
} from "./SendCommandUseCase";
import { SendCommandUseCase } from "./SendCommandUseCase";

let logger: LoggerPublisherService;
let sessionService: DeviceSessionService;
Expand All @@ -32,7 +29,7 @@ describe("SendCommandUseCase", () => {
});

it("should send a command to a connected device", async () => {
const deviceSession = deviceSessionStubBuilder();
const deviceSession = deviceSessionStubBuilder({}, () => logger);
sessionService.addDeviceSession(deviceSession);
const useCase = new SendCommandUseCase(sessionService, () => logger);

Expand Down
120 changes: 109 additions & 11 deletions packages/core/src/api/device-session/DeviceSessionState.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,115 @@
import { BatteryStatusFlags } from "@api/command/os/GetBatteryStatusCommand";
import { DeviceStatus } from "@api/device/DeviceStatus";
import { DeviceSessionId } from "@api/device-session/types";

export type SessionStateConstructorArgs = {
sessionId: DeviceSessionId;
deviceStatus: DeviceStatus;
/**
* The battery status of a device.
*/
export type BatteryStatus = {
level: number;
voltage: number;
temperature: number;
current: number;
status: BatteryStatusFlags;
};

export class DeviceSessionState {
public readonly sessionId: DeviceSessionId;
public readonly deviceStatus: DeviceStatus;
/**
* The firmware version of a device.
*/
export type FirmwareVersion = {
/**
* Microcontroller Unit version
*/
mcu: string;

/**
* Bootloader version
*/
bootloader: string;

constructor({ sessionId, deviceStatus }: SessionStateConstructorArgs) {
this.sessionId = sessionId;
this.deviceStatus = deviceStatus;
}
/**
* Operating System version
*/
os: string;
};

/**
* The state types of a device session.
*/
export enum DeviceSessionStateType {
Connected,
ReadyWithoutSecureChannel,
ReadyWithSecureChannel,
}

type DeviceSessionBaseState = {
readonly sessionStateType: DeviceSessionStateType;

/**
* The status of the device.
*/
deviceStatus: DeviceStatus;

/**
* The name of the device.
*/
deviceName?: string;
};

type DeviceSessionReadyState = {
/**
* The battery status of the device.
* TODO: This should not be optional, but it is not in the current implementation.
*/
batteryStatus?: BatteryStatus;

/**
* The firmware version of the device.
* TODO: This should not be optional, but it is not in the current implementation.
*/
firmwareVersion?: FirmwareVersion;

/**
* The current application running on the device.
*/
currentApp: string;
};

/**
* The state of a connected device session.
*/
export type ConnectedState = DeviceSessionBaseState & {
/**
* The type of the device session state.
*/
readonly sessionStateType: DeviceSessionStateType.Connected;
};

/**
* The state of a device session when it is ready without a secure channel.
*/
export type ReadyWithoutSecureChannelState = DeviceSessionBaseState &
DeviceSessionReadyState & {
/**
* The type of the device session state.
*/
readonly sessionStateType: DeviceSessionStateType.ReadyWithoutSecureChannel;
};

/**
* The state of a device session when it is ready with a secure channel.
*/
export type ReadyWithSecureChannelState = DeviceSessionBaseState &
DeviceSessionReadyState & {
/**
* The type of the device session state.
*/
readonly sessionStateType: DeviceSessionStateType.ReadyWithSecureChannel;
};

/**
* The state of a device session.
*/
export type DeviceSessionState =
| ConnectedState
| ReadyWithoutSecureChannelState
| ReadyWithSecureChannelState;
2 changes: 1 addition & 1 deletion packages/core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ export { LogLevel } from "./logger-subscriber/model/LogLevel";
export { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger";
export * from "./types";
export { ConnectedDevice } from "./usb/model/ConnectedDevice";
export { DeviceSessionState } from "@api/device-session/DeviceSessionState";
export { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import semver from "semver";
import { DeviceModelId } from "@api/device/DeviceModel";

/**
* Represents the info of a device model
* The info of a device model
*/
export class InternalDeviceModel {
id: DeviceModelId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import {
DeviceSession,
SessionConstructorArgs,
} from "@internal/device-session/model/DeviceSession";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { connectedDeviceStubBuilder } from "@internal/usb/model/InternalConnectedDevice.stub";

export const deviceSessionStubBuilder = (
props: Partial<SessionConstructorArgs> = {},
loggerFactory: (tag: string) => LoggerPublisherService,
) =>
new DeviceSession({
connectedDevice: connectedDeviceStubBuilder(),
id: "fakeSessionId",
...props,
});
new DeviceSession(
{
connectedDevice: connectedDeviceStubBuilder(),
id: "fakeSessionId",
...props,
},
loggerFactory,
);
61 changes: 42 additions & 19 deletions packages/core/src/internal/device-session/model/DeviceSession.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { inject } from "inversify";
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from "uuid";

import { Command } from "@api/command/Command";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { DeviceModelId } from "@api/device/DeviceModel";
import { DeviceStatus } from "@api/device/DeviceStatus";
import { DeviceSessionState } from "@api/device-session/DeviceSessionState";
import {
DeviceSessionState,
DeviceSessionStateType,
} from "@api/device-session/DeviceSessionState";
import { DeviceSessionId } from "@api/device-session/types";
import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes";
import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService";
import { InternalConnectedDevice } from "@internal/usb/model/InternalConnectedDevice";

import { DeviceSessionRefresher } from "./DeviceSessionRefresher";

export type SessionConstructorArgs = {
connectedDevice: InternalConnectedDevice;
id?: DeviceSessionId;
Expand All @@ -21,15 +29,28 @@ export class DeviceSession {
private readonly _id: DeviceSessionId;
private readonly _connectedDevice: InternalConnectedDevice;
private readonly _deviceState: BehaviorSubject<DeviceSessionState>;
private readonly _refresher: DeviceSessionRefresher;

constructor({ connectedDevice, id = uuidv4() }: SessionConstructorArgs) {
constructor(
{ connectedDevice, id = uuidv4() }: SessionConstructorArgs,
@inject(loggerTypes.LoggerPublisherServiceFactory)
loggerModuleFactory: (tag: string) => LoggerPublisherService,
) {
this._id = id;
this._connectedDevice = connectedDevice;
this._deviceState = new BehaviorSubject<DeviceSessionState>(
new DeviceSessionState({
sessionId: this._id,
this._deviceState = new BehaviorSubject<DeviceSessionState>({
sessionStateType: DeviceSessionStateType.Connected,
deviceStatus: DeviceStatus.CONNECTED,
});
this._refresher = new DeviceSessionRefresher(
{
refreshInterval: 1000,
deviceStatus: DeviceStatus.CONNECTED,
}),
sendApduFn: (rawApdu: Uint8Array) => this.sendApdu(rawApdu),
updateStateFn: (state: DeviceSessionState) =>
this.setDeviceSessionState(state),
},
loggerModuleFactory("device-session-refresher"),
);
}

Expand All @@ -45,28 +66,30 @@ export class DeviceSession {
return this._deviceState.asObservable();
}

public setDeviceSessionState(state: DeviceSessionState) {
this._deviceState.next(state);
}

private updateDeviceStatus(deviceStatus: DeviceStatus) {
const sessionState = this._deviceState.getValue();
this._deviceState.next(
new DeviceSessionState({
...sessionState,
deviceStatus,
}),
);
this._refresher.setDeviceStatus(deviceStatus);
this._deviceState.next({
...sessionState,
deviceStatus,
});
}

async sendApdu(rawApdu: Uint8Array) {
this.updateDeviceStatus(DeviceStatus.BUSY);

const errorOrResponse = await this._connectedDevice.sendApdu(rawApdu);

return errorOrResponse.map((response) => {
this.updateDeviceStatus(
CommandUtils.isLockedDeviceResponse(response)
? DeviceStatus.LOCKED
: DeviceStatus.CONNECTED,
);
return response;
return errorOrResponse.ifRight((response) => {
if (CommandUtils.isLockedDeviceResponse(response)) {
this.updateDeviceStatus(DeviceStatus.LOCKED);
} else {
this.updateDeviceStatus(DeviceStatus.CONNECTED);
}
});
}

Expand Down
Loading

0 comments on commit 0f797f0

Please sign in to comment.