Exchange data with the device
In this guide, you’ll learn how to send commands to a connected Ledger device and process the responses using the Device Management Kit (DMK). By the end, you’ll understand multiple ways to communicate with your hardware device.
What you’ll learn
- How to send low-level APDU commands to a device
- How to use pre-defined commands for common operations
- How to work with device actions for complex flows
- How to handle user interactions during device communication
Prerequisites
Before starting this tutorial, make sure you’ve completed:
You should have a working DMK instance and a connected device with a valid sessionId
.
Step 1: Sending an APDU command
APDU (Application Protocol Data Unit) commands are the low-level protocol used to communicate with Ledger devices. While it’s generally recommended to use higher-level abstractions, understanding APDUs can be helpful for advanced use cases.
import {
ApduBuilder,
ApduParser,
CommandUtils,
} from "@ledgerhq/device-management-kit";
// Build the APDU to open the Bitcoin app
const openAppApduArgs = {
cla: 0xe0,
ins: 0xd8,
p1: 0x00,
p2: 0x00,
};
const apdu = new ApduBuilder(openAppApduArgs)
.addAsciiStringToData("Bitcoin")
.build();
// Send the APDU to the device
const apduResponse = await dmk.sendApdu({ sessionId, apdu });
// Parse the result
const parser = new ApduParser(apduResponse);
// Check if the command was successful
if (!CommandUtils.isSuccessResponse(apduResponse)) {
throw new Error(
`Unexpected status word: ${parser.encodeToHexaString(
apduResponse.statusCode,
)}`,
);
}
Step 2: Using pre-defined commands
For most common operations, the DMK provides pre-defined commands that are easier to use and maintain. The sendCommand
method handles building the APDU, sending it to the device, and parsing the response.
❗️ Error Handling
Most commands will reject with an error if:
- The device is locked - check the device session state with
dmk.getDeviceSessionState({ sessionId })
first- The response status word is not
0x9000
(success)
Opening an app
import { OpenAppCommand } from "@ledgerhq/device-management-kit";
// Create the command to open the Bitcoin app
const command = new OpenAppCommand("Bitcoin");
// Send the command and wait for the user to confirm on the device
await dmk.sendCommand({ sessionId, command });
Closing an app
import { CloseAppCommand } from "@ledgerhq/device-management-kit";
// Create the command to close the current app
const command = new CloseAppCommand();
// Send the command
await dmk.sendCommand({ sessionId, command });
Getting OS information
import { GetOsVersionCommand } from "@ledgerhq/device-management-kit";
// Create the command to get OS version information
const command = new GetOsVersionCommand();
// Send the command and get the response
const { seVersion, mcuSephVersion, mcuBootloaderVersion } =
await dmk.sendCommand({ sessionId, command });
console.log(`OS Version: ${seVersion}`);
ℹ️ Tip: This information is also available in the device session state, which you can access with
dmk.getDeviceSessionState({ sessionId })
.
Getting app information
import { GetAppAndVersionCommand } from "@ledgerhq/device-management-kit";
// Create the command to get app information
const command = new GetAppAndVersionCommand();
// Send the command and get the response
const { name, version } = await dmk.sendCommand({ sessionId, command });
console.log(`Running app: ${name} v${version}`);
Step 3: Working with Device Actions
Device actions define a sequence of commands to be sent to the device. They’re useful for complex operations that require user interaction, like opening an app and performing operations within it.
Device actions return an observable that emits different states during execution, allowing your application to respond to intermediate steps.
Opening an app with a Device Action
import {
OpenAppDeviceAction,
OpenAppDAState,
DeviceActionStatus,
UserActionRequiredType,
} from "@ledgerhq/device-management-kit";
// Create the device action
const openAppDeviceAction = new OpenAppDeviceAction({ appName: "Bitcoin" });
// Execute the device action
const { observable, cancel } = await dmk.executeDeviceAction({
sessionId,
deviceAction: openAppDeviceAction,
});
// Subscribe to state changes
observable.subscribe({
next: (state: OpenAppDAState) => {
switch (state.status) {
case DeviceActionStatus.NotStarted:
console.log("Action not started yet");
break;
case DeviceActionStatus.Pending:
const {
intermediateValue: { userActionRequired },
} = state;
switch (userActionRequired) {
case UserActionRequiredType.None:
console.log("No user action required");
break;
case UserActionRequiredType.ConfirmOpenApp:
console.log(
"The user should confirm the app opening on the device",
);
break;
case UserActionRequiredType.UnlockDevice:
console.log("The user should unlock the device");
break;
default:
// Always handle all possible user actions
throw new Error("Unhandled user action required");
}
break;
case DeviceActionStatus.Stopped:
console.log("Action has been cancelled");
break;
case DeviceActionStatus.Completed:
const { output } = state;
console.log("App opened successfully", output);
break;
case DeviceActionStatus.Error:
const { error } = state;
console.error("An error occurred during the action", error);
break;
}
},
error: (error) => {
console.error("Subscription error:", error);
},
complete: () => {
console.log("Device action complete");
},
});
// If you need to cancel the action
// cancel();
What you’ve learned
In this tutorial, you’ve learned how to:
- Send low-level APDU commands to a Ledger device
- Use pre-defined commands for common operations
- Work with device actions for complex flows
- Handle user interactions during device communication
Next steps
- Check out the sample app a comprehensive demonstration of the Device Management Kit’s capabilities in a React application.
- Learn how to build custom commands for specific applications
- Explore how to handle errors during device communication