Skip to content

Commit

Permalink
Merge pull request #3 from LeoDJ/flowBasedConfig
Browse files Browse the repository at this point in the history
First Milestone: PixelBridge with flow-based config is usable on the MateLight
  • Loading branch information
LeoDJ committed Feb 4, 2022
2 parents 87c388c + f68b50a commit 61029b6
Show file tree
Hide file tree
Showing 81 changed files with 17,231 additions and 1,161 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ It's targeted to do the image data conversion for the [modLED project](https://g


## How to run
Run with `npm start` (don't forgetto run `npm i` the first time).
Run with `npm start` (don't forget to run `npm i` and `npm run build` the first time).

## Development
Develop with `npm run dev`. Then access http://localhost:8081 for the development frontend. (Port 8080 will still host the outdated release frontend)

**Disclaimer**: This is my first big Typescript project. So there are bound to be horrific architectural decisions and many other beginner's mistakes. Please excuse that.
The premise is to get to the first milestone with as little premature optimization as possible, otherwise I would never arrive there.
15,620 changes: 14,666 additions & 954 deletions package-lock.json

Large diffs are not rendered by default.

51 changes: 43 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,58 @@
"description": "",
"main": "main.ts",
"dependencies": {
"@types/sharp": "^0.26.1",
"core-js": "^3.6.5",
"cors": "^2.8.5",
"express": "^4.17.1",
"sharp": "^0.26.3"
"rete": "^1.4.5",
"rete-area-plugin": "^0.2.1",
"rete-code-plugin": "^0.2.0",
"rete-comment-plugin": "^0.7.0-rc.1",
"rete-connection-plugin": "^0.9.0",
"rete-context-menu-plugin": "^0.6.0",
"rete-history-plugin": "^0.2.2",
"rete-keyboard-plugin": "^0.1.2",
"rete-minimap-plugin": "^0.3.1",
"rete-task-plugin": "^0.3.0",
"rete-vue-render-plugin": "^0.5.0",
"serialport": "^9.2.8",
"sharp": "^0.23.4",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"@types/express": "^4.17.9",
"@types/node": "^14.14.10",
"@types/serialport": "^8.0.2",
"@types/sharp": "^0.26.1",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-typescript": "^7.0.0",
"concurrently": "^6.5.1",
"copyfiles": "^2.4.1",
"typescript": "^4.1.2"
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"nodemon": "^2.0.15",
"typescript": "^4.1.2",
"vue-template-compiler": "^2.6.11"
},
"scripts": {
"start": "node dist/main.js",
"dev": "concurrently --kill-others \"npm run devBackend\" \"npm run devWebsite\"",
"test": "echo \"Error: no test specified\" && exit 1",
"copyFiles": "copyfiles -u 3 src/webinterface/static/* dist/webinterface/static/",
"build": "tsc && npm run copyFiles",
"start": "npm run build && node dist/main.js",
"devWebsite": "nodemon -e html,css,js,png,jpg --watch src --exec npm run copyFiles",
"dev": "nodemon --watch src -e ts --exec npm start"
"copyFiles": "copyfiles -u 3 src/webinterface/frontend/dist/** dist/webinterface/frontend",
"build": "tsc && npm run buildWebsite",
"buildBackend": "tsc",
"buildWebsite": "vue-cli-service build",
"startBackend": "npm run buildBackend && node dist/main.js --inspect",
"devBackend": "nodemon --watch src --ignore src/webinterface/frontend -e ts --exec npm run startBackend",
"devWebsite": "vue-cli-service serve"
},
"author": "",
"license": "MIT"
Expand Down
80 changes: 80 additions & 0 deletions src/common/MappingGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@


export type MapType = 'line' | 'snake';
export type MapOrientation = 'horz' | 'vert';
export type MapFlip = 'none' | 'even' | 'odd';
export type MapStart = 'tl' | 'tr' | 'bl' | 'br';

export interface MappingParams {
mapType: MapType;
mapOrientation: MapOrientation;
mapFlip?: MapFlip;
mapStart: MapStart;
}

export interface MappingEntry {
position: number;
flipped: boolean;
}

export class MappingGenerator {

constructor(protected mappingParams: MappingParams) { }

// TODO: implement rotation of frames
async generateMapping(frameWidth: number, frameHeight: number): Promise<MappingEntry[]> {
const snake = this.mappingParams.mapType == 'snake';
const vertical = this.mappingParams.mapOrientation == 'vert';
const startingLeft = this.mappingParams.mapStart == 'tl' || this.mappingParams.mapStart == 'bl';
const startingTop = this.mappingParams.mapStart == 'tl' || this.mappingParams.mapStart == 'tr';

// ‧͙⁺˚* Magic, do not touch! *˚⁺‧͙

// for vertical orientation, the meaning of X and Y is swapped
const width = vertical ? frameHeight : frameWidth;
const height = vertical ? frameWidth : frameHeight;

let mapping: MappingEntry[] = [];
for (let i = 0; i < width * height; i++) {
let x: number, y: number;

// for vertical orientation, the meaning of X and Y is swapped
// booleans related if the
let flipX = vertical ? !startingTop : !startingLeft;
let flipY = vertical ? !startingLeft : !startingTop;

if (!flipY)
y = Math.floor(i / width);
else
y = height - Math.floor(i / width) - 1;

// implement snake by flipping x depending on which y we're on
if (snake) {
const flipOnEvenLine = flipY && (height % 2 == 0); // normally flip on odd lines, except when starting from bottom and height is even (flipY is said start flag, horz/vert corrected)
if (y % 2 == (flipOnEvenLine ? 0 : 1)) {
flipX = !flipX;
}
}

if (!flipX)
x = Math.floor(i % width);
else
x = width - (i % width) - 1;

let newPos: number;
// for vertical orientation, the meaning of X and Y is swapped
if (!vertical)
newPos = y * width + x;
else
newPos = x * height + y;

let flipModule = false;
if (this.mappingParams.mapFlip && this.mappingParams.mapFlip != 'none') {
flipModule = (y % 2 == 0) != (this.mappingParams.mapFlip == 'even');
}

mapping[newPos] = { position: i, flipped: flipModule };
}
return mapping;
}
}
52 changes: 52 additions & 0 deletions src/common/backendInstanceManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NodeData } from "rete/types/core/data";

export interface InstanceState {
instance: any;
params: any;
}

export class BackendInstanceManager {
instances: { [id: number]: InstanceState } = {};

constructor() {}

async createOrReconfigureInstance(node: NodeData, newParams: any, createNewInstance: () => any) {
// check if instance already exists for given node ID
if (node.id in this.instances) {
const instanceState = this.instances[node.id];
// check if parameters have changed
if (JSON.stringify(newParams) !== JSON.stringify(instanceState.params)) {
// recreate instance with new parameters
if (instanceState.instance.close) {
await instanceState.instance.close();
}
delete instanceState.instance;
instanceState.params = newParams;
instanceState.instance = createNewInstance();
}
}
else {
const newInstanceState: InstanceState = {
params: newParams,
instance: createNewInstance()
}
this.instances[node.id] = newInstanceState;
}
}

getInstance (node: NodeData) {
return this.instances[node.id];
}

async handleRemovedNodes(removedNodeIds: string[]) {
removedNodeIds.forEach(async (nodeId) => {
if (nodeId in this.instances) {
// call closing function if it exists
if (this.instances[nodeId].instance?.close) {
await this.instances[nodeId].instance.close();
}
delete this.instances[nodeId];
}
});
}
}
2 changes: 1 addition & 1 deletion src/frame.interface.ts → src/common/frame.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface Frame {
export interface Frame {
width: number;
height: number;
buffer: Buffer; // color order: RGB
Expand Down
7 changes: 7 additions & 0 deletions src/common/frameArr.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Frame } from "./frame.interface";

export interface FrameArr {
width: number; // width in amount of frames
height: number; // height in amount of frames
frames: Frame[];
}
4 changes: 4 additions & 0 deletions src/common/resolution.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Resolution {
x: number;
y: number;
}
20 changes: 20 additions & 0 deletions src/common/reteTask.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as Rete from "rete";
import { WorkerInputs } from "rete/types/core/data";

export interface ReteTask {
inputs: WorkerInputs;
component: Rete.Component;
worker: unknown;
next: Array<{
key: string;
task: ReteTask;
}>;
outputData: any;
closed: string[];

getInputs(type: string): string[];
reset(): void;
run(data: any, needReset?: boolean, garbage?: any[], propagate?: boolean): any; // returns outputData from called worker
clone(root: boolean, oldTask: ReteTask, newTask: ReteTask): ReteTask;

}
2 changes: 2 additions & 0 deletions src/common/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@


87 changes: 87 additions & 0 deletions src/common/worker/artnetInputComponentWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as Rete from "rete";

import { NodeData, WorkerInputs } from "rete/types/core/data";
import { ArtnetSource } from "../../sources/ArtnetSource";
import { BackendInstanceManager, InstanceState } from "../backendInstanceManager";
import { Frame } from "../frame.interface";
import { Resolution } from "../resolution.interface";
import { ReteTask } from "../reteTask.interface";

interface ArtnetInputParams {
port: number;
startUniverse: number;
resolution: Resolution;
}

interface ArtnetInputState extends InstanceState {
instance: ArtnetSource;
params: ArtnetInputParams;
}

export class ArtnetInputComponentWorker extends Rete.Component {
constructor(protected instMgr: BackendInstanceManager) {
super("ArtNet Input");
}

tasks: {[id: number]: ReteTask} = {};

[x: string]: any; // make Typescript happy (allow arbitrary member variables, as there is no definition file for Rete Tasks)
task = {
outputs: {frame: 'option'},
init: (task: ReteTask, node: NodeData) => { // gets called on engine.process
this.tasks[node.id] = task;
task.run(null); // init node instance with parameters from inputs (has to be done via the worker)
}
}

async builder(node: Rete.Node) {
// see node builder definition in webinterface/frontend/src/node-editor/components
}

// TODO: remove created artnet source when node gets deleted
initBackend = async (node: NodeData, inputs: WorkerInputs) => {
const nodeParams: ArtnetInputParams = {
port: (inputs['port']?.length ? inputs['port'][0] : node.data.port) as number,
startUniverse: (inputs['universe']?.length ? inputs['universe'][0] : node.data.startUniverse) as number,
resolution: (inputs['outRes']?.length ? inputs['outRes'][0] : node.data.resolution) as Resolution
};


// check for undefined parameters
if (nodeParams.port == undefined || nodeParams.startUniverse == undefined || nodeParams.resolution?.x == undefined || nodeParams.resolution?.y == undefined) {
return;
}

const task = this.tasks[node.id];

this.instMgr.createOrReconfigureInstance(node, nodeParams, () =>
new ArtnetSource(
nodeParams.resolution.x,
nodeParams.resolution.y,
(f: Frame) => { this.tasks[node.id].run({frame: f}); },
nodeParams.port,
nodeParams.startUniverse)
);

// createOrReconfigureInstance(node, this.artnetSources, nodeParams, () =>
// new ArtnetSource(
// nodeParams.resolution.x,
// nodeParams.resolution.y,
// (f: Frame) => { this.tasks[node.id].run({frame: f}); },
// nodeParams.port,
// nodeParams.startUniverse)
// );
}

async worker(node: NodeData, inputs: WorkerInputs, data: any) {
if (data === null) {
this.closed = ['frame']; // stop propagating event
this.component.initBackend(node, inputs); // worker is run outside of current class context, so we need to acess initBackend via .component
}
else {
this.closed = []; // enable propagating event again
data.fromId = node.id;
// don't have to do more, frame data is already contained in data (called from ArtnetSource frame callback)
}
}
}
Loading

0 comments on commit 61029b6

Please sign in to comment.