Close Menu
  • Articles
    • Learn Electronics
    • Product Review
    • Tech Articles
  • Electronics Circuits
    • 555 Timer Projects
    • Op-Amp Circuits
    • Power Electronics
  • Microcontrollers
    • Arduino Projects
    • STM32 Projects
    • AMB82-Mini IoT AI Camera
    • BLE Projects
  • IoT Projects
    • ESP8266 Projects
    • ESP32 Projects
    • ESP32 MicroPython
    • ESP32-CAM Projects
    • LoRa/LoRaWAN Projects
  • Raspberry Pi
    • Raspberry Pi Projects
    • Raspberry Pi Pico Projects
    • Raspberry Pi Pico W Projects
  • Electronics Calculator
Facebook X (Twitter) Instagram
  • About Us
  • Disclaimer
  • Privacy Policy
  • Contact Us
  • Advertise With Us
Facebook X (Twitter) Instagram Pinterest YouTube LinkedIn
How To Electronics
  • Articles
    • Learn Electronics
    • Product Review
    • Tech Articles
  • Electronics Circuits
    • 555 Timer Projects
    • Op-Amp Circuits
    • Power Electronics
  • Microcontrollers
    • Arduino Projects
    • STM32 Projects
    • AMB82-Mini IoT AI Camera
    • BLE Projects
  • IoT Projects
    • ESP8266 Projects
    • ESP32 Projects
    • ESP32 MicroPython
    • ESP32-CAM Projects
    • LoRa/LoRaWAN Projects
  • Raspberry Pi
    • Raspberry Pi Projects
    • Raspberry Pi Pico Projects
    • Raspberry Pi Pico W Projects
  • Electronics Calculator
How To Electronics
Home » Flight Black-Box Motion Recorder System using ESP32 & BMI160
ESP32 Projects IoT Projects

Flight Black-Box Motion Recorder System using ESP32 & BMI160

Mamtaz AlamBy Mamtaz AlamUpdated:December 7, 202510 Mins Read
Share Facebook Twitter LinkedIn Telegram Reddit WhatsApp
Flight Black-Box Motion Recorder using ESP32 & BMI160
Share
Facebook Twitter LinkedIn Pinterest Email Reddit Telegram WhatsApp

Overview

In this project, we will build a Flight Black-Box Motion Recorder System using ESP32 and the BMI160 IMU sensor. The BMI160 is a highly integrated 6-axis motion sensor that combines a 3-axis accelerometer and a 3-axis gyroscope in a single compact chip.

The ESP32–BMI160 Flight Black-Box Motion Recorder System is a demonstration device designed to study and analyze flight motion. It can also be used as a functional prototype that mimics the behavior of real aircraft black-box motion monitoring systems.

The device continuously tracks acceleration, rotation, vibration, orientation, and key motion events such as free-falls and impacts. Data is visualized on an onboard OLED screen and a beautifully designed real-time web dashboard. This compact system works like a mini flight recorder, perfect for drones, RC aircraft, vehicles, or motion-analysis experiments.

Before moving ahead, you can take a look at some tutorials & a few projects built using the BMI160 accelerometer & gyroscope module:

  • DIY Pedometer using BMI160 ESP32
  • BMI160 ESP32 Interfacing Guide
  • BMI160 Arduino Interfacing Guide
  • BMI160 Raspberry Pi Pico Interfacing Guide

Bill of Materials

To build the Flight Black-Box Motion Recorder System, we need following components.

S.N.Components NameQuantityPurchase Link
1ESP32 Board1Amazon | AliExpress
2BMI160 Accelerometer Gyroscope1Amazon | AliExpress
30.96" OLED Display1Amazon | AliExpress
Push Button Switch1Amazon | AliExpress
Resistor 1K1Amazon | AliExpress
3.7V Lithium-ion Battery1Amazon | AliExpress
Boost Converter Module1Amazon | AliExpress
Slide Switch1Amazon | AliExpress




BMI160 Accelerometer/Gyroscope Module

The BMI160 is a high-performance, ultra-low-power 6-axis Inertial Measurement Unit (IMU) that integrates a 3-axis accelerometer and a 3-axis gyroscope into a single compact package. Designed by Bosch Sensortec, it delivers precise motion, orientation, and vibration measurements, making it ideal for modern embedded systems. It supports I²C/SPI communication.

BMI160 Gyroscope Accelerometer
Fig: Front & Back of BMI160

Technically, the BMI160 offers a wide selection of measurement ranges, including ±2g to ±16g for acceleration and ±125°/s to ±2000°/s for angular velocity. It features 16-bit output resolution, excellent noise performance, and advanced power modes—from full operation at under 1 mA to 3 µA in suspend mode—making it well-suited for battery-powered applications. The sensor also includes a 1024-byte FIFO, two programmable interrupt engines, and support for programmable sampling rates up to 1600 Hz, enabling efficient data logging and real-time motion tracking.

With built-in features such as step counting, motion detection, gesture recognition, and 6D orientation sensing, the BMI160 stands out as a versatile and highly capable IMU for robotics, mobile devices, augmented reality, cameras, toys, drones, and navigation systems.


Flight Black-Box Motion Recorder using ESP32 & BMI160

Lets build the hardware part of the Flight Black-Box Motion Recorder using ESP32 & BMI160. We will see the schematic, hardware, circuit etc and understand the system in detail.

Block Diagram of Flight Black-Box Motion Recorder

A Black-Box Motion Recorder System is a compact, self-powered device designed to capture, analyze, and visualize motion activity from moving platforms such as drones, RC aircraft, vehicles, or handheld devices. It continuously records acceleration, rotation, vibration, orientation, and event-based motion such as impacts or free-falls, allowing users to understand real-world dynamics, stability, and abnormal behavior.

Block Diagram of Flight Black-Box Motion Recorder
Fig: Block Diagram of Flight Black-Box Motion Recorder

Based on the block diagram, the system consists of the following functional units:

  • Li-Po Battery → Buck-Boost Converter – Supplies a stable 5V to power the device.
  • ESP32 Microcontroller – Reads motion data, processes acceleration and rotation, detects events, and manages storage.
  • BMI160 IMU Sensor – Provides 6-axis acceleration and gyroscope measurements.
  • OLED Display – Shows real-time motion values, orientation, peaks, and diagnostics.
  • Push Button – Allows page switching, reset operations, and calibration control.



Circuit Diagram/Schematic

First, let’s look at the hardware design of the ESP32-based Flight Black-Box Motion Recorder System, which captures motion data during flight and displays it on an OLED screen.

Flight Black-Box Motion Recorder System
Fig: ESP32 BMI160 Hardware Design

In this system, the ESP32 is the main controller that handles motion processing, orientation calculation, event detection, data logging, and Wi-Fi dashboard hosting. The BMI160 IMU sensor provides precise 3-axis acceleration and 3-axis gyroscope data required to analyze flight dynamics. The OLED display shows real-time motion parameters such as G-force, rotation, vibration, orientation, peaks, and diagnostic information.

Both the BMI160 sensor and the OLED display share the same I²C bus on the ESP32 (SDA and SCL pins). A push button connected to GPIO 23 is used for page switching, resetting peaks, and entering calibration mode during startup.

Fig: Hardware Board (Top Side)

A 3.7 V Li-Po battery powers the entire circuit. Since the ESP32 requires 5 V, a buck-boost converter is used to regulate the battery voltage to a stable 5 V. This converter must be adjusted to exactly 5 V before use, as many modules ship with a default output around 12 V.

Fig: Hardware Board (Bottom Side)

All components are mounted on a zero-PCB protoboard for a compact and durable assembly. Female headers hold the ESP32, OLED, and BMI160 modules in place, while a slide switch provides convenient on/off control.

PCB Ordering Online

To order the PCB, visit the HQ NextPCB Official Website and upload the Gerber file using the Quote Now option. You can then choose your required parameters, such as Material Type, Dimensions, Quantity, Thickness, and Solder Mask Color.

HQ NextPCB is making PCB prototyping more affordable for new users by offering 0.1$ total cost when PCB price ≤ $10 on your first PCB order. They also offer $0 shipping cost when shipping cost ≤ $10. With this promotion, you can enjoy free shipping on your first order—no restrictions on size, layers, or quantity.

Here is the campaign detail: Unlock $0.1 PCB Prototyping. You can also use the DFM Analysis tool called HQDFM for PCB analysis.

Once all the details are filled in, select your country and shipping method. After confirming everything, you can place the order and wait for your boards to arrive.




Source Code/Program

Lets go the program/code of the Flight Motion Recorder System using BMI160 & ESP32. The following program turns the ESP32 + BMI160 + OLED into a flight black-box motion recorder with both an onboard display and a live web dashboard.

From the following lines in main.ino file, you need to change the WiFi SSID and Password. Replace it with your home network credentials.

1
2
const char* WIFI_SSID     = "******************";
const char* WIFI_PASSWORD = "******************";

The code has two files main.ino file and webpage.h file. The webpage file is seperated from main code and integrated in the header file.

main.ino code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WebServer.h>
#include "webpage.h"  // <-- keep webpage.h in the same folder
 
// ===================== Wi-Fi / Web (STA-only) =====================
const char* WIFI_SSID     = "**************";
const char* WIFI_PASSWORD = "**************";
 
WebServer server(80);
 
struct Telemetry {
  float nowG=0, gyroAbs=0, vibRms=0;
  float roll=0, pitch=0;
  float ax=0, ay=0, az=0, gx=0, gy=0, gz=0;
  float hz=0;
  uint32_t lastEventAgeMs=0;
  uint8_t addr=0x68;
} telem;
 
// ===================== OLED =====================
#define OLED_W 128
#define OLED_H 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 oled(OLED_W, OLED_H, &Wire);
 
// ---------- Layout ----------
static inline int rowY(int i){ return 12 + 10*i; } // rows at 12,22,32,42,52
const int COL1_X = 2;
const int COL2_X = 66;
 
// ===================== Pins =====================
#define SDA_PIN 21
#define SCL_PIN 22
#define BUTTON_PIN 23  // push-button to 3V3, using INPUT_PULLDOWN
 
// ===================== BMI160 (register-level) =====================
static const uint8_t BMI160_ADDR_PRIMARY = 0x68;
static const uint8_t BMI160_ADDR_ALT     = 0x69;
static const uint8_t REG_GYRO_START  = 0x0C; // GxL..GzH
static const uint8_t REG_ACCEL_START = 0x12; // AxL..AzH
static const uint8_t REG_CMD         = 0x7E;
static const uint8_t CMD_SOFTRESET   = 0xB6;
static const uint8_t CMD_ACC_NORMAL  = 0x11;
static const uint8_t CMD_GYR_NORMAL  = 0x15;
 
static const float ACC_LSB_PER_G    = 16384.0f; // ±2g default
static const float GYRO_LSB_PER_DPS = 131.0f;   // ±250 dps default
 
uint8_t bmi_addr = BMI160_ADDR_PRIMARY;
 
// ===================== Persistence (NVS) =====================
Preferences prefs;
struct Calib {
  float ax_off=0, ay_off=0, az_off=0;  // g (aim: az ~ +1g at rest)
  float gx_off=0, gy_off=0, gz_off=0;  // dps
} calib;
 
struct Peaks {
  float maxG=0.0f,   minG=  999.0f;
  float maxDPS=0.0f, minDPS=99999.0f;
  float maxVib=0.0f;
  float lastImpactG=0.0f;
  uint32_t freefalls=0, impacts=0, events=0;
} peaks;
 
void loadNVS() {
  prefs.begin("bb-rec", true);
  calib.ax_off = prefs.getFloat("ax_off", 0.0f);
  calib.ay_off = prefs.getFloat("ay_off", 0.0f);
  calib.az_off = prefs.getFloat("az_off", 0.0f);
  calib.gx_off = prefs.getFloat("gx_off", 0.0f);
  calib.gy_off = prefs.getFloat("gy_off", 0.0f);
  calib.gz_off = prefs.getFloat("gz_off", 0.0f);
  peaks.maxG      = prefs.getFloat("maxG", 0.0f);
  peaks.minG      = prefs.getFloat("minG", 999.0f);
  peaks.maxDPS    = prefs.getFloat("maxDPS", 0.0f);
  peaks.minDPS    = prefs.getFloat("minDPS", 99999.0f);
  peaks.maxVib    = prefs.getFloat("maxVib", 0.0f);
  peaks.lastImpactG= prefs.getFloat("lastImp", 0.0f);
  peaks.freefalls = prefs.getULong("freefalls", 0);
  peaks.impacts   = prefs.getULong("impacts",   0);
  peaks.events    = prefs.getULong("events",    0);
  prefs.end();
}
void saveNVS() {
  prefs.begin("bb-rec", false);
  prefs.putFloat("ax_off", calib.ax_off);
  prefs.putFloat("ay_off", calib.ay_off);
  prefs.putFloat("az_off", calib.az_off);
  prefs.putFloat("gx_off", calib.gx_off);
  prefs.putFloat("gy_off", calib.gy_off);
  prefs.putFloat("gz_off", calib.gz_off);
  prefs.putFloat("maxG",   peaks.maxG);
  prefs.putFloat("minG",   peaks.minG);
  prefs.putFloat("maxDPS", peaks.maxDPS);
  prefs.putFloat("minDPS", peaks.minDPS);
  prefs.putFloat("maxVib", peaks.maxVib);
  prefs.putFloat("lastImp",peaks.lastImpactG);
  prefs.putULong("freefalls", peaks.freefalls);
  prefs.putULong("impacts",   peaks.impacts);
  prefs.putULong("events",    peaks.events);
  prefs.end();
}
void clearPeaks(){ peaks = Peaks(); saveNVS(); }
 
// ===================== I2C helpers =====================
bool i2cWriteReg(uint8_t addr, uint8_t reg, uint8_t val) {
  Wire.beginTransmission(addr);
  Wire.write(reg); Wire.write(val);
  return Wire.endTransmission() == 0;
}
bool i2cReadBytes(uint8_t addr, uint8_t startReg, uint8_t len, uint8_t* buf) {
  Wire.beginTransmission(addr);
  Wire.write(startReg);
  if (Wire.endTransmission(false) != 0) return false;
  uint8_t got = Wire.requestFrom(addr, len);
  if (got != len) return false;
  for (uint8_t i=0;i<len;i++) buf[i]=Wire.read();
  return true;
}
static inline int16_t to_i16(uint8_t lo, uint8_t hi) {
  return (int16_t)((uint16_t)lo | ((uint16_t)hi << 8));
}
 
// ===================== BMI160 bring-up =====================
bool bmi160Begin() {
  for (uint8_t attempt=0; attempt<2; ++attempt) {
    uint8_t addr = (attempt==0)? BMI160_ADDR_PRIMARY : BMI160_ADDR_ALT;
    if (!i2cWriteReg(addr, REG_CMD, CMD_SOFTRESET)) continue;
    delay(100);
    if (!i2cWriteReg(addr, REG_CMD, CMD_ACC_NORMAL)) continue; delay(50);
    if (!i2cWriteReg(addr, REG_CMD, CMD_GYR_NORMAL)) continue; delay(60);
    uint8_t tmp[6];
    if (!i2cReadBytes(addr, REG_ACCEL_START, 6, tmp)) continue;
    bmi_addr = addr;
    return true;
  }
  return false;
}
bool readAccel_g(float& ax, float& ay, float& az) {
  uint8_t b[6]; if (!i2cReadBytes(bmi_addr, REG_ACCEL_START, 6, b)) return false;
  ax = to_i16(b[0], b[1]) / ACC_LSB_PER_G;
  ay = to_i16(b[2], b[3]) / ACC_LSB_PER_G;
  az = to_i16(b[4], b[5]) / ACC_LSB_PER_G;
  return true;
}
bool readGyro_dps(float& gx, float& gy, float& gz) {
  uint8_t b[6]; if (!i2cReadBytes(bmi_addr, REG_GYRO_START, 6, b)) return false;
  gx = to_i16(b[0], b[1]) / GYRO_LSB_PER_DPS;
  gy = to_i16(b[2], b[3]) / GYRO_LSB_PER_DPS;
  gz = to_i16(b[4], b[5]) / GYRO_LSB_PER_DPS;
  return true;
}
 
// ===================== Sampling / Filters =====================
static const float SAMPLE_HZ   = 200.0f;
static const uint32_t SAMPLE_US= (uint32_t)(1e6f / SAMPLE_HZ);
float roll_deg=0, pitch_deg=0;
 
const int VIB_WIN = 64;
float vibBuf[VIB_WIN]; int vibIdx=0; int vibCnt=0;
 
const int CHART_W = 124; // safe bars between x=2..125 (inclusive)
float chartA[CHART_W]; int chA=0;
float chartG[CHART_W]; int chG=0;
float chartV[CHART_W]; int chV=0;
 
float measuredHz = 0.0f;
uint32_t fpsCount=0, fpsLastMs=0;
 
// ===================== Events / Thresholds =====================
const float FREEFALL_G = 0.25f;
const uint32_t FREEFALL_MIN_MS = 30;
const float IMPACT_G  = 2.5f;
bool inFreefall=false;
uint32_t freefallStartMs=0;
uint32_t lastEventMs=0;
 
// ===================== Pages =====================
enum Page {
  PAGE_HOME=0,         // Summary (very compact)
  PAGE_ACCEL,          // Accel chart
  PAGE_GYRO,           // Gyro chart
  PAGE_VIB,            // Vib RMS chart
  PAGE_ORIENT,         // Orientation (bubble + numbers)
  PAGE_PEAKS_A,        // MaxG / MinG / MaxVib
  PAGE_PEAKS_B,        // MaxDPS / MinDPS / LastImpactG
  PAGE_EVENTS,         // Freefalls / Impacts / Last event
  PAGE_DIAG_A,         // Addr/Hz + ax/ay
  PAGE_DIAG_B,         // az + gx/gy/gz + |a|/|w|
  PAGE_COUNT
};
int page = PAGE_HOME;
 
// ===================== Button =====================
struct ButtonState { bool last=false; uint32_t lastMs=0; bool pressed=false; uint32_t startMs=0; } btn;
const uint16_t DEBOUNCE_MS=30, LONGPRESS_MS=1200;
 
void pollButton(uint32_t nowMs) {
  bool lvl = digitalRead(BUTTON_PIN); // pulldown: LOW idle, HIGH pressed
  if (lvl != btn.last && (nowMs - btn.lastMs) >= DEBOUNCE_MS) {
    btn.last = lvl; btn.lastMs = nowMs;
    if (lvl) { btn.pressed = true; btn.startMs = nowMs; }
    else if (btn.pressed) {
      uint32_t dur = nowMs - btn.startMs; btn.pressed = false;
      if (dur >= LONGPRESS_MS) {
        clearPeaks();
        oled.clearDisplay();
        oled.fillRect(0,0,128,10,SSD1306_WHITE);
        oled.setTextColor(SSD1306_BLACK); oled.setTextSize(1);
        oled.setCursor(2,1); oled.print("Black-Box");
        oled.setTextColor(SSD1306_WHITE);
        oled.setCursor(100,1); oled.print("OK");
        oled.setCursor(COL1_X, rowY(2)); oled.print("Peaks reset");
        oled.display(); delay(500);
      } else {
        page = (page + 1) % PAGE_COUNT;
      }
    }
  }
}
 
// ===================== UI helpers =====================
void header(const char* title) {
  oled.fillRect(0,0,128,10,SSD1306_WHITE);
  oled.setTextSize(1);
  oled.setTextColor(SSD1306_BLACK);
  oled.setCursor(2,1); oled.print(title);
  oled.setCursor(106,1);
  oled.print(page+1); oled.print('/'); oled.print(PAGE_COUNT);
  oled.setTextColor(SSD1306_WHITE);
}
void stripChart(const char* title, float* buf, int headIdx, float nowVal, float vmax, const char* unit) {
  header(title);
  oled.drawRect(1, 12, 126, 34, SSD1306_WHITE);
  int y0 = 45; // bottom inside
  for (int i=0;i<CHART_W;i++) {
    int idx = (headIdx + i) % CHART_W;
    float vv = buf[idx]; if (vv > vmax) vv = vmax;
    int h = (int)(vv * (32.0f / vmax));
    int x = 2 + i;
    if (h > 0) oled.drawLine(x, y0, x, y0 - h, SSD1306_WHITE);
  }
  oled.setCursor(COL1_X, rowY(4));
  if (unit[0]=='d') { oled.print((int)nowVal); oled.print(unit); }
  else { oled.print(nowVal,1); oled.print(unit); }
}
 
// ===================== Pages =====================
static inline float clampf(float v, float lo, float hi){ return v<lo?lo:(v>hi?hi:v); }
 
void pageHome(float gRes, float gyroAbs, float vibRms) {
  header("Summary");
  oled.setCursor(COL1_X, rowY(0)); oled.print("NowG:"); oled.print(gRes,2); oled.print(" g");
  oled.setCursor(COL2_X, rowY(0)); oled.print("|w| :"); oled.print((int)gyroAbs); oled.print(" dps");
  oled.setCursor(COL1_X, rowY(1)); oled.print("Vib :"); oled.print(vibRms,2); oled.print(" g");
  oled.setCursor(COL2_X, rowY(1)); oled.print("Hz  :"); oled.print((int)(measuredHz+0.5f));
  oled.setCursor(COL1_X, rowY(2)); oled.print("MaxG:"); oled.print(peaks.maxG,2);
  oled.setCursor(COL2_X, rowY(2)); oled.print("MaxD:"); oled.print((int)peaks.maxDPS);
  oled.setCursor(COL1_X, rowY(3)); oled.print("MinG:"); oled.print(peaks.minG,2);
  oled.setCursor(COL2_X, rowY(3)); oled.print("MinD:"); oled.print((int)peaks.minDPS);
  oled.setCursor(COL1_X, rowY(4)); oled.print("F:"); oled.print(peaks.freefalls); oled.print(" I:"); oled.print(peaks.impacts);
}
void pageAccel(float gRes) {
  float v = fabsf(gRes - 1.0f); if (v > 2.0f) v = 2.0f;
  chartA[chA] = v; chA = (chA+1) % CHART_W;
  stripChart("Accel |a|-1g", chartA, chA, v, 2.0f, "g");
}
void pageGyro(float gyroAbs) {
  float v = gyroAbs; if (v > 1200.0f) v = 1200.0f;
  chartG[chG] = v; chG = (chG+1) % CHART_W;
  stripChart("Gyro |w|", chartG, chG, gyroAbs, 1200.0f, "dps");
}
void pageVib(float vibRms) {
  float v = vibRms; if (v > 2.0f) v = 2.0f;
  chartV[chV] = v; chV = (chV+1) % CHART_W;
  stripChart("Vibration RMS", chartV, chV, vibRms, 2.0f, "g");
}
void pageOrient(float gRes) {
  header("Orientation");
  int cx = 24;
  int cy = 29;
  int R  = 14;
  oled.drawCircle(cx, cy, R, SSD1306_WHITE);
  int rx = (int)(clampf(roll_deg, -45, 45)  * (R/45.0f));
  int ry = (int)(clampf(pitch_deg,-45, 45) * (R/45.0f));
  oled.fillCircle(cx+rx, cy-ry, 3, SSD1306_WHITE);
  oled.setCursor(COL2_X-10, rowY(1)); oled.print("Roll :"); oled.print(roll_deg,1);
  oled.setCursor(COL2_X-10, rowY(2)); oled.print("Pitch:"); oled.print(pitch_deg,1);
  oled.setCursor(COL2_X-10, rowY(3)); oled.print("|a|:"); oled.print(gRes,2); oled.print("g");
}
void pagePeaksA() {
  header("Peaks A");
  oled.setCursor(COL1_X, rowY(0)); oled.print("MaxG : "); oled.print(peaks.maxG,2); oled.print(" g");
  oled.setCursor(COL1_X, rowY(1)); oled.print("MinG : "); oled.print(peaks.minG,2); oled.print(" g");
  oled.setCursor(COL1_X, rowY(2)); oled.print("MaxV : "); oled.print(peaks.maxVib,2); oled.print(" g");
}
void pagePeaksB() {
  header("Peaks B");
  oled.setCursor(COL1_X, rowY(0)); oled.print("MaxD : "); oled.print((int)peaks.maxDPS); oled.print(" dps");
  oled.setCursor(COL1_X, rowY(1)); oled.print("MinD : "); oled.print((int)peaks.minDPS); oled.print(" dps");
  oled.setCursor(COL1_X, rowY(2)); oled.print("LastG: "); oled.print(peaks.lastImpactG,2); oled.print(" g");
}
void pageEvents(uint32_t nowMs) {
  header("Events");
  uint32_t ago = (lastEventMs==0)? 0 : (nowMs - lastEventMs);
  oled.setCursor(COL1_X, rowY(0)); oled.print("Freefalls: "); oled.print(peaks.freefalls);
  oled.setCursor(COL1_X, rowY(1)); oled.print("Impacts  : "); oled.print(peaks.impacts);
  oled.setCursor(COL1_X, rowY(2));
  if (lastEventMs==0) { oled.print("Last: None"); }
  else {
    if (ago < 1000) { oled.print("Last: "); oled.print(ago); oled.print(" ms"); }
    else if (ago < 60000) { oled.print("Last: "); oled.print((int)(ago/1000)); oled.print(" s"); }
    else { oled.print("Last: "); oled.print((int)(ago/60000)); oled.print(" min"); }
  }
}
void pageDiagA(float ax, float ay) {
  header("Diag A");
  oled.setCursor(COL1_X, rowY(0)); oled.print("Addr: 0x"); oled.print(bmi_addr, HEX);
  oled.setCursor(COL1_X, rowY(1)); oled.print("Hz  : "); oled.print((int)(measuredHz+0.5f));
  oled.setCursor(COL1_X, rowY(2)); oled.print("ax  : "); oled.print(ax,2); oled.print(" g");
  oled.setCursor(COL1_X, rowY(3)); oled.print("ay  : "); oled.print(ay,2); oled.print(" g");
}
void pageDiagB(float az, float gx, float gy, float gz, float gRes, float gyroAbs) {
  header("Diag B");
  oled.setCursor(COL1_X, rowY(0)); oled.print("az  : "); oled.print(az,2); oled.print(" g");
  oled.setCursor(COL1_X, rowY(1)); oled.print("gx  : "); oled.print((int)gx);  oled.print(" dps");
  oled.setCursor(COL1_X, rowY(2)); oled.print("gy  : "); oled.print((int)gy);  oled.print(" dps");
  oled.setCursor(COL1_X, rowY(3)); oled.print("gz  : "); oled.print((int)gz);  oled.print(" dps");
  oled.setCursor(COL1_X, rowY(4)); oled.print("|a| : "); oled.print(gRes,2);   oled.print(" g |w|:");
  oled.print((int)gyroAbs);
}
 
// ===================== (moved here) Web handlers & helpers =====================
String uptimeString() {
  uint32_t ms = millis();
  uint32_t s = ms / 1000;
  uint32_t m = s / 60; s %= 60;
  uint32_t h = m / 60; m %= 60;
  char buf[32];
  snprintf(buf, sizeof(buf), "%02u:%02u:%02u", h, m, s);
  return String(buf);
}
void handleRoot() {
  server.send_P(200, "text/html", INDEX_HTML);
}
void handleData() {
  // Build compact JSON without extra libs
  String j = "{";
  j += "\"nowG\":" + String(telem.nowG, 4);
  j += ",\"gyroAbs\":" + String(telem.gyroAbs, 2);
  j += ",\"vibRms\":" + String(telem.vibRms, 4);
  j += ",\"roll\":" + String(telem.roll, 3);
  j += ",\"pitch\":" + String(telem.pitch, 3);
  j += ",\"ax\":" + String(telem.ax, 4);
  j += ",\"ay\":" + String(telem.ay, 4);
  j += ",\"az\":" + String(telem.az, 4);
  j += ",\"gx\":" + String(telem.gx, 2);
  j += ",\"gy\":" + String(telem.gy, 2);
  j += ",\"gz\":" + String(telem.gz, 2);
  j += ",\"hz\":" + String(telem.hz, 1);
  j += ",\"addr\":" + String((unsigned)telem.addr);
 
  j += ",\"peaks\":{";
  j += "\"maxG\":" + String(peaks.maxG,2) + ",";
  j += "\"minG\":" + String(peaks.minG,2) + ",";
  j += "\"maxDPS\":" + String(peaks.maxDPS,0) + ",";
  j += "\"minDPS\":" + String(peaks.minDPS,0) + ",";
  j += "\"maxVib\":" + String(peaks.maxVib,2) + ",";
  j += "\"lastImpactG\":" + String(peaks.lastImpactG,2);
  j += "}";
 
  j += ",\"counts\":{";
  j += "\"freefalls\":" + String(peaks.freefalls) + ",";
  j += "\"impacts\":" + String(peaks.impacts) + ",";
  j += "\"events\":" + String(peaks.events);
  j += "}";
 
  uint32_t nowMs = millis();
  uint32_t ageMs = (lastEventMs==0)? 0 : (nowMs - lastEventMs);
  j += ",\"lastEventAgeMs\":" + String(ageMs);
  j += ",\"uptime\":\"" + uptimeString() + "\"";
  j += "}";
  server.send(200, "application/json", j);
}
 
// ===================== Setup / Loop =====================
void runCalibration(); // forward (implemented earlier)
 
void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN, 400000);
  pinMode(BUTTON_PIN, INPUT_PULLDOWN);
 
  if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { /* headless ok */ }
  else {
    oled.clearDisplay();
    header("Black-Box Ready");
    oled.setCursor(COL1_X, rowY(2)); oled.print("Press btn to cycle pages");
    oled.display();
    delay(400);
  }
 
  loadNVS();
  if (!bmi160Begin()) {
    oled.clearDisplay(); header("IMU Fail");
    oled.setCursor(COL1_X, rowY(2)); oled.print("Check wiring 0x68/0x69");
    oled.display();
  }
 
  // Hold button at boot to calibrate
  if (digitalRead(BUTTON_PIN) == HIGH) runCalibration();
 
  for (int i=0;i<VIB_WIN;i++)  vibBuf[i]=0;
  for (int i=0;i<CHART_W;i++) { chartA[i]=0; chartG[i]=0; chartV[i]=0; }
 
  fpsLastMs = millis();
 
  // ---- Wi-Fi bring-up (STA only) ----
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");
  unsigned long t0 = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) {
    Serial.print(".");
    delay(300);
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi: "); Serial.println(WiFi.localIP());
  } else {
    Serial.println("WiFi not connected (STA only). Server still starts.");
  }
 
  // ---- HTTP routes ----
  server.on("/", handleRoot);
  server.on("/data", HTTP_GET, handleData);
  server.begin();
  Serial.println("HTTP server started");
}
 
void loop() {
  static uint32_t nextSample = micros();
  uint32_t now = micros();
 
  pollButton(millis());
 
  if ((int32_t)(now - nextSample) < 0) { server.handleClient(); return; }
  nextSample += SAMPLE_US;
 
  float ax, ay, az, gx, gy, gz;
  if (!readAccel_g(ax, ay, az)) { server.handleClient(); return; }
  if (!readGyro_dps(gx, gy, gz)) { server.handleClient(); return; }
 
  // Apply calibration
  ax -= calib.ax_off; ay -= calib.ay_off; az -= calib.az_off;
  gx -= calib.gx_off; gy -= calib.gy_off; gz -= calib.gz_off;
 
  float gRes    = sqrtf(ax*ax + ay*ay + az*az);
  float gyroAbs = sqrtf(gx*gx + gy*gy + gz*gz);
 
  // Complementary filter
  float dt = SAMPLE_US / 1e6f;
  float rollAcc  = atan2f(ay, az) * 180.0f / PI;
  float pitchAcc = atan2f(-ax, sqrtf(ay*ay + az*az)) * 180.0f / PI;
  roll_deg  = 0.98f*(roll_deg  + gx*dt) + 0.02f*rollAcc;
  pitch_deg = 0.98f*(pitch_deg + gy*dt) + 0.02f*pitchAcc;
 
  // Vibration RMS (|a|-1g)
  float hp = gRes - 1.0f;
  vibBuf[vibIdx] = hp*hp; vibIdx = (vibIdx+1) % VIB_WIN; if (vibCnt < VIB_WIN) vibCnt++;
  float vibSum=0.0f; for (int i=0;i<vibCnt;i++) vibSum += vibBuf[i];
  float vibRms = sqrtf(vibSum / (float)(vibCnt>0?vibCnt:1));
 
  // Peaks/mins
  bool changed=false;
  if (gRes    > peaks.maxG)    { peaks.maxG = gRes; changed=true; }
  if (gRes    < peaks.minG)    { peaks.minG = gRes; changed=true; }
  if (gyroAbs > peaks.maxDPS)  { peaks.maxDPS = gyroAbs; changed=true; }
  if (gyroAbs < peaks.minDPS)  { peaks.minDPS = gyroAbs; changed=true; }
  if (vibRms  > peaks.maxVib)  { peaks.maxVib = vibRms; changed=true; }
  if (changed) saveNVS();
 
  // Events
  uint32_t nowMs = millis();
  if (!inFreefall && gRes < FREEFALL_G) { inFreefall = true; freefallStartMs = nowMs; }
  else if (inFreefall && gRes >= FREEFALL_G) {
    if (nowMs - freefallStartMs >= FREEFALL_MIN_MS) {
      peaks.freefalls++; peaks.events++; saveNVS(); lastEventMs = nowMs;
    }
    inFreefall = false;
  }
  if (gRes >= IMPACT_G) {
    peaks.impacts++; peaks.events++; peaks.lastImpactG = gRes; saveNVS(); lastEventMs = nowMs;
  }
 
  // Charts (bounded width/height)
  float aDisp = fabsf(gRes - 1.0f); if (aDisp > 2.0f) aDisp = 2.0f;
  chartA[chA] = aDisp; chA = (chA+1) % CHART_W;
 
  float gDisp = gyroAbs; if (gDisp > 1200.0f) gDisp = 1200.0f;
  chartG[chG] = gDisp; chG = (chG+1) % CHART_W;
 
  float vDisp = vibRms; if (vDisp > 2.0f) vDisp = 2.0f;
  chartV[chV] = vDisp; chV = (chV+1) % CHART_W;
 
  // Measured loop rate
  fpsCount++;
  uint32_t ms = millis();
  if (ms - fpsLastMs >= 1000) {
    measuredHz = (float)fpsCount * 1000.0f / (float)(ms - fpsLastMs);
    fpsCount = 0; fpsLastMs = ms;
  }
 
  // Update telemetry snapshot for web
  telem.nowG = gRes;
  telem.gyroAbs = gyroAbs;
  telem.vibRms = vibRms;
  telem.roll = roll_deg;
  telem.pitch = pitch_deg;
  telem.ax = ax; telem.ay = ay; telem.az = az;
  telem.gx = gx; telem.gy = gy; telem.gz = gz;
  telem.hz = measuredHz;
  telem.addr = bmi_addr;
  telem.lastEventAgeMs = (lastEventMs==0)? 0 : (ms - lastEventMs);
 
  // Draw page
  oled.clearDisplay();
  switch (page) {
    case PAGE_HOME:    pageHome(gRes, gyroAbs, vibRms);                    break;
    case PAGE_ACCEL:   pageAccel(gRes);                                    break;
    case PAGE_GYRO:    pageGyro(gyroAbs);                                  break;
    case PAGE_VIB:     pageVib(vibRms);                                    break;
    case PAGE_ORIENT:  pageOrient(gRes);                                   break;
    case PAGE_PEAKS_A: pagePeaksA();                                       break;
    case PAGE_PEAKS_B: pagePeaksB();                                       break;
    case PAGE_EVENTS:  pageEvents(ms);                                     break;
    case PAGE_DIAG_A:  pageDiagA(ax, ay);                                  break;
    case PAGE_DIAG_B:  pageDiagB(az, gx, gy, gz, gRes, gyroAbs);           break;
  }
  oled.display();
 
  // Serve web requests
  server.handleClient();
}
 
// ===================== Calibration (hold button at boot) =====================
void runCalibration() {
  oled.clearDisplay();
  oled.fillRect(0,0,128,10,SSD1306_WHITE);
  oled.setTextColor(SSD1306_BLACK); oled.setTextSize(1);
  oled.setCursor(2,1); oled.print("Calibrating...");
  oled.setTextColor(SSD1306_WHITE);
 
  // Progress bar frame in row 1
  oled.drawRect(2, rowY(1)-2, 124, 8, SSD1306_WHITE);
  oled.display();
 
  const int N=200;
  float sax=0, say=0, saz=0, sgx=0, sgy=0, sgz=0;
  for (int i=0;i<N;i++) {
    float ax, ay, az, gx, gy, gz;
    if (!readAccel_g(ax,ay,az)) { i--; continue; }
    if (!readGyro_dps(gx,gy,gz)) { i--; continue; }
    sax+=ax; say+=ay; saz+=az;
    sgx+=gx; sgy+=gy; sgz+=gz;
    int w = (i+1) * 122 / N;
    oled.fillRect(3, rowY(1)-1, w, 6, SSD1306_WHITE);
    oled.display();
    delay(3);
  }
  float axm=sax/N, aym=say/N, azm=saz/N;
  float gxm=sgx/N, gym=sgy/N, gzm=sgz/N;
 
  calib.ax_off = axm;
  calib.ay_off = aym;
  calib.az_off = azm - 1.0f;   // want +1g on Z at rest
  calib.gx_off = gxm;
  calib.gy_off = gym;
  calib.gz_off = gzm;
  saveNVS();
 
  oled.clearDisplay();
  oled.fillRect(0,0,128,10,SSD1306_WHITE);
  oled.setTextColor(SSD1306_BLACK); oled.setCursor(2,1); oled.print("Calib Done");
  oled.setTextColor(SSD1306_WHITE);
  oled.setCursor(COL1_X, rowY(1)); oled.printf("ax %.3f  ay %.3f", calib.ax_off, calib.ay_off);
  oled.setCursor(COL1_X, rowY(2)); oled.printf("az %.3f", calib.az_off);
  oled.setCursor(COL1_X, rowY(3)); oled.printf("gx %.2f  gy %.2f", calib.gx_off, calib.gy_off);
  oled.setCursor(COL1_X, rowY(4)); oled.printf("gz %.2f", calib.gz_off);
  oled.display();
  delay(800);
}



webpage.h code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#pragma once
#include <pgmspace.h>
 
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Flight Black-Box Motion Recorder System</title>
<style>
  :root{
    --page:#f6f9ff; --panel:#ffffff; --panel2:#f8fbff;
    --text:#0a2742; --muted:#647e97; --border:#e3ecf7; --grid:#e9eef6;
    --blue:#2b76ff; --vio:#7b61ff; --teal:#15b89a; --orange:#ff8c3a;
    --shadow: 0 8px 22px rgba(21,44,88,.10); --radius: 14px;
    --kpi-h: 86px; --plane-h: 160px; --chart-h: 100px; --gutter: 12px; --pad: 10px; --title: 12px; --val: 22px;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;color:var(--text);
    font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Helvetica, Arial;
    background:
      radial-gradient(1100px 700px at 10% -30%, #eaf2ff 0%, transparent 55%),
      radial-gradient(800px 500px at 120% 30%, #eef2ff 0%, transparent 60%),
      linear-gradient(180deg,#f8fbff, var(--page));
  }
  header{
    position:sticky;top:0;z-index:10; backdrop-filter:saturate(1.1) blur(6px);
    background:linear-gradient(90deg,rgba(255,255,255,.9),rgba(255,255,255,.9));
    border-bottom:1px solid var(--border); padding:6px 12px; display:flex; align-items:center; gap:8px;
  }
  header .logo{ width:22px;height:22px;border-radius:6px; background: conic-gradient(from 180deg, var(--blue), var(--vio), var(--teal), var(--blue)); box-shadow: inset 0 0 3px rgba(255,255,255,.7), 0 0 0 1px #dfe8fa; animation: spin 10s linear infinite;}
  @keyframes spin{to{transform:rotate(360deg)}}
  header h1{margin:0;font-size:14px;letter-spacing:.2px;font-weight:800}
  header .pill{margin-left:auto;font-size:11px;color:var(--muted); padding:4px 8px;border-radius:999px;border:1px solid var(--border); background: var(--panel); box-shadow: var(--shadow);}
 
  main{ padding:12px; display:grid; gap:var(--gutter); grid-template-columns:repeat(12,1fr); max-width:1180px; margin:0 auto; }
  .card{ grid-column:span 12; position:relative; overflow:hidden; border-radius:var(--radius); padding:var(--pad); border:1px solid var(--border); background: linear-gradient(180deg, var(--panel), var(--panel2)); box-shadow: var(--shadow); }
  .row{display:grid; gap:var(--gutter); grid-template-columns:repeat(12,1fr)}
  .span-3{grid-column:span 3}.span-4{grid-column:span 4}.span-5{grid-column:span 5}.span-6{grid-column:span 6}.span-7{grid-column:span 7}.span-12{grid-column:span 12}
 
  .kpi{ border:1px solid var(--border); border-radius:12px; padding:8px; min-height:var(--kpi-h); background: linear-gradient(180deg,#ffffff,#f8fbff); }
  .kpi h3{ margin:0 0 4px; color:var(--muted); font-size:var(--title); font-weight:800; letter-spacing:.2px }
  .kpi .val{ font-size:var(--val); font-weight:900; letter-spacing:.2px }
  .kpi .sub{ color:var(--muted); font-size:10px; margin-left:6px }
 
  .chart{ height:var(--chart-h); border-radius:12px; border:1px solid var(--border); background: linear-gradient(180deg,#ffffff,#f7faff); position:relative; overflow:hidden }
  .chart canvas{ width:100%; height:100%; display:block }
  .chart .shine{ position:absolute; inset:0; background:linear-gradient(90deg, transparent, rgba(0,0,0,.04), transparent); transform:translateX(-100%); animation: sweep 7s ease-in-out infinite }
  @keyframes sweep{50%{transform:translateX(100%)}100%{transform:translateX(100%)}}
 
  /* Bullet key-value rows (no numbers) */
  .kv{ display:grid; grid-template-columns: 12px 1fr auto; align-items:center; gap:6px 10px; }
  .bullet{ width:8px; height:8px; border-radius:50%; background:#0a2742; box-shadow: 0 1px 3px rgba(0,0,0,.15); }
  .label{ font-size:12px; color:var(--muted) }
  .val{ font-weight:800; font-size:13px; color:var(--text) }
 
  /* Attitude */
  .plane-wrap{ position:relative; height:var(--plane-h); border-radius:12px; border:1px solid var(--border); background: linear-gradient(180deg,#ffffff,#f6f9ff); overflow:hidden }
  .sky{ position:absolute; inset:0; opacity:.45; background:
    radial-gradient(3px 3px at 10% 30%, #b0c2e3, transparent 60%),
    radial-gradient(2px 2px at 40% 60%, #b0c2e3, transparent 60%),
    radial-gradient(2px 2px at 70% 20%, #b0c2e3, transparent 60%),
    radial-gradient(3px 3px at 85% 75%, #b0c2e3, transparent 60%),
    radial-gradient(2px 2px at 25% 80%, #b0c2e3, transparent 60%); animation: twinkle 6s ease-in-out infinite alternate }
  @keyframes twinkle{from{opacity:.25}to{opacity:.55}}
  .horizon{position:absolute; left:0; right:0; top:50%; height:1px; background:#d8e2f2; transform-origin:50% 50%}
  .plane{ position:absolute; left:50%; top:50%; width:96px; height:96px; transform:translate(-50%,-50%); display:grid; place-items:center; filter: drop-shadow(0 6px 18px rgba(20,40,90,.22)); transition: transform 120ms linear }
  .plane svg{ width:96px; height:96px }
  .plane path{ fill:url(#fuselageDark); stroke:#324b77; stroke-width:.8 }
  .bank-glow{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:180px; height:180px; border-radius:50%; background:radial-gradient(closest-side, rgba(30,70,140,.14), transparent 70%) }
 
  h3[accent]{ font-weight:800; font-size:var(--title); letter-spacing:.2px; margin:0 0 8px; color:var(--muted) }
  h3 .dot{ display:inline-block; width:7px; height:7px; border-radius:50%; margin-right:6px; vertical-align:middle }
  .blue .dot{ background:var(--blue) } .vio .dot{ background:var(--vio) } .teal .dot{ background:var(--teal) } .orange .dot{ background:var(--orange) }
 
  @media (max-width: 980px){ .span-7,.span-5{grid-column:span 12} :root{ --plane-h: 180px } }
</style>
</head>
<body>
  <header>
    <div class="logo" aria-hidden="true"></div>
    <h1>Flight Black-Box Motion Recorder System</h1>
    <div class="pill" id="uptime">—</div>
  </header>
 
  <main>
    <!-- KPI Row -->
    <section class="card span-12" style="padding:8px;">
      <div class="row">
        <div class="span-3">
          <div class="kpi">
            <h3 class="blue" accent><span class="dot"></span>Now |a| (m/s²)</h3>
            <div class="val" id="nowA_ms2">—</div>
            <span class="sub" id="addr">I²C: —</span>
          </div>
        </div>
        <div class="span-3">
          <div class="kpi">
            <h3 class="vio" accent><span class="dot"></span>|ω| (dps)</h3>
            <div class="val" id="gyroAbs">—</div>
            <span class="sub" id="hz">Rate: — Hz</span>
          </div>
        </div>
        <div class="span-3">
          <div class="kpi">
            <h3 class="teal" accent><span class="dot"></span>Vibration (m/s²)</h3>
            <div class="val" id="vib_ms2">—</div>
            <span class="sub" id="vibRms">RMS: —</span>
          </div>
        </div>
        <div class="span-3">
          <div class="kpi">
            <h3 class="orange" accent><span class="dot"></span>Roll / Pitch</h3>
            <div class="val"><span id="roll">—</span>° / <span id="pitch">—</span>°</div>
            <span class="sub" id="lastEvent">Last: —</span>
          </div>
        </div>
      </div>
    </section>
 
    <!-- Visualizer (7) + Diagnostics (5) -->
    <section class="card span-12" style="padding:8px;">
      <div class="row">
        <div class="span-7">
          <h3 class="blue" accent><span class="dot"></span>Attitude Visualizer</h3>
          <div class="plane-wrap">
            <div class="sky"></div>
            <div class="horizon" id="horizon"></div>
            <div class="bank-glow" id="bankGlow"></div>
            <div class="plane" id="plane">
              <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="plane">
                <defs>
                  <linearGradient id="fuselageDark" x1="0" x2="1" y1="0" y2="1">
                    <stop offset="0%" stop-color="#2a4a8a"/>
                    <stop offset="55%" stop-color="#1f3b73"/>
                    <stop offset="100%" stop-color="#16325f"/>
                  </linearGradient>
                </defs>
                <path d="M50 12 L57 28 L92 40 L92 48 L57 48 L50 86 L43 48 L8 48 L8 40 L43 28 Z"/>
                <path d="M46 30 Q50 26 54 30 L50 40 Z" fill="#dce6f8" opacity=".6" stroke="none"/>
              </svg>
            </div>
          </div>
        </div>
 
        <!-- Diagnostics: ax/ay/az (left column), gx/gy/gz (right column) -->
        <div class="span-5">
          <h3 class="vio" accent><span class="dot"></span>Diagnostics (axes)</h3>
          <div class="row">
            <div class="span-6">
              <div class="kv"><div class="bullet"></div><div class="label">ax (m/s²)</div><div class="val" id="ax_ms2">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">ay (m/s²)</div><div class="val" id="ay_ms2">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">az (m/s²)</div><div class="val" id="az_ms2">—</div></div>
            </div>
            <div class="span-6">
              <div class="kv"><div class="bullet"></div><div class="label">gx (dps)</div><div class="val" id="gx">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">gy (dps)</div><div class="val" id="gy">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">gz (dps)</div><div class="val" id="gz">—</div></div>
            </div>
          </div>
        </div>
      </div>
    </section>
 
    <!-- Peaks & Events (bulleted, tidy) -->
    <section class="card span-12" style="padding:8px;">
      <div class="row">
        <div class="span-6">
          <h3 class="teal" accent><span class="dot"></span>Peaks (m/s² / dps)</h3>
          <div class="row">
            <div class="span-6">
              <div class="kv"><div class="bullet"></div><div class="label">Max |a|</div><div class="val" id="maxA_ms2">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">Max |ω|</div><div class="val" id="maxDPS">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">Max Vib</div><div class="val" id="maxVib_ms2">—</div></div>
            </div>
            <div class="span-6">
              <div class="kv"><div class="bullet"></div><div class="label">Min |a|</div><div class="val" id="minA_ms2">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">Min |ω|</div><div class="val" id="minDPS">—</div></div>
              <div class="kv"><div class="bullet"></div><div class="label">Last Impact</div><div class="val" id="lastImpact_ms2">—</div></div>
            </div>
          </div>
        </div>
        <div class="span-6">
          <h3 class="orange" accent><span class="dot"></span>Events</h3>
          <div class="kv"><div class="bullet"></div><div class="label">Free-falls</div><div class="val" id="freefalls">—</div></div>
          <div class="kv"><div class="bullet"></div><div class="label">Impacts</div><div class="val" id="impacts">—</div></div>
        </div>
      </div>
    </section>
 
    <!-- Charts Row -->
    <section class="card span-12" style="padding:8px;">
      <div class="row">
        <div class="span-4">
          <h3 class="blue" accent><span class="dot"></span>Accel |a|-1g (m/s²)</h3>
          <div class="chart"><span class="shine"></span><canvas id="cAccel"></canvas></div>
        </div>
        <div class="span-4">
          <h3 class="vio" accent><span class="dot"></span>Gyro |ω| (dps)</h3>
          <div class="chart"><span class="shine"></span><canvas id="cGyro"></canvas></div>
        </div>
        <div class="span-4">
          <h3 class="teal" accent><span class="dot"></span>Vibration RMS (m/s²)</h3>
          <div class="chart"><span class="shine"></span><canvas id="cVib"></canvas></div>
        </div>
      </div>
    </section>
  </main>
 
<script>
(() => {
  const el = id => document.getElementById(id);
  const G = 9.81; // m/s² per g
 
  function makeChart(canvasId, ymax, colorTop, colorBottom) {
    const c = el(canvasId), ctx = c.getContext('2d');
    const W = c.width = c.clientWidth || 360;
    const H = c.height = c.clientHeight || 100;
    const N = 200;
    const data = new Array(N).fill(0);
    function draw() {
      ctx.clearRect(0,0,W,H);
      ctx.strokeStyle = "#e9eef6"; ctx.lineWidth = 1;
      for (let y=0;y<H;y+=20){ ctx.beginPath(); ctx.moveTo(0,y+0.5); ctx.lineTo(W,y+0.5); ctx.stroke(); }
      const g = ctx.createLinearGradient(0,0,0,H);
      g.addColorStop(0, colorTop); g.addColorStop(1, colorBottom);
      ctx.lineWidth = 2; ctx.strokeStyle = g;
      ctx.beginPath();
      for (let i=0;i<N;i++){
        const vx = (i/(N-1))*W;
        const v  = Math.min(ymax, Math.max(0, data[i]));
        const vy = H - (v/ymax)*(H-8) - 4;
        if(i===0) ctx.moveTo(vx,vy); else ctx.lineTo(vx,vy);
      }
      ctx.stroke();
    }
    function push(v){ data.push(v); data.shift(); draw(); }
    draw(); return { push };
  }
 
  const chartAccel = makeChart('cAccel', 2*G, 'rgba(43,118,255,0.95)','rgba(43,118,255,0.20)');
  const chartGyro  = makeChart('cGyro', 1200.0, 'rgba(123,97,255,0.95)','rgba(123,97,255,0.20)');
  const chartVib   = makeChart('cVib', 2*G, 'rgba(21,184,154,0.95)','rgba(21,184,154,0.20)');
 
  function fmtMs(ms){ if(!ms) return 'None'; if(ms<1000) return ms+' ms'; const s=Math.floor(ms/1000); return s<60? s+' s' : Math.floor(s/60)+' min'; }
  function clamp(v,lo,hi){ return v<lo?lo:v>hi?hi:v; }
 
  function updatePlane(roll, pitch){
    const p = el('plane'), h = el('horizon'), glow = el('bankGlow');
    const r = clamp(roll,-70,70), pch = clamp(pitch,-35,35);
    p.style.transform = `translate(-50%,-50%) rotate(${r}deg) translateY(${pch*-0.8}px)`;
    h.style.transform = `translateY(-50%) rotate(${-r*0.6}deg) translateY(${pch*1.1}px)`;
    glow.style.background = `radial-gradient(closest-side, rgba(30,70,140,${Math.min(0.08 + Math.abs(r)/180, 0.18)}), transparent 70%)`;
  }
 
  async function tick(){
    try{
      const r = await fetch('/data',{cache:'no-store'});
      if(!r.ok) throw new Error('HTTP '+r.status);
      const d = await r.json();
 
      // g -> m/s² conversions
      const nowA_ms2 = d.nowG*G, vib_ms2 = d.vibRms*G;
      const ax_ms2 = d.ax*G, ay_ms2 = d.ay*G, az_ms2 = d.az*G;
      const maxA_ms2 = d.peaks.maxG*G, minA_ms2 = d.peaks.minG*G;
      const lastImp_ms2 = d.peaks.lastImpactG*G, maxVib_ms2 = d.peaks.maxVib*G;
 
      // KPIs
      el('nowA_ms2').textContent = nowA_ms2.toFixed(2);
      el('gyroAbs').textContent  = Math.round(d.gyroAbs);
      el('vib_ms2').textContent  = vib_ms2.toFixed(2);
      el('vibRms').textContent   = `RMS: ${vib_ms2.toFixed(2)} m/s²`;
      el('hz').textContent       = `Rate: ${Math.round(d.hz)} Hz`;
      el('roll').textContent     = d.roll.toFixed(1);
      el('pitch').textContent    = d.pitch.toFixed(1);
      el('addr').textContent     = `I²C: 0x${d.addr.toString(16)}`;
      el('lastEvent').textContent= fmtMs(d.lastEventAgeMs);
      el('uptime').textContent   = d.uptime;
 
      // Diagnostics
      el('ax_ms2').textContent = ax_ms2.toFixed(2);
      el('ay_ms2').textContent = ay_ms2.toFixed(2);
      el('az_ms2').textContent = az_ms2.toFixed(2);
      el('gx').textContent = Math.round(d.gx);
      el('gy').textContent = Math.round(d.gy);
      el('gz').textContent = Math.round(d.gz);
 
      // Peaks & events
      el('maxA_ms2').textContent     = maxA_ms2.toFixed(2);
      el('minA_ms2').textContent     = minA_ms2.toFixed(2);
      el('maxDPS').textContent       = Math.round(d.peaks.maxDPS);
      el('minDPS').textContent       = Math.round(d.peaks.minDPS);
      el('maxVib_ms2').textContent   = maxVib_ms2.toFixed(2);
      el('lastImpact_ms2').textContent= lastImp_ms2.toFixed(2);
      el('freefalls').textContent    = d.counts.freefalls;
      el('impacts').textContent      = d.counts.impacts;
 
      // Charts
      chartAccel.push(Math.min(2*G, Math.abs(d.nowG - 1.0)*G));
      chartGyro.push(Math.min(1200, d.gyroAbs));
      chartVib.push(Math.min(2*G, vib_ms2));
 
      updatePlane(d.roll, d.pitch);
    }catch(e){
      // silent retry
    }finally{
      setTimeout(tick, 120);
    }
  }
  tick();
})();
</script>
</body>
</html>
)rawliteral";

At the top of the main code, it includes libraries for I²C (Wire), the OLED (Adafruit_GFX / SSD1306), non-volatile storage (Preferences), Wi-Fi, and a simple HTTP server. It defines your Wi-Fi credentials and creates a WebServer on port 80. A Telemetry struct holds the latest motion values (G-force, gyro magnitude, vibration RMS, roll, pitch, raw axes, sampling rate, etc.) which are later sent to the webpage as JSON.

The BMI160 is accessed at register level: helper functions i2cWriteReg, i2cReadBytes, readAccel_g, and readGyro_dps talk directly to the sensor, while bmi160Begin() does a soft reset and puts the accelerometer and gyro into normal mode. Calibration offsets and peak statistics are stored in flash using Preferences; loadNVS() and saveNVS() read and write those values, and clearPeaks() resets them. The code then implements a 200 Hz sampling loop, computes |a| and |ω|, runs a complementary filter to estimate roll and pitch, and calculates a vibration RMS window.

Events such as free-fall and impacts are detected using thresholds on |a|; counters and peak values are updated and saved to NVS. Several page functions (pageHome, pageAccel, pageGyro, pageVib, pageOrient, pagePeaksA/B, pageEvents, pageDiagA/B) draw different views on the OLED, including scrolling strip charts. A debounced push button cycles through pages on short press and clears peaks on long press.

Wi-Fi is configured in STA mode; the ESP32 connects to your router, starts an HTTP server, and serves the HTML UI from webpage.h at /. The /data handler builds a compact JSON string from the telem struct and peaks/event counters so the web page can update its gauges and graphs in real time. The loop() function ties everything together: it samples the IMU at a fixed rate, updates telemetry and peaks, draws the selected OLED page, and continuously services incoming web requests. A separate runCalibration() routine (triggered by holding the button at boot) averages multiple samples to compute new offsets and shows progress on the OLED.




Testing the ESP32 BMI160 Flight Black-Box Motion Recorder System

To test the Flight Black-Box Motion Recorder System, you need to upload the above code to the ESP32 Board. Once uploading is done, the OLED starts showing the flight data.

Data Visualization on OLED Display

The OLED Display has 10 pages. The push button is used to navigate to different pages from 1 to 10. Press the push button and you will see details of different BMI160 Data.

  • Short press – Cycles through the 10 OLED pages, so you can view different BMI160 motion data screens.
  • Long press (~1.2 s) – Clears all stored peaks/events and shows a “Peaks reset” message.
  • Hold while powering/resetting – Starts the calibration routine to learn new sensor offsets.
  • Page 1 – Summary:
    On the first page, it shows the live G-force, gyro rotation magnitude, vibration level, sampling rate (Hz), and a quick overview of peak values and event counts, such as impacts and free-falls.

  • Page 2 – Acceleration Chart:
    This page displays a real-time scrolling chart of acceleration (|a|−1g), allowing you to visually observe movement spikes, shaking, and flight stability.

  • Page 3 – Gyroscope Chart:
    Here you see a moving graph of the gyro magnitude (|ω|), showing how fast the device is rotating and helping you understand angular motion.

  • Page 4 – Vibration RMS Chart:
    This page plots the vibration intensity using RMS calculations, making it easy to detect turbulence, mechanical vibration, or sudden shocks.

  • Page 5 – Orientation (Attitude Indicator):
    It shows a bubble-style roll and pitch visualizer along with numeric roll, pitch, and G-force values—similar to an aircraft attitude indicator.

  • Page 6 – Peaks A:
    This page displays maximum G-force, minimum G-force, and maximum vibration recorded since power-on or last reset.

  • Page 7 – Peaks B:
    It shows maximum and minimum rotational speed (DPS) along with the last detected impact G-force value.

  • Page 8 – Events:
    This page shows count of free-falls, impacts, and how long ago the last motion event happened in milliseconds, seconds, or minutes.

  • Page 9 – Diagnostics A:
    Here the IMU address, sampling frequency (Hz), and raw acceleration values (ax, ay) are shown for debugging and sensor verification.

  • Page 10 – Diagnostics B:
    This page provides az, individual gyro axis values (gx, gy, gz), and combined magnitudes |a| and |ω|, helping you confirm the sensor’s accuracy and health.



Data Visualization on Web Dashboard

Since the ESP32 connects to the WiFi network. The ESP32 BMI160 Flight Black-Box Motion Recorder System can be monitored remotely on a Webpage connected to the same network on a local webserver.

The web dashboard displays all real-time motion data coming from the ESP32 and BMI160 sensor. It shows live acceleration, gyroscope values, vibration RMS, roll/pitch attitude, diagnostic axes, peaks, events, and smooth scrolling charts for accel, gyro, and vibration. The center also includes an animated airplane that tilts according to the device’s motion.

To test it, simply power your ESP32, connect your phone or laptop to the same Wi-Fi network, and open the device’s IP address (shown in the Serial Monitor). Move, shake, tilt, or drop the device gently and watch the numbers, airplane attitude, peaks, and charts update instantly on the webpage.

This project can be further improved by adding onboard SD-card logging, GPS integration, and a 3D WebGL attitude viewer for richer analysis. With these upgrades, it becomes a more complete and professional black-box recorder system.

Share. Facebook Twitter Pinterest LinkedIn Tumblr Email Reddit Telegram WhatsApp
Previous ArticleESP8266 & DHT11 Humidity Temperature Monitor on ThingSpeak
Next Article DIY Colorimeter using AS7265x Spectroscopy Sensor & ESP32

Related Posts

IoT Based PM & Air Quality Monitoring System using ESP32

IoT Based PM & Air Quality Monitoring System using ESP32

DIY ESP32 MLX90640 IR Thermal Camera with Live Web Display

DIY ESP32 MLX90640 IR Thermal Camera with Live Web Display

Updated:May 10, 20261K
IoT Activity Tracker with ESP32 & Accelerometer Gyroscope

IoT Activity Tracker with ESP32 & Accelerometer/Gyroscope

Updated:May 2, 2026

ESP32 IoT Vehicle Motion Analyzer with MPU6050 & LIS3MDL

Updated:April 27, 20261K
High-Accuracy Pitch, Roll, Yaw with ESP32 & BNO08x IMU

High-Accuracy Pitch, Roll, Yaw with ESP32 & BNO08x IMU

Updated:April 27, 20262K
DIY Colorimeter using AS7265x Spectroscopy Sensor & ESP32

DIY Colorimeter using AS7265x Spectroscopy Sensor & ESP32

Updated:February 1, 20261K
Add A Comment

CommentsCancel reply

Latest Posts
IoT Based PM & Air Quality Monitoring System using ESP32

IoT Based PM & Air Quality Monitoring System using ESP32

May 31, 2026
DIY ESP32 MLX90640 IR Thermal Camera with Live Web Display

DIY ESP32 MLX90640 IR Thermal Camera with Live Web Display

May 10, 2026
IoT Activity Tracker with ESP32 & Accelerometer Gyroscope

IoT Activity Tracker with ESP32 & Accelerometer/Gyroscope

May 2, 2026
A Guide to Sourcing Obsolete ICs for Vintage Projects

Beyond AliExpress: A Guide to Sourcing Obsolete ICs for Vintage Projects

April 21, 2026

ESP32 IoT Vehicle Motion Analyzer with MPU6050 & LIS3MDL

April 27, 2026
Building a Smart Sensor Node with a BLE Microcontroller

Building a Smart Sensor Node with a BLE Microcontroller

February 26, 2026
High-Accuracy Pitch, Roll, Yaw with ESP32 & BNO08x IMU

High-Accuracy Pitch, Roll, Yaw with ESP32 & BNO08x IMU

April 27, 2026
DIY Colorimeter using AS7265x Spectroscopy Sensor & ESP32

DIY Colorimeter using AS7265x Spectroscopy Sensor & ESP32

February 1, 2026
Top Posts & Pages
  • 12V DC to 220V AC Inverter Circuit & PCB
    12V DC to 220V AC Inverter Circuit & PCB
  • IoT AC Energy Meter with PZEM-004T & ESP32 WebServer
    IoT AC Energy Meter with PZEM-004T & ESP32 WebServer
  • IoT Based Drinking Water Quality Monitoring with ESP32
    IoT Based Drinking Water Quality Monitoring with ESP32
  • LD2410 Sensor with ESP32 - Human Presence Detection
    LD2410 Sensor with ESP32 - Human Presence Detection
  • ESP32 CAN Bus Tutorial | Interfacing MCP2515 CAN Module with ESP32
    ESP32 CAN Bus Tutorial | Interfacing MCP2515 CAN Module with ESP32
  • DIY IoT Water pH Meter using pH Sensor & ESP32
    DIY IoT Water pH Meter using pH Sensor & ESP32
  • Buck Converter: Basics, Working, Design & Application
    Buck Converter: Basics, Working, Design & Application
  • How to use Modbus RTU with ESP32 to read Sensor Data
    How to use Modbus RTU with ESP32 to read Sensor Data
Categories
  • Arduino Projects (197)
  • Articles (60)
    • Learn Electronics (19)
    • Product Review (15)
    • Tech Articles (28)
  • Electronics Circuits (46)
    • 555 Timer Projects (21)
    • Op-Amp Circuits (7)
    • Power Electronics (13)
  • IoT Projects (204)
    • ESP32 MicroPython (7)
    • ESP32 Projects (81)
    • ESP32-CAM Projects (15)
    • ESP8266 Projects (76)
    • LoRa/LoRaWAN Projects (22)
  • Microcontrollers (38)
    • AMB82-Mini IoT AI Camera (4)
    • BLE Projects (18)
    • STM32 Projects (19)
  • Raspberry Pi (93)
    • Raspberry Pi Pico Projects (57)
    • Raspberry Pi Pico W Projects (12)
    • Raspberry Pi Projects (24)
Follow Us
  • Facebook
  • Twitter
  • Pinterest
  • Instagram
  • YouTube
About Us

“‘How to Electronics’ is a vibrant community for electronics enthusiasts and professionals. We deliver latest insights in areas such as Embedded Systems, Power Electronics, AI, IoT, and Robotics. Our goal is to stimulate innovation and provide practical solutions for students, organizations, and industries. Join us to transform learning into a joyful journey of discovery and innovation.

Copyright © How To Electronics. All rights reserved.
  • About Us
  • Disclaimer
  • Privacy Policy
  • Contact Us
  • Advertise With Us

Type above and press Enter to search. Press Esc to cancel.

Ad Blocker Enabled!
Ad Blocker Enabled!
Looks like you're using an ad blocker. Please allow ads on our site. We rely on advertising to help fund our site.