From 5eb3ab6bc949a49e895fd3e3a9e18d2e4c7aad15 Mon Sep 17 00:00:00 2001 From: NBSgamesAT Date: Wed, 25 May 2022 10:58:02 +0200 Subject: [PATCH] TableTennisDisplay added. Together with new documentation and even more infos. --- README.md | 30 ++- cpp-files/tableTennisDisplay.h | 355 +++++++++++++++++++++++++++++++++ tableTennisDisplay.yaml | 51 +++++ 3 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 cpp-files/tableTennisDisplay.h create mode 100644 tableTennisDisplay.yaml diff --git a/README.md b/README.md index 9672122..ea7551b 100644 --- a/README.md +++ b/README.md @@ -37,4 +37,32 @@ Topic `tabletenniscounter/control/score/count` will be send with the value of ei ### Undoing last points (Up to 10 points) Topic `tabletenniscounter/control/score/undo` will be sent with no payload to worry about. The counting part on the respberry pi is supposed to keep a list of the last 10 points. -More infos are added soon! +## Table tennis LED display +### Overall +This is probably the more exciting bit about this. It's the part that actually displays the numbers on the display. The part, that tries to displays all the numbers in a way people can use it to read their current score. + +We have a LOT of stuff of stuff to discuss here. + +First of all: It does take advantage of custom components. CustomMQTTDevices to be precise. + +Second of all: It uses the NeoPixelBus Library to communicate with the ledstrip. + +Third of all: It does also use the esphome native api. + +And a disclaimer as well: The native api part was the latest thing added and is by no means ready to be used. In fact it is only used right now to publish the current score to home assistent but nothing else as it cannot display the numbers sent to the esphome over the native api. This feature is planned but not yet implemented tho. + +For the MQTT part we have only one topic to worry about. + +### MQTT JSON message +MQTT topic `tabletenniscounter/display/number/command` is used to send the score that should be display encoded as a JSON message. So let's go over the different key/values attributes. + +- state => "ON"/"OFF": This must always be included and must be ON if any other fuctionallity is intended to be used as OFF is going to turn the ledstrip OFF no matter what +- player1 => Number 0 - 99: This tells the display the score of player red (which is also the left number). This MUST be used together with player2 and player1Start/displayGreen. +- player2 => Number 0 - 99: This tells the display the score of player blue (which is also the right number). This MUST be used together with player1 and player1Start/displayGreen. +- player1Start => true/false: This if this is used to display which player is supposed to start. If the value is true, the colon between the two numbers is displayed as red, if it's false, the colon is blue. This attribute can be replaced with displayGreen. +- displayGreen: The value for this does not matter. If it's there, the colon is green. +- player1Winner => true/false (OPTIONAL): Set this to true, and player 1 (red) will be displayed as the winner by displaying the red number as yellow. If it's false, player 2 (blue) will be displayed as the winner. Omit this attribute if no one should be displayed at the winner. + +Should state ON be sent without any other attribute, the nbs logo is displayed with a little minus at the end for no particular reason. + +More infos are coming soon. \ No newline at end of file diff --git a/cpp-files/tableTennisDisplay.h b/cpp-files/tableTennisDisplay.h new file mode 100644 index 0000000..45c2a57 --- /dev/null +++ b/cpp-files/tableTennisDisplay.h @@ -0,0 +1,355 @@ +#include "esphome.h" +#include "NeoPixelBus.h" +//#include "multi_effect_handler.h" +#define SEGMENT_LENGTH 5 + +class NbsEffect { + private: + int goalRed = 0, goalGreen = 0, goalBlue = 0; + double currentRed = 0, currentGreen = 0, currentBlue = 0; + double calcRed = 0, calcGreen = 0, calcBlue = 0; + int stepsPerSecond = 0; + int totalSteps = 0; + int currentStep = 0; + bool finalStep = true; + + public: + NbsEffect(int stepsPerSecond_){ + stepsPerSecond = stepsPerSecond_; + } + + void setGoalColorAsRgb(int red, int green, int blue, int timeInMilliseconds){ + + if(goalRed == red && goalGreen == green && goalBlue == blue){ + return; + } + + finalStep = false; + + currentRed = (double) goalRed; + currentGreen = (double) goalGreen; + currentBlue = (double) goalBlue; + + goalRed = red; + goalGreen = green; + goalBlue = blue; + + double timeInSeconds = (double) timeInMilliseconds / 1000.0; + totalSteps = (int) (timeInSeconds * (double) stepsPerSecond); + currentStep = totalSteps; + + calcRed = ((double) goalRed - currentRed) / totalSteps; + calcGreen = ((double) goalGreen - currentGreen) / totalSteps; + calcBlue = ((double) goalBlue - currentBlue) / totalSteps; + + currentStep = 0; + } + + int debugGoalRed(){ + return goalRed; + } + double debugCurrentRed(){ + return currentRed; + } + double debugCalcRed(){ + return calcRed; + } + + bool step(){ + if(currentStep >= totalSteps){ + return false; + } + ++currentStep; + if(currentStep == totalSteps){ + currentRed = (double) goalRed; + currentGreen = (double) goalGreen; + currentBlue = (double) goalBlue; + finalStep = true; + return true; + } + currentRed += calcRed; + currentGreen += calcGreen; + currentBlue += calcBlue; + return true; + } + + bool wasFinalStep(){ + return finalStep; + } + + int getCurrentRed(){ + return (int) currentRed; + } + int getCurrentGreen(){ + return (int) currentGreen; + } + int getCurrentBlue(){ + return (int) currentBlue; + } +}; + +NeoPixelBus nbsStrip(143, D5); +class CustomNumberDisplayComponent : public Component, public CustomMQTTDevice { +const int middleSegment = SEGMENT_LENGTH * 14; + + +const int transitionLength = 500; // milliseconds +const int middleLength = 2; +const double brightness = 0.17; + +NbsEffect *colorAnimation[4][7]; +NbsEffect *middleSegmentAnim; + +unsigned long lastUpdate = 0; +bool shouldShowNBS = true; + +bool numbers[14][7] = { + {0, 1, 1, 1, 1, 1, 1}, // 0 + {0, 1, 0, 0, 0, 0, 1}, // 1 + {1, 1, 1, 0, 1, 1, 0}, // 2 + {1, 1, 1, 0, 0, 1, 1}, // 3 + {1, 1, 0, 1, 0, 0, 1}, // 4 + {1, 0, 1, 1, 0, 1, 1}, // 5 + {1, 0, 1, 1, 1, 1, 1}, // 6 + {0, 1, 1, 0, 0, 0, 1}, // 7 + {1, 1, 1, 1, 1, 1, 1}, // 8 + {1, 1, 1, 1, 0, 1, 1}, // 9 + {0, 0, 0, 0, 0, 0, 0}, // No Display (10) + {1, 0, 0, 0, 0, 0, 0}, // - (11) + {1, 0, 0, 0, 1, 0, 1}, // n (12) + {1, 0, 0, 1, 1, 1, 1} // b (13) +}; + + public: + void setup() override { + + // Segment mapping + int segPixelCounter = 0; + for(int i = 0; i < 4; ++i){ + for(int j = 0; j < 7; ++j){ + colorAnimation[i][j] = new NbsEffect(40); + } + } + middleSegmentAnim = new NbsEffect(40); + + nbsStrip.Begin(); + setNumber(0, 12, 1.0, 0.0, 0.0); + setNumber(1, 13, 0.0, 1.0, 0.0); + setNumber(2, 5, 0.0, 0.0, 1.0); + setNumber(3, 11, 0.5, 0.5, 0.5); + for(int led = middleSegment; led < middleSegment + 2; ++led){ + nbsStrip.SetPixelColor(led, RgbColor(0, 0, 0)); + } + nbsStrip.Show(); + lastUpdate = millis(); + + // also supports JSON messages + subscribe_json("tabletenniscounter/display/number/command", &CustomNumberDisplayComponent::on_json_message, 2); + + + } + + void on_json_message(const std::string &topic, JsonObject root) { + //checker = 0; + if(!root.containsKey("state")){ + return; + } + if(root["state"] != "ON"){ + setNumber(0, 10); + setNumber(1, 10); + setNumber(2, 10); + setNumber(3, 10); + turnSegmentOff(middleSegmentAnim); + publish_json("tabletenniscounter/display/number/state", [=](JsonObject root2) { + root2["state"] = "OFF"; + }, 2, false); + return; + } + if(shouldShowNBS){ + shouldShowNBS = false; + lastUpdate = millis(); + } + if (!root.containsKey("player1") || !root.containsKey("player2") || !(root.containsKey("player1Start") || root.containsKey("displayGreen"))){ + setNumber(0, 12, 1.0, 0.0, 0.0); + setNumber(1, 13, 0.0, 1.0, 0.0); + setNumber(2, 5, 0.0, 0.0, 1.0); + setNumber(3, 11, 0.5, 0.5, 0.5); + turnSegmentOff(middleSegmentAnim); + publish_json("tabletenniscounter/display/number/state", [=](JsonObject root2) { + root2["state"] = "ON"; + }, 2, false); + return; + } + int player1 = root["player1"]; + int player2 = root["player2"]; + + id(red_num).set(player1); + id(blue_num).set(player2); + bool player1Start = root["player1Start"]; + // do something with Json Object + if(player1 > 99) { player1 = 99; } + else if(player1 < 0) { player1 = 0; } + if(player2 > 99) { player2 = 99; } + else if(player2 < 0) { player2 = 0; } + + int player1T = player1 / 10; + int player1O = player1 % 10; + int player2T = player2 / 10; + int player2O = player2 % 10; + + int handleColorWinChange = 0; + if(root.containsKey("player1Winner")){ + if(root["player1Winner"]){ + handleColorWinChange = 1; + } + else{ + handleColorWinChange = 2; + } + } + + if(player1T == 0) + setNumber(0, 10); + else{ + if(handleColorWinChange == 1){ + setNumber(0, player1T, 1.0, 1.0, 0.0); + }else{ + setNumber(0, player1T); + } + } + if(handleColorWinChange == 1){ + setNumber(1, player1O, 1.0, 1.0, 0.0); + }else{ + setNumber(1, player1O); + } + if(player2T == 0){ + if(handleColorWinChange == 2){ + setNumber(2, player2O, 1.0, 1.0, 0.0); + }else{ + setNumber(2, player2O); + } + setNumber(3, 10); + } + else{ + if(handleColorWinChange == 2){ + setNumber(2, player2T, 1.0, 1.0, 0.0); + setNumber(3, player2O, 1.0, 1.0, 0.0); + }else{ + setNumber(2, player2T); + setNumber(3, player2O); + } + } + + if(root.containsKey("displayGreen")){ + turnSegmentOn(middleSegmentAnim, brightness, 0.0, 1.0, 0.0); + } + else{ + if(player1Start){ + turnSegmentOn(middleSegmentAnim, brightness, 1.0, 0.0, 0.0); + } + else{ + turnSegmentOn(middleSegmentAnim, brightness, 0.0, 0.0, 1.0); + } + } + + // publish JSON using lambda syntax + publish_json("tabletenniscounter/display/number/state", [=](JsonObject root2) { + root2["state"] = "ON"; + root2["player1"] = player1; + root2["player2"] = player2; + root2["player1Start"] = player1Start; + }, 2, false); + } + + void loop() override{ + + if(lastUpdate + 25 >= millis()){ + return; + } + if(lastUpdate + 50 < millis()){ + //ESP_LOGW("custom_display", "LAGWARN, lastUpdate timer was reseted!"); + lastUpdate = millis(); + } + + lastUpdate += 25; + + bool didUpdate = false; + for(int i = 0; i < 4; ++i){ + for(int j = 0; j < 7; ++j){ + NbsEffect *effect = colorAnimation[i][j]; + int segStart = calculateSegStart(i, j); + if(effect->step()){ + didUpdate = true; + for(int led = segStart; led < segStart + SEGMENT_LENGTH; ++led){ + nbsStrip.SetPixelColor(led, RgbColor(effect->getCurrentRed(), effect->getCurrentGreen(), effect->getCurrentBlue())); + } + } + } + } + if(colorAnimation[0][0]->wasFinalStep() && shouldShowNBS){ + shouldShowNBS = false; + lastUpdate = millis() + 5000; + setNumber(0, 10, 0.0, 0.0, 0.0); + setNumber(1, 10, 0.0, 0.0, 0.0); + setNumber(2, 10, 0.0, 0.0, 0.0); + setNumber(3, 10, 0.0, 0.0, 0.0); + } + if(middleSegmentAnim->step()){ + didUpdate = true; + //ESP_LOGD("custom", "#%d, Color Code: %d, %d, %d", checker++, middleSegmentAnim->getCurrentRed(), middleSegmentAnim->getCurrentGreen(), middleSegmentAnim->getCurrentBlue()); + for(int led = middleSegment; led < middleSegment + 2; ++led){ + nbsStrip.SetPixelColor(led, RgbColor(middleSegmentAnim->getCurrentRed(), middleSegmentAnim->getCurrentGreen(), middleSegmentAnim->getCurrentBlue())); + } + } + if(didUpdate){ + nbsStrip.Show(); + } + } + + int calculateSegStart(int digit, int segment){ + int start = (digit * SEGMENT_LENGTH * 7) + (segment * SEGMENT_LENGTH); + if(digit >= 2){ + start += 2; + } + return start; + } + + void turnSegmentOn(NbsEffect* segment, double brightness, double red, double green, double blue){ + segment->setGoalColorAsRgb(makeColorAsInt(brightness, red, 220), makeColorAsInt(brightness, green, 170), makeColorAsInt(brightness, blue, 235), 500); + } + void turnSegmentOff(NbsEffect* segment){ + segment->setGoalColorAsRgb(0, 0, 0, 500); + } + void setNumber(int digit, int number, double red, double green, double blue){ + if(digit < 0 || digit > 3){ + return; + } + for(int i = 0; i < 7; ++i){ + if(numbers[number][i]){ + turnSegmentOn(colorAnimation[digit][i], brightness, red, green, blue); + } + else{ + turnSegmentOff(colorAnimation[digit][i]); + } + } + } + void setNumber(int digit, int number){ + if(digit < 0 || digit > 3){ + return; + } + double red = 0; + double blue = 0; + if(digit == 0 || digit == 1){ + red = 1.0; + blue = 0.0; + } + else { + red = 0.0; + blue = 1.0; + } + setNumber(digit, number, red, 0.0, blue); + } + int makeColorAsInt(double brightness, double value, double colorMax){ + return (int) (colorMax * value * brightness); + } +}; + diff --git a/tableTennisDisplay.yaml b/tableTennisDisplay.yaml new file mode 100644 index 0000000..7095132 --- /dev/null +++ b/tableTennisDisplay.yaml @@ -0,0 +1,51 @@ +esphome: + name: tabletenniscounter + platform: ESP8266 + board: nodemcuv2 + includes: + - cpp-files/tableTennisDisplay.h + libraries: + - "makuna/NeoPixelBus" + +custom_component: +- lambda: |- + auto customNumberDisplay = new CustomNumberDisplayComponent(); + return {customNumberDisplay}; + +# Enable logging +logger: + +ota: + password: !secret ttdpassword + +api: + password: !secret ttdpassword + encryption: + key: !secret ttdkey + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + fast_connect: true + +number: + - platform: template + name: "Red Number" + id: red_num + optimistic: true + min_value: 0 + max_value: 99 + step: 1 + - platform: template + name: "Blue Number" + id: blue_num + optimistic: true + min_value: 0 + max_value: 99 + step: 1 + +mqtt: + broker: !secret mqtt_broker_1 + client_id: "tableTennisCounter" + username: !secret mqtt_broker_1_username + password: !secret mqtt_broker_1_password \ No newline at end of file