DocumentationDevice interactionIntegration WalkthroughsHow to ...Build a custom command

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

MethodDescriptionExample
add8BitUIntToData(value)Add single bytebuilder.add8BitUIntToData(255)
add16BitUIntToData(value)Add 2-byte integerbuilder.add16BitUIntToData(65535)
add32BitUIntToData(value)Add 4-byte integerbuilder.add32BitUIntToData(0xFFFFFFFF)
addHexaStringToData(hex)Add hex stringbuilder.addHexaStringToData("deadbeef")
addAsciiStringToData(text)Add ASCII textbuilder.addAsciiStringToData("hello")
addBufferToData(buffer)Add raw bufferbuilder.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

MethodDescriptionReturns
extract8BitUInt()Extract 1-byte integernumber | undefined
extract16BitUInt()Extract 2-byte integernumber | undefined
extract32BitUInt()Extract 4-byte integernumber | undefined
extractFieldByLength(length)Extract fixed-length fieldUint8Array | undefined
extractFieldLVEncoded()Extract length-value fieldUint8Array | undefined
encodeToHexaString(field)Convert to hex stringstring
encodeToString(field)Convert to ASCII stringstring

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 CodeError TypeDescriptionSolution
5515DeviceLockedErrorDevice is lockedAsk user to unlock device
5501ActionRefusedErrorUser refused actionRetry or inform user
5502PinNotSetErrorPIN not setGuide user to set PIN
6e00CLA not supportedInvalid command classCheck your CLA value
6d00INS not supportedInvalid instructionCheck your INS value
6a86Incorrect parametersInvalid P1/P2Verify parameter values

Debugging Tips

  1. Enable Logging: Use the logger to debug command execution:

    // The SDK automatically logs APDU exchanges when in development mode
  2. Validate APDU Construction: Check your APDU before sending:

    const apdu = builder.build();
    console.log("APDU:", apdu.getRawApdu());
  3. 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:

Sample Applications

See complete implementations in:

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Nano S, Ledger Vault, Bolos are trademarks owned by Ledger SAS