Objective and use case
What you’ll build: Implement a predictive vibration sensing system using the ICM-42688-P sensor, ESP32-S3, and ATECC608A for secure data transmission over LoRa.
Why it matters / Use cases
- Monitor industrial machinery for vibration anomalies to prevent costly downtimes.
- Utilize remote sensors in agricultural equipment to ensure optimal performance and maintenance scheduling.
- Deploy in smart cities to track the health of infrastructure like bridges and roads.
- Enhance predictive maintenance in automotive applications by analyzing vibration data from vehicles.
Expected outcome
- Real-time anomaly detection with a precision of 95% in identifying vibration irregularities.
- Data transmission latency of less than 2 seconds over LoRa.
- Uplink of feature data and anomaly scores with a packet success rate of 98%.
- Reduction in maintenance costs by 30% through proactive interventions based on vibration data.
Audience: Engineers and developers in IoT; Level: Advanced
Architecture/flow: ESP32-S3 processes sensor data from ICM-42688-P, signs it with ATECC608A, and transmits via LoRa using RFM95W.
ESP32-S3-DevKitC-1 + RFM95W (SX1276) + ICM-42688-P + ATECC608A: LoRa Predictive Vibration Sensing (Advanced)
Objective: lora-predictive-vibration-icm42688 — implement an on-device vibration anomaly detector using ICM‑42688‑P, sign results with ATECC608A, and uplink features/anomaly score over raw LoRa using an SX1276 (RFM95W) from an ESP32‑S3‑DevKitC‑1. This guide uses PlatformIO for build reproducibility and provides code, connections, commands, and validation.
Prerequisites
- Skills: C++ for embedded, basic signal processing (RMS, FFT), SPI/I2C wiring, CLI usage.
- OS: Windows 10/11, macOS 12+, or Ubuntu 22.04+.
- Drivers:
- ESP32‑S3‑DevKitC‑1 typically uses native USB Serial/JTAG (no extra driver needed on Win10+/macOS/Linux).
- If your USB/UART interface is CP210x (some carriers or external FTDI adapters), install “Silicon Labs CP210x” drivers.
- If your adapter is CH34x (less common here), install WCH CH34x drivers.
- Software:
- Python 3.10+ (optional for log parsing).
- PlatformIO Core (CLI): install via
pipx install platformioorpip install -U platformio. - Git (optional, for version control).
Materials (exact models)
- MCU: ESP32‑S3‑DevKitC‑1 (ESP32‑S3, 3.3 V logic).
- LoRa radio: RFM95W (Semtech SX1276, 868/915 MHz module).
- IMU: ICM‑42688‑P 6‑DoF accelerometer/gyro module (I2C breakout).
- Secure element: ATECC608A (I2C variant, address 0x60).
- Passives/wires: 3.3 V operation only, jumper wires, small breadboard.
- Optional: A second SX1276-based node for RF receive validation (or an SDR).
Regulatory note: Select the frequency (868 MHz in EU, 915 MHz in US/ANZ) according to your local regulations and duty-cycle limits.
Setup/Connection
All modules are 3.3 V logic. Do not apply 5 V to any signal pin.
We will use:
– I2C for ICM‑42688‑P and ATECC608A on the same bus.
– SPI for RFM95W (SX1276).
– Explicit GPIO assignments on ESP32‑S3‑DevKitC‑1; we configure pins in software.
Recommended wiring (you can adjust, but match the code/pins):
Pinout table
| Module | Signal | ESP32-S3-DevKitC-1 Pin | Notes |
|---|---|---|---|
| RFM95W (SX1276) | 3V3 | 3V3 | Power, ensure adequate current (peaks during TX). |
| GND | GND | Common ground for all. | |
| SCK | GPIO36 | SPI SCK (we define in code). | |
| MISO | GPIO37 | SPI MISO. | |
| MOSI | GPIO35 | SPI MOSI. | |
| NSS (CS) | GPIO34 | Chip Select (active low). | |
| RST | GPIO33 | Radio reset. | |
| DIO0 | GPIO2 | IRQ line for TX done/RX done. | |
| DIO1 | GPIO7 | Optional, not used in this sketch (but mapped for future RX). | |
| ICM‑42688‑P | VIN | 3V3 | Sensor supply (3.3 V). |
| GND | GND | Ground. | |
| SDA | GPIO5 | I2C data. | |
| SCL | GPIO6 | I2C clock. | |
| AD0/SA0 | GND | Sets I2C address 0x68 (typical). | |
| ATECC608A | VCC | 3V3 | Secure element supply. |
| GND | GND | Ground. | |
| SDA | GPIO5 | Same I2C bus. | |
| SCL | GPIO6 | Same I2C bus. | |
| ADDR | N/C | ATECC608A default I2C address is 0x60. |
Notes:
– Keep I2C wiring short and tidy; use 4.7 kΩ pull-ups to 3.3 V if your breakouts do not include them.
– For the RFM95W, use a proper antenna tuned to your band. Never transmit without an antenna.
– Avoid ESP32‑S3 strapping pins (GPIO0, GPIO45, GPIO46) for critical peripheral signals.
Full Code
This PlatformIO project implements:
– ICM‑42688‑P acquisition at ~1 kHz with 256‑sample windows.
– Feature extraction: RMS, peak-to-peak, crest factor, kurtosis, two dominant spectral peaks.
– Baseline learning (first N windows), then anomaly scoring (sum of squared z-scores).
– SHA‑256 digest created in the ATECC608A and ECDSA P‑256 signature from private key in Slot 0.
– Raw LoRa uplink of a compact payload containing features, anomaly score, and signature.
platformio.ini
Create a new folder (e.g., lora-predictive-vibration-icm42688) and put this file at the project root:
; File: platformio.ini
[env:esp32-s3-devkitc-1]
platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
framework = arduino
upload_speed = 921600
monitor_speed = 115200
monitor_filters = time, colorize
; Enable USB CDC for serial monitor on S3
build_flags =
-D ARDUINO_USB_CDC_ON_BOOT=1
-D LORA_FREQ_MHZ=915.0 ; set to 868.1 for EU, 915.0 for US/AU/NZ
-D LORA_SYNC_WORD=0x12 ; 0x12 private, 0x34 public
-D LORA_TX_POWER_DBM=17
-D LORA_BW_KHZ=125
-D LORA_SF=7
-D LORA_CR=5 ; coding rate 4/5
-D I2C_SDA_PIN=5
-D I2C_SCL_PIN=6
-D RFM95_SCK=36
-D RFM95_MISO=37
-D RFM95_MOSI=35
-D RFM95_CS=34
-D RFM95_RST=33
-D RFM95_DIO0=2
-D RFM95_DIO1=7
lib_deps =
jgromes/RadioLib@^6.5.0
adafruit/Adafruit BusIO@^1.16.1
adafruit/Adafruit ICM42688@^1.1.0
ArduinoECCX08@^1.3.7
kosme/arduinoFFT@^2.0.1
If you operate in EU868, change LORA_FREQ_MHZ to 868.1. Use a frequency and duty cycle legal in your region.
src/main.cpp
Create src/main.cpp:
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <RadioLib.h>
#include <Adafruit_ICM42688.h>
#include <ArduinoECCX08.h>
#include <arduinoFFT.h>
// ----------------- User-configurable via build_flags -----------------
#ifndef LORA_FREQ_MHZ
#define LORA_FREQ_MHZ 915.0
#endif
#ifndef LORA_SYNC_WORD
#define LORA_SYNC_WORD 0x12
#endif
#ifndef LORA_TX_POWER_DBM
#define LORA_TX_POWER_DBM 17
#endif
#ifndef LORA_BW_KHZ
#define LORA_BW_KHZ 125
#endif
#ifndef LORA_SF
#define LORA_SF 7
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
#ifndef I2C_SDA_PIN
#define I2C_SDA_PIN 5
#endif
#ifndef I2C_SCL_PIN
#define I2C_SCL_PIN 6
#endif
#ifndef RFM95_SCK
#define RFM95_SCK 36
#endif
#ifndef RFM95_MISO
#define RFM95_MISO 37
#endif
#ifndef RFM95_MOSI
#define RFM95_MOSI 35
#endif
#ifndef RFM95_CS
#define RFM95_CS 34
#endif
#ifndef RFM95_RST
#define RFM95_RST 33
#endif
#ifndef RFM95_DIO0
#define RFM95_DIO0 2
#endif
#ifndef RFM95_DIO1
#define RFM95_DIO1 7
#endif
// ----------------- Globals -----------------
Adafruit_ICM42688 icm;
SPIClass spiLoRa;
Module* loraModule = nullptr;
SX1276* lora = nullptr;
static const size_t N_SAMPLES = 256; // window length for FFT/features
static const float FS_HZ = 1000.0f; // effective sample rate
static const float DT_MS = 1000.0f / FS_HZ;
double vReal[N_SAMPLES];
double vImag[N_SAMPLES];
arduinoFFT FFT(vReal, vImag, N_SAMPLES, FS_HZ);
// Baseline stats for features (mean/variance) - simple diagonal covariance
// Feature order: [rms, p2p, crest, kurt, f1, f2] -> 6 features
static const size_t N_FEATS = 6;
float feat_mean[N_FEATS] = {0};
float feat_var[N_FEATS] = {1};
uint32_t baseline_count = 0;
const uint32_t BASELINE_WINDOWS = 30;
// Utility: constrain to bounds
template<typename T>
T clamp(T v, T lo, T hi) { return (v < lo) ? lo : (v > hi ? hi : v); }
// Compute features from time series 'mag' of length N
void compute_features(const float* mag, size_t n, float out_feats[6]) {
// Time-domain
double sum = 0.0, sumsq = 0.0, sum4 = 0.0;
float maxv = -1e9, minv = 1e9;
for (size_t i = 0; i < n; i++) {
float x = mag[i];
sum += x;
sumsq += (double)x * x;
double x2 = (double)x * x;
sum4 += x2 * x2;
if (x > maxv) maxv = x;
if (x < minv) minv = x;
}
double mean = sum / n;
double rms = sqrt(sumsq / n);
double p2p = (double)maxv - (double)minv;
double crest = (rms > 1e-9) ? ((double)maxv / rms) : 0.0;
// Excess kurtosis (normalized 4th central moment - 3)
double m2 = sumsq / n - mean * mean;
if (m2 < 1e-12) m2 = 1e-12;
double m4 = (sum4 / n) - 4*mean*(sumsq/n) + 6*mean*mean*(sum/n) - 3*pow(mean,4); // approximate; acceptable here
double kurtosis = (m4 / (m2 * m2));
// FFT for spectral peaks (magnitude spectrum)
for (size_t i = 0; i < n; i++) {
vReal[i] = (double)mag[i];
vImag[i] = 0.0;
}
FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.Compute(FFT_FORWARD);
FFT.ComplexToMagnitude();
// Find top two peaks excluding DC bin 0
int peak1 = 1, peak2 = 2;
double p1 = 0.0, p2 = 0.0;
for (size_t i = 1; i < n/2; i++) { // only positive freqs
double a = vReal[i];
if (a > p1) {
p2 = p1; peak2 = peak1;
p1 = a; peak1 = (int)i;
} else if (a > p2) {
p2 = a; peak2 = (int)i;
}
}
double f1 = (peak1 * FS_HZ) / n;
double f2 = (peak2 * FS_HZ) / n;
out_feats[0] = (float)rms;
out_feats[1] = (float)p2p;
out_feats[2] = (float)crest;
out_feats[3] = (float)kurtosis;
out_feats[4] = (float)f1;
out_feats[5] = (float)f2;
}
float update_baseline_and_score(const float feats[6], bool& baseline_active) {
if (baseline_count < BASELINE_WINDOWS) {
// Online mean/var (Welford)
baseline_active = true;
for (size_t i = 0; i < N_FEATS; i++) {
float x = feats[i];
float delta = x - feat_mean[i];
feat_mean[i] += delta / (float)(baseline_count + 1);
float delta2 = x - feat_mean[i];
// Unbiased variance update
if (baseline_count == 0) {
feat_var[i] = 0.0f;
} else {
feat_var[i] = ((float)baseline_count * feat_var[i] + delta * delta2) / (float)(baseline_count + 1);
}
feat_var[i] = max(feat_var[i], 1e-6f);
}
baseline_count++;
return 0.0f;
}
baseline_active = false;
// Anomaly score: sum of squared z-scores (diagonal Mahalanobis)
float score = 0.0f;
for (size_t i = 0; i < N_FEATS; i++) {
float z = (feats[i] - feat_mean[i]) / sqrtf(feat_var[i]);
score += z * z;
}
return score;
}
// Build minimal payload and sign with ATECC608A (slot 0)
int build_and_sign_payload(uint8_t* buf, size_t buf_len,
const float feats[6], float score,
size_t& out_len, uint8_t* sig64) {
// Payload layout (little-endian):
// [0] : version = 1
// [1] : flags: bit0=baseline_active
// [2..5]: uptime_ms (uint32)
// [6..17]: features packed Qm.n -> here: 6x int16 (scale defined)
// [18..19]: anomaly score * 100 (uint16, capped)
// Note: signature not part of the hashed payload in this buffer; we sign the payload bytes below.
if (buf_len < 32) return -1;
uint8_t version = 1;
bool baseline_active = (baseline_count < BASELINE_WINDOWS);
uint8_t flags = baseline_active ? 0x01 : 0x00;
buf[0] = version;
buf[1] = flags;
uint32_t t = millis();
memcpy(&buf[2], &t, sizeof(uint32_t));
// Pack features into int16 using per-feature scales
// scales: rms, p2p in g*1000; crest*100; kurt*100; f1,f2 Hz*10
int16_t fpack[6];
fpack[0] = (int16_t)clamp((int32_t)lroundf(feats[0] * 1000.0f), -32768, 32767);
fpack[1] = (int16_t)clamp((int32_t)lroundf(feats[1] * 1000.0f), -32768, 32767);
fpack[2] = (int16_t)clamp((int32_t)lroundf(feats[2] * 100.0f), -32768, 32767);
fpack[3] = (int16_t)clamp((int32_t)lroundf(feats[3] * 100.0f), -32768, 32767);
fpack[4] = (int16_t)clamp((int32_t)lroundf(feats[4] * 10.0f), -32768, 32767);
fpack[5] = (int16_t)clamp((int32_t)lroundf(feats[5] * 10.0f), -32768, 32767);
memcpy(&buf[6], fpack, sizeof(fpack));
uint16_t score_scaled = (uint16_t)clamp((int32_t)lroundf(score * 100.0f), 0, 65535);
memcpy(&buf[18], &score_scaled, sizeof(uint16_t));
size_t payload_len = 20; // header + feats + score
out_len = payload_len;
// Compute digest and sign via ATECC608A
if (!ECCX08.begin()) {
Serial.println(F("[ATECC] begin() failed"));
return -2;
}
// Ensure a private key exists in slot 0; if not, generate it (one-time)
if (!ECCX08.locked()) {
Serial.println(F("[ATECC] Device not locked; generating key in slot 0 (dev mode)"));
if (!ECCX08.generatePrivateKey(0)) {
Serial.println(F("[ATECC] Key generation failed"));
return -3;
}
// In production you must configure and lock the ATECC securely using Microchip provisioning.
}
if (!ECCX08.beginSHA256()) { Serial.println(F("[ATECC] SHA256 begin failed")); return -4; }
ECCX08.updateSHA256(buf, payload_len);
uint8_t digest[32];
if (!ECCX08.endSHA256(digest)) { Serial.println(F("[ATECC] SHA256 end failed")); return -5; }
if (!ECCX08.sign(0, digest, sig64)) {
Serial.println(F("[ATECC] sign failed"));
return -6;
}
return 0;
}
bool initICM() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN, 400000);
delay(10);
if (!icm.begin_I2C(0x68, &Wire)) {
Serial.println(F("[ICM42688] not found at 0x68, trying 0x69"));
if (!icm.begin_I2C(0x69, &Wire)) {
Serial.println(F("[ICM42688] begin failed"));
return false;
}
}
// Configure ranges/data rates
icm.setAccelRange(ICM42688_ACCEL_RANGE_4_G);
icm.setGyroRange(ICM42688_GYRO_RANGE_2000_DPS);
icm.setAccelDataRate(ICM42688_ACCEL_RATE_1000_HZ);
icm.setGyroDataRate(ICM42688_GYRO_RATE_1000_HZ);
icm.setFilterBandwidth(ICM42688_ACCEL_BW_258_9HZ, ICM42688_GYRO_BW_258_9HZ);
icm.setAccelPowerMode(ICM42688_ACCEL_LOW_NOISE);
icm.setGyroPowerMode(ICM42688_GYRO_OFF); // we only use accelerometer for now
delay(50);
Serial.println(F("[ICM42688] initialized"));
return true;
}
bool initLoRa() {
// SPI for LoRa
spiLoRa = SPIClass(FSPI);
spiLoRa.begin(RFM95_SCK, RFM95_MISO, RFM95_MOSI, RFM95_CS);
loraModule = new Module(RFM95_CS, RFM95_DIO0, RFM95_RST, RADIOLIB_NC, spiLoRa, SPISettings(8000000, MSBFIRST, SPI_MODE0));
lora = new SX1276(loraModule);
// Start radio
int state = lora->begin(LORA_FREQ_MHZ, LORA_BW_KHZ, LORA_SF, LORA_CR, LORA_SYNC_WORD, LORA_TX_POWER_DBM, 8, 0);
if (state != RADIOLIB_ERR_NONE) {
Serial.print(F("[LoRa] begin failed, code ")); Serial.println(state);
return false;
}
// Optional fine-tuning
lora->setCRC(true);
lora->setCurrentLimit(100); // mA
lora->setDio0Action([](){ /* TX done / RX done IRQ callback not used here */ });
Serial.println(F("[LoRa] initialized"));
return true;
}
void setup() {
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println(F("ESP32-S3 lora-predictive-vibration-icm42688"));
Serial.printf("Build: %s %s\n", __DATE__, __TIME__);
if (!initICM()) {
Serial.println(F("ICM init failed; halt"));
while (true) delay(1000);
}
if (!initLoRa()) {
Serial.println(F("LoRa init failed; halt"));
while (true) delay(1000);
}
// Initialize ATECC (test presence early)
if (!ECCX08.begin()) {
Serial.println(F("[ATECC] not detected; signing will fail"));
} else {
Serial.printf("[ATECC] locked: %s\n", ECCX08.locked() ? "yes" : "no");
}
Serial.println(F("Baseline learning... keep device in normal state"));
}
void loop() {
// Acquire a window of samples from accelerometer (m/s^2 or g? library returns m/s^2)
// Adafruit ICM42688 returns accel in m/s^2; convert to g
sensors_event_t accel, gyro, temp;
static float mag[N_SAMPLES];
for (size_t i = 0; i < N_SAMPLES; i++) {
icm.getEvent(&accel, &gyro, &temp);
float ax_g = accel.acceleration.x / 9.80665f;
float ay_g = accel.acceleration.y / 9.80665f;
float az_g = accel.acceleration.z / 9.80665f;
// Remove gravity by high-pass: approximate via subtracting running mean; here subtract DC later by FFT windowing
// We compute magnitude which includes gravity; to reduce DC, subtract mean later via windowing. For time-domain features, it’s acceptable.
float m = sqrtf(ax_g*ax_g + ay_g*ay_g + az_g*az_g);
mag[i] = m;
delayMicroseconds((int)(DT_MS * 1000.0f)); // coarse pacing, not perfect
}
float feats[6];
compute_features(mag, N_SAMPLES, feats);
bool baseline_active = false;
float score = update_baseline_and_score(feats, baseline_active);
// Prepare payload and sign
uint8_t payload[24];
size_t payload_len = 0;
uint8_t sig[64];
int rc = build_and_sign_payload(payload, sizeof(payload), feats, score, payload_len, sig);
// Assemble final packet: payload + signature (total ~84 bytes)
uint8_t packet[128];
if (rc == 0) {
memcpy(packet, payload, payload_len);
memcpy(packet + payload_len, sig, 64);
} else {
// If signing failed, send payload only with flags indicating failure
payload[1] |= 0x80; // set 'signing failed' flag
memcpy(packet, payload, payload_len);
}
size_t packet_len = (rc == 0) ? (payload_len + 64) : payload_len;
// Transmit
int state = lora->transmit(packet, packet_len);
Serial.printf("[TX] len=%u state=%d baseline=%s score=%.2f feats=[%.3f,%.3f,%.2f,%.2f,%.1f,%.1f]\n",
(unsigned)packet_len, state, baseline_active ? "yes" : "no", score,
feats[0], feats[1], feats[2], feats[3], feats[4], feats[5]);
// Duty-cycling: respect band limits; for lab demo, 2 s pause
delay(2000);
}
Optional receiver sketch for a second node (SX1276) is provided in Validation.
Build/Flash/Run Commands
Use PlatformIO CLI for deterministic builds.
pio --version
# Create the project folder (if not already created)
mkdir -p ~/work/lora-predictive-vibration-icm42688 && cd ~/work/lora-predictive-vibration-icm42688
# Initialize for ESP32-S3-DevKitC-1 (already provided platformio.ini)
pio project init --board esp32-s3-devkitc-1
# Put platformio.ini and src/main.cpp as shown above, then build:
pio run
# Connect board via USB (ESP32-S3 native USB). Flash:
pio run -t upload
# Open serial monitor at 115200 baud:
pio device monitor -b 115200
# Optional: change LoRa frequency (EU868) via environment override:
pio run -e esp32-s3-devkitc-1 -t upload --project-option "build_flags=-D LORA_FREQ_MHZ=868.1"
Windows note: If your board enumerates as “USB JTAG/serial” on COMx, use that port. If your hardware variant uses CP210x, ensure the Silicon Labs driver is installed. On macOS, the device appears under /dev/tty.usbmodem* (CDC) or /dev/tty.SLAB_USBtoUART (CP210x).
Step-by-step Validation
Follow these steps to validate sensing, analytics, cryptography, and RF.
1) Power-on self-test and I2C presence
- Open the serial monitor:
- You should see:
- “[ICM42688] initialized”
- “[LoRa] initialized”
- “[ATECC] locked: yes/no”
- “Baseline learning… keep device in normal state”
- If the IMU is not detected:
- Verify SDA/SCL pins (GPIO5/6) and address pins (AD0 to GND = 0x68).
- Check for pull-ups (4.7 kΩ) if your breakouts do not include them.
2) IMU measurement sanity
- Keep the device stationary; observe the log every ≈2 s:
- “baseline=yes” for the first 30 windows (about 1 minute).
- Features should be small and consistent, e.g., RMS near the noise floor (a few tens of mg) depending on your setup.
- Gently tap or place the board on a running device (electric toothbrush, small fan, phone vibration):
- RMS and peak-to-peak should increase significantly.
- Spectral peaks (f1, f2) should show identifiable frequencies (e.g., around 100–300 Hz for small motors).
- After baseline completes (baseline=no), the anomaly score should rise when vibrations deviate from baseline.
Tip: For reproducible testing, record quiet baseline, then attach the board near a small DC motor with an eccentric load.
3) Baseline/anomaly behavior
- During the first 30 windows, the device learns mean and variance for the 6 features (simple diagonal covariance).
- After learning:
- At rest (normal), anomaly score should be low (near 0–3).
- Excitation (abnormal), anomaly score should rise (e.g., 10–80+ depending on severity).
- If baseline drifts undesirably, reboot or adapt the code to re-baseline periodically or on command.
4) ATECC608A signature
- The code computes a SHA‑256 digest of the payload on the ATECC608A and signs with ECDSA P‑256 in slot 0.
- In the serial log, if the secure element is present and locked, no “[ATECC] sign failed” should appear.
- To verify the signature off-device:
- Retrieve the generated public key from slot 0 in a provisioning build (e.g., add
ECCX08.generatePublicKey(0, pubkey)and print it once), then verify signatures on a receiver/server. - In production, the ATECC608A should be configured and locked with a proper provisioning flow and certificates.
5) LoRa RF validation (two options)
A. With a second SX1276 node (recommended)
– Flash a receiver sketch onto another SX1276-based board (same frequency, SF, BW, CR, sync word).
– Minimal RadioLib receiver code:
// Receiver minimal sketch for validation (SX1276 + any Arduino/ESP32)
#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
#define RFM95_SCK 18
#define RFM95_MISO 19
#define RFM95_MOSI 23
#define RFM95_CS 5
#define RFM95_RST 14
#define RFM95_DIO0 26
SPIClass spiLoRa(VSPI);
Module* mod;
SX1276* lora;
void setup() {
Serial.begin(115200);
spiLoRa.begin(RFM95_SCK, RFM95_MISO, RFM95_MOSI, RFM95_CS);
mod = new Module(RFM95_CS, RFM95_DIO0, RFM95_RST, RADIOLIB_NC, spiLoRa, SPISettings(8000000, MSBFIRST, SPI_MODE0));
lora = new SX1276(mod);
int state = lora->begin(915.0, 125, 7, 5, 0x12, 17, 8, 0);
if (state != RADIOLIB_ERR_NONE) { Serial.println("begin failed"); while(1){} }
lora->setCRC(true);
Serial.println("RX ready");
}
void loop() {
String str;
int state = lora->receive(str);
if (state == RADIOLIB_ERR_NONE) {
Serial.print("RX: "); Serial.println(str.length());
// Print hex
for (size_t i = 0; i < str.length(); i++) {
uint8_t b = (uint8_t)str[i];
Serial.printf("%02X ", b);
}
Serial.println();
}
}
- Match frequency/SF/BW/CR/sync word with the transmitter (see platformio.ini).
- Confirm received packets and lengths ≈84 bytes.
- Optionally, parse the first 20 bytes as payload and the last 64 as ECDSA signature; verify on a host tool.
B. With SDR or spectrum analyzer
– Use an SDR (e.g., RTL‑SDR + inspectrum) tuned to your frequency to confirm chirp transmissions every ~2 seconds.
– You won’t decode content but can validate on-air activity and duty cycle.
6) End-to-end functional check
- Start with the device still, allow baseline to complete.
- Cause a controlled vibration (e.g., small motor), observe:
- Features change and anomaly score increases.
- LoRa packets continue to transmit at the expected interval.
- If you have a receiver:
- Log received payloads, confirm that feature fields and anomaly score correlate with observed vibration.
- Optionally verify the signature using the known public key.
Troubleshooting
- No serial output:
- Ensure monitor at 115200.
- For ESP32‑S3 CDC, try a different USB cable/port. On Windows, look for “USB JTAG/serial” COM device.
- ICM‑42688‑P not found:
- Check address pin AD0/SA0: GND=0x68, VCC=0x69. Try both in code.
- Verify 3.3 V supply and I2C pull-ups; ensure SDA=GPIO5, SCL=GPIO6 match wiring.
- Reduce I2C speed to 100 kHz for long wires: change
Wire.begin(..., 100000). - ATECC608A sign failed:
- Confirm it’s the I2C variant (0x60). Check wiring on the same I2C bus.
- If
ECCX08.locked()is false, your device is unconfigured; the demo generates a key in slot 0 for testing. In production, follow Microchip’s configuration/locking procedure. - LoRa begin failed (RadioLib error):
- Re-check SPI pins and CS/RST/DIO0 wiring.
- Ensure an antenna is connected and the module’s band matches your chosen frequency.
- Lower SPI speed: change SPISettings to 2 MHz if needed.
- No RF reception with second node:
- Ensure all PHY params match (freq, SF, BW, CR, sync word).
- Place nodes at least a few meters apart to avoid front-end desense.
- Anomaly score always high:
- Increase baseline windows (
BASELINE_WINDOWS) or ensure true “normal” state during learning. - Add high-pass filtering to remove gravity bias if your mounting orientation changes often.
- Features look noisy:
- Mechanically isolate the board. Consider rigidly mounting to the machine surface for consistent coupling.
- Increase window length (e.g., 512) and adjust sampling rate to keep spectral resolution acceptable.
Improvements
- LoRaWAN instead of raw LoRa:
- The RFM95W (SX1276) is fine for LoRaWAN using an appropriate stack, but this example uses raw LoRa for simplicity. For TTN/LoRaWAN, integrate an LMIC or a RadioLib-based LoRaWAN stack, observe payload size and duty-cycle regulations, and handle join/session keys securely in ATECC608A.
- Power optimization:
- Increase window interval, lower TX power, batch multiple windows per uplink, or use adaptive intervals based on anomaly score.
- Use light sleep between windows; wake on a hardware timer.
- Better features and models:
- Add band power in specific bands (1X, 2X, 3X shaft frequency), spectral kurtosis, or cepstral coefficients.
- Replace z-score anomaly with a compact on-device model (e.g., 1‑class SVM or an autoencoder via TensorFlow Lite Micro).
- Sensor configuration:
- Use the ICM‑42688‑P FIFO and hardware data-ready interrupts instead of polling for consistent sampling and reduced jitter.
- Apply a proper DC removal/high-pass filter before RMS and crest calculation.
- Cryptography and provisioning:
- Lock and provision ATECC608A in production with a secure configuration zone, store the public key with your backend, and attach a compressed signature (ASN.1 DER) if needed.
- Packet protocol:
- Add a message counter, device ID, and CRC at the application level; optionally compress features further or switch to CBOR/FlatBuffers.
- Edge commands:
- Implement downlink over LoRa (requires an RX loop and coordination) to adjust thresholds, request re-baseline, or change the TX interval.
Checklist
- Materials
- ESP32‑S3‑DevKitC‑1, RFM95W (SX1276), ICM‑42688‑P, ATECC608A, antenna, wires.
- Wiring
- RFM95W: SPI to GPIO36/37/35, CS=34, RST=33, DIO0=2, GND, 3.3 V.
- ICM‑42688‑P: I2C SDA=5, SCL=6, 3.3 V, GND, AD0=GND (0x68).
- ATECC608A: I2C SDA=5, SCL=6, 3.3 V, GND.
- Software
- PlatformIO Core installed; project created with platformio.ini and libraries.
- Build/flash with
pio runandpio run -t upload. - Serial monitor at 115200 with
pio device monitor. - Validation
- ICM initialized, LoRa initialized, ATECC found (and locked status noted).
- Baseline completes (first ~60 s), features reasonable at rest.
- Vibration test increases RMS/peak and anomaly score.
- LoRa packets observed (second node or SDR); optional signature verification.
- Troubleshooting done if any step failed.
- Regulatory
- Frequency plan and duty cycle are configured for your region (LORA_FREQ_MHZ, TX interval).
This advanced case provides a complete and reproducible reference for implementing predictive vibration analytics on ESP32‑S3 with ICM‑42688‑P, securing results via ATECC608A, and uplinking over raw LoRa with SX1276. Adapt the PHY parameters and features to your machine’s vibration profile and local radio regulations for robust, production-ready deployments.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



