372 lines
14 KiB
JavaScript
Executable File
372 lines
14 KiB
JavaScript
Executable File
/* animateLights.js
|
|
* LED animation for house lightning
|
|
*/
|
|
|
|
load('api_rpc.js');
|
|
load("api_neopixel.js");
|
|
load("api_file.js");
|
|
|
|
let floor = ffi('double floor(double)');
|
|
|
|
let colors = {
|
|
black: {r:0, g:0, b:0},
|
|
red: {r:255, g:0, b:0},
|
|
green: {r:0, g:255, b:0},
|
|
blue: {r:0, g:0, b:255},
|
|
yellow: {r:255, g:255, b:0},
|
|
orange: {r:255, g:127, b:0},
|
|
purple: {r:255, g:0, b:255},
|
|
white: {r:255, g:255, b:255},
|
|
dimmedWhite: {r:50, g:50, b:50}
|
|
};
|
|
|
|
|
|
let LightAnimation = {
|
|
numberOfBulbs: 22,
|
|
ledPin: 2,
|
|
that: this,
|
|
running: false,
|
|
mode: "demo", // "demo", "stop", "animate"
|
|
generalBrightness: 30, // percentage, value 0..100
|
|
description: "Default house lightning",
|
|
defaultColor: colors.dimmedWhite,
|
|
defaultMode: "demo",
|
|
tickResolution: 1000, // number of milliseconds between ticks
|
|
currentTick: 0,
|
|
animationConfig: {}, // overwritten when loading animationConfig.js
|
|
colorSequence: [
|
|
colors.dimmedWhite,
|
|
colors.red,
|
|
colors.green,
|
|
colors.blue,
|
|
colors.yellow,
|
|
colors.orange,
|
|
colors.purple,
|
|
colors.red,
|
|
colors.red,
|
|
colors.green,
|
|
colors.green,
|
|
colors.white,
|
|
colors.white
|
|
],
|
|
adjustBrightness: function (value) { return floor(value / LightAnimation.generalBrightness); },
|
|
changeMode: function (newMode) {
|
|
if (newMode === "animate") {
|
|
LightAnimation.running = true;
|
|
LightAnimation.mode = "start-animate"; // through transient mode start-animate to animate
|
|
} else
|
|
if (newMode === "stop") {
|
|
LightAnimation.running = true; // first we want to turn off LEDs, thus needs stop-mode but running
|
|
LightAnimation.mode = "stop";
|
|
} else
|
|
if (newMode === "demo") {
|
|
LightAnimation.mode = "demo";
|
|
LightAnimation.running = true;
|
|
} else
|
|
if (newMode === "cycle") {
|
|
LightAnimation.mode = "cycle";
|
|
LightAnimation.running = true;
|
|
} else
|
|
{
|
|
print("IGNORED: changeMode called with invalid mode = ", newMode);
|
|
}
|
|
},
|
|
updateAnimationConfig: function (config) {
|
|
LightAnimation.animationConfig = JSON.parse(config);
|
|
return LightAnimation.verifyAnimationConfig();
|
|
},
|
|
verifyAnimationConfig: function() {
|
|
if (LightAnimation.animationConfig.comment === undefined) {
|
|
return {error: 'config is incomplete or invalid / comment not found'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights === undefined) {
|
|
return {error: 'config is incomplete or invalid / lights definition not found'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights[0] === undefined) {
|
|
return {error: 'config is incomplete or invalid / lights is not an array?'};
|
|
}
|
|
let i;
|
|
for (i=0; i<LightAnimation.animationConfig.lights.length; ++i) {
|
|
if (LightAnimation.animationConfig.lights[i].level === undefined) {
|
|
return {error: 'config is incomplete or invalid / level missing in lights definition'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights[i].room === undefined) {
|
|
return {error: 'config is incomplete or invalid / room missing in lights definition'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights[i].on === undefined) {
|
|
return {error: 'config is incomplete or invalid / on missing in lights definition'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights[i].on[0] === undefined) {
|
|
return {error: 'config is incomplete or invalid / on not an array in lights definition?'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights[i].on.length !== 3) {
|
|
return {error: 'config is incomplete or invalid / on must have 3 elements for R/G/B in lights definition?'};
|
|
}
|
|
if (LightAnimation.animationConfig.lights[i].id === undefined) {
|
|
return {error: 'config is incomplete or invalid / id missing in lights definition'};
|
|
}
|
|
}
|
|
return {result: 'ok'};
|
|
},
|
|
findNextAnimationStep: function (lightIndex, afterStep) {
|
|
let i = afterStep + 1;
|
|
|
|
for (i=afterStep + 1; i<LightAnimation.animationConfig.animation.length; ++i) {
|
|
if (LightAnimation.animationConfig.animation[i].lightsIndex === lightIndex)
|
|
return i;
|
|
}
|
|
// nothing found, thus start from the beginning
|
|
for (i=0; i<=afterStep; ++i) {
|
|
if (LightAnimation.animationConfig.animation[i].lightsIndex === lightIndex)
|
|
return i;
|
|
}
|
|
// nothing found again, thus it seems, there is no config for this lightIndex
|
|
return -1;
|
|
}
|
|
};
|
|
|
|
gc(true);
|
|
// load("animationConfig.js", LightAnimation);
|
|
load("animationConfig.js");
|
|
/*
|
|
LightAnimation.animationConfig = {
|
|
"comment": "lights.cfg / JSON",
|
|
"lights": [
|
|
{ "level":"EG", "room":"Wohnzimmer", "on":[200,200,200], "id":"EG-WoZi" }
|
|
],
|
|
"animation": [
|
|
{ "lightsIndex": 2, "stepType": "on", "time": 1, "comment": "Fr. S. wacht auf" }
|
|
]
|
|
};
|
|
*/
|
|
|
|
// print(LightAnimation.verifyAnimationConfig());
|
|
gc(true);
|
|
|
|
print("Initialize neoPixel");
|
|
let colorOrder = NeoPixel.GRB;
|
|
let strip = NeoPixel.create(LightAnimation.ledPin, LightAnimation.numberOfBulbs, colorOrder);
|
|
let i = 0;
|
|
strip.clear();
|
|
strip.show();
|
|
// initialize test pattern
|
|
for (i=0; i<LightAnimation.numberOfBulbs; ++i) {
|
|
if (i % 2) {
|
|
strip.setPixel(i, 80, 20, 20);
|
|
} else {
|
|
strip.setPixel(i, 20, 80, 20);
|
|
}
|
|
}
|
|
strip.show();
|
|
|
|
|
|
// Add RPC handlers
|
|
print("Initialize RPC Handlers");
|
|
RPC.addHandler('HouseLightning.Start', function(args) {
|
|
LightAnimation.changeMOde("animate");
|
|
return {result: 'ok'};
|
|
}, null);
|
|
|
|
RPC.addHandler('HouseLightning.Stop', function(args) {
|
|
LightAnimation.changeMode("stop");
|
|
return {result: 'ok'};
|
|
}, null);
|
|
|
|
RPC.addHandler('HouseLightning.DemoMode', function(args) {
|
|
LightAnimation.changeMode("demo");
|
|
return {result: 'ok'};
|
|
}, null);
|
|
|
|
RPC.addHandler('HouseLightning.CycleMode', function(args) {
|
|
LightAnimation.changeMOde("cycle");
|
|
return {result: 'ok'};
|
|
}, null);
|
|
|
|
RPC.addHandler('HouseLightning.GetAnimationConfig', function(args) {
|
|
return LightAnimation.animationConfig;
|
|
}, null);
|
|
|
|
RPC.addHandler('HouseLightning.SetAnimationConfig', function(args) {
|
|
if (args !== undefined && args.config !== undefined) {
|
|
return LightAnimation.updateAnimationConfig(args.config);
|
|
} else {
|
|
return {error: 'parameter config is required'};
|
|
}
|
|
}, null);
|
|
|
|
function demoAnimation(strip, shiftBy) {
|
|
let i=0, colorIndex=0, color;
|
|
|
|
for (i=0; i<LightAnimation.numberOfBulbs; ++i) {
|
|
colorIndex = shiftBy % LightAnimation.colorSequence.length;
|
|
color = LightAnimation.colorSequence[colorIndex];
|
|
strip.setPixel(i, LightAnimation.adjustBrightness(color.r), LightAnimation.adjustBrightness(color.g), LightAnimation.adjustBrightness(color.b));
|
|
// print("runNeo shift =", shiftBy, ", colorIndex =", colorIndex, ", r =", color.r, ", g =", color.g, ", b =", color.b);
|
|
shiftBy++;
|
|
}
|
|
strip.show();
|
|
}
|
|
|
|
// initialize module
|
|
let configString = "";
|
|
/*configString = File.read("lights.cfg");
|
|
if (configString === "" || configString === null) configString = "***default***";
|
|
// print("configString = ", configString);
|
|
LightAnimation.animationConfig = JSON.parse(configString);
|
|
configString = null;*/
|
|
|
|
// LightAnimation.animationConfig = animationConfig;
|
|
|
|
|
|
|
|
|
|
// print("LightAnimation.animationConfig = ", JSON.stringify(LightAnimation.animationConfig));
|
|
print("Animation length: ", LightAnimation.animationConfig.animation.length);
|
|
// initialize internal data structure
|
|
let defaultLEDstate = {
|
|
index: 0, // LED #
|
|
nextTick: 0, // next action necessary on tick #
|
|
rampTickDelta: 0, // to be added on nextTick for Ramp-Animation
|
|
mode: "off", // "on", "off", "blink", "tv", "fire", "welding"
|
|
currentLEDCtrl: 0, // index on LEDCtrl[LEDnum][...]
|
|
numLEDCtrl: 0, // number of LEDCtrl entries for this bulb
|
|
modeStep: 0, // dependent on mode to ctrl animation details
|
|
clockEndHours: 23, // time, this animation should end
|
|
clockEndMinutes: 59,
|
|
currentRed: colors.dimmedWhite.r, currentGreen: colors.dimmedWhite.g, currentBlue: colors.dimmedWhite.b,
|
|
onRed: colors.dimmedWhite.r, onGreen: colors.dimmedWhite.g, onBlue: colors.dimmedWhite.b,
|
|
targetRed: 0, targetGreen: 0, targetBlue: 0,
|
|
currentAnimationStep:0 // animation step in LightAnimation.animationConfig.animation[step]
|
|
};
|
|
let LEDstate = [];
|
|
|
|
|
|
for (i=0; i<1/*LightAnimation.numberOfBulbs*/; ++i) {
|
|
gc(true);
|
|
print("Init LEDstate", i, ", free RAM ", Sys.free_ram());
|
|
LEDstate[i] =Object.create(defaultLEDstate);
|
|
LEDstate[i].index = i;
|
|
LEDstate[i].nextTick = defaultLEDstate.nextTick;
|
|
LEDstate[i].rampTickDelta = defaultLEDstate.rampTickDelta;
|
|
LEDstate[i].mode = defaultLEDstate.mode;
|
|
LEDstate[i].currentLEDCtrl = defaultLEDstate.currentLEDCtrl;
|
|
LEDstate[i].numLEDCtrl = defaultLEDstate.numLEDCtrl;
|
|
LEDstate[i].modeStep = defaultLEDstate.modeStep;
|
|
LEDstate[i].clockEndHours = defaultLEDstate.clockEndHours;
|
|
LEDstate[i].clockEndMinutes = defaultLEDstate.clockEndMinutes;
|
|
LEDstate[i].currentRed = defaultLEDstate.currentRed;
|
|
LEDstate[i].currentGreen = defaultLEDstate.currentGreen;
|
|
LEDstate[i].currentBlue = defaultLEDstate.currentBlue;
|
|
LEDstate[i].targetRed = defaultLEDstate.targetRed;
|
|
LEDstate[i].targetGreen = defaultLEDstate.targetGreen;
|
|
LEDstate[i].targetBlue = defaultLEDstate.targetBlue;
|
|
LEDstate[i].onRed = defaultLEDstate.onRed; //LightAnimation.animationConfig.lights.on[0];
|
|
LEDstate[i].onGreen = defaultLEDstate.onGreen; //LightAnimation.animationConfig.lights.on[1];
|
|
LEDstate[i].onBlue = defaultLEDstate.onBlue; //LightAnimation.animationConfig.lights.on[2];
|
|
// LEDstate[i].currentAnimationStep = LightAnimation.findNextAnimationStep(i, -1);
|
|
}
|
|
|
|
print("LEDstate index0=", LEDstate[0].index, ", index1=", LEDstate[1].index);
|
|
print("LEDstate=", JSON.stringify(LEDstate));
|
|
|
|
function animateBulb(bulb, currentTick) {
|
|
if (LEDstate[bulb].mode === "on") {
|
|
strip.setPixel(bulb, LightAnimation.adjustBrightness(LEDstate[bulb].currentRed), LightAnimation.adjustBrightness(LEDstate[bulb].currentGreen), LightAnimation.adjustBrightness(LEDstate[bulb].currentBlue));
|
|
} else
|
|
if (LEDstate[bulb].mode === "off") {
|
|
strip.setPixel(bulb, 0, 0, 0);
|
|
} else print ("animateBulb: mode not yet implemented - ", LEDstate[bulb].mode);
|
|
}
|
|
|
|
let shiftAnimation = 1; // needed for demo mode
|
|
|
|
function animateTick() {
|
|
// started for every single ticks; does all changes necessary according to animation configuration
|
|
print("***animate***");
|
|
return;
|
|
if (LightAnimation.running) {
|
|
if (LightAnimation.mode === "demo") {
|
|
demoAnimation(strip, shiftAnimation);
|
|
shiftAnimation++;
|
|
// print("*** demo-mode, next shift=", shiftAnimation);
|
|
} else if (LightAnimation.mode === "stop") {
|
|
strip.clear();
|
|
strip.show();
|
|
LightAnimation.running = false;
|
|
print("*** stopped");
|
|
} else if (LightAnimation.mode === "start-animate") {
|
|
strip.clear();
|
|
strip.show();
|
|
LightAnimation.running = true;
|
|
LightAnimation.mode = "animate";
|
|
print("*** started");
|
|
} else if (LightAnimation.mode === "animate") {
|
|
LightAnimation.currentTick++;
|
|
// do animation step changes
|
|
for (i=0; i<LightAnimation.animationConfig.animation.length; ++i) {
|
|
if (LightAnimation.animationConfig.animation[i].time === animateTick) {
|
|
// activate new state
|
|
let bulbIndex = LightAnimation.animationConfig.animation[i].lightsIndex;
|
|
let nextAnimationStep = LightAnimation.findNextAnimationStep(bulbIndex, LEDstate[bulbIndex].currentAnimationStep);
|
|
LEDstate[bulbIndex].currentAnimationStep = nextAnimationStep;
|
|
LEDstate[bulbIndex].nextTick = LightAnimation.animationConfig.animation[nextAnimationStep].time;
|
|
LEDstate[bulbIndex].mode = LightAnimation.animationConfig.animation[nextAnimationStep].stepType;
|
|
LEDstate[bulbIndex].modeStep = 0; // reset to start
|
|
if (LEDstate[bulbIndex].mode === "off") {
|
|
LEDstate[bulbIndex].targetRed = 0;
|
|
LEDstate[bulbIndex].targetGreen = 0;
|
|
LEDstate[bulbIndex].targetBlue = 0;
|
|
LEDstate[bulbIndex].currentRed = LEDstate[bulbIndex].targetRed;
|
|
LEDstate[bulbIndex].currentGreen = LEDstate[bulbIndex].targetGreen;
|
|
LEDstate[bulbIndex].currentBlue = LEDstate[bulbIndex].targetBlue;
|
|
} else
|
|
if (LEDstate[bulbIndex].mode === "on") {
|
|
LEDstate[bulbIndex].targetRed = LEDstate[bulbIndex].onRed;
|
|
LEDstate[bulbIndex].targetGreen = LEDstate[bulbIndex].onGreen;
|
|
LEDstate[bulbIndex].targetBlue = LEDstate[bulbIndex].onBlue;
|
|
LEDstate[bulbIndex].currentRed = LEDstate[bulbIndex].targetRed;
|
|
LEDstate[bulbIndex].currentGreen = LEDstate[bulbIndex].targetGreen;
|
|
LEDstate[bulbIndex].currentBlue = LEDstate[bulbIndex].targetBlue;
|
|
} else
|
|
print("ERROR: Unsupported mode in LEDstate[", bulbIndex, "].mode=", LEDstate[bulbIndex].mode);
|
|
// updateBulbAnimation(bulbIndex, animateTick, LightAnimation.animationConfig.animation[i]);
|
|
/*
|
|
if (LightAnimation.animationConfig.animation[i].stepType === "on") {
|
|
LEDstate[bulbIndex].mode = "on";
|
|
strip.setPixel(bulbIndex, LEDstate[bulbIndex].defaultColor.r, LEDstate[bulbIndex].defaultColor.g, LEDstate[bulbIndex].defaultColor.b);
|
|
} else
|
|
if (LightAnimation.animationConfig.animation[i].stepType === "off") {
|
|
LEDstate[LightAnimation.lightsIndex].mode = "off";
|
|
strip.setPixel(LightAnimation.lightsIndex, 0, 0, 0);
|
|
} else print("*** animation stepType not implemented: ", LightAnimation.animationConfig[i].stepType);
|
|
*/
|
|
}
|
|
}
|
|
// do microanimation
|
|
for (i=0; i<LightAnimation.numberOfBulbs; ++i) {
|
|
animateBulb(i, LightAnimation.currentTick);
|
|
if (LightAnimation.currentTick >= LEDstate[i].nextTick) {
|
|
// update this animated bulb
|
|
}
|
|
// strip.setPixel called in animateBulb
|
|
// strip.setPixel(i, LightAnimation.adjustBrightness(LEDstate[i].currentRed), LightAnimation.adjustBrightness(LEDstate[i].currentGreen), LightAnimation.adjustBrightness(LEDstate[i].currentBlue));
|
|
}
|
|
strip.show();
|
|
} else print("*** Unknown LightAnimation.mode =", LightAnimation.mode);
|
|
}
|
|
}
|
|
|
|
// Garbage collection before startup, full=true --> reclaim RAM to OS
|
|
gc(true);
|
|
|
|
print("Initialize timer, start in 5 seconds");
|
|
// Show start pattern for 5 seconds, then switch to default colors
|
|
Timer.set(5000 /* msec */, false /* repeat */, function() {
|
|
print("START animation");
|
|
for (i=0; i<LightAnimation.numberOfBulbs; ++i) {
|
|
strip.setPixel(i, LightAnimation.defaultColor.r, LightAnimation.defaultColor.g, LightAnimation.defaultColor.b);
|
|
}
|
|
strip.show();
|
|
Timer.set(LightAnimation.tickResolution, true /* repeat */, animateTick, null);
|
|
}, null);
|