Recording data from two adxl345 sensors

Source Audio's icon

Your last question (if I understand it correctly)

if M1 value changed from 0.2 >= 0.5, or better say more then set amount like 0.3 that is not going to work like that because sensor will never jump, but change values step wise. how would you set starting points ?Maybe if within last 10 or 100 readings average value changed more then 0.2 ...

say zl.stream > zl.median then pass that again through the if threshold thing.or something else ? how do you imagine that ?

values that do not pass change threshold don't get registered at all.so your solution will depend on the reality - I mean what really is to be expected from your sensors.

Source Audio's icon

I actually discovered code you posted for ESP now, you allready there use smoothing, threshold etc.

why do that twice ?

and where is that humidity sensor getting in ?

As you are sending a list, why do you also need IDs , like X1, Y1 etc ?

one can split a list without that and needs less bytes ...

ESP32 code seems to be running strictly on ESP32 3> library,

there are places in your code that need a FIX.

I see the goal to send individual sensors with real threshold and "if changed" state from ESP32 in order to preserve power (if run on battery) which would mean that you need ID for each of them from beginning.

humidity sensor must be using a different board as it is not a part of that code you posted.

receiving ESP should then again get and route that mesages to max the way it is proper for your entire project.

F_Dos's icon

would you have that max patch active whole year 24/7 ???

Not the whole here but eventually it should run on a small pc for a few month (this is in the future). It must connect to max in real time so it will effect some sounds.

and where is that humidity sensor getting in ?

I am using two ESP32S3 - one called "sender" that the ADXL345 sensor is attached to it and sends that sensor value to the other ESP32S3 called "Receiver" . to that receiver I also attached the analog moisture sensor (M1) via analog pin. This receiver is connected to the computer via USB and sends all data to MAX (ADXL345 sensor x y z values, and M1 analog value)

As you are sending a list, why do you also need IDs , like X1, Y1 etc ?

I'm using this because I might attached more adxl345 sensor in the future

there are places in your code that need a FIX.

could you give an example?

humidity sensor must be using a different board as it is not a part of that code you posted.

True. It is attached to the receiver board as I write above. Here is the Receiver board code:

#include <WiFi.h>
#include <esp_now.h>

typedef struct {
  float x;
  float y;
  float z;
} SensorData;

SensorData receivedData;
bool newDataReceived = false;

#define MOISTURE_PIN A10
#define LED_PIN LED_BUILTIN

unsigned long lastMoistureRead = 0;
const int MOISTURE_INTERVAL = 500; // read every 500ms

// LED state
bool ledState = false;
unsigned long lastLedToggle = 0;
unsigned long lastReceiveTime = 0;
const unsigned long COMM_TIMEOUT = 2000;      // ms without data = "waiting" mode
const unsigned long BLINK_WAITING = 300;     // heartbeat interval when idle
const unsigned long BLINK_ACTIVE = 1000;       // interval when receiving data

void onReceive(const esp_now_recv_info *info, const uint8_t *data, int len) {
  memcpy(&receivedData, data, sizeof(receivedData));
  newDataReceived = true;
  lastReceiveTime = millis();
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) { Serial.println("ESP-NOW init failed"); while(1); }
  esp_now_register_recv_cb(onReceive);

  Serial.println("Ready");
}

void loop() {
  unsigned long now = millis();

  // Decide current blink mode based on recent communication
  bool commActive = (now - lastReceiveTime) < COMM_TIMEOUT;
  unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;

  // Toggle LED at the appropriate interval
  if (now - lastLedToggle >= blinkInterval) {
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
    lastLedToggle = now;
  }

  // Read moisture every 500ms without blocking
  if (now - lastMoistureRead >= MOISTURE_INTERVAL) {
    int raw = analogRead(MOISTURE_PIN);
    float moisture = 1.0 - (raw / 4095.0);
    Serial.print("M1 "); Serial.println(moisture, 3);
    lastMoistureRead = now;
  }

  // Process ESP-NOW data immediately when received
  if (newDataReceived) {
    Serial.print("X1 "); Serial.println(receivedData.x, 3);
    Serial.print("Y1 "); Serial.println(receivedData.y, 3);
    Serial.print("Z1 "); Serial.println(receivedData.z, 3);
    newDataReceived = false;
  }
}

F_Dos's icon

Your last question (if I understand it correctly)

if M1 value changed from 0.2 >= 0.5, or better say more then set amount like 0.3 that is not going to work like that because sensor will never jump, but change values step wise. how would you set starting points ?Maybe if within last 10 or 100 readings average value changed more then 0.2 ...

say zl.stream > zl.median then pass that again through the if threshold thing.or something else ? how do you imagine that ?

values that do not pass change threshold don't get registered at all.so your solution will depend on the reality - I mean what really is to be expected from your sensors.

Thanks, that's a good point — you're right, the sensor won't jump like that, it changes gradually. I'll think about how to set the threshold based on what's realistic for the sensor. If you have a suggestion for how you'd set it up, I'd be glad to hear it.

As for what's realistic to expect from the sensor: moisture changes are slow, not instant — when the ground gets watered, the reading drifts up over roughly a few minutes rather than jumping, and then stays relatively stable (with small noise) until it dries out again over a much longer stretch of time (hours). So a fast, single-reading threshold probably isn't the right fit here.

Source Audio's icon

Listen, I would help you with the code, if you provide more infos, like where is that humidity sensor connected, what model it is etc.

for example DHT22 takes about 2000 ms to perform a single internal measurement.

that is too slow compared to accel if it moves.

at beginning you wanted 2 acccel sensors, is that still in play ?

how many ESP32 are in play, etc etc

sorry , I posted before having your answer above.

it explains few things.

I would let ESP32 send 3 accel values separated, each one only if it changes and passes threshold. that gives you best real time capture in receiving ESP32.

there you can add M1 and M2 and another ESP32 sender with another sensor if needed.

( if you capture a tree, it makes no sense to place 2 accel sensors close on same tree, i2c bus needs a very short wire ...)

in max you get them again prepended with IDs , I would rather use numeric 1 2 3 4 5 6 7 8 instead of X1 X2 etc.

then you can process them in whatever way you want.

F_Dos's icon

Listen, I would help you with the code, if you provide more infos, like where is that humidity sensor connected, what model it is etc.

Capacitive Soil Moisture Sensor V2.0 . This sensoe is attached to analog pin of the receiver ESP32S3

at beginning you wanted 2 acccel sensors, is that still in play ?

how many ESP32 are in play, etc etc

For the testing I'm doing now I'm using:
1 sender ESP for 1 ADXL345 sensor
1 receiver ESP for receiving the data from the sender ESP and reading the data from the moisture sensor.
In the future (perhaps in a week or two) I would like to connect at least another ADXL345 to its own ESP32 (Sender 2)

Source Audio's icon

ok, to your code, you seem to use strictly esp32 lib v3 , which would not compile on older versions,

which actually are more efficient.

then you have Serial.end at the setup() end, and to my understanding it woujd disturb USB connection.

delay(20) is not good in the loop, rather use non blocking millis.

the treshold and "if changed" check make no benefit if you send a list with 3 values on ANY change.

F_Dos's icon
F_Dos's icon

I would let ESP32 send 3 accel values separated, each one only if it changes and passes threshold. that gives you best real time capture in receiving ESP32.

OK that sounds good!

in max you get them again prepended with IDs , I would rather use numeric 1 2 3 4 5 6 7 8 instead of X1 X2 etc.

why numeric? I think it might be easier to understand the value if using x y and z .

( if you capture a tree, it makes no sense to place 2 accel sensors close on same tree, i2c bus needs a very short wire ...)

I will try to capture data from 1 or 2 different branches of the same tree, as I think I won't get much data if I place the sensor on the tree trunk. (Ideally I wanted to capture all sorts of hidden data from a tree, but I decided to start with these two types of sensors since I couldn't think of any other ideas, and I wanted to start experimenting with something.) So if you have any ideas for different sensors, I'd be glad to hear them.

Regarding the I2C — I know it needs to be a maximum of 20cm, which is why I'm using an ESP32, so I can keep the sensor very close to it and then send the data to the receiver, which will be down the tree, close to the computer.

Source Audio's icon

honestly, you will maybe not get much difference from 3 sensors in such close location.

then I need few infos - battery power ? if yes one needs power saving functions.

if it gets larger distance between sender and receiver ESP boards, how much in meters ?

if it is so close, why use ESP32 at all ?

you could use PCA9615 Differential Extender to get up to 30 meter distance for 2 sensors.

if you need more then 2 sensors then add a multiplexer like TCA9548A to be able to use same kind of sensors.

that is because adxl345 only have 2 selectable addresses.

at the end you could maybe use simple arduino without WiFi.

F_Dos's icon

then I need few infos - battery power ? if yes one needs power saving functions.

I will power the Receiver from the computer it connected to.

Regarding the Sender/s For testing I will power it from 5V phone charger but honestly if it possible to use 3.7 battery that can hold enough time of few months this will be the best. If not I will just use phone charger.

if it gets larger distance between sender and receiver ESP boards, how much in meters ?

around 10 meters maximum of 30 meters (open space)

if it is so close, why use ESP32 at all ?

It is not so close, and even if so I wanted to seperate the accel from the rest as to be able to locate it also up in the tree

For the moment speaking I want to keep it simple - meaning one adxl345 for each ESP32S3. I prefer to keep the wire sort and send the data over ESP NOW as in my code now.

using PCA9615 and TCA9548A sounds like a valid solution but for the moment speaking I would prefer not adding more boards

Source Audio's icon

I don't want to turn your project upsidedown.

you can test now with 1 sender, one receiver and see how it goes.

You would need a damn large powerbank to hold few months.

but I still need to make suggestions, sorry. if you were using analog instead of i2C

accelerator, you could have used very cheap telephone wires for each sensor.

3 analog axis could be merged using 3 resistors to add into single output,

and all could work on single ESP32 at the tree bottom.

.........

enough, here is sender code with most of your code used, (calibration, threshold etc)

set to send individual axes, and saving battery power by using 80MHz CPU.

One could save more, but about that some other time.

............

#include <Wire.h>

#include <Adafruit_Sensor.h>

#include <Adafruit_ADXL345_U.h>

#include <WiFi.h>

#include <esp_now.h>

Adafruit_ADXL345_Unified accel1 = Adafruit_ADXL345_Unified(1);

const float THRESHOLD = 0.01; // increase this maybe ?

const float RANGE = 2.0;

const float ALPHA = 0.2;

const int CALIBRATION_SAMPLES = 100;

unsigned long lastTx = 0;

const unsigned long TX_INT = 10; // increase to 20 to slow TX freq

uint8_t receiverMAC[] = {0x68, 0xEE, 0x8F, 0x46, 0x4E, 0x34};

typedef struct { uint8_t id; float value; } SensorPacket;

SensorPacket dataToSend;

bool sensor1ok = false;

struct SensorState { float centerX = 0, centerY = 0, centerZ = 9.8; float prevX = 0.5, prevY = 0.5, prevZ = 0.5; float filtX = 0.5, filtY = 0.5, filtZ = 0.5; };

SensorState s1;

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3

void onSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {}

#define REG_CB esp_now_register_send_cb((esp_now_send_cb_t)onSent)

#else

void onSent(const uint8_t *mac_addr, esp_now_send_status_t status) {}

#define REG_CB esp_now_register_send_cb(onSent)

#endif

void calibrate() {float sx = 0, sy = 0, sz = 0; for (int i = 0; i < CALIBRATION_SAMPLES; i++) {sensors_event_t event;

if (sensor1ok) {accel1.getEvent(&event); sx += event.acceleration.x; sy += event.acceleration.y; sz += event.acceleration.z;} delay(20);}

if (sensor1ok) {s1.centerX = sx / CALIBRATION_SAMPLES; s1.centerY = sy / CALIBRATION_SAMPLES; s1.centerZ = sz / CALIBRATION_SAMPLES;}}

void setup() {WiFi.mode(WIFI_STA); if (esp_now_init() != ESP_OK) {while(1);}

REG_CB; esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, receiverMAC, 6); peerInfo.channel = 0; peerInfo.encrypt = false;

if (esp_now_add_peer(&peerInfo) != ESP_OK) { while(1);} Wire.begin(); Wire.setClock(400000);

sensor1ok = accel1.begin(0x53); if (sensor1ok) { accel1.setRange(ADXL345_RANGE_2_G); accel1.setDataRate(ADXL345_DATARATE_3200_HZ);}

calibrate(); setCpuFrequencyMhz(80);}

void loop() {if (sensor1ok) {sensors_event_t event; accel1.getEvent(&event);

float nx = 0.5 + (event.acceleration.x - s1.centerX) / (2.0 * RANGE);

float ny = 0.5 + (event.acceleration.y - s1.centerY) / (2.0 * RANGE);

float nz = 0.5 + (event.acceleration.z - s1.centerZ) / (2.0 * RANGE);

s1.filtX = ALPHA nx + (1 - ALPHA) s1.filtX;

s1.filtY = ALPHA ny + (1 - ALPHA) s1.filtY;

s1.filtZ = ALPHA nz + (1 - ALPHA) s1.filtZ;

if (millis() - lastTx >= TX_INT) {bool sent = false;

if (abs(s1.filtX - s1.prevX) > THRESHOLD) {s1.prevX = s1.filtX; dataToSend.id = 1; dataToSend.value = s1.filtX; esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket)); sent = true; delayMicroseconds(500);}

if (abs(s1.filtY - s1.prevY) > THRESHOLD) {s1.prevY = s1.filtY; dataToSend.id = 2; dataToSend.value = s1.filtY; esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket)); sent = true; delayMicroseconds(500);}

if (abs(s1.filtZ - s1.prevZ) > THRESHOLD) {s1.prevZ = s1.filtZ; dataToSend.id = 3; dataToSend.value = s1.filtZ; esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket)); sent = true;}

if (sent) {lastTx = millis();}}}}

Source Audio's icon

here is receiver code, ready for second accel sensor input and 2 humidity sensors

you xan simply read that ESP sender prepends 1 2 3 for 4 axes, second sebder should use 4 5 6

...................

#include <WiFi.h>

#include <esp_now.h>

typedef struct {uint8_t id; float value;} SensorPacket;

volatile SensorPacket sharedPacket;

volatile bool newPacketReceived = false;

SensorPacket packet;

float currentM1 = 0.45, currentM2 = 0.45;

float prevM1 = -1.0, prevM2 = -1.0;

unsigned long lastMRead = 0;

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3

void onDataRecv(const esp_now_recv_info_t info, const uint8_t data, int len) {

#else

void onDataRecv(const uint8_t mac_addr, const uint8_t data, int len) {

#endif

if (len == sizeof(SensorPacket)) {memcpy((void*)&sharedPacket, data, sizeof(SensorPacket)); newPacketReceived = true;}}

void checkHumidity() {

unsigned long now = millis();

if (now - lastMRead >= 2000) {lastMRead = now;

currentM1 = analogRead(A2) / 4095.0;

currentM2 = analogRead(A3) / 4095.0;

if (currentM1 != prevM1) {prevM1 = currentM1; Serial.print("M1 "); Serial.println(currentM1, 3);}

if (currentM2 != prevM2) {prevM2 = currentM2; Serial.print("M2 "); Serial.println(currentM2, 3);}}}

void setup() {Serial.begin(115200); WiFi.mode(WIFI_STA);

if (esp_now_init() != ESP_OK) {while(1);}

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3

esp_now_register_recv_cb((esp_now_recv_cb_t)onDataRecv);

#else

esp_now_register_recv_cb(onDataRecv);

#endif

}

void loop() {

if (newPacketReceived) {newPacketReceived = false; noInterrupts(); packet = sharedPacket; interrupts();

if (packet.id == 1) {Serial.print("X1 "); Serial.println(packet.value, 3);}

if (packet.id == 2) {Serial.print("Y1 "); Serial.println(packet.value, 3);}

if (packet.id == 3) {Serial.print("Z1 "); Serial.println(packet.value, 3);}

if (packet.id == 4) {Serial.print("X2 "); Serial.println(packet.value, 3);}

if (packet.id == 5) {Serial.print("Y2 "); Serial.println(packet.value, 3);}

if (packet.id == 6) {Serial.print("Z2 "); Serial.println(packet.value, 3);}}

checkHumidity();}

F_Dos's icon

Thanks for the above!!! I just left home and will be back only in a day or two. I will comment after testing. Thanks for the help meanwhile

Source Audio's icon

As I don't have arduino 2 and ESP32 lib 3 at hand I can not compile and test here.

I reuploaded the code, because after first upload I tried to copy the code and it pasted incomplete here.

so please redownload the code again.

F_Dos's icon

3 analog axis could be merged using 3 resistors to add into single output,

Is this possible to merge inside MAX digitally?

so I will have the RAW data of the 3 axes but also the merged one?

--

Regarding the system that show time with no new data in a certain time range - right now it show the recording time. for example no change between 00:06:22 to 00:06:33 . How it is possible to show as well the acctual time in another row? so if the recording started at 10:01:22 AM it will show: 00:06:22 00:06:22 10:07:44 10:07:55.

Regarding your new code - I am still trying to upload it to the ESP and will update if it works well as soon I will finish


Do you think it's possible to extract any "unseen" movement or activity from the tree with this kind of sensor — information that isn't necessarily visible? If not, do you have any suggestions for sensors that could capture hidden data from within the tree, some kind of internal vibration or activity that isn't visible from the outside?

F_Dos's icon

Regarding your codes: I fix some compilation errors and now they both works.

I do have a lot of noise in the adxl345 . See short video:

Edit: changing the threshold value fix it of course.

Recording test.mp4



codes after fix: (I also added some led flashing to know if there is connection between the boards)

Receiver:

#include <WiFi.h>
#include <esp_now.h>

typedef struct {uint8_t id; float value;} SensorPacket;

volatile SensorPacket sharedPacket;
volatile bool newPacketReceived = false;
SensorPacket packet;
float currentM1 = 0.45, currentM2 = 0.45;
float prevM1 = -1.0, prevM2 = -1.0;
unsigned long lastMRead = 0;

// ADDED: LED heartbeat state
#define LED_PIN LED_BUILTIN
bool ledState = false;
unsigned long lastLedToggle = 0;
volatile unsigned long lastReceiveTime = 0;
const unsigned long COMM_TIMEOUT = 2000;
const unsigned long BLINK_WAITING = 300;
const unsigned long BLINK_ACTIVE = 1000;

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
void onDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
#else
void onDataRecv(const uint8_t *mac_addr, const uint8_t *data, int len) {
#endif
  if (len == sizeof(SensorPacket)) {
    memcpy((void*)&sharedPacket, data, sizeof(SensorPacket));
    newPacketReceived = true;
    lastReceiveTime = millis();  // ADDED - updates on every packet, including heartbeat
  }
}

void checkHumidity() {
  unsigned long now = millis();
  if (now - lastMRead >= 2000) {
    lastMRead = now;
    currentM1 = analogRead(A10) / 4095.0;
    currentM2 = analogRead(A3) / 4095.0;
    if (currentM1 != prevM1) { prevM1 = currentM1; Serial.print("M1 "); Serial.println(currentM1, 3); }
    if (currentM2 != prevM2) { prevM2 = currentM2; Serial.print("M2 "); Serial.println(currentM2, 3); }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);       // ADDED
  digitalWrite(LED_PIN, LOW);     // ADDED

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) { while(1); }
#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
  esp_now_register_recv_cb(onDataRecv);
#else
  esp_now_register_recv_cb(onDataRecv);
#endif
}

void loop() {
  // ADDED: LED heartbeat toggle
  unsigned long now = millis();
  bool commActive = (now - lastReceiveTime) < COMM_TIMEOUT;
  unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;
  if (now - lastLedToggle >= blinkInterval) {
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
    lastLedToggle = now;
  }

  if (newPacketReceived) {
    newPacketReceived = false;
    noInterrupts();
    packet.id = sharedPacket.id;
    packet.value = sharedPacket.value;
    interrupts();

    if (packet.id == 1) { Serial.print("X1 "); Serial.println(packet.value, 3); }
    if (packet.id == 2) { Serial.print("Y1 "); Serial.println(packet.value, 3); }
    if (packet.id == 3) { Serial.print("Z1 "); Serial.println(packet.value, 3); }
    if (packet.id == 4) { Serial.print("X2 "); Serial.println(packet.value, 3); }
    if (packet.id == 5) { Serial.print("Y2 "); Serial.println(packet.value, 3); }
    if (packet.id == 6) { Serial.print("Z2 "); Serial.println(packet.value, 3); }
    // id == 99 is heartbeat only - lastReceiveTime already updated above, nothing to print
  }
  checkHumidity();
}

Sender:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>
#include <WiFi.h>
#include <esp_now.h>

Adafruit_ADXL345_Unified accel1 = Adafruit_ADXL345_Unified(1);

const float THRESHOLD = 0.1;
const float RANGE = 2.0;
const float ALPHA = 0.2;
const int CALIBRATION_SAMPLES = 100;

unsigned long lastTx = 0;
const unsigned long TX_INT = 10;

uint8_t receiverMAC[] = { 0x68, 0xEE, 0x8F, 0x46, 0x4E, 0x34 };

typedef struct {
  uint8_t id;
  float value;
} SensorPacket;
SensorPacket dataToSend;
bool sensor1ok = false;

struct SensorState {
  float centerX = 0, centerY = 0, centerZ = 9.8;
  float prevX = 0.5, prevY = 0.5, prevZ = 0.5;
  float filtX = 0.5, filtY = 0.5, filtZ = 0.5;
};
SensorState s1;

// ADDED: LED heartbeat state
#define LED_PIN LED_BUILTIN
bool ledState = false;
unsigned long lastLedToggle = 0;
unsigned long lastAckTime = 0;
const unsigned long COMM_TIMEOUT = 2000;
const unsigned long BLINK_WAITING = 300;
const unsigned long BLINK_ACTIVE = 1000;

// ADDED: independent heartbeat ping, not tied to sensor threshold
const unsigned long HEARTBEAT_INTERVAL = 500;
unsigned long lastHeartbeat = 0;
SensorPacket heartbeatPacket;

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
void onSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }  // ADDED
}
#define REG_CB esp_now_register_send_cb((esp_now_send_cb_t)onSent)
#else
void onSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }  // ADDED
}
#define REG_CB esp_now_register_send_cb(onSent)
#endif

void calibrate() {
  float sx = 0, sy = 0, sz = 0;
  for (int i = 0; i < CALIBRATION_SAMPLES; i++) {
    sensors_event_t event;
    if (sensor1ok) {
      accel1.getEvent(&event);
      sx += event.acceleration.x;
      sy += event.acceleration.y;
      sz += event.acceleration.z;
    }
    delay(20);
  }
  if (sensor1ok) {
    s1.centerX = sx / CALIBRATION_SAMPLES;
    s1.centerY = sy / CALIBRATION_SAMPLES;
    s1.centerZ = sz / CALIBRATION_SAMPLES;
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);         // ADDED
  digitalWrite(LED_PIN, LOW);       // ADDED

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    while (1)
      ;
  }
  REG_CB;
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, receiverMAC, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    while (1)
      ;
  }
  Wire.begin();
  Wire.setClock(400000);
  sensor1ok = accel1.begin(0x53);
  if (sensor1ok) {
    accel1.setRange(ADXL345_RANGE_2_G);
    accel1.setDataRate(ADXL345_DATARATE_3200_HZ);
  }
  calibrate();
  setCpuFrequencyMhz(80);
}

void loop() {
  // ADDED: LED heartbeat toggle
  unsigned long now = millis();
  bool commActive = (now - lastAckTime) < COMM_TIMEOUT;
  unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;
  if (now - lastLedToggle >= blinkInterval) {
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
    lastLedToggle = now;
  }

  // ADDED: send independent heartbeat, regardless of sensor data changes
  if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {
    heartbeatPacket.id = 99;  // reserved ID, not a real axis
    heartbeatPacket.value = 0;
    esp_now_send(receiverMAC, (uint8_t *)&heartbeatPacket, sizeof(SensorPacket));
    lastHeartbeat = now;
  }

  if (sensor1ok) {
    sensors_event_t event;
    accel1.getEvent(&event);
    float nx = 0.5 + (event.acceleration.x - s1.centerX) / (2.0 * RANGE);
    float ny = 0.5 + (event.acceleration.y - s1.centerY) / (2.0 * RANGE);
    float nz = 0.5 + (event.acceleration.z - s1.centerZ) / (2.0 * RANGE);

    s1.filtX = ALPHA * nx + (1 - ALPHA) * s1.filtX;
    s1.filtY = ALPHA * ny + (1 - ALPHA) * s1.filtY;
    s1.filtZ = ALPHA * nz + (1 - ALPHA) * s1.filtZ;

    if (millis() - lastTx >= TX_INT) {
      bool sent = false;
      if (abs(s1.filtX - s1.prevX) > THRESHOLD) {
        s1.prevX = s1.filtX;
        dataToSend.id = 1;
        dataToSend.value = s1.filtX;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtY - s1.prevY) > THRESHOLD) {
        s1.prevY = s1.filtY;
        dataToSend.id = 2;
        dataToSend.value = s1.filtY;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtZ - s1.prevZ) > THRESHOLD) {
        s1.prevZ = s1.filtZ;
        dataToSend.id = 3;
        dataToSend.value = s1.filtZ;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
      }
      if (sent) { lastTx = millis(); }
    }
  }
}
Source Audio's icon

I posted this code to match your wish to use inputs as controls, not really to write log files.

for that reason only individual values get sent, when they pass treshold.

You can deal in max with them in any way you want.

And of course keep your old Arduino code as is, if it fits your plan better.

merging 3 axis into single value was idea in case you would use ANALOG acceleartor instead of i2c,

which would let you connect several sensors to same arduino using longer wires.

to merge 3 digital axis as they are now is also possible, but makes no real benefit.

new code sends individual values if they pass threshold.

you could have long period of 1 or maybe 2 axis with no activity,

what sense it makes to merge them afterwards ?

I would simplify this and sum all 3 axis to have single value before you send them

using common threshold. then you know if there was any movement or not.

and in reality if you get strong wind and rain you would remove electronics anyway, or ?

Source Audio's icon

to other questions:

dealing with writting logs to coll shoud be done at the end, when you have finished concept.

other types of sensors - depends on envirinment, you could add mic to capture noise,

or light sensors , which could react not only with day/night, but also if leaves pass the sunshine more or less through. then motion detection, temperature ...

F_Dos's icon

I would simplify this and sum all 3 axis to have single value before you send them

Thanks for explaining the reasoning — that makes sense. For point 5, this would only run during non-rainy periods anyway, so extreme weather durability isn't really a concern here.

For point 4, I'd love an example of summing the 3 axes into a single value with a common threshold — would that just be summing the raw filtered values?

to other questions:

dealing with writting logs to coll shoud be done at the end, when you have finished concept.

other types of sensors - depends on envirinment, you could add mic to capture noise,

or light sensors , which could react not only with day/night, but also if leaves pass the sunshine more or less through. then motion detection, temperature ...

Any idea of sensor to capture data from inside the tree itself?

Source Audio's icon

about real time vs recording time :

you capture using 1 ms grain, date objects outputs only 1 sec grain.

how precise should that time stamp be ?

and do you want every captured line to have timestamp, or only that marked periods of no activity ? ?

........

to merge 3 axis, one could keep all as is, but sum absolute distance to center of each of them, and send the sum if it passes threshold.

value is still 0. ~ 1.

here is loop example to do that, sorry that LED and heartbeat stuff is out for this example :

void loop() {unsigned long now = millis();

if (sensor1ok) {sensors_event_t event;accel1.getEvent(&event);

float dx = abs(event.acceleration.x - s1.centerX) / (2.0 * RANGE);

float dy = abs(event.acceleration.y - s1.centerY) / (2.0 * RANGE);

float dz = abs(event.acceleration.z - s1.centerZ) / (2.0 * RANGE);

float totalDistance = (dx + dy + dz) / 3.0;

s1.filtDist = ALPHA totalDistance + (1 - ALPHA) s1.filtDist;

if (now - lastTx >= TX_INT) { float totalChange = abs(s1.filtDist - s1.prevDist);

if (totalChange > THRESHOLD) {s1.prevDist = s1.filtDist;dataToSend.id = 1;dataToSend.value = s1.filtDist;

esp_now_send(receiverMAC, (const uint8_t *)&dataToSend, sizeof(SensorPacket));

lastTx = now;}}}}

.............................................................................................

your scanning frequency is too high, I would drop it to 100Hz, exactly as fast as you scan for changes in the code.

then calibration could be better, use more samples, shorter delay.

like 400 samples using delay(10); that takes 4 seconds to calibrate, could be ok with no wind.

otherwise repeat calibration, maybe even insert manual trigger...

.............................................................................................

As alternative one could smooth the sum changes a bit using sqrt , it would maybe need some threshold adjustment :

void loop() {unsigned long now = millis();

if (sensor1ok) {sensors_event_t event;accel1.getEvent(&event);

float dx = event.acceleration.x - s1.centerX;

float dy = event.acceleration.y - s1.centerY;

float dz = event.acceleration.z - s1.centerZ;

float absoluteDistance = sqrt(dx*dx + dy*dy + dz*dz) / (2.0 * RANGE);

s1.filtDist = ALPHA absoluteDistance + (1 - ALPHA) s1.filtDist;

if (now - lastTx >= TX_INT) { float totalChange = abs(s1.filtDist - s1.prevDist);

if (totalChange > THRESHOLD) {s1.prevDist = s1.filtDist;dataToSend.id = 1;dataToSend.value = s1.filtDist;

esp_now_send(receiverMAC, (const uint8_t *)&dataToSend, sizeof(SensorPacket));

lastTx = now;}}}}

F_Dos's icon

Thanks, this is really helpful — a few responses:

On the timestamp question: a 1-second grain is fine for my purposes. I don't need per-line timestamps for every capture — I'm mainly interested in marking the transition points (when a period of no-activity ends/begins), so timestamping only those marked periods makes more sense than logging every single line.

On sampling rate and calibration: that makes sense in terms of efficiency, but I want to flag one thing — this data is ultimately meant to control sound in real time (live), so I want to make sure dropping to 100Hz doesn't introduce noticeable latency or choppiness in the live audio response. Is 100Hz still fast enough for that kind of live control, or would you recommend keeping it higher given the real-time use case? I'll go ahead with the calibration change (400 samples, delay(10)) either way, since that only affects startup.

On merging the 3 axes: thanks for both examples! I'll go with whichever one you'd recommend as the more reliable starting point — the simple sum-of-absolute-distances version, or the sqrt/magnitude version? Given I'll need to retune the threshold either way, I'd rather start with the one you'd trust more for real-world tree movement, and adjust from there.

Source Audio's icon

you have 10ms timer for sending values, but scan axes 32 times faster ? (3200Hz)

the math is simple: every 10ms (100Hz) sensor delivers current values

also every 10ms ESP32 checks the values and sends them if changed > threshold.

............

I would start with abs version, and try sqrt if you feel it is too jumpy.

.............

you can insert timestamp into coll line when you get that inactivity bang,

simply grab it from date and insert into coll.

But I don't know what exactly you use, in case it is valid value change

that came in after that long pause, then you can merge timestamp into THAT coll line.

if you just get info "there was no activity for soooo loooong" then you need to insert extra line into coll, with rec time as index, - - - - (as many as data in coll) and add timestamp.

that is so in case you need extra column in jit.cellblock to display pauses.

when you clarify that question, I could post the code example

Source Audio's icon

I will add few more infos

If you feel like needing higher sampling rate form accel, you could increase

the frequency to for example 400Hz but then send interval also has to be faster, in that case 2.5 ms.

in addition, if you add a Mic, LDR or something else, analog , then situation would change because you migh want ALL to send ONLY if changed, having own IDs.

to short the story maybe your receiving ESP should use higher BAUD rate

to accomodate for more sensors at that speed.

that XIAO should be ok with 230400 / 460800 / or 921600

but USB cable must short and good quality.

F_Dos's icon

value is still 0. ~ 1.

I must say that also in the individual values and in the merge values I'm getting numbers that are smaller or larger then 0. - 1.

for example see this video, from left to right: x, y, z, merge

Recording at 2026-07-26 15.56.40.mp4


I change the code a little so I'm sending to max both individual axes and the merge:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>
#include <WiFi.h>
#include <esp_now.h>

Adafruit_ADXL345_Unified accel1 = Adafruit_ADXL345_Unified(1);

const float THRESHOLD = 0.1;        // threshold for individual X/Y/Z axes
const float THRESHOLD_DIST = 0.1;   // threshold for merged T1 value (tune separately)
const float RANGE = 2.0;
const float ALPHA = 0.2;
const int CALIBRATION_SAMPLES = 400;
const int CALIBRATION_DELAY = 10;

unsigned long lastTx = 0;
const unsigned long TX_INT = 10;

uint8_t receiverMAC[] = { 0x68, 0xEE, 0x8F, 0x46, 0x4E, 0x34 };

typedef struct {
  uint8_t id;
  float value;
} SensorPacket;
SensorPacket dataToSend;
bool sensor1ok = false;

struct SensorState {
  float centerX = 0, centerY = 0, centerZ = 9.8;
  float prevX = 0.5, prevY = 0.5, prevZ = 0.5;
  float filtX = 0.5, filtY = 0.5, filtZ = 0.5;
  float filtDist = 0.0;
  float prevDist = 0.0;
};
SensorState s1;

#define LED_PIN LED_BUILTIN
bool ledState = false;
unsigned long lastLedToggle = 0;
unsigned long lastAckTime = 0;
const unsigned long COMM_TIMEOUT = 2000;
const unsigned long BLINK_WAITING = 300;
const unsigned long BLINK_ACTIVE = 1000;

const unsigned long HEARTBEAT_INTERVAL = 500;
unsigned long lastHeartbeat = 0;
SensorPacket heartbeatPacket;

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
void onSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }
}
#define REG_CB esp_now_register_send_cb((esp_now_send_cb_t)onSent)
#else
void onSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }
}
#define REG_CB esp_now_register_send_cb(onSent)
#endif

void calibrate() {
  float sx = 0, sy = 0, sz = 0;
  for (int i = 0; i < CALIBRATION_SAMPLES; i++) {
    sensors_event_t event;
    if (sensor1ok) {
      accel1.getEvent(&event);
      sx += event.acceleration.x;
      sy += event.acceleration.y;
      sz += event.acceleration.z;
    }
    delay(CALIBRATION_DELAY);
  }
  if (sensor1ok) {
    s1.centerX = sx / CALIBRATION_SAMPLES;
    s1.centerY = sy / CALIBRATION_SAMPLES;
    s1.centerZ = sz / CALIBRATION_SAMPLES;
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    while (1)
      ;
  }
  REG_CB;
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, receiverMAC, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    while (1)
      ;
  }
  Wire.begin();
  Wire.setClock(400000);
  sensor1ok = accel1.begin(0x53);
  if (sensor1ok) {
    accel1.setRange(ADXL345_RANGE_2_G);
    accel1.setDataRate(ADXL345_DATARATE_100_HZ);
  }
  calibrate();
  setCpuFrequencyMhz(80);
}

void loop() {
  unsigned long now = millis();
  bool commActive = (now - lastAckTime) < COMM_TIMEOUT;
  unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;
  if (now - lastLedToggle >= blinkInterval) {
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
    lastLedToggle = now;
  }

  if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {
    heartbeatPacket.id = 99;
    heartbeatPacket.value = 0;
    esp_now_send(receiverMAC, (uint8_t *)&heartbeatPacket, sizeof(SensorPacket));
    lastHeartbeat = now;
  }

  if (sensor1ok) {
    sensors_event_t event;
    accel1.getEvent(&event);

    // Individual axes (as before)
    float nx = 0.5 + (event.acceleration.x - s1.centerX) / (2.0 * RANGE);
    float ny = 0.5 + (event.acceleration.y - s1.centerY) / (2.0 * RANGE);
    float nz = 0.5 + (event.acceleration.z - s1.centerZ) / (2.0 * RANGE);

    s1.filtX = ALPHA * nx + (1 - ALPHA) * s1.filtX;
    s1.filtY = ALPHA * ny + (1 - ALPHA) * s1.filtY;
    s1.filtZ = ALPHA * nz + (1 - ALPHA) * s1.filtZ;

    // Merged/T1 value (in addition, not replacing)
    float dx = event.acceleration.x - s1.centerX;
    float dy = event.acceleration.y - s1.centerY;
    float dz = event.acceleration.z - s1.centerZ;
    float absoluteDistance = sqrt(dx * dx + dy * dy + dz * dz) / (2.0 * RANGE);
    s1.filtDist = ALPHA * absoluteDistance + (1 - ALPHA) * s1.filtDist;

    if (now - lastTx >= TX_INT) {
      bool sent = false;
      if (abs(s1.filtX - s1.prevX) > THRESHOLD) {
        s1.prevX = s1.filtX;
        dataToSend.id = 1;
        dataToSend.value = s1.filtX;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtY - s1.prevY) > THRESHOLD) {
        s1.prevY = s1.filtY;
        dataToSend.id = 2;
        dataToSend.value = s1.filtY;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtZ - s1.prevZ) > THRESHOLD) {
        s1.prevZ = s1.filtZ;
        dataToSend.id = 3;
        dataToSend.value = s1.filtZ;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtDist - s1.prevDist) > THRESHOLD_DIST) {
        s1.prevDist = s1.filtDist;
        dataToSend.id = 7;
        dataToSend.value = s1.filtDist;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
      }
      if (sent) { lastTx = now; }
    }
  }
}

Source Audio's icon

you mean in your original code, or my new codes ?

P.S I had a look at your original code, and yes, I removed constrain 0. ~ 1.

which explains higher values.

inserting constrain might strip your control range, bit in favor of slower and shorter movements, let's have it in again

// THIS IS FOR ESP32 Lib v 3

#include <Wire.h>

#include <Adafruit_Sensor.h>

#include <Adafruit_ADXL345_U.h>

#include <WiFi.h>

#include <esp_now.h>

Adafruit_ADXL345_Unified accel1 = Adafruit_ADXL345_Unified(1);

const float THRESHOLD = 0.1;

const float RANGE = 2.0;

const float ALPHA = 0.2;

const int CALIBRATION_SAMPLES = 400;

unsigned long lastTx = 0;

const unsigned long TX_INT = 10;

uint8_t receiverMAC[] = { 0x68, 0xEE, 0x8F, 0x46, 0x4E, 0x34 };

typedef struct {uint8_t id;float value;} SensorPacket; SensorPacket dataToSend; bool sensor1ok = false;

struct SensorState {float centerX = 0, centerY = 0, centerZ = 9.8; float prevDist = 0.0; float filtDist = 0.0;}; SensorState s1;

#define LED_PIN LED_BUILTIN

bool ledState = false;

unsigned long lastLedToggle = 0; unsigned long lastAckTime = 0;

const unsigned long COMM_TIMEOUT = 2000;

const unsigned long BLINK_WAITING = 300;

const unsigned long BLINK_ACTIVE = 1000;

const unsigned long HEARTBEAT_INTERVAL = 500; unsigned long lastHeartbeat = 0;

SensorPacket heartbeatPacket;

void onSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }}

void calibrate() {float sx = 0, sy = 0, sz = 0; for (int i = 0; i < CALIBRATION_SAMPLES; i++) {sensors_event_t event;

if (sensor1ok) {accel1.getEvent(&event); sx += event.acceleration.x; sy += event.acceleration.y;sz += event.acceleration.z;} delay(10);}

if (sensor1ok) {s1.centerX = sx / CALIBRATION_SAMPLES;s1.centerY = sy / CALIBRATION_SAMPLES;s1.centerZ = sz / CALIBRATION_SAMPLES;}}

void setup() {pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);

WiFi.mode(WIFI_STA); if (esp_now_init() != ESP_OK) {while (1);}

esp_now_register_send_cb((esp_now_send_cb_t)onSent); esp_now_peer_info_t peerInfo = {};

memcpy(peerInfo.peer_addr, receiverMAC, 6);

peerInfo.channel = 0; peerInfo.encrypt = false;

if (esp_now_add_peer(&peerInfo) != ESP_OK) {while (1);}

Wire.begin(); Wire.setClock(400000);

sensor1ok = accel1.begin(0x53);

if (sensor1ok) {accel1.setRange(ADXL345_RANGE_2_G);accel1.setDataRate(ADXL345_DATARATE_100_HZ);}

calibrate(); setCpuFrequencyMhz(80); }

void loop() {unsigned long now = millis(); bool commActive = (now - lastAckTime) < COMM_TIMEOUT;

unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;

if (now - lastLedToggle >= blinkInterval) {ledState = !ledState;digitalWrite(LED_PIN, ledState); lastLedToggle = now;}

if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {heartbeatPacket.id = 99; heartbeatPacket.value = 0;

esp_now_send(receiverMAC, (const uint8_t *)&heartbeatPacket, sizeof(SensorPacket));

lastHeartbeat = now;}

if (sensor1ok) {sensors_event_t event;accel1.getEvent(&event);

float nx = constrain(0.5 + ((event.acceleration.x - s1.centerX) / (2.0 * RANGE)), 0.0, 1.0);

float ny = constrain(0.5 + ((event.acceleration.y - s1.centerY) / (2.0 * RANGE)), 0.0, 1.0);

float nz = constrain(0.5 + ((event.acceleration.z - s1.centerZ) / (2.0 * RANGE)), 0.0, 1.0);

float totalDistance = (nx + ny + nz) / 3.0;

s1.filtDist = ALPHA totalDistance + (1 - ALPHA) s1.filtDist;

if (now - lastTx >= TX_INT) { float totalChange = abs(s1.filtDist - s1.prevDist);

if (totalChange > THRESHOLD) {s1.prevDist = s1.filtDist;dataToSend.id = 1;dataToSend.value = s1.filtDist;

esp_now_send(receiverMAC, (const uint8_t *)&dataToSend, sizeof(SensorPacket));

lastTx = now;}}}}

F_Dos's icon

you mean in your original code, or my new codes ?

In your new code

F_Dos's icon

I finished uploading the new code — it's working well. I made small modification so in max I'm getting the individual values as well the merged one.

Regarding the system that shows time with no new data within a certain time range — right now it shows the recording time (elapsed time since the recording started). For example, no change between 00:06:22 and 00:06:33. Is it possible to also show the actual clock time in another row? So if the recording started at 10:01:22 AM, it would show: 00:06:22 00:06:33 10:07:44 10:07:55 (recording time followed by actual clock time).


I also wanted to gently circle back to something I asked before: do you have any thoughts or suggestions on sensors or approaches that could capture "hidden" data from the tree — information that isn't necessarily visible from the outside, some kind of internal activity or signal within the tree itself?

Source Audio's icon

I can not imagine what could that be, I mean that tree inner life, what to capture.

you will need to digg into biology, chemistry, don't know what.

.........

can you post that little part of your code where you get that pause reported.

and is it meant during capturing or when reading file.

If you write that into log, then reading file should not calculate anything,

also playback must be changed to filter or route new coll lines properly

Source Audio's icon

P.S. sending 3 individual axes as well as merged one is simply bad.

If you send 3 of them ayway, then merge them in max.

Source Audio's icon
F_Dos's icon

If you send 3 of them ayway, then merge them in max.

Make sense. I will do so.

edit:

is this valid way to merge the 3 axes?

expr (abs($f1-0.5)+abs($f2-0.5)+abs($f3-0.5))/3.

can you post that little part of your code where you get that pause reported.

you mean the part in MAX?

Max part:

Max Patch
Copy patch and select New From Clipboard in Max.

Here is the full sender code without the merge.

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_ADXL345_U.h>
#include <WiFi.h>
#include <esp_now.h>

Adafruit_ADXL345_Unified accel1 = Adafruit_ADXL345_Unified(1);

const float THRESHOLD = 0.1;
const float RANGE = 2.0;   // TUNE THIS: increase if values clip at 0 or 1
const float ALPHA = 0.2;
const int CALIBRATION_SAMPLES = 400;
const int CALIBRATION_DELAY = 10;

unsigned long lastTx = 0;
const unsigned long TX_INT = 10;

uint8_t receiverMAC[] = { 0x68, 0xEE, 0x8F, 0x46, 0x4E, 0x34 };

typedef struct {
  uint8_t id;
  float value;
} SensorPacket;
SensorPacket dataToSend;
bool sensor1ok = false;

struct SensorState {
  float centerX = 0, centerY = 0, centerZ = 9.8;
  float prevX = 0.5, prevY = 0.5, prevZ = 0.5;
  float filtX = 0.5, filtY = 0.5, filtZ = 0.5;
};
SensorState s1;

#define LED_PIN LED_BUILTIN
bool ledState = false;
unsigned long lastLedToggle = 0;
unsigned long lastAckTime = 0;
const unsigned long COMM_TIMEOUT = 2000;
const unsigned long BLINK_WAITING = 300;
const unsigned long BLINK_ACTIVE = 1000;

const unsigned long HEARTBEAT_INTERVAL = 500;
unsigned long lastHeartbeat = 0;
SensorPacket heartbeatPacket;

#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
void onSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }
}
#define REG_CB esp_now_register_send_cb((esp_now_send_cb_t)onSent)
#else
void onSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }
}
#define REG_CB esp_now_register_send_cb(onSent)
#endif

void calibrate() {
  float sx = 0, sy = 0, sz = 0;
  for (int i = 0; i < CALIBRATION_SAMPLES; i++) {
    sensors_event_t event;
    if (sensor1ok) {
      accel1.getEvent(&event);
      sx += event.acceleration.x;
      sy += event.acceleration.y;
      sz += event.acceleration.z;
    }
    delay(CALIBRATION_DELAY);
  }
  if (sensor1ok) {
    s1.centerX = sx / CALIBRATION_SAMPLES;
    s1.centerY = sy / CALIBRATION_SAMPLES;
    s1.centerZ = sz / CALIBRATION_SAMPLES;
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    while (1)
      ;
  }
  REG_CB;
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, receiverMAC, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    while (1)
      ;
  }
  Wire.begin();
  Wire.setClock(400000);
  sensor1ok = accel1.begin(0x53);
  if (sensor1ok) {
    accel1.setRange(ADXL345_RANGE_2_G);
    accel1.setDataRate(ADXL345_DATARATE_100_HZ);
  }
  calibrate();
  setCpuFrequencyMhz(80);
}

void loop() {
  unsigned long now = millis();
  bool commActive = (now - lastAckTime) < COMM_TIMEOUT;
  unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;
  if (now - lastLedToggle >= blinkInterval) {
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
    lastLedToggle = now;
  }

  if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {
    heartbeatPacket.id = 99;
    heartbeatPacket.value = 0;
    esp_now_send(receiverMAC, (uint8_t *)&heartbeatPacket, sizeof(SensorPacket));
    lastHeartbeat = now;
  }

  if (sensor1ok) {
    sensors_event_t event;
    accel1.getEvent(&event);

    float nx = constrain(0.5 + (event.acceleration.x - s1.centerX) / (2.0 * RANGE), 0.0, 1.0);
    float ny = constrain(0.5 + (event.acceleration.y - s1.centerY) / (2.0 * RANGE), 0.0, 1.0);
    float nz = constrain(0.5 + (event.acceleration.z - s1.centerZ) / (2.0 * RANGE), 0.0, 1.0);

    s1.filtX = ALPHA * nx + (1 - ALPHA) * s1.filtX;
    s1.filtY = ALPHA * ny + (1 - ALPHA) * s1.filtY;
    s1.filtZ = ALPHA * nz + (1 - ALPHA) * s1.filtZ;

    if (now - lastTx >= TX_INT) {
      bool sent = false;
      if (abs(s1.filtX - s1.prevX) > THRESHOLD) {
        s1.prevX = s1.filtX;
        dataToSend.id = 1;
        dataToSend.value = s1.filtX;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtY - s1.prevY) > THRESHOLD) {
        s1.prevY = s1.filtY;
        dataToSend.id = 2;
        dataToSend.value = s1.filtY;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
        delayMicroseconds(500);
      }
      if (abs(s1.filtZ - s1.prevZ) > THRESHOLD) {
        s1.prevZ = s1.filtZ;
        dataToSend.id = 3;
        dataToSend.value = s1.filtZ;
        esp_now_send(receiverMAC, (uint8_t *)&dataToSend, sizeof(SensorPacket));
        sent = true;
      }
      if (sent) { lastTx = now; }
    }
  }
}

F_Dos's icon

Thanks for the above time signature screen shot!!

F_Dos's icon

I have added another ESP32 sender with its own adxl345.

in max I received it as X2 Y2 Z2.

Do you recommend to make a separate file saver same as the first one I already got? (copy paste the whole system)

F_Dos's icon

I don't mind saving it to the same file but I would likeif it is possible to separate that system into 3:

1 for first accel sensor X1 Y1 Z1

2 for moisture sensor M1

3 for second accel sensor X2 Y2 Z2

is that possible?

edit: OK it is make no sense to separate the data saved file into 3 seperated files, so basically my question is how to separate the system that show time with no active data within certain window time

The idea of that system is to know when there was time without active area, so that I can, in a negated way, know when the sensor was active

F_Dos's icon

Sorry if I'm causing confusion. I'll try to explain myself again:

The current state is as follows:

I'm sending data from two different ESP32s, each with an ADXL345:
1: X1, Y1, Z1
2: X2, Y2, Z2

On the Receiver side, I also have a moisture sensor, M1.

In Max, I'm receiving all this data and saving it into a coll and a text file using the system you built.

How can I separate the system that shows a time window of X duration with no active data, so that sensor 1 has its own display (X1, Y1, Z1), sensor 2 has its own display (X2, Y2, Z2), and sensor 3 (M1) has its own as well?

I hope this makes it clearer/makes more sense.


Edit — from what I understand about how the system works, it records all the data together on the same line, and if there's no change in any of the values, it won't record a new line. But it's enough for just one of the values to change for a new line to be recorded. So, as the system is now, if one sensor is still for 2 minutes but the other changes every 5 seconds, I won't be able to separate the two sensors. So I might need to record each sensor to its own file in order to separate that "no active window" display system?

F_Dos's icon

Following up on the RANGE/normalization discussion — right now RANGE is a fixed constant on the ESP32, baked into the value before it's sent and recorded, and the constrain(..., 0.0, 1.0) clips any value outside that range before it's ever recorded.

I'm worried that both of these together mean I permanently lose information: strong movements get clipped to exactly 1.0 (or 0.0) with no way to recover how strong they actually were, and if I later want to try a different RANGE, I'd need to re-flash and re-record from scratch.

I'm considering instead sending the raw acceleration delta (before dividing by RANGE, and without any clipping) and doing both the normalization and the clipping live in Max — so RANGE becomes an adjustable parameter I can tweak while listening back to already-recorded data, and the clip only affects the live sound/display path, not the recorded file itself.

Does that make sense as an approach? Any concerns with sending raw m/s² values over ESP-NOW instead of the current clipped 0-1 values — precision, range of the float, anything I'm missing?

Source Audio's icon

At the beginning of this new code, I posted that it was meant for real time control and not for logging of any kind.

It gives you separated values, so that you can follow each sensor, and reduce data flow over ESP now and Serial port.

if you want to pack and log that list, nothing changes compared to old code.

but you can get info about each single sensor if you want when dumping the coll,

just route each of them to separated detector.

or only split accel 1, accel 2 and M1, or record 3 files, or 7 files ?

.................

you wanted constrain 0 – 1 there it is. if you want to remove it, then do it simple as that.

then in max you can see how to deal with the values.

I am not going to suggest anything again, because it is competely unknown what you want to do with the values, beside logging them.

you either measure absolute change to calibrated middle point, or change between 2 readings,

no matter in which direction.

that would create completely different effect as control.

You can also decide to send a list as you did before using set interval, and do all the rest in max.

remove threshold , remove constrain and simply send and send.

Is not efficient for battery life at all, but you can also use power supply.

Decide at what interval to send and adjust ADXL345_DATARATE and TX_INT

and in case of very fast update lift CPU to 160

on receiver side increase BAUD to 921600 and poll in max as fast as needed.

I actually thought that you want to use sensors in real time, not to record and playback.

If it is only playback, think about inactivity periods , if you spool playback at silent part, you will have no control change till new coll line triggers. Unless you implement a sort of event chasing.

F_Dos's icon

At the beginning of this new code, I posted that it was meant for real time control and not for logging of any kind

I actually thought that you want to use sensors in real time, not to record and playback.

The playback and recorded system is for testing and seeing the data. After that period it will be indeed live and not playback

F_Dos's icon

Unless you implement a sort of event chasing.

What does it means?

just route each of them to separated detector.

Could you please show how this can be done?

or only split accel 1, accel 2 and M1, or record 3 files, or 7 files ?

Do you think it is better method?

In that case I will need to duplicate the current system right? Making 4 different coll, one for each sensor

Source Audio's icon

again your first decision should be to use raw output at steady rate , or not.

once you decide, I would upload working code that either sends raw list using single ID,

or 3 axis variant with no constrain, or then better merged one without constrain.

untill that , I don't want to bang my head about unknown constructs

F_Dos's icon

again your first decision should be to use raw output at steady rate , or not.

If you said all the filtering happens in esp can be made easily in MAX, I would say raw output. If this will complicate, I would leave the codes as they are

Source Audio's icon

maybe you could test this code, it outputs 0. if no move from calibrated point, can go up to max 4.9 (abs) in either direction, and sends a list if ANY of 3 axes passes the threshold.

that if you keep RANGE 2.

anyway, to have snappier reaction to changes, ALPHA 0.2 is a bit too laggy.

I would lift it to 0.5 at least.

here is the code :

// THIS IS FOR ESP32 Lib v 3 - 3-AXIS LIST SENDER UNCONSTRAINED

// Calibrated point outputs 0. max movent in any direction can swing up to 4.9

#include <Wire.h>

#include <Adafruit_Sensor.h>

#include <Adafruit_ADXL345_U.h>

#include <WiFi.h>

#include <esp_now.h>

Adafruit_ADXL345_Unified accel1 = Adafruit_ADXL345_Unified(1);

const float THRESHOLD = 0.1;

const float RANGE = 2.0;

const float ALPHA = 0.5; // was 0.2 - changed to have faster changes

const int CALIBRATION_SAMPLES = 400;

unsigned long lastTx = 0;

const unsigned long TX_INT = 10;

uint8_t receiverMAC[] = { 0x68, 0xEE, 0x8F, 0x46, 0x4E, 0x34 };

typedef struct {uint8_t id; float x; float y; float z;} SensorPacket; SensorPacket dataToSend; bool sensor1ok = false;

struct SensorState {float centerX = 0, centerY = 0, centerZ = 9.8; float prevX = 0.0, prevY = 0.0, prevZ = 0.0; float filtX = 0.0, filtY = 0.0, filtZ = 0.0;}; SensorState s1;

#define LED_PIN LED_BUILTIN

bool ledState = false;

unsigned long lastLedToggle = 0; unsigned long lastAckTime = 0;

const unsigned long COMM_TIMEOUT = 2000;

const unsigned long BLINK_WAITING = 300;

const unsigned long BLINK_ACTIVE = 1000;

const unsigned long HEARTBEAT_INTERVAL = 500; unsigned long lastHeartbeat = 0;

SensorPacket heartbeatPacket;

void onSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {if (status == ESP_NOW_SEND_SUCCESS) { lastAckTime = millis(); }}

void calibrate() {float sx = 0, sy = 0, sz = 0; for (int i = 0; i < CALIBRATION_SAMPLES; i++) {sensors_event_t event;

if (sensor1ok) {accel1.getEvent(&event); sx += event.acceleration.x; sy += event.acceleration.y;sz += event.acceleration.z;} delay(10);}

if (sensor1ok) {s1.centerX = sx / CALIBRATION_SAMPLES;s1.centerY = sy / CALIBRATION_SAMPLES;s1.centerZ = sz / CALIBRATION_SAMPLES;}}

void setup() {pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);

WiFi.mode(WIFI_STA); if (esp_now_init() != ESP_OK) {while (1);}

esp_now_register_send_cb((esp_now_send_cb_t)onSent); esp_now_peer_info_t peerInfo = {};

memcpy(peerInfo.peer_addr, receiverMAC, 6);

peerInfo.channel = 0; peerInfo.encrypt = false;

if (esp_now_add_peer(&peerInfo) != ESP_OK) {while (1);}

Wire.begin(); Wire.setClock(400000);

sensor1ok = accel1.begin(0x53);

if (sensor1ok) {accel1.setRange(ADXL345_RANGE_2_G);accel1.setDataRate(ADXL345_DATARATE_100_HZ);}

calibrate(); setCpuFrequencyMhz(80); }

void loop() {unsigned long now = millis(); bool commActive = (now - lastAckTime) < COMM_TIMEOUT;

unsigned long blinkInterval = commActive ? BLINK_ACTIVE : BLINK_WAITING;

if (now - lastLedToggle >= blinkInterval) {ledState = !ledState;digitalWrite(LED_PIN, ledState); lastLedToggle = now;}

if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {heartbeatPacket.id = 99; heartbeatPacket.x = 0; heartbeatPacket.y = 0; heartbeatPacket.z = 0;

esp_now_send(receiverMAC, (const uint8_t *)&heartbeatPacket, sizeof(SensorPacket));

lastHeartbeat = now;}

if (sensor1ok) {sensors_event_t event;accel1.getEvent(&event);

float nx = abs(event.acceleration.x - s1.centerX) / RANGE;

float ny = abs(event.acceleration.y - s1.centerY) / RANGE;

float nz = abs(event.acceleration.z - s1.centerZ) / RANGE;

s1.filtX = ALPHA nx + (1 - ALPHA) s1.filtX;

s1.filtY = ALPHA ny + (1 - ALPHA) s1.filtY;

s1.filtZ = ALPHA nz + (1 - ALPHA) s1.filtZ;

if (now - lastTx >= TX_INT) {

float changeX = abs(s1.filtX - s1.prevX); float changeY = abs(s1.filtY - s1.prevY); float changeZ = abs(s1.filtZ - s1.prevZ);

if (changeX > THRESHOLD || changeY > THRESHOLD || changeZ > THRESHOLD) {

s1.prevX = s1.filtX; s1.prevY = s1.filtY; s1.prevZ = s1.filtZ;

dataToSend.id = 1; dataToSend.x = s1.filtX; dataToSend.y = s1.filtY; dataToSend.z = s1.filtZ;

esp_now_send(receiverMAC, (const uint8_t *)&dataToSend, sizeof(SensorPacket));

lastTx = now;}}}}

F_Dos's icon

maybe you could test this code

Thanks, I will test in in a few hours when I will be back at home

What do you offer regarding the separation of the three sensors?

Source Audio's icon

here is receiver code, it will fit with the sender code above and also raw axes version.

Serial.begin(921600); is set in case you decide to use raw data and short update interval,

like 5 or less ms

// THIS IS FOR ESP32 Lib v 3 ONLY - 3 axes as LIST , PREFIX to Serial A1 A2

// accel raw range at 2G -9.81 ~ 9.81 or 0. ~ 4.9 for calibrated unconstrained version

#include <WiFi.h>

#include <esp_now.h>

typedef struct {uint8_t id; float x; float y; float z;} SensorPacket;

volatile SensorPacket sharedPacket;

volatile bool newPacketReceived = false;

SensorPacket packet;

float currentM1 = 0.45;

float prevM1 = -1.0;

unsigned long lastMRead = 0;

void onDataRecv(const esp_now_recv_info_t info, const uint8_t data, int len) {

if (len == sizeof(SensorPacket)) {memcpy((void*)&sharedPacket, (void*)data, sizeof(SensorPacket)); newPacketReceived = true;}}

void checkHumidity() {unsigned long now = millis();

if (now - lastMRead >= 2000) {lastMRead = now;

currentM1 = analogRead(A2) / 4095.0;

if (currentM1 != prevM1) {prevM1 = currentM1; Serial.print("M1 "); Serial.println(currentM1, 3);}}}

void setup() {Serial.begin(921600); WiFi.mode(WIFI_STA);

if (esp_now_init() != ESP_OK) {while(1);}

esp_now_register_recv_cb((esp_now_recv_cb_t)onDataRecv);}

void loop() {

if (newPacketReceived) {newPacketReceived = false; noInterrupts(); packet = sharedPacket; interrupts();

if (packet.id == 1) {Serial.print("A1 "); Serial.print(packet.x, 3); Serial.print(" "); Serial.print(packet.y, 3); Serial.print(" "); Serial.println(packet.z, 3);}

else if (packet.id == 2) {Serial.print("A2 "); Serial.print(packet.x, 3); Serial.print(" "); Serial.print(packet.y, 3); Serial.print(" "); Serial.println(packet.z, 3);}}

checkHumidity();}

Source Audio's icon

I am not sure how to deal with you logs and play tests, to be honest.

as it is only going to be used for tests, maybe having larger coll files

should not be a problem.

You could also try to speed up testing procedure by shaking the tree,

blowing at it using strong ventilator and similar, to get idea what to expect.

Instead of recording hours of long log files, waiting for a bit of wind ...

it would be maybe easier to split 2 accel sensors, and completely leave humidity sensor out of logs,

what to expect from it ?

F_Dos's icon

it would be maybe easier to split 2 accel sensors, and completely leave humidity sensor out of logs,

Sounds as ok solution!

I do want to make one long recording of about 24 hours to get some idea of behaviour.

I might just duplicate the current system and split the 3 sensors?

Source Audio's icon

1 hour of recording if you enter 1 line every 10ms will use 19.3 MB of hd space.

I do not know if coll and jit.cellblock can deal with 24 hours version.

see if you use 2 ESP32 which send at 10ms interval, worst case is that 1 of 6 axis moves ALL the time.

you poll serial object using 10 ms that gives you 100 lines per second.

360000 lines per 1 hour or 8640000 lines in 24 hours.

that is worst case.

you could make a test by putting all that lines uzing uzi to fill up, and then write it to disk,

read and dump it.

or use js to write logs directly to HD using coll format, so that you can read it later

F_Dos's icon

I do not know if coll and jit.cellblock can deal with 24 hours version.

I recorded 36 Hours of the M1 sensor with no problem.

I think I will just duplicate the recorded system into 3 so I could have each sensor data seperated

I will try your new codes soon! thank you very much.

F_Dos's icon

Great, all three codes are working well now — thank you!

Now that the filtering, threshold, and range/normalization used to happen on the ESP side and now happens raw, I wanted to gently ask: what would you recommend as the right way to handle that filtering, thresholding, and range mapping now in Max instead? Is there a standard set of objects you'd suggest for smoothing (something like the ALPHA low-pass we had), thresholding, and remapping the raw ~0-4.9 range into something more usable — or does it depend a lot on what I'm trying to do with the data downstream?

F_Dos's icon

OK I think it is too complicated for me to make all filtering in max as well as seperate the system that show inactivity within a time window.

I think I will just stay with the previous code that output the filtered data ( X1 Y1 Z1 X2 Y2 Z2 M1) and I will not separate the inactivity time windows to every sensor rather stay with one system to all sensor

duplicate the system to show each sensor alone sound too complicated because it is necceseraly means to duplicate the current system into 3 and write 3 different files at once and read them at onces into 3 different systems

F_Dos's icon

OK here is my very ugly solution for separating the 3 sensors.

Max Patch
Copy patch and select New From Clipboard in Max.