155 Commits
Author SHA1 Message Date
Oleg Kalachev d825652a71 Docs fix
Reflect lpf.h was renamed to filter.h
2026-08-16 00:10:47 +03:00
Oleg Kalachev 37f267ecc9 Change type hints of the most List arguments to Sequence in pyflix
These arguments are effectively immutable, so Sequence makes more sense - the user may use tuples in addition to lists.
2026-08-14 10:36:10 +03:00
Oleg Kalachev 3450932dce Add testing of simulation run 2026-08-14 09:56:04 +03:00
Oleg Kalachev 149c891cfc Put binaries into docs artifact only in master and dev branch of the upstream repo 2026-08-14 04:26:30 +03:00
Oleg Kalachev 0512f5ce68 Add sloc calculator to ci 2026-08-14 04:21:36 +03:00
Oleg Kalachev f12b239af9 Fix simulation run 2026-08-14 04:19:41 +03:00
Oleg Kalachev 0a377cfe6f Fix default windup value in pid
INFINITY makes much more sense, since otherwise i term would be ineffective at all by default
2026-08-14 03:06:34 +03:00
Oleg Kalachev fd8bf03cc7 Update all used actions
Some of them were deprecated.
2026-08-14 02:35:34 +03:00
Oleg Kalachev 3ea680605a Fix typo 2026-08-14 00:59:36 +03:00
Oleg Kalachev 5edac09473 Make arduino-cli commands more clear in Makefile
Move sketch name before other arguments.
2026-08-14 00:21:51 +03:00
Oleg Kalachev 7312aeff32 Rename lpf.h to filter.h
To implement more filters (like notch) potentially.
2026-08-12 02:01:56 +03:00
Oleg Kalachev d65c5c0eb1 Build espnow-proxy in ci 2026-08-12 01:59:52 +03:00
Oleg Kalachev 76456b778b Publish official pre-built binaries
Add qspi and opi psram binaries.
Build binary for Flix2 board.
Add config.h file for defining parameter defaults.
Set appropriate pwm frequency for c3/s3 automatically using config.h.
Remove pid parameters definitions, the may be set is config.h now.
2026-08-11 06:02:55 +03:00
Oleg Kalachev f79c444f07 Configure imu using parameters instead of editing the source code
Choose the model and optionally custom pin numbers.
2026-08-11 05:36:31 +03:00
Oleg Kalachev 2a96f4795d Add missing parameter for yaw rate windup 2026-08-11 05:18:06 +03:00
Oleg Kalachev 6c55ab3625 Refactor building
Switch to plain esp32 fqbn - it works on d1 as well.
Move debug level setting from fqbn to arduino-cli command to simplify things.
Add C3 and S3 builds to commit artifacts.
2026-08-11 05:15:40 +03:00
Oleg Kalachev 97edb8e65b Minor docs changes 2026-08-11 05:03:56 +03:00
Oleg Kalachev bfeee7a00a Use up-to-date method for installing arduino-cli in simulator ci 2026-08-11 05:01:13 +03:00
Oleg Kalachev 3f4577bb2c Make Rate always trigger on first step 2026-08-11 05:01:02 +03:00
Oleg Kalachev 070eb8a0de Add parameter WIFI_BROADCAST for always broadcasting mode
May be used in some cases, like multiple gcs
2026-08-08 01:04:43 +03:00
Oleg Kalachev aee43c6548 Allow hot reconnection from another gcs over wifi
Switch back to udp broadcasting if no messages from gcs for 5 seconds
2026-08-07 18:24:45 +03:00
Oleg Kalachev 8e276bde38 Add Oleg1405 build 2026-08-04 22:48:39 +03:00
Oleg Kalachev a5991930d1 Disable voltage lpf by default
Filtering does more harm than good in voltage monitoring.
(alpha = 1 efficiently disables the filter.)
2026-08-03 15:57:29 +03:00
Oleg Kalachev 406f7a4af5 Add tilt disarm failsafe 2026-08-02 02:13:50 +03:00
Oleg Kalachev 5ee6d91440 Don't send discovery message if encrypting is disabled in espnow 2026-08-02 02:07:17 +03:00
Oleg Kalachev 38925d7885 Updates and fixes in usage article 2026-08-02 02:02:28 +03:00
Oleg Kalachev 5c81181221 Add threshold for rc sticks move for quitting auto mode 2026-08-02 01:58:16 +03:00
Oleg Kalachev fd249fcaa6 Add position control demo video to the readme 2026-08-01 22:33:26 +03:00
Oleg Kalachev 8917849711 Remove unneeded declaration from the sim 2026-07-15 18:40:51 +03:00
Oleg Kalachev 1cdc7a8641 Fix rc in simulator
Virtual rc is disabled if rxRxPin < 0
2026-07-15 18:40:35 +03:00
Oleg Kalachev d64bf24c6d Add alicanerus' build 2026-07-08 22:29:33 +03:00
Oleg Kalachev fabd5e072d Fixes in the docs 2026-07-04 21:40:39 +03:00
Oleg Kalachev 1ae85ff118 Add argument to motor testing commands for specifying thrust 2026-07-02 22:22:33 +03:00
Oleg Kalachev 26a0dd65be Minor fixes and updates 2026-06-30 12:19:29 +03:00
Oleg Kalachev 9d439afd80 Store only changed parameters in flash instead of all
Rationale:
1. Parameters NVS storage is limited and storing unchanged parameters is wasteful.
2. This allows changing parameters in the code without having to erase the flash storage.
3. Updating the firmware version may change the parameter defaults.
2026-06-28 17:21:07 +03:00
Oleg Kalachev 64b21a3a6b Show default parameter values in p command output
Also move float comparing logic to a distinct util function.
2026-06-24 06:42:09 +03:00
Oleg Kalachev 545eed8944 Add Nerush and Konstantinos Paraskevas builds 2026-06-19 05:15:17 +03:00
Oleg Kalachev 518abf1555 Simplify rc code
Utilize the new method for getting channel values.
2026-06-14 04:45:42 +03:00
Oleg Kalachev 17df1c5396 Separate repository vscode settings and local vscode settings
Use dangmai.workspace-default-settings externsion for that.
2026-06-11 20:37:35 +03:00
Oleg Kalachev 8e2ffd7c69 Remove core installation when running the sim
Split `dependencies` target to `core` and `libs` targets.
Move additional urls declaration and connection timeout from arduino-cli.yaml to Makefile for simplicity and transparency.
Update ESP32 core url.
Remove arduino-cli.yaml.
2026-06-10 18:10:42 +03:00
Oleg Kalachev 52b74afba6 Bring back espnow tx buffering keeping resends disabled
Buffering is needed for sending large prints, otherwise the espnow internal buffer overflows.
Make onSent always think there was success send, so there won't be any resends.
Print lost espnow packets count on wifi console command.
2026-06-09 03:23:30 +03:00
Oleg Kalachev 0ca2473655 Make each mavlink message rate configured separately
Add parameters:
* MAV_RATE_ATT - ATTITUDE_QUATERNION rate.
* MAV_RATE_RC - RC_CHANNELS_RAW rate.
* MAV_RATE_MOT - ACTUATOR_CONTROL_TARGET rate.
* MAV_RATE_IMU - SCALED_IMU rate.
2026-06-09 02:59:10 +03:00
Oleg Kalachev b4c2fe3988 Print total RAM in sys command 2026-06-09 02:49:39 +03:00
Oleg Kalachev e51b47b798 Add command for setting the wifi mode easier 2026-06-09 02:44:09 +03:00
Oleg Kalachev 71abe1bcdb Minor changes
Simplify the code, print imu temperature
2026-06-09 02:25:20 +03:00
Oleg Kalachev 0f2e384ce6 Don't apply level correction on idle thrust 2026-06-09 02:12:27 +03:00
Oleg Kalachev 5e153a210d Update ESP32-Core to 3.3.10 2026-06-09 02:05:52 +03:00
Oleg Kalachev 9d47bcb82e Minor changes in docs 2026-05-31 18:10:00 +03:00
Oleg Kalachev 1fafc27b39 Make it possible to unassign motor pin using -1 parameter value 2026-05-30 16:57:27 +03:00
Oleg Kalachev faca48ced3 Replace ps and psq commands with st command + minor changes
Re-arrange commands order.
Make command parser consider \r in addition to \n.
2026-05-30 16:51:15 +03:00
Oleg Kalachev a5dbd2c829 Add erase command to makefile 2026-05-30 16:48:12 +03:00
Oleg Kalachev 59f9528d34 Increase buffer for print, make sys output more correct
usStackHighWaterMark is not stack size, it's the minimum stack size
2026-05-30 11:11:35 +03:00
Oleg Kalachev 607b2ff0b7 Add malagis custom pcb version of Flix project to builds 2026-05-28 20:15:09 +03:00
Oleg Kalachev 22c06f76c4 Add Awab Anas' build 2026-05-28 19:29:05 +03:00
Oleg Kalachev 488ceb3004 Set the debug level to error by default to see the errors 2026-05-28 19:25:29 +03:00
Oleg Kalachev b83c9b3845 Consider mavlink connected only when the gcs message is parsed 2026-05-28 18:41:34 +03:00
Oleg Kalachev 2f4b1423e6 Typo and minor code style changes 2026-05-28 18:39:44 +03:00
Oleg Kalachev 4e32414dae Support ESP-NOW connection in pyflix
Set arbitrary pymavlink connection string using device parameter or FLIX_DEVICE env variable.
pyflix@0.16.
2026-05-28 18:22:16 +03:00
Oleg Kalachev a294883dea Make p command show all parameters starting with the arg 2026-05-27 13:58:51 +03:00
Oleg Kalachev cdfba72a0b Fix simulator run
Add missing extern variables.
Fix warning.
2026-05-27 11:03:50 +03:00
Oleg Kalachev 18e81720e0 Add video of pcb version flights to the readme 2026-05-26 14:23:56 +03:00
Oleg Kalachev 91173d06c9 Various minor changes 2026-05-22 08:03:46 +03:00
Oleg KalachevandGitHub fdcc9533b3 Implement ESP-NOW support (#40) 2026-05-21 10:48:31 +03:00
Oleg Kalachev bd2b1bd5de Improve voltage measurement
Apply PWM_VOLT_PIN without reboot.
Check if the voltage pin can be used with ADC when setting up.
Set voltage to NAN, when it's unknown (including pyflix).
pyflix@0.15.
Don't send BATTERY_STATUS when voltage is unknown.
Add dummy voltage to the simulator.
2026-05-18 00:30:42 +03:00
Oleg Kalachev 4530c05b5c Add Flix 1.5 to builds 2026-05-17 06:46:11 +03:00
Oleg Kalachev 3816ae376f Bring back initializing the lpf with the first input value
It's much better for voltage measuring and slightly better for gyro bias estimation.
2026-05-17 06:21:13 +03:00
Oleg Kalachev 72a72fde80 Some docs improvements 2026-05-14 21:59:15 +03:00
Oleg Kalachev e53051a349 Fix console command parsing 2026-05-13 06:00:51 +03:00
Oleg Kalachev f8a9f1f838 Remove reboot requirement when changing RC_RX_PIN 2026-05-13 01:27:01 +03:00
Oleg Kalachev 76af83fc88 Improve firmware overview article 2026-05-13 00:35:43 +03:00
Oleg Kalachev dd176180a7 Update help message
Remove 'welcome to', add copyright and repo link.
2026-05-12 17:50:28 +03:00
Oleg Kalachev 48c33c7050 Fix and improve wifi subsystem
Fix a fault when wifi is disabled (udp can't be used without wifi).
Print RSSI and channel in wifi command.
2026-05-10 22:39:34 +03:00
Oleg Kalachev 35e94f6ea6 Support ESP32-C3
User Serial1 for rc instead of Serial2, as ESP32-C3 has only 2 Serials.
Add CI build for ESP32-C3.
2026-05-10 21:07:20 +03:00
Oleg Kalachev 1f48e379e3 Improve the rc calibration code
More convenient steps order.
Improve the readability a bit.
2026-05-10 19:05:27 +03:00
Oleg Kalachev ee3c6999ab Add mavlink joystick app usage to the docs 2026-05-10 14:50:27 +03:00
Oleg Kalachev 34c6993842 Some fixes in the docs 2026-05-10 02:33:03 +03:00
Oleg Kalachev b62f2f9427 Rename some wifi parameters for better alphabetical sort 2026-05-09 17:55:55 +03:00
Oleg KalachevandMarina Tikhomirova 7dfef17165 Add Ina Tix' build
Co-authored-by: Marina Tikhomirova <Ina.tix@yandex.ru>
2026-05-09 17:49:59 +03:00
Oleg Kalachev 8c8046676b Simplify port definition in Makefile 2026-05-09 17:14:29 +03:00
Oleg Kalachev 702ec9792e Some updates in troubleshooting article 2026-05-09 15:57:25 +03:00
Oleg Kalachev 06e2047097 Make motor testing signal 0.2 instead of full power
Testing with full power is too dangerous and inconvenient.
2026-05-08 00:53:19 +03:00
Oleg Kalachev 87480476c2 Fix motors pwm frequency for esp32s3 etc in the docs 2026-05-08 00:47:29 +03:00
Oleg Kalachev 68271c508c Print firmware build date and time in sys command 2026-05-08 00:31:51 +03:00
Oleg Kalachev e81e84e7fc Some updates to docs 2026-05-07 20:15:55 +03:00
Oleg Kalachev 5f1a938d4f Fix imu rotation definition
The X axis should be pointing to the mounting holes, not pins side.
2026-05-07 19:54:16 +03:00
Oleg Kalachev bd270db493 Reduce angle drift by adding level correction to the estimator
Leverage a priori knowledge that the drone's average attitude is level.
Explanation: https://t.me/opensourcequadcopter/158.
2026-05-05 21:27:47 +03:00
Oleg Kalachev dbf24ea611 Expose lpf alpha of rate pids to parameters
Add parameters: CTL_R_RATE_D_A, CTL_P_RATE_D_A, CTL_Y_RATE_D_A.
2026-05-03 15:04:16 +03:00
Oleg Kalachev 08683d696d Some updates in the usage doc 2026-05-01 15:26:59 +03:00
Oleg Kalachev 9ca6841558 Exit auto mode when sticks moved only when mode switch is not configured 2026-04-29 15:07:44 +03:00
Oleg Kalachev 28da2d3c8e Fix in pyflix documentation
pyflix@0.14
2026-04-28 20:46:49 +03:00
Oleg Kalachev c6632ae6e4 Add info on setting flight modes using rc mode switch 2026-04-28 20:43:52 +03:00
Oleg Kalachev 35ca754583 Fix Vector::rotationVectorBetween implementation for parallel vectors 2026-04-28 15:38:52 +03:00
Oleg Kalachev 2ccda03573 Implement motors output desaturation
So the drone continues stabilization on max thrust.
2026-04-28 13:23:42 +03:00
Oleg Kalachev 485a39e740 Disable wi-fi power save to improve responsiveness 2026-04-27 16:46:36 +03:00
Oleg Kalachev 9bffe5b52f Some fixes in docs 2026-04-26 06:05:02 +03:00
Oleg Kalachev d6a79d6c66 Pass acc data in mG in SCALED_IMU to comply with mavlink standard
https://mavlink.io/en/messages/common.html#SCALED_IMU
pyflix@0.13
2026-04-24 07:42:39 +03:00
Oleg Kalachev 350a82bfed Minor fix 2026-04-23 15:34:54 +03:00
Oleg Kalachev 6e439859bc Move disabling brown-out code to power subsystem 2026-04-23 15:06:07 +03:00
Oleg Kalachev 835b2243e8 Minor fix in sys command
String works with printf %s, but actually it's a UB.
2026-04-23 07:25:59 +03:00
Oleg Kalachev ed4e2d87d1 Fix imu command output
Gyro field contained filtered gyro instead of scaled only gyro.
2026-04-23 07:12:25 +03:00
Oleg Kalachev 51cd5fc691 Implement battery voltage monitoring
Add power subsystem.
Add PWR_VOLT_PIN, PWR_VOLT_SCALE, PWR_VOLT_LPF_A parameters.
Support BATTERY_STATUS mavlink messages streaming.
Add pw cli command.
Add voltage field to pyflix library.
pyflix@0.12.
2026-04-22 11:35:37 +03:00
Oleg Kalachev d8591ea2a9 Fix working with parameters in pyflix examples
PITCH_P parameter was renamed to CTL_P_P
2026-04-18 05:23:47 +03:00
Oleg Kalachev c434107eaf Add parameter for configuring sbus pin number, disable sbus by default 2026-03-27 00:56:34 +03:00
Oleg Kalachev 814427dbfd Minor docs change 2026-03-27 00:40:19 +03:00
Oleg Kalachev 0730ceeffa Add new user builds 2026-02-21 07:12:36 +03:00
Oleg Kalachev a687303062 Make motor parameters apply without reboot
Add callback to parameter definition to call after parameter is changed.
2026-02-19 04:56:12 +03:00
Oleg Kalachev b2daf2587f Minor parameters code simplifications
readOnly is false by default
INFINITY == INFINITY, so remove redundant check
2026-02-19 02:59:38 +03:00
Oleg Kalachev a8c25d8ac0 Minor updates to usage article 2026-02-04 17:52:23 +03:00
Oleg Kalachev 3e49d41986 Make rc channel numbers and calibration params use int instead of float
As parameter subsystems supports int now, and int is much more natural here.
2026-02-02 20:36:22 +03:00
Oleg Kalachev 67430c7aac Several minor changes 2026-02-02 18:46:36 +03:00
Oleg Kalachev 3631743a29 Drop messages from another systems in pyflix
We shouldn't pass messages where system id != our system id. 
This change may be useful when there are many drones in one network.
2026-02-02 18:28:20 +03:00
Oleg Kalachev 3dde380bb7 Add parameters for list of modes bound to rc switch
Parameters: CTL_FLT_MODE_0, CTL_FLT_MODE_1, CTL_FLT_MODE_2.
Also fix a bug with incorrect choosing the mode from controlMode.
2026-01-27 16:38:20 +03:00
Oleg Kalachev 377b21429b Fix error when launching the sim
Also make the parameters WIFI_LOC_PORT and WIFI_REM_PORT work in the sim.
2026-01-27 16:32:52 +03:00
Oleg Kalachev 1ac443d6f8 Add a build by Arky Matsekh 2026-01-27 15:17:58 +03:00
Oleg Kalachev 964c0f7bc1 Make setting parameter in console printing actual parameter value.
In some cases, it would not be equal to the requested value.
2026-01-27 09:28:01 +03:00
Oleg Kalachev 40bdaacedb Make motor subsystem configurable using parameters
Motor pins: MOT_PIN_FL, MOT_PIN_FR, MOT_PIN_RL, MOT_PIN_RR.
PWM configuration: MOT_PWM_FREQ, MOT_PWM_RES, MOT_PWM_STOP, MOT_PWM_MIN, MOT_PWM_MAX.
MOT_PWM_MAX = -1 chooses duty cycle mode for brushed motors (default).
2026-01-27 08:40:52 +03:00
Oleg Kalachev 7d74f3d5cd Minor docs fixes 2026-01-27 07:21:21 +03:00
Oleg Kalachev 9fd35ba361 Simplify lpf filter code
Begin with zero instead of the initializing value, as the latter doesn't make much sense in practice, but complicates the code much.
2026-01-24 09:43:46 +03:00
Oleg Kalachev ca50f75576 Various minor fixes 2026-01-24 09:34:16 +03:00
Oleg Kalachev e47a31f981 Fix mavlink parameter set acknowledgement value
If the parameter is integer the acknowledgement should contain the rounded value.
2026-01-24 09:32:49 +03:00
Oleg Kalachev 7ad3022798 Add parameter for configuring gyro bias lpf
+ reset the filter on `reset` command
2026-01-24 09:31:32 +03:00
Oleg Kalachev 5b654e4d8e Update ESP32-Core to 3.3.6 2026-01-23 02:41:43 +03:00
Oleg Kalachev cf10ec6161 Update MAVLink-Arduino to 2.0.16 2026-01-23 01:11:35 +03:00
Oleg Kalachev 6d01cd2e79 Make failsafe configurable using parameters
SF_RC_LOSS_TIME - time without rc to activate failsafe.
SD_DESCEND_TIME - total time to decrease the throttle to zero.
Make controlTime nan on the start to simplify the logic.
2026-01-22 23:57:52 +03:00
Oleg Kalachev 0abb18c616 Make parameter names case-insensitive
+ minor fix
2026-01-22 23:11:47 +03:00
Oleg Kalachev 30326a5662 Add parameters for configuring the mavlink subsystem
MAV_SYS_ID - mavlink system id.
MAV_RATE_SLOW - rate of slow telemetry (e. g. heartbeats).
MAV_RATE_FAST - rate of fast telemetry (e. g. attitude, imu data).
2026-01-22 23:04:45 +03:00
Oleg Kalachev dd3575174b Add wifi configuration using parameters and cli
Add console commands to setup wifi.
Add a parameter for choosing between STA and AP mode.
Add parameters for udp ports.
Remove WIFI_ENABLED macro.
2026-01-22 22:58:43 +03:00
Oleg Kalachev c0f3301da4 Support integer parameters in addition to floats
The variable pointer is stored as a union field.
If `.integer` field is true, then integer pointer should be used.
Interfaces to parameters (cli and mavlink) keep working using floats.
Setting a non-finite value to int parameter will cause an error.
`.value` field is renamed to `.cache`.
2026-01-22 22:54:05 +03:00
a.golubtsovandOleg Kalachev a6bad3a69b Add log dir creation before log writing 2026-01-22 17:56:23 +03:00
Oleg Kalachev 9a9bd07251 Add correct attitude estimation video to the usage article 2026-01-15 23:46:23 +03:00
Oleg Kalachev 28f5855a57 Re-arrange control.ino declarations to make a bit more sensible
So the control command is above the PID controllers.
2026-01-13 17:43:53 +03:00
Oleg Kalachev 7e24ee30f7 Documentation and book updates
Improve the main list of features.
Use lowercase imu variable for consistency with the firmware code.
Minor fixes.
2026-01-13 17:26:40 +03:00
Oleg Kalachev 2a8faf5759 Fix logo svg slightly 2026-01-08 19:45:08 +03:00
Oleg Kalachev f4e58a652a Add project logo 2026-01-08 17:58:59 +03:00
Oleg Kalachev 6c46328da1 Minor doc fixes 2026-01-04 15:01:53 +03:00
Oleg Kalachev c8e5e08b03 Move all global variable declarations to the appropriate subsystems
As it makes the subsystems code easier to understand.
Declare the most used variables in main sketch file as forward declarations.
Make all control input zero by default (except controlMode).
Minor changes.
2026-01-03 13:28:18 +03:00
Oleg Kalachev a5e3dfcf69 Some updates to the docs 2026-01-03 12:18:47 +03:00
Oleg Kalachev d6e8be0c05 Add parameters for easier IMU orientation definition 2025-12-26 21:14:15 +03:00
Oleg Kalachev 68d16855df Add motors rotation diagram to usage article 2025-12-25 07:22:09 +03:00
Oleg Kalachev 0547ea548b Add parameters for acc weight and rates lpf alpha 2025-12-24 05:43:55 +03:00
Oleg Kalachev c02dba6812 Rename gyroCalibrationFilter to gyroBiasFilter
Which seems a better name
2025-12-24 05:36:43 +03:00
Oleg Kalachev f1dc4a0400 Updates to user builds article 2025-12-17 02:21:51 +03:00
Oleg Kalachev 158827ac55 Add new user builds, add school 548 course overview
+ minor doc fixes.
2025-12-13 21:09:33 +03:00
Oleg Kalachev 36ca30c3e4 Minor docs fix 2025-11-27 18:34:32 +03:00
Oleg Kalachev 48711b55e1 Add tip about CNT_TILT_MAX parameter to the simulator article 2025-11-26 17:34:08 +03:00
Oleg Kalachev 4d583185a9 Rename manual mode to raw mode
Make it callable from the console.
Increase the coefficient.
Corresponding change in pyflix.
pyflix@0.11.
2025-11-25 04:44:16 +03:00
Oleg Kalachev d757ffa853 Move yaw dead zone handling from mavlink to control subsystem
So yaw dead zone works the same for rc and mavlink.
2025-11-22 05:11:46 +03:00
Oleg Kalachev 5352386486 Minor updates to pyflix library, pyflix@0.10
Fixes to documentation.
Improve logger format.
2025-11-22 05:07:46 +03:00
Oleg Kalachev 9b5872740f Add wifi cli command
To show wi-fi info.
2025-11-22 04:46:54 +03:00
Oleg Kalachev 31dbdaf241 Group control parameters
Also add IMU group to accelerometer calibration parameters.
2025-11-19 01:50:46 +03:00
Oleg Kalachev f4b56262b1 Remove unneeded SERIAL_BAUDRATE define 2025-11-14 20:23:15 +03:00
Oleg Kalachev 49039f752d Refactor Wi-Fi log download
Use MAVLink LOG_REQUEST_DATA and LOG_DATA for download log instead of console.
Make Wi-Fi download default way of downloading the log.
Make `log` command only print the header and `log dump` dump the log.
2025-11-14 20:21:05 +03:00
Oleg Kalachev 348721acc9 Updates in documentation
Fixes, updates, new illustrations.
2025-11-10 20:16:14 +03:00
Oleg Kalachev 774144c430 Many updates to documentation
Updates to main readme.
Add much more info to usage article.
Move simulator building to simulation's readme.
Improve assembly article.
Many fixes.
Updates in diagrams.
2025-11-06 13:55:52 +03:00
Oleg Kalachev 0e6651ab82 Add Rate class for running the code at fixed rate 2025-11-06 13:41:33 +03:00
22 changed files with 179 additions and 448 deletions
+18 -11
View File
@@ -13,7 +13,7 @@ jobs:
env: env:
ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1 ARDUINO_SKETCH_ALWAYS_EXPORT_BINARIES: 1
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install Arduino CLI - name: Install Arduino CLI
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- name: Build firmware for ESP32 - name: Build firmware for ESP32
@@ -28,22 +28,20 @@ jobs:
run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc,PSRAM=opi EXTRA=--output-dir=flix/build/esp32.esp32.esp32s3.opi run: make BOARD=esp32:esp32:esp32s3:CDCOnBoot=cdc,PSRAM=opi EXTRA=--output-dir=flix/build/esp32.esp32.esp32s3.opi
- name: Build firmware for Flix2 - name: Build firmware for Flix2
run: make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc,PSRAM=opi EXTRA='--build-property "compiler.cpp.extra_flags=-DFLIX2" --output-dir=flix/build/esp32.esp32.flix2' run: make BOARD=esp32:esp32:esp32s3:FlashSize=4M,CDCOnBoot=cdc,PSRAM=opi EXTRA='--build-property "compiler.cpp.extra_flags=-DFLIX2" --output-dir=flix/build/esp32.esp32.flix2'
- name: Build firmware for STM32F4
run: |
arduino-cli core install STMicroelectronics:stm32 --additional-urls https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json
make BOARD=STMicroelectronics:stm32:GenF4
- name: Upload binaries - name: Upload binaries
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: firmware-binary name: firmware-binary
path: flix/build path: flix/build
- name: Build espnow-proxy
run: arduino-cli compile --fqbn esp32:esp32:esp32 tools/espnow-proxy
- name: Check c_cpp_properties.json - name: Check c_cpp_properties.json
run: tools/check_c_cpp_properties.py run: tools/check_c_cpp_properties.py
build_macos: build_macos:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install Arduino CLI - name: Install Arduino CLI
run: brew install arduino-cli run: brew install arduino-cli
- name: Build firmware - name: Build firmware
@@ -54,7 +52,7 @@ jobs:
build_windows: build_windows:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install Arduino CLI - name: Install Arduino CLI
run: choco install arduino-cli run: choco install arduino-cli
- name: Install Make - name: Install Make
@@ -75,7 +73,7 @@ jobs:
DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget build-essential cmake g++ pkg-config gnupg2 lsb-release sudo DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget build-essential cmake g++ pkg-config gnupg2 lsb-release sudo
- name: Install Arduino CLI - name: Install Arduino CLI
run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install Gazebo - name: Install Gazebo
run: | run: |
sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list' sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list'
@@ -86,7 +84,16 @@ jobs:
run: sudo apt-get install -y libsdl2-dev run: sudo apt-get install -y libsdl2-dev
- name: Build simulator - name: Build simulator
run: make build_simulator run: make build_simulator
- uses: actions/upload-artifact@v4 - name: Run simulator
env:
GAZEBO_MODEL_PATH: ${{ github.workspace }}/gazebo/models
GAZEBO_PLUGIN_PATH: ${{ github.workspace }}/gazebo/build
run: |
OUT=$(timeout -k 10s 120s gzserver --verbose gazebo/flix.world 2>&1 | tee /dev/stderr)
if echo "$OUT" | grep -Pq "\[Err\](?! \[RenderEngine)"; then
exit 1
fi
- uses: actions/upload-artifact@v7
with: with:
name: gazebo-plugin-binary name: gazebo-plugin-binary
path: gazebo/build/*.so path: gazebo/build/*.so
@@ -98,7 +105,7 @@ jobs:
steps: steps:
- name: Install Arduino CLI - name: Install Arduino CLI
run: brew install arduino-cli run: brew install arduino-cli
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Clean up python binaries # Workaround for https://github.com/actions/setup-python/issues/577 - name: Clean up python binaries # Workaround for https://github.com/actions/setup-python/issues/577
run: | run: |
rm -f /usr/local/bin/2to3* rm -f /usr/local/bin/2to3*
+12 -6
View File
@@ -16,7 +16,7 @@ jobs:
markdownlint: markdownlint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install markdownlint - name: Install markdownlint
run: npm install -g markdownlint-cli2 run: npm install -g markdownlint-cli2
- name: Run markdownlint - name: Run markdownlint
@@ -25,13 +25,16 @@ jobs:
build_book: build_book:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: markdownlint needs: markdownlint
env:
BINARIES: ${{ github.event_name == 'push' && (github.ref_name == 'master' || github.ref_name == 'dev') && github.repository == 'okalachev/flix' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install mdBook - name: Install mdBook
run: cargo install mdbook --vers 0.4.43 --locked run: cargo install mdbook --vers 0.4.43 --locked
- name: Build book - name: Build book
run: cd docs && mdbook build run: cd docs && mdbook build
- name: Wait for Build to complete - name: Wait for Build to complete
if: ${{ env.BINARIES }}
uses: lewagon/wait-on-check-action@v1.9.1 uses: lewagon/wait-on-check-action@v1.9.1
with: with:
ref: ${{ github.sha }} ref: ${{ github.sha }}
@@ -39,6 +42,7 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30 wait-interval: 30
- name: Find firmware binaries - name: Find firmware binaries
if: ${{ env.BINARIES }}
id: build_run id: build_run
run: | run: |
RUN_ID=$(gh api "repos/${{ github.repository }}/actions/workflows/build.yml/runs?head_sha=${{ github.sha }}&per_page=1" --jq '.workflow_runs[0].id') RUN_ID=$(gh api "repos/${{ github.repository }}/actions/workflows/build.yml/runs?head_sha=${{ github.sha }}&per_page=1" --jq '.workflow_runs[0].id')
@@ -46,7 +50,8 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download firmware binaries - name: Download firmware binaries
uses: actions/download-artifact@v4 if: ${{ env.BINARIES }}
uses: actions/download-artifact@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }} repository: ${{ github.repository }}
@@ -54,6 +59,7 @@ jobs:
name: firmware-binary name: firmware-binary
path: docs/build path: docs/build
- name: Create shortcuts for firmware binaries - name: Create shortcuts for firmware binaries
if: ${{ env.BINARIES }}
working-directory: docs/build working-directory: docs/build
run: | run: |
for FQBN in esp32.esp32.*; do for FQBN in esp32.esp32.*; do
@@ -64,12 +70,12 @@ jobs:
ln -s "$FQBN/flix.ino.bootloader.bin" "flix.$BOARD.bootloader.bin" ln -s "$FQBN/flix.ino.bootloader.bin" "flix.$BOARD.bootloader.bin"
done done
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v5
with: with:
path: docs/build path: docs/build
deploy: deploy:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} if: ${{ github.event_name == 'push' && github.ref_name == 'master' }}
concurrency: concurrency:
group: "pages" group: "pages"
cancel-in-progress: true cancel-in-progress: true
@@ -81,4 +87,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v5
+24 -4
View File
@@ -10,7 +10,7 @@ jobs:
csv_to_ulog: csv_to_ulog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Build csv_to_ulog - name: Build csv_to_ulog
run: cd tools/csv_to_ulog && mkdir build && cd build && cmake .. && make run: cd tools/csv_to_ulog && mkdir build && cd build && cmake .. && make
- name: Test csv_to_ulog - name: Test csv_to_ulog
@@ -22,13 +22,13 @@ jobs:
pyflix: pyflix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install Python build tools - name: Install Python build tools
run: pip install build run: pip install build
- name: Build pyflix - name: Build pyflix
run: python3 -m build tools run: python3 -m build tools
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: pyflix name: pyflix
path: | path: |
@@ -37,7 +37,7 @@ jobs:
python_tools: python_tools:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install Python dependencies - name: Install Python dependencies
run: pip install -r tools/requirements.txt run: pip install -r tools/requirements.txt
- name: Test csv_to_mcap tool - name: Test csv_to_mcap tool
@@ -46,3 +46,23 @@ jobs:
echo -e "t,x,y,z\n0,1,2,3\n1,4,5,6" > log.csv echo -e "t,x,y,z\n0,1,2,3\n1,4,5,6" > log.csv
./csv_to_mcap.py log.csv ./csv_to_mcap.py log.csv
test $(stat -c %s log.mcap) -eq 883 test $(stat -c %s log.mcap) -eq 883
sloc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: sudo apt-get install -y cloc jq
- name: Print source lines of code
run: cloc --by-file-by-lang flix
- name: Checkout previous revision
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
path: prev
- name: Annotate total source lines
run: |
SLOC_CURR=$(cloc flix --json | jq -r '.SUM.code')
SLOC_PREV=$(cloc prev/flix --json | jq -r '.SUM.code')
DIFF=$(printf '%+d' "$((SLOC_CURR - SLOC_PREV))")
echo "* Current SLOC: $SLOC_CURR" >> $GITHUB_STEP_SUMMARY
echo "* Previous SLOC: $SLOC_PREV" >> $GITHUB_STEP_SUMMARY
echo "* Diff: $DIFF" >> $GITHUB_STEP_SUMMARY
+4 -4
View File
@@ -4,10 +4,10 @@ PORT := $(strip $(wildcard /dev/serial/by-id/usb-Silicon_Labs_CP21* /dev/serial/
export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h export ARDUINO_NETWORK_CONNECTION_TIMEOUT := 1h
build: .core .libs build: .core .libs
arduino-cli compile --fqbn $(BOARD) --build-property "build.core_debug_level=1" flix $(EXTRA) arduino-cli compile flix --fqbn $(BOARD) --build-property "build.core_debug_level=1" $(EXTRA)
upload: build upload: build
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" flix arduino-cli upload flix --fqbn $(BOARD) -p "$(PORT)"
erase: erase:
arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool arduino-cli burn-bootloader --fqbn $(BOARD) -p "$(PORT)" -P esptool
@@ -27,8 +27,8 @@ libs .libs:
touch .libs touch .libs
upload_proxy: .core .libs upload_proxy: .core .libs
arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy arduino-cli compile tools/espnow-proxy --fqbn $(BOARD)
arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy arduino-cli upload tools/espnow-proxy --fqbn $(BOARD) -p "$(PORT)"
gazebo/build cmake: gazebo/CMakeLists.txt gazebo/build cmake: gazebo/CMakeLists.txt
mkdir -p gazebo/build mkdir -p gazebo/build
+1 -1
View File
@@ -31,7 +31,7 @@
* [`vector.h`](https://github.com/okalachev/flix/blob/master/flix/vector.h), [`quaternion.h`](https://github.com/okalachev/flix/blob/master/flix/quaternion.h) — библиотеки векторов и кватернионов. * [`vector.h`](https://github.com/okalachev/flix/blob/master/flix/vector.h), [`quaternion.h`](https://github.com/okalachev/flix/blob/master/flix/quaternion.h) — библиотеки векторов и кватернионов.
* [`pid.h`](https://github.com/okalachev/flix/blob/master/flix/pid.h) — ПИД-регулятор. * [`pid.h`](https://github.com/okalachev/flix/blob/master/flix/pid.h) — ПИД-регулятор.
* [`lpf.h`](https://github.com/okalachev/flix/blob/master/flix/lpf.h) — фильтр нижних частот. * [`filter.h`](https://github.com/okalachev/flix/blob/master/flix/filter.h) — фильтр нижних частот.
### Подсистема управления ### Подсистема управления
+1 -1
View File
@@ -34,7 +34,7 @@ Utility files:
* [`vector.h`](../flix/vector.h), [`quaternion.h`](../flix/quaternion.h) — vector and quaternion libraries. * [`vector.h`](../flix/vector.h), [`quaternion.h`](../flix/quaternion.h) — vector and quaternion libraries.
* [`pid.h`](../flix/pid.h) — generic PID controller. * [`pid.h`](../flix/pid.h) — generic PID controller.
* [`lpf.h`](../flix/lpf.h) — generic low-pass filter. * [`filter.h`](../flix/filter.h) — generic low-pass filter.
### Control subsystem ### Control subsystem
+1 -1
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;
+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"
const int RAW = 0, ACRO = 1, STAB = 2, AUTO = 3; // flight modes const int RAW = 0, ACRO = 1, STAB = 2, AUTO = 3; // flight modes
+1 -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
View File
+1 -1
View File
@@ -7,7 +7,7 @@
#include <Wire.h> #include <Wire.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"
IMU *imu; IMU *imu;
-9
View File
@@ -19,27 +19,18 @@ const int MOTOR_REAR_LEFT = 0, MOTOR_REAR_RIGHT = 1, MOTOR_FRONT_RIGHT = 2, MOTO
void setupMotors() { void setupMotors() {
print("Setup motors\n"); print("Setup motors\n");
// Configure pins // Configure pins
#ifdef ESP32
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (motorPins[i] < 0) continue; // skip unassigned motors if (motorPins[i] < 0) continue; // skip unassigned motors
ledcAttach(motorPins[i], pwmFrequency, pwmResolution); ledcAttach(motorPins[i], pwmFrequency, pwmResolution);
pwmFrequency = ledcChangeFrequency(motorPins[i], pwmFrequency, pwmResolution); // when reconfiguring pwmFrequency = ledcChangeFrequency(motorPins[i], pwmFrequency, pwmResolution); // when reconfiguring
} }
#else
analogWriteResolution(pwmResolution);
analogWriteFrequency(pwmFrequency);
#endif
sendMotors(); sendMotors();
} }
void sendMotors() { void sendMotors() {
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (motorPins[i] < 0) continue; // skip unassigned motors if (motorPins[i] < 0) continue; // skip unassigned motors
#ifdef ESP32
ledcWrite(motorPins[i], getDutyCycle(motors[i])); ledcWrite(motorPins[i], getDutyCycle(motors[i]));
#else
analogWrite(motorPins[i], getDutyCycle(motors[i]));
#endif
} }
} }
+5 -5
View File
@@ -3,7 +3,7 @@
// Parameters storage in flash memory // Parameters storage in flash memory
#include "prefs.h" #include <Preferences.h>
#include "util.h" #include "util.h"
extern int channelZero[16], channelMax[16]; extern int channelZero[16], channelMax[16];
@@ -22,7 +22,7 @@ struct Parameter {
const char *name; // max length is 15 const char *name; // max length is 15
bool integer; bool integer;
union { float *f; int *i; }; // pointer to the variable union { float *f; int *i; }; // pointer to the variable
float inital; // default value float initial; // default value
float cache; // what's stored in flash float cache; // what's stored in flash
void (*callback)(); // called after parameter change void (*callback)(); // called after parameter change
Parameter(const char *name, float *variable, void (*callback)() = nullptr) : name(name), integer(false), f(variable), callback(callback) {}; Parameter(const char *name, float *variable, void (*callback)() = nullptr) : name(name), integer(false), f(variable), callback(callback) {};
@@ -150,7 +150,7 @@ void setupParameters() {
storage.begin("flix"); storage.begin("flix");
// Read parameters from storage // Read parameters from storage
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
parameter.inital = parameter.getValue(); parameter.initial = parameter.getValue();
if (storage.isKey(parameter.name)) { if (storage.isKey(parameter.name)) {
parameter.setValue(storage.getFloat(parameter.name)); parameter.setValue(storage.getFloat(parameter.name));
} }
@@ -211,10 +211,10 @@ void printParameters(const char *filter) {
for (auto &parameter : parameters) { for (auto &parameter : parameters) {
if (strncasecmp(parameter.name, filter, strlen(filter))) continue; if (strncasecmp(parameter.name, filter, strlen(filter))) continue;
if (floatEquals(parameter.getValue(), parameter.inital)) { // parameter changed if (floatEquals(parameter.getValue(), parameter.initial)) { // parameter changed
print("%-15s %-13g\n", parameter.name, parameter.getValue()); print("%-15s %-13g\n", parameter.name, parameter.getValue());
} else { } else {
print("%-15s %-13g [%g]\n", parameter.name, parameter.getValue(), parameter.inital); print("%-15s %-13g [%g]\n", parameter.name, parameter.getValue(), parameter.initial);
} }
} }
} }
+2 -2
View File
@@ -5,7 +5,7 @@
#pragma once #pragma once
#include "lpf.h" #include "filter.h"
class PID { class PID {
public: public:
@@ -18,7 +18,7 @@ public:
LowPassFilter<float> lpf; // low pass filter for derivative term LowPassFilter<float> lpf; // low pass filter for derivative term
PID(float p, float i = 0, float d = 0, float windup = 0, float dAlpha = 1, float dtMax = 0.1) : PID(float p, float i = 0, float d = 0, float windup = INFINITY, float dAlpha = 1, float dtMax = 0.1) :
p(p), i(i), d(d), windup(windup), lpf(dAlpha), dtMax(dtMax) {} p(p), i(i), d(d), windup(windup), lpf(dAlpha), dtMax(dtMax) {}
float update(float error) { float update(float error) {
+1 -5
View File
@@ -3,11 +3,9 @@
// Power management // Power management
#ifdef ESP32
#include <soc/soc.h> #include <soc/soc.h>
#include <soc/rtc_cntl_reg.h> #include <soc/rtc_cntl_reg.h>
#endif #include "filter.h"
#include "lpf.h"
#include "util.h" #include "util.h"
float voltage = NAN; float voltage = NAN;
@@ -16,9 +14,7 @@ int voltagePin = -1;
float voltageScale = 2; float voltageScale = 2;
void setupPower() { void setupPower() {
#ifdef ESP32
REG_CLR_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_ENA); // disable reset on low voltage REG_CLR_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_ENA); // disable reset on low voltage
#endif
if (digitalPinToAnalogChannel(voltagePin) == -1) voltagePin = -1; // test ADC pin if (digitalPinToAnalogChannel(voltagePin) == -1) voltagePin = -1; // test ADC pin
} }
-283
View File
@@ -1,283 +0,0 @@
#ifdef ESP32
#include <Preferences.h>
#else
#include <Arduino.h>
#include <EEPROM.h>
#include <string.h>
class Preferences {
public:
Preferences() = default;
~Preferences() = default;
bool begin(const char *name, bool readOnly = false, const char *partition_label = NULL) {
(void)name;
(void)readOnly;
(void)partition_label;
load();
started = true;
return true;
}
void end() {
started = false;
}
bool clear() {
if (!started) return false;
for (auto &entry : entries) {
entry = Entry();
}
return save();
}
size_t putFloat(const char *key, float value) {
if (!started) return 0;
int index = ensureKey(key);
if (index < 0) return 0;
entries[index].hasFloat = true;
entries[index].hasString = false;
entries[index].stringValue = "";
entries[index].floatValue = value;
if (!save()) return 0;
return sizeof(float);
}
float getFloat(const char *key, float defaultValue = NAN) {
if (!started) return defaultValue;
int index = findKey(key);
if (index < 0 || !entries[index].hasFloat) return defaultValue;
return entries[index].floatValue;
}
size_t putString(const char *key, const char *value) {
if (!started) return 0;
int index = ensureKey(key);
if (index < 0) return 0;
if (!value) value = "";
if (strlen(value) > MAX_STRING_LEN) return 0;
entries[index].hasString = true;
entries[index].hasFloat = false;
entries[index].stringValue = value;
if (!save()) return 0;
return entries[index].stringValue.length();
}
size_t putString(const char *key, String value) {
return putString(key, value.c_str());
}
String getString(const char *key, String defaultValue = String()) {
if (!started) return defaultValue;
int index = findKey(key);
if (index < 0 || !entries[index].hasString) return defaultValue;
return entries[index].stringValue;
}
bool isKey(const char *key) {
if (!started) return false;
return findKey(key) >= 0;
}
private:
static const int MAX_ENTRIES = 128;
static const int MAX_KEY_LEN = 15;
static const int MAX_STRING_LEN = 95;
static const uint32_t MAGIC = 0x46504B56; // "VKPF"
static const uint8_t VERSION = 1;
static const uint8_t TYPE_FLOAT = 1;
static const uint8_t TYPE_STRING = 2;
static const int STORAGE_SIZE = 4096;
struct Entry {
bool used = false;
char key[MAX_KEY_LEN + 1] = {};
bool hasFloat = false;
float floatValue = NAN;
bool hasString = false;
String stringValue;
};
bool started = false;
Entry entries[MAX_ENTRIES];
static void writeU16(uint8_t *dst, uint16_t value) {
dst[0] = static_cast<uint8_t>(value & 0xFF);
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
}
static void writeU32(uint8_t *dst, uint32_t value) {
dst[0] = static_cast<uint8_t>(value & 0xFF);
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
dst[2] = static_cast<uint8_t>((value >> 16) & 0xFF);
dst[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
}
static uint16_t readU16(const uint8_t *src) {
return static_cast<uint16_t>(src[0]) |
(static_cast<uint16_t>(src[1]) << 8);
}
static uint32_t readU32(const uint8_t *src) {
return static_cast<uint32_t>(src[0]) |
(static_cast<uint32_t>(src[1]) << 8) |
(static_cast<uint32_t>(src[2]) << 16) |
(static_cast<uint32_t>(src[3]) << 24);
}
int availableStorageSize() {
int n = STORAGE_SIZE;
if (EEPROM.length() > 0 && EEPROM.length() < n) n = EEPROM.length();
return n;
}
bool save() {
const int storageSize = availableStorageSize();
if (storageSize < 16) return false;
uint8_t buffer[STORAGE_SIZE] = {};
int pos = 0;
writeU32(buffer + pos, MAGIC);
pos += 4;
buffer[pos++] = VERSION;
buffer[pos++] = 0;
int lengthPos = pos;
pos += 2;
for (int i = 0; i < MAX_ENTRIES; i++) {
if (!entries[i].used) continue;
if (!entries[i].hasFloat && !entries[i].hasString) continue;
const uint8_t keyLen = static_cast<uint8_t>(strnlen(entries[i].key, MAX_KEY_LEN));
if (keyLen == 0) continue;
if (entries[i].hasFloat) {
const int recordSize = 3 + keyLen + 4;
if (pos + recordSize > storageSize) return false;
buffer[pos++] = TYPE_FLOAT;
buffer[pos++] = keyLen;
buffer[pos++] = 4;
memcpy(buffer + pos, entries[i].key, keyLen);
pos += keyLen;
float value = entries[i].floatValue;
memcpy(buffer + pos, &value, sizeof(value));
pos += sizeof(value);
} else if (entries[i].hasString) {
const uint8_t valueLen = static_cast<uint8_t>(entries[i].stringValue.length());
const int recordSize = 3 + keyLen + valueLen;
if (pos + recordSize > storageSize) return false;
buffer[pos++] = TYPE_STRING;
buffer[pos++] = keyLen;
buffer[pos++] = valueLen;
memcpy(buffer + pos, entries[i].key, keyLen);
pos += keyLen;
if (valueLen > 0) {
memcpy(buffer + pos, entries[i].stringValue.c_str(), valueLen);
pos += valueLen;
}
}
}
writeU16(buffer + lengthPos, static_cast<uint16_t>(pos));
for (int i = 0; i < storageSize; i++) {
EEPROM.write(i, buffer[i]);
}
EEPROM.commit();
return true;
}
void load() {
const int storageSize = availableStorageSize();
if (storageSize < 16) return;
for (auto &entry : entries) entry = Entry();
uint8_t buffer[STORAGE_SIZE] = {};
for (int i = 0; i < storageSize; i++) {
buffer[i] = EEPROM.read(i);
}
int pos = 0;
if (readU32(buffer + pos) != MAGIC) return;
pos += 4;
if (buffer[pos++] != VERSION) return;
pos++; // flags
const uint16_t totalLen = readU16(buffer + pos);
pos += 2;
if (totalLen < pos || totalLen > storageSize) return;
while (pos + 3 <= totalLen) {
const uint8_t type = buffer[pos++];
const uint8_t keyLen = buffer[pos++];
const uint8_t valueLen = buffer[pos++];
if (keyLen == 0 || keyLen > MAX_KEY_LEN) return;
if (pos + keyLen + valueLen > totalLen) return;
char key[MAX_KEY_LEN + 1] = {};
memcpy(key, buffer + pos, keyLen);
key[keyLen] = '\0';
pos += keyLen;
int index = ensureKey(key);
if (index < 0) return;
if (type == TYPE_FLOAT && valueLen == 4) {
float value = NAN;
memcpy(&value, buffer + pos, sizeof(value));
entries[index].hasFloat = true;
entries[index].hasString = false;
entries[index].stringValue = "";
entries[index].floatValue = value;
} else if (type == TYPE_STRING && valueLen <= MAX_STRING_LEN) {
char value[MAX_STRING_LEN + 1] = {};
if (valueLen > 0) memcpy(value, buffer + pos, valueLen);
value[valueLen] = '\0';
entries[index].hasString = true;
entries[index].hasFloat = false;
entries[index].stringValue = value;
}
pos += valueLen;
}
}
int findKey(const char *key) {
if (!key) return -1;
for (int i = 0; i < MAX_ENTRIES; i++) {
if (!entries[i].used) continue;
if (strncmp(entries[i].key, key, MAX_KEY_LEN + 1) == 0) return i;
}
return -1;
}
int ensureKey(const char *key) {
if (!key) return -1;
if (strlen(key) > MAX_KEY_LEN) return -1;
int index = findKey(key);
if (index >= 0) return index;
for (int i = 0; i < MAX_ENTRIES; i++) {
if (entries[i].used) continue;
entries[i].used = true;
entries[i].hasFloat = false;
entries[i].hasString = false;
entries[i].floatValue = NAN;
entries[i].stringValue = "";
strncpy(entries[i].key, key, MAX_KEY_LEN);
entries[i].key[MAX_KEY_LEN] = '\0';
return i;
}
return -1;
}
};
#endif
-6
View File
@@ -6,11 +6,7 @@
#pragma once #pragma once
#include <math.h> #include <math.h>
#ifdef ESP32
#include <soc/soc.h>
#include <soc/rtc_cntl_reg.h>
#include <ESP32_NOW_Serial.h> #include <ESP32_NOW_Serial.h>
#endif
const float ONE_G = 9.80665; const float ONE_G = 9.80665;
extern float t; extern float t;
@@ -57,7 +53,6 @@ void splitString(String& str, String& token0, String& token1, String& token2) {
if (token2.c_str() == NULL) token2 = ""; if (token2.c_str() == NULL) token2 = "";
} }
#ifdef ESP32
// Simplified ESP-NOW Serial without resends // Simplified ESP-NOW Serial without resends
class ESPNOWSerial : public ESP_NOW_Serial_Class { class ESPNOWSerial : public ESP_NOW_Serial_Class {
public: public:
@@ -68,7 +63,6 @@ public:
ESP_NOW_Serial_Class::onSent(true); // always report success to avoid resends ESP_NOW_Serial_Class::onSent(true); // always report success to avoid resends
} }
}; };
#endif
// Rate limiter // Rate limiter
class Rate { class Rate {
+96 -97
View File
@@ -3,13 +3,13 @@
// Wi-Fi and ESP-NOW communication // Wi-Fi and ESP-NOW communication
// #include <WiFi.h> #include <WiFi.h>
// #include <WiFiAP.h> #include <WiFiAP.h>
// #include <WiFiUdp.h> #include <WiFiUdp.h>
// #include <MacAddress.h> #include <MacAddress.h>
// #include <ESP32_NOW_Serial.h> #include <ESP32_NOW_Serial.h>
// #include "prefs.h" #include <Preferences.h>
// #include "util.h" #include "util.h"
extern Preferences storage; // use the main preferences storage extern Preferences storage; // use the main preferences storage
@@ -20,121 +20,120 @@ int wifiLongRange = 0;
int wifiBroadcast = 0; // 0 - broadcast until connected, 1 - always broadcast int wifiBroadcast = 0; // 0 - broadcast until connected, 1 - always broadcast
int udpLocalPort = 14550; int udpLocalPort = 14550;
int udpRemotePort = 14550; int udpRemotePort = 14550;
// IPAddress udpRemoteIP = "255.255.255.255"; IPAddress udpRemoteIP = "255.255.255.255";
// WiFiUDP udp; WiFiUDP udp;
// ESPNOWSerial espnow(NULL, 0, WIFI_IF_AP); ESPNOWSerial espnow(NULL, 0, WIFI_IF_AP);
// ESPNOWSerial espnowBroadcast(ESP_NOW.BROADCAST_ADDR, 0, WIFI_IF_AP); ESPNOWSerial espnowBroadcast(ESP_NOW.BROADCAST_ADDR, 0, WIFI_IF_AP);
int espnowChannel = 6; int espnowChannel = 6;
void setupWiFi() { void setupWiFi() {
// print("Setup Wi-Fi\n"); print("Setup Wi-Fi\n");
// WiFi.enableLongRange(wifiLongRange); WiFi.enableLongRange(wifiLongRange);
// if (wifiMode == W_AP) { if (wifiMode == W_AP) {
// WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str()); WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str());
// udp.begin(udpLocalPort); udp.begin(udpLocalPort);
// } }
// if (wifiMode == W_STA) { if (wifiMode == W_STA) {
// WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str()); WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str());
// udp.begin(udpLocalPort); udp.begin(udpLocalPort);
// } }
// if (wifiMode == W_ESPNOW) { if (wifiMode == W_ESPNOW) {
// WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
// WiFi.setChannel(espnowChannel); WiFi.setChannel(espnowChannel);
// espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str())); espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str()));
// String key = storage.getString("ESPNOW_PEER_KEY", ""); String key = storage.getString("ESPNOW_PEER_KEY", "");
// espnow.setKey(key.isEmpty() ? nullptr : (const uint8_t *)key.c_str()); espnow.setKey(key.isEmpty() ? nullptr : (const uint8_t *)key.c_str());
// espnow.begin(); espnow.begin();
// espnowBroadcast.begin(); espnowBroadcast.begin();
// } }
// WiFi.setSleep(false); // disable power save WiFi.setSleep(false); // disable power save
} }
void sendWiFi(const uint8_t *buf, int len) { void sendWiFi(const uint8_t *buf, int len) {
// if (espnow) { if (espnow) {
// espnow.write(buf, len); espnow.write(buf, len);
// static Rate discovery(2); static Rate discovery(2);
// if (espnow.isEncrypted() && discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device if (espnow.isEncrypted() && discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device
// return; return;
// } }
// if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return; if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return;
// bool broadcast = wifiBroadcast || !(t - mavlinkTime < 5); // broadcast if lost connection bool broadcast = wifiBroadcast || !(t - mavlinkTime < 5); // broadcast if lost connection
// udp.beginPacket(broadcast ? IPAddress(255, 255, 255, 255) : udpRemoteIP, udpRemotePort); udp.beginPacket(broadcast ? IPAddress(255, 255, 255, 255) : udpRemoteIP, udpRemotePort);
// udp.write(buf, len); udp.write(buf, len);
// udp.endPacket(); udp.endPacket();
} }
int receiveWiFi(uint8_t *buf, int len) { int receiveWiFi(uint8_t *buf, int len) {
// if (espnow) { if (espnow) {
// return espnow.read(buf, len); return espnow.read(buf, len);
// } }
// if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return 0; if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return 0;
// udp.parsePacket(); udp.parsePacket();
// if (udp.remoteIP()) udpRemoteIP = udp.remoteIP(); if (udp.remoteIP()) udpRemoteIP = udp.remoteIP();
// return udp.read(buf, len); return udp.read(buf, len);
return 0;
} }
void printWiFiInfo() { void printWiFiInfo() {
// if (espnow) { if (espnow) {
// print("Mode: ESP-NOW\n"); print("Mode: ESP-NOW\n");
// print("ESP-NOW version: %d\n", ESP_NOW.getVersion()); print("ESP-NOW version: %d\n", ESP_NOW.getVersion());
// print("Max packet size: %d\n", ESP_NOW.getMaxDataLen()); print("Max packet size: %d\n", ESP_NOW.getMaxDataLen());
// print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); print("MAC: %s\n", WiFi.softAPmacAddress().c_str());
// print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str()); print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str());
// print("Encrypted: %d\n", espnow.isEncrypted()); print("Encrypted: %d\n", espnow.isEncrypted());
// print("Channel: %d\n", espnow.getChannel()); print("Channel: %d\n", espnow.getChannel());
// print("Lost packets: %d\n", espnow.lost); print("Lost packets: %d\n", espnow.lost);
// } else if (WiFi.getMode() == WIFI_MODE_AP) { } else if (WiFi.getMode() == WIFI_MODE_AP) {
// print("Mode: Access Point (AP)\n"); print("Mode: Access Point (AP)\n");
// print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); print("MAC: %s\n", WiFi.softAPmacAddress().c_str());
// print("SSID: %s\n", WiFi.softAPSSID().c_str()); print("SSID: %s\n", WiFi.softAPSSID().c_str());
// print("Password: ***\n"); print("Password: ***\n");
// print("Channel: %d\n", WiFi.channel()); print("Channel: %d\n", WiFi.channel());
// print("Clients: %d\n", WiFi.softAPgetStationNum()); print("Clients: %d\n", WiFi.softAPgetStationNum());
// print("IP: %s\n", WiFi.softAPIP().toString().c_str()); print("IP: %s\n", WiFi.softAPIP().toString().c_str());
// print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); print("Remote IP: %s\n", udpRemoteIP.toString().c_str());
// } else if (WiFi.getMode() == WIFI_MODE_STA) { } else if (WiFi.getMode() == WIFI_MODE_STA) {
// print("Mode: Client (STA)\n"); print("Mode: Client (STA)\n");
// print("Connected: %d\n", WiFi.isConnected()); print("Connected: %d\n", WiFi.isConnected());
// print("MAC: %s\n", WiFi.macAddress().c_str()); print("MAC: %s\n", WiFi.macAddress().c_str());
// print("SSID: %s\n", WiFi.SSID().c_str()); print("SSID: %s\n", WiFi.SSID().c_str());
// print("Password: ***\n"); print("Password: ***\n");
// print("Channel: %d\n", WiFi.channel()); print("Channel: %d\n", WiFi.channel());
// print("RSSI: %d dBm\n", WiFi.RSSI()); print("RSSI: %d dBm\n", WiFi.RSSI());
// print("IP: %s\n", WiFi.localIP().toString().c_str()); print("IP: %s\n", WiFi.localIP().toString().c_str());
// print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); print("Remote IP: %s\n", udpRemoteIP.toString().c_str());
// } else { } else {
// print("Mode: Disabled\n"); print("Mode: Disabled\n");
// } }
// print("MAVLink connected: %d\n", valid(mavlinkTime)); print("MAVLink connected: %d\n", valid(mavlinkTime));
} }
void configWiFi(int mode, const char *first, const char *second) { void configWiFi(int mode, const char *first, const char *second) {
// MacAddress mac; MacAddress mac;
// if (mode == W_AP && strlen(first) > 0 && strlen(second) >= 8) { if (mode == W_AP && strlen(first) > 0 && strlen(second) >= 8) {
// storage.putString("WIFI_AP_SSID", first); storage.putString("WIFI_AP_SSID", first);
// storage.putString("WIFI_AP_PASS", second); storage.putString("WIFI_AP_PASS", second);
// } else if (mode == W_STA && strlen(first) > 0 && strlen(second) >= 8) { } else if (mode == W_STA && strlen(first) > 0 && strlen(second) >= 8) {
// storage.putString("WIFI_STA_SSID", first); storage.putString("WIFI_STA_SSID", first);
// storage.putString("WIFI_STA_PASS", second); storage.putString("WIFI_STA_PASS", second);
// } else if (mode == W_ESPNOW && mac.fromString(first)) { } else if (mode == W_ESPNOW && mac.fromString(first)) {
// storage.putString("ESPNOW_PEER_MAC", first); storage.putString("ESPNOW_PEER_MAC", first);
// storage.putString("ESPNOW_PEER_KEY", strlen(second) == ESP_NOW_KEY_LEN ? second : ""); storage.putString("ESPNOW_PEER_KEY", strlen(second) == ESP_NOW_KEY_LEN ? second : "");
// } else { } else {
// print("Invalid configuration\n"); print("Invalid configuration\n");
// return; return;
// } }
// print("✓ Reboot to apply new settings\n"); print("✓ Reboot to apply new settings\n");
} }
void setWiFiMode(const String& mode) { void setWiFiMode(const String& mode) {
+1 -1
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;
+1 -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"
+1
View File
@@ -14,6 +14,7 @@
// Mocks // Mocks
int wifiMode = 1; int wifiMode = 1;
int wifiLongRange = 0; int wifiLongRange = 0;
int wifiBroadcast = 0;
int espnowChannel = 6; int espnowChannel = 6;
const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3; const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3;
+8 -8
View File
@@ -243,7 +243,7 @@ class Flix:
time.sleep(1) time.sleep(1)
@staticmethod @staticmethod
def _mavlink_to_flu(v: List[float]) -> List[float]: def _mavlink_to_flu(v: Sequence[float]) -> List[float]:
if len(v) == 3: # vector if len(v) == 3: # vector
return [v[0], -v[1], -v[2]] return [v[0], -v[1], -v[2]]
elif len(v) == 4: # quaternion elif len(v) == 4: # quaternion
@@ -252,8 +252,8 @@ class Flix:
raise ValueError(f'List must have 3 (vector) or 4 (quaternion) elements') raise ValueError(f'List must have 3 (vector) or 4 (quaternion) elements')
@staticmethod @staticmethod
def _flu_to_mavlink(v: List[float]) -> List[float]: def _flu_to_mavlink(v: Sequence[float]) -> List[float]:
return Flix._mavlink_to_flu(v) return Flix._mavlink_to_flu(v) # flu to mavlink is the same as mavlink to flu
def _command_send(self, command: int, params: Sequence[float]): def _command_send(self, command: int, params: Sequence[float]):
if len(params) != 7: if len(params) != 7:
@@ -320,13 +320,13 @@ class Flix:
def set_armed(self, armed: bool): def set_armed(self, armed: bool):
self._command_send(mavlink.MAV_CMD_COMPONENT_ARM_DISARM, (1 if armed else 0, 0, 0, 0, 0, 0, 0)) self._command_send(mavlink.MAV_CMD_COMPONENT_ARM_DISARM, (1 if armed else 0, 0, 0, 0, 0, 0, 0))
def set_position(self, position: List[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1): def set_position(self, position: Sequence[float], yaw: Optional[float] = None, wait: bool = False, tolerance: float = 0.1):
raise NotImplementedError('Position control is not implemented yet') raise NotImplementedError('Position control is not implemented yet')
def set_velocity(self, velocity: List[float], yaw: Optional[float] = None): def set_velocity(self, velocity: Sequence[float], yaw: Optional[float] = None):
raise NotImplementedError('Velocity control is not implemented yet') raise NotImplementedError('Velocity control is not implemented yet')
def set_attitude(self, attitude: List[float], thrust: float): def set_attitude(self, attitude: Sequence[float], thrust: float):
if len(attitude) == 3: if len(attitude) == 3:
attitude = Quaternion([attitude[0], attitude[1], attitude[2]]).q # type: ignore attitude = Quaternion([attitude[0], attitude[1], attitude[2]]).q # type: ignore
elif len(attitude) != 4: elif len(attitude) != 4:
@@ -339,7 +339,7 @@ class Flix:
[attitude[0], attitude[1], attitude[2], attitude[3]], [attitude[0], attitude[1], attitude[2], attitude[3]],
0, 0, 0, thrust) 0, 0, 0, thrust)
def set_rates(self, rates: List[float], thrust: float): def set_rates(self, rates: Sequence[float], thrust: float):
if len(rates) != 3: if len(rates) != 3:
raise ValueError('Rates must be [roll_rate, pitch_rate, yaw_rate]') raise ValueError('Rates must be [roll_rate, pitch_rate, yaw_rate]')
if not (0 <= thrust <= 1): if not (0 <= thrust <= 1):
@@ -351,7 +351,7 @@ class Flix:
[1, 0, 0, 0], [1, 0, 0, 0],
rates[0], rates[1], rates[2], thrust) rates[0], rates[1], rates[2], thrust)
def set_motors(self, motors: List[float]): def set_motors(self, motors: Sequence[float]):
if len(motors) != 4: if len(motors) != 4:
raise ValueError('motors must have 4 values') raise ValueError('motors must have 4 values')
if not all(0 <= m <= 1 for m in motors): if not all(0 <= m <= 1 for m in motors):