diff --git a/examples/netz/index.html b/examples/netz/index.html
new file mode 100644
index 0000000..43a2556
--- /dev/null
+++ b/examples/netz/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+ Wall Plotter — zigzag
+
+
+
+
+
+
+ Wall Plotter — zigzag
+
+
+
diff --git a/examples/netz/sketch.js b/examples/netz/sketch.js
new file mode 100644
index 0000000..2c98882
--- /dev/null
+++ b/examples/netz/sketch.js
@@ -0,0 +1,109 @@
+let wp;
+
+let area = 320;
+
+function setup() {
+ const canvas = createCanvas(windowWidth, windowHeight);
+ canvas.parent('sketch-host');
+
+ wp = new WallPlotter({
+ penUpGcode: 'M280 P0 S120',
+ penDownGcode: 'M280 P0 S30',
+ penUpDelayMs: 250,
+ penDownDelayMs: 150,
+ });
+ wp.setSpeed(10000, 10000);
+ wp.setTimingMultiplier(1); // tune if machine is slower than speed estimate, the higher the slower
+ wp.enableKeyInputs();
+ wp.enablePlotButtons();
+}
+
+function draw() {
+ background(28, 28, 32);
+ wp.showInfos();
+}
+
+function windowResized() {
+ resizeCanvas(windowWidth, windowHeight);
+}
+
+async function plot() {
+ await drawArt(false);
+}
+
+async function test() {
+ await drawArt(true);
+}
+
+async function drawArt(testOnly) {
+ wp.startPath();
+
+
+ wp.move(-area, -area);
+ //wp.wait(1000 * 10);
+
+ let step = area / 40;
+
+ let points = [];
+ let last_points = [];
+ let row = -1;
+
+ for (let y = -area; y <= area; y+=step){
+
+ // clear points
+ if(points.length>0){
+ last_points = deepCopy(points);
+ points = [];
+ }
+
+ let px = -area;
+ let py = y+random(10)
+ if (row==1){
+ py = last_points[0].y;
+ }
+
+
+ wp.move(px, py);
+ points.push(createVector(px,py));
+
+ let index = 1;
+ for (let x = -area+step; x <= area; x+=step){
+ px = x;
+ py = y+random(step);
+
+ if (row==1 && index%2==0){
+ py = last_points[index].y
+ }
+
+ if (row==1 && index%2!=0){
+ py = last_points[index-1].y+random(step*3);
+ }
+
+ if (last_points.length>0 && row==-1 && index%2!=0){
+ py = last_points[index].y
+ }
+
+ if (last_points.length>0 && row==-1 && index%2==0){
+ py = last_points[index-1].y+random(step*1);
+ }
+
+ wp.draw(px, py);
+ points.push(createVector(px,py));
+
+ index++;
+ }
+
+ row = row*-1;
+
+ }
+ await wp.runPath(testOnly);
+}
+
+
+function deepCopy(copy_array){
+ let new_array = []
+ for(let i=0; i
-
+
diff --git a/lib/README.md b/lib/README.md
index 4aa1779..2665817 100644
--- a/lib/README.md
+++ b/lib/README.md
@@ -47,6 +47,7 @@ Keyboard controls. Call from `setup()`. No arguments.
| Key | Action |
|-----|--------|
| `C` | Connect / disconnect |
+| `T` | Run Test (preview) |
| `H` | Set home here (`G92 X0 Y0`) — required before Plot |
| `0` | Travel to `(0, 0)` |
| `P` | Toggle pen |
diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js
index 42b9aee..89dda8e 100644
--- a/lib/WallPlotter.js
+++ b/lib/WallPlotter.js
@@ -18,7 +18,7 @@ class WallPlotter {
// Static constants
// ---------------------------------------------------------------------------
- static VERSION = '2.2.3';
+ static VERSION = '2.2.5';
static MIN_FEED_MM_MIN = 1;
static MAX_FEED_MM_MIN = 10000;
/** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */
@@ -165,7 +165,7 @@ class WallPlotter {
// Public API — setup / UI
// ===========================================================================
- /** Enable keyboard jog, home, pen toggle, connect (C), stop (S). Call from setup(). */
+ /** Enable keyboard jog, home, pen toggle, connect (C), test (T), stop (S). Call from setup(). */
enableKeyInputs() {
if (this._keysEnabled) return;
this._keysEnabled = true;
@@ -588,13 +588,24 @@ class WallPlotter {
const waitOk = options.waitOk !== false;
const okPromise = waitOk
- ? this._waitForOk(options.timeoutMs ?? WallPlotter.OK_TIMEOUT_MS)
+ ? this._waitForOk(options.timeoutMs ?? WallPlotter.OK_TIMEOUT_MS, {
+ command,
+ waitLogAfterMs: options.waitLogAfterMs,
+ })
: null;
- await this.writer.write(new TextEncoder().encode(command + '\n'));
- this.onLog('→ ' + command);
- if (!okPromise) return;
try {
- await okPromise;
+ const written = this.writer.write(
+ new TextEncoder().encode(command + '\n')
+ );
+ if (okPromise) {
+ await Promise.all([
+ written.then(() => this.onLog('→ ' + command)),
+ okPromise,
+ ]);
+ } else {
+ await written;
+ this.onLog('→ ' + command);
+ }
} catch (err) {
if (err.name === 'AbortError') throw err;
this._okIgnoreUntil = performance.now() + 400;
@@ -818,6 +829,15 @@ class WallPlotter {
} finally {
this.feedRateTravel = savedFeedTravel;
this.feedRateDraw = savedFeedDraw;
+ if (!testOnly && this.connected) {
+ try {
+ await this.penUp();
+ } catch (_) {
+ /* port may be gone */
+ }
+ } else {
+ this.isPenDown = false;
+ }
this.isPlotting = false;
this.isPaused = false;
this._pauseStartedAt = 0;
@@ -898,7 +918,7 @@ class WallPlotter {
}
async _animatePosition(fromX, fromY, cmd, duration) {
- this.isPenDown = cmd.type === 'draw';
+ if (this._plotTestOnly) this.isPenDown = cmd.type === 'draw';
const waitMs = Math.max(duration, 1);
let elapsed = 0;
let last = performance.now();
@@ -976,11 +996,11 @@ class WallPlotter {
async _sendSegmentAndWait(cmd) {
await this._sendSegment(cmd);
+ const motionMs = Math.round(this._lastSegmentMs);
await this.send('M400', {
- timeoutMs: Math.max(
- WallPlotter.OK_TIMEOUT_MS,
- Math.round(this._lastSegmentMs) + 20000
- ),
+ timeoutMs: Math.max(WallPlotter.OK_TIMEOUT_MS, motionMs + 20000),
+ // M400 is supposed to last as long as the move. Don't call that "stuck".
+ waitLogAfterMs: Math.max(WallPlotter.OK_WAIT_LOG_MS, motionMs + 1500),
});
}
@@ -988,9 +1008,15 @@ class WallPlotter {
// Internals — serial I/O
// ===========================================================================
- _waitForOk(timeoutMs) {
+ _waitForOk(timeoutMs, options = {}) {
+ const command = options.command || '';
+ const waitLogAfterMs = options.waitLogAfterMs ?? WallPlotter.OK_WAIT_LOG_MS;
+ const waitLabel = command ? ` (${command})` : '';
+
return new Promise((resolve, reject) => {
let settled = false;
+ let timer = null;
+ let waitLogTimer = null;
let waitLogInterval = null;
const finish = (fn, arg) => {
@@ -1004,6 +1030,17 @@ class WallPlotter {
fn(arg);
};
+ const armTimer = () => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ const err = new Error(
+ `timeout waiting for Marlin ok${waitLabel} (${Math.round(timeoutMs / 1000)}s silent)`
+ );
+ err.name = 'TimeoutError';
+ entry.reject(err);
+ }, timeoutMs);
+ };
+
const entry = {
resolve: () => {
if (this.statusText === 'Waiting for plotter…') {
@@ -1015,20 +1052,17 @@ class WallPlotter {
finish(resolve);
},
reject: (err) => finish(reject, err),
+ bump: () => {
+ if (!settled) armTimer();
+ },
};
- const timer = setTimeout(() => {
- entry.reject(
- new Error(
- `timeout waiting for Marlin ok (${Math.round(timeoutMs / 1000)}s)`
- )
- );
- }, timeoutMs);
+ armTimer();
- const waitLogTimer = setTimeout(() => {
+ waitLogTimer = setTimeout(() => {
if (settled) return;
const started = performance.now();
- this.onLog('… waiting for Marlin ok');
+ this.onLog(`… waiting for Marlin ok${waitLabel}`);
if (this.statusText !== 'Waiting for plotter…') {
this._statusBeforeOkWait = this.statusText;
}
@@ -1036,9 +1070,9 @@ class WallPlotter {
this._redrawSketch();
waitLogInterval = setInterval(() => {
const s = Math.round((performance.now() - started) / 1000);
- this.onLog(`… still waiting for Marlin ok (${s}s)`);
+ this.onLog(`… still waiting for Marlin ok${waitLabel} (${s}s)`);
}, WallPlotter.OK_WAIT_PING_MS);
- }, WallPlotter.OK_WAIT_LOG_MS);
+ }, waitLogAfterMs);
this._okResolvers.push(entry);
});
@@ -1066,12 +1100,7 @@ class WallPlotter {
const { value, done } = await plotter.reader.read();
if (done) break;
plotter._rxBuf += new TextDecoder().decode(value);
- const lines = plotter._rxBuf.split(/\r?\n/);
- plotter._rxBuf = lines.pop();
- for (const line of lines) {
- const trimmed = line.trim();
- if (trimmed) plotter._handleResponse(trimmed);
- }
+ plotter._drainRxBuffer();
}
} catch (err) {
if (plotter.readLoopActive) {
@@ -1090,9 +1119,28 @@ class WallPlotter {
loop();
}
+ _drainRxBuffer() {
+ const lines = this._rxBuf.split(/\r?\n/);
+ this._rxBuf = lines.pop();
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (trimmed) this._handleResponse(trimmed);
+ }
+ // USB-CDC sometimes delivers a complete "ok" without a trailing newline
+ // until the next chunk. Don't leave that sitting in the buffer.
+ const rest = this._rxBuf.trim();
+ if (/^(echo:\s*)?ok\b/i.test(rest)) {
+ this._handleResponse(rest);
+ this._rxBuf = '';
+ }
+ }
+
_handleResponse(line) {
if (/busy:\s*processing/i.test(line)) {
this._machineBusy = true;
+ for (const entry of this._okResolvers) {
+ if (typeof entry.bump === 'function') entry.bump();
+ }
const now = performance.now();
if (now - this._lastBusyLogAt > 4000) {
this._lastBusyLogAt = now;
@@ -1101,14 +1149,14 @@ class WallPlotter {
return;
}
this.onLog('← ' + line);
- if (/^ok\b/i.test(line)) {
+ if (/^(echo:\s*)?ok\b/i.test(line)) {
this._machineBusy = false;
- if (performance.now() < this._okIgnoreUntil) {
- this.onLog('← ok ignored (stale)');
- return;
- }
const next = this._okResolvers.shift();
- if (next) next.resolve();
+ if (next) {
+ next.resolve();
+ } else if (performance.now() < this._okIgnoreUntil) {
+ this.onLog('← ok ignored (stale, no waiter)');
+ }
}
this._parseMachineBounds(line);
if (line.startsWith('X:')) {
@@ -1176,6 +1224,10 @@ class WallPlotter {
await this.stopPlot();
return false;
}
+ if (k === 't') {
+ this._onTestButton();
+ return false;
+ }
if (!this._keysEnabled || !this.connected) return;
@@ -1613,6 +1665,7 @@ class WallPlotter {
'H set current pos as home (G92) — required first',
'0 go to origin (0, 0)',
'P toggle pen (test up/down angles)',
+ 'T run test (preview)',
'S stop sending (in-flight stroke still finishes)',
'C connect / disconnect',
];