mirror of
https://gitlab.com/zwirbel/illucat.git
synced 2025-12-15 01:42:22 +01:00
Merge branch 'release-1.0' into 'master'
Release 1.0 See merge request 0x1d/illucat!2
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -13,6 +13,8 @@
|
|||||||
"deque": "cpp",
|
"deque": "cpp",
|
||||||
"list": "cpp",
|
"list": "cpp",
|
||||||
"unordered_map": "cpp",
|
"unordered_map": "cpp",
|
||||||
"vector": "cpp"
|
"vector": "cpp",
|
||||||
|
"tuple": "cpp",
|
||||||
|
"utility": "cpp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
46
README.md
46
README.md
@@ -1,43 +1,9 @@
|
|||||||
# Illumination-Cat
|
# Illumination-Cat
|
||||||
This is the brain of the the almighty [Illumination Cat](https://www.thingiverse.com/thing:2974862).
|
This is the brain of the the almighty Illumination-Cat.
|
||||||
|
|
||||||
## API
|
## Resources & Documentation
|
||||||
### WebSocket
|
[3D Model](https://www.thingiverse.com/thing:2974862)
|
||||||
Endpoint: /pixel
|
[Installation](https://gitlab.com/0x1d/illucat/blob/master/installation.md)
|
||||||
|
[API](https://gitlab.com/0x1d/illucat/blob/master/api.md)
|
||||||
|
[OctoPrint Stuff](https://github.com/FrYakaTKoP/simple-octo-ws2812)
|
||||||
|
|
||||||
Fields:
|
|
||||||
|Field|Type|Description|
|
|
||||||
| --- |---| ---|
|
|
||||||
| topic | String | Defines which functionality is executed, usually to set a value or activate a pattern |
|
|
||||||
| payload | String | data to be set |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
``` json
|
|
||||||
{
|
|
||||||
"topic": "pixels/color",
|
|
||||||
"payload": "13505813"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
#### Topics
|
|
||||||
| Topic | Data |
|
|
||||||
| ----- | ---- |
|
|
||||||
| pixels/color | |
|
|
||||||
| pixels/color2 | |
|
|
||||||
| pixels/pattern | |
|
|
||||||
| pixels/totalSteps | |
|
|
||||||
| pixels/brightness | |
|
|
||||||
|
|
||||||
### REST
|
|
||||||
#### Endpoints
|
|
||||||
POST /pixel/state
|
|
||||||
POST /config
|
|
||||||
|
|
||||||
### Mesh
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- Enduser setup: initial setup where the cat opens an access point for configuration
|
|
||||||
- WiFi: connect to existing AP as client or build a mesh network where all cats act as a collective
|
|
||||||
- Web controls: colors and patterns can be changed through the web interface
|
|
||||||
- OTA plugin: cats connected to an AP can be updated over-the-air via TCP flash method
|
|
||||||
- [0%] audio output
|
|
||||||
- [0%] OctoPrint plugin: connect to an OctoPrint instance and reflect print status via colors
|
|
||||||
74
api.md
Normal file
74
api.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# API
|
||||||
|
Two API architectures are supported: WebSocket and REST.
|
||||||
|
Both can be used with the same set of fields.
|
||||||
|
As everything can be controlled with topics, only a few fields are required:
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ----- | ---- | ---- |
|
||||||
|
| topic | String | Topic where the payload is dispatched |
|
||||||
|
| payload | String | Payload to be dispatched |
|
||||||
|
| broadcast | Integer | Where to send the payload; 0 = local, 1 = broadcast |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- topic
|
||||||
|
- payload
|
||||||
|
- broadcast
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
All functionality can be used by sending messages to these topics.
|
||||||
|
|
||||||
|
| topic | type | payload |
|
||||||
|
| ----- | ---- | ---- |
|
||||||
|
| pixels/colorWheel | Integer | Value from 0 to 255 to cycle through all colors |
|
||||||
|
| pixels/color | Integer | RGB color as integer. By calling this topic, all LEDs of the strip are set synchronously, stopping current running animation. |
|
||||||
|
| pixels/color2 | Integer |RGB color as integer. Sets the second color used in animations. Does not stop current running animation. |
|
||||||
|
| pixels/pattern | Integer | Value from 0 to 5 to set the current animation. Available animations: { NONE = 0, RAINBOW_CYCLE = 1, THEATER_CHASE = 2, COLOR_WIPE = 3, SCANNER = 4, FADE = 5 } |
|
||||||
|
| pixels/totalSteps | Integer | Number of steps of an animation. |
|
||||||
|
| pixels/brightness | Integer | Integer from 0 to 255 to set the overall brightness of the strip by bitshifting the current colors in memory. Use with caution as running the LEDs on full brightness requires a lot of power. |
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
Endpoint: /pixel
|
||||||
|
Send a JSON String containing the mandatory fields.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"topic": "pixels/color",
|
||||||
|
"payload": 13505813
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## REST
|
||||||
|
#### Endpoints
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
POST /pixel/api
|
||||||
|
Request: Form post with topic and payload as fields
|
||||||
|
Response: 200 + final payload as JSON
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```shell
|
||||||
|
# set color by wheel
|
||||||
|
curl -v -X POST \
|
||||||
|
--data "topic=pixels/colorWheel" \
|
||||||
|
--data "payload=20" \
|
||||||
|
http://illucat/pixel/api
|
||||||
|
|
||||||
|
# set color only on current node
|
||||||
|
curl -v -X POST \
|
||||||
|
--data "topic=pixels/colorWheel" \
|
||||||
|
--data "payload=20" \
|
||||||
|
--data "broadcast=1"\
|
||||||
|
http://illucat/pixel/api
|
||||||
|
|
||||||
|
# set brightness
|
||||||
|
curl -v -X POST \
|
||||||
|
--data "topic=pixels/brightness" \
|
||||||
|
--data "payload=10" \
|
||||||
|
http://illucat/pixel/api
|
||||||
|
|
||||||
|
# run rainbow pattern
|
||||||
|
curl -v -X POST \
|
||||||
|
--data "topic=pixels/pattern" \
|
||||||
|
--data "payload=1" \
|
||||||
|
http://illucat/pixel/api
|
||||||
|
```
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>ESP Kit</title>
|
<title>Illu-Cat</title>
|
||||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" href="/favicon-32x32.png">
|
||||||
<link rel="stylesheet" type="text/css" href="styles.css">
|
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -21,6 +21,13 @@
|
|||||||
.sui > .content {
|
.sui > .content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
@media screen and (min-width: 968px) {
|
||||||
|
.sui > .content {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
.sui label {
|
.sui label {
|
||||||
color: #b3b2b2;
|
color: #b3b2b2;
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,9 @@ input[type=range]:focus::-ms-fill-lower {
|
|||||||
input[type=range]:focus::-ms-fill-upper {
|
input[type=range]:focus::-ms-fill-upper {
|
||||||
background: #0eb4bb;
|
background: #0eb4bb;
|
||||||
}
|
}
|
||||||
|
.form-row input[type="range"] {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
/* The switch - the box around the slider */
|
/* The switch - the box around the slider */
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -274,13 +284,13 @@ form .form-row input[type="checkbox"] {
|
|||||||
transform: scale(2);
|
transform: scale(2);
|
||||||
}
|
}
|
||||||
.ColorPicker {
|
.ColorPicker {
|
||||||
flex: none !important;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
}
|
}
|
||||||
.sui select {
|
.sui select {
|
||||||
|
flex: 5;
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
color: #eeeeee;
|
color: #eeeeee;
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ void PRINT_MSG(Print &out, const char* prefix, const char* format, ...) {
|
|||||||
vsnprintf(ptr, sizeof(formatString)-1-strlen(formatString), formatString, args );
|
vsnprintf(ptr, sizeof(formatString)-1-strlen(formatString), formatString, args );
|
||||||
va_end (args);
|
va_end (args);
|
||||||
formatString[ sizeof(formatString)-1 ]='\0';
|
formatString[ sizeof(formatString)-1 ]='\0';
|
||||||
out.print(ptr);
|
out.println(ptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,13 @@ lib_deps =
|
|||||||
ESPAsyncTCP
|
ESPAsyncTCP
|
||||||
TaskScheduler
|
TaskScheduler
|
||||||
SPIFFS
|
SPIFFS
|
||||||
|
painlessMesh
|
||||||
|
ESP8266mDNS
|
||||||
|
ArduinoOTA
|
||||||
|
ArduinoJson
|
||||||
|
ESP Async WebServer
|
||||||
|
ESPAsyncTCP
|
||||||
|
Adafruit NeoPixel
|
||||||
|
|
||||||
[env:build]
|
[env:build]
|
||||||
platform = ${common.platform}
|
platform = ${common.platform}
|
||||||
@@ -30,13 +37,17 @@ upload_speed = ${common.upload_speed}
|
|||||||
monitor_baud = ${common.monitor_baud}
|
monitor_baud = ${common.monitor_baud}
|
||||||
framework = ${common.framework}
|
framework = ${common.framework}
|
||||||
build_flags = -Wl,-Teagle.flash.4m1m.ld
|
build_flags = -Wl,-Teagle.flash.4m1m.ld
|
||||||
;lib_extra_dirs = ~/src/illucat/.piolibdeps/sprocket-core/lib
|
-DSPROCKET_PRINT=1
|
||||||
lib_deps = ${common.lib_deps}
|
lib_deps = ${common.lib_deps}
|
||||||
painlessMesh
|
https://gitlab.com/wirelos/sprocket-core.git#develop
|
||||||
ESP8266mDNS
|
|
||||||
ArduinoOTA
|
[env:release]
|
||||||
ArduinoJson
|
platform = ${common.platform}
|
||||||
ESP Async WebServer
|
board = ${common.board}
|
||||||
ESPAsyncTCP
|
upload_speed = ${common.upload_speed}
|
||||||
Adafruit NeoPixel
|
monitor_baud = ${common.monitor_baud}
|
||||||
https://gitlab.com/wirelos/sprocket-core.git#develop
|
framework = ${common.framework}
|
||||||
|
build_flags = -Wl,-Teagle.flash.4m1m.ld
|
||||||
|
-DSPROCKET_PRINT=0
|
||||||
|
lib_deps = ${common.lib_deps}
|
||||||
|
https://gitlab.com/wirelos/sprocket-core.git#master
|
||||||
@@ -55,99 +55,93 @@ class IlluCat : public MeshSprocket {
|
|||||||
pixelConfig.defaultColor = 100;
|
pixelConfig.defaultColor = 100;
|
||||||
|
|
||||||
}
|
}
|
||||||
// TDOO remove
|
|
||||||
virtual void scanningAnimation() {
|
virtual void scanningAnimation() {
|
||||||
pixels->Scanner(pixels->Wheel(COLOR_NOT_CONNECTED), pixelConfig.updateInterval);
|
pixels->Scanner(pixels->Wheel(COLOR_NOT_CONNECTED), pixelConfig.updateInterval);
|
||||||
//pixels->Fade(0, pixels->Color(255,255,255), 4, pixelConfig.updateInterval, FORWARD);
|
|
||||||
}
|
}
|
||||||
virtual void defaultAnimation() {
|
virtual void defaultAnimation() {
|
||||||
String defaultStr = String(defaultState.value);
|
String defaultStr = String(defaultState.value);
|
||||||
PIXEL_FNCS[defaultState.mode](pixels, defaultStr.c_str());
|
PIXEL_FNCS[defaultState.mode](pixels, defaultStr.c_str());
|
||||||
//pixels->RainbowCycle(150);
|
|
||||||
}
|
}
|
||||||
Sprocket* activate(Scheduler* scheduler, Network* network) {
|
Sprocket* activate(Scheduler* scheduler, Network* network) {
|
||||||
|
|
||||||
net = static_cast<MeshNet*>(network);
|
net = static_cast<MeshNet*>(network);
|
||||||
net->mesh.onNewConnection(bind(&IlluCat::onNewConnection,this, _1));
|
|
||||||
//net->mesh.onChangedConnections(bind(&IlluCat::onConnectionChanged,this));
|
|
||||||
|
|
||||||
|
// load config files from SPIFFS
|
||||||
if(SPIFFS.begin()){
|
if(SPIFFS.begin()){
|
||||||
pixelConfig.fromFile("/pixelConfig.json");
|
pixelConfig.fromFile("/pixelConfig.json");
|
||||||
defaultState.fromFile("/pixelState.json");
|
defaultState.fromFile("/pixelState.json");
|
||||||
state = defaultState;
|
state = defaultState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialize services
|
||||||
pixels = new NeoPattern(pixelConfig.length, pixelConfig.pin, NEO_GRB + NEO_KHZ800);
|
pixels = new NeoPattern(pixelConfig.length, pixelConfig.pin, NEO_GRB + NEO_KHZ800);
|
||||||
server = new AsyncWebServer(80);
|
server = new AsyncWebServer(80);
|
||||||
ws = new AsyncWebSocket("/pixel");
|
ws = new AsyncWebSocket("/pixel");
|
||||||
dnsServer = new DNSServer();
|
dnsServer = new DNSServer();
|
||||||
|
|
||||||
|
// add plugins
|
||||||
addPlugin(new OtaTcpPlugin(otaConfig));
|
addPlugin(new OtaTcpPlugin(otaConfig));
|
||||||
addPlugin(new WebServerPlugin(webConfig, server));
|
addPlugin(new WebServerPlugin(webConfig, server));
|
||||||
addPlugin(new WebConfigPlugin(server));
|
addPlugin(new WebConfigPlugin(server));
|
||||||
addPlugin(new PixelPlugin(pixelConfig, pixels));
|
addPlugin(new PixelPlugin(pixelConfig, pixels));
|
||||||
|
|
||||||
|
|
||||||
// TODO plugin?
|
|
||||||
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
|
||||||
dnsServer->start(DNS_PORT, "*", WiFi.softAPIP());
|
|
||||||
Serial.println("SoftAP IP: " + WiFi.softAPIP().toString());
|
|
||||||
|
|
||||||
defaultAnimation();
|
defaultAnimation();
|
||||||
|
|
||||||
// TODO plugin
|
// configure DNS
|
||||||
|
// plugin?
|
||||||
|
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
|
dnsServer->start(DNS_PORT, "*", WiFi.softAPIP());
|
||||||
|
String softApPrt = "SoftAP IP: " + WiFi.softAPIP().toString();
|
||||||
|
PRINT_MSG(Serial, SPROCKET_TYPE, softApPrt.c_str());
|
||||||
|
|
||||||
|
// TODO move to plugin
|
||||||
// setup web stuff
|
// setup web stuff
|
||||||
server->serveStatic("/pixelConfig.json", SPIFFS, "pixelConfig.json");
|
server->serveStatic("/pixelConfig.json", SPIFFS, "pixelConfig.json");
|
||||||
server->on("/pixel/pattern", HTTP_POST, bind(&IlluCat::patternWebRequestHandler, this, _1));
|
server->on("/pixel/api", HTTP_POST, bind(&IlluCat::patternWebRequestHandler, this, _1));
|
||||||
ws->onEvent(bind(&IlluCat::onWsEvent, this, _1, _2, _3, _4, _5, _6));
|
ws->onEvent(bind(&IlluCat::onWsEvent, this, _1, _2, _3, _4, _5, _6));
|
||||||
server->addHandler(ws);
|
server->addHandler(ws);
|
||||||
|
|
||||||
// FIXME OnDisable is triggered after last scan, aprx. 10 sec
|
|
||||||
// FIXME chck if this is also triggered when new connection arrives
|
|
||||||
//net->mesh.stationScan.task.setOnDisable(bind(&IlluCat::defaultAnimation,this));
|
|
||||||
return MeshSprocket::activate(scheduler, network);
|
return MeshSprocket::activate(scheduler, network);
|
||||||
} using MeshSprocket::activate;
|
} using MeshSprocket::activate;
|
||||||
|
|
||||||
void patternWebRequestHandler(AsyncWebServerRequest *request) {
|
// TODO move to utils
|
||||||
Serial.println("POST /pixel/state");
|
String getRequestParameterOrDefault(AsyncWebServerRequest *request, String param, String defaultValue, bool isPost = true){
|
||||||
if(request->hasParam("state", true)) {
|
if(request->hasParam(param, isPost)) {
|
||||||
String inStr = request->getParam("state", true)->value();
|
return request->getParam(param, isPost)->value();
|
||||||
dispatch(0, inStr);
|
|
||||||
}
|
}
|
||||||
request->send(200, "text/plain", "OK");
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void patternWebRequestHandler(AsyncWebServerRequest *request) {
|
||||||
|
PRINT_MSG(Serial, SPROCKET_TYPE, "POST /pixel/api");
|
||||||
|
currentMessage.topic = getRequestParameterOrDefault(request, "topic", "");
|
||||||
|
currentMessage.payload = getRequestParameterOrDefault(request, "payload", "");
|
||||||
|
currentMessage.broadcast = atoi(getRequestParameterOrDefault(request, "broadcast", "0").c_str());
|
||||||
|
String msg = currentMessage.toJsonString();
|
||||||
|
publish(currentMessage.topic, currentMessage.payload);
|
||||||
|
if(currentMessage.broadcast){
|
||||||
|
net->mesh.sendBroadcast(msg);
|
||||||
|
}
|
||||||
|
request->send(200, "text/plain", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
|
virtual void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
|
||||||
if(type == WS_EVT_DATA){
|
if(type == WS_EVT_DATA){
|
||||||
String frame = WsUtils::parseFrameAsString(type, arg, data, len, 0);
|
String frame = WsUtils::parseFrameAsString(type, arg, data, len, 0);
|
||||||
net->mesh.sendBroadcast(frame);
|
|
||||||
dispatch(0, frame);
|
dispatch(0, frame);
|
||||||
client->text(String(millis()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void dispatch( uint32_t from, String &msg ) {
|
virtual void dispatch( uint32_t from, String &msg ) {
|
||||||
//Serial.println(msg);
|
|
||||||
currentMessage.fromJsonString(msg);
|
currentMessage.fromJsonString(msg);
|
||||||
if(currentMessage.valid){
|
if(currentMessage.valid){
|
||||||
currentMessage.from = from;
|
currentMessage.from = from;
|
||||||
publish(currentMessage.topic, currentMessage.payload);
|
publish(currentMessage.topic, currentMessage.payload);
|
||||||
|
if(currentMessage.broadcast){
|
||||||
|
net->mesh.sendBroadcast(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void onNewConnection(uint32_t nodeId){
|
|
||||||
PRINT_MSG(Serial, SPROCKET_TYPE, "connected to %u", nodeId);
|
|
||||||
// publish current state to new node
|
|
||||||
// TODO broadcast enable/disable flag
|
|
||||||
//String stateJson = currentMessage.toJsonString();
|
|
||||||
//net->mesh.sendSingle(nodeId, stateJson);
|
|
||||||
}
|
|
||||||
//virtual void onConnectionChanged(){
|
|
||||||
// PRINT_MSG(Serial, SPROCKET_TYPE, "connection changed");
|
|
||||||
// //if(!net->mesh.getNodeList().size()){
|
|
||||||
// // defaultAnimation();
|
|
||||||
// //}
|
|
||||||
//}
|
|
||||||
|
|
||||||
void loop(){
|
void loop(){
|
||||||
MeshSprocket::loop();
|
MeshSprocket::loop();
|
||||||
dnsServer->processNextRequest();
|
dnsServer->processNextRequest();
|
||||||
|
|||||||
@@ -38,11 +38,17 @@ class PixelPlugin : public Plugin {
|
|||||||
animation.set(TASK_MILLISECOND * pixelConfig.updateInterval, TASK_FOREVER, bind(&PixelPlugin::animate, this));
|
animation.set(TASK_MILLISECOND * pixelConfig.updateInterval, TASK_FOREVER, bind(&PixelPlugin::animate, this));
|
||||||
userScheduler->addTask(animation);
|
userScheduler->addTask(animation);
|
||||||
animation.enable();
|
animation.enable();
|
||||||
Serial.println("NeoPixels activated");
|
PRINT_MSG(Serial, SPROCKET_TYPE, "NeoPixels activated");
|
||||||
}
|
}
|
||||||
// TODO set the whole pixel state
|
void setState(String msg) {
|
||||||
void setState(String msg){
|
|
||||||
state.fromJsonString(msg);
|
state.fromJsonString(msg);
|
||||||
|
pixels->setBrightness(state.brightness);
|
||||||
|
pixels->ColorSet(state.color);
|
||||||
|
pixels->Index = 0;
|
||||||
|
pixels->Color1 = state.color;
|
||||||
|
pixels->Color2 = state.color2;
|
||||||
|
pixels->TotalSteps = state.totalSteps;
|
||||||
|
pixels->ActivePattern = (pattern) state.pattern;
|
||||||
}
|
}
|
||||||
void colorWheel(String msg){
|
void colorWheel(String msg){
|
||||||
int color = atoi(msg.c_str());
|
int color = atoi(msg.c_str());
|
||||||
|
|||||||
Reference in New Issue
Block a user