diff --git a/.gitignore b/.gitignore
index 3c25e1e..dc9966e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
node_modules/
dist/
*.log
+*.swp
+*.swo
diff --git a/examples/balls/Ball.ts b/examples/balls/Ball.ts
index 7ca6b65..8e8a4ee 100644
--- a/examples/balls/Ball.ts
+++ b/examples/balls/Ball.ts
@@ -1,4 +1,4 @@
-import { Rect } from '@laser-dac/draw';
+import { Circle } from '@laser-dac/draw';
import Victor = require('victor');
interface BallOptions {
@@ -12,8 +12,8 @@ export class Ball {
x: number;
y: number;
vector: Victor = new Victor(
- (Math.random() - 0.5) / 200,
- (Math.random() - 0.5) / 200
+ (Math.random() - 0.5) / 2,
+ (Math.random() - 0.5) / 2
);
constructor(options: BallOptions) {
@@ -22,9 +22,9 @@ export class Ball {
this.radius = options.radius;
}
- update = () => {
- this.x += this.vector.x;
- this.y += this.vector.y;
+ update = (timeStep: number) => {
+ this.x += this.vector.x * timeStep;
+ this.y += this.vector.y * timeStep;
this.updateBounds();
};
@@ -32,31 +32,34 @@ export class Ball {
updateBounds = () => {
// Left bound.
if (this.x - this.radius <= 0) {
+ this.x = this.radius;
this.vector.invertX();
}
// Right bound.
- if (this.x + this.radius >= 1) {
+ if (this.x + this.radius > 1) {
+ this.x = 1 - this.radius;
this.vector.invertX();
}
// Top bound.
if (this.y - this.radius <= 0) {
+ this.y = this.radius;
this.vector.invertY();
}
// Bottom bound.
- if (this.y + this.radius >= 1) {
+ if (this.y + this.radius > 1) {
+ this.y = 1 - this.radius;
this.vector.invertY();
}
};
draw = () =>
- new Rect({
- x: this.x - this.radius,
- y: this.y - this.radius,
- width: this.radius * 2,
- height: this.radius * 2,
+ new Circle({
+ x: this.x,
+ y: this.y,
+ radius: this.radius,
color: [0, 1, 0]
});
}
diff --git a/examples/balls/index.ts b/examples/balls/index.ts
index d75dcb8..1bc6c52 100644
--- a/examples/balls/index.ts
+++ b/examples/balls/index.ts
@@ -1,6 +1,5 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { EtherDream } from '@laser-dac/ether-dream';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, Rect } from '@laser-dac/draw';
import { Ball } from './Ball';
@@ -8,18 +7,15 @@ const NUMBER_OF_BALLS = 4;
(async () => {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new EtherDream());
- }
+ dac.useAll(await getDevices());
await dac.start();
const balls: Ball[] = [];
for (let i = 0; i < NUMBER_OF_BALLS; i++) {
balls.push(
new Ball({
- x: 0.5,
- y: 0.5,
+ x: Math.random(),
+ y: Math.random(),
radius: Math.random() / 5 + 0.05
})
);
@@ -28,6 +24,8 @@ const NUMBER_OF_BALLS = 4;
const scene = new Scene({
resolution: 70
});
+
+ let lastTime = Date.now();
function renderFrame() {
const bounds = new Rect({
x: 0,
@@ -38,10 +36,13 @@ const NUMBER_OF_BALLS = 4;
});
scene.add(bounds);
+ const curTime = Date.now();
+ const timeStep = (curTime - lastTime) / 1000;
balls.forEach(ball => {
- ball.update();
+ ball.update(timeStep);
scene.add(ball.draw());
});
+ lastTime = curTime;
}
scene.start(renderFrame);
diff --git a/examples/calibration/index.ts b/examples/calibration/index.ts
new file mode 100755
index 0000000..3211c42
--- /dev/null
+++ b/examples/calibration/index.ts
@@ -0,0 +1,54 @@
+import * as express from 'express';
+import { Server as WebSocketServer } from 'ws';
+import * as http from 'http';
+import * as path from 'path';
+import { Renderer } from './renderer';
+
+const PORT = 8321;
+
+const renderer = new Renderer();
+
+const server = http.createServer();
+const app = express();
+app.use(express.static(path.join(__dirname, '/public')));
+const wss = new WebSocketServer({ server });
+
+server.on('request', app);
+server.listen(PORT, function() {
+ console.log(`Started Square Interactive demo on http://localhost:${PORT}`);
+});
+
+wss.on('connection', function connection(ws, req) {
+ const id = req.url!.replace('/?id=', '');
+
+ function send(type: string, data: any) {
+ ws.send(JSON.stringify({ type, data }));
+ }
+
+ function sendSettings() {
+ send("SettingsToClient", renderer.getParams());
+ }
+
+ function sendStats() {
+ send("stats", renderer.getStats());
+ };
+
+ const statsInterval = setInterval(sendStats, 300);
+ sendSettings();
+ sendStats();
+
+ ws.on('message', function incoming(message) {
+ // TODO: should handle illegal JSON so the server can't crash...
+ const payload = JSON.parse(message as string);
+ if (payload.type === 'UPDATEPARAMS') {
+ console.log('Updating Parameters', payload.data);
+ renderer.updateParams(payload.data);
+ } else {
+ console.log('Unknown websocket message', payload);
+ }
+ });
+
+ ws.on('close', function incoming(message) {
+ clearInterval(statsInterval);
+ });
+});
diff --git a/examples/calibration/public/index.html b/examples/calibration/public/index.html
new file mode 100755
index 0000000..4ee1698
--- /dev/null
+++ b/examples/calibration/public/index.html
@@ -0,0 +1,19 @@
+
+
+
+ Laser Calibration
+
+
+
+
+
+
+
+
diff --git a/examples/calibration/public/index.js b/examples/calibration/public/index.js
new file mode 100755
index 0000000..8880c3d
--- /dev/null
+++ b/examples/calibration/public/index.js
@@ -0,0 +1,62 @@
+import WebsocketClient from './websocket.js';
+import uuid from './uuid.js';
+
+const host = window.document.location.host.replace(/:.*/, '');
+const ws = new WebsocketClient();
+
+const ppsInput = document.getElementById('pps');
+const resolutionInput = document.getElementById('resolution');
+const blankingPointsInput = document.getElementById('blankingPoints');
+const maxWaitPointsInput = document.getElementById('maxWaitPoints');
+
+const statsTable = document.getElementById('stats-table');
+
+function sendMessage(msg) {
+ console.log("Sending:", msg.data);
+ ws.send(JSON.stringify(msg));
+}
+
+function sendFormValues() {
+ sendMessage({
+ type: 'UPDATEPARAMS',
+ data: {
+ pps: parseInt(ppsInput.value, 10),
+ resolution: parseInt(resolutionInput.value, 10),
+ blankingPoints: parseInt(blankingPointsInput.value, 10),
+ maxWaitPoints: parseInt(maxWaitPointsInput.value, 10)
+ }
+ });
+}
+
+ws.onopen = function() {
+ console.log('Websocket connection opened.');
+
+ ppsInput.addEventListener('input', sendFormValues);
+ resolutionInput.addEventListener('input', sendFormValues);
+ blankingPointsInput.addEventListener('input', sendFormValues);
+ maxWaitPointsInput.addEventListener('input', sendFormValues);
+};
+
+function displayStats(stats) {
+ statsTable.innerText = JSON.stringify(stats, null, 2);
+}
+
+function updateSettings(settings) {
+ ppsInput.value = settings.pps;
+ resolutionInput.value = settings.resolution;
+ blankingPointsInput.value = settings.blankingPoints;
+ maxWaitPointsInput.value = settings.maxWaitPoints;
+}
+
+ws.onmessage = function(ev) {
+ const message = JSON.parse(ev.data);
+ switch (message.type) {
+ case 'stats': displayStats(message.data); break;
+ case 'SettingsToClient': updateSettings(message.data); break;
+ default: console.error("Unknown message type: " + message.type, message)
+ }
+};
+
+const uniqueId = uuid();
+
+ws.open(`ws://${host}:8321?id=${uniqueId}`);
diff --git a/examples/calibration/public/uuid.js b/examples/calibration/public/uuid.js
new file mode 100755
index 0000000..2a1883f
--- /dev/null
+++ b/examples/calibration/public/uuid.js
@@ -0,0 +1,24 @@
+function guid() {
+ return (
+ s4() +
+ s4() +
+ '-' +
+ s4() +
+ '-' +
+ s4() +
+ '-' +
+ s4() +
+ '-' +
+ s4() +
+ s4() +
+ s4()
+ );
+}
+
+function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+}
+
+export default guid;
diff --git a/examples/calibration/public/websocket.js b/examples/calibration/public/websocket.js
new file mode 100755
index 0000000..fad50dc
--- /dev/null
+++ b/examples/calibration/public/websocket.js
@@ -0,0 +1,62 @@
+function WebSocketClient() {
+ this.autoReconnectInterval = 1 * 1000; // ms
+}
+WebSocketClient.prototype.open = function(url) {
+ this.url = url;
+ this.instance = new WebSocket(this.url);
+ this.instance.onopen = this.onopen;
+ this.instance.onmessage = event => {
+ this.onmessage(event);
+ };
+ this.instance.onclose = e => {
+ switch (e.code) {
+ case 1000: // CLOSE_NORMAL
+ console.log('WebSocket: closed');
+ break;
+ default:
+ // Abnormal closure
+ this.reconnect(e);
+ break;
+ }
+ this.onclose(e);
+ };
+ this.instance.onerror = e => {
+ switch (e.code) {
+ case 'ECONNREFUSED':
+ this.reconnect(e);
+ break;
+ default:
+ this.onerror(e);
+ break;
+ }
+ };
+};
+WebSocketClient.prototype.send = function(data, option) {
+ try {
+ this.instance.send(data, option);
+ } catch (e) {
+ console.warn('Failed sending websocket msg', e);
+ }
+};
+WebSocketClient.prototype.reconnect = function(e) {
+ console.log(`WebSocketClient: retry in ${this.autoReconnectInterval}ms`, e);
+ var that = this;
+ setTimeout(function() {
+ console.log('WebSocketClient: reconnecting...');
+ that.open(that.url);
+ }, this.autoReconnectInterval);
+};
+WebSocketClient.prototype.onopen = function(e) {
+ console.log('WebSocketClient: open', arguments);
+};
+WebSocketClient.prototype.onmessage = function(e) {
+ console.log('WebSocketClient: message', arguments);
+};
+WebSocketClient.prototype.onerror = function(e) {
+ console.log('WebSocketClient: error', arguments);
+};
+WebSocketClient.prototype.onclose = function(e) {
+ console.log('WebSocketClient: closed', arguments);
+};
+
+export default WebSocketClient;
diff --git a/examples/calibration/renderer.ts b/examples/calibration/renderer.ts
new file mode 100755
index 0000000..b1a1232
--- /dev/null
+++ b/examples/calibration/renderer.ts
@@ -0,0 +1,93 @@
+import { DAC } from '@laser-dac/core';
+import { getDevices } from '@laser-dac/device-selector';
+import { Circle, Scene, Rect, loadIldaFile, Ilda } from '@laser-dac/draw';
+import * as path from 'path';
+
+interface IClient {
+ x: number;
+ y: number;
+ id: string;
+}
+
+export class Renderer {
+ pps = 25000;
+ resolution = 70;
+ blankingPoints = 24;
+ maxWaitPoints = 10;
+
+ dac: DAC;
+ scene: Scene;
+
+ constructor() {
+ this.dac = new DAC();
+
+ this.scene = new Scene({
+ resolution: this.resolution,
+ blankingPoints: this.blankingPoints,
+ maxWaitPoints: this.maxWaitPoints,
+ });
+
+ this.start();
+ }
+
+ getStats() {
+ return this.dac.getStats();
+ }
+
+ updateParams(data: any) {
+ this.pps = data.pps;
+ this.dac.setPointsRate(this.pps);
+ this.resolution = data.resolution;
+ this.blankingPoints = data.blankingPoints;
+ this.maxWaitPoints = data.maxWaitPoints;
+
+ this.scene.setOptions({
+ resolution: this.resolution,
+ blankingPoints: this.blankingPoints,
+ maxWaitPoints: this.maxWaitPoints,
+ });
+ }
+
+ getParams() {
+ return {
+ pps: this.pps,
+ resolution: this.resolution,
+ blankingPoints: this.blankingPoints,
+ maxWaitPoints: this.maxWaitPoints,
+ }
+ }
+
+ removeClient(id: string) {
+ const index = this.activeClients.findIndex(client => client.id === id);
+ if (index >= 0) {
+ this.activeClients.splice(index, 1);
+ }
+ }
+
+ renderTestPattern() {
+ const bounds = new Rect({
+ x: 0,
+ y: 0,
+ width: 1,
+ height: 1,
+ color: [1, 1, 1]
+ });
+ this.scene.add(bounds);
+
+ const circle = new Circle({
+ x: 0.5,
+ y: 0.5,
+ radius: 0.25,
+ color: [1, 1, 1]
+ });
+ this.scene.add(circle);
+ }
+
+ async start() {
+ this.dac.useAll(await getDevices());
+ await this.dac.start();
+
+ this.scene.start(this.renderTestPattern.bind(this), 60);
+ this.dac.stream(this.scene, this.pps);
+ }
+}
diff --git a/examples/font/index.ts b/examples/font/index.ts
index 3c0cd46..a172261 100644
--- a/examples/font/index.ts
+++ b/examples/font/index.ts
@@ -1,6 +1,5 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { EtherDream } from '@laser-dac/ether-dream';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, HersheyFont, loadHersheyFont, Timeline } from '@laser-dac/draw';
import * as path from 'path';
@@ -38,10 +37,7 @@ const textAnimation = new Timeline({
(async () => {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new EtherDream());
- }
+ dac.useAll(await getDevices());
await dac.start();
const scene = new Scene();
diff --git a/examples/ilda-animation/index.ts b/examples/ilda-animation/index.ts
index f97fcf8..648a36a 100644
--- a/examples/ilda-animation/index.ts
+++ b/examples/ilda-animation/index.ts
@@ -1,6 +1,5 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { EtherDream } from '@laser-dac/ether-dream';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, Ilda, loadIldaFile } from '@laser-dac/draw';
import * as path from 'path';
@@ -8,10 +7,7 @@ const boeing = loadIldaFile(path.resolve(__dirname, './boeing.ild'));
(async () => {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new EtherDream());
- }
+ dac.useAll(await getDevices());
await dac.start();
const scene = new Scene();
diff --git a/examples/pong/renderer.ts b/examples/pong/renderer.ts
index 81d3e97..a77e5bd 100644
--- a/examples/pong/renderer.ts
+++ b/examples/pong/renderer.ts
@@ -1,6 +1,5 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { EtherDream } from '@laser-dac/ether-dream';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, Rect } from '@laser-dac/draw';
import { Player } from './Player';
import { Ball } from './Ball';
@@ -27,10 +26,7 @@ export class Renderer {
async start() {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new EtherDream());
- }
+ dac.useAll(await getDevices());
await dac.start();
const ball = new Ball({
diff --git a/examples/square-interactive/renderer.ts b/examples/square-interactive/renderer.ts
index c71ddf1..495761c 100755
--- a/examples/square-interactive/renderer.ts
+++ b/examples/square-interactive/renderer.ts
@@ -1,6 +1,5 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { EtherDream } from '@laser-dac/ether-dream';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, Rect, loadIldaFile, Ilda } from '@laser-dac/draw';
import * as path from 'path';
@@ -48,10 +47,7 @@ export class Renderer {
async start() {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new EtherDream());
- }
+ dac.useAll(await getDevices());
await dac.start();
const scene = new Scene();
diff --git a/examples/static-shapes/index.ts b/examples/static-shapes/index.ts
index 5301ea3..b0ab64e 100644
--- a/examples/static-shapes/index.ts
+++ b/examples/static-shapes/index.ts
@@ -1,14 +1,10 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { Helios } from '@laser-dac/helios';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, Rect, Path, Line } from '@laser-dac/draw';
(async () => {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new Helios());
- }
+ dac.useAll(await getDevices());
await dac.start();
const scene = new Scene({
diff --git a/examples/svg-path/index.ts b/examples/svg-path/index.ts
index bd71d1a..54df4fe 100644
--- a/examples/svg-path/index.ts
+++ b/examples/svg-path/index.ts
@@ -1,6 +1,5 @@
import { DAC } from '@laser-dac/core';
-import { Simulator } from '@laser-dac/simulator';
-import { EtherDream } from '@laser-dac/ether-dream';
+import { getDevices } from '@laser-dac/device-selector';
import { Scene, Svg, loadSvgFile } from '@laser-dac/draw';
import * as path from 'path';
@@ -8,10 +7,8 @@ const logoFile = loadSvgFile(path.resolve(__dirname, './logo.svg'));
(async () => {
const dac = new DAC();
- dac.use(new Simulator());
- if (process.env.DEVICE) {
- dac.use(new EtherDream());
- }
+ dac.useAll(await getDevices());
+
await dac.start();
const scene = new Scene({
diff --git a/packages/beyond/src/BeyondLib.ts b/packages/beyond/src/BeyondLib.ts
index 7393a3e..405c82a 100644
--- a/packages/beyond/src/BeyondLib.ts
+++ b/packages/beyond/src/BeyondLib.ts
@@ -26,7 +26,10 @@ const libPath = path
// dont'get placed inside the "app.asar" bundle, but instead get placed in a separate directory called "app.asar.unpacked"
.replace('app.asar', 'app.asar.unpacked');
-const BeyondLib = ffi.Library(libPath, {
+// Even Windows 64-bit is called win32 here.
+const isSupported = process.platform === "win32";
+
+const BeyondLib = isSupported && ffi.Library(libPath, {
ldbCreate: ['int', []],
ldbDestroy: ['int', []],
ldbBeyondExeReady: ['int', []],
diff --git a/packages/beyond/src/index.ts b/packages/beyond/src/index.ts
index b107e9d..8f73305 100644
--- a/packages/beyond/src/index.ts
+++ b/packages/beyond/src/index.ts
@@ -13,6 +13,9 @@ export class Beyond extends Device {
private started = false;
async start() {
+ if (!this.isSupported()) {
+ return false;
+ }
this.stop();
beyondLib.ldbCreate();
this.started = true;
@@ -37,6 +40,10 @@ export class Beyond extends Device {
}
}
+ isSupported(): boolean {
+ return process.platform === "win32";
+ }
+
private convertPoint(p: Point) {
return {
x: relativeToPosition(p.x),
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 142d35f..c2db7ce 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,6 +1,8 @@
const DEFAULT_POINTS_RATE = 30000;
const DEFAULT_FPS = 30;
+const shutdownCallbacks: {(): void;}[] = [];
+
export interface Point {
x: number;
y: number;
@@ -14,9 +16,30 @@ export interface Scene {
}
export abstract class Device {
+ protected pointsRate: number = 30000;
+
abstract start(): Promise;
abstract stop(): void;
abstract stream(scene: Scene, pointsRate: number, fps: number): void;
+ // This is for any last-minute synchronous cleanup so the laser doesn't
+ // stay on after the program stops.
+ onShutdownSync(): void {};
+
+ isSupported(): boolean {
+ return true;
+ }
+
+ setPointsRate(pointsRate: number) {
+ this.pointsRate = pointsRate;
+ }
+
+ getPointsRate(): number {
+ return this.pointsRate;
+ }
+
+ getStats(): Object {
+ return {};
+ }
}
export class DAC {
@@ -24,6 +47,11 @@ export class DAC {
use(device: Device) {
this.devices.push(device);
+ shutdownCallbacks.push(device.onShutdownSync);
+ }
+
+ useAll(devices: Device[]) {
+ devices.forEach((device) => this.use(device));
}
remove(device: Device) {
@@ -58,4 +86,61 @@ export class DAC {
device.stream(scene, pointsRate, fps);
}
}
+
+ setPointsRate(pointsRate: number) {
+ this.devices.forEach((device) => device.setPointsRate(pointsRate));
+ }
+
+ getStats(): Object {
+ const allDeviceStats = this.devices.reduce((stats, device) => {
+ const deviceStats = device.getStats();
+ if (deviceStats) {
+ stats[device.constructor.name] = deviceStats;
+ }
+ return stats;
+ }, {});
+
+ return {
+ devices: allDeviceStats
+ };
+ }
+}
+
+function exitHandler(options: any, exitCode: any) {
+ if (options.cleanup) {
+ console.info("Cleaning up");
+ shutdownCallbacks.forEach((fn) => fn());
+ }
+
+ if (exitCode || exitCode === 0) {
+ console.info(exitCode);
+ }
+
+ if (options.exit) {
+ process.exit();
+ }
}
+
+// Based on an answer from
+// https://stackoverflow.com/questions/14031763/doing-a-cleanup-action-just-before-node-js-exits
+function gracefulShutdown() {
+ // Prevent the program from closing instantly.
+ process.stdin.resume();
+
+ // Do something when app is closing.
+ process.on('exit', exitHandler.bind(null, {cleanup: true}));
+
+ // Catch ctrl+c event.
+ process.on('SIGINT', exitHandler.bind(null, {exit: true}));
+
+ process.on('SIGTERM', exitHandler.bind(null, {exit: true}));
+
+ // Catch "kill pid" (for example: nodemon restart).
+ process.on('SIGUSR1', exitHandler.bind(null, {exit: true}));
+ process.on('SIGUSR2', exitHandler.bind(null, {exit: true}));
+
+ // Catch uncaught exceptions.
+ process.on('uncaughtException', exitHandler.bind(null, {exit: true}));
+}
+
+gracefulShutdown();
diff --git a/packages/device-selector/README.md b/packages/device-selector/README.md
new file mode 100644
index 0000000..5d7e972
--- /dev/null
+++ b/packages/device-selector/README.md
@@ -0,0 +1,35 @@
+# @laser-dac/device-selector
+
+This package is an easy adapter for multiple devices.
+
+```
+yarn add @laser-dac/device-selector
+npm i @laser-dac/device-selector
+```
+
+## Usage
+
+```js
+import { DAC } from '@laser-dac/core';
+import { getDevices } from '@laser-dac/device-selector';
+
+const dac = new DAC();
+// Automatically select a supported device and a Simulator.
+dac.useAll(await getDevices());
+const started = await dac.start();
+if (started) {
+ const pps = 30000; // points per second
+ const fps = 120; // frames per second
+ // draw a horizontal red line from left to right in the center
+ // @laser-dac/draw can help you with drawing points!
+ const scene = {
+ points: [
+ { x: 0.1, y: 0.5, r: 1, g: 0, b: 0 },
+ { x: 0.9, y: 0.5, r: 1, g: 0, b: 0 }
+ ]
+ };
+ dac.stream(scene, pps, fps);
+}
+```
+
+See for more usage info and examples the [Laser DAC project on GitHub](https://github.com/Volst/laser-dac).
diff --git a/packages/device-selector/package.json b/packages/device-selector/package.json
new file mode 100644
index 0000000..4d78c84
--- /dev/null
+++ b/packages/device-selector/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@laser-dac/device-selector",
+ "version": "0.4.1",
+ "description": "Device-selecting adapter for laser-dac",
+ "license": "MIT",
+ "author": "Sterling Hirsh ",
+ "repository": "Volst/laser-dac",
+ "keywords": [
+ "laser"
+ ],
+ "engines": {
+ "node": ">=8.0"
+ },
+ "main": "dist/index.js",
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@laser-dac/core": "^0.4.1",
+ "@laser-dac/beyond": "^0.3.0",
+ "@laser-dac/easylase": "^0.2.0",
+ "@laser-dac/ether-dream": "^0.4.1",
+ "@laser-dac/helios": "^0.3.0",
+ "@laser-dac/laserdock": "^0.4.0",
+ "@laser-dac/simulator": "^0.3.3"
+ },
+ "scripts": {
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
+ "watch": "tsc -p tsconfig.build.json --watch",
+ "prepublishOnly": "npm run -s build"
+ }
+}
diff --git a/packages/device-selector/src/index.ts b/packages/device-selector/src/index.ts
new file mode 100644
index 0000000..f916c4d
--- /dev/null
+++ b/packages/device-selector/src/index.ts
@@ -0,0 +1,52 @@
+import { Device } from '@laser-dac/core';
+
+import { Simulator } from '@laser-dac/simulator';
+
+import { Beyond } from '@laser-dac/beyond';
+import { Easylase } from '@laser-dac/easylase';
+import { EtherDream } from '@laser-dac/ether-dream';
+import { Helios } from '@laser-dac/helios';
+import { Laserdock } from '@laser-dac/laserdock';
+
+const apis: Record = {
+ helios: Helios,
+ laserdock: Laserdock,
+ beyond: Beyond,
+ easylase: Easylase,
+ 'ether-dream': EtherDream // This one is a bit slower to check.
+};
+
+// Get the first DAC that works.
+export async function autoSelect(): Promise {
+ console.log("Searching for DAC in " + Object.keys(apis).join(', '));
+ for (const api in apis) {
+ const dac = new apis[api]();
+ if (await dac.start()) {
+ console.log("Auto Selected DAC " + api);
+ await dac.stop();
+ return dac;
+ }
+ }
+ console.log("No DAC found");
+ return null;
+}
+
+// Get a Simulator and the first autodetected device.
+// TODO: Get multiple devices instead of just the first?
+export async function getDevices(): Promise {
+ const devices: Device[] = [new Simulator()];
+ const requestedDevice = process.env.DEVICE;
+
+ if (requestedDevice) {
+ if (requestedDevice in apis) {
+ devices.push(new apis[requestedDevice]());
+ } else {
+ const autoSelected = await autoSelect();
+ if (autoSelected) {
+ devices.push(autoSelected);
+ }
+ }
+ }
+
+ return devices;
+}
diff --git a/packages/device-selector/tsconfig.build.json b/packages/device-selector/tsconfig.build.json
new file mode 100644
index 0000000..5a2694a
--- /dev/null
+++ b/packages/device-selector/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "include": ["src/index.ts"]
+}
diff --git a/packages/draw/src/Circle.ts b/packages/draw/src/Circle.ts
index 70f63d7..fc5db94 100644
--- a/packages/draw/src/Circle.ts
+++ b/packages/draw/src/Circle.ts
@@ -1,7 +1,7 @@
import { Shape } from './Shape';
import { Color, Point } from './Point';
+import { SceneOptions } from './Scene';
import { Wait } from './Wait';
-import { BLANKING_AMOUNT } from './constants';
// TODO: I don't like these options being duplicated in the class
// I have a feeling there is a better way...
@@ -26,15 +26,15 @@ export class Circle extends Shape {
this.color = options.color;
}
- draw(resolution: number): Point[] {
+ draw(options: SceneOptions): Point[] {
const circumference = 2.0 * this.radius * Math.PI;
- const pointCount = Math.round(circumference * resolution);
+ const pointCount = Math.round(circumference * options.resolution);
const points: Point[] = new Wait({
x: this.x + this.radius,
y: this.y,
color: [0, 0, 0],
- amount: BLANKING_AMOUNT
+ amount: options.blankingPoints
}).draw();
// If there are less then 3 points just return blank
@@ -62,7 +62,7 @@ export class Circle extends Shape {
x: this.x + this.radius,
y: this.y,
color: [0, 0, 0],
- amount: BLANKING_AMOUNT
+ amount: options.blankingPoints
}).draw()
);
}
diff --git a/packages/draw/src/CubicCurve.ts b/packages/draw/src/CubicCurve.ts
index 8923954..66786b7 100644
--- a/packages/draw/src/CubicCurve.ts
+++ b/packages/draw/src/CubicCurve.ts
@@ -1,4 +1,5 @@
import { Shape } from './Shape';
+import { SceneOptions } from './Scene';
import { Point, Color } from './Point';
import Bezier = require('bezier-js');
@@ -29,7 +30,7 @@ export class CubicCurve extends Shape {
this.color = options.color;
}
- draw(resolution: number): Point[] {
+ draw(options: SceneOptions): Point[] {
const curve = new Bezier(
this.from.x,
this.from.y,
@@ -42,7 +43,7 @@ export class CubicCurve extends Shape {
);
const distance = curve.length();
- const steps = Math.round(distance * resolution);
+ const steps = Math.round(distance * options.resolution);
// When there are less than two steps, we can skip making a curve altogether.
if (steps < 2) {
return [new Point(this.to.x, this.to.y, this.color)];
diff --git a/packages/draw/src/HersheyFont.ts b/packages/draw/src/HersheyFont.ts
index 069d1da..29bddf0 100644
--- a/packages/draw/src/HersheyFont.ts
+++ b/packages/draw/src/HersheyFont.ts
@@ -2,6 +2,7 @@ import * as fs from 'fs';
import { Shape } from './Shape';
import { Color, Point } from './Point';
import { Path } from './Path';
+import { SceneOptions } from './Scene';
interface HersheyCharacter {
leftPos: number;
@@ -40,7 +41,7 @@ export class HersheyFont implements Shape {
this.color = options.color;
}
- draw(resolution: number): Point[] {
+ draw(options: SceneOptions): Point[] {
const paths: string[] = [];
let xpos = 0;
@@ -73,7 +74,7 @@ export class HersheyFont implements Shape {
y: this.y,
color: this.color,
path: paths.join(' ')
- }).draw(resolution);
+ }).draw(options);
}
}
diff --git a/packages/draw/src/Ilda.ts b/packages/draw/src/Ilda.ts
index 683ee5f..0575a7f 100644
--- a/packages/draw/src/Ilda.ts
+++ b/packages/draw/src/Ilda.ts
@@ -2,6 +2,7 @@ import * as fs from 'fs';
import { fromByteArray, Section } from '@laser-dac/ilda-reader';
import { Shape } from './Shape';
import { Color, Point } from './Point';
+import { isBlankingPoint } from './helpers';
export const XY_RESOLUTION = 65535;
@@ -47,15 +48,16 @@ export class Ilda extends Shape {
const color = this.color;
const size = this.size || 1;
- return section.points.map((point: Point) => {
- const isBlank = point.r === 0 && point.g === 0 && point.b === 0;
- return {
- x: x + (1 - convertCoordinate(point.x)) * size,
- y: y + convertCoordinate(point.y) * size,
- r: color !== undefined && !isBlank ? color[0] : point.r,
- g: color !== undefined && !isBlank ? color[1] : point.g,
- b: color !== undefined && !isBlank ? color[2] : point.b
- };
+ // These are Point objects from the ilda file,
+ // not laser-dac Point objects.
+ return section.points.map((point) => {
+ const pointColor: Color = color !== undefined && !isBlankingPoint(point)
+ ? color
+ : [point.r, point.g, point.b];
+ return new Point(
+ x + (1 - convertCoordinate(point.x)) * size,
+ y + convertCoordinate(point.y) * size,
+ pointColor);
});
}
}
diff --git a/packages/draw/src/Line.ts b/packages/draw/src/Line.ts
index 1d7e8ea..e7854bd 100644
--- a/packages/draw/src/Line.ts
+++ b/packages/draw/src/Line.ts
@@ -1,7 +1,7 @@
import { Shape } from './Shape';
import { Point, Color } from './Point';
import { Wait } from './Wait';
-import { BLANKING_AMOUNT, MAX_WAIT_AMOUNT } from './constants';
+import { SceneOptions } from './Scene';
interface Coordinates {
x: number;
@@ -25,8 +25,8 @@ export class Line extends Shape {
blankBefore: boolean;
blankAfter: boolean;
- waitAmount: number;
- blankingAmount: number;
+ waitAmount: number | undefined;
+ blankingAmount: number | undefined;
constructor(options: LineOptions) {
super();
@@ -36,16 +36,18 @@ export class Line extends Shape {
this.blankBefore = options.blankBefore || false;
this.blankAfter = options.blankAfter || false;
- this.waitAmount = options.waitAmount || MAX_WAIT_AMOUNT;
- this.blankingAmount = options.blankingAmount || BLANKING_AMOUNT;
+ this.waitAmount = options.waitAmount;
+ this.blankingAmount = options.blankingAmount;
}
- draw(resolution: number) {
+ draw(options: SceneOptions): Point[] {
const distanceX = this.from.x - this.to.x;
const distanceY = this.from.y - this.to.y;
// Calculate distance using the Pythagorean theorem.
const distance = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
- const steps = Math.round(distance * resolution);
+ const steps = Math.round(distance * options.resolution);
+ const waitAmount = this.waitAmount ?? options.maxWaitPoints;
+ const blankingAmount = this.blankingAmount ?? options.blankingPoints;
let points: Point[] = [];
@@ -55,7 +57,7 @@ export class Line extends Shape {
x: this.from.x,
y: this.from.y,
color: [0, 0, 0],
- amount: this.blankingAmount
+ amount: blankingAmount
}).draw();
}
@@ -77,7 +79,7 @@ export class Line extends Shape {
x: this.to.x,
y: this.to.y,
color: this.color,
- amount: this.waitAmount / 2
+ amount: waitAmount / 2
}).draw()
];
}
diff --git a/packages/draw/src/Path.ts b/packages/draw/src/Path.ts
index 8541de3..1d50580 100644
--- a/packages/draw/src/Path.ts
+++ b/packages/draw/src/Path.ts
@@ -1,5 +1,6 @@
import { Shape } from './Shape';
import { Point, Color } from './Point';
+import { SceneOptions } from './Scene';
import { Line } from './Line';
import { Wait } from './Wait';
import { CubicCurve } from './CubicCurve';
@@ -7,7 +8,6 @@ import { SVGPathData } from 'svg-pathdata';
import { CommandM, SVGCommand } from 'svg-pathdata/lib/types';
import { QuadCurve } from './QuadCurve';
import { flatten } from './helpers';
-import { BLANKING_AMOUNT, MAX_WAIT_AMOUNT } from './constants';
import arcToBezier = require('svg-arc-to-cubic-bezier');
interface PathOptions {
@@ -31,8 +31,8 @@ export class Path extends Shape {
// Works exactly like SVG path. Learn everything about it: https://css-tricks.com/svg-path-syntax-illustrated-guide/
path: string;
- waitAmount: number;
- blankingAmount: number;
+ waitAmount: number | undefined;
+ blankingAmount: number | undefined;
constructor(options: PathOptions) {
super();
@@ -43,8 +43,8 @@ export class Path extends Shape {
this.color = options.color;
this.path = options.path;
- this.waitAmount = options.waitAmount || MAX_WAIT_AMOUNT;
- this.blankingAmount = options.blankingAmount || BLANKING_AMOUNT;
+ this.waitAmount = options.waitAmount;
+ this.blankingAmount = options.blankingAmount;
}
transformSize = (command: SVGCommand) => {
@@ -76,7 +76,7 @@ export class Path extends Shape {
return command;
};
- draw(resolution: number) {
+ draw(options: SceneOptions): Point[] {
const pathData = new SVGPathData(this.path)
// Transforms relative commands to absolute so we don't have to implement relative commands at all!
.toAbs()
@@ -94,6 +94,12 @@ export class Path extends Shape {
let prevX = 0;
let prevY = 0;
+ // Any shapes should be constructed with undefined blankingAmount
+ // if this.blankingAmount is undefined. This will cause drawing to
+ // use the value from SceneOptions at render time. The same goes
+ // for waitAmount.
+ const blankingAmount = this.blankingAmount ?? options.blankingPoints;
+
const points = pathData.commands.reduce(
(accumulator: Point[], command: SVGCommand) => {
let commandPoints: Point[] = [];
@@ -103,7 +109,7 @@ export class Path extends Shape {
commandPoints = new Wait({
x: command.x,
y: command.y,
- amount: this.blankingAmount
+ amount: blankingAmount
}).draw();
lastMoveCommand = command;
@@ -122,7 +128,7 @@ export class Path extends Shape {
color: this.color,
waitAmount: this.waitAmount,
blankingAmount: this.blankingAmount
- }).draw(resolution);
+ }).draw(options);
prevX = toX;
prevY = toY;
break;
@@ -140,7 +146,7 @@ export class Path extends Shape {
control: { x: command.x2, y: command.y2 }
},
color: this.color
- }).draw(resolution);
+ }).draw(options);
prevX = command.x;
prevY = command.y;
break;
@@ -151,7 +157,7 @@ export class Path extends Shape {
to: { x: command.x, y: command.y },
control: { x: command.x1, y: command.y1 },
color: this.color
- }).draw(resolution);
+ }).draw(options);
prevX = command.x;
prevY = command.y;
break;
@@ -184,7 +190,7 @@ export class Path extends Shape {
control: { x: curve.x2, y: curve.y2 }
},
color: this.color
- }).draw(resolution);
+ }).draw(options);
curvePrevX = curve.x;
curvePrevY = curve.y;
Array.prototype.push.apply(commandPoints, curvePoints);
@@ -206,7 +212,7 @@ export class Path extends Shape {
blankAfter: true,
waitAmount: this.waitAmount,
blankingAmount: this.blankingAmount
- }).draw(resolution);
+ }).draw(options);
prevX = lastMoveCommand.x;
prevY = lastMoveCommand.y;
break;
diff --git a/packages/draw/src/QuadCurve.ts b/packages/draw/src/QuadCurve.ts
index 5b1f599..1468c62 100644
--- a/packages/draw/src/QuadCurve.ts
+++ b/packages/draw/src/QuadCurve.ts
@@ -1,5 +1,6 @@
import { Shape } from './Shape';
import { Point, Color } from './Point';
+import { SceneOptions } from './Scene';
import Bezier = require('bezier-js');
interface BezierCoordinates {
@@ -28,7 +29,7 @@ export class QuadCurve extends Shape {
this.color = options.color;
}
- draw(resolution: number): Point[] {
+ draw(options: SceneOptions): Point[] {
const curve = new Bezier(
this.from.x,
this.from.y,
@@ -39,7 +40,7 @@ export class QuadCurve extends Shape {
);
const distance = curve.length();
- const steps = Math.round(distance * resolution);
+ const steps = Math.round(distance * options.resolution);
// When there are less than two steps, we can skip making a curve altogether.
if (steps < 2) {
return [new Point(this.to.x, this.to.y, this.color)];
diff --git a/packages/draw/src/Rect.ts b/packages/draw/src/Rect.ts
index b478c48..adc06e2 100644
--- a/packages/draw/src/Rect.ts
+++ b/packages/draw/src/Rect.ts
@@ -1,11 +1,8 @@
import { Shape } from './Shape';
-import { Color } from './Point';
+import { SceneOptions } from './Scene';
+import { Color, Point } from './Point';
import { Line } from './Line';
import { Wait } from './Wait';
-import { MAX_WAIT_AMOUNT } from './constants';
-
-//
-const WAIT_AMOUNT = MAX_WAIT_AMOUNT / 2;
// TODO: I don't like these options being duplicated in the class
// I have a feeling there is a better way...
@@ -33,7 +30,8 @@ export class Rect extends Shape {
this.color = options.color;
}
- draw(resolution: number) {
+ draw(options: SceneOptions): Point[] {
+ const waitAmount = options.maxWaitPoints / 2;
return [
// Top.
...new Line({
@@ -41,13 +39,13 @@ export class Rect extends Shape {
to: { x: this.x + this.width, y: this.y },
color: this.color,
blankBefore: true
- }).draw(resolution),
+ }).draw(options),
...new Wait({
x: this.x + this.width,
y: this.y,
color: this.color,
- amount: WAIT_AMOUNT
+ amount: waitAmount
}).draw(),
// Right.
@@ -55,13 +53,13 @@ export class Rect extends Shape {
from: { x: this.x + this.width, y: this.y },
to: { x: this.x + this.width, y: this.y + this.height },
color: this.color
- }).draw(resolution),
+ }).draw(options),
...new Wait({
x: this.x + this.width,
y: this.y + this.height,
color: this.color,
- amount: WAIT_AMOUNT
+ amount: waitAmount
}).draw(),
// Bottom.
@@ -69,13 +67,13 @@ export class Rect extends Shape {
from: { x: this.x + this.width, y: this.y + this.height },
to: { x: this.x, y: this.y + this.height },
color: this.color
- }).draw(resolution),
+ }).draw(options),
...new Wait({
x: this.x,
y: this.y + this.height,
color: this.color,
- amount: WAIT_AMOUNT
+ amount: waitAmount
}).draw(),
// Left.
@@ -84,7 +82,7 @@ export class Rect extends Shape {
to: { x: this.x, y: this.y },
color: this.color,
blankAfter: true
- }).draw(resolution)
+ }).draw(options)
];
}
}
diff --git a/packages/draw/src/Scene.ts b/packages/draw/src/Scene.ts
index 121d802..208908d 100644
--- a/packages/draw/src/Scene.ts
+++ b/packages/draw/src/Scene.ts
@@ -1,32 +1,41 @@
import { Point } from './Point';
import { Shape } from './Shape';
+import { clamp } from './helpers';
-// Frames per second; 30fps will be enough for most use-cases.
-const DEFAULT_FPS = 30;
-const DEFAULT_RESOLUTION = 500;
-
-interface SceneOptions {
+export interface SceneOptions {
// This number sets the requested number of points from a perpendicular line drawn from one side of the projection to the other.
// Decreasing this number will make drawing faster but less accurate, increasing will make it slower but more accurate.
- resolution?: number;
+ resolution: number;
+ fps: number;
+ blankingPoints: number;
+ // This value determines how many times a Wait-point should be applied for the sharpest possible angles (360 degree angles).
+ maxWaitPoints: number;
}
+const defaultOptions: SceneOptions = {
+ resolution: 500,
+ fps: 30,
+ blankingPoints: 24,
+ maxWaitPoints: 10,
+};
+
type TransformFn = (points: Point[]) => Point[];
export class Scene {
points: Point[] = [];
- resolution: number;
interval?: NodeJS.Timer;
+ options: SceneOptions;
- constructor(options?: SceneOptions) {
- this.resolution = (options && options.resolution) || DEFAULT_RESOLUTION;
+ constructor(options: Partial = {}) {
+ this.options = Object.assign(defaultOptions, options);
}
add(shape: Shape, transformer?: TransformFn) {
- let points = shape.draw(this.resolution);
+ let points = shape.draw(this.options);
if (transformer) {
points = transformer(points);
}
+ points = clamp(points);
this.points = this.points.concat(points);
}
@@ -34,7 +43,7 @@ export class Scene {
this.points = [];
}
- start(renderFrame: () => void, fps: number = DEFAULT_FPS) {
+ start(renderFrame: () => void, fps: number = this.options.fps) {
const ms = 1000 / fps;
this.interval = setInterval(() => {
this.reset();
@@ -53,4 +62,8 @@ export class Scene {
this.interval = undefined;
}
}
+
+ setOptions(options: Partial) {
+ this.options = Object.assign(this.options, options);
+ }
}
diff --git a/packages/draw/src/Shape.ts b/packages/draw/src/Shape.ts
index b6c5e11..faa7da8 100644
--- a/packages/draw/src/Shape.ts
+++ b/packages/draw/src/Shape.ts
@@ -1,7 +1,8 @@
import { Point } from './Point';
+import { SceneOptions } from './Scene';
export class Shape {
- draw(resolution: number): Point[] {
+ draw(options: SceneOptions): Point[] {
return [];
}
}
diff --git a/packages/draw/src/Svg.ts b/packages/draw/src/Svg.ts
index 068a79b..2f66689 100644
--- a/packages/draw/src/Svg.ts
+++ b/packages/draw/src/Svg.ts
@@ -1,10 +1,10 @@
import * as fs from 'fs';
import { Shape } from './Shape';
+import { SceneOptions } from './Scene';
import { parse, Node } from 'svg-parser';
import { Path } from './Path';
import { Color, Point } from './Point';
import { hexToRgb, flatten } from './helpers';
-import { BLANKING_AMOUNT, MAX_WAIT_AMOUNT } from './constants';
const DEFAULT_COLOR: Color = [0, 1, 0];
const ALLOWED_NODES = ['path', 'polyline', 'polygon', 'rect', 'line'];
@@ -25,8 +25,8 @@ export class Svg extends Shape {
size: number;
file: Node;
color: Color;
- waitAmount: number;
- blankingAmount: number;
+ waitAmount: number | undefined;
+ blankingAmount: number | undefined;
private pathNodes: Node[] = [];
constructor(options: SvgOptions) {
@@ -36,8 +36,8 @@ export class Svg extends Shape {
this.file = options.file;
this.color = options.color || DEFAULT_COLOR;
this.size = options.size || 1;
- this.waitAmount = options.waitAmount || MAX_WAIT_AMOUNT;
- this.blankingAmount = options.blankingAmount || BLANKING_AMOUNT;
+ this.waitAmount = options.waitAmount;
+ this.blankingAmount = options.blankingAmount;
}
parseViewBox(raw: string) {
@@ -71,7 +71,7 @@ export class Svg extends Shape {
}
};
- draw(resolution: number) {
+ draw(options: SceneOptions): Point[] {
const viewBox = this.parseViewBox(String(this.file.attributes.viewBox));
const aspectRatio = viewBox.width / viewBox.height;
const width = viewBox.width / this.size;
@@ -92,7 +92,7 @@ export class Svg extends Shape {
height,
waitAmount: this.waitAmount,
blankingAmount: this.blankingAmount
- }).draw(resolution);
+ }).draw(options);
});
return flatten(points) as Point[];
diff --git a/packages/draw/src/Timeline.ts b/packages/draw/src/Timeline.ts
index ea90ef0..ecfa5fa 100644
--- a/packages/draw/src/Timeline.ts
+++ b/packages/draw/src/Timeline.ts
@@ -1,4 +1,6 @@
import { Shape } from './Shape';
+import { Point } from './Point';
+import { SceneOptions } from './Scene';
interface TimelineOptions {
items: TimelineItem[];
@@ -41,10 +43,10 @@ export class Timeline extends Shape {
return currentItem;
}
- draw(resolution: number) {
+ draw(options: SceneOptions): Point[] {
const item = this.getItem();
if (item) {
- return item.render().draw(resolution);
+ return item.render().draw(options);
} else {
return [];
}
diff --git a/packages/draw/src/constants.ts b/packages/draw/src/constants.ts
deleted file mode 100644
index 09e68c6..0000000
--- a/packages/draw/src/constants.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// TODO: find out what this does exactly
-export const RESOLUTION = 150;
-
-export const BLANKING_AMOUNT = 24;
-
-// This value determines how many times a Wait-point should be applied for the sharpest possible angles (360 degree angles).
-export const MAX_WAIT_AMOUNT = 10;
diff --git a/packages/draw/src/helpers.ts b/packages/draw/src/helpers.ts
index 4d66eac..4f62c69 100644
--- a/packages/draw/src/helpers.ts
+++ b/packages/draw/src/helpers.ts
@@ -27,6 +27,40 @@ export function flatten(arr: any[], result: any[] = []) {
return result;
}
-export function isBlankingPoint(point: Point) {
+export function isBlankingPoint(point: Pick) {
return !point.r && !point.g && !point.b;
}
+
+export function outOfBounds(p: Point): boolean {
+ return Math.min(p.x, p.y, p.r, p.g, p.b) < 0
+ || Math.max(p.x, p.y, p.r, p.g, p.b) > 1;
+}
+
+export function pointsEqual(a: Point, b: Point): boolean {
+ return a.x === b.x
+ && a.y === b.y
+ && a.r === b.r
+ && a.g === b.g
+ && a.b === b.b;
+}
+
+export function makeTransformer(fn: (p: Point) => Point): (points: Point[]) => Point[] {
+ return (points: Point[]) => points.map(fn);
+}
+
+export function clampPoint(point: Point): Point {
+ point.x = point.x < 0 ? 0 : point.x > 1 ? 1 : point.x;
+ point.y = point.y < 0 ? 0 : point.y > 1 ? 1 : point.y;
+ point.r = point.r < 0 ? 0 : point.r > 1 ? 1 : point.r;
+ point.g = point.g < 0 ? 0 : point.g > 1 ? 1 : point.g;
+ point.b = point.b < 0 ? 0 : point.b > 1 ? 1 : point.b;
+ return point;
+}
+
+export function monochromePoint(p: Point): Point {
+ p.r = p.g = p.b = Math.max(p.r, p.g, p.b);
+ return p;
+}
+
+export const clamp = makeTransformer(clampPoint);
+export const monochrome = makeTransformer(monochromePoint);
diff --git a/packages/draw/src/index.ts b/packages/draw/src/index.ts
index 0cc5714..2e9c8f7 100644
--- a/packages/draw/src/index.ts
+++ b/packages/draw/src/index.ts
@@ -8,4 +8,5 @@ export { HersheyFont, loadHersheyFont } from './HersheyFont';
export { Timeline } from './Timeline';
export { rotate } from './transformers/rotate';
export { distort } from './transformers/distort';
+export { clamp, monochrome } from './helpers';
export { Svg, loadSvgFile } from './Svg';
diff --git a/packages/easylase/src/EasylaseLib.ts b/packages/easylase/src/EasylaseLib.ts
index aabbc05..8de5bfc 100644
--- a/packages/easylase/src/EasylaseLib.ts
+++ b/packages/easylase/src/EasylaseLib.ts
@@ -37,7 +37,10 @@ const libPath = path
// dont'get placed inside the "app.asar" bundle, but instead get placed in a separate directory called "app.asar.unpacked"
.replace('app.asar', 'app.asar.unpacked');
-const EasylaseLib = ffi.Library(libPath, {
+// Even Windows 64-bit is called win32 here.
+const isSupported = process.platform === "win32";
+
+const EasylaseLib = isSupported && ffi.Library(libPath, {
jmLaserEnumerateDevices: ['int', []],
jmLaserStopOutput: ['int', ['int']],
jmLaserCloseDevice: ['int', ['int']],
diff --git a/packages/easylase/src/index.ts b/packages/easylase/src/index.ts
index bdb82ee..b0ecb30 100644
--- a/packages/easylase/src/index.ts
+++ b/packages/easylase/src/index.ts
@@ -7,6 +7,10 @@ export class Easylase extends Device {
deviceHandle?: number;
async start() {
+ if (!this.isSupported()) {
+ return false;
+ }
+
this.stop();
const cards = easylaseLib.enumerateDevices();
if (cards) {
@@ -32,6 +36,10 @@ export class Easylase extends Device {
}
}
+ isSupported(): boolean {
+ return process.platform === "win32";
+ }
+
private convertPoint(p: Point) {
return {
x: relativeToPosition(p.x),
diff --git a/packages/helios/src/HeliosLib.ts b/packages/helios/src/HeliosLib.ts
index ed8e5bb..499db41 100644
--- a/packages/helios/src/HeliosLib.ts
+++ b/packages/helios/src/HeliosLib.ts
@@ -70,10 +70,17 @@ export function setShutter(dacNum: number, shutterValue: boolean): number {
return HeliosLib.SetShutter(dacNum, shutterValue);
}
+export enum FrameMode {
+ QueueLoop = 0,
+ ImmediateLoop = 1,
+ QueueSingle = 2,
+ ImmediateSingle = 3
+}
+
export function writeFrame(
dacNum: number,
pps: number,
- flags: 0 | 1,
+ flags: FrameMode,
points: any[],
numOfPoints: number
): number {
diff --git a/packages/helios/src/convert.ts b/packages/helios/src/convert.ts
index 0c185e5..5d08ec2 100644
--- a/packages/helios/src/convert.ts
+++ b/packages/helios/src/convert.ts
@@ -1,9 +1,14 @@
-// The Helios DAC firmware uses 12-bit integers for the resolution, ranging from 0 to 4096.
+// The Helios DAC firmware uses 12-bit integers for the resolution, ranging from 0 to 4095.
// Source: https://github.com/Grix/helios_dac/blob/master/sdk/HeliosDac.h
-export const XY_RESOLUTION = 4096;
+export const XY_RESOLUTION = 4095;
export const COLOR_RESOLUTION = 255;
-export function relativeToPosition(n: number) {
+export function relativeToX(n: number) {
+ return Math.floor(n * XY_RESOLUTION);
+}
+
+// Helios has 0 Y at the bottom, but laser-dac has 0 Y at the top.
+export function relativeToY(n: number) {
return Math.floor((1 - n) * XY_RESOLUTION);
}
diff --git a/packages/helios/src/index.ts b/packages/helios/src/index.ts
index e970c40..65b2b33 100644
--- a/packages/helios/src/index.ts
+++ b/packages/helios/src/index.ts
@@ -1,37 +1,108 @@
-import { Device } from '@laser-dac/core';
+import { Device, Point } from '@laser-dac/core';
import * as heliosLib from './HeliosLib';
-import { relativeToPosition, relativeToColor } from './convert';
+import { relativeToX, relativeToY, relativeToColor } from './convert';
+import { Scene } from '@laser-dac/draw';
+
+enum FrameResult {
+ Success = 'Success',
+ NotReady = 'Not Ready',
+ Fail = 'Fail',
+ Empty = 'Empty'
+}
// This controls the intensity signal of points written to the DAC.
// For many laser projectors this won't make a difference, but some projectors map this to the shutter so the laser won't turn on if we don't pass the max value.
const INTENSITY = 255;
const MAX_POINTS = 4094;
+const MIN_PPS = 7;
+const MAX_PPS = 65535;
export class Helios extends Device {
- private interval?: NodeJS.Timer;
+ private interval?: NodeJS.Timeout;
+ private dacNum: number = 0;
+ private sendNextImmediate: boolean = false;
+ private lastPoint?: Point;
+ /*
+ We could initialize lastPoint to the center to capture the initial startup jump
+ but it doesn't seem that useful.
+ {
+ x: 0.5,
+ y: 0.5,
+ r: 0,
+ g: 0,
+ b: 0,
+ };
+ */
+
+ private stats = {
+ startTime: 0,
+ secondsPerPoint: 0,
+ msPerPoint: 0,
+ microsecondsPerPoint: 0,
+ maxTheoreticalAccel: 0,
+ fixedFrameRate: {
+ fps: 0,
+ allottedFrameMs: 0,
+ allottedFramePoints: 0,
+ frameAllotmentPercentOfDeviceLimit: 0,
+ },
+ points: {
+ [FrameResult.Success]: 0,
+ [FrameResult.NotReady]: 0,
+ [FrameResult.Fail]: 0,
+ [FrameResult.Empty]: 0, // Should always be 0.
+ },
+ frames: {
+ [FrameResult.Success]: 0,
+ [FrameResult.NotReady]: 0,
+ [FrameResult.Fail]: 0,
+ [FrameResult.Empty]: 0,
+ },
+ lastFrame: {
+ points: 0,
+ result: FrameResult.Empty,
+ drawMs: 0,
+ maxJumpX: 0,
+ maxJumpY: 0,
+ maxJump: 0,
+ maxSpeed: 0,
+ maxAccel: 0,
+ },
+ allFrames: {
+ maxJumpX: 0,
+ maxJumpY: 0,
+ maxJump: 0,
+ maxSpeed: 0,
+ maxAccel: 0,
+ }
+ };
async start() {
this.stop();
const devices = heliosLib.openDevices();
if (devices) {
- heliosLib.setShutter(0, true);
+ heliosLib.setShutter(this.dacNum, true);
return true;
}
return false;
}
stop() {
- heliosLib.setShutter(0, false);
+ heliosLib.setShutter(this.dacNum, false);
heliosLib.closeDevices();
if (this.interval) {
clearInterval(this.interval);
}
}
- private convertPoint(p: heliosLib.IPoint) {
+ onShutdownSync() {
+ heliosLib.closeDevices();
+ }
+
+ private convertPoint(p: Point): heliosLib.IPoint {
return {
- x: relativeToPosition(p.x),
- y: relativeToPosition(p.y),
+ x: relativeToX(p.x),
+ y: relativeToY(p.y),
r: relativeToColor(p.r),
g: relativeToColor(p.g),
b: relativeToColor(p.b),
@@ -39,20 +110,197 @@ export class Helios extends Device {
};
}
+ setPointsRate(pointsRate: number) {
+ // TODO: Make this throw?
+ if (pointsRate > MAX_PPS) {
+ console.error(`Helios cannot exceed ${MAX_PPS} pps (${pointsRate} requested)`);
+ return;
+ } else if (pointsRate < MIN_PPS) {
+ console.error(`Helios cannot subsceed ${MIN_PPS} pps (${pointsRate} requested)`);
+ return;
+ }
+
+ super.setPointsRate(pointsRate);
+ this.sendNextImmediate = true;
+ this.stats.secondsPerPoint = 1 / pointsRate;
+ this.stats.msPerPoint = 1000 / pointsRate;
+ this.stats.microsecondsPerPoint = 1000000 / pointsRate;
+
+ // Going from 0 to 1 in a single point means a velocity of 1 Screen Unit (SU)
+ // in secondsPerPoint amount of time. secondsPerPoint is 1 / pointsRate seconds,
+ // so 1 / (1 / pointsRate) is just pointsRate pointsRate SU/s.
+ //
+ // Max acceleration would be going across the screen from 0 to 1 in a
+ // single point and then back to 0 on the next point. That's a change in
+ // velocity of 2 * pointsRate SU/s in one secondsPerPoint
+ // amount of time, measured in SU/s/s.
+ //
+ // But SU/s/s isn't that useful of a measurement since that acceleration only
+ // applies for the duration of a single point, which is usually in microseconds (us).
+ // So let's keep the speed in SU/s but make acceleration in SU/s/us
+ this.stats.maxTheoreticalAccel = 2 * pointsRate / this.stats.microsecondsPerPoint;
+ }
+
+ sendFrame(points: Point[], pointsRate: number): FrameResult {
+ if (!points.length) {
+ return FrameResult.Empty;
+ }
+
+ if (!this.sendNextImmediate && heliosLib.getStatus(this.dacNum) !== 1) {
+ return FrameResult.NotReady;
+ }
+
+ const frameMode = this.sendNextImmediate
+ ? heliosLib.FrameMode.ImmediateSingle
+ : heliosLib.FrameMode.QueueSingle;
+
+ this.sendNextImmediate = false;
+
+ const limitedPoints = points.length > MAX_POINTS ? points.slice(0, MAX_POINTS) : points;
+ const converted = limitedPoints.map(this.convertPoint);
+ const success = heliosLib.writeFrame(this.dacNum, pointsRate,
+ frameMode, converted, converted.length);
+
+ return success === 1 ? FrameResult.Success : FrameResult.Fail;
+ }
+
stream(
- scene: { points: heliosLib.IPoint[] },
+ scene: Scene,
pointsRate: number,
fps: number
) {
+ this.setPointsRate(pointsRate);
+ const frameTime = this.stats.fixedFrameRate.allottedFrameMs = Math.round(1000 / fps);
+ const allottedFramePoints = this.stats.fixedFrameRate.allottedFramePoints = Math.round(pointsRate / fps);
+
+ console.log(`Streaming at ${pointsRate} pps, ${fps} fps`);
+ console.log(`${frameTime}ms per frame, ${allottedFramePoints} points per frame`);
+
+ this.stats.fixedFrameRate.fps = fps;
+ this.stats.fixedFrameRate.frameAllotmentPercentOfDeviceLimit =
+ 100 * allottedFramePoints / MAX_POINTS;
+ this.stats.startTime = Date.now();
+
this.interval = setInterval(() => {
- if (!scene.points.length) {
- return;
- }
- if (heliosLib.getStatus(0) !== 1) {
- return;
+ const points = scene.points;
+ const result = this.sendFrame(points, this.pointsRate);
+
+ this.stats.lastFrame.points = points.length;
+ this.stats.lastFrame.result = result;
+ this.stats.lastFrame.drawMs = 1000 * points.length / this.pointsRate;
+ this.stats.points[result] += points.length;
+ ++this.stats.frames[result];
+
+ this.recordContentStats(points);
+
+ switch (result) {
+ case FrameResult.Fail:
+ console.error("Helios failed sending a frame");
+ break;
+ case FrameResult.NotReady:
+ console.error("Helios not ready.");
+ break;
}
- const points = scene.points.map(this.convertPoint).slice(0, MAX_POINTS);
- heliosLib.writeFrame(0, pointsRate, 0, points, points.length);
- }, 1000 / fps);
+ }, frameTime);
+ }
+
+ private calculateStats() {
+ const duration = (Date.now() - this.stats.startTime) / 1000;
+ const successPps = Math.round(this.stats.points[FrameResult.Success] / duration);
+ const attemptedPoints = this.stats.points[FrameResult.Success] +
+ this.stats.points[FrameResult.NotReady] +
+ this.stats.points[FrameResult.Fail];
+
+ const attemptedPps = Math.round(attemptedPoints / duration);
+ const successPpsPercentOfNominal = 100 * successPps / this.pointsRate;
+ const attemptedPpsPercentOfNominal = 100 * attemptedPps / this.pointsRate;
+
+ const avgFramePoints = Math.round(this.stats.points[FrameResult.Success] /
+ this.stats.frames[FrameResult.Success]);
+
+ const avgFrameDisplayMs = Math.round(1000 * avgFramePoints / this.pointsRate);
+ const avgFrameAllotmentUtilization = 100 * avgFramePoints / this.stats.fixedFrameRate.allottedFramePoints;
+ const avgFrameDeviceLimitUtilization = 100 * avgFramePoints / MAX_POINTS;
+
+ const totalFrames = this.stats.frames[FrameResult.Success] +
+ this.stats.frames[FrameResult.NotReady] +
+ this.stats.frames[FrameResult.Fail] +
+ this.stats.frames[FrameResult.Empty];
+
+ const notReadyFramePercent = 100 * this.stats.frames[FrameResult.NotReady] / totalFrames;
+
+ return {
+ duration,
+ successPps,
+ attemptedPps,
+ successPpsPercentOfNominal,
+ attemptedPpsPercentOfNominal,
+ avgFramePoints,
+ avgFrameDisplayMs,
+ avgFrameAllotmentUtilization,
+ avgFrameDeviceLimitUtilization,
+ notReadyFramePercent,
+ }
+ }
+
+ recordContentStats(framePoints: Point[]) {
+ let maxJumpX = 0;
+ let maxJumpY = 0;
+ let maxJump = 0;
+ let maxSpeed = 0;
+ let maxAccel = 0;
+
+ let lastJumpX = 0;
+ let lastJumpY = 0;
+
+ framePoints.forEach((point: Point) => {
+ const lastPoint = this.lastPoint ?? point;
+ const jumpX = point.x - lastPoint.x;
+ const jumpXAbs = Math.abs(jumpX);
+
+ const jumpY = point.y - lastPoint.y;
+ const jumpYAbs = Math.abs(jumpY);
+
+ const largerJump = Math.max(jumpXAbs, jumpYAbs);
+
+ const largerSpeed = largerJump / this.stats.secondsPerPoint;
+
+ const jumpDiffX = Math.abs(jumpX - lastJumpX);
+ const jumpDiffY = Math.abs(jumpY - lastJumpY);
+ const largerJumpDiff = Math.max(jumpDiffX, jumpDiffY);
+
+ // Acceleration is mearued here in Screen Units per second per microsecond (SU/s/us).
+ const speedChange = largerJumpDiff / this.stats.secondsPerPoint;
+ const accel = speedChange / this.stats.microsecondsPerPoint;
+
+ maxJumpX = Math.max(maxJumpX, jumpXAbs);
+ maxJumpY = Math.max(maxJumpY, jumpYAbs);
+ maxJump = Math.max(maxJump, largerJump);
+ maxSpeed = Math.max(maxSpeed, largerSpeed);
+ maxAccel = Math.max(maxAccel, accel);
+
+ lastJumpX = jumpX;
+ lastJumpY = jumpY;
+ this.lastPoint = point;
+ });
+
+ this.stats.lastFrame.maxJumpX = maxJumpX;
+ this.stats.lastFrame.maxJumpY = maxJumpY;
+ this.stats.lastFrame.maxJump = maxJump;
+ this.stats.lastFrame.maxSpeed = maxSpeed;
+ this.stats.lastFrame.maxAccel = maxAccel;
+
+ this.stats.allFrames.maxJumpX = Math.max(this.stats.allFrames.maxJumpX, maxJumpX);
+ this.stats.allFrames.maxJumpY = Math.max(this.stats.allFrames.maxJumpY, maxJumpY);
+ this.stats.allFrames.maxJump = Math.max(this.stats.allFrames.maxJump, maxJump);
+ this.stats.allFrames.maxSpeed = Math.max(this.stats.allFrames.maxSpeed, maxSpeed);
+ this.stats.allFrames.maxAccel = Math.max(this.stats.allFrames.maxAccel, maxAccel);
+ }
+
+ getStats(): Object {
+ return {
+ calculated: this.calculateStats(),
+ ...this.stats
+ };
}
}