/* * 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 "opcclient.h" #include "svl/SVL.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 Vec3 point; // 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 PixelInfoVec pixels; }; // Abstract base class for one LED effect class Effect { public: 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); bool hasLayout(); const rapidjson::Document& getLayout(); Effect* getEffect(); OPCClient& getClient(); // Main loop body void doFrame(); void doFrame(float timeDelta); // Minimal main loop void run(); // 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; FrameInfo frameInfo; int usage(const char *name); }; inline PixelInfo::PixelInfo(unsigned index, const rapidjson::Value& layout) : point(0, 0, 0), index(index), layout(layout) { if (isMapped()) { 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; // Defaults setMaxFrameRate(300); setServer("localhost"); } 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); // Init pixel info frameInfo.init(layout); return true; } inline const rapidjson::Document& EffectRunner::getLayout() { return layout; } inline bool EffectRunner::hasLayout() { return layout.IsArray(); } 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) { if (!getEffect() || !hasLayout()) { return; } frameInfo.advance(timeDelta); effect->beginFrame(frameInfo); // Only calculate the effect if we have a connection if (opc.tryConnect()) { uint8_t *dest = OPCClient::Header::view(frameBuffer).data(); for (PixelInfoIter i = frameInfo.pixels.begin(), e = frameInfo.pixels.end(); i != e; ++i) { Vec3 rgb(0, 0, 0); const PixelInfo &p = *i; if (p.isMapped()) { effect->calculatePixel(rgb, p); } for (unsigned i = 0; i < 3; i++) { *(dest++) = std::min<int>(255, std::max<int>(0, rgb[i] * 255 + 0.5)); } } opc.write(frameBuffer); } effect->endFrame(frameInfo); // 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; } return true; } 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"); return false; } return true; } void EffectRunner::argumentUsage() { fprintf(stderr, "[-fps LIMIT] [-layout FILE.json] [-server HOST[:port]]"); } static inline float sq(float a) { // Fast square return a*a; }