Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow entering safe mode from DFU #111

Merged
merged 7 commits into from
Jun 20, 2024
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
3 changes: 3 additions & 0 deletions src/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ class Device extends DeviceBase {
* @return {Promise}
*/
enterSafeMode({ timeout = globalOptions.requestTimeout } = {}) {
if (this.isInDfuMode) {
return this._dfu.enterSafeMode();
}
return this.sendRequest(Request.SAFE_MODE, null /* msg */, { timeout });
}

Expand Down
53 changes: 43 additions & 10 deletions src/dfu.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

const { DeviceError, UsbStallError, DeviceProtectionError } = require('./error');
const { DeviceError, UsbStallError, DeviceProtectionError, UnsupportedDfuseCommandError } = require('./error');

/**
* A generic DFU error.
Expand Down Expand Up @@ -131,7 +131,8 @@ const DfuseCommand = {
DFUSE_COMMAND_GET_COMMAND: 0x00,
DFUSE_COMMAND_SET_ADDRESS_POINTER: 0x21,
DFUSE_COMMAND_ERASE: 0x41,
DFUSE_COMMAND_READ_UNPROTECT: 0x92
DFUSE_COMMAND_READ_UNPROTECT: 0x92,
DFUSE_COMMAND_ENTER_SAFE_MODE: 0xfa // Particle's extension
};

const DfuBmRequestType = {
Expand Down Expand Up @@ -170,6 +171,7 @@ class Dfu {
let desc = await this._getConfigDescriptor(0); // Use the default config
desc = this._parseConfigDescriptor(desc);
this._allInterfaces = desc.interfaces;
this._supportedDfuseCommands = [];
}

/**
Expand All @@ -189,7 +191,7 @@ class Dfu {
* @return {Promise}
*/
async leave() {
await this._goIntoDfuIdleOrDfuDnloadIdle();
await this._goIntoIdleState({ dnloadIdle: true });

await this._sendDnloadRequest(Buffer.alloc(0), 2);

Expand All @@ -201,6 +203,20 @@ class Dfu {
state => (state === DfuDeviceState.dfuMANIFEST || state === DfuDeviceState.dfuDNLOAD_IDLE));
}

/**
* Enter safe mode.
*
* @returns {Promise}
*/
async enterSafeMode() {
await this._checkDfuseCommandSupported(DfuseCommand.DFUSE_COMMAND_ENTER_SAFE_MODE);
await this._goIntoIdleState({ dnloadIdle: true });
const data = Buffer.alloc(1);
data[0] = DfuseCommand.DFUSE_COMMAND_ENTER_SAFE_MODE;
await this._sendDnloadRequest(data, 0 /* wValue */);
await this._pollUntil((state) => state === DfuDeviceState.dfuMANIFEST);
}

/**
* Set the alternate interface for DFU and initialize memory information.
*
Expand Down Expand Up @@ -328,15 +344,17 @@ class Dfu {
}
}

async _goIntoDfuIdleOrDfuDnloadIdle() {
async _goIntoIdleState({ dnloadIdle = false, uploadIdle = false } = {}) {
try {
const state = await this._getStatus();
if (state.state === DfuDeviceState.dfuERROR) {
// If we are in dfuERROR state, simply issue DFU_CLRSTATUS and we'll go into dfuIDLE
await this._clearStatus();
}

if (state.state !== DfuDeviceState.dfuIDLE && state.state !== DfuDeviceState.dfuDNLOAD_IDLE) {
if (state.state !== DfuDeviceState.dfuIDLE &&
!(dnloadIdle && state.state === DfuDeviceState.dfuDNLOAD_IDLE) &&
!(uploadIdle && state.state === DfuDeviceState.dfuUPLOAD_IDLE)) {
// If we are in some kind of an unknown state, issue DFU_CLRSTATUS, which may fail,
// but the device will go into dfuERROR state, so a subsequent DFU_CLRSTATUS will get us
// into dfuIDLE
Expand All @@ -347,9 +365,11 @@ class Dfu {
await this._clearStatus();
}

// Confirm we are in dfuIDLE or dfuDNLOAD_IDLE
// Confirm we are in dfuIDLE or, optionally, in dfuDNLOAD_IDLE or dfuUPLOAD_IDLE
const state = await this._getStatus();
if (state.state !== DfuDeviceState.dfuIDLE && state.state !== DfuDeviceState.dfuDNLOAD_IDLE) {
if (state.state !== DfuDeviceState.dfuIDLE &&
!(dnloadIdle && state.state === DfuDeviceState.dfuDNLOAD_IDLE) &&
!(uploadIdle && state.state === DfuDeviceState.dfuUPLOAD_IDLE)) {
throw new DfuError('Invalid state');
}
return state;
Expand Down Expand Up @@ -538,9 +558,11 @@ class Dfu {
}

const commandNames = {
0x00: 'GET_COMMANDS',
0x21: 'SET_ADDRESS',
0x41: 'ERASE_SECTOR'
[DfuseCommand.DFUSE_COMMAND_GET_COMMAND]: 'GET_COMMANDS',
[DfuseCommand.DFUSE_COMMAND_SET_ADDRESS_POINTER]: 'SET_ADDRESS',
[DfuseCommand.DFUSE_COMMAND_ERASE]: 'ERASE_SECTOR',
[DfuseCommand.DFUSE_COMMAND_READ_UNPROTECT]: 'READ_UNPROTECT',
[DfuseCommand.DFUSE_COMMAND_ENTER_SAFE_MODE]: 'ENTER_SAFE_MODE',
};

const payload = Buffer.alloc(5);
Expand Down Expand Up @@ -851,6 +873,17 @@ class Dfu {
throw new Error('Failed to return to idle state after abort: state ' + state.state);
}
}

async _checkDfuseCommandSupported(cmd) {
if (!this._supportedDfuseCommands.length) {
await this._goIntoIdleState({ uploadIdle: true });
const data = await this._sendUploadReqest(DEFAULT_TRANSFER_SIZE, 0 /* value */); // Get command
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already a part of the DFU implementation in Device OS so it will work on old devices, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this command is part of the DfuSe spec.

this._supportedDfuseCommands = [...data];
}
if (!this._supportedDfuseCommands.includes(cmd)) {
throw new UnsupportedDfuseCommandError('Unsupported DfuSe command');
}
}
}

module.exports = {
Expand Down
11 changes: 11 additions & 0 deletions src/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ class DeviceProtectionError extends DeviceError {
}
}

/**
* An error reported when the issued DfuSe command is not supported by the device.
*/
class UnsupportedDfuseCommandError extends DeviceError {
constructor(...args) {
super(...args);
this.name = this.constructor.name;
}
}

function assert(val, msg = null) {
if (!val) {
throw new InternalError(msg ? msg : 'Assertion failed');
Expand All @@ -139,5 +149,6 @@ module.exports = {
RequestError,
UsbStallError,
DeviceProtectionError,
UnsupportedDfuseCommandError,
assert
};
3 changes: 2 additions & 1 deletion src/particle-usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { WifiAntenna, WifiCipher, EapMethod, WifiSecurityEnum } = require('./wifi
const { WifiSecurity } = require('./wifi-device-legacy');
const { CloudConnectionStatus, ServerProtocol } = require('./cloud-device');
const { Result } = require('./result');
const { DeviceError, NotFoundError, NotAllowedError, StateError, TimeoutError, MemoryError, ProtocolError, UsbError, InternalError, RequestError, DeviceProtectionError } = require('./error');
const { DeviceError, NotFoundError, NotAllowedError, StateError, TimeoutError, MemoryError, ProtocolError, UsbError, InternalError, RequestError, DeviceProtectionError, UnsupportedDfuseCommandError } = require('./error');
const { config } = require('./config');
const { setDevicePrototype } = require('./set-device-prototype');

Expand Down Expand Up @@ -69,6 +69,7 @@ module.exports = {
InternalError,
RequestError,
DeviceProtectionError,
UnsupportedDfuseCommandError,
getDevices,
openDeviceById,
openNativeUsbDevice,
Expand Down
60 changes: 58 additions & 2 deletions test/dfu-device.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { fakeUsb, expect } = require('./support');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const { DfuDeviceState, DfuseCommand } = require('../src/dfu');
const { DfuDeviceState, DfuseCommand, DfuRequestType, DfuBmRequestType } = require('../src/dfu');
const { UnsupportedDfuseCommandError } = require('../src/error');

const { getDevices } = proxyquire('../src/particle-usb', {
'./device-base': proxyquire('../src/device-base', {
Expand Down Expand Up @@ -93,7 +94,7 @@ describe('dfu device', () => { // actually tests src/dfu.js which is the dfu dri
expect(argonDev.isOpen).to.be.true;
let error;
try {
await argonDev._dfu._goIntoDfuIdleOrDfuDnloadIdle();
await argonDev._dfu._goIntoIdleState({ dnloadIdle: true });
await argonDev._dfu._dfuseCommand(0x21, 0x08060000);
} catch (_error) {
error = _error;
Expand Down Expand Up @@ -163,5 +164,60 @@ describe('dfu device', () => { // actually tests src/dfu.js which is the dfu dri
expect(dfuseCommandStub.callCount).to.equal(247);
});
});

describe('enterSafeMode', () => {
let dev;
let dfuClass;

function mockDfuClassDevice(dev) {
const dfuClass = dev.usbDevice.dfuClass;
// DfuSe "Get" command
sinon.stub(dfuClass, 'deviceToHostRequest').withArgs(sinon.match({
bmRequestType: DfuBmRequestType.DEVICE_TO_HOST,
bRequest: DfuRequestType.DFU_UPLOAD,
wValue: 0
})).returns(Buffer.from([
0x00, // Get command
0xfa // Enter safe mode (Particle's extension)
]));
dfuClass.deviceToHostRequest.callThrough();
return dfuClass;
}

beforeEach(async () => {
fakeUsb.addBoron({ dfu: true });
const devs = await getDevices();
dev = devs[0];
dfuClass = mockDfuClassDevice(dev);
await dev.open();
});

afterEach(async () => {
if (dev) {
await dev.close();
}
});

it('sends a DfuSe command to the device', async () => {
sinon.spy(dfuClass, 'hostToDeviceRequest');
await dev.enterSafeMode();
expect(dfuClass.hostToDeviceRequest).to.be.calledWith(sinon.match({
bmRequestType: DfuBmRequestType.HOST_TO_DEVICE,
bRequest: DfuRequestType.DFU_DNLOAD,
wValue: 0
}), Buffer.from([0xfa]));
});

it('fails if the required DfuSe command is not supported', async () => {
dfuClass.deviceToHostRequest.withArgs(sinon.match({
bmRequestType: DfuBmRequestType.DEVICE_TO_HOST,
bRequest: DfuRequestType.DFU_UPLOAD,
wValue: 0
})).returns(Buffer.from([
0x00 // Get command
]));
await expect(dev.enterSafeMode()).to.be.eventually.rejectedWith(UnsupportedDfuseCommandError);
});
});
});
});
4 changes: 4 additions & 0 deletions test/support/fake-usb.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,10 @@ class Device {
get isOpen() {
return this._open;
}

get dfuClass() {
return this._dfu;
}
}

async function getUsbDevices(filters) {
Expand Down
Loading