How to send data only on change to reduce traffic

I have built a device to control a winemaking process and I can connect and send variable updates from the Android app to the ESP32 controller.
I am however concerned that time is spent sending from device to cloud and cloud to device even when nothing changes.
I see on the app widgets some have setting to send only on release, but seems that others sync on every Virtual Pin Write or Virtual Pin Read.
Is there a smart way to only send/read update only of there is a change to one or more values?
Should I set a flag if a value changes but this would need tracking of old value and new value for each parameter and seems complex?

Appreciate any guidance as all are more experienced than I am.

#define BLYNK_TEMPLATE_ID 
#define BLYNK_TEMPLATE_NAME
#define BLYNK_FIRMWARE_VERSION "0.1.0"

#define BLYNK_PRINT Serial

//#define BLYNK_DEBUG
#define APP_DEBUG

#include "BlynkEdgent.h"
#include <FS.h>
#include <ezTime.h>
#include <TFT_eWidget.h>  // Widget library
#include <SPI.h>
#include <TFT_eSPI.h>    // Hardware-specific library
#include "Free_Fonts.h"  // Include the header file attached to this sketch

TFT_eSPI tft = TFT_eSPI();  // Invoke custom library

Timezone local;
// This function creates the timer object. It's part of Blynk library
BlynkTimer timer;

#define CALIBRATION_FILE "/TouchCalData1"
bool REPEAT_CAL = false;

// Hardware definitions
#define LED_PIN 22  // On board LED

// Software Definitions
#define BUTTON_W 120
#define BUTTON_H 40
#define BUTTON_PAD 20

BLYNK_CONNECTED() {                      // Send requests for internal data when connected
  Blynk.sendInternal("utc", "time");     // Unix timestamp (with msecs)
  Blynk.sendInternal("utc", "tz_rule");  // POSIX TZ rule
}
// Receive UTC data from cloud
BLYNK_WRITE(InternalPinUTC) {
  String cmd = param[0].asStr();
  if (cmd == "time") {
    const uint64_t utc_time = param[1].asLongLong();
    UTC.setTime(utc_time / 1000, utc_time % 1000);
    Serial.print("Unix time (UTC): ");
    Serial.println(utc_time);
  } else if (cmd == "tz_rule") {
    String tz_rule = param[1].asStr();
    local.setPosix(tz_rule);
    Serial.print("POSIX TZ rule:   ");
    Serial.println(tz_rule);
  }
}

/*------------------------------------------------------------------------------------------------------------------------------*/
// Cloud variables
String DateTimeCurrent;        // VO --> Current date and time
byte CycleFrequency[4];        // V1 --> Cycle frequency # times per day 1-24
float TempCurrent = 73.2;      // V2 --> Current temp
float TimerCountdown;          // V3 --> hh:mm:ss remaining in cycle
String CycleReason = "Power";  // V4 --> Reason for punch, Powere, Auto, Manual, Temp
byte CycleTimeDown[4];         // V5 --> Seconds down 10 - 255
byte CycleTimeUp[4];           // V6 --> Seconds up 10 - 255
byte NumCyclesTime[4];         // V7 --> Number of cycles when Time triggered
byte NumCyclesTemp[4];         // V8 --> Number of cycles when Temp triggered
bool Mode;                     // V9 --> 0 = Manual, 1 = Auto
int Phase;                     // V10 -> Enumerated Phase code 0,1,2,3
bool Shake;                    // V11 -> 0 = No Shake, 1 = Shake
int Pressure;                  // V12 -> Pressue psi 0-255
bool State;                    // V13 -> 0 = paused, 1 = Running
byte TempDwell[4];             // V14 -> 0-255 minutes delay between Temp punch events
float TempMax;                 // V15 -> Recorded Max Temp until cleared
byte TempThreshold[4];         // V16 -> Trigger point to start temp plunge
bool Reset;                    // V17 -> Reset max temp
/*------------------------------------------------------------------------------------------------------------------------------*/

// Working Variables
int ScreenNo = 1;     // 1 = home, 2 = config
float Timer;          // temporary to test and will removed.
unsigned long Time1;  // temporary to test and will removed.
unsigned long Time2;  // temporary to test and will removed.

ButtonWidget btn_Mode = ButtonWidget(&tft);      // Auto-Manual
ButtonWidget btn_State = ButtonWidget(&tft);     // Running-Ready-Paused
ButtonWidget btn_Profile = ButtonWidget(&tft);   // Innoc-Lag-Active-Finish
ButtonWidget btn_Config = ButtonWidget(&tft);    // Calibrate-Profile update
ButtonWidget btn_Increase = ButtonWidget(&tft);  // Can this be a generic increase for selected button???
ButtonWidget btn_Decrease = ButtonWidget(&tft);
ButtonWidget btn_Calibrate = ButtonWidget(&tft);  // Calibrate screen touch

// Create an array of button instances to use in display loops
ButtonWidget *btn[] = { &btn_Mode, &btn_State, &btn_Profile, &btn_Config, &btn_Increase, &btn_Decrease, &btn_Calibrate };
uint8_t buttonCount = sizeof(btn) / sizeof(btn[0]);              // How many buttons
const char *Profile[] = { "Innoc", "Active", "Lag", "Finish" };  // Button names
//int Pn = 0;

// Get parameters from platform
// No need to get V0, V2, V3, V4, V12, V15 as they are only sent to client
BLYNK_WRITE(V1) {
  CycleFrequency[Phase] = param.asInt();
  DrawScreen();
}
BLYNK_WRITE(V5) {
  CycleTimeDown[Phase] = param.asInt();
  DrawScreen();
}
BLYNK_WRITE(V6) {
  CycleTimeUp[Phase] = param.asInt();
  DrawScreen();
}
BLYNK_WRITE(V7) {
  NumCyclesTime[Phase] = param.asInt();
}
BLYNK_WRITE(V8) {
  NumCyclesTemp[Phase] = param.asInt();
}
BLYNK_WRITE(V9) {
  Mode = param.asInt();
}
BLYNK_WRITE(V10) {        // Get enumerated Phase from Cloud
  Phase = param.asInt();  // enumerated value for 0,1,2,3
  Serial.println(Phase);
}
BLYNK_WRITE(V11) {
  Shake = param.asInt();
}
BLYNK_WRITE(V13) {
  State = param.asInt();
}
BLYNK_WRITE(V14) {
  TempDwell[Phase] = param.asInt();
}
BLYNK_WRITE(V16) {
  TempThreshold[Phase] = param.asInt();
}
BLYNK_WRITE(V17) {  // Get Reset from Cloud
  Reset = param.asInt();
  if (Reset == 1) {
    TimerCountdown = 120.0;
    TempCurrent = 69.0;
  }
}

void setup() {
  Serial.begin(115200);                    // Start Serial
  timer.setInterval(1000L, secTimer);      // Set up second event timer
  timer.setInterval(60000L, minTimer);     // Set up minute event timer
  while (!Serial) { ; }                    // Wait incase  serial is not ready
  tft.begin();                             // Start screen
  tft.setRotation(1);                      // Set screen as Landscape
  tft.fillScreen(TFT_BLACK);               // Clear SCreen
  tft.setTextColor(TFT_GREEN, TFT_BLACK);  // Set text color
  touch_calibrate();                       // Check and calibrate the touch screen if required and retrieve the scaling factors
  BlynkEdgent.begin();                     // Start Blynk Agent
  initButtons();                                  // Initialize buttons
  Serial.print("Buttons = ");
  Serial.println(buttonCount);
}

void loop() {
  BlynkEdgent.run();  // runs Blynk Agent
  timer.run();        // runs BlynkTimer
  waitForSync();      // Wait until time has been set by eztime
  static uint32_t scanTime = millis();
  uint16_t t_x = 9999, t_y = 9999;  // To store default unused touch coordinates

  // Scan keys every 50ms at most -- CHange to Timer interupt
  if (millis() - scanTime >= 50) {
    // Pressed will be set true if there is a valid touch on the screen
    bool pressed = tft.getTouch(&t_x, &t_y);

    scanTime = millis();
    if (ScreenNo = 1) {
      for (uint8_t b = 0; b < buttonCount; b++) {
        if (pressed) {
          if (btn[b]->contains(t_x, t_y)) {
            btn[b]->press(true);
            btn[b]->pressAction();
            Serial.println(ScreenNo);
          }
        } else {
          btn[b]->press(false);
          btn[b]->releaseAction();
        }
      }
    }
  }
  //-------------------------------------------------------------------------
  // Other stuff
  // Get Temp
  // Get pressure
  // Plunge if time or temp
}


void secTimer() {  // This function describes what will happen with each timer tick - Send all necessary data to BLYNK cloud
  Blynk.beginGroup();  // Send data to cloud
  Blynk.virtualWrite(V1, CycleFrequency[Phase]);
  Blynk.virtualWrite(V16, TempThreshold[Phase]);
  Blynk.virtualWrite(V5, CycleTimeDown[Phase]);
  Blynk.virtualWrite(V6, CycleTimeUp[Phase]);

  Blynk.virtualWrite(V7, NumCyclesTime[Phase]);
  Blynk.virtualWrite(V8, NumCyclesTemp[Phase]);
  Blynk.virtualWrite(V14, TempDwell[Phase]);

  Blynk.virtualWrite(V2, TempCurrent);
  Blynk.virtualWrite(V3, TimerCountdown);

  Blynk.virtualWrite(V9, Mode);
  Blynk.virtualWrite(V10, Phase);
  Blynk.virtualWrite(V11, Shake);
  Blynk.virtualWrite(V13, State);
  Blynk.virtualWrite(V17, Reset);
  Blynk.endGroup();

  // Update the time display on the TFT screen
  DrawScreen();
}

void minTimer() {
  //   if (minuteChanged()) WriteToSomeDisplay(UTC.dateTime("H:i"));  https://github.com/ropg/ezTime?tab=readme-ov-file#working-with-time-values

  DateTimeCurrent = local.dateTime("D h:i a  ");  // Get the current formatted time string
  Blynk.virtualWrite(V0, DateTimeCurrent);        // Current day-date-time
  Blynk.virtualWrite(V4, CycleReason);            // Cycle Reason
  Blynk.virtualWrite(V15, TempMax);               // Max temp
  Blynk.virtualWrite(V12, Pressure);              // Current pressure
}
More display/logic functions but no Blynk functions

Network Data

Why?
Do you have an internet connection that charges you based on your traffic, or that limits your data in some way?

Yes, you could do that, its the simplest way if you really need to reduce your traffic.

Your code has some issues though. The way that you’re using BlynkTimers is all wrong and combining BlynkTimer and Millis comparison is also a bad idea.

Your current code sends data to Blynk every second, which seems to include a temperature reading, but you have no code in the sketch you’ve posted to take any reading of the temperature.
You also seem to be sending preset values to Blynk every second, rather than sensing them when they change (presumably these changes come from your TFT screen).

I think sorting-out your code structure would be your best approach.

Pete.

Thanks Pete,
The network consideration is more for when I have multiple devices 7+ in the same area and we have fairly “rural” internet at present that is not high bandwidth.
Also just not wanting to do it poorly if there is a better way.

I was using millis from some old code while I’m migrating to Blynk. Will probably go to a software interrupt for touch scanning.
Temp reading and many other parameters here is dummied up here also before I migrate everything while I show the app to the users and get feedback.

Is there a specific issue with the setup of the Blynk Timers or is it the inclusion of the millis?
Are you suggesting I only use The Timers, but setting up a 1s and 1min timer is OK?

I had also grouped the pins but do not think that is required since I am not referencing a specific time although was thinking to use this capability to recover stored values after device reboot.

Thanks!

You should use BLYNK_CONNECTED() and ‘Blynk.syncVirtual(vPin) for that.

The normal approach is to use timers to call specific functions. If you want to take temperature readings every 5 or 10 seconds then have a timer that calls a function to take the reading and send it to Blynk immediately (or compare it to the last reading and send it to Blynk if it’s changed.

Different pieces of data will have different sampling rates, so 1 second and 1 minute aren’t likely to be appropriate.

Pete.