3 Commits
Author SHA1 Message Date
Oleg Kalachev 7c53e88963 Add notch filter for the gyro 2026-07-08 16:07:03 +03:00
Oleg Kalachev 28f015569b Add log reset command to cli 2026-07-08 01:07:59 +03:00
Oleg Kalachev 83d1c5c68a Implement entirely new logging subsystem
Make topic-based logging mechanics.
Print any log value to console.
Possibility to expose any logging value to mavlink.
2026-06-30 13:03:43 +03:00
25 changed files with 598 additions and 127 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ Additional articles:
|*Boost converter (optional, for more stable power supply)*|*5V output*|<img src="docs/img/buck-boost.jpg" width=100>|1| |*Boost converter (optional, for more stable power supply)*|*5V output*|<img src="docs/img/buck-boost.jpg" width=100>|1|
|Motor|8520 3.7V brushed motor.<br>Motor with exact 3.7V voltage is needed, not ranged working voltage (3.7V — 6V).<br>Make sure the motor shaft diameter and propeller hole diameter match!|<img src="docs/img/motor.jpeg" width=100>|4| |Motor|8520 3.7V brushed motor.<br>Motor with exact 3.7V voltage is needed, not ranged working voltage (3.7V — 6V).<br>Make sure the motor shaft diameter and propeller hole diameter match!|<img src="docs/img/motor.jpeg" width=100>|4|
|Propeller|55 mm or 65 mm|<img src="docs/img/prop.jpg" width=100>|4| |Propeller|55 mm or 65 mm|<img src="docs/img/prop.jpg" width=100>|4|
|MOSFET (transistor)|UMW 100N03A or [analog](https://t.me/opensourcequadcopter/33).<br>Warning: don't use KIA 100N03A or other manufacturers, they might not work!|<img src="docs/img/100n03a.jpg" width=100>|4| |MOSFET (transistor)|100N03A or [analog](https://t.me/opensourcequadcopter/33)|<img src="docs/img/100n03a.jpg" width=100>|4|
|Pull-down resistor<br>Voltage measurement resistor|10 kΩ|<img src="docs/img/resistor10k.jpg" width=100>|6| |Pull-down resistor<br>Voltage measurement resistor|10 kΩ|<img src="docs/img/resistor10k.jpg" width=100>|6|
|3.7V Li-Po battery|LW 952540 (or any compatible by the size).<br>Make sure the battery has enough discharge rate — 25C or more!|<img src="docs/img/battery.jpg" width=100>|1| |3.7V Li-Po battery|LW 952540 (or any compatible by the size).<br>Make sure the battery has enough discharge rate — 25C or more!|<img src="docs/img/battery.jpg" width=100>|1|
|Battery connector cable|MX2.0 2P female|<img src="docs/img/mx.png" width=100>|1| |Battery connector cable|MX2.0 2P female|<img src="docs/img/mx.png" width=100>|1|
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

+4 -4
View File
@@ -198,7 +198,7 @@ After this setup, you should see the battery voltage in QGroundControl top panel
## Setup remote control ## Setup remote control
There are several ways to control the drone's flight: using **smartphone** (Wi-Fi), using **SBUS remote control**, or using **USB remote control** (Wi-Fi/ESP-NOW). There are several ways to control the drone's flight: using **smartphone** (Wi-Fi), using **SBUS remote control**, or using **USB remote control** (Wi-Fi).
### Control with a smartphone ### Control with a smartphone
@@ -243,7 +243,7 @@ If your drone doesn't have RC receiver installed, you can use USB remote control
3. Power up the drone. 3. Power up the drone.
4. Connect your computer to the appeared `flix` Wi-Fi network (password: `flixwifi`). 4. Connect your computer to the appeared `flix` Wi-Fi network (password: `flixwifi`).
5. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically. 5. Launch QGroundControl app. It should connect and begin showing the drone's telemetry automatically.
6. Go to the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Joystick*. Calibrate your USB remote control there. 6. Go the the QGroundControl menu ⇒ *Vehicle Setup* ⇒ *Joystick*. Calibrate you USB remote control there.
7. Use the USB remote control to fly the drone! 7. Use the USB remote control to fly the drone!
## Flight ## Flight
@@ -339,7 +339,7 @@ To setup ESP-NOW communication:
espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E
``` ```
Run this line as a console command on each drone you want to bind to this proxy board. [The maximum number](https://github.com/espressif/esp-idf/blob/e95cab4be8fd293e3f3323181e7a2280874da6f7/components/esp_wifi/include/esp_now.h#L32-L33) of simultaneously connected drones is 20 (unencrypted) or 6 (encrypted). Run this line as a console command on each drone you want to bind to this proxy board. [The maximum number](https://github.com/espressif/esp-idf/blob/e95cab4be8fd293e3f3323181e7a2280874da6f7/components/esp_wifi/include/esp_now.h#L32-L33) of simultaneously connected drones is 20 (unencrypted) io 6 (encrypted).
3. Set the `WIFI_MODE` parameter to `3` on the drone: 3. Set the `WIFI_MODE` parameter to `3` on the drone:
@@ -356,7 +356,7 @@ To setup ESP-NOW communication:
## Flight log ## Flight log
After the flight, you can download the flight log wirelessly for analysis. Use the following command on your computer for that: After the flight, you can download the flight log for analysis wirelessly. Use the following command on your computer for that:
```bash ```bash
make log make log
-9
View File
@@ -4,15 +4,6 @@ This page contains user-built drones based on the Flix project. Publish your pro
--- ---
Author: Alican Erüst.<br>
Description: QX95 mm frame, 55 mm propellers, 3.7 V 25C 1050 mAh LiPo battery, MPU6050 IMU, Logitech F310 gamepad controller, with a total quadcopter weight of 66 g.
<img src="img/user/alicanerus/1.jpg" height=200> <img src="img/user/alicanerus/2.jpg" height=200> <img src="img/user/alicanerus/3.jpg" height=200>
[Flight video](https://drive.google.com/file/d/1k0WeWTKnCAfaugkX7LcmNxsUuq79RL8Z/view?usp=sharing).
---
Author: [Неруш Михаил](https://t.me/NerushMV).<br> Author: [Неруш Михаил](https://t.me/NerushMV).<br>
Description: custom frame made of 4 mm plywood, 8520 brushed motors, 75 mm propellers, MPU-6500. FlySky FS-i6X with ESP32-based adapter for ESP-NOW communication (using PPM output).<br> Description: custom frame made of 4 mm plywood, 8520 brushed motors, 75 mm propellers, MPU-6500. FlySky FS-i6X with ESP32-based adapter for ESP-NOW communication (using PPM output).<br>
Sources and materials: [link](https://drive.google.com/drive/folders/1uWiDcuorLrtVs_IIR7Y13omij-7Q1nx8). Sources and materials: [link](https://drive.google.com/drive/folders/1uWiDcuorLrtVs_IIR7Y13omij-7Q1nx8).
+25 -8
View File
@@ -6,7 +6,7 @@
#include "pid.h" #include "pid.h"
#include "vector.h" #include "vector.h"
#include "util.h" #include "util.h"
#include "lpf.h" #include "filter.h"
extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT; extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT;
extern const int RAW, ACRO, STAB, AUTO; extern const int RAW, ACRO, STAB, AUTO;
@@ -51,7 +51,13 @@ const char* motd =
"espnow <mac> [<key>] - configure ESP-NOW peer\n" "espnow <mac> [<key>] - configure ESP-NOW peer\n"
"mot - show motor output\n" "mot - show motor output\n"
"log [dump] - print log header [and data]\n" "log [dump] - print log header [and data]\n"
"mfr/mfl/mrr/mrl [<thrust>] - test motor (remove props)\n" "mfr, mfl, mrr, mrl - test motor (remove props)\n"
"log - show log info\n"
"log header - show log header\n"
"log reset - reset log\n"
"log <name> <rate> - setup log topic rate\n"
"l <str> - show log values starting with str\n"
"l expose <name> - expose log value to telemetry\n"
"sys - show system info\n" "sys - show system info\n"
"reset - reset drone's state\n" "reset - reset drone's state\n"
"reboot - reboot the drone\n"; "reboot - reboot the drone\n";
@@ -153,27 +159,38 @@ void doCommand(String str, bool echo = false) {
} else if (command == "mot") { } else if (command == "mot") {
print("front-right %g front-left %g rear-right %g rear-left %g\n", print("front-right %g front-left %g rear-right %g rear-left %g\n",
motors[MOTOR_FRONT_RIGHT], motors[MOTOR_FRONT_LEFT], motors[MOTOR_REAR_RIGHT], motors[MOTOR_REAR_LEFT]); motors[MOTOR_FRONT_RIGHT], motors[MOTOR_FRONT_LEFT], motors[MOTOR_REAR_RIGHT], motors[MOTOR_REAR_LEFT]);
} else if (command == "log") { } else if (command == "log" && arg0 == "") {
printLogInfo();
} else if (command == "log" && arg1 != "") {
configLogThrottle(arg0.c_str(), arg1.toFloat());
} else if (command == "log" && arg0 == "header") {
printLogHeader(); printLogHeader();
if (arg0 == "dump") printLogData(); } else if (command == "log" && arg0 == "reset") {
resetLog();
} else if (command == "l" && arg0 == "expose" && arg1 != "") {
exposeLogValue(arg1.c_str());
} else if (command == "l") {
printLogValues(arg0.c_str());
} else if (command == "cr") { } else if (command == "cr") {
calibrateRC(); calibrateRC();
} else if (command == "ca") { } else if (command == "ca") {
calibrateAccel(); calibrateAccel();
} else if (command == "mfr") { } else if (command == "mfr") {
testMotor(MOTOR_FRONT_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_FRONT_RIGHT);
} else if (command == "mfl") { } else if (command == "mfl") {
testMotor(MOTOR_FRONT_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_FRONT_LEFT);
} else if (command == "mrr") { } else if (command == "mrr") {
testMotor(MOTOR_REAR_RIGHT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_REAR_RIGHT);
} else if (command == "mrl") { } else if (command == "mrl") {
testMotor(MOTOR_REAR_LEFT, arg0.isEmpty() ? 0.2 : arg0.toFloat()); testMotor(MOTOR_REAR_LEFT);
} else if (command == "sys") { } else if (command == "sys") {
#ifdef ESP32 #ifdef ESP32
print("Chip: %s\n", ESP.getChipModel()); print("Chip: %s\n", ESP.getChipModel());
print("Temperature: %.1f °C\n", temperatureRead()); print("Temperature: %.1f °C\n", temperatureRead());
print("Total RAM: %d KB\n", ESP.getHeapSize() / 1024); print("Total RAM: %d KB\n", ESP.getHeapSize() / 1024);
print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024); print("Free heap: %d KB\n", ESP.getFreeHeap() / 1024);
print("PSRAM: %d KB\n", ESP.getPsramSize() / 1024);
print("Free PSRAM: %d KB\n", ESP.getFreePsram() / 1024);
print("Firmware: " __DATE__ " " __TIME__ "\n"); print("Firmware: " __DATE__ " " __TIME__ "\n");
// Print tasks table // Print tasks table
print("Num Task MinSt Prio Core CPU%%\n"); print("Num Task MinSt Prio Core CPU%%\n");
+1 -1
View File
@@ -6,7 +6,7 @@
#include "vector.h" #include "vector.h"
#include "quaternion.h" #include "quaternion.h"
#include "pid.h" #include "pid.h"
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
#define PITCHRATE_P 0.05 #define PITCHRATE_P 0.05
+8 -1
View File
@@ -5,7 +5,7 @@
#include "quaternion.h" #include "quaternion.h"
#include "vector.h" #include "vector.h"
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
Vector rates; // estimated angular rates, rad/s Vector rates; // estimated angular rates, rad/s
@@ -15,6 +15,12 @@ bool landed;
float accWeight = 0.003; float accWeight = 0.003;
float levelWeight = 0.0002; float levelWeight = 0.0002;
LowPassFilter<Vector> ratesFilter(0.2); // cutoff frequency ~ 40 Hz LowPassFilter<Vector> ratesFilter(0.2); // cutoff frequency ~ 40 Hz
NotchFilter<Vector> ratesNotch(382, 40);
void setupEstimate() {
print("Setup estimation\n");
ratesNotch.reset();
}
void estimate() { void estimate() {
applyGyro(); applyGyro();
@@ -25,6 +31,7 @@ void estimate() {
void applyGyro() { void applyGyro() {
// filter gyro to get angular rates // filter gyro to get angular rates
rates = ratesFilter.update(gyro); rates = ratesFilter.update(gyro);
rates = ratesNotch.update(rates);
// apply rates to attitude // apply rates to attitude
attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(rates * dt)); attitude = Quaternion::rotate(attitude, Quaternion::fromRotationVector(rates * dt));
+98
View File
@@ -0,0 +1,98 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Low pass and notch filters
#pragma once
template <typename T> // Using template to make the filter usable for scalar and vector values
class LowPassFilter {
public:
float alpha; // smoothing constant, 1 means filter disabled
T output;
LowPassFilter(float alpha): alpha(alpha) {};
T update(const T input) {
if (!init) {
init = true;
return output = input;
}
return output += alpha * (input - output);
}
void setCutOffFrequency(float cutOffFreq, float dt) {
alpha = 1 - exp(-2 * PI * cutOffFreq * dt);
}
void reset() {
init = false;
}
private:
bool init = false;
};
template <typename T>
class NotchFilter {
public:
float frequency;
float bandwidth;
T output;
NotchFilter(float frequency, float bandwidth): frequency(frequency), bandwidth(bandwidth) {
reset();
};
T update(const T input) {
if (frequency <= 0 || bandwidth <= 0) return input;
if (!init) {
init = true;
x1 = x2 = input;
y1 = y2 = input;
return output = input;
}
output = b0 * input + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1;
x1 = input;
y2 = y1;
y1 = output;
return output;
}
void reset() {
const float dt = 0.001f;
float f = frequency;
float bw = bandwidth;
if (f < 0) f = 0;
if (bw < 1e-6f) bw = 1e-6f;
float q = f / bw;
if (q < 1e-3f) q = 1e-3f;
const float w0 = 2.0f * PI * f * dt;
const float c = cos(w0);
const float s = sin(w0);
const float alpha = s / (2.0f * q);
const float a0 = 1.0f + alpha;
const float invA0 = 1.0f / a0;
b0 = 1.0f * invA0;
b1 = -2.0f * c * invA0;
b2 = 1.0f * invA0;
a1 = -2.0f * c * invA0;
a2 = (1.0f - alpha) * invA0;
init = false;
}
private:
float b0, b1, b2, a1, a2;
T x1, x2, y1, y2;
bool init = false;
};
+3 -1
View File
@@ -26,6 +26,8 @@ void setup() {
setupWiFi(); setupWiFi();
setupIMU(); setupIMU();
setupRC(); setupRC();
setupEstimate();
setupLog();
setLED(false); setLED(false);
print("Initializing complete\n"); print("Initializing complete\n");
} }
@@ -40,6 +42,6 @@ void loop() {
handleInput(); handleInput();
processMavlink(); processMavlink();
readVoltage(); readVoltage();
logData(); loopLog();
syncParameters(); syncParameters();
} }
+1 -1
View File
@@ -6,7 +6,7 @@
#include <SPI.h> #include <SPI.h>
#include <FlixPeriph.h> #include <FlixPeriph.h>
#include "vector.h" #include "vector.h"
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
MPU9250 imu(SPI); MPU9250 imu(SPI);
+247 -52
View File
@@ -1,77 +1,272 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com> // Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix // Repository: https://github.com/okalachev/flix
// In-RAM logging // Logging subsystem
#include "vector.h" #include "vector.h"
#include "util.h" #include "util.h"
#define LOG_RATE 100 int logMemory = 0; // 0 - RAM, 1 - PSRAM, -1 - disabled
#define LOG_DURATION 10 float logUsage = 0.5; // fraction of free memory to use for log
#define LOG_SIZE LOG_DURATION * LOG_RATE
Vector attitudeEuler; struct LogValue {
Vector attitudeTargetEuler;
struct LogEntry {
const char *name; const char *name;
float *value; Value value;
float lastValue = NAN;
bool logged = true; // if false, use only for triggering log update
LogValue() : name(nullptr), value() {}; // empty value constructor
template <typename T>
LogValue(const char *name, T value, bool logged = true) : name(name), value(value), logged(logged) {};
}; };
LogEntry logEntries[] = { struct LogTopic {
{"t", &t}, LogValue values[10];
{"rates.x", &rates.x}, int length = 0; // number of logged values
{"rates.y", &rates.y}, float throttle; // max update rate, Hz
{"rates.z", &rates.z}, float lastUpdate = -INFINITY;
{"ratesTarget.x", &ratesTarget.x},
{"ratesTarget.y", &ratesTarget.y}, LogTopic(float throttle, LogValue v0, LogValue v1 = {}, LogValue v2 = {}, LogValue v3 = {}, LogValue v4 = {}, LogValue v5 = {}, LogValue v6 = {}, LogValue v7 = {}, LogValue v8 = {}, LogValue v9 = {}) :
{"ratesTarget.z", &ratesTarget.z}, throttle(throttle), values{v0, v1, v2, v3, v4, v5, v6, v7, v8, v9} {
{"attitude.x", &attitudeEuler.x}, // Count logged values
{"attitude.y", &attitudeEuler.y}, for (auto& v : values) {
{"attitude.z", &attitudeEuler.z}, if (v.name == nullptr) break;
{"attitudeTarget.x", &attitudeTargetEuler.x}, if (v.logged) length++;
{"attitudeTarget.y", &attitudeTargetEuler.y}, }
{"attitudeTarget.z", &attitudeTargetEuler.z}, };
{"thrustTarget", &thrustTarget}
LogTopic(LogValue v0, LogValue v1 = {}, LogValue v2 = {}, LogValue v3 = {}, LogValue v4 = {}, LogValue v5 = {}, LogValue v6 = {}, LogValue v7 = {}, LogValue v8 = {}, LogValue v9 = {}) :
LogTopic(INFINITY, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9) {};
}; };
const int logColumns = sizeof(logEntries) / sizeof(logEntries[0]); LogTopic logTopics[] = {
float logBuffer[LOG_SIZE][logColumns]; // time
LogTopic({"t", &t}), // must be the first topic
LogTopic(1, {"loopRate", &loopRate}),
void prepareLogData() { // imu
attitudeEuler = attitude.toEuler(); LogTopic(
attitudeTargetEuler = attitudeTarget.toEuler(); {"gyro.x", &gyro.x},
} {"gyro.y", &gyro.y},
{"gyro.z", &gyro.z}),
void logData() { LogTopic(50,
if (!armed) return; {"acc.x", &acc.x},
static int logPointer = 0; {"acc.y", &acc.y},
static Rate period(LOG_RATE); {"acc.z", &acc.z}),
if (!period) return;
prepareLogData(); LogTopic(10,
{"gyroBias.x", &gyroBias.x},
{"gyroBias.y", &gyroBias.y},
{"gyroBias.z", &gyroBias.z}),
for (int i = 0; i < logColumns; i++) { // estimation
logBuffer[logPointer][i] = *logEntries[i].value; LogTopic(50,
} {"rates.x", &rates.x},
{"rates.y", &rates.y},
{"rates.z", &rates.z},
{"attitude.roll", []() { return attitude.getRoll(); }},
{"attitude.pitch", []() { return attitude.getPitch(); }},
{"attitude.yaw", []() { return attitude.getYaw(); }}),
logPointer++; // rc
if (logPointer >= LOG_SIZE) { LogTopic(10,
logPointer = 0; {"controlTime", &controlTime, false}, // trigger value
{"controlRoll", &controlRoll},
{"controlPitch", &controlPitch},
{"controlYaw", &controlYaw},
{"controlThrottle", &controlThrottle}),
// control
LogTopic({"armed", &armed}),
LogTopic({"mode", &mode}),
LogTopic(10,
{"ratesTarget.x", &ratesTarget.x},
{"ratesTarget.y", &ratesTarget.y},
{"ratesTarget.z", &ratesTarget.z},
{"attitudeTarget.roll", []() { return attitudeTarget.getRoll(); }},
{"attitudeTarget.pitch", []() { return attitudeTarget.getPitch(); }},
{"attitudeTarget.yaw", []() { return attitudeTarget.getYaw(); }},
{"thrustTarget", &thrustTarget}),
// motors
LogTopic(
{"motors[0]", &motors[0]},
{"motors[1]", &motors[1]},
{"motors[2]", &motors[2]},
{"motors[3]", &motors[3]}),
// misc
LogTopic(5,
{"voltage", &voltage},
{"temp", &temperatureRead},
{"imuTemp", []() { return imu.getTemp(); }}),
};
void *logBuffer; // buffer for log data
size_t logCapacity;
size_t logCursor = 0;
size_t logLength = 0;
LogValue *logExposed = nullptr; // log values exposed to telemetry
void setupLog() {
print("Setup log\n");
free(logBuffer); // when reconfiguring
logBuffer = nullptr;
logCursor = 0;
logLength = 0;
if (logMemory == 0) {
logCapacity = ESP.getFreeHeap() * logUsage;
logBuffer = (uint8_t *)calloc(logCapacity, 1);
} else if (logMemory == 1) {
logCapacity = ESP.getFreePsram() * logUsage;
logBuffer = (uint8_t *)heap_caps_calloc(logCapacity, 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
} }
} }
void printLogHeader() { void loopLog() {
for (int i = 0; i < logColumns; i++) { if (logBuffer == nullptr || !armed) return;
print("%s%s", logEntries[i].name, i < logColumns - 1 ? "," : "\n");
}
}
void printLogData() { if (!logLength) resetLog(); // reset state on first log write
for (int i = 0; i < LOG_SIZE; i++) {
if (logBuffer[i][0] == 0) continue; // skip empty records static Rate sync(2);
for (int j = 0; j < logColumns; j++) { if (sync) {
print("%g%s", logBuffer[i][j], j < logColumns - 1 ? "," : "\n"); const uint8_t marker[] = {0x1A, 0x91, 0x4F, 0xF6, 0x7F};
writeLog(&marker, sizeof(marker)); // write sync marker
}
for (uint8_t i = 0; i < sizeof(logTopics) / sizeof(logTopics[0]); i++) {
LogTopic& topic = logTopics[i];
if (t - topic.lastUpdate < 1 / topic.throttle) continue; // throttle topic
if (!isTopicUpdated(i)) continue; // skip if topic was't updated
topic.lastUpdate = t;
writeLog(&i, sizeof(i)); // write topic index
for (auto& value : topic.values) {
if (value.name == nullptr) break;
if (!value.logged) continue;
value.lastValue = value.value.get();
writeLog(&value.lastValue, sizeof(float)); // write value
} }
} }
} }
void resetLog() {
for (auto& topic : logTopics) {
topic.lastUpdate = -INFINITY;
for (auto& value : topic.values) {
value.lastValue = NAN;
}
}
logCursor = 0;
logLength = 0;
}
void writeLog(const void *data, size_t size) {
size_t first = min(size, logCapacity - logCursor);
size_t second = size - first;
memcpy(logBuffer + logCursor, data, first);
logCursor = (logCursor + first) % logCapacity;
if (second > 0) {
memcpy(logBuffer + logCursor, data + first, second);
logCursor = (logCursor + second) % logCapacity;
}
logLength = min(logLength + size, logCapacity);
}
void readLog(void *data, size_t position, size_t size) {
if (logLength == logCapacity) {
position = (logCursor + position) % logCapacity;
}
size_t first = min(size, logCapacity - position);
size_t second = size - first;
memcpy(data, logBuffer + position, first);
if (second > 0) {
memcpy(data + first, logBuffer, second);
}
}
bool isTopicUpdated(const uint8_t topic) {
LogTopic& logTopic = logTopics[topic];
bool updated = false;
for (auto& value : logTopic.values) {
if (value.name == nullptr) break;
float v = value.value.get();
if (!floatEquals(value.lastValue, v)) {
value.lastValue = v;
updated = true;
}
}
return updated;
}
void printLogInfo() {
if (logMemory == -1) return print("Log: disabled\n");
print("Memory: %s\n", logMemory == 0 ? "RAM" : "PSRAM");
print("Usage: %.f%%\n", logUsage * 100);
print("Capacity: %u bytes\n", (unsigned)logCapacity);
print("Used: %u bytes\n", (unsigned)logLength);
print("Estimated duration: %d seconds\n", estimateLogDuration());
}
int estimateLogDuration() {
float bandwidth = 0;
for (LogTopic& topic : logTopics) {
float rate = isinf(topic.throttle) ? loopRate : topic.throttle;
bandwidth += rate * topic.length * sizeof(float);
}
return logCapacity / bandwidth;
}
void printLogHeader() {
int i = 0;
for (auto& topic : logTopics) {
print("Topic #%d (%g Hz):\n", i++, topic.throttle);
for (auto& value : topic.values) {
if (value.name == nullptr) break;
print(" %s%s\n", value.name, value.logged ?"" : " (not logged)");
}
}
}
void printLogValues(const char *filter) {
for (LogTopic& topic : logTopics) {
for (LogValue& value : topic.values) {
if (value.name == nullptr) break;
if (strncasecmp(value.name, filter, strlen(filter))) continue;
print("%s = %g\n", value.name, value.value.get());
}
}
}
void configLogThrottle(const char *name, float throttle) {
for (LogTopic& topic : logTopics) {
for (LogValue& value : topic.values) {
if (value.name == nullptr) break;
if (strcasecmp(value.name, name) != 0) continue;
topic.throttle = throttle;
print("Log throttle for %s set to %.1f Hz\n", name, throttle);
return;
}
}
print("Log value not found: %s\n", name);
}
void exposeLogValue(const char *name) {
for (int i = 0; i < sizeof(logTopics) / sizeof(logTopics[0]); i++) {
LogTopic& topic = logTopics[i];
for (LogValue& value : topic.values) {
if (value.name == nullptr) break;
if (strcasecmp(value.name, name) != 0) continue;
logExposed = &value;
print("Log value %s exposed\n", name);
return;
}
}
print("Log value not found: %s\n", name);
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (c) 2023 Oleg Kalachev <okalachev@gmail.com>
// Repository: https://github.com/okalachev/flix
// Low pass filter implementation
#pragma once
template <typename T> // Using template to make the filter usable for scalar and vector values
class LowPassFilter {
public:
float alpha; // smoothing constant, 1 means filter disabled
T output;
LowPassFilter(float alpha): alpha(alpha) {};
T update(const T input) {
if (!init) {
init = true;
return output = input;
}
return output += alpha * (input - output);
}
void setCutOffFrequency(float cutOffFreq, float dt) {
alpha = 1 - exp(-2 * PI * cutOffFreq * dt);
}
void reset() {
init = false;
}
private:
bool init = false;
};
+44 -6
View File
@@ -16,6 +16,7 @@ Rate telemetryAttitude(20);
Rate telemetryRC(10); Rate telemetryRC(10);
Rate telemetryMotors(10); Rate telemetryMotors(10);
Rate telemetryIMU(15); Rate telemetryIMU(15);
Rate telemetryTopic(10);
bool mavlinkConnected = false; bool mavlinkConnected = false;
String mavlinkPrintBuffer; String mavlinkPrintBuffer;
@@ -84,6 +85,12 @@ void sendMavlink() {
0, 0, 0, 0); 0, 0, 0, 0);
sendMessage(&msg); sendMessage(&msg);
} }
if (telemetryTopic && logExposed != nullptr) {
mavlink_msg_named_value_float_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg,
time, logExposed->name, logExposed->value.get());
sendMessage(&msg);
}
} }
void sendMessage(const void *msg) { void sendMessage(const void *msg) {
@@ -92,6 +99,26 @@ void sendMessage(const void *msg) {
sendWiFi(buf, len); sendWiFi(buf, len);
} }
static uint8_t mavlinkBatch[ESP_NOW_MAX_DATA_LEN_V2];
static int mavlinkBatchSize = 0;
void batchMessage(const void *msg) {
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
int len = mavlink_msg_to_send_buffer(buf, (mavlink_message_t *)msg);
if (mavlinkBatchSize + len > sizeof(mavlinkBatch)) {
sendWiFi(mavlinkBatch, mavlinkBatchSize);
mavlinkBatchSize = 0;
}
memcpy(mavlinkBatch + mavlinkBatchSize, buf, len);
mavlinkBatchSize += len;
}
void flushBatchMessages() {
sendWiFi(mavlinkBatch, mavlinkBatchSize);
mavlinkBatchSize = 0;
}
void receiveMavlink() { void receiveMavlink() {
uint8_t buf[MAVLINK_MAX_PACKET_LEN]; uint8_t buf[MAVLINK_MAX_PACKET_LEN];
int len = receiveWiFi(buf, MAVLINK_MAX_PACKET_LEN); int len = receiveWiFi(buf, MAVLINK_MAX_PACKET_LEN);
@@ -225,18 +252,29 @@ void handleMavlink(const void *_msg) {
armed = motors[0] > 0 || motors[1] > 0 || motors[2] > 0 || motors[3] > 0; armed = motors[0] > 0 || motors[1] > 0 || motors[2] > 0 || motors[3] > 0;
} }
if (msg.msgid == MAVLINK_MSG_ID_LOG_REQUEST_LIST) {
const uint32_t qgcEpoch = 1262304000; // qgc accepts only timestamps after 2010-01-01
mavlink_message_t response;
mavlink_msg_log_entry_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &response,
0, 1, 0, qgcEpoch + t * 60, logLength); // put fake unique date to make qgc happy with saving logs
sendMessage(&response);
}
if (msg.msgid == MAVLINK_MSG_ID_LOG_REQUEST_DATA) { if (msg.msgid == MAVLINK_MSG_ID_LOG_REQUEST_DATA) {
mavlink_log_request_data_t m; mavlink_log_request_data_t m;
mavlink_msg_log_request_data_decode(&msg, &m); mavlink_msg_log_request_data_decode(&msg, &m);
if (m.target_system && m.target_system != mavlinkSysId) return; if (m.target_system && m.target_system != mavlinkSysId) return;
// Send all log records for (int i = 0; i < m.count; i += MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN) {
for (int i = 0; i < sizeof(logBuffer) / sizeof(logBuffer[0]); i++) { int chunkSize = min(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN, (int)(m.count - i));
mavlink_message_t msg; mavlink_message_t response;
mavlink_msg_log_data_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &msg, 0, i, uint8_t data[MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN];
sizeof(logBuffer[0]), (uint8_t *)logBuffer[i]); readLog(data, m.ofs + i, chunkSize);
sendMessage(&msg); mavlink_msg_log_data_pack(mavlinkSysId, MAV_COMP_ID_AUTOPILOT1, &response,
m.id, m.ofs + i, chunkSize, data);
batchMessage(&response);
} }
flushBatchMessages();
} }
// Handle commands // Handle commands
+2 -2
View File
@@ -51,9 +51,9 @@ bool motorsActive() {
return motors[0] != 0 || motors[1] != 0 || motors[2] != 0 || motors[3] != 0; return motors[0] != 0 || motors[1] != 0 || motors[2] != 0 || motors[3] != 0;
} }
void testMotor(int n, float thrust) { void testMotor(int n) {
print("Testing motor %d\n", n); print("Testing motor %d\n", n);
motors[n] = thrust; motors[n] = 0.2;
delay(50); // ESP32 may need to wait until the end of the current cycle to change duty https://github.com/espressif/arduino-esp32/issues/5306 delay(50); // ESP32 may need to wait until the end of the current cycle to change duty https://github.com/espressif/arduino-esp32/issues/5306
sendMotors(); sendMotors();
pause(3); pause(3);
+18
View File
@@ -74,6 +74,8 @@ Parameter parameters[] = {
{"EST_ACC_WEIGHT", &accWeight}, {"EST_ACC_WEIGHT", &accWeight},
{"EST_LVL_WEIGHT", &levelWeight}, {"EST_LVL_WEIGHT", &levelWeight},
{"EST_RATES_LPF_A", &ratesFilter.alpha}, {"EST_RATES_LPF_A", &ratesFilter.alpha},
{"EST_RATES_NF_F", &ratesNotch.frequency, setupEstimate},
{"EST_RATES_NF_BW", &ratesNotch.bandwidth, setupEstimate},
// motors // motors
{"MOT_PIN_FL", &motorPins[MOTOR_FRONT_LEFT], setupMotors}, {"MOT_PIN_FL", &motorPins[MOTOR_FRONT_LEFT], setupMotors},
{"MOT_PIN_FR", &motorPins[MOTOR_FRONT_RIGHT], setupMotors}, {"MOT_PIN_FR", &motorPins[MOTOR_FRONT_RIGHT], setupMotors},
@@ -121,6 +123,22 @@ Parameter parameters[] = {
{"MAV_RATE_RC", &telemetryRC.rate}, {"MAV_RATE_RC", &telemetryRC.rate},
{"MAV_RATE_MOT", &telemetryMotors.rate}, {"MAV_RATE_MOT", &telemetryMotors.rate},
{"MAV_RATE_IMU", &telemetryIMU.rate}, {"MAV_RATE_IMU", &telemetryIMU.rate},
{"MAV_RATE_TOPIC", &telemetryTopic.rate},
// log
{"LOG_MEMORY", &logMemory, setupLog},
{"LOG_USAGE", &logUsage, setupLog},
{"LOG_RATE_000", &logTopics[0].throttle},
{"LOG_RATE_001", &logTopics[1].throttle},
{"LOG_RATE_002", &logTopics[2].throttle},
{"LOG_RATE_003", &logTopics[3].throttle},
{"LOG_RATE_004", &logTopics[4].throttle},
{"LOG_RATE_005", &logTopics[5].throttle},
{"LOG_RATE_006", &logTopics[6].throttle},
{"LOG_RATE_007", &logTopics[7].throttle},
{"LOG_RATE_008", &logTopics[8].throttle},
{"LOG_RATE_009", &logTopics[9].throttle},
{"LOG_RATE_010", &logTopics[10].throttle},
{"LOG_RATE_011", &logTopics[11].throttle},
// power // power
{"PWR_VOLT_PIN", &voltagePin, setupPower}, {"PWR_VOLT_PIN", &voltagePin, setupPower},
{"PWR_VOLT_SCALE", &voltageScale}, {"PWR_VOLT_SCALE", &voltageScale},
+1 -1
View File
@@ -5,7 +5,7 @@
#pragma once #pragma once
#include "lpf.h" #include "filter.h"
class PID { class PID {
public: public:
+1 -1
View File
@@ -5,7 +5,7 @@
#include <soc/soc.h> #include <soc/soc.h>
#include <soc/rtc_cntl_reg.h> #include <soc/rtc_cntl_reg.h>
#include "lpf.h" #include "filter.h"
#include "util.h" #include "util.h"
float voltage = NAN; float voltage = NAN;
+43
View File
@@ -64,6 +64,49 @@ public:
} }
}; };
// Simple variant type for logging and parameters
struct Value {
enum { EMPTY, FLOAT, INT, BOOL, FLOAT_FN, INT_FN, BOOL_FN } type;
union {
void *pointer;
float *_float;
int *_int;
bool *_bool;
float (*floatFn)();
int (*intFn)();
bool (*boolFn)();
};
Value() : type(EMPTY), pointer(nullptr) {};
Value(float *pt) : type(FLOAT), _float(pt) {};
Value(int *pt) : type(INT), _int(pt) {};
Value(bool *pt) : type(BOOL), _bool(pt) {};
Value(float (*fn)()) : type(FLOAT_FN), floatFn(fn) {};
Value(int (*fn)()) : type(INT_FN), intFn(fn) {};
Value(bool (*fn)()) : type(BOOL_FN), boolFn(fn) {};
float get() const {
switch (type) {
case FLOAT: return *_float;
case INT: return *_int;
case BOOL: return *_bool ? 1 : 0;
case FLOAT_FN: return floatFn();
case INT_FN: return intFn();
case BOOL_FN: return boolFn() ? 1 : 0;
default: return NAN;
}
};
void set(float value) const {
switch (type) {
case FLOAT: *_float = value; break;
case INT: *_int = value; break;
case BOOL: *_bool = (value != 0); break;
default: break;
}
};
};
// Rate limiter // Rate limiter
class Rate { class Rate {
public: public:
+1
View File
@@ -156,6 +156,7 @@ HardwareSerial Serial, Serial1, Serial2;
class EspClass { class EspClass {
public: public:
void restart() { Serial.println("Ignore reboot in simulation"); } void restart() { Serial.println("Ignore reboot in simulation"); }
uint32_t getFreeHeap() { return 300 * 1024; } // assume 300 KB free heap
} ESP; } ESP;
unsigned long __delayTime = 0; unsigned long __delayTime = 0;
+12 -3
View File
@@ -9,7 +9,7 @@
#include "quaternion.h" #include "quaternion.h"
#include "Arduino.h" #include "Arduino.h"
#include "wifi.h" #include "wifi.h"
#include "lpf.h" #include "filter.h"
extern float t, dt; extern float t, dt;
extern float controlRoll, controlPitch, controlYaw, controlThrottle, controlMode; extern float controlRoll, controlPitch, controlYaw, controlThrottle, controlMode;
@@ -38,7 +38,7 @@ const char* getModeName();
void sendMotors(); void sendMotors();
int getDutyCycle(float value); int getDutyCycle(float value);
bool motorsActive(); bool motorsActive();
void testMotor(int, float); void testMotor(int n);
void print(const char* format, ...); void print(const char* format, ...);
void pause(float duration); void pause(float duration);
void doCommand(String str, bool echo); void doCommand(String str, bool echo);
@@ -48,8 +48,17 @@ void normalizeRC();
void calibrateRC(); void calibrateRC();
void calibrateRCChannel(int*, uint16_t[16], uint16_t[16], const char*); void calibrateRCChannel(int*, uint16_t[16], uint16_t[16], const char*);
void printRCCalibration(); void printRCCalibration();
void loopLog();
void resetLog();
void writeLog(const void *data, size_t size);
void readLog(void *data, size_t position, size_t size);
bool isTopicUpdated(const uint8_t topic);
void printLogInfo();
int estimateLogDuration();
void printLogHeader(); void printLogHeader();
void printLogData(); void printLogValues(const char *filter);
void configLogThrottle(const char *name, float throttle);
void exposeLogValue(const char *name);
void processMavlink(); void processMavlink();
void sendMavlink(); void sendMavlink();
void sendMessage(const void *msg); void sendMessage(const void *msg);
+2 -1
View File
@@ -23,7 +23,7 @@
#include "estimate.ino" #include "estimate.ino"
#include "safety.ino" #include "safety.ino"
#include "log.ino" #include "log.ino"
#include "lpf.h" #include "filter.h"
#include "mavlink.ino" #include "mavlink.ino"
#include "motors.ino" #include "motors.ino"
#include "parameters.ino" #include "parameters.ino"
@@ -55,6 +55,7 @@ public:
initNode(); initNode();
Serial.begin(0); Serial.begin(0);
setupParameters(); setupParameters();
setupLog();
gzmsg << "Flix plugin loaded" << endl; gzmsg << "Flix plugin loaded" << endl;
} }
+10 -1
View File
@@ -9,6 +9,7 @@ Usage:
import csv import csv
import json import json
import docopt import docopt
import math
from mcap.writer import Writer from mcap.writer import Writer
args = docopt.docopt(__doc__) args = docopt.docopt(__doc__)
@@ -39,7 +40,15 @@ channel_id = writer.register_channel(
) )
for row in csv_reader: for row in csv_reader:
data = {key: float(value) for key, value in zip(header, row)} if row[0] == '': continue
data = {}
for key, value in zip(header, row):
if value == '' or math.isnan(float(value)):
data[key] = None
else:
data[key] = float(value)
data = {key: float(value) if value != '' else None for key, value in zip(header, row)}
timestamp = round(float(row[0]) * 1e9) timestamp = round(float(row[0]) * 1e9)
writer.add_message(channel_id=channel_id, log_time=timestamp, data=json.dumps(data).encode(), publish_time=timestamp,) writer.add_message(channel_id=channel_id, log_time=timestamp, data=json.dumps(data).encode(), publish_time=timestamp,)
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Convert flight from Flix format to CSV
Usage:
log_to_csv.py <input_file>
"""
import os
from pyflix import Flix
import docopt
import struct
import csv
DIR = os.path.dirname(os.path.realpath(__file__))
HEADER_FILE = os.path.join(DIR, 'log/log_header.txt')
# Read log header
try:
# read from file
header = open(HEADER_FILE, 'r').read()
except FileNotFoundError:
flix = Flix()
header = flix.cli('log header') # receive the log schema
open(HEADER_FILE, 'w').write(header) # save to file
# Parse log header
topics = []
for line in header.splitlines():
if not line.startswith(' '):
topics.append([])
elif 'not logged' not in line:
topics[-1].append(line.strip())
# Read log file
args = docopt.docopt(__doc__)
input_file = args['<input_file>']
outfile_file = input_file + '.csv'
data = open(input_file, 'rb').read()
# Search for sync marker
SYNC_MARKER = bytes.fromhex('1A 91 4F F6 7F')
sync_offset = data.find(SYNC_MARKER)
if sync_offset == -1:
raise ValueError('Sync marker not found in log file')
data = data[sync_offset + len(SYNC_MARKER):]
data = data.replace(SYNC_MARKER, b'') # remove all other sync markers
header_row = [f'{value}' for topic in topics for value in topic]
rows = []
offset = 0
while offset < len(data):
try:
topic = struct.unpack_from('B', data, offset)[0]
offset += 1
if topic >= len(topics):
raise ValueError(f'Invalid topic {topic} at offset {offset}')
if topic == 0 or not rows:
rows.append({})
for name in topics[topic]:
value = struct.unpack_from('<f', data, offset)[0]
offset += 4
rows[-1][name] = value
except struct.error as e:
break
# Write CSV file
with open(outfile_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=header_row, extrasaction='ignore')
writer.writeheader()
rows = filter(lambda row: float(row.get('t', 0)), rows)
writer.writerows(rows)