@andrian.yablonskyy/thub-common 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrian.yablonskyy/thub-common",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Shared job-spec schema, state enums and a thin API client used by the Agent and Client",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,7 +16,8 @@
16
16
  'use strict';
17
17
 
18
18
  const Ajv = require('ajv'),
19
- addFormats = require('ajv-formats');
19
+ addFormats = require('ajv-formats'),
20
+ { MAX_POWER_PORTS } = require('./power');
20
21
 
21
22
  // The config file's key for an HW Client's devices. Older files call it
22
23
  // `hw`, still read (and rewritten as `hw-devices` on the next save).
@@ -46,7 +47,29 @@ const HW_DEVICES = 'hw-devices',
46
47
  properties: {
47
48
  stlinks: list(device({ serial: { type: 'string', pattern: '^[A-Za-z0-9]{1,64}$' } })),
48
49
  uarts: list(device({ baudRate: { type: 'integer', minimum: 50, maximum: 4000000 } })),
49
- usbs: list(device())
50
+ usbs: list(device()),
51
+ // USB port power switched with uhubctl (README §8.7): each port by its
52
+ // hub's location and port number, as `uhubctl` lists them
53
+ // ("Current status for hub 1-1.4" … "Port 2").
54
+ usbPower: {
55
+ type: 'object',
56
+ additionalProperties: false,
57
+ properties: {
58
+ ports: {
59
+ type: 'array',
60
+ maxItems: MAX_POWER_PORTS,
61
+ items: {
62
+ type: 'object',
63
+ additionalProperties: false,
64
+ required: ['hub', 'port'],
65
+ properties: {
66
+ hub: { type: 'string', pattern: '^[0-9]+(-[0-9]+(\\.[0-9]+)*)?$', maxLength: 64 },
67
+ port: { type: 'integer', minimum: 1, maximum: 255 }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
50
73
  }
51
74
  },
52
75
  // An SW Client has no settings of its own: its editable section is empty.
@@ -62,8 +85,8 @@ function hwDevicesOf(file){
62
85
  return file?.[HW_DEVICES] !== undefined ? file[HW_DEVICES] : file?.hw;
63
86
  }
64
87
 
65
- // An hw-devices section without the power control Clients no longer have
66
- // (relays, power — relay boards and uhubctl), so a config file that still
88
+ // An hw-devices section without the older power control Clients no longer
89
+ // have (relays, power — relay boards, and uhubctl before usbPower), so a config file that still
67
90
  // has them reports, saves and imports without them. Returns
68
91
  // { section, dropped } (dropped: the names left out).
69
92
  function withoutPowerControl(hw){
package/src/index.js CHANGED
@@ -22,5 +22,6 @@ module.exports = {
22
22
  ...require('./client-config'),
23
23
  ...require('./split-args'),
24
24
  ...require('./env-list'),
25
+ ...require('./power'),
25
26
  ApiClient: require('./api-client').ApiClient
26
27
  };
@@ -13,6 +13,8 @@
13
13
 
14
14
  'use strict';
15
15
 
16
+ const { POWER_ACTIONS, MAX_RESET_DELAY_SEC } = require('./power');
17
+
16
18
  // An environment variable name a job may set (`--env NAME=value`).
17
19
  const ENV_NAME_PATTERN = '^[A-Za-z_][A-Za-z0-9_]*$',
18
20
 
@@ -56,7 +58,7 @@ const ENV_NAME_PATTERN = '^[A-Za-z_][A-Za-z0-9_]*$',
56
58
  // Client runs with `sh -c`, `args` as "$@" (`--arg`, repeatable).
57
59
  command: { type: 'string', minLength: 1, maxLength: 4096 },
58
60
  args: { type: 'array', items: { type: 'string' }, default: [] },
59
- // Passed to the command as THUB_SUITE (`--suite`).
61
+ // Passed to the command as JOB_SUITE (`--suite`).
60
62
  suite: { type: 'string', default: 'default' },
61
63
  // Files the Client downloads into the task's work directory before
62
64
  // running the command (`--download-file`, repeatable).
@@ -92,6 +94,19 @@ const ENV_NAME_PATTERN = '^[A-Za-z_][A-Za-z0-9_]*$',
92
94
  // not an identity: nothing authenticates or enforces it.
93
95
  user: { type: 'string', minLength: 1 },
94
96
  meta: { type: 'object' },
97
+ // USB port power on the Client (uhubctl, README §8.7) at the job's
98
+ // start (before the DUT is prepared) and end (whatever the verdict):
99
+ // `--power-on-start` / `--power-on-end` on|off|reset, and the reset's
100
+ // off time `--power-reset-delay` (seconds; 1 when not given). HW only.
101
+ power: {
102
+ type: 'object',
103
+ additionalProperties: false,
104
+ properties: {
105
+ onStart: { enum: POWER_ACTIONS },
106
+ onEnd: { enum: POWER_ACTIONS },
107
+ resetDelaySec: { type: 'number', minimum: 0, maximum: MAX_RESET_DELAY_SEC }
108
+ }
109
+ },
95
110
  // Exercises the full pipeline (schedule, accept, state transitions,
96
111
  // logs, artifact, result) without flashing/running anything for real —
97
112
  // see README §7.1 "Dry-run the pipeline".
package/src/power.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @file packages/shared/src/power.js
3
+ * @description USB port power (uhubctl on the Client): the actions, the reset delay and the checks on a power
4
+ * request — shared by the Agent, the Coordinator and the Client (README §8.7)
5
+ *
6
+ * @author Andrian Yablonskyy
7
+ * @copyright Copyright (c) 2026 Andrian Yablonskyy. All rights reserved.
8
+ *
9
+ * This file is part of TestHub and is proprietary and confidential.
10
+ * Unauthorized copying, modification, distribution, or use of this file,
11
+ * via any medium, is strictly prohibited without prior written permission
12
+ * from AdSystem.PRO.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ // `reset`: off, wait the reset delay, on.
18
+ const POWER_ACTIONS = Object.freeze(['on', 'off', 'reset']),
19
+ DEFAULT_RESET_DELAY_SEC = 1,
20
+ MAX_RESET_DELAY_SEC = 60,
21
+ // A Client's hw-devices.usbPower.ports holds at most this many.
22
+ MAX_POWER_PORTS = 8;
23
+
24
+ // Errors (strings) for a request to switch a Client's ports: `action` one of
25
+ // POWER_ACTIONS, `delaySec` (reset only, optional) 0..MAX_RESET_DELAY_SEC,
26
+ // `port` (optional) a 1-based position in the Client's usbPower.ports.
27
+ function powerRequestErrors({ action, delaySec, port } = {}){
28
+ const errors = [];
29
+ if (!POWER_ACTIONS.includes(action)){
30
+ errors.push(`action must be one of ${POWER_ACTIONS.join(', ')}`);
31
+ }
32
+ if (delaySec !== undefined && delaySec !== null){
33
+ if (typeof delaySec !== 'number' || !Number.isFinite(delaySec) || delaySec < 0 || delaySec > MAX_RESET_DELAY_SEC){
34
+ errors.push(`the reset delay (delaySec) must be a number of seconds, 0-${MAX_RESET_DELAY_SEC}`);
35
+ }
36
+ else if (action !== 'reset'){
37
+ errors.push('the reset delay (delaySec) applies to reset only');
38
+ }
39
+ }
40
+ if (port !== undefined && port !== null && (!Number.isInteger(port) || port < 1 || port > MAX_POWER_PORTS)){
41
+ errors.push(`port must be a usbPower port number, 1-${MAX_POWER_PORTS}`);
42
+ }
43
+ return errors;
44
+ }
45
+
46
+ module.exports = { POWER_ACTIONS, DEFAULT_RESET_DELAY_SEC, MAX_RESET_DELAY_SEC, MAX_POWER_PORTS, powerRequestErrors };
@@ -69,6 +69,10 @@ function crossFieldErrors(spec){
69
69
  if (reserved.length){
70
70
  errors.push(`/env ${reserved.join(', ')}: set by the Client itself (THUB_*, JOB_*) — use other names`);
71
71
  }
72
+ // USB power is an HW Client's (its hw-devices.usbPower ports).
73
+ if (spec.power && spec.target?.type !== 'hw'){
74
+ errors.push('/power USB port power is for HW jobs only (--type hw)');
75
+ }
72
76
  return errors;
73
77
  }
74
78