Skip to content

Commit

Permalink
Merge pull request #462 from Zondax/dev
Browse files Browse the repository at this point in the history
Support for Flex device
  • Loading branch information
ftheirs authored Jul 19, 2024
2 parents 4f473a4 + d9eb0f3 commit 803c9ae
Show file tree
Hide file tree
Showing 15 changed files with 2,280 additions and 1,817 deletions.
893 changes: 0 additions & 893 deletions .yarn/releases/yarn-4.1.1.cjs

This file was deleted.

894 changes: 894 additions & 0 deletions .yarn/releases/yarn-4.3.1.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.1.1.cjs
yarnPath: .yarn/releases/yarn-4.3.1.cjs
38 changes: 19 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
"url": "https://github.com/zondax/zemu/issues"
},
"dependencies": {
"@grpc/grpc-js": "^1.10.6",
"@grpc/proto-loader": "^0.7.12",
"@ledgerhq/hw-transport-http": "^6.29.5",
"axios": "^1.6.8",
"axios-retry": "^4.1.0",
"@grpc/grpc-js": "^1.11.1",
"@grpc/proto-loader": "^0.7.13",
"@ledgerhq/hw-transport-http": "^6.30.1",
"axios": "^1.7.2",
"axios-retry": "^4.4.1",
"dockerode": "^4.0.2",
"elfy": "^1.0.0",
"fs-extra": "^11.2.0",
Expand All @@ -44,29 +44,29 @@
"randomstring": "^1.3.0"
},
"devDependencies": {
"@ledgerhq/hw-transport": "^6.30.5",
"@types/dockerode": "^3.3.28",
"@ledgerhq/hw-transport": "^6.31.1",
"@types/dockerode": "^3.3.30",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@types/pngjs": "^6.0.4",
"@types/node": "^20.14.11",
"@types/pngjs": "^6.0.5",
"@types/randomstring": "^1.3.0",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@zondax/ledger-substrate": "^0.44.2",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@zondax/ledger-substrate": "^0.44.7",
"copyfiles": "^2.4.1",
"eslint": "^8.56.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^17.6.20",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^17.9.0",
"eslint-plugin-promise": "^6.4.0",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.2",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.3",
"ts-node": "^10.9.2",
"typescript": "5.4.5"
"typescript": "5.5.3"
},
"files": [
"dist/**/*"
Expand Down
100 changes: 75 additions & 25 deletions src/Zemu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** ******************************************************************************
* (c) 2018 - 2023 Zondax AG
* (c) 2018 - 2024 Zondax AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -37,6 +37,7 @@ import {
WINDOW_STAX,
WINDOW_S,
WINDOW_X,
WINDOW_FLEX,
DEFAULT_STAX_APPROVE_KEYWORD,
DEFAULT_STAX_REJECT_KEYWORD,
DEFAULT_NANO_APPROVE_KEYWORD,
Expand All @@ -51,19 +52,21 @@ import EmuContainer from "./emulator";
import GRPCRouter from "./grpc";

import { ClickNavigation, scheduleToNavElement, TouchNavigation } from "./actions";
import { dummyButton, tapContinueButton, TouchElements } from "./buttons";
import { getTouchElement } from "./buttons";
import {
ActionKind,
ButtonKind,
SwipeDirection,
type IButton,
type IDeviceWindow,
type IEvent,
type INavElement,
type ISnapshot,
type IStartOptions,
type ISwipeCoordinates,
type TModel,
} from "./types";
import { zondaxToggleExpertMode, zondaxStaxEnableSpecialMode } from "./zondax";
import { isTouchDevice, zondaxToggleExpertMode, zondaxTouchEnableSpecialMode } from "./zondax";

export default class Zemu {
public startOptions!: IStartOptions;
Expand Down Expand Up @@ -146,6 +149,7 @@ export default class Zemu {
nanox: 0xc0de0001,
nanosp: 0xc0de0001,
stax: 0xc0de0001,
flex: 0xc0de0001,
};
const elfApp = fs.readFileSync(elfPath);
const elfInfo = elfy.parse(elfApp);
Expand All @@ -159,7 +163,7 @@ export default class Zemu {
this.startOptions = options;
const approveWord = options.approveKeyword;
const rejectWord = options.rejectKeyword;
if (options.model === "stax") {
if (isTouchDevice(options.model)) {
this.startOptions.approveKeyword = approveWord.length === 0 ? DEFAULT_STAX_APPROVE_KEYWORD : approveWord;
this.startOptions.rejectKeyword = rejectWord.length === 0 ? DEFAULT_STAX_REJECT_KEYWORD : rejectWord;
} else {
Expand Down Expand Up @@ -197,8 +201,9 @@ export default class Zemu {
this.log(`Wait for start text`);

if (this.startOptions.startText.length === 0) {
this.startOptions.startText =
this.startOptions.model === "stax" ? DEFAULT_STAX_START_TEXT : DEFAULT_NANO_START_TEXT;
this.startOptions.startText = isTouchDevice(this.startOptions.model)
? DEFAULT_STAX_START_TEXT
: DEFAULT_NANO_START_TEXT;
}
const start = new Date();
let found = false;
Expand All @@ -217,10 +222,9 @@ export default class Zemu {
}
const events = await this.getEvents();
if (!reviewPendingFound && events.some((event: IEvent) => reviewPendingRegex.test(event.text))) {
const nav =
this.startOptions.model === "stax"
? new TouchNavigation([ButtonKind.ConfirmYesButton])
: new ClickNavigation([0]);
const nav = isTouchDevice(this.startOptions.model)
? new TouchNavigation(this.startOptions.model, [ButtonKind.ConfirmYesButton])
: new ClickNavigation([0]);
await this.navigate("", "", nav.schedule, true, false);
reviewPendingFound = true;
}
Expand Down Expand Up @@ -307,6 +311,8 @@ export default class Zemu {
return WINDOW_X;
case "stax":
return WINDOW_STAX;
case "flex":
return WINDOW_FLEX;
default:
throw new Error(`model ${this.startOptions.model} not recognized`);
}
Expand Down Expand Up @@ -442,15 +448,15 @@ export default class Zemu {
async enableSpecialMode(
nanoModeText: string,
nanoIsSecretMode: boolean = false,
staxToggleSettingButton?: ButtonKind,
touchToggleSettingButton?: ButtonKind,
path = ".",
testcaseName = "",
waitForScreenUpdate = true,
takeSnapshots = false,
startImgIndex = 0,
timeout = DEFAULT_METHOD_TIMEOUT,
): Promise<number> {
if (this.startOptions.model !== "stax") {
if (!isTouchDevice(this.startOptions.model)) {
const expertImgIndex = await this.toggleExpertMode(testcaseName, takeSnapshots, startImgIndex);
let tmpImgIndex = await this.navigateUntilText(
path,
Expand All @@ -477,7 +483,7 @@ export default class Zemu {
timeout,
);
} else {
const nav = zondaxStaxEnableSpecialMode(staxToggleSettingButton);
const nav = zondaxTouchEnableSpecialMode(this.startOptions.model, touchToggleSettingButton);
return await this.navigate(path, testcaseName, nav.schedule, waitForScreenUpdate, takeSnapshots, startImgIndex);
}
}
Expand Down Expand Up @@ -588,7 +594,7 @@ export default class Zemu {
startImgIndex,
timeout,
);
if (this.startOptions.model === "stax") {
if (isTouchDevice(this.startOptions.model)) {
// Avoid taking a snapshot of the final animation
await this.waitUntilScreenIs(this.mainMenuSnapshot);
await this.takeSnapshotAndOverwrite(path, testcaseName, lastSnapshotIdx);
Expand All @@ -604,7 +610,7 @@ export default class Zemu {
timeout = DEFAULT_METHOD_TIMEOUT,
): Promise<boolean> {
const rejectKeyword = this.startOptions.rejectKeyword;
if (this.startOptions.model !== "stax") {
if (!isTouchDevice(this.startOptions.model)) {
return await this.navigateAndCompareUntilText(
path,
testcaseName,
Expand All @@ -616,7 +622,7 @@ export default class Zemu {
} else {
const takeSnapshots = true;
const runLastAction = false;
// For Stax devices navigate until reject keyword --> Reject --> Confirm rejection
// For Stax/Flex devices navigate until reject keyword --> Reject --> Confirm rejection
// reject keyword should be actually approve keyword (issue with OCR)
const navLastIndex = await this.navigateUntilText(
path,
Expand All @@ -628,7 +634,10 @@ export default class Zemu {
timeout,
runLastAction,
);
const rejectConfirmationNav = new TouchNavigation([ButtonKind.RejectButton, ButtonKind.ConfirmYesButton]);
const rejectConfirmationNav = new TouchNavigation(this.startOptions.model, [
ButtonKind.RejectButton,
ButtonKind.ConfirmYesButton,
]);
// Overwrite last snapshot since navigate starts taking a snapshot of the current screen
const lastIndex = await this.navigate(
path,
Expand Down Expand Up @@ -671,7 +680,7 @@ export default class Zemu {

let start = new Date();
let found = false;
const isStaxDevice = this.startOptions.model === "stax";
const touchDevice = isTouchDevice(this.startOptions.model);

const textRegex = new RegExp(text, "i");

Expand All @@ -691,8 +700,8 @@ export default class Zemu {
if (found) break;

const nav: INavElement = {
type: isStaxDevice ? ActionKind.Touch : ActionKind.RightClick,
button: tapContinueButton, // For clicks, this will be ignored
type: touchDevice ? ActionKind.Touch : ActionKind.RightClick,
button: getTouchElement(this.startOptions.model, ButtonKind.SwipeContinueButton), // For clicks, this will be ignored
};
await this.runAction(nav, filename, waitForScreenUpdate, true);
start = new Date();
Expand All @@ -701,11 +710,11 @@ export default class Zemu {
if (!runLastAction) return imageIndex; // do not run last action if requested

// Approve can be performed with Tap or PressAndHold
const staxApproveButton = TouchElements.get(this.startOptions.approveAction);
const approveButton = getTouchElement(this.startOptions.model, this.startOptions.approveAction);

const nav: INavElement = {
type: isStaxDevice ? ActionKind.Touch : ActionKind.BothClick,
button: staxApproveButton ?? dummyButton,
type: touchDevice ? ActionKind.Touch : ActionKind.BothClick,
button: approveButton,
};
await this.runAction(nav, filename, waitForScreenUpdate, true);
return imageIndex;
Expand Down Expand Up @@ -833,18 +842,59 @@ export default class Zemu {
return await this.click("/button/both", filename, waitForScreenUpdate, waitForEventsChange);
}

private getSwipeCoordinates(button: IButton): ISwipeCoordinates {
let newX = button.x;
let newY = button.y;
const SWIPE_PX_MOVEMENT = 10;

switch (button.direction) {
case SwipeDirection.SwipeUp:
newY += SWIPE_PX_MOVEMENT;
break;

case SwipeDirection.SwipeDown:
newY -= SWIPE_PX_MOVEMENT;
break;

case SwipeDirection.SwipeRight:
newX += SWIPE_PX_MOVEMENT;
break;

case SwipeDirection.SwipeLeft:
newX -= SWIPE_PX_MOVEMENT;
break;

case SwipeDirection.NoSwipe:
break;
}
return {
x: newX,
y: newY,
};
}

async fingerTouch(
button: IButton,
filename: string = "",
waitForScreenUpdate: boolean = true,
waitForEventsChange: boolean = false,
): Promise<ISnapshot> {
if (this.startOptions.model !== "stax") throw new Error("fingerTouch method can only be used with stax device");
if (!isTouchDevice(this.startOptions.model))
throw new Error("fingerTouch method can only be used with touchable devices");
const prevEvents = await this.getEvents();
const prevScreen = await this.snapshot();

const fingerTouchUrl = `${this.transportProtocol}://${this.host}:${this.speculosApiPort}/finger`;
const payload = { action: "press-and-release", x: button.x, y: button.y, delay: button.delay };

// Add x2, y2 params only for Swipe
const swipe = this.getSwipeCoordinates(button);
const payload = {
action: "press-and-release",
x: button.x,
y: button.y,
delay: button.delay,
...(button.direction !== SwipeDirection.NoSwipe ? { x2: swipe.x, y2: swipe.y } : {}),
};
await axios.post(fingerTouchUrl, payload);
this.log(`Touch /finger -> ${filename}`);

Expand Down
11 changes: 6 additions & 5 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** ******************************************************************************
* (c) 2018 - 2023 Zondax AG
* (c) 2018 - 2024 Zondax AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************* */
import { dummyButton, TouchElements } from "./buttons";
import { ActionKind, type ButtonKind, type INavElement } from "./types";
import { getTouchElement } from "./buttons";
import { ActionKind, ButtonKind, type TModel, type INavElement } from "./types";

export function scheduleToNavElement(clickSchedule: Array<INavElement | number>): INavElement[] {
const dummyButton = getTouchElement("nanos", ButtonKind.QuitAppButton);
const nav: INavElement[] = [];
for (const click of clickSchedule) {
if (typeof click !== "number") {
Expand Down Expand Up @@ -51,10 +52,10 @@ export class ClickNavigation {
export class TouchNavigation {
schedule: INavElement[];

constructor(buttonKindArray: ButtonKind[]) {
constructor(model: TModel, buttonKindArray: ButtonKind[]) {
this.schedule = [];
for (const buttonKind of buttonKindArray) {
const touchButton = TouchElements.get(buttonKind);
const touchButton = getTouchElement(model, buttonKind);
if (touchButton == null) throw new Error("Undefined touch action");
this.schedule.push({
type: ActionKind.Touch,
Expand Down
Loading

0 comments on commit 803c9ae

Please sign in to comment.