From 945bf78459dd928a3306e1034780c2c75aee5502 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Wed, 29 Aug 2018 10:54:22 +0200 Subject: [PATCH] add neopixel example --- lib/NeoPattern/NeoPattern.cpp | 338 ++++++++++++++++++++++++ lib/NeoPattern/NeoPatternState.cpp | 58 ++++ lib/NeoPattern/NeoPattern_api_json.h | 16 ++ lib/NeoPattern/NeoPattern_api_modes.cpp | 66 +++++ lib/sprocket-utils/utils_print.cpp | 25 ++ lib/sprocket-utils/utils_print.h | 15 ++ platformio.ini | 17 +- src/examples/meshPixel/MeshPixel.h | 67 +++++ src/examples/meshPixel/README.md | 7 + src/examples/meshPixel/config.h | 43 +++ src/examples/meshPixel/main.cpp | 25 ++ 11 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 lib/NeoPattern/NeoPattern.cpp create mode 100644 lib/NeoPattern/NeoPatternState.cpp create mode 100644 lib/NeoPattern/NeoPattern_api_json.h create mode 100644 lib/NeoPattern/NeoPattern_api_modes.cpp create mode 100644 lib/sprocket-utils/utils_print.cpp create mode 100644 lib/sprocket-utils/utils_print.h create mode 100644 src/examples/meshPixel/MeshPixel.h create mode 100644 src/examples/meshPixel/README.md create mode 100644 src/examples/meshPixel/config.h create mode 100644 src/examples/meshPixel/main.cpp diff --git a/lib/NeoPattern/NeoPattern.cpp b/lib/NeoPattern/NeoPattern.cpp new file mode 100644 index 0000000..bff35c8 --- /dev/null +++ b/lib/NeoPattern/NeoPattern.cpp @@ -0,0 +1,338 @@ +#ifndef __NeoPattern_INCLUDED__ +#define __NeoPattern_INCLUDED__ + +#include + +using namespace std; +using namespace std::placeholders; + +struct NeoPixelConfig { + int pin; + int length; + int brightness; + int updateInterval; + int defaultColor; +}; + +/** + * Original NeoPattern code by Bill Earl + * https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview + * + * Custom modifications by 0x1d: + * - default OnComplete callback that sets pattern to reverse + * - separate animation update from timer; Update now updates directly, UpdateScheduled uses timer + */ + +// Pattern types supported: +enum pattern { NONE = 0, RAINBOW_CYCLE = 1, THEATER_CHASE = 2, COLOR_WIPE = 3, SCANNER = 4, FADE = 5 }; +// Patern directions supported: +enum direction { FORWARD, REVERSE }; + +// NeoPattern Class - derived from the Adafruit_NeoPixel class +class NeoPattern : public Adafruit_NeoPixel +{ + public: + + // Member Variables: + pattern ActivePattern; // which pattern is running + direction Direction; // direction to run the pattern + + unsigned long Interval; // milliseconds between updates + unsigned long lastUpdate; // last update of position + + uint32_t Color1, Color2; // What colors are in use + uint16_t TotalSteps; // total number of steps in the pattern + uint16_t Index; // current step within the pattern + uint16_t completed = 0; + + void (*OnComplete)(int); // Callback on completion of pattern + + // Constructor - calls base-class constructor to initialize strip + NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int)) + :Adafruit_NeoPixel(pixels, pin, type) + { + OnComplete = callback; + } + + NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type) + :Adafruit_NeoPixel(pixels, pin, type) + { + bind(&NeoPattern::onCompleteDefault, this, pixels); + } + + void onCompleteDefault(int pixels) { + if(ActivePattern != RAINBOW_CYCLE){ + Reverse(); + } + Serial.println("pattern completed"); + } + + // Update the pattern + void Update() + { + switch(ActivePattern) + { + case RAINBOW_CYCLE: + RainbowCycleUpdate(); + break; + case THEATER_CHASE: + TheaterChaseUpdate(); + break; + case COLOR_WIPE: + ColorWipeUpdate(); + break; + case SCANNER: + ScannerUpdate(); + break; + case FADE: + FadeUpdate(); + break; + default: + break; + } + } + + void UpdateScheduled() + { + if((millis() - lastUpdate) > Interval) // time to update + { + lastUpdate = millis(); + Update(); + } + } + + // Increment the Index and reset at the end + void Increment() + { + completed = 0; + if (Direction == FORWARD) + { + Index++; + if (Index >= TotalSteps) + { + Index = 0; + if (OnComplete != NULL) + { + completed = 1; + OnComplete(numPixels()); // call the comlpetion callback + } + } + } + else // Direction == REVERSE + { + --Index; + if (Index <= 0) + { + Index = TotalSteps-1; + if (OnComplete != NULL) + { + completed = 1; + OnComplete(numPixels()); // call the comlpetion callback + } + } + } + } + + // Reverse pattern direction + void Reverse() + { + if (Direction == FORWARD) + { + Direction = REVERSE; + Index = TotalSteps-1; + } + else + { + Direction = FORWARD; + Index = 0; + } + } + + // Initialize for a RainbowCycle + void RainbowCycle(uint8_t interval, direction dir = FORWARD) + { + ActivePattern = RAINBOW_CYCLE; + Interval = interval; + TotalSteps = 255; + Index = 0; + Direction = dir; + } + + // Update the Rainbow Cycle Pattern + void RainbowCycleUpdate() + { + for(int i=0; i< numPixels(); i++) + { + setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255)); + } + show(); + Increment(); + } + + // Initialize for a Theater Chase + void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD) + { + ActivePattern = THEATER_CHASE; + Interval = interval; + TotalSteps = numPixels(); + Color1 = color1; + Color2 = color2; + Index = 0; + Direction = dir; + } + + // Update the Theater Chase Pattern + void TheaterChaseUpdate() + { + for(int i=0; i< numPixels(); i++) + { + if ((i + Index) % 3 == 0) + { + setPixelColor(i, Color1); + } + else + { + setPixelColor(i, Color2); + } + } + show(); + Increment(); + } + + // Initialize for a ColorWipe + void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD) + { + ActivePattern = COLOR_WIPE; + Interval = interval; + TotalSteps = numPixels(); + Color1 = color; + Index = 0; + Direction = dir; + } + + // Update the Color Wipe Pattern + void ColorWipeUpdate() + { + setPixelColor(Index, Color1); + show(); + Increment(); + } + + // Initialize for a SCANNNER + void Scanner(uint32_t color1, uint8_t interval) + { + ActivePattern = SCANNER; + Interval = interval; + TotalSteps = (numPixels() - 1) * 2; + Color1 = color1; + Index = 0; + } + + // Update the Scanner Pattern + void ScannerUpdate() + { + for (int i = 0; i < numPixels(); i++) + { + if (i == Index) // Scan Pixel to the right + { + setPixelColor(i, Color1); + } + else if (i == TotalSteps - Index) // Scan Pixel to the left + { + setPixelColor(i, Color1); + } + else // Fading tail + { + setPixelColor(i, DimColor(getPixelColor(i))); + } + } + show(); + Increment(); + } + + // Initialize for a Fade + void Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir = FORWARD) + { + ActivePattern = FADE; + Interval = interval; + TotalSteps = steps; + Color1 = color1; + Color2 = color2; + Index = 0; + Direction = dir; + } + + // Update the Fade Pattern + void FadeUpdate() + { + // Calculate linear interpolation between Color1 and Color2 + // Optimise order of operations to minimize truncation error + uint8_t red = ((Red(Color1) * (TotalSteps - Index)) + (Red(Color2) * Index)) / TotalSteps; + uint8_t green = ((Green(Color1) * (TotalSteps - Index)) + (Green(Color2) * Index)) / TotalSteps; + uint8_t blue = ((Blue(Color1) * (TotalSteps - Index)) + (Blue(Color2) * Index)) / TotalSteps; + + ColorSet(Color(red, green, blue)); + show(); + Increment(); + } + + // Calculate 50% dimmed version of a color (used by ScannerUpdate) + uint32_t DimColor(uint32_t color) + { + // Shift R, G and B components one bit to the right + uint32_t dimColor = Color(Red(color) >> 1, Green(color) >> 1, Blue(color) >> 1); + return dimColor; + } + + // Set all pixels to a color (synchronously) + void ColorSet(uint32_t color) + { + for (int i = 0; i < numPixels(); i++) + { + setPixelColor(i, color); + } + show(); + } + + // Returns the Red component of a 32-bit color + uint8_t Red(uint32_t color) + { + return (color >> 16) & 0xFF; + } + + // Returns the Green component of a 32-bit color + uint8_t Green(uint32_t color) + { + return (color >> 8) & 0xFF; + } + + // Returns the Blue component of a 32-bit color + uint8_t Blue(uint32_t color) + { + return color & 0xFF; + } + + // Input a value 0 to 255 to get a color value. + // The colours are a transition r - g - b - back to r. + uint32_t Wheel(byte WheelPos) + { + if(WheelPos == 0) return Color(0,0,0); + WheelPos = 255 - WheelPos; + if(WheelPos < 85) + { + return Color(255 - WheelPos * 3, 0, WheelPos * 3); + } + else if(WheelPos < 170) + { + WheelPos -= 85; + return Color(0, WheelPos * 3, 255 - WheelPos * 3); + } + else + { + WheelPos -= 170; + return Color(WheelPos * 3, 255 - WheelPos * 3, 0); + } + } +}; + +#endif \ No newline at end of file diff --git a/lib/NeoPattern/NeoPatternState.cpp b/lib/NeoPattern/NeoPatternState.cpp new file mode 100644 index 0000000..0fa9a6a --- /dev/null +++ b/lib/NeoPattern/NeoPatternState.cpp @@ -0,0 +1,58 @@ +#ifndef __NEOPATTERN_STATE__ +#define __NEOPATTERN_STATE__ + +#include +#include "NeoPattern_api_json.h" +#include "NeoPattern_api_modes.cpp" +#include "utils_print.h" + +// TODO move ARRAY_LENGTH to core lib +#define ARRAY_LENGTH(array) sizeof(array)/sizeof(array[0]) + +struct NeoPatternState { + uint mode; + uint value; + const char* valueStr; + // ------------------------------------------------------------------------------------------ + //Check if given object is valid and contains fields: JSON_MODE_NODE, JSON_VALUE + int verifyJsonObject(JsonObject& json){ + return json.success() + && json.containsKey(JSON_MODE_NODE) + && json.containsKey(JSON_VALUE); + }; + JsonObject& toJsonObject() { + StaticJsonBuffer<200> jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + root["mode"] = mode; + root["value"] = value; + return root; + } + String toJsonString(){ + StaticJsonBuffer<200> jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + root["mode"] = mode; + root["value"] = value; + String jsonString; + root.printTo(jsonString); + return jsonString; + } + // Map a json object to this struct. + void fromJsonObject(JsonObject& json){ + if(!verifyJsonObject(json)){ + PRINT_MSG(Serial, "PatternState.fromJsonObject", "cannot parse JSON"); + return; + } + mode = atoi(json[JSON_MODE_NODE]); + mode = mode < ARRAY_LENGTH(PIXEL_FNCS) ? mode : 0; + value = json[JSON_VALUE]; + valueStr = json[JSON_VALUE]; + }; + // Parse a json string and map parsed object + void fromJsonString(String& str){ + StaticJsonBuffer<200> jsonBuffer; + JsonObject& json = jsonBuffer.parseObject(str); + fromJsonObject(json); + }; +}; + +#endif \ No newline at end of file diff --git a/lib/NeoPattern/NeoPattern_api_json.h b/lib/NeoPattern/NeoPattern_api_json.h new file mode 100644 index 0000000..61c3f70 --- /dev/null +++ b/lib/NeoPattern/NeoPattern_api_json.h @@ -0,0 +1,16 @@ +#ifndef __PIXEL_JSON_API__ +#define __PIXEL_JSON_API__ +/* +modes: PIXELS_OFF = 0, COLOR_WHEEL_MODE = 1, COLOR_MODE = 2, PATTERN_MODE = 3 +patterns: NONE = 0, RAINBOW_CYCLE = 1, THEATER_CHASE = 2, COLOR_WIPE = 3, SCANNER = 4, FADE = 5 +{ + "mode": int, + "value": int || String +} +*/ + +#define JSON_MODE_NODE "mode" +#define JSON_VALUE "value" +#define JSON_ACTION_NODE "action" + +#endif \ No newline at end of file diff --git a/lib/NeoPattern/NeoPattern_api_modes.cpp b/lib/NeoPattern/NeoPattern_api_modes.cpp new file mode 100644 index 0000000..c87a3e6 --- /dev/null +++ b/lib/NeoPattern/NeoPattern_api_modes.cpp @@ -0,0 +1,66 @@ +#ifndef __NEOPATTERN_API_MODES__ +#define __NEOPATTERN_API_MODES__ + +#include "NeoPattern.cpp" + +enum PIXEL_MODE { PIXELS_OFF = 0, COLOR_WHEEL_MODE = 1, COLOR_MODE = 2, PATTERN_MODE = 3}; +typedef void (*PIXEL_FP)(NeoPattern*, const char *); + +/* + Array of function pointers to be used as lookup table using the int values of PIXEL_MODE. + TODO header file + separate functions instead of lambdas +*/ +const PIXEL_FP PIXEL_FNCS[] = { + /* + PIXESL_OFF + Sets all pixels to black. + */ + [](NeoPattern* pixels, const char *color){ + pixels->clear(); + pixels->ColorSet(0); + }, + /* + COLOR_WHEEL_MODE + Input: integer color from 0 to 155 + Uses the color wheel to set a color. + If given integer is <= 1, set the color to black. + By using this function, Color1 and Color2 is set on the pixels; + Color1 = new color, Color2 = last color. + */ + [](NeoPattern* pixels, const char *color){ + int c1 = atoi(color); + int c2 = pixels->Color1; + pixels->Color1 = c1; + pixels->Color2 = c2; + pixels->ActivePattern = NONE; + if(c1 <= 1) { + pixels->ColorSet(0); + return; + } + pixels->ColorSet(pixels->Wheel(c1)); + }, + /* + COLOR_MODE + Input: rgb hex color without # + parses the hex string to r,g,b and sets all pixels accordingly + */ + [](NeoPattern* pixels, const char *color){ + int r, g, b; + sscanf(color, "%02x%02x%02x", &r, &g, &b); + pixels->ColorSet(pixels->Color(r,g,b)); + }, + /* + PATTERN_MODE + Input: id of the pattern + Sets the active pattern on the strip. + As every pattern has another API, all fields need to be set before, for example by using COLOR_WHEEL_MODE. + */ + [](NeoPattern* pixels, const char *id){ + pattern p = (pattern)atoi(id); + pixels->Interval = 50; + pixels->TotalSteps = pixels->numPixels(); + pixels->ActivePattern = p; + } +}; + +#endif \ No newline at end of file diff --git a/lib/sprocket-utils/utils_print.cpp b/lib/sprocket-utils/utils_print.cpp new file mode 100644 index 0000000..20425ee --- /dev/null +++ b/lib/sprocket-utils/utils_print.cpp @@ -0,0 +1,25 @@ +#include "utils_print.h" + +int FORMAT_BUFFER_SIZE(const char* format, ...) { + va_list args; + va_start(args, format); + int result = vsnprintf(NULL, 0, format, args); + va_end(args); + return result + 1; // safe byte for \0 +} +void PRINT_MSG(Print &out, const char* prefix, const char* format, ...) { + if(SPROCKET_PRINT){ + out.print(String(prefix) + String(": ")); + char formatString[128], *ptr; + strncpy_P( formatString, format, sizeof(formatString) ); // copy in from program mem + // null terminate - leave last char since we might need it in worst case for result's \0 + formatString[ sizeof(formatString)-2 ]='\0'; + ptr=&formatString[ strlen(formatString)+1 ]; // our result buffer... + va_list args; + va_start (args,format); + vsnprintf(ptr, sizeof(formatString)-1-strlen(formatString), formatString, args ); + va_end (args); + formatString[ sizeof(formatString)-1 ]='\0'; + out.print(ptr); + } +} \ No newline at end of file diff --git a/lib/sprocket-utils/utils_print.h b/lib/sprocket-utils/utils_print.h new file mode 100644 index 0000000..51ac397 --- /dev/null +++ b/lib/sprocket-utils/utils_print.h @@ -0,0 +1,15 @@ +#ifndef __SPROCKET_UTILS__ +#define __SPROCKET_UTILS__ + +#include + +#ifndef SPROCKET_PRINT +#define SPROCKET_PRINT 1 +#endif + +// TODO move to sprocket + +int FORMAT_BUFFER_SIZE(const char* format, ...); +void PRINT_MSG(Print &out, const char* prefix, const char* format, ...); + +#endif \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 5124119..72ca256 100644 --- a/platformio.ini +++ b/platformio.ini @@ -81,4 +81,19 @@ framework = ${common.framework} lib_deps = ${common.lib_deps} ESP8266mDNS painlessMesh - ArduinoOTA \ No newline at end of file + ArduinoOTA + +[env:meshPixel] +src_filter = +<*> - + +platform = ${common.platform} +board = ${common.board} +upload_speed = ${common.upload_speed} +monitor_baud = ${common.monitor_baud} +framework = ${common.framework} +lib_deps = ${common.lib_deps} + painlessMesh + ESP8266mDNS + ArduinoOTA + ESP Async WebServer + ESPAsyncTCP + Adafruit NeoPixel \ No newline at end of file diff --git a/src/examples/meshPixel/MeshPixel.h b/src/examples/meshPixel/MeshPixel.h new file mode 100644 index 0000000..3d1b1d7 --- /dev/null +++ b/src/examples/meshPixel/MeshPixel.h @@ -0,0 +1,67 @@ +#ifndef __MESH_APP__ +#define __MESH_APP__ + +#include +#include +#include + +#include "config.h" +#include "NeoPattern.cpp" +#include "NeoPatternState.cpp" +#include "NeoPattern_api_json.h" +#include "NeoPattern_api_modes.cpp" +#include "utils_print.h" + +using namespace std; +using namespace std::placeholders; + + +class MeshPixel : public MeshSprocket { + public: + NeoPixelConfig pixelConfig; + NeoPattern* pixels; + NeoPatternState state; + Task animation; + + MeshPixel(SprocketConfig cfg, OtaConfig otaCfg, WebServerConfig webCfg, NeoPixelConfig pixelCfg) : MeshSprocket(cfg, otaCfg, webCfg) { + pixelConfig = pixelCfg; + pixels = new NeoPattern(pixelCfg.length, pixelCfg.pin, NEO_GRB + NEO_KHZ800); + pixels->begin(); + pixels->setBrightness(pixelCfg.brightness); + pixels->Scanner(pixels->Wheel(COLOR_NOT_CONNECTED), pixelCfg.updateInterval); + } + + Sprocket* activate(Scheduler* scheduler, Network* network) { + // call parent method that enables dispatching and plugins + MeshSprocket::activate(scheduler, network); + net->mesh.onNewConnection(bind(&MeshPixel::newConnection,this, _1)); + net->mesh.onChangedConnections(bind(&MeshPixel::connectionChanged,this)); + // pixel task + animation.set(TASK_MILLISECOND * pixelConfig.updateInterval, TASK_FOREVER, bind(&MeshPixel::animate, this, pixels)); + addTask(animation); + return this; + } using MeshSprocket::activate; + + void animate(NeoPattern* pixels){ + pixels->Update(); + } + + void onMessage( uint32_t from, String &msg ) { + PRINT_MSG(Serial, SPROCKET_TYPE, "msg from %u = %s\n", from, msg.c_str()); + state.fromJsonString(msg); + PIXEL_FNCS[state.mode](pixels, state.valueStr); + } + + void newConnection(uint32_t nodeId){ + PRINT_MSG(Serial, SPROCKET_TYPE, "connected to %u", nodeId); + pixels->ActivePattern = NONE; + pixels->ColorSet(pixels->Wheel(COLOR_CONNECTED)); + } + void connectionChanged(){ + if(!net->mesh.getNodeList().size()){ + pixels->Scanner(pixels->Wheel(COLOR_NOT_CONNECTED), pixelConfig.updateInterval); + } + } +}; + +#endif \ No newline at end of file diff --git a/src/examples/meshPixel/README.md b/src/examples/meshPixel/README.md new file mode 100644 index 0000000..53e7306 --- /dev/null +++ b/src/examples/meshPixel/README.md @@ -0,0 +1,7 @@ +# Mesh Sprocket Example + +## OTA +mosquitto_sub -h citadel.lan -p 1883 -v -t '#' + +Enable OTA: +mosquitto_pub -h citadel.lan -p 1883 -t '/down/wirelos/gateway' -m '{"target":"broadcast", "domain": "wirelos", "msg": {"target":"broadcast", "type": 3, msg: "OTA"} }' diff --git a/src/examples/meshPixel/config.h b/src/examples/meshPixel/config.h new file mode 100644 index 0000000..6c27eb9 --- /dev/null +++ b/src/examples/meshPixel/config.h @@ -0,0 +1,43 @@ +#ifndef __MESH_CONFIG__ +#define __MESH_CONFIG__ + +// Scheduler config +#define _TASK_SLEEP_ON_IDLE_RUN +#define _TASK_STD_FUNCTION + +// Chip config +#define SPROCKET_TYPE "MeshPixel" +#define SERIAL_BAUD_RATE 115200 +#define STARTUP_DELAY 3000 + +// Mesh config +#define SPROCKET_MODE 0 +#define WIFI_CHANNEL 11 +#define MESH_PORT 5555 +#define MESH_PREFIX "whateverYouLike" +#define MESH_PASSWORD "somethingSneaky" +#define STATION_SSID "Th1ngs4p" +#define STATION_PASSWORD "th3r31sn0sp00n" +#define HOSTNAME "mesh-node" +#define MESH_DEBUG_TYPES ERROR | STARTUP | CONNECTION +//ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE + +// OTA config +#define OTA_PORT 8266 +#define OTA_PASSWORD "" + +// WebServer +#define WEB_CONTEXT_PATH "/" +#define WEB_DOC_ROOT "/www" +#define WEB_DEFAULT_FILE "index.html" + +// NeoPixel +#define LED_STRIP_PIN D8 +#define LED_STRIP_LENGTH 24 +#define LED_STRIP_BRIGHTNESS 12 +#define LED_STRIP_UPDATE_INTERVAL 100 +#define LED_STRIP_DEFAULT_COLOR 100 +#define COLOR_CONNECTED LED_STRIP_DEFAULT_COLOR +#define COLOR_NOT_CONNECTED 254 + +#endif \ No newline at end of file diff --git a/src/examples/meshPixel/main.cpp b/src/examples/meshPixel/main.cpp new file mode 100644 index 0000000..adaedfa --- /dev/null +++ b/src/examples/meshPixel/main.cpp @@ -0,0 +1,25 @@ +#include "config.h" +#include "MeshNet.h" +#include "MeshPixel.h" + +MeshNet net({ + SPROCKET_MODE, WIFI_CHANNEL, + MESH_PORT, MESH_PREFIX, MESH_PASSWORD, + STATION_SSID, STATION_PASSWORD, HOSTNAME, + MESH_DEBUG_TYPES +}); +MeshPixel sprocket( + { STARTUP_DELAY, SERIAL_BAUD_RATE }, + { OTA_PORT, OTA_PASSWORD }, + { WEB_CONTEXT_PATH, WEB_DOC_ROOT, WEB_DEFAULT_FILE }, + { LED_STRIP_PIN, LED_STRIP_LENGTH, LED_STRIP_BRIGHTNESS, LED_STRIP_UPDATE_INTERVAL, LED_STRIP_DEFAULT_COLOR } +); + +void setup() { + sprocket.join(net); +} + +void loop() { + sprocket.loop(); + yield(); +} \ No newline at end of file