-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlighting.js
207 lines (169 loc) · 6.3 KB
/
lighting.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
/*
Lighting module - controls lighting via ArtNet, by fetching commands from the server
*/
const config = require('./config');
const _ = require('logger');
const fs = require('fs/promises');
const axios = require('axios');
const { join } = require('path');
const { handle } = require('express/lib/application');
/**
* Set the minimum log level to BASE (the lowest level) so we see everything.
*/
_.setMinimumLevel(_.LogLevel.BASE);
/**
* The set of acceptable commands from the server
* @type {string|RegExp[]}
*/
let COMMANDS = [];
/**
* The enabled merge controllers for different fixtures, each function should take a command and return an object of
* DMX data
* @type {Function[]}
*/
const mergers = [];
/**
* An object containing the output of currently running animations. The result of each animator is determined by itself
* so only use effects you know the result of
* @type {{}}
*/
const effects = {};
/**
* Loads all the fixture profiles and binds them to the addresses specified in the addresses object
* @returns {Promise<void>}
*/
async function loadFixtures() {
const fixtures = require('./fixtures');
for (const [group, fixture] of Object.entries(fixtures)) {
_.trace(`registering ${fixture.name} with addresses ${fixture.patch}`);
const entries = fixture.patch;
console.log("PREFIX OPTION: " + group);
if (!Object.values(entries).map((e) => Array.isArray(e)).reduce((prev, cur) => prev && cur)) {
for (const [prefix, addressConfig] of Object.entries(entries)) {
const {convert, commands} = require(join(__dirname, 'fixture-profiles', fixture.profile))(group, addressConfig, effects);
mergers.push(convert);
COMMANDS.push(...commands);
_.trace('Adding commands: ', commands);
}
} else {
const {convert, commands} = require(join(__dirname, 'fixture-profiles', fixture.profile))(undefined, entries, effects);
mergers.push(convert);
COMMANDS.push(...commands);
_.trace('Adding commands: ', commands);
}
}
_.trace('all profiles loaded, final commands:');
}
/**
* Finds all animator files and requires them pushing their executors and creating the initial values in the effects
* object. Animator functions will be called every 10ms.
* @returns {Promise<void>}
*/
async function enableAnimators() {
let animationTime = 0;
let animators = [];
const animationHandler = () => {
if (++animationTime > 255) animationTime = 0;
animators.forEach((f) => f(animationTime, effects));
};
setInterval(animationHandler, 10);
const files = await fs.readdir(join(__dirname, 'animators'));
files.filter((name) => name.endsWith('.js')).forEach((name) => {
const {executor, index, initial} = require(join(__dirname, 'animators', name));
animators.push(executor);
effects[index] = initial;
});
}
/**
* Fetches commands from the server and processes them.
* @returns {Promise<void>}
*/
async function fetchCommands() {
axios.get(`${config.crowdcontrolServer}/getLightRequests.php`)
.then((response) => {
response.data.forEach((command) => {
handleCommand(command);
});
})
.catch((error) => {
console.error("Fetch Error:", error);
});
}
let activeCommands = [];
/**
* Handles an incoming message from server. It will check the command against the list of possible
* acceptable commands (without a '-' symbol if present). It will remove the command if it is prefixed with '-' and if
* not it will replace all conflicting commands (with the same two part prefix) to prevent overlapping instructions. If
* it is not found or accepted it will reject it.
* @param command
* @returns
*/
function handleCommand(command) {
const test = command.startsWith('-') ? command.substring(1) : command;
const matched = COMMANDS
.map((e) => typeof (e) === 'string' ? e.toLowerCase() === test.toLowerCase() : e.test(test))
.reduce((old, now) => old || now);
if (!matched) {
_.warn(`message "${command}" rejected because it is not contained within the valid commands`);
return;
}
if (command.startsWith('-')) {
const toRemove = command.substring(1);
activeCommands = activeCommands.filter((e) => e !== toRemove);
_.debug(`removed command ${toRemove}`);
return;
}
// Find all conflicting commands
const [key, action] = command.split('.');
const conflictKey = `${key}.${action}`;
// const conflictKey = command.substr(0, command.lastIndexOf('.') + 1);
const resultant = [].concat(
// All active commands that conflict
activeCommands.filter((e) => !e.startsWith(conflictKey)),
// Plus the new command
[command],
);
_.info(`Command: ${command} has caused a change of ${resultant.length - activeCommands.length} instructions`);
activeCommands = resultant;
console.trace(activeCommands);
}
const lastOutgoingData = {};
/**
* Sends artnet data based on the activeCommands
*/
function sendArtnetUpdate() {
// TODO: determine what universes to send to based on config from somewhere
for (let universe = 0; universe < 50; universe++) {
const commands = activeCommands
.map((command) => mergers.map((merger) => merger(universe, command)))
.flat()
.reduce(
(prev, cur) => Object.assign(prev, cur),
{}
);
if (Object.keys(commands).length === 0) {
continue;
}
// array of each channel value in universe
var dmx = lastOutgoingData[universe] ?? Array(512).fill(0);
Object.keys(commands).forEach((k) => {
dmx[k] = commands[k];
});
artnet.set(universe, 1, dmx);
lastOutgoingData[universe] = dmx;
}
}
var options = {
iface: config.lightingInterface,
};
const artnet = require('artnet')(options);
module.exports = async () => {
console.log("Lighting module initialized");
await loadFixtures()
.then(enableAnimators)
.catch(console.error);
console.log("Begin fetching commands");
setInterval(fetchCommands, 100);
console.log("Begin outputting artnet")
setInterval(sendArtnetUpdate, 1);
}