Build a Custom Command
You can build your own command simply by extending the Command
class and implementing the getApdu
and parseResponse
methods.
Then you can use the sendCommand
method to send it to a connected device.
This is strongly recommended over direct usage of sendApdu
.
Prerequisites
Before building custom commands, ensure you have:
- Device Management Kit v1.0.0 or higher
- Understanding of APDU protocol basics
- A connected device session (see Getting Started)
Quick Example
Here’s a complete example of a custom command:
import {
Command,
CommandResult,
CommandResultFactory,
CommandUtils,
GlobalCommandErrorHandler,
ApduBuilder,
ApduBuilderArgs,
ApduParser,
ApduResponse,
InvalidStatusWordError,
} from "@ledgerhq/device-management-kit";
export interface MyCustomCommandArgs {
customParam: number;
}
export interface MyCustomResponse {
customAttributes1: number;
customAttributes2: string;
}
export class MyCustomCommand
implements Command<MyCustomResponse, MyCustomCommandArgs>
{
args: MyCustomCommandArgs;
constructor(args: MyCustomCommandArgs) {
this.args = args;
}
getApdu(): Apdu {
// Main args for the APDU
const apduArgs: ApduBuilderArgs = {
cla: 0xe0, // Command CLA
ins: 0x02, // Command INS
p1: 0x00, // Parameter P1
p2: 0x00, // Parameter P2
};
// Add the data attached to the APDU with the builder
const builder = new ApduBuilder(apduArgs);
builder.add32BitUIntToData(this.args.customParam);
return builder.build();
}
parseResponse(response: ApduResponse): CommandResult<MyCustomResponse> {
// Create Apdu Parser
const parser = new ApduParser(response);
// FIRST: check status word
if (!CommandUtils.isSuccessResponse(response)) {
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
}
// Extract fields from the response
const customAttributes1 = parser.extract8BitUInt();
if (customAttributes1 === undefined) {
return CommandResultFactory({
error: new InvalidStatusWordError("Custom attribute 1 is missing"),
});
}
const customAttributes2 = parser.encodeToString(
parser.extractFieldLVEncoded(),
);
if (!customAttributes2) {
return CommandResultFactory({
error: new InvalidStatusWordError("Custom attribute 2 is missing"),
});
}
return CommandResultFactory({
data: {
customAttributes1,
customAttributes2,
},
});
}
}
Usage
Once you’ve created your custom command, use it with the Device Management Kit:
import { MyCustomCommand } from "./MyCustomCommand";
// Create and send your custom command
const command = new MyCustomCommand({ customParam: 42 });
const result = await dmk.sendCommand({
sessionId,
command,
});
if (result.isSuccess()) {
console.log("Custom attributes:", result.data);
} else {
console.error("Command failed:", result.error);
}
ApduBuilder Usage
The ApduBuilder
class helps you construct APDU commands with proper data encoding:
const builder = new ApduBuilder({
cla: 0xe0,
ins: 0x02,
p1: 0x00,
p2: 0x00,
});
// Add different data types
builder.add8BitUIntToData(0x01); // Single byte
builder.add16BitUIntToData(0x1234); // Two bytes
builder.add32BitUIntToData(0x12345678); // Four bytes
builder.addHexaStringToData("deadbeef"); // Hex string
builder.addAsciiStringToData("hello"); // ASCII string
builder.addBufferToData(buffer); // Raw buffer
const apdu = builder.build();
Available Builder Methods
Method | Description | Example |
---|---|---|
add8BitUIntToData(value) | Add single byte | builder.add8BitUIntToData(255) |
add16BitUIntToData(value) | Add 2-byte integer | builder.add16BitUIntToData(65535) |
add32BitUIntToData(value) | Add 4-byte integer | builder.add32BitUIntToData(0xFFFFFFFF) |
addHexaStringToData(hex) | Add hex string | builder.addHexaStringToData("deadbeef") |
addAsciiStringToData(text) | Add ASCII text | builder.addAsciiStringToData("hello") |
addBufferToData(buffer) | Add raw buffer | builder.addBufferToData(new Uint8Array([1,2,3])) |
ApduParser Usage
The ApduParser
class helps you extract data from APDU responses:
const parser = new ApduParser(response);
// Extract different data types
const byte = parser.extract8BitUInt();
const short = parser.extract16BitUInt();
const int = parser.extract32BitUInt();
// Extract fields by length
const fixedField = parser.extractFieldByLength(32);
// Extract length-value encoded fields
const lvField = parser.extractFieldLVEncoded();
// Convert to different formats
const hexString = parser.encodeToHexaString(field);
const asciiString = parser.encodeToString(field);
Available Parser Methods
Method | Description | Returns |
---|---|---|
extract8BitUInt() | Extract 1-byte integer | number | undefined |
extract16BitUInt() | Extract 2-byte integer | number | undefined |
extract32BitUInt() | Extract 4-byte integer | number | undefined |
extractFieldByLength(length) | Extract fixed-length field | Uint8Array | undefined |
extractFieldLVEncoded() | Extract length-value field | Uint8Array | undefined |
encodeToHexaString(field) | Convert to hex string | string |
encodeToString(field) | Convert to ASCII string | string |
Error Handling and Troubleshooting
Common Error Patterns
Always implement proper error handling in your parseResponse
method:
parseResponse(response: ApduResponse): CommandResult<MyResponse> {
const parser = new ApduParser(response);
// 1. Check status word first
if (!CommandUtils.isSuccessResponse(response)) {
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
}
// 2. Validate extracted fields
const field = parser.extract8BitUInt();
if (field === undefined) {
return CommandResultFactory({
error: new InvalidStatusWordError("Required field is missing"),
});
}
// 3. Check remaining data length
if (parser.getUnparsedRemainingLength() < expectedLength) {
return CommandResultFactory({
error: new InvalidStatusWordError("Insufficient response data"),
});
}
return CommandResultFactory({ data: { field } });
}
Common Error Codes
Status Code | Error Type | Description | Solution |
---|---|---|---|
5515 | DeviceLockedError | Device is locked | Ask user to unlock device |
5501 | ActionRefusedError | User refused action | Retry or inform user |
5502 | PinNotSetError | PIN not set | Guide user to set PIN |
6e00 | CLA not supported | Invalid command class | Check your CLA value |
6d00 | INS not supported | Invalid instruction | Check your INS value |
6a86 | Incorrect parameters | Invalid P1/P2 | Verify parameter values |
Debugging Tips
-
Enable Logging: Use the logger to debug command execution:
// The SDK automatically logs APDU exchanges when in development mode
-
Validate APDU Construction: Check your APDU before sending:
const apdu = builder.build(); console.log("APDU:", apdu.getRawApdu());
-
Check Response Length: Ensure you’re not reading beyond the response:
const parser = new ApduParser(response); console.log("Response length:", response.data.length); console.log("Remaining:", parser.getUnparsedRemainingLength());
Custom Error Types
For app-specific errors, create custom error types:
export type MyCommandErrorCodes = "custom_error_1" | "custom_error_2";
export const MY_COMMAND_ERRORS = {
"6f01": { message: "Custom error 1", tag: "CustomError1" },
"6f02": { message: "Custom error 2", tag: "CustomError2" },
} as const;
export class MyCommandError extends DeviceExchangeError<MyCommandErrorCodes> {
constructor(args: CommandErrorArgs<MyCommandErrorCodes>) {
super(args);
}
}
// In your parseResponse method:
if (!CommandUtils.isSuccessResponse(response)) {
const errorCode = parser.encodeToHexaString(response.statusCode);
if (isCommandErrorCode(errorCode, MY_COMMAND_ERRORS)) {
return CommandResultFactory({
error: new MyCommandError({
...MY_COMMAND_ERRORS[errorCode],
errorCode,
}),
});
}
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(response),
});
}
Examples and References
Built-in Command Examples
Study these existing commands for reference:
ListAppsCommand
- Simple command with parsingGetOsVersionCommand
- Complex response parsingOpenAppCommand
- Command with parameters
Sample Applications
See complete implementations in:
- Sample App - Web-based example
- Mobile App - React Native example