Skip to content

Commit

Permalink
Merge pull request #111 from particle-iot/enter-safe-mode-from-dfu/sc…
Browse files Browse the repository at this point in the history
…-127946

Allow entering safe mode from DFU
  • Loading branch information
sergeuz committed Jun 20, 2024
2 parents 08d4053 + 2e647b4 commit d98eec3
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 13 deletions.
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
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

0 comments on commit d98eec3

Please sign in to comment.