🤖Have you ever tried Chat.M5Stack.com before asking??😎
    M5Stack Community
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    • Register
    • Login

    M5Stick->SIM7080-> MQTT->Thingspeak-->Sample code

    Scheduled Pinned Locked Moved Modules
    1 Posts 1 Posters 2.1k Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H Offline
      HappyUser
      last edited by

      Dear all,
      Please let me share with you my sample code. Needs cleaning up, but it could be a good example for those that are interested in using the SIM7080 module.

      #include <M5StickC.h>
      #include "AXP192.h"
      #include "DHT12.h"
      #include <Wire.h>
      #include "Adafruit_Sensor.h"
      #include <Adafruit_BMP280.h> // Dit is de pressure sensor. https://cdn-shop.adafruit.com/datasheets/BST-BMP280-DS001-11.pdf
      //#include "bmm150.h"
      //#include "bmm150_defs.h"
      //#include "M5_SIM7080G.h"

      DHT12 dht12;
      //BMM150 bmm = BMM150();
      //bmm150_mag_data value_offset;
      Adafruit_BMP280 bme;

      float tmp = 0.0;
      float hum = 0.0;
      float pressure = 0.0;
      float BatVoltage;

      String ClientId="";
      String Username="
      ";
      String Password="****";
      String ChannelId="*****";

      const int buffer_size=5000;
      char Receive_buffer[buffer_size];
      long time_taken;

      char date_time_char_array[50];
      char date_time_char_array_processed[50];
      const char allowed_char[]="0123456789/,:+-";

      typedef struct {
      int Second;
      int Minute;
      int Hour;
      int Day;
      int Month;
      int Year; // offset from 1970;
      int Timezone;
      } Date_time_struct;

      Date_time_struct Thisday;
      bool DateTime_Error=false;
      char buf[10];

      uint8_t setup_flag = 1;

      const long SIMWaitTime=10000;
      const long Delay_between_commands=300;

      int RSSI;

      char* Pntr;
      char* Pntr_1;
      char* Pntr_2;

      char IP_buffer[20]; // hier komt het toegewezen IP adress in

      char itoa_buffer[10];
      #define max_float_digits 20 //7 // including NULL let op, in retrieve data gebruik ik precision 4 ipv 2
      #define dtostrf_precision 4
      #define dtostrf_leader 15

      String Date_payload; //="&created_at=2023-05-11 09:20:59";

      #define uS_TO_S_FACTOR 1000000 /* Conversion factor for micro seconds to seconds */

      bool Wakeup_Other_Cause;
      bool Wakeup_Through_Button;
      bool Wakeup_Through_Time;
      int Sleep_Period=10;

      int Sleep_Period_After_Error=10;

      int Error_Code;

      //#define BMM150_CHIP_ID_ADDR 0x76

      void setup() {
      // put your setup code here, to run once:
      M5.begin(false,true,true);
      Serial.begin(115200);
      delay(300);

      M5.Axp.SetLDO2(false);
      M5.Axp.SetLDO3(false);

      Wire.end();
      //delay(300);
      Wire.begin(0,26);
      Serial.println();
      Serial.println();
      Serial.println();
      Serial.println();

      print_wakeup_reason();

      esp_sleep_wakeup_cause_t wakeup_reason;
      wakeup_reason = esp_sleep_get_wakeup_cause();
      Wakeup_Other_Cause=false;
      Wakeup_Through_Button=false;
      Wakeup_Through_Time=false;

       switch(wakeup_reason)
         {
           case ESP_SLEEP_WAKEUP_EXT0 :  Wakeup_Through_Button=true;break;
           case ESP_SLEEP_WAKEUP_EXT1 :  break;  // nu toch de server
           case ESP_SLEEP_WAKEUP_TIMER : Wakeup_Through_Time=true;break;
           case ESP_SLEEP_WAKEUP_TOUCHPAD : break;
           case ESP_SLEEP_WAKEUP_ULP :break;
           default : Wakeup_Other_Cause=true; break;
      

      }

      //M5.Lcd.setRotation(3);
      //M5.Lcd.fillScreen(BLACK);
      //M5.Lcd.setCursor(0, 0, 2);
      //M5.Lcd.println("ENV TEST");
      //pinMode(M5_BUTTON_HOME, INPUT);

      Serial.println("Init doing");
      /*
      if(bmm.initialize() == BMM150_E_ID_NOT_CONFORM) {
      Serial.println("Chip ID can not read!");
      while(1);
      } else {
      Serial.println("Initialize done!");
      }
      */

      if (!bme.begin(0x76)){
      Serial.println("Could not find a valid BMP280 sensor, check wiring!");
      while (1);
      }

      tmp = dht12.readTemperature();
      hum = dht12.readHumidity();
      pressure = bme.readPressure();
      BatVoltage=M5.Axp.GetBatVoltage();

      Serial.println(tmp);
      Serial.println(hum);
      Serial.println(pressure);
      Serial.println(BatVoltage);

      //while (1) {delay(300);}

      Serial2.begin(115200, SERIAL_8N1,33,32); // M5 Stick Port B

      Serial2.print("AT+IPR=115200\r"); //
      Read_Response_WaitFor("OK",SIMWaitTime);
      delay(Delay_between_commands);

      Serial2.print("AT+CREBOOT\r"); //
      Read_Response_WaitFor("OK",30000);
      delay(Delay_between_commands);
      //Read_Response_OK(3000);
      Serial.println(Receive_buffer);

      Serial2.print("ATZ\r"); // Request TA Serial Number Identification(IMEI)
      Read_Response_WaitFor("OK",SIMWaitTime);
      delay(Delay_between_commands);
      //Read_Response_OK(3000);
      Serial.println(Receive_buffer);

      Serial2.print("AT+GSN\r"); // Request TA Serial Number Identification(IMEI)
      Read_Response_WaitFor("OK",SIMWaitTime);
      delay(Delay_between_commands);
      //Read_Response_OK(3000);
      Serial.println(Receive_buffer);

      int max_trials=5;
      Error_Code=0;

      while (1) {

      //Serial.print("********************************* Trail : ");
      //Serial.println(max_trials);

      //************************************************** READ SMS

      /*
      Signal Strength General Results
      -50 to -79 dBm Considered great signal (4 to 5 bars)
      -80 to -89 dBm Considered good signal (3 to 4 bars)
      -90 to -99 dBm Considered average signal (2 to 3 bars)
      -100 to -109 dBm Considered poor signal (1 to 2 bars)
      -110 to -120 dBm Considered very poor signal (0 to 1 bar)

      */

      Serial2.print("AT+CSQ\r"); // Clock returns *PSUTTZ: 23/04/14,11:56:48","+08",1 time zone in quarters of an hour
      Read_Response(2000); //Read_Response_WaitFor("OK",SIMWaitTime);
      //Read_Response_OK(2000);
      Serial.println(Receive_buffer);

      RSSI=0;
      Pntr=strstr(Receive_buffer,"+CSQ:");
      if (Pntr!=NULL)
      {
      Pntr_1=strtok(Pntr,":,");
      if (Pntr_1!=NULL);
      {
      Pntr_1=strtok(NULL,":,");
      if (Pntr_1!=NULL)
      {
      //Serial.println(Pntr_1);
      RSSI=atoi(Pntr_1);
      switch (RSSI) {
      case 0: RSSI=-115;break;
      case 1: RSSI=-111;break;
      case 99 : RSSI=99;break;
      default : RSSI=-112+RSSI;break;

                   }
      
                }
          }
      
      }
      

      Serial.print("Rssi : ");
      Serial.println(RSSI);

      if (RSSI==0 or RSSI==99)
      {
      Serial.println();
      Serial.println();
      Serial.println("********************************************* NO SIGNAL");
      Serial.println("********************************************* Going to sleep");
      Serial.println();
      Serial.println();
      Error_Code=1;
      break;

         //esp_sleep_enable_timer_wakeup(60 * uS_TO_S_FACTOR);
         //esp_deep_sleep_start();
      }
      

      Serial2.print("AT+CLTS=1\r"); // Get local timestamp
      Read_Response_WaitFor("OK",SIMWaitTime);
      //Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+CCLK?\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      //Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      if (Process_DateandTime(false)) {Error_Code=4;}

      //AT+CSSLCFG="SSLVERSIO N",<ctxindex>,<sslversion>

      Serial2.print("AT+CSSLCFG="SSLVERSION",0,3\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+CNACT=0,1\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+CNACT?\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      if (!GET_IP_Address ()) 
         {
            Serial.println("No IP address");
            Serial.println("Program stalled");
            Error_Code=2;
            break;
            //while (1) {delay(300);}
        }
      

      Serial2.print("AT+SMCONF="URL","mqtt3.thingspeak.com","1883"\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="KEEPTIME",60\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="CLEANSS",1\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="QOS",0\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="TOPIC","My Topic"\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="MESSAGE","My Message"\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="RETAIN",0\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONF="SUBHEX",0\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial.println("AT+SMCONF="CLIENTID",""+ClientId+""\r");
      Serial2.print("AT+SMCONF="CLIENTID",""+ClientId+""\r");

      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial.println("AT+SMCONF="USERNAME",""+Username+""\r");
      Serial2.print("AT+SMCONF="USERNAME",""+Username+""\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial.println("AT+SMCONF="PASSWORD",""+ Password+ ""\r");
      Serial2.print("AT+SMCONF="PASSWORD",""+ Password+ ""\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      Serial2.print("AT+SMCONN\r");
      Read_Response_WaitFor("OK",SIMWaitTime);
      Serial.println(Receive_buffer);
      delay(Delay_between_commands);

      if(strstr(Receive_buffer,"ERROR") != NULL)
      {
      Serial.println("SMCONN ERROR " );
      Error_Code=3;
      break;
      }
      else
      {
      Serial.println("We have a MQTT connection");
      // Format
      // https://nl.mathworks.com/help/thingspeak/publishtoachannelfeed.html
      // created_at=2014-12-31 23:59:59

      /*
      In the Payload pane, use the following settings:

      Topic: channels/33301/publish
      Payload: field1=45&field2=60&status=MQTTPUBLISH
      This PUBLISH message publishes a value of 45 to field1 and 60 to field2 of channel 33301, along with a status message MQTTPUBLISH.

      lengte van de payload nog
      */
      //String Date_payload="&created_at=2023-05-11 09:20:59";
      String payload;

              payload="field1="+String(int(pressure))+"&field2="+String(int(tmp))+"&field3="+String(int(hum))+"&field4="+String(int(BatVoltage*100))+Date_payload;
              String Mqtt_Message;
      
              int Payload_Length;
              Payload_Length=payload.length();
              Serial.print(payload);
              Serial.print ("   ");
              Serial.println(Payload_Length);
              //Mqtt_Message="AT+SMPUB=\"channels/888617/publish\","+String(Payload_Length)+",1,1\r";
      
              Mqtt_Message="AT+SMPUB=\"channels/"+ ChannelId+"/publish\","+String(Payload_Length)+",1,1\r";
              Serial.println(Mqtt_Message);
              Serial2.print(Mqtt_Message);
              //Serial2.print("AT+SMPUB=\"channels/888617/publish/fields/field1\",1,1,1\r");   // lengte moet goed zijn      
              Read_Response_WaitFor("OK",SIMWaitTime);
              Serial.println(Receive_buffer);
              delay(Delay_between_commands); 
              String Str;
              //pressure=12;
              Serial.println(pressure);
              Str=String(int(pressure/100))+"\r";
              Serial.println(Str);
              //Serial2.print("9\r");
              Serial2.print(payload);
              //Serial2.print(Str);        
              Read_Response_WaitFor("OK",SIMWaitTime);
              Serial.println(Receive_buffer);
              delay(Delay_between_commands); 
              break;
          }
      

      } //end while

      Serial.print("Done with error code : ");
      Serial.println(Error_Code);

      if (Error_Code==0)
      {
      esp_sleep_enable_timer_wakeup(Sleep_Period * uS_TO_S_FACTOR);
      esp_deep_sleep_start();
      }
      else
      {
      esp_sleep_enable_timer_wakeup(Sleep_Period_After_Error * uS_TO_S_FACTOR);
      esp_deep_sleep_start();
      }

      while (1) {delay(300);}

      } // end of setup

      void loop()
      {

      }

      bool Read_Response(long time_out) {
      int k=0;
      long wait_until;
      char ccc;
      //while (!Serial2.available() and millis()<wait_until) {delay(10);}
      wait_until=millis()+time_out;
      while (millis()<wait_until)
      {
      while (Serial2.available())
      {
      ccc=Serial2.read();
      //Serial.print(ccc);
      if (k<buffer_size-1)
      {
      Receive_buffer[k]=ccc;
      k++;
      }
      }
      Receive_buffer[k]=NULL;
      //if (strstr(Receive_buffer,"OK")!=NULL)
      // {
      //Serial.println("OK found");
      // SIM7600_Error=0;
      //break;
      //}
      }
      //if (k>=buffer_size-1) {SIM7600_Error=1; strcpy(Receive_buffer,SIM7600_Error_list[SIM7600_Error].c_str());return;}
      //if (millis()>=wait_until) {SIM7600_Error=2; strcpy(Receive_buffer,SIM7600_Error_list[SIM7600_Error].c_str()); return;}
      Receive_buffer[k]=NULL;
      if (strstr(Receive_buffer,"ERROR")!=NULL) {return true;} else {return false;}
      }

      bool Read_Response_WaitFor(char* StopCharArray,long time_out) { // false als niet gevonden
      int k=0;
      long wait_until;
      char ccc;
      //while (!Serial2.available() and millis()<wait_until) {delay(10);}
      time_taken=millis();
      wait_until=time_taken+time_out;
      while (millis()<wait_until)
      {
      while (Serial2.available())
      {
      ccc=Serial2.read();
      //Serial.print(ccc);
      if (k<buffer_size-1)
      {
      Receive_buffer[k]=ccc;
      k++;
      }
      }
      Receive_buffer[k]=NULL;
      if (strstr(Receive_buffer,StopCharArray)!=NULL)
      {
      //Serial.println("OK found");
      // SIM7600_Error=0;
      Receive_buffer[k]=NULL;
      return false;
      }
      }
      //if (k>=buffer_size-1) {SIM7600_Error=1; strcpy(Receive_buffer,SIM7600_Error_list[SIM7600_Error].c_str());return;}
      //if (millis()>=wait_until) {SIM7600_Error=2; strcpy(Receive_buffer,SIM7600_Error_list[SIM7600_Error].c_str()); return;}
      Receive_buffer[k]=NULL;
      time_taken=millis()-time_taken;
      Serial.println();
      Serial.print("1] This operation took ");
      Serial.print(time_taken);
      Serial.println(" millisec");
      return true; //if (strstr(Receive_buffer,"ERROR")!=NULL) {return true;} else {return false;}
      }

      void clear_Receive_buffer(){
      for (int i=0;i<buffer_size;i++) {Receive_buffer[i]=NULL;}
      }

      bool Process_DateandTime(bool PSUTZ) {
      char *Process_ptr;
      bool Date_Error;
      Date_Error=false;

      //Process_ptr=strstr(Receive_buffer,"CCLK:");
      if (PSUTZ) {Process_ptr=strstr(Receive_buffer,"PSUTTZ:");}
      else {Process_ptr=strstr(Receive_buffer,"CCLK:");}

      if (Process_ptr!=NULL)
      {

         //strcpy(date_time_char_array,"PSUTTZ: 23/04/14,11:56:48\",\"+08\",1");
         //strcat(date_time_char_array,"\0");
        //Serial.println(date_time_char_array);
         Serial.println(Process_ptr);
         strcpy(date_time_char_array,Process_ptr);
      
        Serial.println(date_time_char_array);
      
       int k=0;
       for (int i=0; i<strlen(date_time_char_array); i++)
         {
            if (strchr(allowed_char,date_time_char_array[i])!=NULL)
             {
               date_time_char_array_processed[k]=date_time_char_array[i];
               k++;
             }
         }
      
      
      
        Serial.println(date_time_char_array_processed); 
      
      
      
        Process_ptr=date_time_char_array_processed+1;
        Serial.println(Process_ptr);
         while (1)
           {
             Process_ptr=strtok(Process_ptr,"/,:");     // 23/04/14,11:56:48,+08,1
             if (Process_ptr==NULL) {DateTime_Error=true;break;}
             Thisday.Year=atoi(Process_ptr);
      
              Process_ptr=strtok(NULL,"/,:");     // 23/04/14,11:56:48,+08,1
              if (Process_ptr==NULL) {DateTime_Error=true;break;}
              Thisday.Month=atoi(Process_ptr);
      
              Process_ptr=strtok(NULL,"/,:");     // 23/04/14,11:56:48,+08,1
               if (Process_ptr==NULL) {DateTime_Error=true;break;}
                Thisday.Day=atoi(Process_ptr);
      
      
              Process_ptr=strtok(NULL,"/,:");     // 23/04/14,11:56:48,+08,1
               if (Process_ptr==NULL) {DateTime_Error=true;break;}
               Thisday.Hour=atoi(Process_ptr);
      
               Process_ptr=strtok(NULL,"/,:");     // 23/04/14,11:56:48,+08,1
               if (Process_ptr==NULL) {DateTime_Error=true;break;}
               Thisday.Minute=atoi(Process_ptr);
      
               Process_ptr=strtok(NULL,"/,:");     // 23/04/14,11:56:48,+08,1
              if (Process_ptr==NULL) {DateTime_Error=true;break;}
              Thisday.Second=atoi(Process_ptr);
      
               Process_ptr=strtok(NULL,"/,:");     // 23/04/14,11:56:48,+08,1
                if (Process_ptr==NULL) {DateTime_Error=true;break;}
                Thisday.Timezone=atoi(Process_ptr);
                Thisday.Hour=Thisday.Hour+(Thisday.Timezone/4);
      
      
               break;
      
             }
      
      
       
             //Append_this_Struct.Photo_Taken=SetDateTime(Thisday.Hour,Thisday.Minute,Thisday.Second, Thisday.Day,Thisday.Month,(Thisday.Year+30));    // CurrentYear (2023-1970) == 55 yea 23 plus 32 dus
             //print_time_t(Append_this_Struct.Photo_Taken);
             Serial.println();
             Serial.print(Thisday.Year);
             Serial.print("/");
             Serial.print( Thisday.Month);
             Serial.print("/");
             Serial.print( Thisday.Day);
             Serial.print(",");
             Serial.print(Thisday.Hour);
             Serial.print(":");
             Serial.print( Thisday.Minute);
             Serial.print(":");
             Serial.print( Thisday.Second);
             Serial.print(",");
             Serial.println( Thisday.Timezone);
      
             
      
             Date_payload="&created_at=20"+String(Thisday.Year)+"-"+Int_to_2_Digits(Thisday.Month)+"-"+Int_to_2_Digits(Thisday.Day)+" "+Int_to_2_Digits(Thisday.Hour)+":"+Int_to_2_Digits(Thisday.Minute)+":"+Int_to_2_Digits(Thisday.Second);
             Serial.println(Date_payload);
                //Serial.println("Done with it");
               //while (1) {delay(300);}
      
             if (Thisday.Year<23)   {Date_Error=true; }
             if (Thisday.Month<0 or Thisday.Month>12 )   {Date_Error=true; }
             if (Thisday.Day<0 or Thisday.Month>31 )   {Date_Error=true; }
             if (Thisday.Hour<0 or Thisday.Hour>12 )   {Date_Error=true; }
             if (Thisday.Minute<0 or Thisday.Minute>60 )   {Date_Error=true; }
             if (Thisday.Second<0 or Thisday.Second>60 )   {Date_Error=true; }
      }
      

      else
      {
      //strcpy(fnameJPG,"AA00000001012011.JPG");
      Date_Error=true;
      }

      return Date_Error;
      }

      String Int_to_2_Digits(int this_int){

      if (this_int<10)
      {return("0"+String(this_int));}
      else
      {return(String(this_int));}

      }

      bool GET_IP_Address () {

      char *p0;
      char *p1;
      int span;
      //Serial.println("GET_IP_Address");

          //strcpy(Receive_buffer,"CHOPEN: 0,\"aap.noot.mies\"");
          //strcpy(Receive_buffer,"CHOPEN: 0,\"\"");
           p0=strstr(Receive_buffer,"CNACT: ");
           if (p0!=NULL)
             {
               p0=p0+strlen("CNACT: ");
               p1=strstr(p0,"\n");
               if (p1!=NULL)
                 {
                   span=min(19,p1-p0);
                   strncpy(IP_buffer, p0, span);
                   IP_buffer[span]='\0';
                   Serial.print("IP_buffer : ");
                   Serial.println(IP_buffer);
                   Serial.print(" : ");
                   Serial.println(strlen(IP_buffer));
                   if (strlen(IP_buffer) >8) {return true;} else {return false;}
                 }
               else
                { 
                  return (false);
                  Serial.println("No IP address");
                }
             }
           else
             {
               return (false);
               //$Serial.println("No IP address");
             }
      

      }

      void print_wakeup_reason(){
      esp_sleep_wakeup_cause_t wakeup_reason;

      wakeup_reason = esp_sleep_get_wakeup_cause();

      switch(wakeup_reason)
      {
      case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
      case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
      case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
      case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
      case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
      default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
      }
      }

      1 Reply Last reply Reply Quote 0

      Hello! It looks like you're interested in this conversation, but you don't have an account yet.

      Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.

      With your input, this post could be even better 💗

      Register Login
      • First post
        Last post