@microtronics/studio-cli 0.41.0 → 0.43.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/CHANGELOG.md CHANGED
@@ -1,11 +1,35 @@
1
1
  # Changelog
2
2
 
3
- ## 0.41.0 (2026-01-12)
3
+ ## 0.43.0 (2026-01-19)
4
4
 
5
5
  ### Features
6
6
 
7
- * add validation for maxInstancesPerSite manifest property ([065dc98](https://bitbucket.org/microtronics-core/vscode-studio/commits/065dc9809e8dccb75b74c5b52a58ce4ec15b5860))
8
- * use manifest maxInstancesPerSite for addon package header ([0350d2a](https://bitbucket.org/microtronics-core/vscode-studio/commits/0350d2af8ee3aae0ddbb00d7af5cd0b1c2c4cdd5))
7
+ * add debug option to enable mdnCom logging ([34dfd29](https://bitbucket.org/microtronics-core/vscode-studio/commits/34dfd29cf2f5e6776e31463a85f0484a6907b13b))
8
+ * add support for uploading any amx file to an usb device ([e89f108](https://bitbucket.org/microtronics-core/vscode-studio/commits/e89f1087e22e6c0cc88c8b68bbbcc4f1d813dc4f))
9
+ * add USB device detection and listing functionality ([906e3e1](https://bitbucket.org/microtronics-core/vscode-studio/commits/906e3e12bca647f5ceba8b4196f00d507be163d9))
10
+ * add USB device listing command ([fb16e1a](https://bitbucket.org/microtronics-core/vscode-studio/commits/fb16e1ab0c9a52e38467b664e4b2b6adb135a249)), closes [#close](https://bitbucket.org/microtronics-core/vscode-studio/issues/close)
11
+
12
+ ### Bug Fixes
13
+
14
+ * ensure proper exit code on successful USB upload ([cc1ae63](https://bitbucket.org/microtronics-core/vscode-studio/commits/cc1ae632669e785017c2afaa689c3750ba22d7fb))
15
+
16
+ ## 0.42.0 (2026-01-13)
17
+
18
+ ### Features
19
+
20
+ * add isIotApp helper function to check IoT app project type ([98ce8a1](https://bitbucket.org/microtronics-core/vscode-studio/commits/98ce8a1358bdd4d2ba1635cd4ef1a3f6e8ec0004))
21
+ * add POST endpoint for addon timeseries data insertion ([ace6880](https://bitbucket.org/microtronics-core/vscode-studio/commits/ace6880f801faefc303a8020c7acf87b06db5709))
22
+
23
+ ### Bug Fixes
24
+
25
+ * restrict write access rejection to IoT apps only ([13aa98f](https://bitbucket.org/microtronics-core/vscode-studio/commits/13aa98f261632b576681a894ecb375b7ab5a4b99))
26
+
27
+ ## 0.41.0 (2026-01-12)
28
+
29
+ ### Features
30
+
31
+ * add validation for maxInstancesPerSite manifest property ([065dc98](https://bitbucket.org/microtronics-core/vscode-studio/commits/065dc9809e8dccb75b74c5b52a58ce4ec15b5860))
32
+ * use manifest maxInstancesPerSite for addon package header ([0350d2a](https://bitbucket.org/microtronics-core/vscode-studio/commits/0350d2af8ee3aae0ddbb00d7af5cd0b1c2c4cdd5))
9
33
  * validate manifest before building package ([e70fc0e](https://bitbucket.org/microtronics-core/vscode-studio/commits/e70fc0ef9c89849ce2c19ec0a9bb575a764a3b07))
10
34
 
11
35
  ## 0.40.0 (2025-11-11)
@@ -2274,6 +2274,11 @@ export declare namespace Manifest {
2274
2274
  * @param manifest
2275
2275
  */
2276
2276
  export function isApp(manifest: Manifest.Manifest): boolean;
2277
+ /**
2278
+ * Check if the current project is an iot app project
2279
+ * @param manifest
2280
+ */
2281
+ export function isIotApp(manifest: Manifest.Manifest): boolean;
2277
2282
  /**
2278
2283
  * Check if the current project is an analytics project
2279
2284
  * @param manifest
@@ -495,7 +495,7 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
495
495
  apiPathOptions = [
496
496
  {
497
497
  path: `/api/1/sites/{siteUid}/addons/{addonUid}/${containerType}/${containerDefinition.title}`,
498
- params: [addonIdRef],
498
+ params: [contextIdRef, addonIdRef],
499
499
  tags: [containerRouteTagName]
500
500
  }
501
501
  ];
@@ -511,7 +511,8 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
511
511
  apiPathOptions.push(appCenterPathOptions);
512
512
  const apiPathMethods = {
513
513
  get: undefined,
514
- put: undefined
514
+ put: undefined,
515
+ post: undefined
515
516
  };
516
517
  const getMethod = {
517
518
  tags: [],
@@ -610,6 +611,41 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
610
611
  ...defaultResponseCodes
611
612
  }
612
613
  };
614
+ if (isAddonProject) {
615
+ apiPathMethods.post = {
616
+ tags: [],
617
+ summary: `Insert data into the "${tagNameFromContainerTitle(containerDefinition.title)}" container`,
618
+ parameters: [
619
+ {
620
+ name: 'allow_future_date',
621
+ in: 'query',
622
+ description: 'Allow inserting records with a stamp value in the future',
623
+ schema: {
624
+ type: 'boolean'
625
+ }
626
+ }
627
+ ],
628
+ requestBody: {
629
+ content: {
630
+ 'application/json': {
631
+ schema: {
632
+ $ref: `#/components/schemas/${containerDefinition.title}`
633
+ }
634
+ }
635
+ }
636
+ },
637
+ responses: {
638
+ 201: {
639
+ ...successfulOperationResponse
640
+ },
641
+ 400: {
642
+ description: 'Bad request',
643
+ ...defaultErrorResponse
644
+ },
645
+ ...defaultResponseCodes
646
+ }
647
+ };
648
+ }
613
649
  // only site and addons support timeseries
614
650
  const pathOptions = apiPathOptions[0];
615
651
  const methods = structuredClone(apiPathMethods);
@@ -634,7 +670,9 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
634
670
  };
635
671
  youngestMethod.get.summary = `Get the youngest record of "${containerDefinition.title}" container`;
636
672
  youngestMethod.get.responses['200'] = histdataSingleResponse;
637
- youngestMethod.get.parameters[1].schema.$ref = `#/components/schemas/${containerDefinition.title}RequestSingle`;
673
+ const parameterIdx = isAddonProject ? 2 : 1;
674
+ youngestMethod.get.parameters[parameterIdx].schema.$ref =
675
+ `#/components/schemas/${containerDefinition.title}RequestSingle`;
638
676
  apiDescription.paths[youngestPath] = youngestMethod;
639
677
  const oldestPath = `${pathOptions.path}/oldest`;
640
678
  const oldestMethod = {
@@ -642,7 +680,8 @@ function generatePathInformationFromContainer(apiDescription, containerDefinitio
642
680
  };
643
681
  oldestMethod.get.summary = `Get the oldest record of "${containerDefinition.title}" container`;
644
682
  oldestMethod.get.responses['200'] = histdataSingleResponse;
645
- oldestMethod.get.parameters[1].schema.$ref = `#/components/schemas/${containerDefinition.title}RequestSingle`;
683
+ oldestMethod.get.parameters[parameterIdx].schema.$ref =
684
+ `#/components/schemas/${containerDefinition.title}RequestSingle`;
646
685
  apiDescription.paths[oldestPath] = oldestMethod;
647
686
  }
648
687
  }
@@ -68,7 +68,8 @@ function generateXMLFromMap(manifest, ddeMap) {
68
68
  xmlAttr('download', '0');
69
69
  }
70
70
  // set edit=99 on upward containers to reject write access via API
71
- if ((manifest_1.Manifest.isApp(manifest) || manifest_1.Manifest.isLibrary(manifest)) &&
71
+ // only for IoT apps and libraries. For addons or analytics this is not needed.
72
+ if ((manifest_1.Manifest.isIotApp(manifest) || manifest_1.Manifest.isLibrary(manifest)) &&
72
73
  [TransferDirection.up, TransferDirection.upPlus].includes(containerDefinition.transferDirection)) {
73
74
  xmlAttr('edit', '99');
74
75
  }
package/out/main.js CHANGED
@@ -13,6 +13,7 @@ const update_notifier_1 = __importDefault(require("update-notifier"));
13
13
  const api_1 = require("./api");
14
14
  const log_1 = require("./log");
15
15
  const argumentParser_1 = require("./argumentParser");
16
+ const usb_1 = require("./usb");
16
17
  const program = new commander_1.Command();
17
18
  const notifier = (0, update_notifier_1.default)({ pkg: package_json_1.default });
18
19
  const logger = new log_1.Log.Logger();
@@ -145,6 +146,33 @@ program
145
146
  const { env, token } = getDefaultOptions(opts);
146
147
  main(command, (0, api_1.pack)(cwd, cliFS_1.cliFS, logger, env, token, opts.phase));
147
148
  });
149
+ // USB commands
150
+ const usbCommand = program.command('usb').description('USB device management commands');
151
+ usbCommand
152
+ .command('list')
153
+ .description('List available USB devices')
154
+ .action(async () => {
155
+ await (0, usb_1.printUsbDevices)(logger);
156
+ notifier.notify();
157
+ });
158
+ usbCommand
159
+ .command('upload')
160
+ .description('Upload an AMX binary to a USB device')
161
+ .requiredOption('-s, --serial <serial>', 'Device serial number (16 characters hex)')
162
+ .requiredOption('-f, --file <path>', 'Path to the AMX file (relative to current directory)')
163
+ .requiredOption('--debug', 'Enable debug logging', false)
164
+ .action(async (opts) => {
165
+ const fileUri = vscode_uri_1.Utils.joinPath(cwd, opts.file);
166
+ const result = await (0, usb_1.uploadToDevice)(opts.serial, fileUri, cliFS_1.cliFS, logger, opts.debug);
167
+ notifier.notify();
168
+ if (!result.success) {
169
+ logger.error(result.message);
170
+ process.exit(1);
171
+ }
172
+ else {
173
+ process.exit(0);
174
+ }
175
+ });
148
176
  module.exports = function (argv) {
149
177
  program.parse(argv);
150
178
  };
package/out/manifest.js CHANGED
@@ -161,6 +161,17 @@ var Manifest;
161
161
  return manifest.type === ProjectTypes.app;
162
162
  }
163
163
  Manifest.isApp = isApp;
164
+ /**
165
+ * Check if the current project is an iot app project
166
+ * @param manifest
167
+ */
168
+ function isIotApp(manifest) {
169
+ if (manifest.type === undefined) {
170
+ return true;
171
+ }
172
+ return manifest.type === ProjectTypes.app;
173
+ }
174
+ Manifest.isIotApp = isIotApp;
164
175
  /**
165
176
  * Check if the current project is an analytics project
166
177
  * @param manifest
package/out/usb.js ADDED
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listUsbDevices = listUsbDevices;
4
+ exports.printUsbDevices = printUsbDevices;
5
+ exports.findDeviceBySerialNumber = findDeviceBySerialNumber;
6
+ exports.uploadToDevice = uploadToDevice;
7
+ const usb_1 = require("usb");
8
+ const uto_master_1 = require("uto-master");
9
+ const utoStack_1 = require("uto-master/dist/src/utilities/utoStack");
10
+ // Microtronics vendor ID
11
+ const MICROTRONICS_VENDOR_ID = 0x2544;
12
+ /**
13
+ * Custom WebUSB instance configured to allow Microtronics devices
14
+ */
15
+ const customWebUSB = new usb_1.WebUSB({
16
+ // Bypass checking for authorized devices
17
+ allowedDevices: [
18
+ {
19
+ vendorId: MICROTRONICS_VENDOR_ID
20
+ }
21
+ ]
22
+ });
23
+ /**
24
+ * Lists all available USB devices filtered by Microtronics vendor ID
25
+ * @returns Promise resolving to an array of USB device information
26
+ */
27
+ async function listUsbDevices() {
28
+ const devices = await customWebUSB.getDevices();
29
+ return devices.map((device) => ({
30
+ productName: device.productName,
31
+ serialNumber: device.serialNumber || device.productName?.split(' ').pop(),
32
+ vendorId: device.vendorId,
33
+ productId: device.productId
34
+ }));
35
+ }
36
+ /**
37
+ * Prints USB device product names using the provided logger
38
+ * @param logger - The logger instance to use for output
39
+ */
40
+ async function printUsbDevices(logger) {
41
+ const devices = await listUsbDevices();
42
+ if (devices.length === 0) {
43
+ logger.info('No USB devices found.');
44
+ return;
45
+ }
46
+ logger.info('Available USB devices:');
47
+ devices.forEach((device, index) => {
48
+ const name = device.productName ||
49
+ `Unknown Device (VID: 0x${device.vendorId.toString(16)}, PID: 0x${device.productId.toString(16)})`;
50
+ const serial = device.serialNumber ? ` [${device.serialNumber}]` : '';
51
+ logger.log(` ${index + 1}. ${name}${serial}`);
52
+ });
53
+ }
54
+ /**
55
+ * Find a USB device by its serial number
56
+ * @param serialNumber - The 16-character hex serial number of the device
57
+ * @returns The USB device or null if not found
58
+ */
59
+ async function findDeviceBySerialNumber(serialNumber) {
60
+ const devices = await customWebUSB.getDevices();
61
+ for (const device of devices) {
62
+ // Check direct serial number or serial from product name
63
+ const deviceSerial = device.serialNumber || device.productName?.split(' ').pop();
64
+ if (deviceSerial?.toLowerCase() === serialNumber.toLowerCase()) {
65
+ return device;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Upload an AMX binary file to a USB device
72
+ * @param serialNumber - The serial number of the target device
73
+ * @param fileUri - The URI to the AMX file
74
+ * @param fs - The filesystem interface for file operations
75
+ * @param logger - The logger instance for output
76
+ * @param debugLog - Enable debug logging
77
+ * @returns Promise resolving to an UploadResult
78
+ */
79
+ async function uploadToDevice(serialNumber, fileUri, fs, logger, debugLog = false) {
80
+ // Check if file exists
81
+ const fileStat = await fs.stat(fileUri);
82
+ if (!fileStat) {
83
+ return {
84
+ success: false,
85
+ message: `File not found: ${fileUri.fsPath}`
86
+ };
87
+ }
88
+ // Find the device by serial number
89
+ logger.info(`Searching for device with serial number: ${serialNumber}`);
90
+ const device = await findDeviceBySerialNumber(serialNumber);
91
+ if (!device) {
92
+ return {
93
+ success: false,
94
+ message: `Device not found with serial number: ${serialNumber}`
95
+ };
96
+ }
97
+ logger.info(`Device found: ${device.productName}`);
98
+ // Read the file contents
99
+ const fileData = (await fs.readFile(fileUri));
100
+ // Initialize UTO Master
101
+ const utoMaster = new uto_master_1.UtoMaster();
102
+ let deviceConnected = false;
103
+ try {
104
+ await utoMaster.init();
105
+ // Set up UTO transmit callback
106
+ utoMaster.utoTx(async (data) => {
107
+ if (device.opened) {
108
+ await device.transferOut(uto_master_1.USB_CONFIGURATION.utoEndpoint, data);
109
+ }
110
+ });
111
+ if (debugLog) {
112
+ utoMaster.on('mdnCom', (message) => {
113
+ logger.log('mdnCom', message);
114
+ });
115
+ utoMaster.on('mdnComError', (message) => {
116
+ logger.log('mdnComError', message);
117
+ });
118
+ }
119
+ // Set up progress reporting
120
+ let lastProgress = -1;
121
+ utoMaster.on('installing', (data) => {
122
+ if (!data) {
123
+ return;
124
+ }
125
+ // /*const progress = Math.round(data.progress);
126
+ if (data !== lastProgress) {
127
+ lastProgress = data;
128
+ logger.info(`Upload progress: ${data}%`);
129
+ }
130
+ });
131
+ // Open device and claim interfaces
132
+ await device.open();
133
+ await device.selectConfiguration(uto_master_1.USB_CONFIGURATION.configurationNumber);
134
+ await device.claimInterface(uto_master_1.USB_CONFIGURATION.utoInterface);
135
+ deviceConnected = true;
136
+ logger.info('Device connected, starting upload...');
137
+ // Start UTO polling for receiving data
138
+ startUtoPolling(device, utoMaster, () => deviceConnected);
139
+ await utoMaster.requestDeviceInfo();
140
+ // Upload the file
141
+ await utoMaster.sendFile(utoStack_1.UtoFileType.pawn, new Uint8Array(fileData.buffer, fileData.byteOffset, fileData.length));
142
+ logger.done('Upload completed successfully!');
143
+ return {
144
+ success: true,
145
+ message: 'File uploaded successfully'
146
+ };
147
+ }
148
+ catch (error) {
149
+ const errorMessage = error instanceof Error ? error.message : String(error);
150
+ return {
151
+ success: false,
152
+ message: `Upload failed: ${errorMessage}`
153
+ };
154
+ }
155
+ finally {
156
+ // Clean up - close device
157
+ deviceConnected = false;
158
+ if (device.opened) {
159
+ try {
160
+ await device.close().catch();
161
+ }
162
+ catch {
163
+ // Ignore close errors
164
+ }
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Start polling for UTO data from the device
170
+ * @param device - The USB device
171
+ * @param utoMaster - The UTO Master instance
172
+ * @param isConnected - Function that returns connection status
173
+ */
174
+ async function startUtoPolling(device, utoMaster, isConnected) {
175
+ while (isConnected()) {
176
+ try {
177
+ const result = await device.transferIn(uto_master_1.USB_CONFIGURATION.utoEndpoint, uto_master_1.USB_CONFIGURATION.utoSize);
178
+ if (result.status === 'ok' && result.data) {
179
+ const uint8Data = new Uint8Array(result.data.buffer);
180
+ await utoMaster.utoRx(uint8Data);
181
+ }
182
+ }
183
+ catch {
184
+ // Connection lost or polling stopped
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ //# sourceMappingURL=usb.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microtronics/studio-cli",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": "Microtronics Studio CLI Tool",
5
5
  "main": "./out/api.js",
6
6
  "typings": "./dist/studio-cli.d.ts",
@@ -45,6 +45,8 @@
45
45
  "semver": "^7.7.2",
46
46
  "tinytar-fix": "^0.1.1",
47
47
  "update-notifier": "^7.3.1",
48
+ "usb": "^2.14.0",
49
+ "uto-master": "bitbucket:microtronics-core/wasm-uto-master",
48
50
  "vscode-uri": "^3.0.8",
49
51
  "yaml": "^2.6.1"
50
52
  },