Skip to content
Snippets Groups Projects
effect.h 9.01 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * 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:
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        PixelInfo(unsigned index, const rapidjson::Value& layout);
    
    
        // 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;
    
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
    // 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:
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        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.
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        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
    
        // 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;
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        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();
    }
    
    
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
    inline FrameInfo::FrameInfo()
        : timeDelta(0), time(0) {}
    
    inline void FrameInfo::init(const rapidjson::Value &layout)
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        timeDelta = 0;
        time = 0;
        pixels.clear();
    
        for (unsigned i = 0; i < layout.Size(); i++) {
            PixelInfo p(i, layout[i]);
            pixels.push_back(p);
        }
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
    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;
    
        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);
    
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        // 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()) {
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        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));
                }
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
        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]]");
    
    Micah Elizabeth Scott's avatar
    Micah Elizabeth Scott committed
    static inline float sq(float a)
    {
        // Fast square
        return a*a;
    }