diff --git a/lib/flashing/README.md b/lib/flashing/README.md index d622105..b00e8cb 100644 --- a/lib/flashing/README.md +++ b/lib/flashing/README.md @@ -34,6 +34,15 @@ There are multiple possibilities here: - If you do that, use `generic-flasher-boot-switch`. - if testing balenaOS - flash the internal storage of the device once manually. Then leave the switch in the internal storage position. Then connect it to the autokit. Future boots, the device will automatically boot from a flasher image on the SD card if present, or internal storage otherwise. This method requires no automated boot switch toggling, but may be weak for testing BSP updates +#### Power of detection for flasher images + +There are currently 2 ways to detect that a device running the flasher image has powered down after internal flashing: + +1. `ethernet`: (default) The ethernet carrier signal is used - the autokit will use the interface configured via the `WIRED_IF` env variable. `POWER_OFF_TIMEOUT` can be used to control the number of cycles to wait and check the signal. +2. `serial`: The serial output of the DUT is monitored for a message `reboot: Power down` , configurable via the `POWER_OFF_MESSAGE` env variable. The interface defined via the `DEV_SERIAL` will be used, and this requires the `ftdi` serial implementation to be used. `POWER_OFF_TIMEOUT` can be used to set a timeout in ms (default 5 mins). + +The method is selectable via `POWER_OFF_SELECTOR`, with `ethernet` or `serial` + ### USB-BOOT / raspberry pi compute module devices (CM3/CM4) For raspberry pi compute module devices, the autokit can bring them up into mass storage mode then write the OS image. diff --git a/lib/flashing/index.ts b/lib/flashing/index.ts index b038086..e251219 100644 --- a/lib/flashing/index.ts +++ b/lib/flashing/index.ts @@ -6,6 +6,7 @@ import * as sdk from 'etcher-sdk'; import * as fs from 'fs/promises'; import { BlockDeviceAdapter } from 'etcher-sdk/build/scanner/adapters'; import { Autokit } from '../'; +import { waitForPowerOff } from './powerDetection'; /** * Flash an image to a disk - this is the low level function used to flash a disk (SD card, USD storage device etc) @@ -129,22 +130,6 @@ async function flashSD(filename: string, autoKit: Autokit){ } -/** - * Checks whether the DUT is powered using Ethernet carrier detection - **/ -async function checkDutPower(autoKit:Autokit) { - const [stdout, _stderr] = await exec(`cat /sys/class/net/${autoKit.network.wiredIface}/carrier`); - const file = stdout.toString(); - if (file.includes('1')) { - console.log(`DUT is currently On`); - return true; - } else { - console.log(`DUT is currently Off`); - return false; - } -} - - const KEY_DELAY = Number(process.env.KEY_DELAY) || 500; async function keyboardSequence(autoKit: Autokit, keyboard: [string]){ for(let key of keyboard){ @@ -181,53 +166,8 @@ async function flashFlasher(filename: string, autoKit: Autokit, jumper: boolean, await autoKit.power.on(); - + await waitForPowerOff(autoKit); - // dut will now internally flash - need to wait until we detect DUT has powered off - // can be done through ethernet carrier signal, or through current measurement (or something else...) - // FOR NOW: use the network - // check if the DUT is on first - let dutOn = false; - while (!dutOn) { - console.log(`waiting for DUT to be on`); - dutOn = await checkDutPower(autoKit); - await delay(1000 * 5); // 5 seconds between checks - } - // once we confirmed the DUT is on, we wait for it to power down again, which signals the flashing has finished - // wait initially for 60s and then every 10s before checking if the board performed a shutdown after flashing the internal storage - const POLL_INTERVAL = 1000; // 1 second - const POLL_TRIES = 20; // 20 tries - const TIMEOUT_COUNT = 30; - let attempt = 0; - await delay(1000 * 60); - while (dutOn) { - await delay(1000 * 10); // 10 seconds between checks - console.log(`waiting for DUT to be off`); - dutOn = await checkDutPower(autoKit); - // occasionally the DUT might appear to be powered down, but it isn't - we want to confirm that the DUT has stayed off for an interval of time - if (!dutOn) { - let offCount = 0; - console.log(`detected DUT has powered off - confirming...`); - for (let tries = 0; tries < POLL_TRIES; tries++) { - await delay(POLL_INTERVAL); - dutOn = await checkDutPower(autoKit); - if (!dutOn) { - offCount += 1; - } - } - console.log( - `DUT stayted off for ${offCount} checks, expected: ${POLL_TRIES}`, - ); - if (offCount !== POLL_TRIES) { - // if the DUT didn't stay off, then we must try the loop again - dutOn = true; - } - } - attempt += 1; - if (attempt === TIMEOUT_COUNT){ - throw new Error(`Timed out while trying to flash internal storage!!`) - } - } console.log('Internally flashed - powering off DUT'); // power off and toggle mux. await delay(1000*10); @@ -527,41 +467,7 @@ async function flashJetson(filename: string, autoKit: Autokit, deviceType: strin if(nvme){ // wait for jetson to power off - const POLL_INTERVAL = 1000; // 1 second - const POLL_TRIES = 20; // 20 tries - const TIMEOUT_COUNT = 30000; - let attempt = 0; - let dutOn = true; - await delay(1000 * 60); - while (dutOn) { - await delay(1000 * 10); // 10 seconds between checks - console.log(`waiting for DUT to be off`); - dutOn = await checkDutPower(autoKit); - // occasionally the DUT might appear to be powered down, but it isn't - we want to confirm that the DUT has stayed off for an interval of time - if (!dutOn) { - let offCount = 0; - console.log(`detected DUT has powered off - confirming...`); - for (let tries = 0; tries < POLL_TRIES; tries++) { - await delay(POLL_INTERVAL); - dutOn = await checkDutPower(autoKit); - if (!dutOn) { - offCount += 1; - } - } - console.log( - `DUT stayted off for ${offCount} checks, expected: ${POLL_TRIES}`, - ); - if (offCount !== POLL_TRIES) { - // if the DUT didn't stay off, then we must try the loop again - dutOn = true; - } - } - attempt += 1; - if (attempt === TIMEOUT_COUNT){ - await autoKit.power.off(); - throw new Error(`Timed out while trying to flash internal storage!!. Powered off DUT.`) - } - } + await waitForPowerOff(autoKit); console.log('Internally flashed - powering off DUT'); } diff --git a/lib/flashing/powerDetection/ethernet.ts b/lib/flashing/powerDetection/ethernet.ts new file mode 100644 index 0000000..35dec87 --- /dev/null +++ b/lib/flashing/powerDetection/ethernet.ts @@ -0,0 +1,67 @@ +import { Autokit } from '../../'; +import { exec } from 'mz/child_process'; +import { delay } from 'bluebird'; + + +const TIMEOUT_COUNT = Number(process.env.POWER_OFF_TIMEOUT) || 30 + +/** + * Checks whether the DUT is powered using Ethernet carrier detection + **/ +async function checkDutPower(autoKit:Autokit) { + const [stdout, _stderr] = await exec(`cat /sys/class/net/${autoKit.network.wiredIface}/carrier`); + const file = stdout.toString(); + if (file.includes('1')) { + console.log(`DUT is currently On`); + return true; + } else { + console.log(`DUT is currently Off`); + return false; + } +} + +export async function waitForPowerOffEthernet(autoKit:Autokit):Promise{ + // dut will now internally flash - need to wait until we detect DUT has powered off + // FOR NOW: use the network + // check if the DUT is on first + let dutOn = false; + while (!dutOn) { + console.log(`waiting for DUT to be on`); + dutOn = await checkDutPower(autoKit); + await delay(1000 * 5); // 5 seconds between checks + } + + const POLL_INTERVAL = 1000; // 1 second + const POLL_TRIES = 20; // 20 tries + let attempt = 0; + await delay(1000 * 60); + while (dutOn) { + await delay(1000 * 10); // 10 seconds between checks + console.log(`waiting for DUT to be off`); + dutOn = await checkDutPower(autoKit); + // occasionally the DUT might appear to be powered down, but it isn't - we want to confirm that the DUT has stayed off for an interval of time + if (!dutOn) { + let offCount = 0; + console.log(`detected DUT has powered off - confirming...`); + for (let tries = 0; tries < POLL_TRIES; tries++) { + await delay(POLL_INTERVAL); + dutOn = await checkDutPower(autoKit); + if (!dutOn) { + offCount += 1; + } + } + console.log( + `DUT stayed off for ${offCount} checks, expected: ${POLL_TRIES}`, + ); + if (offCount !== POLL_TRIES) { + // if the DUT didn't stay off, then we must try the loop again + dutOn = true; + } + } + attempt += 1; + if (attempt === TIMEOUT_COUNT){ + await autoKit.power.off(); + throw new Error(`Timed out while trying to flash internal storage!!. Powered off DUT.`) + } + } +} \ No newline at end of file diff --git a/lib/flashing/powerDetection/index.ts b/lib/flashing/powerDetection/index.ts new file mode 100644 index 0000000..5e44d45 --- /dev/null +++ b/lib/flashing/powerDetection/index.ts @@ -0,0 +1,15 @@ +import { Autokit } from "../.."; +import { waitForPowerOffEthernet } from "./ethernet"; +import { waitForPowerOffSerial } from "./serial"; + +const powerOffSelector = process.env.POWER_OFF_SELECTOR || 'ethernet' + +const powerOffFunctions: {[key: string]: (autoKit: Autokit) => Promise } = { + ethernet: waitForPowerOffEthernet, + serial: waitForPowerOffSerial, +}; + +const waitForPowerOff = powerOffFunctions[powerOffSelector]; + + +export { waitForPowerOff } \ No newline at end of file diff --git a/lib/flashing/powerDetection/serial.ts b/lib/flashing/powerDetection/serial.ts new file mode 100644 index 0000000..a5330be --- /dev/null +++ b/lib/flashing/powerDetection/serial.ts @@ -0,0 +1,37 @@ +import { Autokit } from '../../'; +import { ReadlineParser } from '@serialport/parser-readline'; +import { Readable } from 'serialport'; + +const powerOffMessage = process.env.POWER_OFF_MESSAGE || 'reboot: Power down' +const timeout = Number(process.env.POWER_OFF_TIMEOUT) || 1000*60*5; + +/** + * Checks whether the DUT has powered down by checking for a serial message + **/ +export async function waitForPowerOffSerial(autoKit:Autokit):Promise { + const serialport = await autoKit.serial.open(); + + if(serialport !== undefined){ + return new Promise((resolve, reject) => { + + let timer = setTimeout(async () => { + await autoKit.serial.close(); + reject(`Timed out while waiting for power down message over serial!`); + }, timeout) + + // Examine each line of the serial output from the DUT as it comes in + const parser = serialport.pipe(new ReadlineParser({ delimiter: '\n' })); + parser.on('data', async(data:string) => { + console.log(`Serial line: ${data.toString()}`); + if(data.toString().includes(powerOffMessage)){ + console.log(`### Detected power off message ###`) + await autoKit.power.off(); + await autoKit.serial.close(); + clearTimeout(timer); + resolve(); + } + }) + }) + + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1067a17..6829a59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@balena/usbrelay": "^0.1.4", + "@serialport/parser-readline": "^12.0.0", "bluebird": "^3.7.2", "bluebird-retry": "^0.11.0", "dbus-next": "^0.10.2", @@ -804,6 +805,20 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/@serialport/bindings/node_modules/@serialport/parser-readline": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.2.4.tgz", + "integrity": "sha512-Z1/qrZTQUVhNSJP1hd9YfDvq0o7d87rNwAjjRKbVpa7Qi51tG5BnKt43IV3NFMyBlVcRe0rnIb3tJu57E0SOwg==", + "dependencies": { + "@serialport/parser-delimiter": "9.2.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, "node_modules/@serialport/parser-byte-length": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-9.2.4.tgz", @@ -849,14 +864,25 @@ } }, "node_modules/@serialport/parser-readline": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.2.4.tgz", - "integrity": "sha512-Z1/qrZTQUVhNSJP1hd9YfDvq0o7d87rNwAjjRKbVpa7Qi51tG5BnKt43IV3NFMyBlVcRe0rnIb3tJu57E0SOwg==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", "dependencies": { - "@serialport/parser-delimiter": "9.2.4" + "@serialport/parser-delimiter": "12.0.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-readline/node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "engines": { + "node": ">=12.0.0" }, "funding": { "url": "https://opencollective.com/serialport/donate" @@ -3627,6 +3653,20 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/serialport/node_modules/@serialport/parser-readline": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.2.4.tgz", + "integrity": "sha512-Z1/qrZTQUVhNSJP1hd9YfDvq0o7d87rNwAjjRKbVpa7Qi51tG5BnKt43IV3NFMyBlVcRe0rnIb3tJu57E0SOwg==", + "dependencies": { + "@serialport/parser-delimiter": "9.2.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", diff --git a/package.json b/package.json index d10559e..275541d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "license": "Apache-2.0", "dependencies": { "@balena/usbrelay": "^0.1.4", + "@serialport/parser-readline": "^12.0.0", "bluebird": "^3.7.2", "bluebird-retry": "^0.11.0", "dbus-next": "^0.10.2",