bb-com-extension 0.1.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.
@@ -0,0 +1,442 @@
1
+ import * as __WEBPACK_EXTERNAL_MODULE_fs__ from "fs";
2
+ /******/ var __webpack_modules__ = ({
3
+
4
+ /***/ "fs"
5
+ /*!*********************!*\
6
+ !*** external "fs" ***!
7
+ \*********************/
8
+ (module) {
9
+
10
+ module.exports = __WEBPACK_EXTERNAL_MODULE_fs__;
11
+
12
+ /***/ },
13
+
14
+ /***/ "./dist/src/ComLoaderTemplate.js"
15
+ /*!***************************************!*\
16
+ !*** ./dist/src/ComLoaderTemplate.js ***!
17
+ \***************************************/
18
+ (__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
19
+
20
+ __webpack_require__.r(__webpack_exports__);
21
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
22
+ /* harmony export */ "default": () => (/* export default binding */ __WEBPACK_DEFAULT_EXPORT__)
23
+ /* harmony export */ });
24
+ /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__() {
25
+ return `import { ComManager, ComType, ComMacro } from 'ellipsis-com'
26
+ import {autowired, named, factory} from 'ellipsis-ioc'
27
+
28
+ // TODO: Adopt a propper logging system.
29
+ let debug: (...args: any[]) => void
30
+ export function enableDebug(enable = true) {
31
+ if (enable) {
32
+ debug = console.log;
33
+ } else {
34
+ debug = () => { };
35
+ }
36
+ }
37
+ enableDebug(false)
38
+
39
+ const comManager = new ComManager()
40
+
41
+ @named('com-loader') // named so that it autocreates an instance
42
+ export class ComLoader {
43
+
44
+ @autowired('openapi-doc')
45
+ openapiDoc:any
46
+
47
+ constructor() {
48
+ setTimeout(() => {
49
+ comManager.init(this.loadTypes())
50
+ }, 1000)
51
+ }
52
+
53
+ @factory('com-manager') // make the manage available for injection
54
+ getComManager() {
55
+ return comManager
56
+ }
57
+
58
+ /**
59
+ * Load the com types from the OpenAPI document.
60
+ * TODO: This method needs to be moved into the bb-com-extension.
61
+ * @returns
62
+ */
63
+ loadTypes(): ComType[] {
64
+ const types: ComType[] = []
65
+
66
+ // Check for com types in the OpenAPI document:
67
+ if(!this.openapiDoc['x-bb-com-types']) {
68
+ debug("No com types found in OpenAPI document.")
69
+ return []
70
+ }
71
+
72
+ // Iterate over all com types in the OpenAPI document:
73
+ Object.keys(this.openapiDoc['x-bb-com-types']).forEach( (name: string) => {
74
+ const comType = this.openapiDoc['x-bb-com-types'][name]
75
+
76
+ // Ensure the baud rate is a number:
77
+ if(typeof comType.baud === 'string') {
78
+ comType.baud = parseInt(comType.baud)
79
+ }
80
+
81
+ // Extract all macros that specify this com type by name:
82
+ const macros = {} as {[method: string]: ComMacro[]}
83
+ Object.values(this.openapiDoc.paths).forEach( (path: any) => {
84
+ Object.keys(path)
85
+ .filter(method => ['get', 'post', 'put', 'patch', 'delete'].includes(method))
86
+ .filter(method => path[method]['x-bb-com-macro'])
87
+ .forEach(method => {
88
+ if(!path[method]['x-bb-com-macro'].commands ||
89
+ !path[method]['x-bb-com-macro'].responses ||
90
+ path[method]['x-bb-com-macro'].commands.length !== path[method]['x-bb-com-macro'].responses.length)
91
+ {
92
+ throw new Error('x-bb-com-macro commands and responses must be arrays of the same length in openapi.json at path '+path+'/'+method+'.')
93
+ }
94
+ if(!path[method].operationId) {
95
+ throw new Error(\`OperationId missing for method \${method} at path \${path} in openapi.json.\`)
96
+ }
97
+ macros[path[method].operationId] = (path[method]['x-bb-com-macro'].commands as string[]).map((c, i) => (
98
+ new ComMacro(c, new RegExp(path[method]['x-bb-com-macro'].responses[i].replace(/^\\/|\\/$/g, ''), 'gm'))
99
+ ))
100
+ })
101
+ })
102
+
103
+ // Init macro:
104
+ if(!comType.initCommands || comType.initCommands.length === 0) {
105
+ throw new Error(\`Com type '\${name}' is missing init commands.\`)
106
+ }
107
+ if(!comType.initResponses || comType.initResponses.length === 0) {
108
+ throw new Error(\`Com type '\${name}' is missing init responses.\`)
109
+ }
110
+ if(comType.initCommands.length !== comType.initResponses.length) {
111
+ throw new Error(\`Com type '\${name}' init commands and responses must be arrays of the same length.\`)
112
+ }
113
+ macros.init = []
114
+ comType.initCommands.forEach((cmd: string, i: number) => {
115
+ // Extract flags and strip slashes if present:
116
+ let response = comType.initResponses[i] as string
117
+ let flags = ''
118
+ if(response.startsWith('/')) {
119
+ const lastSlash = response.lastIndexOf('/')
120
+ if(lastSlash < response.length - 1) {
121
+ flags = response.substring(lastSlash + 1)
122
+ }
123
+ response = response.substring(1, lastSlash)
124
+ }
125
+
126
+ // Add the macro:
127
+ macros.init.push(
128
+ new ComMacro(
129
+ cmd,
130
+ new RegExp(response, flags)
131
+ )
132
+ )
133
+ })
134
+
135
+ // Add the com type:
136
+ types.push(new ComType(
137
+ name,
138
+ comType.baud,
139
+ macros as {[method: string]: ComMacro[], init: ComMacro[]},
140
+ comType.startupDelay
141
+ ))
142
+ debug(\`Loaded com type \${comType.toString()}\`);
143
+ })
144
+ return types
145
+ }
146
+ }
147
+ `;
148
+ }
149
+
150
+
151
+ /***/ }
152
+
153
+ /******/ });
154
+ /************************************************************************/
155
+ /******/ // The module cache
156
+ /******/ var __webpack_module_cache__ = {};
157
+ /******/
158
+ /******/ // The require function
159
+ /******/ function __webpack_require__(moduleId) {
160
+ /******/ // Check if module is in cache
161
+ /******/ var cachedModule = __webpack_module_cache__[moduleId];
162
+ /******/ if (cachedModule !== undefined) {
163
+ /******/ return cachedModule.exports;
164
+ /******/ }
165
+ /******/ // Check if module exists (development only)
166
+ /******/ if (__webpack_modules__[moduleId] === undefined) {
167
+ /******/ var e = new Error("Cannot find module '" + moduleId + "'");
168
+ /******/ e.code = 'MODULE_NOT_FOUND';
169
+ /******/ throw e;
170
+ /******/ }
171
+ /******/ // Create a new module (and put it into the cache)
172
+ /******/ var module = __webpack_module_cache__[moduleId] = {
173
+ /******/ // no module.id needed
174
+ /******/ // no module.loaded needed
175
+ /******/ exports: {}
176
+ /******/ };
177
+ /******/
178
+ /******/ // Execute the module function
179
+ /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
180
+ /******/
181
+ /******/ // Return the exports of the module
182
+ /******/ return module.exports;
183
+ /******/ }
184
+ /******/
185
+ /************************************************************************/
186
+ /******/ /* webpack/runtime/define property getters */
187
+ /******/ (() => {
188
+ /******/ // define getter functions for harmony exports
189
+ /******/ __webpack_require__.d = (exports, definition) => {
190
+ /******/ for(var key in definition) {
191
+ /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
192
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
193
+ /******/ }
194
+ /******/ }
195
+ /******/ };
196
+ /******/ })();
197
+ /******/
198
+ /******/ /* webpack/runtime/hasOwnProperty shorthand */
199
+ /******/ (() => {
200
+ /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
201
+ /******/ })();
202
+ /******/
203
+ /******/ /* webpack/runtime/make namespace object */
204
+ /******/ (() => {
205
+ /******/ // define __esModule on exports
206
+ /******/ __webpack_require__.r = (exports) => {
207
+ /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
208
+ /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
209
+ /******/ }
210
+ /******/ Object.defineProperty(exports, '__esModule', { value: true });
211
+ /******/ };
212
+ /******/ })();
213
+ /******/
214
+ /************************************************************************/
215
+ var __webpack_exports__ = {};
216
+ // This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
217
+ (() => {
218
+ /*!***********************!*\
219
+ !*** ./dist/index.js ***!
220
+ \***********************/
221
+ __webpack_require__.r(__webpack_exports__);
222
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
223
+ /* harmony export */ "default": () => (/* binding */ BBComExtension)
224
+ /* harmony export */ });
225
+ /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ "fs");
226
+ /* harmony import */ var _src_ComLoaderTemplate_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./src/ComLoaderTemplate.js */ "./dist/src/ComLoaderTemplate.js");
227
+
228
+
229
+ function logp(message) {
230
+ console.log(message);
231
+ }
232
+ function logerror(message) {
233
+ console.error(message);
234
+ }
235
+ class BBComExtension {
236
+ config;
237
+ bbDoc;
238
+ openapiDoc;
239
+ constructor(config, // CliConfig
240
+ bbDoc, openapiDoc) {
241
+ this.config = config;
242
+ this.bbDoc = bbDoc;
243
+ this.openapiDoc = openapiDoc;
244
+ }
245
+ getConfig() {
246
+ return {
247
+ name: "bb-com-extension",
248
+ types: {
249
+ 'com-type': 'Type of serial device.',
250
+ 'com-macro': 'A macro to send over the serial link.',
251
+ 'com-utils': 'Helper utilities for accessing serial communications.'
252
+ },
253
+ options: {
254
+ '--init-commands <string>': 'The serial command to send to initialise the device.',
255
+ '--init-responses <string>': 'The expected response from the device after sending the init command, given as a regular expression.',
256
+ '--commands <string>': 'The sequence of commands to send in the macro, separated by semicolons.',
257
+ '--responses <string>': 'Expected responses for each command given as regular expressions, separated by semicolons.',
258
+ '--baud <string>': 'The baud rate for the serial connection.',
259
+ '--method <string>': 'The HTTP method to use (GET, POST, etc.) for a macro.',
260
+ '--startup-delay <number>': 'Optional delay in milliseconds to wait after opening the port before sending the init commands, to allow devices extra time to boot up.'
261
+ }
262
+ };
263
+ }
264
+ add(type) {
265
+ if (type === 'com-type') {
266
+ this.addType();
267
+ }
268
+ else if (type === 'com-macro') {
269
+ this.addMacro();
270
+ }
271
+ }
272
+ /**
273
+ * bb add com-type --path /serial --baud 9600 --init "INIT_CMD" --init-response "/OK/"
274
+ */
275
+ addType() {
276
+ if (!this.openapiDoc['x-bb-com-types']) {
277
+ this.openapiDoc['x-bb-com-types'] = {};
278
+ }
279
+ this.openapiDoc['x-bb-com-types'][this.config.name] = {
280
+ baud: this.config.baud,
281
+ startupDelay: this.config.startupDelay,
282
+ initCommands: this.config.initCommands.split(';'),
283
+ initResponses: this.parseResponses(this.config.initResponses)
284
+ };
285
+ logp(`Added com type '${this.config.name}'.`);
286
+ }
287
+ /**
288
+ * bb add com-macro --path init --commands "CMD1;CMD2;CMD3" --responses "/RESP1/;/RESP2/;/RESP3/"
289
+ */
290
+ addMacro() {
291
+ if (!this.config.path) {
292
+ logerror("Please provide a path for the macro using --path.");
293
+ return;
294
+ }
295
+ const openApiPath = this.blackboxToOpenapiPath(this.config.path);
296
+ if (!openApiPath) {
297
+ logerror(`Path ${this.config.path} not found in OpenAPI document.`);
298
+ return;
299
+ }
300
+ const path = this.openapiDoc.paths[openApiPath];
301
+ if (!path) {
302
+ logerror(`Path ${this.config.path} not found in OpenAPI document.`);
303
+ return;
304
+ }
305
+ const method = this.config.method;
306
+ if (!method || !path[method.toLowerCase()]) {
307
+ logerror(`Method '${method}' not found at path ${this.config.path} in OpenAPI document.`);
308
+ return;
309
+ }
310
+ if (path[method.toLowerCase()]['x-bb-com-macro']) {
311
+ logerror(`A com macro already exists at path ${this.config.path} for method ${method}.`);
312
+ return;
313
+ }
314
+ if (!this.config.commands || !this.config.responses) {
315
+ logerror("Please provide both commands and responses for the macro using --commands and --responses.");
316
+ return;
317
+ }
318
+ const macroDef = {
319
+ name: this.config.name,
320
+ commands: this.config.commands.split(';'),
321
+ responses: this.parseResponses(this.config.responses)
322
+ };
323
+ path[method.toLowerCase()]['x-bb-com-macro'] = macroDef;
324
+ logp(`Added com macro at path '${this.config.path}' with ${macroDef.commands.length} command(s).`);
325
+ }
326
+ /**
327
+ * Extract the responses as regular expressions.
328
+ * Responses will be in the form: /response1/;/response2/;/response3/
329
+ * @param responsesStr
330
+ * @returns
331
+ */
332
+ parseResponses(responsesStr) {
333
+ if (!responsesStr.startsWith('/') || !responsesStr.endsWith('/')) {
334
+ logerror("Responses must be provided as regular expressions enclosed in slashes (/).");
335
+ return [];
336
+ }
337
+ return responsesStr.substring(1, responsesStr.length - 1).split('/;/').map((resp) => resp.trim());
338
+ }
339
+ delete(type) {
340
+ console.log(`Deleting com of type '${type} - NOT YET IMPLEMENTED.`);
341
+ }
342
+ blackboxToOpenapiPath(bbPath) {
343
+ for (let p in this.openapiDoc.paths) {
344
+ if (p.replaceAll(/\{[^}]+\}/gm, '#') === bbPath) {
345
+ return p;
346
+ }
347
+ }
348
+ return undefined;
349
+ }
350
+ generate(type) {
351
+ if (type !== 'com-utils') {
352
+ logerror(`Generation for type '${type}' not supported.`);
353
+ return;
354
+ }
355
+ // Create ComManager.ts if it doesn't exist:
356
+ if (!fs__WEBPACK_IMPORTED_MODULE_0__["default"].existsSync('gensrc/ComLoader.ts')) {
357
+ fs__WEBPACK_IMPORTED_MODULE_0__["default"].writeFileSync('gensrc/ComLoader.ts', (0,_src_ComLoaderTemplate_js__WEBPACK_IMPORTED_MODULE_1__["default"])());
358
+ logp("Generated gensrc/ComLoader.ts for serial communications.");
359
+ }
360
+ // Add ellipsis-com to package.json dependencies if not already present:
361
+ const packageJson = JSON.parse(fs__WEBPACK_IMPORTED_MODULE_0__["default"].readFileSync('package.json', 'utf-8'));
362
+ if (!packageJson.dependencies) {
363
+ packageJson.dependencies = {};
364
+ }
365
+ if (!packageJson.dependencies['ellipsis-com']) {
366
+ packageJson.dependencies['ellipsis-com'] = '^0.1.0';
367
+ fs__WEBPACK_IMPORTED_MODULE_0__["default"].writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
368
+ logp("Added 'ellipsis-com' to package.json dependencies.\nPlease run 'npm install' to install the new dependency.");
369
+ }
370
+ /*
371
+ // Generate services for each path with a com-type:
372
+ Object.keys(this.openapiDoc.paths).forEach( path => {
373
+
374
+ // Skip if the service file already exists:
375
+ const serviceName = path.substring(path.lastIndexOf('/')+1)
376
+ const serviceFileName = `src/${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)}Service.ts`
377
+ if(fs.existsSync(serviceFileName)) {
378
+ logp(`Service file ${serviceFileName} already exists - skipping generation.`)
379
+ return
380
+ }
381
+
382
+ const parentPathObj = this.openapiDoc.paths[path]
383
+ const comType = parentPathObj['x-bb-com-type']
384
+ if(!comType) {
385
+ return
386
+ }
387
+
388
+ // Extract methods for the parent service node:
389
+ const methods = Object.keys(parentPathObj)
390
+ .filter((method: string) => ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase()))
391
+ .reduce((methods: any[], method) => {
392
+ if (parentPathObj[method]['x-bb-com-macro']) {
393
+ const operationId = parentPathObj[method].operationId
394
+ methods.push({
395
+ path,
396
+ method,
397
+ operationId
398
+ })
399
+ }
400
+ return methods
401
+ }, [])
402
+
403
+ // Extract methods for the child object nodes:
404
+ if(parentPathObj['x-bb-service']?.access !== 'unique') {
405
+ Object.keys(this.openapiDoc.paths)
406
+ .filter( p => p.startsWith(path+'/') ) // child nodes
407
+ .forEach( p => {
408
+ const pathObj = this.openapiDoc.paths[p]
409
+ Object.keys(pathObj)
410
+ .filter((method: string) => ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase()))
411
+ .filter( method => !pathObj[method]['x-bb-com-macro']?.name || pathObj[method]['x-bb-com-macro'].name === comType.name) // Either the name is not specific (default to parent) or it matches the com-type name of the parent.
412
+ .forEach( method => {
413
+ if (pathObj[method]['x-bb-com-macro']) {
414
+ const operationId = pathObj[method].operationId
415
+ methods.push({
416
+ path: p,
417
+ method,
418
+ operationId
419
+ })
420
+ }
421
+ })
422
+ })
423
+ }
424
+
425
+ // Generate service code:
426
+ const serviceCode = serviceTemplate(this.openapiDoc, {
427
+ serviceName,
428
+ bbName: comType.name,
429
+ methods: methods
430
+ })
431
+
432
+ // Write service file:
433
+ fs.writeFileSync(serviceFileName, serviceCode)
434
+ logp(`Generated com service class ${serviceFileName}.`)
435
+ })*/
436
+ }
437
+ }
438
+
439
+ })();
440
+
441
+ const __webpack_exports__default = __webpack_exports__["default"];
442
+ export { __webpack_exports__default as default };
package/index.ts ADDED
@@ -0,0 +1,220 @@
1
+ import fs from 'fs'
2
+ import comLoaderTemplate from './src/ComLoaderTemplate.js'
3
+
4
+ function logp(message: string) {
5
+ console.log(message)
6
+ }
7
+
8
+ function logerror(message: string) {
9
+ console.error(message)
10
+ }
11
+
12
+ export default class BBComExtension {
13
+ constructor(
14
+ public config: any, // CliConfig
15
+ public bbDoc: any,
16
+ public openapiDoc: any
17
+ ) {}
18
+
19
+ getConfig() {
20
+ return {
21
+ name: "bb-com-extension",
22
+ types: {
23
+ 'com-type': 'Type of serial device.',
24
+ 'com-macro': 'A macro to send over the serial link.',
25
+ 'com-utils': 'Helper utilities for accessing serial communications.'
26
+ },
27
+ options: {
28
+ '--init-commands <string>': 'The serial command to send to initialise the device.',
29
+ '--init-responses <string>': 'The expected response from the device after sending the init command, given as a regular expression.',
30
+ '--commands <string>': 'The sequence of commands to send in the macro, separated by semicolons.',
31
+ '--responses <string>': 'Expected responses for each command given as regular expressions, separated by semicolons.',
32
+ '--baud <string>': 'The baud rate for the serial connection.',
33
+ '--method <string>': 'The HTTP method to use (GET, POST, etc.) for a macro.',
34
+ '--startup-delay <number>': 'Optional delay in milliseconds to wait after opening the port before sending the init commands, to allow devices extra time to boot up.'
35
+ }
36
+ }
37
+ }
38
+
39
+ add(type: string) {
40
+ if(type === 'com-type') {
41
+ this.addType()
42
+ } else if(type === 'com-macro') {
43
+ this.addMacro()
44
+ }
45
+ }
46
+
47
+ /**
48
+ * bb add com-type --path /serial --baud 9600 --init "INIT_CMD" --init-response "/OK/"
49
+ */
50
+ addType() {
51
+ if(!this.openapiDoc['x-bb-com-types']) {
52
+ this.openapiDoc['x-bb-com-types'] = {}
53
+ }
54
+ this.openapiDoc['x-bb-com-types'][this.config.name] = {
55
+ baud: this.config.baud,
56
+ startupDelay: this.config.startupDelay,
57
+ initCommands: this.config.initCommands.split(';'),
58
+ initResponses: this.parseResponses(this.config.initResponses)
59
+ }
60
+ logp(`Added com type '${this.config.name}'.`)
61
+ }
62
+
63
+ /**
64
+ * bb add com-macro --path init --commands "CMD1;CMD2;CMD3" --responses "/RESP1/;/RESP2/;/RESP3/"
65
+ */
66
+ addMacro() {
67
+ if(!this.config.path) {
68
+ logerror("Please provide a path for the macro using --path.")
69
+ return
70
+ }
71
+ const openApiPath = this.blackboxToOpenapiPath(this.config.path)
72
+ if(!openApiPath) {
73
+ logerror(`Path ${this.config.path} not found in OpenAPI document.`)
74
+ return
75
+ }
76
+ const path = this.openapiDoc.paths[openApiPath]
77
+ if(!path) {
78
+ logerror(`Path ${this.config.path} not found in OpenAPI document.`)
79
+ return
80
+ }
81
+ const method = this.config.method
82
+ if(!method || !path[method.toLowerCase()]) {
83
+ logerror(`Method '${method}' not found at path ${this.config.path} in OpenAPI document.`)
84
+ return
85
+ }
86
+ if(path[method.toLowerCase()]['x-bb-com-macro']) {
87
+ logerror(`A com macro already exists at path ${this.config.path} for method ${method}.`)
88
+ return
89
+ }
90
+ if(!this.config.commands || !this.config.responses) {
91
+ logerror("Please provide both commands and responses for the macro using --commands and --responses.")
92
+ return
93
+ }
94
+
95
+ const macroDef = {
96
+ name: this.config.name,
97
+ commands: this.config.commands.split(';'),
98
+ responses: this.parseResponses(this.config.responses)
99
+ }
100
+ path[method.toLowerCase()]['x-bb-com-macro'] = macroDef
101
+ logp(`Added com macro at path '${this.config.path}' with ${macroDef.commands.length} command(s).`)
102
+ }
103
+
104
+ /**
105
+ * Extract the responses as regular expressions.
106
+ * Responses will be in the form: /response1/;/response2/;/response3/
107
+ * @param responsesStr
108
+ * @returns
109
+ */
110
+ private parseResponses(responsesStr: any) {
111
+ if(!responsesStr.startsWith('/') || !responsesStr.endsWith('/')) {
112
+ logerror("Responses must be provided as regular expressions enclosed in slashes (/).")
113
+ return []
114
+ }
115
+ return responsesStr.substring(1, responsesStr.length - 1).split('/;/').map( (resp: string) => resp.trim() )
116
+ }
117
+
118
+ delete(type: string) {
119
+ console.log(`Deleting com of type '${type} - NOT YET IMPLEMENTED.`);
120
+ }
121
+
122
+ blackboxToOpenapiPath(bbPath: string) {
123
+ for(let p in this.openapiDoc.paths) {
124
+ if(p.replaceAll(/\{[^}]+\}/gm, '#') === bbPath) {
125
+ return p
126
+ }
127
+ }
128
+ return undefined
129
+ }
130
+
131
+ generate(type: string) {
132
+ if(type !== 'com-utils') {
133
+ logerror(`Generation for type '${type}' not supported.`)
134
+ return
135
+ }
136
+
137
+ // Create ComManager.ts if it doesn't exist:
138
+ if(!fs.existsSync('gensrc/ComLoader.ts')) {
139
+ fs.writeFileSync('gensrc/ComLoader.ts', comLoaderTemplate())
140
+ logp("Generated gensrc/ComLoader.ts for serial communications.")
141
+ }
142
+
143
+ // Add ellipsis-com to package.json dependencies if not already present:
144
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
145
+ if(!packageJson.dependencies) {
146
+ packageJson.dependencies = {}
147
+ }
148
+ if(!packageJson.dependencies['ellipsis-com']) {
149
+ packageJson.dependencies['ellipsis-com'] = '^0.1.0'
150
+ fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2))
151
+ logp("Added 'ellipsis-com' to package.json dependencies.\nPlease run 'npm install' to install the new dependency.")
152
+ }
153
+ /*
154
+ // Generate services for each path with a com-type:
155
+ Object.keys(this.openapiDoc.paths).forEach( path => {
156
+
157
+ // Skip if the service file already exists:
158
+ const serviceName = path.substring(path.lastIndexOf('/')+1)
159
+ const serviceFileName = `src/${serviceName.charAt(0).toUpperCase() + serviceName.slice(1)}Service.ts`
160
+ if(fs.existsSync(serviceFileName)) {
161
+ logp(`Service file ${serviceFileName} already exists - skipping generation.`)
162
+ return
163
+ }
164
+
165
+ const parentPathObj = this.openapiDoc.paths[path]
166
+ const comType = parentPathObj['x-bb-com-type']
167
+ if(!comType) {
168
+ return
169
+ }
170
+
171
+ // Extract methods for the parent service node:
172
+ const methods = Object.keys(parentPathObj)
173
+ .filter((method: string) => ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase()))
174
+ .reduce((methods: any[], method) => {
175
+ if (parentPathObj[method]['x-bb-com-macro']) {
176
+ const operationId = parentPathObj[method].operationId
177
+ methods.push({
178
+ path,
179
+ method,
180
+ operationId
181
+ })
182
+ }
183
+ return methods
184
+ }, [])
185
+
186
+ // Extract methods for the child object nodes:
187
+ if(parentPathObj['x-bb-service']?.access !== 'unique') {
188
+ Object.keys(this.openapiDoc.paths)
189
+ .filter( p => p.startsWith(path+'/') ) // child nodes
190
+ .forEach( p => {
191
+ const pathObj = this.openapiDoc.paths[p]
192
+ Object.keys(pathObj)
193
+ .filter((method: string) => ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase()))
194
+ .filter( method => !pathObj[method]['x-bb-com-macro']?.name || pathObj[method]['x-bb-com-macro'].name === comType.name) // Either the name is not specific (default to parent) or it matches the com-type name of the parent.
195
+ .forEach( method => {
196
+ if (pathObj[method]['x-bb-com-macro']) {
197
+ const operationId = pathObj[method].operationId
198
+ methods.push({
199
+ path: p,
200
+ method,
201
+ operationId
202
+ })
203
+ }
204
+ })
205
+ })
206
+ }
207
+
208
+ // Generate service code:
209
+ const serviceCode = serviceTemplate(this.openapiDoc, {
210
+ serviceName,
211
+ bbName: comType.name,
212
+ methods: methods
213
+ })
214
+
215
+ // Write service file:
216
+ fs.writeFileSync(serviceFileName, serviceCode)
217
+ logp(`Generated com service class ${serviceFileName}.`)
218
+ })*/
219
+ }
220
+ }
package/nodemon.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "exec": "tsc && webpack && cp bundle/index.js ../csiro-glombo/.bb/extensions/bb-com-extension-index.js",
3
+ "ext": "ts,json",
4
+ "ignore": [
5
+ "dist/*",
6
+ "node_modules/*",
7
+ "test/*"
8
+ ]
9
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "bb-com-extension",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Blackbox extension to allow creation of com port based services.",
6
+ "main": "index.ts",
7
+ "scripts": {
8
+ "dev-build": "nodemon",
9
+ "build": "tsc && webpack",
10
+ "test": "jest"
11
+ },
12
+ "author": "Ben Millar",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@types/node": "^24.9.2",
16
+ "@types/webpack-node-externals": "^3.0.4",
17
+ "nodemon": "^3.1.10",
18
+ "ts-loader": "^9.5.4",
19
+ "typescript": "^5.9.3",
20
+ "webpack": "^5.102.1",
21
+ "webpack-cli": "^6.0.1",
22
+ "webpack-node-externals": "^3.0.0"
23
+ }
24
+ }
@@ -0,0 +1,125 @@
1
+ export default function () {
2
+ return `import { ComManager, ComType, ComMacro } from 'ellipsis-com'
3
+ import {autowired, named, factory} from 'ellipsis-ioc'
4
+
5
+ // TODO: Adopt a propper logging system.
6
+ let debug: (...args: any[]) => void
7
+ export function enableDebug(enable = true) {
8
+ if (enable) {
9
+ debug = console.log;
10
+ } else {
11
+ debug = () => { };
12
+ }
13
+ }
14
+ enableDebug(false)
15
+
16
+ const comManager = new ComManager()
17
+
18
+ @named('com-loader') // named so that it autocreates an instance
19
+ export class ComLoader {
20
+
21
+ @autowired('openapi-doc')
22
+ openapiDoc:any
23
+
24
+ constructor() {
25
+ setTimeout(() => {
26
+ comManager.init(this.loadTypes())
27
+ }, 1000)
28
+ }
29
+
30
+ @factory('com-manager') // make the manage available for injection
31
+ getComManager() {
32
+ return comManager
33
+ }
34
+
35
+ /**
36
+ * Load the com types from the OpenAPI document.
37
+ * TODO: This method needs to be moved into the bb-com-extension.
38
+ * @returns
39
+ */
40
+ loadTypes(): ComType[] {
41
+ const types: ComType[] = []
42
+
43
+ // Check for com types in the OpenAPI document:
44
+ if(!this.openapiDoc['x-bb-com-types']) {
45
+ debug("No com types found in OpenAPI document.")
46
+ return []
47
+ }
48
+
49
+ // Iterate over all com types in the OpenAPI document:
50
+ Object.keys(this.openapiDoc['x-bb-com-types']).forEach( (name: string) => {
51
+ const comType = this.openapiDoc['x-bb-com-types'][name]
52
+
53
+ // Ensure the baud rate is a number:
54
+ if(typeof comType.baud === 'string') {
55
+ comType.baud = parseInt(comType.baud)
56
+ }
57
+
58
+ // Extract all macros that specify this com type by name:
59
+ const macros = {} as {[method: string]: ComMacro[]}
60
+ Object.values(this.openapiDoc.paths).forEach( (path: any) => {
61
+ Object.keys(path)
62
+ .filter(method => ['get', 'post', 'put', 'patch', 'delete'].includes(method))
63
+ .filter(method => path[method]['x-bb-com-macro'])
64
+ .forEach(method => {
65
+ if(!path[method]['x-bb-com-macro'].commands ||
66
+ !path[method]['x-bb-com-macro'].responses ||
67
+ path[method]['x-bb-com-macro'].commands.length !== path[method]['x-bb-com-macro'].responses.length)
68
+ {
69
+ throw new Error('x-bb-com-macro commands and responses must be arrays of the same length in openapi.json at path '+path+'/'+method+'.')
70
+ }
71
+ if(!path[method].operationId) {
72
+ throw new Error(\`OperationId missing for method \${method} at path \${path} in openapi.json.\`)
73
+ }
74
+ macros[path[method].operationId] = (path[method]['x-bb-com-macro'].commands as string[]).map((c, i) => (
75
+ new ComMacro(c, new RegExp(path[method]['x-bb-com-macro'].responses[i].replace(/^\\/|\\/$/g, ''), 'gm'))
76
+ ))
77
+ })
78
+ })
79
+
80
+ // Init macro:
81
+ if(!comType.initCommands || comType.initCommands.length === 0) {
82
+ throw new Error(\`Com type '\${name}' is missing init commands.\`)
83
+ }
84
+ if(!comType.initResponses || comType.initResponses.length === 0) {
85
+ throw new Error(\`Com type '\${name}' is missing init responses.\`)
86
+ }
87
+ if(comType.initCommands.length !== comType.initResponses.length) {
88
+ throw new Error(\`Com type '\${name}' init commands and responses must be arrays of the same length.\`)
89
+ }
90
+ macros.init = []
91
+ comType.initCommands.forEach((cmd: string, i: number) => {
92
+ // Extract flags and strip slashes if present:
93
+ let response = comType.initResponses[i] as string
94
+ let flags = ''
95
+ if(response.startsWith('/')) {
96
+ const lastSlash = response.lastIndexOf('/')
97
+ if(lastSlash < response.length - 1) {
98
+ flags = response.substring(lastSlash + 1)
99
+ }
100
+ response = response.substring(1, lastSlash)
101
+ }
102
+
103
+ // Add the macro:
104
+ macros.init.push(
105
+ new ComMacro(
106
+ cmd,
107
+ new RegExp(response, flags)
108
+ )
109
+ )
110
+ })
111
+
112
+ // Add the com type:
113
+ types.push(new ComType(
114
+ name,
115
+ comType.baud,
116
+ macros as {[method: string]: ComMacro[], init: ComMacro[]},
117
+ comType.startupDelay
118
+ ))
119
+ debug(\`Loaded com type \${comType.toString()}\`);
120
+ })
121
+ return types
122
+ }
123
+ }
124
+ `
125
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "nodeNext",
5
+ "outDir": "./dist",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "isolatedModules": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": [
14
+ "src/**/*",
15
+ "index.ts"
16
+ ],
17
+ "exclude": [
18
+ "node_modules",
19
+ "**/*.spec.ts"
20
+ ]
21
+ }
@@ -0,0 +1,63 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { readFileSync } from 'fs';
4
+ import nodeExternals from 'webpack-node-externals';
5
+
6
+ // __dirname is not available in ES Modules, so we need to recreate it.
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ // Read dependencies from package.json to exclude them from the bundle
11
+ const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
12
+ const dependencies = Object.keys(pkg.dependencies || {});
13
+ const peerDependencies = Object.keys(pkg.peerDependencies || {});
14
+ const externals = dependencies.concat(peerDependencies);
15
+
16
+ export default {
17
+ // Sets the bundling mode.
18
+ mode: 'development',
19
+ // IMPORTANT: Set `devtool` to `false` to disable the `eval` source map.
20
+ devtool: false,
21
+ // Enable experimental module output
22
+ experiments: {
23
+ outputModule: true,
24
+ },
25
+ // Your module's main entry point.
26
+ entry: './dist/index.js',
27
+ // How the module is outputted.
28
+ output: {
29
+ filename: 'index.js',
30
+ path: path.resolve(__dirname, 'bundle'),
31
+ // IMPORTANT: Set the library type to 'module' for ESM output
32
+ library: {
33
+ type: 'module',
34
+ },
35
+ // Explicitly set chunk format to module for a clean build
36
+ chunkFormat: 'module',
37
+ },
38
+ // Tells Webpack to handle .ts and .js files.
39
+ resolve: {
40
+ extensions: ['.ts', '.js'],
41
+ },
42
+ module: {
43
+ rules: [
44
+ {
45
+ test: /\.ts$/,
46
+ use: 'ts-loader',
47
+ exclude: /node_modules/,
48
+ },
49
+ ],
50
+ },
51
+ // Specifies the target environment as Node.js.
52
+ target: 'node23',
53
+ // Exclude external dependencies from the bundle.
54
+ externals: [
55
+ nodeExternals(),
56
+ ...externals,
57
+ ],
58
+ // Optionally, add optimization settings for readability.
59
+ optimization: {
60
+ // Disable minification for readable output.
61
+ minimize: false,
62
+ },
63
+ };