forked from FabMo/FabMo-Engine
-
Notifications
You must be signed in to change notification settings - Fork 0
/
macros.js
459 lines (433 loc) · 15.1 KB
/
macros.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
/*
* macros.js
*
* Functions and data relating to macros.
*
* Macros are sort of "canned routines" that are analagous to the "custom cuts" in SB3.
* They are in fact, invoked in the same way in the OpenSBP runtime as they were in SB3,
* by using the C# command (C3 to home the tool, C2 for Z-zero, etc.)
*
* Macros are stored on disk at (for example) /opt/fabmo/macros - anything in this directory is scanned
* at startup and files containing an appropriate header are loaded into memory. Ideally macros can be
* in any file format, but the OpenSBP format is the only one that is actually implemented right now.
* When macros are modified by the user they are saved back to the files that they were loaded from. The
* header in each macro file contains metadata that identifies the macro, its custom-cut number, and description
*
* The macro headers are part of the files, but they are not displayed to the user when editing. The user
* is able to edit those fields, but only as exposed through the UI in the macro manager. This prevents
* users from corrupting the headers and creating a bunch of edge cases when editing macros.
*/
var fs = require("fs-extra");
var path = require("path");
var async = require("async");
var config = require("./config");
var log = require("./log").logger("macro");
// The marker in the header that signifies a macro.
// TODO - This is used to create files, but not in the regexs used to parse them (see below)
var MARKER = "!FABMO!";
// All the loaded macros will be stored here
var macros = {};
// These functions create macro headers from the specified options
// options:
// name - The macro display name
// description - The macro description
// enabled - Whether or not the macro is enabled (TODO: Is this used?)
var _createGCodeHeader = function (options) {
var name = options.name || "Untitled Macro";
var description = options.description || "";
var enabled = options.enabled || true;
return (
"(" +
MARKER +
"name:" +
name +
")\n" +
"(" +
MARKER +
"description:" +
description +
")\n" +
"(" +
MARKER +
"enabled:" +
enabled +
")\n"
);
};
var _createOpenSBPHeader = function (options) {
var name = options.name || "Untitled Macro";
var description = options.description || "";
var enabled = options.enabled || true;
return (
"'" +
MARKER +
"name:" +
name +
"\n" +
"'" +
MARKER +
"description:" +
description +
"\n" +
"'" +
MARKER +
"enabled:" +
enabled +
"\n"
);
};
var _deleteMacroFile = function (index, callback) {
var macro_path = config.getDataDir("macros");
var opensbp = path.join(macro_path, "macro_" + index + ".sbp");
var gcode = path.join(macro_path, "macro_" + index + ".nc");
// eslint-disable-next-line no-unused-vars
fs.unlink(opensbp, function (err) {
// eslint-disable-next-line no-unused-vars
fs.unlink(gcode, function (err) {
callback(null);
});
});
};
// Given a number and a type, construct a path to the corresponding macro file
var _createMacroFilename = function (id, type) {
var macro_path = config.getDataDir("macros");
switch (type) {
case "nc":
return path.join(macro_path, "macro_" + id + ".nc");
case "sbp":
return path.join(macro_path, "macro_" + id + ".sbp");
default:
throw new Error("Invalid macro type: " + type);
}
};
// Create default macro content for the specified macro.
// (Use if you want "new" macros to be non-empty)
var _createMacroDefaultContent = function (macro) {
switch (macro.type) {
case "nc":
//return '( ' + macro.name + ' )\n( ' + macro.description + ' )\n\n';
return "";
case "sbp":
//return "' " + macro.name + "\n' " + macro.description + "\n\n";
return "";
default:
throw new Error("Invalid macro type: " + macro.type);
}
};
// Iterate over the lines in the macro file, and parse out lines that appear to be part of the header
// filename - The filename of the macro to parse out
// callback - called with the parsed contents of the macro file, eg:
// {name : 'My Macro', description:'Move to X=10',content : 'MZ,0.5\nMX,10'}
var _parseMacroFile = function (filename, callback) {
var re = /[(']!FABMO!(\w+):([^)]*)\)?/;
var obj = {};
var ok = false;
fs.readFile(filename, function (err, data) {
if (err) {
log.error(err);
} else {
var lines = data.toString().split("\n");
var i = 0;
while (i < lines.length) {
var line = lines[i];
var groups = line.match(re);
if (groups) {
ok = true;
var key = groups[1];
var value = groups[2];
obj[key] = value;
} else {
break;
}
i += 1;
}
if (ok) {
obj.filename = filename;
obj.content = lines.slice(i, lines.length).join("\n");
callback(null, obj);
} else {
try {
log.error(
"File " +
filename +
" failed to parse. Unlinking it so it can be replaced."
);
fs.unlink(filename);
} finally {
callback(null, undefined);
}
}
}
});
};
// Update an existing macro with new content
// id - The macro to update
// macro - The macro object that contains the new content
// callback - called on completion, with an error if appropriate
var update = function (id, macro, callback) {
// Get the old macro data
var old_macro = get(id);
if (old_macro) {
// Here, we're updating an existing macro
// We only update fields that were provided in the macro passed in
// Other fields, we leave alone.
// Tried moving this function to outer scope but it caused issues with
// saving macros, will address later
// eslint-disable-next-line no-inner-declarations
function savemacro(id, callback) {
old_macro.name = macro.name || old_macro.name;
old_macro.description = macro.description || old_macro.description;
old_macro.content = macro.content || old_macro.content;
old_macro.index = macro.index || old_macro.index;
old_macro.type = macro.type || old_macro.type;
old_macro.filename = _createMacroFilename(
old_macro.index,
old_macro.type
);
save(id, callback);
}
// This function takes an id, and the macro can carry an index as well
// If the incoming macros index is different than the id that was passed,
// we interpret that as an intent to move that macro to a new index.
if (macro.index) {
var new_index = parseInt(macro.index);
// If there's already a macro at the index that we're moving to, that's an error.
// we're not going to write it.
if (get(new_index)) {
return callback(
new Error("There is already a macro #" + new_index)
);
}
// If the new index is different we actually want to move the macro,
// so we assign it to the new index, trash the macro at the old index
// trash the file at the old index, and finally save the file at the new index
if (new_index != old_macro.index) {
macros[new_index] = old_macro;
delete macros[old_macro.index];
// eslint-disable-next-line no-unused-vars
_deleteMacroFile(old_macro.index, function (err) {
savemacro(new_index, callback);
});
} else {
// Provided an index with the macro, but it's the the same, so no move needed
savemacro(id, callback);
}
} else {
// Not moving the macro (didn't provide an index) so just save it
savemacro(id, callback);
}
} else {
// In this case, we're "updating" a macro that doesn't exist, so create a new one
// (filling in any attributes that were not provided by the update)
var new_macro = {
name: macro.name || "Untitled Macro",
description: macro.description || "Macro Description",
type: macro.type || "sbp",
enabled: macro.enabled || true, // TODO fix this
index: id,
};
new_macro.filename = _createMacroFilename(id, new_macro.type);
new_macro.content =
macro.content || _createMacroDefaultContent(new_macro);
macros[id] = new_macro;
save(id, callback);
}
};
// Commit the provided macro id to disk.
// callback is called with the macro object that was saved (or error)
var save = function (id, callback) {
var macro = get(id);
if (macro) {
var macro_path = config.getDataDir("macros");
var file_path = path.join(
macro_path,
"macro_" + macro.index + "." + macro.type
);
switch (macro.type) {
case "nc":
var header = _createGCodeHeader(macro);
break;
case "sbp":
// eslint-disable-next-line no-redeclare
var header = _createOpenSBPHeader(macro);
break;
default:
setImmediate(
callback,
new Error("Invalid macro type: " + macro.type)
);
break;
}
fs.open(file_path, "w", function (err, fd) {
if (err) {
log.error(err);
return callback(err);
}
let contentString = header + macro.content;
var contents = Buffer.from(contentString);
fs.write(
fd,
contents,
0,
contents.length,
0,
// eslint-disable-next-line no-unused-vars
function (err, written, string) {
if (err) {
log.error(err);
return callback(err);
}
fs.fsync(fd, function (err) {
if (err) {
log.error(err);
}
fs.closeSync(fd);
log.debug("fsync()ed " + file_path);
callback(err, macro);
});
}
);
});
} else {
callback(new Error("No such macro " + id));
}
};
// Load all macros from disk
var load = function (callback) {
var macro_path = config.getDataDir("macros");
var re = /macro_([0-9]+)\.(nc|sbp)/;
macros = {};
fs.readdir(macro_path, function (err, files) {
if (err) {
callback(err);
} else {
for (var i = 0; i < files.length; i++) {
files[i] = path.join(macro_path, files[i]);
}
async.map(files, _parseMacroFile, function (err, results) {
results.forEach(function (info) {
if (info) {
var groups = info.filename.match(re);
if (groups) {
var idx = parseInt(groups[1]);
var ext = groups[2];
info.index = idx;
info.type = ext;
macros[idx] = info;
}
}
});
callback(null);
});
}
});
};
// Return the full list of macros (scrubbed)
var list = function () {
var retval = [];
for (var key in macros) {
retval.push(getInfo(key));
}
return retval;
};
// Get the metadata for a macro by index (null if no macro with that index)
var getInfo = function (idx) {
var macro = get(idx);
if (macro) {
return {
name: macro.name,
description: macro.description,
enabled: macro.enabled,
type: macro.type,
index: parseInt(macro.index),
};
} else {
return null;
}
};
// Retrieve a macro by index
var get = function (idx) {
return macros[idx] || null;
};
// Run a macro by index.
var run = function (idx) {
var machine = require("./machine").machine;
var bypassInterlock = false;
var info = macros[idx];
log.debug(idx);
log.debug(info);
if (parseInt(idx) === 2) {
bypassInterlock = true;
}
if (info) {
machine.runFile(info.filename, bypassInterlock);
} else {
throw new Error("No such macro.");
}
};
// Delete a macro by index
// callback returns an error only
var del = function (idx, callback) {
var info = macros[idx];
if (info) {
_deleteMacroFile(idx, function (err) {
if (err) {
callback(err);
} else {
delete macros[idx];
callback(null);
}
});
} else {
callback(new Error("No such macro: " + idx));
}
};
// Copy macros from the current profile to the macros directory.
// Only copies macros if they do not exist.
var loadProfileMacros = function (callback) {
var installedMacrosDir = config.getDataDir("macros");
var profileMacrosDir = config.getProfileDir("macros");
var copyIfNotExists = function (fn, callback) {
var a = path.join(profileMacrosDir, fn);
var b = path.join(installedMacrosDir, fn);
fs.stat(b, function (err, stats) {
if (!err && stats.isFile()) {
log.debug(
"Not Copying " +
a +
" -> " +
b +
" because it already exists."
);
callback();
} else {
log.debug(
"Copying " +
a +
" -> " +
b +
" because it doesnt already exist."
);
// eslint-disable-next-line no-unused-vars
fs.copy(a, b, function (err, data) {
callback(err);
});
}
});
};
fs.readdir(profileMacrosDir, function (err, files) {
if (err) {
return callback(err);
}
async.map(files, copyIfNotExists, callback);
});
};
exports.load = load;
exports.list = list;
exports.get = get;
exports.del = del;
exports.run = run;
exports.getInfo = getInfo;
exports.update = update;
exports.save = save;
exports.loadProfile = loadProfileMacros;