Multi-Device Batch Execution & Result Collection


Please refer to JavaScript Modules for more information.


How to Use

Overview:

This script example demonstrates how to use ArrayOutput to collect execution results from each device in a multi-device batch workflow, and how to read and analyze those results in a unified way using ArrayOutputReturn.
The example covers result structures, common access patterns, error checking, and debugging techniques, and can be directly copied, executed, and applied to real-world projects.

Before Running:

  1. Download and install Total Control 11.0 (Update 30) or later (Download)
  2. Connect Android devices (supports USB connection or TCP connection)
  3. Install SigmaTestApp on the Android devices as the test application (How to install)
  4. Select the test devices in the Total Control interface (Note: getDevices() in the script only returns the currently selected devices)
  5. Save the example code as a .js file (for example, TestArrayOutput.js) and place it in the Total Control default script directory
  6. Adjust the following configuration items according to your test scenario:
    • TARGET_PKG: Target app package name
    • SEARCH_TEXT: Text to be entered
    • SEARCH_X/Y: Relative coordinates of the search box (0–1)
    • LOG_DIR: Output directory for logs and screenshots
    • WAIT_LAUNCH_MS: App launch wait time
  7. Open the Script Terminal (Main Panel → Scripts → Script Terminal), then execute:
    >> sigmaLoad("TestArrayOutput.js");


Source Code

/**
 * ArrayOutput Full-Feature Example: Multi-Device Batch Execution + Detailed Walkthrough
 * (Ready to copy and run)
 *
 * What you will learn / see:
 * 1) ArrayOutput:
 *    - Used to collect execution results from each device (value + error)
 * 2) ArrayOutputReturn:
 *    - Used to read/query results (read-only wrapper)
 * 3) global.showOutputFlag:
 *    - When enabled, getReturn() automatically prints a structured table
 *    - Also assigns the latest ArrayOutputReturn to global.ao (global debug alias)
 * 4) Three forms of ret.get():
 *    - ret.get()                 => Returns the full records array
 *                                   (each item includes name / object / id / value / error)
 *    - ret.get(device)           => Returns the value for the specified device
 *    - ret.get([device...])      => Returns a value array aligned with the input order
 * 5) Dynamic helpers (objects / values / errors / ids / names):
 *    - No arguments: returns the entire column as an array
 *    - One argument: returns the field value for the specified object
 * 6) success():
 *    - Returns true if all errors are empty
 * 7) lastError():
 *    - Returns the last record whose error is not empty
 *      (scanned from bottom to top)
 * 8) findObj():
 *    - Accepts either a device object
 *    - Or a String(device) (i.e. the device id)
 *
 * Notes:
 * - The runFlow() below uses common Total Control APIs
 *   (runApp / isAppForeground / click2 / inputText / ...)
 * - If some API names differ in your environment, simply replace them;
 *   this does not affect the ArrayOutput demonstration logic
 */


var { getDevices, Device } = require("sigma/device");

// ===================== Config: App and Actions Used in This Example =====================
var TARGET_PKG = "com.sigma_rt.sigmatestapp";
var SEARCH_TEXT = "500";
var WAIT_LAUNCH_MS = 1200;
var SEARCH_X = 0.5352;
var SEARCH_Y = 0.135;

// ===================== Utilities: Device Names, Logs, and Key Actions =====================
function devName(d) {
    // Handles different device object fields: name, getName(), or SN
    return d.name || (d.getName && d.getName()) || d.SN || "UnknownDevice";
}

function logDev(d, s) {
    print("[" + devName(d) + "] " + s);
}

function press(d, keyConst) {
    // Common Total Control usage: send(KEY_*, STATE_PRESS)
    var ret = d.send(keyConst, tcConst.STATE_PRESS);
    if (ret !== 0) throw new Error("send failed: " + lastError());
}

/**
 * step(): A simple "step wrapper"
 * - Purpose: Wraps try/catch and stringifies errors for cleaner workflow code
 * - Returns: { ok: true, ret: any } or { ok: false, err: string }
 */
function step(label, fn) {
    try {
        var r = fn();
        return { ok: true, ret: r };
    } catch (e) {
        return { ok: false, err: String(e && e.message ? e.message : e) };
    }
}

// ===================== Single-Device Flow: Returns value (object) on success, throws on failure (errors collected externally) =====================

/**
 * The return value of runFlow(d) will be used as the value in ArrayOutput.add(d, value).
 * If runFlow throws an exception, the outer catch will call out.add(d, null, errorText).
 */
function runFlow(d) {
    // This is the final "value" to be returned and collected (structured for easier subsequent analysis)
    var result = {
        ok: false,
        step: "", // Records the step where a failure or termination occurred
        fg: "", // Foreground package name (on success)
		act: "", // Activity name (on success)
    };

    logDev(d, "=== DEVICE BEGIN ===");

    // 1) Launch the App
    var s1 = step("runApp", function () {
        var r = d.runApp(TARGET_PKG);
        if (r !== 0) throw new Error(lastError());
        sleep(WAIT_LAUNCH_MS);
        return 0;
    });
    if (!s1.ok) {
        result.step = "runApp";
        throw new Error("runApp failed: " + s1.err);
    }

    // 2) Verify foreground + retrieve foreground info
    var s2 = step("foregroundCheck", function () {
        var r = d.isAppForeground(TARGET_PKG);
        if (r !== 0) throw new Error("not foreground: " + lastError());
        result.fg = d.getForegroundApp() || "";
        result.act = d.getActivity() || "";
        return 0;
    });
    if (!s2.ok) {
        result.step = "foregroundCheck";
        throw new Error("foregroundCheck failed: " + s2.err);
    }

    // 3) Click the search box
    var s3 = step("clickSearch", function () {
        var r = d.click2(SEARCH_X, SEARCH_Y, { dx: 0.08, dy: 0.04 });
        if (r !== 0) throw new Error(lastError());
        sleep(250);
        return 0;
    });
    if (!s3.ok) {
        result.step = "clickSearch";
        throw new Error("clickSearch failed: " + s3.err);
    }

    // 4) Enter text and press Enter
    var s4 = step("inputText", function () {
        var r = d.inputText(SEARCH_TEXT + "\n");
        if (r !== 0) throw new Error("inputText failed: " + lastError());
        sleep(600);
        return 0;
    });
    if (!s4.ok) {
        result.step = "inputText";
        throw new Error("inputText failed: " + s4.err);
    }

    // 5) Turn page (example: failure is non-fatal, no throw here; just demonstrating continuation)
    step("paging", function () {
        d.pgDn();
        sleep(200);
        d.shiftPgDn();
        sleep(300);
        return 0;
    });

    // 6) Go back / return to home
    var s6 = step("backHome", function () {
        press(d, tcConst.KEY_BACK);
        sleep(200);
        press(d, tcConst.KEY_HOME);
        sleep(300);
        return 0;
    });
    if (!s6.ok) {
        result.step = "backHome";
        throw new Error("backHome failed: " + s6.err);
    }

    // 7) Close the app (failure is non-fatal)
    step("closeApp", function () {
        d.closeApp(TARGET_PKG);
        return 0;
    });

    result.ok = true;
    result.step = "done";
    logDev(d, "=== DEVICE END (OK) ===");
    return result;
}

// ===================== Main Entry: Collection + Return Wrapper + Full-Feature Read Demonstration =====================

function main() {
    /**
	 * Purpose of showOutputFlag:
	 * - When calling out.getReturn(), the module prints a simplified table:
	 *     [{ name, object: id, value, error }, ...]
	 * - Also assigns global.ao to the latest return object 
	 *   (so you can conveniently use ao.values() directly in the console)
	 */
    global.showOutputFlag = true;

    if (Device.connectAll) Device.connectAll();

    var devices = getDevices();
    if (!devices || devices.length === 0) {
        print("No selected devices. Please select devices first.");
        return;
    }

    print("Selected devices: " + devices.length);

    // 1) Create the collector
    var out = new ArrayOutput();

    // 2) Batch execution: each device generates a value or an error
    for (var i = 0; i < devices.length; i++) {
        var d = devices[i];

        print("\n==============================");
        print("RUN " + (i + 1) + "/" + devices.length + " : " + devName(d));
        print("==============================");

        try {
            // Success: value is a structured object (recommended)
            var value = runFlow(d);
            out.add(d, value); // error is omitted, defaulting to null
        } catch (e) {
            // Failure: value is null, error is a string
            out.add(d, null, String(e && e.message ? e.message : e));
        }
    }

    // 3) Obtain the read-only return object (core functionality)
    var ret = out.getReturn();

    // 4) Verify the global alias
    print("\n-- global alias check --");
    print("ret === global.ao ? " + (ret === global.ao));

    // =========================================================
    // A. Status APIs: toString / success / lastError
	// =========================================================
    print("\n-- status APIs --");
    print(ret.toString()); // "ArrayOutputReturn: N elements"
    print("success() = " + ret.success()); // Returns true only if there are no errors

    var lastErr = ret.lastError(); // {object, id, error} or undefined
    if (lastErr) {
        print("lastError.id=" + lastErr.id);
        print("lastError.error=" + lastErr.error);
    } else {
        print("lastError() = undefined (no errors)");
    }

    // =========================================================
    // // B. get() in three forms (the most important access method)
    // =========================================================
    print("\n-- get() forms --");

    // 1) ret.get(): Retrieve all records (each containing: name / object / id / value / error)
    var all = ret.get();
    print("get() records length = " + all.length);
    // Example structure of all[0]:
	// {
	//   name: "DeviceName",
	//   object: ,
	//   id: "String(deviceObject)",
	//   value: ,
	//   error: 
	// }

	// 2) ret.get(device): Retrieve the value for a single device (not the full record)
    var d0 = devices[0];
    print("get(device0) value = " + JSON.stringify(ret.get(d0)));

    // 3) ret.get([device...]): Retrieve values for multiple devices (aligned with the input array order)
    var pick = devices.slice(0, Math.min(3, devices.length));
    print("get([device0..]) values = " + JSON.stringify(ret.get(pick)));

    // =========================================================
    // C. Dynamic helpers: objects / values / errors / ids / names
	// =========================================================
	print("\n-- helpers (no args) --");
	// No arguments: returns the entire column as an array
	// Note: objects() returns the device objects themselves; may appear as [object ...] when printed
    print("objects() = " + ret.objects());
    print("values()  = " + JSON.stringify(ret.values()));
    print("errors()  = " + JSON.stringify(ret.errors()));
    print("ids()     = " + JSON.stringify(ret.ids()));
    print("names()   = " + JSON.stringify(ret.names()));

    print("\n-- helpers (single target) --");
    // Single argument: retrieve a specific field for a given device
    print("values(device0) = " + JSON.stringify(ret.values(d0)));
    print("errors(device0) = " + JSON.stringify(ret.errors(d0)));	
    print("ids(device0)    = " + JSON.stringify(ret.ids(d0))); 
    print("names(device0)  = " + JSON.stringify(ret.names(d0)));
    print("objects(device0)= " + ret.objects(d0));

    // =========================================================
    // D. findObj: Locate index (supports either object or string id)
    // =========================================================
    print("\n-- findObj() --");

    // 1) Pass an object: match using identity or String(obj)
    var idxByObj = ret.findObj(d0);
    print("findObj(device0) = " + idxByObj);

    // 2) Pass a string: match using String(this.output[i].object)
	// This corresponds to the id stored as String(obj) when using add()
    var id0 = String(d0);
    var idxById = ret.findObj(id0);
    print("findObj(String(device0)) = " + idxById + ' (id="' + id0 + '")');

    // =========================================================
    // E. Global ao: Quick debugging access (same object as ret)
    // =========================================================
    print("\n-- global ao usage --");
    print("ao.values()  = " + JSON.stringify(ao.values()));
    print("ao.errors()  = " + JSON.stringify(ao.errors()));
    print("ao.success() = " + ao.success());

    // =========================================================
    // F. Common practical use: generate a more readable summary output
    // =========================================================
    print("\n===== SUMMARY (computed) =====");
    var names = ret.names();
    var errs = ret.errors();
    var vals = ret.values();

    var okCount = 0;
    for (var k = 0; k < names.length; k++) {
        if (!errs[k]) {
            okCount++;
            print("[" + names[k] + "] OK step=" + (vals[k] && vals[k].step));
        } else {
            print("[" + names[k] + "] FAIL err=" + errs[k]);
        }
    }
    print("Success: " + okCount + " / " + names.length);

    /**
	 * Additional tips (common usage recommendations):
	 * - It is recommended to store structured objects in value 
	 *   (e.g., { ok, step, fg, act, ... }) for easier later analysis and troubleshooting
	 * - It is recommended to store errors as a string: String(err)
	 * - On failure, you can include more information in the error text or value 
	 *   (e.g., screenshot paths, execution time, key parameters)
	 */
}

main();	

Execution Result

Selected devices: 2
 
==============================
RUN 1/2 : TestDevice03
==============================
[TestDevice03] === DEVICE BEGIN ===
[TestDevice03] info: HUAWEI HLK-AL00 1080x2340
[TestDevice03] >> START  Step1 app.runApp(com.sigma_rt.sigmatestapp)
[TestDevice03] << END    Step1 app.runApp(com.sigma_rt.sigmatestapp)  (cost 2591 ms)
[TestDevice03] >> START  Step2 app.isAppForeground + getForegroundApp/getActivity
[TestDevice03] foregroundApp=com.sigma_rt.sigmatestapp
[TestDevice03] activity=com.sigma_rt.sigmatestapp/.MainActivity
[TestDevice03] << END    Step2 app.isAppForeground + getForegroundApp/getActivity  (cost 405 ms)
[TestDevice03] >> START  Step3 input.click2(searchBox)
[TestDevice03] << END    Step3 input.click2(searchBox)  (cost 364 ms)
[TestDevice03] >> START  Step4 tests.inputText(SEARCH_TEXT + \n)
[TestDevice03] << END    Step4 tests.inputText(SEARCH_TEXT + \n)  (cost 1032 ms)
[TestDevice03] >> START  Step5 navKeys.pgDn + navKeys.shiftPgDn
[TestDevice03] << END    Step5 navKeys.pgDn + navKeys.shiftPgDn  (cost 716 ms)
[TestDevice03] >> START  Step6 input.send(KEY_BACK) + input.send(KEY_HOME)
[TestDevice03] << END    Step6 input.send(KEY_BACK) + input.send(KEY_HOME)  (cost 653 ms)
[TestDevice03] >> START  Step7 app.closeApp(com.sigma_rt.sigmatestapp)
[TestDevice03] << END    Step7 app.closeApp(com.sigma_rt.sigmatestapp)  (cost 254 ms)
[TestDevice03] === DEVICE END (OK) ===
[TestDevice03] OK
 
==============================
RUN 2/2 : T110
==============================
[T110] === DEVICE BEGIN ===
[T110] info: HUAWEI HLK-AL00 1080x2340
[T110] >> START  Step1 app.runApp(com.sigma_rt.sigmatestapp)
[T110] << END    Step1 app.runApp(com.sigma_rt.sigmatestapp)  (cost 2578 ms)
[T110] >> START  Step2 app.isAppForeground + getForegroundApp/getActivity
[T110] foregroundApp=com.sigma_rt.sigmatestapp
[T110] activity=com.huawei.android.launcher/.unihome.UniHomeLauncher
[T110] << END    Step2 app.isAppForeground + getForegroundApp/getActivity  (cost 483 ms)
[T110] >> START  Step3 input.click2(searchBox)
[T110] << END    Step3 input.click2(searchBox)  (cost 343 ms)
[T110] >> START  Step4 tests.inputText(SEARCH_TEXT + \n)
[T110] << END    Step4 tests.inputText(SEARCH_TEXT + \n)  (cost 1032 ms)
[T110] >> START  Step5 navKeys.pgDn + navKeys.shiftPgDn
[T110] << END    Step5 navKeys.pgDn + navKeys.shiftPgDn  (cost 693 ms)
[T110] >> START  Step6 input.send(KEY_BACK) + input.send(KEY_HOME)
[T110] << END    Step6 input.send(KEY_BACK) + input.send(KEY_HOME)  (cost 633 ms)
[T110] >> START  Step7 app.closeApp(com.sigma_rt.sigmatestapp)
[T110] << END    Step7 app.closeApp(com.sigma_rt.sigmatestapp)  (cost 291 ms)
[T110] === DEVICE END (OK) ===
[T110] OK
 
===== SUMMARY =====
Success: 2 / 2
0

TCHelp