TableTennisDisplay added. Together with new documentation and even more infos.
This commit is contained in:
parent
41b9d2d175
commit
5eb3ab6bc9
30
README.md
30
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)
|
### 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.
|
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.
|
355
cpp-files/tableTennisDisplay.h
Normal file
355
cpp-files/tableTennisDisplay.h
Normal file
@ -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<NeoGrbFeature, NeoEsp8266BitBangWs2812xMethod> 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
51
tableTennisDisplay.yaml
Normal file
51
tableTennisDisplay.yaml
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user