xp-command 1.4.0 → 1.5.0

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/README.md CHANGED
@@ -46,11 +46,7 @@ This installs xp-command globally so you can run it from anywhere.
46
46
 
47
47
  ## 🛠 X-Plane setup
48
48
 
49
- You shouldn't need to perform any extra setup steps in X-Plane. There is no plugin that needs to be installed. If you still want to check if the Web API (which allows xp-command to communicate with X-Plane) is configured correctly, here is how you can do it:
50
-
51
- 1. In X-Plane 12, go to **Settings → Data Output**
52
- 2. Find **"Enable Web Server API"** and make sure it's enabled
53
- 3. Note the **port number** (default is `8086`)
49
+ You do not need to do anything inside X-Plane for xp-command to work. X-Plane 12 runs a local Web API automatically on `http://localhost:8086` as long as it is not started with the `--no_web_server` command-line option and the port is free. Just make sure X-Plane is running on the same machine as xp-command and that nothing (firewall or another process) is blocking or using port `8086`.
54
50
 
55
51
  ---
56
52
 
@@ -166,11 +162,12 @@ You can edit these YAML files to add aircraft-specific commands or modify existi
166
162
  - `pattern`: Regular expression matching your command (`hdg18`)
167
163
  - `type`: Either `get` (read value and copy to clipboard) or `set` (write value)
168
164
  - `dataref`: X-Plane dataref path (find these in X-Plane's DataRef Editor)
169
- - `transform`: Optional value conversions (multiply, divide, round, etc.)
165
+ - `command`: X-Plane command path (find these in X-Plane's DataRef Editor)
166
+ - `transform`: Optional value conversions (`mult<number>` to multiply or divide, `round`, `toFixed<number>` etc.)
170
167
 
171
168
  **Finding datarefs**: Use the [DataRefTool plugin](https://datareftool.com) or check [X-Plane datarefs documentation](https://developer.x-plane.com/datarefs/).
172
169
 
173
- #### Array Datarefs
170
+ #### Array datarefs
174
171
 
175
172
  Many X-Plane datarefs are arrays (e.g., for multiple engines, generators, radios). You can access specific array elements using `[index]` notation:
176
173
 
@@ -194,11 +191,48 @@ Here is an example of a custom set command which sets multiple datarefs at once
194
191
  - sim/flightmodel2/gear/is_chocked[2]
195
192
  ```
196
193
 
197
- ## 🔄 Resetting Aircraft Profiles
194
+ #### Using commands
195
+
196
+ In addition to reading/writing datarefs, you can trigger X-Plane commands. Commands are actions (like button presses) rather than values.
197
+
198
+ **Command field**: Use `command` instead of or alongside `dataref` in `set` type operations.
199
+
200
+ **Duration syntax**: Append `[duration]` to specify how long the command stays active (in seconds):
201
+ - `command` or `command[0]`: Press and immediately release (default behavior)
202
+ - `command[5]`: Hold for 5 seconds then release
203
+ - Maximum duration: 10 seconds
204
+
205
+ **Examples**:
206
+
207
+ ```yaml
208
+ # Toggle anti-ice group on
209
+ - pattern: "^i(1)$"
210
+ type: set
211
+ command:
212
+ - FJS/Q4XP/Knobs/prop_heat_up
213
+ - FJS/Q4XP/Knobs/Airframe_deice_mode_up
214
+ dataref:
215
+ - FJS/Q4XP/Manips/TwoSwitch_Anim[8]
216
+ - FJS/Q4XP/Manips/TwoSwitch_Anim[9]
217
+ - FJS/Q4XP/Manips/TwoSwitch_Anim[10]
218
+ ```
219
+
220
+ Here is an example demonstrating the usage of the duration option: In the Dash 8 (Q4XP), in order to change the transponder mode, you must press a tiny button for at least two seconds! Not anymore:
221
+
222
+ ```
223
+ # Toggle transponder mode
224
+ - pattern: "^x$"
225
+ type: set
226
+ command: FJS/Q4XP/SoftKey/arcdu_1/skr4[2]
227
+ ```
228
+
229
+ **Finding commands**: Use the [DataRefTool plugin](https://datareftool.com) to browse available commands.
230
+
231
+ ## 🔄 Resetting aircraft profiles
198
232
 
199
233
  If you've edited an aircraft configuration and xp-command crashes or stops working, you can reset to default settings by deleting the config files.
200
234
 
201
- ### Profile Location
235
+ ### Profile location
202
236
 
203
237
  Aircraft profiles are stored in:
204
238
 
package/bin/index.js CHANGED
@@ -15,7 +15,12 @@ import { homedir } from "os";
15
15
  import { join } from "path";
16
16
 
17
17
  import packageJson from "../package.json" with { type: "json" };
18
- import { getDatarefValues, initAPI, setDatarefValues } from "../src/api.js";
18
+ import {
19
+ activateCommands,
20
+ getDatarefValues,
21
+ initAPI,
22
+ setDatarefValues,
23
+ } from "../src/api.js";
19
24
  import { copyToClipboard } from "../src/clipboard.js";
20
25
  import { editConfig, getConfig } from "../src/config.js";
21
26
  import { clearLine, hideCursor, showCursor } from "../src/console.js";
@@ -97,11 +102,34 @@ const processCommand = async (command) => {
97
102
 
98
103
  /** @type {Array<[RegExp, (regExpResult: Array<string> | null) => Promise<void>]>} */
99
104
  const matches = config.commands.map((c) => {
105
+ if (!c.type) {
106
+ return [
107
+ new RegExp(c.pattern),
108
+ async () => {
109
+ spinner.fail(
110
+ chalk.red(`${PREFIX} Missing type (must be get or set)!`),
111
+ );
112
+ hideCursor();
113
+ await sleep(1500);
114
+ clearLine();
115
+ return;
116
+ },
117
+ ];
118
+ }
119
+
100
120
  switch (c.type) {
101
121
  case "get":
102
122
  return [
103
123
  new RegExp(c.pattern),
104
124
  async () => {
125
+ if (!c.dataref) {
126
+ spinner.fail(chalk.red(`${PREFIX} Missing dataref!`));
127
+ hideCursor();
128
+ await sleep(1500);
129
+ clearLine();
130
+ return;
131
+ }
132
+
105
133
  /** @type {number|string|Array<number|string>}*/
106
134
  let value = await getDatarefValues(c.dataref);
107
135
  c.transform?.forEach((t) => {
@@ -125,16 +153,30 @@ const processCommand = async (command) => {
125
153
  return [
126
154
  new RegExp(c.pattern),
127
155
  async (regExpResult) => {
128
- let value = String(regExpResult[1]);
129
-
130
- if (isNaN(Number(value))) {
131
- const base64 = Buffer.from(value, "utf-8").toString("base64");
132
- await setDatarefValues(c.dataref, base64);
133
- } else {
134
- c.transform?.forEach((t) => {
135
- value = String(getTransformedValue(value, t));
136
- });
137
- await setDatarefValues(c.dataref, Number(value));
156
+ if (!c.command && !c.dataref) {
157
+ spinner.fail(
158
+ chalk.red(`${PREFIX} Neither dataref nor command provided!`),
159
+ );
160
+ hideCursor();
161
+ await sleep(1500);
162
+ clearLine();
163
+ return;
164
+ }
165
+
166
+ if (c.command) {
167
+ await activateCommands(c.command);
168
+ }
169
+ if (c.dataref) {
170
+ let value = String(regExpResult[1]);
171
+ if (isNaN(Number(value))) {
172
+ const base64 = Buffer.from(value, "utf-8").toString("base64");
173
+ await setDatarefValues(c.dataref, base64);
174
+ } else {
175
+ c.transform?.forEach((t) => {
176
+ value = String(getTransformedValue(value, t));
177
+ });
178
+ await setDatarefValues(c.dataref, Number(value));
179
+ }
138
180
  }
139
181
  spinner.succeed(chalk.green(`${PREFIX} ${command}`));
140
182
  hideCursor();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-command",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Quick cockpit commands for X-Plane 12 - set your radios, altimeter, autopilot, and more from the terminal while flying.",
5
5
  "keywords": [
6
6
  "xplane",
package/src/api.js CHANGED
@@ -37,6 +37,22 @@ const parseDataref = (datarefString) => {
37
37
  return [match[1], match[2] ? parseInt(match[2]) : null];
38
38
  };
39
39
 
40
+ /**
41
+ * @param {string} commandString
42
+ * @return {[string, number|null]}
43
+ */
44
+ const parseCommand = (commandString) => {
45
+ // Regex pattern: captures base name and optional duration value
46
+ const pattern = /^(.+?)(?:\[(\d+)\])?$/;
47
+ const match = commandString.match(pattern);
48
+
49
+ if (!match) {
50
+ throw new CustomError("invalid_command");
51
+ }
52
+
53
+ return [match[1], match[2] ? parseInt(match[2]) : null];
54
+ };
55
+
40
56
  /**
41
57
  * @param {string} datarefName
42
58
  * @return { Promise<number | null> }
@@ -65,6 +81,54 @@ export const initAPI = (options) => {
65
81
  port = options.port;
66
82
  };
67
83
 
84
+ /**
85
+ * @param {string} commandName
86
+ * @return {Promise<number|string>}
87
+ */
88
+ export const getCommandId = async (commandName) => {
89
+ const url = new URL(`http://localhost:${port}/api/v2/commands`);
90
+ url.searchParams.set("filter[name]", commandName);
91
+ url.searchParams.set("fields", "id");
92
+
93
+ const response = await fetch(url);
94
+ const json = /** @type { {data:[{id: number }]} | APIError } */ (
95
+ await response.json()
96
+ );
97
+
98
+ if ("error_code" in json) {
99
+ throw new CustomError(json.error_code);
100
+ }
101
+
102
+ return json.data[0]?.id ?? null;
103
+ };
104
+
105
+ /**
106
+ * @param {string} commandNameWithOptionalDuration
107
+ */
108
+ export const activateCommand = async (commandNameWithOptionalDuration) => {
109
+ const [commandName, duration] = parseCommand(commandNameWithOptionalDuration);
110
+
111
+ const commandId = await getCommandId(commandName);
112
+
113
+ const url = new URL(
114
+ `http://localhost:${port}/api/v2/command/${commandId}/activate`,
115
+ );
116
+
117
+ const response = await fetch(url, {
118
+ method: "POST",
119
+ body: JSON.stringify({ duration: duration || 0 }),
120
+ });
121
+ const json = /** @type { {data: number|string } | APIError } */ (
122
+ await response.json()
123
+ );
124
+
125
+ if (json && "error_code" in json) {
126
+ throw new CustomError(json.error_code);
127
+ }
128
+
129
+ return json;
130
+ };
131
+
68
132
  /**
69
133
  * @param {string} datarefNameWithOptionalIndex
70
134
  * @return {Promise<number|string>}
@@ -175,3 +239,19 @@ export const setDatarefValues = async (
175
239
  await setDatarefValue(datarefNamesWithOptionalIndex, value);
176
240
  }
177
241
  };
242
+
243
+ /**
244
+ * @param {string|Array<string>} commandNamesWithOptionalDuration
245
+ * @return {Promise<void>}
246
+ */
247
+ export const activateCommands = async (commandNamesWithOptionalDuration) => {
248
+ if (Array.isArray(commandNamesWithOptionalDuration)) {
249
+ await Promise.all(
250
+ commandNamesWithOptionalDuration.map((command) =>
251
+ activateCommand(command),
252
+ ),
253
+ );
254
+ } else {
255
+ await activateCommand(commandNamesWithOptionalDuration);
256
+ }
257
+ };
package/src/config.js CHANGED
@@ -26,6 +26,7 @@ const __dirname = dirname(__filename);
26
26
  * @property {RegExp} pattern - Regular expression pattern to match commands
27
27
  * @property {'get' | 'set'} type - Operation type
28
28
  * @property {string|Array<string>} dataref - X-Plane dataref path(s)
29
+ * @property {string|Array<string>} command - X-Plane command path(s)
29
30
  * @property {Transform[]} transform - Array of transformation operations to apply
30
31
  */
31
32