@slaplacian/tsups 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/serial.js +28 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slaplacian/tsups",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Cross-platform CLI monitor for TS Shara UPS units (Megatec/Q1 serial protocol)",
5
5
  "keywords": [
6
6
  "ups",
package/src/serial.js CHANGED
@@ -1,10 +1,27 @@
1
+ import { readdirSync } from 'node:fs';
1
2
  import { SerialPort } from 'serialport';
2
3
  import { BAUD_RATE, CMD, isInfoReply } from './protocol.js';
3
4
 
5
+ // USB serial devices by name. Used on Linux when SerialPort.list() cannot run:
6
+ // it shells out to udevadm, which minimal containers do not have.
7
+ export function scanDevPorts(dir = '/dev', names = readdirSync(dir)) {
8
+ return names
9
+ .filter((n) => /^tty(ACM|USB)\d+$/.test(n))
10
+ .sort()
11
+ .map((n) => ({ path: `${dir}/${n}` }));
12
+ }
13
+
4
14
  export async function listPorts() {
5
- return SerialPort.list();
15
+ try {
16
+ return await SerialPort.list();
17
+ } catch (err) {
18
+ if (process.platform !== 'linux') throw err;
19
+ return scanDevPorts();
20
+ }
6
21
  }
7
22
 
23
+ export const isPermissionError = (err) => /permission denied|EACCES/i.test(`${err?.code} ${err?.message}`);
24
+
8
25
  // The TS Shara USB interface (STM32 CDC, 0483:5740) holds each reply until the
9
26
  // host writes again. A bare CR is ignored by the UPS but releases the reply.
10
27
  const KICK = '\r';
@@ -127,8 +144,11 @@ export class UpsConnection {
127
144
  }
128
145
 
129
146
  // Probe: send "I", accept the port if the reply contains "TS".
147
+ // Throws when no UPS was found but some port could not be opened for lack of
148
+ // permission, since "not found" would hide the real cause.
130
149
  export async function detectPort({ match = 'TS', timeout = 3000, log = () => {} } = {}) {
131
150
  const ports = await listPorts();
151
+ const denied = [];
132
152
  for (const info of ports) {
133
153
  const lower = info.path.toLowerCase();
134
154
  if (lower.includes('bluetooth') || lower.includes('blth')) continue;
@@ -139,9 +159,16 @@ export async function detectPort({ match = 'TS', timeout = 3000, log = () => {}
139
159
  if (reply.includes(match)) return { path: info.path, info: reply };
140
160
  } catch (err) {
141
161
  log(`${info.path}: ${err.message}`);
162
+ if (isPermissionError(err)) denied.push(info.path);
142
163
  } finally {
143
164
  await conn.close();
144
165
  }
145
166
  }
167
+ if (denied.length) {
168
+ throw new Error(
169
+ `permission denied on ${denied.join(', ')}; add your user to the "dialout" group ` +
170
+ '(or "uucp" on Arch) and log in again, or run with sudo',
171
+ );
172
+ }
146
173
  return null;
147
174
  }