Skip to content

Commit

Permalink
Performance enhancements for loading large G-code files (#108, #111)
Browse files Browse the repository at this point in the history
  • Loading branch information
cheton committed Jan 7, 2017
1 parent a510610 commit 8337795
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 225 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@
"express-session": "~1.14.2",
"font-awesome": "~4.7.0",
"fs-extra": "~1.0.0",
"gcode-interpreter": "~1.0.0",
"gcode-parser": "~1.0.0",
"gcode-toolpath": "~1.0.0",
"gcode-interpreter": "~1.1.1",
"gcode-parser": "~1.1.0",
"gcode-toolpath": "~1.1.1",
"history": "~3.2.1",
"hogan.js": "~3.0.2",
"i18next": "~4.1.4",
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/api.gcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ export const get = (req, res) => {
name: controller.sender.name,
data: controller.sender.gcode,
size: controller.sender.gcode.length,
remain: controller.sender.remain.length,
sent: controller.sender.sent.length,
total: controller.sender.total,
sent: controller.sender.sent,
received: controller.sender.received,
createdTime: controller.sender.createdTime,
startedTime: controller.sender.startedTime,
finishedTime: controller.sender.finishedTime
Expand Down
25 changes: 11 additions & 14 deletions src/app/controllers/Grbl/GrblController.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,18 @@ class GrblController {

const data = line + '\n';
this.serialport.write(data);

dbg(`[Grbl] > ${line}`);
});

// Sender
this.sender = new Sender({
streamingProtocol: STREAMING_PROTOCOL_CHAR_COUNTING,

this.sender = new Sender(STREAMING_PROTOCOL_CHAR_COUNTING, {
// Grbl has a 127 character serial receive buffer.
// Use a lower value to deduct the length of regular commands:
// - parser state command ($G\n)
// - current status command: (?)
//
// The amount of free space in the serial receive buffer is 125 (i.e. 127 - 2 - 1).
streamingBufferSize: 120
bufferSize: 120
});
this.sender.on('gcode', (gcode = '') => {
if (this.isClose()) {
Expand All @@ -143,6 +140,7 @@ class GrblController {
gcode = ('' + gcode).trim();
if (gcode.length > 0) {
this.serialport.write(gcode + '\n');
dbg(`[Grbl] > ${gcode}`);
}
});

Expand Down Expand Up @@ -502,17 +500,16 @@ class GrblController {
'load': () => {
const [name, gcode, callback = noop] = args;

this.sender.load(name, gcode, (err) => {
if (err) {
callback(err);
return;
}
const ok = this.sender.load(name, gcode);
if (!ok) {
callback(new Error(`Invalid G-code: name=${name}`));
return;
}

log.debug(`[Grbl] Load G-code: name="${this.sender.name}", size=${this.sender.gcode.length}, total=${this.sender.total}`);
log.debug(`[Grbl] Load G-code: name="${this.sender.name}", size=${this.sender.gcode.length}, total=${this.sender.total}`);

this.workflowState = WORKFLOW_STATE_IDLE;
callback(null, { name: name, gcode: gcode });
});
this.workflowState = WORKFLOW_STATE_IDLE;
callback(null, { name: name, gcode: gcode });
},
'unload': () => {
this.workflowState = WORKFLOW_STATE_IDLE;
Expand Down
22 changes: 10 additions & 12 deletions src/app/controllers/TinyG2/TinyG2Controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ class TinyG2Controller {
});

// Sender
this.sender = new Sender({
streamingProtocol: STREAMING_PROTOCOL_SEND_RESPONSE
});
this.sender = new Sender(STREAMING_PROTOCOL_SEND_RESPONSE);
this.sender.on('gcode', (gcode = '') => {
if (this.isClose()) {
log.error(`[TinyG2] The serial port "${this.options.port}" is not accessible`);
Expand All @@ -120,6 +118,7 @@ class TinyG2Controller {
if (gcode.length > 0) {
const cmd = JSON.stringify({ gc: gcode });
this.serialport.write(cmd + '\n');
dbg(`[TinyG2] > ${cmd}`);
}
});

Expand Down Expand Up @@ -492,17 +491,16 @@ class TinyG2Controller {
'load': () => {
const [name, gcode, callback = noop] = args;

this.sender.load(name, gcode, (err) => {
if (err) {
callback(err);
return;
}
const ok = this.sender.load(name, gcode);
if (!ok) {
callback(new Error(`Invalid G-code: name=${name}`));
return;
}

log.debug(`[TinyG2] Load G-code: name="${this.sender.name}", size=${this.sender.gcode.length}, total=${this.sender.total}`);
log.debug(`[TinyG2] Load G-code: name="${this.sender.name}", size=${this.sender.gcode.length}, total=${this.sender.total}`);

this.workflowState = WORKFLOW_STATE_IDLE;
callback(null, { name: name, gcode: gcode });
});
this.workflowState = WORKFLOW_STATE_IDLE;
callback(null, { name: name, gcode: gcode });
},
'unload': () => {
this.workflowState = WORKFLOW_STATE_IDLE;
Expand Down
157 changes: 85 additions & 72 deletions src/app/lib/gcode-sender.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,53 @@
import _ from 'lodash';
import events from 'events';
import * as parser from 'gcode-parser';

export const STREAMING_PROTOCOL_SEND_RESPONSE = 0;
export const STREAMING_PROTOCOL_CHAR_COUNTING = 1;

const DEFAULT_STREAMING_BUFFER_SIZE = 127;
const stripLine = (() => {
const re1 = new RegExp(/\s*[%#;].*/g); // Strip everything after %, #, or ; to the end of the line, including preceding spaces
const re2 = new RegExp(/\s*\(.*\)/g); // Remove anything inside the parentheses
return line => line.replace(re1, '').replace(re2, '').trim();
})();

class GCodeSender extends events.EventEmitter {
protocol = STREAMING_PROTOCOL_SEND_RESPONSE;
// The following properties are only used in character-counting streaming protocol
transmitBufferSize = 127; // Defaults to 127
transmitDataLength = 0;
transmitDataQueue = [];
transmitLine = '';

name = '';
gcode = '';
remain = [];
sent = [];
streamingBufferSize = DEFAULT_STREAMING_BUFFER_SIZE;
streamingProtocol = STREAMING_PROTOCOL_SEND_RESPONSE;
streamingQueue = []; // used in char-counting streaming protocol
lines = [];
total = 0;
sent = 0;
received = 0;
createdTime = 0;
startedTime = 0;
finishedTime = 0;
changed = false;

// @param {number} streamingProtocol The streaming protocol. 0 for send-response (default), 1 for character-counting.
// @param {number} streamingBufferSize The buffer size used in character-counting streaming protocol. Defaults to 127.
constructor(options) {
// @param {number} [protocol] The streaming protocol. 0 for send-response (default), 1 for character-counting.
// @param {object} [options] The options object.
// @param {number} [options.bufferSize] The buffer size used in character-counting streaming protocol. Defaults to 127.
constructor(protocol = STREAMING_PROTOCOL_SEND_RESPONSE, options = {}) {
super();

const {
streamingProtocol = STREAMING_PROTOCOL_SEND_RESPONSE,
streamingBufferSize = DEFAULT_STREAMING_BUFFER_SIZE
} = { ...options };

if (_.includes([
STREAMING_PROTOCOL_SEND_RESPONSE,
STREAMING_PROTOCOL_CHAR_COUNTING
], streamingProtocol)) {
this.streamingProtocol = streamingProtocol;
], protocol)) {
this.protocol = protocol;
}
if (_.isNumber(streamingBufferSize) && streamingBufferSize > 0) {
this.streamingBufferSize = streamingBufferSize;

if (this.protocol === STREAMING_PROTOCOL_CHAR_COUNTING) {
const { bufferSize } = { ...options };

if (_.isNumber(bufferSize) && bufferSize > 0) {
this.transmitBufferSize = bufferSize;
}
}

this.on('change', () => {
Expand All @@ -48,56 +56,50 @@ class GCodeSender extends events.EventEmitter {
}
get state() {
return {
protocol: this.protocol,
name: this.name,
size: this.gcode.length,
remain: this.remain.length,
sent: this.sent.length,
received: this.received,
total: this.total,
streaming: {
proto: this.streamingProtocol,
queue: this.streamingQueue.join('').length
},
sent: this.sent,
received: this.received,
createdTime: this.createdTime,
startedTime: this.startedTime,
finishedTime: this.finishedTime
};
}
load(name, gcode, callback) {
parser.parseString(gcode, (err, results) => {
if (err) {
callback(err);
return;
}
load(name, gcode = '') {
if (typeof gcode !== 'string') {
return false;
}

this.name = name;
this.gcode = gcode;
this.remain = _(results)
.map('words')
.map((words) => {
return _.map(words, (word) => word[0] + word[1]).join(' ') + '\n';
})
.value();
this.sent = [];
this.streamingQueue = [];
this.total = this.remain.length;
this.createdTime = new Date().getTime();
this.startedTime = 0;
this.finishedTime = 0;

this.emit('load', { name: name, gcode: gcode });
this.emit('change');
this.transmitDataLength = 0;
this.transmitDataQueue = [];
this.transmitLine = '';

this.name = name;
this.gcode = gcode;
this.lines = gcode.split('\n')
.filter(line => (line.trim().length > 0));
this.total = this.lines.length;
this.sent = 0;
this.createdTime = new Date().getTime();
this.startedTime = 0;
this.finishedTime = 0;

callback();
});
this.emit('load', { name: name, gcode: gcode });
this.emit('change');

return true;
}
unload() {
this.transmitDataLength = 0;
this.transmitDataQueue = [];
this.transmitLine = '';

this.name = '';
this.gcode = '';
this.remain = [];
this.sent = [];
this.streamingQueue = [];
this.total = 0;
this.sent = 0;
this.createdTime = 0;
this.startedTime = 0;
this.finishedTime = 0;
Expand All @@ -106,20 +108,23 @@ class GCodeSender extends events.EventEmitter {
this.emit('change');
}
next() {
if (this.remain.length === 0 && this.sent.length === 0) {
if (this.total === 0) {
return;
}
if (this.remain.length > 0 && this.sent.length === 0) {
if (this.total > 0 && this.sent === 0) {
this.startedTime = new Date().getTime();
this.emit('start', { time: this.startedTime });
this.emit('change');
}

const streamingMethod = {
[STREAMING_PROTOCOL_SEND_RESPONSE]: () => {
while (this.remain.length > 0) {
const gcode = ('' + this.remain.shift()).trim();
this.sent.push(gcode);
while (this.sent < this.total) {
const line = this.lines[this.sent];
const gcode = stripLine(line);

this.sent++;

this.emit('change');

if (gcode.length > 0) {
Expand All @@ -133,22 +138,28 @@ class GCodeSender extends events.EventEmitter {
}
},
[STREAMING_PROTOCOL_CHAR_COUNTING]: () => {
this.streamingQueue.shift();
if (this.transmitDataQueue.length > 0) {
const dataLength = this.transmitDataQueue.shift();
this.transmitDataLength -= dataLength;
}

while (this.remain.length > 0) {
const gcode = ('' + this.remain.shift()).trim();
const streamingQueueLength = this.streamingQueue.join('').length;
while (this.sent < this.total) {
const line = this.lines[this.sent];
this.transmitLine = this.transmitLine || stripLine(line);

if (gcode.length + streamingQueueLength >= this.streamingBufferSize) {
this.remain.unshift(gcode);
if (this.transmitLine.length + this.transmitDataLength >= this.transmitBufferSize) {
break;
}

this.sent.push(gcode);
const gcode = this.transmitLine;
this.transmitLine = ''; // clear transmitLine

this.sent++;
this.emit('change');

if (gcode.length > 0) {
this.streamingQueue.push(gcode);
this.transmitDataLength += gcode.length;
this.transmitDataQueue.push(gcode.length);
this.emit('gcode', gcode);
} else {
this.ack(); // Ack for empty lines
Expand All @@ -157,20 +168,22 @@ class GCodeSender extends events.EventEmitter {
// Continue to the next line if empty
}
}
}[this.streamingProtocol];
}[this.protocol];

streamingMethod && streamingMethod();

if (this.remain.length === 0) {
if (this.sent >= this.total) {
this.finishedTime = new Date().getTime();
this.emit('done', { time: this.finishedTime });
this.emit('change');
}
}
rewind() {
this.remain = this.sent.concat(this.remain);
this.sent = [];
this.streamingQueue = [];
this.transmitDataLength = 0;
this.transmitDataQueue = [];
this.transmitLine = '';

this.sent = 0;
this.received = 0;
this.startedTime = 0;
this.finishedTime = 0;
Expand Down
3 changes: 1 addition & 2 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@
"express-jwt": "~5.1.0",
"express-session": "~1.14.2",
"fs-extra": "~1.0.0",
"gcode-parser": "~0.8.2",
"hogan.js": "~3.0.2",
"i18next": "~4.1.4",
"i18next-express-middleware": "~1.0.2",
"i18next-node-fs-backend": "~0.1.3",
"jsonwebtoken": "~7.2.1",
"lodash": "~4.17.3",
"lodash": "~4.17.4",
"method-override": "~2.3.7",
"minimatch": "~3.0.3",
"morgan": "~1.7.0",
Expand Down
Loading

0 comments on commit 8337795

Please sign in to comment.