Project showcase

I Let Everyone On The Internet Control My X-Mas Decoration © CC BY

Control any of the 55 RGB LEDs on the tree, let Santa twerk for you, control a model railroad, or display custom text on an LED dot matrix.

  • 4,981 views
  • 7 comments
  • 38 respects

Components and supplies

WS2812 RGB LED
Or compatible types. We used P9823's.
×1
Ardgen mega
Arduino Mega 2560 & Genuino Mega 2560
×1
J9wz3weildga2z9yc6xb
Arduino Mega Proto Shield
or similar proto shield.
×1
Pi 3 02
Raspberry Pi 3 Model B
×1
12009 06
SparkFun Level Shifter Board
or compatible for shifting 5V Arduino level to 3.3V Pi level
×1
LED dot matrix with MAX7219 control IC
×8
Ard nano
Arduino Nano R3
×1
Abx00004 iso both
Arduino MKR1000
×1
8 channel relay board
currently we're only using 4 channels
×1
5V, 5A (switching) power supply
×1

Necessary tools and machines

09507 01
Soldering iron (generic)
Drill
Jigsaw

Apps and online services

About this project

Have you always wanted anyone in the world to be able to control your X-mas decoration in your living room? - No? - We neither, but we built it anyway.

We proudly present: www.controlmyxmastree.com

The idea

I had the idea quite a while ago, since I always loved projects where you could control somebody's X-mas lighting, a robot arm or generally anything over the web and watching it live on stream. But I was never really sure, if there's anyone else in the world who would dig it and I had no idea how to get requests from anyone on the internet to my Arduino.

Then one day - a few weeks ago - I told a friend what I had in mind and surprisingly he was immediately into it. He's a computer scientist and said he could program an api on node.js, which would run on a raspberry pi. The concept was born.

Diagram

I made a quick, professionell and definitely not shitty looking diagram to describe our basic setup:

To sum it up briefly:

We made a simple webpage, which uses ajax to send http POST requests to our api.

The api is a node application which runs on a Raspberry Pi with a fix and from the web available IP. It sends two bytes over a serial port to an Arduino Mega with a custom shield (Why mega? I had one laying around and its multiple serial ports come in very handy for debugging). The first byte we call: "status byte" the second one: "data byte" (in dependence on Midi).

The Mega then runs code which interprets the serial data and controls all the actuators plus LEDs (P9823 / WS2812).

We also implemented a little hidden switch behind the tree, which counts the laps of the train to display the covered distance on the website later on. Therefore the Arduino sends the current lap count back to the Pi via uart.

There's also one digital output pin from the Arduino connected to a digital input of the Pi, to tell it whether the Mega is ready to receive data or not.

LED dot matrix and TelegramBot

The LED dot matrix display is generally one completely separate system. It utilizes an Arduino MKR1000 for connecting to the internet, which acts as the Telegram bot (see Telegram bot api documentation and Telegram Bot library) and also gets other data from the web (eg. weather, time, ...). The Telegram bot enables people online to send text to the display or to send their current location to show local weather conditions. Why Telegram? - We didn't want to put a simple text input to the webpage, so we could keep away 'spam bots' and as many 'trolls' as possible. And also we simply like Telegram and wanted to try it. ;)

The MKR1000 then connects via uart to an Arduino Nano, which controls the dot matrix (md_parola library).

A few things..

The only things missing are: a power supply, some relays connected to mains voltage with four outlets and setting up the live stream.

Conclusion

I don't know whether I should have gone more into detail, but for now I think it's sufficient. If you want to know more about it, just leave a comment below. We've made some videos in the process, so if there's any interest we'd love to share them.

Speaking of sharing: We definitely want to share all our code, protocols and diagrams, but we hadn't the time to put them all together yet. We were kind of in a rush the last days building it (TIME MANAGEMENT!), so the code is ugly and incomprehensible. But since the project is quite limited to this time of the year, we wanted to post it as soon as possible. We're really ashamed of the dirty code, but we'll fix it soon and edit it in here. Till' then: We want to see Santa twerk.

I hope that we could bring some delight to you, your family and/or friends with our little project. It was a joy building and we're definitely not done yet. We had planed a lot more: more actuators, camera angles and statistics on the webpage. But we'll add them shortly.. so stay tuned. ;)

P.S. Excuse my bad english and supposably many mistakes. Kind regards from Germany. ;)

Code

Serial_Data_Read.inoArduino
Code for the "main" Arduino Mega. Reads data from pi and controls all LEDs and devices.
#include "FastLED.h"
#include "leds_lookup_table.h"
#include <EEPROM.h>

#define DEBUG 0

#define READY_PIN 28
#define SANTA_PIN 5
#define TRAIN_PIN 39  //relay 3
#define LIGHT_CHAIN_PIN 35  //relay 1
#define SPOT_LIGHT_PIN 37
#define FRONT_LIGHT_PIN 41
#define LAP_COUNT_PIN 6


#define NUM_LEDS 55    //number of total P9823's 
#define DATA_PIN 2
#define FADE_DELAY 50
#define FADE_TIME 2000
#define CHANCE_SANTA_SOLO 20 //chance of a santa solo in percent

#define PiSerial Serial1
#define DebugSerial Serial

//status bytes knnen werte von 1 bis 191 haben (0 = undefined)
#define LED_START 1
#define LED_END 55
#define TEXT_EINGABE 124
#define TWERKING_SANTA 69
#define TRAIN 56
#define LIGHT_CHAIN 57
#define SPARKLING 58
#define ANIMATIONS 59
#define TREE_BLACKOUT 60

//info/data bytes von 192 bis 254
#define OFF 192
#define ON 193
#define FARBEN_START 194
#define FARBEN_END 210

#define DATA_ERROR 255

struct ledStepStruct {
  int rStep, gStep, bStep;
  boolean fade;
};

byte data[2], rndLED, hueTemp;
char text[31];
boolean newText, santaFlag, trainFlag, lightChainFlag, sparklingFlag, sparkleOn, santaSoloFlag, pinActive = true;
const unsigned int fadeSteps = FADE_TIME / FADE_DELAY;
unsigned long currentMillis, lastFadeTime, santaTime, trainTime, lightChainTime, sparklingTime, lastSparkleTime, lastLapCount, currentTime, trainActionTime, lastTrainAction;
unsigned int lapCounter;

CRGB leds[NUM_LEDS], ledsAim[NUM_LEDS], ledBefore;

ledStepStruct ledsStep[NUM_LEDS];

void setup() {
  PiSerial.begin(9600);

  DebugSerial.begin(9600);

  pinMode(READY_PIN, OUTPUT);
  pinMode(SANTA_PIN, OUTPUT);
  pinMode(TRAIN_PIN, OUTPUT);
  pinMode(LIGHT_CHAIN_PIN, OUTPUT);
  pinMode(SPOT_LIGHT_PIN, OUTPUT);
  pinMode(FRONT_LIGHT_PIN, OUTPUT);
  pinMode(LAP_COUNT_PIN, INPUT_PULLUP);


  FastLED.addLeds<WS2812B, DATA_PIN, RGB>(leds, NUM_LEDS);
  FastLED.setBrightness(70);
  FastLED.clear();
  FastLED.show();

  digitalWrite(READY_PIN, HIGH);
  digitalWrite(SANTA_PIN, LOW);
  digitalWrite(TRAIN_PIN, LOW);
  digitalWrite(LIGHT_CHAIN_PIN, HIGH);
  digitalWrite(SPOT_LIGHT_PIN, LOW);
  digitalWrite(FRONT_LIGHT_PIN, HIGH);

  lightChainFlag = true;

  //lapCounter = 200; //add counts up to now, since eeprom wasnt' implemented yet
  //EEPROM.put(0, lapCounter);
  EEPROM.get(0, lapCounter);

}

void loop() {

  handleSerialCommunication();
  handleData();
  handleColorFade();
  handleControl();
  handleSparkling();
  handleLapCount();

}

void handleLapCount() {
  currentTime = millis();
  if (!digitalRead(LAP_COUNT_PIN) && pinActive && currentTime - trainActionTime > 100) {
    lapCounter++;
    pinActive = false;
    lastLapCount = currentTime;
    PiSerial.print(lapCounter);
    PiSerial.flush();
    EEPROM.put(0, lapCounter);
    //Serial.println(lapCounter);
  }

  if (digitalRead(LAP_COUNT_PIN) && currentTime - lastLapCount > 2000) {
    pinActive = true;
  }
}

void handleSparkling() {
  if (sparklingFlag) {
    if (millis() - sparklingTime > 20000) {
      sparklingFlag = false;
      leds[rndLED] = ledBefore;
      FastLED.show();
      sparkleOn = false;
    }
    else if (!sparkleOn && millis() - lastSparkleTime > 100) {
      rndLED = random8(NUM_LEDS);
      ledBefore = leds[rndLED];
      leds[rndLED] += CRGB::White;
      FastLED.show();
      sparkleOn = true;
      lastSparkleTime = millis();
    }
    else if (sparkleOn && millis() - lastSparkleTime > 50) {
      leds[rndLED] = ledBefore;
      FastLED.show();
      sparkleOn = false;
      lastSparkleTime = millis();
    }
  }
}

void handleControl() {
  if (santaFlag) {
    if (santaTime == 0) {
      digitalWrite(SANTA_PIN, HIGH);
      santaTime = millis();
    }
    else if (millis() - santaTime > 20000) {
      digitalWrite(SANTA_PIN, LOW);
      santaTime = 0;
      santaFlag = false;
    }
  }

  else if (santaSoloFlag) {

    if (trainFlag) {
      trainFlag = false;
      digitalWrite(TRAIN_PIN, LOW);
    }
    if (lightChainFlag) {
      digitalWrite(LIGHT_CHAIN_PIN, LOW);
    }
    FastLED.clear();
    FastLED.show();

    digitalWrite(FRONT_LIGHT_PIN, LOW);
    FastLED.delay(1000);
    digitalWrite(SPOT_LIGHT_PIN, HIGH);
    FastLED.delay(500);
    digitalWrite(SANTA_PIN, HIGH);
    FastLED.delay(50);
    digitalWrite(SANTA_PIN, LOW);

    /*
        for (int i; i < 500; i++) {
          fill_rainbow(leds, NUM_LEDS, hueTemp, 1);
          FastLED.show();
          hueTemp++;
          FastLED.delay(25);
        }
        FastLED.clear();
        FastLED.show();
    */
    for ( int k = 0; k < 20; k++ ) {
      triangle();
    }


    digitalWrite(SPOT_LIGHT_PIN, LOW);

    digitalWrite(SANTA_PIN, HIGH);
    FastLED.delay(50);
    digitalWrite(SANTA_PIN, LOW);

    FastLED.delay(1000);

    digitalWrite(FRONT_LIGHT_PIN, HIGH);
    digitalWrite(LIGHT_CHAIN_PIN, HIGH);
    lightChainFlag = true;

    for (byte i = 0; i < NUM_LEDS; i++) {
      leds[i] = ledsAim[i];
    }
    FastLED.show();
    santaSoloFlag = false;
    digitalWrite(READY_PIN, HIGH);
  }

  if (trainFlag) {
    if (millis() - trainTime > 30000) {
      trainFlag = false;
      digitalWrite(TRAIN_PIN, LOW);
    }
  }

  if (!lightChainFlag) {
    if (millis() - lightChainTime > 60000) {
      lightChainFlag = true;
      digitalWrite(LIGHT_CHAIN_PIN, HIGH);
    }
  }
}

void handleColorFade() {
  currentMillis = millis();
  if (currentMillis - lastFadeTime >= FADE_DELAY) {

    for (byte i = 0; i < NUM_LEDS; i++) {
      if (ledsStep[i].fade) {
        ledsStep[i].fade = false;
        if (leds[i].r != ledsAim[i].r) {
          ledsStep[i].fade = true;
          int rTemp = (int)leds[i].r + (int)ledsStep[i].rStep;
          if ((ledsStep[i].rStep > 0 && rTemp > ledsAim[i].r) || (ledsStep[i].rStep < 0 && rTemp < ledsAim[i].r) || rTemp > 255 || rTemp < 0) leds[i].r = ledsAim[i].r;
          else leds[i].r = rTemp;

          if ((ledsStep[i].rStep > 0 && leds[i].r > ledsAim[i].r) || (ledsStep[i].rStep < 0 && leds[i].r < ledsAim[i].r)) leds[i].r = ledsAim[i].r;
        }
        if (leds[i].g != ledsAim[i].g) {
          ledsStep[i].fade = true;
          int gTemp = (int)leds[i].g + (int)ledsStep[i].gStep;
          if ((ledsStep[i].gStep > 0 && gTemp > ledsAim[i].g) || (ledsStep[i].gStep < 0 && gTemp < ledsAim[i].g) || gTemp > 255 || gTemp < 0) leds[i].g = ledsAim[i].g;
          else leds[i].g = gTemp;
        }
        if (leds[i].b != ledsAim[i].b) {
          ledsStep[i].fade = true;
          int bTemp = (int)leds[i].b + (int)ledsStep[i].bStep;
          if ((ledsStep[i].bStep > 0 && bTemp > ledsAim[i].b) || (ledsStep[i].bStep < 0 && bTemp < ledsAim[i].b) || bTemp > 255 || bTemp < 0) leds[i].b = ledsAim[i].b;
          else leds[i].b = bTemp;
        }

      }
    }
    FastLED.show();
    lastFadeTime = currentMillis;
  }


}

void handleSerialCommunication() {
  if (PiSerial.available() > 0) {   //if there's any serial data
    /*
      if (data[0] == TEXT_EINGABE) {  //if the upcoming data is a text; indicated by first data byte = 124
      //inputString = PiSerial.readString();

      for (byte textIndex = 0; textIndex < (data[1] - 193); textIndex++) {  //194 = textlnge 1, ..., 224 = textlnge 30
        text[textIndex] = PiSerial.read();
      }
      text[data[1] - 193] = '\0';

      newText = true;   //text saved
      data[0] = 0;
      data[1] = 0;

      }
    */
    //else if !!!!!!! wenn oben auskommentiert

    if (PiSerial.peek() >= 1 && PiSerial.peek() <= 191) {    //if first data byte is valid
      PiSerial.readBytes(data, 2);
      //if (data[0] == TEXT_EINGABE) digitalWrite(READY_PIN, LOW);
#if DEBUG
      DebugSerial.println("Empfangen: " + (String)data[0] + ", " + (String)data[1]);
#endif
    }
    else {  //else; first data byte is invalid (0 or greater than 191)
      byte errorMessage = PiSerial.read();
      //PiSerial.print(DATA_ERROR);
      //PiSerial.print(errorMessage);
#if DEBUG
      DebugSerial.print("DATA_ERROR: ");
      DebugSerial.println(errorMessage);
#endif
    }
  }
}

void handleData() {
  if (data[0] >= LED_START && data[0] <= LED_END) {   //if data is LED data
    if (data[1] < OFF || data[1] > FARBEN_END) {   //if second data byte has invalid color
      //invalid color/data byte
      //PiSerial.print(DATA_ERROR);
      //PiSerial.print(data[1]);
#if DEBUG
      DebugSerial.print("DATA_ERROR_INVALID_COLOR: ");
      DebugSerial.println(data[1]);
#endif

    }
    else {  //color value is valid
      //led mit farbe (in data[1]) ansteuern
      byte i = data[0] - 1;

      if (data[1] == OFF) ledsAim[i] = CRGB::Black;
      else if (data[1] == ON) ledsAim[i] = CRGB::White; //hier warmwei einfgen
      else ledsAim[i].setHue((data[1] - 194) * 16);  //set aim color for led to chosen value

      setLEDFade(i);

    }
    data[0] = 0;
    data[1] = 0;
  }

  else if (data[0] == TWERKING_SANTA) {


    if (!santaFlag) {
      digitalWrite(READY_PIN, LOW);

      if (random8(100) > CHANCE_SANTA_SOLO - 1) {
        santaFlag = true;
        digitalWrite(READY_PIN, HIGH);
      }
      else santaSoloFlag = true;
    }

    data[0] = 0;
    data[1] = 0;

  }

  else if ( data[0] == TRAIN) {
    currentTime = millis();
    if (currentTime - lastTrainAction > 1000) {
      if (data[1] == OFF && trainFlag) {
        trainActionTime = currentTime;
        lastTrainAction = trainActionTime;
        digitalWrite(TRAIN_PIN, LOW);
        trainFlag = false;
      }

      else if (data[1] == ON && !trainFlag) {
        trainActionTime = currentTime;
        lastTrainAction = trainActionTime;
        digitalWrite(TRAIN_PIN, HIGH);
        trainFlag = true;
        trainTime = millis();
      }
    }
    data[0] = 0;
    data[1] = 0;

    
  }

  else if (data[0] == LIGHT_CHAIN) {
    if (data[1] == OFF && lightChainFlag) {
      digitalWrite(LIGHT_CHAIN_PIN, LOW);
      lightChainFlag = false;
      lightChainTime = millis();
    }

    else if (data[1] == ON && !lightChainFlag) {
      digitalWrite(LIGHT_CHAIN_PIN, HIGH);
      lightChainFlag = true;
    }
    data[0] = 0;
    data[1] = 0;
  }

  else if (data[0] == SPARKLING) {
    if (data[1] == ON && !sparklingFlag) {
      sparklingFlag = true;
      sparklingTime = millis();
    }

    else if (data[1] == OFF && sparklingFlag) {
      sparklingFlag = false;
    }

    data[0] = 0;
    data[1] = 0;
  }

  else if (data[0] == ANIMATIONS) {
    digitalWrite(READY_PIN, LOW);
    animations();
    digitalWrite(READY_PIN, HIGH);
    data[0] = 0;
    data[1] = 0;
  }

  else if (data[0] == TREE_BLACKOUT) {

    for (byte i = 0; i < NUM_LEDS; i++) {
      ledsAim[i] = CRGB::Black;
      setLEDFade(i);
    }

    data[0] = 0;
    data[1] = 0;
  }


  /*
    if (newText) {    //handle text entry
    //inputString.toCharArray(text, 31);

    #if DEBUG
    DebugSerial.println(text);    //debug
    DebugSerial.flush();
    #endif
    digitalWrite(READY_PIN, HIGH);
    //text anzeigen
    newText = false;
    }
  */
}

void animations() {
  fillSpiralCCW();
  fillSpiralCW();
  fillSpiralCCW();
  fillSpiralCW();

  for (byte i = 0; i < NUM_LEDS; i++) {
    leds[i] = ledsAim[i];
  }
  FastLED.show();
}

void setLEDFade(byte i) {
  if (leds[i] != ledsAim[i]) {
    ledsStep[i].fade = true;

    if (ledsAim[i].r != leds[i].r) {
      ledsStep[i].rStep = ((int)ledsAim[i].r - (int)leds[i].r) / (int)fadeSteps;
      if (ledsStep[i].rStep == 0) {
        if (ledsAim[i].r > leds[i].r) ledsStep[i].rStep = 1;
        else ledsStep[i].rStep = -1;
      }
#if DEBUG
      DebugSerial.println("rStep: " + (String)ledsStep[i].rStep);    //debug
#endif
    }
    if (ledsAim[i].g != leds[i].g) {
      ledsStep[i].gStep = ((int)ledsAim[i].g - (int)leds[i].g) / (int)fadeSteps;
      if (ledsStep[i].gStep == 0) {
        if (ledsAim[i].g > leds[i].g) ledsStep[i].gStep = 1;
        else ledsStep[i].gStep = -1;
      }
#if DEBUG
      DebugSerial.println("gStep: " + (String)ledsStep[i].gStep);    //debug
#endif
    }
    if (ledsAim[i].b != leds[i].b) {
      ledsStep[i].bStep = ((int)ledsAim[i].b - (int)leds[i].b) / (int)fadeSteps;
      if (ledsStep[i].bStep == 0) {
        if (ledsAim[i].b > leds[i].b) ledsStep[i].bStep = 1;
        else ledsStep[i].bStep = -1;
      }
#if DEBUG
      DebugSerial.println("bStep: " + (String)ledsStep[i].bStep);    //debug
#endif
    }

  }
}


void fillSpiralCCW() {

  FastLED.clear();
  FastLED.show();

  for (byte i = 0; i < (sizeof(spiralCCW) / sizeof(byte)); i++) {
    leds[spiralCCW[i]].setHue(hueTemp);
    FastLED.show();
    FastLED.delay(30);
    hueTemp += 2;
  }

  for (byte i = (sizeof(spiralCCW) / sizeof(byte)) - 1; i > 0; i--) {
    leds[spiralCCW[i]] = CRGB::Black;
    FastLED.show();
    FastLED.delay(30);
  }
}

void fillSpiralCW() {

  FastLED.clear();
  FastLED.show();

  for (byte i = 0; i < (sizeof(spiralCW) / sizeof(byte)); i++) {
    leds[spiralCW[i]].setHue(hueTemp);
    FastLED.show();
    FastLED.delay(30);
    hueTemp += 2;
  }

  for (byte i = (sizeof(spiralCW) / sizeof(byte)) - 1; i > 0; i--) {
    leds[spiralCW[i]] = CRGB::Black;
    FastLED.show();
    FastLED.delay(30);
  }
}


void frameMarquee() {
  FastLED.clear();
  FastLED.show();

  for (int j = 0; j < 400; j++) {
    for (byte i = 0; i < (sizeof(frame) / sizeof(byte)); i++) {
      leds[frame[i]].setHue(hueTemp);
      hueTemp += 2;
    }
    hueTemp = 2 * j;
    FastLED.show();
    FastLED.delay(40);
  }
  FastLED.clear();
  FastLED.show();
}

void innerFrame() {
  for (int j = 0; j < 15; j++) {

    hueTemp += 40;

    for (byte i = 0; i < (sizeof(frame) / sizeof(byte)); i++) {
      leds[frame[i]].setHue(hueTemp);
    }
    FastLED.show();
    FastLED.delay(200);

    for (byte i = 0; i < (sizeof(frame) / sizeof(byte)); i++) {
      leds[frame[i]] = CRGB::Black;
    }

    hueTemp += 40;

    for (byte i = 0; i < (sizeof(inner) / sizeof(byte)); i++) {
      leds[inner[i]].setHue(hueTemp);
    }
    FastLED.show();
    FastLED.delay(200);

    for (byte i = 0; i < (sizeof(inner) / sizeof(byte)); i++) {
      leds[inner[i]] = CRGB::Black;
    }
  }
  FastLED.clear();
  FastLED.show();
}

void triangle() {
  hueTemp += 16;
  for (byte i = 0; i < (sizeof(triangle1) / sizeof(byte)); i++) {
    leds[triangle1[i]].setHue(hueTemp);
  }
  FastLED.show();
  FastLED.delay(150);
  for (byte i = 0; i < (sizeof(triangle1) / sizeof(byte)); i++) {
    leds[triangle1[i]] = CRGB::Black;
  }
  hueTemp += 16;
  for (byte i = 0; i < (sizeof(triangle2) / sizeof(byte)); i++) {
    leds[triangle2[i]].setHue(hueTemp);
  }
  FastLED.show();
  FastLED.delay(150);
  for (byte i = 0; i < (sizeof(triangle2) / sizeof(byte)); i++) {
    leds[triangle2[i]] = CRGB::Black;
  }
  hueTemp += 16;
  for (byte i = 0; i < (sizeof(triangle3) / sizeof(byte)); i++) {
    leds[triangle3[i]].setHue(hueTemp);
  }
  FastLED.show();
  FastLED.delay(150);
  for (byte i = 0; i < (sizeof(triangle3) / sizeof(byte)); i++) {
    leds[triangle3[i]] = CRGB::Black;
  }
  hueTemp += 16;
  for (byte i = 0; i < (sizeof(triangle4) / sizeof(byte)); i++) {
    leds[triangle4[i]].setHue(hueTemp);
  }
  FastLED.show();
  FastLED.delay(150);
  for (byte i = 0; i < (sizeof(triangle4) / sizeof(byte)); i++) {
    leds[triangle4[i]] = CRGB::Black;
  }

}
leds_lookup_table.hArduino
Look up table for the LEDs on the tree.
byte frame[] = {52, 51, 43, 42, 32, 31, 19, 18, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 25, 26, 38, 39, 47, 48, 54, 53};

byte inner[] = {50, 49, 44, 45, 46, 41, 40, 33, 34, 35, 36, 37, 30, 29, 28, 27, 20, 21, 22, 23, 24, 17, 16, 15, 14, 13, 12, 11, 10};

byte spiralCCW[] = {52, 51, 43, 32, 18, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 38, 47, 48, 54, 53, 50, 44, 42, 33, 31, 19, 17,
                    16, 15, 14, 13, 12, 11, 10, 25, 26, 37, 39, 46, 49, 45, 41, 34, 30, 20, 21, 22, 23, 24,
                    27, 36, 40, 35, 29, 28, 35};
byte spiralCW[] = {52, 53, 54, 48, 47, 38, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 18, 32, 43, 51, 50, 49, 46, 39, 37, 26, 25, 10, 11, 12, 13, 14, 15, 16, 17, 19, 31,
                   33, 42, 44, 45, 40, 36, 27, 24, 23, 22, 21, 20, 30, 34, 41, 35, 28, 29};

byte triangle1[] = {35, 29, 28};

byte triangle2[] = {45, 41, 34, 30, 20, 21, 22, 23, 24, 27, 36, 40};

byte triangle3[] = {53, 50, 44, 42, 33, 31, 19, 17, 16, 15, 14, 13, 12, 11, 10, 25, 26, 37, 39, 46, 49};

byte triangle4[] = {52, 51, 43, 32, 18, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 38, 47, 48, 54};
Telegram_Bot_xmas.inoArduino
Code for the Arduino MKR1000.
#define TEXT_SERIAL 1
#define WATCHDOG 1
#define DEBUG 0

#include <Arduino.h>
#include <Time.h>
#include <TimeLib.h>
#include <WiFi101.h>
//#include <WiFiUdp.h>
#include <SPI.h>
#include <TelegramBot.h>
//#include <time.h>
//#include <RTCZero.h>

#if TEXT_SERIAL
#include <wiring_private.h>
#endif

#if WATCHDOG
#include <Adafruit_SleepyDog.h>
#endif

#if TEXT_SERIAL
Uart textSerial (&sercom3, 0, 1, SERCOM_RX_PAD_1, UART_TX_PAD_0);

void SERCOM3_Handler()    // Interrupt handler for SERCOM3
{
  textSerial.IrqHandler();
}
#endif

#define ledPin 6
#define RBUFFSIZE 1000

#define MAX_MESSAGES 16

#define TIME 1
#define TEXT 2
#define WEATHER 3
#define STATS 4

/*
  #define BEER_NAME 0
  #define BEER_ID 1
*/

char responseBuffer[RBUFFSIZE];
int  rbindex = 0;

boolean startCapture;
unsigned long lastTimeSentTime, lastTextSent, currentTime, lastClockSent;

// Initialize Wifi connection

const char ssid[] = "";           // network SSID
const char pass[] = "";           // network key

// Initialize Telegram BOT

const char BotToken[] = "";

char server[] = "api.openweathermap.org";

String eingabe; //Nachrichten[128];
char Nachrichten[MAX_MESSAGES][100];
byte numMessages, messageIndex, sendMessageIndex;

WiFiSSLClient client;
WiFiClient clienthttp;

TelegramBot  bot(BotToken, client);

//RTCZero rtc;

//TelegramKeyboard keyboardStart;
/*
const char* row1Start[] = {"/help", "/wlan"};
const char* row2Start[] = {"/ip", "/signal"};
//const char* row3Start[] = {"/alloff"};
*/

time_t tClock = 0;


void setup() {

  delay(3000);

#if TEXT_SERIAL
  textSerial.begin(9600);           // Begin Uart for sending text to LED matrix
#endif

  Serial.begin(9600);

#if TEXT_SERIAL
  pinPeripheral(0, PIO_SERCOM);   // Assign pins 0 & 1 SERCOM functionality
  pinPeripheral(1, PIO_SERCOM);
#endif

#if WATCHDOG
  Watchdog.enable(10000);
#endif

  // attempt to connect to Wifi network:
#if DEBUG
  Serial.print(F("Verbine mit WLAN: "));
  Serial.println(ssid);
#endif

  while (WiFi.begin(ssid, pass) != WL_CONNECTED) {
    delay(500);
  }

#if DEBUG
  Serial.println(F("Mit WLAN verbunden."));
#endif
/*
  byte counterT = 0;
  while (tClock == 0 && counterT < 6) {
    delay(1000);
    counterT++;

    tClock = WiFi.getTime();

  }
  Serial.println(tClock);
  */
  getCurrentTime();
  
  if (tClock != 0) {
    textSerial.write(TIME);
    textSerial.write(hour(tClock));
    textSerial.write(minute(tClock));
    textSerial.write(second(tClock));
  }
  tClock = 0;
  
  lastClockSent = millis();

  bot.begin();

  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
/*
  keyboardStart.addRow(row1Start, 2);
  keyboardStart.addRow(row2Start, 2);
  //keyboardStart.addRow(row3Start, 1);
  */

  message m = bot.getUpdates();
}

void loop() {
#if WATCHDOG
  Watchdog.reset();
#endif

  if ( WiFi.status() != WL_CONNECTED) {
    //WiFi.disconnect();
    while (WiFi.begin(ssid, pass) != WL_CONNECTED) {
#if DEBUG
      Serial.print(".");
#endif
      delay(1000);
    }
  }
  currentTime = millis();

  if (numMessages > 0 && currentTime - lastTextSent > 20000) {
    textSerial.write(TEXT);
    textSerial.print(String(Nachrichten[sendMessageIndex]));
    lastTextSent = currentTime;
    sendMessageIndex++;
    numMessages--;
    if (sendMessageIndex >= MAX_MESSAGES) sendMessageIndex = 0;
  }

  if (currentTime - lastTextSent > 120000) {
    textSerial.write(TEXT);
    textSerial.print("To send custom text, open/download the 'Telegram' App and add: TannenbaumBot.");
    lastTextSent = currentTime;
  }

  if (currentTime - lastClockSent > 600000) {
    tClock = 0;

    getCurrentTime();
    /*
    byte counterT = 0;
    while (tClock == 0 && counterT < 6) {
      counterT++;
      delay(500);
      tClock = WiFi.getTime();
    }
    */
    if (tClock != 0) {
      textSerial.write(TIME);
      textSerial.write(hour(tClock));
      textSerial.write(minute(tClock));
      textSerial.write(second(tClock));
    }
    

    /*
      textSerial.write(rtc.getHours());
      textSerial.write(rtc.getMinutes());
      textSerial.write(rtc.getSeconds());
    */

    lastClockSent = currentTime;
  }

  message m = bot.getUpdates();
  /*
    if(millis() - lastTimeSentTime > 600000) {
      textSerial.write(TIME);
      textSerial.print(String(rtc.getEpoch()) + "\n");
      lastTimeSentTime = millis();
    }
  */

  if (Serial.available() > 0) {
    eingabe = Serial.readStringUntil('\n');
    if (!eingabe.equalsIgnoreCase("i")) {
      bot.sendMessage("", eingabe);
      eingabe = "";
    }
  }

  else if (eingabe.equalsIgnoreCase("i")) {
    if (WL_CONNECTED) {
      Serial.println(F("Mit WLAN verbunden"));
      IPAddress ip = WiFi.localIP();
      Serial.print(F("IP Address: "));
      Serial.println(ip);

      // print the received signal strength:
      long rssi = WiFi.RSSI();
      Serial.print(F("signal strength (RSSI):"));
      Serial.print(rssi);
      Serial.println(F(" dBm"));
      eingabe = "";
    }
    else Serial.println(F("KEINE Verbindung zum WLAN"));
    eingabe = "";
  }

  if ( m.chat_id != 0 ) {
    if (1) { // if a message is received from unique Chats m.sender.equals("xxxxx")

      //Serial.println("Location: " + m.location.lon + ", " + m.location.lat);
      //Serial.println("Name: " + m.first_name);
      //Serial.println(m.chat_id);
      //Serial.println(m.sender);

#if DEBUG
      time_t t = m.date.toInt();
      t = t + 1 * 60 * 60;

      Serial.print(m.sender);
      Serial.print(F(" hat um "));
      Serial.print(hour(t));
      Serial.print(":");
      Serial.print(minute(t));

      Serial.print(F(" folgendes gesendet: "));
      Serial.println(m.text);
#endif

      if (m.location.lon.length() != 0) {
        bot.sendMessage(m.chat_id, "Requesting weather data...");
#if DEBUG
        Serial.println(F("Verbinde zu api.openweathermap.org"));
#endif

        weatherHttpRequest(m.location.lat, m.location.lon, m.chat_id);

      }

      if (m.text.equalsIgnoreCase("/help")) {
        bot.sendMessage(m.chat_id, "To send custom text, type: /text and your message. For example: '/text Santa rocks!'. To display the current weather in your area, simply send your location.");
     
      
      }
/*
      else if (m.text.equalsIgnoreCase("/watchdog")) {
        while (1);
      }
*/
      else if (m.text.equalsIgnoreCase("/start")) {
        bot.sendMessage(m.chat_id, "Welcome " + m.first_name + "! Send /help for more information.");

      }

      else if (m.text.startsWith("/text") || m.text.startsWith("/Text")) {
        m.text.remove(0, 6);
        m.text += "\n";

        if (m.text.length() <= 2) {
          bot.sendMessage(m.chat_id, "Sorry, your text is too short. Please send at least 2 characters.");
        }
        else if (m.text.length() >= 75) {
          bot.sendMessage(m.chat_id, "Sorry, your text is too long. The maxmimal text length is 74 characters.");
        }
        else if (numMessages == MAX_MESSAGES) {
          bot.sendMessage(m.chat_id, "Sorry, there are currently too many people sending messages to Santa. Try again later.");
        }
        else {
          if (m.first_name.length() >= 21) m.first_name.remove(20);
          eingabe = m.first_name + ": " + m.text;
          eingabe.toCharArray(Nachrichten[messageIndex], 100);
          eingabe = "";
          numMessages++;
          messageIndex++;
          if (messageIndex >= MAX_MESSAGES) messageIndex = 0;
          bot.sendMessage(m.chat_id, "Your text is on position " + String(numMessages) + " in the pipeline. It will be displayed in approximately " + String((numMessages) * 20) + " seconds.");
        }

#if DEBUG
        Serial.println("Der reine Text lautet: " + m.text);

#endif
        
      }
      /*
            else if (m.text.startsWith("/bierID")) {
              m.text.remove(0, 7);
              m.text.trim();
              beerHttpRequest(BEER_ID, m.text, m.chat_id);
            }

            else if (m.text.startsWith("/bier") || m.text.startsWith("/Bier")) {
              m.text.remove(0, 6);
              m.text.trim();
              beerHttpRequest(BEER_NAME, m.text, m.chat_id);
            }
      */
      /*
      else if (m.text.equalsIgnoreCase("/wlan")) {
        bot.sendMessage(m.chat_id, ssid);

      }
      else if (m.text.equalsIgnoreCase("/ip")) {
        IPAddress ip = WiFi.localIP();
        bot.sendMessage(m.chat_id, String(ip[0]) + String(".") + \
                        String(ip[1]) + String(".") + \
                        String(ip[2]) + String(".") + \
                        String(ip[3]));
      }
      else if (m.text.equalsIgnoreCase("/signal")) {
        long rssi = WiFi.RSSI();
        bot.sendMessage(m.chat_id, String(rssi) + " dBm");
      }
      */
    }

    else {
      bot.sendMessage(m.chat_id, "Sie sind nicht befugt diesen Befehl zu verwenden.");

      Serial.println(m.chat_id);
      Serial.println(m.sender);
    }
  }
}

int findText(String needle, String haystack) {
  int foundpos = -1;
  for (int i = 0; i <= haystack.length() - needle.length(); i++) {
    if (haystack.substring(i, needle.length() + i) == needle) {
      foundpos = i;
    }
  }
  return foundpos;
}

void weatherHttpRequest(String lat, String lon, String chatId) {
  if (clienthttp.connect(server, 80)) {
#if DEBUG
    Serial.println(F("Verbunden zum Server"));
#endif

    clienthttp.println("GET /data/2.5/weather?lat=" + lat + "&lon=" + lon + "&APPID=**yourapikey** HTTP/1.1");
    clienthttp.println("Host: api.openweathermap.org");
    clienthttp.println("Connection: close");
    clienthttp.println();

    responseBuffer[0] = '\0';
    rbindex = 0;

    startCapture = false;
  }

  else {
    bot.sendMessage(chatId, "Cannot connect to weather server.");
    return;
  }

  char c;
  unsigned long startTime = millis();
  while (clienthttp.connected() && millis() - startTime < 7000) {
    if (clienthttp.available()) {
      c = clienthttp.read();
      if (c == '{') {
        startCapture = true;
      }
      if (startCapture && rbindex < RBUFFSIZE) {
        responseBuffer[rbindex] = c;
        rbindex++;
      }
    }
  }


  if (clienthttp.connected()) {
    clienthttp.stop();
    clienthttp.flush();
#if DEBUG
    Serial.println(F("Client won't disconnect"));
#endif
    bot.sendMessage(chatId, "Data couldn't be recieved.");
    return;
  }

#if DEBUG
  Serial.println(F("Received bytes."));
  //Serial.print(strlen(responseBuffer));
  Serial.println(F("Disconnecting."));
#endif

  clienthttp.stop();
  clienthttp.flush();

  StaticJsonBuffer<RBUFFSIZE> jsonBuffer;

  JsonObject& root = jsonBuffer.parseObject(responseBuffer);

  if (!root.success()) {
#if DEBUG
    Serial.println(F("parseObject() fehlgeschlagen"));
#endif

    bot.sendMessage(chatId, "Datenverarbeitung fehlgeschlagen.");
  }
  else {
    char bufferw[20];
    char bufferc[30];
    root["weather"][0]["description"].prettyPrintTo(bufferw, sizeof(bufferw));
    root["name"].prettyPrintTo(bufferc, sizeof(bufferc));

#if DEBUG
    Serial.println("Schnee: " + String((float)root["snow"]["3h"]));
#endif
    //bot.sendMessage(m.chat_id, "Aktuelle Wetter fr " + String(bufferc) + ":");
    textSerial.write(TEXT);
    eingabe = "Current weather in " + String(bufferc) + ": " + String(bufferw) + " at " + String(((double)root["main"]["temp"]) - 273.15, 1) + " C";
    textSerial.print(eingabe);
    lastTextSent = millis();
    eingabe = "";
    
    bot.sendMessage(chatId, "The current weather in your region will be displayed soon.");

#if DEBUG
    Serial.print(F("Aktuelle Temperatur: "));
    Serial.print(((double)root["main"]["temp"]) - 273.15);
    Serial.println(F(" C"));

    Serial.print(F("Beschreibung: "));
    Serial.println(bufferw);
#endif
  }
}
void getCurrentTime() {
  if (clienthttp.connect("api.timezonedb.com", 80)) {
    clienthttp.println("GET /v2/get-time-zone?key=**yourapikey**&format=json&by=zone&zone=Europe/London HTTP/1.1");
    clienthttp.println("Host: api.timezonedb.com");
    clienthttp.println("Connection: close");
    clienthttp.println();

    responseBuffer[0] = '\0';
    rbindex = 0;

    startCapture = false;
  }

  else {
    Serial.println("Verbindung zum Zeitserver fehlgeschlagen");
    return;
  }

   char c;
  unsigned long startTime = millis();

  while (clienthttp.connected() && millis() - startTime < 7000) {
    if (clienthttp.available()) {
      c = clienthttp.read();
      if (c == '{') {
        startCapture = true;
      }
      if (startCapture && rbindex < RBUFFSIZE) {
        responseBuffer[rbindex] = c;
        rbindex++;
      }
    }
  }

  if (clienthttp.connected()) {
    clienthttp.stop();
    clienthttp.flush();
  #if DEBUG
    Serial.println(F("Client won't disconnect"));
  #endif
    return;
  }


  #if DEBUG
  Serial.println(F("Received bytes."));
  //Serial.print(strlen(responseBuffer));
  Serial.println(F("Disconnecting."));
  #endif

  clienthttp.stop();
  clienthttp.flush();

  StaticJsonBuffer<RBUFFSIZE> jsonBuffer;

  JsonObject& root = jsonBuffer.parseObject(responseBuffer);

  if (!root.success()) {
  #if DEBUG
    Serial.println(F("parseObject() fehlgeschlagen"));
  #endif

  }
  else {
    tClock = root["timestamp"];
    Serial.println(tClock);
  }
}
//wanted to implement the brewery db but couldn't make it work so fart
/*
  void beerHttpRequest(boolean type, String request, String chatId) {
  if (clienthttp.connect("api.brewerydb.com", 80)) {
  #if DEBUG
    Serial.println(F("Verbunden zum Server"));
  #endif

    if (type == BEER_NAME) clienthttp.println("GET /v2/search?q=" + request + "&type=beer&key=**yourapikey**&format=json HTTP/1.1");
    else clienthttp.println("GET /v2/beer/" + request + "?key=**yourapikey**&format=json HTTP/1.1");
    clienthttp.println("Host: api.brewerydb.com");
    clienthttp.println("Connection: close");
    clienthttp.println();

    responseBuffer[0] = '\0';
    rbindex = 0;

    startCapture = false;

  }

  else {
    bot.sendMessage(chatId, "Verbindung zur Brewery Databse nicht mglich.");
    return;
  }

  char c;
  unsigned long startTime = millis();

  while (clienthttp.connected() && millis() - startTime < 7000) {
    if (clienthttp.available()) {
      c = clienthttp.read();
      if (c == '{') {
        startCapture = true;
      }
      if (startCapture && rbindex < RBUFFSIZE) {
        responseBuffer[rbindex] = c;
        rbindex++;
      }
    }
  }

  if (clienthttp.connected()) {
    clienthttp.stop();
    clienthttp.flush();
  #if DEBUG
    Serial.println(F("Client won't disconnect"));
  #endif
    bot.sendMessage(chatId, "Daten konnten nicht empfangen werden.");
    return;
  }


  #if DEBUG
  Serial.println(F("Received bytes."));
  //Serial.print(strlen(responseBuffer));
  Serial.println(F("Disconnecting."));
  #endif

  clienthttp.stop();
  clienthttp.flush();

  StaticJsonBuffer<RBUFFSIZE> jsonBuffer;

  JsonObject& root = jsonBuffer.parseObject(responseBuffer);

  if (!root.success()) {
  #if DEBUG
    Serial.println(F("parseObject() fehlgeschlagen"));
  #endif

    bot.sendMessage(chatId, "Datenverarbeitung fehlgeschlagen.");
  }
  else {

    if (type == BEER_NAME) {
      char bufferName[10][50]; // char bufferName[10][50];
      char bufferID[10][9];   //char bufferID[10][6];
      int results = root["totalResults"];  // int results = atoi(&root["totalResults"]);
      if (results > 10) results = 10;
      for (byte i = 0; i < results; i++) {

        root["data"][i]["id"].prettyPrintTo(bufferID[i], sizeof(bufferID[i]));
        root["data"][i]["name"].prettyPrintTo(bufferName[i], sizeof(bufferName[i]));
      }

      String beerMessage;

      if (results > 0) {
        beerMessage = "Hier die ersten " + String(results) + " Treffer. Meintest du:";
        for (byte i = 0; i < results; i++) {
          beerMessage += '\n';
          beerMessage += String(bufferName[i]);
          beerMessage += ": /bierID";
          String IDtemp = String(bufferID[i]);
          IDtemp.remove(0, 1);
          IDtemp.remove(6, 1);
          beerMessage += IDtemp;
        }
      }

      else beerMessage = "Zu diesem Suchbegriff gab es leider keine Treffer.";

      bot.sendMessage(chatId, beerMessage);
  #if DEBUG
      Serial.println(F("test"));
  #endif
    }
    else {
      char bufferDescription[1000];
      char bufferStyle[50];

      if (root["data"]["description"] != NULL) {
        root["data"]["description"].prettyPrintTo(bufferDescription, sizeof(bufferDescription));
        bot.sendMessage(chatId, bufferDescription);
  #if DEBUG
        Serial.println(bufferDescription);
  #endif
      }
      if (root["data"]["style"]["description"] != NULL) {
        root["data"]["style"]["description"].prettyPrintTo(bufferDescription, sizeof(bufferDescription));
        root["data"]["style"]["name"].prettyPrintTo(bufferStyle, sizeof(bufferStyle));

        bot.sendMessage(chatId, "Es handelt sich um ein: " + String(bufferStyle) + ".'\n'" + String(bufferDescription));
  #if DEBUG
        Serial.println("Es handelt sich um ein: " + String(bufferStyle) + ".'\n'" + String(bufferDescription));
  #endif
      }
    }
  }
  }
*/
dot_matrix_scrolling_text_test.inoArduino
Code for the Arduino Nano to run the dot matrix.
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <Time.h>
#include <TimeLib.h>
#include "Parola_Fonts_data.h"

#define MAX_DEVICES 8
#define CLK_PIN   13
#define DATA_PIN  11
#define CS_PIN    10

#define TIME 1
#define TEXT 2
#define WEATHER 3
#define STATS 4

#define NUM_TIMEZONES 8

MD_Parola P = MD_Parola(CS_PIN, MAX_DEVICES);

uint8_t scrollSpeed = 30;    // default frame delay value
textEffect_t scrollEffect = PA_SCROLL_LEFT;
textPosition_t scrollAlign = PA_CENTER;
uint16_t scrollPause = 0; // in milliseconds

#define	BUF_SIZE	100
char curMessage[BUF_SIZE] = "www.steuermeinentannenbaum.de"; //{0x01};
char newMessage[BUF_SIZE];
String newMessageString = "";
bool newMessageAvailable = false, showClock = false;
unsigned long lastNewMessageTime;
byte dataByte = 0, zoneIndex, timeBuffer[3];

struct timeZonesStruct {
  int GMT;
  String name;
};

timeZonesStruct timeZones[NUM_TIMEZONES] = {
  {1, "Berlin"},
  {0, "UK"},
  { -8, "LA"},
  { -6, "Texas"},
  { -5, "NYC"},
  {11, "Sydney"},
  {9, "Tokyo"},
  {3, "Russia"}
};

/*
  void readSerial(void)
  {
  static char *cp = newMessage;

  while (Serial1.available())
  {
     cp = (char)Serial1.read();
    if ((*cp == '\n') || (cp - newMessage >= BUF_SIZE-2)) // end of message character or full buffer
    {
       cp = '\0'; // end the string
      // restart the index for next filling spree and flag we have a message waiting
      cp = newMessage;
      newMessageAvailable = true;
      Serial.println(cp);
      //Serial.flush();
    }
    else  // move char pointer to next position
      cp++;
  }
  }
*/

void readSerial(void) {
  if (Serial.available()) {
    if (dataByte == 0) dataByte = Serial.read();
    else if (dataByte == TEXT) {
      newMessageString = Serial.readStringUntil('\n');
      newMessageString.toCharArray(newMessage, BUF_SIZE);
      newMessageAvailable = true;
      showClock = false;
      lastNewMessageTime = millis();
      dataByte = 0;
    }
    else if (dataByte == TIME) {
      Serial.readBytes(timeBuffer, 3);
      setTime(timeBuffer[0], timeBuffer[1], timeBuffer[2], day(), month(), year());
      dataByte = 0;
    }
  }
}



void setup()
{
  Serial.begin(9600);

  randomSeed(analogRead(0));


  P.begin();
  P.setFont(ExtASCII);
  P.displayClear();
  P.displaySuspend(false);

  //P.setScrollSpacing(1);

  lastNewMessageTime = millis();

  P.displayText(curMessage, scrollAlign, scrollSpeed, scrollPause, scrollEffect, scrollEffect);

  //newMessage[0] = '\0';
}

void loop()
{
  readSerial();

  if (millis() - lastNewMessageTime > 30000) showClock = true;

  if (P.displayAnimate())
  {
    if (showClock) {
      P.setTextEffect(PA_SCROLL_DOWN, PA_SCROLL_DOWN);
      P.setPause(5000);

      newMessageString = timeZones[zoneIndex].name + ": ";
      newMessageString += returnLocalTime(zoneIndex);

      zoneIndex++;
      if (zoneIndex >= NUM_TIMEZONES) zoneIndex = 0;

      newMessageString.toCharArray(newMessage, BUF_SIZE);
      strcpy(curMessage, newMessage);
      newMessageString = "";

      P.displayReset();
    }
    else if (newMessageAvailable)
    {
      P.setTextEffect(PA_SCROLL_LEFT, PA_SCROLL_LEFT);
      P.setPause(0);

      strcpy(curMessage, newMessage);
      newMessageAvailable = false;
      newMessageString = "";
    }
    P.displayReset();
  }
}

String returnLocalTime(byte index) {
  String temp;
  int hourInt = hour() + timeZones[index].GMT;

  if (hourInt < 0) hourInt = 24 + hourInt;
  else if (hourInt > 23) hourInt -= 24;

  if (hourInt < 10) temp += "0";
  temp += String(hourInt) + ":";
  if (minute() < 10) temp += "0";
  temp += String(minute());

  return temp;
}
Parola_Fonts_data.hArduino
Fonts for the dot matrix to allow umlaute etc.
// Data file for UTF-8 example user defined fonts
#ifndef FONTS_DATA_H
#define FONTS_DATA_H

MD_MAX72XX::fontType_t ExtASCII[] PROGMEM =
{
  0,		// 0 - 'Unused'
  0,		// 1 - 'Unused'
  0,		// 2 - 'Unused'
  0,		// 3 - 'Unused'
  0,		// 4 - 'Unused'
  0,		// 5 - 'Unused'
  0,		// 6 - 'Unused'
  0,		// 7 - 'Unused'
  0,		// 8 - 'Unused'
  0,		// 9 - 'Unused'
  0,		// 10 - 'Unused'
  0,		// 11 - 'Unused'
  0,		// 12 - 'Unused'
  0,		// 13 - 'Unused'
  0,		// 14 - 'Unused'
  0,		// 15 - 'Unused'
  0,		// 16 - 'Unused'
  0,		// 17 - 'Unused'
  0,		// 18 - 'Unused'
  0,		// 19 - 'Unused'
  0,		// 20 - 'Unused'
  0,		// 21 - 'Unused'
  0,		// 22 - 'Unused'
  0,		// 23 - 'Unused'
  0,		// 24 - 'Unused'
  0,		// 25 - 'Unused'
  0,		// 26 - 'Unused'
  0,		// 27 - 'Unused'
  0,		// 28 - 'Unused'
  0,		// 29 - 'Unused'
  0,		// 30 - 'Unused'
  0,		// 31 - 'Unused'
  2, 0, 0,		// 32 - 'Space'
  1, 95,		// 33 - '!'
  3, 7, 0, 7,		// 34 - '"'
  5, 20, 127, 20, 127, 20,		// 35 - '#'
  5, 36, 42, 127, 42, 18,		// 36 - '$'
  5, 35, 19, 8, 100, 98,		// 37 - '%'
  5, 54, 73, 86, 32, 80,		// 38 - '&'
  2, 4, 3,		// 39
  3, 28, 34, 65,		// 40 - '('
  3, 65, 34, 28,		// 41 - ')'
  5, 42, 28, 127, 28, 42,		// 42 - '*'
  5, 8, 8, 62, 8, 8,		// 43 - '+'
  2, 128, 96,		// 44 - ','
  5, 8, 8, 8, 8, 8,		// 45 - '-'
  2, 96, 96,		// 46 - '.'
  5, 32, 16, 8, 4, 2,		// 47 - '/'
  5, 62, 81, 73, 69, 62,		// 48 - '0'
  3, 66, 127, 64,		// 49 - '1'
  5, 114, 73, 73, 73, 70,		// 50 - '2'
  5, 33, 65, 73, 77, 51,		// 51 - '3'
  5, 24, 20, 18, 127, 16,		// 52 - '4'
  5, 39, 69, 69, 69, 57,		// 53 - '5'
  5, 60, 74, 73, 73, 49,		// 54 - '6'
  5, 65, 33, 17, 9, 7,		// 55 - '7'
  5, 54, 73, 73, 73, 54,		// 56 - '8'
  5, 70, 73, 73, 41, 30,		// 57 - '9'
  1, 20,		// 58 - ':'
  2, 128, 104,		// 59 - ';'
  4, 8, 20, 34, 65,		// 60 - '<'
  5, 20, 20, 20, 20, 20,		// 61 - '='
  4, 65, 34, 20, 8,		// 62 - '>'
  5, 2, 1, 89, 9, 6,		// 63 - '?'
  5, 62, 65, 93, 89, 78,		// 64 - '@'
  5, 124, 18, 17, 18, 124,		// 65 - 'A'
  5, 127, 73, 73, 73, 54,		// 66 - 'B'
  5, 62, 65, 65, 65, 34,		// 67 - 'C'
  5, 127, 65, 65, 65, 62,		// 68 - 'D'
  5, 127, 73, 73, 73, 65,		// 69 - 'E'
  5, 127, 9, 9, 9, 1,		// 70 - 'F'
  5, 62, 65, 65, 81, 115,		// 71 - 'G'
  5, 127, 8, 8, 8, 127,		// 72 - 'H'
  3, 65, 127, 65,		// 73 - 'I'
  5, 32, 64, 65, 63, 1,		// 74 - 'J'
  5, 127, 8, 20, 34, 65,		// 75 - 'K'
  5, 127, 64, 64, 64, 64,		// 76 - 'L'
  5, 127, 2, 28, 2, 127,		// 77 - 'M'
  5, 127, 4, 8, 16, 127,		// 78 - 'N'
  5, 62, 65, 65, 65, 62,		// 79 - 'O'
  5, 127, 9, 9, 9, 6,		// 80 - 'P'
  5, 62, 65, 81, 33, 94,		// 81 - 'Q'
  5, 127, 9, 25, 41, 70,		// 82 - 'R'
  5, 38, 73, 73, 73, 50,		// 83 - 'S'
  5, 3, 1, 127, 1, 3,		// 84 - 'T'
  5, 63, 64, 64, 64, 63,		// 85 - 'U'
  5, 31, 32, 64, 32, 31,		// 86 - 'V'
  5, 63, 64, 56, 64, 63,		// 87 - 'W'
  5, 99, 20, 8, 20, 99,		// 88 - 'X'
  5, 3, 4, 120, 4, 3,		// 89 - 'Y'
  5, 97, 89, 73, 77, 67,		// 90 - 'Z'
  3, 127, 65, 65,		// 91 - '['
  5, 2, 4, 8, 16, 32,		// 92 - '\'
  3, 65, 65, 127,		// 93 - ']'
  5, 4, 2, 1, 2, 4,		// 94 - '^'
  5, 64, 64, 64, 64, 64,		// 95 - '_'
  2, 3, 4,		// 96 - '`'
  5, 32, 84, 84, 120, 64,		// 97 - 'a'
  5, 127, 40, 68, 68, 56,		// 98 - 'b'
  5, 56, 68, 68, 68, 40,		// 99 - 'c'
  5, 56, 68, 68, 40, 127,		// 100 - 'd'
  5, 56, 84, 84, 84, 24,		// 101 - 'e'
  4, 8, 126, 9, 2,		// 102 - 'f'
  5, 24, 164, 164, 156, 120,		// 103 - 'g'
  5, 127, 8, 4, 4, 120,		// 104 - 'h'
  3, 68, 125, 64,		// 105 - 'i'
  4, 64, 128, 128, 122,		// 106 - 'j'
  4, 127, 16, 40, 68,		// 107 - 'k'
  3, 65, 127, 64,		// 108 - 'l'
  5, 124, 4, 120, 4, 120,		// 109 - 'm'
  5, 124, 8, 4, 4, 120,		// 110 - 'n'
  5, 56, 68, 68, 68, 56,		// 111 - 'o'
  5, 252, 24, 36, 36, 24,		// 112 - 'p'
  5, 24, 36, 36, 24, 252,		// 113 - 'q'
  5, 124, 8, 4, 4, 8,		// 114 - 'r'
  5, 72, 84, 84, 84, 36,		// 115 - 's'
  4, 4, 63, 68, 36,		// 116 - 't'
  5, 60, 64, 64, 32, 124,		// 117 - 'u'
  5, 28, 32, 64, 32, 28,		// 118 - 'v'
  5, 60, 64, 48, 64, 60,		// 119 - 'w'
  5, 68, 40, 16, 40, 68,		// 120 - 'x'
  5, 76, 144, 144, 144, 124,		// 121 - 'y'
  5, 68, 100, 84, 76, 68,		// 122 - 'z'
  3, 8, 54, 65,		// 123 - '{'
  1, 119,		// 124 - '|'
  3, 65, 54, 8,		// 125 - '}'
  5, 2, 1, 2, 4, 2,		// 126 - '~'
  0,		// 127 - 'Unused'
  6, 20, 62, 85, 85, 65, 34,		// 128 - 'Euro sign'
  5, 28, 62, 124, 62, 28,		// 129 - 'Heart' costum char!!!
  2, 128, 96,		// 130 - 'Single low 9 quotation mark'
  5, 192, 136, 126, 9, 3,		// 131 - 'f with hook'
  4, 128, 96, 128, 96,		// 132 - 'Single low 9 quotation mark'
  8, 96, 96, 0, 96, 96, 0, 96, 96,		// 133 - 'Horizontal ellipsis'
  3, 4, 126, 4,		// 134 - 'Dagger'
  3, 20, 126, 20,		// 135 - 'Double dagger'
  4, 2, 1, 1, 2,		// 136 - 'Modifier circumflex'
  7, 35, 19, 104, 100, 2, 97, 96,		// 137 - 'Per mille sign'
  5, 72, 85, 86, 85, 36,		// 138 - 'S with caron'
  3, 8, 20, 34,		// 139 - '< quotation'
  6, 62, 65, 65, 127, 73, 73,		// 140 - 'OE'
  0,		// 141 - 'Not used'
  5, 68, 101, 86, 77, 68,		// 142 - 'z with caron'
  0,		// 143 - 'Not used'
  0,		// 144 - 'Not used'
  2, 3, 4,		// 145 - 'Left single quote mark'
  2, 4, 3,		// 146 - 'Right single quote mark'
  4, 3, 4, 3, 4,		// 147 - 'Left double quote marks'
  4, 4, 3, 4, 3,		// 148 - 'Right double quote marks'
  4, 0, 24, 60, 24,		// 149 - 'Bullet Point'
  3, 8, 8, 8,		// 150 - 'En dash'
  5, 8, 8, 8, 8, 8,		// 151 - 'Em dash'
  4, 2, 1, 2, 1,		// 152 - 'Small ~'
  7, 1, 15, 1, 0, 15, 2, 15,		// 153 - 'TM'
  5, 72, 85, 86, 85, 36,		// 154 - 's with caron'
  3, 34, 20, 8,		// 155 - '> quotation'
  7, 56, 68, 68, 124, 84, 84, 8,		// 156 - 'oe'
  0,		// 157 - 'Not used'
  5, 68, 101, 86, 77, 68,		// 158 - 'z with caron'
  5, 12, 17, 96, 17, 12,		// 159 - 'Y diaresis'
  2, 0, 0,		// 160 - 'Non-breaking space'
  1, 125,		// 161 - 'Inverted !'
  5, 60, 36, 126, 36, 36,		// 162 - 'Cent sign'
  5, 72, 126, 73, 65, 102,		// 163 - 'Pound sign'
  5, 34, 28, 20, 28, 34,		// 164 - 'Currency sign'
  5, 43, 47, 252, 47, 43,		// 165 - 'Yen'
  1, 119,		// 166 - '|'
  4, 102, 137, 149, 106,		// 167 - 'Section sign'
  3, 1, 0, 1,		// 168 - 'Spacing diaresis'
  7, 62, 65, 93, 85, 85, 65, 62,		// 169 - 'Copyright'
  3, 13, 13, 15,		// 170 - 'Feminine Ordinal Ind.'
  5, 8, 20, 42, 20, 34,		// 171 - '<<'
  5, 8, 8, 8, 8, 56,		// 172 - 'Not sign'
  0,		// 173 - 'Soft Hyphen'
  7, 62, 65, 127, 75, 117, 65, 62,		// 174 - 'Registered Trademark'
  5, 1, 1, 1, 1, 1,		// 175 - 'Spacing Macron Overline'
  3, 2, 5, 2,		// 176 - 'Degree'
  5, 68, 68, 95, 68, 68,		// 177 - '+/-'
  3, 25, 21, 19,		// 178 - 'Superscript 2'
  3, 17, 21, 31,		// 179 - 'Superscript 3'
  2, 2, 1,		// 180 - 'Acute accent'
  4, 252, 64, 64, 60,		// 181 - 'micro (mu)'
  5, 6, 9, 127, 1, 127,		// 182 - 'Paragraph Mark'
  2, 24, 24,		// 183 - 'Middle Dot'
  3, 128, 128, 96,		// 184 - 'Spacing sedilla'
  2, 2, 31,		// 185 - 'Superscript 1'
  4, 6, 9, 9, 6,		// 186 - 'Masculine Ordinal Ind.'
  5, 34, 20, 42, 20, 8,		// 187 - '>>'
  6, 64, 47, 16, 40, 52, 250,		// 188 - '1/4'
  6, 64, 47, 16, 200, 172, 186,		// 189 - '1/2'
  6, 85, 53, 31, 40, 52, 250,		// 190 - '3/4'
  5, 48, 72, 77, 64, 32,		// 191 - 'Inverted ?'
  5, 120, 20, 21, 22, 120,		// 192 - 'A grave'
  5, 120, 22, 21, 20, 120,		// 193 - 'A acute'
  5, 122, 21, 21, 21, 122,		// 194 - 'A circumflex'
  5, 120, 22, 21, 22, 121,		// 195 - 'A tilde'
  5, 120, 21, 20, 21, 120,		// 196 - 'A diaresis'
  5, 120, 20, 21, 20, 120,		// 197 - 'A ring above'
  6, 124, 10, 9, 127, 73, 73,		// 198 - 'AE'
  5, 30, 161, 161, 97, 18,		// 199 - 'C sedilla'
  4, 124, 85, 86, 68,		// 200 - 'E grave'
  4, 124, 86, 85, 68,		// 201 - 'E acute'
  4, 126, 85, 85, 70,		// 202 - 'E circumflex'
  4, 124, 85, 84, 69,		// 203 - 'E diaresis'
  3, 68, 125, 70,		// 204 - 'I grave'
  3, 68, 126, 69,		// 205 - 'I acute'
  3, 70, 125, 70,		// 206 - 'I circumplex'
  3, 69, 124, 69,		// 207 - 'I diaresis'
  6, 4, 127, 69, 65, 65, 62,		// 208 - 'Capital Eth'
  5, 124, 10, 17, 34, 125,		// 209 - 'N tilde'
  5, 56, 68, 69, 70, 56,		// 210 - 'O grave'
  5, 56, 70, 69, 68, 56,		// 211 - 'O acute'
  5, 58, 69, 69, 69, 58,		// 212 - 'O circumflex'
  5, 56, 70, 69, 70, 57,		// 213 - 'O tilde'
  5, 56, 69, 68, 69, 56,		// 214 - 'O diaresis'
  5, 34, 20, 8, 20, 34,		// 215 - 'Multiplication sign'
  7, 64, 62, 81, 73, 69, 62, 1,		// 216 - 'O slashed'
  5, 60, 65, 66, 64, 60,		// 217 - 'U grave'
  5, 60, 64, 66, 65, 60,		// 218 - 'U acute'
  5, 58, 65, 65, 65, 58,		// 219 - 'U circumflex'
  5, 60, 65, 64, 65, 60,		// 220 - 'U diaresis'
  5, 12, 16, 98, 17, 12,		// 221 - 'Y acute'
  4, 127, 18, 18, 12,		// 222 - 'Capital thorn'
  4, 254, 37, 37, 26,		// 223 - 'Small letter sharp S'
  5, 32, 84, 85, 122, 64,		// 224 - 'a grave'
  5, 32, 84, 86, 121, 64,		// 225 - 'a acute'
  5, 34, 85, 85, 121, 66,		// 226 - 'a circumflex'
  5, 32, 86, 85, 122, 65,		// 227 - 'a tilde'
  5, 32, 85, 84, 121, 64,		// 228 - 'a diaresis'
  5, 32, 84, 85, 120, 64,		// 229 - 'a ring above'
  7, 32, 84, 84, 124, 84, 84, 8,		// 230 - 'ae'
  5, 24, 36, 164, 228, 40,		// 231 - 'c sedilla'
  5, 56, 84, 85, 86, 88,		// 232 - 'e grave'
  5, 56, 84, 86, 85, 88,		// 233 - 'e acute'
  5, 58, 85, 85, 85, 90,		// 234 - 'e circumflex'
  5, 56, 85, 84, 85, 88,		// 235 - 'e diaresis'
  3, 68, 125, 66,		// 236 - 'i grave'
  3, 68, 126, 65,		// 237 - 'i acute'
  3, 70, 125, 66,		// 238 - 'i circumflex'
  3, 69, 124, 65,		// 239 - 'i diaresis'
  4, 48, 75, 74, 61,		// 240 - 'Small eth'
  4, 122, 9, 10, 113,		// 241 - 'n tilde'
  5, 56, 68, 69, 70, 56,		// 242 - 'o grave'
  5, 56, 70, 69, 68, 56,		// 243 - 'o acute'
  5, 58, 69, 69, 69, 58,		// 244 - 'o circumflex'
  5, 56, 70, 69, 70, 57,		// 245 - 'o tilde'
  5, 56, 69, 68, 69, 56,		// 246 - 'o diaresis'
  5, 8, 8, 42, 8, 8,		// 247 - 'Division sign'
  6, 64, 56, 84, 76, 68, 58,		// 248 - 'o slashed'
  5, 60, 65, 66, 32, 124,		// 249 - 'u grave'
  5, 60, 64, 66, 33, 124,		// 250 - 'u acute'
  5, 58, 65, 65, 33, 122,		// 251 - 'u circumflex'
  5, 60, 65, 64, 33, 124,		// 252 - 'u diaresis'
  4, 156, 162, 161, 124,		// 253 - 'y acute'
  4, 252, 72, 72, 48,		// 254 - 'small thorn'
  4, 157, 160, 160, 125,		// 255 - 'y diaresis'
};

#endif
TelegramBot.cppArduino
I made some changes to the TelegramBot library to get the first name and location of the user. And also a function to convert utf16 to extended ascii for use with the dot matrix.
#include "TelegramBot.h"

TelegramBot::TelegramBot(const char* token, Client &client)	{
	this->client = &client;
	this->token=token;
}


void TelegramBot::begin()	{
	if(!client->connected()){
		client->connect(HOST, SSL_PORT);
	}
}

/************************************************************************************
 * GetUpdates - function to receive messages from telegram as a Json and parse them *
 ************************************************************************************/
message TelegramBot::getUpdates()  {
		begin();

		//Send your request to api.telegram.org
		String getRequest = "GET /bot"+String(token)+"/getUpdates?limit=1&offset="+String(last_message_recived)+" HTTP/1.1";
		client->println(getRequest);
		client->println("User-Agent: curl/7.37.1");
		client->println("Host: api.telegram.org");
		client->println("Accept: */*");
		client->println();

		String payload = readPayload();
	    if (payload != "") {
			message m;
			StaticJsonBuffer<JSON_BUFF_SIZE> jsonBuffer;
			JsonObject & root = jsonBuffer.parseObject(payload);



			if(root.success()){

				int update_id = root["result"][0]["update_id"];
				update_id = update_id+1;

				if(last_message_recived != update_id ){
					String sender = root["result"][0]["message"]["from"]["username"];
					String text = root["result"][0]["message"]["text"];
					String chat_id = root["result"][0]["message"]["chat"]["id"];
					String date = root["result"][0]["message"]["date"];
                    String first_name = root["result"][0]["message"]["from"]["first_name"];
                    String lon = root["result"][0]["message"]["location"]["longitude"];
                    String lat = root["result"][0]["message"]["location"]["latitude"];

					m.sender = sender;
					m.text = text;
					m.chat_id = chat_id;
					m.date = date;
                    m.first_name = first_name;
                    m.location.lon = lon;
                    m.location.lat = lat;
                    
					last_message_recived=update_id;
					return m;
				}else{
					m.chat_id = "";
					return m;
				}
			}
			else{
				Serial.println("");
				Serial.println("Message too long, skipped.");
				Serial.println("");
				int update_id_first_digit=0;
				int update_id_last_digit=0;
				for(int a =0; a<3; a++){
					update_id_first_digit= payload.indexOf(':',update_id_first_digit+1);
				}
				for(int a =0; a<2; a++){
					update_id_last_digit= payload.indexOf(',',update_id_last_digit+1);
				}
			last_message_recived = payload.substring(update_id_first_digit+1,update_id_last_digit).toInt() +1;
			}
		}
	}

// send message function
// send a simple text message to a telegram char
String TelegramBot::sendMessage(String chat_id, String text)  {
	if(chat_id!="0" && chat_id!=""){
		StaticJsonBuffer<JSON_BUFF_SIZE> jsonBuffer;
		JsonObject& buff = jsonBuffer.createObject();
		buff["chat_id"] = chat_id;
		buff["text"] = text;

		String msg;
		buff.printTo(msg);
		return postMessage(msg);
	} else {
		Serial.println("Chat_id not defined");
	}
}

// send a message to a telegram chat with a reply markup
String TelegramBot::sendMessage(String chat_id, String text, TelegramKeyboard &keyboard_markup, bool one_time_keyboard, bool resize_keyboard)  {
		StaticJsonBuffer<JSON_BUFF_SIZE> jsonBuffer;
		JsonObject& buff = jsonBuffer.createObject();
		buff["chat_id"] = chat_id;
		buff["text"] = text;

		JsonObject& reply_markup = buff.createNestedObject("reply_markup");
		JsonArray& keyboard = reply_markup.createNestedArray("keyboard");

		for (int a = 1 ; a <= keyboard_markup.length() ; a++){
			JsonArray& row = keyboard.createNestedArray();
				for( int b = 1; b <= keyboard_markup.rowSize(a) ; b++){
					row.add(keyboard_markup.getButton(a,b));
				}
		}

		reply_markup.set<bool>("one_time_keyboard", one_time_keyboard);
		reply_markup.set<bool>("resize_keyboard", resize_keyboard);
		reply_markup.set<bool>("selective", false);

		String msg;
		buff.printTo(msg);
		// Serial.println(msg);
		return postMessage(msg);
}

// gets the telegram json string
// posts the message to telegram
// returns the payload
String TelegramBot::postMessage(String msg) {
		begin();

		client->println("POST /bot"+String(token)+"/sendMessage"+" HTTP/1.1");
		client->println("Host: api.telegram.org");
	    client->println("Content-Type: application/json");
	    client->println("Connection: close");
	    client->print("Content-Length: ");
	    client->println(msg.length());
	    client->println();
	    client->println(msg);

		return readPayload();
}

// reads the payload coming from telegram server
// returns the payload string
String TelegramBot::readPayload(){
	char c;
	String payload="";
		//Read the answer and save it in String payload
		while (client->connected()) {
		payload = client->readStringUntil('\n');
		if (payload == "\r") {
			break;
		 }
	  }
	payload = client->readStringUntil('\r');
	// Serial.println(payload);
	return convertText(payload);
}

String TelegramBot::convertText(String unicodeStr){ //converts UTF-16 Unicode to extended Ascii. Emojis not supported
  String out = "";
  int len = unicodeStr.length();
  char iChar;
  char* error;
  for (int i = 0; i < len; i++){
     iChar = unicodeStr[i];
     if(iChar == '\\'){ // got escape char
       iChar = unicodeStr[++i];
       if(iChar == 'u'){ // got unicode hex
         char unicode[6];
         unicode[0] = '0';
         unicode[1] = 'x';
         for (int j = 0; j < 4; j++){
           iChar = unicodeStr[++i];
           unicode[j + 2] = iChar;
         }
         long unicodeVal = strtol(unicode, &error, 16); //convert the string
         if (unicodeVal == 0x20AC) unicodeVal = 0x80; // euro sign
         //Serial.print("strtol: ");
         //Serial.println(unicodeVal, HEX);
         out += (char)unicodeVal;
       } else if(iChar == '/'){
         out += iChar;
       } else if(iChar == 'n'){
         out += '\n';
       }
     } else if (iChar == '<' && unicodeStr[i + 1] == '3') { //convert "<3" to costum set 'heart' char, ascii: 0x81
         long charTemp = 0x81;
         out += (char)charTemp;
         i++;
     } else {
       out += iChar;
     }
  }
  return out;
}
TelegramBot.hArduino
Adapt to changed *.cpp
// Copyright Casa Jasmina 2016
// LGPL License
//
// TelegramBot library
// https://github.com/CasaJasmina/TelegramBot-Library

#ifndef TelegramBot_h
#define TelegramBot_h

#include <Arduino.h>
#include <ArduinoJson.h>
#include <Client.h>
#include <TelegramKeyboard.h>

#define HOST "api.telegram.org"
#define SSL_PORT 443
#define JSON_BUFF_SIZE 10000

struct locationStruct{
    String lon;
    String lat;
};

struct message{
  String text;
  String chat_id;
  String sender;
  String first_name;
  String date;
  locationStruct location;
};


class TelegramBot
{
  public:
    TelegramBot(const char* token, Client &client);
  	void begin();
    String sendMessage(String chat_id, String text);
    String sendMessage(String chat_id, String text, TelegramKeyboard &keyboard_markup, bool one_time_keyboard = true, bool resize_keyboard = true);
    String postMessage(String msg);
    message getUpdates();
    

  private:
      String readPayload();
      String convertText(String unicodeStr);
      const char* token;
      
      int last_message_recived;

      Client *client;
};

#endif

Schematics

Basic connections and data exchange.
Our basic concept of how this whole mess works.
Svggraph (5) qdn4mnuc5z

Comments

Similar projects you might like

Smart Bird Feeder

Project showcase by ESIEE-Amiens Students

  • 700 views
  • 8 comments
  • 7 respects

Smart Plant IoT

Project tutorial by Peter Ma and Shin Ae Hong

  • 1,852 views
  • 0 comments
  • 10 respects

Autopilot Drone

Project in progress by suhaskd

  • 516 views
  • 0 comments
  • 9 respects

Stringent, the $15 Wall Plotter

Project tutorial by Fredrik Stridsman

  • 14,580 views
  • 18 comments
  • 86 respects
Add projectSign up / Login