Newer
Older
/*
* LED Effect framework
*
* Copyright (c) 2014 Micah Elizabeth Scott <micah@scanlime.org>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include <math.h>
#include <unistd.h>
#include <algorithm>
#include <vector>
#include <sys/time.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "rapidjson/rapidjson.h"
#include "rapidjson/filestream.h"
#include "rapidjson/document.h"
// Information about one LED pixel
class PixelInfo {
public:
PixelInfo(unsigned index, const rapidjson::Value& layout);
// Point coordinates
// Index in the framebuffer
unsigned index;
// Parsed JSON for this pixel's layout
const rapidjson::Value &layout;
// Is this pixel being used, or is it a placeholder?
bool isMapped() const;
typedef std::vector<PixelInfo> PixelInfoVec;
typedef std::vector<PixelInfo>::const_iterator PixelInfoIter;
// Information about one Effect frame
class FrameInfo {
public:
FrameInfo();
void init(const rapidjson::Value &layout);
void advance(float timeDelta);
// Seconds passed since the last frame
float timeDelta;
// Time since the pattern started
double time;
// Info for every pixel
// Abstract base class for one LED effect
virtual void beginFrame(const FrameInfo& f);
virtual void endFrame(const FrameInfo& f);
// Calculate a pixel value, using floating point RGB in the range [0, 1].
// Caller is responsible for clamping if necessary. This supports effects
// that layer with other effects using greater than 8-bit precision.
virtual void calculatePixel(Vec3& rgb, const PixelInfo& p) = 0;
};
class EffectRunner {
public:
EffectRunner();
bool setServer(const char *hostport);
bool setLayout(const char *filename);
void setEffect(Effect* effect);
void setMaxFrameRate(float fps);
const rapidjson::Document& getLayout();
Effect* getEffect();
OPCClient& getClient();
void doFrame();
void doFrame(float timeDelta);
// Simple argument parsing and main loop
int main(int argc, char **argv);
protected:
// Extensibility for argument parsing
virtual bool parseArgument(int &i, int &argc, char **argv);
virtual bool validateArguments();
virtual void argumentUsage();
private:
float minTimeDelta;
rapidjson::Document layout;
OPCClient opc;
Effect *effect;
struct timeval lastTime;
std::vector<uint8_t> frameBuffer;
inline PixelInfo::PixelInfo(unsigned index, const rapidjson::Value& layout)
: point(0, 0, 0), index(index), layout(layout)
const rapidjson::Value& pointValue = layout["point"];
if (pointValue.IsArray()) {
for (unsigned i = 0; i < 3 && i < pointValue.Size(); i++) {
point[i] = pointValue[i].GetDouble();
}
}
}
}
inline bool PixelInfo::isMapped() const
{
return layout.IsObject();
}
inline FrameInfo::FrameInfo()
: timeDelta(0), time(0) {}
inline void FrameInfo::init(const rapidjson::Value &layout)
timeDelta = 0;
time = 0;
pixels.clear();
for (unsigned i = 0; i < layout.Size(); i++) {
PixelInfo p(i, layout[i]);
pixels.push_back(p);
}
inline void FrameInfo::advance(float timeDelta)
{
this->timeDelta = timeDelta;
this->time += timeDelta;
}
inline void Effect::beginFrame(const FrameInfo &f) {}
inline void Effect::endFrame(const FrameInfo &f) {}
inline EffectRunner::EffectRunner()
: minTimeDelta(0), effect(0)
{
lastTime.tv_sec = 0;
lastTime.tv_usec = 0;
Micah Elizabeth Scott
committed
// Defaults
setMaxFrameRate(300);
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
}
inline void EffectRunner::setMaxFrameRate(float fps)
{
minTimeDelta = 1.0 / fps;
}
inline bool EffectRunner::setServer(const char *hostport)
{
return opc.resolve(hostport);
}
inline bool EffectRunner::setLayout(const char *filename)
{
FILE *f = fopen(filename, "r");
if (!f) {
return false;
}
rapidjson::FileStream istr(f);
layout.ParseStream<0>(istr);
fclose(f);
if (layout.HasParseError()) {
return false;
}
if (!layout.IsArray()) {
return false;
}
// Set up an empty framebuffer, with OPC packet header
int frameBytes = layout.Size() * 3;
frameBuffer.resize(sizeof(OPCClient::Header) + frameBytes);
OPCClient::Header::view(frameBuffer).init(0, opc.SET_PIXEL_COLORS, frameBytes);
return true;
}
inline const rapidjson::Document& EffectRunner::getLayout()
{
return layout;
}
inline bool EffectRunner::hasLayout()
{
return layout.IsArray();
}
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
inline void EffectRunner::setEffect(Effect *effect)
{
this->effect = effect;
}
inline Effect* EffectRunner::getEffect()
{
return effect;
}
inline void EffectRunner::run()
{
while (true) {
doFrame();
}
}
inline void EffectRunner::doFrame()
{
struct timeval now;
gettimeofday(&now, 0);
float delta = (now.tv_sec - lastTime.tv_sec)
+ 1e-6 * (now.tv_usec - lastTime.tv_usec);
lastTime = now;
// Max timestep; jump ahead if we get too far behind.
const float maxStep = 0.1;
if (delta > maxStep) {
delta = maxStep;
}
doFrame(delta);
}
inline void EffectRunner::doFrame(float timeDelta)
{
return;
}
frameInfo.advance(timeDelta);
effect->beginFrame(frameInfo);
Micah Elizabeth Scott
committed
// Only calculate the effect if we have a connection
if (opc.tryConnect()) {
Micah Elizabeth Scott
committed
uint8_t *dest = OPCClient::Header::view(frameBuffer).data();
Micah Elizabeth Scott
committed
for (PixelInfoIter i = frameInfo.pixels.begin(), e = frameInfo.pixels.end(); i != e; ++i) {
Vec3 rgb(0, 0, 0);
const PixelInfo &p = *i;
Micah Elizabeth Scott
committed
if (p.isMapped()) {
effect->calculatePixel(rgb, p);
}
Micah Elizabeth Scott
committed
for (unsigned i = 0; i < 3; i++) {
*(dest++) = std::min<int>(255, std::max<int>(0, rgb[i] * 255 + 0.5));
}
Micah Elizabeth Scott
committed
opc.write(frameBuffer);
}
// Extra delay, to adjust frame rate
if (timeDelta < minTimeDelta) {
usleep((minTimeDelta - timeDelta) * 1e6);
}
}
inline OPCClient& EffectRunner::getClient()
{
return opc;
}
inline int EffectRunner::main(int argc, char **argv)
{
for (int i = 1; i < argc; i++) {
if (!parseArgument(i, argc, argv)) {
return usage(argv[0]);
if (!validateArguments()) {
return usage(argv[0]);
}
run();
return 0;
}
inline int EffectRunner::usage(const char *name)
{
fprintf(stderr, "usage: %s ", name);
argumentUsage();
fprintf(stderr, "\n");
return 1;
}
bool EffectRunner::parseArgument(int &i, int &argc, char **argv)
{
if (!strcmp(argv[i], "-fps") && (i+1 < argc)) {
float rate = atof(argv[++i]);
if (rate <= 0) {
fprintf(stderr, "Invalid frame rate\n");
return false;
setMaxFrameRate(rate);
return true;
}
if (!strcmp(argv[i], "-layout") && (i+1 < argc)) {
if (!setLayout(argv[++i])) {
fprintf(stderr, "Can't load layout from %s\n", argv[i]);
return false;
if (!strcmp(argv[i], "-server") && (i+1 < argc)) {
if (!setServer(argv[++i])) {
fprintf(stderr, "Can't resolve server name %s\n", argv[i]);
return false;
}
return true;
return false;
}
bool EffectRunner::validateArguments()
{
if (!hasLayout()) {
fprintf(stderr, "No layout specified\n");
fprintf(stderr, "[-fps LIMIT] [-layout FILE.json] [-server HOST[:port]]");
static inline float sq(float a)
{
// Fast square
return a*a;
}