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 +43 -9
- package/bin/index.js +53 -11
- package/package.json +1 -1
- package/src/api.js +80 -0
- package/src/config.js +1 -0
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
|
|
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
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
|
|