From bdfdcfab2d055d583177ffee6d52b8dc5adc4142 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Mon, 9 Mar 2026 20:40:04 +1100 Subject: [PATCH 01/99] Tools: autotest: deduplicate ExternalAHRS mission into Generic_Missions All EAHRS plane tests (VectorNav, MicroStrain5, MicroStrain7, InertialLabs) used identical per-test ap1.txt mission files (SHA256 0289f48fb154...). Replace them with a single Generic_Missions/externalahrs.txt and add a fly_generic_mission() helper to arduplane.py that loads from that directory. fly_external_AHRS() no longer takes a mission argument. Co-Authored-By: Claude Sonnet 4.6 --- .../ArduPlane_Tests/MicroStrainEAHRS5/ap1.txt | 7 ------- .../ArduPlane_Tests/MicroStrainEAHRS7/ap1.txt | 7 ------- .../ArduPlane_Tests/VectorNavEAHRS/ap1.txt | 7 ------- .../externalahrs.txt} | 0 Tools/autotest/arduplane.py | 18 ++++++++++++------ 5 files changed, 12 insertions(+), 27 deletions(-) delete mode 100644 Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS5/ap1.txt delete mode 100644 Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS7/ap1.txt delete mode 100644 Tools/autotest/ArduPlane_Tests/VectorNavEAHRS/ap1.txt rename Tools/autotest/{ArduPlane_Tests/InertialLabsEAHRS/ap1.txt => Generic_Missions/externalahrs.txt} (100%) diff --git a/Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS5/ap1.txt b/Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS5/ap1.txt deleted file mode 100644 index ab2be0d1a45470..00000000000000 --- a/Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS5/ap1.txt +++ /dev/null @@ -1,7 +0,0 @@ -QGC WPL 110 -0 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363262 149.165237 584.090027 1 -1 0 3 22 0.000000 0.000000 0.000000 0.000000 -35.361553 149.163956 20.000000 1 -2 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.364540 149.162857 50.000000 1 -3 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.367333 149.163164 28.000000 1 -4 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.366814 149.165878 28.000000 1 -5 0 3 21 0.000000 0.000000 0.000000 1.000000 -35.362947 149.165179 0.000000 1 diff --git a/Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS7/ap1.txt b/Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS7/ap1.txt deleted file mode 100644 index ab2be0d1a45470..00000000000000 --- a/Tools/autotest/ArduPlane_Tests/MicroStrainEAHRS7/ap1.txt +++ /dev/null @@ -1,7 +0,0 @@ -QGC WPL 110 -0 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363262 149.165237 584.090027 1 -1 0 3 22 0.000000 0.000000 0.000000 0.000000 -35.361553 149.163956 20.000000 1 -2 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.364540 149.162857 50.000000 1 -3 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.367333 149.163164 28.000000 1 -4 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.366814 149.165878 28.000000 1 -5 0 3 21 0.000000 0.000000 0.000000 1.000000 -35.362947 149.165179 0.000000 1 diff --git a/Tools/autotest/ArduPlane_Tests/VectorNavEAHRS/ap1.txt b/Tools/autotest/ArduPlane_Tests/VectorNavEAHRS/ap1.txt deleted file mode 100644 index ab2be0d1a45470..00000000000000 --- a/Tools/autotest/ArduPlane_Tests/VectorNavEAHRS/ap1.txt +++ /dev/null @@ -1,7 +0,0 @@ -QGC WPL 110 -0 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363262 149.165237 584.090027 1 -1 0 3 22 0.000000 0.000000 0.000000 0.000000 -35.361553 149.163956 20.000000 1 -2 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.364540 149.162857 50.000000 1 -3 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.367333 149.163164 28.000000 1 -4 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.366814 149.165878 28.000000 1 -5 0 3 21 0.000000 0.000000 0.000000 1.000000 -35.362947 149.165179 0.000000 1 diff --git a/Tools/autotest/ArduPlane_Tests/InertialLabsEAHRS/ap1.txt b/Tools/autotest/Generic_Missions/externalahrs.txt similarity index 100% rename from Tools/autotest/ArduPlane_Tests/InertialLabsEAHRS/ap1.txt rename to Tools/autotest/Generic_Missions/externalahrs.txt diff --git a/Tools/autotest/arduplane.py b/Tools/autotest/arduplane.py index 2d6fde18c644f4..ef617bba20a26a 100644 --- a/Tools/autotest/arduplane.py +++ b/Tools/autotest/arduplane.py @@ -3010,7 +3010,13 @@ def TerrainLoiterToCircle(self): self.wait_altitude(alt*0.9, alt*1.1, minimum_duration=10, relative=True) self.fly_home_land_and_disarm() - def fly_external_AHRS(self, sim, eahrs_type, mission): + def fly_generic_mission(self, filename, mission_timeout=60.0, strict=True): + """Fly a mission from the Generic_Missions directory.""" + self.progress("Flying generic mission %s" % filename) + num_wp = self.load_generic_mission(filename, strict=strict) - 1 + self.fly_mission_waypoints(num_wp, mission_timeout=mission_timeout) + + def fly_external_AHRS(self, sim, eahrs_type): """Fly with external AHRS""" self.customise_SITL_commandline(["--serial4=sim:%s" % sim]) @@ -3033,7 +3039,7 @@ def fly_external_AHRS(self, sim, eahrs_type, mission): self.wait_ready_to_arm() self.arm_vehicle() - self.fly_mission(mission) + self.fly_generic_mission("externalahrs.txt") def wait_and_maintain_wind_estimate( self, @@ -3232,19 +3238,19 @@ def test_replay_wind_and_airspeed_bit(self): def VectorNavEAHRS(self): '''Test VectorNav EAHRS support''' - self.fly_external_AHRS("VectorNav", 1, "ap1.txt") + self.fly_external_AHRS("VectorNav", 1) def MicroStrainEAHRS5(self): '''Test MicroStrain EAHRS series 5 support''' - self.fly_external_AHRS("MicroStrain5", 2, "ap1.txt") + self.fly_external_AHRS("MicroStrain5", 2) def MicroStrainEAHRS7(self): '''Test MicroStrain EAHRS series 7 support''' - self.fly_external_AHRS("MicroStrain7", 7, "ap1.txt") + self.fly_external_AHRS("MicroStrain7", 7) def InertialLabsEAHRS(self): '''Test InertialLabs EAHRS support''' - self.fly_external_AHRS("ILabs", 5, "ap1.txt") + self.fly_external_AHRS("ILabs", 5) def GpsSensorPreArmEAHRS(self): '''Test pre-arm checks related to EAHRS_SENSORS using the MicroStrain7 driver''' From 0a9de83842154d6cfbcb65ae5cb9428a8d6b7b36 Mon Sep 17 00:00:00 2001 From: LoyalLinjy Date: Tue, 10 Mar 2026 14:10:31 +0800 Subject: [PATCH 02/99] AP_Filesystem_FlashMemory_LittleFS:Add support for ZB25VQ128Q --- libraries/AP_Filesystem/AP_Filesystem_FlashMemory_LittleFS.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/AP_Filesystem/AP_Filesystem_FlashMemory_LittleFS.cpp b/libraries/AP_Filesystem/AP_Filesystem_FlashMemory_LittleFS.cpp index ab5d5675e19cad..a17f73a224b5cb 100644 --- a/libraries/AP_Filesystem/AP_Filesystem_FlashMemory_LittleFS.cpp +++ b/libraries/AP_Filesystem/AP_Filesystem_FlashMemory_LittleFS.cpp @@ -618,6 +618,7 @@ void AP_Filesystem_FlashMemory_LittleFS::mark_dead() #define JEDEC_ID_CYPRESS_S25FL064L 0x016017 #define JEDEC_ID_CYPRESS_S25FL128L 0x016018 #define JEDEC_ID_GIGA_GD25Q16E 0xC84015 +#define JEDEC_ID_ZBIT_ZB25VQ128 0x5E4018 /* Hardware-specific constants */ @@ -788,6 +789,7 @@ uint32_t AP_Filesystem_FlashMemory_LittleFS::find_block_size_and_count() { case JEDEC_ID_WINBOND_W25Q128: case JEDEC_ID_WINBOND_W25Q128_2: case JEDEC_ID_CYPRESS_S25FL128L: + case JEDEC_ID_ZBIT_ZB25VQ128: block_count = 256; /* 16MiB */ break; From 6a5d0d1ab52da020c94c6ad2dcd2af1ab891e5e9 Mon Sep 17 00:00:00 2001 From: LoyalLinjy Date: Tue, 10 Mar 2026 14:12:49 +0800 Subject: [PATCH 03/99] AP_Logger: Add support for ZB25VQ128Q --- libraries/AP_Logger/AP_Logger_Flash_JEDEC.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/AP_Logger/AP_Logger_Flash_JEDEC.cpp b/libraries/AP_Logger/AP_Logger_Flash_JEDEC.cpp index 5f833f6f13d837..025bbdee9063a2 100644 --- a/libraries/AP_Logger/AP_Logger_Flash_JEDEC.cpp +++ b/libraries/AP_Logger/AP_Logger_Flash_JEDEC.cpp @@ -58,6 +58,7 @@ extern const AP_HAL::HAL& hal; #define JEDEC_ID_CYPRESS_S25FL064L 0x016017 #define JEDEC_ID_CYPRESS_S25FL128L 0x016018 #define JEDEC_ID_GIGA_GD25Q16E 0xC84015 +#define JEDEC_ID_ZBIT_ZB25VQ128 0x5E4018 void AP_Logger_Flash_JEDEC::Init() { @@ -147,6 +148,7 @@ bool AP_Logger_Flash_JEDEC::getSectorCount(void) case JEDEC_ID_WINBOND_W25Q128: case JEDEC_ID_WINBOND_W25Q128_2: case JEDEC_ID_CYPRESS_S25FL128L: + case JEDEC_ID_ZBIT_ZB25VQ128: blocks = 256; df_PagePerBlock = 256; df_PagePerSector = 16; From 24f6e9bea323900b4387fd2740f0ae20ece672f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Lindh=C3=A9?= Date: Thu, 19 Mar 2026 17:36:46 +0100 Subject: [PATCH 04/99] AP_ExternalAHRS: Add support for Kebni SensAItion as External AHRS It works in two modes: IMU (external sensor for EKF3) or INS (performs sensor fusion instead of EKF3). The parser for incoming binary serial data is a separate class, for testability. Includes unittest for the parser. # Conflicts: # libraries/AP_ExternalAHRS/AP_ExternalAHRS.cpp # libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h --- libraries/AP_ExternalAHRS/AP_ExternalAHRS.cpp | 11 +- libraries/AP_ExternalAHRS/AP_ExternalAHRS.h | 7 +- .../AP_ExternalAHRS_SensAItion.cpp | 416 +++++++++++ .../AP_ExternalAHRS_SensAItion.h | 114 +++ .../AP_ExternalAHRS_SensAItion_Parser.cpp | 401 ++++++++++ .../AP_ExternalAHRS_SensAItion_Parser.h | 159 ++++ .../AP_ExternalAHRS/AP_ExternalAHRS_config.h | 4 + .../tests/test_sensaition_parser.cpp | 706 ++++++++++++++++++ libraries/AP_ExternalAHRS/tests/wscript | 6 + 9 files changed, 1820 insertions(+), 4 deletions(-) create mode 100644 libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.cpp create mode 100644 libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.h create mode 100644 libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.cpp create mode 100644 libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.h create mode 100644 libraries/AP_ExternalAHRS/tests/test_sensaition_parser.cpp create mode 100755 libraries/AP_ExternalAHRS/tests/wscript diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS.cpp b/libraries/AP_ExternalAHRS/AP_ExternalAHRS.cpp index 387810dc4e7f92..47ce3e6a939334 100644 --- a/libraries/AP_ExternalAHRS/AP_ExternalAHRS.cpp +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS.cpp @@ -28,6 +28,7 @@ #include "AP_ExternalAHRS_InertialLabs.h" #include "AP_ExternalAHRS_SBG.h" #include "AP_ExternalAHRS_GSOF.h" +#include "AP_ExternalAHRS_SensAItion.h" #include #include @@ -60,7 +61,7 @@ const AP_Param::GroupInfo AP_ExternalAHRS::var_info[] = { // @Param: _TYPE // @DisplayName: AHRS type // @Description: Type of AHRS device - // @Values: 0:None,1:VectorNav,2:MicroStrain5,5:InertialLabs,6:Trimble GSOF,7:MicroStrain7,8:SBG + // @Values: 0:None,1:VectorNav,2:MicroStrain5,5:InertialLabs,6:Trimble GSOF,7:MicroStrain7,8:SBG,11:SensAItion // @User: Standard AP_GROUPINFO_FLAGS("_TYPE", 1, AP_ExternalAHRS, devtype, HAL_EXTERNAL_AHRS_DEFAULT, AP_PARAM_FLAG_ENABLE), @@ -74,8 +75,7 @@ const AP_Param::GroupInfo AP_ExternalAHRS::var_info[] = { // @Param: _OPTIONS // @DisplayName: External AHRS options // @Description: External AHRS options bitmask - // @Bitmask: 0:Vector Nav use uncompensated values for accel gyro and mag. - // @Bitmask: 1:SBG uses EKF as GNSS. + // @Bitmask: 0:Vector Nav use uncompensated values for accel gyro and mag, 1:SBG uses EKF as GNSS, 2:SensAItion used as AHRS // @User: Standard AP_GROUPINFO("_OPTIONS", 3, AP_ExternalAHRS, options, 0), @@ -138,6 +138,11 @@ void AP_ExternalAHRS::init(void) backend = NEW_NOTHROW AP_ExternalAHRS_InertialLabs(this, state); return; #endif +#if AP_EXTERNAL_AHRS_SENSAITION_ENABLED + case DevType::SensAItion: + backend = NEW_NOTHROW AP_ExternalAHRS_SensAItion(this, state); + return; +#endif #if AP_EXTERNAL_AHRS_SBG_ENABLED case DevType::SBG: diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS.h b/libraries/AP_ExternalAHRS/AP_ExternalAHRS.h index a9789bfbbcf174..c4097738e4d871 100644 --- a/libraries/AP_ExternalAHRS/AP_ExternalAHRS.h +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS.h @@ -36,6 +36,7 @@ class AP_ExternalAHRS { friend class AP_ExternalAHRS_backend; friend class AP_ExternalAHRS_SBG; friend class AP_ExternalAHRS_VectorNav; + friend class AP_ExternalAHRS_SensAItion; AP_ExternalAHRS(); @@ -68,6 +69,9 @@ class AP_ExternalAHRS { #endif // 9 reserved for EulerNav // 10 reserved for Aeron +#if AP_EXTERNAL_AHRS_SENSAITION_ENABLED + SensAItion = 11, +#endif }; static AP_ExternalAHRS *get_singleton(void) { @@ -141,7 +145,7 @@ class AP_ExternalAHRS { } baro_data_message_t; typedef struct { - Vector3f field; + Vector3f field; // Magnetic flux density (mgauss) } mag_data_message_t; typedef struct { @@ -188,6 +192,7 @@ class AP_ExternalAHRS { enum class OPTIONS { VN_UNCOMP_IMU = 1U << 0, SBG_EKF_AS_GNSS = 1U << 1, + SENSAITION_INS = 1U << 2, }; bool option_is_set(OPTIONS option) const { return (options.get() & int32_t(option)) != 0; } diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.cpp b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.cpp new file mode 100644 index 00000000000000..9c639228fc80b1 --- /dev/null +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.cpp @@ -0,0 +1,416 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +/* + Support for SensAItion serial connected INS and IMU + Implements SensAItion protocol with ArduPilot-specific adaptations +*/ + +#include "AP_ExternalAHRS_SensAItion.h" + +#if AP_EXTERNAL_AHRS_SENSAITION_ENABLED + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +const float MINIMUM_INTERESTING_BAROMETER_CHANGE_p = 1.0f; +const float MINIMUM_INTERESTING_TEMP_CHANGE_degc = 0.1f; +const uint32_t BARO_UPDATE_TIMEOUT_ms = 100U; +} + +extern const AP_HAL::HAL &hal; + +AP_ExternalAHRS_SensAItion::AP_ExternalAHRS_SensAItion(AP_ExternalAHRS *_frontend, AP_ExternalAHRS::state_t &_state) : + AP_ExternalAHRS_backend(_frontend, _state), + parser(AP_ExternalAHRS_SensAItion_Parser::ConfigMode::IMU) +{ + ins_mode_enabled = option_is_set(AP_ExternalAHRS::OPTIONS::SENSAITION_INS); + + auto mode = ins_mode_enabled ? + AP_ExternalAHRS_SensAItion_Parser::ConfigMode::INTERLEAVED_INS : + AP_ExternalAHRS_SensAItion_Parser::ConfigMode::IMU; + + if (ins_mode_enabled) { + parser = AP_ExternalAHRS_SensAItion_Parser(mode); + } + + auto &sm = AP::serialmanager(); + uart = sm.find_serial(AP_SerialManager::SerialProtocol_AHRS, 0); + baudrate = sm.find_baudrate(AP_SerialManager::SerialProtocol_AHRS, 0); + port_num = sm.find_portnum(AP_SerialManager::SerialProtocol_AHRS, 0); + if (!uart || baudrate == 0 || port_num == -1) { + GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "KEBNI: Serial Port Not Found!"); + return; + } + + if (ins_mode_enabled) { + set_default_sensors(uint16_t(AP_ExternalAHRS::AvailableSensor::IMU) | + uint16_t(AP_ExternalAHRS::AvailableSensor::GPS) | + uint16_t(AP_ExternalAHRS::AvailableSensor::BARO) | + uint16_t(AP_ExternalAHRS::AvailableSensor::COMPASS)); + } else { + set_default_sensors(uint16_t(AP_ExternalAHRS::AvailableSensor::IMU)); + } + + if (!hal.scheduler->thread_create( + FUNCTOR_BIND_MEMBER(&AP_ExternalAHRS_SensAItion::update_thread, void), + "AHRS_SensAItion", 2048, AP_HAL::Scheduler::PRIORITY_SPI, 0)) { + GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "KEBNI: Failed to create thread!"); + } +} + +int8_t AP_ExternalAHRS_SensAItion::get_port() const +{ + return uart ? port_num : -1; +} + +const char* AP_ExternalAHRS_SensAItion::get_name() const +{ + return "Kebni SensAItion"; +} + +bool AP_ExternalAHRS_SensAItion::healthy() const +{ + const uint32_t now_ms = AP_HAL::millis(); + + WITH_SEMAPHORE(driver_state.semaphore); + + if ((now_ms - driver_state.last_imu_pkt_ms) > 160) { + // IMU is required at high rate for health + return false; + } + + if (ins_mode_enabled) { + if ((now_ms - driver_state.last_ins_pkt_ms) > 400) { + // INS packets must also have high enough rate + return false; + } + + if (!(driver_state.last_sensor_valid & 0x01)) { + // IMU not available + return false; + } + + if (driver_state.last_gnss1_fix < 3) { + // No 3D satellite fix + return false; + } + } + + return true; +} + +bool AP_ExternalAHRS_SensAItion::initialised() const +{ + return setup_complete; +} + +bool AP_ExternalAHRS_SensAItion::pre_arm_check(char *failure_msg, uint8_t failure_msg_len) const +{ + if (!healthy()) { + hal.util->snprintf(failure_msg, failure_msg_len, "SensAItion Unhealthy"); + return false; + } + + if (ins_mode_enabled) { + uint8_t last_alignment_status; + { + WITH_SEMAPHORE(driver_state.semaphore); + last_alignment_status = driver_state.last_alignment_status; + } + if (last_alignment_status != 1) { + hal.util->snprintf(failure_msg, failure_msg_len, "SensAItion Aligning"); + return false; + } + } + + return true; +} + +void AP_ExternalAHRS_SensAItion::get_filter_status(nav_filter_status &status) const +{ + memset(&status, 0, sizeof(status)); + + status.flags.initalized = initialised(); + + if (healthy()) { + WITH_SEMAPHORE(driver_state.semaphore); + if (ins_mode_enabled && driver_state.last_alignment_status == 1) { + status.flags.attitude = true; + status.flags.horiz_pos_abs = true; + status.flags.vert_pos = true; + status.flags.horiz_vel = true; + status.flags.vert_vel = true; + status.flags.using_gps = true; + status.flags.horiz_pos_rel = true; + status.flags.pred_horiz_pos_abs = true; + status.flags.pred_horiz_pos_rel = true; + } + } +} + +bool AP_ExternalAHRS_SensAItion::get_variances(float &velVar, float &posVar, float &hgtVar, Vector3f &magVar, float &tasVar) const +{ + WITH_SEMAPHORE(driver_state.semaphore); + if (ins_mode_enabled && driver_state.last_alignment_status == 1) { + posVar = driver_state.last_h_pos_quality * pos_gate_scale; + velVar = driver_state.last_vel_quality * vel_gate_scale; + hgtVar = driver_state.last_v_pos_quality * hgt_gate_scale; + tasVar = 0; //not used + return true; + } + + return false; +} + +uint8_t AP_ExternalAHRS_SensAItion::num_gps_sensors() const +{ + return ins_mode_enabled ? 1 : 0; +} + +// --------------------------------------------------------------------------- +// THREAD & PROFILER +// --------------------------------------------------------------------------- +void AP_ExternalAHRS_SensAItion::update_thread() +{ + while (true) { + if (!check_uart()) { + hal.scheduler->delay_microseconds(500); + } + } +} + +bool AP_ExternalAHRS_SensAItion::check_uart() +{ + WITH_SEMAPHORE(sem_handle); + + if (!uart) { + return false; + } + + if (!setup_complete) { + uart->begin(baudrate); + setup_complete = true; + GCS_SEND_TEXT(MAV_SEVERITY_INFO, "KEBNI: INIT. Mode:%d Baud:%u", + (int)ins_mode_enabled, (unsigned)baudrate); + } + + const auto nread = uart->read(buffer, sizeof(buffer)); + if (nread <= 0) { + return false; + } + + const uint32_t now_ms = AP_HAL::millis(); + + AP_ExternalAHRS_SensAItion_Parser::Measurement meas; + bool found_measurement = false; + ssize_t parsed_bytes = 0; + while (parsed_bytes < nread) { + const size_t max_bytes_to_parse = nread - parsed_bytes; + parsed_bytes += parser.parse_stream(&buffer[parsed_bytes], max_bytes_to_parse, meas); + + switch (meas.type) { + case AP_ExternalAHRS_SensAItion_Parser::MeasurementType::UNINITIALIZED: + // The parser is done parsing the whole buffer, but didn't find any + // more complete message. (We might be in the middle of a message + // that will be finished later.) + break; + case AP_ExternalAHRS_SensAItion_Parser::MeasurementType::IMU: + handle_imu(meas, now_ms); + found_measurement = true; + break; + case AP_ExternalAHRS_SensAItion_Parser::MeasurementType::AHRS: + handle_ahrs(meas, now_ms); + found_measurement = true; + break; + case AP_ExternalAHRS_SensAItion_Parser::MeasurementType::INS: + handle_ins(meas, now_ms); + found_measurement = true; + break; + } + } + + return found_measurement; +} + +void AP_ExternalAHRS_SensAItion::handle_imu(const AP_ExternalAHRS_SensAItion_Parser::Measurement& meas, uint32_t now_ms) +{ + { + WITH_SEMAPHORE(state.sem); + state.accel = meas.acceleration_mss; + state.gyro = meas.angular_velocity_rads; + } + + // INS + ins.accel = meas.acceleration_mss; + ins.gyro = meas.angular_velocity_rads; + ins.temperature = meas.temperature_degc; + AP::ins().handle_external(ins); + +#if AP_COMPASS_EXTERNALAHRS_ENABLED + // COMPASS + mag.field = meas.magnetic_field_mgauss; + AP::compass().handle_external(mag); +#endif + +#if AP_BARO_EXTERNALAHRS_ENABLED + bool baro_updated = false; +#endif + + { + WITH_SEMAPHORE(driver_state.semaphore); + driver_state.last_imu_pkt_ms = now_ms; + +#if AP_BARO_EXTERNALAHRS_ENABLED + // BARO + // ArduPlane has an internal check that triggers an error if there are too many barometer + // readings with the same value. At high sampling rates, we triggered that check because + // the barometer is internally sampled at a lower rate. To avoid that, we only update the value + // if it changes OR a certain minimum time has passed. + const bool pressure_changed = fabsf(baro.pressure_pa - meas.air_pressure_p) > MINIMUM_INTERESTING_BAROMETER_CHANGE_p; + const bool temp_changed = fabsf(baro.temperature - meas.temperature_degc) > MINIMUM_INTERESTING_TEMP_CHANGE_degc; + const bool timeout = now_ms > driver_state.last_baro_update_ms + BARO_UPDATE_TIMEOUT_ms; + + if (pressure_changed || temp_changed || timeout) { + driver_state.last_baro_update_ms = now_ms; + baro.instance = 0; + baro.pressure_pa = meas.air_pressure_p; + baro.temperature = meas.temperature_degc; + baro_updated = true; + } +#endif + + } + +#if AP_BARO_EXTERNALAHRS_ENABLED + // Do the update after releasing the semaphore, for speed + if (baro_updated) { + AP::baro().handle_external(baro); + } +#endif +} + +void AP_ExternalAHRS_SensAItion::handle_ahrs(const AP_ExternalAHRS_SensAItion_Parser::Measurement& meas, uint32_t now_ms) +{ + { + WITH_SEMAPHORE(driver_state.semaphore); + driver_state.last_quat_pkt_ms = now_ms; + } + + { + WITH_SEMAPHORE(state.sem); + state.quat = meas.orientation; + state.have_quaternion = true; + } +} + +void AP_ExternalAHRS_SensAItion::handle_ins(const AP_ExternalAHRS_SensAItion_Parser::Measurement& meas, uint32_t now_ms) +{ + // STATE + { + WITH_SEMAPHORE(state.sem); + state.location = Location( + meas.location.lat, + meas.location.lng, + meas.location.alt, + Location::AltFrame::ABSOLUTE + ); + state.velocity = meas.velocity_ned; + state.have_location = true; + state.have_velocity = true; + state.last_location_update_us = AP_HAL::micros(); + + if (!state.have_origin && meas.alignment_status) { + state.origin = Location( + meas.location.lat, + meas.location.lng, + meas.location.alt, + Location::AltFrame::ABSOLUTE + ); + state.have_origin = true; + GCS_SEND_TEXT(MAV_SEVERITY_NOTICE, "KEBNI: Origin Set."); + } + } + + { + WITH_SEMAPHORE(driver_state.semaphore); + + // Local data + driver_state.last_ins_pkt_ms = now_ms; + driver_state.last_alignment_status = meas.alignment_status; + driver_state.last_sensor_valid = meas.sensor_valid; + driver_state.last_gnss1_fix = meas.gnss1_fix; + driver_state.last_gnss2_fix = meas.gnss2_fix; + driver_state.last_error_flags = meas.error_flags; + driver_state.last_h_pos_quality = meas.pos_accuracy.xy().length(); + driver_state.last_v_pos_quality = meas.pos_accuracy.z; + driver_state.last_vel_quality = meas.vel_accuracy.length(); + + // GPS + gps.horizontal_pos_accuracy = driver_state.last_h_pos_quality; + gps.vertical_pos_accuracy = driver_state.last_v_pos_quality; + } + + // GPS - continued + gps.gps_week = meas.gps_week; + gps.ms_tow = meas.time_itow_ms; + gps.fix_type = AP_GPS_FixType(meas.gnss1_fix); + gps.satellites_in_view = meas.num_sats_gnss1; + gps.horizontal_vel_accuracy = meas.vel_accuracy.xy().length(); + gps.latitude = meas.location.lat; + gps.longitude = meas.location.lng; + + // Note: SensAItion reports altitude relative to WGS84, not MSL. + // But we expect the user to reset the altitude to 0 at start, + // so the absolute reference should not matter. + gps.msl_altitude = meas.location.alt; + gps.ned_vel_north = meas.velocity_ned.x; + gps.ned_vel_east = meas.velocity_ned.y; + gps.ned_vel_down = meas.velocity_ned.z; + + // 3. Estimate DOPs (Unitless) using assumed UERE of 3.0m + // This answers "What is HDOP/VDOP?" + const float ASSUMED_UERE = 3.0f; + + float est_hdop = gps.horizontal_pos_accuracy / ASSUMED_UERE; + float est_vdop = gps.vertical_pos_accuracy / ASSUMED_UERE; + + // 4. Sanity Clamping (DOP cannot be 0, and rarely < 0.6) + if (est_hdop < 0.7f) { + est_hdop = 0.7f; + } + if (est_vdop < 0.7f) { + est_vdop = 0.7f; + } + gps.hdop = est_hdop; + gps.vdop = est_vdop; + + // Handle + uint8_t instance; + if (AP::gps().get_first_external_instance(instance)) { + AP::gps().handle_external(gps, instance); + } +} + +#endif // AP_EXTERNAL_AHRS_SENSAITION_ENABLED diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.h b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.h new file mode 100644 index 00000000000000..578a78c2b52d8e --- /dev/null +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion.h @@ -0,0 +1,114 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#pragma once + +#include "AP_ExternalAHRS_config.h" + +#if AP_EXTERNAL_AHRS_SENSAITION_ENABLED + +#include "AP_ExternalAHRS_backend.h" +#include "AP_ExternalAHRS_SensAItion_Parser.h" + +/* +This class is the interface to Kebni's SensAItion range of inertial navigation +sensors, which can provide raw sensor data and/or a sensor fusion solution. +*/ +class AP_ExternalAHRS_SensAItion : public AP_ExternalAHRS_backend +{ +public: + AP_ExternalAHRS_SensAItion(AP_ExternalAHRS *frontend, AP_ExternalAHRS::state_t &_state); + + // Hardware Identification + int8_t get_port() const override; + const char* get_name() const override; + + // Health & Status Interface + bool healthy() const override; + bool initialised() const override; + bool pre_arm_check(char *failure_msg, uint8_t failure_msg_len) const override; + void get_filter_status(nav_filter_status &status) const override; + bool get_variances(float &velVar, float &posVar, float &hgtVar, Vector3f &magVar, float &tasVar) const override; + + // GPS Interface + uint8_t num_gps_sensors() const override; + + // Main Loop + void update() override + { + check_uart(); + } + +private: + mutable HAL_Semaphore sem_handle; + + // The member variables below are accessed both from our own + // thread and the main thread and should be protected + // by sem_handle! + // ======================================================= + AP_ExternalAHRS::ins_data_message_t ins; + AP_ExternalAHRS::mag_data_message_t mag; + AP_ExternalAHRS::baro_data_message_t baro; + AP_ExternalAHRS::gps_data_message_t gps; + AP_ExternalAHRS_SensAItion_Parser parser; + + // UART + AP_HAL::UARTDriver *uart; + uint8_t buffer[AP_ExternalAHRS_SensAItion_Parser::MAX_PACKET_SIZE]; + + bool setup_complete; + + // End of member variables protected by sem_handle + // ======================================================= + + // These states are accessed by the public accessor functions, + // so to avoid slowing them down by waiting for sem_handle, + // we use the included semaphore to protect access to them. + // USAGE: + // - If taking both semaphores, always take sem_handle first! + // - Hold this semaphore only while accessing the driver state + // to minimize the waiting time for it! + struct { + mutable HAL_Semaphore semaphore; + + uint32_t last_imu_pkt_ms; + uint32_t last_ins_pkt_ms; // Only used in INS mode + uint32_t last_quat_pkt_ms; // Only used in INS mode + uint32_t last_baro_update_ms; + + // Last known values from INS packet (Packet 2) + uint8_t last_alignment_status; + uint8_t last_gnss1_fix; + uint8_t last_gnss2_fix; + uint8_t last_sensor_valid; + float last_h_pos_quality = 999.9f; + float last_v_pos_quality = 999.9f; + float last_vel_quality = 999.9f; + uint32_t last_error_flags; + } driver_state; + + // Read-only after construction, do not need semaphore + bool ins_mode_enabled = false; + uint32_t baudrate = 460800; + int8_t port_num = -1; + + void update_thread(); + bool check_uart(); + + void handle_imu(const AP_ExternalAHRS_SensAItion_Parser::Measurement& meas, uint32_t now_ms); + void handle_ahrs(const AP_ExternalAHRS_SensAItion_Parser::Measurement& meas, uint32_t now_ms); + void handle_ins(const AP_ExternalAHRS_SensAItion_Parser::Measurement& meas, uint32_t now_ms); +}; +#endif // AP_EXTERNAL_AHRS_SENSAITION_ENABLED diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.cpp b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.cpp new file mode 100644 index 00000000000000..5ddcb289741112 --- /dev/null +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.cpp @@ -0,0 +1,401 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include // Required for memchr, memmove +#include "AP_ExternalAHRS_SensAItion_Parser.h" +#include +#include +#include +#include + +// Constructor +AP_ExternalAHRS_SensAItion_Parser::AP_ExternalAHRS_SensAItion_Parser(ConfigMode mode) : + config_mode(mode) +{ + reset_parser(); +} + +size_t AP_ExternalAHRS_SensAItion_Parser::parse_stream(const uint8_t* data, size_t data_size, Measurement& meas) +{ + for (size_t i = 0; i < data_size; i++) { + if (parse_single_byte(data[i])) { + decode_packet(meas); + reset_parser(); // Ready for more packets in same buffer + return i + 1; // No of bytes parsed + } + } + + meas = Measurement(); // Uninitialized + return data_size; +} + +void AP_ExternalAHRS_SensAItion_Parser::reset_parser() +{ + parse_state = ParseState::WAITING_HEADER; + packet_buffer_len = 0; + target_payload_len = 0; + current_packet_id = PacketID::UNKNOWN; +} + +// Error Handler +void AP_ExternalAHRS_SensAItion_Parser::handle_invalid_packet() +{ + // Fallback if called with empty buffer, which should never happen + if (packet_buffer_len == 0) { + return; + } + + // Look for a new header byte starting from index 1 to resync + uint8_t *p = (uint8_t *)memchr(&packet_buffer[1], HEADER_BYTE, packet_buffer_len - 1); + + if (p == nullptr) { + // No header found, reset completely + reset_parser(); + return; + } + + const size_t bytes_to_discard = p - packet_buffer; + const size_t bytes_to_keep = packet_buffer_len - bytes_to_discard; + + memmove(&packet_buffer[0], p, bytes_to_keep); + packet_buffer_len = bytes_to_keep; + + // Determine State based on Mode and remaining data + if (config_mode == ConfigMode::INTERLEAVED_INS) { + if (packet_buffer_len >= 2) { + // We have Header + Potential ID. Process it immediately. + uint8_t id = packet_buffer[1]; + switch (static_cast(id)) { + case PacketID::IMU: + target_payload_len = PAYLOAD_SIZE_IMU; + parse_state = ParseState::COLLECTING_PAYLOAD; + current_packet_id = PacketID::IMU; + break; + case PacketID::AHRS: + target_payload_len = PAYLOAD_SIZE_QUAT; + parse_state = ParseState::COLLECTING_PAYLOAD; + current_packet_id = PacketID::AHRS; + break; + case PacketID::INS: + target_payload_len = PAYLOAD_SIZE_INS; + parse_state = ParseState::COLLECTING_PAYLOAD; + current_packet_id = PacketID::INS; + break; + default: + // The "New" ID is also bad. Drop header and retry. + reset_parser(); + return; + } + } else { + parse_state = ParseState::WAITING_ID; + } + } else { + // Legacy Mode + target_payload_len = PAYLOAD_SIZE_IMU; + parse_state = ParseState::COLLECTING_PAYLOAD; + } +} + +// Checksum Validator & Deep Debugger +bool AP_ExternalAHRS_SensAItion_Parser::buffer_contains_valid_packet() const +{ + if (packet_buffer_len < 2 || packet_buffer[0] != HEADER_BYTE) { + return false; + } + + const uint8_t calculated = crc_xor_of_bytes(&packet_buffer[1], packet_buffer_len - 2); + const uint8_t received = packet_buffer[packet_buffer_len - 1]; + + if (calculated != received) { + return false; + } + + return true; +} + +// The Core State Machine +bool AP_ExternalAHRS_SensAItion_Parser::parse_single_byte(uint8_t byte) +{ + // Safety: Prevent buffer overflow + if (packet_buffer_len >= MAX_PACKET_SIZE) { + handle_invalid_packet(); + if (packet_buffer_len >= MAX_PACKET_SIZE) { + reset_parser(); + } + } + + switch (parse_state) { + case ParseState::WAITING_HEADER: + if (byte == HEADER_BYTE) { + packet_buffer_len = 0; + packet_buffer[packet_buffer_len++] = byte; + + if (config_mode == ConfigMode::INTERLEAVED_INS) { + parse_state = ParseState::WAITING_ID; + } else { + target_payload_len = PAYLOAD_SIZE_IMU; + parse_state = ParseState::COLLECTING_PAYLOAD; + } + } + break; + + case ParseState::WAITING_ID: + packet_buffer[packet_buffer_len++] = byte; + + switch (static_cast(byte)) { + case PacketID::IMU: + target_payload_len = PAYLOAD_SIZE_IMU; + current_packet_id = PacketID::IMU; + parse_state = ParseState::COLLECTING_PAYLOAD; + break; + + case PacketID::AHRS: + target_payload_len = PAYLOAD_SIZE_QUAT; + current_packet_id = PacketID::AHRS; + parse_state = ParseState::COLLECTING_PAYLOAD; + break; + + case PacketID::INS: + target_payload_len = PAYLOAD_SIZE_INS; + current_packet_id = PacketID::INS; + parse_state = ParseState::COLLECTING_PAYLOAD; + break; + + default: + parse_errors++; + handle_invalid_packet(); + break; + } + break; + + case ParseState::COLLECTING_PAYLOAD: + packet_buffer[packet_buffer_len++] = byte; + + // Calculate Expected Total Length + // Legacy: Header(1) + Payload(N) + CRC(1) + // Interleaved: Header(1) + ID(1) + Payload(N) + CRC(1) + const size_t overhead = (config_mode == ConfigMode::INTERLEAVED_INS) ? 3 : 2; + const size_t expected_total_len = target_payload_len + overhead; + + if (packet_buffer_len >= expected_total_len) { + if (buffer_contains_valid_packet()) { + valid_packets++; + return true; + } + + parse_errors++; + handle_invalid_packet(); + return false; + } + break; + } + + return false; +} + +bool AP_ExternalAHRS_SensAItion_Parser::validate_checksum() const +{ + return buffer_contains_valid_packet(); +} + +// Router +void AP_ExternalAHRS_SensAItion_Parser::decode_packet(Measurement& measurement) +{ + const uint8_t* payload = nullptr; + + if (config_mode == ConfigMode::INTERLEAVED_INS) { + payload = &packet_buffer[2]; // Skip Header + ID + switch (current_packet_id) { + case PacketID::IMU: + decode_imu(payload, measurement); + break; + case PacketID::AHRS: + decode_ahrs(payload, measurement); + break; + case PacketID::INS: + decode_ins(payload, measurement); + break; + case PacketID::UNKNOWN: + assert(false && "decode_packet() should only be called with a valid packet ID!"); + break; + } + } else { + payload = &packet_buffer[1]; // Skip Header + decode_imu(payload, measurement); + } +} + +// IMU Decoder (Packet 0) +void AP_ExternalAHRS_SensAItion_Parser::decode_imu(const uint8_t* payload, Measurement& measurement) +{ + // ... [Code omitted: No changes to decoders, use your existing implementation] ... + // Accel (Bytes 0-11): 3 x Int32 (ug) + const int32_t accel_x_ug = be32toh_ptr(&payload[0]); + const int32_t accel_y_ug = be32toh_ptr(&payload[4]); + const int32_t accel_z_ug = be32toh_ptr(&payload[8]); + + // Gyro (Bytes 12-23): 3 x Int32 (udeg/s) + const int32_t gyro_x_udegs = be32toh_ptr(&payload[12]); + const int32_t gyro_y_udegs = be32toh_ptr(&payload[16]); + const int32_t gyro_z_udegs = be32toh_ptr(&payload[20]); + + // Temp (Bytes 24-25): 1 x Int16 (Scaled) + const int16_t temp_raw = be16toh_ptr(&payload[24]); + + // Mag (Bytes 26-31): 3 x Int16 (mGauss) + const int16_t mag_x_mgauss = be16toh_ptr(&payload[26]); + const int16_t mag_y_mgauss = be16toh_ptr(&payload[28]); + const int16_t mag_z_mgauss = be16toh_ptr(&payload[30]); + + // Baro (Bytes 32-35): 1 x Int32 (0.1 Pa) + const int32_t baro_raw = be32toh_ptr(&payload[32]); + + // Accel: ug -> m/s^2 (Note: 1,000,000 ug = 9.81 m/s^2 roughly) + const float ug_to_mss = 1.0e-6f * GRAVITY_MSS; + measurement.acceleration_mss = Vector3f(accel_x_ug, accel_y_ug, accel_z_ug) * ug_to_mss; + + // Gyro: udeg/s -> rad/s (Note: 1,000,000 udeg/s = 1 deg/s = 0.017 rad/s) + const float udeg_to_rad = 1.0e-6f * DEG_TO_RAD; + measurement.angular_velocity_rads = Vector3f(gyro_x_udegs, gyro_y_udegs, gyro_z_udegs) * udeg_to_rad; + + // Temp: (Raw * 0.008) + 20 + measurement.temperature_degc = ((float)temp_raw * 0.008f) + 20.0f; + + // Mag: mGauss (Pass-through, ArduPilot expects mGauss) + measurement.magnetic_field_mgauss = Vector3f(mag_x_mgauss, mag_y_mgauss, mag_z_mgauss); + + // Baro: 0.1 Pa -> Pascal + measurement.air_pressure_p = (float)baro_raw * 0.1f; + + // Metadata + measurement.type = MeasurementType::IMU; + measurement.timestamp_us = AP_HAL::micros64(); +} + +void AP_ExternalAHRS_SensAItion_Parser::decode_ahrs(const uint8_t* payload, Measurement& measurement) +{ + // Payload layout: W (0-3), X (4-7), Y (8-11), Z (12-15) + const int32_t quat_w_raw = be32toh_ptr(&payload[0]); + const int32_t quat_x_raw = be32toh_ptr(&payload[4]); + const int32_t quat_y_raw = be32toh_ptr(&payload[8]); + const int32_t quat_z_raw = be32toh_ptr(&payload[12]); + + const float scale_factor = 1.0e-6f; + + measurement.orientation = Quaternion( + (float)quat_w_raw * scale_factor, + (float)quat_x_raw * scale_factor, + (float)quat_y_raw * scale_factor, + (float)quat_z_raw * scale_factor + ); + + measurement.type = MeasurementType::AHRS; + measurement.timestamp_us = AP_HAL::micros64(); +} + +void AP_ExternalAHRS_SensAItion_Parser::decode_ins(const uint8_t* payload, Measurement& measurement) +{ + // --- 1. PARSE RAW BYTES (Big-Endian) --- + + // 0-3: Num Sats (G2, G1) + measurement.num_sats_gnss2 = payload[1]; + measurement.num_sats_gnss1 = payload[3]; + + // 4-7: Error Flags + measurement.error_flags = be32toh_ptr(&payload[4]); + + // 8: Sensor Valid + measurement.sensor_valid = payload[8]; + + // 9-16: Lat/Lon (1e-7 deg) + const int32_t lat_raw = be32toh_ptr(&payload[9]); + const int32_t lon_raw = be32toh_ptr(&payload[13]); + + // 17-28: Velocity N, E, D (mm/s) + const int32_t vel_n_mm = be32toh_ptr(&payload[17]); + const int32_t vel_e_mm = be32toh_ptr(&payload[21]); + const int32_t vel_d_mm = be32toh_ptr(&payload[25]); + + // 29-32: Altitude relative to WGS 84 ellipsoid (mm) + const int32_t alt_raw_mm = be32toh_ptr(&payload[29]); + + // 33: Alignment Status + measurement.alignment_status = payload[33]; + + // 34-37: Time of week (ms) + measurement.time_itow_ms = be32toh_ptr(&payload[34]); + + // 38-39: GNSS Fix + measurement.gnss2_fix = payload[38]; + measurement.gnss1_fix = payload[39]; + + // 40-44: UTC Date/Time + const uint16_t year = (uint16_t)((payload[40]<<8) | payload[41]); + const uint16_t month = (uint16_t)((payload[42]<<8) | payload[43]); + const uint8_t day = payload[44]; + + // 45-68: Accuracy Metrics (mm or mm/s) + const int32_t acc_lat_mm = be32toh_ptr(&payload[45]); + const int32_t acc_lon_mm = be32toh_ptr(&payload[49]); + const int32_t acc_vn_mm = be32toh_ptr(&payload[53]); + const int32_t acc_ve_mm = be32toh_ptr(&payload[57]); + const int32_t acc_vd_mm = be32toh_ptr(&payload[61]); + const int32_t acc_vd_pos_mm = be32toh_ptr(&payload[65]); + + // --- 2. POPULATE & CONVERT --- + const float mm_to_cm = 0.1f; + measurement.location = Location(lat_raw, lon_raw, alt_raw_mm * mm_to_cm, Location::AltFrame::ABSOLUTE); + + const float mms_to_ms = 0.001f; + const float mm_to_m = 0.001f; + + measurement.velocity_ned.x = (float)vel_n_mm * mms_to_ms; + measurement.velocity_ned.y = (float)vel_e_mm * mms_to_ms; + measurement.velocity_ned.z = (float)vel_d_mm * mms_to_ms; + + // Accuracy + measurement.pos_accuracy.x = (float)acc_lat_mm * mm_to_m; // Horizontal Latitude + measurement.pos_accuracy.y = (float)acc_lon_mm * mm_to_m; // Horizontal Longitude + measurement.pos_accuracy.z = (float)acc_vd_pos_mm * mm_to_m; // Vertical + + measurement.vel_accuracy.x = (float)acc_vn_mm * mms_to_ms; + measurement.vel_accuracy.y = (float)acc_ve_mm * mms_to_ms; + measurement.vel_accuracy.z = (float)acc_vd_mm * mms_to_ms; + + // GPS Week Calculation (Gate check) + if (measurement.gnss1_fix >= 2) { + measurement.gps_week = calculate_gps_week(year, month, day); + } else { + measurement.gps_week = 0; + } + + measurement.type = MeasurementType::INS; + measurement.timestamp_us = AP_HAL::micros64(); + +} + +uint16_t AP_ExternalAHRS_SensAItion_Parser::calculate_gps_week(uint16_t year, uint8_t month, uint8_t day) +{ + // Convert to BCD form (DDMMYY) - the GPS_Backend function assumes year is 20YY + const uint32_t bcd_date = year % 100U + month * 100U + day * 10000U; + + const uint32_t bcd_time_ms = 0U; + uint16_t gps_week; + uint32_t gps_time_ms; + AP_GPS_Backend::BCD_to_gps_time(bcd_date, bcd_time_ms, gps_week, gps_time_ms); + + return gps_week; +} diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.h b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.h new file mode 100644 index 00000000000000..c17a87abea24f2 --- /dev/null +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_SensAItion_Parser.h @@ -0,0 +1,159 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +/* + SensAItion Protocol Parser + Handles binary protocol parsing (state machine, packet validation, checksums) + Separated from ArduPilot state management for testability +*/ + +#pragma once + +#include +#include +#include + +class AP_ExternalAHRS_SensAItion_Parser +{ +public: + // Configuration Mode defined in EAHRS_OPTIONS (Bit 1) + enum class ConfigMode { + IMU = 0, // IMU Only Mode (No ID byte, fixed 38 byte packet) + INTERLEAVED_INS = 1 // Interleaved Mode (Header -> ID -> Payload) + }; + + // Type of data contained in a decoded Measurement + enum class MeasurementType { + UNINITIALIZED = 0, + IMU, + AHRS, // Maps to Packet 1 (Orientation) + INS // Maps to Packet 2 (Navigation) + }; + + // Public Constants (Available to Driver) + static const uint16_t MAX_PACKET_SIZE = 1024; + static const uint8_t HEADER_BYTE = 0xFA; + + // Container for decoded data passed to Backend + struct Measurement { + MeasurementType type = MeasurementType::UNINITIALIZED; + uint64_t timestamp_us; + + // Packet 0 (IMU) Data + Vector3f acceleration_mss; + Vector3f angular_velocity_rads; + Vector3f magnetic_field_mgauss; + float temperature_degc; + float air_pressure_p; + + // Packet 1 (Orientation) Data + Quaternion orientation; + + // Packet 2 (INS) Data + Location location; // Lat/Lon/Alt + Vector3f velocity_ned; // North/East/Down (m/s) + + // Accuracy Metrics (Vectors as requested) + // ArduPilot often uses float for horiz/vert, but Vector3f is more flexible + // if the sensor provides 3-axis accuracy. + // Based on your config (AccLat, AccLon, AccPosD), we have 3 components. + Vector3f pos_accuracy; // North/East/Down (m) + Vector3f vel_accuracy; // North/East/Down (m/s) + + // Status & Health Flags + uint8_t alignment_status; // 1 = Align OK + uint8_t gnss1_fix; + uint8_t gnss2_fix; + + uint8_t num_sats_gnss1; + uint8_t num_sats_gnss2; + + // Time + uint32_t time_itow_ms; // GNSS time of week (ms) + uint16_t gps_week; // Calculated Week Number + + uint32_t error_flags; // Bitmask from sensor + uint8_t sensor_valid; // Validity bitmask + }; + + // Constructor + AP_ExternalAHRS_SensAItion_Parser(ConfigMode mode); + + // Parse at most 'data_size' bytes from 'data', return # of bytes parsed. + // If we parsed less than 'data_size' bytes, 'meas' contains a measurement, + // otherwise it contains a valid measurement OR is uninitialized. + size_t parse_stream(const uint8_t* data, size_t data_size, Measurement& meas); + + // Number of parsed full length buffers that did not contain a valid packet + uint32_t get_parse_errors() const + { + return parse_errors; + } + + // Number of valid packets received during object lifetime + uint32_t get_valid_packets() const + { + return valid_packets; + } + +private: + // Payload Sizes (Excluding Header, ID, CRC) + static const uint8_t PAYLOAD_SIZE_IMU = 36; // Packet 0 + static const uint8_t PAYLOAD_SIZE_QUAT = 16; // Packet 1 + static const uint8_t PAYLOAD_SIZE_INS = 69; // Packet 2 + + // Packet IDs (Interleaved Mode) + enum class PacketID : uint8_t { + IMU = 0x00, + AHRS = 0x01, + INS = 0x02, + UNKNOWN = 0xFF + }; + + // Internal State Machine + enum class ParseState { + WAITING_HEADER, + WAITING_ID, + COLLECTING_PAYLOAD + }; + + // Core Logic + void reset_parser(); // Does not reset packet and parse error counters + bool parse_single_byte(uint8_t byte); // Return true if it was the last byte of a valid packet + void handle_invalid_packet(); // Robust error recovery (memmove) + bool buffer_contains_valid_packet() const; + bool validate_checksum() const; + + // Decoders + void decode_packet(Measurement& measurement); + void decode_imu(const uint8_t* payload, Measurement& measurement); + void decode_ahrs(const uint8_t* payload, Measurement& measurement); + void decode_ins(const uint8_t* payload, Measurement& measurement); + + //Helpers + uint16_t calculate_gps_week(uint16_t year, uint8_t month, uint8_t day); + + // Members + ConfigMode config_mode; + ParseState parse_state; + PacketID current_packet_id; + + uint8_t packet_buffer[MAX_PACKET_SIZE]; + size_t packet_buffer_len; + size_t target_payload_len; + + uint32_t valid_packets; + uint32_t parse_errors; +}; diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h index 378d91daf86bb0..b0ca8bdf01febd 100644 --- a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h @@ -41,3 +41,7 @@ #ifndef AP_EXTERNALAHRS_GSOF_DEBUG_ENABLED #define AP_EXTERNALAHRS_GSOF_DEBUG_ENABLED 0 #endif + +#ifndef AP_EXTERNAL_AHRS_SENSAITION_ENABLED +#define AP_EXTERNAL_AHRS_SENSAITION_ENABLED AP_EXTERNAL_AHRS_BACKEND_DEFAULT_ENABLED +#endif diff --git a/libraries/AP_ExternalAHRS/tests/test_sensaition_parser.cpp b/libraries/AP_ExternalAHRS/tests/test_sensaition_parser.cpp new file mode 100644 index 00000000000000..6053bb877b3fee --- /dev/null +++ b/libraries/AP_ExternalAHRS/tests/test_sensaition_parser.cpp @@ -0,0 +1,706 @@ +#include +#include + +const AP_HAL::HAL& hal = AP_HAL::get_HAL(); + +namespace +{ +// Units definitions Kebni +constexpr float UG_PER_MSS = 1e6f / 9.80665f; +constexpr float UDEGS_PER_RADS = 1e6f * 180.0f / 3.1415926f; +constexpr float MHPA_PER_PA = 1e3f / 100.0f; + +using Parser = AP_ExternalAHRS_SensAItion_Parser; +} + +// --- HELPER FUNCTIONS --- +static bool is_equal(const float f1, const float f2, const float eps) +{ + return (fabs(f1 - f2) < eps); +} + +static void fill_be32(uint8_t* data, size_t& loc, int32_t val) +{ + data[loc++] = (val >> 24) & 0xFF; + data[loc++] = (val >> 16) & 0xFF; + data[loc++] = (val >> 8) & 0xFF; + data[loc++] = val & 0xFF; +} + +static void fill_be16(uint8_t* data, size_t& loc, int16_t val) +{ + data[loc++] = (val >> 8) & 0xFF; + data[loc++] = val & 0xFF; +} + +static void fill_u8(uint8_t* data, size_t& loc, uint8_t val) +{ + data[loc++] = val; +} + +// --- UPDATED STRUCT (Matches Parser + Date/Week Support) --- +struct Measurement { + Parser::MeasurementType type; + + // Packet 0 (IMU) Data + Vector3f acceleration_mss; + Vector3f angular_velocity_rads; + Vector3f magnetic_field_mgauss; + float temperature_degc; + float air_pressure_p; + + // Packet 1 (Orientation) Data + Quaternion orientation; + + // Packet 2 (INS) Data + Location location; // Lat/Lon/Alt + Vector3f velocity_ned; // North/East/Down m/s + + // Accuracy Metrics (Vectors) + Vector3f pos_accuracy; // X=Lat, Y=Lon, Z=Alt (meters) + Vector3f vel_accuracy; // X=VelN, Y=VelE, Z=VelD (m/s) + + // Status & Health Flags + uint8_t alignment_status; // 1 = Align OK + uint8_t gnss1_fix; + uint8_t gnss2_fix; + + uint8_t num_sats_gnss1; + uint8_t num_sats_gnss2; + + // Time & Date (Input for Generator) + uint32_t time_itow_ms; + uint16_t year; + uint8_t month; + uint8_t day; + + // The Calculated Result (Output from Parser) + uint16_t gps_week; + + uint32_t error_flags; // Bitmask from sensor + uint8_t sensor_valid; // Byte 49 (New in v5) +}; + +// --- UPDATED GENERATOR (Matches 69-Byte Parser Layout) --- +static void fill_simulated_packet(uint8_t* data, size_t& data_length, + const Measurement& m, + Parser::ConfigMode mode) +{ + size_t idx = 0; + + // 1. Header & ID + data[idx++] = 0xFA; + if (mode == Parser::ConfigMode::INTERLEAVED_INS) { + switch (m.type) { + case Parser::MeasurementType::IMU: + data[idx++] = 0x00; + break; + case Parser::MeasurementType::AHRS: + data[idx++] = 0x01; + break; + case Parser::MeasurementType::INS: + data[idx++] = 0x02; + break; + default: + break; + } + } + + // 2. Payload + if (m.type == Parser::MeasurementType::IMU) { + fill_be32(data, idx, m.acceleration_mss.x * UG_PER_MSS); + fill_be32(data, idx, m.acceleration_mss.y * UG_PER_MSS); + fill_be32(data, idx, m.acceleration_mss.z * UG_PER_MSS); + fill_be32(data, idx, m.angular_velocity_rads.x * UDEGS_PER_RADS); + fill_be32(data, idx, m.angular_velocity_rads.y * UDEGS_PER_RADS); + fill_be32(data, idx, m.angular_velocity_rads.z * UDEGS_PER_RADS); + fill_be16(data, idx, (int16_t)((m.temperature_degc - 20.0f) / 0.008f)); + fill_be16(data, idx, m.magnetic_field_mgauss.x); + fill_be16(data, idx, m.magnetic_field_mgauss.y); + fill_be16(data, idx, m.magnetic_field_mgauss.z); + fill_be32(data, idx, m.air_pressure_p * MHPA_PER_PA); + + } else if (m.type == Parser::MeasurementType::AHRS) { + fill_be32(data, idx, m.orientation.q1 * 1e6); + fill_be32(data, idx, m.orientation.q2 * 1e6); + fill_be32(data, idx, m.orientation.q3 * 1e6); + fill_be32(data, idx, m.orientation.q4 * 1e6); + + } else if (m.type == Parser::MeasurementType::INS) { + // --- 69-BYTE LAYOUT --- + + // 0-3: Sats (Big Endian of 2 shorts) + // GNSS2 is first Short, GNSS1 is second Short + // We put values in the LSB of each Short: [00][Count] + fill_u8(data, idx, 0); // GNSS2 Hi + fill_u8(data, idx, m.num_sats_gnss2); // GNSS2 Lo + fill_u8(data, idx, 0); // GNSS1 Hi + fill_u8(data, idx, m.num_sats_gnss1); // GNSS1 Lo + + // 4-7: Flags + fill_be32(data, idx, m.error_flags); + + // 8: Valid + fill_u8(data, idx, m.sensor_valid); + + // 9-32: Nav Data + fill_be32(data, idx, m.location.lat); + fill_be32(data, idx, m.location.lng); + + // Velocity N, E, D + fill_be32(data, idx, m.velocity_ned.x * 1000); + fill_be32(data, idx, m.velocity_ned.y * 1000); + fill_be32(data, idx, m.velocity_ned.z * 1000); + + // Altitude + fill_be32(data, idx, m.location.alt * 10); // cm -> mm + + // 33: Alignment + fill_u8(data, idx, m.alignment_status); + + // 34-37: iTOW + fill_be32(data, idx, m.time_itow_ms); + + // 38-39: GNSS Fix (Mask 5 -> 2 Bytes) + // Wire: [GNSS2][GNSS1] + fill_u8(data, idx, m.gnss2_fix); + fill_u8(data, idx, m.gnss1_fix); + + // 40-43: UTC Date (Mask F -> 4 Bytes) + // Wire: [Y_MSB][Y_LSB][Pad][Month] + fill_u8(data, idx, (m.year >> 8) & 0xFF); + fill_u8(data, idx, m.year & 0xFF); + fill_u8(data, idx, 0); // Pad + fill_u8(data, idx, m.month); + + // 44: UTC Time (Mask 8 -> 1 Byte) + // Wire: [Day] + fill_u8(data, idx, m.day); + + // 45-68: Accuracy (6 x 4 Bytes) + // Order: Lat, Lon, VelN, VelE, VelD, PosD + fill_be32(data, idx, m.pos_accuracy.x * 1000); // Lat Acc + fill_be32(data, idx, m.pos_accuracy.y * 1000); // Lon Acc + fill_be32(data, idx, m.vel_accuracy.x * 1000); // Vel N + fill_be32(data, idx, m.vel_accuracy.y * 1000); // Vel E + fill_be32(data, idx, m.vel_accuracy.z * 1000); // Vel D + fill_be32(data, idx, m.pos_accuracy.z * 1000); // Pos D + } + + // 4. CRC + uint8_t checksum = 0; + for (size_t i = 1; i < idx; ++i) { + checksum ^= data[i]; + } + data[idx++] = checksum; + + data_length = idx; +} + +// --- DEFAULT FACTORY --- +static Measurement default_measurement(Parser::MeasurementType type) +{ + Measurement m = {}; + m.type = type; + if (type == Parser::MeasurementType::IMU) { + m.acceleration_mss.x = 4.0f; + m.acceleration_mss.y = 5.0f; + m.acceleration_mss.z = 6.0f; + m.angular_velocity_rads.x = 0.1f; + m.angular_velocity_rads.y = 0.2f; + m.angular_velocity_rads.z = 0.3f; + m.temperature_degc = 56.0f; + m.magnetic_field_mgauss.x = 31.0f; + m.magnetic_field_mgauss.y = 41.0f; + m.magnetic_field_mgauss.z = 51.0f; + m.air_pressure_p = 1235.0f; + } + if (type == Parser::MeasurementType::INS) { + m.location.lat = 590000000; + m.location.lng = 180000000; + m.location.alt = 5000; + m.velocity_ned = Vector3f(1, 0, 0); + + m.pos_accuracy = Vector3f(0.5f, 0.5f, 0.8f); + m.vel_accuracy = Vector3f(0.1f, 0.1f, 0.1f); + + m.alignment_status = 1; + m.gnss1_fix = 3; + m.gnss2_fix = 0; + m.num_sats_gnss1 = 12; + m.time_itow_ms = 1000; + m.year = 2025; + m.month = 12; + m.day = 10; + m.sensor_valid = true; + } + return m; +} +static bool cmp_packages(Measurement& in, Parser::Measurement& out) +{ + if (in.type != out.type) { + return false; + } + // + + // + switch (in.type) { + case Parser::MeasurementType::IMU: + if (!is_equal(in.acceleration_mss[0], out.acceleration_mss[0], 0.00001f) || + !is_equal(in.acceleration_mss[1], out.acceleration_mss[1], 0.00001f) || + !is_equal(in.acceleration_mss[2], out.acceleration_mss[2], 0.00001f) || + !is_equal(in.angular_velocity_rads[0], out.angular_velocity_rads[0], 0.000001f) || + !is_equal(in.angular_velocity_rads[1], out.angular_velocity_rads[1], 0.000001f) || + !is_equal(in.angular_velocity_rads[2], out.angular_velocity_rads[2], 0.000001f) || + !is_equal(in.magnetic_field_mgauss[0], out.magnetic_field_mgauss[0]) || + !is_equal(in.magnetic_field_mgauss[1], out.magnetic_field_mgauss[1]) || + !is_equal(in.magnetic_field_mgauss[2], out.magnetic_field_mgauss[2]) || + !is_equal(in.temperature_degc, out.temperature_degc) || + !is_equal(in.air_pressure_p, out.air_pressure_p)) { + return false; + } + break; + case Parser::MeasurementType::AHRS: + if (!is_equal(in.orientation.q1, out.orientation.q1) || + !is_equal(in.orientation.q2, out.orientation.q2) || + !is_equal(in.orientation.q3, out.orientation.q3) || + !is_equal(in.orientation.q4, out.orientation.q4)) { + return false; + } + break; + case Parser::MeasurementType::INS: + if (in.location.lat != out.location.lat || + in.location.lng != out.location.lng || + in.location.alt != out.location.alt || + !is_equal(in.velocity_ned[0], out.velocity_ned[0]) || + !is_equal(in.velocity_ned[1], out.velocity_ned[1]) || + !is_equal(in.velocity_ned[2], out.velocity_ned[2]) || + !is_equal(in.pos_accuracy[0], out.pos_accuracy[0]) || + !is_equal(in.pos_accuracy[1], out.pos_accuracy[1]) || + !is_equal(in.pos_accuracy[2], out.pos_accuracy[2]) || + !is_equal(in.vel_accuracy[0], out.vel_accuracy[0]) || + !is_equal(in.vel_accuracy[1], out.vel_accuracy[1]) || + !is_equal(in.vel_accuracy[2], out.vel_accuracy[2]) || + in.alignment_status != out.alignment_status || + in.gnss1_fix != out.gnss1_fix || + in.gnss2_fix != out.gnss2_fix || + in.num_sats_gnss1 != out.num_sats_gnss1 || + in.num_sats_gnss2 != out.num_sats_gnss2 || + in.time_itow_ms != out.time_itow_ms || + out.gps_week != 2396 || + in.error_flags != out.error_flags || + in.sensor_valid != out.sensor_valid) { + return false; + } + break; + default: + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// LEGACY MODE TESTS +// --------------------------------------------------------------------------- + +TEST(SensAItionParser, Legacy_IMU_HappyPath) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::IMU); + EXPECT_EQ(len, 38u); + + Parser::Measurement meas; + const auto parsed_bytes = parser.parse_stream(buffer, len, meas); + + EXPECT_EQ(parsed_bytes, 38u); + EXPECT_EQ(meas.type, Parser::MeasurementType::IMU); + EXPECT_TRUE(cmp_packages(in, meas)); +} + +TEST(SensAItionParser, Legacy_RejectsInvalidChecksum) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::IMU); + buffer[len - 1] += 1; + uint32_t err_start = parser.get_parse_errors(); + + Parser::Measurement meas; + parser.parse_stream(buffer, len, meas); + + EXPECT_GT(parser.get_parse_errors(), err_start); +} + +TEST(SensAItionParser, Legacy_RejectsTooSmallBuffer) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::IMU); + uint32_t valid_start = parser.get_valid_packets(); + + Parser::Measurement meas; + parser.parse_stream(buffer, len - 5, meas); + + EXPECT_EQ(parser.get_valid_packets(), valid_start); +} + +TEST(SensAItionParser, Legacy_ValidPacketsCount) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::IMU); + uint32_t valid_start = parser.get_valid_packets(); + + Parser::Measurement meas; + for (int i = 0; i < 5; i++) { + parser.parse_stream(buffer, len, meas); + } + + EXPECT_EQ(parser.get_valid_packets(), valid_start + 5); +} + +TEST(SensAItionParser, Legacy_FalseHeaderInPayload) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t packet[38]; + size_t len = 38; + fill_simulated_packet(packet, len, in, Parser::ConfigMode::IMU); + packet[5] = 0xFA; + uint8_t checksum = 0; + for (size_t i = 1; i < len - 1; ++i) { + checksum ^= packet[i]; + } + packet[len - 1] = checksum; + uint32_t start_valid = parser.get_valid_packets(); + + Parser::Measurement meas; + for (size_t i = 0; i < len; i++) { + parser.parse_stream(&packet[i], 1, meas); + } + + EXPECT_EQ(parser.get_valid_packets(), start_valid + 1); +} + +TEST(SensAItionParser, Legacy_FragmentedHeaderRecovery) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t valid[38]; + size_t len = 38; + fill_simulated_packet(valid, len, in, Parser::ConfigMode::IMU); + uint8_t stream[120]; + size_t slen = 0; + stream[slen++] = 0xFA; stream[slen++] = 0x00; + memcpy(&stream[slen], valid, 38); + slen += 38; + memcpy(&stream[slen], valid, 38); + slen += 38; + uint32_t start_valid = parser.get_valid_packets(); + + Parser::Measurement meas; + for (size_t i = 0; i < slen; i++) { + parser.parse_stream(&stream[i], 1, meas); + } + + EXPECT_GE(parser.get_valid_packets() - start_valid, 1u); +} + +// --------------------------------------------------------------------------- +// INTERLEAVED MODE TESTS +// --------------------------------------------------------------------------- + +TEST(SensAItionParser, Interleaved_IMU_HappyPath) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::IMU); + in.acceleration_mss.x = 2.5f; + uint8_t buffer[64]; + size_t len = 64; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + EXPECT_EQ(len, 39u); + + Parser::Measurement meas; + const auto parsed_bytes = parser.parse_stream(buffer, len, meas); + + EXPECT_EQ(parsed_bytes, 39u); + EXPECT_EQ(meas.type, Parser::MeasurementType::IMU); + EXPECT_NEAR(meas.acceleration_mss.x, 2.5f, 0.01f); + EXPECT_TRUE(cmp_packages(in, meas)); +} + +TEST(SensAItionParser, Interleaved_INS_HappyPath) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::INS); + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + EXPECT_EQ(len, 72u); // Verified + + Parser::Measurement meas; + parser.parse_stream(buffer, len, meas); + + EXPECT_EQ(meas.type, Parser::MeasurementType::INS); + EXPECT_TRUE(cmp_packages(in, meas)); +} + +TEST(SensAItionParser, Interleaved_InvalidID) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + uint8_t bad[] = { 0xFA, 0x99, 0x00, 0x00 }; + uint32_t start_err = parser.get_parse_errors(); + + Parser::Measurement meas; + parser.parse_stream(bad, 4, meas); + + EXPECT_GT(parser.get_parse_errors(), start_err); +} + +TEST(SensAItionParser, Handle_Interleaved_Packets) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + + // Create a buffer with one INS and IMU packet back-to-back + uint8_t stream[200]; + size_t len = 0; + + // Create INS Packet + auto m_ins = default_measurement(Parser::MeasurementType::INS); + m_ins.location.lat = 123456789; // Unique marker + size_t ins_len = 0; + fill_simulated_packet(&stream[0], ins_len, m_ins, Parser::ConfigMode::INTERLEAVED_INS); + len += ins_len; + + // Create IMU Packet immediately after + auto m_imu = default_measurement(Parser::MeasurementType::IMU); + m_imu.acceleration_mss.z = -15.0f; // Unique marker + size_t imu_len = 0; + fill_simulated_packet(&stream[len], imu_len, m_imu, Parser::ConfigMode::INTERLEAVED_INS); + len += imu_len; + + // Parse first packet + Parser::Measurement meas; + auto parsed_bytes = parser.parse_stream(stream, len, meas); + + EXPECT_EQ(parsed_bytes, ins_len); + EXPECT_EQ(meas.type, Parser::MeasurementType::INS); + EXPECT_TRUE(cmp_packages(m_ins, meas)); + + // Parse second packet + parsed_bytes = parser.parse_stream(&stream[parsed_bytes], len - parsed_bytes, meas); + EXPECT_EQ(parsed_bytes, imu_len); + EXPECT_EQ(meas.type, Parser::MeasurementType::IMU); + EXPECT_TRUE(cmp_packages(m_imu, meas)); +} + +TEST(SensAItionParser, Interleaved_MixedStream_Transitions) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto m_imu = default_measurement(Parser::MeasurementType::IMU); + auto m_ahrs = default_measurement(Parser::MeasurementType::AHRS); + auto m_ins = default_measurement(Parser::MeasurementType::INS); + uint8_t stream[200]; + size_t len = 0; + size_t part_len; + + part_len = 200 - len; + fill_simulated_packet(&stream[len], part_len, m_imu, Parser::ConfigMode::INTERLEAVED_INS); + len += part_len; + part_len = 200 - len; + fill_simulated_packet(&stream[len], part_len, m_ahrs, Parser::ConfigMode::INTERLEAVED_INS); + len += part_len; + part_len = 200 - len; + fill_simulated_packet(&stream[len], part_len, m_ins, Parser::ConfigMode::INTERLEAVED_INS); + len += part_len; + + Parser::Measurement meas; + auto parsed_bytes = parser.parse_stream(stream, len, meas); + EXPECT_TRUE(cmp_packages(m_imu, meas)); + + parsed_bytes += parser.parse_stream(&stream[parsed_bytes], len - parsed_bytes, meas); + EXPECT_TRUE(cmp_packages(m_ahrs, meas)); + + parsed_bytes += parser.parse_stream(&stream[parsed_bytes], len - parsed_bytes, meas); + EXPECT_EQ(parsed_bytes, len); + EXPECT_TRUE(cmp_packages(m_ins, meas)); +} + +TEST(SensAItionParser, Interleaved_INS_Fragmentation) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::INS); + uint8_t packet[100]; + size_t len = 100; + fill_simulated_packet(packet, len, in, Parser::ConfigMode::INTERLEAVED_INS); + + Parser::Measurement meas; + // Parse all but one byte + auto parsed_bytes = parser.parse_stream(packet, len - 1, meas); + EXPECT_EQ(meas.type, Parser::MeasurementType::UNINITIALIZED); + + // Then parse the finishing byte of the message + parsed_bytes += parser.parse_stream(&packet[parsed_bytes], len - parsed_bytes, meas); + EXPECT_EQ(meas.type, Parser::MeasurementType::INS); +} + +TEST(SensAItionParser, Interleaved_Data_StressTest) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::INS); + in.location.lat = -593293230; + in.velocity_ned.x = -15.5f; + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + + Parser::Measurement meas; + parser.parse_stream(buffer, len, meas); + EXPECT_EQ(meas.location.lat, -593293230); + EXPECT_NEAR(meas.velocity_ned.x, -15.5f, 0.01f); +} + +TEST(SensAItionParser, Interleaved_NoiseRecovery) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::INS); + uint8_t valid[100]; + size_t len = 100; + fill_simulated_packet(valid, len, in, Parser::ConfigMode::INTERLEAVED_INS); + uint8_t stream[200]; + memset(stream, 0xEE, 20); + memcpy(&stream[20], valid, len); + + Parser::Measurement meas; + parser.parse_stream(stream, len + 20, meas); + + EXPECT_EQ(meas.type, Parser::MeasurementType::INS); +} + +TEST(SensAItionParser, Legacy_IMU_PartialStream) +{ + Parser parser(Parser::ConfigMode::IMU); + auto in = default_measurement(Parser::MeasurementType::IMU); + uint8_t packet[100]; + memset(packet, 0x00, 100); // Ensure known content after the packet + size_t chunk_size = 5; + size_t len = 20 * chunk_size; + fill_simulated_packet(packet, len, in, Parser::ConfigMode::IMU); + + Parser::Measurement meas; + for (size_t i = 0; i < len; i += chunk_size) { + parser.parse_stream(&packet[i], chunk_size, meas); + } + + EXPECT_EQ(meas.type, Parser::MeasurementType::IMU); +} + +TEST(SensAItionParser, Interleaved_INS_FullFieldVerification) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::INS); + + // Setup Distinct Values + in.num_sats_gnss1 = 22; + in.num_sats_gnss2 = 18; + in.error_flags = 0xCAFEBABE; + in.sensor_valid = true; + in.location.lat = -593293230; + in.location.lng = 180685810; + in.location.alt = 1500; // cm + in.velocity_ned = Vector3f(-5.5f, 2.2f, 0.5f); + in.alignment_status = 1; + in.time_itow_ms = 987654321; + in.gnss1_fix = 3; + in.gnss2_fix = 2; + + // Time -> Week Calculation Check + // 2025-12-10 is GPS Week 2396 + in.year = 2025; + in.month = 12; + in.day = 10; + uint16_t expected_week = 2396; + + // Vector Accuracy + in.pos_accuracy = Vector3f(0.1f, 0.2f, 0.3f); // Lat, Lon, Alt + in.vel_accuracy = Vector3f(0.4f, 0.5f, 0.6f); // N, E, D + + // Generate & Parse + uint8_t buffer[100]; + size_t len = 100; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + + Parser::Measurement meas; + parser.parse_stream(buffer, len, meas); + + // VERIFICATION + auto& out = meas; + EXPECT_EQ(out.type, Parser::MeasurementType::INS); + EXPECT_EQ(out.num_sats_gnss1, in.num_sats_gnss1); + EXPECT_EQ(out.num_sats_gnss2, in.num_sats_gnss2); + EXPECT_EQ(out.error_flags, in.error_flags); + EXPECT_EQ(out.sensor_valid, in.sensor_valid); + + EXPECT_EQ(out.location.lat, in.location.lat); + EXPECT_EQ(out.location.lng, in.location.lng); + EXPECT_EQ(out.location.alt, in.location.alt); + EXPECT_NEAR(out.velocity_ned.x, in.velocity_ned.x, 0.001f); + + EXPECT_EQ(out.alignment_status, in.alignment_status); + EXPECT_EQ(out.time_itow_ms, in.time_itow_ms); + EXPECT_EQ(out.gnss1_fix, in.gnss1_fix); + EXPECT_EQ(out.gnss2_fix, in.gnss2_fix); + + // Week Verify + EXPECT_EQ(out.gps_week, expected_week) << "GPS Week Calc Failed"; + + // Vector Verify + EXPECT_NEAR(out.pos_accuracy.x, in.pos_accuracy.x, 0.001f); + EXPECT_NEAR(out.pos_accuracy.y, in.pos_accuracy.y, 0.001f); + EXPECT_NEAR(out.pos_accuracy.z, in.pos_accuracy.z, 0.001f); + + EXPECT_NEAR(out.vel_accuracy.x, in.vel_accuracy.x, 0.001f); + EXPECT_NEAR(out.vel_accuracy.y, in.vel_accuracy.y, 0.001f); + EXPECT_NEAR(out.vel_accuracy.z, in.vel_accuracy.z, 0.001f); +} + +TEST(SensAItionParser, GPS_Week_Calculation_EdgeCases) +{ + Parser parser(Parser::ConfigMode::INTERLEAVED_INS); + auto in = default_measurement(Parser::MeasurementType::INS); + uint8_t buffer[100]; + size_t len = 100; + Parser::Measurement meas; + + // CASE 1: Leap Year (Feb 29 2024) + // 2024-02-29 -> Week 2303 + in.year = 2024; in.month = 2; in.day = 29; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + parser.parse_stream(buffer, len, meas); + EXPECT_EQ(meas.gps_week, 2303) << "Failed Leap Year Calc"; + + // CASE 2: No Fix -> Week Should be 0 + in.gnss1_fix = 0; // Lost fix + in.year = 2025; in.month = 1; in.day = 1; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + parser.parse_stream(buffer, len, meas); + EXPECT_EQ(meas.gps_week, 0) << "Week should be 0 when fix is lost"; + + // CASE 3: Year 2000 (translates to 00 in BCD format) should be correctly handled + in = default_measurement(Parser::MeasurementType::INS); + in.year = 2000; in.month = 1; in.day = 31; + fill_simulated_packet(buffer, len, in, Parser::ConfigMode::INTERLEAVED_INS); + parser.parse_stream(buffer, len, meas); + EXPECT_EQ(meas.gps_week, 1047) << "Failed converting the year 2000 to GPS week"; +} + +AP_GTEST_MAIN() diff --git a/libraries/AP_ExternalAHRS/tests/wscript b/libraries/AP_ExternalAHRS/tests/wscript new file mode 100755 index 00000000000000..5f4134d0ccb371 --- /dev/null +++ b/libraries/AP_ExternalAHRS/tests/wscript @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +def build(bld): + bld.ap_find_tests( + use='ap', + ) From bd1e7547f2a82041a8e563b3d0e6d9ad68a50a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Lindh=C3=A9?= Date: Fri, 16 Jan 2026 09:45:33 +0100 Subject: [PATCH 05/99] AP_GPS: Make conversion from BCD to GPS time externally available So we can reuse it from AP_External/AP_ExternalAHRS_SensAItion_Parser. --- libraries/AP_GPS/GPS_Backend.cpp | 34 ++++++++++++++------------------ libraries/AP_GPS/GPS_Backend.h | 8 +++++++- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/libraries/AP_GPS/GPS_Backend.cpp b/libraries/AP_GPS/GPS_Backend.cpp index 380730a9af6794..546513714d4c04 100644 --- a/libraries/AP_GPS/GPS_Backend.cpp +++ b/libraries/AP_GPS/GPS_Backend.cpp @@ -52,11 +52,18 @@ AP_GPS_Backend::AP_GPS_Backend(AP_GPS &_gps, AP_GPS::Params &_params, AP_GPS::GP state.have_vertical_accuracy = false; } -/** - fill in time_week_ms and time_week from BCD date and time components - assumes MTK19 millisecond form of bcd_time +/* + get the last time of week in ms */ -void AP_GPS_Backend::make_gps_time(uint32_t bcd_date, uint32_t bcd_milliseconds) +uint32_t AP_GPS_Backend::get_last_itow_ms(void) const +{ + if (!_have_itow) { + return state.time_week_ms; + } + return (_pseudo_itow_delta_ms == 0)?(_last_itow_ms):((_pseudo_itow/1000ULL) + _pseudo_itow_delta_ms); +} + +void AP_GPS_Backend::BCD_to_gps_time(uint32_t bcd_date, uint32_t bcd_time_ms, uint16_t& gps_week, uint32_t& gps_time_ms) { struct tm tm {}; @@ -64,7 +71,7 @@ void AP_GPS_Backend::make_gps_time(uint32_t bcd_date, uint32_t bcd_milliseconds) tm.tm_mon = ((bcd_date / 100U) % 100U)-1; tm.tm_mday = bcd_date / 10000U; - uint32_t v = bcd_milliseconds; + uint32_t v = bcd_time_ms; uint16_t msec = v % 1000U; v /= 1000U; tm.tm_sec = v % 100U; v /= 100U; tm.tm_min = v % 100U; v /= 100U; @@ -79,20 +86,9 @@ void AP_GPS_Backend::make_gps_time(uint32_t bcd_date, uint32_t bcd_milliseconds) uint32_t ret = unix_time + leap_seconds_unix - unix_to_GPS_secs; // get GPS week and time - state.time_week = ret / AP_SEC_PER_WEEK; - state.time_week_ms = (ret % AP_SEC_PER_WEEK) * AP_MSEC_PER_SEC; - state.time_week_ms += msec; -} - -/* - get the last time of week in ms - */ -uint32_t AP_GPS_Backend::get_last_itow_ms(void) const -{ - if (!_have_itow) { - return state.time_week_ms; - } - return (_pseudo_itow_delta_ms == 0)?(_last_itow_ms):((_pseudo_itow/1000ULL) + _pseudo_itow_delta_ms); + gps_week = ret / AP_SEC_PER_WEEK; + gps_time_ms = (ret % AP_SEC_PER_WEEK) * AP_MSEC_PER_SEC; + gps_time_ms += msec; } /* diff --git a/libraries/AP_GPS/GPS_Backend.h b/libraries/AP_GPS/GPS_Backend.h index 0266431fd4ced5..f779b36857d523 100644 --- a/libraries/AP_GPS/GPS_Backend.h +++ b/libraries/AP_GPS/GPS_Backend.h @@ -117,6 +117,9 @@ class AP_GPS_Backend return gps.option_set(option); } + // Convert BCD date (DDMMYY) and time (MTK19 millisecond form) to GPS week and time + static void BCD_to_gps_time(uint32_t bcd_date, uint32_t bcd_time_ms, uint16_t& gps_week, uint32_t& gps_time_ms); + protected: AP_HAL::UARTDriver *port; ///< UART we are attached to AP_GPS &gps; ///< access to frontend (for parameters) @@ -142,7 +145,10 @@ class AP_GPS_Backend fill in time_week_ms and time_week from BCD date and time components assumes MTK19 millisecond form of bcd_time */ - void make_gps_time(uint32_t bcd_date, uint32_t bcd_milliseconds); + void make_gps_time(uint32_t bcd_date, uint32_t bcd_milliseconds) + { + BCD_to_gps_time(bcd_date, bcd_milliseconds, state.time_week, state.time_week_ms); + } void _detection_message(char *buffer, uint8_t buflen) const; From cbf272fade4051d1b3e7a05129424cc6f382bf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Lindh=C3=A9?= Date: Mon, 16 Feb 2026 12:45:04 +0100 Subject: [PATCH 06/99] AP_Math: Add checksum function used by AP_ExternalAHRS_SensAItion --- libraries/AP_Math/crc.cpp | 9 +++++++++ libraries/AP_Math/crc.h | 1 + 2 files changed, 10 insertions(+) diff --git a/libraries/AP_Math/crc.cpp b/libraries/AP_Math/crc.cpp index 8c784d58ad0a6d..467cb6cb5741e0 100644 --- a/libraries/AP_Math/crc.cpp +++ b/libraries/AP_Math/crc.cpp @@ -272,6 +272,15 @@ uint8_t crc8_rds02uf(const uint8_t *data, uint16_t length) return crc; } +uint8_t crc_xor_of_bytes(const uint8_t* data, uint16_t length) +{ + uint8_t checksum = 0; + for (uint16_t i = 0; i < length; i++) { + checksum ^= data[i]; + } + return checksum; +} + /* xmodem CRC thanks to avr-liberty https://github.com/dreamiurg/avr-liberty diff --git a/libraries/AP_Math/crc.h b/libraries/AP_Math/crc.h index 68243bd16964b8..517a895e7a2bb1 100644 --- a/libraries/AP_Math/crc.h +++ b/libraries/AP_Math/crc.h @@ -29,6 +29,7 @@ uint8_t crc8_dvb_update(uint8_t crc, const uint8_t* buf, const uint16_t buf_len) uint8_t crc8_maxim(const uint8_t *data, uint16_t length); uint8_t crc8_sae(const uint8_t *data, uint16_t length); uint8_t crc8_rds02uf(const uint8_t *data, uint16_t length); +uint8_t crc_xor_of_bytes(const uint8_t* data, uint16_t length); uint16_t crc_xmodem_update(uint16_t crc, uint8_t data); uint16_t crc_xmodem(const uint8_t *data, uint16_t len); uint32_t crc_crc32(uint32_t crc, const uint8_t *buf, uint32_t size); From 9ccc00eee6b4de561b918ccfecfd84ec5a9c2c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Lindh=C3=A9?= Date: Tue, 23 Dec 2025 14:27:46 +0100 Subject: [PATCH 07/99] AP_HAL_SITL: Add support for Kebni SensAItion as External AHRS --- libraries/AP_HAL_SITL/SITL_State_common.cpp | 17 +++++++++++++++++ libraries/AP_HAL_SITL/SITL_State_common.h | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/libraries/AP_HAL_SITL/SITL_State_common.cpp b/libraries/AP_HAL_SITL/SITL_State_common.cpp index acb74cbe3e74c3..ed510160fcb0dc 100644 --- a/libraries/AP_HAL_SITL/SITL_State_common.cpp +++ b/libraries/AP_HAL_SITL/SITL_State_common.cpp @@ -268,6 +268,19 @@ SITL::SerialDevice *SITL_State_Common::create_serial_sim(const char *name, const inertiallabs = NEW_NOTHROW SITL::InertialLabs(); return inertiallabs; + } else if (streq(name, "SensAItion")) { + if (sensaition != nullptr) { + AP_HAL::panic("Only one SensAItion at a time"); + } + sensaition = NEW_NOTHROW SITL::SensAItion(false); + return sensaition; + } else if (streq(name, "SensAItionINS")) { + if (sensaition != nullptr) { + AP_HAL::panic("Only one SensAItion at a time"); + } + sensaition = NEW_NOTHROW SITL::SensAItion(true); + return sensaition; + #if AP_SIM_AIS_ENABLED } else if (streq(name, "AIS")) { if ((ais != nullptr) || (ais_replay != nullptr)) { @@ -399,6 +412,10 @@ void SITL_State_Common::sim_update(void) vectornav->update(); } + if (sensaition != nullptr) { + sensaition->update(); + } + if (microstrain5 != nullptr) { microstrain5->update(); } diff --git a/libraries/AP_HAL_SITL/SITL_State_common.h b/libraries/AP_HAL_SITL/SITL_State_common.h index 82f9488f95e624..fda5a77dd297ec 100644 --- a/libraries/AP_HAL_SITL/SITL_State_common.h +++ b/libraries/AP_HAL_SITL/SITL_State_common.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -166,6 +167,9 @@ class HALSITL::SITL_State_Common { // simulated InertialLabs INS SITL::InertialLabs *inertiallabs; + // simulated SensAItion system: + SITL::SensAItion *sensaition; + #if AP_SIM_JSON_MASTER_ENABLED // Ride along instances via JSON SITL backend SITL::JSON_Master ride_along; From 3867d52f112bf31e3f8e1160052c7b5c034137f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Lindh=C3=A9?= Date: Tue, 23 Dec 2025 14:28:00 +0100 Subject: [PATCH 08/99] SITL: Add support for Kebni SensAItion as External AHRS --- libraries/SITL/SIM_SensAItion.cpp | 398 ++++++++++++++++++++++++++++++ libraries/SITL/SIM_SensAItion.h | 67 +++++ libraries/SITL/SIM_config.h | 3 + 3 files changed, 468 insertions(+) create mode 100644 libraries/SITL/SIM_SensAItion.cpp create mode 100644 libraries/SITL/SIM_SensAItion.h diff --git a/libraries/SITL/SIM_SensAItion.cpp b/libraries/SITL/SIM_SensAItion.cpp new file mode 100644 index 00000000000000..b55bd74ff06637 --- /dev/null +++ b/libraries/SITL/SIM_SensAItion.cpp @@ -0,0 +1,398 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +/* + Simulate SensAItion serial IMU device + Converts SITL flight dynamics to SensAItion protocol packets +*/ + +#include "SIM_config.h" + +#if AP_SIM_EAHRS_SENSATION_ENABLED + +#include "SIM_SensAItion.h" +#include +#include +#include +#include +#include // Required for put_beXX_ptr +#include // Required for struct timeval + +using namespace SITL; + +// Constants +// Gyro: SITL provides deg/s. Driver expects uDeg/s. +// Factor = 1e6. (NOT RAD_TO_UDEG!) +const float GYRO_NOISE_DEG = 0.02f; + +// Baro: SITL provides mhPa +// std 100 mhPa +const float BARO_NOISE_MHPA = 100.0f; + +int sim_log_counter = 0; +static float rand_float_noise() +{ + return ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; +} +// [CPO FIX] Time Sync Helper +// Anchors the simulation timestamp to the absolute UTC start time of the SITL run. +static void simulation_timeval(struct timeval *tv) +{ + uint64_t now = AP_HAL::micros64(); + static uint64_t first_usec; + static struct timeval first_tv; + if (first_usec == 0) { + first_usec = now; + first_tv.tv_sec = AP::sitl()->start_time_UTC; + } + *tv = first_tv; + tv->tv_sec += now / 1000000ULL; + uint64_t new_usec = tv->tv_usec + (now % 1000000ULL); + tv->tv_sec += new_usec / 1000000ULL; + tv->tv_usec = new_usec % 1000000ULL; +} + +static uint32_t get_gps_tow_ms() +{ + struct timeval tv; + gettimeofday(&tv, nullptr); // Get host system time (simulated wall clock) + + // Unix Epoch (1970) vs GPS Epoch (1980) offset is ~315964800 seconds + // But simplistic SITL often just needs "seconds since Sunday". + // Unix Epoch was a Thursday. + // +3 days (259200 sec) aligns the modulo to Sunday. + // 18 leap seconds (approx) for current time. + + // However, the most robust way in ArduPilot SITL is to use the + // simulation start time which is aligned to the host clock. + + uint64_t now_us = AP_HAL::micros64(); + double sim_time_sec = (double)AP::sitl()->start_time_UTC + (now_us * 1.0e-6); + + // GPS leap seconds (18 as of 2024) + // GPS time is ahead of UTC. + sim_time_sec += 18; + + // Seconds in week + uint32_t seconds_in_week = (uint32_t)sim_time_sec % 604800; + uint32_t ms_part = (now_us / 1000) % 1000; + + return (seconds_in_week * 1000) + ms_part; +} + +SensAItion::SensAItion(bool interleaved_mode) : SerialDevice::SerialDevice() +{ + _interleaved_mode = interleaved_mode; +} + +void SensAItion::update(void) +{ + int tick1kHz = (AP_HAL::micros() + 500) / 1000; + if (tick1kHz <= _tick) { + return; + } + _tick = tick1kHz; + + char trash_buf[64]; + read_from_autopilot(trash_buf, sizeof(trash_buf)); + + // [CPO FIX] The "Gentle Start" Delay + // Wait 2 seconds after boot to allow ArduPilot to initialize + // serial ports and parameters before we flood it with data. + // This prevents race conditions during the reboot test. + if (AP_HAL::millis() < 1000) { + return; + } + + if (!init_sitl_pointer()) { + return; + } + + const auto &fdm = _sitl->state; + + // 1. Packet 0: IMU + if ((_tick % _periodMessage0) == _phaseMessage0) { + send_packet_0_imu(fdm); + } + + // 2. Interleaved-only Packets + if (_interleaved_mode) { + + // Packet 1: Orientation + if ((_tick % _periodMessage1) == _phaseMessage1) { + send_packet_1_orientation(fdm); + } + + // Packet 2: INS + if ((_tick % _periodMessage2) == _phaseMessage2) { + send_packet_2_ins(fdm); + } + } + flush_packets(); +} + +void SensAItion::send_packet_0_imu(const struct sitl_fdm &fdm) +{ + + // Convert from SI units to SensAItion units + // Acceleration: m/s² to µg (1e-6 g) + const float gravity = 9.80665f; + int32_t accel_x = (int32_t)(fdm.xAccel / gravity * 1e6); + int32_t accel_y = (int32_t)(fdm.yAccel / gravity * 1e6); + int32_t accel_z = (int32_t)(fdm.zAccel / gravity * 1e6); + + // Angular rates: deg/s to µdeg/s (1e-6 deg/s) + int32_t gyro_x = (int32_t)((fdm.rollRate + (rand_float_noise() * GYRO_NOISE_DEG)) * 1e6); + int32_t gyro_y = (int32_t)((fdm.pitchRate + (rand_float_noise() * GYRO_NOISE_DEG)) * 1e6); + int32_t gyro_z = (int32_t)((fdm.yawRate + (rand_float_noise() * GYRO_NOISE_DEG)) * 1e6); + + // Temperature: special conversion formula + float temp_c = 25.0f; + int16_t temperature = (int16_t)((temp_c - 20.0f) / 0.008f); + + // Magnetometer: Gauss to mGauss + int16_t mag_x = (int16_t)(fdm.bodyMagField.x * 1000); + int16_t mag_y = (int16_t)(fdm.bodyMagField.y * 1000); + int16_t mag_z = (int16_t)(fdm.bodyMagField.z * 1000); + + // Barometer: Calculate pressure from altitude (Pa to 0.1 Pa units) + const float pressure_pa = AP_Baro::get_pressure_for_alt_amsl(fdm.altitude); + int32_t baro = (int32_t)(pressure_pa * 10.0f + rand_float_noise() * BARO_NOISE_MHPA); + + // Periodic status output to verify simulator operation + static uint32_t imu_count = 0; + if (++imu_count % 1000 == 0) { + ::printf("SensAItion: IMU packet #%" PRIu32 " sent - accel_x=%" PRId32 " µg, gyro_x=%" PRId32 " µdeg/s\n", + imu_count, accel_x, gyro_x); + } + + // Pack data in big-endian format (36 bytes data) + uint8_t pkt[36]; + put_be32_ptr(&pkt[0], accel_x); + put_be32_ptr(&pkt[4], accel_y); + put_be32_ptr(&pkt[8], accel_z); + put_be32_ptr(&pkt[12], gyro_x); + put_be32_ptr(&pkt[16], gyro_y); + put_be32_ptr(&pkt[20], gyro_z); + put_be16_ptr(&pkt[24], temperature); + put_be16_ptr(&pkt[26], mag_x); + put_be16_ptr(&pkt[28], mag_y); + put_be16_ptr(&pkt[30], mag_z); + put_be32_ptr(&pkt[32], baro); + + if (_interleaved_mode) { + write_packet(0x00, pkt, sizeof(pkt)); + // --- DETAILED LOGGING (SIM SIDE) --- + // if (sim_log_counter++ % 400 == 0) { + // fprintf(stderr, "[SIM-OUT] IMU Packet (Interleaved Mode: %d)\n", _interleaved_mode); + // fprintf(stderr, " Acc(ug): X=%d Y=%d Z=%d\n", (int)accel_x, (int)accel_y, (int)accel_z); + // fprintf(stderr, " Gyr(ud): X=%d Y=%d Z=%d\n", (int)gyro_x, (int)gyro_y, (int)gyro_z); + // fprintf(stderr, " Mag(mG): X=%d Y=%d Z=%d\n", (int)mag_x, (int)mag_y, (int)mag_z); + // fprintf(stderr, " Bar(0.1Pa): %d | Temp(raw): %d\n", (int)baro, (int)temperature); + // } + } else { + write_legacy_packet(pkt, sizeof(pkt)); + //--- DETAILED LOGGING (SIM SIDE) --- + //if (sim_log_counter++ % 400 == 0) { + // fprintf(stderr, "[SIM-OUT] IMU Packet (Legacy Mode: %d)\n", _interleaved_mode); + // fprintf(stderr, " Acc(ug): X=%d Y=%d Z=%d\n", (int)accel_x, (int)accel_y, (int)accel_z); + // fprintf(stderr, " Gyr(ud): X=%d Y=%d Z=%d\n", (int)gyro_x, (int)gyro_y, (int)gyro_z); + // fprintf(stderr, " Mag(mG): X=%d Y=%d Z=%d\n", (int)mag_x, (int)mag_y, (int)mag_z); + // fprintf(stderr, " Bar(0.1Pa): %d | Temp(raw): %d\n", (int)baro, (int)temperature); + //} + } + +} + +void SensAItion::send_packet_1_orientation(const struct sitl_fdm &fdm) +{ + // Quaternion scaled by 1e6 + int32_t q0 = (int32_t)(fdm.quaternion.q1 * 1.0e6f); // W + int32_t q1 = (int32_t)(fdm.quaternion.q2 * 1.0e6f); // X + int32_t q2 = (int32_t)(fdm.quaternion.q3 * 1.0e6f); // Y + int32_t q3 = (int32_t)(fdm.quaternion.q4 * 1.0e6f); // Z + + uint8_t pkt[16]; + put_be32_ptr(&pkt[0], (uint32_t)q0); + put_be32_ptr(&pkt[4], (uint32_t)q1); + put_be32_ptr(&pkt[8], (uint32_t)q2); + put_be32_ptr(&pkt[12], (uint32_t)q3); + + // --- LOGGING PROBE (SIM SIDE) --- + // if (sim_log_counter % 100 == 0) { // Match Parser 1Hz rate + // fprintf(stderr, "[SIM-OUT] AHRS Packet (ID 0x01)\n"); + // fprintf(stderr, " Quat(1e-6): W=%d X=%d Y=%d Z=%d\n", q0, q1, q2, q3); + // } + + write_packet(0x01, pkt, sizeof(pkt)); +} + +// NOTE: calculate_itow removed in favor of simulation_timeval inline logic + +void SensAItion::send_packet_2_ins(const struct sitl_fdm &fdm) +{ + // --- 1. PREPARE DATA --- + uint8_t align_status = 1; // 1 = Aligned + uint8_t gnss1_fix = 3; // 3D Fix + uint8_t gnss2_fix = 3; + + uint32_t num_sats = 0x0C0C0C0C; + + // [CPO FIX] Calculate accurate GPS Time of Week using the SITL wall clock + // Replaces previous calculate_itow() + struct timeval tv; + simulation_timeval(&tv); + + // Convert TV (Seconds+Micros) to GPS Time of Week (ms) + // We assume SITL starts somewhat recently. + // Calculate seconds since Sunday 00:00:00 UTC + //uint32_t seconds_in_week = 604800; + //uint32_t tow_ms = (tv.tv_sec % seconds_in_week) * 1000 + (tv.tv_usec / 1000); + uint32_t itow = get_gps_tow_ms(); + + // Position (deg -> 1e-7 deg) + int32_t lat = (int32_t)(fdm.latitude * 1.0e7); + int32_t lon = (int32_t)(fdm.longitude * 1.0e7); + + // Velocity (m/s -> mm/s) + int32_t vel_n = (int32_t)(fdm.speedN * 1000.0f); + int32_t vel_e = (int32_t)(fdm.speedE * 1000.0f); + int32_t vel_d = (int32_t)(fdm.speedD * 1000.0f); + + // Altitude (m -> mm) + int32_t alt_mm = (int32_t)(fdm.altitude * 1000.0); + + // Accuracy (mm, mm/s) + int32_t acc_lat_mm = 100; // 0.1m + int32_t acc_lon_mm = 100; // 0.1m + int32_t acc_vn_mm = 20; + int32_t acc_ve_mm = 20; + int32_t acc_vd_mm = 20; + int32_t acc_vd_pos_mm = 100; + uint32_t err_flags = 0; + uint8_t sensor_valid = 0xFF; // All valid + + // Date + uint16_t year = 2025; + uint16_t month = 12; + uint8_t day = 7; + + // --- 2. PACKING (Big Endian - 69 Bytes) --- + uint8_t pkt[69]; + + // Bytes 0-3: Num Sats (4B) + put_be32_ptr(&pkt[0], num_sats); + + // Bytes 4-7: Error Flags (4B) + put_be32_ptr(&pkt[4], err_flags); + + // Byte 8: Sensor Valid (1B) + pkt[8] = sensor_valid; + + // Bytes 9-12: Latitude (4B) + put_be32_ptr(&pkt[9], (uint32_t)lat); + + // Bytes 13-16: Longitude (4B) + put_be32_ptr(&pkt[13], (uint32_t)lon); + + // Bytes 17-28: Velocity N, E, D (12B) + put_be32_ptr(&pkt[17], (uint32_t)vel_n); + put_be32_ptr(&pkt[21], (uint32_t)vel_e); + put_be32_ptr(&pkt[25], (uint32_t)vel_d); + + // Bytes 29-32: Altitude MSL (4B) + put_be32_ptr(&pkt[29], (uint32_t)alt_mm); + + // Byte 33: Alignment Status (1B) + pkt[33] = align_status; + + // Bytes 34-37: Time iTOW (4B) + put_be32_ptr(&pkt[34], itow); + + // 38-39: GNSS Fix + pkt[38] = gnss1_fix; + pkt[39] = gnss2_fix; + + // 40-44: UTC Date/Time + put_be16_ptr(&pkt[40], year); + put_be16_ptr(&pkt[42], month); + pkt[44] = day; + + // 45-68: Accuracy Metrics (mm or mm/s) + put_be32_ptr(&pkt[45], acc_lat_mm); + put_be32_ptr(&pkt[49], acc_lon_mm); + put_be32_ptr(&pkt[53], acc_vn_mm); + put_be32_ptr(&pkt[57], acc_ve_mm); + put_be32_ptr(&pkt[61], acc_vd_mm); + put_be32_ptr(&pkt[65], acc_vd_pos_mm); + + // --- LOGGING PROBE (SIM SIDE) --- + // if (sim_log_counter % 100 == 0) { + // fprintf(stderr, "[SIM-OUT] INS Packet (iTOW: %u)\n", itow); + // fprintf(stderr, " Pos: Lat=%d Lon=%d Alt=%d mm\n", lat, lon, alt_mm); + // fprintf(stderr, " Vel: N=%d E=%d D=%d mm/s\n", vel_n, vel_e, vel_d); + // fprintf(stderr, " Stat: Align=%d Valid=0x%02X\n", align_status, sensor_valid); + // } + + + write_packet(0x02, pkt, sizeof(pkt)); +} + + +void SensAItion::flush_packets() +{ + if (_buffert_cnt > 0) { + write_to_autopilot((const char *)&_buffert, _buffert_cnt); + _buffert_cnt = 0; + } +} + +void SensAItion::write_to_autopilot_buf(const char *data, int length) +{ + memcpy(&_buffert[_buffert_cnt], data, length); + _buffert_cnt += length; +} + +void SensAItion::write_packet(uint8_t msg_id, const uint8_t* payload, uint16_t length) +{ + const uint8_t header = 0xFA; + write_to_autopilot_buf((const char *)&header, 1); + write_to_autopilot_buf((const char *)&msg_id, 1); + write_to_autopilot_buf((const char *)payload, length); + uint8_t crc = (uint8_t)calculate_crc(msg_id, payload, length, true); + write_to_autopilot_buf((const char *)&crc, 1); +} + +void SensAItion::write_legacy_packet(const uint8_t* payload, uint16_t length) +{ + const uint8_t header = 0xFA; + write_to_autopilot_buf((const char *)&header, 1); + write_to_autopilot_buf((const char *)payload, length); + uint8_t crc = (uint8_t)calculate_crc(0, payload, length, false); + write_to_autopilot_buf((const char *)&crc, 1); +} + +uint16_t SensAItion::calculate_crc(uint8_t msg_id, const uint8_t* payload, uint16_t length, bool use_id) +{ + uint8_t crc = 0; + if (use_id) { + crc ^= msg_id; + } + for (uint16_t i = 0; i < length; i++) { + crc ^= payload[i]; + } + return crc; +} + +#endif // AP_SIM_EAHRS_SENSATION_ENABLED \ No newline at end of file diff --git a/libraries/SITL/SIM_SensAItion.h b/libraries/SITL/SIM_SensAItion.h new file mode 100644 index 00000000000000..6f95d440e475ad --- /dev/null +++ b/libraries/SITL/SIM_SensAItion.h @@ -0,0 +1,67 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +/* + Simulate SensAItion serial IMU/AHRS device + Generates high-rate sensor data for ArduPilot testing + +*/ + +#pragma once + +#include "SIM_SerialDevice.h" +#include + +namespace SITL +{ + +class SensAItion : public SerialDevice +{ +public: + SensAItion(bool interleved_mode); + void update(void); + +private: + int _tick = 0; + int _periodMessage0 = 5; + int _phaseMessage0 = 0; + int _periodMessage1 = 100; + int _phaseMessage1 = 0; + int _periodMessage2 = 100; + int _phaseMessage2 = 5; + + uint8_t _buffert[512]; + int _buffert_cnt = 0; + + + void send_packet_0_imu(const struct sitl_fdm &fdm); + void send_packet_1_orientation(const struct sitl_fdm &fdm); + void send_packet_2_ins(const struct sitl_fdm &fdm); + uint32_t calculate_itow(uint64_t now_us, uint32_t start_time_utc); + + + void flush_packets(); + void write_to_autopilot_buf(const char *data, int length); + + void write_packet(uint8_t msg_id, const uint8_t* payload, uint16_t length); + void write_legacy_packet(const uint8_t* payload, uint16_t length); + uint16_t calculate_crc(uint8_t msg_id, const uint8_t* payload, uint16_t length, bool use_id); + + uint32_t last_update_us = 0; + uint32_t tick_count = 0; + + bool _interleaved_mode = false; // Now mutable, Shall be driven by EAHRS_OPTIONS. TODO +}; + +} // namespace SITL diff --git a/libraries/SITL/SIM_config.h b/libraries/SITL/SIM_config.h index beb80e5d310885..0fbc60511fc558 100644 --- a/libraries/SITL/SIM_config.h +++ b/libraries/SITL/SIM_config.h @@ -183,6 +183,9 @@ #define AP_SIM_GPS_UBLOX_ENABLED AP_SIM_GPS_BACKEND_DEFAULT_ENABLED #endif +#ifndef AP_SIM_EAHRS_SENSATION_ENABLED +#define AP_SIM_EAHRS_SENSATION_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL) +#endif // simulated compass support; currently only in SITL, not SimOnHW: #ifndef AP_SIM_COMPASS_BACKEND_DEFAULT_ENABLED From 506d0374ad8152c71b2a5e58d67fd50e275678c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Lindh=C3=A9?= Date: Tue, 23 Dec 2025 14:28:23 +0100 Subject: [PATCH 09/99] Tools: Add ArduPlane tests for Kebni SensAItion - One test in IMU mode and one in INS mode. - Also add build option to enable SensAItion as External AHRS - Also adapt to updated AHRS_OPTIONS definition --- .../KebniSensAItionExternalIMU/ap1.txt | 7 +++ Tools/autotest/arduplane.py | 52 +++++++++++++++++++ Tools/scripts/build_options.py | 2 + .../AP_ExternalAHRS/AP_ExternalAHRS_config.h | 2 +- 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 Tools/autotest/ArduPlane_Tests/KebniSensAItionExternalIMU/ap1.txt diff --git a/Tools/autotest/ArduPlane_Tests/KebniSensAItionExternalIMU/ap1.txt b/Tools/autotest/ArduPlane_Tests/KebniSensAItionExternalIMU/ap1.txt new file mode 100644 index 00000000000000..ab2be0d1a45470 --- /dev/null +++ b/Tools/autotest/ArduPlane_Tests/KebniSensAItionExternalIMU/ap1.txt @@ -0,0 +1,7 @@ +QGC WPL 110 +0 0 0 16 0.000000 0.000000 0.000000 0.000000 -35.363262 149.165237 584.090027 1 +1 0 3 22 0.000000 0.000000 0.000000 0.000000 -35.361553 149.163956 20.000000 1 +2 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.364540 149.162857 50.000000 1 +3 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.367333 149.163164 28.000000 1 +4 0 3 16 0.000000 0.000000 0.000000 0.000000 -35.366814 149.165878 28.000000 1 +5 0 3 21 0.000000 0.000000 0.000000 1.000000 -35.362947 149.165179 0.000000 1 diff --git a/Tools/autotest/arduplane.py b/Tools/autotest/arduplane.py index ef617bba20a26a..6800d3025f751c 100644 --- a/Tools/autotest/arduplane.py +++ b/Tools/autotest/arduplane.py @@ -3252,6 +3252,56 @@ def InertialLabsEAHRS(self): '''Test InertialLabs EAHRS support''' self.fly_external_AHRS("ILabs", 5) + def KebniSensAItionExternalINS(self): + '''Test Kebni SensAItion External INS mode. Or in Ardupilot terminology, ExternalAHRS mode.''' + self.set_parameters({ + "EAHRS_OPTIONS": 4 # INS mode -> Bit 2 set + }) + self.fly_external_AHRS("SensAItionINS", 11) + + def KebniSensAItionExternalIMU(self): + '''Test Kebni SensAItion External IMU-only mode''' + self.customise_SITL_commandline(["--serial4=sim:SensAItion"]) + + self.set_parameters({ + # External AHRS configuration (IMU-only mode at 1000Hz) + "EAHRS_TYPE": 11, # SensAItion External AHRS type + "EAHRS_SENSORS": 14, # IMU(2) + Baro(4) + Compass(8) = 14 + "EAHRS_OPTIONS": 0, # Legacy IMU mode + "SERIAL4_PROTOCOL": 36, # External AHRS protocol + "SERIAL4_BAUD": 460800, # 460800 baud + "GPS1_TYPE": 1, # Use SITL GPS for position data + "AHRS_EKF_TYPE": 3, # Internal EKF3 (fed by external IMU) + "EK3_PRIMARY": 0, # Use external IMU (SensAItion) as primary + "EK3_OPTIONS": 2, # Disable EK3 lane switching + + # Ultra high-rate INS filtering + "INS_GYR_CAL": 1, + }) + self.reboot_sitl() + self.delay_sim_time(10) + + self.progress("Running accelcal") + self.run_cmd( + mavutil.mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, + p5=4, + timeout=10, + ) + + # Wait for EKF3 with external IMU to converge + expected_flags = (mavutil.mavlink.ESTIMATOR_ATTITUDE | + mavutil.mavlink.ESTIMATOR_VELOCITY_HORIZ | + mavutil.mavlink.ESTIMATOR_VELOCITY_VERT | + mavutil.mavlink.ESTIMATOR_POS_HORIZ_REL | + mavutil.mavlink.ESTIMATOR_POS_HORIZ_ABS | + mavutil.mavlink.ESTIMATOR_POS_VERT_ABS) + self.wait_ekf_flags(expected_flags, 0, timeout=60) + + self.wait_ready_to_arm(timeout=120) + self.arm_vehicle() + self.fly_mission("ap1.txt", mission_timeout=120) + self.disarm_vehicle(force=True) + def GpsSensorPreArmEAHRS(self): '''Test pre-arm checks related to EAHRS_SENSORS using the MicroStrain7 driver''' self.customise_SITL_commandline(["--serial4=sim:MicroStrain7"]) @@ -8001,6 +8051,8 @@ def tests1b(self): self.MicroStrainEAHRS5, self.MicroStrainEAHRS7, self.InertialLabsEAHRS, + self.KebniSensAItionExternalINS, + self.KebniSensAItionExternalIMU, self.GpsSensorPreArmEAHRS, self.Deadreckoning, self.EKFlaneswitch, diff --git a/Tools/scripts/build_options.py b/Tools/scripts/build_options.py index 54c03026f9700e..88f0ad5122efcc 100644 --- a/Tools/scripts/build_options.py +++ b/Tools/scripts/build_options.py @@ -49,6 +49,8 @@ def config_option(self): Feature('AHRS', 'AHRS_EXT_VECTORNAV', 'AP_EXTERNAL_AHRS_VECTORNAV_ENABLED', 'Enable VectorNav external AHRS', 0, "AHRS_EXT"), # noqa Feature('AHRS', 'InertialLabs', 'AP_EXTERNAL_AHRS_INERTIALLABS_ENABLED', 'Enable InertialLabs external AHRS', 0, "AHRS_EXT"), # noqa Feature('AHRS', 'SBG', 'AP_EXTERNAL_AHRS_SBG_ENABLED', 'Enable SBG external AHRS', 0, "AHRS_EXT"), + Feature('AHRS', 'AHRS_EXT_SENSAITION', 'AP_EXTERNAL_AHRS_SENSAITION_ENABLED', + 'Enable SensAItion external AHRS', 0, "AHRS_EXT"), Feature('AHRS', 'VISUALODOM', 'HAL_VISUALODOM_ENABLED', 'Enable Visual Odometry', 0, None), Feature('AHRS', 'EKF3_EXTNAV', 'EK3_FEATURE_EXTERNAL_NAV', 'Enable External navigation for EKF3', 0, 'EKF3'), Feature('AHRS', 'EKF3_WINDEST', 'EK3_FEATURE_DRAG_FUSION', 'Enable Wind estimation for EKF3', 0, 'EKF3'), diff --git a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h index b0ca8bdf01febd..6945f178bd66b1 100644 --- a/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h +++ b/libraries/AP_ExternalAHRS/AP_ExternalAHRS_config.h @@ -32,7 +32,7 @@ #ifndef AP_EXTERNAL_AHRS_SBG_ENABLED #define AP_EXTERNAL_AHRS_SBG_ENABLED AP_EXTERNAL_AHRS_BACKEND_DEFAULT_ENABLED -#endif +#endif #ifndef AP_EXTERNAL_AHRS_GSOF_ENABLED #define AP_EXTERNAL_AHRS_GSOF_ENABLED AP_EXTERNAL_AHRS_BACKEND_DEFAULT_ENABLED From 13df895ba120889837f6bffa9d22e658403d6818 Mon Sep 17 00:00:00 2001 From: Arwayday <3521301631@qq.com> Date: Sun, 15 Mar 2026 18:46:02 +0000 Subject: [PATCH 10/99] AP_HAL_ChibiOS: add HWH7 board --- libraries/AP_HAL_ChibiOS/hwdef/HWH7/HWH7.png | Bin 0 -> 220344 bytes libraries/AP_HAL_ChibiOS/hwdef/HWH7/README.md | 125 +++++++++++ .../AP_HAL_ChibiOS/hwdef/HWH7/defaults.parm | 5 + .../AP_HAL_ChibiOS/hwdef/HWH7/hwdef-bl.dat | 67 ++++++ libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef.dat | 210 ++++++++++++++++++ 5 files changed, 407 insertions(+) create mode 100644 libraries/AP_HAL_ChibiOS/hwdef/HWH7/HWH7.png create mode 100644 libraries/AP_HAL_ChibiOS/hwdef/HWH7/README.md create mode 100644 libraries/AP_HAL_ChibiOS/hwdef/HWH7/defaults.parm create mode 100644 libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef-bl.dat create mode 100644 libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef.dat diff --git a/libraries/AP_HAL_ChibiOS/hwdef/HWH7/HWH7.png b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/HWH7.png new file mode 100644 index 0000000000000000000000000000000000000000..2f8dd1cd0073098986ef3efaf2c8ef10cdcf05d6 GIT binary patch literal 220344 zcmeFYXEdDu_cp4R(V};vi(aD>El3E0AbJZzbVl#Jlc>=pI-~dAnP^dl=tO52qPOSH z_xt<(*Lm}t^{lhbd3WwN)|#2SeD=QXYhTyiA5j`_l<+`QAS5ItJQZbyw@656o=8Y2 z%vcz}Csqzw=SWCTkyI39wLP;Am%V&xtj%N|CTQAzR@EnS=@+xCHqo*<%MpDJO%7ec zZozu?{Y&8&jNPBW?GDq85vDVk!d7WBCx-M z;ws~m*2kKhUGnF=pz%^gpM)8*&_V9)i(#i#)j5@}#%iPn_gjb_ANx**lRMrs(+$ zNFX|?InK!c1WBA+!c3xkI&Yr9+u=O+u~!`yXyuRx*Y@L&JANU}j|=K+GFz3+Tg%6N zbG4M$HvfjaE*yaT7!w9Yy!*ztcF@;}k6AMZFKs31A$GVeC1tKBN)Y3?2`Nt`Of4)d z&uGKG_|=VkwhOrxC+-y|m4n|ovLU?dLyz~2PAk+-(=Ea+w@MNxy^j7NM**V2pHWrv z3h1Cs?HDk_7Sb^~)k4` zP|}i5@1yD(jiNtU3o}W_Z4v!_E2ozJzLIB=!HQ$tC9vEctuC>^N7suF{YC9u#3R2& z6C;_VL+aw5=@fcim^^?UXD{^a{r@=FkK)ARx=DExtoVR2S8mIj)>xe#hx$ApE06or zOlai1?m-pymBB(TG?)Z_6frzrBGQ*B>YHj`6$rj*9^Q;&A}%ghBeFl>yyJz0%mk2N zipzuxJKnJQ9&jEHdg%;Qxf9*`UtM~xMm*4})9Fmsd!z5qYGxfCzTmCuNi%++Z?|T# zu4ZKf4i2j<%Z$kFlzzKMlH+y~&pXG}K$(YKzpLv9S&9Lkev0mgTz^87+YzkBj_Y@Z zqMw(wJ{reJta#$r7t;+U7{6&*xn!sFyK;^-cA<6<mpkQCJE}Ht>uigmGtAjQ=x!+@}12LEcW|zkS2}Vlz8{aV> zQ5t0K*;gLF@9-J_=17ovgfyS=K9Ksr0-ii}oMDMy9Cbt62sSKXvXW=n5{R;o;S&E~ z=lfqy7~_rlboKSo8!5M|5ScdsVbr`|aX#;TZ?d7a`oJCU$wIx&@B1iaP~&m*>xY$a z>hmV^zVm$#Pyv|?{?=PV&*^hdon`G#%)jYj z%v2Xuh>6^${eC2HcMjw2GmyFCl)UMSK=fhK^%k!_CG8hjvbiztS4H{6>3j zbk1rx#d`V?=fg*|e&5xaJ)s-IGbG)?=WbKGw0lhu}f8_6O9F0Tl z?2w*@Wj~(r<-9VCftak4In7nIt4Fgy+E5_z_fHm0mVywAX=X6ah`gb9n_!b0%yz~t z83`}LpoJP@2+f?+xxPp6%x$I2;C_=-q!}!EtF>wUHsTS-kxM3exh-B!?2*^``jj&B zKhOfHtxuk($2?hszS2CS0!yAlV^=>C=zIFq z{b^vG`E(QkbTZyomo}XU)aKS_cZqD@KVYW{p*${5N7NgKJs_jr0IZ6*^zf4Is#FVm zUWQuTmwDQty9tqLC2BqJdh3Sz?j`c>J;knwMq@sq+7IwQYarwLX9Z*<@)Q&lsCO93 zl&HHh=v-x`&z2Theul=ZAb78ChGp$l#sI`t_&1`9c5=7=?4=os`|$s-Nd5zxhymJ@LuTX47-q6SqX4J z9!PS$Pd?dTAx`@nn>HUEa6IKWP;^rA?exm-1)s1Ptv>0s)}!oo_*9pdczK|daNF3vOOp0{a+@qz zulg@)!GJuOs%XAW)!FqtSv{F8exv5^WQqB3mh;%l`PyNSTUYe9lo@e56cD@|V*HwB z5IW(Yq`UC4!oXnrJXGZ+SSolZXS&*L^JV?`)yX#j`V|Y@(?Nd8iw4KdXBJFVzRL@d`DAH^{$d|59*|Z&ZRp?9@lL&c?_6Fgv{CSyN6u}h z?`q{SC;d7v=ARsHqvKC87U(-~E$8<51YCOD^&P>?CUU#bb#uEw=T^2^;-^IZ58VM9 zJ`($gc*i@ustM|dZ=2mAjzeuL;O7pl*RR@d4jc4pn~6TwQv3Md`}v+9NYrd+6Kq`Q zjMsm_J?eVEhfC!^%g1K0S{Iu(i-C|zd~_Jleb{JVuQE{+O-d2jjHi|kY4TsEC*vi}DMp9qSIhYx!SK zrrI2O=5r(vgRkezorU7^i7{)=z>hIUF^^O-H;1!W0o#{Yb6wXC?YBJ=Ct=`Dy8D3Z z=Y|0f95baVe{v;*jd*d*4u&4^n;H<>d;hX%>|YiIeLp(C+KC8=Z7{h)dDt|d@40+% z#$`nkTM4e?>B7r>oWev@K~ovKEYwg${7<_OVUPgSRsY>w#Jx{=-d7|`%BVTr^;QCZ zoZPEPyy35X^M`}Pxd8+@!*|c19HYc|Hb$;wJf3G#S2JxZ(VV(r2MDdZ2>ZbX@KW?F z!JPy~RV%4T1!$L*S-0@bLc;O=^8M8i18q@7fycR>)0s7WFTj3wiDSA%CW0*TlNr>J_!*$sXc|<5XxI zc^56Y)2Y>8)D|SO>8DzaoxgfAg;I>Sp)iE~ro{+BzRvzSIJ3r zSCrStqP}i3q=HMWIU~fYXi#!o8E7dZF8;<;`;DoymLL_8>VK#O<*)QxATO0YE29!3 zKQ9p7PlL$cb5;KQLD6%z8h|uWzYGLgo!-vvaC$BL>2%9HJ z{@f5O$YY-wL5%&+6*WixuU6&vKMMPw%P?lr{eSOm7?uMb?``bt`Zyu&0L!-la)0lyDEaYJWD_I&c8!r`hD}BRuRd^nwS)`oE zjGg>8sAvoVl=W*``(u9JRgpWpAw)ms^L!36d9MENaJ5y1%h-U||0-i4+I>R^(p0eZ zzA}qN^X_=UMmQ;>)-1pOKD)91MqBg2xw`2Dn7*%TX$EBgQTsdRcnt#gycj zGiu(ku$y{Y$v7K;>yzt;yZQ(NuO$sNZ5|N#A~#jW{_e1)4Ie^iuPy=Y zPfz2I5fE^n(p(S4rY{!E$ zsAMnmS2xpSZkbp9q_V@ij(P0M$E0zQ5_`E8@4xN+&!UNdMNbz{88|#Vk=~PzAq(sw z?+g?%dBpZTfi9l>UNwqLe;c>IkM*Nao9Vd;zyMDCp{ z{8RHfqv?>~M7|bkpi6~e3x;Sr`R#467S++P^x<)g=~!7x^@OE#HtZkYM5tM6-YqY* z^N;ulq#fz6zD|B!BYtln`S3^&67ACg1$f>$OIm;G8Gsn4h*`|J8UJU&-=l#A(@-y; zW6}A%>H8T+J(a2Qo`cs9X9c0uZE@*~@_2n(y>swJ>WszrE=E0nzZJZuX5i}QRCMyzn;&on@!ba`?UZS>S~UqL7C1&w_H zuyoq}LO`3Af;deY$hc=YexeRl?|>6FeJ*993qKuq!=%sNRXBU}Wc*KDm;oXYr~Tm| ziMYO8i~5mUTTvi66*wnsy&f#G8lv8DP}%IF>=U zQY8+oDDb{UZ@$Sk0kpzRW%D;H7dgTIiQQfj7qoWUn`nXCM$CMx`+*?@|6f)n19}-LDBp?VI7RXhZe9OHKMI@vKVA@s z&;JwF`#&H_4rh6JdCxynO3uHBlUj^P|II`u6F{-Ame%_soHye{HV{Mfx&I~Q0(ih@vvm3|`g$k{_4OqBl8rvrBXFZXKy z0*d6A*7Q*Z@%FhZ?G*{=+g*-?Zol{s_laj+$D`LiWM|9&fLn79D1Jif&8Q*O$a3?O zJ>}~G;w^pOO8t#FmOeudq%Xk}lm8HxjhPwn!w66qjhP(5v`~z9=VxY>94V;}j7TT% z!&h%oUNsAvc%P6ksa6kl|8qvkpTD>o>kSw8m*T&J*Ku+39zL5fy##?jh}W|}ujWXZ z{6>P3lV@uE+~4k>7-Sj%F@8gA9nz{%vPpi5hAvrTi-94z;RrDnmo-zLtS*yFh`=t5 zrj34{cGr2(^2B4D5%P%X-_h91mG)0yw%LG+UB}M;gPmmGGfdK$Ap(nPNy;SYSc$*y2gon=`pE{`v-*c(t^CVNY6tS?^R7H0kv0qnocfSphtxO)H&)YY zy&E8qYFhlCF)_nL&ERM6JF(nVr$fyskXSJ#;C54qV43aTr0`HFcb`e`h`kXZld)nt z$uP8g`X^oY+I@863-Suz^EA|FO2=`Aq7I@;W=JzRKW|y=0gS@JP{Joh8}_m$rJql} zda0@70e|{dF;qO&iWJwgf8e_5mLD?M<@sjvc#V856x-+3caa3$^yem5VQ&5~_-adf zHRj1GYcp}Kk*g6IQA$DSbL}M%p1z0l;89ZK_Gh`EH|e(_q0i{`pXbowdfKe8lN6A6 zFYjrn%IcLg#hc`9x;uA8=yqFBV7M9aQoyg!rOf1VjcRG(QX(!ML_jg*vjDZ7&WIxMvtmk?jqJgfVi0$;s@`pTUm)XJ3&H*N{(g!9$#D&q!{X(>5=u z!?8W0k7l)Y+oYGXq&)k8U@S7dV{>Dxf<3$L~iA?v6``G&o;l=y=1wauqB_-yG35CJb5a<6ZhXI&kd` z$!=T~pFDJRB==wZkY8vEzrFH#`;5K5i`{K<;GiL_8PyFe|6OCEIqxK5k(N+RH+_xO z-fOQR_Nm$EP#&xnU!acJPP!q-MmFQpFgor`Zf&PyS-{B1{D+E^!DPYc{|fi!@xiSR*u@;CQ;E$)^zV} zODbVGaqquGIFT)v!qU|hLg)H0wwk(KD0d6$@=Jy_tok|lFD96z!_W2dZ|_zTB8Mlm z`;IbjdqYNt_^d4Q?J}t_B!xD6nR^Dl4H`cx?(_vL=AGk#B>JBV#ZA!a93Wo-Fq7cl8;Zxrn|ZR7#6hrZh~OBI|a{sj@U?Pvsz|7wV?zt1c3Z^4D6{jbdp0}KoKT}EP8**Hw{v!#o;|*(M z>caS;8X@I>*#^sAqCG5A#CtZVAa%~zzKYbzbLA)=@tzL|mkOZ&DEFakyHMW_j z_TBSnzNuMUQ?-}zho0Ekk#g|jq-4uN5n8Idv>)ChT?({66!nUZ2#iIV_kNWNi(R$L zKHfTiD4y0vrR(Ek1(I_ab!#oKh7EF2IN7{gEa&s+jJU09lYvOAIS1tA<-f=E?8T-& ziCh)jeGIzg$B;am)<(&ZiyNy>VSAW!6DKXxLOZg%f50ybxx(f!SSE+o$_X20O}Lx* zbOfaPnWi6ozI|Izni2Pj?NFctg?8s?FMLH7@~l3`Y*Z1PLB4(1F}@XZX-&&42_Wo~ z*;|Xt)$v!l`8&~F*)aH}%yKV=r12S0MI`)t)N7Cl)=Y-Us87(&HxM_6#>na#1mF3j zy!0r!e;l&|EJ|E-nJR$`z^>VG`Vd5YLh{j&%%H`bCFV`B-1-}h#Lb3dTtT6Eb$;Xo zSk?!BJgS}A-f$`ZgLk`vPp0m+fmfwLq`hkSh{HKg$X-lz5jmkQ=h>(Tc!7QU-i_z$ zc|s{rezmrB5vQ+1LO_&|or$5uq2rD||7~*=IwM#y=}F|`IH*`+IOAjYCYR~G2O~h{ zd=ja0tB)D1B|*ncg?Rg=o8%`X`vaS#Xp@IHH!CrCF1X{=X(Yg$oAkZFpkIPVPI!k+! zBO0ZT?^KS~+G?hpJOZ`@j*x)o4P9RcKkznpP3KK`7vU^66M#II)^p4#^22TU*D|QX zQ0O2|BC2_t6_(fc@bODmk>IEgZ#9;C?H)H^x3hjPdX5{6XW*dy*YikMhK4+DRN9>G3A$Ka3``;*q+KrS5vMA;B}8lL+b@VSfIk(vs-oZl5Zqrjf8p zEH6uiLQRFjDHZfj`E-(Sx8>tAOr7?7lnQeXb~FEkFb*4Qvid^Ou7)exdyi`Ps&t9$ zMw9f?`Z!OQ)6ntdSiHy zPhrCdkpBJLx$2?_Q3?m-)9mruhxFR1V{uh#`$kkc=ac#Jj&)yN1l10*ZH=$>@YszK zABLp1P?ubh#`=bcDqBe)zuGhqC>Y#b!e}+<%wztT>BkdpXlUX#MA6dq}s6e zSEC00`PSs=tq!JrH|Kh74zxmQXMi+mXe&;DCHY?N@MZc$A~-ouwHa#D)t3_Xjas4>Rr4 zZv1L7)ZB4e<%L`5lLJjaQkK7}@9^qe4^i%fjxGrg{?JH$>klR1@GPb}LF&G`5^Zf2 zPkOW0jZ0#l%-q9=D>MV^6H@#3_E|~UKJ`g6XQ`}|NbkFTZm67i_De@cdRA5;<#A)# zTC}HH87W*ywSMR?-s(kS)*?ac6&^631{B-rUj#5r3uuyQPR?r_d-aRicW)YZQiEIT zpuF>hFf%~h^86fsm0~ivDeXNtN~Wf(5yDzRUSdWu>F)QoWYc{SQYDZb$Y%5Pw9ON} zLKX^!MEESX&!t6fH<<$`uSuwOYdHkl;s19pz^*__;86L)y8Dw6eF(`pXj{?Rlz%OM67+q@A3Yj2B@8jx&>v_gx3&uk>Z@jD)P0mE%|1tHenSQD^>dmzsrH%$g?vv^ z=?xG5pm<&IeqQh(@0v3t0=8OrP1}5o128>l?f8q=F!$f@4?Pl6O9cg>iKaFlR(e_a zP<^a}R_Rg!aUdC-zxi=dlbL0wg}(iy`pu5S9{5e}9izq!J2Q4*8A@_!q@E`;3MmmI zM+^rq29g&4(~J-P-G#`3_%DL2kXbFeXUx)Bh15c9^)-A^XuIC^=9XVM@uXra6Gft6JVXscWs29Ecvk6EV^Jy z826bgn!acMjR1KTQT++^!zWZT$$ze*LCvbVd*on!)ldr5HTdadxvv|CHjg+|S`+3( zr+oSmcAdoh%;1B!)L^2uKx1Y}o*XG31ZB{u2L$r$#a}pmj|n-Kq+JcW?aT{ZhSYL! z1R&L>XcL~;WTt+qUWDctcQehY3%W>oDwyfNdzZ@nW#D*Sx^>rK&Bu?;ICqxzWWFaE zyBlGoe9^$H5@;)%beQuj5M`j=ltbXh^vPl>BXOYZ`$Sbvm@Q^?gdHUj5^<3w!FsJ} zPi-Qe=Z|N&)JW|SAkTX>L0;8kTIawv&$%@RT_ zb|5s~SwRW9-At*(ZVK~`Quo0pd4a`~7XJB~lgO#1seR*58`EKgQO?K`7Dtzo}#8QJOum#b`*5G3BOv(f1z~VN`|CzRqq7ap$ zzCNY4&X_c-caQgZEDrI0bA)UJwokvfmrXN%H1EWV$+_4wd)@;cm%pdLSkh_~) z_TI6t5e6k;dD=MWrAkc+KcPV*A%;6yi{IYG=ZQJ`xTjp)d{XRtJN6@hg$I86+(rs0 zaBcO1SL=^Br^MP#ClUpRRTiT1G?$9g_3by9iHm7*2?-Xarbrzf9lOWJLqK5}92^8n zXB|Laj|~Ovkpt!>6%`8Z?!skdWg1#qA&N0%zymY-ym$#aw|}aw+PyhzS36nF9r&4t zIHe#Dby`*me78Rbf8xsl=)NpUO3I$@ZX^s03_byY;HfE1Tmk~74Al?Nw>8F$Yf(d+ z?7X~6ii+s_`}-OuCJDeF`)l@5t0{{iKWF-^)0;)3_$Yy2`m3hHpA(P?yg0OxafnZw zLues|AK@!f<5knf)U7`EfROXc$=JS>5_6R3;#5hVoN~sUvY{l3`sfq99*JuNkpnl; zo(|W>Dl>1~=1enyET@I0rl)aUy!7)!)z%3rC@GoboLXIth>iv^6GgSJ_VE73CbkE3 znh)sESYqRn;G=5ujvv-^NW_Pi`Ns_!A>&4GVpg@##KuSP?^`WntRnUZyAva#3F{$$ z$_+@UQdxp&E>{C-Cu3t{^UBL{hT>`EfvjR+VkW&QKBH1*yt?>EGk?3M?9ryFL#m^r zV`FP8YhXZ4#%mnAzyA@yhBn6*<(jCvxdsqn$xOO>UY_S}JB=zr649SNH#-c?fAA zs+-Z!{5-|WmoH5zFaV73@$(0{iN{$AWcYQiw&wE49s|xW`rP4@QG9gtD+N;YWb>YS zW;Zt)S@5MwNf}6O+G+o{ie4E74}W&>XfQJ+pBj2f%9gS=47mtfKi+(6)GBhQJgHP6|cw(uV(!LaZP7VvR7MjkiGa0Di>8qqT=nwB`YC4tVA2 zLTE3m9l?p4xUN~Sz>cmxIQ*rLj|4uAXkce&r~T#!b93{b&%BnKvfJ_bo<#@S2!W7F zN=mw;NjauBQSeM=-6XG@YmuQg(JW1sz?u8AW<} z`mpqnUG~!51!BWg0Ex@Eox%=QHLyQhUSm0ud{|pBWz(3mUg7FFE2R*rZDYfEKqq$C zBku(Q99~Lw_2%X>sgSjqpr~jh9PZ%e?w;S)Mh$d=DX6KTWbbhpW4>5%nRJ52#xe>I zqEVJ7>uk?E{ciKpL4ys>Gq9N#Iwajw+%9EYNE*U4GSX;;g*eH~g`J(0&{XaSGm1zB zMqq%pS05UMO?!LUWZ~(_=Oi6pnz)7au3{xHoYIIR%uJsw*`ja0#g%+?bo4AfezNx| zRY-KfQ@Vu0{Cwo|^K-pgOYF?d%vz(OfhS~|r5uf?+YTq-nw#831{w-aAMu#Y^1UT}~psS#b<%(fX3}8rx_0l@{D7Zq)eX+$w_uHiarQ_8l8p(ry0R+YjI8D^|ijS3* zRkAn~YjMQ|zL|ZgpPIr=ubw3u^qEb4qh%+j?^lDJGh%?eu%?FpwsYKNtem{%?ERCY zmzO>`IgTjWp10T-4=(ICts-3>I;5;XxzqOCcpC^*8ViYvb;O)Gp6z zhfdPuEBvh_qxDo}y;@?G6~fIqs=2U}lmWs*Q=JYhAE5%K+fH5X;( z2l9)Q6vBRUfzeURlCl!d_mKl6%tX$5srZVTiLbsVzL{9cdQr<|fvn)pGO}Wuy6vG5 zw%3|N`%*wsGB!V-;c&5uAelM8s0c$%O---v1Kzs|y&wg~B&mWOoffpa2@FO!`2;w1 z!C1;G>s13yaVY}i9(ac>f4{wi0E9ufcg%VhEiLWU^+Z7Y`1rUzq(Lr@6G;R0V_*2X zIDeoWrMVGMrY?>aBkpf6Ew)Be=Bv$6fBpI;fT{k|t0&&I`JF~Lu?K#Rx%Y_^HFKuX z0LGv{3`mY~Kn!YwQUcuoe7S9EYAQ&ZBZXE|qr?}NgF47*tB@TPeDo`9eEiLl|Lu{@ z$B)5P2RT+*KAA`m)MUorgcGg={gtXED>@~eu^Y0>Y8Ty~U08_f@VVSL zS?{BuqC!cODBCuq-_fXed(>0suq@ENj;G6z6xk<)tP}S^T@CIAZ^3K6_~q(?{$0F| zy2J{3T;Odo^OvE~e2(kmg1t|k%Lk3!eC`TNnv!R2X(z+#{xMNeB+RznUGf^b>Y0A< z=w9u4cXY%ra}QwBh%2u;iKF~;Ce0#~h|BHs5`e0=lAhx-x;WT;H1yxL(|9N-DBi$UiV{OKco?M9GJ?fA^-TtA(20kGA31A`hXC>fVgD@k;LWe7XOEM5!SGuQ!hSxKQ-R|C>h4VYC^!!J>R zeop!@_xh$Li`7oQztj7-r6kNMqjm-@FvTwktq!o7WR_ey-|)-VXDKa;i;GwL z-Fg6bv2Np$5b;I&49a<{Yit}oSmX`J$%T|R>6217)$EZkf`g}L`+9@XQ9o-TL<=+$ z1I;KSUow^$YvW0v*1lP6ow~D(2o|r}S$n?sW~WCgItE=AKR|5yfeie!F*g*d8g(_F58bpq7jI^T=Vl--aK$ zej{%t#r{R%aDR%Hzg}mW>pU#9{WiSY|!aWB8^LL(hSo@ zfJ?6xJHC_=X;KKspiz7M?u>lr90NyQlNlHP&05Qj_vCC!@2>{z_I;h0{4y8K#i{~j z3R<x#WCDY!)dUM1U|O2W>c94PLEem=GIlx-*`Q6bR*MMr$o?1=N-Hdur+nlmUna{iXb2`dwejXkrI!7@TaY{mrMR}=p!mqr$?YR zJqfTE;0_K~I;a7?_E#Lr%cE`}JXd5Z^Qzf?oNz(9aF*2->X_J{obDUxv9CE_db_0x zj#v@_#YWV511+9LEV!qq$9`d#hGCs~Brxk)8`tZXK+ZXJ2dh7*2|sr*4E^ zN*M|EM_MZ&(o})?+}Q95Z8nIcHP|rqbj>MsJzUY%^l;okHQ;B678K+A4IN zl*z@fy(6DhXv74^GNSxV!V9B%c%u16h+M#{I1~P+LUM1M>&cYNt`&*j7(HIrkNYc899_VybRgT z8KCcqk}Z%E>$b}4+S9BkOSM5pMn-^*Qj(;bc-I^}p17qWcDm1V1O}Z8Ru;GyIY)@W zZ`=1t#<69y&vC)co1{olFvUVmE`vr~;7kgN-3YT?KnZMZalEU1wN_ELU+wS5lFD5! zCrLLlqjva2$#98z1U^Naz-i;aGnC35lV>aL?fv$uk3Rh5?o3M5$rNkkTL!ktSr?`c z<;A0q5ezUj34l8|{MCTt?(S}5Z~x;>IL~-EdHooAm;_CZqEMIGck+a*MJcPWB3lZ2 zdL>6kUT+^C=esLsTwL6zDX(NTzZ^=Mz+osY7fINZH7@9!y39`NVdr{HRa9|t`~aWq zwkFZ`?wd$+l$2;_=wovkyo++3sO)QiH5LFM0V6|y7E#&dd9uZRGH{rU&Ovd1%Ka3x`UBg|;dHjtY+r5uG$Lcn9u!$k0fG|1! zD$#4d8vjxUpfF8cYcRH9yJ&cLxPU_8MQC?YB<ch@2K3tsAFG1R)fmIPk_)k?>f7&u>HL5wLeN2lWQk@rMfzig#~c` zK<21*rVPg(O=s2O>Cf>6q-97^eqnV5EUhXy{tOAX((ftSJ?Mp8Ry=qu zu1~iBm(V+&&C8Tc*S9~b(|omok>z(`uC1G9WlRtow^dv%`-raC_cE3_(Ta3zjN{iY zqWQ*3WPk=OZZ7~V2#40xXm9(Z-(`zNi0rrKNd2OVQb<&0uC|$C1WIlppb`N~3iy^6 zTx8I37*nd|k0+IU34~LJ*wZmX_e~N{VD7ZU#KaVUxd;3v;0gbl`=B6XK*`7fjfIiL z4mk!km1N+p8z@qnZC(o=0N{cT5btiQzVGv|Nc!5fbpVH*rT)X zJ{`h-ksCGPR0A%?&+nO=NBr8!x+cKJ@t@f~Q_#JUg&ke$cC?iM3o-=W?>jxL1AV;;gRNNM%y-_7E!=iuN%Ikx4^)5yotb zrMZe_2@^RK3mb)!&djA2R&s~W&As9>F%DySZC+CPOo^G%dI&19@L6q9islIP9#mK; zj!#Pk)NgrFe_s+QDV5k*3O-z*Lgf1E>*xR-BP*X{v^X=ct9mT&_?obnZx(YXKvY}D`f2RvOgrlFJ9*+~xZQsXWwJIrwy`IfYtsw4au={6ZD z2Sv#`VxF$wDOE03Qz+!%Y59h{lM8e%B!H`J4o$5{XJ(KO-})e*`mCiFMFCC%kaobl zfjv6-^wdTB-MfOaGSJr6R^-dV1(ggaAQ4z?$$D|KnG2>lfe8ZFkdPWQ6$~)rU$_7p zW_Nel2Zx60WNTbKwbnI;I^$n5={4~LL3!*3^{u((6cy3Jk-xp&{&w@Z`GS+aqiE>K z784SXHlEIAMQpCW6u2L(RkA6Ye=KHGyA?+Fhi^@+@5&w7 zO}&$p1WqE}x~+39fQu?DEUeez?f%^B9SOFY8o&LLUR)ftU5%S9jk~*5q{7S(yTp!p z;sWo_%#<~HB+xXa8ecCIawE&UCJ_LU5!Ke}G8c!Gy`@^uF18sm(Z@l7%jJV~M9gyY zeEk{ZE=^}?OweI`MNl&J!I9yIs^X-A-{bMb?;J8%4_^#P9mffmBJ0K4>)H~t^XmRZ zBpx-{dY0?Q8hd?G4o;>>1?F9ExL$PAUs!5ngd|!kTphS@_ zj;Qy(&^|(aT*zu=I0?!KEp;H58rP*Pid4pd+9mL+s^tHUl48S zAdfiEzR!BOHBR|^yt$hj$<~$!H$@M<>&cD=K_f z|6+@DIx6&31d{E!WH+p-k%%^bhW}is-+4;$y!p@1mqO161$*K>CCT^ax3a(J${;dh zwItat!EmvQQ)Zh?fVE}S#=K|d8X292@5Ne2dc6fD2o4KGp$4;?4FB z4X6enG@e30=_V7j;BI2=YZu+|C8aqM#h!3H>cls8yZgT)+z-s>TN}uLH5AkefTrtf zrCjC#n?TiSsJRGJLSf1iL5!KF{BNLHE;p7;a3=k87S z4-}daiQib}Yc{c-JH9C>RrztA65uATYpjR#>DPozFN+KdOF3{cHP}e@r#xt%$=_2<{zx!X(X6-|3K2i#RB} zehoBka=OcO2-AzQPqsm68-3)~7C&5Ku`5g1`_Rc}ERKGm8T+@;tII6M+}~p69gxY> z6;Lv&1Qt>59()Dd3lku*b;+UlVH_XCifq^WqcoX6vvP2f3JLX_=>wwD0m)|(zDGGQ z92o?(U0%Pox+w9ff$kmA4h=`<*zRtryV>ZoCaVxzScF>>Ajz1E>L@Cb0E`m?<#p_D zbr;sqL2u=;;slfs$!j6$quMD29UYRLY#34=)ldSjoYwTI{Q~b`MO#SD-7!;ht&WX- zJ1nVwsg-95aPHll-%j@5AL|x70qv+aD{JtwV^X2AvS>YfF^^+ukjkrq8qJZ=R3uYC zeB>Vx4GuEn5*ScWs>s8Rp1}M&fG3u7Z-`bviF|pavO=nub*(;XugkcrDY%#!3TQ1w z#{7*bB?-hSG$bQxA|%4j(+@0r7j8beC$gMeo?ccWv@B8|?IV*ZVT}1E+DGb10;lLf zg}5Quh#FAVqw$I9?QNviv|o@Yl)rNUwsk?2&y+EvUcG{0w7ph#Lyw^s>M`o1c zgMGgdC#ftP`bdfNOOfGfIyz!J9rfsTbK0A*E;#T%rPCojJykLK=2_*i;!_#*rUk6D7bxgq-8oh&dq>z(IVXN=I)+@j_i4RQA4E(%Hky!LGdD z&SWtbhqt0{V}Q^m6U9ixEb~?4DNF0C828#^S8&KUL5PAn`fcOZt~a;rbRa4w&$kBKPQzPwR?vRO=H25u>8WQn%JQ)#-niI(doZmv z%*RH zwELtw>@(Z+F8p0ynNH$23NB92^p|}0kB!zA99E|GKjgq*s#IZ+DT%u~YvxxqW}@y= zEuxE~*Lf8cq!$`hBgHc34&qe*-3tJYko2b|T)HKK+=SOPeRh@ef!v3j-IITm3I#}` z7lPxIFR1T{E*{1z??#{R$~Jk?5UXs7{c91gWS@eHS`j8DCKy!K(I$us2zcErA+>Rj zgm)yjfBG*%A%I7->fv*tE~_JCrqZsg+r31C3BDR-KTIWu6xEA-%`S-~c>8#Q` zReVpb*W;38OPrJX3G?-~gjH=-q+dl~0M`T72-_v=fPH&wxMQwXb+^@Mjxro&x?TWJ zAG_9`frwwQ_-&JkDMbsCVKNtuxMhA_9X=^(T*ir#s*9A^$%J3$+O)CHkpXMKh2eM3 z5{YxUN_%cMS%!qVs&(BitX5Hz1E2?i_vy)FF)$P%v0}M6+|!=w3HRbZh`(`ug*t=9I6 zee!HQjKW^!QhZs|K(@CT&dS!1yV7jUV0@PnB<1%47Ku&T^C#M4p>mXW0Kararze*GUR*9Fpb^C|eKIyC zK%&SzOcWljjf-DKLGvZ)&1=9H)zToF_VNj(f1ujhVl69Y(FOui%CD)(Agl9Tb&`n+ zx8-G873$WOX8X4ASV?Kr8m5eBY@yN+VU$xd+0-B9L@V?pW%v(0Ef(ktq4#&yTusUf zvicb)I@v4Fs+dPcD-zJB|GZCqyrXKdinPPt$?c*LS!pJDo%Zg7HxANkVoHUoz3r zcV`M?e6%ZkqUP$e!i$}3nafg?pO33+G#i|sFJ$apx>^ULO`%i75JJ^6!fD^}MZY0I z%_#x-?s={4=0(+F9d3E@Ay<^8Ahx+dWJAanppJ=YvXe7;R&k8jUiBO%W7mf4XTjdR z;a22+0&@^Zk2|cPpr!_;)xm0qseXs94oOdggtDUpOsb1|b!FaajyxaOk%H@-8N&kYIrTxNNOvR*= zK6<)=o{*5g)S_m7n*sL_(t384%_jtY%2XXDSt`)!Y+@X*H z=q`53Pgq@RCn;eG*{CHE*#DG4&}9KAah*dor!%GZfBSz-|LF>h*|-HuEnc1r$)2!m z6Fz;ld|l5nrH)1@++J8)j+i?Bm2v;zFPbyQt3AAedURXD*J?`~KN~VCv)=1ieq;9| zj}C-pDIGhMDN(*PXm3FOuU!3@uC0pheeCPSanO+_a-j9P0K5K^jqIlsgTq6q;9yCB0da6(2gr#lFq~*#*#DexHrA%ltj`_! z0^a_gB*Qanf&bm=D_7lLkHbU$PRi{LNYl0yK%)i3bEHBsoqLm*P*#Tf(`)Ym7pZiM z?|VuI9U%xP!>-3mxase5-UoJzvliJYWkJr&sb1;p)_s8(eF`}92Z2OKaN~cZ%vjSE z9pVmcE>wF#^U~8VmBaY-4jaaW2$C7-(|D)mO9~+L*agH5lpkbeYi4#AzfP=kjZMpb zXQ1-`n=ru5p{+A-xiI2?+|jlv;w8RV(5yC19wT!(=2yGdo&l!@Ga57dA@i)UI-LeK znHUWkLC=rKI1;9)K^~}3u`5i~JP^_>KVITUz1=L6<4P0j>Z|KYy*94NZN$@u!2oJ+ zaYf4CZ@$_#;WuSZp>meH(vz8uL3h zl+RmR(b2-msd}lwihgW-{7qPurg=6oHqB|@IM-S3^qHf<@3pT6gTK9gv1W53X#TP< zvs*qZvEeL)d{rj3(!cWlTEL(P@CWbdG-CLg$(^%7kx}wV_%P1=(4!+3=e9y5_=1iN zjI!zH+Q(!g&Js;xuhY4y&0bO>BCPLw<@AUdjf=ptD!1Eh?L?{AHo}m9c1O>k96B#$&U|}T_r7T?rPT)t^B`sn z=OV{x;P|lU32+*+`f<k-9 z^)0h=;9r)lGKxw|c?tN_p1+d}a-Uz%Ld{^4MprdaQKV@C^Dg?yuiK-GR42PiGp9|z z^P&i9JZ3Sdp**~LQwKbjhJiO$%O{&}lG?HE!f6c~swcy3rRK3(G1D{No23bR*liFtxGEE9xW0-4;Fqm@iyUcBArd zg2~>;xFnJYXMoiCPwEQb0kq`t<~zgj|1GmME3^+F?WYi1vk@hbWTH3Y%qc!qB9|PS zPCS)pN4@hp%h3^GW+{-uO-a2%>~SL0>pUH(fQ^dJZW8^J=ND~1i{Ey+KnIg+umeKzioc{1+bD;Ab@ zpBj2H=&Ol~dG&G!%O9DB$eU#|>DCy==ye|f!sj1jrzCFqqP?$A#IpBKc~5Gz+x=Ol zA9tJc|5i4UkRzW~xdXugAPLvk*RNc`=!Vk4FSrR*4+y%|Ki>P^T{=8i~?iV z=v8wtS}&wM2+rl#D3T}$?}z}>#NgmZpu?1R-SEBbXZW(aMx9_+^n)$bX_egdV2TBs zlGFpSi%&k8Fg2|rTBc~(saIQ{m{+{+q_KKtfZ-M;mYA{5dP1pq_VD*^k9)UuSriKl z8TnpAUq6Y&E8J|6m7}Re_s@??1Kb#vtj60t>I?>?!=}iS)mc&+7g?S8Kgz!4meTfC zGecyVh&Njo7~Z)g7hG@`ou&^*+_X4uW5PJE90tf^F7%ha?z*FA% zYe=wv<7G|tUi8l0Jd>_+;oj0Octl6)-4H#k83`hwHAn}#8MQx2^3?y`96%x{c$B=| z;E-!z@4kA->m15{(9}=U|GZ#qnItPHlytnW+&CZG!5Gh9-D0`~39-7HAVm<2>lY&% zHl@DaB|#Xree2_h!S|-LBzI|4tA zR?_|q+6eRpGlM@_1Q?d-x=(o`ES6CB93)NH2i%SOw*_|ostxX*vK^tOmy0764dGo& zRt(E9UKz~d3u@(P7FRfBz&{^lCfCyP|7F>LI!(?*RbqQ0qZ(;tz0^ot;z1TE)o!?d zvuI*tZCzTAT~%FO-_`Z|>}lgHNujAUH2h3}?M)F?e-i~!^?GzU3&#!h%@N@QLDimK zh4#PeS+&oagpa=r>ztzaYDyoof! z1IH%pm}c9#wIdkzN&t^G{_3q3T|mP~E!6$o6bC9H`ui~;M!L{TEr^B{3Cv6pELnbz z##^iUeMo8fG|Ek(y#4`ca}tL5BI5mAXV5*n-`d-q9}~mhOn%ecnpL#tOPwEdq|SCy znIhWb$gk4)uAhME7i8CJij1&@JxtKbbpjRSn+^NEdo`OYmQ|*$u(H$=ZZ-~XBde=*8UARcN$Qe>9F>xrgD<>4F zi7sXrGq&zv^6f$5BLmVRs`KCQcBtRa_Ho-1s`gUAIw@dntH#caeORM;-tUv~QR(=)73R@(rlHh7v1OcUObT*Og)Qa=^>{1QjT!y%U`se1f z!`5E2q$#a+`KDCr)w=EfCUabMPU`G`!1SM8NfO5V@2dot3~qk;XMaS#w(?>!RKB-$fN1>ih=)>M#L$`!PJ$bQ63(t!I^i<5`= z;NG}6CT10num9hoO2Q8nuOKZOcZwAn*P7RSzF zYrQwOupc!aO5T*I-kksDg9s-CV-r0d`1S;3=UjaTfP5*W$G0W6o%e|XLy6~Mr9KB5Nq)Wq)^G4U`z`z^-zo$;LtMdloTYBmCwZpQQ4>0edY(oGBjIs_4L}m?8rej5^%#O~v8QX6 z+8z**oyX5;JPVks5eu==rEiY8#%bpJ8?kb2Lv*AZCD|Dc&vtPIw7mv-2^THS)ew*G zqEWE^zW2#6@ao{wC$+&g1X-rLBvHLuzqPJ*oN*=E@ILO}k>(|-?*_S7$V*QMYgo*g zSysayBCV>DA@lZW9`|ezh^)jzRj3(%n(TS}^2ZELfO6pRFmh2eiJ|ij{X5TT3Gz|v zR9s$hLaj|S3svuq8Zq|i*4@!i>NOj@fiDUjl5}7X6>YT!Ii$wKFtj+$Zo0T0X(vG? za{9TkTjR347!tGPrMfLDMF^REQX!QoSEFAXXAbcs9_xN$aLsYYSyN%MK4lyF-xdq| zHY8T+xofkDp7as|*=RlNEo)~y1aV1F?CLjlgMOHOGZ}4_N4#|Z&ZQNV5lw$B%7W4# znIy5JZi-N54*1~DgcYvL1}Yg&As~Cfk5^$r#+kXtdr9*5nK|svN@r}uv4-7#X%`n4 za#&3)6oTalvLeZ$42dt|r<{TGOg_}Sjw2A_g#Bs$_#7b@O%CGElVxyq&W{OH@a?^f zzv>mcZLZto>7K<>m=O?<(Av5gq74#3Z(p>{PLMIHNxuc5;bLiTa4wF-_pb8+Kgh3& z($NmRb^4TP-`iz8i95>(c0IvUCAdcgh=k=e0j*4rLjBC?MRCW)Lkmng4h5|SLtLDk zt5#&Opj>D!1 zZ<0=LvD>9rHc;#$c#wT0dd97o6dkSpC)c~);u{rW`;9K zcjIRukwCOXr5uQ;tPEtv)d_25xe)$EL8hmjbQR*qvJ;9md5pbl*MHA15Xfj@*S zpn?lfK*w3wqUj(!vHp6BHOVnk+l9DSOC#|Ve7T@KjYVjuHA^}R7Phb>y3esP+ zQb(a15|#{@&r!u2dU|W4m&;$)o|9mz{l`0^{rzuPQQtwQ_cw5{n>j52!cx%?Jt>L^ z``fWv8{Jtj#Y(C z;A?8b(Ed@ei_Efh-gSIANa*N_rBfAEs<7_&;jBYkf)eG(2UG+ofdcDNujIr(pUC!h z&9Fz^pTCce*v7}lm6ercKn(*9s&fW}VkZB5y}~B(fCq$fmDU$jV)y#@#6*2V!_2=; z5<+!0Mb%u=tq2n2FOPt;9txq)-`tQ_?&=7z3DSMt@65qScxgA4YinEDkvy2PBR~= z03)OVt#1(Xc&Dgvh4~cOjASSF1Gjjg_T@S25L42OlQ{Tots&>fF{^yL^O%%V@hvuC zmDvJ=2>W@-M7kU=rBlQ&=cehSqu_LZ%^}r@?}j~&GqZdlxNnRSbsGlo#HHGoSwoqR zRbt0{^{DF$j^{k{G+EV{+{2jj=cYVkV0H%}fURbMO}=wfI{PtiZ`d1@*JuOf<>szC z?D#ytf#yFqFs8%df9fK0NSb#Y%GftTio8&v9rU3~txAg*lRC(_XihmVghSK@@kP**)ET0ZgC^>hur z-He9aa%9}RBUSrv@u!9Z_HH6J7oALvyn+?{`vb?Zz zf_}(G8m3PE11KR9Hj4Zj`^{?P#s;E%!nNDs4D$Ssb>S$OZ|b;JdV0FB>(+aEl}e+i zVQwiJWt_wr5ibn$P#Eo?F1nZK9%eu&^Cz@*$$Xmbt39qW+8mW+sNR$(!3cn}% z8g_WZTV5)Rto^IAsiqhfRzCN*RE^5#N*m)Am>*yI^jbY4JqPNs^*%C%xsiio_w3WQ zfuZ5{&0??$Z^Zyk4q;*{c4^jb46`aX_mZ~_$qEx?UyV+i$1^VmhC`$yOl`=YO4aq z6cA7%af7m}pjk{Yj{3=~tw#H>hl8`lX@2n;QY>}@QCgo+JpO*@z%-f?XT~@ZeoRiT zc;$Sr-ll>lrLdRd*PW5Z$9*AqvF4kRi!?gu6$^1J2NxF~H#e^0ak%>*(Gt4~|Lb1p zdYQ)wa??Y1(L&U@%Z9U|cBKwbD^<+fW91<{)-P`61DGkTqo{44I!z9BW8<5_^ASMk z9DhNXMogV{*_H+_9QOVG%Kk6MwqSR5x~W8aOiV>aygEO|*@U0Dr}tjiTzL9a~2#nG6U{e8~5tp5xq}jUNO2^4wRNSil&lbbUY_n4^nB&s^>;%SKZdZ7;oaAkUa2_J zlY(C)H{OK$Mf(N^zf`U`d}>Z|%hm&%Mb)kY$I{_0NX=)z6p|tGC_I?mstHF}_!S0e zK-5pIiq7dG%O#1eR#O$F{&1bhW67#0@(wTO5k4q_8|>(NtiKpH=;twmN%x=r>V0p} z@pPq(iiki0P|=w-IibQCjlbQWQ!Ase@`@rlW7gO*#tG3Q(ogjxU+?i!f>N!`=~PN) zy1)j>-(*8-aFSKaC}7{*m?9i5J(R>z@kNw2519mLbBPaWAZDxjZXy}|{pXocNlEJ{ zr!4wmVSCb$0OYWO-CHE)4P;vino;XH)k{kR;J3IQwCUew9If&!QEY z`wh9a$5DZW^efcuZTWv|pXuf4F2d#8wF}i*l9d?Ycw#xS(o65-@F1f6owJ@etx3ZL zbWZ##O)`rf{?sa0BMo%H{1bXCE&kN3trtPz!eu>4f$5mmt zL0<3MA~$s{)r4fzMMWG|=GQ|C0wOINGUDU08uV)mU4HrHHF&HPv@S}|B7zq?z`)!g zX?%~AlJXc7%52}gQVo(*K0oGdirqkaUgxxYcT|#^4SX+~)L(rnFiM#k&iB#GR3-b$ zY!>RPD}35}p(N3DnF~ZDJjc^wBCB%5e1Y=th}GNs_AKm((4l)ja1vg~4u7Jz(dc>I z&{F1^ey3K+xF@5=G|#IN%?O&utqhasuSp7GhfYd6Ko9qDKCjC9M7$pxOT_W9N#+q4 zx)3e3#HOlOwsXAG;l{**u5SFhhL0{Ou$F665)%uK*E&PBs>j~JEy|%Pv!xopEmwU9Ufeq+ut*+%oj65{v1Vg6 zcM_+~)CQ0MxfP|S=e7S|CU`|Yx{?I?*dAhO7P4h*fITw;g!0(T#y|BPu}oQc-qytt znTL4)pQ8SyPeeLp>QI$J`u}4A0@IcBF){xks__*WSeXlBb$W;vtNv@Oi`#s6FAto( zrsJN@Y63oT9U!%`FW_(;kPX04eYJRB|k6`*RH^1Y|Yw+r%H7zjWyTDwAqNh4bjol(^HM+aSu35J8RTXBZqrNd*vgwHd7FkXjb6S zR&Du8n1MjqKSbZkd?@{gjJf0mB(81mLVMyr-!6$!Mgmhe3U9GCTFJ-zZI-?%0uh!4TOh@86%RfZr50Z2 zRl*O4Z9*srVmjZ{V8n#K&1ok0OUG5Q(W+*0HIkgCh)z}eylkei$6MqPyXBub7o!x6 zB{?!28MV_!DyJ3c5B|xIG&rb;kCvx7YJx!MxLT^t!hbxY3jfGVW!Xx{_sIK&C10*qY@>JH@Ed4v-x{aD#TMA58!-cR4NfI7~$Ww*^&+3#aNL! zrB!;F&1~~mt0H=HvyBL`K+vd>Zd$pnRNO?UMl^sV6noj`?c$fR_{pf!kh677UI;sa z6o)tzBq!Q5E4A`;t2vgwB8m=1iKIO^XOH*xy>@mqqO{;#mtWCEl?yH+VM3>;EltPKv@1P^UXb;6ka1Y|9pu?6HnPOLI8sY5tXFWV~%Qt!| zp81{9otshXwvkp6Q}(OXi05a94JCWLelI$ z#!v#nSmXpP+NAG@opyHQ*SFbNdpx%sBhlM~Fm0nyYIj?<+;0WTtl6IK9cK;mhhoz2 z>FQ@24~%3_SuA-$%Z^v2Ie22Bnu^~QET?a5_v|_^E2;V+xYt$9u`6gaE~HEeDW$sO zDN57$PC!8PTV#hT1iny~_<>Ko7BX1E_nbI_X(&Am@&{i~TJ|9u7hV-mgcTcQOSOXSC8=g@=Wx*nuEb{S5XN~gx{jDG`G=n{aB%T!Y6ouS zMb*9t#ZA5z1G&2ExLt7?vJZ!05%t@Wf=b)0@ig{vyH zIjyEDmFVt$L{aMiFu-C>zdTZnu?&;iqE8-4GjUZY)A2=ww+TpzNCuyFoKXR@Gsbf< zjMW7}vai|ut)jO`ACL88@gVy0N*`vkl}d5vy61vUTT?&#({&C#d4W~WV;HJ63P`hy zGhqG0k?$7?{^5yF?|KQzsHGZS*KU&clUb!A|Hb++st#=|*M6Jjc!S@`iHXIi$nYq5 zDCGl|Z#G!x)uey)W}abY@;VJ}dI)U2m;9F_FZ+k>pu^>7IINb-02tfYzA5XK`|*%3EHBlT+zSQV#OHeMO|eMqV)@@Y0bXVn9LJpv^^!1 zM#UT~78vrHHFgY=%abmA<>AGKB#8sbYqS$OpyKH#fSeTQO@n}?!Rl=(+8WU*qR&;!zw{x_oZ2M z4-nph6K)ARcfTI8P9E{pK|CdiVqsPjl~2U2PVivF$+`b&E#GM%1TymO#2f5H;_1J_ zg{&7OYIdxu(CP3EdZPNtKpYs+AVQ5}dCiox4ib z6EL6CRuu{)Nic@YS# zsr)>5o)!A9i5T)ie)7WO_u^Mc)DCJf_mnh=mW!2Q6(XsLT^u0g$wnWl{_G31G&u@t zn*_a{q8CwkMH)GdaPx_L5)iAi<~Ki?kjozeh43a)?$3E#!-Emk3b|h43Zhg62A-^G zxpEAN6wz_B{4u!Bz6sW@i3NUn0+$0bq+f);2JG zlg&O)BVfe}=-Y8KI0HK>!VHZZFX zM_g5tEg7~EET31z-15!TmKtW34FZ=7+ z8QSC*z#Odf-m+(O4;_~I_u`CJ;B0Ib#^~7f>L$ zdrGzUH~8hVvhuO)tOwZV4_?pnO|UXuQXjtxKtI_fQ0M(@aR5HMlVt-22F_c)$u{6S z{6sOgE@Smo3tQW-B*s3-fbl-+i3g2Dd&+hM33z9enT9z{iASO*5%A@7INp?0Z8ro) zp9BGW>`=|>`-sk9cq`c2%}uh!$RSmLlwx&oxiK+mw3uGY!D8*DeBPF^&*)YDKUKg~ zW4&U$_~51|ugkIZMAbw?CJ~o9COp>853Av`3JNgBA+*1N9WiwW_~R*^By3_u)BBk7 z*NJa)zRuIE(ETfRPzZgSgp0cEtm#!0{xS8vMqyRYtLR$}5A#e>*0la!r&YbKwf6_D zD~w~GV)1~D4GYlb`eG;SnCr{M9#45nso_-Bw(f_tq^RX}*&bch86FQ~EKc}(t^-tI zsx;B>?-jc1e)s8gH7%!Ngg5#RXLX$hQasd{YcaEOMEFL=H4C!_+T>}Hw;o2{6|`9i zf7lw>e-4lTp*N$leUV4beNGBMxk`d!*Sx>j3na5`5`I?Yj%ScS_|V)}(7x z^1)_v=V;zelQjj1zVoNEUVwPGX+XpuoKj>;L7f)wC){}sDV1tJ=9M(Da%(87pI~-J zOh<~;a45#|YUl=yDn@kD(m*UraFat`_bvt0rPSk^t|s5y7Aj1`M^+a9ADv(KkmC~)fXYhOI_)rJo)N8T7>7_+IhB5 z>3Y3q3j@Y2?<#b=Gk5=+p{BbU>G(Ukdq8l>D^k^7A$8l;iJO;F+_k11A2ZND`Fqw? z?RT@+SHQoIFQ<;Hen@hZF!8YAy3wEWo2`&Ah(keH)hKF(Gp6CEXMAjPcE?-bNBf0r zcjTKUSHJAV)$6PTAOAv0ITG{eSgCj3mA9XDS(+Gc$AI7SddGJny4QZ*>`T3J_5SeA zi$qjvXh6?bE5={ev&FN3`|&-VG(__vFAoL{4&PY{_}kg5#xy(la2Bjk|LLbE6{pdC zW?`We@ZU#7!9KQvg7LDE+B#oOB5;9BC5EHP`XX`v;ICLRqg@FE^w#}NPS?e7CqUfw zLWO!RcwKs*uxDpmc`)g@Ul@XRSH6|?6T=|``D#Nz z`maBw%YyQT`6q_M5o1cFB+l3$o}NH8>__WO0Z64~BQmo;UaekMv6|s_MX{`5L9_FN ztSz#sw~=&}6gl-uv^%cWpg{Q}hWQ=nLxA-!Fte8C7gZ0DXF^EO8sry^QgYc-aUh|B zOwKDN#dfHEKfkEeV#;&I-vG!+@~WzT#X)q_5s-zct;W_fyRnp`VieD`QZnXJ#KKM~t4QfGJ#7PrKw_j-z7z3#PbNl;Wdl*$NCLf(j z%z?NC1SaScDPf;C{j##Me1R-0tu%@g>_cWGH02q#Xc8P-8~nuRCNkQ27DPI;ZE=jO zfY!)&H*B!R_5m?0EHo_*U;4Ubo-34BljseOx24vtCcN1yb`p;vVqjODkhUhFQv34y zufLD6LwG`|6sI2WD26(DOUie=oO-qP#RDZas@a|&`;YWJ2?}D=QH&8WsA3 z4JV271kjD}oB4sD1JYT8QGD@t#dM~;WHF`H8k{&O3Je{B#s&INnG!A;vR<=+rj#g< z81zwOZ`)U7=8&ysmArx$v8sBxs#+%|mSc1CyfZIN#Oqjta%9`(dH;+hZEUUWODu3_ zWdjK1{{B9oM7Mc){*s=dM@D^W)}k9b|n!LGqdkuiOrW> z>%5lg0+45X9Om@mBDCH8%pLfG;#t<7tGvy9KxzgQEE=2{zS|M7PbMZKS7MW3@j=nE zfcLyvKd-v_?zg=cIw$mfF+XK;X|gQ?9$f0oru}_iBTK7yga@&bMHPHdoN5cO zN((G4rUb(#kWN}D?#dmxRZ=V)i5iwCjN&4`E-&Mck6ZOwR2D-{$J4UiL|a|A0Sb{J0pDBY-2II&@}e0^p6>`B9}L zZI<$P)v|U^hi`!W)MAVW#$sUhNvX|i27Kbt;-Sk+^x0Xy;#rCIEY;7z-0Ht8IA=2; ztM_rdx2RWg(7WgAU1@3QCj$eA?=sKAH|5%nY-#A}RdIt-f!f+=P4^rG(wE++IX?hu%$gfWJIrrSE+fWD4a z=7h)Go3L7#(H0#&^Uop+Vq~0ztIFd^4*fwM9K@E+ddO1c7k6y9@A794UCU()(!tloI0QDL0HDs%CK;?NtR&YfwQ6-i@W# zJg~NBaJdw~tBaFs;PKkVwluUIMAHNW5}gEj_FGORw(K(irPYx)w@ zKQKUEBRa+F9Xjp87Rnr8Bw({LpZJYVCG4lFD1zKm9<L$8FqT*xdg&1fsbWW%!|p%A=h(+ow%~uS4~cGy6%m6_S`mS z{JYG^XU&gS2aG@RDVEZr5H4hYq^g{=?L(B?iBv9sLXA4?efAxJ;Y+kuvKj<(uD;!ue zETr9{rfWaHlPV+g^(Ao@+J z^aVTbakRkCkV)xIA(sWxqfz$r*7j^_h3b&j#epx`az9}GU~%mHppL#R(~e4y64-B_+okJjK~m*@jc#-9%bGN+LC3uD@=qM6E61$FSOa>XTxXl%+k@Vt+&gLEqfaS zNd%u|uMsx^n$DxrRy5%(;`spLNlfw)5IF-|x3d7e;#p(@0wU*4+20 ziE_rwEWm?!V63*RlJf@Lhm7x9#)GN53n?l-W|o?IwfvmO8B$4d<2hABr7fR#eE!4r zEUB6=G1=GmR>)bIM_fG2Ec3tCbc|in%-TBAl6Um?Z_3*4+bchL*~I*(V=U*5Tx(O) zHVto^KykHpLu*lUHPl{-F|Ob{rVK6pJZU*Mfd~a!< z>Sq?qrdK#D8I?Zv_9%&p}JRQVMd*3}c^jvAMh{SHn*_YT1I4Gj&_6KAlPoqw`8Wu;0S zoWvro1t@9X3TB!#S>ruGBg4bUhaPIhe~4tX$@Mz_L}>{447_1fLUT9jtx+q{+?tkm z!bGqa=;e!oRV-8q>ebdXHfdJ}=X>AgD6j%1ytDkByrzbWB?i@UL6XwZkt1CcfH9h! zQ^@&Uhj?OYvVF7PU!*e~`5Iu-QH$=?j(q84gZ-xW>|;4)wmgoLFl^Q{T38PNsBJrILo zU!MFD;EPuP>(Bj0A3F9OsEBHzf3~Ee0yc)PvXyk}N>$H^u)}om5_`}!gfn36A7%hm)#q+*Oj<+$XxSf*r$D6zmZo28UkuyMd4Qij zq7k~X^Jge=+B0Qnh>|Aw5v}J9(<>7wAg@xL_IqVU8d_39P7_e~JEOOYWKqYiDrhVP zmz-IRyz6R`G9ks$A6(~%xngp%3vt8t{BXY1vR>C8ey&iamzjB9K5sBv%a6rIloLy` z_{q3t2-JrSkVrUW9g3sq1Y%p>GufG$rha}MQs-Z5efF-1HvC;;c19jp34tN&GA;)I z<%-z3K0oqGU%xF`G@oj^szX8pmlL1Z_m;mg`3wv0xRLdV*ft~gy)Z7&`hD3j5l%G?jq#@+sf7}4Unx=HIpW|}bnEw>#0a`zAD7O*B1Q4NsWD=8r z`g-v$7IwM`PotpoFdEmV`@`mM2x+!URYf8y_wg`%FplJIs0Qg zh=f1NkVy5ZfXis7)}+0^guU`nZ&PG|fH|uEgq+4$e`nrZD;Ff#N1&$cX7O%({rDYi ziJXh34P6QhlE~%m)bZOzvNe%!1bN!?-%W&U(EY7gA|=gkr3tl7v+2dCG}NdojMRiM zeCpHZ#&$2KJb9Xv53_N|Uyj+iL+RxygDzr`0JST8BwEEygVr|p$6Qsl!kHJV!HFf^ z`w~sLMG`U!EqJ)23{{9?;nw-2=s1=LUg~0`88--oK9$sWc#RHjB-kd{*$#tCvmv?k zh(O1=68VHP`@RI&B=@GH12{dO4JD_M!JFZv|@UKq3%xbLDbO#3|nuS836-bU6BX~!7&!b-+$B4DKw!O_z#;dSiP zW1XLV+(&#_%7UlkXO3`z7DjwfWWD~T`XrPThq^dC|IspmIe#t84u~bOAJ3c5PdcTg zx;zu&ZO$IyuIl>{@H3CGVq5w1@J3e0RXL#V6oO{-8Wt-C2~izO_f>zEyWw*_aw9*IUCsUi7n?R#CHGS62Je7~8u;V%!&TAbbDp$5xuEndyg7PedB|CPp7DrQn-dcVk^ z5T&g-lXKe(4QIUQLQmI)2qDaqN8d_H#i3xfoP4>mPD)E7_rE2@Dw0`b1TzDyR8}rW zUE8OAZeA=h;I|FX;*m?LWj9|PjOH?m;f!GG1)-(SGeB~Q&f=H6zE zyFc8L$NW?Wb~F2$-^(>36x!Rn{if3RbidU_{--l0B3&5T> z&)R6bzhL$!o)>oSfd?Is(INJnd4B)-HrcO@p3MlW! zUambNgf??`yn}2c69_PI5T}IQ7)DoC=$Z}iXO&yNi*5Vk$eptARsbCL4`Q{YMy3*! zw&V5A-(Ydw;!7R2`F{rx0b=4Y8AC65RP>%wd{QjNF`jdy|6>6>RAwDJ6H^=DNFB35 zM`C%X)XmVapUEE?W@hWzw}hz&!si8|vQNw~5(9>$lvwl__SimqnygJzp>CV4Qmf7n zx-*pg_=q)CT&(RnZ5XD6bZmbtuo~ayS@~;koB|VvIcbK*bj{9vP1*3?zHOZwZN zxlPSiCG3iKA$H_qx0xfgK>{l6913OrpNo11I>O9IsXp45TMUderzG}Fd;&UlSth0? z;)jpX`|y=!%CqiP<&r-e0u;w)KTq=naCkyf1nfKSMZeA2$zOh@(Rcgv6OEXJ1R$=s*6*eeX#$!;;b^5K=mD-@LVD`ONn<*_0}Ji6E6+zy zu$(7|Dwa5Df*K;Nj@lA-Ar210Gdl|>Z(y7p$)^+@wF+R>U8%U!x+d-IlhzCozow{e z|3)9L!5@vL(j~``F*WflUQM#MK|H&PlEI@|glCM}G{XsG%Wr$;WN};7ewC9u))~ zTYcXj0xGa+c}5s?lPN!gAtlhqn2%TZ(+Hbku#G4pFr>S4DEUD5C03rMdRy2!Y|U=6iaU#Rvz?D7+U zLW2YcC7;Torua#?-*iMp)eJU5k8(wYRNA|GRWFxvc5}womeBeMytpeGNHukV!Cp2* zzG875ZN=pIVZ|UzXycQq{SVv}QSWFY2`6)waINN+Z(A{R!IUFrfJyB2?X4yY;hRF= zAVO$7h3|%s1=uCffu^MzsY|y9mz08F*A1?$l2xn;NxB0o6(GidRRPxiE?;9f&OA{_ z(D`!aqYgeA!ie3?vtmWkxQ@@u6XX8=*v(vV=hagsKl8|!@_GAs?MQCQ5x|5DC_P5h zO+3NXGo`81EaA~&`S;F_s|jKm<`?tglBnEYzhOw;-H5TXk1Mb$WP=Rro8k zD%vwkRqKzxcy{zv1Z601uDh2UkvpbR)hNS$r8z3k?%qnr^uF9-f>RaL%(Fpz+2e^D z9V(k&Cl2MYH=5y_XxidL^LUD8W@aG(e5OrO)`=xBbhbOUUMkwO>LNPw0T$>I4Xc2MF=Zbaw=aAfilXXe=xboE>EWPrRo8Zb)sbBdDrtXcNCh?&XLyeSMG zYNzyHjsOb8yO9^4s_nbmu2Kk77QfG>V$>DPM&HD{uIky({;MO8VJNb4-LLl0WcU8m z8t(qtxXB4V!4?3X=LFjb?yK>EKK=1cPLwd%2I81CW8JTH+W$hu*AsGNocnMUmOS!2 zuErcVrC3RNvaKdAnY|}P(@%ucVc^x3>@Ir#yB>e$Us~?n@nPi zn=2_fJ{90*bD%uV#sl5Iu#?xAYx_$$XPN zK*Du&Yo81DWd4thc=b6+R0F(lzS&W;Ba997VSwR%-2A+bQiJ$|ted4C{m zQAUz(t~rS$s@<Y0;z>$L&=;l=aAS&C|zDETj-<`x%UpNXOUw+4ps z2nY;)cIlp<_tewV6C!%X0K@((sYhgGXZZnu>n@-)r7?1` z-eQD&_)D4X)Ry1uBt7YkGS4;vJt}LVLG#^3q6;4=`_JxZPe&5&u{u(RGa#Me)YAI6 z)}|pdE5|+IQ4F50jsgj{KAup6^&p1E)^5Bby9_?Y?m_-wUQ4m??`s`8TQJFd3$wy- zv!>({@4Np;(=|us;lBOvR9mgpT3TGTZQHhOTg$Fx*RobEEZclqw((?}@4eqS@4uZ+ zxSt!>b$z7ip@+?oiImVwv60ri@Cm=~DbOT8+7%Jn_Y}qHZ6h6U*s?x*#kAG`Npu4r{c|Hm80=65Jc=it(>77b#670>DpK?GxNVnA8zU{fr= zrvPs)ljjXCX{HLZQ*F}eo?;P~p5L8&jF16hVQfc&~gPW&9*@rJ)te1S;K!>NIPCN-SwRUz+1ftT48&w>hu2Zg<)YPw_ zgLUzu-EXP4S>Bi_*W~0GdxGm`Vt3mTa^;i^g9&XGJN-tyXB77)B$_Eg(8+(8yZB8l z<8D(hzLS&ow=>(}__a9~BceH%p9zsp3kG9ZC=b`f2ihiDNl8OgR7&%6#kIulQ`c5A z21Z1H6D$0WN*9-yNCBAc=XVx{HX%86#ti@1zh5yWen`QUDkp=A8Xg|ip%GJF2XG~_ zU;_d^h={~RMiK$_lE$csdCAbDSHTKuk^AHJF#C#!Wtg8I5jXd9Tv;xRx3Q$%dA?r- zt=+anqLKw-u)Yv77ym{+&e=G*^Yn!lm#jiL+TU7ZAUYcw2$hc^Q#uIn^(`cC@c089 z*cl`ntyqJrnkp&|u-WXsdp}&SH_;>+|B)UGC-AA0m<|sE#3?zV(?hE)uqDJv90T9L zR!6ZHL}K8-ZK|-pEY>s1?+SviFIEcvd&MKg7{=hlA^amqd~(Ny_kPqKE?>bCR7>3* z-#hArpBDJ57i62&G)m7evd7N$iVkk*97OpG>dT<+bkxMY;%IG(LbGR<3A;?%%=9i_ z(iE*6Hxr9PZ(wnd!&FIG{iBOG{o*)hE79GqrWXNb8M1dG8@QDh4E^v?@O(FqgObu6 zlD+yzJ0lzvo5aMqF(FW{|%8qYm5w>3o~GYrf*_+un}+iCFv%*(?>{EwNz89 z;1FqSQ6wu>ihbdqv$K>f&-YY#zAX!(OJVOu0wRoB9p}*BNnbPMy=Zy5Moy+FUw1FZ zZxZ-!Zh_4))$KSIWw#-O2euQHWWO`FifWlwW85aF&2A5Ct~{`|I}(HNjvR08Xver1 zfi>q7*l~2y2#ZNpR!WUt?g6?l9$}yh0=k+#H>3=%`F~JA*On2UpdeadiDH?!iYleo zy?K@+G^xSI`_1At{9BbW8r!zUo;kHNbls%@{m`cKsYu*v>(22!n4~{_A$VwmCO7)( z>Pi%*D4mR_G@x%F@blnxzx*b< zS<9`^#HWIA<&GBa%8#lwf`au^$_a1a=n%AvqfqNtM@u^$P0_dCKtO+16vxG2uI{%gPSgazs#bZzv8isB-V&Yr;DTQ@|Y zHaic%lR8`RlytuPOXM1ji?bHO^Id$Op})}*(n2dw|2pJFK)p%tGa#mTp!=e%uqZ&LLDQch}qq% z>uH8-VFm?TYi6FGVCH{~CQ>T?szfxv0FwU_81*-!Ok^*fdsJUgwPt(k;5#@xueaDd zl*)#COyHFh;|c)TW7_TL&tl8VGxE9yt^`R8uWVka7N?3g7=-gdFnmZUWF2fp$QvBvQ6nibC!Nv-iT`>uJGwrcCr{<7EhBGZ(#A;NjW za0&Wj;^fC|`sy#eG#IdHeERxpoc)oxWXpze!Vc~YT?_>17&d+kmosuULk_9L=3|i1 z1c>DF#nvd^uj3a)L6v_n^mlv*Ha>JrTl(s+$0ji$Wrg|^Qo|bRC1-hfqo>2V5XCZg z-S0l@)7dY_?}~l0ez+l;J<3ON`nB(r-TjFWiQ(-{!){Z2xY>%2t==m1?D)F5*5x#PQvZjgbD; zy#zaLpl%&Ld4%}LWFL!85>*@&k&@p=v;+MCitj0Gh`LVQ-B!G_l6(I6IFatdf)4n$ zUT5RhUXxolHmBkk-p6i1Ff4C$rd<6YO-LyIyc_e$VPA>2;s9`!je&vT>iGIVEKl2$ z_1s*s!__{_x8K%-&rjxU56gl;3sy3FYA|`$bZmcVmSpQ?m@z$#neAPtqpI|epiovR zW2g?Bl5tq#S?Lut$eS$7hVTh4%T2)HV1|L!hDwp*$`IoXh6tSYES7wDY`bR1@XoLh zR`HrUSOF|-_FP7~wfam1k293UJo7k>Q4mp&=Kgf$E>ST0Cuz#V9*i5PhQlLQ_Ab-d zU*L6KdR}pzqrXI-pzE%+d%K|E{Jkz>yPA0S{FwbheZc$JeixtTBkSUN#{7K6`-1-K zag2APxnb%4;3cc_ah@Ib;dqwKs%_Z!p7kZX{dQFcn)q@?KVls{ih)Rzf4SYkc|s*$ z;^uXcz6o@4iniPpJLqz@y$3~c2a%EQgM-(0<&(X9qP%nzsNlkdD+|z)^?{sRtl(!Q zqK9W^b!}7DPgY2YQ-ofAdf^8X(+E%|V%xS<@s(fSVbWHV?Y|xD!}k91b3$LaQ(K*O z26J}Jo$VX(74yR4^yjQ0n)5uTcdm|JmY9IZ4<4t`LUXSp%ZIsUR-6VAMV{AJwbYs8 z@))>DC=oP4U6_qaFx%BH?VInF_DhiboizT=k`9>U6v4)cw-4n*!;)y+`0sXy^#}i0 z68qeCmxwG+rYto!|BreO+N+`O@9W4B+kv?!DC!%$?i}(U0siNDqa$eU+liVfg=n&lz8>)KAk8Oc5g2t2h9nUjpA7*GVd>gnjq3DyOi|f`Z@PPp!@8SpP(`>i)~- z=>n*(_n3R)r5#oNyjK9{i)r}iYh^>xw-KIKPd@!8_iX!&j{8v4zLEK|POlT%?S@MswpYaObK1!e zB#;PBu|K0Jj2Ka5QtsMSD;9?zl%Jv?e(>?-u+=q#NY(x1i5rze8dAg9TqCb>RPh=) z{Cxu7tH8ezuXJhDT4ZL~T3}%5U>^LtEnBa*TUpi+WxiKQ(_y@Y`{t*XpQtinQ82xO zO#)3l-rLh!R(3wdo1QlJF!5l49ABSYq-M_Vmns&OmOZ15DMiYV6kVM7t36DpsFYi$3zYMOgfM9js`0~uey7bq-kUXW3uq6Z0m_*Y; zJFiIe@3e88Sbl0&76E3W5CK|ALU4k|wFY*QraZ%~SKPfv(6viqpNLZY!++lI7-C*z6Wax!(|M%*{yFOu~O7g&o@+MRa9W!KN!~5d(PFC#Pacti5xfd z$!8haqg&f(&T7v}dAEkxu}OdXY5#OpK2Og#!oI>i!@Y8#JlE3bm#mX!WJV?^7|@qQ zJ<4VngXQp%26~9eB9s5K0~&yUWwE<^I=Prm{w-E3PW1e=+j6d%Ay>rv2Q> z&2J0TUuydCQkWwn`R`$<2|lMgX$VVjvzoAxmwtWyDjh6A9ZS^xz1N*%y@dLD*BEzE z;fJnuCfZh}zB+3|DOpm-2b3;umy4azTQ8pc1miz0Xr&p<*?dksW9`w4FKZW%=iM=< zdzC9)=*?{-McK_!qS@Yf+!T5rl=P7#^Bwvz$1y(458Qp_E>nyCO*J^4_c?dmM?`dU z$-|QK=*WkPvYpK-`=&)jnP+su9XiE+gjSejdXrHT^FZUyMX_!doUEG#TtDrDLkzcu zpR#n{bWdNBNjbL9t+8DZL4M^E1o{hxX5+<+`aiZdPQr5Yx_HoL;6W-v7zRLIygl|&Vi$VUGl=c-I!peZ2x<~I}*a}moC zWG!KyOGbekGj!|bf3(;S+E4P};0-ekWwcGL9EnpZLMc3NKSO3bXv~T3x*Ueck1vbQ zv534a>9j9f@LVkd?+Ri^UE9&!y$640a&@rs-dHzOmK-K)q!n&I>rgZ*QZ%#xC%^mq zdk1;0-l2U+gztt>f?G@W{QO5-cS+Jee>7-5E-Y8l1!}xcY~tXTj*4f|OOu$fG}%$J z5so!WTZBG#7%tv*_9ZvMAYZiDcs@ZZlSf`BAn@nDZd7@0WHn8VD?PZq02ps0vpPLK5IR zqV{_9eS$f8>VS2ZD_(jE%08Df}=7xeC z#uV~Nc&^GYj&GVEkbK1GJM9usWEoVcd0g2bfbPKxKYm8A9d6J8tY z)6vPMC+|x(*Q4ao&pO9$LA;&duI^vt8SWpd`2rS4b~A+ z>%0m@M??x5@ujcIGq<}m%OK6#g`Td{l}rU8aZw=(%R3m4j~S!L?%ikz19M;dF7)?* zYxX>5eyMN%K!y_=-Wlkj+U%5zrC0cxK>?SxqxPYqzIn+|M{EKRbq<_EjienzBfNCc zSpt`8>0(6rC5SzrgR*Y~47>O8;&phzmL%i`&{JoOnTj z4wL@4*_N164tS|2jLoOko;1~} zlD-I0YZGPD;1$ECKsDLLTxwPOk}hwXP5X#e&u*X)oUbMxL13-@ZEuK!fZ!uF385xh zenQ8B?8If>?Fx3&vD1D#nEgwL2g4r}=viR+48|eMue>nez z1Vd`)uzmm5%|@QsEs3*MMI(*jP!cl%T0;%vd&|yqHG;--v#Dz`Nq{?YCzOPatmT4m zqY$w2WCao~FR!jfIm6iThvHJd20*j%a@tqVel5EhVj&>lGNd}Z37t%8-lbq=ZGI!B zKj4hNR12#eqDR+hSt)(?co82N-Dgi}L*3t5kY=jW8x%-QTw1X{Sh&_eG3=3K%$@oB zM5v1>mQGS3b|;Q0=)|4F`b(6F97>SV5JBv~)=Tu>w5`ms6C>J5N5v$ap8Szsn$guZWA)-hGrYGg-NCMBJEBU? zsMdN>O@*XB+t~QGb^7)7r$-;(c|!?co%Wv^Ps=XRY{B zSW^=ZV4y^>#P7l=m6g9@V62()KFqgdqvs|>Qn-!{#mYteQ4}s15XHy_VzbdXwT!_* z;5I&L*Nf_GwJ<|tey@LXoKD(v{A@f*$3+aMUrnljs;~&*6Yj?vTiqXuMJ8b) z)KtR|_JY!w{zg%77n=Nz#OOS;XW}a~WxLu?y?s7?dbEgzWsDgav^`7Zbq`v6+3$b6 zx!OJm*>W}uCv;B|I3KwIZjK;m&W#IqV(+r$O>^@gomDhW1n>~r7>K2-Uz!-|i;`wy zxu=s(pSY_?Xd&0ahNw!%} za02eH+&C1lhyIh4IB-l;dLJ|)B}JqTg1=O|`>N07X=R!r%?DP=Qql!`CowstaIv9- zr*?4&So_uHBKz2J{Y)2|eia}YF{6b1O|~pCGUHd==o*6{^vPN~37?%0h{;R#_Xato zGVe0<=%NZgkdVYlN%ff9-WaIEftz9MPdB`xpmrtjw~X7{+akba4>Q-m=HcpI(@UVN zwAz7iE;bxDnLPU7OeP2$8sy|C+h~pG3i|k>>(G7&`v-V1FanRn!LTp%6MItQH*%kj zGycxZh=5e?I_o@h4Rr$3Hfu?I_qd0bAlWDBP~E$o_OrnmywhPsZ~ykTr+zLESH|lR zqA$#4YSRjIgfmyaOARw(_dY5`6sYn)&$CKLh!G8Dag%?-%gQM)56;TM^ziWdr8z64 znv9q&+r4Q|5VV|NedAqcEmlPB-^mmoPu|xbe|aUmcN9fO9}678m(JUcpNC&T{L9YG zB>$0_0g*};1iB^>hxHmXb>toKd)+T0VAHO4DvHWlmo`6I82mvFzD1h4e(dVUQv(b7{hO-=pOd)rV134x019FV$uvR>w%E`?`#J~|i zs=C(g<{h_kt-N_SI62L2ZFTKZ0?gBjOEsqYy9}`% zMkp-M2iSvUVHE(L&16lE{F4a3Qhww~#7$7M>>grN-Lftwh$ATxdV_^%4B%FpT0-0S5D$i$wD}E1OpgYDt7U zIuc@(;hP1~y;PsDQ}iu{I;Oi)5syvBi$}rYJDwW_QdVYc+#jTaa@22U=fAWhRvCC^ zXY7=DCMwiqF2|E1%fP}(mVf|zKq*PnZZ1qs5i=#s9TL4y0Q(2T#c!Uu4T(5H(ErpZ zA>bM`-=34cL|@u^$E-G&f6CO|7;m401Z3Y*9((Qh+P?&IHVnV;v|TT+Zd$jyp#Cw! zxZmTwh}yh4g|bf3_~0vRv5-w9$Pw+;j{cQPSYK!3bWxj{iI&$r$*EKMyRc7+bozJc zkY7TaAQosI292l)J_qcX2h6KS7%{|a&NX223{zu*NEN_ji6PCx!9tULup4TMakxDt z|5tVv!~yQY>zIB)QBloKf|afV1iQ5k{lcPvrSl!E(EiQCRo}>pii+G~AXq3-0{k1< zwTogW*IIfeIw>*~k=a&Bg8!@*(~^JM-&g9Tkx;7qTJwX&{aIK@!Wwet#nW~cW&3w= zF=7l#NkD`mtUuaR<^~OD4>VqsibTvg;HYe?PV9UpOeqBVRs2kuyn}n}>atS})$3oI zejz*p6rYO<8WG-dwO_#=uKe+aJf{`fk8$7pcGw=$@&(WRV!f^DVx6^H>xz4xqXgwF zB;+po#*GVn29rxmT2nG~7)DlBR5v&k8h^s0qoZ>TC-<2C1$+;wa9deg<`oqc?eg?& z+Jn%@&|ZUx>nfC{iuH@VdH39ELoVFJurPBFBHq93x;nbMOSI?Y&w1SNrkid@DC#33 z;+Ib$>VGVVy}bi!>*}g4lvyjjaj;z!%^qw=v9A2g$q7Qhn)&T{<73ly2lFpno+b2+ zu)H{l>E6*%L0Q=we3ZhXZY}#>)Tyc4SckI{ifpuFe&ylzN}5;37?1_A*+Glr4+`X8 zCP#iAaNv$j%(Bab=YWyL(5@M_@Ymjx8#%B6%0~qa^AMH6&oI07qDUhC5Q1PpAFoE$ zJt7q@)U)%DxaodshVp3N7MCdeyoSjr(h=T^gTgD){yhpX8;e{&&~pN~7dUA%Y@IbCH=mmKqoOGoys za5$KTBWPt#JKikMzfar9S6*b1M+xHN16zjPQ zRj4*-3Ze?ZWo7`C325~!0@I@!b45bXSPGe%eosO}^!pB5S^qm%czi9Ju+OmSV?coX z{R1JGvK3U)@{=VDc;nf4SQ6Xyq&qh?H#_2>k>;eGi$>$RurK;|L~t@(O1c{mvBdVF~M`rXs$ z7e+_L=(ly$NN{?RR^&e#5@90<*U&}kGOdJ(VyW-ASC3|yq81i3EHE*V<}J@XT5rAD z;Kp1%kObB)?m*TuR^#=8rK3?alK*nE>z9u(j*As-_ZBuydxaU)KT9cq!{_DA&6kK? zB!Pcf_PBtDj4hvEN&zZEVED_}=uII#tALU&XBG?)abxIBuf96c`&+lCE7tcRgttrx zy-DC-uAy2tOCt$E#LKIz196!wbUZx#x~t#9gkEc_LXFsgWvL9V2bA;d&g=5#pXZyF z=aC6=m{P@W|I*UP=;(<>PCip89Ln1eL|l;6Y-Vj;5*q24mkHFiVLeL&x~v@YbU(|6 z`=?b|YxD)*TwUp)2qS$Y0eP!Q+@2=YvBRj#ZP?f$jad4IhKq4(BcM~#)8pIXQAA0N zD2!L=rCwS?`(w{YrX@P%fQd)Ts`Mdi8%9|8;-Q9M*k?xT<-hi`2UxeqHd1%`OMcuz z3Nc8_Wxdyiq`1$~qHhnUhQxxL>P32t=GPDLNEMy;)1y?(_n8j`)BfzWShYusIE4H^Xz5oGlfjWoBV z!HX%T)QCB9Y0ct|%eCsWvj#@H2QLVpO&U@Zhwp)hJ?6htou42Ke|I;4BR%QxK=W4E ze#nXEYU-PwBQ%q;xGeqP!yjum^ToF>XGifuc_K6pz1or7PnUE_?`~EW*`aRA%Hjoj z2Q&D%p1-oysI;eOQh-$Gn}Wn~dwY5srfiat!$Yw?2HF@m>(FKlB0XdL&Y(JHDv<0_sQ)!JC6Be48DIzrH;tX$ zGP*D+LcKg@OB4ki;S5qtKpeG)93Pw?F+Rpepl?VLI~~z2DMBeYqA`k*Fn2t^)o;5W z^F>gbD-5&N>@lFn#N7YfmHb6kM~qeMJ;pBQ5bN{R$zP>ZY059glQkV-2{>$Zc!nMy zDId7+l%fVZ`Ikbkf-1%iu0YPQgQF9B!lbankt3^g$&U^PVzeb_VXZ>4d0OLH`)^u^ zx6?KGtPSzOtJz>M^<4(Yqn}pOwSio-=lr-*5swYRZ7zxt5#O6Os4ugqb@BtGAz`{xA!hFGtfP zclJdcj0f_OQIN`9UBsJagJu?Id_&lVe`SxL>PYJN!H&1a`qTpz*X?JzYfD}PM8r+}u2;wKa2neLeR>4)Rvn&W~Z$ zC=u$e^^c-4B2`pjNHf@Red=!P;Sa}W$1%pruxT|PgzeFc(hBXm5u~*or3sgpnWttf zdNxU(p6suWGk3NI)fv9RIYpN?sI3PVzELMsAkD_BASg&JgV5{FHNmqITbPiH`aYZD z!6HH&E=G?oFQ+yx*o_BCN#ejHs*kerZR`XDQ|)e3SVi#w;5+P2WCPaq&LH?Y^9xM| z%U4|Kv!Q5vmy=JX&Mx5(dW%2A^6!paAJ`p?g6mrfGL_3LPTiO$%p(AhU)jzx`uv0+ zH#mYFFmh(6`FPNN4S>qfMO)p@{b^@2Od|%grzd(HPisJaG!-j23@3CQ>QoovkcUaz z5yBGAqvK59=?P<_Tle}ToE8~9t3Fy_T2gvJ(-b_r7c8D$C@v}rt0Ov4L_o?%{Fw{2 zz)D%ZmtBO1;A+C-Q!<6q0o6>6a-0f#hKuvhb};L{PKcdsYlk`i}L zEotdL|E;2ag;^{X-2GKaIWMC&S~6<{0;x6~!v*ah+F^2`Ir~wI!;`a<5=AAAinxS! zN%WY)y3jGzsM{71D4Sx(G7@iX^lx-Wjd3o_QJv_?ZrjGi~JoLr) zOuTLWRMQI@P!<{$6F=sFO4jDFfQ7E|M9chp`NILM`1E7`QT)&zt>ezeii`!8^UBL z8g@BZ=uAn6OJBFHW&>`YAg}o(VhNR7R?gp+TN2IJDZX}JZKLDKRva8-l8zHswbg$P zib z8rZZi7k;PeTBK*2aW+B!-x$o`k%!a4!9lXnp_0J0lDshL@Egd1*(MU#@k8p1r;Xny zSG&)`)+op@K`w=pN#RHopWKfk6^9EJ;usOxd^ux3m|WXdku@qVL`$Bsxs>!S`Qyq2 z2`RrDJ?Kq>U{pwil}h%!cKKCW`ZjK%kxt_COF%XV7qqD{muq($eG-K>{@m9LM6yh(0�J%%PwJmR#n-He_1yeSp0DO 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%0A5f3W!pNU5Cfi4RweM)L=dL4uO>0Gcj7aVL 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 literal 0 HcmV?d00001 diff --git a/libraries/AP_HAL_ChibiOS/hwdef/HWH7/README.md b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/README.md new file mode 100644 index 00000000000000..26cd745c4f546d --- /dev/null +++ b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/README.md @@ -0,0 +1,125 @@ +# HWH7 Flight Controller (RTF-H7-FC-8S) + +The **HWH7** is a flight controller designed and produced by HW. + +## Features + +- MCU: STM32H743, 480 MHz, 2 MB Flash. +- IMUs: Dual SPI IMU support (two sensor positions). Default/probed: ICM-42688P .Alternative/probed: BMI270. +- Barometer: I2C barometer (SPL06 or DPS310 at 0x76). +- OSD: AT7456E (SPI) + optional MSP DisplayPort (for DJI/HD systems). +- Logging: microSD (SDMMC1 4-bit). +- PWM Outputs: 12 total. Outputs 1–8 support bidirectional DShot. +- ESC connectivity: up to two ESCs via 8‑pin ribbon connectors (board wiring). +- Power (board hardware): 4S–8S LiPo input (12–35.6 V), on-board 5V/3A and 12V/3A BECs. +- Other hardware (board option): 512Mb blackbox flash. + +## Pinout / Connectors + +![image](HWH7.png) + +## Serial Ports (UARTs) + +Serial ordering (ArduPilot): `SERIAL0..SERIAL8` = `OTG1, USART1, USART2, USART3, UART4, UART5, USART6, UART7, UART8` + +| Typical Label | ArduPilot Port | HW Port | TX Pin | RX Pin | Default Use | +| --- | --- | --- | --- | --- | --- | +| USB Type‑C | SERIAL0 | OTG1 | PA11(DM) | PA12(DP) | MAVLink over USB | +| UART1 (TX1/RX1) | SERIAL1 | USART1 | PA9 | PA10 | RC Input (SBUS/DSM/CRSF via serial RCIN) | +| UART2 (TX2/RX2) | SERIAL2 | USART2 | PA2 | PA3 | DisplayPort | +| UART3 | SERIAL3 | USART3 | PD8 | PD9 | (connected to internal BT module, not currently usable by ArduPilot) | +| UART4 (TX4/RX4) | SERIAL4 | UART4 | PD1 | PD0 | GPS | +| UART5 | SERIAL5 | UART5 | PB6 | PB5 | User | +| UART6 | SERIAL6 | USART6 | PC6 | PC7 | User | +| UART7 | SERIAL7 | UART7 | PE8 | PE7 | User | +| UART8 | SERIAL8 | UART8 | PE1 | PE0 | ESC Telemetry | + +## PWM Outputs + +The HWH7 supports up to 12 PWM or DShot outputs. These outputs are organized into 5 groups based on the MCU timers: + +| Output | MCU Pin | Timer | Notes | +|---:|---|---|---| +| 1 | PE14 | TIM1_CH4 | bidirectional DShot capable | +| 2 | PE13 | TIM1_CH3 | bidirectional DShot capable | +| 3 | PE11 | TIM1_CH2 | bidirectional DShot capable | +| 4 | PE9  | TIM1_CH1 | bidirectional DShot capable | +| 5 | PD12 | TIM4_CH1 | bidirectional DShot capable | +| 6 | PD13 | TIM4_CH2 | bidirectional DShot capable | +| 7 | PD14 | TIM4_CH3 | bidirectional DShot capable | +| 8 | PD15 | TIM4_CH4 | bidirectional DShot capable | +| 9 | PE5  | TIM15_CH1 | standard PWM | +| 10 | PE6 | TIM15_CH2 | standard PWM | +| 11 | PA0 | TIM2_CH1 | “LED pad” (can be used as an output) | +| 12 | PA1 | TIM5_CH2 | buzzer output (HAL_BUZZER_PIN) | + +- **Group 1:** PWM 1, 2, 3, 4 (TIM1) +- **Group 2:** PWM 5, 6, 7, 8 (TIM4) +- **Group 3:** PWM 9, 10 (TIM15) +- **Group 4:** PWM 11 (TIM2) +- **Group 5:** PWM 12 (TIM5) + +- **Rate and Protocol Consistency:** All channels within the same group **must** use the same output rate and protocol. If any channel in a group is configured for DShot, all other channels in that group must also be configured for DShot. +- **Bi-directional DShot:** Support is available for **PWM 1 through 8** (Groups 1 and 2). +- **Timer Grouping Warning:** Be cautious when mixing different types of servos, ESCs, or other timer-based outputs. For example, if PWM 9 is used for a standard servo and PWM 10 is used for a NeoPixel LED, they will conflict because they share **Group 3 (TIM15)**. In typical setups, PE5 and PE6 are used together as servo outputs by default. + +## Battery Monitor (ADC) + +The board has a built-in voltage sensor and external current sensor input. + +- **BATT_MONITOR** = 4 (Analog Voltage and Current) +- **BATT_VOLT_PIN** = 10 (PC0) +- **BATT_CURR_PIN** = 11 (PC1) +- **BATT_VOLT_MULT** = 11.0 +- **BATT_AMP_PERVLT** = 5.882 (or specify your sensor sensitivity like 170mV/A) + +## User GPIOs + +- GPIO 62 (PD10): Status LED. +- GPIO 80 (PE2) controls the camera output to the connectors marked "CAM1" and "CAM2". Setting this GPIO high switches the video output from CAM1 to CAM2. By default, RELAY1 is configured to control this pin and sets the GPIO high (CAM1). +- GPIO 81 (PE4): controls the VTX power output to the connectors marked "12V".Setting this GPIO low switches the video power off.By default, RELAY2 is configured to control this pin and sets the GPIO high (Power ON). +- GPIO 82 (PD10): This pin is active-low. The Bluetooth function is disabled by default when the pin is low. This feature is currently not supported in ArduPilot. + +## Compass + +The HWH7 does not have a builtin compass, but you can attach an external compass using I2C on the SDA and SCL pads. + +## ESC Telemetry + +The HWH7 board features two 8-pin ESC connectors. Telemetry from these connectors is routed to **UART8**. + +Note that due to hardware routing: + +- **ESC Connector 1** telemetry is connected to **UART8_RX (PE0)**. +- **ESC Connector 2** telemetry is connected to **UART8_TX (PE1)**. + +Because both signals share the same UART port on different pins, **simultaneous telemetry from both ESCs is not supported.** You must choose which ESC to receive telemetry from by configuring the `SERIAL8_OPTIONS` parameter: + +- **To use telemetry from ESC 1:** Set `SERIAL8_OPTIONS` to `0` (default). +- **To use telemetry from ESC 2:** Set `SERIAL8_OPTIONS` to `8` (SwapTXRX), which allows the flight controller to listen for telemetry on the TX8 pin. + +## OSD + +The HWH7 includes an integrated AT7456E analog OSD chip, accessible via the CAM1/2 and VTX pins. MSP DisplayPort OSD is also supported and enabled by default for use with digital video systems. + +## microSD Logging + +The board uses **SDMMC1** (4-bit) for microSD logging. + +The bootloader hwdef enables flashing firmware from microSD (`AP_BOOTLOADER_FLASH_FROM_SD_ENABLED`). + +## Board ID + +`AP_HW_HWH7` + +## Loading Firmware + +Initial firmware load can be done with DFU by plugging in USB with the bootloader button pressed. + +Firmware can be found on the `firmware server `__ in folders marked "HWH7" + +Then load the `*_with_bl.hex` firmware using a DFU tool. + +Once the initial firmware is loaded you can update the firmware using any ArduPilot ground station software. + +Updates should be done with the `*.apj` firmware files. diff --git a/libraries/AP_HAL_ChibiOS/hwdef/HWH7/defaults.parm b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/defaults.parm new file mode 100644 index 00000000000000..fc01c726981868 --- /dev/null +++ b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/defaults.parm @@ -0,0 +1,5 @@ +# setup for LEDs on TIM2chan1 +SERVO11_FUNCTION 120 + +#enable second OSD type +OSD_TYPE2 = 5 \ No newline at end of file diff --git a/libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef-bl.dat b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef-bl.dat new file mode 100644 index 00000000000000..834c0f977ea48b --- /dev/null +++ b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef-bl.dat @@ -0,0 +1,67 @@ +# hw definition file for processing by chibios_pins.py +# for HWH7 hardware + +# MCU class and specific type +MCU STM32H7xx STM32H743xx + +# board ID. See Tools/AP_Bootloader/board_types.txt +APJ_BOARD_ID AP_HW_HWH7 + +# crystal frequency +OSCILLATOR_HZ 8000000 + +# flash size +FLASH_SIZE_KB 2048 + +# bootloader is installed at zero offset +FLASH_RESERVE_START_KB 0 + +# the location where the bootloader will put the firmware +FLASH_BOOTLOADER_LOAD_KB 128 + +# order of UARTs (and USB) +SERIAL_ORDER OTG1 USART6 + +PC6 USART6_TX USART6 +PC7 USART6_RX USART6 + +# PA10 IO-debug-console +PA11 OTG_FS_DM OTG1 +PA12 OTG_FS_DP OTG1 + +# pins for SWD debugging +PA13 JTMS-SWDIO SWD +PA14 JTCK-SWCLK SWD + +# LEDs +PB2 LED_BOOTLOADER OUTPUT LOW +define HAL_LED_ON 0 + +# CS pins +PA4 IMU1_CS CS +PB12 IMU2_CS CS +PA15 MAX7456_CS CS + + + +# FATFS support: +define CH_CFG_USE_MEMCORE 1 +define CH_CFG_USE_HEAP 1 +define CH_CFG_USE_SEMAPHORES 0 +define CH_CFG_USE_MUTEXES 1 +define CH_CFG_USE_DYNAMIC 1 +define CH_CFG_USE_WAITEXIT 1 +define CH_CFG_USE_REGISTRY 1 + +# microSD support +PC8 SDMMC1_D0 SDMMC1 +PC9 SDMMC1_D1 SDMMC1 +PC10 SDMMC1_D2 SDMMC1 +PC11 SDMMC1_D3 SDMMC1 +PC12 SDMMC1_CK SDMMC1 +PD2 SDMMC1_CMD SDMMC1 + +# enable FAT filesystem support (needs a microSD defined via SDMMC) +define HAL_OS_FATFS_IO 1 + +define AP_BOOTLOADER_FLASH_FROM_SD_ENABLED 1 diff --git a/libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef.dat b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef.dat new file mode 100644 index 00000000000000..c384391d160fce --- /dev/null +++ b/libraries/AP_HAL_ChibiOS/hwdef/HWH7/hwdef.dat @@ -0,0 +1,210 @@ +# hw definition file for processing by chibios_pins.py +# for HWH7 hardware + +# MCU class and specific type +MCU STM32H7xx STM32H743xx + +# board ID. See Tools/AP_Bootloader/board_types.txt +APJ_BOARD_ID AP_HW_HWH7 + +# crystal frequency +OSCILLATOR_HZ 8000000 + +# system timer +STM32_ST_USE_TIMER 12 +define CH_CFG_ST_RESOLUTION 16 + +#disable safety button by default +define BOARD_SAFETY_ENABLE_DEFAULT 0 + +# flash size +FLASH_SIZE_KB 2048 +FLASH_RESERVE_START_KB 128 + +# MCU Clock +MCU_CLOCKRATE_MHZ 480 + +define HAL_STORAGE_SIZE 32768 +STORAGE_FLASH_PAGE 14 + +# default to all pins low to avoid ESD issues +DEFAULTGPIO OUTPUT LOW PULLDOWN + +# order of UARTs (and USB) +SERIAL_ORDER OTG1 USART1 USART2 USART3 UART4 UART5 USART6 UART7 UART8 + +# USB +USB_STRING_MANUFACTURER "ArduPilot" +PA11 OTG_FS_DM OTG1 +PA12 OTG_FS_DP OTG1 + +# SWD +PA13 JTMS-SWDIO SWD +PA14 JTCK-SWCLK SWD + +# SPI1 (ICM-42688 #1) +PA5 SPI1_SCK SPI1 +PA6 SPI1_MISO SPI1 +PA7 SPI1_MOSI SPI1 +PA4 IMU1_CS CS + +# SPI2 (ICM-42688 #2) +PD3 SPI2_SCK SPI2 +PC2 SPI2_MISO SPI2 +PC3 SPI2_MOSI SPI2 +PB12 IMU2_CS CS + +# SPI3 - AT7456E +PB3 SPI3_SCK SPI3 +PB4 SPI3_MISO SPI3 +PD6 SPI3_MOSI SPI3 +PA15 MAX7456_CS CS + +# only one I2C bus +I2C_ORDER I2C2 + +# I2C2 +PB10 I2C2_SCL I2C2 +PB11 I2C2_SDA I2C2 + +# barometers +BARO SPL06 I2C:0:0x76 +BARO DPS310 I2C:0:0x76 + +# Battery monitor (defaults from user) +define HAL_BATT_MONITOR_DEFAULT 4 +# VBAT on PC0 (ADC INP10), current on PC1 (ADC INP11) +PC0 BATT_VOLTAGE_SENS ADC1 SCALE(1) +PC1 BATT_CURRENT_SENS ADC1 SCALE(1) +define HAL_BATT_VOLT_PIN 10 +define HAL_BATT_CURR_PIN 11 +define HAL_BATT_VOLT_SCALE 11.0 # 11:1 divider +define HAL_BATT_CURR_SCALE 5.882 # A/V (170 mV/A -> 1 / 0.17) + +# LED +PB2 LED_RED OUTPUT LOW GPIO(62) +define AP_NOTIFY_GPIO_LED_1_ENABLED 1 +define AP_NOTIFY_GPIO_LED_1_PIN 62 + +# USART1 +PA9 USART1_TX USART1 +PA10 USART1_RX USART1 + +# USART2 +PA2 USART2_TX USART2 +PA3 USART2_RX USART2 + +# USART3 +PD8 USART3_TX USART3 +PD9 USART3_RX USART3 + +# UART4 +PD1 UART4_TX UART4 +PD0 UART4_RX UART4 + +# UART5 +PB6 UART5_TX UART5 +PB5 UART5_RX UART5 + +# USART6 +PC6 USART6_TX USART6 +PC7 USART6_RX USART6 + +# UART7 +PE8 UART7_TX UART7 +PE7 UART7_RX UART7 + +# UART8 +PE1 UART8_TX UART8 +PE0 UART8_RX UART8 + +# PWM output pins +PE14 TIM1_CH4 TIM1 PWM(1) GPIO(50) +PE13 TIM1_CH3 TIM1 PWM(2) GPIO(51) BIDIR +PE11 TIM1_CH2 TIM1 PWM(3) GPIO(52) +PE9 TIM1_CH1 TIM1 PWM(4) GPIO(53) BIDIR +PD12 TIM4_CH1 TIM4 PWM(5) GPIO(54) BIDIR +PD13 TIM4_CH2 TIM4 PWM(6) GPIO(55) +PD14 TIM4_CH3 TIM4 PWM(7) GPIO(56) BIDIR +PD15 TIM4_CH4 TIM4 PWM(8) GPIO(57) + +PE5 TIM15_CH1 TIM15 PWM(9) GPIO(58) +PE6 TIM15_CH2 TIM15 PWM(10) GPIO(59) + +# LED-pad +PA0 TIM2_CH1 TIM2 PWM(11) GPIO(60) +define DEFAULT_NTF_LED_TYPES 455 + +# BUZZER +PA1 TIM5_CH2 TIM5 PWM(12) GPIO(61) +define HAL_BUZZER_PIN 61 +define AP_NOTIFY_BUZZER_ENABLED 1 + +# microSD support +PC12 SDMMC1_CK SDMMC1 +PD2 SDMMC1_CMD SDMMC1 +PC8 SDMMC1_D0 SDMMC1 +PC9 SDMMC1_D1 SDMMC1 +PC10 SDMMC1_D2 SDMMC1 +PC11 SDMMC1_D3 SDMMC1 +define FATFS_HAL_DEVICE SDCD1 + +# SPI devices +SPIDEV bmi270_0 SPI1 DEVID1 IMU1_CS MODE3 1*MHZ 10*MHZ +SPIDEV icm42688_0 SPI1 DEVID2 IMU1_CS MODE3 2*MHZ 16*MHZ + +SPIDEV bmi270_1 SPI2 DEVID1 IMU2_CS MODE3 1*MHZ 10*MHZ +SPIDEV icm42688_1 SPI2 DEVID2 IMU2_CS MODE3 2*MHZ 16*MHZ + +SPIDEV osd SPI3 DEVID4 MAX7456_CS MODE0 10*MHZ 10*MHZ + +# DAM +DMA_NOSHARE SPI1* SPI2* + +# two IMUs +# ICM42688(BMI270) + ICM42688(BMI270) +IMU BMI270 SPI:bmi270_0 ROTATION_YAW_270 +IMU Invensensev3 SPI:icm42688_0 ROTATION_NONE + +IMU BMI270 SPI:bmi270_1 ROTATION_YAW_180 +IMU Invensensev3 SPI:icm42688_1 ROTATION_YAW_270 + +define HAL_DEFAULT_INS_FAST_SAMPLE 3 + +# compass +define ALLOW_ARM_NO_COMPASS +define AP_COMPASS_PROBING_ENABLED 1 +define HAL_I2C_INTERNAL_MASK 0 +define HAL_COMPASS_AUTO_ROT_DEFAULT 2 + +# enable FAT filesystem support (needs a microSD defined via SDMMC) +define HAL_OS_FATFS_IO 1 + +# setup for OSD +define OSD_ENABLED 1 +define HAL_OSD_TYPE_DEFAULT 1 +ROMFS_WILDCARD libraries/AP_OSD/fonts/font*.bin + +# setup for BF migration +define HAL_FRAME_TYPE_DEFAULT 12 + +# User-defined buttons +# camera switching +PE2 CAM_SWITCH OUTPUT LOW GPIO(80) +define RELAY1_PIN_DEFAULT 80 + +# Image transmission power switch +PE4 VTX_POWER OUTPUT HIGH GPIO(81) +define RELAY2_PIN_DEFAULT 81 + +# Bluetooth control pins +PD10 USER_GPIO3 OUTPUT LOW GPIO(82) + +# Serial functions +define DEFAULT_SERIAL1_PROTOCOL SerialProtocol_RCIN +define DEFAULT_SERIAL2_PROTOCOL SerialProtocol_MSP_DisplayPort +define DEFAULT_SERIAL3_PROTOCOL SerialProtocol_None +define DEFAULT_SERIAL8_PROTOCOL SerialProtocol_ESCTelemetry + +# build ABIN for flash-from-bootloader support: +env BUILD_ABIN True From 6b07a9d516b958af47ae9cebd58b1faa48aaa63b Mon Sep 17 00:00:00 2001 From: Arwayday <3521301631@qq.com> Date: Sun, 15 Mar 2026 18:46:16 +0000 Subject: [PATCH 11/99] Tools: add HWH7 bootloader --- Tools/bootloaders/HWH7_bl.bin | Bin 0 -> 39668 bytes Tools/bootloaders/HWH7_bl.hex | 2482 +++++++++++++++++++++++++++++++++ 2 files changed, 2482 insertions(+) create mode 100644 Tools/bootloaders/HWH7_bl.bin create mode 100644 Tools/bootloaders/HWH7_bl.hex diff --git a/Tools/bootloaders/HWH7_bl.bin b/Tools/bootloaders/HWH7_bl.bin new file mode 100644 index 0000000000000000000000000000000000000000..391cc8c9a2b7446d39ccfdfb75e880a6b07ec485 GIT binary patch literal 39668 zcmc$Gdt6k<_4v%acOSq4i=ZwKWglP`*2fAOi6n7Zma9AzAF(wiHVbIG2uT*r1Dmui zJ{r_CQL%|aG#b)GW4|IONzjlrnlx$Cw0lJp(6*9T-N5E?SHQjO0{3_30%_8I`}=%< zzt89Q`TcSC&d!`UbLPyMGiT16xk!$*pHoPA{eSS_C;w+~{|VP8EK&}@qg#lSi{aS> z&k=Y&f#*Nf`G1)|IM#0DuaI&sJpWx=sf&PCcs9ZFTX;Bl{yVtwyg?{W%}2^*@aX;n z&ab!p?{SRd`oDd5{XfS4e@oZ@k9t%$;*l`o-SFnTMXo+&?Va!&2ETXaEA5tB2>l73 zCAa5P{rMKcO1rzS_<48VqtBC)bGLXXsa^KNl42gA&%aTJFS&j9SRJ|6)fZPwYAN{n zK{@|ssF+`APR>s(*kirhog*+2LGP4&&G-H#Kv`m8z4c!AGAOmeBY2Tk^8*+KzZ`JI z4aF5rT>LvLedRuZNvvHs=tcbdgE2)Devk+4o)(H7^zg+qzEo|2#5m?Te{3vWFKUt(>1eIO3J_ zn+0@j#Ea01wt_a4rgS4?lDn7}XTxJg{9|qZO1e(Vr>xnRY7Vfr-cP05~TJ9IkE4)(XBz(wuBO(18WblXhydOV3+*OOSOrAC;dX{ z7Jjemh);!~CkvquBY2ut)f@`+DMGO^mR_6_6{7iCs}lNxG3^(#sEf6L^aYUdtnpQa@%>7hlUmx6Znh?5n;g#?nh$Uh@9luT`9~=!1%(qF!31 z4J!I^P~Ml6J4iU0k09L~8WglDLP=OiBT=9XMF9m=P<|^yqC)8x=ezh?S1s^pyX&Z* zQkK|8X&L0hMrp}Gp{SAPBw8>Ap3bSf_*~xlE-I_UX6aR04u`0HM|@HEi1j1(HTFjx zD?DqOPx+Ko3FP6RBA^F4{FdZg;8hixHp*8Rd%Dv?MqU@B`}SCH?l{Lzjq>2m2vu&c zJQrHNeaOlLseZx0?d%aqApVz{2 zUJv_}m&z`&Wq8eOn3u?o)$3e{+*z%d|4k6{%ZB9qXCpFJ&i_$lSWjDY;X!M>2WK9% z=G>~C%eZk0N+mPsFC(gE#6KZm{?icRXAUdj-=l(&MSQ{_%5r1yb%dfIfk=vQXosjjGOTUm{*9qm%^0U(S%<^Jl0+q+e}!E+lXS zUT9rfxU?XNjd$SLh(F+BGV81YSL!GN{b#wg$5mL^;Gq4ej2=Ll^;RN%2=J(blvWIU zEanv=U5D_c_JZ)Gx>R`wElbPv9b-{D;rOnH@sQQcN;DU%Egy@5Hr3i=nKvZ+tlRWigH80D|1B&NnaFP z=G@!ePh9BxgX}`zlFSD%-78n6u|>T#PvX1I3y*CWyr|8N^=qSVapRcA-vkc=Jjiff zSeh}OJ36Y{S+ZVf7YgkNQA#a5qKGA3j%?H_1UjDR?cbcdCUoxA>O~#3n>pGgD3^jf z29FlRRyVd`-Rp~DoV0xuACVCmKSM-@S0ouZyhz3;!9OA+R6}g@p+c%Y!F~{G(k9x2 z46)7w1uN{Z-bGL^{e->3ej+d@F^tWX!Ib8@!bAr$A>O!!jMg>bq#r5!u9DLC29Nr6 zbh4?Z6Th-tFz}y;BjqqWyCRTs7QDxwi7KR2L?PuMyjMmd+TW|8uWGv47v@!O4R2q=982bR_SMy zn15Rk1Jg&|_D_!<;D8eiz`1Y)Ixjgl)Pn0Ue{pbTGj5>y2s*+9>t+h2vn|5>L&AN4 zONBo1qM0W=B%fHe2|g*D!PYK=Wt| z&$9QjQ=C%@*VvB%B@6n?o5pF`++Ws2T4qKvw21E!$ykfG?{CrocQ*I0Y*PAUG+@@& zSr^=1()9L#Yx9CYK~r?$+j(?d^4|!L5&vfq8O7TRn&<{Rj}~wQ?|l|@!@uek+is-1 z8y*Wh&w~CScFRZ5@+OH73;L6rl4rzpe&E+q%<)v7M$hM4^E5W2+se{=F@I8^>qXs8 z;3+2*_a3D`>U)NICf~@?zK;wyjN}&G4KY9Aj2$>a| z`!y0(6+r%qG_Y%7*k(u`MXymFy{HDdpzN$B=-?EHubWU6KmN?a`Bw)K zUnFYUP^yv5KgqY3v+0Xy{w^4mLN2XH%hG(AV1yjIa0mRZ5p=8`-j|{L2IqtpRHB?! zH|q**cG{llFtZJg{R?F<-)4FeisKjE1@+wm`Tu7w9_HS_2*YMIYYG*C!(!9)UTG>k@3VgIkhddl}gj%R0+M+fE0)m-fwFNxyU+O1DAj&R&|CZrRow z&cxaN=#csP=P-LG*|zq|D81!Jw$+vlTU5F11;*E+m%aSvl6!4aZQFYpI@VHQ$+Rsg zm%l(kTk@ArLdi_qwq8nVXRhtJa>Waz9mUJXp=6qEi`2?}mN~YgOP1J3E8}fQEv#2c zEht|;@~ic0^>t7p4Sx8z7f1!B69pANex9u*03L7T|S1D`2p6&64pH1_i4=^ zqg8omI-ylI(Xs|;%^Sl?A4(^*!uT%+S}hXRdu%32ChBaFAblp=-D9-M52Xyo+uB#F}cuzSg{&UKzvm(o}(Ewe7Wmw@u1JGU)&DTklrfXL-P8u%#Exfzis` z7;#7iV@ToqEjrsY$fJE#%P8O{k+~vEs!d;H1AKGy;t18$dW+tc+g<(Y`p>@kbNhQg zdh5e4uR66UP0>#er2qbzg|V#pDERF$zh_g{OH;;6F6zcHa{N!`pr;2$Oo{7BN|8?BzJXz26V?yJ% zLtWIPKdZT2^^Csc;KQn{8^d)~`-gWHrT_Z5+)c050lhNc2EFW15^zsOudoM?9#Fp6YsiLQ98AOo;Ho=P8zm85XJCBfz_+z9k$Y={7CGQ7+Z zZ+p0~3|gpKmPE#YF0>@s$_q2cD4POffI%ynZ0kTCHX24Hx(r5vP+J1y-cq{QR@%#i zrGXsp=%vH*Z22GuYd}t#VOB7QN_uJK6kBaCr6gncm~E}phRL=P@bwuUw*juGiMHo@ znW!kAj9MmPoJ%l9rP;y{J_kOw-wwDY6)mo^y>@%+odoMN8{lM!CZY}zjB^Q2`4rm{ z%f@)+A*+OUF2O6GX#1&UL;L~>=Q@H@o(2;96APKyqx1nL`E*;>;vZS=vB5aP4EjEe znPa#jV1&l|JZp?X8fGhC1er~5**Np>LZwgPplDzImzkA=NPwRhCOwWDYuKRob>jdt8AqAy_AC`7>2-=g`Z0Ex5Rt2Xh#M-R_It*NR9;f`n zi>8M>#r;7i!~b;{Y5Tl?%|u!?*!Xc|FKGaaVPOI*V{2hI9M4j)mtt6#R>r>_BKwIF z51RgvCwqyJE#_U}8_RR~b7dP#J{96-lhp6wE#p&zGVo=hkgTt`-&46Xx0t^tYO6K# zVn+FLoO|h0@JlVj3ohx1KdW@7UGMk|p7UUxkY9A}l7M!D2Lb$1nEC^}$N%g3sXzFC zQ4zYdPf+c4WtKRif?>D9?SF#P0U7^3aFOEQ60=wtUmsEzt}eIo=So+X5Z@O`fgcPw z%s&%RSij!dk2choN^fg@X~~=5KL8Ak!IODiT}R{boihHjL2~ARmJ>e`GALRp7V9GY z3iNA1VHN`!qPF>ZXWWu9xXSivt@7|xnc$5+b>R|q-_ zW{EBLw{oWNcdYpxJIFX01Hyl4f7PlkLaFK{GJe~j)@61WYPVWdo#1__#aS}`i9tiH z%!adD2VAoZuKo^ZyNs_K%yd9rChWHsWZG|ZeCho=;M)(6jKBK~*;A1n6>`9Ag7jWQ z{>k{}HiTq_2@Y9dVr@ghRx8z|w#j&w*v$(`U4kPE>}$p1v}U72Yis{9%Oc~Kw2`!o z&l*I=K2H|xdk8G41D@ukXyAFZM?gyWhvfNtgcNgTGcqXk%Jnsn76WcKCXnuzpbjq7 z5Q^MLjUJ3I#96dnX>+ztaZE1!pf7@%p_*Fznl*xZ&=Jn!ySG?l+31ULgZR-U&6I#E zZhP^3+OMR);%clBEOl|Z9{eRHCMB$3Qtb&V!|k_jbH7jxc@_9>%Fj@J+^Ph!M@OkZCj(ZNuwC?mz;LS{5a`pfc>oA(SN%S%?8 zOK8Tk@%(;o0?EXvS7A#_Cm5PDuBIKYP_MBfy$$T#Kiq+`Wv!Ax?gDJ|g;+=1;d1 zc^IE}1RuenY^{1zRv2F^E7a7Af$CvO7X{v*$A(MZRJM=LDRXPMO}TgvV2=@S_9~0g zVCh2)&Yo_G zqd~*NiH-^>&$VncZ=IBv2y@u%BtDnS>WF4B@OzO?nOI!RBW29uSm67I0^_3jjv+Dv zs!&!lzMG`FhT?dU`P}ohHzFVFBfvwrVFa_9W>WT>p|89O*t4~aLP$ntp+WyJh-*5)_SF7fK#&spSm@t8oUHrJey+kF_k{a-S{A_r+w!# zDLhv;|b!U@&p^g#}G z?x4p&gH<~hnt>Ntt&!kO*0hDe3TlG0AR7Gsf+wLGr4M*&YOmDpvHH1H)eP?rsbNj< zaR!IaOX^r|EuRf`4c|5>8Wml5dPEl;PNg`UCa0FoYBx8d=|K-pF**ZYlrjMOeegmf zq65%0(6TNWSRT(kv9$u33OxG-WM)i-TqbzREO%(T-x|pD5c%%40Xt{5*VBuG<)KiSlQGc45T#;WW5zM_EnB4nqym8NhkA z-+Q4FXa0oitw4sE(^gK^uoDHGjLq6Kl}85W<2Q6+Muv+p9ddg$IP;eq$?v9(5k?>P zxdv$$Z9MWzlvc|(Yi}EzW5_t@^@@g;Q_$;(4h8i3L#fwcQm?bcMa>H6F?DAR^jT%z{{S*x=dxH* z6Xp!1aU-okQ~$(aZ!hb(1wl3AO>6` zT6>wjV`o`S?5?N1>eL9f4Qk=k#wYABep#*H3rd+QP!Y;DGRhSHjZmgM4^sGk<^+)4 z`j&Gb>9lV-SnB69Uupp#IGWdb0c05FHF&7#lje5hFTz_YvB6S}_b~jKpp133HCA*x z4_E3dE+o`hdoMOtgXhe2lcQ|dU=Pfv&*Um?S4_Tx9M4IkO4*q zz&V3su?lmr_IGPxeb)|2b&!E!FF0WiWNuw62L1+;QplYa3YHh#UTHW8R%V1gHJ|Yp zAvvE$eDj`tLXp`ACuEw;t1Z|xvvU+q&MEat0Cx!BCJrw;bz1nb8AfIERBoSezxitO z>Zu7<4d7jEKHj`fc+iZqu}R%|T3BPQXg)1GWZrC&cQXD;kh4DzpB!*4pgK+qrDlk- zxV=)l-Kug^T3Vac&H&92C;^rmSa0qk&`Yb}q3$BKE#?(WF^|Jp;6Zqh>7UH*haEu7 zo$vL16!wpn*Xn!>6gFdBHLU!yw*8N5(S}F$MOcKtp zKI~H}t}E`fy&x2!yk797x>4A*t5rh+t!@j+LPLT5*LL)$1Nq^u(mhD=|1Xx~ah(Vc{6NZSaWts7<6y?qtutB}d- zNnoGcv6*VO#<0vqGgt+G7&=_`h2$NcF6JgttlWc*A9wYw-)jA+>+8f`pz8w|KO#3| zmp7SJc3pt+lXcP6l&RE1cVd&?L!~b7ra?}Foi!EMNBX%0q8yNM;V4&W!qp@j>5amp zW$)mf3(39{0f!a2cS*a$M}}r~W8X3*slN(FY$nvVoQ>l#1EUhA^yBc4`jkqgI|P32 zWOqEAywQ9#m5phhr#0@_+%U+&hUSVsC-$v|`cb5Q+aLRs;kM=v`xKMIgm5P|^f`@E z3zQe#r)5*EKm$XXcq;Hr|{S`X0tr-c1BMiR|Y2^Vjz4NY!zcZ2l_Y+AAo%}>`}$Q;~^b;bghD2 z1oZ|6D|f7=PQ%$6oWk@Y8>BYR>e8|dKMhX#%*}ch>DGp$X>eXO7}mX3`Hk1Wo{7(4 z9o2BA0H++&Y!y2WS^GQSgqEK&s6U-B~f&SbkEH;fIs1!Uk`3{JZfY~+#m6v)4? zCbHS8_~sJQ5ns-xAqwIo4!9m+nOhSK%&q0R3|7OcGsVERAc<1kWespB^+ynih|+!m zGhPhXhp=%|CpNxPY2nYg82^uH#{Vn^3-bXq<0*)g8-x|_G^~TJ&5yx4__jIB)#kgT|qH93BS&srb%fj6{+@gIhgCBc2d8MC$wn;M<>L#?*mbTDi%Ik=;*tOl8u zdTfP^|0uvgtR=hOx~Ow&QfV3Ee-&Ma-Jv;%Acl+azl7cwEG{u%461EV^VZ=qd5G$Z z^(pB*@P@u@t@6R?L&r>(@&67mO$>IIt!pdWamuRe`n-blD?L~fPu`z~_P60X%i`-` zL~BfO9_?JBeZTb5T0EDTvAKU!6Ju0xxSKJ;j7=?s*}M66byH+2>y`QA5Rn?P7Tg%b zMj6}Jbse-X-&L|>%Tz-p_}ene)*gY@uG|*Suaqpt7*6p3ew+EbCtq*t6sV^tR zfZtn1(=)uKrDaFT3~P%3=M&}?y-Ip>e^t|2;7c{&UOm`dM&A-+-}J4RCHAjtBJx1b zfPQ8c^Df#@^$gT{+Kf$Gxn)UYEY!fE&HcY<0^3YaY%Xn8y>)Z{<4wsR4@H14V{kwG ze;8sN#1+hd3g%bZAVig|&}NeSk|r`w#X$1l;gZqWmoR8AS?PTnY~cm{)0(Pm)QsZZ zh4klTQ{7$h3ec3aZy9~U8!wn1>MpRq21GG>`SOw+ z;>T35GDj_&-}XrP@0)$=Pg(c7@wDURjcq7J)(z`N2pd6bcH$H*@!39aO@desmk;FO zb|;B6#M8d?rdRvcGwBw7tLrsC6;5S?Ef-Or2bvIK7C5%N66`l${A@AsEZDKxTo|vQ zd)NP=?;{)mQIor;UTs;gDPqA+3j>QzTa!n@`B0|&0niC)(Rv$MP{KQ^q zhOv9;{3He$*BIC)MdGBlPk+VJj;J8*Vv?~e^8P7M46 zdL(1VEr`)|f6z;;v`L+0m-Y`&iKLJI40g{)(2rhEZqnBtEUnkefs0=5I;`ppeZ49T z^467&1U?ZDmw^?ky`zk^-7M?yD!{8Dv_`?o3A8)JKzne1n~XhDL3s1CK}XyC>gyFu zdSbPTOO!`lhLu`c32I?&{K~@^m2QPr-hIP_K@NBh&uz7S!wK#0RTd@@-wgvYFRw`E zZrlN~sytFbcz4g>{?T}>ic{9ei=!*eD!_kJ!Vmizm>(NJ zrmuR4Ux(m@_1AH%LS7CrQ-chvZSz$mRBunGJ(M(i_?irshIny^Wr7untV@I3lht_M zX8gL}PLusfFPu;iT2e&@X5DoUGJe4wt1v?Ezk!%JXo({j&!vCL$sv~PUva%47|hZ7 z>6ygxf6JGfe4(S5wc{7{(7Ij4cwz_tO81dmFqHVHf3aPVldIm42;q zVcwNa0#V;-W=8_a;zsa*@AK>h`HFBLy}FgQGi=2At@gt8=SwbB8NlndV?CS}WxNMF z)9yhn?5f+pR9nPArJMM3gjU)CBX&npxfZYqBZr+R{HN93U=8w zNWd~;$-^8ko;7c9Es)} z32YJ-(hg{iC}XwFF4n>-hz0h1m!LM+KYPZ?b9I8@q-ENgJUOXh&J-q^k?984>-EbC z4EVL~-2VXP7Yn3x3k&6l#C>ha>)@z6@!T&6RpIbtuVmjBicTJ>SOhZgOPE&=ayKjH zRijx?dT#yv#w_45-B?9?CMcN8(I&=2V2it>P4|IiRcKx%UvO!YJnyohLczNgF9?x` zudm(s)QY@&xsAJ!DVv*Hjdi4b8NWb=>mm*Ko;A6~La^^lEvu_ei8Ew%X`O18*^Br= zu_B4yP1WHBLMg;KiS&UQYXZmAQwL*MSwmt$o%IVYvwF3hxf~C1u%y2awEbc#84a?! z7sC$bC69|;?`^lX!$?4E?QPMy#rlOufYBhcrFwWN#2h}*MCiV%sJ|2eaZ?t!ogw4b zfF|6_Ee4J-$^uxI$r$0iqvNz&C_H%pMk|G) zmTe974IG2Lo@2NMR}AsA#5<0zYnP_^GM*^A#*v&HA1>)==!FNa^@)6 zlWh@nX0rd%G}o}%-FWT^@25TVeHJ(|CG&cHXuAP|V)O>*IYTl3o{I5d=L)gvFj#r7wnT>?xlbujPAcOw`7I9eo%l{P>n67i^xM`X zBU|Pi2}~Q=2B#?~e4CJp7P0Y?gaTI;k$!b}B9EhA{8E8-!h`q@mjzlUEf<>KVf!3! z`(@GfQ1ZJkX*6A+OvSwutk1%!=KWp!r&?LFKqaFzD;MdHRw;B8A04%aoe&n~H>10Z zweBeYJcRF32{?@MwE3f>zvlHmMxW1T=RDCL_x>s*32Jgi+q5JTa4Fyv*8&x6d z);4zk6P-_3@)88xpn}#k?t6s^i1vj=!0s-C7Q#?*uR0CEN^4=?@kd7Q67X&>XUU^H z0H+Oa%i%3J9()``7W%+`JsTRO%h-hPd5cTzF6QN7Ixhp1CO5&@oJ{1YXf2!QR?Usn z;ndcUe*oHo>&L zkoopS1S828SdxxG}b22L)B{*0(5vvLPJ%LF>~B=$UL3gZgc*y=s78=hqf!hZoYGw!}B zB$*rS;9)3fko4}0WY=V>9#M@jRua#+8z_?Tld_6??Mot~PF2)$a9gx#J zLs|TPb)j^`-*!Rk#A7hp_VY|C#C6Z2xE0XGA3b}fK67@o9W)E^F*O-NEQ*IUO=}$V zgtg6ty=ERy%XUvS10^%THp2@BLDIFbZ$jZ^Ydx%C?fb$)v(m6ADMN^{qusZgwKHkY z%$bqg+u#`|b1dH}Y zI^&k$G?C%!yrQ>E!4WxHSm4Xbds*`-jxLD_YKyn}m}* z2qLZ7{O8u-gY2o&;CaLcYXl1-|H(f{+c$a%)I-zyy|4L8Bs_=Q+Its))_b$1*Nf7X zZkcwPcQy1crKxe#RaiGcCJ|$t3o@x=zu06bHI#$ofs;j~V{^+&uU8V8Tt8xPpgXQO z;vt@z#PNqUk+U%bYi1#Mnv|!Kji`R}?U56+pv-;ZLHK>=#Ac(c6SGke5g$K@>Z{wt zS&<%)yQO&ERnSop;Kh8ePl@Bfx3FyFu>H?ox%SFUnYQyLaz6%h^I>?_!js(kNe@!f zN8?y}&qx4)jXm*dM8R^HI}s313|ggO^lgQl>CH%wk2yfQeFSz9_}m}{IU*$}`4?cm zF5G`|H^k0B1T#qV@*1!ziKs!m1@a&wV*@_W zX=jcml!yU};BGrO_yE{CZ@c9w8a^o+?k~8|^db)g+j3+DO&KC~*?f-Wm2$cv0W3zssVbT&qv0N-3xLg+a{1S@ayT zf|>^D2IgpD`Bp)v6$5L747;lBm;;}pV7E6C&vbaNK?-o!z;rw6B< zT2@0=#Agl)lgzPvOtgjH27VF--hY6%GP;SQ>t`!s57bpIa|^TPoLn&=W=W?i+>!en#cAqeN>BI1AhB3x80*&(;crurUgQb>nsV532CY1Uxb;`QIk0|14c()mH z)iD2nb_>77MK`S8Q5cTrlhdLEOCqBHT3~Tyk z8ob+PdOtSIXP2=S7@glk=}X+tz>jHRiAH*F06n?P_7=qC%wy+6Pi_uh@G4S&31i@F zec>c{A@q1R1$tv70OQJbw-Jp2=j<>Joz2vDx`L8)#aAH)&Uy|Gt^kU{uTE{A%~D5) z17C|IYGWqcd6{(5*)C5Bz!<|>DByqlBn8-sRvHQX6tFALj%p+|Xe7CGOW~)Uf9fN( z)W#WTV`XCb8Kh_W<;F^W_BFM9Mgbb!WEJq zB9K?*>Pu>9?+Iy_dxQJYSu%sJ0@iEZ4e*^MLbaLDYqLmX$ z$WC}3;q=*qLIccz!=T+d3Cznx^E$Z1a^`om-}2qX@d)@;U)`!(RB@OV7?{ zm6`M(TfirwRDoY2X)-$iFh)sp<8D|B%C`U<`AxB;gl`d2EeYkZo4Vu=x^sCg9a}X% z4(B;yrTusE6q7hhe?a!We!%jKR}KQu`&D+T8zO6TN@8 z@Gw|lj8zZ|$>nJ(VDDQ;W zN()`+%E|=W2I7t&J_xx99Kp`F>Mtf2Y;8%-hkp+l-QjH9lsN)3UB|xBTxX5#m?KaR z&J6#yWqEM>z)`FIigIRH=d~~8yewQjA#Rc;Aqlw>=77IL!}9I>6A;7@B_=eq1ib3( zkXM^M=rv~tyb0L@US(#VcWJiY+cywBFO?QJTYDQ~BoSq~8+@b6R%KzlV@DW)+llM#vVsnPRl_~5 zQP>&(1{((Z$9r%%Lts~w1K#goC;uDljz|fwOu`GW>hPN=$uKWS) z5BT~B;QS6Q=Lc}g!hhi_z^We|$7%XE*l}7VzCH?gzk{9lZ?NOEN_>4Zw)vvqLgSc6 z-W}%`GN|&^tqE*IQH1?I!GI_`_T?fb(`vVvZgYwP;&-gGci7Jh$~or+)g0JMy9u99 z{{|_EEJquKR&|%!sP@DdV>}a#N-jD@p}i)2)QzY8T|i3@!}l-Hv=!Y9M5#rmAg$l4 zNC|o6K@_;hTEI;!D(*!|_vA7@G%3n=4*eNwoP%9dsAXfbc9BQuG!x&{Ie5bk9ire# z8qFr@$^Dy)qdb$xV5slGkn||bec%P9!Bd+|({`C-B9CPuj)7Cbds6DaSNo8I399<< zw(C-4c3a7W=KW8>UEv`!iM=p4dZqr^s8yt|>Q-~gHuypT_@u|aRWN$*)de+vHiQim z^GlzigVe1EhXHQDTxp50Z)l4txF~e0bv(xB;eAm+F{5b|18H3>p`KF~b_ktXJx@^= z;N1cFqiOQJ2{c>XPTD5lSx}CBV?zwQKi0moeQV`;i1k?NP`ApvR!=QLlrM)q73%NP z*wpRdXQFp17J;Ux=mP1jq{F2*L;q1u^40WCxwV3%V9m1j_zF=DU(a~I zM{ST5Hm;QwUN3`Zt-77Wq#`H8>Ysj}iJJ%U^mifch4m3_>cR`9^d@YI;i3!oTcfxr z=+h;wwcnL-0p1CAd;N{~cTu z;GfJz7G8OjV88OSE05~O-lRp69&d3pJ=!8!CnVYdx4P9?gz4Ds^75h<>=zjm3e@)i5%_L7PiK934{UMY(K{|)Vd z7$gyE)$JwaYBt^;^g;~oe+w&@;bvRR5kj|=f z#IH_n`H=UT+?Az>Zy#QIwycs!AWqF;F9@r4YuPF5>dT7zf3x9Ane~=}db#upcu&IX z;aosG-1bGKx-iBOVR@=;^v=-(V9PFQn+ZD%d_em!#lIdhL+mK*)VF>?c32Sc^>cB& z>_CWz)Aet>%5HI((4EZYR^q)t&xSQ{np?|`()kG54s_lgXbs&OSed&9Xsj64He)F! zmEip)g^y_;&VZiY6(V-D-&0nJ>n{mKyX)6sm{p^*X_Wp$rG|>D@!HUtd{AAb{4{)QAV}lj40rWVYGZZ*I z@}(Exa>PJ1Sa|mkPD64si{?VAlji2o+B$nU^9LTD!I2=N#G{$UwA&(UA5Q_Lw%@Wk;-tbv$5 zSJfA=7UsZNZ=<4^H`63-!FDOng{7~}O%Vg|JrwDyL|@3lFOPn^2x^uXp@bbPfx2PD z+a1Sim04W2e9~o-BlBm4Yvt<8J79E{!g~4(xXE?r6{U{UOcU>bsG>|bNg%CklE+;x zl}{MMez_;sP;XUorpnCbJ9vviE!9wq^TwdB%QctZcqF#`c=OV7khF!Gg`|az>y2 zN=QC@fl4Q>T2O-Zqzvj<8sIitiRF7R@O3Mf4jEhmg4?4)>qK@7Xs`(O4J*31-EpLR zTKN+Rt=5Q(8aACB3ZxA;mYZyiB}TT<@jzlDte>i`rbG>!0cDB9^V!cGX4?>aljgc3 z+vf9*)`^CHH((vv>Zr2@;CyEyOE7~U%zmeYncy8BDg(S_-{GZUZy(u}!8Rr0Jwxyf z8hl_r+wGVQxW61CxO=>$))$ce2dtIhHk5LqglgCZH*={Hy7XKRLg#v9G3s_@tEWen zQD?;v1HGjU;-E9O*jFc6OX8hK2hrLY_0|;k1xIcP=JkWHgCnbm zC`Y%HBvsSpquksI+1gIh;A4mF8n7*K~t)T#X zRu*#J_qq$p%M5Ue8ckE+H9qEAoBEq2#0p0IuSEJdtn3oz^KeFU8GK&IV8$^f(><9l!_o<-AA&O1=A4gPCnuT+FVjtTlh;w76n>i8=a3JK;7 zh?hmi*C4K8oQmgNztnGrh##7V8=OXRcf(sTO4HqiyT49Ef34!Ai)^ZA2@ zio>O&7&ZV$))vSm>`ws&Rop}QbVIzQIauS*4WQjmhL0ig|Q38oxw^N=(3bxkXm zPz{+7m1%D>RK)U8(SVQI=%La#_y4%*`ug~4oQC*)E((6@TnK)5xC)C2zPKQLd_Ma` zcMQ;_FB1b(gZFvyJ)8SiH_i9j7O$OVE4a59n>6l;>_10hCQ!Dy-vnR&%7zy2Xrhm& zvuP4N5mL!iC_yIi_M#?y?}GlxO`{w*JMuR;aUkES`IUE+OE-aUPYdw%9o-%;GFZU- z3B0M3!>y<%0Z$)yYR3a-8Wi7NPAU9;Svi#5;ATIK`Dh${7o241Hh4)72b*!)=Kk&z zq*uoVX&9wZx%mv%tj+zGPS6s6JSXVmlzx=Mf0Rl-94onSg3JeUvcR`1i=AF&@gtHg zgF8$dFrht|wgj_Q55v7hrD>x|?Y3_h6^;fU$Vsd>!tcC0{icFSDaZ4=s@QYONFEpA)N*97sv8QdJm*YnHRpH1#q)T`g2H= zvYb)443f@V_@C6g&)ulKJAc1{bQrlle=&i z%85o#Y=b-Va8CPMF%6>4Vz~HC*f3$oE=w$o89r8Iruc_O0BRB3;BU}v(mD`-ZBViyp}!RW z`4|sK`YNOe{Hb?Gc}3Fy1!+>&GztfNq4;BCaGOWrG6>uzNE3Tl-TFhSCQDSsarXmN z%O$F!ID#W*m|_<-6S|7~EgMUKF0#{y)B84cyQM-5Tm}1@*vy+-ClumTW*S6C1CM2o zA^$ci*Pg}_UaJbJ-EputCK%8Q6yG-#R><7mXC<5jugg9-_kca4Wh2F(8HT&L?b;WS z^6>|e^1uT~8SWTeNh3BzWLm+uR=6ovdhgm4+dP^B{aA59cvVBeh$3kvCG=#q(wiAJ zwt2U3pZRzjPSsAKU_WUV$=E##9;MVL5&t#V^F?TzaNkmrL&gyC*WbwknZ*!g%YYPV zz)m$h2@qzmRNterwIkliYVGm=qBSnA#lUlWDzanV}1VY6GP z(ISH`o$gFd2QNHuU!NZ0&h<>mq`T(3PWEkC>PGgu=*9`A# zGx>WG6T`v%N``z#Vi3M5276jaS%#s;_oU~XK;}F4N6>uhQo#VXPKX?jG>`I=@oiU_ zrznBy*G57Pa<}e*@b%SAzK1&W=W1+D2UUyZi|qthY0YK(O(-$ZWgUv)H;uW{rxGdjBlV)Wo1 zJ{b{cYlvvj(<7r67<{W1#t`!b?HDvP(vc`7B$%^9fe&Fs-tWPgWIX<WXrTRDz`{&mz5q&+Y*dj$qW;1D{FT~Wkalu zw%tf*w?WXkJkIa^nHj8hzrXMA_51I~ybkBwbI;Q~kDv2-9CKCNhuyKP-{lhnDX3*?= z-0W0@sw8Fnep}UCt5(@)b8>tqWz&4LFInf#?wgxW-D;tf|%I_Zt@K` zcqXH)`VDhD4Hu6>Dyg=k0y<8R*8F2-hq}f1rdne=fG?@>IF*XEqSCN75&4W8uJPC} z365s~b8LF!wxWu)g0*2Sv$mnVR0~O}P(4=E^CJA?=cg?H$J$NZZ=w&Ww&-AMXe77U zxNl@6)Pp?OgQC@1MYDNI*6wbpZ@XBrmZMxNsLDofXqD2Mcx#MqHDXGIQloA!Nx?XV zZa`E&OaH3}u{H@MQOPX*FCKy|>lU9v`)X{!$7unR@rZWryK5A(D;@z*N!%OhMLWCG zi|V?$Z~16{@JkrJL=T?a8jSoze$bPs#mKh{zWdeRbqhX4-FG?F#n;DE)q9e{sg|lc zk!q3pTzA~%F;2(*HH7DL2fyI_SGPA_ary0cGhpS|=vR@KNbnxanWML4&I$j`oA-+i2QVLBaKxQzQH%wU8Vu^=(9OCP#P01^rM8W;H{uCIS5)ad}T_+aNa5>gw*M)CpP=>~6z8x(egn?j3Ixt;RnAqJ3DY1~26n?B>raPdSE9tyDzWAsgitGVQ7V z@xJ4S??F=y-(5*Jj)-5$R2_HGifQB@?IqL{GZQP^jQM+A~P2nYKLni#Lz*Sp0>O725T&(> z<{9ZCve}t=1?Doff%&Ri#KL7tsj@1u&N19U=RfrSbZwW5Dwy@dGS-(SL{AX+(ekK!!KX^t-X6nbtmH1*|Om3zTyumd$N zzIMdeG(gV;cKu)jldH3Snv?|pb4>CL#`g?D-V5lzuK zC8bf6>&ivM$B@kl=qw5Kt*E4fdr^?%UC*P`uiv5X(jxu*Mu!rQdShj$EFqWwolq|7 zLlNVu^&~2SToTUrQ7|9DY7<;$rN+U$r0v8i-zNV8r5+g@=oUfo+pRzXyW=)xn;a=G zl~XW7pI5de?hMUs%+{EsB;^~d`oZ^z*(%yqRnu4sZQVrH=~LU*j zu2R+b&~F@C?@SlTl0hO7qjId(>praHC1xa~wPv7H;Vf~D?+RE*?^;19&3^^wavjP$ zdS0|LtMN_PAt-W|1^j=;JTNvVEVa7ppg(A{;t8+rok~RBPQW)}u!kDJ4=yNHms2&) zE|qD_EWkyHUb-9L9B6q)*{nq7J%g2S)u6@wh5Ko=XOH9WXD2~Nm(gxW1a3IIo(RYM zx%zgszR#%3xDgs#N(A48!45UqzE2U8U|XdlE)RG+Lp$2>K4NM9fyc5IDQyR_Rt2AS zkg&Mz=fyc`IQK$xaGjf_cX$Nf*J`D^9@_IBb`|DNI3<_`-wEJ}LV5MxWkHA!>Y4p% ztucv~ybIkiy#~V!$Q$Og7NXC3$C|stX+3dys2wb-uY%G^Ir`rT;)PqO4@3Kj;A>Ph zg{RKz^Tnk2gnY6-W<2(re)^g*YMW7V8`Hd=j8|P@6%|57Afy z{+?FqldaHWuoG0q11%G7ocjRqUmkJVgf7153SU4I?a5S1UGp*Gr_{Jw2e6aCU3zJ! z@;xen*O!Bnj_9@jFXhDl59Q2*ZjdV=8MO?!j!3VrzzzLm1Pnlha@EpSZeH6gWNrtW)PCBJR z;T(jIhQl`_{8l(zh47o<@Z$)-9uDtCcyBm-9N|6Ta2LY6!{HHxpA85XAHSRG0B$6} zeLou^x_zz10lk22@K38d!r>hjjipVkGZ*{ho7&XfW=&z6S`XYe(O5ujTCpDI0qO5E zF}+CkLW)Omu8kN`+NYoe*-_f3_*yB&5)4bA1xa%ZIM*CLy^tuA4-L99^Hq#8&^2j4 zXTE0QQKItWg#tSB=+;U7EI^-;I79OA8M*?}8w4#_Gc_HW0@izsRV|hZLMd76`sV?2 z;Vx>1U)8+x065%ncjB)y-!YZ%9rh2a&SYkJuW1!Cx=x&D+14u2Dr7^~_(Hzl+azTF z653D_ahHdTSrUg#6OV#?Q*vMTI3I?HlW2SRMv0zi#ogN+x-**uK2sOGGe^!8y$o75 zr(|8x6}n67y$1lHrnk_II&`0wI>0M^$TG7R(tiJbmv*B4x@OIc$$${KFX2itA;BEl zAB=@Oh1UlB7gSycee;FDORE#mqUdmoLi!RzX>|o-{*J(f?$DEnr*8wj-AC)7Qw)yU z4OrrasHLFHg9@l;kjD*Z`aTq(FP<5|DIo_m7eBlQI9=!)44s=l!ro`R6}oWHzIv3g z53U)GP8mIR_hJ81^}QxQh}q&M(qvL~BomPIu>We{==-5jg!I6ZtUBpP8unkKrY(7| ziQtvb1Nuhsx=T%|(4ZDToAr&xYc4fa%$m^gxmpcqjzjCFJKRB6Fi-Fge@C>iqi1nOG$Bu5t|Y!WgJ zh7WERzaV!hIR#LID8-b!sD1n^o`HMXJ7Z3qTqHQOtw-0 zQ@NMvM!aMZ<|dsAt%8`l&}pe}o=}vuX@5(=|4ksWs8jJm_A#PxCD!-W%$4$KH7ey4 zIDEBOO;QTQkxz5!u2sm5{8qj;U*9|lW%Pr`hUyU8JF!~qLML)lU7B_(CA7w(QUd-% z;M}1UJeY#s&{~c{spDl%OxxK?Yu+x=+QI%dzzBX}Mm$YS(>F(AKK>KC1j`aY`;xqp zZ40z#a9Zs_n#II4(ynzqQav0nEe-ge2w;cI(xp|D9fxkxDIL}_=u!!Yg3~um{4z+z zt%h%Puhfm4p;2863k^pCs1;Utb`4R?F9E?3Gwc$ZpaHR+}Yb5OI6W4 z5VW`hm~f1Gi*(r2eLE{?=Pk0RO}))AZgXe6UGu;P(B#Q+y@I(Hm%?s}!nel3(LQU0 zZ$upFIkav{YfQon`v5!yq}Pz8k9a_=0+a&0@;jlqgmbibg6kItHoI0dPV!CA#?@*t1V!!v-EonC_aY36aY@b{TN^s2gny=wI>u2>tl7suc3C7Ka^@Sxa zQvoMEj4t`GIiv~ZNjBpHFUPtRkoyz7>1Yvl9_@~8+~m&Hoos(iC$w*JSL@zu=g&_< z9M`?uzQh<VN!~Ebe;;IY=Ihr8RpBp<4hW+C~15KX#0`)!D3Pz0Y^QhIM)XYpM#a zmev+l%6)(n3Fmn?d}d8_RW&EdQWi#f!;sf_1D|HRQQMnmUb%hf0o_uV(5&A8~D zms&h<^k%xZeSy1G8Z-gv0Fe^TZOz-qdyh+eQE|^1>0hu`L3L7HRHuY`7NMR3XTT5M zSEg^|zSD14EhbK7!hz|6YcB1wRQ(&+Jwo@*p4O`7eT}fvCdPs$KhbMg%JW`Q?!>N< zs_b=$r|*CbkXIejSurarJ0`u)3)y|_wp2Hr!+#hR0nf3_qwx-C6w@Cmxs92fMW@e` z9-sc)Sm)0@fDr@sLS|)G4fqpsZ}6^>-?- zdJ~eIZw~ig;=@Zsy@_y4OwCtX;7PpY?R zc%(mSIMy0_xM_H-KYBRU0=snms|@0WJLAiftMgAaO+GuZk#d6qy0q9A6*buvH7q*M zH1W(>RXSReAJsv6UieiPfgSj=n_Iu=u7~DnoaR@3)J-|L-I@G^D%f6tq=+;51(mUq z9J|)u<1Bdrv7~#^-g67H_na`>dvc8-&ZFW1$p~G-t<)!W$3#}WjI&y6BpX2A33wu>3yNJ$1C+47<|t0I=3Udup+m6FA!g z^$uWNf^Yc_f(MD3LVGpOquoMVwACS<9^W^r7GoC?%rYvhRoXMsrKO{3WPzi^kOM zmt-?`P%4qi%aQ3!n7;80XzbB00rA8ELT^Y$J(yJ=2JvW(DRL zvl9<*zz?;U(8_$YzgpHGp?FG9?%RID7R?eRt8kqKXLe!57FY)j;mYv#Th?U@QijTZ zKcOhP@T`(D&{bxT3{erz$Rz{|fWy{TeNP*FJ#51rFilc&K@plHn=PI9Z&58&1Lg!1 zhcwIo^x6r+(TwwxMHj77Vh317oTu+!#%s}vnqpt|MrGlbWrm`2>#TRwpdDlWx5tc) z+9J9&Yl?=u371A|Cs-!hHXHbuOt`A%@!6PI$WmZQE@u3>)a?w~mm7Z`w_gW-bh24@ zFKpJaahr7#|}bfZ949#eLCEPzy}U3kKpl!T|n@7_b|X^As%mx4fca7@EuKaG7I2?xxZGVUPtUQ zj+6$Lrs*W#?yI&VhXG+o?*`){!*P*3vm*Xk2X2zL!ma8$g|j>MxSE_pYvM6ZTyDHf zbnnuRoQW|**TOsCe*!+kP>}_!$OvQtqs&=lkU<;Ga-RcKN#FD~ z!t{VTt+dUozm33kuI^_oQue-HHA9n})3*%{zV|J051-8(3l=@9ielztH zcu!J*5xRq*2phC>?=XTR)5l|;RM0xxaWy*!?M;()JFj`~Fu5@4NGeT!4^+V1mwE5>)2gWL>4;!#r>{jC2X(%&T(H|%|E+?|6 z7dWt^PCPH5@n;JVa>JkBjxvGi6R^f&QNFpM9Nc;sYkM_=&iPv0Z?};YL$G{j506E= za%k@g>PrTN5@|}JT-B~lW#Z80!2*7zqVb^2zM1c6{k@cPBdlj8;fpzL zm||V4wy*jYP8L_)2w!9_Hse~)kG4sDEBHL;6276NT}_UUbh4Ban9=F+{3_&5c*S1o zE}OdRVC=T1x9`&PnOj>+ogHX#UA&`5baH4#NO8Td)-mv<^gb+5iP~p zQ&U=yJ=M9I&sv7Eb6^ElP2Pl?(rq#laF)tvMOG?{bZNp`i)T+CYU<%(6nx!P#@%3C>`&*SH{E5fQ`FqrIn8eGa9L z9jJZ?U$h_&G9lHaP9H19cP+r17>xP?==XAB#247WpXkd)9WveDwHR4ouO7TshrQ=P zeLE~w8%mZDj&6McNc71sWc7yF0tV&E!$e+l}@D= zTDf8p+s?CTAxuR`E86w|@$&#PxEUNTL(+wHkCA-My3Hp+JL>mP8obMmUrhBC@+$NQ z_3?W5bmc4gmcxm8xM`Y?sYb(0MZjsmXGSzdQtW1x1HM48!Zsl zI8g>ppf=PWbk`2{>I_Gq;y)NF9Tz|H`AlV1>ny(w<~6#8vhy4O>;_|M(1LkH&Dkg(hW>1omR zi5hRHZaUaoJ@LzqTBW{EGTf?4Pokfy>w69Qy1YYu(!`4$@p+MXwQ{Nae$bW*zPm(n z-R0izo@&2xpnAn!Wugal7LWSBZe_D#I$_uCx?MVrhkTlqz=vPg4vPe^+2Z4Pl5)oU zT%89_Z`vd80cXXizdLY1ifV7iOpWzvC-y?3rm8fm)j41+bq>^*_R4cGyDG5XC$;Q{ zrUSjWzsw2_gD5Z6vGd>RqVl|;S)L11qWcBb_g3{Egz4>P)Y@8h751hGL+|3?tQWC^ z;#N3&5>^RYKr-p&k3|gW`YqTODmF{Kk7Aae15W{QD8l~UyfZjS15LY>-#9)y>(eGs ziA<gwUq)VRh3*`)mVb(YA1iZ;ZTjwkF7&ERO2&avk?x}_{7-1 zwBt2CI<^<#P>q|%+z5wid~j?g!l4@P8|&5Fi!s%w&};zj#5>gAnCFqy?~}CFkrV(pP8|4!Gk$7nc$%r4qq)G9yTKsH+*isi+VcXe{|S#d3>zl z@!55Gd?NdJ9`mBT(xed(8mR6d{p`N;-2`l>!Fneqk*yLR8Fb!Sd)75r{`HR ztfxn@lqbQNPuxE{m1{vsT8aNF2@s|<2c-dCA^HX2k2wUPmHQa_HU#Mi-wU5sYry+o0|>STHgORG ztXIQ2Ho&H_-98Xt_JPl_|2@Py)^=<#BhHLCGvdtQIM74bh8}y#hMtlV&_ftKAMKA| z<6%%A5Uxe}K_8{pBFzrO?Lb`6N8Ap?)gi79aX}w(b%=W$agQS|=p*iN#O+4hZp7_2 z65Rs6ZN$HIC`4iS`cUY*xup~sIDJYfdT0R9DM?}uAZ;?RrxZ?35pV(|&skd3iBW$_ z8fh;qB&z>Tc}Za>Xorbq;Qe##EXym}>3$k-LC+Ydz9&(>z9?}+%z*tN3AUiBGZWV$ zH7jkHtlM0boYM;P|zIb;nh5-Rw`pYzTNexZBP^A#&srxhl`b(T|odyzgF`2}CvO-+4i zKN<8E!1_+{z}E84GP@(s(nITp|X@rVz3CltoINk%fZ zEY>aha>RJVVa=8V{O!PIR@(!9FMMVX&G3`JIRgG!7Np$($H9)m(iTt_;GTfOkXn5V zke3yLfiI5M=s$~W@|FJPc${hH=7}KR>!rwd=m9iMrZM}WO{~+Knx`#hH#(d6^ zQGe;ssK0M;)IS5^;UVZP4bP4lZ!udc?5f&awdz?wCIb?IO7m;>$RuM6|?D!FmI{C%O}4+>wD} zKPDf8R`Z7;AtIJdYjswNWzb~-J`=)fLvp3D+|A6fQ(}~c^4PpD%XMAWpJfH*59tg8 zo~1MA2x?*^gVLrPgUm^r>Y=Bi`u{uk$07*8y*Q5;BbOjgJ7X zNCW1uq4IB)poOe@#?eqNoTd||JoTq|9J{;{Ek${*+5p0_Hmp}ZUNJi=}%i{B#- zODV@`lwyhM7Lg@ltfU`|m9xT#aS|$NY>vqe(rId87{v(7sNdC!aYDT`P8LwDNK8s=hy7GzTyPuKmv$ z@cj`3eh6bm)^8>SdDI`NoP}}wBdk}@sL_~BsiJY?pjkAOOQ*kp7FKCK_++s8694Ew zgD@G*rdrqYuqqdlC1TSLx?ZU)Zdno>v3iUcN$do9FlfYd*Cy`m)#{!D3^>_kpPh78 z(`Of#t5W(44q!Uc!=rV1tiG%DR{kG;SYv&wzC7pnp2XYf;{H(D~C){tvQx zfqtIs1rJ!`mdfpo(t=%y(Vkej4U$G}a+^CbZwt6Fgz##hzTg-G8i`?W{F%THEci^) zEe2TSv;Mw!nW;pbUIN+s{^FixlSzB?qSg}iD)aF^GmEv0JsP&4RUBd$Qimj8m)+>} z!KN`uQfM$!JID%2S^{WBRY$I~EP)IP$qmf5NUk}-@cXbk%(F`@!ZoFMeiM6LYrv2r z4Kd$W-8M^pFwO4++*U3873-r`{fn(u{luIkiM>KaEhN%w)piyOTb?02AM6;v#fw%{ z;!9dt6z~I(vR_e=SSDCHX`Th+IO2j&c#pIs2Of+UibR|`BB+N|i(dnNsRIr(*{myC z;xIPfrkE4;_5|hO#BHDo>;dh1C2_2QVn=DmY1W`WUFKtw3N%4!0%%&uin1}^3@EI*_nV2uJ>NujcImhO_SEe>%n9cZJ*YmdmM%6FRRfaxx^lrRqG~PMOaKDYKAl z+`jx2Met6Nt*(C&mAChVn5WJ)!r~qOY2qd0kL6goubcW~Blcm?BQ+(^1q4mS3$;_C zY`QF7drl9k@#P);y89Z!l*#cBs4GvvS1}D@A!G`+<^bYSURlUNqq~s z*>xP+o~o@A3fHV1^{0<)RJ!EILJ{kWJxyGfitB>HpU!j>bq(08^U~Z=|I!gH_#kX{ zW-b9Pr~oeI3DCf?!!ou-79lfO)tp?Uan&e)Rc<@@rX$uXVxL6(kp_7O%G}bZO~1lh zUzq}q=@6&osK0Qyp>nr!P}u`6#NN}|E_wq!ahP4GZ#WH{UZeh;;pTEJPVFlDs6X$p z=zc_|QnS{z$+iklkYDe+dHv4id7V&nR@nk?f+U=+aacjIbWZ)PLr?bk z(s6s5t#%7}+_f!8F9ZB=1MWaK_`5%70!J-UBsOjXH9fe4LtY&8Ma*Z>70#Q^W)6a5 zg6bDtnT=F0N-W~nvJAgB5jMcUM-M6-;*uy|neCd@^VZDG$;`CP%U)A92T&J_F~UuP zgQ(OEd2`v-S4XhfW%sPUugblC!@aZ0%H4N;@A>MVuDQp3#{>TneaHH;jmm!?yJmGL zo+=CfT`>`4T)47HOn)B!t8nA*CLw$T?s>eM06zh)^yV9~G7IKifBp3=9L(A@dtTS(uQFD8Uq!{5jT`T{Z~giQ%y(|M@2-2-tTvbC zIq{k1w2kXG+`oFm{dX-&y>CVYP=i0M&0pclz1eIwGk95<6u{riYZqSkbIbJ)vGj}u zi&ovvoXhib@#oCX&tI}+Np5cb(@z%{7cXCaYw_~p-NnEDP41H7 z-@X3EfySnTZz98?mbVVSedL|iqwgMTJANWJ_x%q(Jo(YbfBLi68!BnZ+0JvHezxTE z;xABK@mF7ey&PrU`mb+&eHZ!%E?)AhgG0k3qhkRsYIKI^p#H7osc2srOBB1CeFBxXL2o{&yBp0&*BdtT+MG_PGK{D7{40)9^sGT_ZT#2ZO88k{!9KOujNnS z_YB{`FY#yjbG#0}MZzw=o4>%#!f$vzetYnH5xvp;U>9=na|sc=RIszn_?qPceai~rNxo93<+lm9 z3qKZq0)^&fLbn1dS$G&K4{L;92#*Ml3a|4#fxaHJSF^rKPZZ9 zR2&ln;%lPB9uc>SkBQsFMsbpOOcYpy*dQJk_2NfJ^|<&;@ky~mj0X}if(?rO;(&Nj zyd?VZ8x)7|vq@&LL^A4R$-=iu*YJ0xX#Qu3v!jwn%Hw64=@QM!|22iPiVQaOSzbSN zC1)nR)@agd&G2n-c7&IibnI5sO{P_*n@zWyZZZADB;!q$DVn`yI&6C1^nvL^(@9gO z>6}S54VrpPy`~}4u&EkpYT@eP4wxi%$kbwLGQDm3$i!Kj$zu|JDm{2wvh zn8I0RN>z$cs6l8;3TU?)TgGfj&Jg(83@yJ8es#ub&aR%W&kZDsjQx>~u=DJ1?C-3L z{eyM09@fjg2C?>E>>GBRonRlZuh@HRfL&r2*f1Mp0rnU6DN|Vk>tLU-r}-Bk4)#MG zZXffqee5l4-mkOQ*b(+N`z_ng4zs6HpH6)yRY;wd`Yt<$i}b6kB6V5nk5X?>-NRmD z#?-jf?9`0Zlk{L@T}!HRKm}CJR5}xY(@Av3eQ)H z(X0y3QE*1eFHK}y!_RS)Ut?tT;pceDubqSzgwiKde%)+llu#d>neyv%SX%gb8s#_4 z2Z<&0K8^B6T+3F3pED?bG3b@ZYoJf&cWUtOvXVckve30a(fOkHK$*I}3L=9Od`|@gKt(ATN3mKCD*h{|Wyr z+zGfV@J#6j;r|0J1+t|R;1b|8aD9mT4DM=#DcyLxDsYzEOuV_{f7iikt(}<}udbx1 zefUG-)qy}bL45?qG?YE~4&mYPca)@lA5b~rd?m|SIN$vreH4X4Lw}SHWrW^Eg(JWB zj)Ir#``+ttegEIS%LO=5Lf`~Tbrk2`l$Uz71JW|3`8O@eU7CvT2zCtogdby!;p~|G zT`QQ`2RHr>TSoBDi}w_ueq!LGC2xGp2tUlZz!L6c`WcuvC*WR%b0SR|c#{l*lL^>F zGYt|l2?%PYF(cPZ!&xa!69mmPE#o@P!y*`Y4cKi3zynOD;ewsK&E&BdxO-7n49+UF z&2EUv{SgWkL^MJY4N~|ES;3DA<}urx%&hFW*UZbApX+eu<-3p;-Gt Mihdq{{qO$&2Gw0|IsgCw literal 0 HcmV?d00001 diff --git a/Tools/bootloaders/HWH7_bl.hex b/Tools/bootloaders/HWH7_bl.hex new file mode 100644 index 00000000000000..8543455103e72f --- /dev/null +++ b/Tools/bootloaders/HWH7_bl.hex @@ -0,0 +1,2482 @@ +:020000040800F2 +:1000000000060020E1020008E3020008E302000805 +:10001000E3020008E3020008E3020008E30200082C +:10002000E3020008E3020008E30200080177000889 +:10003000E3020008E3020008E3020008E30200080C +:10004000E3020008E3020008E3020008E3020008FC +:10005000E3020008E3020008E3020008E3020008EC +:10006000E3020008E3020008E3020008CD460008AE +:10007000F5460008214700084D4700087947000869 +:10008000A1470008CD470008E3020008E30200088A +:10009000E3020008E3020008E3020008E3020008AC +:1000A000E3020008E3020008E3020008E30200089C +:1000B000E3020008E3020008E3020008E30200088C +:1000C000E3020008E3020008E3020008E30200087C +:1000D000E3020008E3020008E3020008E30200086C +:1000E000E3020008E3020008E3020008E30200085C +:1000F000E3020008E3020008E3020008F9470008F1 +:10010000E30200080D86000835860008E3020008B7 +:10011000E3020008E3020008E3020008E30200082B +:10012000254800084D48000879480008A5480008FF +:10013000D1480008E3020008E3020008E3020008D7 +:10014000E3020008E3020008E3020008E3020008FB +:10015000F94800082549000851490008218600088F +:10016000E3020008E3020008E3020008E3020008DB +:10017000E3020008ED820008E3020008E302000841 +:10018000E3020008E3020008E3020008E3020008BB +:10019000E3020008E3020008E3020008E3020008AB +:1001A000E3020008E3020008E3020008E30200089B +:1001B000E3020008E3020008E3020008E30200088B +:1001C000E3020008E3020008E3020008E30200087B +:1001D000E3020008D9820008E3020008E3020008F5 +:1001E000E3020008E3020008E3020008E30200085B +:1001F000E3020008E3020008E3020008E30200084B +:10020000E3020008E3020008E3020008E30200083A +:10021000E3020008E3020008E3020008E30200082A +:10022000E3020008E3020008E3020008E30200081A +:10023000E3020008E3020008E3020008E30200080A +:10024000E3020008E3020008E3020008E3020008FA +:10025000E3020008E3020008E3020008E3020008EA +:10026000E3020008E3020008E3020008E3020008DA +:10027000E3020008E3020008E3020008E3020008CA +:10028000E3020008E3020008E3020008E3020008BA +:10029000E3020008E3020008E3020008E3020008AA +:1002A000E3020008E3020008E3020008E30200089A +:1002B000E3020008E3020008E3020008E30200088A +:1002C000E3020008E3020008E3020008E30200087A +:1002D000E3020008E3020008E3020008E30200086A +:1002E00002E000F000F8FEE772B6374880F30888B5 +:1002F000364880F3098836483649086040F20000E5 +:10030000CCF200004EF63471CEF200010860BFF36B +:100310004F8FBFF36F8F40F20000C0F2F0004EF637 +:100320008851CEF200010860BFF34F8FBFF36F8F8B +:100330004FF00000E1EE100A4EF63C71CEF20001E3 +:100340000860062080F31488BFF36F8F07F03AFA35 +:1003500006F0B6FB4FF055301F491B4A91423CBF97 +:1003600041F8040BFAE71D49184A91423CBF41F895 +:10037000040BFAE71A491B4A1B4B9A423EBF51F83D +:10038000040B42F8040BF8E700201849184A914280 +:100390003CBF41F8040BFAE707F052FA06F014FCF0 +:1003A000144C154DAC4203DA54F8041B8847F9E7A6 +:1003B00000F0C4F9114C124DAC4203DA54F8041B9E +:1003C0008847F9E707F03ABA00060020002200202B +:1003D0000000000808ED00E00000002000060020FA +:1003E000889A0008002200206C2200207022002041 +:1003F000D45D0020E0020008E0020008E0020008EE +:10040000E00200082DE9F04F2DED108AC1F80CD064 +:10041000D0F80CD0BDEC108ABDE8F08F002383F338 +:1004200011882846A047002006F02CF9FEE706F0C8 +:100430007FF800DFFEE7000053B94AB9002908BF82 +:1004400000281CBF4FF0FF314FF0FF3000F074B9AF +:10045000ADF1080C6DE904CE00F006F8DDF804E01B +:10046000DDE9022304B070472DE9F047089D0446FA +:100470008E46002B4DD18A42944669D9B2FA82F257 +:1004800052B101FA02F3C2F1200120FA01F10CFA93 +:1004900002FC41EA030E94404FEA1C48210CBEFBCB +:1004A000F8F61FFA8CF708FB16E341EA034306FB54 +:1004B00007F199420AD91CEB030306F1FF3080F0E3 +:1004C0001F81994240F21C81023E63445B1AA4B230 +:1004D000B3FBF8F008FB103344EA034400FB07F7D2 +:1004E000A7420AD91CEB040400F1FF3380F00A8113 +:1004F000A74240F207816444023840EA0640E41B08 +:1005000000261DB1D4400023C5E900433146BDE8B3 +:10051000F0878B4209D9002D00F0EF800026C5E955 +:10052000000130463146BDE8F087B3FA83F6002E6D +:100530004AD18B4202D3824200F2F980841A61EBE5 +:10054000030301209E46002DE0D0C5E9004EDDE703 +:1005500002B9FFDEB2FA82F2002A40F09280A1EBEB +:100560000C014FEA1C471FFA8CFE0126200CB1FB40 +:10057000F7F307FB131140EA01410EFB03F0884239 +:1005800008D91CEB010103F1FF3802D2884200F2C6 +:10059000CB804346091AA4B2B1FBF7F007FB101158 +:1005A00044EA01440EFB00FEA64508D91CEB0404F6 +:1005B00000F1FF3102D2A64500F2BB800846A4EB51 +:1005C0000E0440EA03409CE7C6F12007B34022FA3C +:1005D00007FC4CEA030C20FA07F401FA06F31C436B +:1005E000F9404FEA1C4900FA06F3B1FBF9F8200C78 +:1005F0001FFA8CFE09FB181140EA014108FB0EF0BE +:10060000884202FA06F20BD91CEB010108F1FF3A0D +:1006100080F08880884240F28580A8F10208614419 +:10062000091AA4B2B1FBF9F009FB101144EA014127 +:1006300000FB0EFE8E4508D91CEB010100F1FF34D2 +:100640006CD28E456AD90238614440EA0840A0FB6A +:100650000294A1EB0E01A142C846A64656D353D040 +:100660005DB1B3EB080261EB0E0101FA07F722FA64 +:1006700006F3F1401F43C5E9007100263146BDE88D +:10068000F087C2F12003D8400CFA02FC21FA03F3F0 +:10069000914001434FEA1C471FFA8CFEB3FBF7F071 +:1006A00007FB10360B0C43EA064300FB0EF69E4296 +:1006B00004FA02F408D91CEB030300F1FF382FD22F +:1006C0009E422DD9023863449B1B89B2B3FBF7F6D7 +:1006D00007FB163341EA034106FB0EF38B4208D9B0 +:1006E0001CEB010106F1FF3816D28B4214D9023EF1 +:1006F0006144C91A46EA004638E72E46284605E70F +:100700000646E3E61846F8E64B45A9D2B9EB0208DF +:1007100064EB0C0E0138A3E74646EAE7204694E76F +:100720004046D1E7D0467BE7023B614432E73046A2 +:1007300009E76444023842E7704700BF38B501F06A +:1007400095FA01F051FC06F0D7FE054606F0CAFF07 +:100750000446D0B90F4B9D4219D001339D4241F25E +:10076000883504BF01240025002006F0CFFE0CB11F +:1007700000F076F801F0D6FB00F02EFD08B100F095 +:100780006FF8284600F01AF9F9E70025ECE705466E +:10079000EAE700BF010007B008B501F033FAA0F1A5 +:1007A00020035842584108BD07B5042101900DEBC4 +:1007B000010001F045FA03B05DF804FB38B53023C1 +:1007C00083F31188174803680BB106F03FF8002344 +:1007D000154A4FF47A71134806F02EF8002383F37C +:1007E0001188124C236813B12368013B23606368AE +:1007F00013B16368013B63600D4D2B7833B96368B7 +:100800007BB9022001F0F8FA322363602B78032BC6 +:1008100007D163682BB9022001F0EEFA4FF47A7326 +:10082000636038BD70220020BD07000890230020BF +:1008300088220020084B187003280CD8DFE800F04D +:1008400008050208022001F0CFBA022001F0C2BA66 +:10085000024B00225A60704788220020902300201B +:10086000F8B5504B504A1C461968013100F0998088 +:1008700004339342F8D162684C4B9A4240F2918023 +:100880004B4B9B6803F1006303F500339A4280F001 +:100890008880002001F010FA0220FFF7CBFF454BC3 +:1008A0000021D3F8E820C3F8E810D3F81021C3F8EA +:1008B0001011D3F81021D3F8EC20C3F8EC10D3F8C2 +:1008C0001421C3F81411D3F81421D3F8F020C3F87D +:1008D000F010D3F81821C3F81811D3F81821D3F861 +:1008E000802042F00062C3F88020D3F8802022F0FC +:1008F0000062C3F88020D3F88020D3F8802042F033 +:100900000072C3F88020D3F8802022F00072C3F870 +:100910008020D3F8803072B64FF0E023C3F8084D42 +:10092000D4E90004BFF34F8FBFF36F8F224AC2F8A0 +:100930008410BFF34F8F536923F480335361BFF3A7 +:100940004F8FD2F8803043F6E076C3F3C905C3F386 +:100950004E335B0103EA060C29464CEA81770139E4 +:10096000C2F87472F9D2203B13F1200FF2D1BFF319 +:100970004F8FBFF36F8FBFF34F8FBFF36F8F5369ED +:1009800023F4003353610023C2F85032BFF34F8F7A +:10099000BFF36F8F302383F31188854680F3088877 +:1009A0002047F8BD0000020820000208FFFF0108F0 +:1009B000002200200044025800ED00E02DE9F04F35 +:1009C00095B0DFF8D8922022FF21029004A8D9F830 +:1009D000085001F051FAA84A1378A3B90121A74899 +:1009E00011700360302383F3118803680BB105F0A5 +:1009F0002DFF0023A24A4FF47A71A04805F01CFF96 +:100A0000002383F31188029B13B19E4B029A1A6054 +:100A10009D4A1378032B03D000231370994A536027 +:100A20004FF0000B029C5E46DA46CDF804B0012080 +:100A300001F0DAF924B1934B1B68002B00F02882F7 +:100A4000002001F0DFF8B0F10008F3DB012001F035 +:100A5000C1F9A8F121031F2BE9D801A252F823F014 +:100A6000E10A0008FD0A00088B0B00082F0A0008A5 +:100A70002F0A00082F0A0008150C0008070E0008AE +:100A8000F90C0008550D00087B0D0008A10D0008A9 +:100A90002F0A0008B30D0008D70D00084F0E0008FC +:100AA000690B00082F0A0008970E0008ED0A0008DD +:100AB000690B00082F0A0008550D00082F0A0008CE +:100AC0002F0A00082F0A00082F0A00082F0A000822 +:100AD0002F0A00082F0A00082F0A00088B0B0008B5 +:100AE0000220FFF759FE002840F0FF81029B0221FF +:100AF0006648BBF1000F08BF1C4640E04FF47A7017 +:100B000001F080F8071EF1DB0220FFF745FE002808 +:100B1000ECD0013F052F00F2EA81DFE807F0030780 +:100B20000A0D10330520FFF73FFE14E0D9F800004E +:100B3000F9E7D9F80400F6E7D9F80800F3E74FF031 +:100B40001C08404608F1040801F0B8F8FFF72CFE35 +:100B5000B8F12C0FF5D10120019B4FF0000A00FAEB +:100B600007F71F43FBB2019301F0D2F92EB1019BAD +:100B700003F00B030B2B08BF00240221444801F0B3 +:100B80005FF854E7D9F80C00CDE7002EAED0019BFA +:100B900003F00B030B2BA9D10220FFF7FDFD074645 +:100BA0000028A3D00120002601F086F80220FFF7DC +:100BB00041FE1FFA86FB584601F08EF8044688B1C4 +:100BC000A8F14002584601365142514101F094F8D3 +:100BD0000028EED1BB46044602212E483E4601F0D5 +:100BE0002FF824E725460120FFF724FED9F8083026 +:100BF000AB4207D9284601F061F8013040F07A8114 +:100C00000435F3E70025204BBB463E461D701D4BC7 +:100C10005D60A9E7002E3FF469AF019B03F00B0371 +:100C20000B2B7FF463AF0220FFF704FE322000F0AD +:100C3000E9FFB0F10008FFF659AF18F003077FF4A1 +:100C400055AF08EB0503D9F8082093423FF64EAFA5 +:100C5000B8F5807F3FF74AAF0F4BB84503931FDDD0 +:100C60004FF47A7000F0CEFF0028FFF63FAF039BF1 +:100C7000013703F8010BF0E78C23002070220020DD +:100C8000BD0700089023002088220020449000081F +:100C90003C900008409000088C2200200022002098 +:100CA000C820FFF779FD074600283FF41FAF1F2D2E +:100CB00011D8C5F1200204AB25F003008F4942454D +:100CC000184428BF4246039201F0B0F8039AFF216E +:100CD0008A4801F0D1F84FEAA803C8F38702874990 +:100CE0002846039301F0D0F8064600283FF474AF7D +:100CF000039B05EB830537E70220FFF74DFD002836 +:100D00003FF4F4AE01F016F800283FF4EFAE0027F0 +:100D1000B846D9F80830BB4218D91F2F11D8049B08 +:100D200001330ED027F0030314AA134453F8403CB8 +:100D30000C93404604220CA9043701F067F9804661 +:100D4000E7E7384600F0BAFF0C90F2E74046FFF7BD +:100D50002BFD09E7002364210CA80C9300F066FF2B +:100D600000287FF4C3AE0220FFF716FD00283FF4F1 +:100D7000BDAE0C9800F0D4FFE9E7002364210CA875 +:100D80000C9300F053FF00287FF4B0AE0220FFF771 +:100D900003FD00283FF4AAAE0C9800F0C3FFD6E78D +:100DA0000220FFF7F9FC00283FF4A0AE00F0D2FFCC +:100DB000CDE70220FFF7F0FC00283FF497AE0CA926 +:100DC000142000F0CDFF0390FFF7EEFC03990CA870 +:100DD00000F036FFC8E60220FFF7DEFC00283FF4F3 +:100DE00085AE474B0CAF03F1100C1868083353F86D +:100DF000041C3A46634503C21746F6D11020FFF79C +:100E0000D3FC1021E3E7322000F0FCFE071EFFF6C2 +:100E10006DAEBB077FF46AAE07EB0A03D9F8082072 +:100E200093423FF663AE0220FFF7B6FC00283FF482 +:100E30005DAE27F003075744BA453FF495AE5046E0 +:100E40000AF1040A00F03AFFFFF7AEFCF4E74FF4B2 +:100E50007A70FFF7A1FC00283FF448AE00F06AFF6B +:100E6000002849D0049B01330BD0082204A900209C +:100E700001F00AF800283FD02022FF2104A800F04A +:100E8000FBFF2048022100F0DBFE1F4805F000FCBC +:100E900015B0BDE8F08F002E3FF428AE019B03F0A3 +:100EA0000B030B2B7FF422AE002364210CA80C93C0 +:100EB00000F0BCFE074600287FF418AE0220FFF7C2 +:100EC0006BFC804600283FF411AE02210D4800F073 +:100ED000B7FE41F2883005F0DBFB0C9801F062F8B8 +:100EE00046463C4601F014F8A1E506463CE64FF0C4 +:100EF000000AFBE5BB466FE637466DE68C22002014 +:100F0000289000083C900008A0860100704700006F +:100F100070470000704700002DE9F04100F5803770 +:100F2000044616463B7C5BB9C0681030204400F094 +:100F30007DFFE5683544B5F5004FE56002D816B190 +:100F4000BDE8F081DEB905F07F0605F11000002153 +:100F5000C6F180062044F6B232462E4400F08CFFE3 +:100F6000A06804F11008324600F10060414600F527 +:100F7000003006F061F830B901233B74E0E74FF42C +:100F800000463546ECE7A26805F1100140463244C0 +:100F90002144A260E268521BE26000F047FF022099 +:100FA000BDE8F04100F028BF183000F0E9BC0000B7 +:100FB00010B5044601F046F8204610BD10B50446B1 +:100FC00001F040F8204610BDC0B2A0F14103052B4E +:100FD00002D83738C0B27047A0F16103052B94BF27 +:100FE00057383038F6E7000070B504461546084615 +:100FF00003220C4900F024FF014688B908346E1C16 +:1010000015F91100FFF7E0FF024616F91100013152 +:10101000FFF7DAFF102940EA021004F8010BEFD1C4 +:1010200070BD00BF489000082DE9F04FADF53F7D41 +:101030000746416801222AA802F0F4FE002840F089 +:1010400087800646824681461125DFF80C81DFF84D +:101050000CB101AB4FF4805241462AA803F042F88C +:10106000002875D1019AB2F5805F71D8002A65D049 +:101070000446019A9442ECD2282D0FD008DC132D9F +:101080002DD01E2D39D0112D13D00134A4B2F0E78C +:10109000322D2DD0372D2FD02D2DF6D13B68121BA0 +:1010A00008EB040138461B692D259847BDF804401C +:1010B000EBE7121B022A09D9594608EB040000F09D +:1010C000BFFE18B902342825A4B2DEE718F80430B0 +:1010D0003A2B3DD00A2B1CBFA1461325D5E718F8A3 +:1010E00004300A2B34D03A2B04BFA2463225CCE779 +:1010F00018F80430202BC8D0264618F804300A2BE4 +:101100001AD1AAEB090208EB090102A811254F2AFE +:1011100028BF4F2207F074FFA21B08EB060116A898 +:101120004F2A28BF4F2207F06BFF3B6816AA02A97F +:10113000DB6838469847A8E71E25A6E73B6838468F +:1011400004491B69984701200DF53F7DBDE8F08FEC +:101150000020F9E74A910008982300204C900008ED +:1011600000F1180110B5044686B00846019100F060 +:10117000F1FB2046FFF758FF60B1019902A800F08B +:1011800049FC102204F1080102A807F0F9FEB0FAA8 +:1011900080F0400906B010BD70B504460025EEB2DF +:1011A000304600F099FD58B100213046013500F07D +:1011B000A3FD08B9002070BD022000F01DFEEEE77F +:1011C0002046FFF731FF0028F4D004F58034207C5E +:1011D00080F00100EFE70000F0B5C9B006F0A4F917 +:1011E00000F04CFF18B90025284649B0F0BD69460B +:1011F0002A4803F039F800284BD1294C204603F047 +:1012000063F8284803F060F8274803F05DF82146AA +:10121000224803F0D5F80028E5D1702000F00CFF3B +:10122000064610B1214B44600360336830469B682A +:101230009847054600282ED01A4F1948394603F022 +:10124000BFF805460028CED1194800F0F5FE044647 +:1012500038B1184B4760036000F58033C0E9025590 +:101260001D74236820469B689847054628B10E499F +:101270000C4803F0A5F80028B5D1336830465B6808 +:1012800098471CB1236820465B68984700F0DEFE53 +:10129000AAE70025FAE70446EFE700BF50900008F0 +:1012A00060900008779000088D900008B0900008CA +:1012B00014000100CC9000082DE9F04FD44A8DB005 +:1012C0000B68D0F804A001931A440368D14E1A4465 +:1012D000D1F81C90DFF8B4C3DFF8B4B3D0E902341E +:1012E000634003EA0A03634013444A6802920AEB2C +:1012F0007363029CC84A2244C468224484688AEA10 +:1013000004051D40654015448A68039203EB35557A +:10131000039CC24A2244846822448AEA03042C4083 +:1013200084EA0A041444CA6805EBF43404921644AF +:1013300083EA0502224056445A4032440E6905961B +:1013400004EBB222059FB64E3E441E4485EA0403D8 +:1013500013406B4033444E69069602EB7363069F5D +:10136000B04E3E442E4484EA02051D40654035449B +:101370008E69079603EB3555079FAB4E3E442644D6 +:1013800082EA03042C4054403444A84E4E4405EBFA +:10139000F434164483EA050222405A4032440E6A6D +:1013A000089604EBB222089FA14E3E441E4485EAF3 +:1013B000040313406B4033444E6A099602EB736397 +:1013C000099F9C4ED1F830E03E44D1F83880F34478 +:1013D0002E4484EA02051D40654035448E6AA6F518 +:1013E000244703EB35550A964F3F274482EA03040E +:1013F0002C4054403C44CF6A0B9705EBF4340B9ED1 +:101400008D4F3744029E174483EA050222405A401A +:101410003A448A4F774404EBB2221F4485EA04031E +:1014200013406B403B444F6BBC4402EB7363654419 +:1014300084EA020C0CEA030C8CEA040C6544DFF825 +:1014400054C2C44403EB3555A44482EA03042C403F +:1014500054406444D1F83CC0794905EBF43461440C +:10146000114483EA050222405A400A44754904EBBC +:10147000B2223144079E194484EA02032B406340A0 +:101480000B44714902EBF36331440B9E0D4482EA35 +:1014900003012140514029446C4D03EBF151354487 +:1014A000019E254483EA010414405C402C44684DAD +:1014B00001EBB4443544069E154481EA04021A4007 +:1014C0004A402A44634D04EB323235440A9E1D449F +:1014D00084EA02030B4063402B445F4D02EBF3634D +:1014E0003544059E0D4482EA0301214051402944C0 +:1014F0005A4D03EBF1516544254483EA010414403D +:101500005C402C44564D01EBB4443544099E1544CF +:1015100081EA04021A404A402A44524D04EB323216 +:101520003544049E1D4484EA02030B4063402B446F +:101530004D4D02EBF36345440D4482EA0301214023 +:1015400051402944494D03EBF1513544089E2C4448 +:1015500083EA010515405D402C44454D01EBB44440 +:101560003544039E2A4481EA04051D404D402A4427 +:10157000404D04EB32323D442B4484EA020593444F +:101580000D4065402B443C4D02EBF3633544069E11 +:10159000294482EA0305254055402944374D03EB91 +:1015A000F1514D442C4483EA010515405D4025442A +:1015B00001EBB54581EA050404EA03024A405A44B6 +:1015C000A6F5B82B089E05EB3232ABF2BE6B544049 +:1015D0005B4423442A4C344402EB33730B9E0C448B +:1015E00085EA020159402144264C344403EB7151F1 +:1015F000029E254482EA03044C402544224C444484 +:1016000001EB3545144483EA01026A40224443E079 +:1016100078A46AD7EECEBDC156B7C7E8DB702024E8 +:10162000AF0F7CF52AC68747134630A8019546FDC3 +:10163000D8988069AFF7448BBED75C892211906B34 +:101640002108B44962251EF640B340C0515A5E26B7 +:10165000AAC7B6E95D102FD65314440281E6A1D87B +:10166000C8FBD3E7E6CDE121D60737C3870DD5F414 +:10167000ED145A4505E9E3A9F8A3EFFCD9026F6719 +:1016800081F6718722619D6D0C38E5FD937198FD9F +:101690008A4C2A8D8E4379A6934C344405EB7222F2 +:1016A000059E1C4481EA0503534023448F4C344477 +:1016B00002EB33730A9E0C4485EA0201594021442F +:1016C0008B4C4C4403EB7151254482EA03044C409B +:1016D0002C44884D354401EB3444019E154483EA83 +:1016E000010262402A44844D3D4404EB72221D44B1 +:1016F00081EA040353402B44804D354402EB33739D +:10170000049E294484EA02055D4029447C4D354409 +:1017100003EB7151079E254482EA03044C402C449C +:10172000784D354401EB3444099E2A4483EA01058F +:1017300065401544744A324404EB7525039E1344F6 +:1017400081EA04026A401A44704B734405EB32721A +:101750000B4484EA0501514019446D4B634402EB8C +:1017600071511C4485EA02034B401C44694B3344CD +:1017700001EB3444019E1D4482EA010363402B4483 +:10178000654D04EB73233544069E154463EA01025C +:1017900062402A44614D03EBB2624D4462EA04099F +:1017A00029445F4D89EA0309454449442C445D4D71 +:1017B00002EBB1513544049E61EA03081D4488EAF6 +:1017C0000208444401EB744464EA02034B402B4496 +:1017D000554D04EBF323754463EA010E15448EEA7C +:1017E000040E0EEB0502514D03EBB262354462EA82 +:1017F000040E29440A9D8EEA030EA5F580164C4D71 +:101800007144A6F6833602EBB151264461EA030423 +:1018100054403444029E01EB7444354464EA0206A9 +:101820001D444E407319089E424D04EBF32335448A +:1018300063EA01061544664072193F4D03EBB2623C +:10184000654462EA040629443C4D5E403144079EEB +:1018500002EBB151354461EA03062C44384D564041 +:101860003D443444059E1D4401EB744464EA020384 +:101870004B402B44334D04EBF32335440B9E15446E +:1018800063EA010262402A442F4D03EBB262354401 +:10189000039E0D4462EA0401594029442B4D02EB9A +:1018A000B15135442A4E254461EA030454402C4486 +:1018B000099D01EB74442E4464EA02051E4485EA46 +:1018C00001039D1903681A440AEB040303EBF52393 +:1018D0000260436083681C44C36819448460C1602B +:1018E0000DB0BDE8F08F00BF44EABEA4A9CFDE4B27 +:1018F000604BBBF670BCBFBEC67E9B28FA27A1EA30 +:101900008530EFD4051D880439D0D4D9E599DBE6BC +:10191000F87CA21F6556ACC4442229F497FF2A43E1 +:10192000A72394AB39A093FCC3595B6592CC0C8F71 +:10193000D15D84854F7EA86FE0E62CFE144301A3A1 +:10194000A111084E827E53F735F23ABDBBD2D72A99 +:1019500091D386EB094B036003F18833436003F1B5 +:101960002943A3F59613A3F68B638360A3F1883311 +:10197000C3600023C0E90433704700BF01234567FB +:101980002DE9F8431446026905460E46E300C2F30A +:10199000C50800F118079B18036122BF4369013392 +:1019A000436112F4FC7F436903EB5473436114D029 +:1019B000C8F1400907EB08004C4504D22246BDE8B7 +:1019C000F84307F0EDBA403C4A464E4407F0E8FAC7 +:1019D000444439462846FFF76FFCA04606EB04094D +:1019E000B8F13F0FA9EB08010AD94022384607F0A9 +:1019F000D7FA39462846A8F14008FFF75DFCEFE723 +:101A0000A1096FF03F02384602FB014206EB81114B +:101A1000D5E7000070B50B6901F1180506460C46C4 +:101A2000C3F3C503EA18501C8022EA54C3F13F02F5 +:101A3000072A1FD8002100F01FFA29462046FFF789 +:101A40003BFC38220021284600F016FA236929467B +:101A50002046236563696365FFF72EFC214610224B +:101A6000304607F09DFA204658220021BDE870401C +:101A700000F002BAC3F137020021E5E72DE9F84F83 +:101A80004FF47A7306460D46002402FB03F7DFF895 +:101A90005080DFF8509098F900305FFA84FA5A1CB1 +:101AA00001D0A34212D159F824002A4631460368D6 +:101AB000D3F820B03B46D847854207D1074B0120D9 +:101AC00083F800A0BDE8F88F0124E4E7002CFBD0E8 +:101AD0004FF4FA7004F0DCFD0020F3E7DC33002063 +:101AE000102200201422002000232DE9F0471A467E +:101AF000054688460120194602F0D2FF04468020A0 +:101B000005F0B8FB0746002861D0002C5FD03B4EA3 +:101B100028464FF0800A736943F0C0637361D6F8BA +:101B2000143143F0C063C6F8143105F025FA05EB13 +:101B3000080200FB05F34FF4D06C50434FF03D0E0C +:101B4000834233D23269520143D4D6F81021510175 +:101B50003FD4216803F100628A602268D76022685E +:101B6000C2F804A02268C2F800C0D4F80090D9F8E6 +:101B7000002042F00102C9F80020D4F80090D9F802 +:101B8000002022F01E02C9F80020D4F80090D9F8F5 +:101B90000020D207FBD494F80C90803361680EFAD1 +:101BA00009F20A60CCE7002005F0C4FA204602F0F2 +:101BB000B7FF384605F09AFB104B5A6942F0C062F5 +:101BC0005A61D3F8142142F0C062C3F81421BDE871 +:101BD000F0870B4B9A6A42F400729A629A6A42F456 +:101BE00000429A6261B60026012005F0A3FAB045D2 +:101BF000D9D9A819013605F0D7F9F8E70020005225 +:101C000000ED00E0002307B5024601210DF10700B9 +:101C10008DF80730FFF732FF20B19DF8070003B0C1 +:101C20005DF804FB4FF0FF30F9E700000A4604219D +:101C300008B5FFF723FF80F00100C0B2404208BDA5 +:101C4000074B0A4630B41978064B53F82140014639 +:101C500023682046DD69044BAC4630BC604700BFBA +:101C6000DC33002014220020A086010070B5104C47 +:101C70000025104E05F084F92080306823888342C7 +:101C80000CD800252088013805F076F92388054412 +:101C9000013BB5F5802F2380F4D370BD05F06CF9BE +:101CA000336805440133B5F5003F3360E5D3E8E719 +:101CB000DE3300209833002005F03CBA00F10060CC +:101CC00000F500300068704700F10060920000F5F8 +:101CD000003005F0B1B90000054B1A68054B1B88B0 +:101CE0009B1A834202D9104405F046B90020704780 +:101CF00098330020DE33002038B50446074D29B163 +:101D000028682044BDE8384005F04EB928682044D2 +:101D100005F038F90028F3D038BD00BF9833002013 +:101D20000020704700F1FF5000F58F10D0F8000838 +:101D300070470000064991F8243033B1002308228F +:101D4000086A81F82430FFF7BFBF0120704700BF49 +:101D50009C330020014B1868704700BF0010005CE6 +:101D6000194B01380322084470B51D68174BC5F3A1 +:101D70000B042D0C1E88A6420BD15C680A46013C60 +:101D8000824213460FD214F9016F4EB102F8016B73 +:101D9000F6E7013A03F10803ECD181420B4602D287 +:101DA0002C2203F8012B0424094A1688AE4204D1E0 +:101DB000984284BF967803F8016B013C02F104025B +:101DC000F3D1581A70BD00BF0010005C1C22002027 +:101DD0000C910008022803D1024B4FF480229A6133 +:101DE000704700BF00040258022802D1014B0422B0 +:101DF0009A61704700040258022804D1024A5369CC +:101E000083F00403536170470004025870B5044620 +:101E10004FF47A764CB1412C254628BF412506FB6C +:101E200005F0641B04F034FCF4E770BD002310B52A +:101E3000934203D0CC5CC4540133F9E710BD0000D9 +:101E4000013810B510F9013F3BB191F900409C42B7 +:101E500003D11AB10131013AF4E71AB191F9002026 +:101E6000981A10BD1046FCE703460246D01A12F934 +:101E7000011B0029FAD1704702440346934202D065 +:101E800003F8011BFAE770472DE9F8431F4D14468C +:101E90000746884695F8242052BBDFF870909CB323 +:101EA00095F824302BB92022FF2148462F62FFF7F6 +:101EB000E3FF95F824004146C0F1080205EB8000DD +:101EC000A24228BF2246D6B29200FFF7AFFF95F894 +:101ED0002430A41B17441E449044E4B2F6B2082EEA +:101EE00085F82460DBD1FFF725FF0028D7D108E073 +:101EF0002B6A03EB82038342CFD0FFF71BFF00283E +:101F0000CBD10020BDE8F8830120FBE79C33002003 +:101F1000024B1A78024B1A70704700BFDC33002066 +:101F20001022002038B51A4C1A4D204603F0A8FEA6 +:101F30002946204603F0D0FE2D684FF47A70D5F87C +:101F40009020D2F8043843F00203C2F80438FFF7B7 +:101F50005DFF1149284603F0CDFFD5F890200F4DC5 +:101F6000D2F80438286823F002030D49A042C2F8D1 +:101F700004384FF4E1330B6001D003F0DFFD6868F3 +:101F8000A04204D00649BDE8384003F0D7BD38BDB3 +:101F900038400020089900081099000814220020F9 +:101FA000C43300200C4B70B50C4D04461E780C4B0E +:101FB00055F826209A420DD00A4B002118221846C7 +:101FC000FFF75AFF0460014655F82600BDE870404F +:101FD00003F0B4BD70BD00BFDC330020142200202C +:101FE00038400020C433002008B571B6054B19886D +:101FF000054B1888FFF778FD02210E20FFF774FDCE +:1020000061B608BDDE3300209833002030B50A44A5 +:10201000084D91420DD011F8013B5840082340F380 +:102020000004013B2C4013F0FF0384EA5000F6D17A +:10203000EFE730BD2083B8ED0121884238BF084664 +:1020400005F050B908B105F051B9704710B5084C0A +:1020500001220849002001F0C1FE23783BB1064867 +:1020600003F0E4FC044803F017FD0023237010BDC7 +:10207000E03300201C910008C03600202DE9F0411B +:10208000234C244E236DBBB901224FF4004104F1CF +:10209000500005F06FFA236D1B686BB901224FF4F5 +:1020A000805104F1500005F065FA236D1B681BB9DF +:1020B00000203070BDE8F08133780BB1FFF7C6FF28 +:1020C0000324154F4FF00008114D13492846C7F857 +:1020D000048003F0D3FC284603F00CFB38B1013C2C +:1020E000284603F0D9FC14F0FF04E1D0EDE70B49DA +:1020F00001220B4801F072FE014618B1284603F098 +:1021000095FCECE7074800F00FF80120D1E700BF8D +:10211000C0360020E0330020342200201C9100084B +:10212000E4330020209100080FB4002004B0704771 +:102130000068704703460068596870470B0A0170D1 +:1021400043700B0C090E8370C1707047110A027046 +:1021500003714170110C120E8170C2701A0A427123 +:102160001A0C1B0E8271C37170470000C36A0239DA +:10217000023B8B4283BF4389006C01FB03000020BC +:1021800070470000C2F307238A76CB760378032BCF +:1021900001BF120C0A75120A4A75704700F10B0153 +:1021A0000022D30143EA520310F8012B52FA83F3C1 +:1021B0008842DAB2F5D110467047000010B5417878 +:1021C00004460020013102464901022A16BFA35CE1 +:1021D000032203EBC03302F101021EBF9BB203EBEB +:1021E000500398B29142F0D810BD000002684AB185 +:1021F000134613F8011B1F290DD93A29F9D1911C57 +:102200008B4202D04FF0FF3070471278302AF9D15C +:10221000036000207047014B18787047303600206B +:1022200038B50D46044618B9092000232B6038BD87 +:102230000368002BF8D01A78002AF5D08188DA8854 +:102240009142F1D1587804F0BFFB10F00100EBD1BE +:102250002368EBE738B50D4640F25231144602F0E0 +:10226000B9F9FF2807D9012C0BD9030A0224687099 +:102270002B70204638BD30B1002CFAD001242870D4 +:10228000F7E70024F5E70446F3E700002DE9F843FB +:102290000026D0F8008005460C468E76836B002B16 +:1022A0004AD098F80030042B4BD1334635464027AE +:1022B00020E0B7F5187F80F0C480F90606F101062A +:1022C00008BF0237D05B02372BB900F5205292B21B +:1022D000B2F5006F0DD305F11A01C5F1FF0240EA16 +:1022E00003402144FFF7B6FF002800F0AA80054410 +:1022F00000200346D8F8102092F82310B142D8D815 +:10230000002B40F09E80002D00F09B800023254490 +:10231000AB766373D8F81020137903F03701DB062E +:1023200021730BD402F13800FFF704FFC4E9000168 +:1023300093896381D3892381BDE8F8830020014616 +:10234000F4E7C36C01335ED1EA6B00232E26551EE1 +:10235000184615F8011F013020290CD0052908BFA7 +:10236000E521092804D10B2B9EBFE71801337E73AA +:10237000E718013379730B28EBD1E118002048737B +:10238000A17E00294BD1002B40D06FF00C0604F148 +:102390000D000825361B331810F8011B002938D012 +:1023A0002E298BB24AD0A3F14101192903D8117B00 +:1023B0000D4200D020330373EDE7B9F1000F05D1D2 +:1023C00000F520539BB2B3F5006F0BD307F11A0150 +:1023D000C7F1FF0240EA09402144FFF73BFF48B143 +:1023E0000744002002368146D8F80C30985B00285C +:1023F000E3D13846B9F1000F4FF0000218BF0020BA +:1024000023189A76A0E7B1463746EDE73F23A37637 +:102410000123234400219976137B03B96373D37A94 +:1024200002F11C0003F03F0323730023FFF780FE3B +:1024300020606360D38A6381138B7CE710250B4691 +:10244000B9E73F230125A37660E7000038B50546CC +:10245000002435F8020B08B9204638BD02F0EEF82A +:102460006308C2B203EBC43312FA83F39AB2C0F327 +:10247000072303EB520303EBC2339CB2E9E70000EE +:1024800037B5C37804461BB90025284603B030BDD4 +:1024900000F14C01826C01234078019104F0BAFAFA +:1024A000054680B9A36BE070A06C226BC31A9342FF +:1024B000EAD2A3780199022BE6D102440123607885 +:1024C00004F0A8FAE1E70125DFE7000038B5836CE6 +:1024D00005460C468B4210D0FFF7D2FF60B922466A +:1024E000012305F14C01687804F070FA00281CBF44 +:1024F0004FF0FF340120AC6438BD0020FCE7000041 +:1025000038B500230446C3704FF0FF338364FFF7F0 +:10251000DDFF00284BD1B4F84A524AF655239D42BC +:1025200007D10B22254904F14C0006F029FD0028B3 +:102530003FD094F84C30EB2B03D01833DBB2012B97 +:102540002ED84AF655239D4206D108221C4904F193 +:102550009E0006F015FD48B3B4F85730B3F5007F80 +:102560001ED194F85930DBB15A1E1A4218D1B4F872 +:102570005A30ABB194F85C30013B012B10D8B4F861 +:102580005D306BB1B4F85F307F2B06D804F16C007E +:10259000FFF7CEFDB0F5803F02D3B4F8623053B9F7 +:1025A0004AF6552085420CBF0220032038BD042086 +:1025B000FCE70120FAE70020F8E700BF4C91000893 +:1025C0005891000802392DE9F04701F007044FF057 +:1025D000010A466C05460AFA04F41746984606EBCB +:1025E0001136C1F3C809E4B2314628460136FFF777 +:1025F0006DFF18B10120BDE8F087994605EB09028F +:1026000092F84C30234214BF01210021414513D0E0 +:102610006340013F82F84C3085F803A0EBD06400A2 +:1026200014F0FF04EAD109F1010301244FF000097D +:10263000B3F5007FE1D1D7E70220DCE701290246AC +:10264000F8B50C4640F28C800668F36A8B4240F283 +:1026500087803378013B032B00F28280DFE803F0B0 +:102660000229384B04EB5405B16B304601EB552180 +:10267000FFF72CFF10B14FF0FF30F8BD6F1CC5F312 +:102680000805B16B3046354401EB572195F84C50A5 +:10269000FFF71CFF0028EED1C7F30807E3073E440D +:1026A00096F84C0045EA00204CBF0009C0F30B002F +:1026B000E3E7B16B304601EB1421FFF707FF002879 +:1026C000D9D1640004F4FF742644B6F84C00D4E772 +:1026D000B16B304601EBD411FFF7F8FE0028CAD1E8 +:1026E000A40006F14C0004F4FE742044FFF720FD22 +:1026F00020F07040C1E7D0E90430D57953EA0001F9 +:1027000001D0916801B95DBB9168022DA4EB010174 +:102710000DD1013B728940F1FF305B0A43EAC0539F +:10272000B3FBF2F399421BD81CD0601CA5E7032D24 +:1027300002D193698B42F8D8D3699BB9B16B30460B +:1027400001EBD411FFF7C2FE002894D1A0004C3653 +:1027500000F4FE703044FFF7EBFC20F000408CE703 +:1027600001208AE76FF0004087E70000F8B50668AF +:1027700004460D463378042B0CBF4FF080524FF4C3 +:1027800000128A4201D80220F8BDCA06FBD1826835 +:102790000163D2B9022B13D83389B3EB551FF2D999 +:1027A000F36BA363A36B6263002BECD003EB5523A5 +:1027B0004C36C5F308050020A3633544E563E3E721 +:1027C000F36BC271002BE7D01A4677897F02BD42B6 +:1027D000114604D23046FFF7C9FCA063E2E7204669 +:1027E000FFF72CFF431C024606D00128CBD9F36A21 +:1027F0008342C8D9ED1BEAE70120C5E701292DE98D +:10280000F04706460C46174608D9C36A8B4205D9DD +:102810000378022B62D003D8012B22D0022552E08C +:10282000033B012BFAD8816B01EBD411FFF74EFE6D +:102830000546002847D1A40006F14C0304F4FE74B9 +:102840001C443378042B07D0204627F07047FFF74D +:102850006FFC00F07040074339462046FFF76EFCDE +:102860002FE001EB5108816B01EB5821FFF72EFEA1 +:10287000054640BB14F0010406F14C0908F1010AB9 +:10288000C8F3080808BFFBB230461FBF19F808306C +:1028900003F00F023B0103F0F00318BF134309F8E4 +:1028A00008300123B16BF37001EB5A21FFF70EFEE4 +:1028B000054640B9CAF3080A44B1C7F3071709F837 +:1028C0000A700123F3702846BDE8F08719F80A3032 +:1028D000C7F3032723F00F031F43F0E7816B01EBDE +:1028E0001421FFF7F3FD05460028ECD1640006F142 +:1028F0004C0304F4FF741F551919C7F307274F70D1 +:10290000DFE70000F8B504460E461746E3690BB949 +:102910001846F8BD012BA6EB0305206814BFAA1CBE +:102920003A46691CFFF76AFF0028F2D1E369013BD0 +:10293000E361EBE701292DE9F84306460C4617460B +:10294000056802D80220BDE8F883EB6A8B42F9D90A +:102950007AB9A14621463046A046FFF76FFE0446ED +:10296000B0B92B78042B02D1002F43D1F77100208E +:10297000E9E72B78042B02D1C379022BE9D04FF081 +:10298000FF3239462846FFF739FF0028E1D0DAE761 +:102990000128D7D0421C01D10120D4E72B78042B89 +:1029A00019D1EA6AAB69023A93421CD308F10102D9 +:1029B000A2420CD02B78042B08D10023A2EB0902F1 +:1029C00049462846FFF7FEFD0028BCD1A146EB6A28 +:1029D000A342BFD8C5E7002241462846FFF70EFFB5 +:1029E0000028DED0AFE70133AB612B7943F0010360 +:1029F0002B71DBE7F3798BB9B468BC4202D10223B7 +:102A0000F371B4E721463046FFF718FE012899D943 +:102A1000431CC1D001348442EFD0A8E7032BA6D1D8 +:102A2000B368BB42A3D8B2691344BB429FD3E6E765 +:102A300070B5C3790446032B06D181688369CD182C +:102A4000A94203D10023E371002070BD4E1C206811 +:102A50003246FFF7D3FE0028F7D13146F0E70000F9 +:102A60002DE9F74305460191FFF70AFD04460028CA +:102A700049D105F14C09019928464FF40072FFF73E +:102A800075FB2146A86407464846FFF7F5F96C89AF +:102A90006402B4F5004F28BF4FF40044B4F5007F42 +:102AA0002FD9204604F026FC804630B12246002172 +:102AB000640A0026FFF7E0F909E06408EEE7234620 +:102AC000BA194146687803F0A5FF18B926446B8906 +:102AD0009E42F4D3404604F01DFC6889801B18BF59 +:102AE000012003B0BDE8F08301366B899E42F4D229 +:102AF0000123BA194946687803F08CFF0028F3D007 +:102B0000EBE70026F1E70120EBE70000F8B504460B +:102B1000FFF7B6FC0546002842D12378032B37D1B6 +:102B20002779012F34D104F14C0601464FF400728D +:102B30003046FFF7A1F955234122722184F84A3229 +:102B4000AA2304F50D7084F84F2084F84B325223E9 +:102B500084F8301284F84C3084F84D30612384F8C6 +:102B6000311284F84E3084F83332A16984F832226D +:102B7000FFF7E4FA616904F50E70FFF7DFFA626BA4 +:102B80003B46314601326078A26403F043FF257171 +:102B900000226078114603F061FF003818BF012061 +:102BA000F8BD000000232DE9F0430B6085B00F460F +:102BB0001546FFF71BFB061EC0F2B281804B53F88F +:102BC0002640002C00F0AE813C6005F0FE05237825 +:102BD0006BB1607803F0F8FEC70708D41DB110F0A0 +:102BE000040500D00A25284605B0BDE8F08300237F +:102BF000F0B22370607003F0D3FEC10700F194813E +:102C00000DB14207EED400212046FFF779FC0228DF +:102C100040F099806E4604F2122304F252213246AB +:102C200018461033FFF784FA42F8040B8B42F7D1B1 +:102C3000002556F8041B00297DD02046FFF760FCD4 +:102C4000012879D80128A26C40F0C08004F1570314 +:102C500004F18C0113F8015B002D7BD18B42F9D17B +:102C6000B4F8B430B3F5807F74D194F8B830092B40 +:102C700070D104F19400FFF75DFA4FF0FF3317189D +:102C800041F10001BB4275EB010363D304F1A000E5 +:102C9000FFF74EFA94F8BA302063012BA37059D194 +:102CA00094F8B99003FA09F91FFA89F36381002BAC +:102CB00050D0444B04F1A800FFF73AFA0646984278 +:102CC00048D8831C626304F1A400E362FFF730FA82 +:102CD00000EB020804F19C00C4F84080FFF728FADA +:102CE00010441FFA89F2A06306FB02F313EB0803FA +:102CF00045EB05029F4271EB02032BD32E4604F1F4 +:102D0000AC00FFF715FAE06365B96389B34221D9D6 +:102D1000E16B2046FFF72AFA81192046FFF7D6FB20 +:102D200098B90136631993F84C30812B14D02035B3 +:102D3000C5F30805E8E703200135042D7FF479AFDA +:102D4000042807D101E0042801D101254BE701281F +:102D50007FF678AF0D2546E705F1140004F14C0627 +:102D60003044FFF7E5F901280546F3D9E36A8342C9 +:102D7000F0D96189821E236C02FB01336364A16B6D +:102D8000204601EBD511FFF7A1FB0028DDD105F0AE +:102D90007F0006EB8000FFF7CBF9431C03D0013521 +:102DA000A842ECD0D6E70425C4E90500064A257000 +:102DB00000251388E56101339BB21380E38012E79D +:102DC00034360020FDFFFF7F38360020B4F857303E +:102DD000B3F5007FBED1B4F8626026B904F170008B +:102DE000FFF7A6F9064694F85C302663591EA370D7 +:102DF0000129AFD894F859506581002DAAD0691ED9 +:102E00002942A7D1B4F85D8018F00F0FA4F808800C +:102E1000A0D1B4F85F0018B904F16C00FFF788F98D +:102E2000B4F85A10002995D006FB03FE01EB181CDC +:102E3000F44460458ED3A0EB0C00A842B0FBF5F340 +:102E400088D33E48834285D84FF6F57083426DD9CA +:102E500003259F1C114402EB0C03032DE762626300 +:102E6000A16323644CD1B4F8763053EA08037FF4AD +:102E700071AFBB0004F17800FFF75AF9E06303F289 +:102E8000FF13B6EB532FFFF465AF4FF0FF33032D65 +:102E9000C4E905334FF08003237187D1B4F87C3047 +:102EA000012B83D1511C2046FFF710FB00287FF433 +:102EB0007DAFB4F84A224AF6552320719A427FF436 +:102EC00075AF1F4B04F14C00FFF732F998427FF4C5 +:102ED0006DAF03F1FF5304F50C70FFF729F903F50B +:102EE0000053203398427FF461AF04F50D70FFF773 +:102EF0001FF9A06104F50E70FFF71AF9606155E73C +:102F0000B8F1000F3FF426AF7144022D4FEA47039A +:102F1000E1631EBFD91907F0010303EB5103AEE7CC +:102F20000B2560E60C255EE603255CE640F6F575AC +:102F3000AB428CBF022501258BE700BFF5FFFF0FD9 +:102F4000525261412DE9F84F07460568884649B954 +:102F50006E69C6B1EB6AB34298BF0126AB69A3B9EB +:102F6000002405E0FFF76AFB0128044603D801248A +:102F70002046BDE8F88F421C00F0D280EB6A834205 +:102F8000F6D84646EAE70126E8E72A78EB6A042AFB +:102F900040F08380A6F1020A023B4FF0010B9A45F4 +:102FA00028BF4FF0000AD146696C284601EB193161 +:102FB000FFF78CFA00283BD109F00703EA6AC9F34E +:102FC000C8010BFA03F3901EDBB26A184C4609F1F4 +:102FD000010992F84C20814502EA030233BF5B00ED +:102FE00000234FF40071DBB228BF9946B2B9023416 +:102FF000631E0333BCD80123214628461A46FFF737 +:10300000E1FA0228B3D0012800F08A80B8F1000F5D +:1030100013D10223FB710028A9D130E0CA450AD0A0 +:10302000002BD2D10131B1F5007FBDD20123CCE715 +:103030004FF0FF34DCE70024DAE7FB79022B07D1FD +:10304000731CA342E7D0BB68F31ABB610323FB7177 +:1030500008F10102FB69A24205D113B10133FB6102 +:10306000D9E70223FBE70BB90123FB612246414666 +:103070003846FFF747FC00284FD10123FB61EA6A7D +:10308000AB69023A6C6193429CBF03F1FF33AB61C1 +:103090002B7943F001032B716AE7464514D1741C68 +:1030A0003846A34298BF02242146FFF7C7FA0128F9 +:1030B0003FF45DAF431C33D0E0B16B69012B03D902 +:1030C000EA6A934238BF1E4634460134EB6AA34293 +:1030D00003D8012E7FF644AF022421463846FFF77D +:1030E000ADFA48B101283FF442AF013018D0B442E4 +:1030F000EBD135E7002CE7D04FF0FF3221462846D0 +:10310000FFF77CFB48B9B8F1000FB8D02246414622 +:103110002846FFF773FB0028B1D001287FF427AFC2 +:103120004FF0FF3424E700002DE9F8430668044619 +:10313000076B894633782037042B0CBF4FF0805340 +:103140004FF40013BB429CBF00238363836B73B1B6 +:10315000C7F30808B8F1000F3CD10133416B83631A +:1031600039B93389B3EB571F34D80023A363042044 +:103170000AE07389013B13EA57232BD1FFF75EFA6C +:103180000128054602D80220BDE8F883421C01D17F +:103190000120F9E7F36A834216D8B9F1000FE4D0B1 +:1031A000616B2046FFF7CEFE0546C8B10128EAD084 +:1031B000431CEDD001463046FFF752FC0028E7D112 +:1031C000E37943F00403E371294630466563FEF773 +:1031D000CDFFA0634C36002027634644E663D3E767 +:1031E0000720D1E7F8B50E46002104460768FFF72F +:1031F000BDFA98B90546A16B3846FFF767F968B97B +:103200003A78E36B042A1B780CD11B060ED50546D1 +:1032100001212046FFF788FF0028ECD0042808BFD2 +:10322000072006E0E52B01D0002BF0D10135B54297 +:10323000EED1F8BDC16C4B1C2DE9F0410446056888 +:10324000066B1FD1E5274FF00108A16B2846FFF759 +:103250003DF998B92A78E36B042A09BF1A781F70E0 +:1032600002F07F021A7085F80380236BB3420DD2FF +:1032700000212046FFF758FF0028E6D0042808BFA9 +:10328000022003E0FFF772FA0028DBD0BDE8F081EE +:103290002DE9F04105460068A96B0669FFF716F9AC +:1032A000044620B9EB6B1A78852A03D00224204605 +:1032B000BDE8F081324603F1200153F8040B8B4244 +:1032C00042F8040BF9D1777801377F01A7F1600349 +:1032D000B3F5007FEAD800212846FFF725FF042830 +:1032E0000446E3D00028E2D1A96B2868FFF7EEF886 +:1032F00004460028DBD1EB6B1A78C02AD6D106F140 +:10330000200203F1200153F8040B8B4242F8040B16 +:10331000F9D196F823300F222C33B3FBF2F3B7EB3D +:10332000431FC3D34FF0400800212846FFF7FCFE9F +:1033300004280446BAD00028B9D1A96B2868FFF741 +:10334000C5F804460028B2D1EB6B1A78C12AADD17A +:10335000B8F5187F09D206EB080203F1200153F8F3 +:10336000040B8B4242F8040BF9D108F120084745C1 +:10337000DAD8B8F5187F9AD83046FEF71FFF738861 +:10338000834294D092E700000B68002210B50360DE +:1033900004460B6A83604B6AC261C37123F0FF036A +:1033A000896AC0E90432C164FFF7E0F920B9204618 +:1033B000BDE81040FFF76CBF10BD0000F8B5036812 +:1033C000054601271C692046FEF7F8FEA070000A9A +:1033D0006678E0702846E96CFFF7C8F920B102284A +:1033E00028BF0220C0B2F8BDA96B2868FFF76EF8AD +:1033F0000028F4D1EB6B04F1200254F8041B944232 +:1034000043F8041BF9D12B68DF70002EE7D00021B0 +:103410002846013EFFF788FEE0E700002DE9F8436B +:103420004FF0FF0806460768042445464FF6FF792B +:10343000B16B11B9002C73D063E03846FFF746F842 +:10344000044600285DD1F06B0378002B6ED03A78EB +:10345000042A11D1852B4DD1336B3046F364FFF72D +:1034600017FF044600284CD13B691B7903F03F034A +:10347000B3712046BDE8F883C27AE52B02F03F0223 +:10348000B27143D02E2B41D022F0200108293DD02B +:103490000F2A40D1590637D503F0BF05336B90F89A +:1034A0000D80F364437B434530D1428B72BB03787C +:1034B0000D21FC6823F04003DFF874E0013B4B432F +:1034C00001211EF801CB30F80CC009B3FF2B1DD829 +:1034D00024F813C06146013301320D2AF1D102787C +:1034E000520605D521B1FF2B10D8002224F8132055 +:1034F000013DEDB200213046FFF716FE04460028DC +:1035000096D00023B363B4E7AB42CBD0FF25F1E7FD +:10351000CC45E1D0FAE72DB9FEF740FE404501D198 +:103520000024A6E74FF0FF33F364A2E70424E8E7A2 +:10353000F49100082DE9F04F002187B00446D0F83F +:103540000090FFF713F9804670B999F80030042B0A +:1035500033D1D9F80C00FEF779FF07462046FFF774 +:103560005DFF054620B18046404607B0BDE8F08FBC +:10357000D9F810309A8CBA42F0D193F823B0402693 +:103580005D4506D1D9F80C3033F81530002BE5D164 +:10359000EAE7F106D9F8103008BF0236985B01F06F +:1035A0004DF8D9F80C30824633F8150001F046F892 +:1035B0008245D3D102360135E2E74FF0FF0A4FF0E2 +:1035C000FF3B5546C4F84CB0A16B4846FEF77EFF62 +:1035D00000285CD1E66B3778002F77D0F27AE52FA0 +:1035E00002F03F03A37103D0120704D50F2B04D0C0 +:1035F000C4F84CB04FE00F2B54D194F84B30580620 +:103600003FD4790645D5236B07F0BF0796F80DA088 +:10361000E364737B53453ED1738B002B3BD13578EC +:103620000121D9F80C3005F03F0501930D23013D30 +:103630005D43284B13F8012BB25A71B3FF2D05934C +:1036400029D81046049200F0F9FF6B1C03900293F6 +:10365000019B33F8150000F0F1FF039981421AD164 +:10366000049A029D1146059B1B4A9342E2D133788E +:103670005A0604D519B1019B33F815305BB97D1E8C +:10368000EDB200212046FFF74FFD00289CD0804678 +:103690006AE7BD42BDD0FF25F3E74FF6FF708242D7 +:1036A000E2D0F8E72DB93046FEF778FD50453FF4FB +:1036B0005BAF94F84B30DB079AD40B2204F1400146 +:1036C000304605F05DFC002892D14DE74FF004082C +:1036D0004AE700BFF4910008019200082DE9F04F7D +:1036E00090F84BB099B004461BF0A00540F06881FB +:1036F0000668F26832F81530002B4AD13378042B73 +:1037000040F087800F230E352046B5FBF3F5A91C4A +:10371000FFF768FD8146002877D1236B0135A3EBC5 +:103720004515E3795A07E56435D523F004032046AF +:10373000E371FFF77DF950BB4FF0FF32616B20461C +:10374000FFF7E0F818BBA3682BB3214604A8FFF7E6 +:103750001BFEE0B970894FF40071D4E90423E0FB4B +:1037600001233069C4E904233830FEF7EFFC3069E7 +:10377000D4E904232830FEF7E9FCE379326904A890 +:1037800043F0010382F82130FFF718FE18B181469B +:103790003BE00135AEE7D6E9035440220021204644 +:1037A000FEF76AFB8523012140222370C0234FF0DE +:1037B000C10C04EB010884F8203000231E469E460D +:1037C000571C04F802C0F0B2023204F807E021B13D +:1037D00035F8131009B10133DBB20F0AA15408F810 +:1037E00002700232D706F2D135F813700136002F7D +:1037F000E6D184F82330831C28466370FEF726FE4A +:1038000084F82400000A84F82500484619B0BDE871 +:10381000F08F04F140070DF1100A1BF0010F97E83B +:1038200007008AE8070000F0D38040234FF001082A +:1038300084F84B30BC46F368B8F1050F9AE80700EE +:10384000ACE803002CF8022B4FEA12428CF800205F +:1038500059D9981E424630F8021F002942D10DF175 +:103860000F0C072102F00F0E914612090EF13000E5 +:10387000392888BF0EF1370001390CF8010902D050 +:10388000B9F10F0FEED818AB7E205A1802F8580C79 +:1038900038460022914206D010F801CB02F1010E09 +:1038A000BCF1200F31D104F13F0C072902F10102D4 +:1038B00097BF18AB20205818013198BF10F8580C4A +:1038C000072A0CF80200F0D92046FFF733FE8146A4 +:1038D000002878D108F10108B8F1640FAAD14FF09F +:1038E000070992E74FF0100C01F0010E49080EEBAA +:1038F0004202D30344BF82F4883282F02102BCF139 +:10390000010CF1D1A7E74246A9E77246C2E7216B55 +:103910002046A1EB4511FEF729FF814600287FF4E0 +:1039200074AF4FF6FF783846FEF738FC0190A16B74 +:103930003046FEF7CBFD814600287FF466AFE36B8F +:10394000E9B2019A4FF00D0CD6F80CE05A734FF023 +:103950000F02DFF8E0A0DA724A1E18730CFB02F2C5 +:1039600084469876D87640451AF8019B0CF1010CF4 +:1039700018BF3EF8120003EB090B18BF013203F821 +:1039800009004FEA1029002808BF4046BCF10D0F7E +:103990008BF80190E7D1404502D03EF812200AB9D9 +:1039A00041F040011970012300212046F370FFF718 +:1039B000BBFB814600287FF428AF013DB7D11BE057 +:1039C0004FF0060921E704287FF41FAF84F84BB0BD +:1039D0001BF0020F20461BBF0C350D210125B5FB46 +:1039E000F1F518BF01352946FFF7FCFB8146002899 +:1039F0007FF40BAF013D8AD1A16B3046FEF766FD27 +:103A0000814600287FF401AF01462022E06BFEF7DB +:103A100033FAE36B03CF18605960BA7839889A7229 +:103A2000198194F84B30E26B03F0180313730123F0 +:103A3000F370EAE6F491000810B504460A46343003 +:103A4000FEF776FB886004F13800FEF773FBC2E9ED +:103A5000040194F8213003F00203D3710023D361F1 +:103A600010BD000003284B8B04BF8A8A43EA02433F +:103A7000184670472DE9F04F0B7899B004468946F7 +:103A80002F2BD0F800B001D05C2B09D14A46137817 +:103A9000914601322F2BFAD05C2BF8D0002301E0A5 +:103AA000DBF81C30A3600023E3619BF80030042B9B +:103AB0001ED1A368E3B1DBF82030214604A82362BD +:103AC000DBF824306362DBF82830A362FFF75CFC8C +:103AD0000346002854D1DBF8102002F13800FEF72D +:103AE00027FBC4E9040392F8213003F00203E371D9 +:103AF00099F800301F2B00F23581802300212046E9 +:103B000084F84B3019B0BDE8F04FFEF72FBE4946A0 +:103B10000B78894601312F2BFAD05C2BF8D01F2B64 +:103B20008CBF00250425012F2FD113882E2B31D1D6 +:103B3000002322F8173004F140029F428CBF2E214F +:103B4000202101330B2B02F8011BF6D145F0200593 +:103B5000204684F84B50FFF7EDFC94F84B300028DA +:103B600000F0E78004280BD1990603F0040240F12D +:103B7000DC80002A00F0F6808023002084F84B309F +:103B800019B0BDE8F08F0425CDE7022F02D153888C +:103B90002E2BCAD0911E87BB002322F81730002F8E +:103BA00000F0118132F81300194601332028F9D0B2 +:103BB00009B92E2801D145F00305901E30F81730C1 +:103BC0002E2B01D0013FF9D14FF020334FF0000AE6 +:103BD0006364D0462364C4F847300823481C32F895 +:103BE00011600090F6B1202E03D02E2E0DD1B842D8 +:103BF00010D045F003050099F0E731F81730202B7D +:103C000001D02E2BC8D1013FC5E79A4505D20099B6 +:103C1000B9423BD10B2B30D101E00B2B27D145F022 +:103C200003050B2394F84020E52A04BF052284F8FD +:103C30004020082B04BF4FEA88085FFA88F808F094 +:103C40000C030C2B03D008F00303032B01D145F028 +:103C50000205A8073FF57CAF18F0010F18BF45F02B +:103C6000100518F0040F18BF45F0080570E700991B +:103C7000B94202D045F00305D4D84FEA88080B2397 +:103C80004FF0080A00975FFA88F8B4E77F2E15D93D +:103C9000304640F25231CDE9022345F00203019350 +:103CA00000F098FC10F0800F0646DDE9022316D0E4 +:103CB00000F07F0646498E5D019D46B3314645487A +:103CC000CDE9012305F086F9DDE90123F8B9A6F174 +:103CD000410189B219291ED848F0020810E0FF28D6 +:103CE000EAD9591E8A4503D345F003059A4682E76F +:103CF00004EB0A01000A0AF1010A019D81F8400063 +:103D000004EB0A010AF1010A81F8406073E745F00B +:103D100003055F26F4E7A6F1610189B219299EBF68 +:103D2000203E48F00108B6B2EAE7002A08BF0520A5 +:103D300026E75A073FF524AFA379DB0645D59BF864 +:103D40000000042835D1A3682146E2792362236963 +:103D5000DBF8100023F0FF0313436362E36CA362FC +:103D6000FFF76AFE23680027DA6819F8010B0028BC +:103D70003FF409AF40F25231009200F04BFC05468F +:103D800008B31F28009A7FF6FEAE2F283FF4BFAE7F +:103D90005C283FF4BCAE7F2805D801460E4805F0EC +:103DA00019F9009A78B9FF2F0DD022F81750013772 +:103DB000DBE7216B0BF14C03C1F308011944FFF75A +:103DC00051FEA060CEE70620DAE60520D8E600BF67 +:103DD000749100086D910008649100081FB5CDE949 +:103DE000001003A814460391FEF700FA002815DB23 +:103DF0000B4A52F820300BB100211970019B0BB116 +:103E00000021197042F820302CB1002201A9684627 +:103E1000FEF7C8FE0446204604B010BD0B24FAE7A6 +:103E2000343600202DE9F04798B0904605460191C0 +:103E3000002800F0528102F03F0603A901A8324693 +:103E4000FEF7B0FE002840F04681039B4FF48C60E3 +:103E5000049303F04FFA0746002800F04081039BCB +:103E600000F500720199D86004A81A61FFF702FEFC +:103E7000044620B99DF95B30002BB8BF062418F02A +:103E80001C0F00F0CD80002C4CD0042C40D104A895 +:103E9000FFF724FC044600283AD146F00806039BAD +:103EA0001A78042A40F08380186929462B60FFF7AE +:103EB000C3FD039B1E22002118690230FDF7DCFFC1 +:103EC000039C00211A2220692630FDF7D5FF2369C3 +:103ED00020221A71246903F047FA014601220834AE +:103EE0002046FEF72BF9039B04A81B6983F82120C9 +:103EF000FFF764FA044658B9A96801B30246284698 +:103F0000FEF718FDAB68039A0446013B5361B4B158 +:103F1000384603F0FFF90CB100232B60204618B09F +:103F2000BDE8F0879DF8163013F0110F40F0848043 +:103F300018F0040F40F0C98018F0080FAFD1039AB1 +:103F400031071399936C48BF46F04006E964AB64AF +:103F50001078042872D1069B9DF817102B62089BDD +:103F6000106923F0FF030B4329466B62179BAB627A +:103F7000FFF762FDDDF80CA00024002205F15008D7 +:103F8000BAF8063021464046C5F800A0AB800023B1 +:103F900085F8306085F831406C64C5E90E234FF434 +:103FA0000072FDF769FFB20653D40024B0E703F0B6 +:103FB000DBF90146009013980E30FEF7BFF8139816 +:103FC00000991630FEF7BAF8039C13992078FFF792 +:103FD00049FD202300228046CB7220461399FEF72C +:103FE000D1F8139B002201211A775A779A77DA7752 +:103FF000039BD970B8F1000FA1D0414604A8D3F8B3 +:104000004890FEF797FC0446002881D14946039862 +:10401000FEF75CFA039B044608F1FF30586176E72F +:10402000002C7FF475AF9DF81630DC064FD418F0E5 +:10403000020F84D0D80782D5072469E7FFF712FD65 +:104040000023A86001F11C00FEF772F86B61286183 +:1040500090E7D5E9046956EA0903A6D0BAF80AA0A0 +:10406000A9684FEA4A2AC5E90E69B24574EB09030B +:104070001BD300242964002C7FF44AAFC6F3080345 +:10408000002B92D0039C2046FEF770F808B3760A06 +:104090000123414646EAC95682196A64607802F0F3 +:1040A00095FC041E18BF012432E72846FEF7C6FA25 +:1040B000B6EB0A06014669F10009012803D9431C41 +:1040C000D3D10124D6E70224D4E7082420E704242E +:1040D0001EE702241CE704461EE709241EE71124FC +:1040E0001CE700002DE9F04F994685B00023884673 +:1040F00003A90446C9F800301646FEF791F80546B4 +:1041000080BB94F831506DBB94F8303013F001034C +:10411000009300F0A68004F1500AD4E90432D4E9F7 +:104120000E011B1A62EB0102B34272F1000238BFAA +:104130001E46BEB1D4E90E10C1F30803002B40F0B7 +:104140008280039B5A894B0A013A43EAC0531A40C2 +:104150001BD151EA000309D1A06801280DD802251E +:1041600084F83150284605B0BDE8F08F216C204618 +:104170000192FEF763FA019AEFE7431C04D1012391 +:10418000009D84F83130EDE72064DDF80CB0216C3F +:104190005846FDF7EBFF0028E1D0B6F5007F02EBB3 +:1041A000000731D3BBF80A1002EB5620730A88428D +:1041B0009BF8010088BF8B1A3A464146019302F0F2 +:1041C00005FC0028DBD194F93020019B002A0BDA92 +:1041D000606CC01B984207D24FF40072514608EB46 +:1041E0004020FDF723FE019B5F02D9F80030F61B4B +:1041F000B8443B44C9F80030D4E90E32DB1942F12F +:104200000002C4E90E3294E7626CBA421AD094F903 +:104210003030002B0DDA012351469BF8010002F0EB +:10422000F9FB0028ABD194F8303003F07F0384F819 +:104230003030039801233A465146407802F0C6FBDD +:1042400000289CD16764A16B4046C1F30801C1F509 +:1042500000775144B74228BF37463A46FDF7E6FD9E +:10426000C3E707257EE7000070B596B00E46002232 +:10427000019002A901A8FEF795FC0446E0B94FF4AD +:104280008C6003F037F80546D8B1029B00F5007248 +:104290000199D86002A81A61FFF7ECFB044640B907 +:1042A0009DF95330002B0ADB1EB1314602A8FDF701 +:1042B000EDFF284603F02EF8204616B070BD062408 +:1042C000F7E71124F8E7000070B5B8B002220190BA +:1042D00003A901A8FEF766FC044608BB039B4FF444 +:1042E0008C60109303F006F80546002866D0039B07 +:1042F00000F500720199D86010A81A61FFF7BAFBA7 +:10430000044650B99DF88B30980655D4190653D4FD +:104310009DF84630DA0706D50724284602F0FAFF52 +:10432000204638B070BD039B04931878042814D13C +:1043300004A91869FFF780FB069E9DF84630DB064E +:1043400010D410A8FEF776FF04460028E5D156BB2E +:104350000398FEF7DBFB0446DFE71F99FFF782FBBC +:104360000646EAE7039BDA69B242D5D024930021DE +:10437000269624A81B78042B01BFDDE90823CDE98C +:1043800028239DF817308DF89730FEF7EFF9044693 +:104390000028C2D124A8FFF741F804460028BBD06A +:1043A0000428BAD1CDE70246314604A8FEF7C2FA86 +:1043B00004460028B1D1CBE70624AEE71124AFE7CD +:1043C000F0B5BDB0CDE900106846FDF70FFF022241 +:1043D00003A901A8FEF7E6FB0446002841D1039B90 +:1043E0004FF48C60149302F085FF0546002800F01E +:1043F000EE80039B00F5007214AE0199D8601A613B +:104400003046FFF737FB044640BB9DF89B3013F066 +:10441000A00F40F0D880039B009F1A78042A68D12F +:104420001B6904AC03F1400C1868083353F8041CF2 +:104430002246634503C21446F6D15022314628A8CD +:10444000FDF7F4FC394628A8FFF714FB04460028C2 +:104450004CD12A9A169B9A4206D00824284602F08C +:1044600059FF20463DB0F0BD349A209B9A42F4D1CA +:1044700028A8FFF733F904460028EFD1039B04AFC7 +:104480001B6993F801E093F823C09C8C3A460833EB +:1044900003CAB24243F8080C43F8041C1746F5D18E +:1044A000039B28A81B6983F801E0039B1A6982F823 +:1044B00023C01A6982F82440240A82F825401A6928 +:1044C0001379D9065CBF43F020031371FEF776FF22 +:1044D00004460028C2D13046FEF7ACFE0446002850 +:1044E000BCD10398FEF712FB0446B7E70428B5D108 +:1044F000BEE7239A04AB02F1200C1068083252F890 +:10450000041C1C46624503C42346F6D150223146A2 +:1045100028A8FDF78BFC394628A8FFF7ABFA04461C +:1045200000284CD12A9A169B9A4296D1349A209B05 +:104530009A4292D128A8FFF7D1F8044600288DD1DD +:1045400037990DF11D030DF12D0001F10D0253F806 +:10455000044B834242F8044BF9D11888012710809C +:104560009B7893709DF81B30039CDA0658BF43F08C +:104570002003CB72E770CB7ADB06ACD5169A2A9B68 +:104580009A42A8D02078FFF76DFA01462046FDF741 +:10459000EDFD0146C8B12046FDF798FF044600280E +:1045A0007FF45CAF039890F86D302E2B93D12A9A4C +:1045B00000F16C01FDF7E6FD039BDF708BE704283B +:1045C0007FF44CAFB6E7062448E7022446E71124FF +:1045D00047E700007F2810B501D880B210BDB0F5C4 +:1045E000803F13D240F2523399420FD10849002242 +:1045F00031F8024B93B2844203D103F18000C0B280 +:10460000ECE70132802AF3D11346F6E70020E5E714 +:10461000B49400087F280DD940F25233994208D152 +:10462000FF2806D800F10040034B803833F8100013 +:1046300070470020704700BFB4940008B0F5803F79 +:10464000F0B522D21F4A83B21F49B0F5805F28BF60 +:104650000A46141D34F8042C2146AAB1934213D300 +:1046600034F8025C2E0AEFB252FA85F5A84222DA3B +:10467000082E09D8DFE806F0050A10121416181AD9 +:104680001C00801A34F810301846F0BD981A00F05B +:1046900001001B1A9BB2F7E7103BFBE7203BF9E751 +:1046A000303BF7E71A3BF5E70833F3E7503BF1E718 +:1046B000A3F5E353EEE70434002ECBD101EB470220 +:1046C000C7E700BF04920008F893000808B5074B3D +:1046D000074A196801F03D01996053680BB1906871 +:1046E0009847BDE8084003F043B800BF000002400F +:1046F0003C36002008B5084B1968890901F03D01D6 +:104700008A019A60054AD3680BB110699847BDE8E1 +:10471000084003F02DB800BF000002403C360020E6 +:1047200008B5084B1968090C01F03D010A049A60AC +:10473000054A53690BB190699847BDE8084003F0FA +:1047400017B800BF000002403C36002008B5084BF7 +:104750001968890D01F03D018A059A60054AD369FF +:104760000BB1106A9847BDE8084003F001B800BFDC +:10477000000002403C36002008B5074B074A596844 +:1047800001F03D01D960536A0BB1906A9847BDE8CA +:10479000084002F0EDBF00BF000002403C360020A0 +:1047A00008B5084B5968890901F03D018A01DA60B2 +:1047B000054AD36A0BB1106B9847BDE8084002F078 +:1047C000D7BF00BF000002403C36002008B5084BB0 +:1047D0005968090C01F03D010A04DA60054A536B7F +:1047E0000BB1906B9847BDE8084002F0C1BF00BF15 +:1047F000000002403C36002008B5084B5968890D7E +:1048000001F03D018A05DA60054AD36B0BB1106CEB +:104810009847BDE8084002F0ABBF00BF000002406F +:104820003C36002008B5074B074A196801F03D01E6 +:104830009960536C0BB1906C9847BDE8084002F04A +:1048400097BF00BF000402403C36002008B5084B6B +:104850001968890901F03D018A019A60054AD36C03 +:104860000BB1106D9847BDE8084002F081BF00BF52 +:10487000000402403C36002008B5084B1968090CBA +:1048800001F03D010A049A60054A536D0BB1906D29 +:104890009847BDE8084002F06BBF00BF000402402B +:1048A0003C36002008B5084B1968890D01F03D0120 +:1048B0008A059A60054AD36D0BB1106E9847BDE822 +:1048C000084002F055BF00BF000402403C36002003 +:1048D00008B5074B074A596801F03D01D960536E8E +:1048E0000BB1906E9847BDE8084002F041BF00BF91 +:1048F000000402403C36002008B5084B596889097D +:1049000001F03D018A01DA60054AD36E0BB1106FE8 +:104910009847BDE8084002F02BBF00BF00040240EA +:104920003C36002008B5084B5968090C01F03D01E0 +:104930000A04DA60054A536F0BB1906F9847BDE8DF +:10494000084002F015BF00BF000402403C360020C2 +:1049500008B5084B5968890D01F03D018A05DA60F8 +:10496000054AD36F13B1D2F880009847BDE80840DC +:1049700002F0FEBE000402403C36002000230C4939 +:1049800010B51A460B4C0B6054F82300026001EB83 +:10499000430004334260402BF6D1074A4FF0FF3307 +:1049A0009360D360C2F80834C2F80C3410BD00BF65 +:1049B0003C360020B4950008000002400F28F8B5EE +:1049C00010D9102810D0112811D0122808D10F2486 +:1049D0000720DFF8C8E00126DEF80050A04208D921 +:1049E000002653E00446F4E70F240020F1E70724F3 +:1049F000FBE706FA00F73D424AD1264C4FEA001C7D +:104A00003D4304EB00160EEBC000CEF80050C0E9A9 +:104A10000123FBB273B12048D0F8D83043F0010332 +:104A2000C0F8D830D0F8003143F00103C0F80031AD +:104A3000D0F8003117F47F4F0ED01748D0F8D83097 +:104A400043F00203C0F8D830D0F8003143F002033D +:104A5000C0F80031D0F8003154F80C00036823F09E +:104A60001F030360056815F00105FBD104EB0C037F +:104A70003D2493F80CC05F6804FA0CF43C602124D8 +:104A80000560446112B1987B00F06AFC3046F8BDC5 +:104A90000130A3E7B4950008004402583C360020DA +:104AA00010B5302484F31188FFF788FF002383F3C7 +:104AB000118810BD10B50446807B00F067FC01230F +:104AC0001549627B03FA02F20B6823EA0203DAB2A9 +:104AD0000B6072B9114AD2F8D81021F00101C2F866 +:104AE000D810D2F8001121F00101C2F80011D2F85B +:104AF000002113F47F4F0ED1084BD3F8D82022F0B9 +:104B00000202C3F8D820D3F8002122F00202C3F831 +:104B10000021D3F8003110BD3C360020004402587B +:104B200008B5302383F31188FFF7C4FF002383F314 +:104B3000118808BD836CC26A8B42506810B506D9D3 +:104B40005A1E4C0002EB4103B3FBF4F3184410BDB2 +:104B500001F001038A0748BF43F002034A0748BF38 +:104B600043F008030A0748BF43F00403CA0648BFDE +:104B700043F010038A06426B48BF43F020031343FF +:104B80004363704710B5074C204600F06BFF064B9F +:104B90000022C4E91023054BA364054BE363054BD6 +:104BA000E36410BDC03600200070005200B4C4049D +:104BB0001437002014390020C36A0BB9104BC362AC +:104BC0000379012B11D10F4B98420ED10E4BD3F824 +:104BD000D42042F48032C3F8D420D3F8FC2042F42D +:104BE0008032C3F8FC20D3F8FC30436C00221A65F5 +:104BF000DA621A605A605A624FF0FF329A63704765 +:104C0000B4960008C0360020004402580379012BF6 +:104C100024D0436C00221A65DA621A605A605A6224 +:104C20004FF0FF329A630E4B984217D10D4BDA6F5B +:104C300042F48032DA67DA6F22F48032DA67DA6FB0 +:104C4000D3F8D42022F48032C3F8D420D3F8FC2047 +:104C500022F48032C3F8FC20D3F8FC30704700BF48 +:104C6000C03600200044025810B5446C0649FFF7D6 +:104C700061FF6060236842F2107043F00303236019 +:104C8000BDE8104001F004BD801A06000129F8B506 +:104C9000466C0B4F09D175680A493D40FFF74AFF42 +:104CA000054345F480557560F8BD746806493C407D +:104CB000FFF740FF044344F480547460F4E700BFFE +:104CC00000ECFFFF80F0FA0240787D01436C002287 +:104CD0005A601A6070470000426C0129536823F43F +:104CE000404304D0022905D001B95360704743F412 +:104CF0008043FAE743F40043F7E70000436C41F4D4 +:104D000080519A60D9605A6B1206FCD580229A6352 +:104D10007047000010B541F48851446CA260E16016 +:104D2000616B11F04502FBD0A26311F0040203D0C5 +:104D3000FFF70EFF012010BD616910461960FAE708 +:104D400010B541F48851446CA260E160616B11F0D0 +:104D50004502FBD0A26311F0050203D0FFF7F8FE75 +:104D6000012010BD616910461960FAE773B513465A +:104D700004460E46302282F31188426CD26B32B167 +:104D80004FF0FF314030019301F08EFC019B606CCD +:104D900000220265C263C262456B15F4807504D1BE +:104DA00085F31188012002B070BD4FF0FF3181639F +:104DB00082F31188012E06D90C21204602B0BDE8ED +:104DC0007040FFF7BDBF1046EDE7000073B5446CBF +:104DD0000E4600250192616BA1632565E562FFF730 +:104DE000B7FE012E07D9019B2A460C2102B0BDE86F +:104DF0007040FFF7A5BF02B070BD000010B541F4D0 +:104E00009851446CA260E160616B11F04502FBD0E7 +:104E1000A26311F03F0203D0FFF79AFE012010BDFC +:104E2000216A10461960E1695960A1699960616958 +:104E3000D960F4E72DE9F74304460191006D01A91B +:104E40001746984602F0AEFB064600284AD0626C30 +:104E50002046DDF804905568C5F3090501356B005F +:104E6000A56CB5FBF3F54FF47A73B5FBF3F55D4331 +:104E7000556200F0BDFD50BB636C4FF0FF32012561 +:104E80004146C3F8589020461D659A634FF49572C9 +:104E9000DA6342F207029F62DA62E36C0A9AFFF772 +:104EA0004FFFA0B9E26C104B11680B407BB929464B +:104EB0002046FFF75BFF054648B92E463A46019962 +:104EC000206D02F0A7FB304603B0BDE8F083E26C32 +:104ED00020460121FFF77AFF3A460199206D02F042 +:104EE00099FBF0E70126EEE708E0FFFD2DE9F74327 +:104EF0001F46436C01924FF47A725D6804468846FF +:104F0000C5F3090501356E00856CB5FBF6F5B5FBFB +:104F1000F2F555435D6200F06BFD20B10125284696 +:104F200003B0BDE8F0837E0201A9206D324602F095 +:104F300039FB05460028F1D0636C019AD4F84C90F7 +:104F40009A6501221A654FF0FF329A634FF4957209 +:104F5000DA639E62236BDB064B4658BF4FEA482854 +:104F6000012F42461BD912212046FFF7E9FEC0B9A6 +:104F7000D9F80020104B13409BB9636C42F29302A6 +:104F800039462046DA62E26CFFF7F0FE804640B90F +:104F900032460199206D454602F03CFBBFE71121E6 +:104FA000E2E7E26C39462046FFF710FF32460199EE +:104FB000206D02F02FFBB2E708E0FFFD2DE9F34181 +:104FC0001F46436C01924FF47A725D68044688462E +:104FD000C5F3090501356E00856CB5FBF6F5B5FB2B +:104FE000F2F555435D6200F003FD20B1012528462E +:104FF00002B0BDE8F0817E0201A9206D324602F0C8 +:1050000021FB05460028F1D0636C019A9A650122C4 +:105010001A654FF0FF329A634FF48D72DA639E6225 +:10502000236BE66CDB06334658BF4FEA4828012F56 +:10503000424619D919212046FFF782FEB0B93268DD +:105040000F4B134093B9636C42F2910239462046EC +:10505000DA62E26CFFF78AFE064638B901993546F6 +:10506000206D02F02BFBC2E71821E4E73946204609 +:10507000E26CFFF7ABFE0199206D02F01FFBB6E773 +:1050800008E0FFFD12F0030F2DE9F04107460C4642 +:1050900015461E4617D00E44B44202D10020BDE88A +:1050A000F0810123FA6B21463846FFF71FFF0028E5 +:1050B000F5D128464FF40072F96B05F500750134FF +:1050C000FCF7B4FEE8E7BDE8F041FFF70FBF0000D2 +:1050D00012F0030F2DE9F04107460C4615461E4617 +:1050E00017D00E44B44202D10020BDE8F081294619 +:1050F0004FF40072F86B05F50075FCF797FE01237D +:10510000FA6B21463846FFF759FF0028EDD10134EC +:10511000E8E7BDE8F041FFF751BF0000002070470D +:10512000302310B583F311880024436C40302146AE +:10513000DC6301F0C7FA84F3118810BD026843688C +:105140001143016003B1184770470000024A136819 +:1051500043F0C003136070470014014013B50E4CB8 +:10516000204600F0DFFC04F1140000234FF400722D +:105170000A49009400F07AF9094B4FF4007209498A +:1051800004F13800009400F0F3F9074A074BC4E932 +:10519000172302B010BD00BF183900208439002049 +:1051A0004D510008843B00200014014000E1F5054A +:1051B000037C30B5244C002918BF0C46012B11D1BB +:1051C000224B98420ED1224BD3F8F02042F020021D +:1051D000C3F8F020D3F8182142F02002C3F81821B8 +:1051E000D3F818312268036EC16D03EB5203846655 +:1051F000B3FBF2F36268150442BF23F0070503F026 +:10520000070343EA4503CB60A36843F040034B60C8 +:10521000E36843F001038B6042F4967343F00103AB +:105220000B604FF0FF330B62510505D512F01022D1 +:1052300005D0B2F1805F04D080F8643030BD7F23A8 +:10524000FAE73F23F8E700BFBC96000818390020B2 +:10525000004402582DE9F047C66D05463768F469E9 +:10526000210734621AD014F0080118BF4FF480717E +:10527000E20748BF41F02001A3074FF0300348BFC9 +:1052800041F04001600748BF41F0800183F311887D +:10529000281DFFF753FF002383F31188E2050AD589 +:1052A000302383F311884FF48061281DFFF746FFF8 +:1052B000002383F311884FF030094FF0000A14F0F7 +:1052C000200838D13B0616D54FF0300905F1380AD1 +:1052D000200610D589F31188504600F07DF900288A +:1052E00036DA0821281DFFF729FF27F080033360F5 +:1052F000002383F31188790614D5620612D5302372 +:1053000083F31188D5E913239A4208D12B6C33B16A +:1053100027F040071021281DFFF710FF37600023FA +:1053200083F31188E30618D5AA6E1369ABB15069EF +:10533000BDE8F047184789F31188736A284695F845 +:105340006410194000F008FC8AF31188F469B6E78C +:10535000B06288F31188F469BAE7BDE8F08700000D +:10536000090100F16043012203F56143C9B283F8EA +:10537000001300F01F039A4043099B0003F16043B0 +:1053800003F56143C3F880211A60704700F01F03E2 +:1053900001229A40430900F160409B0000F5614002 +:1053A00003F1604303F56143C3F88020C3F8802113 +:1053B000002380F800337047F8B51546826804462C +:1053C0000B46AA4200D28568A1692669761AB542C1 +:1053D0000BD218462A46FCF729FDA3692B44A3618A +:1053E0002846A3685B1BA360F8BD0CD9AF1B184609 +:1053F0003246FCF71BFD3A46E1683044FCF716FDE7 +:10540000E3683B44EBE718462A46FCF70FFDE368E8 +:10541000E5E7000083689342F7B50446154600D2DD +:105420008568D4E90460361AB5420BD22A46FCF7E7 +:10543000FDFC63692B4463612846A3685B1BA36082 +:1054400003B0F0BD0DD93246AF1B0191FCF7EEFC65 +:1054500001993A46E0683144FCF7E8FCE3683B44D4 +:10546000E9E72A46FCF7E2FCE368E4E710B50A4402 +:105470000024C361029B8460C16002610362C0E9D1 +:105480000000C0E9051110BD08B5D0E9053293420E +:1054900001D1826882B98268013282605A1C4261FD +:1054A00019700021D0E904329A4224BFC3684361D5 +:1054B00001F022F9002008BD4FF0FF30FBE70000AB +:1054C00070B5302304460E4683F31188A568A5B154 +:1054D000A368A269013BA360531CA36115782269EC +:1054E000934224BFE368A361E3690BB12046984768 +:1054F000002383F31188284607E03146204601F057 +:10550000EBF80028E2DA85F3118870BD2DE9F74F3A +:1055100004460E4617469846D0F81C904FF0300AC5 +:105520008AF311884FF0000B154665B12A463146C3 +:105530002046FFF741FF034660B94146204601F08F +:10554000CBF80028F1D0002383F31188781B03B037 +:10555000BDE8F08FB9F1000F03D001902046C84795 +:10556000019B8BF31188ED1A1E448AF31188DCE746 +:10557000C160C361009B82600362C0E905111144F0 +:10558000C0E9000001617047F8B504460D461646B3 +:10559000302383F31188A768A7B1A368013BA360F8 +:1055A00063695A1C62611D70D4E904329A4224BFB7 +:1055B000E3686361E3690BB120469847002080F3FC +:1055C000118807E03146204601F086F80028E2DA2B +:1055D00087F31188F8BD0000D0E9052310B59A4281 +:1055E00001D182687AB982680021013282605A1C36 +:1055F00082611C7803699A4224BFC368836101F009 +:105600007BF8204610BD4FF0FF30FBE72DE9F74F48 +:1056100004460E4617469846D0F81C904FF0300AC4 +:105620008AF311884FF0000B154665B12A463146C2 +:105630002046FFF7EFFE034660B94146204601F0E1 +:105640004BF80028F1D0002383F31188781B03B0B6 +:10565000BDE8F08FB9F1000F03D001902046C84794 +:10566000019B8BF31188ED1A1E448AF31188DCE745 +:105670000379052B05BF836A002001204B6004BF1E +:105680004FF400730B60704770B55D1E866A044668 +:105690000D44B54205D9436B43F0800343630120B9 +:1056A00070BD06250571FFF7EDFC05232371F7E7B3 +:1056B00070B55D1E866A04460D44B54205D9436B3C +:1056C00043F080034363012070BD07250571FFF798 +:1056D000FFFC05232371F7E738B505790446052D4E +:1056E00005D108230371FFF719FD257138BD01208D +:1056F000FCE700000323F0B5037185B00446FFF713 +:10570000B3FA002220461146FFF7F8FA4FF4D5729B +:1057100003AB08212046FFF713FB0246B8B901236B +:105720002363039BC3F30323012B09D103AB37216D +:105730002046FFF705FB18B9A44B039A1340ABB101 +:1057400020460125FFF7C2FA0223237137E103AB9C +:10575000002237212046FFF7F3FA28B99B4A039B22 +:105760001A40002A00F0A78002232363236B03F072 +:105770000F03022B40F0A9806425954E42F2107071 +:1057800000F086FF03AB324601212046FFF7C2FA44 +:105790000028D5D1039B002B80F293805A0003D5BB +:1057A000236B43F010032363002204F1080302215A +:1057B0002046FFF723FB02460028C1D104F138033D +:1057C00003212046FFF7BCFA0028B9D104F11805DF +:1057D000A26B092120462B46FFF710FB0028AFD112 +:1057E00002ABA26B07212046FFF7AAFA0646002863 +:1057F000A6D1236B03F00F03022B40F08F807E2293 +:105800007F21284603F010F9012840F28780E76BDA +:1058100042F2107000F03CFF08234FF4007239464A +:1058200020460096FFF706FB002889D1384603F092 +:1058300049F9236BA06203F00F03022B72D103AB73 +:10584000644A06212046FFF77BFA002871D15F49A0 +:10585000039B1940B1FA81F149092046FFF716FA76 +:1058600002AB4FF4007210212046FFF769FA05469B +:1058700000287FF465AF554E029B33427FF460AF42 +:10588000236B13F00E0F03F00F0273D0022A7FF484 +:1058900057AFE36A1978012900F09480022900F0DB +:1058A0009380002900F089804B4F2046FFF714FABF +:1058B00003AB3A4676E0114620462263FFF71EFA14 +:1058C00054E7013D7FF45AAF3AE7444D6426444A19 +:1058D0003E4F012B18BF154603AB0022372120464F +:1058E000FFF72EFA00287FF42BAF039B3B427FF497 +:1058F00027AF03AB2A4629212046FFF70BFA0028E1 +:105900007FF41EAF039B002BFFF648AF013E3FF430 +:1059100017AF42F2107000F0BBFEDDE7284603F03F +:10592000A5F886E77E227F212846E66B03F07CF807 +:1059300008B9002191E70023402231462046009318 +:105940000623FFF777FA0028F3D1B3895BBA9B07E8 +:10595000EFD5244B40223146204600930623FFF723 +:1059600069FA0028E5D1317C01F00F010F3918BF29 +:10597000012172E7E36A1978F9B101297FF4E0AEF9 +:105980002046FFF7A9F903ABA26B37212046FFF7AA +:10599000D7F900287FF4D4AE039B33427FF4D0AE16 +:1059A00003AB022206212046FFF7CAF900287FF444 +:1059B000C7AE039B33427FF4C3AE05232371284651 +:1059C00005B0F0BD084F70E7084F6EE708E0FFFD37 +:1059D0000080FFC00001B9030000B7030080FF5042 +:1059E00000001080F1FFFF800001B7030002B70341 +:1059F00037B504460C4D01ABA26B0D212046FFF7D5 +:105A00009FF978B9019B2B420BD1C3F34323042B9D +:105A100008D0053B022B04D84FF47A7000F038FE12 +:105A2000E9E7012003B030BD08E0FFFD70B5302389 +:105A3000054683F3118803790024022B03D184F3F4 +:105A40001188204670BD0423037184F31188022657 +:105A5000FFF7CEFF04462846FFF738F92E71F0E72E +:105A6000FFF790B8044B03600123037100234363E5 +:105A7000C0E90A33704700BFD496000810B5302340 +:105A8000044683F31188C162FFF796F802230020D1 +:105A9000237180F3118810BD10B53023044683F3C1 +:105AA0001188FFF7B3F800230122E362227183F328 +:105AB000118810BD026843681143016003B11847A3 +:105AC000704700001430FFF721BD00004FF0FF3396 +:105AD0001430FFF71BBD00003830FFF797BD000002 +:105AE0004FF0FF333830FFF791BD00001430FFF75F +:105AF000E7BC00004FF0FF311430FFF7E1BC0000BD +:105B00003830FFF741BD00004FF0FF323830FFF76B +:105B10003BBD0000012914BF6FF013000020704747 +:105B2000FFF71CBB044B036000234360C0E9023352 +:105B300001230374704700BFF896000810B53023A6 +:105B4000044683F31188FFF733FB022300202374FC +:105B500080F3118810BD000038B5C36904460D46B6 +:105B60001BB904210844FFF7A5FF294604F11400DE +:105B7000FFF78AFC002806DA201D4FF40061BDE81B +:105B80003840FFF797BF38BD026843681143016092 +:105B900003B118477047000013B5406B00F58054FF +:105BA000D4F8A4381A681178042914D1017C022988 +:105BB00011D11979012312898B4013420BD101A90C +:105BC0004C3002F05FFED4F8A4480246019B2179D4 +:105BD000206800F0DFF902B010BD0000143002F0C0 +:105BE000E1BD00004FF0FF33143002F0DBBD0000D8 +:105BF0004C3002F0B3BE00004FF0FF334C3002F0E7 +:105C0000ADBE0000143002F0AFBD00004FF0FF3118 +:105C1000143002F0A9BD00004C3002F07FBE00003D +:105C20004FF0FF324C3002F079BE00000020704788 +:105C300010B500F58054D4F8A4381A6811780429F6 +:105C400017D1017C022914D15979012352898B4043 +:105C500013420ED1143002F041FD024648B1D4F88F +:105C6000A4484FF4407361792068BDE8104000F00B +:105C70007FB910BD406BFFF7DBBF0000704700002D +:105C80007FB5124B012504260446036000230574EA +:105C900000F1840243602946C0E902330C4B0290B4 +:105CA000143001934FF44073009602F0F3FC094B5B +:105CB00004F69442294604F14C000294CDE90063B5 +:105CC0004FF4407302F0BAFD04B070BD2097000895 +:105CD000755C0008995B00080A68302383F311881B +:105CE0000B790B3342F823004B79133342F823002E +:105CF0008B7913B10B3342F8230000F58053C3F8BE +:105D0000A41802230374002080F311887047000058 +:105D100038B5037F044613B190F85430ABB9012570 +:105D2000201D0221FFF730FF04F114006FF0010184 +:105D3000257700F0EDFC04F14C0084F854506FF02E +:105D40000101BDE8384000F0E3BC38BD10B50121C9 +:105D500004460430FFF718FF0023237784F85430FB +:105D600010BD000038B504460025143002F0AAFC2E +:105D700004F14C00257702F079FD201D84F8545081 +:105D80000121FFF701FF2046BDE83840FFF750BF73 +:105D900090F8803003F06003202B06D190F881202A +:105DA0000023212A03D81F2A06D800207047222A60 +:105DB000FBD1C0E91D3303E0034A42670722826733 +:105DC000C3670120704700BF3C22002037B500F5B3 +:105DD0008055D5F8A4381A68117804291AD1017CA5 +:105DE000022917D11979012312898B40134211D14D +:105DF00000F14C04204602F0F9FD58B101A92046FB +:105E000002F040FDD5F8A4480246019B21792068A4 +:105E100000F0C0F803B030BD01F10B03F0B550F84D +:105E2000236085B004460D46FEB1302383F311880C +:105E300004EB8507301D0821FFF7A6FEFB6806F17D +:105E40004C005B691B681BB1019002F029FD0198B1 +:105E500003A902F017FD024648B1039B29462046DC +:105E600000F098F8002383F3118805B0F0BDFB68BB +:105E70005A691268002AF5D01B8A013B1340F1D100 +:105E800004F18002EAE70000133138B550F82140F0 +:105E9000ECB1302383F3118804F58053D3F8A428A0 +:105EA0001368527903EB8203DB689B695D6845B137 +:105EB00004216018FFF768FE294604F1140002F07F +:105EC00017FC2046FFF7B4FE002383F3118838BD8A +:105ED0007047000001F04ABD01234022002110B5A7 +:105EE000044600F8303BFBF7C7FF0023C4E9013349 +:105EF00010BD000010B53023044683F3118824221E +:105F0000416000210C30FBF7B7FF204601F050FD47 +:105F100002230020237080F3118810BD70B500EBC0 +:105F20008103054650690E461446DA6018B1102206 +:105F30000021FBF7A1FFA06918B110220021FBF797 +:105F40009BFF31462846BDE8704001F037BE000097 +:105F500083682022002103F0011310B504468360FA +:105F60001030FBF789FF2046BDE8104001F0B2BEBB +:105F7000F0B4012500EB810447898D40E4683D437E +:105F8000A469458123600023A2606360F0BC01F036 +:105F9000CFBE0000F0B4012500EB810407898D40DD +:105FA000E4683D436469058123600023A260636067 +:105FB000F0BC01F045BF000070B502230025044687 +:105FC000242203702946C0F888500C3040F8045C45 +:105FD000FBF752FF204684F8705001F083FD6368A0 +:105FE0001B6823B129462046BDE87040184770BDA4 +:105FF0000378052B10B504460AD080F88C300523B1 +:10600000037043681B680BB1042198470023A36009 +:1060100010BD00000178052906D190F88C20436856 +:1060200002701B6803B118477047000070B590F804 +:106030007030044613B1002380F8703004F1800200 +:10604000204601F06BFE63689B68B3B994F880301A +:1060500013F0600535D00021204602F05DF90021E3 +:10606000204602F04DF963681B6813B106212046F3 +:106070009847062384F8703070BD20469847002862 +:10608000E4D0B4F88630A26F9A4288BFA36794F92F +:106090008030A56F002B4FF0300380F20381002D7C +:1060A00000F0F280092284F8702083F31188002127 +:1060B0002046D4E91D23FFF76DFF002383F31188E9 +:1060C000DAE794F8812003F07F0343EA022340F2E9 +:1060D0000232934200F0C58021D8B3F5807F48D0CA +:1060E0000DD8012B3FD0022B00F09380002BB2D1B2 +:1060F00004F1880262670222A267E367C1E7B3F591 +:10610000817F00F09B80B3F5407FA4D194F882306A +:10611000012BA0D1B4F8883043F0020332E0B3F58C +:10612000006F4DD017D8B3F5A06F31D0A3F5C06381 +:10613000012B90D86368204694F882205E6894F81A +:106140008310B4F88430B047002884D04368636774 +:106150000368A3671AE0B3F5106F36D040F6024229 +:1061600093427FF478AF5C4B63670223A3670023FD +:10617000C3E794F88230012B7FF46DAFB4F8883018 +:1061800023F00203A4F88830C4E91D55E56778E7D9 +:10619000B4F88030B3F5A06F0ED194F88230204669 +:1061A00084F88A3001F0FCFC63681B6813B101219C +:1061B00020469847032323700023C4E91D339CE73E +:1061C00004F18B0363670123C3E72378042B10D109 +:1061D000302383F311882046FFF7BAFE85F3118838 +:1061E0000321636884F88B5021701B680BB1204633 +:1061F000984794F88230002BDED084F88B3004234B +:10620000237063681B68002BD6D002212046984774 +:10621000D2E794F8843020461D0603F00F010AD51A +:1062200001F06EFD012804D002287FF414AF2B4B3F +:106230009AE72B4B98E701F055FDF3E794F882308D +:10624000002B7FF408AF94F8843013F00F01B3D023 +:106250001A06204602D502F077F8ADE702F068F89A +:10626000AAE794F88230002B7FF4F5AE94F88430DE +:1062700013F00F01A0D01B06204602D502F04CF807 +:106280009AE702F03DF897E7142284F8702083F330 +:1062900011882B462A4629462046FFF769FE85F3DA +:1062A0001188E9E65DB1152284F8702083F3118826 +:1062B00000212046D4E91D23FFF75AFEFDE60B22FC +:1062C00084F8702083F311882B462A4629462046FD +:1062D000FFF760FEE3E700BF50970008489700080B +:1062E0004C97000838B590F870300446002B3ED02B +:1062F000063BDAB20F2A34D80F2B32D8DFE803F08E +:1063000037313108223231313131313131313737A2 +:10631000856FB0F886309D4214D2C3681B8AB5FBE6 +:10632000F3F203FB12556DB9302383F311882B462A +:106330002A462946FFF72EFE85F311880A2384F8A2 +:1063400070300EE0142384F87030302383F311880A +:10635000002320461A461946FFF70AFE002383F35E +:10636000118838BDC36F03B198470023E7E70021C8 +:10637000204601F0D1FF0021204601F0C1FF6368F3 +:106380001B6813B10621204698470623D7E7000073 +:1063900010B590F870300446142B29D017D8062B6E +:1063A00005D001D81BB110BD093B022BFBD8002141 +:1063B000204601F0B1FF0021204601F0A1FF6368F3 +:1063C0001B6813B1062120469847062319E0152BB8 +:1063D000E9D10B2380F87030302383F31188002338 +:1063E0001A461946FFF7D6FD002383F31188DAE732 +:1063F000C3689B695B68002BD5D1C36F03B1984715 +:10640000002384F87030CEE70023826880F82430BF +:1064100083691B6899689142FBD25A6803604260A5 +:1064200010605860704700000023826880F82430B4 +:1064300083691B6899689142FBD85A68036042607F +:10644000106058607047000008B50846302383F399 +:10645000118891F82430032B05D0042B0DD02BB9D3 +:1064600083F3118808BD8B6A00221A604FF0FF3356 +:106470008362FFF7C9FF0023F2E7D1E9003213601E +:106480005A60F3E7034610B51B68984203D09C6836 +:106490008A689442F8D25A680B604A601160596069 +:1064A00010BD0000FFF7B0BF38B50B4B1C68DD68AE +:1064B00085F82400226853601A600122DC60A3681A +:1064C00084F82420934201D101F016F92946204690 +:1064D000BDE83840F9F796BF883D00200C4B30B539 +:1064E000DD684B1C87B004460FD02B46094A68462E +:1064F00000F0A2F92046FFF7D7FF009B13B16846D2 +:1065000000F0A4F9A86A07B030BDFFF7CDFFF9E7A6 +:10651000883D002049640008044B1A68DB689068D5 +:106520009B68984294BF002001207047883D00205E +:1065300038B50C4B1C68DD68226853601A60012274 +:10654000DC6084F82420AB68934201D101F0D6F8D6 +:106550002846FFF769FF01462046BDE83840F9F7B5 +:1065600051BF00BF883D0020044B1A68DB68926869 +:106570009B689A4201D9FFF7DBBF7047883D002036 +:1065800038B50123084C00252370656002F0C8FA75 +:1065900002F0EEFA0549064802F0C4FB022323701C +:1065A00085F3118838BD00BF30400020589700089F +:1065B000883D002000F080B9034A516853685B1A97 +:1065C0009842FBD8704700BF001000E08B604B631F +:1065D0000023CA6100F128020B6302230A618B8445 +:1065E0000123886181F8263001F11003C26A4A61F3 +:1065F0001360C36201F12C030846CB6270470000B0 +:10660000D0E90131026841F8183CA1F19C03383906 +:10661000CB60036941F8243C436941F8203C034BBB +:1066200041F8043CC3680248FFF7D0BF1D040008CE +:10663000883D002008B5FFF7E3FFBDE80840FFF7FD +:1066400031BF000038B50E4BDC6804F12C05A062A8 +:10665000E06AA8420FD194F826303BB994F825306F +:106660009B0702BFD4E9043213605A600F20BDE8D3 +:106670003840FFF719BF0368E362FFF713FFE7E74E +:10668000883D0020302383F31188FFF7DBBF000033 +:1066900008B50146302383F311880820FFF71EFF59 +:1066A000002383F3118808BD054BDB6821B103602B +:1066B00098620320FFF712BF4FF0FF30704700BF12 +:1066C000883D002003682BB10022026018469962C1 +:1066D000FFF7E8BE70470000064BDB6839B142683F +:1066E00018605A60136043600420FFF7F7BE4FF054 +:1066F000FF307047883D00200368984206D01A6832 +:106700000260506018469962FFF7CCBE70470000E7 +:1067100038B504460D462068844200D138BD036870 +:1067200023605C608562FFF7BDFEF4E7036810B587 +:106730009C68A2420CD85C688A600B604C60216047 +:10674000596099688A1A9A604FF0FF33836010BDD0 +:10675000121B1B68ECE700000A2938BF0A2170B53C +:1067600004460D460A26601902F0D4F902F0BCF97D +:10677000041BA54203D8751C04462E46F3E70A2ED7 +:1067800004D90120BDE8704002F094BB70BD000048 +:10679000F8B5144B0D460A2A4FF00A07D96103F1E8 +:1067A0001001826038BF0A224160196914460160F5 +:1067B00048601861A81802F09DF902F095F9431B92 +:1067C0000646A34206D37C1C28192746354602F00C +:1067D000A1F9F2E70A2F04D90120BDE8F84002F040 +:1067E00069BBF8BD883D0020F8B506460D4602F0AD +:1067F0007BF90F4A134653F8107F9F4206D12A4671 +:1068000001463046BDE8F840FFF7C2BFD169BB681A +:10681000441A2C1928BF2C46A34202D92946FFF757 +:106820009BFF224631460348BDE8F840FFF77EBF94 +:10683000883D0020983D0020C0E90323002310B4C8 +:106840005DF8044B4361FFF7CFBF000010B5194C52 +:10685000236998420DD08168D0E9003213605A60F4 +:106860009A680A449A60002303604FF0FF33A361E3 +:1068700010BD0268234643F8102F536000220260C7 +:1068800022699A4203D1BDE8104002F03DB99368F5 +:1068900081680B44936002F027F92269E1699268EC +:1068A000441AA242E4D91144BDE81040091AFFF786 +:1068B00053BF00BF883D00202DE9F047DFF8BC80C2 +:1068C00008F110072C4ED8F8105002F00DF9D8F846 +:1068D0001C40AA68031B9A423ED814444FF000099A +:1068E000D5E90032C8F81C4013605A60C5F8009022 +:1068F000D8F81030B34201D102F006F989F31188BB +:10690000D5E9033128469847302383F311886B6912 +:10691000002BD8D002F0E8F86A69A0EB040982469F +:106920004A450DD2022002F0C5FA0022D8F81030F4 +:10693000B34208D151462846BDE8F047FFF728BFCB +:10694000121A2244F2E712EB09092946384638BFE9 +:106950004A46FFF7EBFEB5E7D8F81030B34208D04F +:106960001444C8F81C00211AA960BDE8F047FFF7DD +:10697000F3BEBDE8F08700BF983D0020883D0020B1 +:1069800038B502F0B1F8054AD2E90845031B1819D9 +:1069900045F10001C2E9080138BD00BF883D002073 +:1069A00010B560B9074804790368053C9B6818BFB7 +:1069B0000124984708B144F00404204610BD012486 +:1069C000FBE700BFC0360020FFF7EABF2DE9F04724 +:1069D000884617469A460446B0B90D4E3579052DBE +:1069E00005D003240DE0013D15F0FF050ED03268FF +:1069F000534639463046D2F814904246C8470028DC +:106A0000F1D12046BDE8F0870424FAE70124F8E735 +:106A1000C03600202DE9F047884617469A460446BE +:106A2000B0B90D4E3579052D05D003240DE0013D9B +:106A300015F0FF050ED03268534639463046D2F87D +:106A400018904246C8470028F1D12046BDE8F0879B +:106A50000424FAE70124F8E7C036002037B50C46D5 +:106A6000154670B951B101290BD107486946036831 +:106A70001B6A984710B9019B04462B60204603B05F +:106A800030BD0424FAE700BFC03600200020704764 +:106A9000FEE70000704700004FF0FF307047000035 +:106AA0004B6843608B688360CB68C3600B6943614C +:106AB0004B6903628B6943620B6803607047000097 +:106AC00008B53C4B40F2FF713B48D3F888200A439D +:106AD000C3F88820D3F8882022F4FF6222F007024E +:106AE000C3F88820D3F88820D3F8E0200A43C3F8FD +:106AF000E020D3F808210A43C3F808212F4AD3F82D +:106B000008311146FFF7CCFF00F5806002F11C014F +:106B1000FFF7C6FF00F5806002F13801FFF7C0FF04 +:106B200000F5806002F15401FFF7BAFF00F58060C4 +:106B300002F17001FFF7B4FF00F5806002F18C01F3 +:106B4000FFF7AEFF00F5806002F1A801FFF7A8FF94 +:106B500000F5806002F1C401FFF7A2FF00F580603C +:106B600002F1E001FFF79CFF00F5806002F1FC01FB +:106B7000FFF796FF02F58C7100F58060FFF790FF3C +:106B800001F07AFC0E4BD3F8902242F00102C3F8D8 +:106B90009022D3F8942242F00102C3F894220522F5 +:106BA000C3F898204FF06052C3F89C20054AC3F800 +:106BB000A02008BD00440258000002586C9700084D +:106BC00000ED00E01F00080308B501F04FFDFFF7DE +:106BD000D7FC104BD3F8DC2042F04002C3F8DC2095 +:106BE000D3F8042122F04002C3F80421D3F8043181 +:106BF000094B1A6842F008021A601A6842F004024F +:106C00001A6000F063FD00F039FBBDE8084000F0B9 +:106C1000B5B800BF00440258001802480120704770 +:106C2000002070477047000002290CD0032904D0CF +:106C30000129074818BF00207047032A05D80548D6 +:106C400000EBC2007047044870470020704700BF47 +:106C50006C9900084C2200202099000870B59AB069 +:106C600005460846144601A900F0C2F801A8FBF742 +:106C7000FBF8431C0022C6B25B001046C5E9003495 +:106C800023700323023404F8013C01ABD1B2023477 +:106C90008E4201D81AB070BD13F8011B013204F8FE +:106CA000010C04F8021CF1E708B5302383F31188C6 +:106CB0000348FFF78BF8002383F3118808BD00BF5A +:106CC0003840002090F8803003F01F02012A07D1DD +:106CD00090F881200B2A03D10023C0E91D3315E071 +:106CE00003F06003202B08D1B0F884302BB990F862 +:106CF0008120212A03D81F2A04D8FFF749B8222A65 +:106D0000EBD0FAE7034A426707228267C367012094 +:106D1000704700BF4322002007B5052917D8DFE8D8 +:106D200001F0191603191920302383F31188104A32 +:106D300001210190FFF7F2F8019802210D4AFFF7B7 +:106D4000EDF80D48FFF70EF8002383F3118803B028 +:106D50005DF804FB302383F311880748FEF7D8FF62 +:106D6000F2E7302383F311880348FEF7EFFFEBE7E8 +:106D7000C0980008E49800083840002038B50C4D51 +:106D80000C4C2A460C4904F10800FFF767FF05F197 +:106D9000CA0204F110000949FFF760FF05F5CA7245 +:106DA00004F118000649BDE83840FFF757BF00BF9F +:106DB000105900204C220020A0980008AA98000832 +:106DC000B598000870B5044608460D46FBF74CF828 +:106DD000C6B22046013403780BB9184670BD32465E +:106DE0002946FBF72DF80028F3D10120F6E7000033 +:106DF0002DE9F84F05460C46FBF736F82849C6B290 +:106E00002846FFF7DFFF08B1033EF6B225492846C2 +:106E1000FFF7D8FF08B11036F6B2632E0DD8DFF8B1 +:106E20008080DFF88090204FDFF884A0DFF884B006 +:106E30002E7846B92670BDE8F88F29462046BDE871 +:106E4000F84F02F0D5B8252E25D107224146284615 +:106E5000FAF7F6FF28B9DBF80030073544F8043BB1 +:106E6000E6E7082249462846FAF7EAFF98B9A21C45 +:106E70000E4B197802320909C95D02F8041C13F897 +:106E8000011B01F00F015345C95D02F8031CF0D14D +:106E900018340835CCE7013504F8016BC8E700BFAA +:106EA0008C990008B59800089999000800E8F11F2E +:106EB0000CE8F11F94990008BFF34F8F044B1A6937 +:106EC0005107FCD1D3F810215207F8D1704700BF09 +:106ED0000020005208B50D4B1B78ABB9FFF7ECFF53 +:106EE0000B4BDA68D10704D50A4A5A6002F188329E +:106EF0005A60D3F80C21D20706D5064AC3F80421FC +:106F000002F18832C3F8042108BD00BF6E5B002087 +:106F1000002000522301674508B5114B1B78F3B9D7 +:106F2000104B1A69510703D5DA6842F04002DA6063 +:106F3000D3F81021520705D5D3F80C2142F04002B6 +:106F4000C3F80C21FFF7B8FF064BDA6842F00102E4 +:106F5000DA60D3F80C2142F00102C3F80C2108BD1D +:106F60006E5B0020002000520F289ABF00F5806061 +:106F700040040020704700004FF4003070470000CC +:106F8000102070470F2808B50BD8FFF7EDFF00F56C +:106F900000330268013204D104308342F9D1012068 +:106FA00008BD0020FCE700000F2838B505463FD893 +:106FB000FFF782FF1F4CFFF78DFF4FF0FF330728CD +:106FC0006361C4F814311DD82361FFF775FF030214 +:106FD00043F02403E360E36843F08003E360236944 +:106FE0005A07FCD42846FFF767FFFFF7BDFF4FF4B1 +:106FF000003100F09BFA2846FFF78EFFBDE83840CD +:10700000FFF7C0BFC4F81031FFF756FFA0F1080327 +:107010001B0243F02403C4F80C31D4F80C3143F0C4 +:107020008003C4F80C31D4F810315B07FBD4D9E7E6 +:10703000002038BD002000522DE9F84F04460D46CF +:10704000104644EA0203DE0602D00020BDE8F88FB5 +:1070500020F01F00DFF8D4B0DFF8D4A0FFF73AFF2C +:1070600005EB0008454503D10120FFF755FFEDE78B +:1070700020222946204601F083FF10B92034203514 +:10708000F0E7234604F120021F68791CDDD10433A8 +:107090009342F9D104F178432048214EB3F5801F83 +:1070A000204B38BF184603F1F80332BFD946D1460A +:1070B0001E46FFF701FF0760A4EB050C336805F1DE +:1070C0001C0143F0020333602B1FD9F8007017F046 +:1070D0000507FAD153F8042F8B424CF80320F4D162 +:1070E000BFF34F8FFFF7E8FE4FF0FF3303603368C5 +:1070F00023F002033360BFF34F8F0B4BC3F85C42A6 +:10710000BFF34F8FBFF36F8F20222946204601F037 +:1071100037FF0028B2D03846A7E700BF142100523D +:107120000C2000521420005200ED00E0102000520C +:107130001021005210B5084C237828B11BB9FFF775 +:10714000C9FE0123237010BD002BFCD02070BDE8C8 +:107150001040FFF7E1BE00BF6E5B00202DE9F04F4D +:107160000D4685B0814658B111F00D0614BF20229E +:10717000082211F00803019304D0431E034269D092 +:10718000002435E0002E37D009F11F0121F01F093E +:107190004FF00108314F05F00403DFF8CCA005EAF9 +:1071A000080BBBF1000F32D07869C0072FD408F16B +:1071B00001080C37B8F1060FF3D19EB9284D4946A6 +:1071C000A819019201F0F6FC0446002839D12036B6 +:1071D000019AA02EF3D1494601F0ECFC04460028A8 +:1071E0002FD1019A49461F4801F0E4FC044660BBD8 +:1071F000204605B0BDE8F08F0029C9D101462846D8 +:10720000029201F0D7FC0446D8B9029AC0E713B144 +:1072100078694107CBD5AC0702D578698007C6D518 +:10722000019911B178690107C1D549460AEB4810A7 +:10723000CDE9022301F0BEFC0446DDE9022300286B +:10724000B5D04A460021204601E04A460021FAF71F +:1072500013FECDE70246002E96D199E7AC990008BF +:10726000B05B0020705B0020905B00200021FFF7E6 +:1072700075BF00000121FFF771BF000000231A460F +:1072800070B51A600433B3F5806FFAD1104D012444 +:10729000104E06EB441001342A6955F80C1F01F01A +:1072A00075FC062CF5D137254FF4C0542046FFF766 +:1072B000E1FF014628B122460748BDE8704001F0D1 +:1072C00065BCC4EBC404013D4FEAD404EED170BDEB +:1072D000AC990008905B0020705B00200421FFF750 +:1072E0003DBF00004843FFF7C1BF000008B101F0F7 +:1072F000D3BC7047B0F5805F10B5044607D8FFF7E0 +:10730000EDFF28B92046BDE81040FFF7AFBF0020D1 +:1073100010BD0000FFF7EABF70B5AAB140EA010353 +:1073200013F01F030FD1094C0144A5686D0706D562 +:107330002568A84203D366683544A94204D90333BB +:107340000C34122BF1D10022104670BDAC9900080C +:1073500008B501F0B7FD034AD2E90032C01842EB8C +:10736000010108BD505C0020434BD3E900232DE907 +:10737000F34113437CD0FFF7EBFF404A0023002783 +:10738000F9F75AF806460D463D4A0023F9F754F836 +:107390000023144630462946394AF9F74DF84FF490 +:1073A00061613C23ADF80170B4FBF1F5B4FBF3F679 +:1073B00001FB154103FB16464624B1FBF3F1314BAB +:1073C000F6B28DF8004098423CD84FF0640C4FF470 +:1073D000C87EA30704F26C7225D1B2FBFCF30CFB50 +:1073E000132313BBB2FBFEF30EFB1322B2FA82F39C +:1073F0005B0903F26D18621C8045D2B217D90FB138 +:107400008DF800400022204C4FF00C0C17460CFB6E +:107410000343D4B2013213F804C084450CD8A0EB66 +:107420000C000127F5E70023E3E70123E1E7A0EBE8 +:10743000080014460127CCE70FB18DF80140431C2A +:107440008DF802309DF80100431C9DF80000503873 +:10745000400640EA43509DF8023040EA034040EACB +:10746000560040EAC52040EA411002B0BDE8F08174 +:107470004FF40410F9E700BF505C002040420F00B9 +:107480008051010090230B00F49900080244074B3F +:10749000D2B210B5904200D110BD441C00B253F8D6 +:1074A000200041F8040BE0B2F4E700BF5040005860 +:1074B0000E4B30B51C6F240405D41C6F1C671C6F69 +:1074C00044F400441C670A4C02442368D2B243F4DB +:1074D00080732360074B904200D130BD441C51F8AB +:1074E000045B00B243F82050E0B2F4E700440258D5 +:1074F000004802585040005807B5012201A9002059 +:10750000FFF7C4FF019803B05DF804FB13B5044610 +:10751000FFF7F2FFA04205D0012201A9002001944B +:10752000FFF7C6FF02B010BD704700000144BFF373 +:107530004F8F064B884204D3BFF34F8FBFF36F8F3B +:107540007047C3F85C022030F4E700BF00ED00E0B4 +:107550000144BFF34F8F064B884204D3BFF34F8FD4 +:10756000BFF36F8F7047C3F870022030F4E700BF9D +:1075700000ED00E070B5054616460C4601201021CE +:10758000FFF7B0FE286046733CB1204636B1FFF7E6 +:10759000A5FE2B68186000B19C6070BDFFF76AFE05 +:1075A000F7E70000F8B50F461546044648B905F15F +:1075B0001F010126386821F01F01FFF7B7FF304691 +:1075C000F8BD427B29463868FFF7A6FE064600282C +:1075D000EDD13B686360A368AB4210D213B1206861 +:1075E000FFF784FE637B28462BB1FFF777FE206010 +:1075F00020B9A060E3E7FFF73DFEF8E7A56020684B +:1076000005F11F01012621F01F013860FFF78EFFF1 +:107610002673D4E770B50E4602F11F0104461546E5 +:1076200021F01F013046FFF793FF4CB12168B142B2 +:1076300006D1606810B12A46FAF7F8FB00232373DD +:1076400070BD0000F8B50F461446054648B904F170 +:107650001F010126386821F01F01FFF779FF30462E +:10766000F8BD427B21463868FFF756FE06460028E3 +:10767000EDD1AB68A34210D213B12868FFF736FEF4 +:107680006B7B20462BB1FFF729FE286020B9A8604C +:10769000E5E7FFF7EFFDF8E7AC60396819B122467E +:1076A0002868FAF7C3FB286804F11F01012621F0BE +:1076B0001F013860FFF74CFF2E73D0E720B103683D +:1076C0008B4204BF0023037370470000034B1A680A +:1076D0001AB9034AD2F8D0241A607047585C0020C7 +:1076E0000040025808B5FFF7F1FF024B1868C0F3DD +:1076F000806008BD585C00207047000070470000A3 +:10770000EFF30983054968334A6B22F001024A63AB +:1077100083F30988002383F31188704700EF00E0AA +:10772000302080F3118862B60D4B0E4AD96821F4DF +:10773000E0610904090C0A430B49DA60D3F8FC2024 +:1077400042F08072C3F8FC20084AC2F8B01F1168EA +:1077500041F0010111602022DA7783F8220070479E +:1077600000ED00E00003FA0555CEACC5001000E0C6 +:10777000302310B583F311880E4B5B6813F400635C +:1077800014D0F1EE103AEFF309844FF08073683CA7 +:10779000E361094BDB6B236684F30988FEF7BCFECB +:1077A00010B1064BA36110BD054BFBE783F31188B5 +:1077B000F9E700BF00ED00E000EF00E02F04000853 +:1077C0003204000870B5BFF34F8FBFF36F8F1A4AB2 +:1077D0000021C2F85012BFF34F8FBFF36F8F536970 +:1077E00043F400335361BFF34F8FBFF36F8FC2F881 +:1077F0008410BFF34F8FD2F8803043F6E074C3F3A8 +:10780000C900C3F34E335B0103EA0406014646EAAE +:1078100081750139C2F86052F9D2203B13F1200F73 +:10782000F2D1BFF34F8F536943F480335361BFF3F9 +:107830004F8FBFF36F8F70BD00ED00E0FEE70000DB +:10784000214B2248224A70B5904237D3214BC11EAA +:10785000DA1C121A22F003028B4238BF00220021E8 +:10786000FAF70AFB1C4A0023C2F88430BFF34F8F9B +:10787000D2F8803043F6E074C3F3C900C3F34E334B +:107880005B0103EA0406014646EA81750139C2F844 +:107890006C52F9D2203B13F1200FF2D1BFF34F8F7E +:1078A000BFF36F8FBFF34F8FBFF36F8F0023C2F80B +:1078B0005032BFF34F8FBFF36F8F70BD53F8041B6F +:1078C00040F8041BC0E700BFF49A0008D45D002014 +:1078D000D45D0020D45D002000ED00E0074BD3F81C +:1078E000D81021EA0001C3F8D810D3F8002122EA09 +:1078F0000002C3F80021D3F8003170470044025859 +:1079000070B5D0E9244300224FF0FF359E6804EBA8 +:1079100042135101D3F80009002805DAD3F8000911 +:1079200040F08040C3F80009D3F8000B002805DAC6 +:10793000D3F8000B40F08040C3F8000B013263180D +:107940009642C3F80859C3F8085BE0D24FF0011320 +:10795000C4F81C3870BD0000890141F020010161AC +:1079600003699B06FCD41220FEF726BE10B50A4C14 +:107970002046FEF7B1FA094BC4F89030084BC4F822 +:107980009430084C2046FEF7A7FA074BC4F8903015 +:10799000064BC4F8943010BD5C5C00200000084029 +:1079A000309A0008F85C0020000004403C9A00086F +:1079B00070B503780546012B5CD1434BD0F890405D +:1079C000984258D1414B0E216520D3F8D82042F07F +:1079D0000062C3F8D820D3F8002142F00062C3F857 +:1079E0000021D3F80021D3F8802042F00062C3F8D0 +:1079F0008020D3F8802022F00062C3F88020D3F8E2 +:107A00008030FDF7ADFC324BE360324BC4F80038F8 +:107A10000023D5F89060C4F8003EC02323604FF4E3 +:107A20000413A3633369002BFCDA01230C203361B8 +:107A3000FEF7C2FD3369DB07FCD41220FEF7BCFD64 +:107A40003369002BFCDA00262846A660FFF758FFB2 +:107A50006B68C4F81068DB68C4F81468C4F81C6864 +:107A600083BB1D4BA3614FF0FF336361A36843F0F9 +:107A70000103A36070BD194B9842C9D1134B4FF05D +:107A80008060D3F8D82042F00072C3F8D820D3F831 +:107A9000002142F00072C3F80021D3F80021D3F88E +:107AA000802042F00072C3F88020D3F8802022F0BA +:107AB0000072C3F88020D3F88030FFF70FFF0E214B +:107AC0004D209EE7064BCDE75C5C00200044025849 +:107AD0004014004003002002003C30C0F85C00204D +:107AE000083C30C0F8B5D0F89040054600214FF072 +:107AF00000662046FFF730FFD5F8941000234FF0C2 +:107B000001128F684FF0FF30C4F83438C4F81C28D5 +:107B100004EB431201339F42C2F80069C2F8006BC4 +:107B2000C2F80809C2F8080BF2D20B68D5F8902009 +:107B3000C5F89830636210231361166916F01006B9 +:107B4000FBD11220FEF738FDD4F8003823F4FE6391 +:107B5000C4F80038A36943F4402343F01003A36141 +:107B60000923C4F81038C4F814380B4BEB604FF0FD +:107B7000C043C4F8103B094BC4F8003BC4F810697B +:107B8000C4F80039D5F8983003F1100243F480139B +:107B9000C5F89820A362F8BD0C9A00084080001038 +:107BA000D0F8902090F88A10D2F8003823F4FE63C1 +:107BB00043EA0113C2F80038704700002DE9F8438A +:107BC00000EB8103D0F890500C468046DA680FFA3B +:107BD00081F94801166806F00306731E022B05EBB7 +:107BE00041134FF0000194BFB604384EC3F8101B88 +:107BF0004FF0010104F1100398BF06F1805601FA1D +:107C000003F3916998BF06F5004600293AD0578AD8 +:107C100004F15801374349016F50D5F81C180B4344 +:107C20000021C5F81C382B180127C3F81019A740EC +:107C30005369611E9BB3138A928B9B08012A88BFEC +:107C40005343D8F89820981842EA034301F14002C0 +:107C50002146C8F89800284605EB82025360FFF7DA +:107C60007BFE08EB8900C3681B8A43EA84534834CF +:107C70001E4364012E51D5F81C381F43C5F81C78EB +:107C8000BDE8F88305EB4917D7F8001B21F4004144 +:107C9000C7F8001BD5F81C1821EA0303C0E704F15C +:107CA0003F030B4A2846214605EB83035A60FFF742 +:107CB00053FE05EB4910D0F8003923F40043C0F817 +:107CC0000039D5F81C3823EA0707D7E700800010F1 +:107CD00000040002D0F894201268C0F89820FFF742 +:107CE0000FBE00005831D0F8903049015B5813F4B2 +:107CF000004004D013F4001F0CBF02200120704785 +:107D00004831D0F8903049015B5813F4004004D05A +:107D100013F4001F0CBF02200120704700EB81010B +:107D2000CB68196A0B6813604B685360704700009A +:107D300000EB810330B5DD68AA691368D36019B917 +:107D4000402B84BF402313606B8A1468D0F89020C6 +:107D50001C4402EB4110013C09B2B4FBF3F4634351 +:107D6000033323F0030343EAC44343F0C043C0F8A2 +:107D7000103B2B6803F00303012B0ED1D2F8083817 +:107D800002EB411013F4807FD0F8003B14BF43F0A6 +:107D9000805343F00053C0F8003B02EB4112D2F88D +:107DA000003B43F00443C2F8003B30BD2DE9F041F5 +:107DB000D0F8906005460C4606EB4113D3F8087BDB +:107DC0003A07C3F8087B08D5D6F814381B0704D542 +:107DD00000EB8103DB685B689847FA071FD5D6F88C +:107DE0001438DB071BD505EB8403D968CCB98B6944 +:107DF000488A5A68B2FBF0F600FB16228AB9186866 +:107E0000DA6890420DD2121AC3E90024302383F3BA +:107E1000118821462846FFF78BFF84F31188BDE8BF +:107E2000F081012303FA04F26B8923EA02036B81D8 +:107E3000CB68002BF3D021462846BDE8F041184717 +:107E400000EB81034A0170B5DD68D0F890306C69B1 +:107E50002668E66056BB1A444FF40020C2F81009A9 +:107E60002A6802F00302012A0AB20ED1D3F80808E8 +:107E700003EB421410F4807FD4F8000914BF40F0E3 +:107E8000805040F00050C4F8000903EB4212D2F8D1 +:107E9000000940F00440C2F800090122D3F8340878 +:107EA00002FA01F10143C3F8341870BD19B9402E2C +:107EB00084BF4020206020681A442E8A8419013C27 +:107EC000B4FBF6F440EAC44040F00050C6E70000BE +:107ED0002DE9F843D0F8906005460C464F0106EBBB +:107EE0004113D3F8088918F0010FC3F808891CD092 +:107EF000D6F81038DB0718D500EB8103D3F80CC097 +:107F0000DCF81430D3F800E0DA68964530D2A2EB02 +:107F10000E024FF000091A60C3F80490302383F377 +:107F20001188FFF78DFF89F3118818F0800F1DD09D +:107F3000D6F834380126A640334217D005EB840327 +:107F40000134D5F89050D3F80CC0E4B22F44DCF8DB +:107F5000142005EB0434D2F800E05168714514D3C5 +:107F6000D5F8343823EA0606C5F83468BDE8F88346 +:107F7000012303FA01F2038923EA02030381DCF8F7 +:107F80000830002BD1D09847CFE7AEEB0103BCF807 +:107F90001000834228BF0346D7F8180980B2B3EB1C +:107FA000800FE3D89068A0F1040959F8048FC4F851 +:107FB0000080A0EB09089844B8F1040FF5D81844E4 +:107FC0000B4490605360C8E72DE9F84FD0F890500B +:107FD00004466E69AB691E4016F480586E6103D08A +:107FE000BDE8F84FFDF7E8BF002E12DAD5F8003EE5 +:107FF0009B0705D0D5F8003E23F00303C5F8003EEB +:10800000D5F80438204623F00103C5F80438FEF7FC +:1080100001F8370505D52046FFF772FC2046FDF72D +:10802000E7FFB0040CD5D5F8083813F0060FEB685D +:1080300023F470530CBF43F4105343F4A053EB608C +:1080400031071BD56368DB681BB9AB6923F00803F4 +:10805000AB612378052B0CD1D5F8003E9A0705D0EB +:10806000D5F8003E23F00303C5F8003E2046FDF797 +:10807000D1FF6368DB680BB120469847F30200F13B +:10808000BA80B70226D5D4F8909000274FF0010AA5 +:1080900009EB4712D2F8003B03F44023B3F5802FDD +:1080A00011D1D2F8003B002B0DDA62890AFA07F3EE +:1080B00022EA0303638104EB8703DB68DB6813B107 +:1080C0003946204698470137D4F89430FFB29B6870 +:1080D0009F42DDD9F00619D5D4F89000026AC2F3A8 +:1080E0000A1702F00F0302F4F012B2F5802F00F02D +:1080F000CA80B2F5402F09D104EB8303002200F5BA +:108100008050DB681B6A974240F0B0803003D5F89E +:10811000185835D5E90303D500212046FFF746FE60 +:10812000AA0303D501212046FFF740FE6B0303D5C8 +:1081300002212046FFF73AFE2F0303D503212046F4 +:10814000FFF734FEE80203D504212046FFF72EFE98 +:10815000A90203D505212046FFF728FE6A0203D5B0 +:1081600006212046FFF722FE2B0203D507212046D9 +:10817000FFF71CFEEF0103D508212046FFF716FE8E +:10818000700340F1A780E90703D500212046FFF7DF +:108190009FFEAA0703D501212046FFF799FE6B0732 +:1081A00003D502212046FFF793FE2F0703D50321B5 +:1081B0002046FFF78DFEEE0603D504212046FFF78B +:1081C00087FEA80603D505212046FFF781FE690634 +:1081D00003D506212046FFF77BFE2A0603D507219B +:1081E0002046FFF775FEEB0574D520460821BDE853 +:1081F000F84FFFF76DBED4F890904FF0000B4FF0A2 +:10820000010AD4F894305FFA8BF79B689F423FF6DF +:1082100038AF09EB4713D3F8002902F44022B2F536 +:10822000802F20D1D3F80029002A1CDAD3F80029A6 +:1082300042F09042C3F80029D3F80029002AFBDB62 +:108240003946D4F89000FFF787FB22890AFA07F332 +:1082500022EA0303238104EB8703DB689B6813B1E5 +:108260003946204698470BF1010BCAE7910701D127 +:10827000D0F80080072A02F101029CBF03F8018BAD +:108280004FEA18283FE704EB830300F58050DA68D3 +:10829000D2F818C0DCF80820DCE9001CA1EB0C0CBB +:1082A00000218F4208D1DB689B699A683A449A6042 +:1082B0005A683A445A6029E711F0030F01D1D0F807 +:1082C00000808C4501F1010184BF02F8018B4FEA67 +:1082D0001828E6E7BDE8F88F08B50348FFF774FEF5 +:1082E000BDE80840FFF744BA5C5C002008B50348CD +:1082F000FFF76AFEBDE80840FFF73ABAF85C0020D5 +:10830000D0F8903003EB4111D1F8003B43F4001357 +:10831000C1F8003B70470000D0F8903003EB4111EA +:10832000D1F8003943F40013C1F800397047000058 +:10833000D0F8903003EB4111D1F8003B23F4001347 +:10834000C1F8003B70470000D0F8903003EB4111BA +:10835000D1F8003923F40013C1F800397047000048 +:10836000064BD3F8DC200243C3F8DC20D3F8042109 +:108370001043C3F80401D3F8043170470044025895 +:1083800008B53C4B4FF0FF31D3F8802062F000423B +:10839000C3F88020D3F8802002F00042C3F8802088 +:1083A000D3F88020D3F88420C3F88410D3F8842035 +:1083B0000022C3F88420D3F88400D86F40F0FF4037 +:1083C00040F4FF0040F4DF4040F07F00D867D86FF2 +:1083D00020F0FF4020F4FF0020F4DF4020F07F0079 +:1083E000D867D86FD3F888006FEA40506FEA5050D2 +:1083F000C3F88800D3F88800C0F30A00C3F88800E7 +:10840000D3F88800D3F89000C3F89010D3F8900008 +:10841000C3F89020D3F89000D3F89400C3F89410D8 +:10842000D3F89400C3F89420D3F89400D3F89800BC +:10843000C3F89810D3F89800C3F89820D3F89800A0 +:10844000D3F88C00C3F88C10D3F88C00C3F88C20C0 +:10845000D3F88C00D3F89C00C3F89C10D3F89C1080 +:10846000C3F89C20D3F89C30FCF788FABDE808409C +:1084700000F0E8B80044025808B50122534BC3F895 +:108480000821534BD3F8F42042F00202C3F8F42041 +:10849000D3F81C2142F00202C3F81C210222D3F8B7 +:1084A0001C314C4BDA605A689104FCD54A4A1A6078 +:1084B00001229A60494ADA6000221A614FF4404270 +:1084C0009A61444B9A699204FCD51A6842F480720E +:1084D0001A603F4B1A6F12F4407F04D04FF4803281 +:1084E0001A6700221A671A6842F001021A60384BB4 +:1084F0001A685007FCD500221A611A6912F0380276 +:10850000FBD1012119604FF0804159605A67344A0C +:10851000DA62344A1A611A6842F480321A602C4BCB +:108520001A689103FCD51A6842F480521A601A68DE +:108530009204FCD52C4A2D499A6200225A63196391 +:1085400001F57C01DA6301F2E71199635A64284A64 +:108550001A64284ADA621A6842F0A8521A601C4B60 +:108560001A6802F02852B2F1285FF9D148229A61C4 +:108570004FF48862DA6140221A621F4ADA641F4AA5 +:108580001A651F4A5A651F4A9A6532231E4A1360AC +:10859000136803F00F03022BFAD10D4A136943F05D +:1085A00003031361136903F03803182BFAD14FF05A +:1085B0000050FFF7D5FE4FF08040FFF7D1FE4FF09F +:1085C0000040BDE80840FFF7CBBE00BF008000516F +:1085D000004402580048025800C000F002000001A8 +:1085E0000000FF01008890081210200063020901BA +:1085F000470E0508DD0BBF01200000200000011020 +:108600000910E000000101100020005208B50348E5 +:10861000FCF786FDBDE80840FFF7AAB8C036002089 +:1086200008B50348FCF716FEBDE80840FFF7A0B800 +:108630001839002008B500F08DFCBDE80840FFF7B0 +:1086400097B8000008B509217A20FCF789FE0921B6 +:108650003120FCF785FE07213220FCF781FE0C213A +:108660004720BDE80840FCF77BBE000008B5FFF7D7 +:1086700087FE00F043FCFDF753FAFDF7F1F9FDF733 +:1086800029FCFDF7FBFAFEF7CDFABDE8084000F043 +:1086900029BA000030B50433039C0172002104FBA9 +:1086A0000325C160C0E90653049B0363059BC0E931 +:1086B0000000C0E90422C0E90842C0E90A1143638E +:1086C00030BD00000022416AC260C0E90411C0E967 +:1086D0000A226FF00101FEF71BB80000D0E9043256 +:1086E000934201D1C2680AB9181D70470020704733 +:1086F000036919600021C2680132C260C269134473 +:1087000082699342036124BF436A0361FDF7F4BFAA +:1087100038B504460D46E3683BB162690020131D7D +:108720001268A3621344E36207E0237A33B929464F +:108730002046FDF7D1FF0028EDDA38BD6FF00100CB +:10874000FBE70000C368C269013BC360436913448F +:1087500082699342436124BF436A43610023836279 +:10876000036B03B11847704770B53023044683F399 +:108770001188866A3EB9FFF7CBFF054618B186F32C +:108780001188284670BDA36AE26A13F8015B934220 +:10879000A36202D32046FFF7D5FF002383F311889D +:1087A000EFE700002DE9F84F04460E4617469846BD +:1087B0004FF0300989F311880025AA46D4F828B073 +:1087C000BBF1000F09D141462046FFF7A1FF20B1C0 +:1087D0008BF311882846BDE8F88FD4E90A12A7EB7D +:1087E000050B521A934528BF9346BBF1400F1BD986 +:1087F000334601F1400251F8040B914243F8040B57 +:10880000F9D1A36A403640354033A362D4E90A2344 +:108810009A4202D32046FFF795FF8AF31188BD42A2 +:10882000D8D289F31188C9E730465A46F9F7FEFADB +:10883000A36A5E445D445B44A362E7E710B5029C13 +:108840000433017203FB0421C460C0E90613002352 +:10885000C0E90A33039B0363049BC0E90000C0E93D +:108860000422C0E90842436310BD0000026A6FF0B1 +:108870000101C260426AC0E904220022C0E90A2262 +:10888000FDF746BFD0E904239A4201D1C26822B95C +:10889000184650F8043B0B60704700231846FAE76F +:1088A000C3680021C2690133C3604369134482690C +:1088B0009342436124BF436A4361FDF71DBF00003B +:1088C00038B504460D46E3683BB1236900201A1D04 +:1088D000A262E2691344E36207E0237A33B92946CE +:1088E0002046FDF7F9FE0028EDDA38BD6FF00100F3 +:1088F000FBE7000003691960C268013AC260C269FF +:10890000134482699342036124BF436A03610023D5 +:108910008362036B03B118477047000070B53023C2 +:108920000D460446114683F31188866A2EB9FFF777 +:10893000C7FF10B186F3118870BDA36A1D70A36ACA +:10894000E26A01339342A36204D3E1692046043909 +:10895000FFF7D0FF002080F31188EDE72DE9F84FF5 +:1089600004460D46904699464FF0300A8AF3118826 +:108970000026B346A76A4FB949462046FFF7A0FF35 +:1089800020B187F311883046BDE8F88FD4E90A0793 +:108990003A1AA8EB0607974228BF1746402F1BD963 +:1089A00005F1400355F8042B9D4240F8042BF9D102 +:1089B000A36A40364033A362D4E90A239A4204D31F +:1089C000E16920460439FFF795FF8BF3118846458E +:1089D000D9D28AF31188CDE729463A46F9F726FA23 +:1089E000A36A3D443E443B44A362E5E7D0E9042347 +:1089F0009A4217D1C3689BB1836A8BB1043B9B1A1F +:108A00000ED01360C368013BC360C3691A44836915 +:108A10009A42026124BF436A0361002383620123F7 +:108A2000184670470023FBE701F01F03F0B502F082 +:108A30001F0456095A1C0123B6EB511F50F826504B +:108A400003FA02F34FEA511703F1FF333DBF50F829 +:108A50002720C4F12000134003EA05003BBF03FABE +:108A600000F225FA04F0E0401043F0BD70B57E221C +:108A70007F210546FFF7D8FF18B1012819D0002043 +:108A800070BD3E2249212846FFF7CEFF2F22044623 +:108A900031212846FFF7C8FF06460134502202362E +:108AA00053212846B440FFF7BFFF093804FA00F00D +:108AB000E6E7302245212846FFF7B6FF0130800265 +:108AC000DEE7000090F8D63090F8D7201B0403EBC7 +:108AD000026390F8D42090F8D500134403EB0020F3 +:108AE0007047000000F00CBA014B586A704700BF95 +:108AF000000C0040034B002258631A610222DA6026 +:108B0000704700BF000C0040014B0022DA60704744 +:108B1000000C0040014B5863704700BF000C004040 +:108B2000024B034A1A60034A5A607047AC5D00204A +:108B3000D85D002000000220074B494210B55C6858 +:108B4000201A08401968821A8A4203D3A24201D827 +:108B50005A6010BD0020FCE7AC5D002008B5302352 +:108B600083F31188FFF7E8FF002383F3118808BD22 +:108B70000448054B03600023C0E901330C3000F0CA +:108B800017B900BFB45D00205D8B0008CB1D083A0B +:108B900023F00703591A521A10B4D2080024C0E96E +:108BA000004384600C301C605A605DF8044B00F098 +:108BB000FFB800002DE9F74F364FCD1D884600283D +:108BC00018BF0746082A4FEAD50538BF082207F123 +:108BD0000C003C1D9146019000F02CF9019809F120 +:108BE0000701C9F1000E2246246864B900F02CF98F +:108BF0003B68CBB308224946E8009847044698B33F +:108C000040E9027830E004EB010CD4F804A00CEA4F +:108C10000E0C0AF10106ACF1080304EBC6069E42F5 +:108C2000E1D9A6EB0C0CB5EBEC0F4FEAEC0BDAD864 +:108C30009C421DD204F10802AB45A3EB02024FEAAD +:108C4000E202626009D9691CED4303EBC1025D4495 +:108C50005560256843F8315022601C46C3F80480F3 +:108C600044F8087B00F0F0F8204603B0BDE8F08F30 +:108C7000AA45216802D111602346EEE7013504EBD5 +:108C8000C50344F8351003F10801761AF6105E604A +:108C90001360F1E7B45D002073B50446A0F1080548 +:108CA00050F8080C54F8043C061D0C3007330190B2 +:108CB000DB0844F8043C00F0BDF8334601989E42BE +:108CC0001A6801D0AB4228D20AB1954225D244F8A5 +:108CD000082C54F8042C1D60013254F8081C05EBD4 +:108CE000C206B14206D14E68324444F8042C0A68E8 +:108CF00044F8082C5E68711C03EBC1018D4207D15A +:108D000054F8042C013232445A6054F8082C1A608A +:108D100002B0BDE8704000F097B81346CFE70000FE +:108D2000FEE7000070B51E4B0025044686B0586073 +:108D30000E4605638163FEF7F3FC04F12803A5608A +:108D40006563C4E90A3304F11003C4E904334FF046 +:108D5000FF33C4E90044C4E90635FFF7C5FE2B46DE +:108D6000024604F13C012046C4E9082380230D4A51 +:108D70006567FDF72BFC7368E0600B4A0362012313 +:108D8000009280F824306846F26801923269CDE999 +:108D90000223064BCDE90435FDF74CFC06B070BD4F +:108DA00030400020509A0008489A0008218D0008A1 +:108DB0000023C0E9000083600361704770B51C4B5D +:108DC00005468468DE685CB3B44213D1036901339D +:108DD000036170BDA36094F8243083B1062B15D1D4 +:108DE000A06A2146D4E9003213605A60FDF74AFBBD +:108DF000A36A9C68B368A2689A42EBD306E0D4E900 +:108E00000032204613605A60FDF74CFB284631467D +:108E1000FDF738FBB5620620BDE87040FDF744BBA6 +:108E20000369866001330361336BC3603063D0E74D +:108E3000883D002008B5302383F31188FFF7BEFF7B +:108E4000002383F3118808BD194BD96883688B42CE +:108E500010B520D1302383F311880269013A0261F1 +:108E6000B2B90468C368A0420B631ED04A6B9BB9B9 +:108E700001238A60036103681A68026050601A6BFC +:108E80008360C26018631846FDF70CFBFDF76CFBAE +:108E9000002383F3118810BD1C68A34203D0A4688B +:108EA000A24238BF2246DB68E1E78260F0E700BFFC +:108EB000883D0020024A536B18435063704700BF3F +:108EC000883D002038B5EFF311859DB9EFF3058497 +:108ED000C4F30804302334B183F31188FDF750FD47 +:108EE00085F3118838BD83F31188FDF749FD84F3BC +:108EF000118838BDBDE83840FDF742BD704700001D +:108F0000114BD3F8E82042F00802C3F8E820D3F868 +:108F1000102142F00802C3F810210C4AD3F8103196 +:108F2000D36B43F00803D363C722094B9A624FF017 +:108F3000FF32DA6200229A615A63DA605A600122D3 +:108F40005A611A60704700BF004402580010005C6C +:108F5000000C0040094A08B51169D3680B40D9B22A +:108F60009B076FEA0101116107D5302383F3118854 +:108F7000FDF720FB002383F3118808BD000C00409F +:108F800010B501390244904201D1002005E0037878 +:108F900011F8014FA34201D0181B10BD0130F2E7B8 +:108FA000884210B501EB020402D98442234607D857 +:108FB000431EA14208D011F8012B03F8012FF8E756 +:108FC000024401468A4200D110BD13F8014D02F857 +:108FD000014DF7E7C9B2034610F8012B1AB18A42D6 +:108FE000F9D118467047002918BF0023F9E700009F +:108FF000034611F8012B03F8012B002AF9D1704721 +:1090000010B50139034632B111F8014F03F8014B95 +:10901000013A002CF7D11A440021934200D110BD2F +:1090200003F8011BF9E70000613036643765353815 +:109030003232303734656131000000001210000018 +:1090400012110000121300004D4435002D2D0A00AE +:109050002F6172647570696C6F742E6162696E0045 +:109060002F6172647570696C6F742D7665726966B4 +:10907000792E6162696E002F6172647570696C6F20 +:10908000742D666C6173682E6162696E002F617267 +:10909000647570696C6F742D666C61736865642E9D +:1090A0006162696E00000000000000000000000026 +:1090B000150F0008B10F000861110008E90F000842 +:1090C000A90F00080000000000000000110F0008B8 +:1090D000BD0F0008991100080D0F0008190F0008B6 +:1090E00053544D333248373F3F3F0053544D333292 +:1090F000483733782F3732780053544D333248375E +:1091000034332F3735332F373530000001105A00F4 +:109110000310590001205800032056002F000000C2 +:109120005375636365737366756C6C79206D6F75C9 +:109130006E746564205344436172642028736C6FBD +:1091400077646F776E3D2575290A0000EB769045B0 +:1091500058464154202020004641543332202020DC +:10916000000000002A3A3C3E7C223F7F002B2C3B33 +:109170003D5B5D00435545414141414345454549BE +:10918000494941414592924F4F4F5555594F554F7F +:109190009C4F9E9F41494F55A5A5A6A7A8A9AAAB3C +:1091A000ACADAEAFB0B1B2B3B4414141B8B9BABBE6 +:1091B000BCBDBEBFC0C1C2C3C4C54141C8C9CACB82 +:1091C000CCCDCECFD1D145454549494949D9DADB46 +:1091D000DCDD49DF4FE14F4F4F4FE6E8E85555558D +:1091E0005959EEEFF0F1F2F3F4F5F6F7F8F9FAFB6E +:1091F000FCFDFEFF01030507090E10121416181CD2 +:109200001E00000061001A03E0001703F8000703C6 +:10921000FF0001007801000130013201060139012F +:1092200010014A012E017901060180014D0043021F +:109230008101820182018401840186018701870105 +:1092400089018A018B018B018D018E018F019001B3 +:109250009101910193019401F60196019701980102 +:1092600098013D029B019C019D0120029F01A001EC +:10927000A001A201A201A401A401A601A701A701C6 +:10928000A901AA01AB01AC01AC01AE01AF01AF0174 +:10929000B101B201B301B301B501B501B701B80124 +:1092A000B801BA01BB01BC01BC01BE01F701C0019C +:1092B000C101C201C301C401C501C401C701C80184 +:1092C000C701CA01CB01CA01CD011001DD010100B6 +:1092D0008E01DE011201F3010300F101F401F4013A +:1092E000F8012801220212013A020900652C3B0212 +:1092F0003B023D02662C3F0240024102410246020F +:109300000A015302400081018601550289018A0148 +:1093100058028F015A0290015C025D025E025F02F8 +:1093200093016102620294016402650266026702AF +:10933000970196016A02622C6C026D026E029C011A +:10934000700271029D01730274029F01760277021E +:10935000780279027A027B027C02642C7E027F0210 +:10936000A60181028202A901840285028602870287 +:10937000AE014402B101B20145028D028E028F029C +:1093800090029102B7017B030300FD03FE03FF037C +:10939000AC0304008603880389038A03B103110325 +:1093A000C2030200A303A303C4030803CC03030006 +:1093B0008C038E038F03D8031801F2030A00F9030C +:1093C000F303F403F503F603F703F703F903FA03D2 +:1093D000FA033004200350041007600422018A04B9 +:1093E0003601C1040E01CF040100C004D0044401C1 +:1093F00061052604000000007D1D0100632C001E95 +:109400009601A01E5A01001F0806101F0606201F05 +:109410000806301F0806401F0606511F0700591F87 +:10942000521F5B1F541F5D1F561F5F1F601F0806E2 +:10943000701F0E00BA1FBB1FC81FC91FCA1FCB1F3A +:10944000DA1FDB1FF81FF91FEA1FEB1FFA1FFB1FB4 +:10945000801F0806901F0806A01F0806B01F040002 +:10946000B81FB91FB21FBC1FCC1F0100C31FD01FE4 +:109470000206E01F0206E51F0100EC1FF31F0100BA +:10948000FC1F4E21010032217021100284210100B5 +:109490008321D0241A05302C2F04602C0201672C64 +:1094A0000601752C0201802C6401002D260841FF65 +:1094B0001A030000C700FC00E900E200E400E0003D +:1094C000E500E700EA00EB00E800EF00EE00EC004A +:1094D000C400C500C900E600C600F400F600F200B2 +:1094E000FB00F900FF00D600DC00F800A300D80064 +:1094F000D7009201E100ED00F300FA00F100D10085 +:10950000AA00BA00BF00AE00AC00BD00BC00A100C4 +:10951000AB00BB0091259225932502252425C1008F +:10952000C200C000A9006325512557255D25A20072 +:10953000A5001025142534252C251C2500253C25A7 +:10954000E300C3005A25542569256625602550256A +:109550006C25A400F000D000CA00CB00C800310187 +:10956000CD00CE00CF0018250C2588258425A60027 +:10957000CC008025D300DF00D400D200F500D50058 +:10958000B500FE00DE00DA00DB00D900FD00DD00E2 +:10959000AF00B400AD00B1001720BE00B600A700B8 +:1095A000F700B800B000A800B700B900B300B200DF +:1095B000A025A00010000240080002400008024060 +:1095C00000000B002800024008000240040802408E +:1095D00006010C004000024008000240080802405A +:1095E00010020D0058000240080002400C08024022 +:1095F00016030E00700002400C00024010080240EA +:1096000000040F00880002400C00024014080240D1 +:1096100006051000A00002400C000240180802409D +:1096200010061100B80002400C0002401C08024065 +:1096300016072F00100402400804024020080240D0 +:1096400000083800280402400804024024080240B0 +:10965000060939004004024008040240280802407C +:10966000100A3A0058040240080402402C08024044 +:10967000160B3B00700402400C040240300802400C +:10968000000C3C00880402400C04024034080240F4 +:10969000060D4400A00402400C04024038080240B9 +:1096A000100E4500B80402400C0402403C08024081 +:1096B000160F4600010000000000000000960000A8 +:1096C000000000000000000000000000000000009A +:1096D00000000000000000001D6C0008216C000864 +:1096E000F55600082D5A000889560008B1560008A2 +:1096F000D95600087156000800000000E15A000821 +:10970000CD5A0008095B0008F55A0008015B000803 +:10971000ED5A0008D95A0008C55A0008155B000820 +:1097200000000000F95B0008E55B0008215C000810 +:109730000D5C0008195C0008055C0008F15B00087E +:10974000DD5B00082D5C0008000000000100000047 +:109750000000000063300000549700080000000083 +:1097600000000000003E0020304000200001806A20 +:1097700000000000AAAAAAAA00010064FFFF0000DE +:109780000000000000A00A0010000001000000001E +:10979000AAAAAAAA00000001FBFF00000000000026 +:1097A0000000000000A0AA0200000000AAAAAAAAC5 +:1097B00000505500FFFF000000000077CCCC0C00EB +:1097C0002000000000000000AAAAAAAA10000000C1 +:1097D000FFFF0000000C000000000000000000007F +:1097E00000000000AAAAAAAA00000000FFFF0000D3 +:1097F0000000000000000000000000000000000069 +:10980000AAAAAAAA00000000FFFF000000000000B2 +:10981000000000000000000000000000AAAAAAAAA0 +:1098200000000000FFFF000000000000000000003A +:109830000000000000000000AAAAAAAA0000000080 +:10984000FFFF00000000000000000000000000001A +:1098500000000000AAAAAAAA00000000FFFF000062 +:1098600000000000000000000000000000000000F8 +:10987000AAAAAAAA00000000FFFF00000000000042 +:10988000000000000000000000000000AAAAAAAA30 +:1098900000000000FFFF00000000000000000000CA +:1098A0004172647550696C6F740025424F41524497 +:1098B000252D424C002553455249414C25000000BE +:1098C0000200000000000000195E0008895E000828 +:1098D00040004000E0580020F05800200200000046 +:1098E000000000000300000000000000D15E00083E +:1098F00000000000100000000059002000000000DF +:1099000001000000000000005C5C0020010102007A +:10991000196D0008296C0008C56C0008A96C0008C6 +:10992000430000002899000809024300020100C01A +:1099300032090400000102020100052400100105A3 +:109940002401000104240202052406000107058207 +:10995000030800FF09040100020A000000070501D6 +:1099600002400000070581024000000012000000D4 +:1099700074990008120110010200004009124157B9 +:1099800000020102030100000403090425424F41C3 +:109990005244250048574837003031323334353689 +:1099A000373839414243444546000000000000205A +:1099B000000002000200000000000030000004006F +:1099C000080000000000002400000800040000005F +:1099D0000004000000FC0000020000000000043051 +:1099E00000800000080000000000003800000100B6 +:1099F000010000001F1C1F1E1F1E1F1F1E1F1E1FF9 +:109A00001F1D1F1E1F1E1F1F1E1F1E1F00000000E8 +:109A10002D600008E56200089163000840004000E6 +:109A2000945D0020945D002001000000A45D0020F2 +:109A3000800000004001000008000000000100005C +:109A4000001000000800000069646C650000000060 +:109A50006D61696E002C0438040438080C10141C65 +:109A600020242526000000000000640401000400FA +:109A700000000000000C001028303400EC69FF7F6B +:109A800001000000FFFFFFFFC7040000000000000E +:109A900000001A0000000000FF0000003840002015 +:109AA0001839002000000000E09000088304000046 +:109AB000EB90000850040000F9900008010000003D +:109AC0000000000000960000000008009600000062 +:109AD0000008000004000000889900080000000051 +:109AE0000000000000000000000000000000000076 +:049AF0000000000072 +:00000001FF From 2c93d89f2f32325be27c242c13ba0de628d797bf Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 12 Apr 2025 09:19:01 +1000 Subject: [PATCH 12/99] AC_Fence: added FENCE_ALT_TYPE this allows for fence altitudes to be above ground level, absolute, origin or above home (the default and current behaviour) --- libraries/AC_Fence/AC_Fence.cpp | 93 +++++++++++++++++++++++++++++---- libraries/AC_Fence/AC_Fence.h | 4 ++ 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/libraries/AC_Fence/AC_Fence.cpp b/libraries/AC_Fence/AC_Fence.cpp index 0915a502d9d024..513bf46e0cac71 100644 --- a/libraries/AC_Fence/AC_Fence.cpp +++ b/libraries/AC_Fence/AC_Fence.cpp @@ -14,6 +14,7 @@ #include #include #include +#include extern const AP_HAL::HAL& hal; @@ -75,7 +76,7 @@ const AP_Param::GroupInfo AC_Fence::var_info[] = { // @Param{Copter, Plane, Sub}: ALT_MAX // @DisplayName: Fence Maximum Altitude - // @Description: Maximum altitude allowed before geofence triggers + // @Description: Maximum altitude allowed before geofence triggers. See FENCE_ALT_TYPE for reference frame. // @Units: m // @Range: 10 1000 // @Increment: 1 @@ -107,7 +108,7 @@ const AP_Param::GroupInfo AC_Fence::var_info[] = { // @Param{Copter, Plane, Sub}: ALT_MIN // @DisplayName: Fence Minimum Altitude - // @Description: Minimum altitude allowed before geofence triggers + // @Description: Minimum altitude allowed before geofence triggers. See FENCE_ALT_TYPE for reference frame. // @Units: m // @Range: -100 100 // @Increment: 1 @@ -164,6 +165,13 @@ const AP_Param::GroupInfo AC_Fence::var_info[] = { // @User: Standard AP_GROUPINFO("MARGIN_XY", 13, AC_Fence, _margin_ne_m, 0), + // @Param: ALT_FRAME + // @DisplayName: Altitude frame + // @Description: The altitude reference frame for all altitude fences. Can be AMSL, home-relative, origin relative or AGL. Applies to both FENCE_ALT_MAX and FENCE_ALT_MIN. + // @User: Advanced + // @Values: 0:Above sea level, 1:Above Home, 2: Above Origin, 3: Above Terrain + AP_GROUPINFO_FRAME("ALT_TYPE", 12, AC_Fence, _alt_type, float(Location::AltFrame::ABOVE_HOME), AP_PARAM_FRAME_COPTER | AP_PARAM_FRAME_SUB | AP_PARAM_FRAME_TRICOPTER | AP_PARAM_FRAME_HELI | AP_PARAM_FRAME_PLANE), + AP_GROUPEND }; @@ -550,6 +558,63 @@ bool AC_Fence::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) c return true; } +/* + get our altitude in the FENCE_ALT_TYPE frame + return false if not available + */ +bool AC_Fence::get_alt_U_in_frame(float &alt) const +{ + const auto &ahrs = AP::ahrs(); + + ahrs.get_relative_position_D_home(alt); + + // translate to +ve up + alt = -alt; + + /* + convert to desired altitude frame + */ + const auto alt_frame = (Location::AltFrame)_alt_type.get(); + switch (alt_frame) { + case Location::AltFrame::ABOVE_HOME: + break; + case Location::AltFrame::ABOVE_ORIGIN: + // try and get it relative origin + if (!ahrs.get_relative_position_D_origin_float(alt)) { + // treat as a breach if origin not known + return false; + } + alt = -alt; + break; + case Location::AltFrame::ABSOLUTE: { + float home_alt; + if (!ahrs.get_home().get_alt_m(Location::AltFrame::ABSOLUTE, home_alt)) { + // treat not known home alt as a breach + return false; + } + alt += home_alt; + break; + } + case Location::AltFrame::ABOVE_TERRAIN: { +#if AP_TERRAIN_AVAILABLE + // get height above ground level with extrapolation. We extrapolate to prevent + // a fence breach due to a momentary cache miss in the terrain library + auto *terrain = AP::terrain(); + if (terrain == nullptr || !terrain->height_above_terrain(alt, true)) { + // treat not known home alt as a breach + return false; + } + break; +#else + // no terrain available + return false; +#endif + // note that unknown frames are treated as ABOVE_HOME + } + } + return true; +} + /// returns true if we have freshly breached the maximum altitude /// fence; also may set up a fallback fence which, if breached, will /// cause the altitude fence to be freshly breached @@ -561,9 +626,11 @@ bool AC_Fence::check_fence_alt_max() return false; } - float curr_alt_d_m; - AP::ahrs().get_relative_position_D_home(curr_alt_d_m); - const float _curr_alt_u_m = -curr_alt_d_m; // translate Down to Up + float _curr_alt_u_m; + if (!get_alt_U_in_frame(_curr_alt_u_m)) { + // if we can't get the alt then it is a breach + return true; + } // record distance above/below breach _alt_max_breach_distance_m = _curr_alt_u_m - _alt_max_m; @@ -612,9 +679,11 @@ bool AC_Fence::check_fence_alt_min() return false; } - float curr_alt_d_m; - AP::ahrs().get_relative_position_D_home(curr_alt_d_m); - const float _curr_alt_u_m = -curr_alt_d_m; // translate Down to Up + float _curr_alt_u_m; + if (!get_alt_U_in_frame(_curr_alt_u_m)) { + // if we can't get the alt then it is a breach + return true; + } // record distance above/below breach _alt_min_breach_distance_m = _alt_min_m - _curr_alt_u_m; @@ -668,9 +737,11 @@ bool AC_Fence::auto_enable_fence_floor() return false; } - float _curr_alt_d_m; - AP::ahrs().get_relative_position_D_home(_curr_alt_d_m); - const float _curr_alt_u_m = -_curr_alt_d_m; // translate Down to Up + float _curr_alt_u_m; + if (!get_alt_U_in_frame(_curr_alt_u_m)) { + // if we can't get the alt then don't enable yet + return true; + } // check if we are over the altitude fence if (!floor_enabled() && _curr_alt_u_m >= get_safe_alt_min_m()) { diff --git a/libraries/AC_Fence/AC_Fence.h b/libraries/AC_Fence/AC_Fence.h index 59a5a3cdabc357..6ec6520a532221 100644 --- a/libraries/AC_Fence/AC_Fence.h +++ b/libraries/AC_Fence/AC_Fence.h @@ -238,6 +238,9 @@ class AC_Fence /// retrieve the current NED position relative to home bool get_current_position_NED(Vector3f& currpos) const; + // get altitude in FENCE_ALT_TYPE frame + bool get_alt_U_in_frame(float &alt) const; + // additional checks for the different fence types: bool pre_arm_check_polygon(char *failure_msg, const uint8_t failure_msg_len) const; bool pre_arm_check_circle(char *failure_msg, const uint8_t failure_msg_len) const; @@ -263,6 +266,7 @@ class AC_Fence AP_Int16 _ret_altitude; // return to this altitude AP_Int16 _options; // options bitmask, see OPTIONS enum AP_Float _notify_freq; // margin notification frequency + AP_Enum _alt_type; // altitude frame // backup fences float _alt_max_backup_m; // backup altitude upper limit in meters used to refire the breach if the vehicle continues to move further away From 0e7ea0453792d91d0d071635009dc3575d696b62 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 29 Nov 2025 20:42:31 +0000 Subject: [PATCH 13/99] Copter: use fence max alt frame for RTL --- ArduCopter/mode_rtl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArduCopter/mode_rtl.cpp b/ArduCopter/mode_rtl.cpp index 1425d304730011..5235a5ac6cd1b3 100644 --- a/ArduCopter/mode_rtl.cpp +++ b/ArduCopter/mode_rtl.cpp @@ -567,8 +567,8 @@ void ModeRTL::compute_return_target() // the vehicle not climbing at all as RTL begins. This can be overly conservative and it might be better // to apply the fence alt limit independently on the origin_point and return_target if ((copter.fence.get_enabled_fences() & AC_FENCE_TYPE_ALT_MAX) != 0) { - // get return target as alt-above-home so it can be compared to fence's alt - if (rtl_path.return_target.get_alt_m(Location::AltFrame::ABOVE_HOME, target_alt_m)) { + // get return target in max alt frame so it can be compared to fence's alt + if (rtl_path.return_target.get_alt_m(copter.fence.get_alt_max_frame(), target_alt_m)) { float fence_alt_m = copter.fence.get_safe_alt_max_m(); if (target_alt_m > fence_alt_m) { // reduce target alt to the fence alt From c0124c60af5d117e7a9bb52f2fc3c1c15db5c802 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 6 Dec 2025 18:36:29 +0000 Subject: [PATCH 14/99] Plane: use get_relative_safe_alt_min_m and get_relative_safe_alt_max_m --- ArduPlane/altitude.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArduPlane/altitude.cpp b/ArduPlane/altitude.cpp index 74fe102183f9b3..73d1466d68cb96 100644 --- a/ArduPlane/altitude.cpp +++ b/ArduPlane/altitude.cpp @@ -412,11 +412,11 @@ void Plane::check_fbwb_altitude(void) // taking fence max and min altitude (with margin) const uint8_t enabled_fences = plane.fence.get_enabled_fences(); if ((enabled_fences & AC_FENCE_TYPE_ALT_MIN) != 0) { - min_alt_cm = plane.fence.get_safe_alt_min_m()*100.0; + min_alt_cm = plane.fence.get_relative_safe_alt_min_m()*100.0; should_check_min = true; } if ((enabled_fences & AC_FENCE_TYPE_ALT_MAX) != 0) { - max_alt_cm = plane.fence.get_safe_alt_max_m()*100.0; + max_alt_cm = plane.fence.get_relative_safe_alt_max_m()*100.0; should_check_max = true; } #endif From 4c89af5d181548f9e64bf756b9c81537fd5e3a65 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Tue, 9 Dec 2025 09:25:49 +0000 Subject: [PATCH 15/99] AP_Arming: check fence terrain requirements in pre-arm --- libraries/AP_Arming/AP_Arming.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/AP_Arming/AP_Arming.cpp b/libraries/AP_Arming/AP_Arming.cpp index 54239d349df3c5..0ce5f86815ac23 100644 --- a/libraries/AP_Arming/AP_Arming.cpp +++ b/libraries/AP_Arming/AP_Arming.cpp @@ -1227,11 +1227,13 @@ bool AP_Arming::terrain_database_required() const { #if AP_MISSION_ENABLED AP_Mission *mission = AP::mission(); - if (mission == nullptr) { - // no mission support? - return false; + if (mission != nullptr && mission->contains_terrain_alt_items()) { + return true; } - if (mission->contains_terrain_alt_items()) { +#endif +#if AP_FENCE_ENABLED + const AC_Fence* fence = AP::fence(); + if (fence != nullptr && fence->terrain_database_required()) { return true; } #endif From 00e77a075f4162319b8ed4eabf6d340bdd2a8901 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 29 Nov 2025 18:11:24 +0000 Subject: [PATCH 16/99] AC_Fence: specify alt frame for min and max fences separately add accessors for max alt frame pre-arm check for terrain frames only enable terrain checks if terrain is available introduce get_relative_safe_alt_max_m and get_relative_safe_alt_min_m methods dummy implementation of terrain_database_required --- libraries/AC_Fence/AC_Fence.cpp | 134 +++++++++++++++++++------------- libraries/AC_Fence/AC_Fence.h | 30 ++++++- 2 files changed, 104 insertions(+), 60 deletions(-) diff --git a/libraries/AC_Fence/AC_Fence.cpp b/libraries/AC_Fence/AC_Fence.cpp index 513bf46e0cac71..e66e4637d818ed 100644 --- a/libraries/AC_Fence/AC_Fence.cpp +++ b/libraries/AC_Fence/AC_Fence.cpp @@ -76,7 +76,7 @@ const AP_Param::GroupInfo AC_Fence::var_info[] = { // @Param{Copter, Plane, Sub}: ALT_MAX // @DisplayName: Fence Maximum Altitude - // @Description: Maximum altitude allowed before geofence triggers. See FENCE_ALT_TYPE for reference frame. + // @Description: Maximum altitude allowed before geofence triggers. See FENCE_ALT_MAX_TP for reference frame. // @Units: m // @Range: 10 1000 // @Increment: 1 @@ -108,7 +108,7 @@ const AP_Param::GroupInfo AC_Fence::var_info[] = { // @Param{Copter, Plane, Sub}: ALT_MIN // @DisplayName: Fence Minimum Altitude - // @Description: Minimum altitude allowed before geofence triggers. See FENCE_ALT_TYPE for reference frame. + // @Description: Minimum altitude allowed before geofence triggers. See FENCE_ALT_MIN_TP for reference frame. // @Units: m // @Range: -100 100 // @Increment: 1 @@ -165,13 +165,20 @@ const AP_Param::GroupInfo AC_Fence::var_info[] = { // @User: Standard AP_GROUPINFO("MARGIN_XY", 13, AC_Fence, _margin_ne_m, 0), - // @Param: ALT_FRAME - // @DisplayName: Altitude frame - // @Description: The altitude reference frame for all altitude fences. Can be AMSL, home-relative, origin relative or AGL. Applies to both FENCE_ALT_MAX and FENCE_ALT_MIN. + // @Param: ALT_MAX_TP + // @DisplayName: Altitude max frame type + // @Description: The altitude reference frame for the maximum altitude fence. Can be AMSL, home-relative, origin relative or AGL. // @User: Advanced // @Values: 0:Above sea level, 1:Above Home, 2: Above Origin, 3: Above Terrain - AP_GROUPINFO_FRAME("ALT_TYPE", 12, AC_Fence, _alt_type, float(Location::AltFrame::ABOVE_HOME), AP_PARAM_FRAME_COPTER | AP_PARAM_FRAME_SUB | AP_PARAM_FRAME_TRICOPTER | AP_PARAM_FRAME_HELI | AP_PARAM_FRAME_PLANE), + AP_GROUPINFO_FRAME("ALT_MAX_TP", 14, AC_Fence, _alt_max_type, float(Location::AltFrame::ABOVE_HOME), AP_PARAM_FRAME_COPTER | AP_PARAM_FRAME_SUB | AP_PARAM_FRAME_TRICOPTER | AP_PARAM_FRAME_HELI | AP_PARAM_FRAME_PLANE), + // @Param: ALT_MIN_TP + // @DisplayName: Altitude min frame type + // @Description: The altitude reference frame for the minimum altitude fence. Can be AMSL, home-relative, origin relative or AGL. + // @User: Advanced + // @Values: 0:Above sea level, 1:Above Home, 2: Above Origin, 3: Above Terrain + AP_GROUPINFO_FRAME("ALT_MIN_TP", 15, AC_Fence, _alt_min_type, float(Location::AltFrame::ABOVE_HOME), AP_PARAM_FRAME_COPTER | AP_PARAM_FRAME_SUB | AP_PARAM_FRAME_TRICOPTER | AP_PARAM_FRAME_HELI | AP_PARAM_FRAME_PLANE), + AP_GROUPEND }; @@ -463,6 +470,10 @@ bool AC_Fence::pre_arm_check_alt(char *failure_msg, const uint8_t failure_msg_le return true; } +bool AC_Fence::terrain_database_required() const +{ + return _alt_min_type == Location::AltFrame::ABOVE_TERRAIN || _alt_max_type == Location::AltFrame::ABOVE_TERRAIN; +} /// pre_arm_check - returns true if all pre-takeoff checks have completed successfully bool AC_Fence::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) const @@ -559,60 +570,51 @@ bool AC_Fence::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) c } /* - get our altitude in the FENCE_ALT_TYPE frame + get our altitude in the supplied frame return false if not available */ -bool AC_Fence::get_alt_U_in_frame(float &alt) const +bool AC_Fence::get_alt_in_frame(float &alt, Location::AltFrame alt_frame) const { const auto &ahrs = AP::ahrs(); - ahrs.get_relative_position_D_home(alt); - - // translate to +ve up - alt = -alt; - - /* - convert to desired altitude frame - */ - const auto alt_frame = (Location::AltFrame)_alt_type.get(); - switch (alt_frame) { - case Location::AltFrame::ABOVE_HOME: - break; - case Location::AltFrame::ABOVE_ORIGIN: + if (alt_frame == Location::AltFrame::ABOVE_ORIGIN) { // try and get it relative origin if (!ahrs.get_relative_position_D_origin_float(alt)) { // treat as a breach if origin not known return false; } alt = -alt; - break; - case Location::AltFrame::ABSOLUTE: { - float home_alt; - if (!ahrs.get_home().get_alt_m(Location::AltFrame::ABSOLUTE, home_alt)) { - // treat not known home alt as a breach - return false; - } - alt += home_alt; - break; - } - case Location::AltFrame::ABOVE_TERRAIN: { -#if AP_TERRAIN_AVAILABLE - // get height above ground level with extrapolation. We extrapolate to prevent - // a fence breach due to a momentary cache miss in the terrain library - auto *terrain = AP::terrain(); - if (terrain == nullptr || !terrain->height_above_terrain(alt, true)) { - // treat not known home alt as a breach - return false; + return true; + } + + if (alt_frame == Location::AltFrame::ABOVE_HOME) { + // try and get it relative origin + ahrs.get_relative_position_D_home(alt); + alt = -alt; + return true; + } + + Location loc; + if (AP::ahrs().get_location(loc)) { + if (loc.get_alt_m(alt_frame, alt)) { + return true; } - break; -#else - // no terrain available - return false; -#endif - // note that unknown frames are treated as ABOVE_HOME } + + return false; +} + +// update safe alt min - home *must* be set before this function is called with absolute reference frames +void AC_Fence::update_safe_alt_min() +{ + // update safe alt min, failure to get home will have already breached above + if (_alt_min_type == Location::AltFrame::ABSOLUTE) { + float home_alt; + UNUSED_RESULT(AP::ahrs().get_home().get_alt_m(Location::AltFrame::ABSOLUTE, home_alt)); + _safe_relhome_alt_min_m = _alt_min_m - home_alt - _margin_m; + } else { + _safe_relhome_alt_min_m = _alt_min_m - _margin_m; } - return true; } /// returns true if we have freshly breached the maximum altitude @@ -627,7 +629,7 @@ bool AC_Fence::check_fence_alt_max() } float _curr_alt_u_m; - if (!get_alt_U_in_frame(_curr_alt_u_m)) { + if (!get_alt_in_alt_max_frame(_curr_alt_u_m)) { // if we can't get the alt then it is a breach return true; } @@ -635,6 +637,15 @@ bool AC_Fence::check_fence_alt_max() // record distance above/below breach _alt_max_breach_distance_m = _curr_alt_u_m - _alt_max_m; + // update safe alt max, failure to get home will have already breached above + if (_alt_max_type == Location::AltFrame::ABSOLUTE) { + float home_alt; + UNUSED_RESULT(AP::ahrs().get_home().get_alt_m(Location::AltFrame::ABSOLUTE, home_alt)); + _safe_relhome_alt_max_m = _alt_max_m - home_alt - _margin_m; + } else { + _safe_relhome_alt_max_m = _alt_max_m - _margin_m; + } + // check if we are over the altitude fence if (_curr_alt_u_m >= _alt_max_m) { // check for a new breach or a breach of the backup fence @@ -680,7 +691,7 @@ bool AC_Fence::check_fence_alt_min() } float _curr_alt_u_m; - if (!get_alt_U_in_frame(_curr_alt_u_m)) { + if (!get_alt_in_alt_min_frame(_curr_alt_u_m)) { // if we can't get the alt then it is a breach return true; } @@ -688,6 +699,9 @@ bool AC_Fence::check_fence_alt_min() // record distance above/below breach _alt_min_breach_distance_m = _alt_min_m - _curr_alt_u_m; + // update safe alt min, failure to get home will have already breached above + update_safe_alt_min(); + // check if we are under the altitude fence if (_curr_alt_u_m <= _alt_min_m) { @@ -738,11 +752,14 @@ bool AC_Fence::auto_enable_fence_floor() } float _curr_alt_u_m; - if (!get_alt_U_in_frame(_curr_alt_u_m)) { + if (!get_alt_in_alt_min_frame(_curr_alt_u_m)) { // if we can't get the alt then don't enable yet return true; } + // update safe alt min, failure to get home will have already breached above + update_safe_alt_min(); + // check if we are over the altitude fence if (!floor_enabled() && _curr_alt_u_m >= get_safe_alt_min_m()) { enable(true, AC_FENCE_TYPE_ALT_MIN, false); @@ -963,9 +980,9 @@ bool AC_Fence::check_destination_within_fence(const Location& loc) { // Altitude fence check - Fence Ceiling if ((get_enabled_fences() & AC_FENCE_TYPE_ALT_MAX)) { - int32_t alt_above_home_cm; - if (loc.get_alt_cm(Location::AltFrame::ABOVE_HOME, alt_above_home_cm)) { - if ((alt_above_home_cm * 0.01f) > _alt_max_m) { + int32_t alt_cm; + if (loc.get_alt_cm(_alt_max_type, alt_cm)) { + if ((alt_cm * 0.01f) > _alt_max_m) { return false; } } @@ -973,9 +990,9 @@ bool AC_Fence::check_destination_within_fence(const Location& loc) // Altitude fence check - Fence Floor if ((get_enabled_fences() & AC_FENCE_TYPE_ALT_MIN)) { - int32_t alt_above_home_cm; - if (loc.get_alt_cm(Location::AltFrame::ABOVE_HOME, alt_above_home_cm)) { - if ((alt_above_home_cm * 0.01f) < _alt_min_m) { + int32_t alt_cm; + if (loc.get_alt_cm(_alt_min_type, alt_cm)) { + if ((alt_cm * 0.01f) < _alt_min_m) { return false; } } @@ -1136,6 +1153,11 @@ void AC_Fence::manual_recovery_start() return; } + // Not armed so return + if (!hal.util->get_soft_armed()) { + return; + } + // record time pilot began manual recovery _manual_recovery_start_ms = AP_HAL::millis(); @@ -1202,7 +1224,7 @@ uint8_t AC_Fence::present() const { return 0; } uint8_t AC_Fence::get_enabled_fences() const { return 0; } bool AC_Fence::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) const { return true; } - +bool AC_Fence::terrain_database_required() const { return false; } uint8_t AC_Fence::check(bool disable_auto_fences) { return 0; } bool AC_Fence::check_destination_within_fence(const Location& loc) { return true; } float AC_Fence::get_breach_distance(uint8_t fence_type) const { return 0.0; } diff --git a/libraries/AC_Fence/AC_Fence.h b/libraries/AC_Fence/AC_Fence.h index 6ec6520a532221..0aff2558e2d9c1 100644 --- a/libraries/AC_Fence/AC_Fence.h +++ b/libraries/AC_Fence/AC_Fence.h @@ -113,6 +113,9 @@ class AC_Fence /// pre_arm_check - returns true if all pre-takeoff checks have completed successfully bool pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) const; + // used by AP_Arming for pre-arm checks + bool terrain_database_required() const; + /// /// methods to check we are within the boundaries and recover /// @@ -151,10 +154,16 @@ class AC_Fence /// get_action - getter for user requested action on limit breach Action get_action() const { return _action; } - /// get_safe_alt - returns maximum safe altitude (i.e. alt_max - margin) + /// get_safe_alt - returns maximum safe altitude in relative frame + float get_relative_safe_alt_max_m() const { return _safe_relhome_alt_max_m ; } + + /// get_safe_alt_min_m - returns the minimum safe altitude in relative frame + float get_relative_safe_alt_min_m() const { return _safe_relhome_alt_min_m ; } + + /// get_safe_alt_max_m - returns maximum safe altitude in alt max frame (i.e. alt_max - margin) float get_safe_alt_max_m() const { return _alt_max_m - _margin_m; } - /// get_safe_alt_min_m - returns the minimum safe altitude (i.e. alt_min + margin) + /// get_safe_alt_min_m - returns the minimum safe altitude in alt min frame (i.e. alt_min + margin) float get_safe_alt_min_m() const { return _alt_min_m + _margin_m; } /// get_radius_m - returns the fence radius in meters @@ -208,6 +217,13 @@ class AC_Fence } #endif + // get altitude in alt max frame + bool get_alt_in_alt_max_frame(float &alt) const { return get_alt_in_frame(alt, _alt_max_type); } + // get altitude in alt min frame + bool get_alt_in_alt_min_frame(float &alt) const { return get_alt_in_frame(alt, _alt_min_type); } + // get alt max frame + Location::AltFrame get_alt_max_frame() const { return (Location::AltFrame)_alt_max_type.get(); } + private: static AC_Fence *_singleton; @@ -223,6 +239,9 @@ class AC_Fence /// check_fence_circle - true if circle fence has been newly breached bool check_fence_circle(); + /// update safe alt min + void update_safe_alt_min(); + /// record_breach - update breach bitmask, time and count void record_breach(uint8_t fence_type); @@ -239,7 +258,7 @@ class AC_Fence bool get_current_position_NED(Vector3f& currpos) const; // get altitude in FENCE_ALT_TYPE frame - bool get_alt_U_in_frame(float &alt) const; + bool get_alt_in_frame(float &alt, Location::AltFrame alt_frame) const; // additional checks for the different fence types: bool pre_arm_check_polygon(char *failure_msg, const uint8_t failure_msg_len) const; @@ -266,7 +285,8 @@ class AC_Fence AP_Int16 _ret_altitude; // return to this altitude AP_Int16 _options; // options bitmask, see OPTIONS enum AP_Float _notify_freq; // margin notification frequency - AP_Enum _alt_type; // altitude frame + AP_Enum _alt_max_type; // altitude max frame type + AP_Enum _alt_min_type; // altitude min frame type // backup fences float _alt_max_backup_m; // backup altitude upper limit in meters used to refire the breach if the vehicle continues to move further away @@ -286,6 +306,8 @@ class AC_Fence // other internal variables float _home_distance_m; // distance from home in meters (provided by main code) float _fence_distance_m; // distance to the nearest fence + float _safe_relhome_alt_max_m ; // calculated safe alt max + float _safe_relhome_alt_min_m ; // calculated safe alt min // breach information uint8_t _breached_fences; // bitmask holding the fence types that were breached (i.e. AC_FENCE_TYPE_ALT_MIN, AC_FENCE_TYPE_CIRCLE) From c0f95db09f841112a14b1a18039347b6eff76d6c Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 29 Nov 2025 20:38:53 +0000 Subject: [PATCH 17/99] AC_Avoidance: take alt frame into account when avoiding min or max alt fences --- libraries/AC_Avoidance/AC_Avoid.cpp | 20 ++++++++++++-------- libraries/AC_Avoidance/AP_OABendyRuler.cpp | 10 +++++----- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/libraries/AC_Avoidance/AC_Avoid.cpp b/libraries/AC_Avoidance/AC_Avoid.cpp index 4557cb2a632afc..d1853979a57e38 100644 --- a/libraries/AC_Avoidance/AC_Avoid.cpp +++ b/libraries/AC_Avoidance/AC_Avoid.cpp @@ -433,17 +433,21 @@ void AC_Avoid::adjust_velocity_z(float kP, float accel_cmss, float& climb_rate_c AC_Fence *fence = AP::fence(); if ((_enabled & AC_AVOID_STOP_AT_FENCE) > 0 && fence) { // calculate distance from vehicle to safe altitude - float veh_alt_m; - _ahrs.get_relative_position_D_home(veh_alt_m); if ((fence->get_enabled_fences() & AC_FENCE_TYPE_ALT_MIN) > 0) { - // fence.get_safe_alt_max_m() is UP, veh_alt_m is DOWN: - min_alt_diff_m = -(fence->get_safe_alt_min_m() + veh_alt_m); - limit_min_alt = true; + float veh_alt_m; + if (fence->get_alt_in_alt_min_frame(veh_alt_m)) { + // fence.get_safe_alt_max_m() is UP, veh_alt_m is UP: + min_alt_diff_m = veh_alt_m - fence->get_safe_alt_min_m(); + limit_min_alt = true; + } } if ((fence->get_enabled_fences() & AC_FENCE_TYPE_ALT_MAX) > 0) { - // fence.get_safe_alt_max_m() is UP, veh_alt_m is DOWN: - max_alt_diff_m = fence->get_safe_alt_max_m() + veh_alt_m; - limit_max_alt = true; + float veh_alt_m; + if (fence->get_alt_in_alt_max_frame(veh_alt_m)) { + // fence.get_safe_alt_max_m() is UP, veh_alt_m is UP: + max_alt_diff_m = fence->get_safe_alt_max_m() - veh_alt_m; + limit_max_alt = true; + } } } #endif diff --git a/libraries/AC_Avoidance/AP_OABendyRuler.cpp b/libraries/AC_Avoidance/AP_OABendyRuler.cpp index efba4253be69e1..be17102d5f8cb6 100644 --- a/libraries/AC_Avoidance/AP_OABendyRuler.cpp +++ b/libraries/AC_Avoidance/AP_OABendyRuler.cpp @@ -501,18 +501,18 @@ bool AP_OABendyRuler::calc_margin_from_alt_fence(const Location &start, const Lo return false; } - int32_t alt_above_home_cm_start, alt_above_home_cm_end; - if (!start.get_alt_cm(Location::AltFrame::ABOVE_HOME, alt_above_home_cm_start)) { + int32_t alt_above_frame_cm_start, alt_above_frame_cm_end; + if (!start.get_alt_cm(fence->get_alt_max_frame(), alt_above_frame_cm_start)) { return false; } - if (!end.get_alt_cm(Location::AltFrame::ABOVE_HOME, alt_above_home_cm_end )) { + if (!end.get_alt_cm(fence->get_alt_max_frame(), alt_above_frame_cm_end )) { return false; } // safe max alt = fence alt - fence margin const float max_fence_alt = fence->get_safe_alt_max_m(); - const float margin_start = max_fence_alt - alt_above_home_cm_start * 0.01f; - const float margin_end = max_fence_alt - alt_above_home_cm_end * 0.01f; + const float margin_start = max_fence_alt - alt_above_frame_cm_start * 0.01f; + const float margin_end = max_fence_alt - alt_above_frame_cm_end * 0.01f; // margin is minimum distance to fence from either start or end location margin = MIN(margin_start,margin_end); From 3b06836c7305ca71bda1586531d7de8f3e409b61 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Wed, 3 Dec 2025 19:12:52 +0000 Subject: [PATCH 18/99] autotest: add alt frame fence tests make home and origin different heights in MaxAltFence add tests for fence alt _TP parameters Co-authored-by: --- Tools/autotest/arducopter.py | 90 +++++- Tools/autotest/quadplane.py | 19 ++ Tools/autotest/vehicle_test_suite.py | 422 +++++++++++++++++++++++++++ 3 files changed, 525 insertions(+), 6 deletions(-) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index 2ed7235ed63c9b..a1943ecaa8345c 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -1955,19 +1955,26 @@ def HorizontalFence(self, timeout=180): "timed out after %u seconds" % (home_distance, timeout,)) # MaxAltFence - fly up until you hit the fence ceiling - def MaxAltFence(self): - '''Test Max Alt Fence''' - self.takeoff(10, mode="LOITER") - """Hold loiter position.""" + def max_alt_fence_frame(self, frame, alt, expected_breach_alt, terrain=0): + self.start_subtest("Test Max Alt Fence in frame %u" % frame) # enable fence, disable avoidance self.set_parameters({ "FENCE_ENABLE": 1, "AVOID_ENABLE": 0, "FENCE_TYPE": 1, + "FENCE_ALT_MAX_TP": frame, + "FENCE_ALT_MAX": alt, "FENCE_ENABLE" : 1, + "TERRAIN_ENABLE": terrain, + "SIM_TERRAIN": terrain, }) + if terrain == 1: + self.install_terrain_handlers_context() + + self.takeoff(10, mode="LOITER") + """Hold loiter position.""" self.change_alt(10) # first east @@ -1989,10 +1996,30 @@ def MaxAltFence(self): # wait for fence to trigger self.wait_mode('RTL', timeout=120) + # verify breach occurred at expected altitude + self.assert_altitude(expected_breach_alt, accuracy=5) + self.wait_rtl_complete() self.zero_throttle() + # MaxAltFence - fly up until you hit the fence ceiling + def MaxAltFence(self): + '''Test Max Alt Fences''' + self.poll_home_position(quiet=False) + home_loc = self.mav.location() + origin_alt = home_loc.alt + + self.max_alt_fence_frame(0, origin_alt + 80, origin_alt + 80) # absolute + self.max_alt_fence_frame(1, 100, origin_alt + 100) # above home + + # set home 50m higher than origin to test that frame 2 uses origin not home + home_loc.alt = origin_alt + 50 + self.set_home(home_loc) + self.max_alt_fence_frame(2, 120, origin_alt + 120) # above origin + + self.max_alt_fence_frame(3, 90, origin_alt + 90, terrain=1) # above terrain + # MaxAltFence - fly up and make sure fence action does not trigger # Also check that the vehicle will not try and descend too fast when trying to backup from a max alt fence due to avoidance def MaxAltFenceAvoid(self): @@ -2046,8 +2073,16 @@ def get_climb_rate(mav, m): self.do_RTL() # fly_alt_min_fence_test - fly down until you hit the fence floor - def MinAltFence(self): + def min_alt_fence_frame(self, frame, alt, terrain=0): '''Test Min Alt Fence''' + + if terrain == 1: + self.set_parameters({ + "TERRAIN_ENABLE": terrain, + "SIM_TERRAIN": terrain, + }) + self.install_terrain_handlers_context() + self.takeoff(30, mode="LOITER", timeout=60) # enable fence, disable avoidance @@ -2055,7 +2090,8 @@ def MinAltFence(self): "AVOID_ENABLE": 0, "FENCE_ENABLE" : 1, "FENCE_TYPE": 8, - "FENCE_ALT_MIN": 20, + "FENCE_ALT_MIN": alt, + "FENCE_ALT_MIN_TP": frame, }) self.change_alt(30) @@ -2090,6 +2126,24 @@ def MinAltFence(self): self.zero_throttle() + def MinAltFence(self): + '''Test Min Alt Fences''' + self.poll_home_position(quiet=False) + home_loc = self.mav.location() + self.set_home(home_loc) + + self.min_alt_fence_frame(0, home_loc.alt + 20) # absolute + self.min_alt_fence_frame(3, 20, 1) # above terrain + self.min_alt_fence_frame(2, 20) # above origin + + # set origin below home alt - test should still pass + nz = mavutil.location(home_loc.lat, home_loc.lng, home_loc.alt - 20, 270) + self.set_origin(nz) + self.set_parameters({ + "SIM_GPS1_ENABLE": 1, + }) + self.min_alt_fence_frame(1, 20) # above home + # MinAltFenceAvoid - fly down and make sure fence action does not trigger # Also check that the vehicle will not try and ascend too fast when trying to backup from a min alt fence due to avoidance def MinAltFenceAvoid(self): @@ -13035,6 +13089,21 @@ def tests1e(self): self.MAV_CMD_NAV_VTOL_LAND, self.clear_roi, self.ReadOnlyDefaults, + self.FenceRelativePreArms, + self.FenceRelativeToHomeMaxAlt, + self.FenceRelativeToHomeMinAlt, + self.FenceRelativeToHomeMaxAltOriginAbove, + self.FenceRelativeToHomeMinAltOriginAbove, + self.FenceRelativeToHomeCliff, + self.FenceRelativeToOriginMaxAlt, + self.FenceRelativeToOriginMinAlt, + self.FenceRelativeToOriginMaxAltHomeAbove, + self.FenceRelativeToOriginMinAltHomeAbove, + self.FenceRelativeToAMSLMaxAlt, + self.FenceRelativeToAMSLMinAlt, + self.FenceRelativeToAMSLCliff, + self.FenceRelativeToTerrainMaxAlt, + self.FenceRelativeToTerrainMinAlt, ]) return ret @@ -15471,6 +15540,15 @@ def PeriphMultiUARTTunnel(self): if ex is not None: raise ex + def FenceRelative_TakeoffMode(self): + '''method for the FenceRelative test to call''' + return 'LOITER' + + def FenceRelative_params(self): + ret = super().FenceRelative_params() + ret["AVOID_ENABLE"] = 0 + return ret + def RCProtocolFailsafe(self): '''ensure we failsafe when the RC protocol failsafe is set''' self.takeoff(10, mode='LOITER') diff --git a/Tools/autotest/quadplane.py b/Tools/autotest/quadplane.py index 6eb6b8c32f04e1..4b337fb1fed096 100644 --- a/Tools/autotest/quadplane.py +++ b/Tools/autotest/quadplane.py @@ -2858,6 +2858,10 @@ def ScriptedArmingChecksApplet(self): self.set_parameter("Q_OPTIONS", 1) self.wait_text("ArmCk: note: Q will land", check_context=True) + def FenceRelative_TakeoffMode(self): + '''method for the FenceRelative test to call''' + return 'QLOITER' + def TerrainAvoidApplet(self): '''Terrain Avoidance with CMTC''' self.start_subtest("Terrain Avoidance Load and Start") @@ -3162,5 +3166,20 @@ def tests(self): self.ScriptedArmingChecksApplet, self.TerrainAvoidApplet, self.TakeoffCheck, + self.FenceRelativePreArms, + self.FenceRelativeToHomeMaxAlt, + self.FenceRelativeToHomeMinAlt, + self.FenceRelativeToHomeMaxAltOriginAbove, + self.FenceRelativeToHomeMinAltOriginAbove, + self.FenceRelativeToHomeCliff, + self.FenceRelativeToOriginMaxAlt, + self.FenceRelativeToOriginMinAlt, + self.FenceRelativeToOriginMaxAltHomeAbove, + self.FenceRelativeToOriginMinAltHomeAbove, + self.FenceRelativeToAMSLMaxAlt, + self.FenceRelativeToAMSLMinAlt, + self.FenceRelativeToAMSLCliff, + self.FenceRelativeToTerrainMaxAlt, + self.FenceRelativeToTerrainMinAlt, ]) return ret diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py index f8981e35075e5a..b3219836ea200a 100644 --- a/Tools/autotest/vehicle_test_suite.py +++ b/Tools/autotest/vehicle_test_suite.py @@ -9702,6 +9702,15 @@ def offset_location_ne(self, location, metres_north, metres_east): location.alt, location.heading) + def offset_location_up(self, location, metres_up): + '''return a new location offset from passed-in location''' + return mavutil.location( + location.lat, + location.lng, + location.alt + metres_up, + location.heading + ) + def offset_location_heading_distance(self, location, bearing, distance): (target_lat, target_lng) = mavextra.gps_newpos( location.lat, @@ -14901,6 +14910,419 @@ def _MotorTest(self, self.delay_sim_time(10) self.end_subtest("Testing percentage output") + def FenceRelative_fly_north_then_descend(self, north_m, timeout=120): + '''fly north_m metres north in GUIDED mode then descend. + + Uses MAV_CMD_DO_REPOSITION (wpnav) to fly off the KalaupapaCliffs + cliff edge where terrain drops to near sea level. After arriving, + switches back to the vehicle's loiter mode and lowers throttle to + descend below the min altitude fence floor. + Caller must call wait_mode('RTL') to confirm the fence breach. + ''' + current_loc = self.mav.location() + target_loc = self.offset_location_heading_distance(current_loc, 0, north_m) + + # At KalaupapaCliffs the terrain rises ~40 m in the first 100 m + # north (interpolated from the 100 m SRTM grid) before dropping + # off the cliff face to near sea level. 215 m AMSL clears this + # ridge (~206 m AMSL) while staying 10 m below both tests' max + # fence altitude (225 m AMSL), so use the higher of the current + # altitude and 215 m AMSL. + reposition_alt_amsl = max(current_loc.alt, 215.0) + + # fly to target using GUIDED mode waypoint navigation + self.run_cmd_int( + mavutil.mavlink.MAV_CMD_DO_REPOSITION, + -1, # ground speed (-1 = use default) + 1, # flags: change mode to GUIDED + 0, # loiter radius + 0, # yaw (no change) + int(target_loc.lat * 1e7), + int(target_loc.lng * 1e7), + reposition_alt_amsl, + frame=mavutil.mavlink.MAV_FRAME_GLOBAL, + ) + self.wait_location(target_loc, accuracy=50, height_accuracy=None, + timeout=timeout) + + # switch back to loiter mode and descend to breach the fence floor + self.change_mode(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1200) + + def FenceRelative_params(self): + '''returns a set of base parameters for FenceRelative test''' + return { + "FENCE_ALT_MAX_TP": 1, # 1 is above-home + "FENCE_ENABLE": 1, + "FENCE_TYPE": 9, # ALT_MAX|ALT_MIN + "FENCE_ALT_MAX": 20, + } + + def FenceRelativePreArms(self): + '''test fence altitude types''' + self.customise_SITL_commandline([ + "--home", "KalaupapaCliffs", + ]) + self.set_parameters(self.FenceRelative_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() # roughly speaking + + self.start_subtest("Above home-relative fence") + self.set_home(self.offset_location_up(original_home, -2)) + self.set_parameters({ + "FENCE_ALT_MIN": -10, + "FENCE_ALT_MAX": 0, + }) + self.assert_prearm_failure("PreArm: Vehicle breaching Max Alt fence") + + def FenceRelativeToHome_params(self): + '''returns parameters for FenceRelativeToHome test''' + ret = self.FenceRelative_params() + ret["FENCE_ALT_MIN_TP"] = 1 # above-home (ALT_MAX_TP=1 already in base) + return ret + + def FenceRelativeToOrigin_params(self): + '''returns parameters for FenceRelativeToOrigin test''' + ret = self.FenceRelative_params() + ret["FENCE_ALT_MAX_TP"] = 2 # above EKF origin + ret["FENCE_ALT_MIN_TP"] = 2 # above EKF origin + return ret + + def FenceRelativeToAMSL_params(self): + '''returns parameters for FenceRelativeToAMSL test''' + ret = self.FenceRelative_params() + ret["FENCE_ALT_MAX_TP"] = 0 # AMSL + ret["FENCE_ALT_MIN_TP"] = 0 # AMSL + # disable fence at setup time: with AMSL frame the base FENCE_ALT_MAX=20 + # would immediately breach at KalaupapaCliffs (~165 m AMSL) + ret["FENCE_ENABLE"] = 0 + return ret + + def FenceRelativeToTerrain_params(self): + '''returns parameters for FenceRelativeToTerrain test''' + ret = self.FenceRelative_params() + ret["FENCE_ALT_MAX_TP"] = 3 # above terrain + ret["FENCE_ALT_MIN_TP"] = 3 # above terrain + ret["TERRAIN_ENABLE"] = 1 + return ret + + def FenceRelativeToHomeMaxAlt(self): + '''fence max-alt threshold is measured relative to home, not EKF origin''' + self.set_parameters(self.FenceRelativeToHome_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = 20 + fence_alt_max = 20 # m above home = 40 m above origin + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 1, # ALT_MAX only + "FENCE_ALT_MAX": fence_alt_max, + }) + self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1800) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = offset_home.alt + fence_alt_max + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToHomeMinAlt(self): + '''fence min-alt threshold is measured relative to home, not EKF origin''' + self.set_parameters(self.FenceRelativeToHome_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = 20 + fence_alt_min = 5 # m above home = 25 m above origin + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 50, # generous ceiling + }) + self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.set_rc(3, 1200) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = offset_home.alt + fence_alt_min + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToHomeMaxAltOriginAbove(self): + '''fence max-alt relative to home when origin is above home''' + self.set_parameters(self.FenceRelativeToHome_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = -20 + fence_alt_max = 30 # m above home = 10 m above origin + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 1, # ALT_MAX only + "FENCE_ALT_MAX": fence_alt_max, + }) + self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1800) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = offset_home.alt + fence_alt_max + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToHomeMinAltOriginAbove(self): + '''fence min-alt relative to home when origin is above home''' + self.set_parameters(self.FenceRelativeToHome_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = -20 + # 26 m above home = 6 m above origin; wrong origin-frame would + # see vehicle (10 m above origin) as below the 26 m fence → breach + fence_alt_min = 26 + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 50, # generous ceiling + }) + self.takeoff(30, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1200) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = offset_home.alt + fence_alt_min + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToHomeCliff(self): + '''home-relative min fence below arming altitude requires cliff to breach''' + self.customise_SITL_commandline(["--home", "KalaupapaCliffs"]) + self.set_parameters(self.FenceRelativeToHome_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = -20 + fence_alt_min = 5 # m above home = ~150 m AMSL, 15 m below arming alt + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 80, # generous ceiling + "FENCE_RADIUS": 10000, # ensure circle fence cannot limit north flight + }) + self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.FenceRelative_fly_north_then_descend(150) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = offset_home.alt + fence_alt_min + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToOriginMaxAlt(self): + '''fence max-alt threshold is measured relative to EKF origin, not home''' + self.set_parameters(self.FenceRelativeToOrigin_params()) + self.wait_ready_to_arm() + origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 + fence_alt_max = 10 # m above origin = 30 m above home + original_home = self.mav.location() + self.set_home(self.offset_location_up(original_home, -20)) + self.set_parameters({ + "FENCE_TYPE": 1, # ALT_MAX only + "FENCE_ALT_MAX": fence_alt_max, + }) + self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1800) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = origin_alt_m + fence_alt_max + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToOriginMinAlt(self): + '''fence min-alt threshold is measured relative to EKF origin, not home''' + self.set_parameters(self.FenceRelativeToOrigin_params()) + self.wait_ready_to_arm() + origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 + fence_alt_min = 3 # m above origin = 23 m above home + original_home = self.mav.location() + self.set_home(self.offset_location_up(original_home, -20)) + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 50, # generous ceiling + }) + self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.set_rc(3, 1200) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = origin_alt_m + fence_alt_min + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToOriginMaxAltHomeAbove(self): + '''fence max-alt relative to origin when home is above origin''' + self.set_parameters(self.FenceRelativeToOrigin_params()) + self.wait_ready_to_arm() + origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 + fence_alt_max = 50 # m above origin = 30 m above home + original_home = self.mav.location() + self.set_home(self.offset_location_up(original_home, 20)) + self.set_parameters({ + "FENCE_TYPE": 1, # ALT_MAX only + "FENCE_ALT_MAX": fence_alt_max, + }) + self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1800) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = origin_alt_m + fence_alt_max + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToOriginMinAltHomeAbove(self): + '''fence min-alt relative to origin when home is above origin''' + self.set_parameters(self.FenceRelativeToOrigin_params()) + self.wait_ready_to_arm() + origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 + # 15 m above origin = 180 m AMSL (5 m below home); vehicle at + # 10 m above home = 30 m above origin is above the origin fence. + # A home-frame interpretation would place the fence at 15 m above + # home = 200 m AMSL, above the vehicle at 195 m → immediate breach. + fence_alt_min = 15 + original_home = self.mav.location() + self.set_home(self.offset_location_up(original_home, 20)) + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 80, # generous ceiling + }) + self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1200) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = origin_alt_m + fence_alt_min + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToAMSLMaxAlt(self): + '''fence max-alt threshold is interpreted as AMSL, not home-relative''' + self.set_parameters(self.FenceRelativeToAMSL_params()) + self.wait_ready_to_arm() + ggo = self.poll_message("GPS_GLOBAL_ORIGIN") + origin_alt_m = ggo.altitude / 1000.0 # mm -> m + fence_max_amsl = origin_alt_m + 25 + self.set_parameters({ + "FENCE_TYPE": 1, # ALT_MAX only + "FENCE_ALT_MAX": fence_max_amsl, + }) + self.takeoff(15, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1800) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = fence_max_amsl + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToAMSLMinAlt(self): + '''fence min-alt threshold is interpreted as AMSL, not home-relative''' + self.set_parameters(self.FenceRelativeToAMSL_params()) + self.wait_ready_to_arm() + ggo = self.poll_message("GPS_GLOBAL_ORIGIN") + origin_alt_m = ggo.altitude / 1000.0 # mm -> m + fence_min_amsl = origin_alt_m + 10 + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_min_amsl, + "FENCE_ALT_MAX": origin_alt_m + 50, # generous AMSL ceiling + }) + self.takeoff(20, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1200) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = fence_min_amsl + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToAMSLCliff(self): + '''AMSL min fence below arming altitude requires cliff to breach''' + self.customise_SITL_commandline(["--home", "KalaupapaCliffs"]) + self.set_parameters(self.FenceRelativeToAMSL_params()) + self.wait_ready_to_arm() + ggo = self.poll_message("GPS_GLOBAL_ORIGIN") + origin_alt_m = ggo.altitude / 1000.0 # mm -> m + fence_min_below_arming = origin_alt_m - 15 + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_min_below_arming, + "FENCE_ALT_MAX": origin_alt_m + 60, # generous AMSL ceiling + "FENCE_RADIUS": 10000, # ensure circle fence cannot limit north flight + }) + self.takeoff(20, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.FenceRelative_fly_north_then_descend(150) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = fence_min_below_arming + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToTerrainMaxAlt(self): + '''fence max-alt threshold is interpreted as AGL (terrain-relative)''' + self.install_terrain_handlers_context() + self.set_parameters(self.FenceRelativeToTerrain_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + # home is placed 20 m below terrain; terrain AMSL ≈ original_home.alt + terrain_alt_amsl = original_home.alt + fence_alt_max = 10 # m AGL = 30 m above home + offset_home = self.offset_location_up(original_home, -20) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 1, # ALT_MAX only + "FENCE_ALT_MAX": fence_alt_max, + }) + self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.assert_mode_is(self.FenceRelative_TakeoffMode()) + self.set_rc(3, 1800) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = terrain_alt_amsl + fence_alt_max + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + + def FenceRelativeToTerrainMinAlt(self): + '''fence min-alt threshold is interpreted as AGL (terrain-relative)''' + self.install_terrain_handlers_context() + self.set_parameters(self.FenceRelativeToTerrain_params()) + self.wait_ready_to_arm() + original_home = self.mav.location() + # home is placed 20 m below terrain; terrain AMSL ≈ original_home.alt + terrain_alt_amsl = original_home.alt + fence_alt_min = 3 # m AGL = 23 m above home + offset_home = self.offset_location_up(original_home, -20) + self.set_home(offset_home) + self.set_parameters({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 50, # generous ceiling + }) + self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) + self.do_fence_enable() + self.set_rc(3, 1200) + self.wait_mode('RTL', timeout=120) + expected_breach_alt = terrain_alt_amsl + fence_alt_min + self.assert_altitude(expected_breach_alt, accuracy=10) + self.disarm_vehicle(force=True) + def MotorTest(self, timeout=60, **kwargs): '''Run Motor Tests''' # common to Copter and QuadPlane self._MotorTest(self.run_cmd, **kwargs) From 372ae7be0c4b5c1ecb106c6b6047b9d88fc13000 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Tue, 3 Mar 2026 14:05:19 +1100 Subject: [PATCH 19/99] AC_Fence: clear manual recovery started on fence disable --- libraries/AC_Fence/AC_Fence.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/AC_Fence/AC_Fence.cpp b/libraries/AC_Fence/AC_Fence.cpp index e66e4637d818ed..37c51e009c5897 100644 --- a/libraries/AC_Fence/AC_Fence.cpp +++ b/libraries/AC_Fence/AC_Fence.cpp @@ -313,6 +313,11 @@ uint8_t AC_Fence::enable(bool value, uint8_t fence_types, bool update_auto_enabl if (!value) { clear_breach(fences_to_change); + if (update_auto_enable) { + // explicit user disable resets the manual-recovery suppression + // window so that a fresh fence enable will check immediately + _manual_recovery_start_ms = 0; + } } return fences_to_change; From 40f609088992e5510a0288ab233f4938276c7d32 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 7 Mar 2026 08:49:05 +0000 Subject: [PATCH 20/99] AC_Fence: add safe alt methods that also return the frame Add get_safe_alt_max_m_and_frame() and get_safe_alt_min_m_and_frame() which return both the safe altitude and its reference frame via output parameters, for use by scripting bindings. --- libraries/AC_Fence/AC_Fence.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libraries/AC_Fence/AC_Fence.h b/libraries/AC_Fence/AC_Fence.h index 0aff2558e2d9c1..fbc192cff956c9 100644 --- a/libraries/AC_Fence/AC_Fence.h +++ b/libraries/AC_Fence/AC_Fence.h @@ -166,6 +166,18 @@ class AC_Fence /// get_safe_alt_min_m - returns the minimum safe altitude in alt min frame (i.e. alt_min + margin) float get_safe_alt_min_m() const { return _alt_min_m + _margin_m; } + /// get_safe_alt_max_m_and_frame - returns maximum safe altitude and its frame via references + void get_safe_alt_max_m_and_frame(float &alt, uint8_t &frame) const { + alt = _alt_max_m - _margin_m; + frame = (uint8_t)_alt_max_type.get(); + } + + /// get_safe_alt_min_m_and_frame - returns minimum safe altitude and its frame via references + void get_safe_alt_min_m_and_frame(float &alt, uint8_t &frame) const { + alt = _alt_min_m + _margin_m; + frame = (uint8_t)_alt_min_type.get(); + } + /// get_radius_m - returns the fence radius in meters float get_radius_m() const { return _circle_radius_m.get(); } From 2ae04dd20c27768f500de046d688c16078f11387 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 7 Mar 2026 08:49:11 +0000 Subject: [PATCH 21/99] AP_Scripting: return alt frame from safe alt fence bindings Update get_safe_alt_min and get_safe_alt_max bindings to also return the altitude frame as a second return value. Existing callers are not broken as Lua silently discards extra return values. --- libraries/AP_Scripting/docs/docs.lua | 10 ++++++---- .../AP_Scripting/generator/description/bindings.desc | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/AP_Scripting/docs/docs.lua b/libraries/AP_Scripting/docs/docs.lua index e85234b9cb399f..a6a8b61d4f3f6f 100644 --- a/libraries/AP_Scripting/docs/docs.lua +++ b/libraries/AP_Scripting/docs/docs.lua @@ -4044,12 +4044,14 @@ function fence:get_margin_breach_time() end ---| 8 # Minimum altitude function fence:get_breaches() end --- Returns minimum safe altitude in meters above home alt frame (i.e. alt_min + margin) ----@return number +-- Returns minimum safe altitude in meters and its altitude frame (i.e. alt_min + margin) +---@return number altitude_m +---@return integer frame -- 0:Above sea level, 1:Above Home, 2:Above Origin, 3:Above Terrain function fence:get_safe_alt_min() end --- Returns maximum safe altitude in meters above home alt frame (i.e. alt_max - margin) ----@return number +-- Returns maximum safe altitude in meters and its altitude frame (i.e. alt_max - margin) +---@return number altitude_m +---@return integer frame -- 0:Above sea level, 1:Above Home, 2:Above Origin, 3:Above Terrain function fence:get_safe_alt_max() end -- Returns configured fences diff --git a/libraries/AP_Scripting/generator/description/bindings.desc b/libraries/AP_Scripting/generator/description/bindings.desc index a46de27611d825..f6ff4a8e6ef7d4 100644 --- a/libraries/AP_Scripting/generator/description/bindings.desc +++ b/libraries/AP_Scripting/generator/description/bindings.desc @@ -1064,10 +1064,10 @@ singleton AC_Fence method get_margin_breaches uint8_t singleton AC_Fence method get_margin_breach_time uint32_t singleton AC_Fence method get_breach_distance float uint8_t'skip_check singleton AC_Fence method get_breach_direction_NED boolean uint8_t'skip_check Vector3f'Null Location'Null -singleton AC_Fence method get_safe_alt_min_m float -singleton AC_Fence method get_safe_alt_min_m rename get_safe_alt_min -singleton AC_Fence method get_safe_alt_max_m float -singleton AC_Fence method get_safe_alt_max_m rename get_safe_alt_max +singleton AC_Fence method get_safe_alt_min_m_and_frame void float'Ref uint8_t'Ref +singleton AC_Fence method get_safe_alt_min_m_and_frame rename get_safe_alt_min +singleton AC_Fence method get_safe_alt_max_m_and_frame void float'Ref uint8_t'Ref +singleton AC_Fence method get_safe_alt_max_m_and_frame rename get_safe_alt_max singleton AC_Fence method present uint8_t singleton AC_Fence method get_enabled_fences uint8_t From 9ce69c15f55b5ded46170be97f302f0528d06f5d Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 7 Mar 2026 11:02:30 +0000 Subject: [PATCH 22/99] autotest: fix fence alt-frame test isolation issues Fix three categories of cross-test pollution in the fence alt-frame tests: 1. Race condition: FenceRelative_params() sets FENCE_TYPE=9 with FENCE_ENABLE=1 then individual tests change FENCE_TYPE and FENCE_ALT_MIN later. Between those calls auto_enable_fence_floor() can enable ALT_MIN with the wrong limit. Fix by merging fence type and limits into the initial set_parameters call. 2. Cliff tests use customise_SITL_commandline to start at KalaupapaCliffs but context_pop does not revert the SITL location for passing tests. Add customise_SITL_commandline([]) at the start of Origin and Terrain test groups. Also add terrain handlers and the N21W157 SRTM tile so SITL physics can model the cliff terrain. 3. context_pop restores FENCE_ENABLE=1 briefly with stale AMSL frame values, causing instant breaches whose C++ state persists into the next test. Fix by adding reboot_sitl() in each AMSL test. --- Tools/autotest/tilecache/srtm/N21W157.hgt.zip | Bin 0 -> 71405 bytes Tools/autotest/vehicle_test_suite.py | 114 +++++++++++------- 2 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 Tools/autotest/tilecache/srtm/N21W157.hgt.zip diff --git a/Tools/autotest/tilecache/srtm/N21W157.hgt.zip b/Tools/autotest/tilecache/srtm/N21W157.hgt.zip new file mode 100644 index 0000000000000000000000000000000000000000..4d28e1331721ef7fca4c91d39479880c2e0004eb GIT binary patch literal 71405 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 z zfL0A&&^p5Ik|48)25@G>qp*`Sg3B zW)3Pi5E*(mJb7bbZYPWQNF6gVAXo0ZpGP>Qt(aYEt|rFy!5F2b6lXV6QKKj^+efgC zz_K$An7_yh%*f5oVC6ka?qy)voqSp9-H7z!g?DSYDzg5u(CH=@-Wp?6 zgIw7rNmJyw-&v<|D+RT*1J1l$*FJJ|_tU!;wYBsb_3RRZAV_vH6cyLE+_$Fqvk@Wh zp@2hqmVyDqC{6Ug%F&wB(J-{Wce93yb~0^_1vCH!v?~Qhgaw$Jl<99``*sZu<{O}C zsoL{s(vol?JbsrHXjI-fjS9paF6FOphE|dmDmUX`sM@_YnzQB9PqIST$=zL$L+^m{ z*RblMR?-edqbPi#c$&NnvhWXHNqs-B8l(#t*g!N8b&N~%#})zaxe$B)0D>#MwrGM2 z2pOttmi|BdMHl0yL-Fp?Kj!Vx0`}1+h&3ss+aeHg5!Dt zjH-Dikxy>e3dvDTpxd*BdP%}QsjjOrH6iS%6M_Fmvqi>2eu`3ca5z5_n=W7#W$1IiIo8etn$+8T2JhPea;50HVBkcYhk z7T#N#Y5Kkk_}ZJ==~Ep!zz5}ErZ3nyXNK+$$eJ9Fho1!|VyCDu_^|reSK~5}LvecW z8`M%M!lw5q3K5!79#_^D9f}b^2PFR~Ue67^wpvcy4WG2wVEP0Bd1mUCRs&7#KNipb zs*Ugj`UExGRu(b81(N1}UGjP?B)HdFsZex?3TOXc)(RQq+RgeYe--w zqZ|6UvF6bC!~lAOx>mD0!9%+SVQ={RBx5pcOu?_8hdBnrm{zN!uv#=vxH3&K88Nf2 z4NI#Jt|J`OYYvH&Hwlc>8YhNL+{M=9Ke)sgkrCn-e8s>1z z$)lftDkVd&eE!Wz>inH0Ua6mGE-34qX8A0v|E6={OZ;bq_ajetncnss&RqLx_m4t} zJiZ_#S^o2>bM=ar-)~m^{p6f}AM)G2MbFWuG+S>WoRzy@`6ug_c$DuaS%EhUsiH!{ z9o-~-4j2#Ot6gd4oMnf6bRqj#@RZP1lVA7pJfg)4$VCysFwUS}O<}3to5cm6-`cuf zL54=AW?p{6ReHU9F5%Yj1Je22lAQRO*P_EfnV84ck%W;Roqt2ybTEbFch_s}hZc7A zwXdnXzLN!x;V3^93iWt_c>$?hF2F zo5lAA8@dpdE@a#2E%`!|>NkO*`!%jmfq&giyMkow_-g(^;cL-P!L9 zq#h#1OvG^CCX>0n^|ZQ?vZYymTGiQ?!zACzZ%3=+A;!D;iSJT&?mk+w&@C)TG_n{t zpAG^sf=Id7#92L;!5{{_p)84l^QZop9(04>PW1@ZO>sNdb^9#iNG@7h3>_xn-VUJT zyI==pa_^z3g|qW+!JLvHw+b&j=w!5H`rG_d4XU*sP*8-`|!tEfynki*qXHeL6Z#^=b^ z0FCx@LbJR@X5XVeMm1Pb_jujT`Z~G1cf3nnTY!)9;w1E{QkL@n2_XnUPTZUi&KJ8F zRllZoxDZmp_;ya;mku7zGG|{@c=0)bwF5^?8fVpaYP;UpHNnTOkyI*oC7Ip@tO{%}I*1$gA)^qOD zml<2ac>du3^29=(PyTsnY@S&6!1X6LlUx3*bO>ENA;TrA_~iPb$4jG6NSnu?CHPHO za)PznJm40@m&4KJyKDi!{g0YgA^g2xi%dE7VGT9B9U`x?;6+5nd1 zJgnrQag_iOfI}JyL94GY0Hm)2g~<4RC9Ik?}>TrhA<=JqBTII z3$4<|G9T`3qjb|J)IttdmeN~kaO;Wil8Sc1jWYy+u06elqoEDZ$B&UaH~teP%Q$cQwv$Brb8})j?pKa?N}3P*Ybb3mxd*Hx2f+ZP6GWULaX< z4=I|?C4~OCeXmQ+iq(Vf_-7pVCwUYGksB|5gfiF7xR-1DF?`T0*c{9%sEbW%PdAfZ zI(N#;f%oUDB>W_?r;UC^u(dsW{~$B)t@K|LZT7tLTbcm#MOSvo^yS1-+wOnUBtUa~ z4R>F)8@IdP*y#mP|FGHyzvLO1fFfDg_tDn|Zs%yFTd6Y)qrHVs2c4#;y~+F+-N?#1 ze0Y91@3uVn(b9_S%3W?o4QnGXHsOouKi!DPvX4nP6+G3_rCwh1vT^e%vf=IO**K5l zay7GXUGYmmo&<=|%~DD9z0dcjE$o5{ZX}SCmIv;r*{og@2*ba_MkCj1dOzH=3KUR! zrwd)V@WPb(>q_>FQv4uEzk(aSh$*~6b006zk{`00NnNG7MG4$)ndx|U?}Ok~mO-P! zLU}{~$&(-l$%ofeZvF|%Cl42s%=F#P#7St+iR~dxztv4{-Dq@o+bR78+>v>q~!|AD5>P@?8hsAfsYitJT`NX0{fOsLnTPnTcaUaZ28*4?5C_^`+MqAn$0g%hnYhne0XarmUj>V*`s#ykn zWh?qw}5RTTop>`7<;;P zk_1tA`-k)MUge`(ofk=86NbOwKU+>4+!VO>B8ehWhW5D_<&v2I;%yuJC6V9?;Pv8A z_xeB#b?U5Loxt2}O^K%Nz(#8<`)&TXE!l8OcS~M2d6qX>B-`H+q-wB=NQ%_RJ~wP( zyxMnv%^g+q*Tdh8w!G!+yOy8k>%7OPwN?{N%pxZ-LK z-;YFoAJLl#YnGV*!<9u4Z68sVuK%Pc@}qXI;(W8@EZ5{{&AsE}SqisNEw)^x<+qv2 z_90)}M?govFI)}VeRyH%MVaIU=VFh~&X&VZ7v0;^o1;VM=}bk0bs@9?R$MB8U%1J{q)+VX!E_9OL;LnWA^^H%6Z>_GTl+ z;tFzyzxr?Z2>)+!58qy!lEpkwb4Xd#)$AA8)oZ+Q4vmm$(HEM%FK5#ESKJ{YAv833 zM6fYer7M@q26{0J$F2^p4w$|(SHWkf+Snx!9_M!$smwd?mClgk`eP?Aqhjoxs1(!1 z;-oD7HMZ$RTl_uGNqcJ&@=)>$K}~7oowbBvl=3}@)!PBG4X_XQIw9%A-KS@sTnKi> zE&s4li{lk~%W0{nN`lof^D#;z!E|>B;v=EDz;}|Pemc7__liN6Y0&+l<&Fs6X=@)f-gj#GNV12D4U?x{QH@ypT zHwJp{cIm_M8Ko~1!9-Q1jCu^1~lJJ@;*fHa- zuSxlM_j63L;bU51;zQp@oRWHmvp=<-f64THZ09cLGyB;kpQr!pZB}r1p0#g`yIBL0 zrwxC*=g)Od$EzW?UL0hYk;=XL1vmUe8$#ZO`J~8cB*a;+1&TZ|p4C?qu?XMvkbCZ8 z`d0Uj+TAON5D4;xT4BiF=*eO3zmyvXkg5TYkF%N`#bU`&*!?~WpWJ5&bhRl_EFAe* z4*Ya^oi9Nm^5*BOJ!UZ!68s9HQf6vGkyb7ZfDNs;Y1Z;F zsad(UA`AptERJHEKA2m0lasb6*!{odA>qi=QFeGyO^^2Ungk0aQiOZBXxLIzdHKd& z%bZ0q>d*5Z+LE(*R8CgnZk*MU^oY0r{aWky+fS1L=MrXa7P3-FyVDbj@-JAxQ!!50 z|Ga6djn3e`)QA%nzhu{Mc21WdTYYd!!Ax|+>*K=?q%h(purgDs)NKIAbNb^``9(`( zN!s&#qfb$H6aA{?(JTMN<;JmYW#Pt~zzUG(?M!@JamD7SuKM@FQLE-IFe0>IwbGp3 zkHALrNsVU5TL>5=!jTQJ;NAQB@LE^?cHUZh5z{dZnjOS1ka8Phl8{jp-`a=2Tc4EkRrw|^|K(- zIh=dY+DlfejPHzS$;9ca!dD%qS{^D)mH&zD3XXq+r|D{)(bCj>!`ZVPIGms4Q=u)D zdp_iz{App8n55K(%zwQ`_b#!deX1N57EigjRrqh{{R3&X|1BY0qg;6Vd*yf)YW!$$ zynp?kqoj#$pv|;!f#QDG2CpmAa&j6M-~UJcGn+`*E#GpJ0zVfsh@tO$&!IgJ zzQ46vd37te!6GPy(-gv^2e@}%{qKy%pRgtYyqVAHMiF+{>KPuhQxo5h*}@sG^}d!# z3rLm_nVdc)0{O@AWyChtVE4g=#q$k$rk|qB!LhFR5eI6#O`3bR2Y2t_lmX=NVS(al z-$;sPO6aqR2}$+mmspB6N*y7b`3G(l*8_DNV!xuWW~zovQdU*Q7pfV`wUkxyqoysa zFR$9K!5`ESP%FJKhiaqf@sWKSQN;_^0XF-TSJ({)ckI+#-EFi{oAuHx7(a+mmY{R029UCBYKq=(%o!#^m%{O^wv613VEw6Qg>xq zG<%FK=4KUUpOXiJWhKRVdbzEh%|^ZLaq>QtQJxYEbPadG7vYnz$E;B%a4_;-pRV85 zaYK-el3Y#Ztx_o_6wV<+sn(3%e5DYpZ|{PjaNQRZiazRGMt+|u*`g`BA-Qh zTNXA~>b@|GvYT!d-VG1BmH~dh@FLvE(7SOgkk-DH=jA!#9K4V=@e3bAu65@_WX}~V z#y`Cxe97hlT(cK_3ki6v<8HwG8ghvvsifxIn3=Sj4>NBDPttGSV^ygX>|t}}d$!MVV2zQ%L8X0zlJ5B0;DwBlcc zG!y;XmTg}9M7Qw-aPn)E@gs^>6Z`yzkiA89m)?AxHTN^&l|Y-nItdJ|Z$ewUid`+j{35|Tf8A^BWo zrY6bCvqKRnlz^!G)U?!X8#_Oo(FKz*F32nRIC2W`kS6ND>lsaP z8vFOZulojaEv@{G#rdAUGWv9K{b{1uKd_&9kwg>X>q-)0VOh?3qot4T7vf3t$RHHI z9e(|X1NbyOJ2Uq?KixIdUMCE%gO$2Nk>}T8SFFzUpLJjS zup;2V5WBH)r|Smf(ynX;L0&Gk;nya)HgL03*UxO3duz7#0Do2^8M^tMYt#kq36;4) zbo}@8jSs5Yr~^ROe(ucS=1N_vP2~$iKuJ`Tw?ng=_s%1DPrYHIGWRjl7ZfglsSNP+eC_+uBs9We)uyIZW3X@72Eq<6 zwVhXU=#Z|JmT-PAlQ?>BMJDaRd%s_wls~0=B#m%CIpkX*G5`MxQ5Q)_BnB6(vJ%N0 zv64|wF(r=IFs0?J*ux*nyJ=@S^{waht*Jkw-QvTqCS+rJ6mhA5Fdn)j`x5_T1xb7Q zZ_~q>{4IXRf2@u|lbbIKfM=SY+MSlYG4c1L{2h+4JCS;i5zb|iTjzfHX50rcE8lM> z2ov5Y^>%@ScgwU5uHSNEs7~aa1jN-t?Q)u>LTwX&n|^6G(Wn+sN|6gOT^z6O-|QiK z?{Lv@eM+OZ8J?W7l3PgiGuIPEhVlenHFcpC>v6xfrY|PHa3J~EeInGq%m0;ZcJiPg z<=$t{dk-AhuRnW7t$Z5(+w)QLv(T@?#iI}SX3gtv_l>%qO=*cToHHM^+_JsO_x*H$ z<8n+GzS%!>jQ(8p#R<3qm)-B~nqcb;cOkXB{4byWe(q8?r99kyD&he6L>zpW{#dSy zbY#o2OziqCcXCd&47D+0>ilBtwe|TGU2{WtlyT50`kzrDJD&GsgSMQObFrU)O*H%{ z!8m+b6&GQ;Y|<^yw?0-2xZCD*jgu~d_;Hz)_R74&*l^*#$@9Dlr>AdBSyA?`)Oj~M z7uhsk>Ug?IBJ7`e?fUvc!tP_k)r@e?21Vnf-(~BQme~)>=5|VQj8Z6-4n>`|t5VhO z-l@TveNelOe8E_-4MXI|EP0cj%i%c~(x1L@Q0^g`JvJKKy6Mso;m|ZZ7=yQ@(sHuN)Nfb1!kMv?W`}9}pxG z0QY(?T}9`~Ctcd^v)8UIDD+jhF+@s{xpF$=)n^0ewZKz*Y5i^<=XNw?9s0Z43p~6= z#B$dJS6`fTtw^|G_dYT;{`3{sXF*zeKOlA^<(y9=4{Se_{i~RGdhODfdW4^a@)A1} zJAqFi&eumOmLm8XVLN$)*D99t zz7o4t<;wDV;|J`Z+k5S|(e2*Hn+CQ?7M3FhX|jpVLNt2D8BW_Bpb7gEgIy`qQ3T_E z$)P=>p`x!UTx&RB*DS&U{NUd<-qX0zyhZ7--kb)PXq3@4|8=)f`g; z78Z=bZ=FLmUe2hz6Z_J2A@q6Hg;?mt?#oq!3wR-3dQ(LtOHVYR9hH;1)CH?8drhh4 zwW6qvJcM|_JrbBzRsL_fNY5oaN;&fzoqn>*IjL5kM||y^l|79&G3l1D+LrMBX1QGP zMd_p3o=47d0*CvQZJ>mIM;{#;@=Tst;aBZN{jp*#5%b3S_SY%DD9?w!!)-TXV6}ge z+Rf~$e_!KlvuR8q99@$>fLq@lD%6JPu-|?^{5h)9f&*>{L*AOSfo2%@&Uv4MesnIPH`Sd z%%N{<@J{{{-?!MoCpHg;1_kNPA_sSk^qvZd;?HeqTw0nD5zlzNW9<1vhJ9uFZup%l z$@)ts8{h|hibLQh^&8-CA@!|L+kBkK(=_y*6s1X>>#GG-MMK>>ZX@rmSUF$0?;_%0 zx#@pu^T2MEeqqSB!h~Wyo(~u=S5>oWhdaQe(M1EN)^&koGkw$9BHNVio^cOrvAx=< zv$OS?Kcz3sYDR48>2!vWmH&{>dcR7y>5r95sj?;Y(y(jQ{Xqeza`e+qx4B=H(JVv< zXxjdSoQ)jeD7~5Ye}_dp!&rdvN^2lwco=^BWrs&D*m1_f+Mxt}zbr2PX2;9f56!y} zp0XeOns+XWfiw2<`HP-$T0H~s_@3o+!^pp?o2}5T8Xg9QQ};ZQZ@&nS@jFb3%&RrG zQ9$176QTC+T`4ECsu!?NF5S^H`UP2unc!zHy@&%5mO!H~`Zm+Hnl_e-&q3Z}>i%++Q4(EzB=WAL`Yi-G0 z;L77`wTQI|ye&V68~>D$9xb9G$MO6gIxM79E~udB{Ku7bLqmpW&o>>kvrg?{9yY!E z*X5c_yp`HkO3bR$h^YYjV|VlM!QczZ%$S(Dyb(pU3W35Z;dAcwnl`{HZ>HUC%=yoz zOHZV+5k!wKAOXYn-{&;Tnh6cJro+cS+f^nxjiN6R8nYib#o{=Onec<&nggguB=Fe(2m zBkyfVO=jg=Ja&pO3iwaSINhX{7h=7bu=r$mT9K|urI_w=T))W6)%o$CbC+7uGq`Ha zsC5PsFBUS5Znwj1KT@S4KbO2DxxFrO>sHfzH#Qax_Zl*HZD~v3rbV-k=3ct-K%)+_Dz1ZNEX9 zR@=JGy(JDvUlL`apWG8Dyr}!eT}Tc@8OKxg*q@R9CO4TsMH!ssH&VDlnflv#!PBeK z0yIYOO|o?aQGvgpk~UH?G4tjm6jmsQ$N&0r)q%gWEq{myHxmi-;vDo2fe1$ zvp%rfFx%3Iqx~&dwuic`90V5 z+|R!EUTf{ODieTsAoa#aLH6?BT}Ji%*XAeNWnI9o0RziH#ktK(L@!~>c~2KILTYxj z{vB>fIra8;^sgjhP3st%GYicSGlG*(?kMP#t2|kbw?D+Cm zsQViuQQJL62soolsGE1Wj*%~<49Ac*dq_dw$Ij{`KGNT&z^}Fjg;L1SDYwBwG-uu? z8@Q^6DbU}Ry<_vkN{6l1;x;s)9qn`rj@UG+AR_HX0ALR)BVh%$nk$g0a{c+aG4_^W zb;nkON1y>hVF@lpXX}9^8UHw8Vyo%Y1jY87mcSg~i8`$Iod$Y~KfZq>xo4zr{VB92 zqOD1G0a|E3vqtVmJZ`3@bHfh6xOmq-yL>!N1Ptlbp8i*F2K>Gp<*<4Zdg~p>)EMU` z5Pk4tv_8`hMG6?BQskn5C%ok<-6L3L`d#+8jklC{*L2a*Kpi`-CwHEhy=u4D#8hKb zp^elHw=sZ2(cD`+X2N)Yvcu5dJi#Bfs)&o>)wUkDtbslGdi0Z!RbHad3y6Y06Ty0} zeO@t#_dzB<{x8Z+@1UV1J?@CIvJV^SX+g_iXI^NKR(>U+JkjGL>c6;TKw#x%wf~>5 zR1YQXx;pJxPvzB8##(WNCDyYd^S&=!k~wyuMAp164~*2{r5B{>+gOH;?lI@O2^>PO zoRcpC1o2mR+H|o|clXAv6;>N1fgRejdhBV$4f90e;3vSrp$Jf)Qa_HGvl=NrxuJ}* zt2wYxhYn07pDn{D7D)^RX12Q zxMlp|UMVK~ryKvf`Z_gXlXWO&rdl#}ZbDZ{h+P13fj3&( ztSJzJClL8oH&FgZ#B-bQO`9<8{e!UxX>BGcEx+Q?ACL~ej$pLgW#MA5G3TDdQa;68 z{qBb>cv5Y-#}rBJHjx-`azbBjkJ~P#1eU+-iT}x#P+7?cjrS8)D+=i$-Y6fnKS zgVJuTP%TZf_Evpdo$w)Nd&B0Sfm%pTR<o|hp#va#njToD>oOlfsHTwS@X^DE%bwk zr)#*Ke>dYgHgA_tdZ0e&vm*Sc8-A*N26-y8Y>Wk! zgw10d`M7KO%`ur>&1&{?@OY$zt zd9*KbNyn*=zts$9!L4mT{iBoy5{5$MY_Xqh|(T~<&8fRqLyDSFkKj<$SRrEzL+ zAGk=CK)Gd84ot6lJ{aaz{MF-DR4%tFgyQH@;wl@2ZcsyuzPf!Hgu-FkwHm-FSN#u) zu{qg(qcv&u=C~f?dUsXv_{(3Y2xLT)W%cV9`_>_-3z_y^(hzCghv4|zpnq*W?Fqf! zp%e3?^-In3eqkdM5>wNZ^^x;YqT?#U&c-<-WnXAXrZY3r0RqU} z;PjNUsBF{W&lUU^q(N24Fgf=^2_c5O57l%uQ8hSeXNV@-j;O`KyAP`hsND`vY=V{7 z9N(gBSg|%$BfcTE3Y}~qkA1LI(#xa-D5h;cqFl%l!81A z-9U~T-(hCMP9FjW)t=S{H{u+ni1fE;MJIod-|tT}yCTwlZdz5tT_2dsH4wBp1TGLG zuCz2w5=X8)TuW7(UVIY5V)aCg%o2G}^y-9)ts3D5l+l2bj8@Yk$q-s;wS$IZC!q3` z=~GMgnPQ`=B4a^$b)|M4tCp?cpfwkJH8dAFF>f+uPrcc|Zs5f>vL(o#Ak)n$Qmn3e z#yq-08QK8Rz>Pukc+K`PC2BA7c7OHN{?!V0=(L@fy{F=b zpOUP%r#}&)Fa(F8-PP%GVu|&(YyKh(808k zPjt7$Qt{7=XIU4zpR0SqcIn%Ng^&*yXLZ_Qrwa)+1A4Iyc+Rpwu@ zce9E$2i;8#_#7REykrhsOzo$5Q~q38yLZ=X#=`Q1a%^mV{woFO!69t8R8?CZ2jK0t5oPXQOEF4yfwAnqoe zrV1Bx&OJv1+em73K8|5l%1E92MWa>(ChHbXvpq*>v}rL*%l(wRl5g=}Z%Lbw#f&fA z-*hZ#5*R1sgyc2#m^pbv>&HmHK=NmUue65V3xAo!*t)fA%yVp7Sm{BYAP^KQK;%C} zYx!YWV;KU*CO?BQ$7bMgaEzw-b?93_D0@ERu6K?EFqvdR8*@M)ox^9No@C~wHyZE9 zXEP@l0qj3QtImfkUS^af6928jwAb#d)->c|CpP%Nk+v>Iyjji&n#b`~H9n#QD)@FO zFLt4cSUlbb4<=7zcpp%1)MF==ZupnP4n=Hd)eP+$d8JW<&eQnu4-5u&oBStpZUf`) zY)&7L(E*&Fm|?@q#K+O#Y!wtpNZ-fS$04__wg@ggEg4VjqD>JRV+LxN`n!;KYR-+ zQM-v*HPIf~)k3{qU(qMl&UF~0RY&opi61&24vMvRvamA@7j~5Vf-AA@K#f^RDn{s} ze;7SLM$}jQ8=8MoJOi3q+*JTGJ<}eH0@|+Nl5A&>$=LaChnlYr!90q#d8NHyh1A&W zSZ~-zTL#+2Zx!u8bc!3nspH;4##^-L`lflIH6lcR=@uNE4uS{&lB;REEz%9xmMt8F zHJd&)4?Pa}e^m+9{kqKUooGvHh;BoCs0=T^5bO+tNfTV%fo?mD!4|KeF6z-ITl{Ko zq>dvNSd?=3F57(O7W57Lyz7Ka6bI>5ezlG~Ij^4fblm2sV&NWJZ!{TpMIG1ikf2qR z*2cM`RDHWO2X;9QR~c%WNG3mqSs_NnVk2jh%|Vy$#+TmjLMBuL+5V&-&|v?q$*D!5 zi9$)wOAc2|hV6}%9UL}HMp=zY&Prii^TAzZ#dUQV!0|+V=^BtBByXH2KP*A@6eu>p zUr*qg(}rZxY29h0jJM=g+#FO&?=!qgjMb1SH21zWptZokCs@GW(1_3A33S=Y6_e^Z_N-t|2%9lcoMdkk*+G=2@Kx9-JN zKvTT(3ki{5SiV(bBtbnm)!&GKR)f}zaBj=#7;YQ$VK@|A*@>FV(9qQ`EwZhZlG~F3 zL2B#jyMXf?!t0P3=cU*=d5)#Zfc*rcRUg(>8st|YS2}@*@r0hJ(wBAck-*${^9Gay z2PL0KmqT6Tjl}9*5ea)M8Dp9$h8Qyme@mqgYV(o1jqdd$nCk3LlVqG67gpRc{@(ac zuOGPp9%xx+>ILjyL1}JJ?Wh+X+>|E?H8b}mQc@r|Cs`S15cIb3PaI2g;o@KCDBD2c zr+?>ZWMslWQRhs&Q;~T&NpiexzKYLEpU5#`fgG=>!onV<=kp}YMICL0+Pu|Af4QaK#KEQxu)?#O$BA%8bBvgG61In&#k0DD_Oj-^`-I355<{@N?ms|jd%Y$25(T}Sw*<+ z!H2oUFS?L0VK7(umSwB7!vp%crnu3wAbM)EN!>sv63j!wOsW7c>-w@hE}?uZSyQ^p zRXW5Pc`?QE6?TE1_n?!E-k#kgkO~y6lIdB~&zJN8h`a*u$gg!rCP+FfNb(dy= zuzNmfhLTUX#pl(Y`B>%{=2mgr@wEp1@IBJA-J#=6|1q#*y5~TUk}^E(A-v?(fKv8Y zTlAetT43KPYaF-?>zzRnj`8!pHfFM;Bj@Yb2UU z)Fs#dDAu;d@9$ipYDUF(tZSz+5a1>Z_Bsd5)o81I&yS5*-{1SVf5jYla(|JsJZ<2e z8bHf`-a?&Ub*cG>sbzes{`kl;J$9USUhyYL3J&G1jUbu$o1yBart@Z7Bk41_hLpN~ zz&0%HRLRjR2Om?6W|48hBAeg;?hC62;(UDqU7;p2Ile0~3(!!Q7V*ys>-w=&}Z znzE6YbFy7gwd5h_)HzRdC(+iTbsD^vBl24vutPt_>i=+vw$H|^?m>+fjZtad+&Z^a zcX`aJKVcELms5^&c*0VqMm5bIlN|Gs`T&ml0v7IFt3w~ZVJMg0;Ljb9R{n|qgrMSf zHRok`&%owHBSW$_MU?I9VpGSGy`y<`k?zIUq@fEhdqlIp6aR>7sa;pb&Oc+0K8-M& zOp&U6MpY>T9FX2&&no=P$fK0n=jWB%Ut7u{BY)mSIkcI|zs7>6*_SbINt)^ZbDN_H z;Mgv_;n7Hf$SB_yYPKRmx$AQS#PX~DO}LONjtp3kozj?2Ct59o4{_J)!)EHerP~Ym zdj5W@o9d)6HN%T@e3vCSPwpLSy9=4ukJDhc)P|uKTLE(_lB-|vm~g9IXE8Tmzmj``D!QO`&`8E#uw}&n)A4SW23kF zIqH9`nc;hxD&uQ*?ktZ%McdL;P_)_5lNGo2ms{2rek&;+ykfdP8#OKQ zOs;Z>0Sv(}Ch**fqCRh=Ybv{0{g#iX**@n1G+Qec%uDbqz>vDfQwm)NseT)bb_!0=~8$lXi-?YA|dQlL^R|qSsri<&aL2ZL(CCv}a zhsA5bK?JPBMS0jVYQsMXEol$+j~`YhQN$ zE<)rtiXeWJuXCSOvH0iHWEelYZznZ@%MZX8AvLB(8$%@Y|40wB@;A{+-1!LI{0W$dm+5xCsncI z)>?wq6d>##eC4$<(qRGW~BpaTg9Fu8Y_lr_A_LU?&a9m*=Hsq%>CcAJp;UeC#u? zFR0e6pU|eTOowfra?El3WQKpQ5K>Sqs|g`WdQ$FNa6JZ0{|{|Ssr24q&dRT0PPq}% zl-o4}4xinswU3d#u#A=tw2(Ev{|$V_s~h|%%R423JCt9^H$mL&98`~EIOm3Ga!hsQ zGI*!5iWbGxC}XLuDr-(`kwPF)4hI(#Zq7@R+p$2#mSktYWZ2>Vt{bYaqOxu_CpbtK z&4<50A{{snLPZ;j>pL@aDTg}WF_=S5%B}V0CT$#TEnEFN#T18m1KeE<)ze0QZ?&Y_ z?aepd+EXps@b@bhRbe)*+PgHNGQI^SF-xe1qOOqgShl1G1vOx%hcihX8z*J>>N<-h zuhONDbq%6lO0cK*K}lKKv91P@$Hn)a;qQ2MR#jT>d`6Gr2z=SKeeq?(uVK&b#b^9! z69Yl3K*pvt(KKrjWhbed#$QB7agCXf5RLqjqaD~ z>c68h6l8cCviUlY^B?ytvszTK(2%zMP?FT>j9VJ;bp7r*P~DcT!LpKQv00h~$Havc z>m&YJ)l1<@{I}!Tt@<>|C7ZX*LopW83Jex;Y0yeZ`iS|c@Z(#99IpbtmT5(;YnLTO z2u<#=&MBu&1io3DzAXSQZmK;2%SbPaLV$SbeVFrlP7vLhO4T{&ga`j z;b>yg{)87hb2z!&ta&(l&P=Z?{Ia9SbRl)K^3cp~^OnL2|COIIHLu;ZEsm%Kci7qX z{N4;&-N0j}Yj05tcb6+7x3*At)k{>bkj6{Vosc{&6z0N{bmhkq6p=m4=3`EC0LT&D z)X2?hxB&kC9?heGDLqZGxMg-ry2W-bRHUc>({X0MW^f&+iIO92@Gnkd7=j>cdJf|L zwj`@5lhgUrliA#O{g|IiRmz0T(^f{L!cT-{ZK222$i4&0G($$r%N&Teb76(){MRaS^JxeBPLZMC z9+|!B_60h^=GY7G9pQPN0bN~F?)b^F^3>W446;Ink`@9^JQG_(x0VC86yT;hMylyqv0$qcalsq*W`NtO1$ZRw^9yi7fVC>pr6{BTV6qS>x z>6{f-)F#PJSrM~vG%=?9x_DH5_|K}u{q@YtaxP0=ai(oU;9y6eZukXy3fORThu&{5 ztJ^i==}U>>t^dg8NH!&dw`=t}%naSm)$_c=g1yTfxQ_=znUv-fZ?eZi8}^rZ4+D>y zi3{N5-qt#|3pPJ(Chb#)iz`v%xY3~eIs3OP;T^n3xH~Xn4-{fSN=U}=BhzJPwuXVp zL7ZO=DPOQ-bS(JCckbI{d~zjv#|dFgce-=}{?9o3@&2@-!a@v~}^Hw&CtZ7~3dstuyOf6j9sbT%*@E>sHC zusnBy|72)Qf+UsUJ9W8yd7PK@1a6ubn(%snE7y{M=hTz1a*3tkx2UVr60hTDj8=iZ zD0BJ--7sztE$4b3ENN%42b+wAw=i4@%7<^~49Hzezj4(XbFy+44F%7u%*x(6P>b>g z-5A&>*}nr@{rrRO%IAH>&H?=3)#Ta z@ol#`IQtG}A+hb5Kk0X%md>y>>)5S|xRZMUPMIOeorp49Xd z-1MLKoZj+C`tiv5ocB_)cLJkKN!hMMMYy(oX+5zj@m7 z(C+88G79JgnmyZZq{SH>>%tD)Mi@OX3~_W&brjNCjn!cS9?q;cS zuA-_IaqI~!+`2{bqB{(Jmh8VTWZgiVn{Di8w@cig*o3{-`R&^e^2O!2%!O^rJFv~{ zezfnn>wFanzeLRoajj|FREjO%z}f+vH7O%zdhbLmA4R&2F2_7szb@dZi9YXkbx229$1h+PL9=r`l22^nDiebqd6SBJsj4V44 z;{=&RMYBg0!T%3v4xN_0+g3aJRON+?8PJD4DWa)SpO&pd1D%M3q@}U_Fl0V5$f#II zcqGeW@W`m1TgqfZK(HZPE&@9jY~f0R_T%m@1Chb6rGPlg8tK&KfeA6V?wyZ@ZjwEn zOy&0seY@sX9|A;!Xno=tn#b7X9LcR+PF9umIvpp5foV$(SX?gmjdD}{N7 zuzmhT;p2%%d0 zm)yY;Bf<4+Vic)k%|)?Pi(?coPTkMjL~G3)6ns9aZo5JQDPiGBwVEi?*MDmx!;v!e zH^aaVq{_M$FD9e3-bJry8gA&tKii6fL$$uRmwS9@sYc|P`d3j#9>1RFdZ-0DR=}fg z)5Kg3DkEDaz6ccF8cN=9&2gm&tz?e4_~zK3-azaziI{)0X-{la2N`udcA-f@&ec!q zna?68v>jkb-Yq^%uC*mtmFHC^1Id!4!8_zReVay?PD%OE&;Xlq%NKs&C=XG#@HzZ`|2bv~i}!vv4axEOiKBPE zA0fZ4DmqX9hcdIkykU`|;KJBRc`cNrz9syym}q3zvK3KL8vFTn`OeCer_cgEtZJlf zw&BoEzK92C)FObij{fjiV;-r|p!to_Qe{IjEvYM@*aM9R25GlNY1bDKiIWPIx8gx7 zfwMxlDH*8|?+`xs#>)B1b&Z-MWp}L0th*K3k z%+2rWZXPm(rckiq@y$V9loz{m`y@<*|6<6v=`1o{MfzwCjLf|{v?k9KHv*OL}IHf95thk6M8ewudyoMthI36 zVpUzp$km>R0oWsaBZk_8)4b(4xfYl%-UE;jlAMYH=83?0RpFD0lIHp|S-X-lVxG87 zXoZ;_I$pW@T>iJK1YbIWmc2!%KC3y|8fvIf+<=QpxCMV`-IouF6s3~pmW2pHd7hVB zk8%G46*)!2r}cbnoPfP9%Mr4Q;H<;*o&&LR(sQ!%U326bu_Qe!;d@^0Rn9_5jw9pH zP_=hi8ICfkw-Kt(UoQQlx2pg|b9jv{RzRZ7v5fqw#9gh+Reg2BD0#$80v;6~7X6`*z`HFkj= z?sOhvEC-A_irlDD<{)yIp5^u2*s0QY{QmQ=Y}2x1KE>Wjl`b@2^spRivzBO1KHa7 zcQCV{Y!|Qd-_xh2YY3Pa(Ru=yE!56)DJ58zbgnu@T@;P%?s^LLdh7*W$$~`(>KNmx zh_D2E@Uqm2!gEuR zjNNeGWP($vEX*X*Q!V2`DM2Pu%g?-Zy7k^|IE&A3U;EXmgiWL^L2K8Oo+Q0*f$tsz zrv5iK?g+x_=hY1~aUWWa2uH?$!!iSpBC>LMIre05R!W~_&d$=3-$H`S6{GG;I_DQ& z8F5jfeI%EvA{2Ih#owCYM-VR9TKAfyN7AcDVQVQ(fFNsZ80ogV=I7>|0I#*ow3@XL zqpmX7n#K@T1ZC9~oOe@^?}hrgCm6Y=1-7vsDm1klz7#835iT-N7U}lQ;4EE+KF|1@ z3ay1+4njS%3{`bTwO3mje#*`$Ij=3bHjDwB4O+{qtQub>Q#xZuGe>sxmEOG3Af3SJ z20w$VY5ue^g&fVjXMoozYbyuyWNj9(TynA5*~A3;9ufL|w^4ggNzW=cmwZo4ZdyQG ziNz7QKtV*utgUvDyQl>exf(q1OfvdlT_5&hKzim+ z4*}~xq6EK$Aa(5{;jk;euCt*Lj)AkZ*1|VAKAye)j#n%db3@zTHo@O7@@slNeNKXX zHUf5+>Ep23>#+;8jr*?xkiV-NNfL4Xtw4_dB@E!iNO-^VsUoZ4!W!gC_nXJTQzP3@ z%gn})5!pTG7rpy&D^}X;F8ruawEN8pmUAU<>ITo>;BB{>(#jB*dU7B-m02EeYuaB5 zJ%)LL@7(nF@+lL%L?(uP7g>CE%Hh@Vy~{=!ihiYZ0bXrUj1u|(9!vg*B`TO|TjN!9 z&(A8{Rcpjfz0pG1Pz??=Wjh2rqJooANso(Y*G>KsuTb+{u;ytsYDSi%`R!0W_YwSk z>*gp;-C*Tflp^q;0~Bz$8{rJ-xdU2e%EZH#a7txP;gX*@dD8;O8z5R%q;*uw~*!1QbU zIOF5TiDyR6hS_~I%4K*qDetuf1B1M zNX~WCT63!!Ifg;YO811)YPmxV z@Xf_1jofyb>U^RdE}wP357$q7G{Pe}bK0AGTnokqb`(Fx-|!tjem&x7U$8>f1v+Vv zX>Jz9fR4-thK7D{n?F7O`&&#-U#MkiPXlKMtoZtjKG=*VvJ1z;=c;G!Q$;eyS%j!2 zP9cHg@&-~?zb|O6S@Z5)wyyfi(s?j_FATcYfBN9FX&FoD_Aa?BU-fzHPvek%FmjGj zRCv#wtWw>nTXjRxzPaEV%=u&)U*RH0Mm5-dJV(~Cq!!fpLT9zwkVr4O-^KS#RH`Il z=OvXJV;xx{Qi^{Su2~z@GiING0UFL%9$T0G4~Ryl?Xoq{uT;u>`uH~(Ml^oM=d&70 zYW(fwF`;s|8`ycs8H)Bbe8jQ+YRoTk}v+2%QVqlm$gwR z>mp5^Ta%ih+FTi;1Qwp_DcAVbPsZknR44IuE9_tiept%;%|rOcqh-WS zea}*aY{b3`Lx5sy{jVY9%$e0&fpq6%>yxDR+Xs_%0<%LK%wK5K6?USafaU_jn%;)v= z*RZ#OD+uK+<1up9+rgFnVgQ01Jz#i`{xvYbt;NyhU0;iTmrc#Xs&BmO!i9D_S zzgph%>f&$|&ooT1g>uhy49JSj7UMXs_(7%TcYGDGFIsvD^TP=OcRk-Xgzn6o#=qYQ z0{u;!vEfa5H#V}_so9=YqVkG8gu${2}TJYqhH9T z??WP=xUQxRufs3lt*wW#1-F({I=|?c>rFdf(}W>{&6xYobAh$l-9^prSll0_nCOCQ zz#*5e73{DQ#!3VPZ2GR8$T6ujXEBZ9^@*(B^S7MwbA7*TY?=aV6AuNlp$u{8U-TpU zKj-c>T%)(R`)nM9%g&tI8b{VSF|eC_hOLg-pdm(xn^;o`KLQn0hE4=Ad%ssY7RdI3 z-X6GYQD>^I*$Cj}WiwNKE!WqA45(3+N%NL+6BoZo4(3)jbV|nj&%4Lv{HKar&X6Wo zu1li$(Vh24@5#699ye16)!|L~+3~c`zhCX4GQSvFex+O|%1?CawF%~ZQWONyDMLfi z6)ARb%&FovnXfjoLmFGl=s~ZP34$4B8jrO>fQmUkoodr8JV3juhM1|**z4Apw3K73 zNZE#MuT})-O4tku9^$mY;n5@oHUSyWlG^ zvvzx?s0z|qk42MLUc5DbQz`3&i_CerE+2lJf7f^`;q`kZ(b*lMB1K;VqC3d1anEr( z<|CuCJ{HsbIyQ5w{dvzi+OCuxBa^tl>*u>wzfsz_UU2b9c-7`t6HE}UKs^E#JiH6y z^(^XAIHU|M?@NxtJmRUiG&C0aizlOp^fV-^@(NCs)GQ!8bovCn2@?#+lofP9~6r z@u?I{bvaw5;k>dIt(a3y#{q>oW?@U`yT<=E0;|6tEloGXf9k$IFsG_bduSr?46l|w zd&fLDLpsQ>n0FM_)qGFluus<3$A9zlx>GOqS8AdL6^G6H1B++#k05TT)IO3q7N2#P zVQLy-8EgA`b5BqOp`AUX2vqkSvrEr~2Ul7{{wI>2A2zo15hW-Jji!z8Kb}@u3YJ5V zHCu!3*^7y5#T8%br36(C=mQ6zM%2QT%8*;EB3(>ghUvaz1A3!E9d_z}1=T)ys|p5Y z&HE)rwQH|Yrp?xA2gPrDBg1=(b_|B`aAV^ykELb(gKo2B1$Sc$1)sYzkID;T6tB`5 zKKB~v@ZTRz`SyV(?OVwO!yFqwZvkIm#im5qr`2vF1nH=0j>nGC7q*U5i6AKaU8)AK zdv}Ge?sph<#U5)G9I_#p{E6TwCnaa~fx^W*R+@7gpQ;f0J#OQpNNYK_L zlC^(&wVY^7izl}4WCa!=KlX0G@RIy}2Z(+mJX4ij4XFF2yaE}uli?2?D^)bLR*UV@Fw=cVwRhGQ9CX59@7pyn zVvCjI*5_b*0fO>}3SO=f10`*Q&wpC>xMAsn_iSn$ngMk;9vvKWMm}bo`=x$0f0$Jd zV8c^1^W0r9<>lR@SFSdvp)~!{g%#`5DCA94A&{rGblrIu;Ci69Pka;5u z8x1{^hC+BgpYiHIxM+1#hw)0h@Zzl1%<1F5V88tl(+9Fa15xFCij=DbJ7$b+N@3X%H_9YAFz1EDRyZeY!Xb%rz*7_2Oaft`;${& z^+Z|Zd&W1vMLxI9Dp^}pr|>oBi<#n>t?^I;@&XWE?~PNA2w%LslkL#|@M(3;g|7bnL-uxS~4pSF;SbRLoH zpE(JDuk}OrI6YFCw9-Db(y9w+zSO3Zr6xNezN=`-LIceeNsgXSmyD8EZ{?D0{ZGAb zxz@ohuPraPS|mgqUwotK2>!Mh;Lm0_O8BrLq3dnOK87!uZ|T5*x3pQvWA-uqgR4lT)vT@ll~$W{2j1)bSGZC!|)$PP;^aq zD}QbdW1pGB5`okQ$zfar5Zg=h&Cu!>@dZ33kJf%A4d=|Rjgq80zEp*0?kqz>uRI)} z%5}9Q9&#RWFHKtGgH?fNQH0^r)?Fn)uQ}Q5cx$B zP%E6gQ%AW(Y`TbWU6pHvHDEwmzNJRze{H^Ei1xFtvZ3iFa;??9ljUKmKMw&`tcLp| zngVfGCo=vU&8ccibU4up0+CUd{5i>q^Aoz{U*3Y^!13oEz33)PY*nxXxU0HxH`yw0 zP|w75hW4Zxo5-TN&(7K*B1R|dxn`IDNmVRI`+7l0ht8k{ zw>{;)F@gSDpCoNU1xm`*W9Mln|51M0(Tdq#&-;jq6A$D5Pm5dtq`1zs>3NW@t&2Lx zQ0eyDR;XQN-jU}>^En~VS4qc4)MBIA-0a7cYSiBhFv#jd#@!s$HiAJ^w-%M(B=ad6G9go+@5?@;cOWTe^n5*hBpiH##0 zhR<|eENZ66e2Iemr;R2Y5Se+eptFUTO%t=>W{&MMpNa@=UQ*t+!#F-C{@xQR>*q{2&z50-(IDw7gIvRLnb)i|Sw?M?DGc+xS) zoyqY`!3Bt^Fa=GHK31#^Xl8P(nfD)=|Ml?NT?6Q*8>{+@@dhr6+huH5e`zbH?V0nL zMpsg76#YaqIdj2&{i@e|g%_Inijwm5a|{&=2F1DSlI4rKLE|a}&Z8C=smj&F2mws> z7B@d)nSy>P?6sPrv6u`s?Zj0_=#?o>I#%-%FE~C`&#_eur%*53| z?9DP0obfVGUOyG4m7E_)M!ECWE9rnEySi*Yzq;8cJ(*YW^=#?|?Ag;hgaNAY>X$j@ z{CwE8KGb$u~|mFQ>YV)w?O&cS3vf7iRV1OTV^`lYqV%^^r4tV_d(u&E>^FHfPp- zFj4FX{zH+PkjmXtxyb`pqC|0AtbL4?&ureiPiK=qh4N12S8KhiS5N@45HsfLE_;su zcW4htsXTQCadiJ~qV%1P6z2+wt&^Fu>^^MBO%kraNWsLfj zTu6x5FwRg_7%!qB9VL%ZJ0)sCS;&P|Gz$pd5Y#`NIh8UVpsw95-Vj1LO_|W!Xz?~N zwb#XCf($%~V7y5554tde^H{Nv>cbr$MYA3DK>sl(Z}F=}dV!Yk80v1~KkPvpwT7uP zI0lw>RuA?WfJ!uvDU~_C659_kygjlDhCeYZscL>+KI^(&llCwZ8nIuDqw%PC(n}AD5r_iT%+NhO%F%q?g$Qkm?ZDT$C~vEpO6{b)W`knr<`UQ1q87s>HG2=LjjEFB-wx>8 zCRN!#rERAjd3HUCj@o8Tu-Otrb@`Xdm3iJ=7PK>j_>-@A6i~(+y-pBM99)U|8)KBMtCZt%5>DVrQ*zzPq8&IH2 zzbP+F+wR*??&sL2=+(~eg37pX(q<~$zqp|qMO$5ZcB`C53lnw^Tds0tI2_R_j2lXt z>nrCBP^~SD9n748$f|(O~@Xh@NHAT3b zkgHDvCs0MDn@o_GDNn~e=_$==UNRb#A=q-J<{iLFPI#?v*47&=D(ifJ+?#V%k ziJxUo+QR>Lk?(~}XJX}@hg1ZO>3fL(oa?6|vt#DpFc!ga*!h|r%AHYsW+m1~dW{#b zF?#s!k7<6Qx~sed3!3IsxLOdj;XFBzTVk`1P><7RET|&>_r!ebi1X|DBLp6KPR`iO08c-@cEu$ z^5{n;2MBx42IKiBiVx8r{@q8ncdN-?qtR@X`({a<{GsjpLnv?%_v?FQ{TW6iXTr^! zf3819J2~9mr_yC+(oYf#ec@+Dm%Qopr|j!&(ZKnF!4k>t`Q)pj`qu9GtUI3t5{;}G zCnbAA6lpZGjq?tSKQu36hiqLqqit_%b5!RI3h8I^TEX%B{z@)OXY~uD(}S%vREI#? z&s>pV|IV##?Vn2H%Qo^|<4?2rInkiQ^z!;^_o)qX!xTGnqu?`Vjo3{#A_dOMnL8V@ z;ljwN9L)d2)L(|R*+pC1a4GH%#fv+%xNGs^5HvW&-L=Ktt!S~J39iA4yGw9)3B?PA z_T}08-rw=%e}1iFU2D!c&N;?BVEI&xGXmN&FP|eNUO|5c63|J#)+AB>RL@FR|DbyN z|5~(PyQWOwrw@RDrXLCIZ{u~h^2LV*aXmj|?XayAT-#&XVTWP}T%kzx9zA~S*)x4D zOS;`Cc2y{qxXBpU<8}4er0KoP!BcoYbrdaW8FaBh(fh+&r;5DG2N|82QSK(2RrVam z`z&Kl=w3%lcBL1dA~sgR&hN$D28fx?4c6y#XRx zb$&vNUCUbHTCh=E0}Q7)F9htpb6=DH(^TdnmvB>Qn<2a>=hPEY)qX%w3Nv}?l_xz& z39sx~Psg82{&$V-R+m15E zz7FpvOIYi45$%;A6-)=9H_D$sb{XXU*O8NAR3pJ6>J8obn=%hHlwGZB`kUR>BLBF4 zr5@$o{^lo^c2o!vpKN_(?Fp|hM0J~4CB)igMzGCSF{{KfLXy^T0-=k|VK{7Ii6VpE ztzI=Z7){AAeB|f)yjzZ7sB>eFpc)gPv`>+qs*lLu3W&rY-{n52t0}qg5}o+9N(H&e zl8CD9Gw^*7={twN!x}iY0>&cA`bC!ueQ&wIzPR@EiKe)7p~GJp_mm*NI?YQceNi*l0e>>~Sw7{*wHEa&rio<^$wJne&Ziv~ zL^Ze97bf_+PxG8``3t#o81sBhz$8-rrzdrsxHF=j?)Gl7E^*D2uCU;}0PFD4{`pTm zmvw4==ECZYrX-SCtcO5d(KdV9rsm{G?GJawnr&NQV_NuC=O(i6lIZ-mWd1AGBl_4T z`GwIM*e2|R7|XZg^Rhj5O;mr|zcE~t7JHI}k#K#h)X5&V;l0S>&Q?;ovyPcO)QNqr* zoE$1cMF?3Pycp3d13vF0qONz$C0?X(OMQ!nVJTNvnz=e>Ai-{J4e89Ni(Q&AG!(^b zxz|{FPhm~(^j-Ay7)#X5<~$VGDtbR80WIMV^q~_$zX;)M^n5ZVHA|WrXKDwRo$%4e z{)~KNVElRnt5D}&sl7UQwV^ppMF_p!n+%NZQ6_j)h*hEge#$GhtaCHME=-qeU5}L- zJ0h(X{I*tX+bA$b@QQ77%)zX*W^j57z793{8%i^xXV z3OYq~W=6c3cQ8ySLowfotXmpk%Vr`3(eu5F-Xj9`-O%{_JrjlBg(_%H?f9A=aN=#7 zDF*PN_UrlmLRacvsk@92PDT`>DnE>-jCeVw5BDlaob|S~r&cXX;X})`|4e1~#;fon zAH^BdLhhGoHWBqG_xn3Z&m01AFMgS7val5~WW=YULp&LL1gN6EUSr14GnR5>%G-_#j?=bt z4O_KRCSzVJZrb@WCKO%q{a6ZxSussnp>Qg=3mnpC=d*=hx}`t>PlLsU!l1_PL~&r5vwlD zd4%Ii#{-Rc4rVELU=+5|0M)|9rkJHgGYBHPYPx-eI$v9Ts^=V^JHTHNlMKxzW8d=G zz+KUWjPu4SEHBik>P+X^G*I`Eq1-CuHi{inF_`6oF}^t_Nvcd?-NOLW7g;^B{Hfo- zt6jo#4e4wyFKphO?g>!^6lr8J@51ync4crv+Ev&?iF%E_4~O5OHTu%|giTGw^)#h4 zlH-sWRX-$Pf1CTEv4;SxZSKj3Q}3ZV`vF#{%d!TFP@KrwvbguH!; znnFYY0c6dht%CnX~s%_X0p^(Tn8*ZjP{p#6W=Wx8GYqIYC+TZ+R?0-4wg7E-6751cK=v#`K z1nOFfs&=gTpxXFURvtZX8H4eI*!A?RYok8CSosLsR0lvsS$E5Pjq8$7KA6{w#OgzD zK5)oKbte#57IhoCx#-|;)c+prN;02nNHXpyw_WWH=&dqPt#&$q(8VwIhJwjT7yCjX zS(6Ph0vmv3{0>`K-Z4C0*z`P#us7y#z=uHSqf_@{9hN_Oi^J;%$$}h|;8F;PIj&2_ zOuSKz955c}M|)&d4bDj8kz$MfMH+*V)%1<*hn<&V#q*O$U4a`&Ja}W z=;$3K`u!g@$?x?xnnnFcuifoDr-&@7IaszSk7*9?R58DT=b)%>AtX}M#U2&7SRSbp z-=K}7EHm5XQutp(rezK~{3=9>S-#I>9OK|F>l~iBRnGBh_ub|D26xZk)o!A&8921| z$g+CJYa2y#4g5P{O3Seot*`kgV%jGj^+QF7HFWN0*RE43R+@qBD6nC?&5NuacL;?=d|Lb$t7_+ z#lY8_Ri(s*eeb{=z7v6pD z+Xls~Q<96yi0z^1YapO_(@*Y^{`wkW_ZFRru4hL(FfM7YK1>Y3AN%mj2PfT%Q9TS- zXUdRtc}Qlmidi8Lz#YQsRgOWror(7oT^WAOCIp{JN$H4%zd^3}{ay zbvK3gVgIsw_jBG4J?V(#-w8;M?9V$i?ZFc6+t5A@i5c7{99;QA)7w<|9Qvvf;dbTJ zLOOYoPLZPyG1OiSRDqW9>hPuUZ4Gst0(tzuc}sO&s)u*efu=Avsmvb};-RWUlBQHq z2>>%Z5rp}$*K6wr`-QU^en*{N9T;_a#f#P_SUqMbF<|sqKOn6{PX;R039o~;mE@?W z;?E3uj#wp9W+J-3)*wJX78kzIvM6Jo!i(B!NE4M9zjA=B!+PfI*s6Y5Gy4L++VJT+ z@e1Z`@CEr>@j(8TW0n(+F5>3Sd{M^^NMg3}964mnT z{g3wBXTuV2V^^`nYf@|sjic-czt?pg&g&|!gG~L#5J`G;b|j z1@@wW1I)S`O^FEw-yfnD0jB2$YMmaz@F7`0A~F43OxX~>4QhQdA=Z~t*lo1)94GX1 z)-hjxF{b_{sNXY>1S(!1TFJ#<6nTq#06NtnY++LxWkrSC(0#?Y4Epy!Kt$(n7zR7p z>Ny`J{utY+^-N3F?yH8}S2gK1v)dY48~F>VV6f4TYx;=MHX!LnX)_-18%uJVJusB? z%GPA+?05C2)N?JFSY_-=c2!VzQ%cR8Oyr0L8U46b8fBV_yQxIAJ&`IHy(nux2Qyfy zy19waieHD_R=SN%<4npJ(kvLxuhVhDuV!;?)g0tRdzeg-&WC6i-Y>4sXgvEHHIv4nn^y_O7Icn$qJxnJ7CeVB5n+o z#a(2AkY$GvI^acHnX10hRwn#PV@d0;#bDeC=KJr{|XH4TW z!U&UiR{yNh)_~ZE~_7UrN>Qs^D_i zk;iYG5X^}~a4ko<{SGi360wFPyfVz}qUiyOZ?}8#TBkEEVcUa3WsU9Zf=7XQM3<(> zKO(!~6?XaCm;25%;Dx-H6734n%wr})B)yAr#-?oV8Sfl5-U^z#BJ+h1at#DF)sy$& z-Q_XW6&CD4?_u&g|NkI}D;E-g%+v__3Af?Ivn}k#9QbyL?cy)>!uPP*DV*@ap)37@ zm=+)2F|LQ!zVX16j(%iWKb);y!Dmg%n1d`rZ znq%5#m4{P_-%%8}m==5raIa8U z{3%WR-U$9=evh#M;)Z90b*I$jcwjn@2ya)j+=kJ+1*-kTc`07|u*k3~qwOY|nJH^% zX~)N3B)^buC=C#1A=M$qWh5IcPG3$`neDtR`0tsvHy_+qyiK=sXPd%uKyDM?((W}H zoaxhZ9UeRF3-c9H_nNlg+>WrFe<%*3iC*d=tUW+|COT;Rl-5RIN}$3P6Hl^bI;O;F zcaeA@K{)6$xq)NAI&PNGaV<^Qr<+}0s=L&d46w$Y%Q4iwv9nJ|P!_*GOAaU!_l8Z4cpGj$A>?eQWtx!)$CG zwOu`bzgn<@e0-WG>fcWoU}s7-9COYPRn<^Jy|X=7*Ra5%+y(v$QJo%sSyw8j^mcGr`8Gx#WXKL7?{OO<*NmpGWh6cBd{P8nUBc+D!f_j{?{o#I z_<$qp1*V`YSh+51PCuwx8odV-3-g)@y*Hg#E&rHF=Ko6UNgEs8M-qC>i&b+f2c^xn z7?4(5GQMw@>XcQBv%%-Al2~$>S-@KU5JrA3NEXmcoz%1tSh0ve#^kkK5jv-4s0^@s ztNCsfy~^v;{54$peE zwf&ItjEEcT(7t*_R2{a%s3!VT^oKeg1+Me8BgY7=8e5&+hg;gJL_I-Mv#hHo>@uP4 zDMy#r$SYmgJ6vf8Jb))f^-2!84O6aubJK2j7RkNWh$_;n5Aad!HnF1}mfL);83B>V zX@&QC1147cO|KVR^7!M19D2!*+n9n`;#Dck3e3e~5x#Wj>>G@-FX>4<9eXC$@~qPT zqzwLdBJO*&g>QO7_hJr0ZHl{}QSw(Vx+QTsMEs)G`#9Uu>S{Ad*2~$B)NqW*J`=`B7Pg)eh5IHM|4kvDoiI2>ogex%@ z46Y3#a{1RRoK#-}oM4V^39X=()iQ&CP7_Xc2=`rl!JM6l&iLuOjapK#b^(}ij+SoU zenWgZg6lpP83!XMmGafYOb6XR9((INr2cSRI-5?8p?06}>)*qv!k|oCDSp)Ce$38P z)L#tL=zF(Or|i`Rll^Vgf=edvg0l4 z0;2`vvTU6v5zfXOlzO`h59S%F5tirAk5wL~yYUFI=6_NX>Iik8J2YZevA??I>=aa* zJ_!&n)^18&3jcC#D!qYL5U-^aYVXymFx(gU);F_gwEbfcLeGmn|LpCyB*Vq?<@TFb3 z6Rw<94d1p+&AN#wcS!5;H^(2&bL0R@`o41ScN>2jI2?a>ZmC=OzL}vFDf$OqPgI@r zt`K#2&Ala!+O@gKRLQU4QQ^1e;#o}dw9tuGd30M>3zp#qR?Zq4UR=;Ab*&%`kV8lM%}spS~&y50XfX1xI(e>uq$9tdcQ^2duX=22(` zhC4b}znl;i^N#*MvM$jM(Woh;g2**uxKR2lp=SE2eX6xE?BUrEX)2cWZ>q^#P=$_k z<*PK_^NKCObt4F^N{my--9LL5cJ+j>@i#~T#wGkJSNLm5mZUh#CTYR_KkL-x5^P10OOqjfK(| zq%ueY0_c4nwmX=7vmj6zW_y>5P^={xE)NHC)r$HI1|4VVmJX}D+cXoFi1loSMn##w z<21j??swS2y>RS}#?E{>!&@;f#Z%5atktkYCqd?XAyYFnfh1Z#_qb1xsDoa~S20MD zCSL?#o-HsEFej6vMsJI@SWcjms-^5p2sgr}Lx8g%o24-(ocQ|eDTCV_`6Kk_vrr#RjH=;hznvzOU_oBH( znLMd_W5rapED0cpX9jBnL750=Kv5M{gZ|J@V~+#y^P)aE@ZT&)j*^P4>Z{dqGx3B5 zu%n;3%QKdG-WsYgY~D3XrEtR%@1-JnOcjTu{b<0U{Zd9tGP!Fc!R}#|U|HsTc9d6V}S zk+eJnUxrrTki%{Lm!%aZBWv5sLiqf_>$Z(8>myEo=lrK@c_t<8Yz`NG;c$M%o?~|L z+B1zTp=;d&Wzz4wp$w6-^h&;ed-Z-JxMzlMU8|ZT8b>NS2BSJT$z@(%J9HoYB{<7v zeR}hQ5LJSWHor6_e(;a@B9ZT<8`QA%*m1uqp+)M@-kuSArfDXFzNb!d`q$?l)(VOg zmF!-wR&y%uFlskoDY!lrdFG*LE08NiozRx= zkKBe(&pAb(vTT&cHHC_3>g9aetN&fYMky0iA>Lf{MF)9GSwXX_Hv7MN+UsKuPBBM` zkt1bFiMtpJJE6^p4TEs?U>$24D__B{%0wyc?p+7F)>h@3g(bmG6Au(|R?INj%lS>s=)7~3nVj=mpd=pNX4ct*`0V@~ zItTL=bUY{SP7i6yjQvy%I@foSdwAVL<-gHr+ylZV_hQzQMEe#O+wleWnT)Zn1y)rE zD-f{DGj4MU_dG0aQGVn;*1?!om9;A=kTo$+F=MbnCcgl9i)BwbsTdme*Eii?{_pu6 zL?KscJfZC>*LafuDonmQzmOMk(*Zls^BVSkXs_Z_4r*!x`Z6T#v5JvTtq$b+u^S%B zYAjhdExJ5nQsSxhDd{NDU9GQJ5J>A~fO7a!TX=NK#@`x5udU1hp(9Mf%z_7Dof;wX z1S6)ZN!z(uPilfE%rCZJ znqOJ$zyGKcvan|@HkU08QSEf~U^h?;tjn)3iCQpUqw?F- z+?l)5=11WS9?$tgYDCN-+8bo&pFNmFZ1TjXWXhYg2Tx+PP`K8x+B2Fa{K*3vRg7%C z9ucQGvYgya_G|?%s`~GsgR>p0hH;~Sg{~yePaDkR?^)R+7+S3|oAd?KNC<$%H|#=p z`ACWgePiv zz3Xer`=1DTwo5Bs96%(ae!@!_^*RPm;w`l1909d+=mitRH~_DMdnJN}CM8cnasQ1px_z8}t*)7C?fnEIDT-(Qij^8LJ3 z9AyfHyPnCIg6(!kxc%o>k*XO@`P-Knj#b4oI~7Qrb>aQY)T&&VXA{>9ziWm%3GQtX z^;|zxxAxTN1w`uA=nQlCH4zkNEevVPXb~YnvP3pA&2@>>$Xq5_bLv>XB9}SO-l)g) z9^;ZWn7#-L@}d0B){?dmWBwz5`F>34DBVXym^07JMkL*9vVAwtJ5iK0|7MD$ypV9? zei`k|8JV+Dt~bC3c6=>I4Ydpio@Fq6%m2OHv`+niuO)oMeC0i=bq8gZE)V77G2;OU zrJ+$@UREN8wn~veuSz9IDb{h5AVN3H8qKPOs6~XsM>nNt!BC4rMrT~(=Bm!->jP{d zb4FKf60*XdB0ppjZXxMB=C#7fZiR0JQ#+M$hyX=@+PQp=FufLk2gh^q3|MqJI?%t| z-Gl2_bmMP$wDXElEL;qu9}|0p6`syXK;?S2uV77y_cBK>c5f2O(fLwQwD1l<1p2T( z5q9g6&*|}W+DMlUhyQQIcW3Y`aJ`J(GoxTe<>|BJY|hX@cwQo}2hO`{JW?+OYRFJ? zn^4wv%WIc!1K&%$k_fT6;W6p2_T~NI6<`7U)*$nnQ*w`#U{X9RLwZhGWRyN*;|wEU z1P#1a7{QI?GMS33F%-Ol;nSJwaxE1+&izVr-am|3k=e!`O;e$dxT3e7AH|GLAb7ES*(!qjHXkNyYj7>>nzX7f_ z(9=YCJP$`rx$QjjqM10^7^@43s%*iQ9EYFQi!Z4q6#4SR`aFQi`jMeF@P-0vsS}hT zIJ4wR#*$`M4k2JLU~IPvmpJ<=W5@XR>kyQH!C#KLSNW1J`^%#8>f6=xDsivWso9lP z%{99Z*K^7acG#-m`W>Cr4}NT|+ifomr2e@KFQtu5;kB`fTHiSvF0+igaZIT~l3Rb# z6%6M3QY9YonZ!aszdVfERt34ujrC7)y2@Mmu=?1f^X;(mhsqCxFAB+;Vh=T3>_1)D zy#fSfI;lKb!E7?ZtW=&E-z|JiKmK=l+4sbNrAD_{kYmxO)5KY-+!tZS@~DOeR{@;; zx5ng~Y&h1sd*sGHI5i_W zZ>T`?P1xb}%CJ-Ub+uOv7|l8ZgQk zYCQ)1&hvbjMDp_@)wC4MTb(mi_3Vu}YySPz6y_Jb_ss0(kvuo0hg3fDcah9H|3Ah7 z&#Cx8v*g`1KKzOC2-M0KUc*MK{$l;kIcQI}IivkrCC%R6w^0a77aw|Bntx@#xHBaH zuYP)hEY4T?Ht*H}KRqVG0uW89(t;XvzKq+RAlB=KdtfSl`{Xtp{d@fD`(66Z482{U zsfetsYc4bghNRHE;p;c>zWg>}!GT%ze*BX+8>Q_F-;}jKRw+=NAZ(OOwoxg~+wwIR zjxEH9Xa%v!MX(Ah+w18CZ~oT%@72v3LWj!0W=$kY^==bl=v_VEu~ReeTICTrImk~9 zlL_UEvYX(wdQO`^9u829$@)@;^M{f4>#O(@Uv38l=FwaQ{GBp?R;5$ni$Stx{8rzf zWWvg#t7)&~?%0}A#-qqI?z%!3a8O8@lHd+h8w+^Ea(IpqHW{T~-pdNqUi@UJjM?{Y zv^ggr-<LGIJj1w)$~LjeuVxTe;ULeDFLOfV%K4S- z&E-`(R3NN_qP+A-3(e2z(VMXUw8MX^Lh|J&9Kgtif_a-K_!K{B3cUwq;&_bkH;-${ zywFAurJ9MmHZDQWqw=#cNITimhTprSozcz^pBp8K=?CF((`)VE-mh}MJH96mJOH^- ztsiSL$GNQ;IKjHAIf|rYbo2jc0({8$1+T&A5(Uv5y#N0{+1#~>b!T4f*6fRxwF$>* z?Wc#}?;DMx51YKMKJqCFY)YV1oyrICM^Q#rExlu&jZdoQMW|}8uw13fZPg}>_ldd7 z3;09h-4`;>(#nIDiKPX@*U^WHW@!UaCai4Do8si)o1snlIZDFa`sS~@w76ZZwh69k z9~yuHEGZP@$v(ztTfSq*Imc+vTT91bT}yD-Yo}q>xyERKXH9iG$6+1C=lhWTfq$EI z$V^Chh*gMCNSmk5tW=p)uT;{{<)6QQR{cat97bJ5{emKZnuBtUGC}2f)*fLdAJtIP z>2T`b;a3xaxh!Dh5GT~F^swkQ875BSe!|@0LdrUz^lW0sSuC;b^RG|Dw~F*Ot-I-a zW(G9JcAlkr;wiQnKXpqC)hQ$HjJZi4oYrZ~jA*)NL=@*dJ24BU&~J?EEWq!uW32tx z>zJb>dj;-1cx(?rZ2Q^hg+=iJW5W-yc4Le)fcI3@Tz)KYoKRrih`gs(*Pl zG8ysk2TZ!_ZUV&mM8B;mN6%& zaBzQNal7KyhWd1B8-3)DDVxE{i5rTocsv1@IX)_!vT)!06xyh$t_a<_EB;&d^%!04 zV*8n1X($&UzL+iEgr0{(>`o3F0_Lr_iUcdwOh*>!%UbBEDI zRoe-?FyM3(`Uh_F-sZj6dkly!=S=j_Dc(EE@9%7<+M7aVLxe-%A^0IsV|vXu@Th=8 zH7Gq?@L@*0{IsegoJ&B3#Mv$4&&Gv&KxI31P1X3c^~ZDk4E}*tBgASOFEC$^jkUu_ zWum~)(CYg$m%+~`lhsh|B>it&5|BacO*d;L4*sZ53%Wl)ZJmm~j2Nzg_PiQeW>yy* zxt0R$6RK#hNOkYN*&L%yuV7I2;W(BhmKU*-YfcdhIVa_?#GJD9yP=4J15Fk9C z=`+2sRzK_L7}{~|Sw~Ei*BQE1un&SbqMDj?Ibygpk^c5%{%72yFf6jdt*~GG%!>P( z=x6E2X1XF4Ebsft;*1=@>`xBLO4l>?iBL@l)Ha6eIO^l~ZwC6`9`%of9T1J+jY1)k z%Za9gjLK)?65H6_sJssLwyLEmSIWAC4h9eqRdba0oKiaAm;W-$Mk)JoC?{0?LCd(& zQ?wk%W7LD2{*f!_vr;L}-zizU=WQL{Gv_^Unr?k*`lz>&Rik5pYKxcmK+T#{z?lV< zh^w3Z^L&$M{yJXk+N4j=`Wx7Mv&Y>AnqZu)By51+mDpID`zAmXmaI+Hwxn3)kV2t` zpjtE$?8Uxe8sZuI2(#R zDh9Ged(tz}Tl{qzXR3$Q+2-A)0C$HdmRe3{9AemFh9|Ob-bWxyfOG44=|wS#-ZDw9 z6{cgiYAgDJE@8C``dwo|nW(~v)`r8El!+7OE*DzP_=wgIfe#7H||8kZf|Zr#BiB>TM_fXWIce34UZlli!69xSd~DuJQTwJOlPd$1*j7r*;;m1VfEg)O8GYvc zCgT)`>!|fiRK6t-Kb$!!iE)SUPaFdZ1f~10>y;`1Mn1WKX?^;Sgxbeo>)S;w3; zO#^0Egq~SLL08;|tG6y!=s);~{tn0{9ByFiBszVihd`+Gb54aWs?(qVjJR1*{5wRUQuloq8xG>A|GNHk{UuDVo$V0 z-wLDhdD!56_%2WJS=HKWrLFm+GppBPfr8eC+-s^6-LhWR0;BiT<{B@?6?p>x5cGBI zwsRLsWpH_hPgUF1R3%W>(0!(oN#SoobHW=0;;xvHyahX#$UU4IF70_PnUhTxq7(m) z;ckLs8Xsn_gf=gSXWU+~+)BA%9g!J_*NlYLiS;vTq6p(r(c+P7;^EWoC+21RDd?*G zRcqaro_d;^%m`{qllZsaJ;vn65LKG-ZXsjqrtp8SX>c?+nX>_SuVpZN@F7aYJ$nku zsJ7T+Pr0_D=or*bPuwu8-&-Bt`=_3~m;%KD3B`=y&_V)2e6C_c<=sGe;7WPq zilz7K`vLmqj2;bNY}o=|&TG7g5@#89GcjaQX3d_Is?^dpbhD`+2{`2DK{ZuLe7odA z1f%PcQV`qu{pP@Hnm}IyS`|CE81S@7zASa7=z`tS>|N03uLKJ8bR7lyXZ0Vrt*}BU zZ?)C+#z7wK!d!6oIYB3&xSHVYlJX(JH9z5tb~;Yg9uJhfCJ*!13!vuP*Lz(7S>|zgS zH>nOHnKv~c%(~?h_X-_@j_g!WUq1~Q)UbPzbcx6W*s3gfzI(dohy3~0EfNo0h96ky z58o^yKxWbI><%j5#WoP#sSn5j_GQ@ohI_u1i~aNY{ZOateRH`BraU+cWaTnoC}2ny zO4U16qN-$xV60@w#7g~a@!0*pCYt@Y+$7`52J{qGjptUvEIqHpEDLNuMeM%eeGoEU zU&lXjp51xxMoS;M$vFl6~#nB(-7lY+}d*UVwrKyHn-6i;TxI5bImLKY zMTGt(!o~9oNq%Kv*&=jvfUhIee}U)8pa#Vj%^iowtOhL+C1QWJ&`()ysRqi*!Ac4S zav7gIvwr0E{*Uz`uH;{0UaKOG6jQ4V@E3OSXa2Lgte%}J%JRm|G1Nz{=>tMKP5qC_ zcy`I?D=ArHY*9}YF`2eewC`dN-i4rlp8y1q1AMa;VV3zeV9BlmgL`vklO0hBTcHHV zC|}AK7=dAdtkYz_8>`c2{DlSiRbA#&U(Qmm=Ir~$UPC37GkXz>k6UErvYsW>uoviH z2X}CO!R%4yK-KE|T>0$tUEdC>`Wx?+f>oP8=BB`Re|A$fRmm!=CweE&YTjJjMhvlD zx@(&nuvB~l76nOYS*O^7ZEyAbv&R3n%s=aV%39+05nuNdTt3@#Ef(EBY`?4Naky}Y zGQk7igp@WdY)C0rot6c_;cfJA=?_M%rl{@i>jAQ`Y1PA{G&jkB3$};!jfjRHYUd(J z-ZVh|(K3(Ic&FkTe?#1=`ZD5rx+jAlr_Mozm?x#`zhlbvWmxlzG}n&3x$+P1!je7Kld{4S(*NF$Nr|PZjzH5wS^BaD%tDX@z5d$s-(+OV^Ba&XijLI7_(udYF6mXtc(c**O!hD|Km>FpY+ctKGV%nlz9|M#NoC4Rv^*P zUpoLBXPh-HQnj;iFE9O-VhNWcs%4PUY8lJcK#S1C;jaXv*v6IFj+lRlG!x62vTyWYN(2q8gz~SVX6|%1uwf%*wg|AM5@vp zd7MyMC(tC-={aqmfe#c6p7zzn)0PivF1mC-~LY==^DD9HS*H*0Cz#q{2N1 zL;Yki2q-9*J zj7H8L|EaV=Chn$@_xJxKRqYIaB@VNGH_p;hBr8)n z(SwzFpo;AYN7ccVj@BZEq5Sw4FG$F1%$dMsfH*X zl6R`_1vhsR=jc2Qv&np z8m+s*$#)G&7pM5bN~Tmn4&%K{-2hlF!KMt0qNzZ$U+1S4)-6BslUVIJHBY(}%May|#H8j7ULm=rE z)sLPxLzkmOF+=v!Oe3C>v5HZy>lx9#K*RL3GGJk#D8de5tb3a|Xrb?0?Q+M_de?$Z z3GjpMAI=EsUEu!Na9$<*qi8n?@+?^K2~c+u2YI-kF*(&V-a&&wGs-Js%FG?jEt6cK zm_*w|O-Am?Av6dcDS8fot~`In${KC<#I693 zadlJG#k34hrBnn6>E>FaATJM-8*z?HQ3y1B&YYvuFlY{aT)AuiJ9U}A9B|Kr&(%03 z;l{?pwfjD35hKhM=>=eX}l6SV`b+z!aX@h zRbf)%sz)}t3i{uUF^cBj^zmo&yR7>;pnN@JzPu%8O2TV&P<=j-(`KJzZAJ1J+;JT} zH6u?HT4Yr>nCpt~Hyud~E@ zKFnxrllv7V$(2U$3W4Sd(7&A!S#uSNNLW%M>EyLd z*vf8PCNcc?${vsQ85*n9{HTB^^C|OGe@So@qT-zwS>sqUL$!ktQ&KO#DyfZj6>FP! z{T<$eD25V1S@VOrN)gj(L#-+9nT;jby_WWhG1i(3H&7;P=%AkYejPdVardH=_m2fG zl`$Et*URH&gWPA~qNrA+-Sd734!z}St%kF$s$asZzbs9vz0MGST4LFNPI_no8`t`D zP`0pxx~8Yr;JSUl^6n}AMR1K47Na$#&`HH+-X=>?kqlK9-Z7eGwnn!3_er{2P_>H{Ck%i34v{)GTKX{B-jhR};pr zHvuf3>_y#$z`Cy~os+5sa+FosupNp3z0E#yP5;=XP>{kH)EB`ooqum8i z-DS%<*NpB*pjyYz6U+569E|Z538U>kXz`}k0JU@V+99CrswQFlv-!&qr=-s;(4xcI z^rE4;C?7?`JLsf;o+NdcKj(DJTA6mJu8BmB`vjXS$tlLO6{k;hY4Ow2vixTY?1Daz z^Mi#?R?^y7{bcrh%t=CxWo1g!dz1xr!E<(6$OR2Jl3I1Wn{H~mA06%}4=p$(#fdkL zD`(s_6Gy4D<*YA2-(_beB<+sV8e&!am8ed}9$ID$RICa<7yk@ULae0qW2wGP)v-|4 zkJ;ba`2U?6&8&{$tMdgt6{s95*__9OX+%TWfqV_t)nITxjw3v9May>fBsong@$jVK zM_YRg9zjd0h?cWtl;*i4>o@*q#Q=#Qv5jwNT9W$w(=*5J!%!pWM;z6%JHpAk6*?n*73rR)W)V+S-ZPgV);v?kIH#674)%)L2!V(`Ll+098n0Oj5!JX| zObL%O*`}($HIk673vX82at48PfauvkhzNyMP+RlC{TP>_%i8fSl!2p{8M(DB$e97O zI3VYa0I%lS>T#AluX&&_G_7_~wFT!q879qW)k9E^JhG2@gQmKqxlg^}57`k%f`-Lp zuGuo=YU*BJfKwplERAYn{po}cNVqKEfmb95xwdW>#lch1fej;z;WeQbVD7|a{leAD zB{pc9ZfN5vEqW_Os3Qk>9BLGfvhj<_+c9`~@8b7mdc^;PQycW2Q#W_6K;8v6t9vfw zD?LA`b3enAAh$dA#FXsz|7uM6g{&$q14Ct_6Rwxip4${>9u<-icLU~X>o{;mg69&` z3Nz{fIP3C-+Gs!5J)!o8e)ouat!X|(bfS=AY`WsXOR8?DO^ zGc)`DkGHq}Yx@2E$6ppG3MkU0G>noMlA?6S*v3Y8jF6PZpkpB2-P>pef`ou{*Jz|0 zDTh+}+0)ni`TG6`-(U9AZo76K*W;Y)T<6@+{qFFB+Pyikl7T0PpAzSq?m!dbdCO2J zo_Z!M1GSXk(q;zGLRj(Vde+^$o{(4GF`?hf%^q{V$e6Ai)(yzbkyw~`J&Dr~a8BcI zqpAia5B05}8BaN^4dfd=jd-XWMgdS9UgsNYC#enhd?u{pp@_7VjZIqS~*1PIyea)()*M zHrq6Jn_2lqpjc(=)MH=R+oKs2Tq~+HXS>UAa+SsS<>EE!?Wi9Ewx+vN8BxcySHkh=uC|E!aiiU%*M+wilU}Xr>gNTR zp8uOniXhbK>Weh(F#_YdbE;f)PuHx8kiedSr7E4|K9D5ujJsNue%51?LmsY;R_TPc zjq2tE@{=1Y52?&&&y?letSl~#!^hQnK+O0t=B&|3G1~A}+IP$z9Zz==!ri0ynr|V0xj=f{|dJC?k&5I85;smF*N7aop}eyFO*L_IM6eN15Cl zpVg|y9>;9yq7 z1J;cK&o=<`+ie?h&GL88Uzvx0T_kf<|F+RQJ}@sHhM8vDQ>z@>)f z%>vT42vM^M-HIxGH=7y^f%RZNo8K8;dGN-s&BL=>09mmj+DJWKldczqa5yMmZ1&Hn z^`OTB^;34tNvuOm#*BSU+jhwBJKcz~(beFOO&ZOi)N|ca(Z*ISnGBaDgY{oII3O_+ zMc`Ml_B2B(3C){lpLf-HZL$vH0$h)ciBi69>OmwhsH6v*CHe8`^VB5#R8C^^0&_Bg z4F24g8iq+#QfGawF0+&as6##ha$eQW{9=xSyv1zR zplABhrC+);*)siX2UkdBCLg>bs)O4cdUdCsz+3waCAO^XpY)zCsU}ar@0X$};x$hu zz>hd8rnk&&R9PZgTC+RPf7bt3RCuMQO~_5%r;HU?B+x@zSY2n%Fs~olYevVJHDNjpC`;k#HOIHr4N##aegX0RGV7^1|$Z8ja^j=zUji4kpm4e zTh!Y+0!mmj>{) z2R@}KcoDiPYK)CJ9Bx;0EHq}&g&M==hknhy7Z~6m2+v7}_KJ|4S9rDg5%WClDhwO^ z*f$9!?_=*{C(s~|#>>~}=*P7@`2IWV;QwxC$FF1MKyqq&aOfW16G>IU9sU6E zAIW*b$sOrj?pF1DAE{2F94#~aUy`{sYisr1sULgm>OJ)4lJs_@N;qrfBO;!UM2#PB zor@X0NNr_yva}@C!axS<9v>^l0-IjFoi6az`ZMb1fm6Jexs!Tl;sq1>ohYAtP8_(W* z!Tk1_{C3Vo^imUQ9#~SmIW{u!f!?HZC8~HrwMmX6-v z;S>1y$m0zs55TzSNthT2a-wDw!J^xod7#_Uod3UTbQ_mjjpq>mouW^Zg;l$%_w5Co z$}kmmP;$C2IInz-kH;nz7I(N5|y)V9N7-^Nfm8d)7EhVz}X&?2v15gRK$)idUA37k6hH8M4PV89pLx^k3$ zc|BNw&I|cePP5 z^m>FKf6D{1mZHiBi7X~`Z1-DCt8wEnL+i@@pWnS@S0#97f>v(%a0{--8zmjyjW`Mq z{T2RxJ3Jy-r9VEEKY%roSHkf8st)Q}sC+L`j-zs9<)W8Ywya8t5~m|7PxMuuY4EJ3 z{qU>()%!M=o3X%XC0tdy*69Sy8v719MkcADJYiUXy;owhecM>8@%ABAP66?c`XyU< ze#(!CkUH)sVZATexODZhqBuIXs6KlHn zRHpcTT{vfc*zf$lH+v-NwxpOkY!wLCzdQRM^zgI*ZmtO~CBUzs>onDOwE(=Xc)JY= zz`WhNcb&UcS4KS%Jh^gnF`w5{pe+IM&hT{^T=XTdWg04K`J_CTw5}dH9#2oyFh))V z>)|S!s}zOauXs*Xusvq}1M(c_KrMJWL(mTEZW7#*vDJB*&34m4gk*@}KFMf%inmx8 zp-j(e+>GvE`bG0mheCz5ucs~aW*wA1;wg(gul2p1KN4G6|V!O*Z9ef%-sSu5Aj7|`JPc#NZuI5JhvtU$g#^Mv%phwn=`>6pR!Z}njLxVGqz9|Oy3BcY=;dXW2Aq`-K9um1F&YF01X zmAc?jxVr=QP+wyU<;-o;sLxk{uX#|nz28@VDi5h+lZAst^x|h5-$-A5p{!GomKZT; zi{X@7sbf3!Y+bU z-81{=94M?JI?-)5iSA7 zY4I()+5H;TaGORGSo__BklScI}{Ce`1h{#FB5i|W5FpX`_J$Xi&IUL4*L7X8L^ zP&n%vYr@-CurTO#N5A^6?Jp+Ql^0DGSfxpN1LY_?&`|qpnv`#1ly}7M-Qk&$7sV;w zF2&`W2t-rAN~swx&@l=a4LU`_{yfr&!lwmsUhd+BKLvG)(0k`FCSZTKk=nnMZ73|S zRW%_y=0JUo-=@e|E2Zc@P0G3K|F^lx78lpu&*L-Gv3V9a0ASnTE(g^?`O8K?DdjG= z>N{Gj9wMpy`&~HaH@j+#l6^*gzCIJSK1d33JoTzNXl18q_CFNdNRm!cc2oWx-*6Js z?e@?|-m>f}xEfFMbf$+zU9M08F}#@fNVuT%isfy=1Hc3^{}?sfv-jQfoyV1;I5M?$ z3FhU@(84#Xc9KJ=M-o)>Y-NZ$P8H=XCixk=FS9U~o-8txs?h?}%%@Bc1FMmR{ZZxvi+K!BJEczqHNb%oTe!=IR|;V(@a_PX8dk z=X^<@gg2cq;6ZsY+_9}U2cVYa@tAcN`EFifGkrG^WO`6q>-}}d^F_lFcw$*N`?MuQ zbor%baNFqp0`Bry@sYzt=A5#gtMs&Qe4*Ww&q%JmJmP!x=v;-Tyug{fPeefh2v$}k zLFY9mPh+q)b@q=E6JMX)Jdb-1%wp8_4jk@#oZZOp27cp{He9Sbai^KL)0#bvJy9VM zB6^swT!NS9|vQ$RCrKCp zD|Lhoe>rJ$<6OEf1LDa+NhgeG&y9rFJEZbOPkXjk;Dl!)4o-qD;cVygkPl!Shs_b# zX|&aLUQEN#2cjjLxzUoV3}ia$$H$q5*~MWVLDTa?zAX%sROR*H*d6)+ls;aX!z-hc znJM?4OOA4y#$MUVd+3A-Jxy$NqCu4Mtmj1XLaPP+E-2x;jF9bw=Mb7XcChsC_sCX= z0(k0q#L_JSzgjk8i z+_-_IXH3lKq(QLrMUr)k0M57xS@b!ylM?)j0kBDJ49yVp&7=ACo7N2PdqgbSVRcAGDK zyc{F)w9ZJzpo%_L6wo&HAIeugg)2gXJ%-IC6(%|sZ^z^2tHTc~H-Bcm4BLn&;0G0O zzopgi92UJ&IQ>ks>;Q#Va$n_*`giX+T(WsmQGWOIbzlyV4iaGQI@O1}sgd;!xHBtS zsL@h;>k~&jKRj>G#a{8j{=JSqK|Z5U?eOb}6jX(QR18$P?8TFi{4Phsn0yelUzM(m zbE+stG=6wIg5Sd{Cdh|Tsh5^+AdkEVJ34UL`sz(D^WlqM=bL1<5D;~>7XfoelpJFIht5>zuQc>XTUCR z(=Im2y9~7=O8H^}lm-XclMNO2iKZ7%2d6)HVxKZ@po+GWD9;6YG?9(o+!cSWNi+Il zz%yT9Wm0fX+ued?H;)^!tRGT5(dY{wacjYOhdCq?gx^BZ9>a7C?V^$BtP}^9|C6{*0%zGsMJtmGFe#kIwaW$dgcF0h!PvZfYKIrg= zqMFq!53u4}qc1?CuRh6fHOzu=et2z6EgautKpTM_)_;@^3$XH3Y?2-HhvmokRQ zAUFez8U!PhcYang<`>qF-)O5re(P@h2GersJFJ0M^a3Ltf;2uxoxr~Leamd+{$_S} z6sG#|rfVE&qb|vYPFDA>0vsz(o>*9iTjBe@`lxlaBE5S;oF2Lcf~$(Mw>l^wF|Nna z~_wWq~6w!6;p>f5eFnOJuUi|N=)FVqCAh>3S5CvrKr|& zUGiix{gX?(%*E7qy1!6G9<-iVn4yEbFCqe0gp=H+Bnzkuj#(N1Eg5fENuaHxtv6?k z^%6`rlXYIHku>p5%KZU(g8y~T6 zzO3l*NmIQ3ZxT#^t_0>;fT6VYHE|s23P;&fvBPhk6HtU-<70-^_-C89NbjrCCD({T z0pH~Yja`tP$Us?~vK^zwwbafqO>R*=>Li;J;n#7pR1!(0+ z7QQCgAv@Xq>Z=)@lKtY{r2{QC=>+}u&$S;~RNYk~w^f1|d-w5fse~+^wkVE9uR20a zB5FVsN7sz|PwPr~Xz_>EHOY!qrj+I`I!#pHcKNS#ZL~e_K{0TrTHGMn1R-yUBxnc{ zR02#}#l8?TD$ps&J;rpnm#eTijV~FH5m)IYIQYm}-3hher8axUY)@T|8S=zHN7WK1 zb?7l%*r`{06JUF60`(Ztf~5oh;B^0|mpr8Htju`vxg##E0$G%sl)h;2P3@zP2GZdd zcX>?nz}8+BvNZ1E%t0}wVJ1JUiACj%)oK|zsLig)a?|FH`5q+OCr-)W_FZOK8YDb( zus&&TNPjndO0YGTaPX;rkXH#h!Vn$S4RncQq;4%7x{m&v^(qJZ{9L%*6UUzalM;UW z-pI~2MbvO)XS@+e7v4A{K$B za)c7FKY&ml@VE(at_vG=>{qCujT;E0m4lGsn0|33Dwo~~9;uviukWFzbB$P7 z&G(=YU$TM*t*V&$UT+D&)&^fsTOTLRQ+r3@ob0TE>`fdrULzkg76H#cmCpD>zWi?o zAc*xT%glHvWlyzFTC==gf8HNAGQVkf8A3Tlk)|v&wCYVXe{S|<{PdF}#{j4wk>rs;&q&nkf1dOrgO|AF z^4noAu#sQ8*~nm|1p_zV#z_BA#hdWuKYb9IB_r zP@O!mv@B9VKxkID?qq_}$z-e2Py2u+$wkq-b}~byNp;eANT%ex`T)DUubM5@bpMbS zuN|T1NqIFI{HudszvSaCNQn!`DhrS6jvR7@IJ1S=ObailYiZ^#dngR`?+ntvKjm>D zcF>8kL45FCnDb%w;k8;{5(5Q=o%7eb!XhL}BRsB_QZ^XIbrSetfL!kJ;wu_gYE&YU^h?;mYh-*|uVznaws;SkIT=!0C7XRLlpiKGG}k zPXj=5tyns-ZA0i?Z|^(*6UYk?+*9|-;^^aB_DN;5BW3(E+V(^3fPQPhH5H@4GRgNb z6XGM%Ls4l+lG@tpy=fm*9PmXzVrXbT`W`lSbJTHiTFSXV{3qod$g`VV$2t?^=DPNc z?-zc{1DzY_vh<>rUF77CPrI;%T-Eyva>9}V48}?6m!Fiq6!S0?c6^XsK1KmcdNm8$ z^aA<|3ohytDfV?Qx`>UsWsAXlx*pex=cqoe#;4PdZZzsdD1#otS3Z*1%NTa8o#>os zF!xe5a%U6?1wcY~%bx~VB_<}EV5Uv2=mw;RW7m92RcL6EpkSD4-xQKcFkA0?tY7R01SYrb9ky!otd!o|g+3TIh zJ~tJ`*_EYz*q=Yl>L`2HO{r#k=zQEXISG=qrt+lN2NvHkDB#~*xf`=8{cSdq_*3xK zugE9AhIhW#O(>I=yOvc1i|T;YcmW{KsZh=$m>Nhh13TOepyh zq{E|kNM>=e_g5hg@Yd34ZA4?yb|Uy@6K^8P&r7|mCNn`P@vyez$fb;6#_ix&KS1^< z7Z&=b0M;>`EW1h}qZfA%;0C!wA)c_nlxdYne=yX5oj??HLmA$p_;RWLexu;qe&$1- zb{0@VKWQ@kc-mGWcTWN=WexVNMPe~7QgdV@sjTWjyd0}vz)JAZ+xGuGX6wAA0LdHo z6s;8rpqT`DR#hH*n6RH$r>*UP$(KE+5)yS|K`DFB#<(MJ%nFQhpmcVi+)|?1Ps~Y(9UJH)7W%Rbw zr&yBGaEg=o*14xYo0&sabB%_Zntw4zCyCqA%ehvL;zDX)~Jj1zmd>}G$)duB-DSp`O48sL7=JdKWc6ODdVYk7WP3E zncAUzrP>^%B@H%~%*hs_O_p?<7Hq~?>Ur)g1Aj02B77$2#Rp;leR@C1i>O}@w5wu$ zg-JFAob+x;9X*oWsr$_xmM8u$UFAyvxCac_r8)!YKKX>dOq^9`Tx)8V&U>lL^AK)+jaW;!hfRi9h8qf@h7B8y9R?NStLFnBz_Wz5H3- zl8Aj$Pav7$r(vdxJ+T29=`Z}dh^&;L01{+FvL92w%vtfU+-PvSX~pVOhS+>|i=zw~ z1*625y1M|WJJbxv8)(Mloto%uUv=ZfP08u?I!rPXCKg`<$MyMZ-FZvAzr5H6eL1dz z$PS|vg(9-w$1i2M`LIiDB=zq-=4uX3{@A`3^!rf%-^*tXU*^xR=CI1xWfy;>hAl^a z%?BSfJylKwk9Qc9x$lcB8IOQ-gr}8e0b|Mc7xBVOI)vIp2PEUW#5~0bY4~ zh@yI3K4vcehFZZL$vnESGg=L6puA*jMK@=p9Wbzm-l&sf;me)O&Q}P#TVV^7LvNh< zZ*7`$O=rA|%&Z78uQO5*7zzGO+riJ&Nher2McKd4-tZ>Vod)K-Leel1@Ath2TWrqS zWd5Js)7l*Gy6Tp#|GYg)t$j++YKpqsSLP9}^;t1As8>ot;dM+B&`nR^bpDTkUo(b! zU}soWSbwwkC0+jNxIWHVZYle6`^*%89%3KSiXE^v>u0Em^V}?_{eD{X;W}3e^d!*CmENb61ueDOa*XatIs8F za$)3aGZP#-liKinIDg()%>Ay$?n7%PDr)>h^|z9Lp#o#mPP+dqIUj!IosvW8->D~5 zH_nGSV%M86N12W9ilv`QahG!l>otM&J7Lc|t%83XotQ!5`!QR~dcZT=qTJpYZ4QmHy1J8Puq9w2_=xLBY@XgJMmY2Ba4ry58%XI4=7bDE{SLodS4QKivwpI+Iu^V_td&R znbf-oHMde(Ieq&}l8_!tHCQ(yGY8*I|mlOJ3B{H>YWqEF99 zg#%mEfUp!U2HErLcY$EtMow6>GNUcD8wQ&5|gQWqS!tO)?DNMiJ;l0ET z`V(fw3Nv6w(dEa=5^0Op$$pQ2qIXAg-`4hj*XjMdrPuIR)+;%DMdr=C;g2K~F1B{T z9wYGfJ?IX*6gDXZ)i3D79>!HU=`Twk@G%F({pvkc0k&Tko>WLis+%*RNdROP@hkOG zrU54GVj^lHS?}2hi)*4YQ_v?y%mZHMoA@`3F~8@cpM{6Q7+F~W9F%uf?DKwQs%toH zC=#6yP8*cIv)R^=AywvTLXg>5QmDV3Lo+qyKE>85$Mi}B)>G4u>R}sGesaGHeszoG zw(JwSJB>EaCly^iil)1BX)erKz20666M%u;?)J8=4D);Kl5`fQwOs3&&4neyqma+* zKz;&)Nk3^|RM;fWCpkfMT>!3PE0!E1EMM$5eV=$d^VM6NIsZnHZj@gm4akp9^g|Qr z5)DnWx(k#@Jn`=$sy4Jj=@F(Dgb1sVG4PzMw?JV=OJdK~eXCzk32?N|~xJi*d# z>i4~mko{M#-#2aORr6tyT_+|dI)N{F(~-*!X#2#ZhQALEXV;FS?)pefr5luv_-cMP z)cl#})cin$2{Ip`sGYUeigQ)(`edU>`LX1L79MAi-RJ5Iyf>80nAC`5H2FR;TnBd| zW3#V+R5wSdu`c#`+!g;Q?Pl~lQPH&i7tDA#VmH$qo_;Ah9_=@NSLun?^vK)+5!CIkk3Q4c=o8W*|em->?X!6IHtWGe0=@9FQm_L;uRO z;@?yGEwTNbuw~%a)YOdLc(M!m)}la<${S)B)`Ooo_)LjBTu{Xm<5nwvcR^MF|cI%6aPAa9Em2MCEqA6 z2s~fume@Z>7}|)~KNDUWIlX9{6<}zzA_)NM%;~0W>_U?x5Z! z|D$vZ+6&$|FRwR8tv@N_mE2T7earZpdh>o1)htC2Z8i7SD31hlQJgoNudNpW7jUpg z$-Aj(jT#b{l{@(#J~Z|W9hOw?+RrT?KDsV&gMIgwQIk!QzpEp4yptza8VaBUlO{`Y zN0Ib?crXcOP6x2cK7Uy}pr@@H!Gp4N%Mgp=7_q)3<3SbIELBTUAZ`rJ&Jj?=LKHQ@ ztDgWU_W=&w+p{eX9^|b9V%Jg4>yP*o8l}Hq*Qnd{!_oxV3UeMDw~h$@m}1}EZL9NI za)m;+e*b2swW00T2^9$VxU@G~$o8}){9X7DRz=307S-DOBmkI&yk%@l18ss)B}X#M zO%KRfyfo?O-gyW=WNeA(8v;iBEMfU(8*ujGb2;{>gveXz9PH9{1=4?{@sTx9#}Wt% zn%V7qJwe7>o`y$B2TuRcvOWJ*BqC&z5x8+OCIajAyu&EcO=~$=t+wMAM%8)4jm?6W zYqbik1b$;bsq<=3IBT^&a*`yaG|U#?!>2WFhU}^9UZZ7WyBP@lUHzDRwo>L--&B)& z?6y|G4Ovy6{4;a&hE;fMx1AAvm>)@r>Y8)Ij(z5y(2FP2l_Ga&p=%oRspeHq#a_ka zfuc_*1oVRarn*YgZPIre790{{wi%2}`SqYbSM1wxx-B2r7IVY!hrV2Tf)gJNjGxso zrwLpb7h4w#ZbBK)bv|1Az+HLBY0%mMz2SCS!DYLFV7fdB@TxIiu zypKp&IabNTU+YIZrN#Xz3&v`Hn7%zTp>b&uFpLF<^`fTqZ&_+(F9{tkQ4Wpwr|SDg z)my-5%%rCGa>@4IRTK@52^dGVEPSwCr0qHU5?3+GVdPa#R^43rZEx$7&UtwF>n!_j zTeOC|f2l+9JpnMMo|qm_m$Nls7wB)xU%pWCZKthcp|$9+%1h$FUsbk!CNQTjYP-T$ z94>$TF@0#pIob>OwH#qhB2@cr#oC^;oy&V%8P7gapZ4abeMYi9PmE_M^+`F-rDV$B zy`t9CZDbd}ULmA^YmUA95};TIaTW)s$-2&*9B-OspA^tkPc}pgb<>`XtQ%UdEq2zR zVu0Yz`)+H<2Di9#7FpJMy+r@TUW+*s<4Mh|Dfuigrk%ccCmk7^ScU1-uD5SETQ=B2 z5D&ZEA*%m)a19$vO2u&+Z9ki5;Ni$@Q#YNge3tzjpB)%!yGhB*0`zAF9? zxv#ctM|f+&p?lZonNhyME#a2VGZy25Eg^KLWNdxxwtfHe(yYG3iP!M0(eER|61x2> zcGmbObw3KiN&NIor=xsBZ9hl9QS%TRsb{h2&F=I~=XB4h^OSt!X<=2f-S<-vJ?Q_u=rvt$!H0ideA(yXa`a;;YD<$1p>V6a5@Qod43FkFAhdLMBafoH zg&u3D8=+H(x|_pzly{yP?M{ZjH!b6!RgV}R6Nt$wgv>wzhFBv%&wE?N%L-v@Os&&G zB{?hFv8z(DW3N3EH|Bro3Mheze){G(m@N0+_f>tpL%ctPV>RM#IrY+^*&$mjzh6Ln zU~zWXtMumj9rd@a;iah--=LN!prx%-zl&m%qVFlL=*0Ic%-O4a***edK>gF>vWRp_ zpD*3V3zn?U{bc=Xuye~$&uTQ$6%p{+uzw&;8@t-$AT*{~2c5HAmm2APmn*^dmr~sNuNaNzY~Yx6ZKa_Pp%&FU(^pKORy8AIBP&N_omA z*JnVT+t;CMs)in|&J(A&H`m83MxK_Vf#tMOzOQnpH5l`_+w@#rJUIC$I0CFdvd;%a@OD zX_O}q4===)?B_WP9coU5zf>3cb3PWOpPnPL@ql;T>)Bsh3{|gTJY>9F89`5#lU4&d z0et6)>8OHGapjiq;KPF7yL5*O_^4TO!&0X2!Y}24bWy6OYUEwEn~3*NyV^G^8fB(C z2GKhfi>04unwS^}q1aL-cE^6Pa>U^?*|tC9emHKd8-~p7ni6`Bmwd2WRWZsqG%8GR zE1Q0P|3HEIm}N6R7u23*b_Q=JdVDZ)YiyHqj(bH_Ya+GR>|2iQ;bZqDkM*CLa4JGn{0QOl5C240AC*qN^zPScRdZ8|TP~|~x z5hcDgMxCw-wYW^TyGTfyDONxYYV@Lmr-EN!Z-&ozzGN(Rl`C{H;I= zb^6GS=y7ngt;Cx;n7T{R9q~~_Jr2R*#~~^2-ce@L?JqMiNl1l$PCeYWLunxcOYQ4` zslvoz25M4Kc9sC!O;D&VXgDOz2uNC~#`Yn|&irNlHxy)$A<=hkJo5vxlhA3{{VIuv z_!Qd^j?8ZVi$oh?!JMqiPae|M6oz+%M6{{=i;+#rp=Bl*KLw8~#(v5NcEw5#`8HJn zk(_pJ>lchcdf2bM-7dxCT9b-drBf+QK|pB-M9b`ELd9q6YxeJNW@*{fd)JD1Zpz$ zk<57>g*QScRGFr3PTOOg>EAs`5b=btjsM!c+lBIJw{I&vK9Ul0Ad?v8iB5TLNOQF z40`HGvVE*6NGob=bE|1=RkLlYpu|B0R(}onMvwko|J6W$#C}m9xZl>DdA-NvhFC%+ zYRmylD*(Q=hl(YI^pv7p3WaRZU3SJvj-kOs2&63Vt{;NZRnOBEyAIPlDz$T=lP`lyUtxL`9Z z0}_4w@|w=D-%7+ksg)F-9+ig>*WWQ3rt2~Lo02^VAAI8UXT_v!r*JUaXDlPpx6-X+ z7=!^2ZDUbPW7|n=^S&HsDCO4>dUcJQ^A-IanV!u3Xx!TfVRN^lVu+a38lG6&qD}Z8 zpRbmi4Sy*(RGsuhM*q&(XYdQ()ZH3=-tHT`Wkj}mruz8Q>L#W2RPAehs#Vf2Cqe_O zJrR0drb+(wqxGmJLC$o7sT zowm&+EcXBNc>eHNz2GZ5L?O;(+7`k~0e5Qw$QnTO?~1G57RUvWStL@B=1o#x3^n~f z&XoG~$<{fB^7T0+IuJ$q$W&xWeK4w^arXSCy1QT^VZ}^x%QC|E2&NDhZvP->H#!m9o6I}D;<9vYw^#z zr(Pd*4Lw2np1QW4I(}G7`!WN-uU^RCKeV|9qORR7%-$`zL0=h>Uif~u;QI{pocsVV zEt~MyGE-|fBd(R;fQ=2*ggSqZhQEBD`TsoG{$oYPKGW_^vnLKIWmFLQB@TyyXfD$%J!8u%p9WsY_^a_+P-ZC0I+q)*ocmh zLQdU9wwDfn!B9IyBh2eThITltD@uy zP8o-3PTlQ66t5j)HW6i^s4H6sF!(*n&Dz})e1G@)dD6_27{#IR7w8SxR3tZtL1oe> zT$&_&&4VHt_=*jvz%?5B_sLeA>|stSAm?p_ zJj$MuTmWoX9JV*LNV+s9_oE|8jw_1yfssbgq0)(a>XM3HLyau;nC;e*dzXqBsuVk0^jTAaQH@40Rzj z5hGS`5h2VydO4}7;~KTPiHJ*ve_jzr^~CnvcysL63dt4yMOXA=Q81T`)>L z@HVtiHh)YSqLF;y9~*jTH^7=$%k@g&<|I|@ZY(@YDXrJ`HU8^o)UxIfP=(7}DPU6n z8df5X|4xfz<)45X4RxJr^hFH1#K*(de0x_GZq%*^> zcde}du>9k0?i#wr_(^d2B=a}A5Bf;Uv5%kz?tCCIXD#)m+}E+y@N37ZI7y0a6kwsh)Ev zya7fDRh*7!k84R>5%vZiJA0#W9mbon&Rw2(*qU(1wS>hJ!Mh^xIXF06lQ@jHWskuFF`)9MeS zQ^!ywI17|>jTE0^X55p`8gP3rU;$SzKcAGnqwi2=o;bW)21S(A18`r&$5%S9P9hs< zE=w7fsMsdBf_uNSeIUO1$ch__Rpf0zpL%ScM44kZ-|flhqT9mQ;*s{NvzxPx$#lv z9Z7X`bqQp4fB*4p4vo9?>#xTA|Ajw=<}UFEl|$! zZNA{ifWG~%CwE}wQ9gOMK@ur)>VZg1;7M@7SQv=?!N|5gjC2 zaVT;jZ=kyTu_#m994(wyOmXl++UZLs)XWV1YoXGZv8-fwGP@v)&>q=-x}j!V)QK6f z^?R%z^QE(UMWeHZI>n-Kv+F8kz#dy5X)jg}%x06CO&>TD?dN__IrdG07yCE#{4qr> z>jNPWD)@8+D0s}HJ$Ta$%y5(?YI;h0_S`{A0Gm(~VV!bTV}iyv(MB1(&!Iw*LhBT# zl^WAG+?%wfm*LYccwY_s!s1=9X1K(I1ofe>80a=1f7e`-nOhY0VZ&T_Le@Z9TIymM zEtK37dTZmC_J8wIX`p8&0$qbDGg)UkctoenfNC|c6fK`ry#@ZD?u2t2`VwtZ-Jrjv zxPS4+7^PnKq+u@CX|6!tKpAPUTM999mDy0b+P%BSR_z6o5*N`Z04hciG+Cykv!Xkb zmhE`44=N=?xL~3B{F5&Zx_j!C_Hnm8^#g5O`~cR;lM6*dg&c!NRTJ_ zzFk#l_f-%Nk<12oyD-js6xU1NbW1RO1Q%#reCxP-`G#?Xt<&liiz7nD1e!zdUR^M< zN1dGE8RR^5Q0*07uxID=p?FWj(Ft%+F-cd~$duERifdSbaqaAW%Dk6 zEUGZ%+WP&t-aH%d)s_yp3?DcrxBth-w6la!sC2*=&@PG!H?C#eS(1*X5lGk^uDT}WgX3ANOtSp_*@?+DJJ(VkS7G-gUX{RplX9_WhcMg)Vqoc5RKvmLR3_ika| zaV^I_Ts=5@(E58=s40H|rn^6R8SdA(Nk1&O63F242|1=*Ptij!@9zQ82cM$CW;7qrP71uV4l*IvAFGQ_T#bVgevWjH*(Ee3OQO9*V!Uy z$U!^Ol^8@j#VWmHbG5^nxl6y8tM1YIW?PbRr|P07b9AjvDaCc@R#R-j&)qFNToVV={8blG&AUI-uXUFW$&>ru!+W>2CL0xMDhMOZ^Za1|9L< ze9X-xRL(h*e_8!ENbJN{8XzKxJ)sO(eL=i#TW`iQk~aFJ1Cia(&(ZCno0f81?*5p1 z2l7ylyW~wTd0lcvFT98cklxc%bFs!Uxg%?q2d=eo23HUMVtUzIveu1AOde*r!S!S~T&FcUDOlz(apdud+g8CtHh zbr#k$7Wdf_9(AK`)=gTi3w1Ysb%b&#;;nAdW4P4mwAw{OwUX-|*OO7cq~}9{c)<&jOtTkUcwog$$B8? z`e;RDVpz)l+!!fL@m6>1E*xl=9>kDRq}6q}(nERxhk8;=bb?x9L0h=;9=s~q)eCx2 z&+A`su@$(|K}_ljZPR68V2|or4C@F!^%YvW9iv-=v0aC=Jxa|xbhUPCGoCezXBZyQ z_$lgs7xky(SA#f~a*_6;bl{vZ#u#JF|22O)UvhzJsSbA4fOTWFQb!zXV9e^AvZv41 z>6#R?2QI>tMr#=Eb*46GC)TqT!`Y=vb*1hMv$~tT`}7zd^rUXnHEchNO+6Fu3bxV^ zouVDuhco>vZuN>@(-*0Cl~(Cu-Kcw6F2}H*!K$9drIMlDiHALflWhvWx`pds!rHFG zzb=h�@OnalYPKsf#$WhC7VF42R)XLp6q{P5H8nF~%5UJ`s){{U1+N{Kz|Y8P$y5 ztHu%M-dJtaP3_eeADX6_ni_LDpB%FfrjfyAnxg?Y*E;Rixs-)$U8U=>ribvV`*nj3 zvh_4Bltu_|*R2@V3)-WTA`WXY)^(WkFY9X<*4Op04rsgfa^-D$Jgz>3X?>aUG_9rH zuUFW26BJ%_cWOr-}Rqv?4nQA^4swhnPsiDuzgC*yz zF~)piB^`;mH)u5a#caK)Ig2YOwH9wtyy~ESN$~%ze=+?PtknrY8iHwVxM+u z8$L5y6C!{6Ia-AIT#x@eq^Gc-ui{geV@l7mb)UY%=k2)EOWL7v>aH2usJnHKp4aPo zRd4Ix^qAh)C3+;{vc9ZmID4J$pyqSf*Be~(ir&JtZlumvSstR!SGoFfU4)fg%6cIN z*nu*EdMPW!(ujVa8re&yXgprmRbBo}ubnZ*7-P)eMrB;;qZy&fs_qn^+gv7eW5 zpog)Yhp?lUv80#vAnUhvK5g~ZT%8@Gfp6>Ely~%ExYcF2(Q|r}qpxuF-B{OGaIo*{ zJ^ffe;JT-2|5eUEgS~xIud$!z+qq2F=nBm35_OBY5pUI9T8CjR;ZBor!u7b{B)&sO zHMLeFKuddQUP0&UF)q@~?UW zFWRp&Xn6$YbCGVwm)_C$^gicridl0~T-N)PZ(&ytV^XhSW#7_I^`9(1jJ%{@*EjI4 zm$9zzVq;&3d3SE3=H-->b#yLnb_CyAg9%R89G-MB&wiRlNA`(U{&z6O7-P&QLJro{ zJo|oNGc|}CWMT7C6QR=TDnAWLbSS47?YHg%! zr(B3vU4$3yjraJ{+kJ<%g(LT3SYN@WzOJ|QA};iz-o~t6XYVzAo9otV0Ie><#qPk9 z-ovMUu3zbESlOY7Xa5;C^L`x&wjKxr`v&J;*1P(pevE}ZfnPnK zd${`%_2x;Z=ms5#obRi!tuuL|)jZuCO;%S8;)<&NhcLz%W6UQ)Rn@?$8e&cz)CP-c zt(Le|U3{yE&rbMMq4L>l#+go-NU^%Jon{~$qp>Wf=tRxa8Cr=QZD!eqU0tO8I;cx@ zH)eA>{mb%S!7$d4a0g17y34~^&)=tMr?nNts8X~-Zm6ldlWnRK0fse{kwjv z2P3C@inDr+WB);o=k*-xpW#uz%2ejack~#B^%AYTNjvZ1T)&~!m-Q@HKCXv!6YUPv zRPKK%{CU*2){eb0NU4UUt)Jd9)hdrzJ^-KL+zsIpsR;+JQ z?+!hTWBrPnuT%GReM`T{4=VI%4EvW;%hk8!5# z0PB>$;WC`&GJP2fdR@>lYZ%Gx~{sj5Ga|&+G8Caol6FcI$v%)lm%VzaonL zWjyOuy%e+PysF>mhdA0VSpPf>>z5q+H?Db3Z)PfHs^V99nLLiXr7vN27hrg4_MTbV z8uRpAO}RC)kuD3vTC63J&1;+n^S%F=emwbhegk8SF(2Xo0Z>Z;0u%!j000080F#(d zTq)W6Vs#b)0Kx<;01E&B00000000000JMPs0000^GBH;%H8(D3XLL|Y1qJ{B00031 P0RTAw005pA0RR91_9a94 literal 0 HcmV?d00001 diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py index b3219836ea200a..33d423f9e556dd 100644 --- a/Tools/autotest/vehicle_test_suite.py +++ b/Tools/autotest/vehicle_test_suite.py @@ -15030,18 +15030,19 @@ def FenceRelativeToHomeMaxAlt(self): def FenceRelativeToHomeMinAlt(self): '''fence min-alt threshold is measured relative to home, not EKF origin''' - self.set_parameters(self.FenceRelativeToHome_params()) - self.wait_ready_to_arm() - original_home = self.mav.location() - home_ofs = 20 fence_alt_min = 5 # m above home = 25 m above origin - offset_home = self.offset_location_up(original_home, home_ofs) - self.set_home(offset_home) - self.set_parameters({ + params = self.FenceRelativeToHome_params() + params.update({ "FENCE_TYPE": 8, # ALT_MIN only "FENCE_ALT_MIN": fence_alt_min, "FENCE_ALT_MAX": 50, # generous ceiling }) + self.set_parameters(params) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = 20 + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() self.set_rc(3, 1200) @@ -15074,20 +15075,21 @@ def FenceRelativeToHomeMaxAltOriginAbove(self): def FenceRelativeToHomeMinAltOriginAbove(self): '''fence min-alt relative to home when origin is above home''' - self.set_parameters(self.FenceRelativeToHome_params()) - self.wait_ready_to_arm() - original_home = self.mav.location() - home_ofs = -20 # 26 m above home = 6 m above origin; wrong origin-frame would # see vehicle (10 m above origin) as below the 26 m fence → breach fence_alt_min = 26 - offset_home = self.offset_location_up(original_home, home_ofs) - self.set_home(offset_home) - self.set_parameters({ + params = self.FenceRelativeToHome_params() + params.update({ "FENCE_TYPE": 8, # ALT_MIN only "FENCE_ALT_MIN": fence_alt_min, "FENCE_ALT_MAX": 50, # generous ceiling }) + self.set_parameters(params) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = -20 + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) self.takeoff(30, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() self.assert_mode_is(self.FenceRelative_TakeoffMode()) @@ -15099,20 +15101,23 @@ def FenceRelativeToHomeMinAltOriginAbove(self): def FenceRelativeToHomeCliff(self): '''home-relative min fence below arming altitude requires cliff to breach''' + self.install_terrain_handlers_context() self.customise_SITL_commandline(["--home", "KalaupapaCliffs"]) - self.set_parameters(self.FenceRelativeToHome_params()) - self.wait_ready_to_arm() - original_home = self.mav.location() - home_ofs = -20 fence_alt_min = 5 # m above home = ~150 m AMSL, 15 m below arming alt - offset_home = self.offset_location_up(original_home, home_ofs) - self.set_home(offset_home) - self.set_parameters({ + params = self.FenceRelativeToHome_params() + params.update({ "FENCE_TYPE": 8, # ALT_MIN only "FENCE_ALT_MIN": fence_alt_min, "FENCE_ALT_MAX": 80, # generous ceiling "FENCE_RADIUS": 10000, # ensure circle fence cannot limit north flight + "TERRAIN_ENABLE": 1, }) + self.set_parameters(params) + self.wait_ready_to_arm() + original_home = self.mav.location() + home_ofs = -20 + offset_home = self.offset_location_up(original_home, home_ofs) + self.set_home(offset_home) self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() self.assert_mode_is(self.FenceRelative_TakeoffMode()) @@ -15124,6 +15129,8 @@ def FenceRelativeToHomeCliff(self): def FenceRelativeToOriginMaxAlt(self): '''fence max-alt threshold is measured relative to EKF origin, not home''' + # restart at default home in case a prior test moved to another location + self.customise_SITL_commandline([]) self.set_parameters(self.FenceRelativeToOrigin_params()) self.wait_ready_to_arm() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 @@ -15145,17 +15152,18 @@ def FenceRelativeToOriginMaxAlt(self): def FenceRelativeToOriginMinAlt(self): '''fence min-alt threshold is measured relative to EKF origin, not home''' - self.set_parameters(self.FenceRelativeToOrigin_params()) - self.wait_ready_to_arm() - origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 fence_alt_min = 3 # m above origin = 23 m above home - original_home = self.mav.location() - self.set_home(self.offset_location_up(original_home, -20)) - self.set_parameters({ + params = self.FenceRelativeToOrigin_params() + params.update({ "FENCE_TYPE": 8, # ALT_MIN only "FENCE_ALT_MIN": fence_alt_min, "FENCE_ALT_MAX": 50, # generous ceiling }) + self.set_parameters(params) + self.wait_ready_to_arm() + origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 + original_home = self.mav.location() + self.set_home(self.offset_location_up(original_home, -20)) self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() self.set_rc(3, 1200) @@ -15167,16 +15175,21 @@ def FenceRelativeToOriginMinAlt(self): def FenceRelativeToOriginMaxAltHomeAbove(self): '''fence max-alt relative to origin when home is above origin''' self.set_parameters(self.FenceRelativeToOrigin_params()) + # reboot to clear stale home offset from prior test + self.reboot_sitl() self.wait_ready_to_arm() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 fence_alt_max = 50 # m above origin = 30 m above home + # take off first from home==origin so relative alt starts at 0 + self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) + # now move home 20 m above origin; vehicle at 10 m above origin + # is safely below the fence max at 50 m above origin original_home = self.mav.location() - self.set_home(self.offset_location_up(original_home, 20)) + self.set_home(self.offset_location_up(original_home, 10)) self.set_parameters({ "FENCE_TYPE": 1, # ALT_MAX only "FENCE_ALT_MAX": fence_alt_max, }) - self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() self.assert_mode_is(self.FenceRelative_TakeoffMode()) self.set_rc(3, 1800) @@ -15187,22 +15200,28 @@ def FenceRelativeToOriginMaxAltHomeAbove(self): def FenceRelativeToOriginMinAltHomeAbove(self): '''fence min-alt relative to origin when home is above origin''' - self.set_parameters(self.FenceRelativeToOrigin_params()) - self.wait_ready_to_arm() - origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 # 15 m above origin = 180 m AMSL (5 m below home); vehicle at # 10 m above home = 30 m above origin is above the origin fence. # A home-frame interpretation would place the fence at 15 m above # home = 200 m AMSL, above the vehicle at 195 m → immediate breach. fence_alt_min = 15 - original_home = self.mav.location() - self.set_home(self.offset_location_up(original_home, 20)) - self.set_parameters({ + params = self.FenceRelativeToOrigin_params() + params.update({ "FENCE_TYPE": 8, # ALT_MIN only "FENCE_ALT_MIN": fence_alt_min, "FENCE_ALT_MAX": 80, # generous ceiling }) - self.takeoff(10, mode=self.FenceRelative_TakeoffMode()) + self.set_parameters(params) + # reboot to clear stale home offset from prior test + self.reboot_sitl() + self.wait_ready_to_arm() + origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 + # take off first from home==origin so relative alt starts at 0 + self.takeoff(30, mode=self.FenceRelative_TakeoffMode()) + # now move home 20 m above origin; vehicle at 30 m above origin + # is safely above the fence min at 15 m above origin + original_home = self.mav.location() + self.set_home(self.offset_location_up(original_home, -10)) self.do_fence_enable() self.assert_mode_is(self.FenceRelative_TakeoffMode()) self.set_rc(3, 1200) @@ -15214,6 +15233,8 @@ def FenceRelativeToOriginMinAltHomeAbove(self): def FenceRelativeToAMSLMaxAlt(self): '''fence max-alt threshold is interpreted as AMSL, not home-relative''' self.set_parameters(self.FenceRelativeToAMSL_params()) + # reboot clears any lingering breach state from prior tests + self.reboot_sitl() self.wait_ready_to_arm() ggo = self.poll_message("GPS_GLOBAL_ORIGIN") origin_alt_m = ggo.altitude / 1000.0 # mm -> m @@ -15234,6 +15255,8 @@ def FenceRelativeToAMSLMaxAlt(self): def FenceRelativeToAMSLMinAlt(self): '''fence min-alt threshold is interpreted as AMSL, not home-relative''' self.set_parameters(self.FenceRelativeToAMSL_params()) + # reboot clears any lingering breach state from prior tests + self.reboot_sitl() self.wait_ready_to_arm() ggo = self.poll_message("GPS_GLOBAL_ORIGIN") origin_alt_m = ggo.altitude / 1000.0 # mm -> m @@ -15254,6 +15277,7 @@ def FenceRelativeToAMSLMinAlt(self): def FenceRelativeToAMSLCliff(self): '''AMSL min fence below arming altitude requires cliff to breach''' + self.install_terrain_handlers_context() self.customise_SITL_commandline(["--home", "KalaupapaCliffs"]) self.set_parameters(self.FenceRelativeToAMSL_params()) self.wait_ready_to_arm() @@ -15265,6 +15289,7 @@ def FenceRelativeToAMSLCliff(self): "FENCE_ALT_MIN": fence_min_below_arming, "FENCE_ALT_MAX": origin_alt_m + 60, # generous AMSL ceiling "FENCE_RADIUS": 10000, # ensure circle fence cannot limit north flight + "TERRAIN_ENABLE": 1, }) self.takeoff(20, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() @@ -15277,6 +15302,8 @@ def FenceRelativeToAMSLCliff(self): def FenceRelativeToTerrainMaxAlt(self): '''fence max-alt threshold is interpreted as AGL (terrain-relative)''' + # restart at default home in case a prior test moved to another location + self.customise_SITL_commandline([]) self.install_terrain_handlers_context() self.set_parameters(self.FenceRelativeToTerrain_params()) self.wait_ready_to_arm() @@ -15302,19 +15329,20 @@ def FenceRelativeToTerrainMaxAlt(self): def FenceRelativeToTerrainMinAlt(self): '''fence min-alt threshold is interpreted as AGL (terrain-relative)''' self.install_terrain_handlers_context() - self.set_parameters(self.FenceRelativeToTerrain_params()) + fence_alt_min = 3 # m AGL = 23 m above home + params = self.FenceRelativeToTerrain_params() + params.update({ + "FENCE_TYPE": 8, # ALT_MIN only + "FENCE_ALT_MIN": fence_alt_min, + "FENCE_ALT_MAX": 50, # generous ceiling + }) + self.set_parameters(params) self.wait_ready_to_arm() original_home = self.mav.location() # home is placed 20 m below terrain; terrain AMSL ≈ original_home.alt terrain_alt_amsl = original_home.alt - fence_alt_min = 3 # m AGL = 23 m above home offset_home = self.offset_location_up(original_home, -20) self.set_home(offset_home) - self.set_parameters({ - "FENCE_TYPE": 8, # ALT_MIN only - "FENCE_ALT_MIN": fence_alt_min, - "FENCE_ALT_MAX": 50, # generous ceiling - }) self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() self.set_rc(3, 1200) From d280abd2e3af1fabb3aa0f4cbfaede5298efef65 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sun, 8 Mar 2026 10:28:53 +0000 Subject: [PATCH 23/99] autotest: make fence alt-frame tests clean up after themselves Each test that modifies home or SITL location now restores state at the end rather than relying on subsequent tests to work around dirty state. Remove startup workarounds (customise_SITL_commandline, reboot_sitl) and add copy.copy before dict update. --- Tools/autotest/vehicle_test_suite.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py index 33d423f9e556dd..f99aa6152eb827 100644 --- a/Tools/autotest/vehicle_test_suite.py +++ b/Tools/autotest/vehicle_test_suite.py @@ -14974,6 +14974,7 @@ def FenceRelativePreArms(self): "FENCE_ALT_MAX": 0, }) self.assert_prearm_failure("PreArm: Vehicle breaching Max Alt fence") + self.customise_SITL_commandline([]) def FenceRelativeToHome_params(self): '''returns parameters for FenceRelativeToHome test''' @@ -15027,11 +15028,12 @@ def FenceRelativeToHomeMaxAlt(self): expected_breach_alt = offset_home.alt + fence_alt_max self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToHomeMinAlt(self): '''fence min-alt threshold is measured relative to home, not EKF origin''' fence_alt_min = 5 # m above home = 25 m above origin - params = self.FenceRelativeToHome_params() + params = copy.copy(self.FenceRelativeToHome_params()) params.update({ "FENCE_TYPE": 8, # ALT_MIN only "FENCE_ALT_MIN": fence_alt_min, @@ -15050,6 +15052,7 @@ def FenceRelativeToHomeMinAlt(self): expected_breach_alt = offset_home.alt + fence_alt_min self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToHomeMaxAltOriginAbove(self): '''fence max-alt relative to home when origin is above home''' @@ -15072,6 +15075,7 @@ def FenceRelativeToHomeMaxAltOriginAbove(self): expected_breach_alt = offset_home.alt + fence_alt_max self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToHomeMinAltOriginAbove(self): '''fence min-alt relative to home when origin is above home''' @@ -15098,6 +15102,7 @@ def FenceRelativeToHomeMinAltOriginAbove(self): expected_breach_alt = offset_home.alt + fence_alt_min self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToHomeCliff(self): '''home-relative min fence below arming altitude requires cliff to breach''' @@ -15126,11 +15131,10 @@ def FenceRelativeToHomeCliff(self): expected_breach_alt = offset_home.alt + fence_alt_min self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.customise_SITL_commandline([]) def FenceRelativeToOriginMaxAlt(self): '''fence max-alt threshold is measured relative to EKF origin, not home''' - # restart at default home in case a prior test moved to another location - self.customise_SITL_commandline([]) self.set_parameters(self.FenceRelativeToOrigin_params()) self.wait_ready_to_arm() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 @@ -15149,6 +15153,7 @@ def FenceRelativeToOriginMaxAlt(self): expected_breach_alt = origin_alt_m + fence_alt_max self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToOriginMinAlt(self): '''fence min-alt threshold is measured relative to EKF origin, not home''' @@ -15171,13 +15176,13 @@ def FenceRelativeToOriginMinAlt(self): expected_breach_alt = origin_alt_m + fence_alt_min self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToOriginMaxAltHomeAbove(self): '''fence max-alt relative to origin when home is above origin''' self.set_parameters(self.FenceRelativeToOrigin_params()) - # reboot to clear stale home offset from prior test - self.reboot_sitl() self.wait_ready_to_arm() + ground_loc = self.mav.location() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 fence_alt_max = 50 # m above origin = 30 m above home # take off first from home==origin so relative alt starts at 0 @@ -15197,6 +15202,7 @@ def FenceRelativeToOriginMaxAltHomeAbove(self): expected_breach_alt = origin_alt_m + fence_alt_max self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(ground_loc) def FenceRelativeToOriginMinAltHomeAbove(self): '''fence min-alt relative to origin when home is above origin''' @@ -15212,9 +15218,8 @@ def FenceRelativeToOriginMinAltHomeAbove(self): "FENCE_ALT_MAX": 80, # generous ceiling }) self.set_parameters(params) - # reboot to clear stale home offset from prior test - self.reboot_sitl() self.wait_ready_to_arm() + ground_loc = self.mav.location() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 # take off first from home==origin so relative alt starts at 0 self.takeoff(30, mode=self.FenceRelative_TakeoffMode()) @@ -15229,6 +15234,7 @@ def FenceRelativeToOriginMinAltHomeAbove(self): expected_breach_alt = origin_alt_m + fence_alt_min self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(ground_loc) def FenceRelativeToAMSLMaxAlt(self): '''fence max-alt threshold is interpreted as AMSL, not home-relative''' @@ -15299,11 +15305,10 @@ def FenceRelativeToAMSLCliff(self): expected_breach_alt = fence_min_below_arming self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.customise_SITL_commandline([]) def FenceRelativeToTerrainMaxAlt(self): '''fence max-alt threshold is interpreted as AGL (terrain-relative)''' - # restart at default home in case a prior test moved to another location - self.customise_SITL_commandline([]) self.install_terrain_handlers_context() self.set_parameters(self.FenceRelativeToTerrain_params()) self.wait_ready_to_arm() @@ -15325,6 +15330,7 @@ def FenceRelativeToTerrainMaxAlt(self): expected_breach_alt = terrain_alt_amsl + fence_alt_max self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def FenceRelativeToTerrainMinAlt(self): '''fence min-alt threshold is interpreted as AGL (terrain-relative)''' @@ -15350,6 +15356,7 @@ def FenceRelativeToTerrainMinAlt(self): expected_breach_alt = terrain_alt_amsl + fence_alt_min self.assert_altitude(expected_breach_alt, accuracy=10) self.disarm_vehicle(force=True) + self.set_home(original_home) def MotorTest(self, timeout=60, **kwargs): '''Run Motor Tests''' # common to Copter and QuadPlane From 48a6223dfbda059bd75acf27756521640afd8d13 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sun, 8 Mar 2026 10:41:20 +0000 Subject: [PATCH 24/99] AC_Fence: add _m suffix to altitude getters and fix comments Rename get_alt_in_frame, get_alt_in_alt_max_frame and get_alt_in_alt_min_frame with _m suffix per ArduPilot naming convention. Reorder get_alt_in_frame_m parameters to match Location::get_alt_m(frame, alt). Fix comment/name mismatches on get_relative_safe_alt_max_m and get_relative_safe_alt_min_m. Add get_alt_min_frame for symmetry. Use get_alt_m instead of get_alt_cm in check_destination_within_fence. --- libraries/AC_Fence/AC_Fence.cpp | 20 ++++++++++---------- libraries/AC_Fence/AC_Fence.h | 18 ++++++++++++------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/libraries/AC_Fence/AC_Fence.cpp b/libraries/AC_Fence/AC_Fence.cpp index 37c51e009c5897..156544c199ab32 100644 --- a/libraries/AC_Fence/AC_Fence.cpp +++ b/libraries/AC_Fence/AC_Fence.cpp @@ -578,7 +578,7 @@ bool AC_Fence::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) c get our altitude in the supplied frame return false if not available */ -bool AC_Fence::get_alt_in_frame(float &alt, Location::AltFrame alt_frame) const +bool AC_Fence::get_alt_in_frame_m(Location::AltFrame alt_frame, float &alt) const { const auto &ahrs = AP::ahrs(); @@ -634,7 +634,7 @@ bool AC_Fence::check_fence_alt_max() } float _curr_alt_u_m; - if (!get_alt_in_alt_max_frame(_curr_alt_u_m)) { + if (!get_alt_in_alt_max_frame_m(_curr_alt_u_m)) { // if we can't get the alt then it is a breach return true; } @@ -696,7 +696,7 @@ bool AC_Fence::check_fence_alt_min() } float _curr_alt_u_m; - if (!get_alt_in_alt_min_frame(_curr_alt_u_m)) { + if (!get_alt_in_alt_min_frame_m(_curr_alt_u_m)) { // if we can't get the alt then it is a breach return true; } @@ -757,7 +757,7 @@ bool AC_Fence::auto_enable_fence_floor() } float _curr_alt_u_m; - if (!get_alt_in_alt_min_frame(_curr_alt_u_m)) { + if (!get_alt_in_alt_min_frame_m(_curr_alt_u_m)) { // if we can't get the alt then don't enable yet return true; } @@ -985,9 +985,9 @@ bool AC_Fence::check_destination_within_fence(const Location& loc) { // Altitude fence check - Fence Ceiling if ((get_enabled_fences() & AC_FENCE_TYPE_ALT_MAX)) { - int32_t alt_cm; - if (loc.get_alt_cm(_alt_max_type, alt_cm)) { - if ((alt_cm * 0.01f) > _alt_max_m) { + float alt_m; + if (loc.get_alt_m(_alt_max_type, alt_m)) { + if (alt_m > _alt_max_m) { return false; } } @@ -995,9 +995,9 @@ bool AC_Fence::check_destination_within_fence(const Location& loc) // Altitude fence check - Fence Floor if ((get_enabled_fences() & AC_FENCE_TYPE_ALT_MIN)) { - int32_t alt_cm; - if (loc.get_alt_cm(_alt_min_type, alt_cm)) { - if ((alt_cm * 0.01f) < _alt_min_m) { + float alt_m; + if (loc.get_alt_m(_alt_min_type, alt_m)) { + if (alt_m < _alt_min_m) { return false; } } diff --git a/libraries/AC_Fence/AC_Fence.h b/libraries/AC_Fence/AC_Fence.h index fbc192cff956c9..eb09df982c38c4 100644 --- a/libraries/AC_Fence/AC_Fence.h +++ b/libraries/AC_Fence/AC_Fence.h @@ -154,10 +154,10 @@ class AC_Fence /// get_action - getter for user requested action on limit breach Action get_action() const { return _action; } - /// get_safe_alt - returns maximum safe altitude in relative frame + /// get_relative_safe_alt_max_m - returns maximum safe altitude in relative frame float get_relative_safe_alt_max_m() const { return _safe_relhome_alt_max_m ; } - /// get_safe_alt_min_m - returns the minimum safe altitude in relative frame + /// get_relative_safe_alt_min_m - returns the minimum safe altitude in relative frame float get_relative_safe_alt_min_m() const { return _safe_relhome_alt_min_m ; } /// get_safe_alt_max_m - returns maximum safe altitude in alt max frame (i.e. alt_max - margin) @@ -230,11 +230,17 @@ class AC_Fence #endif // get altitude in alt max frame - bool get_alt_in_alt_max_frame(float &alt) const { return get_alt_in_frame(alt, _alt_max_type); } + bool get_alt_in_alt_max_frame_m(float &alt) const { return get_alt_in_frame_m(_alt_max_type, alt); } // get altitude in alt min frame - bool get_alt_in_alt_min_frame(float &alt) const { return get_alt_in_frame(alt, _alt_min_type); } + bool get_alt_in_alt_min_frame_m(float &alt) const { return get_alt_in_frame_m(_alt_min_type, alt); } // get alt max frame Location::AltFrame get_alt_max_frame() const { return (Location::AltFrame)_alt_max_type.get(); } + // get alt min frame + Location::AltFrame get_alt_min_frame() const { return (Location::AltFrame)_alt_min_type.get(); } + + // deprecated aliases - use _m suffixed versions + bool get_alt_in_alt_max_frame(float &alt) const { return get_alt_in_alt_max_frame_m(alt); } + bool get_alt_in_alt_min_frame(float &alt) const { return get_alt_in_alt_min_frame_m(alt); } private: static AC_Fence *_singleton; @@ -269,8 +275,8 @@ class AC_Fence /// retrieve the current NED position relative to home bool get_current_position_NED(Vector3f& currpos) const; - // get altitude in FENCE_ALT_TYPE frame - bool get_alt_in_frame(float &alt, Location::AltFrame alt_frame) const; + // get altitude in the supplied frame + bool get_alt_in_frame_m(Location::AltFrame alt_frame, float &alt) const; // additional checks for the different fence types: bool pre_arm_check_polygon(char *failure_msg, const uint8_t failure_msg_len) const; From d12c7e0192d78ddba46d25ee86d779051449fe4b Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sun, 8 Mar 2026 10:41:44 +0000 Subject: [PATCH 25/99] AC_Avoidance: use renamed fence alt getters with _m suffix Update callers to use get_alt_in_alt_min_frame_m and get_alt_in_alt_max_frame_m. Fix comment that referred to max when code was about min alt. --- libraries/AC_Avoidance/AC_Avoid.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/AC_Avoidance/AC_Avoid.cpp b/libraries/AC_Avoidance/AC_Avoid.cpp index d1853979a57e38..df6db48b6514e4 100644 --- a/libraries/AC_Avoidance/AC_Avoid.cpp +++ b/libraries/AC_Avoidance/AC_Avoid.cpp @@ -435,15 +435,15 @@ void AC_Avoid::adjust_velocity_z(float kP, float accel_cmss, float& climb_rate_c // calculate distance from vehicle to safe altitude if ((fence->get_enabled_fences() & AC_FENCE_TYPE_ALT_MIN) > 0) { float veh_alt_m; - if (fence->get_alt_in_alt_min_frame(veh_alt_m)) { - // fence.get_safe_alt_max_m() is UP, veh_alt_m is UP: + if (fence->get_alt_in_alt_min_frame_m(veh_alt_m)) { + // fence.get_safe_alt_min_m() is UP, veh_alt_m is UP: min_alt_diff_m = veh_alt_m - fence->get_safe_alt_min_m(); limit_min_alt = true; } } if ((fence->get_enabled_fences() & AC_FENCE_TYPE_ALT_MAX) > 0) { float veh_alt_m; - if (fence->get_alt_in_alt_max_frame(veh_alt_m)) { + if (fence->get_alt_in_alt_max_frame_m(veh_alt_m)) { // fence.get_safe_alt_max_m() is UP, veh_alt_m is UP: max_alt_diff_m = fence->get_safe_alt_max_m() - veh_alt_m; limit_max_alt = true; From 38178f479cfeca624f9b50c25ea9ef7fc5334b2e Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sun, 8 Mar 2026 10:42:17 +0000 Subject: [PATCH 26/99] AC_Fence: remove deprecated altitude getter aliases --- libraries/AC_Fence/AC_Fence.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/AC_Fence/AC_Fence.h b/libraries/AC_Fence/AC_Fence.h index eb09df982c38c4..6251e667e4b920 100644 --- a/libraries/AC_Fence/AC_Fence.h +++ b/libraries/AC_Fence/AC_Fence.h @@ -238,10 +238,6 @@ class AC_Fence // get alt min frame Location::AltFrame get_alt_min_frame() const { return (Location::AltFrame)_alt_min_type.get(); } - // deprecated aliases - use _m suffixed versions - bool get_alt_in_alt_max_frame(float &alt) const { return get_alt_in_alt_max_frame_m(alt); } - bool get_alt_in_alt_min_frame(float &alt) const { return get_alt_in_alt_min_frame_m(alt); } - private: static AC_Fence *_singleton; From abffcda13c63f2ac22c4ba8ee8b6e2328525d1e6 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Mon, 9 Mar 2026 08:56:02 +0100 Subject: [PATCH 27/99] autotest: use home_position_as_mav_location for fence test home capture After force disarm + set_home in a fence test, the next test's self.mav.location() can return an EKF altitude that does not match the actual home altitude. This caused FenceRelativeToOriginMinAlt to fail on QuadPlane because the altitude offset was applied from a stale vehicle position rather than the true home. Replace all pre-takeoff self.mav.location() calls in fence alt-frame tests with self.home_position_as_mav_location(), which queries the actual HOME_POSITION from the autopilot. --- Tools/autotest/arducopter.py | 1 - Tools/autotest/vehicle_test_suite.py | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index a1943ecaa8345c..ba87c698600d3e 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -1965,7 +1965,6 @@ def max_alt_fence_frame(self, frame, alt, expected_breach_alt, terrain=0): "FENCE_TYPE": 1, "FENCE_ALT_MAX_TP": frame, "FENCE_ALT_MAX": alt, - "FENCE_ENABLE" : 1, "TERRAIN_ENABLE": terrain, "SIM_TERRAIN": terrain, }) diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py index f99aa6152eb827..9fb5ac07ae175c 100644 --- a/Tools/autotest/vehicle_test_suite.py +++ b/Tools/autotest/vehicle_test_suite.py @@ -14965,7 +14965,7 @@ def FenceRelativePreArms(self): ]) self.set_parameters(self.FenceRelative_params()) self.wait_ready_to_arm() - original_home = self.mav.location() # roughly speaking + original_home = self.home_position_as_mav_location() self.start_subtest("Above home-relative fence") self.set_home(self.offset_location_up(original_home, -2)) @@ -15011,7 +15011,7 @@ def FenceRelativeToHomeMaxAlt(self): '''fence max-alt threshold is measured relative to home, not EKF origin''' self.set_parameters(self.FenceRelativeToHome_params()) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() home_ofs = 20 fence_alt_max = 20 # m above home = 40 m above origin offset_home = self.offset_location_up(original_home, home_ofs) @@ -15041,7 +15041,7 @@ def FenceRelativeToHomeMinAlt(self): }) self.set_parameters(params) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() home_ofs = 20 offset_home = self.offset_location_up(original_home, home_ofs) self.set_home(offset_home) @@ -15058,7 +15058,7 @@ def FenceRelativeToHomeMaxAltOriginAbove(self): '''fence max-alt relative to home when origin is above home''' self.set_parameters(self.FenceRelativeToHome_params()) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() home_ofs = -20 fence_alt_max = 30 # m above home = 10 m above origin offset_home = self.offset_location_up(original_home, home_ofs) @@ -15090,7 +15090,7 @@ def FenceRelativeToHomeMinAltOriginAbove(self): }) self.set_parameters(params) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() home_ofs = -20 offset_home = self.offset_location_up(original_home, home_ofs) self.set_home(offset_home) @@ -15119,7 +15119,7 @@ def FenceRelativeToHomeCliff(self): }) self.set_parameters(params) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() home_ofs = -20 offset_home = self.offset_location_up(original_home, home_ofs) self.set_home(offset_home) @@ -15139,7 +15139,7 @@ def FenceRelativeToOriginMaxAlt(self): self.wait_ready_to_arm() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 fence_alt_max = 10 # m above origin = 30 m above home - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() self.set_home(self.offset_location_up(original_home, -20)) self.set_parameters({ "FENCE_TYPE": 1, # ALT_MAX only @@ -15167,7 +15167,7 @@ def FenceRelativeToOriginMinAlt(self): self.set_parameters(params) self.wait_ready_to_arm() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() self.set_home(self.offset_location_up(original_home, -20)) self.takeoff(25, mode=self.FenceRelative_TakeoffMode()) self.do_fence_enable() @@ -15182,7 +15182,7 @@ def FenceRelativeToOriginMaxAltHomeAbove(self): '''fence max-alt relative to origin when home is above origin''' self.set_parameters(self.FenceRelativeToOrigin_params()) self.wait_ready_to_arm() - ground_loc = self.mav.location() + ground_loc = self.home_position_as_mav_location() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 fence_alt_max = 50 # m above origin = 30 m above home # take off first from home==origin so relative alt starts at 0 @@ -15219,7 +15219,7 @@ def FenceRelativeToOriginMinAltHomeAbove(self): }) self.set_parameters(params) self.wait_ready_to_arm() - ground_loc = self.mav.location() + ground_loc = self.home_position_as_mav_location() origin_alt_m = self.poll_message("GPS_GLOBAL_ORIGIN").altitude / 1000.0 # take off first from home==origin so relative alt starts at 0 self.takeoff(30, mode=self.FenceRelative_TakeoffMode()) @@ -15312,7 +15312,7 @@ def FenceRelativeToTerrainMaxAlt(self): self.install_terrain_handlers_context() self.set_parameters(self.FenceRelativeToTerrain_params()) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() # home is placed 20 m below terrain; terrain AMSL ≈ original_home.alt terrain_alt_amsl = original_home.alt fence_alt_max = 10 # m AGL = 30 m above home @@ -15344,7 +15344,7 @@ def FenceRelativeToTerrainMinAlt(self): }) self.set_parameters(params) self.wait_ready_to_arm() - original_home = self.mav.location() + original_home = self.home_position_as_mav_location() # home is placed 20 m below terrain; terrain AMSL ≈ original_home.alt terrain_alt_amsl = original_home.alt offset_home = self.offset_location_up(original_home, -20) From c4f9307ac18dfbeb5c8a29cea8bd858a6e82c24f Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sat, 31 Jan 2026 15:05:34 -0600 Subject: [PATCH 28/99] AP_NavEKF3: loop over states in FuseMagnetometer Kalman gain computation Same form and coefficients for each element. Saves lots of flash. --- libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp | 148 +++++------------- 1 file changed, 40 insertions(+), 108 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp index 2093e7e73c8f04..ddf27347e04b5f 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp @@ -610,59 +610,37 @@ void NavEKF3_core::FuseMagnetometer() 2.0f*q0*q3 + 2.0f*q1*q2 }; - Kfusion[0] = SK_MX[0]*(P[0][19] + P[0][1]*SH_MAG[0] - P[0][2]*SH_MAG[1] + P[0][3]*SH_MAG[2] + P[0][0]*SK_MX[2] - P[0][16]*SK_MX[1] + P[0][17]*SK_MX[4] - P[0][18]*SK_MX[3]); - Kfusion[1] = SK_MX[0]*(P[1][19] + P[1][1]*SH_MAG[0] - P[1][2]*SH_MAG[1] + P[1][3]*SH_MAG[2] + P[1][0]*SK_MX[2] - P[1][16]*SK_MX[1] + P[1][17]*SK_MX[4] - P[1][18]*SK_MX[3]); - Kfusion[2] = SK_MX[0]*(P[2][19] + P[2][1]*SH_MAG[0] - P[2][2]*SH_MAG[1] + P[2][3]*SH_MAG[2] + P[2][0]*SK_MX[2] - P[2][16]*SK_MX[1] + P[2][17]*SK_MX[4] - P[2][18]*SK_MX[3]); - Kfusion[3] = SK_MX[0]*(P[3][19] + P[3][1]*SH_MAG[0] - P[3][2]*SH_MAG[1] + P[3][3]*SH_MAG[2] + P[3][0]*SK_MX[2] - P[3][16]*SK_MX[1] + P[3][17]*SK_MX[4] - P[3][18]*SK_MX[3]); - Kfusion[4] = SK_MX[0]*(P[4][19] + P[4][1]*SH_MAG[0] - P[4][2]*SH_MAG[1] + P[4][3]*SH_MAG[2] + P[4][0]*SK_MX[2] - P[4][16]*SK_MX[1] + P[4][17]*SK_MX[4] - P[4][18]*SK_MX[3]); - Kfusion[5] = SK_MX[0]*(P[5][19] + P[5][1]*SH_MAG[0] - P[5][2]*SH_MAG[1] + P[5][3]*SH_MAG[2] + P[5][0]*SK_MX[2] - P[5][16]*SK_MX[1] + P[5][17]*SK_MX[4] - P[5][18]*SK_MX[3]); - Kfusion[6] = SK_MX[0]*(P[6][19] + P[6][1]*SH_MAG[0] - P[6][2]*SH_MAG[1] + P[6][3]*SH_MAG[2] + P[6][0]*SK_MX[2] - P[6][16]*SK_MX[1] + P[6][17]*SK_MX[4] - P[6][18]*SK_MX[3]); - Kfusion[7] = SK_MX[0]*(P[7][19] + P[7][1]*SH_MAG[0] - P[7][2]*SH_MAG[1] + P[7][3]*SH_MAG[2] + P[7][0]*SK_MX[2] - P[7][16]*SK_MX[1] + P[7][17]*SK_MX[4] - P[7][18]*SK_MX[3]); - Kfusion[8] = SK_MX[0]*(P[8][19] + P[8][1]*SH_MAG[0] - P[8][2]*SH_MAG[1] + P[8][3]*SH_MAG[2] + P[8][0]*SK_MX[2] - P[8][16]*SK_MX[1] + P[8][17]*SK_MX[4] - P[8][18]*SK_MX[3]); - Kfusion[9] = SK_MX[0]*(P[9][19] + P[9][1]*SH_MAG[0] - P[9][2]*SH_MAG[1] + P[9][3]*SH_MAG[2] + P[9][0]*SK_MX[2] - P[9][16]*SK_MX[1] + P[9][17]*SK_MX[4] - P[9][18]*SK_MX[3]); + uint32_t kalman_mask = (1<<10)-1; // values to calculate in Kfusion (others are set to zero) if (!inhibitDelAngBiasStates) { - Kfusion[10] = SK_MX[0]*(P[10][19] + P[10][1]*SH_MAG[0] - P[10][2]*SH_MAG[1] + P[10][3]*SH_MAG[2] + P[10][0]*SK_MX[2] - P[10][16]*SK_MX[1] + P[10][17]*SK_MX[4] - P[10][18]*SK_MX[3]); - Kfusion[11] = SK_MX[0]*(P[11][19] + P[11][1]*SH_MAG[0] - P[11][2]*SH_MAG[1] + P[11][3]*SH_MAG[2] + P[11][0]*SK_MX[2] - P[11][16]*SK_MX[1] + P[11][17]*SK_MX[4] - P[11][18]*SK_MX[3]); - Kfusion[12] = SK_MX[0]*(P[12][19] + P[12][1]*SH_MAG[0] - P[12][2]*SH_MAG[1] + P[12][3]*SH_MAG[2] + P[12][0]*SK_MX[2] - P[12][16]*SK_MX[1] + P[12][17]*SK_MX[4] - P[12][18]*SK_MX[3]); - } else { - // zero indexes 10 to 12 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (!inhibitDelVelBiasStates) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = SK_MX[0]*(P[stateIndex][19] + P[stateIndex][1]*SH_MAG[0] - P[stateIndex][2]*SH_MAG[1] + P[stateIndex][3]*SH_MAG[2] + P[stateIndex][0]*SK_MX[2] - P[stateIndex][16]*SK_MX[1] + P[stateIndex][17]*SK_MX[4] - P[stateIndex][18]*SK_MX[3]); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Sat, 31 Jan 2026 15:08:41 -0600 Subject: [PATCH 29/99] AP_NavEKF3: loop over states in FuseDeclination Kalman gain computation Same form and coefficients for each element. Saves lots of flash. --- libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp index ddf27347e04b5f..ea1a72073cdb55 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_MagFusion.cpp @@ -1213,58 +1213,35 @@ void NavEKF3_core::FuseDeclination(ftype declErr) Hfusion[16] = -HK1*magE; Hfusion[17] = HK1*magN; - Kfusion[0] = -HK4*(P[0][16]*magE-P[0][17]*magN); - Kfusion[1] = -HK4*(P[1][16]*magE-P[1][17]*magN); - Kfusion[2] = -HK4*(P[2][16]*magE-P[2][17]*magN); - Kfusion[3] = -HK4*(P[3][16]*magE-P[3][17]*magN); - Kfusion[4] = -HK4*(P[4][16]*magE-P[4][17]*magN); - Kfusion[5] = -HK4*(P[5][16]*magE-P[5][17]*magN); - Kfusion[6] = -HK4*(P[6][16]*magE-P[6][17]*magN); - Kfusion[7] = -HK4*(P[7][16]*magE-P[7][17]*magN); - Kfusion[8] = -HK4*(P[8][16]*magE-P[8][17]*magN); - Kfusion[9] = -HK4*(P[9][16]*magE-P[9][17]*magN); + uint32_t kalman_mask = (1<<10)-1; // values to calculate in Kfusion (others are set to zero) if (!inhibitDelAngBiasStates) { - Kfusion[10] = -HK4*(P[10][16]*magE-P[10][17]*magN); - Kfusion[11] = -HK4*(P[11][16]*magE-P[11][17]*magN); - Kfusion[12] = -HK4*(P[12][16]*magE-P[12][17]*magN); - } else { - // zero indexes 10 to 12 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (!inhibitDelVelBiasStates) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = -HK4*(P[stateIndex][16]*magE-P[stateIndex][17]*magN); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Sat, 31 Jan 2026 15:16:37 -0600 Subject: [PATCH 30/99] AP_NavEKF3: loop over states in FuseAirspeed Kalman gain computation Same form and coefficients for each element. Saves lots of flash. --- .../AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp | 54 ++++++------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp index 6d2babb16ddddf..a596239f7ccc7a 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp @@ -62,64 +62,40 @@ void NavEKF3_core::FuseAirspeed() } SK_TAS[1] = SH_TAS[1]; + uint32_t kalman_mask = 0; // values to calculate in Kfusion (others are set to zero) + if (tasDataDelayed.allowFusion && !airDataFusionWindOnly) { - Kfusion[0] = SK_TAS[0]*(P[0][4]*SH_TAS[2] - P[0][22]*SH_TAS[2] + P[0][5]*SK_TAS[1] - P[0][23]*SK_TAS[1] + P[0][6]*vd*SH_TAS[0]); - Kfusion[1] = SK_TAS[0]*(P[1][4]*SH_TAS[2] - P[1][22]*SH_TAS[2] + P[1][5]*SK_TAS[1] - P[1][23]*SK_TAS[1] + P[1][6]*vd*SH_TAS[0]); - Kfusion[2] = SK_TAS[0]*(P[2][4]*SH_TAS[2] - P[2][22]*SH_TAS[2] + P[2][5]*SK_TAS[1] - P[2][23]*SK_TAS[1] + P[2][6]*vd*SH_TAS[0]); - Kfusion[3] = SK_TAS[0]*(P[3][4]*SH_TAS[2] - P[3][22]*SH_TAS[2] + P[3][5]*SK_TAS[1] - P[3][23]*SK_TAS[1] + P[3][6]*vd*SH_TAS[0]); - Kfusion[4] = SK_TAS[0]*(P[4][4]*SH_TAS[2] - P[4][22]*SH_TAS[2] + P[4][5]*SK_TAS[1] - P[4][23]*SK_TAS[1] + P[4][6]*vd*SH_TAS[0]); - Kfusion[5] = SK_TAS[0]*(P[5][4]*SH_TAS[2] - P[5][22]*SH_TAS[2] + P[5][5]*SK_TAS[1] - P[5][23]*SK_TAS[1] + P[5][6]*vd*SH_TAS[0]); - Kfusion[6] = SK_TAS[0]*(P[6][4]*SH_TAS[2] - P[6][22]*SH_TAS[2] + P[6][5]*SK_TAS[1] - P[6][23]*SK_TAS[1] + P[6][6]*vd*SH_TAS[0]); - Kfusion[7] = SK_TAS[0]*(P[7][4]*SH_TAS[2] - P[7][22]*SH_TAS[2] + P[7][5]*SK_TAS[1] - P[7][23]*SK_TAS[1] + P[7][6]*vd*SH_TAS[0]); - Kfusion[8] = SK_TAS[0]*(P[8][4]*SH_TAS[2] - P[8][22]*SH_TAS[2] + P[8][5]*SK_TAS[1] - P[8][23]*SK_TAS[1] + P[8][6]*vd*SH_TAS[0]); - Kfusion[9] = SK_TAS[0]*(P[9][4]*SH_TAS[2] - P[9][22]*SH_TAS[2] + P[9][5]*SK_TAS[1] - P[9][23]*SK_TAS[1] + P[9][6]*vd*SH_TAS[0]); - } else { - // zero indexes 0 to 9 - zero_range(&Kfusion[0], 0, 9); + kalman_mask = (1<<10)-1; } if (tasDataDelayed.allowFusion && !inhibitDelAngBiasStates && !airDataFusionWindOnly) { - Kfusion[10] = SK_TAS[0]*(P[10][4]*SH_TAS[2] - P[10][22]*SH_TAS[2] + P[10][5]*SK_TAS[1] - P[10][23]*SK_TAS[1] + P[10][6]*vd*SH_TAS[0]); - Kfusion[11] = SK_TAS[0]*(P[11][4]*SH_TAS[2] - P[11][22]*SH_TAS[2] + P[11][5]*SK_TAS[1] - P[11][23]*SK_TAS[1] + P[11][6]*vd*SH_TAS[0]); - Kfusion[12] = SK_TAS[0]*(P[12][4]*SH_TAS[2] - P[12][22]*SH_TAS[2] + P[12][5]*SK_TAS[1] - P[12][23]*SK_TAS[1] + P[12][6]*vd*SH_TAS[0]); - } else { - // zero indexes 10 to 12 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (tasDataDelayed.allowFusion && !inhibitDelVelBiasStates && !airDataFusionWindOnly) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = SK_TAS[0]*(P[stateIndex][4]*SH_TAS[2] - P[stateIndex][22]*SH_TAS[2] + P[stateIndex][5]*SK_TAS[1] - P[stateIndex][23]*SK_TAS[1] + P[stateIndex][6]*vd*SH_TAS[0]); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Sat, 31 Jan 2026 15:25:10 -0600 Subject: [PATCH 31/99] AP_NavEKF3: loop over states in FuseSideslip Kalman gain computation Same form and coefficients for each element. Saves lots of flash. --- .../AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp | 51 ++++++------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp index a596239f7ccc7a..165d79c78dbb79 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp @@ -319,64 +319,43 @@ void NavEKF3_core::FuseSideslip() SK_BETA[6] = SH_BETA[5]*SH_BETA[11] + SH_BETA[1]*SH_BETA[4]*SH_BETA[10]; SK_BETA[7] = SH_BETA[5]*SH_BETA[9] + SH_BETA[1]*SH_BETA[4]*SH_BETA[8]; + uint32_t kalman_mask = 0; // values to calculate in Kfusion (others are set to zero) + if (!airDataFusionWindOnly) { - Kfusion[0] = SK_BETA[0]*(P[0][0]*SK_BETA[5] + P[0][1]*SK_BETA[4] - P[0][4]*SK_BETA[1] + P[0][5]*SK_BETA[2] + P[0][2]*SK_BETA[6] + P[0][6]*SK_BETA[3] - P[0][3]*SK_BETA[7] + P[0][22]*SK_BETA[1] - P[0][23]*SK_BETA[2]); - Kfusion[1] = SK_BETA[0]*(P[1][0]*SK_BETA[5] + P[1][1]*SK_BETA[4] - P[1][4]*SK_BETA[1] + P[1][5]*SK_BETA[2] + P[1][2]*SK_BETA[6] + P[1][6]*SK_BETA[3] - P[1][3]*SK_BETA[7] + P[1][22]*SK_BETA[1] - P[1][23]*SK_BETA[2]); - Kfusion[2] = SK_BETA[0]*(P[2][0]*SK_BETA[5] + P[2][1]*SK_BETA[4] - P[2][4]*SK_BETA[1] + P[2][5]*SK_BETA[2] + P[2][2]*SK_BETA[6] + P[2][6]*SK_BETA[3] - P[2][3]*SK_BETA[7] + P[2][22]*SK_BETA[1] - P[2][23]*SK_BETA[2]); - Kfusion[3] = SK_BETA[0]*(P[3][0]*SK_BETA[5] + P[3][1]*SK_BETA[4] - P[3][4]*SK_BETA[1] + P[3][5]*SK_BETA[2] + P[3][2]*SK_BETA[6] + P[3][6]*SK_BETA[3] - P[3][3]*SK_BETA[7] + P[3][22]*SK_BETA[1] - P[3][23]*SK_BETA[2]); - Kfusion[4] = SK_BETA[0]*(P[4][0]*SK_BETA[5] + P[4][1]*SK_BETA[4] - P[4][4]*SK_BETA[1] + P[4][5]*SK_BETA[2] + P[4][2]*SK_BETA[6] + P[4][6]*SK_BETA[3] - P[4][3]*SK_BETA[7] + P[4][22]*SK_BETA[1] - P[4][23]*SK_BETA[2]); - Kfusion[5] = SK_BETA[0]*(P[5][0]*SK_BETA[5] + P[5][1]*SK_BETA[4] - P[5][4]*SK_BETA[1] + P[5][5]*SK_BETA[2] + P[5][2]*SK_BETA[6] + P[5][6]*SK_BETA[3] - P[5][3]*SK_BETA[7] + P[5][22]*SK_BETA[1] - P[5][23]*SK_BETA[2]); - Kfusion[6] = SK_BETA[0]*(P[6][0]*SK_BETA[5] + P[6][1]*SK_BETA[4] - P[6][4]*SK_BETA[1] + P[6][5]*SK_BETA[2] + P[6][2]*SK_BETA[6] + P[6][6]*SK_BETA[3] - P[6][3]*SK_BETA[7] + P[6][22]*SK_BETA[1] - P[6][23]*SK_BETA[2]); - Kfusion[7] = SK_BETA[0]*(P[7][0]*SK_BETA[5] + P[7][1]*SK_BETA[4] - P[7][4]*SK_BETA[1] + P[7][5]*SK_BETA[2] + P[7][2]*SK_BETA[6] + P[7][6]*SK_BETA[3] - P[7][3]*SK_BETA[7] + P[7][22]*SK_BETA[1] - P[7][23]*SK_BETA[2]); - Kfusion[8] = SK_BETA[0]*(P[8][0]*SK_BETA[5] + P[8][1]*SK_BETA[4] - P[8][4]*SK_BETA[1] + P[8][5]*SK_BETA[2] + P[8][2]*SK_BETA[6] + P[8][6]*SK_BETA[3] - P[8][3]*SK_BETA[7] + P[8][22]*SK_BETA[1] - P[8][23]*SK_BETA[2]); - Kfusion[9] = SK_BETA[0]*(P[9][0]*SK_BETA[5] + P[9][1]*SK_BETA[4] - P[9][4]*SK_BETA[1] + P[9][5]*SK_BETA[2] + P[9][2]*SK_BETA[6] + P[9][6]*SK_BETA[3] - P[9][3]*SK_BETA[7] + P[9][22]*SK_BETA[1] - P[9][23]*SK_BETA[2]); + kalman_mask = (1<<10)-1; } else { // zero indexes 0 to 9 zero_range(&Kfusion[0], 0, 9); } if (!inhibitDelAngBiasStates && !airDataFusionWindOnly) { - Kfusion[10] = SK_BETA[0]*(P[10][0]*SK_BETA[5] + P[10][1]*SK_BETA[4] - P[10][4]*SK_BETA[1] + P[10][5]*SK_BETA[2] + P[10][2]*SK_BETA[6] + P[10][6]*SK_BETA[3] - P[10][3]*SK_BETA[7] + P[10][22]*SK_BETA[1] - P[10][23]*SK_BETA[2]); - Kfusion[11] = SK_BETA[0]*(P[11][0]*SK_BETA[5] + P[11][1]*SK_BETA[4] - P[11][4]*SK_BETA[1] + P[11][5]*SK_BETA[2] + P[11][2]*SK_BETA[6] + P[11][6]*SK_BETA[3] - P[11][3]*SK_BETA[7] + P[11][22]*SK_BETA[1] - P[11][23]*SK_BETA[2]); - Kfusion[12] = SK_BETA[0]*(P[12][0]*SK_BETA[5] + P[12][1]*SK_BETA[4] - P[12][4]*SK_BETA[1] + P[12][5]*SK_BETA[2] + P[12][2]*SK_BETA[6] + P[12][6]*SK_BETA[3] - P[12][3]*SK_BETA[7] + P[12][22]*SK_BETA[1] - P[12][23]*SK_BETA[2]); - } else { - // zero indexes 10 to 12 = 3 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (!inhibitDelVelBiasStates && !airDataFusionWindOnly) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = SK_BETA[0]*(P[stateIndex][0]*SK_BETA[5] + P[stateIndex][1]*SK_BETA[4] - P[stateIndex][4]*SK_BETA[1] + P[stateIndex][5]*SK_BETA[2] + P[stateIndex][2]*SK_BETA[6] + P[stateIndex][6]*SK_BETA[3] - P[stateIndex][3]*SK_BETA[7] + P[stateIndex][22]*SK_BETA[1] - P[stateIndex][23]*SK_BETA[2]); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Sat, 31 Jan 2026 15:33:56 -0600 Subject: [PATCH 32/99] AP_NavEKF3: loop over states in FuseOptFlow Kalman gain computation Same form and coefficients for most elements. Saves lots of flash. --- .../AP_NavEKF3/AP_NavEKF3_OptFlowFusion.cpp | 87 +++++++------------ 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_OptFlowFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_OptFlowFusion.cpp index d7fdd1dc789b5e..2c7d77d8bbc2a0 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_OptFlowFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_OptFlowFusion.cpp @@ -465,53 +465,38 @@ void NavEKF3_core::FuseOptFlow(const of_elements &ofDataDelayed, bool really_fus Kfusion[4] = t78*(-t90+P[4][0]*t2*t5+P[4][1]*t2*t15+P[4][6]*t2*t10+P[4][2]*t2*t19-P[4][3]*t2*t22+P[4][5]*t2*t27); Kfusion[5] = t78*(t61+P[5][0]*t2*t5-P[5][4]*t2*t7+P[5][1]*t2*t15+P[5][6]*t2*t10+P[5][2]*t2*t19-P[5][3]*t2*t22); Kfusion[6] = t78*(t70+P[6][0]*t2*t5-P[6][4]*t2*t7+P[6][1]*t2*t15+P[6][2]*t2*t19-P[6][3]*t2*t22+P[6][5]*t2*t27); - Kfusion[7] = t78*(P[7][0]*t2*t5-P[7][4]*t2*t7+P[7][1]*t2*t15+P[7][6]*t2*t10+P[7][2]*t2*t19-P[7][3]*t2*t22+P[7][5]*t2*t27); - Kfusion[8] = t78*(P[8][0]*t2*t5-P[8][4]*t2*t7+P[8][1]*t2*t15+P[8][6]*t2*t10+P[8][2]*t2*t19-P[8][3]*t2*t22+P[8][5]*t2*t27); - Kfusion[9] = t78*(P[9][0]*t2*t5-P[9][4]*t2*t7+P[9][1]*t2*t15+P[9][6]*t2*t10+P[9][2]*t2*t19-P[9][3]*t2*t22+P[9][5]*t2*t27); + + // values to calculate in Kfusion (others are set to zero, indices 0-6 ignored) + uint32_t kalman_mask = (1<<7) | (1<<8) | (1<<9); if (!inhibitDelAngBiasStates) { - Kfusion[10] = t78*(P[10][0]*t2*t5-P[10][4]*t2*t7+P[10][1]*t2*t15+P[10][6]*t2*t10+P[10][2]*t2*t19-P[10][3]*t2*t22+P[10][5]*t2*t27); - Kfusion[11] = t78*(P[11][0]*t2*t5-P[11][4]*t2*t7+P[11][1]*t2*t15+P[11][6]*t2*t10+P[11][2]*t2*t19-P[11][3]*t2*t22+P[11][5]*t2*t27); - Kfusion[12] = t78*(P[12][0]*t2*t5-P[12][4]*t2*t7+P[12][1]*t2*t15+P[12][6]*t2*t10+P[12][2]*t2*t19-P[12][3]*t2*t22+P[12][5]*t2*t27); - } else { - // zero indexes 10 to 12 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (!inhibitDelVelBiasStates && !badIMUdata) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = t78*(P[stateIndex][0]*t2*t5-P[stateIndex][4]*t2*t7+P[stateIndex][1]*t2*t15+P[stateIndex][6]*t2*t10+P[stateIndex][2]*t2*t19-P[stateIndex][3]*t2*t22+P[stateIndex][5]*t2*t27); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Sat, 31 Jan 2026 15:51:59 -0600 Subject: [PATCH 33/99] AP_NavEKF3: loop over states in FuseRngBcn Kalman gain computation Same form and coefficients for many elements. Saves lots of flash. --- .../AP_NavEKF3/AP_NavEKF3_RngBcnFusion.cpp | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_RngBcnFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_RngBcnFusion.cpp index 3bf421741a460b..fc79a6e85a3707 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_RngBcnFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_RngBcnFusion.cpp @@ -192,27 +192,20 @@ void NavEKF3_core::FuseRngBcn() Kfusion[7] = -t26*(t22+P[7][8]*t3*t9+P[7][9]*t2*t9); Kfusion[8] = -t26*(t16+P[8][7]*t4*t9+P[8][9]*t2*t9); + // values to calculate in Kfusion (others are set to zero, indices 0-9 ignored) + uint32_t kalman_mask = 0; + if (!inhibitDelAngBiasStates) { - Kfusion[10] = -t26*(P[10][7]*t4*t9+P[10][8]*t3*t9+P[10][9]*t2*t9); - Kfusion[11] = -t26*(P[11][7]*t4*t9+P[11][8]*t3*t9+P[11][9]*t2*t9); - Kfusion[12] = -t26*(P[12][7]*t4*t9+P[12][8]*t3*t9+P[12][9]*t2*t9); - } else { - // zero indexes 10 to 12 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (!inhibitDelVelBiasStates && !badIMUdata) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = -t26*(P[stateIndex][7]*t4*t9+P[stateIndex][8]*t3*t9+P[stateIndex][9]*t2*t9); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Sat, 21 Mar 2026 02:00:12 +0530 Subject: [PATCH 34/99] AP_NavEKF3: Reset to extnav only when configured --- libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp index fc9e27aa8f69a5..4a19468e7ac189 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp @@ -478,9 +478,11 @@ void NavEKF3_core::setAidingMode() GCS_SEND_TEXT(MAV_SEVERITY_INFO, "EKF3 IMU%u initial vel NED = %3.1f,%3.1f,%3.1f (m/s)",(unsigned)imu_index,(double)extNavVelDelayed.vel.x,(double)extNavVelDelayed.vel.y,(double)extNavVelDelayed.vel.z); } // handle height reset as special case - hgtMea = -extNavDataDelayed.pos.z; - posDownObsNoise = sq(constrain_ftype(extNavDataDelayed.posErr, 0.1f, 10.0f)); - ResetHeight(); + if (frontend->sources.getPosZSource(core_index) == AP_NavEKF_Source::SourceZ::EXTNAV) { + hgtMea = -extNavDataDelayed.pos.z; + posDownObsNoise = sq(constrain_ftype(extNavDataDelayed.posErr, 0.1f, 10.0f)); + ResetHeight(); + } #endif // EK3_FEATURE_EXTERNAL_NAV } From 812ffec9aa2ad466eefe3064328370f0c950ee87 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Tue, 17 Mar 2026 11:12:19 +0000 Subject: [PATCH 35/99] AP_NavEKF3: add configuredToUseGPSForPos() --- libraries/AP_NavEKF3/AP_NavEKF3.cpp | 9 +++++++++ libraries/AP_NavEKF3/AP_NavEKF3.h | 3 +++ 2 files changed, 12 insertions(+) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3.cpp b/libraries/AP_NavEKF3/AP_NavEKF3.cpp index 4b0109f56fa268..a3dc700026d3bf 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3.cpp @@ -1596,6 +1596,15 @@ bool NavEKF3::configuredToUseGPSForPosXY(void) const return false; } +// check if configured to use GPS for horizontal position estimation +bool NavEKF3::configuredToUseGPSForPos(void) const +{ + if (configuredToUseGPSForPosXY() || sources.getPosZSource(primary) == AP_NavEKF_Source::SourceZ::GPS) { + return true; + } + return false; +} + // write the raw optical flow measurements // rawFlowQuality is a measured of quality between 0 and 255, with 255 being the best quality // rawFlowRates are the optical flow rates in rad/sec about the X and Y sensor axes. diff --git a/libraries/AP_NavEKF3/AP_NavEKF3.h b/libraries/AP_NavEKF3/AP_NavEKF3.h index cc100df67adf43..ecf29100805afd 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3.h +++ b/libraries/AP_NavEKF3/AP_NavEKF3.h @@ -361,6 +361,9 @@ class NavEKF3 { // check if configured to use GPS for horizontal position estimation bool configuredToUseGPSForPosXY(void) const; + // check if configured to use GPS for position estimation + bool configuredToUseGPSForPos(void) const; + // Writes the default equivalent airspeed and 1-sigma uncertainty in m/s to be used in forward flight if a measured airspeed is required and not available. void writeDefaultAirSpeed(float airspeed, float uncertainty); From 9d4abe63555c6bedc7bad776a2f710ab92efdf03 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Thu, 5 Mar 2026 10:13:21 +0000 Subject: [PATCH 36/99] AP_AHRS: add using_gps_for_posxy and fix recorded origin check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add using_gps_for_posxy() which checks if GPS is configured as the horizontal position source for the configured EKF type. This is narrower than using_gps() which also checks VelZ, VelXY, and GSF yaw sources. Use this in use_recorded_origin_maybe() instead of using_gps() or checking GPS fix status. The recorded origin decision depends on whether GPS will set the EKF origin (immutable once set), which only happens via horizontal position — not VelZ or yaw. The previous using_gps() check blocked recorded origin loading when EK3_SRC1_POSXY was set to EXTNAV but EK3_SRC1_VELZ defaulted to GPS. Also remove the GPS fix status check which was too broad (would block GPS-denied configs with GPS hardware plugged in). --- libraries/AP_AHRS/AP_AHRS.cpp | 35 +++++++++++++++++++++++++++++++++-- libraries/AP_AHRS/AP_AHRS.h | 5 +++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/libraries/AP_AHRS/AP_AHRS.cpp b/libraries/AP_AHRS/AP_AHRS.cpp index f2e1233bc6c56d..e38547e4e04461 100644 --- a/libraries/AP_AHRS/AP_AHRS.cpp +++ b/libraries/AP_AHRS/AP_AHRS.cpp @@ -1549,8 +1549,11 @@ void AP_AHRS::use_recorded_origin_maybe() return; } - // only set if not using GPS - if (using_gps()) { + // don't use recorded origin if the configured EKF uses GPS for + // position — GPS will set a correct origin when it gets + // a fix. Using the recorded origin here would prevent GPS from + // setting it later (EKF origin is immutable once set). + if (using_gps_for_pos()) { return; } @@ -3601,6 +3604,34 @@ bool AP_AHRS::using_gps(void) const return true; } +// check if GPS is configured as the position source for +// the configured EKF type +bool AP_AHRS::using_gps_for_pos(void) const +{ + switch (active_EKF_type()) { +#if HAL_NAVEKF2_AVAILABLE + case EKFType::TWO: + return EKF2.configuredToUseGPSForPosXY(); +#endif +#if HAL_NAVEKF3_AVAILABLE + case EKFType::THREE: + return EKF3.configuredToUseGPSForPos(); +#endif +#if AP_AHRS_DCM_ENABLED + case EKFType::DCM: + return _gps_use != GPSUse::Disable; +#endif +#if AP_AHRS_SIM_ENABLED + case EKFType::SIM: +#endif +#if AP_AHRS_EXTERNAL_ENABLED + case EKFType::EXTERNAL: +#endif + return true; + } + return true; +} + // set and save the alt noise parameter value void AP_AHRS::set_alt_measurement_noise(float noise) { diff --git a/libraries/AP_AHRS/AP_AHRS.h b/libraries/AP_AHRS/AP_AHRS.h index 348c969d887053..ccb6ed0abcc74e 100644 --- a/libraries/AP_AHRS/AP_AHRS.h +++ b/libraries/AP_AHRS/AP_AHRS.h @@ -454,6 +454,11 @@ class AP_AHRS { // always returns true for External and SIM EKF types bool using_gps(void) const; + // check if GPS is configured as the horizontal position source + // for the configured EKF type. Used to decide whether GPS will + // set the EKF origin (which is immutable once set). + bool using_gps_for_pos(void) const; + // set and save the ALT_M_NSE parameter value void set_alt_measurement_noise(float noise); From 05f832c190843f443b752eaac0f7394a568fa7c7 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Tue, 24 Mar 2026 14:17:43 +0000 Subject: [PATCH 37/99] AP_NavEKF3: delay external origin setting until filter is initialised Reject setOriginLLH() calls while statesInitialised is false. InitialiseVariables() resets validOrigin during bootstrap, so an origin set before that point (e.g. from AHRS use_recorded_origin_maybe) is silently lost. Callers already handle the false return by retrying on the next cycle. This replaces the previous AHRS-level workaround that detected and re-applied the lost origin. --- libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp index 4a19468e7ac189..c720b412b83b91 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_Control.cpp @@ -683,6 +683,13 @@ bool NavEKF3_core::assume_zero_sideslip(void) const // returns false if the origin is already set bool NavEKF3_core::setOriginLLH(const Location &loc) { + // reject external origin setting until the filter has finished + // bootstrap initialisation. InitialiseVariables() resets + // validOrigin, so an origin set before that point is lost. + // Callers (e.g. AHRS use_recorded_origin_maybe) will retry. + if (!statesInitialised) { + return false; + } return setOrigin(loc); } From f519de1c3d721a1a724a9632af820e55f2cffca3 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 31 Oct 2025 22:08:30 +0100 Subject: [PATCH 38/99] Plane: Logged rangefinder state --- ArduPlane/Log.cpp | 58 ++++++++++++++++++++++++++++ ArduPlane/Plane.cpp | 8 ++++ ArduPlane/Plane.h | 3 ++ ArduPlane/defines.h | 1 + Tools/autotest/vehicle_test_suite.py | 2 + 5 files changed, 72 insertions(+) diff --git a/ArduPlane/Log.cpp b/ArduPlane/Log.cpp index 827960bcde15ad..b07175c1750d7a 100644 --- a/ArduPlane/Log.cpp +++ b/ArduPlane/Log.cpp @@ -192,6 +192,45 @@ void Plane::Log_Write_Nav_Tuning() logger.WriteBlock(&pkt, sizeof(pkt)); } +#if AP_RANGEFINDER_ENABLED +struct PACKED log_RFNS { + LOG_PACKET_HEADER; + uint64_t time_us; + bool in_use; + bool in_range; + uint8_t in_range_count; + bool have_initial_reading; + float initial_range; + float last_distance; + float correction; + float initial_correction; + float last_stable_correction; + uint32_t last_correction_time_ms; + float height_estimate; +}; + +// Write a Rangefinder State packet +void Plane::Log_Write_RFNS() +{ + const struct log_RFNS pkt { + LOG_PACKET_HEADER_INIT(LOG_RFNS_MSG), + time_us : AP_HAL::micros64(), + in_use : rangefinder_state.in_use, + in_range : rangefinder_state.in_range, + in_range_count : rangefinder_state.in_range_count, + have_initial_reading : rangefinder_state.have_initial_reading, + initial_range : rangefinder_state.initial_range, + last_distance : rangefinder_state.last_distance, + correction : rangefinder_state.correction, + initial_correction : rangefinder_state.initial_correction, + last_stable_correction : rangefinder_state.last_stable_correction, + last_correction_time_ms : rangefinder_state.last_correction_time_ms, + height_estimate : rangefinder_state.height_estimate + }; + logger.WriteBlock(&pkt, sizeof(pkt)); +} +#endif // AP_RANGEFINDER_ENABLED + struct PACKED log_Status { LOG_PACKET_HEADER; uint64_t time_us; @@ -501,6 +540,25 @@ const struct LogStructure Plane::log_structure[] = { { LOG_OFG_MSG, sizeof(log_OFG_Guided), "OFG", "QffffBffB", "TimeUS,Arsp,ArspA,Alt,AltA,AltF,Hdg,HdgA,AltL", "snnmo-d--", "F--------" , true }, #endif + +#if AP_RANGEFINDER_ENABLED +// @LoggerMessage: RFNS +// @Description: Rangefinder state +// @Field: TimeUS: Time since system startup +// @Field: InUse: Whether the rangefinder is in use. +// @Field: InRng: Whether the rangefinder is in range. +// @Field: IRCnt: Count of in-range measurements +// @Field: Ms0OK: Whether there has been an initial measurement. +// @Field: Ms0: The initial measured range +// @Field: Dst: The last recorded distance +// @Field: Cor: The rangefinder correction +// @Field: Cor0: The initial rangefinder correction +// @Field: CorL: The last stable correction +// @Field: TimeCL: The last correction time +// @Field: HE: Height estimate + { LOG_RFNS_MSG, sizeof(log_RFNS), + "RFNS", "QBBBBfffffIf", "TimeUS,InUse,InRng,IRCnt,Ms0OK,Ms0,Dst,Cor,Cor0,CorL,TimeCL,HE", "s----mmmmmsm", "F----00000C0", true }, +#endif // AP_RANGEFINDER_ENABLED }; uint8_t Plane::get_num_log_structures() const diff --git a/ArduPlane/Plane.cpp b/ArduPlane/Plane.cpp index 59ebcd4f83447d..837372bf94bf4e 100644 --- a/ArduPlane/Plane.cpp +++ b/ArduPlane/Plane.cpp @@ -280,6 +280,14 @@ void Plane::update_logging10(void) camera_mount.write_log(); } #endif +#if AP_RANGEFINDER_ENABLED + if (should_log(MASK_LOG_NTUN)) { + if (rangefinder.has_orientation(rangefinder_orientation()) && + (g.rangefinder_landing.get() > 0)) { + Log_Write_RFNS(); + } + } +#endif } /* diff --git a/ArduPlane/Plane.h b/ArduPlane/Plane.h index 1889fdee160521..567f170c441223 100644 --- a/ArduPlane/Plane.h +++ b/ArduPlane/Plane.h @@ -955,6 +955,9 @@ class Plane : public AP_Vehicle { void Log_Write_OFG_Guided(); void Log_Write_Guided(void); void Log_Write_Nav_Tuning(); +#if AP_RANGEFINDER_ENABLED + void Log_Write_RFNS(); +#endif void Log_Write_Status(); void Log_Write_RC(void); void Log_Write_Vehicle_Startup_Messages(); diff --git a/ArduPlane/defines.h b/ArduPlane/defines.h index 2a3d3bb39829b4..55d5644f1ab78f 100644 --- a/ArduPlane/defines.h +++ b/ArduPlane/defines.h @@ -96,6 +96,7 @@ enum log_messages { LOG_OFG_MSG, LOG_TSIT_MSG, LOG_TILT_MSG, + LOG_RFNS_MSG, }; #define MASK_LOG_ATTITUDE_FAST (1<<0) diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py index 9fb5ac07ae175c..0e912db1653de4 100644 --- a/Tools/autotest/vehicle_test_suite.py +++ b/Tools/autotest/vehicle_test_suite.py @@ -2816,6 +2816,8 @@ def all_log_format_ids(self): continue if "#if AP_PLANE_OFFBOARD_GUIDED_SLEW_ENABLED" in line: continue + if "#if AP_RANGEFINDER_ENABLED" in line: + continue if "#end" in line: continue if "LOG_COMMON_STRUCTURES" in line: From 69c8eacbb8ec0029d1256b2ddbf661a2442057ae Mon Sep 17 00:00:00 2001 From: davidsastresas Date: Thu, 26 Mar 2026 15:13:01 +0100 Subject: [PATCH 39/99] AP_Camera_Servo.cpp: Fix broken CAMERA_INFORMATION flags --- libraries/AP_Camera/AP_Camera_Servo.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/AP_Camera/AP_Camera_Servo.cpp b/libraries/AP_Camera/AP_Camera_Servo.cpp index b5308f156c29e0..86dd9ec8c4e02e 100644 --- a/libraries/AP_Camera/AP_Camera_Servo.cpp +++ b/libraries/AP_Camera/AP_Camera_Servo.cpp @@ -9,6 +9,9 @@ extern const AP_HAL::HAL& hal; // initialize the AP_Camera_Servo driver void AP_Camera_Servo::init() { + // call parent init to set default camera info (flags, focal length, etc.) + AP_Camera_Backend::init(); + // set the zoom and focus to the trim point SRV_Channels::set_output_scaled(SRV_Channel::k_cam_zoom, 500); SRV_Channels::set_output_scaled(SRV_Channel::k_cam_focus, 500); From 9e501a345904e479986bbc3c9699260eb7c8da60 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 5 Dec 2025 22:35:33 +0100 Subject: [PATCH 40/99] AP_Logger: Added SPOL message --- libraries/AP_Logger/LogStructure.h | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/libraries/AP_Logger/LogStructure.h b/libraries/AP_Logger/LogStructure.h index eef769820c486b..94069f59b5d2d6 100644 --- a/libraries/AP_Logger/LogStructure.h +++ b/libraries/AP_Logger/LogStructure.h @@ -648,6 +648,13 @@ struct PACKED log_MotBatt { uint8_t mot_fail_flags; }; +struct PACKED log_SPOL { + LOG_PACKET_HEADER; + uint64_t time_us; + uint8_t spool_state; + uint8_t des_spool_state; +}; + struct PACKED log_VER { LOG_PACKET_HEADER; uint64_t time_us; @@ -1166,6 +1173,12 @@ struct PACKED log_VER { // @Field: ThrOut: Throttle output // @Field: FailFlags: bit 0 motor failed, bit 1 motors balanced, should be 2 in normal flight +// @LoggerMessage: SPOL +// @Description: Spool state logging +// @Field: TimeUS: Time since system startup +// @Field: Spl: Motors spool state +// @Field: SplDes: Desired motors spool state + // messages for all boards #define LOG_COMMON_STRUCTURES \ { LOG_FORMAT_MSG, sizeof(log_Format), \ @@ -1288,7 +1301,9 @@ LOG_STRUCTURE_FROM_AIS \ { LOG_VER_MSG, sizeof(log_VER), \ "VER", "QBHBBBBIZHBBII", "TimeUS,BT,BST,Maj,Min,Pat,FWT,GH,FWS,APJ,BU,FV,IMI,ICI", "s-------------", "F-------------", false }, \ { LOG_MOTBATT_MSG, sizeof(log_MotBatt), \ - "MOTB", "QfffffB", "TimeUS,LiftMax,BatVolt,ThLimit,ThrAvMx,ThrOut,FailFlags", "s------", "F------" , true } + "MOTB", "QfffffB", "TimeUS,LiftMax,BatVolt,ThLimit,ThrAvMx,ThrOut,FailFlags", "s------", "F------" , true }, \ + { LOG_SPOL_MSG, sizeof(log_SPOL), \ + "SPOL", "QBB", "TimeUS,Spl,SplDes", "s--", "F--" , true } // message types 0 to 31 reserved for vehicle-specific use @@ -1374,6 +1389,7 @@ enum LogMessages : uint8_t { LOG_SCRIPTING_MSG, LOG_VIDEO_STABILISATION_MSG, LOG_MOTBATT_MSG, + LOG_SPOL_MSG, LOG_VER_MSG, LOG_RCOUT2_MSG, LOG_RCOUT3_MSG, From 1a1c90d121831e5f1d189b7278ed7648c3d653d7 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 5 Dec 2025 22:35:51 +0100 Subject: [PATCH 41/99] AP_Motors: Logged SPOL message bruh --- libraries/AP_Motors/AP_Motors_Class.cpp | 27 +++++++++++++++++++++++++ libraries/AP_Motors/AP_Motors_Class.h | 5 +++++ 2 files changed, 32 insertions(+) diff --git a/libraries/AP_Motors/AP_Motors_Class.cpp b/libraries/AP_Motors/AP_Motors_Class.cpp index 8ba6499c357d5b..000e369b36389f 100644 --- a/libraries/AP_Motors/AP_Motors_Class.cpp +++ b/libraries/AP_Motors/AP_Motors_Class.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #define AP_MOTORS_SLEW_FILTER_CUTOFF 50.0f @@ -274,6 +276,31 @@ void AP_Motors::set_frame_string(const char * str) { } #endif +#if HAL_LOGGING_ENABLED +void AP_Motors::Log_Write_SPOL() { + + const bool spool_state_changed{_spool_state != _logged_spool_state}; + const bool des_spool_state_changed{_spool_desired != _logged_spool_desired}; + + // Log only changes. + if (!spool_state_changed && !des_spool_state_changed) { + return; + } + + _logged_spool_state = _spool_state; + _logged_spool_desired = _spool_desired; + + const struct log_SPOL pkt { + LOG_PACKET_HEADER_INIT(LOG_SPOL_MSG), + time_us : AP_HAL::micros64(), + spool_state : (uint8_t)_spool_state, + des_spool_state : (uint8_t)_spool_desired + }; + AP::logger().WriteBlock(&pkt, sizeof(pkt)); + +} +#endif + // output_test_seq - spin a motor at the pwm value specified // motor_seq is the motor's sequence number from 1 to the number of motors on the frame // pwm value is an actual pwm value that will be output, normally in the range of 1000 ~ 2000 diff --git a/libraries/AP_Motors/AP_Motors_Class.h b/libraries/AP_Motors/AP_Motors_Class.h index d6c2875ee94772..97150a16dedf8b 100644 --- a/libraries/AP_Motors/AP_Motors_Class.h +++ b/libraries/AP_Motors/AP_Motors_Class.h @@ -282,6 +282,9 @@ class AP_Motors { #if HAL_LOGGING_ENABLED // write log, to be called at 10hz virtual void Log_Write() {}; + + // log the spool rate, writes upon change + void Log_Write_SPOL(); #endif enum MotorOptions : uint8_t { @@ -332,6 +335,8 @@ class AP_Motors { LowPassFilterFloat _throttle_slew_filter; // filter for the output of the throttle slew DesiredSpoolState _spool_desired; // desired spool state SpoolState _spool_state; // current spool mode + DesiredSpoolState _logged_spool_desired; // last logged spool state + SpoolState _logged_spool_state; // last logged spool mode // mask of what channels need fast output uint32_t _motor_fast_mask; From 2445dd1b152cbd21d254b8f0b4f858c8ec682041 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 5 Dec 2025 22:36:02 +0100 Subject: [PATCH 42/99] Copter: Logged SPOL message --- ArduCopter/Copter.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ArduCopter/Copter.cpp b/ArduCopter/Copter.cpp index bcab68c2d007b0..29bc950a425d41 100644 --- a/ArduCopter/Copter.cpp +++ b/ArduCopter/Copter.cpp @@ -651,6 +651,8 @@ void Copter::loop_rate_logging() if (should_log(MASK_LOG_IMU_FAST)) { AP::ins().Write_IMU(); } + + motors->Log_Write_SPOL(); } // ten_hz_logging_loop From ee7ce71cb2162c08a1f2da5c3b380e79f8399dd1 Mon Sep 17 00:00:00 2001 From: Tim Tuxworth Date: Wed, 25 Mar 2026 16:46:35 -0600 Subject: [PATCH 43/99] AP_Scripting: plane_follow.lua fix sendtext error if no target vehicle --- libraries/AP_Scripting/applets/plane_follow.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/AP_Scripting/applets/plane_follow.lua b/libraries/AP_Scripting/applets/plane_follow.lua index 09aba10f448798..bda6e1409baf90 100644 --- a/libraries/AP_Scripting/applets/plane_follow.lua +++ b/libraries/AP_Scripting/applets/plane_follow.lua @@ -564,14 +564,13 @@ local follow_mode = { else if reported_target then -- i.e. if we previously reported a target but lost it if (now_ms - lost_target_now_ms) > 5000 then - gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": lost prior target: " .. follow:get_target_sysid()) + gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": lost prior SYSID: " .. tostring(follow:get_target_sysid()) ) lost_target_now_ms = now_ms end end reported_target = false - gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": no target: " .. follow:get_target_sysid()) + gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": no target SYSID: " .. tostring(follow:get_target_sysid()) ) end - return reported_target end function follow_mode.enable() From 9f0d7032346fab696cc353cf084380e4408c86d1 Mon Sep 17 00:00:00 2001 From: Karen Simeon Date: Wed, 25 Mar 2026 12:24:21 +1100 Subject: [PATCH 44/99] AP_Mount_Backend: set RC rate yaw_is_ef flag --- libraries/AP_Mount/AP_Mount_Backend.cpp | 3 +++ libraries/AP_Mount/AP_Mount_Backend.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/libraries/AP_Mount/AP_Mount_Backend.cpp b/libraries/AP_Mount/AP_Mount_Backend.cpp index 96c7690eca92c1..5ab29e7722f12d 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.cpp +++ b/libraries/AP_Mount/AP_Mount_Backend.cpp @@ -120,6 +120,9 @@ void AP_Mount_Backend::update_mnt_target_from_rc_target() mnt_target.angle_rad.yaw_is_ef = FPV_option ? false : _yaw_lock; mnt_target.angle_rad.roll_is_ef = FPV_option ? false : _roll_lock; mnt_target.angle_rad.pitch_is_ef = FPV_option ? false : _pitch_lock; + mnt_target.rate_rads.yaw_is_ef = FPV_option ? false : _yaw_lock; + mnt_target.rate_rads.roll_is_ef = FPV_option ? false : _roll_lock; + mnt_target.rate_rads.pitch_is_ef = FPV_option ? false : _pitch_lock; // if RC_RATE is zero, targets are angle if (_params.rc_rate_max <= 0) { diff --git a/libraries/AP_Mount/AP_Mount_Backend.h b/libraries/AP_Mount/AP_Mount_Backend.h index d0e0beae3d1414..14139a9a430774 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.h +++ b/libraries/AP_Mount/AP_Mount_Backend.h @@ -270,6 +270,8 @@ class AP_Mount_Backend float roll; // roll rate in radians/second float pitch; // roll rate in radians/second float yaw; // roll rate in radians/second + bool pitch_is_ef = true; + bool roll_is_ef = true; bool yaw_is_ef; // if set then `yaw` is a rate in earth frame }; From 4802d10d812695cd96d1fe3f1997ee7456343911 Mon Sep 17 00:00:00 2001 From: Tim Tuxworth Date: Fri, 27 Mar 2026 09:15:54 -0600 Subject: [PATCH 45/99] AP_Scripting: plane_follow.lua fix crosstrack oscillation --- libraries/AP_Scripting/applets/plane_follow.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/AP_Scripting/applets/plane_follow.lua b/libraries/AP_Scripting/applets/plane_follow.lua index bda6e1409baf90..9da4fa5ec186f6 100644 --- a/libraries/AP_Scripting/applets/plane_follow.lua +++ b/libraries/AP_Scripting/applets/plane_follow.lua @@ -25,7 +25,7 @@ FOLLP_TURN_DEG - if the target is more than this many degrees left or right, assume it's turning --]] -SCRIPT_VERSION = "4.7.0-074" +SCRIPT_VERSION = "4.7.0-075" SCRIPT_NAME = "Plane Follow" SCRIPT_NAME_SHORT = "PFollow" @@ -564,12 +564,12 @@ local follow_mode = { else if reported_target then -- i.e. if we previously reported a target but lost it if (now_ms - lost_target_now_ms) > 5000 then - gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": lost prior SYSID: " .. tostring(follow:get_target_sysid()) ) + gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": lost prior SYSID: " .. tostring(follow:get_target_sysid())) lost_target_now_ms = now_ms end end reported_target = false - gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": no target SYSID: " .. tostring(follow:get_target_sysid()) ) + gcs:send_text(MAV_SEVERITY.WARNING, SCRIPT_NAME_SHORT .. ": no target SYSID: " .. tostring(follow:get_target_sysid())) end return reported_target end @@ -919,8 +919,10 @@ function Update() mechanism = 1 -- position/location - for logging end - -- The desired heading needs a PID controller, especially when it gets close. - desired_heading = xt_pid:compute(desired_heading, cross_track_distance, (now_ms - now_heading_ms):tofloat() * 0.001) + -- The desired heading needs a PID controller for crosstrack, but only when it gets close. + if close or too_close_follow_up > 0 then + desired_heading = xt_pid:compute(desired_heading, cross_track_distance, (now_ms - now_heading_ms):tofloat() * 0.001) + end -- dv = interim delta velocity based on the pid controller using projected_distance per loop as the error (we want distance == 0) local dv_error = along_track_distance * refresh_rate * 2.0 From 8b9e6ccdfd42bef9d8c90efc8a6824970a505b4b Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sun, 29 Mar 2026 14:46:23 -0500 Subject: [PATCH 46/99] AP_DAL: correctly comment all fields of replay beacon data message A replay log plainly shows these fields are not zero and are used. They are filled in by `get_vehicle_position_ned` in `AP_DAL_Beacon::start_frame`. Some code sleuthing shows the estimates are the ones generated by the beeacon system, rather than the vehicle itself. --- libraries/AP_DAL/LogStructure.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/AP_DAL/LogStructure.h b/libraries/AP_DAL/LogStructure.h index 26c14c3dde3606..dd3c8e9b199f62 100644 --- a/libraries/AP_DAL/LogStructure.h +++ b/libraries/AP_DAL/LogStructure.h @@ -404,10 +404,10 @@ struct log_RMGI { // @LoggerMessage: RBCH // @Description: Replay Data Beacon Header -// @Field: PX: zero, unused -// @Field: PY: zero, unused -// @Field: PZ: zero, unused -// @Field: AE: zero, unused +// @Field: PX: beacon system estimated vehicle position, North +// @Field: PY: beacon system estimated vehicle position, East +// @Field: PZ: beacon system estimated vehicle position, Down +// @Field: AE: beacon system estimated vehicle position accuracy // @Field: OLat: origin latitude // @Field: OLng: origin longitude // @Field: OAlt: origin altitude From 653b6eddbc49a55dfae3c4fdb4fe04104bdbf075 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Tue, 17 Mar 2026 13:59:13 +1100 Subject: [PATCH 47/99] hwdef: re-instate probing for external IST8310 compasses on fmuv3 boards --- libraries/AP_HAL_ChibiOS/hwdef/fmuv3/hwdef.dat | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/AP_HAL_ChibiOS/hwdef/fmuv3/hwdef.dat b/libraries/AP_HAL_ChibiOS/hwdef/fmuv3/hwdef.dat index f3a7e34951b65f..808a3655ffdf84 100644 --- a/libraries/AP_HAL_ChibiOS/hwdef/fmuv3/hwdef.dat +++ b/libraries/AP_HAL_ChibiOS/hwdef/fmuv3/hwdef.dat @@ -465,3 +465,6 @@ define AP_BATTERY_SMBUS_SOLO_ENABLED (AP_BATTERY_SMBUS_ENABLED && BOARD_FLASH_SI # produce this error if we are on a 1M board define BOARD_CHECK_F427_USE_1M "ERROR: 1M flash use fmuv2" + +# probe for external IST8310 compasses: +define AP_COMPASS_IST8310_EXTERNAL_BUS_PROBING_ENABLED 1 From bf897ca94bbe13b55291b3e234129ea39532f538 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sun, 29 Mar 2026 16:56:07 -0500 Subject: [PATCH 48/99] AP_Camera: fix RunCam param metadata names to match code --- libraries/AP_Camera/AP_RunCam.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/AP_Camera/AP_RunCam.cpp b/libraries/AP_Camera/AP_RunCam.cpp index 6e770fb6d74162..6cb597550d0d3e 100644 --- a/libraries/AP_Camera/AP_RunCam.cpp +++ b/libraries/AP_Camera/AP_RunCam.cpp @@ -51,13 +51,13 @@ const AP_Param::GroupInfo AP_RunCam::var_info[] = { // @User: Advanced AP_GROUPINFO("BT_DELAY", 3, AP_RunCam, _boot_delay_ms, 7000), - // @Param: BTN_DELAY + // @Param: BTN_DELY // @DisplayName: RunCam button delay before allowing further button presses // @Description: Time it takes for the a RunCam button press to be actived in ms. If this is too short then commands can get out of sync. // @User: Advanced AP_GROUPINFO("BTN_DELY", 4, AP_RunCam, _button_delay_ms, RUNCAM_DEFAULT_BUTTON_PRESS_DELAY), - // @Param: MDE_DELAY + // @Param: MDE_DELY // @DisplayName: RunCam mode delay before allowing further button presses // @Description: Time it takes for the a RunCam mode button press to be actived in ms. If a mode change first requires a video recording change then double this value is used. If this is too short then commands can get out of sync. // @User: Advanced From 4291a7fa6c8e41cf6f426a222eb6250d92e73b5e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sun, 29 Mar 2026 14:55:20 -0500 Subject: [PATCH 49/99] AP_DAL: fill missing fields in replay body odometry message Inexplicably, the offset and delay fields were never filled in the RBOH message, despite the fact they have always been available and read from it. Filling these in is necessary for replay to work as the default delay is not zero, so reading zero in replay causes a difference in the replayed data. Also fix the comments which falsely say these fields are not used. --- libraries/AP_DAL/AP_DAL.cpp | 2 ++ libraries/AP_DAL/LogStructure.h | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/AP_DAL/AP_DAL.cpp b/libraries/AP_DAL/AP_DAL.cpp index 9c5398112f6307..0e1df8b4422587 100644 --- a/libraries/AP_DAL/AP_DAL.cpp +++ b/libraries/AP_DAL/AP_DAL.cpp @@ -420,6 +420,8 @@ void AP_DAL::writeBodyFrameOdom(float quality, const Vector3f &delPos, const Vec _RBOH.delAng = delAng; _RBOH.delTime = delTime; _RBOH.timeStamp_ms = timeStamp_ms; + _RBOH.posOffset = posOffset; + _RBOH.delay_ms = delay_ms; WRITE_REPLAY_BLOCK_IFCHANGED(RBOH, _RBOH, old); } diff --git a/libraries/AP_DAL/LogStructure.h b/libraries/AP_DAL/LogStructure.h index dd3c8e9b199f62..2aa3d284c5bafb 100644 --- a/libraries/AP_DAL/LogStructure.h +++ b/libraries/AP_DAL/LogStructure.h @@ -566,10 +566,10 @@ struct log_RWOH { // @Field: DAZ: delta-angle-Z // @Field: DT: delta-time // @Field: TS: data timestamp -// @Field: OX: zero, unused -// @Field: OY: zero, unused -// @Field: OZ: zero, unused -// @Field: D: zero, unused +// @Field: OX: pos-offset-X +// @Field: OY: pos-offset-Y +// @Field: OZ: pos-offset-Z +// @Field: D: delay in body odometry data struct log_RBOH { float quality; Vector3f delPos; From 42617933007c26e36e8bd2cc7d0f91e5da0ee8f9 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 16 Feb 2026 21:45:08 -0600 Subject: [PATCH 50/99] autotest: add copter EKF FuseBodyVel Replay subtest Unsure what the yaw misalignment is about but the log shows the copter flies just fine during RTL. --- Tools/autotest/arducopter.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index ba87c698600d3e..93a96dd81f8c0f 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -11328,6 +11328,40 @@ def test_replay_wind_and_airspeed_bit(self): return current_log_filepath + def test_replay_body_odom_bit(self): + self.set_parameters({ + "LOG_REPLAY": 1, + "LOG_DISARMED": 1, + + "EK3_SRC1_POSXY": 6, + "EK3_SRC1_VELXY": 6, + "EK3_SRC1_POSZ": 6, + "EK3_SRC1_VELZ": 6, + + "SIM_VICON_TMASK": 8, # send VISION_POSITION_DELTA for FuseBodyOdom + + "VISO_TYPE": 2, + "SERIAL5_PROTOCOL": 2, + "ARMING_SKIPCHK": (1 << 18), # suppress yaw misalignment complaint + }) + + self.customise_SITL_commandline(["--serial5=sim:vicon:"]) # implicit reboot + + self.wait_sensor_state(mavutil.mavlink.MAV_SYS_STATUS_LOGGING, True, True, True) + + current_log_filepath = self.current_onboard_log_filepath() + self.progress("Current log path: %s" % str(current_log_filepath)) + + self.change_mode("LOITER") + self.wait_ready_to_arm(require_absolute=False) + self.arm_vehicle() + self.takeoffAndMoveAway() + self.do_RTL() + + self.customise_SITL_commandline([]) # implicit reboot + + return current_log_filepath + def GPSBlendingLog(self): '''Test GPS Blending''' '''ensure we get dataflash log messages for blended instance''' @@ -11690,6 +11724,7 @@ def Replay(self): ('GPS', self.test_replay_gps_bit), ('GPSForYaw', self.test_replay_gps_yaw_bit), ('WindAndAirspeed', self.test_replay_wind_and_airspeed_bit), + ('BodyOdom', self.test_replay_body_odom_bit), ('Beacon', self.test_replay_beacon_bit), ('OpticalFlow', self.test_replay_optical_flow_bit), ] From ac9067dd979d5e0c8d72a3bec60fc718c459cfd7 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Thu, 19 Mar 2026 17:52:59 +1100 Subject: [PATCH 51/99] autotest: add test for Copter yaw speed when no RC input --- Tools/autotest/arducopter.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index 93a96dd81f8c0f..7e3ce2ecb0dc5c 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -150,14 +150,16 @@ def takeoff(self, self.change_mode(mode) if not self.armed(): self.wait_ready_to_arm(require_absolute=require_absolute, timeout=timeout) - self.zero_throttle() + if mode != 'GUIDED': + self.zero_throttle() self.arm_vehicle() if mode == 'GUIDED': self.user_takeoff(alt_min=alt_min, timeout=timeout, max_err=max_err) else: self.set_rc(3, takeoff_throttle) self.wait_altitude(alt_min-1, alt_min+max_err, relative=True, timeout=timeout, minimum_duration=alt_minimum_duration) - self.hover() + if mode != 'GUIDED': + self.hover() self.progress("TAKEOFF COMPLETE") def land_and_disarm(self, timeout=60): @@ -199,6 +201,7 @@ def takeoffAndMoveAway(self, dAlt=50, dDist=50): 4: 1500, }) self.takeoff(alt_min=dAlt, mode='GUIDED') + self.hover() self.change_mode("ALT_HOLD") self.progress("Yaw to east") @@ -845,6 +848,7 @@ def ThrottleFailsafe(self, side=60, timeout=360): self.start_subtest("If you haven't taken off yet RC failure should be instant disarm") self.change_mode("STABILIZE") self.set_parameter("DISARM_DELAY", 0) + self.wait_ready_to_arm() self.arm_vehicle() self.set_parameter("SIM_RC_FAIL", 1) self.disarm_wait(timeout=1) @@ -2275,9 +2279,8 @@ def FenceFloorAutoDisableLanding(self): self.assert_fence_enabled() # Change to RC controlled mode - self.change_mode('LOITER') - self.set_rc(3, 1800) + self.change_mode('LOITER') self.wait_mode('RTL', timeout=120) @@ -2314,9 +2317,8 @@ def FenceFloorAutoEnableOnArming(self): self.assert_fence_enabled() # Change to RC controlled mode - self.change_mode('LOITER') - self.set_rc(3, 1800) + self.change_mode('LOITER') self.wait_mode('RTL', timeout=120) # Assert fence is not healthy now that we are in RTL @@ -12225,7 +12227,7 @@ def SMART_RTL_EnterLeave(self): def SMART_RTL_Repeat(self): '''Test whether Smart RTL catches the repeat''' self.takeoff(alt_min=10, mode='GUIDED') - self.set_rc(3, 1500) + self.hover() self.change_mode("CIRCLE") self.delay_sim_time(1300) self.change_mode("SMART_RTL") @@ -14037,7 +14039,7 @@ def PILOT_THR_BHV(self): "DISARM_DELAY": 0, }) self.takeoff(2, mode='GUIDED') - self.set_rc(3, 1500) + self.hover() self.change_mode('LOITER') self.set_rc(3, 1300) @@ -14050,7 +14052,7 @@ def PILOT_THR_BHV(self): }) self.zero_throttle() self.takeoff(2, mode='GUIDED') - self.set_rc(3, 1500) + self.hover() self.change_mode('LOITER') self.set_rc(3, 1300) @@ -14081,6 +14083,7 @@ def CameraLogMessages(self): attitudes.append(self.assert_receive_message('ATTITUDE')) self.set_rc(12, 1000) + self.hover() self.change_mode('LOITER') self.set_rc(1, 1000) self.set_rc(2, 1000) @@ -14130,6 +14133,7 @@ def LoiterToGuidedHomeVSOrigin(self): self.takeoff(10, mode='GUIDED') here = self.mav.location() self.set_home(here) + self.hover() self.change_mode('LOITER') self.wait_altitude(here.alt-1, here.alt+1, minimum_duration=10) self.disarm_vehicle(force=True) @@ -15331,6 +15335,17 @@ def verify_yaw(mav, m): self.arm_vehicle() self.wait_disarmed() + def RC_OPTIONS_1_FS_THR_ENABLE_0(self): + '''check behaviour with unusual RC parameters''' + self.set_parameters({ + "RC_OPTIONS": 1, + "FS_THR_ENABLE": 0, + }) + self.reboot_sitl() + self.takeoff(10, mode='GUIDED') + self.wait_yaw_speed(0, minimum_duration=10) + self.do_land() + def WaitAndMaintainAttitude_RCFlight(self): '''just test WaitAndMaintainAttitude works''' WaitAndMaintainAttitude(self, 0, 0, epsilon=1).run() @@ -16010,6 +16025,7 @@ def tests2b(self): # this block currently around 9.5mins here self.AHRSOriginRecorded, self.TestTetherStuck, self.ScriptingFlipMode, + self.RC_OPTIONS_1_FS_THR_ENABLE_0, self.ScriptingFlyVelocity, self.EK3_EXT_NAV_vel_without_vert, self.CompassLearnCopyFromEKF, From e3ea5ade39aa2b90ab3444f2b8da6e6928091581 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Thu, 26 Mar 2026 13:11:22 +1100 Subject: [PATCH 52/99] autotest: zero throttle in RTL throttle-down won't have an effect. Throttle-up *might* have an effect, so we need to protect against that. We want to leave the vehicle at zero throttle after the RTL IMO, and zero is a better choice than middle while we *are* doing RTL throttle-down won't have an effect. Throttle-up *might* have an effect, so we need to protect against that. We want to leave the vehicle at zero throttle after the RTL IMO, and zero is a reasonable choice while we *are* doing RTL. --- Tools/autotest/arducopter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index 7e3ce2ecb0dc5c..3d3839e4384687 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -771,7 +771,7 @@ def assert_no_lateral_movement(mav, m): def do_RTL(self, distance_min=None, check_alt=True, distance_max=10, timeout=250, quiet=False): """Enter RTL mode and wait for the vehicle to disarm at Home.""" self.change_mode("RTL") - self.hover() + self.zero_throttle() self.wait_rtl_complete(check_alt=check_alt, distance_max=distance_max, timeout=timeout, quiet=True) def wait_rtl_complete(self, check_alt=True, distance_max=10, timeout=250, quiet=False): @@ -14581,7 +14581,9 @@ def AutoContinueOnRCFailsafe(self): self.assert_mode_is('AUTO') self.change_mode('LOITER') self.wait_groundspeed(0, 0.1, minimum_duration=30, timeout=450) - self.do_RTL() + # do_RTL plays with RC inputs, don't use it. + self.change_mode("RTL") + self.wait_rtl_complete() def MissionRTLYawBehaviour(self): '''check end-of-mission yaw behaviour''' From ba30bb9ba959c999153839f1f1645a56b5b8bc71 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Wed, 25 Mar 2026 17:17:00 +1100 Subject: [PATCH 53/99] RC_Channel: understand ignore-receiver when reading rc inputs previously this option only gated whether we read data from the HAL into our radio_in state. but that would mean read_input would still return true, even if we didn't actually read data from the receiver into the radio state. And that means everything in (e.g. Copter::read_radio thinks we have good channel data when we absolutely do not, meaning has_valid_input is true. Which is bad. --- libraries/RC_Channel/RC_Channels.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/RC_Channel/RC_Channels.cpp b/libraries/RC_Channel/RC_Channels.cpp index 82bb5de3c4e8d9..212fb2b233c936 100644 --- a/libraries/RC_Channel/RC_Channels.cpp +++ b/libraries/RC_Channel/RC_Channels.cpp @@ -86,7 +86,8 @@ uint8_t RC_Channels::get_radio_in(uint16_t *chans, const uint8_t num_channels) // update all the input channels bool RC_Channels::read_input(void) { - if (hal.rcin->new_input()) { + if (hal.rcin->new_input() && + !rc().option_is_enabled(RC_Channels::Option::IGNORE_RECEIVER)) { _has_had_rc_receiver = true; } else if (!has_new_overrides) { return false; From 613e4846c5dbd98b15e733301394f6294df29037 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Mon, 30 Mar 2026 16:34:07 +1100 Subject: [PATCH 54/99] autotest: reduce circling time in Arc waypoint test we spent so long making sure the thing circled that it could end up on the way hme before we checked our distance! --- Tools/autotest/arducopter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index 3d3839e4384687..e58d64f9d1eda9 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -638,7 +638,7 @@ def assert_no_lateral_movement(mav, m): self.wait_circling_point_with_radius( loc, radius, epsilon=1, - min_circle_time=10, + min_circle_time=7, timeout=30, track_angle=False, ) From 3cccd8b85156491b5d751a8685eb5d062c46b72f Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sat, 31 Jan 2026 15:44:41 -0600 Subject: [PATCH 55/99] AP_NavEKF3: loop over states in FuseBodyVel Kalman gain computation Same form and coefficients for most elements. Saves lots of flash. --- .../AP_NavEKF3/AP_NavEKF3_PosVelFusion.cpp | 135 ++++++------------ 1 file changed, 46 insertions(+), 89 deletions(-) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_PosVelFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_PosVelFusion.cpp index e3a17c4fb29842..b005c7d8423ec1 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_PosVelFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_PosVelFusion.cpp @@ -1580,51 +1580,37 @@ void NavEKF3_core::FuseBodyVel() Kfusion[4] = t77*(t69+P[4][5]*t4+P[4][0]*t14-P[4][6]*t11+P[4][1]*t18-P[4][2]*t21+P[4][3]*t24); Kfusion[5] = t77*(t32+P[5][4]*t9+P[5][0]*t14-P[5][6]*t11+P[5][1]*t18-P[5][2]*t21+P[5][3]*t24); Kfusion[6] = t77*(-t81+P[6][5]*t4+P[6][4]*t9+P[6][0]*t14+P[6][1]*t18-P[6][2]*t21+P[6][3]*t24); - Kfusion[7] = t77*(P[7][5]*t4+P[7][4]*t9+P[7][0]*t14-P[7][6]*t11+P[7][1]*t18-P[7][2]*t21+P[7][3]*t24); - Kfusion[8] = t77*(P[8][5]*t4+P[8][4]*t9+P[8][0]*t14-P[8][6]*t11+P[8][1]*t18-P[8][2]*t21+P[8][3]*t24); - Kfusion[9] = t77*(P[9][5]*t4+P[9][4]*t9+P[9][0]*t14-P[9][6]*t11+P[9][1]*t18-P[9][2]*t21+P[9][3]*t24); + + // values to calculate in Kfusion (others are set to zero, indices 0-6 ignored) + uint32_t kalman_mask = (1<<7) | (1<<8) | (1<<9); if (!inhibitDelAngBiasStates) { - Kfusion[10] = t77*(P[10][5]*t4+P[10][4]*t9+P[10][0]*t14-P[10][6]*t11+P[10][1]*t18-P[10][2]*t21+P[10][3]*t24); - Kfusion[11] = t77*(P[11][5]*t4+P[11][4]*t9+P[11][0]*t14-P[11][6]*t11+P[11][1]*t18-P[11][2]*t21+P[11][3]*t24); - Kfusion[12] = t77*(P[12][5]*t4+P[12][4]*t9+P[12][0]*t14-P[12][6]*t11+P[12][1]*t18-P[12][2]*t21+P[12][3]*t24); - } else { - // zero indexes 10 to 12 - zero_range(&Kfusion[0], 10, 12); + kalman_mask |= (1<<10) | (1<<11) | (1<<12); } if (!inhibitDelVelBiasStates && !badIMUdata) { for (uint8_t index = 0; index < 3; index++) { const uint8_t stateIndex = index + 13; if (!dvelBiasAxisInhibit[index]) { - Kfusion[stateIndex] = t77*(P[stateIndex][5]*t4+P[stateIndex][4]*t9+P[stateIndex][0]*t14-P[stateIndex][6]*t11+P[stateIndex][1]*t18-P[stateIndex][2]*t21+P[stateIndex][3]*t24); - } else { - Kfusion[stateIndex] = 0.0f; + kalman_mask |= (1< Date: Mon, 30 Mar 2026 21:42:23 +0100 Subject: [PATCH 56/99] Plane: drop AHRS out of full rate attitude logging --- ArduPlane/Log.cpp | 2 -- ArduPlane/Plane.cpp | 29 +++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/ArduPlane/Log.cpp b/ArduPlane/Log.cpp index b07175c1750d7a..b23a2fc02e8637 100644 --- a/ArduPlane/Log.cpp +++ b/ArduPlane/Log.cpp @@ -53,8 +53,6 @@ void Plane::Log_Write_Attitude(void) if (steerController.active()) { logger.Write_PID(LOG_PIDS_MSG, steerController.get_pid_info()); } - - AP::ahrs().Log_Write(); } // do fast logging for plane diff --git a/ArduPlane/Plane.cpp b/ArduPlane/Plane.cpp index 837372bf94bf4e..f77d116fcb26a0 100644 --- a/ArduPlane/Plane.cpp +++ b/ArduPlane/Plane.cpp @@ -268,13 +268,20 @@ void Plane::update_compass(void) */ void Plane::update_logging10(void) { - bool log_faster = (should_log(MASK_LOG_ATTITUDE_FULLRATE) || should_log(MASK_LOG_ATTITUDE_FAST)); - if (should_log(MASK_LOG_ATTITUDE_MED) && !log_faster) { + const bool attitude_med = should_log(MASK_LOG_ATTITUDE_MED); + const bool attitude_faster = should_log(MASK_LOG_ATTITUDE_FULLRATE) || should_log(MASK_LOG_ATTITUDE_FAST); + + // Log attitude only if no faster logging is selected + if (attitude_med && !attitude_faster) { Log_Write_Attitude(); - ahrs.Write_AOA_SSA(); - } else if (log_faster) { + AP::ahrs().Log_Write(); + } + + // If any attitude logging is enabled log AOA and SSA + if (attitude_med || attitude_faster) { ahrs.Write_AOA_SSA(); } + #if HAL_MOUNT_ENABLED if (should_log(MASK_LOG_CAMERA)) { camera_mount.write_log(); @@ -295,13 +302,19 @@ void Plane::update_logging10(void) */ void Plane::update_logging25(void) { - // MASK_LOG_ATTITUDE_FULLRATE logs at 400Hz, MASK_LOG_ATTITUDE_FAST at 25Hz, MASK_LOG_ATTIUDE_MED logs at 10Hz - // highest rate selected wins - bool log_faster = should_log(MASK_LOG_ATTITUDE_FULLRATE); - if (should_log(MASK_LOG_ATTITUDE_FAST) && !log_faster) { + const bool attitude_fast = should_log(MASK_LOG_ATTITUDE_FAST); + const bool attitude_full_rate = should_log(MASK_LOG_ATTITUDE_FULLRATE); + + // Log at fast rate if fast logging is the fastest enabled + if (attitude_fast && !attitude_full_rate) { Log_Write_Attitude(); } + // Log AHRS at fast rate if either fast or full rate is selected + if (attitude_fast || attitude_full_rate) { + AP::ahrs().Log_Write(); + } + if (should_log(MASK_LOG_CTUN)) { Log_Write_Control_Tuning(); #if AP_INERTIALSENSOR_HARMONICNOTCH_ENABLED From 0b05b743f86201f6382db1d1c569bfddd90621d2 Mon Sep 17 00:00:00 2001 From: Bob Long Date: Tue, 17 Mar 2026 12:58:14 +1100 Subject: [PATCH 57/99] AP_Scripting: add mount-driver example --- .../AP_Scripting/examples/mount-driver.lua | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 libraries/AP_Scripting/examples/mount-driver.lua diff --git a/libraries/AP_Scripting/examples/mount-driver.lua b/libraries/AP_Scripting/examples/mount-driver.lua new file mode 100644 index 00000000000000..1eb25497bf3aa9 --- /dev/null +++ b/libraries/AP_Scripting/examples/mount-driver.lua @@ -0,0 +1,153 @@ +-- mount-driver.lua: Example scripting gimbal driver +-- +-- Template for writing a Lua gimbal driver using the scripting mount backend. +-- Populate send_target_angles and send_target_rates with your gimbal's +-- protocol (serial, CAN, etc). This example simulates a gimbal by tracking +-- targets internally and reporting them back as attitude. +-- +-- Setup: +-- Set MNT1_TYPE = 9 (Scripting) and reboot +-- Copy this script to the APM/scripts directory and reboot +-- +-- Advanced usage: +-- The gimbal can be used as the Nth mount by setting MNTn_TYPE = 9 and +-- modifying the MOUNT_INSTANCE below. + +-- user definitions +local MOUNT_INSTANCE = 0 -- default to MNT1 + +-- global definitions +local INIT_INTERVAL_MS = 3000 -- attempt to initialise the gimbal at this interval +local UPDATE_INTERVAL_MS = 100 -- update at 10hz +local MAV_SEVERITY = {EMERGENCY=0, ALERT=1, CRITICAL=2, ERROR=3, WARNING=4, NOTICE=5, INFO=6, DEBUG=7} + +-- local variables +local sim_state = { + roll_ef_deg=0, -- roll/pitch earth frame, yaw body frame + pitch_ef_deg=0, -- (common for pwm-controlled brushless gimbals) + yaw_bf_deg=0 +} +local initialised = false +local last_update_ms = 0 + +-- wrap yaw angle in degrees to value between 0 and 360 +local function wrap_360(angle) + local res = math.fmod(angle, 360.0) + if res < 0 then + res = res + 360.0 + end + return res +end + +-- wrap yaw angle in degrees to value between -180 and +180 +local function wrap_180(angle_deg) + local res = wrap_360(angle_deg) + if res > 180 then + res = res - 360 + end + return res +end + +-- bind mount type parameter +local MNT_TYPE = Parameter("MNT" .. (MOUNT_INSTANCE + 1) .. "_TYPE") + +-- perform any required initialisation +local function init() + if MNT_TYPE:get() ~= 9 then + gcs:send_text(MAV_SEVERITY.CRITICAL, "MountDriver: set MNT" .. (MOUNT_INSTANCE + 1) .. "_TYPE=9") + return + end + + initialised = true + last_update_ms = millis():tofloat() + gcs:send_text(MAV_SEVERITY.INFO, "MountDriver: started") +end + +-- send target angles (in degrees) to gimbal +local function send_target_angles(roll_ef_deg, pitch_ef_deg, yaw_deg, yaw_is_ef) + -- default argument values + roll_ef_deg = roll_ef_deg or 0 + pitch_ef_deg = pitch_ef_deg or 0 + yaw_deg = yaw_deg or 0 + yaw_is_ef = yaw_is_ef or false + + if yaw_is_ef then + -- convert to body-frame + yaw_deg = wrap_180(yaw_deg - math.deg(ahrs:get_yaw_rad())) + end + + sim_state.roll_ef_deg = roll_ef_deg + sim_state.pitch_ef_deg = pitch_ef_deg + sim_state.yaw_bf_deg = yaw_deg +end + +-- send target rates (in deg/sec) to gimbal +local function send_target_rates(roll_degs, pitch_degs, yaw_degs, yaw_is_ef, dt_s) + -- default argument values + roll_degs = roll_degs or 0 + pitch_degs = pitch_degs or 0 + yaw_degs = yaw_degs or 0 + yaw_is_ef = yaw_is_ef or false + + if yaw_is_ef then + yaw_degs = yaw_degs - math.deg(ahrs:get_gyro():z()) + end + + send_target_angles( + sim_state.roll_ef_deg + roll_degs * dt_s, + sim_state.pitch_ef_deg + pitch_degs * dt_s, + sim_state.yaw_bf_deg + yaw_degs * dt_s, + false + ) +end + +-- the main update function +local function update() + + -- initialise connection to gimbal + if not initialised then + init() + return + end + + -- calculate dt + local now_ms = millis():tofloat() + local dt_s = (now_ms - last_update_ms) / 1000.0 + last_update_ms = now_ms + + -- report gimbal attitude. Must be called periodically or the backend reports + -- unhealthy. Ideally, populate this from a gimbal attitude message. If your + -- gimbal doesn't report attitude but you can detect it is alive, stop calling + -- this when it stops responding so ArduPilot gets real health feedback. Here + -- we just report our sim state directly since there is no real gimbal. + mount:set_attitude_euler(MOUNT_INSTANCE, sim_state.roll_ef_deg, sim_state.pitch_ef_deg, sim_state.yaw_bf_deg) + + -- send angle target + local roll_deg, pitch_deg, yaw_deg, yaw_is_ef = mount:get_angle_target(MOUNT_INSTANCE) + if roll_deg and pitch_deg and yaw_deg then + send_target_angles(roll_deg, pitch_deg, yaw_deg, yaw_is_ef) + return + end + + -- send rate target + local roll_degs, pitch_degs, yaw_degs + roll_degs, pitch_degs, yaw_degs, yaw_is_ef = mount:get_rate_target(MOUNT_INSTANCE) + if roll_degs and pitch_degs and yaw_degs then + send_target_rates(roll_degs, pitch_degs, yaw_degs, yaw_is_ef, dt_s) + return + end +end + +local function protected_wrapper() + local success, err = pcall(update) + if not success then + gcs:send_text(MAV_SEVERITY.ERROR, "MountDriver: " .. err) + return protected_wrapper, 1000 + end + if not initialised then + return protected_wrapper, INIT_INTERVAL_MS + end + return protected_wrapper, UPDATE_INTERVAL_MS +end + +return protected_wrapper() From 1b0b50b3bb8efd19f47f41d34683b868b101252d Mon Sep 17 00:00:00 2001 From: Bob Long Date: Tue, 17 Mar 2026 12:58:15 +1100 Subject: [PATCH 58/99] autotest: add ScriptMountDriver test Co-authored-by: Bob Long --- Tools/autotest/arducopter.py | 116 +++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index e58d64f9d1eda9..833f96020c3ebe 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -13276,6 +13276,121 @@ def ScriptMountAllModes(self): self.do_RTL() + def ScriptMountDriver(self): + '''test scripting mount driver with all modes and camera''' + self.context_push() + + self.set_parameters({ + "SCR_ENABLE": 1, + "MNT1_TYPE": 9, + "MNT1_PITCH_MIN": -45, + "MNT1_PITCH_MAX": 45, + }) + self.reboot_sitl() + + self.install_example_script_context('mount-driver.lua') + self.context_collect('STATUSTEXT') + self.reboot_sitl() + + self.wait_statustext("MountDriver: started", check_context=True, timeout=30) + self.wait_ready_to_arm() + + self.takeoff(20, mode='GUIDED') + + # test RETRACT mode - exercises angle_converted path + self.start_subtest("RETRACT mode") + retract_pitch = -15 + self.set_parameter("MNT1_RETRACT_Y", retract_pitch) + self.run_cmd( + mavutil.mavlink.MAV_CMD_DO_MOUNT_CONTROL, + p7=mavutil.mavlink.MAV_MOUNT_MODE_RETRACT, + ) + self.test_mount_pitch(retract_pitch, 1, mavutil.mavlink.MAV_MOUNT_MODE_RETRACT) + + # test NEUTRAL mode - exercises angle_converted path + self.start_subtest("NEUTRAL mode") + neutral_pitch = -10 + self.set_parameter("MNT1_NEUTRAL_Y", neutral_pitch) + self.run_cmd( + mavutil.mavlink.MAV_CMD_DO_MOUNT_CONTROL, + p7=mavutil.mavlink.MAV_MOUNT_MODE_NEUTRAL, + ) + self.test_mount_pitch(neutral_pitch, 1, mavutil.mavlink.MAV_MOUNT_MODE_NEUTRAL) + + # test MAVLINK_TARGETING with angle + self.start_subtest("MAVLINK_TARGETING") + self.run_cmd( + mavutil.mavlink.MAV_CMD_DO_MOUNT_CONTROL, + p1=20, # pitch + p2=0, # roll + p3=0, # yaw + p7=mavutil.mavlink.MAV_MOUNT_MODE_MAVLINK_TARGETING, + ) + self.test_mount_pitch(20, 1, mavutil.mavlink.MAV_MOUNT_MODE_MAVLINK_TARGETING) + + # test MAVLINK_TARGETING with rate + self.start_subtest("MAVLINK_TARGETING rate") + # start at pitch 0 + self.run_cmd( + mavutil.mavlink.MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW, + p1=0, # pitch angle + p2=0, # yaw angle + ) + self.test_mount_pitch(0, 5, mavutil.mavlink.MAV_MOUNT_MODE_MAVLINK_TARGETING) + # send pitch rate of -30 deg/s for 2 seconds + self.run_cmd( + mavutil.mavlink.MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW, + p1=float('nan'), # pitch angle (NaN = use rate) + p2=float('nan'), # yaw angle (NaN = use rate) + p3=-30, # pitch rate deg/s + p4=0, # yaw rate deg/s + ) + self.delay_sim_time(2) + # expect pitch around -60 + _, mount_pitch, _, _ = self.get_mount_roll_pitch_yaw_deg() + if abs(mount_pitch - (-60)) > 20: + raise NotAchievedException( + "Rate mode pitch incorrect: got=%f want=-60 (+/-20)" % mount_pitch) + self.progress("Rate mode pitch correct: %f degrees (~-60)" % mount_pitch) + + # test GPS_POINT (ROI) + self.start_subtest("GPS_POINT (ROI)") + takeoff_loc = self.mav.location() + t = self.offset_location_ne(takeoff_loc, 20, 0) + self.run_cmd_int( + mavutil.mavlink.MAV_CMD_DO_SET_ROI_LOCATION, + p5=int(t.lat * 1e7), + p6=int(t.lng * 1e7), + p7=0, + frame=mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT, + ) + # We took off to 20m and the target is 20m North, so pitch should be around -45 degrees + self.test_mount_pitch(-45, 5, mavutil.mavlink.MAV_MOUNT_MODE_GPS_POINT) + + # Reposition the aircraft 20m South of the takeoff location, + # so it's now 40m from the target, and pitch should be around + # -27 degrees + t = self.offset_location_ne(takeoff_loc, -20, 0) + self.send_set_position_target_global_int(int(t.lat * 1e7), int(t.lng * 1e7), 20) + self.test_mount_pitch(-27, 5, mavutil.mavlink.MAV_MOUNT_MODE_GPS_POINT, constrained=False) + + # test HOME_LOCATION + self.start_subtest("HOME_LOCATION") + self.run_cmd( + mavutil.mavlink.MAV_CMD_DO_MOUNT_CONTROL, + p7=mavutil.mavlink.MAV_MOUNT_MODE_HOME_LOCATION, + ) + # We are 20m South of home and at 20m altitude, so pitch should be around -45 degrees + self.test_mount_pitch(-45, 5, mavutil.mavlink.MAV_MOUNT_MODE_HOME_LOCATION, constrained=False) + + # Reposition over home again, pitch should move to -90 degrees + self.send_set_position_target_global_int(int(takeoff_loc.lat * 1e7), int(takeoff_loc.lng * 1e7), 20) + self.test_mount_pitch(-90, 5, mavutil.mavlink.MAV_MOUNT_MODE_HOME_LOCATION, constrained=False) + + self.do_RTL() + self.context_pop() + self.reboot_sitl() + def ScriptCopterPosOffsets(self): '''test the copter-posoffset.lua example script''' self.context_push() @@ -15972,6 +16087,7 @@ def tests2b(self): # this block currently around 9.5mins here self.ThrottleGainBoost, self.ScriptMountPOI, self.ScriptMountAllModes, + self.ScriptMountDriver, self.ScriptCopterPosOffsets, self.MountSolo, self.FlyMissionTwice, From e9e6068c2f167f89ef8b8d1d4b15f93d950b1852 Mon Sep 17 00:00:00 2001 From: Bob Long Date: Tue, 17 Mar 2026 12:58:19 +1100 Subject: [PATCH 59/99] AP_Mount: fix get_angle_target for converted cmds If a backend gets the target mode converted to angle, get_angle_target will return the converted angles. This fixes scripting backends that relied on get_angle_target --- libraries/AP_Mount/AP_Mount_Backend.cpp | 9 ++++++++- libraries/AP_Mount/AP_Mount_Backend.h | 1 + libraries/AP_Mount/AP_Mount_Scripting.cpp | 2 ++ libraries/AP_Mount/AP_Mount_Scripting.h | 3 ++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Backend.cpp b/libraries/AP_Mount/AP_Mount_Backend.cpp index 5ab29e7722f12d..3178b903707463 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.cpp +++ b/libraries/AP_Mount/AP_Mount_Backend.cpp @@ -1199,6 +1199,9 @@ void AP_Mount_Backend::_update_mnt_target() void AP_Mount_Backend::send_target_to_gimbal() { + // clear valid flag; set below if angles are sent + mnt_target.angle_converted = false; + // process any pending clear-roi-target // it is assumed that we have already zeroed _roi_target if (clear_roi_pending && natively_supports(MountTargetType::LOCATION)) { @@ -1240,6 +1243,7 @@ void AP_Mount_Backend::send_target_to_gimbal() if (natively_supports(MountTargetType::ANGLE)) { // we integrate the rates into the angle: update_angle_target_from_rate(mnt_target.rate_rads, mnt_target.angle_rad); + mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); return; } @@ -1250,6 +1254,7 @@ void AP_Mount_Backend::send_target_to_gimbal() // we update mnt_target for reporting purposes const Vector3f &angle_bf_target = _params.retract_angles.get(); mnt_target.angle_rad.set(angle_bf_target*DEG_TO_RAD, false); + mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); return; } @@ -1260,6 +1265,7 @@ void AP_Mount_Backend::send_target_to_gimbal() // we update mnt_target for reporting purposes const Vector3f &angle_bf_target = _params.neutral_angles.get(); mnt_target.angle_rad.set(angle_bf_target*DEG_TO_RAD, false); + mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); return; } @@ -1267,6 +1273,7 @@ void AP_Mount_Backend::send_target_to_gimbal() case MountTargetType::LOCATION: if (natively_supports(MountTargetType::ANGLE)) { if (get_angle_target_to_roi(mnt_target.angle_rad)) { + mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); } return; @@ -1294,7 +1301,7 @@ bool AP_Mount_Backend::get_rate_target(float& roll_degs, float& pitch_degs, floa // get target angle in deg. returns true on success bool AP_Mount_Backend::get_angle_target(float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame) { - if (mnt_target.target_type == MountTargetType::ANGLE) { + if (mnt_target.target_type == MountTargetType::ANGLE || mnt_target.angle_converted) { roll_deg = degrees(mnt_target.angle_rad.roll); pitch_deg = degrees(mnt_target.angle_rad.pitch); yaw_deg = degrees(mnt_target.angle_rad.yaw); diff --git a/libraries/AP_Mount/AP_Mount_Backend.h b/libraries/AP_Mount/AP_Mount_Backend.h index 14139a9a430774..007d46e352abb3 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.h +++ b/libraries/AP_Mount/AP_Mount_Backend.h @@ -398,6 +398,7 @@ class AP_Mount_Backend uint32_t last_rate_request_ms; uint32_t poi_start_ms; // time we started trying to find the gimbal POI for an AuxFunc::MOUNT_POI_LOCK bool pointing_at_poi_at_home_alt; + bool angle_converted; // true if a non-angle target was converted to angles by send_target_to_gimbal } mnt_target; // RP earth frame locks accessible by backend diff --git a/libraries/AP_Mount/AP_Mount_Scripting.cpp b/libraries/AP_Mount/AP_Mount_Scripting.cpp index cee5d98b3c7d4a..e50b96583f8556 100644 --- a/libraries/AP_Mount/AP_Mount_Scripting.cpp +++ b/libraries/AP_Mount/AP_Mount_Scripting.cpp @@ -21,6 +21,8 @@ void AP_Mount_Scripting::update() AP_Mount_Backend::update(); update_mnt_target(); + + send_target_to_gimbal(); } // return true if healthy diff --git a/libraries/AP_Mount/AP_Mount_Scripting.h b/libraries/AP_Mount/AP_Mount_Scripting.h index e16bc0e411c69a..273ba75f418a54 100644 --- a/libraries/AP_Mount/AP_Mount_Scripting.h +++ b/libraries/AP_Mount/AP_Mount_Scripting.h @@ -41,9 +41,10 @@ class AP_Mount_Scripting : public AP_Mount_Backend // Scripting doesn't actually send anything (the script polls the // library for the targets) uint8_t natively_supported_mount_target_types() const override { - return NATIVE_ANGLES_ONLY; + return NATIVE_ANGLES_AND_RATES_ONLY; }; void send_target_angles(const MountAngleTarget &angle_rad) override {}; + void send_target_rates(const MountRateTarget &rate_rads) override {}; // get attitude as a quaternion. returns true on success bool get_attitude_quaternion(Quaternion& att_quat) override; From 9d7fd200625fe2a87357dc820c4b7d637346dee0 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Tue, 24 Mar 2026 22:15:08 +1100 Subject: [PATCH 60/99] AP_Mount: fix scripting backend for non-angle target modes Scripting backends (e.g. mount-djirs2-driver.lua) poll get_angle_target() and get_rate_target() rather than receiving pushed targets. Before this fix, get_angle_target() returned false for RETRACT, NEUTRAL, and LOCATION modes because it only checked target_type == ANGLE, even though send_target_to_gimbal() had already written the converted angle into mnt_target.angle_rad. Fix by calling send_target_to_gimbal() from AP_Mount_Scripting::update() (matching every other backend) and implementing send_target_angles() to stamp mnt_target.target_type = ANGLE after send_target_to_gimbal() has written the converted value into mnt_target.angle_rad. This makes get_angle_target() return the converted value without any changes to the base class. Declare NATIVE_ANGLES_AND_RATES_ONLY so that RATE targets are not converted to angles internally; the Lua script receives them via get_rate_target() and performs its own integration. send_target_rates() is a no-op because mnt_target.target_type stays RATE, which is all get_rate_target() checks. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount_Backend.cpp | 9 +-------- libraries/AP_Mount/AP_Mount_Backend.h | 1 - libraries/AP_Mount/AP_Mount_Scripting.cpp | 10 ++++++++++ libraries/AP_Mount/AP_Mount_Scripting.h | 11 ++++++++--- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Backend.cpp b/libraries/AP_Mount/AP_Mount_Backend.cpp index 3178b903707463..5ab29e7722f12d 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.cpp +++ b/libraries/AP_Mount/AP_Mount_Backend.cpp @@ -1199,9 +1199,6 @@ void AP_Mount_Backend::_update_mnt_target() void AP_Mount_Backend::send_target_to_gimbal() { - // clear valid flag; set below if angles are sent - mnt_target.angle_converted = false; - // process any pending clear-roi-target // it is assumed that we have already zeroed _roi_target if (clear_roi_pending && natively_supports(MountTargetType::LOCATION)) { @@ -1243,7 +1240,6 @@ void AP_Mount_Backend::send_target_to_gimbal() if (natively_supports(MountTargetType::ANGLE)) { // we integrate the rates into the angle: update_angle_target_from_rate(mnt_target.rate_rads, mnt_target.angle_rad); - mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); return; } @@ -1254,7 +1250,6 @@ void AP_Mount_Backend::send_target_to_gimbal() // we update mnt_target for reporting purposes const Vector3f &angle_bf_target = _params.retract_angles.get(); mnt_target.angle_rad.set(angle_bf_target*DEG_TO_RAD, false); - mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); return; } @@ -1265,7 +1260,6 @@ void AP_Mount_Backend::send_target_to_gimbal() // we update mnt_target for reporting purposes const Vector3f &angle_bf_target = _params.neutral_angles.get(); mnt_target.angle_rad.set(angle_bf_target*DEG_TO_RAD, false); - mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); return; } @@ -1273,7 +1267,6 @@ void AP_Mount_Backend::send_target_to_gimbal() case MountTargetType::LOCATION: if (natively_supports(MountTargetType::ANGLE)) { if (get_angle_target_to_roi(mnt_target.angle_rad)) { - mnt_target.angle_converted = true; send_target_angles(mnt_target.angle_rad); } return; @@ -1301,7 +1294,7 @@ bool AP_Mount_Backend::get_rate_target(float& roll_degs, float& pitch_degs, floa // get target angle in deg. returns true on success bool AP_Mount_Backend::get_angle_target(float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame) { - if (mnt_target.target_type == MountTargetType::ANGLE || mnt_target.angle_converted) { + if (mnt_target.target_type == MountTargetType::ANGLE) { roll_deg = degrees(mnt_target.angle_rad.roll); pitch_deg = degrees(mnt_target.angle_rad.pitch); yaw_deg = degrees(mnt_target.angle_rad.yaw); diff --git a/libraries/AP_Mount/AP_Mount_Backend.h b/libraries/AP_Mount/AP_Mount_Backend.h index 007d46e352abb3..14139a9a430774 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.h +++ b/libraries/AP_Mount/AP_Mount_Backend.h @@ -398,7 +398,6 @@ class AP_Mount_Backend uint32_t last_rate_request_ms; uint32_t poi_start_ms; // time we started trying to find the gimbal POI for an AuxFunc::MOUNT_POI_LOCK bool pointing_at_poi_at_home_alt; - bool angle_converted; // true if a non-angle target was converted to angles by send_target_to_gimbal } mnt_target; // RP earth frame locks accessible by backend diff --git a/libraries/AP_Mount/AP_Mount_Scripting.cpp b/libraries/AP_Mount/AP_Mount_Scripting.cpp index e50b96583f8556..cc6b132d8d627b 100644 --- a/libraries/AP_Mount/AP_Mount_Scripting.cpp +++ b/libraries/AP_Mount/AP_Mount_Scripting.cpp @@ -41,6 +41,16 @@ void AP_Mount_Scripting::set_attitude_euler(float roll_deg, float pitch_deg, flo current_angle_deg.z = yaw_bf_deg; } +// called by send_target_to_gimbal() after it has written the converted angle +// into mnt_target.angle_rad. Stamp the target_type as ANGLE so that the +// base-class get_angle_target() returns the value to the Lua script. +// This covers every non-ANGLE mode that converts to angles (RETRACT, NEUTRAL, +// LOCATION, and rate-to-angle if the backend were NATIVE_ANGLES_ONLY). +void AP_Mount_Scripting::send_target_angles(const MountAngleTarget &angle_rad) +{ + mnt_target.target_type = MountTargetType::ANGLE; +} + // get attitude as a quaternion. returns true on success bool AP_Mount_Scripting::get_attitude_quaternion(Quaternion& att_quat) { diff --git a/libraries/AP_Mount/AP_Mount_Scripting.h b/libraries/AP_Mount/AP_Mount_Scripting.h index 273ba75f418a54..f880b3df6692ad 100644 --- a/libraries/AP_Mount/AP_Mount_Scripting.h +++ b/libraries/AP_Mount/AP_Mount_Scripting.h @@ -38,12 +38,17 @@ class AP_Mount_Scripting : public AP_Mount_Backend protected: - // Scripting doesn't actually send anything (the script polls the - // library for the targets) + // Scripting backends poll get_angle_target / get_rate_target rather than + // receiving pushed targets, so native support for both types is declared so + // send_target_to_gimbal() never converts rates to angles on the backend's + // behalf. send_target_angles() stamps mnt_target.target_type = ANGLE so + // that get_angle_target() returns the converted value for non-angle modes + // (RETRACT, NEUTRAL, LOCATION) that send_target_to_gimbal() converts and + // stores into mnt_target.angle_rad before calling send_target_angles(). uint8_t natively_supported_mount_target_types() const override { return NATIVE_ANGLES_AND_RATES_ONLY; }; - void send_target_angles(const MountAngleTarget &angle_rad) override {}; + void send_target_angles(const MountAngleTarget &angle_rad) override; void send_target_rates(const MountRateTarget &rate_rads) override {}; // get attitude as a quaternion. returns true on success From afb9b09972e13a673c4c2f7b12ddc2d04574ed52 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Tue, 24 Mar 2026 22:46:38 +1100 Subject: [PATCH 61/99] AP_Mount: gate get_angle/rate_target behind AP_SCRIPTING_ENABLED; scripting backend fully self-contained get_angle_target() and get_rate_target() in AP_Mount_Backend and AP_Mount are now declared and defined only when AP_SCRIPTING_ENABLED is set, matching the existing treatment of get_location_target(). AP_Mount_Backend::write_log() read mnt_target directly when scripting is not compiled in, avoiding the dependency on those methods. AP_Mount_Scripting gains its own ScriptTargetType enum and _angle_target/_rate_target members. send_target_angles() and send_target_rates() now store the pushed target into those members. get_angle_target() / get_rate_target() overrides return the stored values, removing the previous hack of stamping mnt_target.target_type = ANGLE inside send_target_angles(). _script_target_type is reset to NONE at the top of each update() cycle so stale targets are never returned. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount.cpp | 2 +- libraries/AP_Mount/AP_Mount.h | 2 +- libraries/AP_Mount/AP_Mount_Backend.cpp | 9 ++++- libraries/AP_Mount/AP_Mount_Backend.h | 6 +-- libraries/AP_Mount/AP_Mount_Scripting.cpp | 48 ++++++++++++++++++++--- libraries/AP_Mount/AP_Mount_Scripting.h | 27 +++++++++---- 6 files changed, 74 insertions(+), 20 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount.cpp b/libraries/AP_Mount/AP_Mount.cpp index a8dcb1865fcb6e..cf411cdd156ce4 100644 --- a/libraries/AP_Mount/AP_Mount.cpp +++ b/libraries/AP_Mount/AP_Mount.cpp @@ -728,6 +728,7 @@ bool AP_Mount::pre_arm_checks(char *failure_msg, uint8_t failure_msg_len) return true; } +#if AP_SCRIPTING_ENABLED // get target rate in deg/sec. returns true on success bool AP_Mount::get_rate_target(uint8_t instance, float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame) { @@ -748,7 +749,6 @@ bool AP_Mount::get_angle_target(uint8_t instance, float& roll_deg, float& pitch_ return backend->get_angle_target(roll_deg, pitch_deg, yaw_deg, yaw_is_earth_frame); } -#if AP_SCRIPTING_ENABLED // get mount target location. returns true on success bool AP_Mount::get_location_target(uint8_t instance, Location& target_loc) { diff --git a/libraries/AP_Mount/AP_Mount.h b/libraries/AP_Mount/AP_Mount.h index 88c206ed3a18dd..ec5a52402ec21c 100644 --- a/libraries/AP_Mount/AP_Mount.h +++ b/libraries/AP_Mount/AP_Mount.h @@ -246,13 +246,13 @@ class AP_Mount // any failure_msg returned will not include a prefix bool pre_arm_checks(char *failure_msg, uint8_t failure_msg_len); +#if AP_SCRIPTING_ENABLED // get target rate in deg/sec. returns true on success bool get_rate_target(uint8_t instance, float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame); // get target angle in deg. returns true on success bool get_angle_target(uint8_t instance, float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame); -#if AP_SCRIPTING_ENABLED // get mount target location. returns true on success bool get_location_target(uint8_t instance, Location& target_loc); #endif diff --git a/libraries/AP_Mount/AP_Mount_Backend.cpp b/libraries/AP_Mount/AP_Mount_Backend.cpp index 5ab29e7722f12d..4ceafb2d03a0ad 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.cpp +++ b/libraries/AP_Mount/AP_Mount_Backend.cpp @@ -619,7 +619,12 @@ void AP_Mount_Backend::write_log(uint64_t timestamp_us) float target_pitch = nanf; float target_yaw = nanf; bool target_yaw_is_ef = false; - IGNORE_RETURN(get_angle_target(target_roll, target_pitch, target_yaw, target_yaw_is_ef)); + if (mnt_target.target_type == MountTargetType::ANGLE) { + target_roll = degrees(mnt_target.angle_rad.roll); + target_pitch = degrees(mnt_target.angle_rad.pitch); + target_yaw = degrees(mnt_target.angle_rad.yaw); + target_yaw_is_ef = mnt_target.angle_rad.yaw_is_ef; + } // get rangefinder distance float rangefinder_dist = nanf; @@ -1278,6 +1283,7 @@ void AP_Mount_Backend::send_target_to_gimbal() } +#if AP_SCRIPTING_ENABLED // get target rate in deg/sec. returns true on success bool AP_Mount_Backend::get_rate_target(float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame) { @@ -1304,7 +1310,6 @@ bool AP_Mount_Backend::get_angle_target(float& roll_deg, float& pitch_deg, float return false; } -#if AP_SCRIPTING_ENABLED // return target location if available // returns true if a target location is available and fills in target_loc argument bool AP_Mount_Backend::get_location_target(Location &_target_loc) diff --git a/libraries/AP_Mount/AP_Mount_Backend.h b/libraries/AP_Mount/AP_Mount_Backend.h index 14139a9a430774..baf87101129ae9 100644 --- a/libraries/AP_Mount/AP_Mount_Backend.h +++ b/libraries/AP_Mount/AP_Mount_Backend.h @@ -155,13 +155,13 @@ class AP_Mount_Backend // handle GIMBAL_DEVICE_ATTITUDE_STATUS message virtual void handle_gimbal_device_attitude_status(const mavlink_message_t &msg) {} +#if AP_SCRIPTING_ENABLED // get target rate in deg/sec. returns true on success - bool get_rate_target(float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame); + virtual bool get_rate_target(float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame); // get target angle in deg. returns true on success - bool get_angle_target(float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame); + virtual bool get_angle_target(float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame); -#if AP_SCRIPTING_ENABLED // get mount target location. returns true on success bool get_location_target(Location &target_loc); #endif diff --git a/libraries/AP_Mount/AP_Mount_Scripting.cpp b/libraries/AP_Mount/AP_Mount_Scripting.cpp index cc6b132d8d627b..d5521b33dd95a0 100644 --- a/libraries/AP_Mount/AP_Mount_Scripting.cpp +++ b/libraries/AP_Mount/AP_Mount_Scripting.cpp @@ -20,6 +20,10 @@ void AP_Mount_Scripting::update() { AP_Mount_Backend::update(); + // reset script target type so get_angle_target / get_rate_target return + // false until send_target_to_gimbal() writes a fresh target this cycle + _script_target_type = ScriptTargetType::NONE; + update_mnt_target(); send_target_to_gimbal(); @@ -41,14 +45,46 @@ void AP_Mount_Scripting::set_attitude_euler(float roll_deg, float pitch_deg, flo current_angle_deg.z = yaw_bf_deg; } -// called by send_target_to_gimbal() after it has written the converted angle -// into mnt_target.angle_rad. Stamp the target_type as ANGLE so that the -// base-class get_angle_target() returns the value to the Lua script. -// This covers every non-ANGLE mode that converts to angles (RETRACT, NEUTRAL, -// LOCATION, and rate-to-angle if the backend were NATIVE_ANGLES_ONLY). +// called by send_target_to_gimbal() with the angle target for this cycle. +// Store it so get_angle_target() can return it to the Lua script. void AP_Mount_Scripting::send_target_angles(const MountAngleTarget &angle_rad) { - mnt_target.target_type = MountTargetType::ANGLE; + _angle_target = angle_rad; + _script_target_type = ScriptTargetType::ANGLE; +} + +// called by send_target_to_gimbal() with the rate target for this cycle. +// Store it so get_rate_target() can return it to the Lua script. +void AP_Mount_Scripting::send_target_rates(const MountRateTarget &rate_rads) +{ + _rate_target = rate_rads; + _script_target_type = ScriptTargetType::RATE; +} + +// get target angle in deg. returns true on success +bool AP_Mount_Scripting::get_angle_target(float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame) +{ + if (_script_target_type != ScriptTargetType::ANGLE) { + return false; + } + roll_deg = degrees(_angle_target.roll); + pitch_deg = degrees(_angle_target.pitch); + yaw_deg = degrees(_angle_target.yaw); + yaw_is_earth_frame = _angle_target.yaw_is_ef; + return true; +} + +// get target rate in deg/sec. returns true on success +bool AP_Mount_Scripting::get_rate_target(float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame) +{ + if (_script_target_type != ScriptTargetType::RATE) { + return false; + } + roll_degs = degrees(_rate_target.roll); + pitch_degs = degrees(_rate_target.pitch); + yaw_degs = degrees(_rate_target.yaw); + yaw_is_earth_frame = _rate_target.yaw_is_ef; + return true; } // get attitude as a quaternion. returns true on success diff --git a/libraries/AP_Mount/AP_Mount_Scripting.h b/libraries/AP_Mount/AP_Mount_Scripting.h index f880b3df6692ad..940f2cc7db0da6 100644 --- a/libraries/AP_Mount/AP_Mount_Scripting.h +++ b/libraries/AP_Mount/AP_Mount_Scripting.h @@ -36,29 +36,42 @@ class AP_Mount_Scripting : public AP_Mount_Backend // accessors for scripting backends void set_attitude_euler(float roll_deg, float pitch_deg, float yaw_bf_deg) override; + // get target rate in deg/sec. returns true on success + bool get_rate_target(float& roll_degs, float& pitch_degs, float& yaw_degs, bool& yaw_is_earth_frame) override; + + // get target angle in deg. returns true on success + bool get_angle_target(float& roll_deg, float& pitch_deg, float& yaw_deg, bool& yaw_is_earth_frame) override; + protected: // Scripting backends poll get_angle_target / get_rate_target rather than // receiving pushed targets, so native support for both types is declared so // send_target_to_gimbal() never converts rates to angles on the backend's - // behalf. send_target_angles() stamps mnt_target.target_type = ANGLE so - // that get_angle_target() returns the converted value for non-angle modes - // (RETRACT, NEUTRAL, LOCATION) that send_target_to_gimbal() converts and - // stores into mnt_target.angle_rad before calling send_target_angles(). + // behalf. send_target_angles() / send_target_rates() store the converted + // target for retrieval via get_angle_target() / get_rate_target(). uint8_t natively_supported_mount_target_types() const override { return NATIVE_ANGLES_AND_RATES_ONLY; }; void send_target_angles(const MountAngleTarget &angle_rad) override; - void send_target_rates(const MountRateTarget &rate_rads) override {}; + void send_target_rates(const MountRateTarget &rate_rads) override; // get attitude as a quaternion. returns true on success bool get_attitude_quaternion(Quaternion& att_quat) override; private: + enum class ScriptTargetType : uint8_t { + NONE = 0, + ANGLE = 1, + RATE = 2, + }; + // internal variables - uint32_t last_update_ms; // system time of last call to one of the get_ methods. Used for health reporting - Vector3f current_angle_deg; // current gimbal angles in degrees (x=roll, y=pitch, z=yaw) + uint32_t last_update_ms; // system time of last call to one of the get_ methods. Used for health reporting + Vector3f current_angle_deg; // current gimbal angles in degrees (x=roll, y=pitch, z=yaw) + ScriptTargetType _script_target_type {ScriptTargetType::NONE}; + MountAngleTarget _angle_target {}; // last angle target pushed by send_target_angles() + MountRateTarget _rate_target {}; // last rate target pushed by send_target_rates() }; From 6f08e6eb7db2cdcb731a0aafd47767e79c506bc0 Mon Sep 17 00:00:00 2001 From: Iampete1 Date: Fri, 3 Apr 2026 14:08:47 +0100 Subject: [PATCH 62/99] AP_Scripting: fix language check issues --- libraries/AP_Scripting/applets/net-ntrip.lua | 2 +- libraries/AP_Scripting/applets/quadplane_terrain_avoid.lua | 5 +++++ libraries/AP_Scripting/examples/plane-wind-fs.lua | 2 +- libraries/AP_Scripting/examples/rover-motor-driver.lua | 4 ++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/libraries/AP_Scripting/applets/net-ntrip.lua b/libraries/AP_Scripting/applets/net-ntrip.lua index 6f814d26e27a4c..798e72933efd25 100644 --- a/libraries/AP_Scripting/applets/net-ntrip.lua +++ b/libraries/AP_Scripting/applets/net-ntrip.lua @@ -99,7 +99,7 @@ local function process_rtcm_buffer() if send_debug_msg_count then local message_type = (rtcm_buffer:byte(4) << 4) | (rtcm_buffer:byte(5) >> 4) msg_count[message_type] = (msg_count[message_type] or 0) + 1 - gcs:send_named_float(message_type, msg_count[message_type]) + gcs:send_named_float(tostring(message_type), msg_count[message_type]) end local complete_message = rtcm_buffer:sub(1, total_length) diff --git a/libraries/AP_Scripting/applets/quadplane_terrain_avoid.lua b/libraries/AP_Scripting/applets/quadplane_terrain_avoid.lua index 6a031f8302fc13..176f0a0ef55828 100644 --- a/libraries/AP_Scripting/applets/quadplane_terrain_avoid.lua +++ b/libraries/AP_Scripting/applets/quadplane_terrain_avoid.lua @@ -960,6 +960,11 @@ while (distance > 0) { ------------------------------------------------------------------------------- -- Lookahead functions - replaces the c++ functions in AP_Terrain ------------------------------------------------------------------------------- +---@param start_location Location_ud +---@param search_bearing number +---@param search_distance number +---@param search_ratio number +---@return Location_ud|nil function Terrain_Lookahead(start_location, search_bearing, search_distance, search_ratio) local highest_location = nil local climb = 0.0 diff --git a/libraries/AP_Scripting/examples/plane-wind-fs.lua b/libraries/AP_Scripting/examples/plane-wind-fs.lua index 87755bd44789ff..bde0774160d862 100644 --- a/libraries/AP_Scripting/examples/plane-wind-fs.lua +++ b/libraries/AP_Scripting/examples/plane-wind-fs.lua @@ -64,7 +64,7 @@ value = param:get('RTL_RADIUS') if value then value = math.abs(value) if value > 0 then - home_reached_rad = math.abs(value) * 2 + home_reached_rad = value * 2 else value = param:get('WP_LOITER_RAD') if value then diff --git a/libraries/AP_Scripting/examples/rover-motor-driver.lua b/libraries/AP_Scripting/examples/rover-motor-driver.lua index 1000df8bcd049b..b692e5aaa07d75 100644 --- a/libraries/AP_Scripting/examples/rover-motor-driver.lua +++ b/libraries/AP_Scripting/examples/rover-motor-driver.lua @@ -29,6 +29,10 @@ function update() if throttle < 0 then steering = -steering end + + -- Language checker thinks steering could be nil, reassure it that it cannot + ---@cast steering number + SRV_Channels:set_output_norm(K_SCRIPTING1, steering) SRV_Channels:set_output_norm(K_SCRIPTING3, throttle) end From 943da73b6b741eaff778770483fa839155dc6409 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Mon, 13 Apr 2026 10:53:40 +0200 Subject: [PATCH 63/99] mavlink: Update subprepo According to https://github.com/ArduPilot/ardupilot/pull/32663. --- modules/mavlink | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mavlink b/modules/mavlink index 375f640979b0c8..37d35a66317112 160000 --- a/modules/mavlink +++ b/modules/mavlink @@ -1 +1 @@ -Subproject commit 375f640979b0c8e0e5764a38467bf6da09f41142 +Subproject commit 37d35a66317112bc88d97a557c22a1d15c2a0896 From 4c7ed90d103b0a6ae4d5cf0e6ec1e74adb2a8dc3 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Fri, 3 Apr 2026 10:48:09 +1100 Subject: [PATCH 64/99] GCS_MAVLink: adjust for new SYS_STATUS fields --- libraries/GCS_MAVLink/GCS_Common.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/GCS_MAVLink/GCS_Common.cpp b/libraries/GCS_MAVLink/GCS_Common.cpp index 1c58fdbab4f3a2..010fd04d0a1342 100644 --- a/libraries/GCS_MAVLink/GCS_Common.cpp +++ b/libraries/GCS_MAVLink/GCS_Common.cpp @@ -6046,7 +6046,11 @@ void GCS_MAVLINK::send_sys_status() errors1, errors2, dropped_logmessage_count, // errors3 - errors4); // errors4 + errors4, // errors4 + 0, // control_sensors_present2, + 0, // control_sensors_enabled2, + 0 // control_sensors_health2 + ); } void GCS_MAVLINK::send_extended_sys_state() const From 4fdf95e25451199b1725c88b3ab03a35511d1c31 Mon Sep 17 00:00:00 2001 From: Willian Galvani Date: Fri, 5 Sep 2025 10:11:13 -0300 Subject: [PATCH 65/99] Sub: refactor jsbutton_function_is_assigned() out of has_disarm_function() --- ArduSub/AP_Arming_Sub.cpp | 27 +++------------------------ ArduSub/Sub.h | 1 + ArduSub/joystick.cpp | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/ArduSub/AP_Arming_Sub.cpp b/ArduSub/AP_Arming_Sub.cpp index a2294e7f917122..298d8e15718a27 100644 --- a/ArduSub/AP_Arming_Sub.cpp +++ b/ArduSub/AP_Arming_Sub.cpp @@ -13,30 +13,9 @@ bool AP_Arming_Sub::rc_calibration_checks(bool display_failure) } bool AP_Arming_Sub::has_disarm_function() const { - bool has_shift_function = false; - // make sure the craft has a disarm button assigned before it is armed - // check all the standard btn functions - for (uint8_t i = 0; i < 16; i++) { - switch (sub.get_button(i)->function(false)) { - case JSButton::k_shift : - has_shift_function = true; - break; - case JSButton::k_arm_toggle : - return true; - case JSButton::k_disarm : - return true; - } - } - - // check all the shift functions if there's shift assigned - if (has_shift_function) { - for (uint8_t i = 0; i < 16; i++) { - switch (sub.get_button(i)->function(true)) { - case JSButton::k_arm_toggle : - case JSButton::k_disarm : - return true; - } - } + if (sub.jsbutton_function_is_assigned(JSButton::k_arm_toggle) || + sub.jsbutton_function_is_assigned(JSButton::k_disarm)) { + return true; } // check if an AUX function that disarms or estops is setup if (rc().find_channel_for_option(RC_Channel::AUX_FUNC::MOTOR_ESTOP) || diff --git a/ArduSub/Sub.h b/ArduSub/Sub.h index 17f42599e88734..1444a5abac5518 100644 --- a/ArduSub/Sub.h +++ b/ArduSub/Sub.h @@ -509,6 +509,7 @@ class Sub : public AP_Vehicle { JSButton* get_button(uint8_t index); void default_js_buttons(void); void clear_input_hold(); + bool jsbutton_function_is_assigned(JSButton::button_function_t function); void read_barometer(void); void init_rangefinder(void); void read_rangefinder(void); diff --git a/ArduSub/joystick.cpp b/ArduSub/joystick.cpp index 64d9e035baa177..404801df1d5464 100644 --- a/ArduSub/joystick.cpp +++ b/ArduSub/joystick.cpp @@ -849,6 +849,33 @@ void Sub::clear_input_hold() input_hold_engaged = false; } +bool Sub::jsbutton_function_is_assigned(JSButton::button_function_t function) +{ + bool shift_is_assigned = false; + bool function_is_assigned_to_shift = false; + const uint8_t num_buttons = 32; + // Check all 32 buttons for the specified function + for (uint8_t i = 0; i < num_buttons; i++) { + JSButton* current_button = get_button(i); + if (current_button == nullptr) { + continue; + } + if (current_button->function(false) == function) { + return true; + } + if (current_button->function(false) == JSButton::k_shift) { + shift_is_assigned = true; + } + if (current_button->function(true) == function) { + if (shift_is_assigned) { + return true; + } + function_is_assigned_to_shift = true; + } + } + return shift_is_assigned && function_is_assigned_to_shift; +} + #if AP_SCRIPTING_ENABLED bool Sub::is_button_pressed(uint8_t index) { From c5ccd0864e1a4e858953730b6f0f5eece605a23d Mon Sep 17 00:00:00 2001 From: Willian Galvani Date: Tue, 16 Sep 2025 14:44:04 -0300 Subject: [PATCH 66/99] Sub: add parameters migration for lights and actuators --- ArduSub/Parameters.cpp | 157 +++++++++++++++++++++++++++++++++++++++++ ArduSub/Sub.h | 3 + ArduSub/system.cpp | 3 + 3 files changed, 163 insertions(+) diff --git a/ArduSub/Parameters.cpp b/ArduSub/Parameters.cpp index 0041fab2f40d6e..78b9b455a4150f 100644 --- a/ArduSub/Parameters.cpp +++ b/ArduSub/Parameters.cpp @@ -955,3 +955,160 @@ void Sub::update_relay_pins() } } #endif + +// Helper function to set servo function by channel number, 1-indexed +static void set_servo_function(uint8_t channel, SRV_Channel::Function function) +{ + char param_name[20]; + snprintf(param_name, sizeof(param_name), "SERVO%u_FUNCTION", channel); + AP_Param::set_and_save_by_name(param_name, static_cast(function)); +} + + +// PARAMETER_CONVERSION - Added: Mar-2026 +void Sub::update_actuators_from_jsbuttons() +{ + /* + This function is used to update parameters from Sub <=4.5.4 to the newer actuators implementation. + servo_1_inc/min/dec/etc.. were hard-coded to work only on channels 9,10,11. This update means we now have to change + these functions to the corresponding actuator function to keep the same functionality. + For each of SERVO9,10,11, if they are set to DISABLED, check if there are joystick buttons set to actuator functions. + if so, and no other channel is set to that actuator function, set the servo to the actuator function. + */ + + // Configuration constants + const uint8_t FIRST_LEGACY_CHANNEL = 9; + const uint8_t NUM_LEGACY_ACTUATORS = 3; + const size_t FUNCTIONS_PER_SERVO = 9; + + // Legacy servo button functions mapped to actuators 1-3 + static constexpr JSButton::button_function_t servo_functions[NUM_LEGACY_ACTUATORS][FUNCTIONS_PER_SERVO] = { + { + // Actuator 1 (was servo_1_*) + JSButton::button_function_t::k_servo_1_inc, + JSButton::button_function_t::k_servo_1_dec, + JSButton::button_function_t::k_servo_1_min, + JSButton::button_function_t::k_servo_1_max, + JSButton::button_function_t::k_servo_1_center, + JSButton::button_function_t::k_servo_1_min_momentary, + JSButton::button_function_t::k_servo_1_max_momentary, + JSButton::button_function_t::k_servo_1_min_toggle, + JSButton::button_function_t::k_servo_1_max_toggle + }, + { + // Actuator 2 (was servo_2_*) + JSButton::button_function_t::k_servo_2_inc, + JSButton::button_function_t::k_servo_2_dec, + JSButton::button_function_t::k_servo_2_min, + JSButton::button_function_t::k_servo_2_max, + JSButton::button_function_t::k_servo_2_center, + JSButton::button_function_t::k_servo_2_min_momentary, + JSButton::button_function_t::k_servo_2_max_momentary, + JSButton::button_function_t::k_servo_2_min_toggle, + JSButton::button_function_t::k_servo_2_max_toggle + }, + { + // Actuator 3 (was servo_3_*) + JSButton::button_function_t::k_servo_3_inc, + JSButton::button_function_t::k_servo_3_dec, + JSButton::button_function_t::k_servo_3_min, + JSButton::button_function_t::k_servo_3_max, + JSButton::button_function_t::k_servo_3_center, + JSButton::button_function_t::k_servo_3_min_momentary, + JSButton::button_function_t::k_servo_3_max_momentary, + JSButton::button_function_t::k_servo_3_min_toggle, + JSButton::button_function_t::k_servo_3_max_toggle + } + }; + + // Target actuator functions for assignment + static constexpr SRV_Channel::Function actuator_functions[NUM_LEGACY_ACTUATORS] = { + SRV_Channel::Function::k_actuator1, + SRV_Channel::Function::k_actuator2, + SRV_Channel::Function::k_actuator3 + }; + + // Process legacy channels 9-11 + for (uint8_t actuator_idx = 0; actuator_idx < NUM_LEGACY_ACTUATORS; actuator_idx++) { + const uint8_t channel = FIRST_LEGACY_CHANNEL + actuator_idx; + const auto target_function = actuator_functions[actuator_idx]; + + // Skip if actuator function already assigned to any channel + uint8_t existing_channel; + if (SRV_Channels::find_channel(target_function, existing_channel)) { + continue; + } + + // Skip if channel is not disabled + if (SRV_Channels::channel_function(channel - 1) != SRV_Channel::Function::k_none) { + continue; + } + + // Check if any servo/actuator buttons are assigned + bool has_assigned_button = false; + for (size_t func_idx = 0; func_idx < FUNCTIONS_PER_SERVO; func_idx++) { + if (sub.jsbutton_function_is_assigned(servo_functions[actuator_idx][func_idx])) { + has_assigned_button = true; + break; + } + } + + if (has_assigned_button) { + // Assign actuator function to preserve legacy behavior + set_servo_function(channel, target_function); + } + } +} + +// PARAMETER_CONVERSION - Added: Mar-2026 +void Sub::update_lights_from_rcin() +{ + /* + Maps older systems from using RCIN9 and RCIN10 to using lights1 and lights2. + This is only done if there are joystick buttons assigned to the lights functions and there are no channels assigned to the lights functions. + */ + + const uint8_t NUM_LIGHTS = 2; + const uint8_t FUNCTIONS_PER_LIGHT = 3; + static constexpr JSButton::button_function_t lights_button_functions[NUM_LIGHTS][FUNCTIONS_PER_LIGHT] = { + { + JSButton::button_function_t::k_lights1_brighter, + JSButton::button_function_t::k_lights1_dimmer, + JSButton::button_function_t::k_lights1_cycle, + }, + { + JSButton::button_function_t::k_lights2_brighter, + JSButton::button_function_t::k_lights2_dimmer, + JSButton::button_function_t::k_lights2_cycle, + } + }; + + // New, dedicated lights output functions + const SRV_Channel::Function lights_functions[NUM_LIGHTS] = { + SRV_Channel::Function::k_lights1, + SRV_Channel::Function::k_lights2 + }; + + // Legacy, overloaded and hardcoded RCIN passthrough outputs + const SRV_Channel::Function rcin_functions[NUM_LIGHTS] = { + SRV_Channel::Function::k_rcin9, + SRV_Channel::Function::k_rcin10 + }; + + // Confirm the new output is not already assigned, and extract the legacy output for remapping + for (uint8_t light = 0; light < NUM_LIGHTS; light++) { + uint8_t existing_channel; + if (SRV_Channels::find_channel(lights_functions[light], existing_channel) + || !SRV_Channels::find_channel(rcin_functions[light], existing_channel)) { + continue; + } + // We have a potential lights RCIN channel. Do we have lights buttons? + for (uint8_t func_idx = 0; func_idx < FUNCTIONS_PER_LIGHT; func_idx++) { + if (sub.jsbutton_function_is_assigned(lights_button_functions[light][func_idx])) { + // We have buttons assigned to lights. Set the channel to the new, dedicated lights function. + set_servo_function(existing_channel + 1, lights_functions[light]); + break; + } + } + } +} diff --git a/ArduSub/Sub.h b/ArduSub/Sub.h index 1444a5abac5518..cb904489696ae9 100644 --- a/ArduSub/Sub.h +++ b/ArduSub/Sub.h @@ -580,6 +580,9 @@ class Sub : public AP_Vehicle { #if AP_RELAY_ENABLED void update_relay_pins(); #endif + + void update_actuators_from_jsbuttons(); + void update_lights_from_rcin(); bool handle_do_motor_test(mavlink_command_int_t command); bool init_motor_test(); bool verify_motor_test(); diff --git a/ArduSub/system.cpp b/ArduSub/system.cpp index 7e439b71b91508..f9c46e8e0189ea 100644 --- a/ArduSub/system.cpp +++ b/ArduSub/system.cpp @@ -154,6 +154,8 @@ void Sub::init_ardupilot() mainloop_failsafe_enable(); ins.set_log_raw_bit(MASK_LOG_IMU_RAW); + + update_actuators_from_jsbuttons(); g2.actuators.initialize_actuators(); #if LEAKDETECTOR_MAX_INSTANCES > 0 @@ -163,6 +165,7 @@ void Sub::init_ardupilot() update_relay_pins(); #endif // flag that initialisation has completed + update_lights_from_rcin(); ap.initialised = true; } From 2f530dd794547783dd66bf00704eb2f36d1bf6cd Mon Sep 17 00:00:00 2001 From: Willian Galvani Date: Fri, 6 Mar 2026 13:26:23 -0300 Subject: [PATCH 67/99] Sub: create and use param for controlling parameters migration --- ArduSub/Parameters.cpp | 4 ++++ ArduSub/Parameters.h | 2 ++ ArduSub/system.cpp | 9 +++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ArduSub/Parameters.cpp b/ArduSub/Parameters.cpp index 78b9b455a4150f..72e29c8712e263 100644 --- a/ArduSub/Parameters.cpp +++ b/ArduSub/Parameters.cpp @@ -768,6 +768,10 @@ const AP_Param::GroupInfo ParametersG2::var_info[] = { // @Path: ../ArduSub/actuators.cpp AP_SUBGROUPINFO(actuators, "ACTUATOR", 23, ParametersG2, Actuators), + // Hidden param used as a flag for param conversion + // This allows one time conversion while allowing user to flash between versions with and without converted params + AP_GROUPINFO_FLAGS("PARM_FMT_VER", 24, ParametersG2, param_conversion_increment, 0, AP_PARAM_FLAG_HIDDEN), + AP_GROUPEND }; diff --git a/ArduSub/Parameters.h b/ArduSub/Parameters.h index a204937ef72d40..e37bda26fb09aa 100644 --- a/ArduSub/Parameters.h +++ b/ArduSub/Parameters.h @@ -408,6 +408,8 @@ class ParametersG2 { AP_Float surface_nobaro_thrust; Actuators actuators; + // Used to track parameter conversions + AP_Int8 param_conversion_increment; }; extern const AP_Param::Info var_info[]; diff --git a/ArduSub/system.cpp b/ArduSub/system.cpp index f9c46e8e0189ea..fbb9db76067b47 100644 --- a/ArduSub/system.cpp +++ b/ArduSub/system.cpp @@ -155,7 +155,13 @@ void Sub::init_ardupilot() ins.set_log_raw_bit(MASK_LOG_IMU_RAW); - update_actuators_from_jsbuttons(); + // PARAMETER_CONVERSION - Added: Mar-2026 + if (g2.param_conversion_increment < 1) { + update_actuators_from_jsbuttons(); + update_lights_from_rcin(); + g2.param_conversion_increment.set_and_save(1); + } + g2.actuators.initialize_actuators(); #if LEAKDETECTOR_MAX_INSTANCES > 0 @@ -165,7 +171,6 @@ void Sub::init_ardupilot() update_relay_pins(); #endif // flag that initialisation has completed - update_lights_from_rcin(); ap.initialised = true; } From af3acf04fa7c6e2c69e502fde5648df62d4f7fe4 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Mon, 23 Mar 2026 15:09:46 +1100 Subject: [PATCH 68/99] AP_Camera: support per-instance CAMERA_INFORMATION via REQUEST_MESSAGE param2 param2 of MAV_CMD_REQUEST_MESSAGE (512) selects a specific camera instance when requesting CAMERA_INFORMATION (1-based, matching the MAVLink convention used for gimbal_device_id). A param2 of 0 retains the existing behaviour of sending information for all camera instances. The per-instance send is routed through the normal deferred-message path so that COMMAND_ACK is transmitted before the CAMERA_INFORMATION response, matching the order expected by callers that drain non-ACK messages while waiting for acknowledgement. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Camera/AP_Camera.cpp | 20 +++++++++++++++++++- libraries/AP_Camera/AP_Camera.h | 10 ++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/libraries/AP_Camera/AP_Camera.cpp b/libraries/AP_Camera/AP_Camera.cpp index 41daa356a65bdb..06a903d28286d6 100644 --- a/libraries/AP_Camera/AP_Camera.cpp +++ b/libraries/AP_Camera/AP_Camera.cpp @@ -475,7 +475,13 @@ bool AP_Camera::send_mavlink_message(GCS_MAVLINK &link, const enum ap_message ms break; case MSG_CAMERA_INFORMATION: CHECK_PAYLOAD_SIZE2(CAMERA_INFORMATION); - send_camera_information(chan); + if (_camera_information_send_instance >= 0) { + const int16_t instance = _camera_information_send_instance; + _camera_information_send_instance = -1; + send_camera_information((uint8_t)instance, chan); + } else { + send_camera_information(chan); + } break; case MSG_CAMERA_SETTINGS: CHECK_PAYLOAD_SIZE2(CAMERA_SETTINGS); @@ -616,6 +622,18 @@ void AP_Camera::send_camera_information(mavlink_channel_t chan) } } +// send camera information for a specific instance to GCS +void AP_Camera::send_camera_information(uint8_t instance, mavlink_channel_t chan) +{ + WITH_SEMAPHORE(_rsem); + + auto *backend = get_instance(instance); + if (backend == nullptr) { + return; + } + backend->send_camera_information(chan); +} + #if AP_MAVLINK_MSG_VIDEO_STREAM_INFORMATION_ENABLED // send video stream information message to GCS void AP_Camera::send_video_stream_information(mavlink_channel_t chan) diff --git a/libraries/AP_Camera/AP_Camera.h b/libraries/AP_Camera/AP_Camera.h index 1f91c0cad7d12d..2a724fffb8da6a 100644 --- a/libraries/AP_Camera/AP_Camera.h +++ b/libraries/AP_Camera/AP_Camera.h @@ -96,6 +96,12 @@ class AP_Camera { // send the message, true otherwise bool send_mavlink_message(class GCS_MAVLINK &link, const enum ap_message id); + // send camera information for a specific instance (0-based) to GCS + void send_camera_information(uint8_t instance, mavlink_channel_t chan); + + // select which instance to send on the next deferred MSG_CAMERA_INFORMATION send + void set_camera_information_send_instance(int16_t instance) { _camera_information_send_instance = instance; } + // configure camera void configure(float shooting_mode, float shutter_speed, float aperture, float ISO, int32_t exposure_type, int32_t cmd_id, float engine_cutoff_time); void configure(uint8_t instance, float shooting_mode, float shutter_speed, float aperture, float ISO, int32_t exposure_type, int32_t cmd_id, float engine_cutoff_time); @@ -275,6 +281,10 @@ class AP_Camera { bool _is_in_auto_mode; // true if in AUTO mode uint32_t log_camera_bit; // logging bit (from LOG_BITMASK) to enable camera logging AP_Camera_Backend *_backends[AP_CAMERA_MAX_INSTANCES]; // pointers to instantiated backends + // Stashes the 0-based instance requested by MAV_CMD_REQUEST_MESSAGE(CAMERA_INFORMATION, param2). + // Used to pass the target instance through the deferred-message path so that COMMAND_ACK is + // transmitted before the CAMERA_INFORMATION response. -1 means send for all instances. + int16_t _camera_information_send_instance = -1; }; namespace AP { From 57c348a0282fddca3d6540af6023f38f00b9320a Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Mon, 23 Mar 2026 15:09:47 +1100 Subject: [PATCH 69/99] GCS_MAVLink: support per-instance CAMERA_INFORMATION via REQUEST_MESSAGE param2 param2 of MAV_CMD_REQUEST_MESSAGE (512) selects a specific camera instance when requesting CAMERA_INFORMATION (1-based, matching the MAVLink convention used for gimbal_device_id). A param2 of 0 retains the existing behaviour of sending information for all camera instances. The per-instance send is routed through the normal deferred-message path so that COMMAND_ACK is transmitted before the CAMERA_INFORMATION response, matching the order expected by callers that drain non-ACK messages while waiting for acknowledgement. Co-Authored-By: Claude Sonnet 4.6 --- libraries/GCS_MAVLink/GCS_Common.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libraries/GCS_MAVLink/GCS_Common.cpp b/libraries/GCS_MAVLink/GCS_Common.cpp index 010fd04d0a1342..dfd637c6d1f70a 100644 --- a/libraries/GCS_MAVLink/GCS_Common.cpp +++ b/libraries/GCS_MAVLink/GCS_Common.cpp @@ -3291,6 +3291,18 @@ MAV_RESULT GCS_MAVLINK::handle_command_request_message(const mavlink_command_int set_ap_message_interval(MSG_AVAILABLE_MODES_MONITOR, 5000); break; +#if AP_CAMERA_ENABLED + case MSG_CAMERA_INFORMATION: + // param2 selects a specific camera instance (1-based); 0 means all + if (packet.param2 >= 1) { + AP_Camera *camera = AP::camera(); + if (camera != nullptr) { + camera->set_camera_information_send_instance((int16_t)(packet.param2 - 1)); + } + } + break; +#endif + default: break; } From cdafa121c04c862a6caeb52b4d6424ae2fc497aa Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sun, 22 Mar 2026 15:22:24 +1100 Subject: [PATCH 70/99] AP_HAL_SITL: add simulation for ZT30, Topotek and ViewPro gimbals --- libraries/AP_HAL_SITL/SITL_State_common.cpp | 24 +++++++++++++++++++++ libraries/AP_HAL_SITL/SITL_State_common.h | 5 +++++ 2 files changed, 29 insertions(+) diff --git a/libraries/AP_HAL_SITL/SITL_State_common.cpp b/libraries/AP_HAL_SITL/SITL_State_common.cpp index ed510160fcb0dc..a5721061649141 100644 --- a/libraries/AP_HAL_SITL/SITL_State_common.cpp +++ b/libraries/AP_HAL_SITL/SITL_State_common.cpp @@ -223,6 +223,30 @@ SITL::SerialDevice *SITL_State_Common::create_serial_sim(const char *name, const sitl_model->set_volz(&_sitl->volz_sim); return &_sitl->volz_sim; #endif // AP_SIM_VOLZ_ENABLED +#if AP_SIM_SIYI_ZT30_ENABLED + } else if (streq(name, "siyi_zt30")) { + const auto siyi = NEW_NOTHROW SITL::Siyi_ZT30(); + sitl_model->add_siyi(*siyi, arg); + return siyi; +#endif // AP_SIM_SIYI_ZT30_ENABLED +#if AP_SIM_TOPOTEK_ENABLED + } else if (streq(name, "topotek")) { + const auto topotek = NEW_NOTHROW SITL::Topotek(); + sitl_model->add_topotek(*topotek, arg); + return topotek; +#endif // AP_SIM_TOPOTEK_ENABLED +#if AP_SIM_VIEWPRO_ENABLED + } else if (streq(name, "viewpro")) { + const auto viewpro = NEW_NOTHROW SITL::Viewpro(); + sitl_model->add_viewpro(*viewpro, arg); + return viewpro; +#endif // AP_SIM_VIEWPRO_ENABLED +#if AP_SIM_AVT_CM62_ENABLED + } else if (streq(name, "avt_cm62_gimbal")) { + const auto avt_cm62 = NEW_NOTHROW SITL::AVT_CM62(); + sitl_model->add_mount_sim(*avt_cm62); + return avt_cm62; +#endif // AP_SIM_AVT_CM62_ENABLED } else if (streq(name, "megasquirt")) { if (efi_ms != nullptr) { AP_HAL::panic("Only one megasquirt at a time"); diff --git a/libraries/AP_HAL_SITL/SITL_State_common.h b/libraries/AP_HAL_SITL/SITL_State_common.h index fda5a77dd297ec..c54c315a60133c 100644 --- a/libraries/AP_HAL_SITL/SITL_State_common.h +++ b/libraries/AP_HAL_SITL/SITL_State_common.h @@ -23,6 +23,11 @@ #include +#include +#include +#include +#include + #include #include // #include From 963423e64200ff2a79466994a23d48d72e908b03 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sun, 22 Mar 2026 15:22:25 +1100 Subject: [PATCH 71/99] SITL: add simulation for ZT30, Topotek and ViewPro gimbals --- libraries/AP_HAL_SITL/SITL_State_common.cpp | 10 +- libraries/SITL/SIM_AVT_CM62.cpp | 11 + libraries/SITL/SIM_AVT_CM62.h | 33 ++ libraries/SITL/SIM_Aircraft.cpp | 8 + libraries/SITL/SIM_Aircraft.h | 21 ++ libraries/SITL/SIM_Gimbal.cpp | 1 + libraries/SITL/SIM_Gimbal.h | 14 +- libraries/SITL/SIM_MAVLinkGimbalv2.cpp | 358 +++++++++++++++++++ libraries/SITL/SIM_MAVLinkGimbalv2.h | 101 ++++++ libraries/SITL/SIM_Mount.h | 36 ++ libraries/SITL/SIM_Siyi.cpp | 248 +++++++++++++ libraries/SITL/SIM_Siyi.h | 220 ++++++++++++ libraries/SITL/SIM_Siyi_ZT30.cpp | 192 ++++++++++ libraries/SITL/SIM_Siyi_ZT30.h | 185 ++++++++++ libraries/SITL/SIM_Topotek.cpp | 268 ++++++++++++++ libraries/SITL/SIM_Topotek.h | 84 +++++ libraries/SITL/SIM_Viewpro.cpp | 373 ++++++++++++++++++++ libraries/SITL/SIM_Viewpro.h | 124 +++++++ libraries/SITL/SIM_config.h | 35 +- 19 files changed, 2315 insertions(+), 7 deletions(-) create mode 100644 libraries/SITL/SIM_AVT_CM62.cpp create mode 100644 libraries/SITL/SIM_AVT_CM62.h create mode 100644 libraries/SITL/SIM_MAVLinkGimbalv2.cpp create mode 100644 libraries/SITL/SIM_MAVLinkGimbalv2.h create mode 100644 libraries/SITL/SIM_Mount.h create mode 100644 libraries/SITL/SIM_Siyi.cpp create mode 100644 libraries/SITL/SIM_Siyi.h create mode 100644 libraries/SITL/SIM_Siyi_ZT30.cpp create mode 100644 libraries/SITL/SIM_Siyi_ZT30.h create mode 100644 libraries/SITL/SIM_Topotek.cpp create mode 100644 libraries/SITL/SIM_Topotek.h create mode 100644 libraries/SITL/SIM_Viewpro.cpp create mode 100644 libraries/SITL/SIM_Viewpro.h diff --git a/libraries/AP_HAL_SITL/SITL_State_common.cpp b/libraries/AP_HAL_SITL/SITL_State_common.cpp index a5721061649141..3bc7d4936d1f3e 100644 --- a/libraries/AP_HAL_SITL/SITL_State_common.cpp +++ b/libraries/AP_HAL_SITL/SITL_State_common.cpp @@ -226,25 +226,27 @@ SITL::SerialDevice *SITL_State_Common::create_serial_sim(const char *name, const #if AP_SIM_SIYI_ZT30_ENABLED } else if (streq(name, "siyi_zt30")) { const auto siyi = NEW_NOTHROW SITL::Siyi_ZT30(); - sitl_model->add_siyi(*siyi, arg); + sitl_model->add_gimbal_sim(*siyi); return siyi; #endif // AP_SIM_SIYI_ZT30_ENABLED #if AP_SIM_TOPOTEK_ENABLED } else if (streq(name, "topotek")) { const auto topotek = NEW_NOTHROW SITL::Topotek(); - sitl_model->add_topotek(*topotek, arg); + sitl_model->add_gimbal_sim(*topotek); return topotek; #endif // AP_SIM_TOPOTEK_ENABLED #if AP_SIM_VIEWPRO_ENABLED } else if (streq(name, "viewpro")) { const auto viewpro = NEW_NOTHROW SITL::Viewpro(); - sitl_model->add_viewpro(*viewpro, arg); + sitl_model->add_gimbal_sim(*viewpro); return viewpro; #endif // AP_SIM_VIEWPRO_ENABLED #if AP_SIM_AVT_CM62_ENABLED } else if (streq(name, "avt_cm62_gimbal")) { + static uint8_t mavlink_gimbal_count; const auto avt_cm62 = NEW_NOTHROW SITL::AVT_CM62(); - sitl_model->add_mount_sim(*avt_cm62); + avt_cm62->set_instance(mavlink_gimbal_count++); + sitl_model->add_gimbal_sim(*avt_cm62); return avt_cm62; #endif // AP_SIM_AVT_CM62_ENABLED } else if (streq(name, "megasquirt")) { diff --git a/libraries/SITL/SIM_AVT_CM62.cpp b/libraries/SITL/SIM_AVT_CM62.cpp new file mode 100644 index 00000000000000..ae521c9cdbc821 --- /dev/null +++ b/libraries/SITL/SIM_AVT_CM62.cpp @@ -0,0 +1,11 @@ +/* + Simulator for an AVT CM62 MAVLink (Gimbal Protocol v2) gimbal +*/ + +#include "SIM_config.h" + +#if AP_SIM_AVT_CM62_ENABLED + +#include "SIM_AVT_CM62.h" + +#endif // AP_SIM_AVT_CM62_ENABLED diff --git a/libraries/SITL/SIM_AVT_CM62.h b/libraries/SITL/SIM_AVT_CM62.h new file mode 100644 index 00000000000000..4635f115471307 --- /dev/null +++ b/libraries/SITL/SIM_AVT_CM62.h @@ -0,0 +1,33 @@ +/* + Simulator for an AVT CM62 MAVLink (Gimbal Protocol v2) gimbal +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_AVT_CM62_ENABLED + +#include "SIM_MAVLinkGimbalv2.h" +#include + +namespace SITL { + +class AVT_CM62 : public MAVLinkGimbalv2 { + const char *get_vendor_name() const override { return "AVTA"; } + const char *get_model_name() const override { return "SIM_AVTA"; } + // firmware_version encoding: major=1 | (minor=2)<<8 | (patch=3)<<16 + uint32_t get_firmware_version() const override { return (3U << 16) | (2U << 8) | 1U; } + uint16_t get_cap_flags() const override { + return GIMBAL_DEVICE_CAP_FLAGS_HAS_PITCH_AXIS | + GIMBAL_DEVICE_CAP_FLAGS_HAS_YAW_AXIS; + } + float get_pitch_min_rad() const override { return radians(-45.0f); } + float get_pitch_max_rad() const override { return radians( 45.0f); } + float get_yaw_min_rad() const override { return radians(-180.0f); } + float get_yaw_max_rad() const override { return radians( 180.0f); } +}; + +} // namespace SITL + +#endif // AP_SIM_AVT_CM62_ENABLED diff --git a/libraries/SITL/SIM_Aircraft.cpp b/libraries/SITL/SIM_Aircraft.cpp index 28be7e2baeebb0..00cf87694209dc 100644 --- a/libraries/SITL/SIM_Aircraft.cpp +++ b/libraries/SITL/SIM_Aircraft.cpp @@ -1205,6 +1205,14 @@ void Aircraft::update_external_payload(const struct sitl_input &input) #if AP_SIM_GPIO_LED_RGB_ENABLED sim_ledrgb.update(*this); #endif + +#if AP_SIM_MOUNT_ENABLED + for (uint8_t i = 0; i < GIMBAL_SIM_MAX; i++) { + if (gimbal_sims[i] != nullptr) { + gimbal_sims[i]->update(*this); + } + } +#endif } void Aircraft::add_shove_forces(Vector3f &rot_accel, Vector3f &body_accel) diff --git a/libraries/SITL/SIM_Aircraft.h b/libraries/SITL/SIM_Aircraft.h index 13992dfb2b35b9..43ebd2a1bf6198 100644 --- a/libraries/SITL/SIM_Aircraft.h +++ b/libraries/SITL/SIM_Aircraft.h @@ -43,6 +43,10 @@ #include "SIM_GPIO_LED_2.h" #include "SIM_GPIO_LED_3.h" #include "SIM_GPIO_LED_RGB.h" +#include "SIM_Siyi.h" +#include "SIM_Topotek.h" +#include "SIM_Viewpro.h" +#include "SIM_Mount.h" #define MAX_SIM_INSTANCES 16 @@ -178,6 +182,18 @@ class Aircraft { float ambient_temperature_degC() const; +#if AP_SIM_MOUNT_ENABLED + void add_gimbal_sim(Mount &sim) { + for (uint8_t i = 0; i < GIMBAL_SIM_MAX; i++) { + if (gimbal_sims[i] == nullptr) { + gimbal_sims[i] = ∼ + return; + } + } + AP_HAL::panic("Too many gimbal simulators"); + } +#endif // AP_SIM_MOUNT_ENABLED + ADSB *adsb; // takes a PWM range between 1000 and 2000 and returns a floating @@ -425,6 +441,11 @@ class Aircraft { static Aircraft *instances[MAX_SIM_INSTANCES]; HAL_Semaphore pose_sem; + +#if AP_SIM_MOUNT_ENABLED + static constexpr uint8_t GIMBAL_SIM_MAX = 8; + Mount *gimbal_sims[GIMBAL_SIM_MAX]; +#endif }; } // namespace SITL diff --git a/libraries/SITL/SIM_Gimbal.cpp b/libraries/SITL/SIM_Gimbal.cpp index a905a38d731d09..2388ee95939663 100644 --- a/libraries/SITL/SIM_Gimbal.cpp +++ b/libraries/SITL/SIM_Gimbal.cpp @@ -50,6 +50,7 @@ void Gimbal::update(const class Aircraft &aircraft) const Matrix3f &vehicle_dcm = aircraft.get_dcm(); if (!init_done) { dcm = vehicle_dcm; + init_done = true; } const Vector3f &vehicle_gyro = AP::ins().get_gyro(); diff --git a/libraries/SITL/SIM_Gimbal.h b/libraries/SITL/SIM_Gimbal.h index af6da4af7f149d..b860664036bb45 100644 --- a/libraries/SITL/SIM_Gimbal.h +++ b/libraries/SITL/SIM_Gimbal.h @@ -36,7 +36,19 @@ class Gimbal { } void get_deltas(Vector3f &_delta_angle, Vector3f &_delta_velocity, uint32_t &_delta_time_us); - void get_joint_angles(Vector3f &_angles) { _angles = joint_angles; } + void get_joint_angles(Vector3f &_angles) const { _angles = joint_angles; } + + // copy the gimbal body-to-earth rotation matrix + void get_dcm(Matrix3f &out) const { out = dcm; } + + // override the physical joint angle limits (roll, pitch, azimuth) in radians + void set_joint_limits(const Vector3f &lower_limits, const Vector3f &upper_limits) { + lower_joint_limits = lower_limits; + upper_joint_limits = upper_limits; + } + + // force the gimbal body-to-earth rotation matrix to a specific value + void set_dcm(const Matrix3f &m) { dcm = m; init_done = true; } private: diff --git a/libraries/SITL/SIM_MAVLinkGimbalv2.cpp b/libraries/SITL/SIM_MAVLinkGimbalv2.cpp new file mode 100644 index 00000000000000..37e39f5476961a --- /dev/null +++ b/libraries/SITL/SIM_MAVLinkGimbalv2.cpp @@ -0,0 +1,358 @@ +/* + Simulator for MAVLink Gimbal Protocol v2 peripherals +*/ + +#include "SIM_config.h" + +#if AP_SIM_MAVLINKGIMBALV2_ENABLED + +#include "SIM_MAVLinkGimbalv2.h" +#include "SIM_Aircraft.h" +#include +#include + +namespace SITL { + +void MAVLinkGimbalv2::set_instance(uint8_t instance) +{ + Mount::set_instance(instance); + // select the component ID matching this gimbal instance: + // instance 0 → MAV_COMP_ID_GIMBAL (154) + // instance 1 → MAV_COMP_ID_GIMBAL2 (171), 2 → 172, … + _compid = (instance == 0) ? MAV_COMP_ID_GIMBAL + : MAV_COMP_ID_GIMBAL2 + (instance - 1); + + // configure joint limits from subclass declarations (roll kept at ±40°) + _gimbal.set_joint_limits( + Vector3f(radians(-40.0f), get_pitch_min_rad(), get_yaw_min_rad()), + Vector3f(radians( 40.0f), get_pitch_max_rad(), get_yaw_max_rad()) + ); +} + +void MAVLinkGimbalv2::send_mavlink_message(const mavlink_message_t &msg) +{ + uint8_t buf[MAVLINK_MAX_PACKET_LEN]; + const uint16_t len = mavlink_msg_to_send_buffer(buf, &msg); + if (len > 0) { + write_to_autopilot((char*)buf, len); + } +} + +void MAVLinkGimbalv2::update_input() +{ + uint8_t buf[128]; + const ssize_t nread = read_from_autopilot((char*)buf, sizeof(buf)); + for (ssize_t i = 0; i < nread; i++) { + mavlink_message_t msg; + mavlink_status_t status; + if (mavlink_frame_char_buffer(&mav.rxmsg, &mav.status, + buf[i], &msg, &status) == MAVLINK_FRAMING_OK) { + handle_message(msg); + } + } +} + +void MAVLinkGimbalv2::handle_message(const mavlink_message_t &msg) +{ + switch (msg.msgid) { + case MAVLINK_MSG_ID_HEARTBEAT: { + if (!_seen_autopilot_heartbeat) { + _seen_autopilot_heartbeat = true; + _vehicle_system_id = msg.sysid; + _vehicle_component_id = msg.compid; + ::printf("MAVLinkGimbalv2[%u]: using sysid=%u compid=%u\n", + (unsigned)_instance, + (unsigned)_vehicle_system_id, + (unsigned)_compid); + } + break; + } + case MAVLINK_MSG_ID_COMMAND_LONG: { + mavlink_command_long_t cmd; + mavlink_msg_command_long_decode(&msg, &cmd); + // only handle messages addressed to this gimbal + if (cmd.target_system != _vehicle_system_id || + cmd.target_component != _compid) { + break; + } + if (cmd.command == MAV_CMD_REQUEST_MESSAGE) { + const uint32_t requested_msgid = (uint32_t)cmd.param1; + if (requested_msgid == MAVLINK_MSG_ID_GIMBAL_DEVICE_INFORMATION) { + send_gimbal_device_information(); + send_command_ack(msg.sysid, msg.compid, + MAV_CMD_REQUEST_MESSAGE, MAV_RESULT_ACCEPTED); + } + } + break; + } + case MAVLINK_MSG_ID_COMMAND_INT: { + mavlink_command_int_t cmd; + mavlink_msg_command_int_decode(&msg, &cmd); + if (cmd.target_system != _vehicle_system_id || + cmd.target_component != _compid) { + break; + } + if (cmd.command == MAV_CMD_DO_SET_ROI_LOCATION) { + _roi.loc.lat = cmd.x; + _roi.loc.lng = cmd.y; + _roi.loc.set_alt_m(cmd.z, (Location::AltFrame)cmd.frame); + _roi.valid = true; + } else if (cmd.command == MAV_CMD_DO_SET_ROI_NONE) { + _roi.valid = false; + } + break; + } + case MAVLINK_MSG_ID_GIMBAL_DEVICE_SET_ATTITUDE: { + mavlink_gimbal_device_set_attitude_t cmd; + mavlink_msg_gimbal_device_set_attitude_decode(&msg, &cmd); + if (cmd.target_system != _vehicle_system_id || + cmd.target_component != _compid) { + break; + } + const uint16_t flags = cmd.flags; + if (flags & GIMBAL_DEVICE_FLAGS_RETRACT) { + _target.valid = false; + _roi.valid = false; + break; + } + const bool yaw_ef = (flags & GIMBAL_DEVICE_FLAGS_YAW_LOCK) != 0; + const bool rates_valid = !isnan(cmd.angular_velocity_x); + const bool q_valid = !isnan(cmd.q[0]); + if (rates_valid || q_valid) { + // any explicit attitude/rate command cancels ROI tracking + _roi.valid = false; + } + if (rates_valid) { + _target.valid = true; + _target.is_rate = true; + _target.yaw_is_ef = yaw_ef; + _target.rates_rads = Vector3f(cmd.angular_velocity_x, + cmd.angular_velocity_y, + cmd.angular_velocity_z); + } else if (q_valid) { + _target.valid = true; + _target.is_rate = false; + _target.yaw_is_ef = yaw_ef; + _target.attitude = Quaternion(cmd.q[0], cmd.q[1], cmd.q[2], cmd.q[3]); + } + break; + } + default: + break; + } +} + +void MAVLinkGimbalv2::send_heartbeat() +{ + if (!_seen_autopilot_heartbeat) { + return; + } + + mavlink_heartbeat_t hb {}; + hb.type = MAV_TYPE_GIMBAL; + hb.autopilot = MAV_AUTOPILOT_INVALID; + hb.base_mode = 0; + hb.system_status = MAV_STATE_ACTIVE; + hb.mavlink_version = 3; + + mavlink_message_t msg; + mavlink_msg_heartbeat_encode_status( + _vehicle_system_id, _compid, + &mav.status, &msg, &hb); + send_mavlink_message(msg); +} + +void MAVLinkGimbalv2::send_gimbal_device_information() +{ + mavlink_gimbal_device_information_t info {}; + info.time_boot_ms = AP_HAL::millis(); + info.firmware_version = get_firmware_version(); + info.hardware_version = 0; + info.roll_min = NAN; + info.roll_max = NAN; + info.pitch_min = get_pitch_min_rad(); + info.pitch_max = get_pitch_max_rad(); + info.yaw_min = get_yaw_min_rad(); + info.yaw_max = get_yaw_max_rad(); + info.cap_flags = get_cap_flags(); + strncpy_noterm(info.vendor_name, get_vendor_name(), sizeof(info.vendor_name)); + strncpy_noterm(info.model_name, get_model_name(), sizeof(info.model_name)); + + mavlink_message_t msg; + mavlink_msg_gimbal_device_information_encode_status( + _vehicle_system_id, _compid, + &mav.status, &msg, &info); + send_mavlink_message(msg); +} + +// update gimbal physics and compute demanded rates from the current target +void MAVLinkGimbalv2::update_gimbal(const Aircraft &aircraft) +{ + _vehicle_dcm = aircraft.get_dcm(); + + Matrix3f gimbal_dcm; + _gimbal.get_dcm(gimbal_dcm); + + // if tracking an ROI, continuously recompute the earth-frame target + // (replicates AP_Mount_Backend::get_angle_target_to_location()) + if (_roi.valid) { + const Location &veh = aircraft.get_location(); + const float gps_x = Location::diff_longitude(_roi.loc.lng, veh.lng) + * cosf(radians((veh.lat + _roi.loc.lat) * 0.00000005f)) * 0.01113195f; + const float gps_y = (_roi.loc.lat - veh.lat) * 0.01113195f; + int32_t target_alt_cm = 0, veh_alt_cm = 0; + if (_roi.loc.get_alt_cm(Location::AltFrame::ABOVE_HOME, target_alt_cm) && + veh.get_alt_cm(Location::AltFrame::ABOVE_HOME, veh_alt_cm)) { + const float gps_z = (float)(target_alt_cm - veh_alt_cm); // cm + const float horiz_cm = 100.0f * norm(gps_x, gps_y); + float pitch_ef = atan2f(gps_z, horiz_cm); + pitch_ef = constrain_float(pitch_ef, get_pitch_min_rad(), get_pitch_max_rad()); + const float yaw_ef = atan2f(gps_x, gps_y); + _target.valid = true; + _target.is_rate = false; + _target.yaw_is_ef = true; + _target.attitude.from_euler(0.0f, pitch_ef, yaw_ef); + } + } + + if (_target.valid) { + Vector3f demanded_rates; + if (_target.is_rate) { + demanded_rates = _target.rates_rads; + if (_target.yaw_is_ef) { + // transform earth-frame rates to gimbal body frame + demanded_rates = gimbal_dcm.transposed() * demanded_rates; + } + // add vehicle body rates so that rate=0 tracks the vehicle body + // (body-relative stabilisation); matches neutral-mode behaviour + demanded_rates += gimbal_dcm.transposed() * _vehicle_dcm * aircraft.get_gyro(); + _gimbal.set_demanded_rates(demanded_rates); + } else if (_target.yaw_is_ef) { + // earth-frame angle control: P-controller + // clamp target pitch to hardware joint limits before computing error + float t_r, t_p, t_y; + _target.attitude.to_euler(t_r, t_p, t_y); + t_p = constrain_float(t_p, get_pitch_min_rad(), get_pitch_max_rad()); + Quaternion target_clamped; + target_clamped.from_euler(t_r, t_p, t_y); + Quaternion q_current; + q_current.from_rotation_matrix(gimbal_dcm); + Quaternion q_error = q_current.inverse() * target_clamped; + q_error.normalize(); + Vector3f av(q_error.q2, q_error.q3, q_error.q4); + if (q_error.q1 < 0.0f) { + av = -av; + } + const float attitude_gain = 2.0f; // rad/s per radian of error + demanded_rates = av * (2.0f * attitude_gain); + _gimbal.set_demanded_rates(demanded_rates); + } else { + // body-frame angle control: snap gimbal DCM to the commanded + // body-relative orientation directly + Matrix3f body_dcm; + _target.attitude.rotation_matrix(body_dcm); + _gimbal.set_dcm(_vehicle_dcm * body_dcm); + _gimbal.set_demanded_rates(Vector3f{}); + } + } else { + // neutral: track vehicle body using vehicle angular rate feedforward + const Vector3f vehicle_rate_gimbal = gimbal_dcm.transposed() * _vehicle_dcm * aircraft.get_gyro(); + _gimbal.set_demanded_rates(vehicle_rate_gimbal); + } + _gimbal.update(aircraft); +} + +void MAVLinkGimbalv2::send_attitude_status() +{ + if (!_seen_autopilot_heartbeat) { + return; + } + + // report actual gimbal attitude from physics model + Matrix3f gimbal_dcm; + _gimbal.get_dcm(gimbal_dcm); + + uint16_t flags; + Quaternion q; + if (_target.valid && _target.yaw_is_ef) { + // earth-frame target: report actual gimbal DCM so convergence is visible + flags = GIMBAL_DEVICE_FLAGS_ROLL_LOCK | + GIMBAL_DEVICE_FLAGS_PITCH_LOCK | + GIMBAL_DEVICE_FLAGS_YAW_LOCK; + q.from_rotation_matrix(gimbal_dcm); + } else if (_target.valid && !_target.yaw_is_ef && !_target.is_rate) { + // body-frame angle target: report the commanded body-relative attitude + // directly, matching how servo mounts report commanded angles + flags = 0; + q = _target.attitude; + } else { + // neutral or rate control: report body-relative attitude from DCM + flags = 0; + const Matrix3f body_to_gimbal = _vehicle_dcm.transposed() * gimbal_dcm; + q.from_rotation_matrix(body_to_gimbal); + } + + mavlink_gimbal_device_attitude_status_t status {}; + status.target_system = _vehicle_system_id; + status.target_component = _vehicle_component_id; + status.time_boot_ms = AP_HAL::millis(); + status.flags = flags; + status.q[0] = q.q1; // w + status.q[1] = q.q2; // x + status.q[2] = q.q3; // y + status.q[3] = q.q4; // z + status.angular_velocity_x = 0.0f; + status.angular_velocity_y = 0.0f; + status.angular_velocity_z = 0.0f; + status.failure_flags = 0; + + mavlink_message_t msg; + mavlink_msg_gimbal_device_attitude_status_encode_status( + _vehicle_system_id, _compid, + &mav.status, &msg, &status); + send_mavlink_message(msg); +} + +void MAVLinkGimbalv2::send_command_ack(uint8_t target_sysid, uint8_t target_compid, + MAV_CMD command, MAV_RESULT result) +{ + mavlink_command_ack_t ack {}; + ack.command = (uint16_t)command; + ack.result = (uint8_t)result; + ack.progress = 255; + ack.result_param2 = 0; + ack.target_system = target_sysid; + ack.target_component = target_compid; + + mavlink_message_t msg; + mavlink_msg_command_ack_encode_status( + _vehicle_system_id, _compid, + &mav.status, &msg, &ack); + send_mavlink_message(msg); +} + +void MAVLinkGimbalv2::update(const Aircraft &aircraft) +{ + if (!init_sitl_pointer()) { + return; + } + + update_input(); + update_gimbal(aircraft); + + const uint32_t now_ms = AP_HAL::millis(); + + if (now_ms - _last_heartbeat_ms >= 1000) { + _last_heartbeat_ms = now_ms; + send_heartbeat(); + } + + if (now_ms - _last_attitude_status_ms >= 100) { + _last_attitude_status_ms = now_ms; + send_attitude_status(); + } +} + +} // namespace SITL + +#endif // AP_SIM_MAVLINKGIMBALV2_ENABLED diff --git a/libraries/SITL/SIM_MAVLinkGimbalv2.h b/libraries/SITL/SIM_MAVLinkGimbalv2.h new file mode 100644 index 00000000000000..0dd4aad27611a6 --- /dev/null +++ b/libraries/SITL/SIM_MAVLinkGimbalv2.h @@ -0,0 +1,101 @@ +/* + Simulator for MAVLink Gimbal Protocol v2 peripherals + +./Tools/autotest/sim_vehicle.py --gdb --debug -v ArduCopter \ + -A --serial5=sim:avt_cm62_gimbal --speedup=1 + +param set MNT1_TYPE 6 # MAVLink +param set CAM1_TYPE 4 # mount +param set SERIAL5_PROTOCOL 2 # MAVLink2 +reboot + +long REQUEST_MESSAGE 259 # CAMERA_INFORMATION +status *CAM* +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_MAVLINKGIMBALV2_ENABLED + +#include "SIM_Mount.h" +#include "SIM_Gimbal.h" +#include +#include +#include + +namespace SITL { + +/* + Base class for Gimbal Protocol v2 simulators. Handles all MAVLink + framing; subclasses supply device-specific identity data via + pure-virtual accessors. +*/ +class MAVLinkGimbalv2 : public Mount { +public: + void update(const class Aircraft &aircraft) override; + void set_instance(uint8_t instance) override; + +protected: + // device identity — every subclass must implement all of these + virtual const char *get_vendor_name() const = 0; + virtual const char *get_model_name() const = 0; + virtual uint32_t get_firmware_version() const = 0; + virtual uint16_t get_cap_flags() const = 0; + virtual float get_pitch_min_rad() const = 0; + virtual float get_pitch_max_rad() const = 0; + virtual float get_yaw_min_rad() const = 0; + virtual float get_yaw_max_rad() const = 0; + +protected: + // physics model shared by all MAVLink gimbal simulators + Gimbal _gimbal; + +private: + void update_input(); + void update_gimbal(const class Aircraft &aircraft); + void handle_message(const mavlink_message_t &msg); + + void send_heartbeat(); + void send_gimbal_device_information(); + void send_attitude_status(); + void send_command_ack(uint8_t target_sysid, uint8_t target_compid, + MAV_CMD command, MAV_RESULT result); + void send_mavlink_message(const mavlink_message_t &msg); + + uint8_t _compid {MAV_COMP_ID_GIMBAL}; + + bool _seen_autopilot_heartbeat; + uint8_t _vehicle_system_id; + uint8_t _vehicle_component_id; + + uint32_t _last_heartbeat_ms; + uint32_t _last_attitude_status_ms; + + // current demand from the autopilot, set by GIMBAL_DEVICE_SET_ATTITUDE + struct { + bool valid; // true once a command has been received + bool is_rate; // true = rate command, false = angle command + bool yaw_is_ef; // true = rates/attitude expressed in earth frame + Vector3f rates_rads; // demanded body rates (is_rate == true) + Quaternion attitude; // desired attitude (is_rate == false) + } _target; + + struct { + mavlink_message_t rxmsg; + mavlink_status_t status; + } mav; + + Matrix3f _vehicle_dcm; + + // ROI location tracking: set by COMMAND_INT DO_SET_ROI_LOCATION + struct { + bool valid; + Location loc; + } _roi; +}; + +} // namespace SITL + +#endif // AP_SIM_MAVLINKGIMBALV2_ENABLED diff --git a/libraries/SITL/SIM_Mount.h b/libraries/SITL/SIM_Mount.h new file mode 100644 index 00000000000000..b945dc3ae8ce54 --- /dev/null +++ b/libraries/SITL/SIM_Mount.h @@ -0,0 +1,36 @@ +/* + Base class for simulated mount (gimbal) serial peripherals +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_MOUNT_ENABLED + +#include "SIM_SerialDevice.h" + +namespace SITL { + +/* + common base for all simulated mount backends. Inherits SerialDevice + so subclasses can be registered with create_serial_sim() and driven + via a serial port. +*/ +class Mount : public SerialDevice { +public: + + // called each simulation step + virtual void update(const class Aircraft &aircraft) = 0; + + // called before add_gimbal_sim() so subclasses know their + // 0-based instance index (used, e.g., to select a MAVLink component ID) + virtual void set_instance(uint8_t instance) { _instance = instance; } + +protected: + uint8_t _instance; +}; + +} // namespace SITL + +#endif // AP_SIM_MOUNT_ENABLED diff --git a/libraries/SITL/SIM_Siyi.cpp b/libraries/SITL/SIM_Siyi.cpp new file mode 100644 index 00000000000000..139a7989c72e83 --- /dev/null +++ b/libraries/SITL/SIM_Siyi.cpp @@ -0,0 +1,248 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Base class for for Siyi camera simulaors +*/ + +#include "SIM_config.h" + +#if AP_SIM_SIYI_ENABLED + +#include "SIM_Siyi.h" + +#include +#include +#include + +using namespace SITL; + +void Siyi::move_preamble_in_buffer(uint8_t search_start_pos) +{ + uint8_t i; + for (i=search_start_pos; i 60) { + // the number is arbitrary and based off our current + // driver; if you're adding support for longer messages + // just change it. + AP_HAL::panic("Bad length %u", received_length); + } + } + FALLTHROUGH; + case 4: + case 3: // validate control field + if (msg.packed_empty.ctrl != 1) { + if (strict_parsing) { + AP_HAL::panic("Bad control"); + } + move_preamble_in_buffer(3); + return; + } + FALLTHROUGH; + case 2: // validate HEADER2 + if (msg.packed_empty.header2 != HEADER2) { + if (strict_parsing) { + AP_HAL::panic("bad HEADER2"); + } + move_preamble_in_buffer(1); + return; + } + FALLTHROUGH; + case 1: // validate HEADER1 + if (msg.packed_empty.header1 != HEADER1) { + if (strict_parsing) { + AP_HAL::panic("bad HEADER1"); + } + move_preamble_in_buffer(1); + return; + } + break; + case 0: + return; + } + + // this sanity check so we don't use the length fields when + // they're not valid: + if (buflen < sizeof(msg.packed_empty)) { + return; + } + + const uint16_t datalen = le16toh(msg.packed_empty.datalen); + const uint16_t totallen = sizeof(msg.packed_empty) + datalen; + if (totallen > sizeof(msg.buffer)) { + if (strict_parsing) { + AP_HAL::panic("bad datalen"); + } + move_preamble_in_buffer(1); + return; + } + + if (buflen < totallen) { + if (strict_parsing) { + AP_HAL::panic("bad datalen"); + } + move_preamble_in_buffer(1); + return; + } + + const uint16_t calculated_checksum = msg.packed_empty.calculate_checksum(totallen-2); // -2 to omit checksum bytes + const uint16_t received_checksum = UINT16_VALUE(msg.buffer[totallen-1], msg.buffer[totallen-2]); + if (calculated_checksum != received_checksum) { + if (strict_parsing) { + AP_HAL::panic("bad checksum"); + } + move_preamble_in_buffer(1); + return; + } + + if (strict_parsing) { + const uint16_t received_seq = le16toh(msg.packed_empty.seq); + // printf("received_seq=%u\n", unsigned(received_seq)); + if (received_seq != expected_seq) { + AP_HAL::panic("Bad sequence (got=%u vs want=%u)", msg.packed_empty.seq, expected_seq+1); + } + expected_seq = received_seq + 1; + } + + handle_received_message(); + + move_preamble_in_buffer(totallen); +} + +void Siyi::update(const class Aircraft &aircraft) +{ + update_input(); + // update_output(location); +} + +#endif // AP_SIM_SIYI_ENABLED diff --git a/libraries/SITL/SIM_Siyi.h b/libraries/SITL/SIM_Siyi.h new file mode 100644 index 00000000000000..bf5740d5624c58 --- /dev/null +++ b/libraries/SITL/SIM_Siyi.h @@ -0,0 +1,220 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Base class for Siyi camera simulators +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_SIYI_ENABLED + +#include "SIM_Mount.h" + +#include +#include +#include + +namespace SITL { + +class Siyi : public Mount { +public: + + void update(const class Aircraft &aircraft) override; + +protected: + + static constexpr uint8_t HEADER1 { 0x55 }; + static constexpr uint8_t HEADER2 { 0x66 }; + + template + class PACKED PackedMessage { + public: + PackedMessage(T _msg) : + msg(_msg) + { + update_checksum(); + } + uint8_t header1 { HEADER1 }; + uint8_t header2 { HEADER2 }; + uint8_t ctrl; + uint16_t datalen {sizeof(T)-1 }; // -1 to exclude command ID + uint16_t seq; + T msg; + uint16_t checksum; + + uint16_t calculate_checksum(uint16_t len) const WARN_IF_UNUSED { + return crc16_ccitt((uint8_t*)this, len, 0); + } + uint16_t calculate_checksum() const WARN_IF_UNUSED { + return calculate_checksum(sizeof(*this)-2); + } + + void update_checksum() { + checksum = calculate_checksum(); + } + + bool response_requested() const { return (ctrl & 0x1) == 0x1; } + }; + + // message ids + enum class CommandID : uint8_t { + ACQUIRE_FIRMWARE_VERSION = 0x01, + HARDWARE_ID = 0x02, + // AUTO_FOCUS = 0x04, + // MANUAL_ZOOM_AND_AUTO_FOCUS = 0x05, + // MANUAL_FOCUS = 0x06, + GIMBAL_ROTATION = 0x07, + // CENTER = 0x08, + ACQUIRE_GIMBAL_CONFIG_INFO = 0x0A, + // FUNCTION_FEEDBACK_INFO = 0x0B, + PHOTO = 0x0C, + ACQUIRE_GIMBAL_ATTITUDE = 0x0D, + // ABSOLUTE_ZOOM = 0x0F, + SET_CAMERA_IMAGE_TYPE = 0x11, + GET_TEMP_FULL_IMAGE = 0x14, + READ_RANGEFINDER = 0x15, + EXTERNAL_ATTITUDE = 0x22, + SET_TIME = 0x30, + POSITION_DATA = 0x3e, + }; + + class PACKED BaseMessage { + public: + BaseMessage(CommandID _cmdid) : + cmdid{uint8_t(_cmdid)} + { } + uint8_t cmdid; + }; + + class PACKED FirmwareVersionRequest : public BaseMessage { }; + virtual void handle_message(const PackedMessage &request) = 0; + + class PACKED HardwareIDRequest : public BaseMessage { }; + virtual void handle_message(const PackedMessage &request) = 0; + + class PACKED GimbalAttitudeRequest : public BaseMessage { }; + virtual void handle_message(const PackedMessage &request) = 0; + + class PACKED ExternalAttitude : public BaseMessage { + public: + uint32_t time_boot_ms; + float roll, pitch, yaw; + float rollspeed, pitchspeed, yawspeed; + }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled ExternalAttitude message"); + } + + class PACKED SetCameraImageType : public BaseMessage { }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled SetCameraImageType message"); + } + + class PACKED Photo : public BaseMessage { + public: + uint8_t command; + }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled Photo message"); + } + + class PACKED AcquireGimbalConfigInfo : public BaseMessage { + public: + }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled AcquireGimbalConfigInfo message"); + } + + class PACKED GetTempFullImageRequest : public BaseMessage { + public: + uint8_t mode; + }; + virtual void handle_message(const PackedMessage &request) { } + + class PACKED ReadRangefinderRequest : public BaseMessage { }; + virtual void handle_message(const PackedMessage &request) { } + + class PACKED GimbalRotation : public BaseMessage { + public: + uint8_t yaw_rate_pct; + uint8_t pitch_rate_pct; + }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled GimbalRotation message"); + } + + class PACKED PositionData : public BaseMessage { + public: + uint32_t time_boot_ms; + int32_t lat, lon; + int32_t alt_msl, alt_ellipsoid; + int32_t velocity_ned_int32[3]; + }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled PositionData message"); + } + + class PACKED SetTime : public BaseMessage { + public: + uint64_t epoch_us; + }; + virtual void handle_message(const PackedMessage &request) { + AP_HAL::panic("Unhandled SetTime message"); + } + + /* + * Input Handling + */ + void update_input(); + void handle_received_message(); + uint32_t expected_message_length(CommandID id) const; + + union u { + u() {} + uint8_t buffer[256]; // from-autopilot + PackedMessage packed_empty; + PackedMessage packed_firmwareversionrequest; + PackedMessage packed_hardwareidrequest; + PackedMessage packed_gimbalattituderequest; + PackedMessage packed_externalattitude; + PackedMessage packed_setcameraimgetype; + PackedMessage packed_photo; + PackedMessage packed_acquiregimbalconfiginfo; + PackedMessage packed_gettempfullimage; + PackedMessage packed_readrangefinder; + PackedMessage packed_gimbalrotation; + PackedMessage packed_positiondata; + PackedMessage packed_settime; + } msg; + uint8_t buflen; + + void move_preamble_in_buffer(uint8_t search_start_pos=0); + + /* + * Output Handling + */ + void update_output(const Location &location); + + uint16_t expected_seq; + + static constexpr bool strict_parsing = true; + +}; // end class Siyi + +}; // end namespace SITL + +#endif // AP_SIM_SIYI_ENABLED diff --git a/libraries/SITL/SIM_Siyi_ZT30.cpp b/libraries/SITL/SIM_Siyi_ZT30.cpp new file mode 100644 index 00000000000000..96e709c00502e7 --- /dev/null +++ b/libraries/SITL/SIM_Siyi_ZT30.cpp @@ -0,0 +1,192 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Simulator for Siyi ZT30 + + */ + +#include "SIM_config.h" + +#if AP_SIM_SIYI_ZT30_ENABLED + +#include "SIM_Siyi_ZT30.h" + +using namespace SITL; + +void Siyi_ZT30::update(const Aircraft &aircraft) +{ + gimbal.update(aircraft); + + // base class update function: + Siyi::update(aircraft); +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + if (!request.response_requested()) { + // weird + AP_HAL::panic("response not requested?"); + } + const PackedMessage response{ + FirmwareVersion{ + camera_major, camera_minor, camera_patch, + gimbal_major, gimbal_minor, gimbal_patch, + zoom_major, zoom_minor, zoom_patch + }, + }; + write_to_autopilot((char*)&response, sizeof(response)); +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + if (!request.response_requested()) { + // weird + AP_HAL::panic("response not requested?"); + } + const PackedMessage response{ + HardwareID{ + hwid_a, hwid_b + }, + }; + write_to_autopilot((char*)&response, sizeof(response)); +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + if (!request.response_requested()) { + // weird + AP_HAL::panic("response not requested?"); + } + + uint32_t delta_time_us; + Vector3f delta_angles; + Vector3f delta_velocities; + gimbal.get_deltas(delta_angles, delta_velocities, delta_time_us); + const Vector3 rates_decidegrees( + (delta_angles.x * RAD_TO_DEG * 1e6 / delta_time_us), + delta_angles.y * RAD_TO_DEG * 1e6 / delta_time_us, + delta_angles.z * RAD_TO_DEG * 1e6 / delta_time_us + ); + + Vector3f joint_angles; + gimbal.get_joint_angles(joint_angles); + const Vector3 joint_angles_decidegrees( + joint_angles.x * 10 * RAD_TO_DEG, + joint_angles.y * 10 * RAD_TO_DEG, + joint_angles.z * 10 * RAD_TO_DEG + ); + + const PackedMessage response{ + GimbalAttitude{ + joint_angles_decidegrees.x, joint_angles_decidegrees.y, joint_angles_decidegrees.z, + rates_decidegrees.x, rates_decidegrees.y, rates_decidegrees.z + }, + }; + write_to_autopilot((char*)&response, sizeof(response)); +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + // MAVProxy and the ArduPilot driver both request an ACK but the + // cameras don't seem to supply it. + + // if (request.ctrl & 0x1) { + // AP_HAL::panic("Response requested for ExternalAttitude?"); + // } +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + // MAVProxy and the ArduPilot driver both request an ACK but the + // cameras don't seem to supply it. + + // if (request.ctrl & 0x1) { + // AP_HAL::panic("Response requested for SetCameraImageType?"); + // } +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + // MAVProxy and the ArduPilot driver both request an ACK but the + // cameras don't seem to supply it. + + // if (request.ctrl & 0x1) { + // AP_HAL::panic("Response requested for SetCameraImageType?"); + // } +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + // MAVProxy and the ArduPilot driver both request an ACK but the + // cameras don't seem to supply it. + + // if (request.ctrl & 0x1) { + // AP_HAL::panic("Response requested for AcquireGimbalConfigInfo?"); + // } +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + const float max_demanded_yaw_rate_rad = 2*M_PI; + const float max_demanded_pitch_rate_rad = 2*M_PI; + + const Vector3f demanded_rates{ + max_demanded_yaw_rate_rad * request.msg.yaw_rate_pct * 0.01f, + max_demanded_pitch_rate_rad * request.msg.pitch_rate_pct * 0.01f, + 0 + }; + gimbal.set_demanded_rates(demanded_rates); + + // MAVProxy and the ArduPilot driver both request an ACK but the + // cameras don't seem to supply it. + + // if (request.ctrl & 0x1) { + // AP_HAL::panic("Response requested for GimbalRotation?"); + // } +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + // MAVProxy and the ArduPilot driver both request an ACK but the + // cameras don't seem to supply it. + + // if (request.ctrl & 0x1) { + // AP_HAL::panic("Response requested for GimbalRotation?"); + // } +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + const PackedMessage response{ + RangefinderDistance{50}, // 50m simulated distance + }; + write_to_autopilot((char*)&response, sizeof(response)); +} + +void Siyi_ZT30::handle_message(const PackedMessage &request) +{ + epoch_us = request.msg.epoch_us; + + if (!request.response_requested()) { + // weird + return; + } + + const PackedMessage response{ + SetTimeResponse{1}, + }; + write_to_autopilot((char*)&response, sizeof(response)); +} + +#endif // AP_SIM_SIYI_ZT30_ENABLED diff --git a/libraries/SITL/SIM_Siyi_ZT30.h b/libraries/SITL/SIM_Siyi_ZT30.h new file mode 100644 index 00000000000000..3ed6a3be501760 --- /dev/null +++ b/libraries/SITL/SIM_Siyi_ZT30.h @@ -0,0 +1,185 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Simulator for Siyi ZT30 + +./Tools/autotest/sim_vehicle.py --gdb --debug -v ArduCopter -A --serial5=sim:siyi_zt30 --speedup=1 + +param set MNT1_TYPE 8 # siyi +param set CAM1_TYPE 4 # mount +param set SERIAL5_PROTOCOL 8 # gimbal +reboot + +long REQUEST_MESSAGE CAMERA_INFORMATION +status *CAM* + +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_SIYI_ZT30_ENABLED + +#include "SIM_Siyi.h" +#include "SIM_Gimbal.h" + +namespace SITL { + +class Siyi_ZT30 : public Siyi { +public: + + void update(const Aircraft &aircraft) override; + +protected: + + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + void handle_message(const PackedMessage &request) override; + +private: + + // the physical gimbal: + Gimbal gimbal; + + class PACKED FirmwareVersion : Siyi::BaseMessage { + public: + FirmwareVersion( + uint8_t _camera_major, uint8_t _camera_minor, uint8_t _camera_patch, + uint8_t _gimbal_major, uint8_t _gimbal_minor, uint8_t _gimbal_patch, + uint8_t _zoom_major, uint8_t _zoom_minor, uint8_t _zoom_patch + ) : BaseMessage(Siyi::CommandID::ACQUIRE_FIRMWARE_VERSION), + camera(_camera_major << 16 | _camera_minor << 8 | _camera_patch), + gimbal(_gimbal_major << 16 | _gimbal_minor << 8 | _gimbal_patch), + zoom(_zoom_major << 16 | _zoom_minor << 8 | _zoom_patch) + { } + + uint32_t camera; + uint32_t gimbal; + uint32_t zoom; + }; + + class PACKED HardwareID : Siyi::BaseMessage { + public: + HardwareID(uint8_t _a, uint8_t _b) : + BaseMessage(Siyi::CommandID::HARDWARE_ID), + major{_a}, + minor{_b} + { } + + uint8_t major; + uint8_t minor; + }; + + class PACKED GimbalAttitude : Siyi::BaseMessage { + public: + GimbalAttitude(int16_t _yaw_decidegrees, + int16_t _pitch_decidegrees, + int16_t _roll_decidegrees, + int16_t _yaw_rate_decidegrees, + int16_t _pitch_rate_decidegrees, + int16_t _roll_rate_decidegrees + ) : + BaseMessage(Siyi::CommandID::ACQUIRE_GIMBAL_ATTITUDE), + yaw_decidegrees{_yaw_decidegrees}, + pitch_decidegrees{_pitch_decidegrees}, + roll_decidegrees{_roll_decidegrees}, + yaw_rate_decidegreess{_yaw_rate_decidegrees}, + pitch_rate_decidegreess{_pitch_rate_decidegrees}, + roll_rate_decidegreess{_roll_rate_decidegrees} + { } + + int16_t yaw_decidegrees; + int16_t pitch_decidegrees; + int16_t roll_decidegrees; + + int16_t yaw_rate_decidegreess; + int16_t pitch_rate_decidegreess; + int16_t roll_rate_decidegreess; + }; + + class PACKED GimbalConfigInfo : Siyi::BaseMessage { + public: + GimbalConfigInfo(uint8_t _hdr_status, + uint8_t _record_status, + uint8_t _motion_mode, + uint8_t _mounting_dir, + uint8_t _video_mode + ) : + BaseMessage(Siyi::CommandID::ACQUIRE_GIMBAL_CONFIG_INFO), + hdr_status{_hdr_status}, + record_status{_record_status}, + motion_mode{_motion_mode}, + mounting_dir{_mounting_dir}, + video_mode{_video_mode} + { } + + uint8_t reserved1; + uint8_t hdr_status; + uint8_t reserved3; + uint8_t record_status; + uint8_t motion_mode; + uint8_t mounting_dir; + uint8_t video_mode; + }; + + class PACKED RangefinderDistance : Siyi::BaseMessage { + public: + RangefinderDistance(uint16_t _dist_m) : + BaseMessage(Siyi::CommandID::READ_RANGEFINDER), + dist_m{_dist_m} + { } + uint16_t dist_m; + }; + + class PACKED SetTimeResponse : Siyi::BaseMessage { + public: + SetTimeResponse(uint8_t _result) : + BaseMessage(Siyi::CommandID::SET_TIME), + result{_result} + { } + + uint8_t result; + }; + + static constexpr uint8_t hwid_a { '7' }; + static constexpr uint8_t hwid_b { 'A' }; + + static constexpr uint8_t camera_major { 1 }; + static constexpr uint8_t camera_minor { 2 }; + static constexpr uint8_t camera_patch { 3 }; + + static constexpr uint8_t gimbal_major { 4 }; + static constexpr uint8_t gimbal_minor { 5 }; + static constexpr uint8_t gimbal_patch { 6 }; + + static constexpr uint8_t zoom_major { 7 }; + static constexpr uint8_t zoom_minor { 8 }; + static constexpr uint8_t zoom_patch { 9 }; + + uint64_t epoch_us; // from SET_TIME + +}; // end class Siyi_ZT30 +}; // end namespace SITL + +#endif // AP_SIM_SIYI_ZT30_ENABLED diff --git a/libraries/SITL/SIM_Topotek.cpp b/libraries/SITL/SIM_Topotek.cpp new file mode 100644 index 00000000000000..412401e733d70d --- /dev/null +++ b/libraries/SITL/SIM_Topotek.cpp @@ -0,0 +1,268 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Simulator for Topotek gimbal +*/ + +#include "SIM_config.h" + +#if AP_SIM_TOPOTEK_ENABLED + +#include "SIM_Topotek.h" +#include "SIM_Aircraft.h" +#include +#include + +using namespace SITL; + +void Topotek::update(const Aircraft &aircraft) +{ + // Drive GimbalSim toward the angles commanded via GIP/GIY packets. + // Wire encoding: pitch_cd = -pitch_deg * 100 (driver negates on read), + // yaw_cd = yaw_deg * 100. + { + const float target_pitch_rad = -radians(_commanded_pitch_cd * 0.01f); + const float target_yaw_rad = radians(_commanded_yaw_cd * 0.01f); + + Vector3f ja; + gimbal.get_joint_angles(ja); + + Matrix3f gimbal_dcm; + gimbal.get_dcm(gimbal_dcm); + const Vector3f vehicle_rate_gimbal = gimbal_dcm.transposed() * aircraft.get_dcm() * aircraft.get_gyro(); + + static constexpr float GAIN = 10.0f; + gimbal.set_demanded_rates(Vector3f( + vehicle_rate_gimbal.x, + vehicle_rate_gimbal.y + (target_pitch_rad - ja.y) * GAIN, + vehicle_rate_gimbal.z + (target_yaw_rad - ja.z) * GAIN)); + } + + gimbal.update(aircraft); + update_input(); + + // send attitude at 10 Hz + const uint32_t now_ms = AP_HAL::millis(); + if (now_ms - _last_attitude_ms >= 100) { + _last_attitude_ms = now_ms; + send_attitude(); + } +} + +void Topotek::send_attitude() +{ + // Report actual GimbalSim joint angles. + // joint_angles.y = pitch (negative = down), .z = azimuth. + // Wire encoding: pitch_cd = -pitch_deg * 100 (driver negates on read). + // yaw_cd = yaw_deg * 100. + Vector3f ja; + gimbal.get_joint_angles(ja); + const int16_t yaw_cd = (int16_t)(degrees(ja.z) * 100.0f); + const int16_t pitch_cd = (int16_t)(-degrees(ja.y) * 100.0f); + const int16_t roll_cd = (int16_t)(degrees(ja.x) * 100.0f); + + uint8_t data[12]; + uint16_to_hex4((uint16_t)yaw_cd, &data[0]); + uint16_to_hex4((uint16_t)pitch_cd, &data[4]); + uint16_to_hex4((uint16_t)roll_cd, &data[8]); + + send_packet('G', "GIA", false, data, sizeof(data)); +} + +/* + read bytes from autopilot into _buf, then scan for complete packets. + Packet format: + [0] '#' + [1] 'T' or 't' + [2] 'P' or 'p' + [3] 'U' (addr1) + [4] addr2 ('G','D','M','E','P') + [5] data_len as a single ASCII hex char + [6] 'r' or 'w' + [7..9] 3-char command ID + [10..10+data_len-1] data bytes + [10+data_len..11+data_len] 2-byte CRC + Total packet length = 12 + data_len +*/ +void Topotek::move_preamble_in_buffer(uint8_t search_start_pos) +{ + uint8_t i; + for (i = search_start_pos; i < _buflen; i++) { + if (_buf[i] == '#') { + break; + } + } + if (i == 0) { + return; + } + memmove(_buf, &_buf[i], _buflen - i); + _buflen -= i; +} + +void Topotek::update_input() +{ + const ssize_t n = read_from_autopilot((char*)&_buf[_buflen], ARRAY_SIZE(_buf) - _buflen - 1); + if (n < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != 0) { + AP_HAL::panic("Failed to read from autopilot"); + } + return; + } + _buflen += n; + + while (_buflen >= 3) { + // search for '#' at the start + if (_buf[0] != '#') { + move_preamble_in_buffer(1); + continue; + } + if (_buf[1] != 'T' && _buf[1] != 't') { + move_preamble_in_buffer(1); + continue; + } + if (_buf[2] != 'P' && _buf[2] != 'p') { + move_preamble_in_buffer(1); + continue; + } + + // need at least 6 bytes to read the data_len field + if (_buflen < 6) { + break; + } + + // parse data length from ASCII hex char at [5] + const uint8_t data_len = char_to_hex(_buf[5]); + if (data_len == 255) { + // invalid data length — discard '#' + move_preamble_in_buffer(1); + continue; + } + + const uint8_t pkt_len = 12 + data_len; + if (pkt_len > PACKETLEN_MAX) { + move_preamble_in_buffer(1); + continue; + } + + // wait for the full packet + if (_buflen < pkt_len) { + break; + } + + // verify and dispatch the packet + handle_packet(data_len); + move_preamble_in_buffer(pkt_len); + } +} + +void Topotek::handle_packet(uint8_t data_len) +{ + // verify CRC + const uint8_t pkt_len = 12 + data_len; + const uint8_t crc = crc_sum_of_bytes(_buf, pkt_len - 2); + const uint8_t expected_hi = hex2char((crc >> 4) & 0x0f); + const uint8_t expected_lo = hex2char(crc & 0x0f); + if (_buf[pkt_len - 2] != expected_hi || _buf[pkt_len - 1] != expected_lo) { + return; + } + + // ID is at bytes [7..9] + const char *id = (const char*)&_buf[7]; + + if (strncmp(id, "GIA", 3) == 0) { + // attitude request + send_attitude(); + + } else if (strncmp(id, "VSN", 3) == 0) { + const uint8_t data[] { '1', '.', '0', '.', '0' }; + send_packet('D', "VSN", false, data, sizeof(data)); + + } else if (strncmp(id, "PA2", 3) == 0) { + const uint8_t data[] { 'S', 'I', 'M', '_', 'T', 'P' }; + send_packet('G', "PA2", false, data, sizeof(data)); + + } else if (strncmp(id, "SDC", 3) == 0) { + // card present: any 4 bytes that are NOT all 'N' + const uint8_t data[] { '1', '0', '0', '0' }; + send_packet('D', "SDC", false, data, sizeof(data)); + + } else if (strncmp(id, "TRC", 3) == 0) { + // tracking stopped: driver reads _msg_buff[11] as TrackingStatus; + // [11] = data[1] = '0' = 0x30 = STOPPED_TRACKING + const uint8_t data[] { '0', '0' }; + send_packet('D', "TRC", false, data, sizeof(data)); + + } else if (strncmp(id, "GIP", 3) == 0 && data_len >= 4) { + // pitch angle command: data[0..3] = 4 uppercase hex chars for int16 centidegrees + // Wire value is (uint16_t)(-degrees(pitch_rad)*100); store as-is for echo in send_attitude() + _commanded_pitch_cd = (int16_t)( + ((uint16_t)char_to_hex(_buf[10]) << 12) | + ((uint16_t)char_to_hex(_buf[11]) << 8) | + ((uint16_t)char_to_hex(_buf[12]) << 4) | + (uint16_t)char_to_hex(_buf[13])); + + } else if (strncmp(id, "GIY", 3) == 0 && data_len >= 4) { + // body-frame yaw angle command + _commanded_yaw_cd = (int16_t)( + ((uint16_t)char_to_hex(_buf[10]) << 12) | + ((uint16_t)char_to_hex(_buf[11]) << 8) | + ((uint16_t)char_to_hex(_buf[12]) << 4) | + (uint16_t)char_to_hex(_buf[13])); + } + // all other commands (YPR, GIR, PTZ, LAT, LON, ALT, etc.) absorbed silently +} + +void Topotek::send_packet(char addr2, const char id[3], bool write, const uint8_t *data, uint8_t len) +{ + const uint8_t total = 12 + len; + if (total > PACKETLEN_MAX) { + return; + } + + uint8_t pkt[PACKETLEN_MAX]; + uint8_t ofs = 0; + + pkt[ofs++] = '#'; + pkt[ofs++] = 'T'; + pkt[ofs++] = 'P'; + pkt[ofs++] = 'U'; + pkt[ofs++] = (uint8_t)addr2; + pkt[ofs++] = hex2char(len & 0x0f); // data length as single ASCII hex char + pkt[ofs++] = write ? 'w' : 'r'; + pkt[ofs++] = (uint8_t)id[0]; + pkt[ofs++] = (uint8_t)id[1]; + pkt[ofs++] = (uint8_t)id[2]; + + for (uint8_t i = 0; i < len; i++) { + pkt[ofs++] = data[i]; + } + + // checksum: byte sum of all preceding bytes, encoded as 2 uppercase ASCII hex chars + const uint8_t crc = crc_sum_of_bytes(pkt, ofs); + pkt[ofs++] = hex2char((crc >> 4) & 0x0f); + pkt[ofs++] = hex2char(crc & 0x0f); + + write_to_autopilot((const char*)pkt, ofs); +} + +void Topotek::uint16_to_hex4(uint16_t val, uint8_t buf[4]) +{ + buf[0] = hex2char((val >> 12) & 0x0f); + buf[1] = hex2char((val >> 8) & 0x0f); + buf[2] = hex2char((val >> 4) & 0x0f); + buf[3] = hex2char((val ) & 0x0f); +} + +#endif // AP_SIM_TOPOTEK_ENABLED diff --git a/libraries/SITL/SIM_Topotek.h b/libraries/SITL/SIM_Topotek.h new file mode 100644 index 00000000000000..8a8df59d901ea4 --- /dev/null +++ b/libraries/SITL/SIM_Topotek.h @@ -0,0 +1,84 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Simulator for Topotek gimbal + +./Tools/autotest/sim_vehicle.py --gdb --debug -v ArduCopter -A --serial5=sim:topotek --speedup=1 + +param set MNT1_TYPE 12 # topotek +param set SERIAL5_PROTOCOL 8 # gimbal +reboot + +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_TOPOTEK_ENABLED + +#include "SIM_Mount.h" +#include "SIM_Gimbal.h" + +namespace SITL { + +class Topotek : public Mount { +public: + + void update(const Aircraft &aircraft) override; + +private: + + // the physical gimbal: + Gimbal gimbal; + + // input accumulation buffer; also used as working buffer by handle_packet() + static constexpr uint8_t PACKETLEN_MAX = 36; + uint8_t _buf[PACKETLEN_MAX]; + uint8_t _buflen; + + uint32_t _last_attitude_ms; // time of last attitude packet sent + + // last commanded angles from GIP/GIY packets (wire centidegrees, same sign as sent by driver) + int16_t _commanded_pitch_cd; + int16_t _commanded_yaw_cd; + + // read and dispatch incoming packets from autopilot + void update_input(); + + // scan forward from search_start_pos for '#' and move it to _buf[0] + void move_preamble_in_buffer(uint8_t search_start_pos); + + // send gimbal attitude packet to the driver + void send_attitude(); + + // dispatch a complete packet beginning at _buf[0], data_len data bytes + void handle_packet(uint8_t data_len); + + // build and send a response packet + void send_packet(char addr2, const char id[3], bool write, const uint8_t *data, uint8_t len); + + // encode a uint16 as 4 uppercase ASCII hex chars + static void uint16_to_hex4(uint16_t val, uint8_t buf[4]); + + // convert a nibble (0-15) to an uppercase ASCII hex character + static uint8_t hex2char(uint8_t nibble) { + return nibble < 10 ? ('0' + nibble) : ('A' + nibble - 10); + } +}; + +} // namespace SITL + +#endif // AP_SIM_TOPOTEK_ENABLED diff --git a/libraries/SITL/SIM_Viewpro.cpp b/libraries/SITL/SIM_Viewpro.cpp new file mode 100644 index 00000000000000..c0f808d4fa665c --- /dev/null +++ b/libraries/SITL/SIM_Viewpro.cpp @@ -0,0 +1,373 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Simulator for Viewpro gimbal +*/ + +#include "SIM_config.h" + +#if AP_SIM_VIEWPRO_ENABLED + +#include "SIM_Viewpro.h" +#include "SIM_Aircraft.h" +#include +#include + +using namespace SITL; + +// scalar from AP_Mount_Viewpro.cpp +#define VIEWPRO_DEG_TO_OUTPUT (65536.0f / 360.0f) + +void Viewpro::update(const Aircraft &aircraft) +{ + // Drive GimbalSim toward the angles commanded via A1 packets. + // The A1 wire encoding is: pitch_raw = -pitch_deg * DEG_TO_OUTPUT, + // yaw_raw = yaw_deg * DEG_TO_OUTPUT. Invert to recover target radians. + { + static constexpr float OUTPUT_TO_RAD = (360.0f / 65536.0f) * (float)DEG_TO_RAD; + const float target_pitch_rad = -_target_pitch_raw * OUTPUT_TO_RAD; + const float target_yaw_rad = _target_yaw_raw * OUTPUT_TO_RAD; + + Vector3f ja; + gimbal.get_joint_angles(ja); + + // P-gain: drives gimbal to commanded angle with ~0.1 s time constant. + // joint_angles.y = pitch (negative = down), .z = azimuth. + Matrix3f gimbal_dcm; + gimbal.get_dcm(gimbal_dcm); + const Vector3f vehicle_rate_gimbal = gimbal_dcm.transposed() * aircraft.get_dcm() * aircraft.get_gyro(); + + static constexpr float GAIN = 10.0f; + gimbal.set_demanded_rates(Vector3f( + vehicle_rate_gimbal.x, + vehicle_rate_gimbal.y + (target_pitch_rad - ja.y) * GAIN, + vehicle_rate_gimbal.z + (target_yaw_rad - ja.z) * GAIN)); + } + + gimbal.update(aircraft); + update_input(); + + // send attitude at 10 Hz + const uint32_t now_ms = AP_HAL::millis(); + if (now_ms - _last_attitude_ms >= 100) { + _last_attitude_ms = now_ms; + send_t1_f1_b1_d1(); + } +} + +/* + read bytes from the autopilot and process them byte-by-byte through the + Viewpro protocol state machine. + + The driver discards header bytes without storing them. After all three + header bytes are consumed the internal packet buffer starts fresh: + _buf[0] = length+frame_counter byte (bits 0-5 = body_length) + _buf[1] = frame_id byte + _buf[2+] = data bytes + _buf[N] = CRC byte (last) + + body_length = 1(length byte) + 1(frame_id) + data_len + 1(crc) + => data_len = body_length - 3 +*/ +void Viewpro::update_input() +{ + // read new bytes into a scratch buffer; process one at a time + uint8_t scratch[128]; + const ssize_t n = read_from_autopilot((char*)scratch, sizeof(scratch)); + if (n < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != 0) { + AP_HAL::panic("Failed to read from autopilot"); + } + return; + } + + for (ssize_t i = 0; i < n; i++) { + const uint8_t b = scratch[i]; + + switch (_parse_state) { + case ParseState::HEADER1: + if (b == HEADER1) { + _buflen = 0; + _parse_state = ParseState::HEADER2; + } + break; + + case ParseState::HEADER2: + if (b == HEADER2) { + _buflen = 0; + _parse_state = ParseState::HEADER3; + } else { + _buflen = 0; + _parse_state = ParseState::HEADER1; + } + break; + + case ParseState::HEADER3: + if (b == HEADER3) { + _buflen = 0; + _parse_state = ParseState::LENGTH; + } else { + _buflen = 0; + _parse_state = ParseState::HEADER1; + } + break; + + case ParseState::LENGTH: { + const uint8_t body_length = b & 0x3F; + if (body_length < 3) { + _buflen = 0; + _parse_state = ParseState::HEADER1; + break; + } + _data_len = body_length - 3; + _buf[_buflen++] = b; // _buf[0] = length+fc byte + _parse_state = ParseState::FRAMEID; + break; + } + + case ParseState::FRAMEID: + _data_bytes_received = 0; + _buf[_buflen++] = b; // _buf[1] = frame_id + _parse_state = (_data_len > 0) ? ParseState::DATA : ParseState::CHECKSUM; + break; + + case ParseState::DATA: + if (_buflen < PACKETLEN_MAX) { + _buf[_buflen++] = b; + } + _data_bytes_received++; + if (_data_bytes_received >= _data_len) { + _parse_state = ParseState::CHECKSUM; + } + break; + + case ParseState::CHECKSUM: { + if (_buflen < PACKETLEN_MAX) { + _buf[_buflen++] = b; + } + // verify CRC: XOR of _buf[0.._buflen-2] (all bytes except the crc byte itself) + const uint8_t expected_crc = crc_xor_of_bytes(_buf, _buflen - 1); + if (expected_crc == b) { + dispatch_packet(); + } + _buflen = 0; + _parse_state = ParseState::HEADER1; + break; + } + } + } +} + +void Viewpro::dispatch_packet() +{ + // _buf[1] = frame_id; _buf[2+] = data (data_start = 2) + const FrameId frame_id = (FrameId)_buf[1]; + + switch (frame_id) { + case FrameId::HANDSHAKE: + send_t1_f1_b1_d1(); + break; + + case FrameId::U: + // _buf[2] = data[0] = CommConfigCmd + if (_data_len >= 1) { + send_v_response((CommConfigCmd)_buf[2]); + } + break; + + case FrameId::A1: + // _buf layout: [0]=length_fc, [1]=frame_id, [2]=servo_status, + // [3..4]=yaw big-endian, [5..6]=pitch big-endian + // Store raw int16 values so the driver round-trips them without + // any additional float rounding. + if (_data_len >= 5) { + _target_yaw_raw = (int16_t)(((uint16_t)_buf[3] << 8) | _buf[4]); + _target_pitch_raw = (int16_t)(((uint16_t)_buf[5] << 8) | _buf[6]); + } + break; + case FrameId::C1: + case FrameId::C2: + case FrameId::E1: + case FrameId::E2: + case FrameId::M_AHRS: + case FrameId::HEARTBEAT: + // absorb; gimbal physics is updated via update() + break; + + default: + break; + } +} + +/* + send T1_F1_B1_D1 (0x40) packet with current gimbal attitude. + + Driver reads angles using _msg_buff_data_start=2: + data[22] = tracking status (bits 3-4; 0 = stopped) + data[23] = servo_status(upper 4 bits) | roll_12bit_MSByte(lower 4 bits) + data[24] = roll LSByte → roll_deg = 12-bit * 180/4095 - 90 + data[25..26] = yaw int16 big-endian → yaw_deg = val * 360/65536 + data[27..28] = pitch int16 big-endian → pitch_deg = val * 360/65536 (driver negates) + data[29] = image sensor (bits 0-2; stored as val+1; 0→EO1) + data[32] = recording status (bits 0-2; 0=stopped) + data[33..34] = rangefinder × 0.1 m + data[39..40] = zoom × 0.1 +*/ +void Viewpro::send_t1_f1_b1_d1() +{ + static constexpr uint8_t DATA_LEN = 41; + // databuff: frame_id (1 byte) + data (DATA_LEN bytes) + uint8_t databuff[1 + DATA_LEN] {}; + databuff[0] = (uint8_t)FrameId::T1_F1_B1_D1; + + uint8_t *data = &databuff[1]; + + // Report actual GimbalSim joint angles. + // joint_angles.y = pitch (negative = down), .z = azimuth. + // Wire encoding: pitch_out = -pitch_deg * DEG_TO_OUTPUT (driver negates on read). + // yaw_out = yaw_deg * DEG_TO_OUTPUT. + // roll: 12-bit value where 2048 (midpoint of 0..4095) ≈ 0 deg (actual = +0.022 deg, + // within the neutral_tol_deg=0.05 used by MountViewPro). + // Pitch clamp to ±16380 (±89.978 deg): at exactly ±90 deg (raw ±16384) the float32 + // from_euler quaternion with the small roll (~0.022 deg) has DCM[2][0] > 1.0 in + // float64 (Python), triggering pymavlink gimbal-lock → 180 deg instead of ±90 deg. + Vector3f ja; + gimbal.get_joint_angles(ja); + const int16_t yaw_out = (int16_t)(degrees(ja.z) * VIEWPRO_DEG_TO_OUTPUT); + const int16_t pitch_out = (int16_t)constrain_int16( + (int16_t)(-degrees(ja.y) * VIEWPRO_DEG_TO_OUTPUT), + -16380, 16380); + const uint16_t roll_out = 2048U; + + // bytes 0..21: T1/F1 fields — leave as zero + data[22] = 0x00; // tracking stopped + + data[23] = (uint8_t)((roll_out >> 8) & 0x0F); // lower nibble = roll MSB; upper nibble = 0 (servo status) + data[24] = (uint8_t)(roll_out & 0xFF); + + data[25] = (uint8_t)((uint16_t)yaw_out >> 8); + data[26] = (uint8_t)((uint16_t)yaw_out & 0xFF); + + data[27] = (uint8_t)((uint16_t)pitch_out >> 8); + data[28] = (uint8_t)((uint16_t)pitch_out & 0xFF); + + data[29] = 0x00; // image sensor bits 0-2 = 0 → driver stores as EO1 + data[32] = 0x00; // recording stopped + data[33] = 0x00; // rangefinder MSB + data[34] = 0x00; // rangefinder LSB + data[39] = 0x00; // zoom MSB + data[40] = 0x0A; // zoom LSB: 10 × 0.1 = 1.0× zoom + + send_packet(databuff, sizeof(databuff)); +} + +/* + send V (0x02) response to a U communication config command. + + Firmware version response (QUERY_FIRMWARE_VER = 0xD0): + data[0] = 0xD0 (echo of CommConfigCmd) + data[1] = 'S' prefix + data[2..9] = "yyyymmdd" date string + driver reads: major=atoi(data[4..5]), minor=atoi(data[6..7]), patch=atoi(data[8..9]) + Using "20220301": major=22, minor=03, patch=01 + + Model name response (QUERY_MODEL = 0xE4): + data[0] = 0xE4 + data[1+] = model name bytes + driver: memcpy(_model_name, &data[1], data_bytes_received-1) +*/ +void Viewpro::send_v_response(CommConfigCmd cmd) +{ + uint8_t databuff[32] {}; + uint8_t total_len; + + databuff[0] = (uint8_t)FrameId::V; + + switch (cmd) { + case CommConfigCmd::QUERY_FIRMWARE_VER: + databuff[1] = (uint8_t)cmd; + databuff[2] = 'S'; // prefix character + databuff[3] = 'y'; + databuff[4] = 's'; + databuff[5] = '2'; // major hi: "20" + databuff[6] = '0'; + databuff[7] = '2'; // minor hi: "22" + databuff[8] = '2'; + databuff[9] = '0'; // patch hi: "01" + databuff[10] = '1'; + total_len = 11; + break; + + case CommConfigCmd::QUERY_MODEL: + databuff[1] = (uint8_t)cmd; + databuff[2] = 'S'; + databuff[3] = 'I'; + databuff[4] = 'M'; + databuff[5] = '_'; + databuff[6] = 'V'; + databuff[7] = 'P'; + total_len = 8; + break; + + default: + return; + } + + send_packet(databuff, total_len); +} + +/* + build and write a complete Viewpro packet to the autopilot. + databuff[0] = frame_id, databuff[1+] = data payload. + + Wire format: + 0x55 0xAA 0xDC [length_fc] [databuff...] [crc] + where: + length_fc bits 6-7 = frame counter (0-3, increments per packet) + length_fc bits 0-5 = databuff_len + 2 (= body_length) + crc = XOR of [length_fc .. last_databuff_byte] +*/ +void Viewpro::send_packet(const uint8_t *databuff, uint8_t databuff_len) +{ + // total wire bytes: 3 (header) + 1 (length_fc) + databuff_len + 1 (crc) + const uint8_t wire_len = 5 + databuff_len; + if (wire_len > (uint8_t)(PACKETLEN_MAX + 4)) { + return; + } + + uint8_t pkt[PACKETLEN_MAX + 4]; + uint8_t ofs = 0; + + pkt[ofs++] = HEADER1; + pkt[ofs++] = HEADER2; + pkt[ofs++] = HEADER3; + + _frame_counter = (_frame_counter + 1) & 0x03; + const uint8_t body_length = (uint8_t)(databuff_len + 2); + pkt[ofs++] = (uint8_t)((_frame_counter << 6) | (body_length & 0x3F)); + + for (uint8_t i = 0; i < databuff_len; i++) { + pkt[ofs++] = databuff[i]; + } + + // CRC: XOR of bytes [3]..[ofs-1] (length_fc through last data byte) + pkt[ofs] = crc_xor_of_bytes(&pkt[3], ofs - 3); + ofs++; + + write_to_autopilot((const char*)pkt, ofs); +} + + +#endif // AP_SIM_VIEWPRO_ENABLED diff --git a/libraries/SITL/SIM_Viewpro.h b/libraries/SITL/SIM_Viewpro.h new file mode 100644 index 00000000000000..95fb3c4d1b10d3 --- /dev/null +++ b/libraries/SITL/SIM_Viewpro.h @@ -0,0 +1,124 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/* + Simulator for Viewpro gimbal + +./Tools/autotest/sim_vehicle.py --gdb --debug -v ArduCopter -A --serial5=sim:viewpro --speedup=1 + +param set MNT1_TYPE 11 # viewpro +param set SERIAL5_PROTOCOL 8 # gimbal +reboot + +*/ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_VIEWPRO_ENABLED + +#include "SIM_Mount.h" +#include "SIM_Gimbal.h" + +namespace SITL { + +class Viewpro : public Mount { +public: + + void update(const Aircraft &aircraft) override; + +private: + + // the physical gimbal: + Gimbal gimbal; + + // Viewpro header bytes + static constexpr uint8_t HEADER1 = 0x55; + static constexpr uint8_t HEADER2 = 0xAA; + static constexpr uint8_t HEADER3 = 0xDC; + static constexpr uint8_t PACKETLEN_MAX = 63; + + // frame IDs (matching AP_Mount_Viewpro.h) + enum class FrameId : uint8_t { + HANDSHAKE = 0x00, + U = 0x01, + V = 0x02, + HEARTBEAT = 0x10, + A1 = 0x1A, + C1 = 0x1C, + E1 = 0x1E, + C2 = 0x2C, + E2 = 0x2E, + T1_F1_B1_D1 = 0x40, + M_AHRS = 0xB1, + }; + + // U packet communication configuration commands + enum class CommConfigCmd : uint8_t { + QUERY_FIRMWARE_VER = 0xD0, + QUERY_MODEL = 0xE4, + }; + + // packet buffer. + // after header bytes are discarded by the state machine: + // _buf[0] = length+frame_counter byte + // _buf[1] = frame_id + // _buf[2+] = data bytes + // _buf[last] = CRC + uint8_t _buf[PACKETLEN_MAX]; + uint8_t _buflen; + + // parser state + enum class ParseState : uint8_t { + HEADER1, + HEADER2, + HEADER3, + LENGTH, + FRAMEID, + DATA, + CHECKSUM, + } _parse_state; + + uint8_t _data_len; // data bytes expected in current packet + uint8_t _data_bytes_received; // data bytes received so far + + uint8_t _frame_counter; // 2-bit counter for outgoing packets + uint32_t _last_attitude_ms; // time of last T1_F1_B1_D1 packet sent + + // last A1 angle target received from driver, stored as raw int16 wire values + // (same encoding used in T1_F1_B1_D1 replies so the driver round-trips exactly) + int16_t _target_pitch_raw; + int16_t _target_yaw_raw; + + // read and parse incoming bytes from autopilot + void update_input(); + + // dispatch a complete, CRC-verified packet in _buf + void dispatch_packet(); + + // send T1_F1_B1_D1 attitude status packet + void send_t1_f1_b1_d1(); + + // send V response to a U communication config command + void send_v_response(CommConfigCmd cmd); + + // build and write a complete packet; databuff[0] = frame_id + void send_packet(const uint8_t *databuff, uint8_t databuff_len); + +}; + +} // namespace SITL + +#endif // AP_SIM_VIEWPRO_ENABLED diff --git a/libraries/SITL/SIM_config.h b/libraries/SITL/SIM_config.h index 0fbc60511fc558..78693bfe58a67f 100644 --- a/libraries/SITL/SIM_config.h +++ b/libraries/SITL/SIM_config.h @@ -200,8 +200,30 @@ #define AP_SIM_SOLOGIMBAL_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL && HAL_MAVLINK_BINDINGS_ENABLED) #endif -#ifndef AP_SIM_GIMBAL_ENABLED -#define AP_SIM_GIMBAL_ENABLED (AP_SIM_SOLOGIMBAL_ENABLED) +#ifndef AP_SIM_SIYI_ZT30_ENABLED +#define AP_SIM_SIYI_ZT30_ENABLED 1 +#endif + +#ifndef AP_SIM_TOPOTEK_ENABLED +#define AP_SIM_TOPOTEK_ENABLED 1 +#endif + +#ifndef AP_SIM_VIEWPRO_ENABLED +#define AP_SIM_VIEWPRO_ENABLED 1 +#endif + +#ifndef AP_SIM_AVT_CM62_ENABLED +#define AP_SIM_AVT_CM62_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL && HAL_MAVLINK_BINDINGS_ENABLED) +#endif + +// base class for MAVLink (Gimbal Protocol v2) mount simulations: +#ifndef AP_SIM_MAVLINKGIMBALV2_ENABLED +#define AP_SIM_MAVLINKGIMBALV2_ENABLED AP_SIM_AVT_CM62_ENABLED +#endif + +// base class for all simulated gimbal backends: +#ifndef AP_SIM_MOUNT_ENABLED +#define AP_SIM_MOUNT_ENABLED (AP_SIM_SIYI_ENABLED || AP_SIM_TOPOTEK_ENABLED || AP_SIM_VIEWPRO_ENABLED || AP_SIM_MAVLINKGIMBALV2_ENABLED) #endif #ifndef AP_SIM_AIRSPEED_DLVR_ENABLED @@ -355,3 +377,12 @@ #ifndef AP_SIM_XPLANE_ENABLED #define AP_SIM_XPLANE_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL) #endif // AP_SIM_XPLANE_ENABLED + +// base class for Siyi simulations: +#ifndef AP_SIM_SIYI_ENABLED +#define AP_SIM_SIYI_ENABLED AP_SIM_SIYI_ZT30_ENABLED +#endif // AP_SIM_SIYI_ENABLED + +#ifndef AP_SIM_GIMBAL_ENABLED +#define AP_SIM_GIMBAL_ENABLED (AP_SIM_SOLOGIMBAL_ENABLED || AP_SIM_SIYI_ZT30_ENABLED || AP_SIM_TOPOTEK_ENABLED || AP_SIM_VIEWPRO_ENABLED || AP_SIM_MOUNT_ENABLED) +#endif // AP_SIM_GIMBAL_ENABLED From 61feb8d2c8a99fe91e7f08305c37fbdf83829efb Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sun, 22 Mar 2026 15:22:25 +1100 Subject: [PATCH 72/99] autotests: add tests for ZT30, Topotek and ViewPro gimbals --- Tools/autotest/arducopter.py | 185 +++++++++++++++++++++++++-- Tools/autotest/vehicle_test_suite.py | 7 +- 2 files changed, 181 insertions(+), 11 deletions(-) diff --git a/Tools/autotest/arducopter.py b/Tools/autotest/arducopter.py index 833f96020c3ebe..747d82430c8951 100644 --- a/Tools/autotest/arducopter.py +++ b/Tools/autotest/arducopter.py @@ -6253,8 +6253,8 @@ def ManualThrottleModeChange(self): self.wait_disarmed() def constrained_mount_pitch(self, pitch_angle_deg, mount_instance=1): - PITCH_MIN = self.get_parameter("MNT%u_PITCH_MIN" % mount_instance) - PITCH_MAX = self.get_parameter("MNT%u_PITCH_MAX" % mount_instance) + PITCH_MIN = self.get_parameter("MNT%u_PITCH_MIN" % mount_instance, verbose=False) + PITCH_MAX = self.get_parameter("MNT%u_PITCH_MAX" % mount_instance, verbose=False) return min(max(pitch_angle_deg, PITCH_MIN), PITCH_MAX) def test_mount_pitch(self, despitch, despitch_tolerance, mount_mode, timeout=10, hold=0, constrained=True): @@ -6429,7 +6429,7 @@ def test_mount_rc_targetting_rate_control(self, pitch_rc_neutral=1500): self.set_rc(12, 1500) self.test_mount_pitch(0, 0.1, mavutil.mavlink.MAV_MOUNT_MODE_RC_TARGETING) - def mount_test_body(self, pitch_rc_neutral=1500, do_rate_tests=True, constrain_sysid_target=True): + def mount_test_body(self, pitch_rc_neutral=1500, do_rate_tests=True, constrain_sysid_target=True, neutral_tol_deg=0): '''Test Camera/Antenna Mount - assumes a camera is set up and ready to go''' if True: # make sure we're getting gimbal device attitude status @@ -6438,10 +6438,18 @@ def mount_test_body(self, pitch_rc_neutral=1500, do_rate_tests=True, constrain_s # change mount to neutral mode (point forward, not stabilising) self.set_mount_mode(mavutil.mavlink.MAV_MOUNT_MODE_NEUTRAL) - # test pitch is not neutral to start with - mount_roll_deg, mount_pitch_deg, mount_yaw_deg, mount_yaw_is_absolute = self.get_mount_roll_pitch_yaw_deg() - if mount_roll_deg != 0 or mount_pitch_deg != 0 or mount_yaw_deg != 0: - raise NotAchievedException("Mount not neutral") + # wait for mount to report neutral angles (external MAVLink gimbals may + # have in-flight messages from the previous mode in the receive queue) + tstart = self.get_sim_time() + while True: + mount_roll_deg, mount_pitch_deg, mount_yaw_deg, mount_yaw_is_absolute = self.get_mount_roll_pitch_yaw_deg() + if (abs(mount_roll_deg) <= neutral_tol_deg and + abs(mount_pitch_deg) <= neutral_tol_deg and + abs(mount_yaw_deg) <= neutral_tol_deg): + break + if self.get_sim_time() - tstart > 5: + raise NotAchievedException("Mount not neutral (r=%f p=%f y=%f)" % ( + mount_roll_deg, mount_pitch_deg, mount_yaw_deg)) self.takeoff(30, mode='GUIDED') @@ -6456,7 +6464,9 @@ def mount_test_body(self, pitch_rc_neutral=1500, do_rate_tests=True, constrain_s # check gimbal is still not stabilising mount_roll_deg, mount_pitch_deg, mount_yaw_deg, mount_yaw_is_absolute = self.get_mount_roll_pitch_yaw_deg() - if mount_roll_deg != 0 or mount_pitch_deg != 0 or mount_yaw_deg != 0: + if (abs(mount_roll_deg) > neutral_tol_deg or + abs(mount_pitch_deg) > neutral_tol_deg or + abs(mount_yaw_deg) > neutral_tol_deg): raise NotAchievedException("Mount stabilising when not requested") # center RC tilt control and change mount to RC_TARGETING mode @@ -6757,6 +6767,160 @@ def MountSolo(self): constrain_sysid_target=False, # not everything constrains all angles ) + def mount_check_camera_information( + self, + expected_vendor: str, + expected_model: str, + expected_fw_version: int | None = None, + expected_cap_flags: int | None = None, + expected_gimbal_device_id: int | None = None, + timeout: int = 30, + ) -> None: + '''poll CAMERA_INFORMATION, retrying until vendor name, model name and + firmware version all match what the simulated backend sends. Vendor + name is used to detect driver initialisation; model name and firmware + version may arrive in separate handshake messages so we keep retrying + on empty/zero values rather than failing immediately.''' + self.assert_receive_message('GIMBAL_DEVICE_ATTITUDE_STATUS', timeout=10, very_verbose=True) + # param2 of MAV_CMD_REQUEST_MESSAGE selects a specific camera instance + # (1-based); 0 requests all cameras. Use the gimbal_device_id directly + # as the selector so each call retrieves exactly one camera's data. + poll_p2 = expected_gimbal_device_id if expected_gimbal_device_id is not None else 0 + tstart = self.get_sim_time() + while True: + if self.get_sim_time_cached() - tstart > timeout: + raise NotAchievedException( + "Never received complete CAMERA_INFORMATION for vendor=%s" % expected_vendor) + try: + m = self.poll_message('CAMERA_INFORMATION', timeout=5, p2=poll_p2) + except NotAchievedException: + continue + got_vendor = bytes(m.vendor_name).split(b'\x00')[0].decode('utf-8') + if not got_vendor: + continue # driver not initialised yet + if got_vendor != expected_vendor: + raise NotAchievedException( + "Unexpected vendor name: want=%s got=%s" % (expected_vendor, got_vendor)) + got_model = bytes(m.model_name).split(b'\x00')[0].decode('utf-8') + if not got_model: + continue # model name handshake not complete yet + if got_model != expected_model: + raise NotAchievedException( + "Unexpected model name: want=%s got=%s" % (expected_model, got_model)) + if expected_fw_version is not None: + if m.firmware_version == 0: + continue # firmware version handshake not complete yet + if m.firmware_version != expected_fw_version: + raise NotAchievedException( + "Unexpected firmware version: want=0x%08x got=0x%08x" % + (expected_fw_version, m.firmware_version)) + if expected_cap_flags is not None: + if m.flags != expected_cap_flags: + raise NotAchievedException( + "Unexpected cap flags: want=0x%08x got=0x%08x" % + (expected_cap_flags, m.flags)) + return + + def MountSiyiZT30(self): + '''test Siyi ZT30 gimbal using SIM_Siyi_ZT30 simulator''' + self.set_parameters({ + "MNT1_TYPE": 8, # Siyi + "CAM1_TYPE": 4, # Mount + "SERIAL5_PROTOCOL": 8, # gimbal + "RC6_OPTION": 213, # MOUNT1_PITCH + }) + self.customise_SITL_commandline(["--serial5=sim:siyi_zt30:"]) + # camera fw 1.2.3 from SIM_Siyi_ZT30: major=1, minor=2, patch=3 + # firmware_version = major | (minor<<8) | (patch<<16) = 0x030201 + # cap flags: CAPTURE_VIDEO | CAPTURE_IMAGE | HAS_BASIC_ZOOM | HAS_BASIC_FOCUS + self.mount_check_camera_information( + "Siyi", "ZT30", + expected_fw_version=0x030201, + expected_cap_flags=0xC3, + ) + + def MountTopotek(self): + '''test Topotek gimbal using SIM_Topotek simulator''' + self.set_parameters({ + "MNT1_TYPE": 12, # Topotek + "CAM1_TYPE": 4, # Mount + "SERIAL5_PROTOCOL": 8, # gimbal + "RC6_OPTION": 213, # MOUNT1_PITCH + }) + self.customise_SITL_commandline(["--serial5=sim:topotek:"]) + # version "1.0.0" from SIM_Topotek: major=1 | (minor=0)<<8 | (patch=0)<<16 = 1 + # cap flags: CAPTURE_VIDEO | CAPTURE_IMAGE | HAS_BASIC_ZOOM | HAS_BASIC_FOCUS | + # HAS_TRACKING_POINT | HAS_TRACKING_RECTANGLE + self.mount_check_camera_information( + "Topotek", "SIM_TP", + expected_fw_version=1, + expected_cap_flags=0x6C3, + ) + # pitch_rc_neutral=1818: with RC6 min=1000 max=2000 trim=1500 and + # default MNT1_PITCH_MIN=-90 / MNT1_PITCH_MAX=20, norm_input=0.636 + # maps to exactly 0 deg pitch. + # constrain_sysid_target=False: the driver does not yet clamp pitch to + # MNT1_PITCH_MAX before sending, so the 68-deg sysid test is skipped + # until AP_Mount_Topotek::send_target_angles enforces the limit. + self.mount_test_body(pitch_rc_neutral=1818, do_rate_tests=False, + constrain_sysid_target=False) + + def MountViewPro(self): + '''test Viewpro gimbal using SIM_Viewpro simulator''' + self.set_parameters({ + "MNT1_TYPE": 11, # Viewpro + "CAM1_TYPE": 4, # Mount + "SERIAL5_PROTOCOL": 8, # gimbal + "RC6_OPTION": 213, # MOUNT1_PITCH + }) + self.customise_SITL_commandline(["--serial5=sim:viewpro:"]) + # firmware "Sys20220101" from SIM_Viewpro: major=20, minor=22, patch=1 + # firmware_version = major | (minor<<8) | (patch<<16) = 0x011614 + # cap flags: CAPTURE_VIDEO | CAPTURE_IMAGE | HAS_BASIC_ZOOM | HAS_BASIC_FOCUS | + # HAS_TRACKING_POINT | HAS_TRACKING_RECTANGLE + self.mount_check_camera_information( + "Viewpro", "SIM_VP", + expected_fw_version=0x011614, + expected_cap_flags=0x6C3, + ) + # Viewpro roll uses 12-bit encoding (0..4095 = -90..+90 deg); the + # midpoint 2047.5 is not an integer so neutral 0 deg cannot be + # represented exactly. Max quantisation error is 180/(4095*2) ≈ 0.022 deg. + # pitch_rc_neutral=1818: with RC6 min=1000 max=2000 trim=1500 and + # default MNT1_PITCH_MIN=-90 / MNT1_PITCH_MAX=20, norm_input=0.636 + # maps to exactly 0 deg pitch. + # constrain_sysid_target=False: the driver does not yet clamp pitch to + # MNT1_PITCH_MAX before sending, so the 68-deg sysid test is skipped + # until AP_Mount_Viewpro::send_target_angles enforces the limit. + self.mount_test_body(pitch_rc_neutral=1818, neutral_tol_deg=0.05, + do_rate_tests=False, constrain_sysid_target=False) + + def MountAVTCM62(self): + '''test MAVLink (Gimbal Protocol v2) gimbal using SIM_AVT_CM62 simulator''' + self.set_parameters({ + "MNT1_TYPE": 6, # MAVLink + "CAM1_TYPE": 4, # Mount + "SERIAL5_PROTOCOL": 2, # MAVLink2 + "RC6_OPTION": 213, # MOUNT1_PITCH + }) + self.customise_SITL_commandline(["--serial5=sim:avt_cm62_gimbal:"]) + + def MountAVTCM62Dual(self): + '''test two simultaneous MAVLink (Gimbal Protocol v2) gimbals using + two SIM_AVT_CM62 simulators on separate serial ports''' + self.set_parameters({ + "MNT1_TYPE": 6, # MAVLink + "MNT2_TYPE": 6, # MAVLink + "CAM1_TYPE": 4, # Mount + "CAM2_TYPE": 4, # Mount + "SERIAL5_PROTOCOL": 2, # MAVLink2 + "SERIAL6_PROTOCOL": 2, # MAVLink2 + }) + self.customise_SITL_commandline([ + "--serial5=sim:avt_cm62_gimbal:", + "--serial6=sim:avt_cm62_gimbal:", + ]) + def assert_mount_rpy(self, r, p, y, tolerance=1): '''assert mount atttiude in degrees''' got_r, got_p, got_y, yaw_is_absolute = self.get_mount_roll_pitch_yaw_deg() @@ -16090,6 +16254,11 @@ def tests2b(self): # this block currently around 9.5mins here self.ScriptMountDriver, self.ScriptCopterPosOffsets, self.MountSolo, + self.MountSiyiZT30, + self.MountTopotek, + self.MountViewPro, + self.MountAVTCM62, + self.MountAVTCM62Dual, self.FlyMissionTwice, self.FlyMissionTwiceWithReset, self.MissionIndexValidity, diff --git a/Tools/autotest/vehicle_test_suite.py b/Tools/autotest/vehicle_test_suite.py index 0e912db1653de4..8c4b08eb7fe4f0 100644 --- a/Tools/autotest/vehicle_test_suite.py +++ b/Tools/autotest/vehicle_test_suite.py @@ -11394,7 +11394,7 @@ def test_set_message_interval_basic(self): if ex is not None: raise ex - def send_poll_message(self, message_id, target_sysid=None, target_compid=None, quiet=False, mav=None): + def send_poll_message(self, message_id, target_sysid=None, target_compid=None, quiet=False, mav=None, p2=0): if mav is None: mav = self.mav if isinstance(message_id, str): @@ -11402,13 +11402,14 @@ def send_poll_message(self, message_id, target_sysid=None, target_compid=None, q self.send_cmd( mavutil.mavlink.MAV_CMD_REQUEST_MESSAGE, p1=message_id, + p2=p2, target_sysid=target_sysid, target_compid=target_compid, quiet=quiet, mav=mav, ) - def poll_message(self, message_id, timeout=10, quiet=False, mav=None, target_sysid=None, target_compid=None): + def poll_message(self, message_id, timeout=10, quiet=False, mav=None, target_sysid=None, target_compid=None, p2=0): if mav is None: mav = self.mav if target_sysid is None: @@ -11418,7 +11419,7 @@ def poll_message(self, message_id, timeout=10, quiet=False, mav=None, target_sys if isinstance(message_id, str): message_id = eval("mavutil.mavlink.MAVLINK_MSG_ID_%s" % message_id) tstart = self.get_sim_time() # required for timeout in run_cmd_get_ack to work - self.send_poll_message(message_id, quiet=quiet, mav=mav, target_sysid=target_sysid, target_compid=target_compid) + self.send_poll_message(message_id, quiet=quiet, mav=mav, target_sysid=target_sysid, target_compid=target_compid, p2=p2) self.run_cmd_get_ack( mavutil.mavlink.MAV_CMD_REQUEST_MESSAGE, mavutil.mavlink.MAV_RESULT_ACCEPTED, From fad17f884b06d5a35d57b4448e3ad01f0a993fc3 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Fri, 20 Mar 2026 12:35:38 +1100 Subject: [PATCH 73/99] AP_Mount: use 'static constexpr' in place of 'static const' saves bytes. Ram and flash --- libraries/AP_Mount/AP_Mount_Siyi.h | 2 +- libraries/AP_Mount/AP_Mount_Viewpro.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Siyi.h b/libraries/AP_Mount/AP_Mount_Siyi.h index 26fe0a98976114..dc45c4b6aa957e 100644 --- a/libraries/AP_Mount/AP_Mount_Siyi.h +++ b/libraries/AP_Mount/AP_Mount_Siyi.h @@ -324,7 +324,7 @@ class AP_Mount_Siyi : public AP_Mount_Backend_Serial // buffer holding bytes from latest packet. This is only used to calculate the crc uint8_t _msg_buff[AP_MOUNT_SIYI_PACKETLEN_MAX]; uint8_t _msg_buff_len; - const uint8_t _msg_buff_data_start = 8; // data starts at this byte of _msg_buff + static constexpr uint8_t _msg_buff_data_start = 8; // data starts at this byte of _msg_buff // parser state and unpacked fields struct PACKED { diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.h b/libraries/AP_Mount/AP_Mount_Viewpro.h index 45d55a69712da4..242f7ce0275e6a 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.h +++ b/libraries/AP_Mount/AP_Mount_Viewpro.h @@ -384,7 +384,7 @@ class AP_Mount_Viewpro : public AP_Mount_Backend_Serial // internal variables uint8_t _msg_buff[AP_MOUNT_VIEWPRO_PACKETLEN_MAX]; // buffer holding latest bytes from gimbal uint8_t _msg_buff_len; // number of bytes held in msg buff - const uint8_t _msg_buff_data_start = 2; // data starts at this byte of _msg_buff + static constexpr uint8_t _msg_buff_data_start = 2; // data starts at this byte of _msg_buff // parser state and unpacked fields struct { From 4c12d4f44d64019150668da0f25b8396cf78b327 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Fri, 20 Mar 2026 13:02:19 +1100 Subject: [PATCH 74/99] AP_Mount: Viewpro: replace strncpy with memcpy to fix -Wstringop-truncation Making _msg_buff_data_start static constexpr allowed the compiler to constant-fold the source pointer in the strncpy call, exposing a -Wstringop-truncation warning: the source buffer extends 62 bytes but only 10 are copied. The truncation is intentional (bounding to the destination size), so replace with memcpy + explicit null terminator to make the intent clear and silence the warning. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount_Viewpro.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.cpp b/libraries/AP_Mount/AP_Mount_Viewpro.cpp index d8f752ffd6574c..9437ae277265c1 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.cpp +++ b/libraries/AP_Mount/AP_Mount_Viewpro.cpp @@ -237,7 +237,8 @@ void AP_Mount_Viewpro::process_packet() } case CommConfigCmd::QUERY_MODEL: // gimbal model, length is 10 bytes - strncpy((char *)_model_name, (const char *)&_msg_buff[_msg_buff_data_start+1], sizeof(_model_name)-1); + memset(_model_name, '\0', sizeof(_model_name)); + memcpy(_model_name, &_msg_buff[_msg_buff_data_start+1], sizeof(_model_name)-1); _got_model_name = true; GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_text_prefix, (const char*)_model_name); break; From a636a057cf258307ee723f6eeb1942c9134e9bca Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Fri, 20 Mar 2026 13:05:00 +1100 Subject: [PATCH 75/99] AP_Mount: use char[] rather than uint8_t[] for gimbal model name The _model_name member in Viewpro and Topotek drivers holds a null-terminated ASCII string, so char[] is the correct type. Using uint8_t[] required pervasive (char*) and (const char*) casts at every point of use. Switch to char[] and remove the now-redundant casts, adding (const uint8_t*) only where the MAVLink API requires it. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount_Topotek.cpp | 4 ++-- libraries/AP_Mount/AP_Mount_Topotek.h | 2 +- libraries/AP_Mount/AP_Mount_Viewpro.cpp | 6 +++--- libraries/AP_Mount/AP_Mount_Viewpro.h | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Topotek.cpp b/libraries/AP_Mount/AP_Mount_Topotek.cpp index 42079c24114ffd..f802f08e6089c3 100644 --- a/libraries/AP_Mount/AP_Mount_Topotek.cpp +++ b/libraries/AP_Mount/AP_Mount_Topotek.cpp @@ -459,7 +459,7 @@ void AP_Mount_Topotek::send_camera_information(mavlink_channel_t chan) const // copy model name if available if (_got_gimbal_model_name) { - strncpy((char*)model_name, (const char*)_model_name, ARRAY_SIZE(model_name)); + strncpy((char*)model_name, _model_name, ARRAY_SIZE(model_name)); } // capability flags @@ -1040,7 +1040,7 @@ void AP_Mount_Topotek::gimbal_version_analyse() // gimbal model name message analysis void AP_Mount_Topotek::gimbal_model_name_analyse() { - strncpy((char *)_model_name, (const char *)_msg_buff + 10, char_to_hex(_msg_buff[5])); + strncpy(_model_name, (const char *)_msg_buff + 10, char_to_hex(_msg_buff[5])); // display gimbal model name to user GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_message_prefix, _model_name); diff --git a/libraries/AP_Mount/AP_Mount_Topotek.h b/libraries/AP_Mount/AP_Mount_Topotek.h index eebcee96645feb..64e618e5e40962 100644 --- a/libraries/AP_Mount/AP_Mount_Topotek.h +++ b/libraries/AP_Mount/AP_Mount_Topotek.h @@ -254,7 +254,7 @@ class AP_Mount_Topotek : public AP_Mount_Backend_Serial bool _got_gimbal_model_name; // true if gimbal's model name has been received bool _last_zoom_stop; // true if zoom has been stopped (used to re-send in order to handle lost packets) bool _last_focus_stop; // true if focus has been stopped (used to re-sent in order to handle lost packets) - uint8_t _model_name[16]; // gimbal model name + char _model_name[16]; // gimbal model name uint8_t _sent_time_count; // count of current time messages sent to gimbal uint32_t _firmware_ver; // firmware version Vector3f _current_angle_rad; // current angles in radians received from gimbal (x=roll, y=pitch, z=yaw) diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.cpp b/libraries/AP_Mount/AP_Mount_Viewpro.cpp index 9437ae277265c1..d37d3af347bbd1 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.cpp +++ b/libraries/AP_Mount/AP_Mount_Viewpro.cpp @@ -240,7 +240,7 @@ void AP_Mount_Viewpro::process_packet() memset(_model_name, '\0', sizeof(_model_name)); memcpy(_model_name, &_msg_buff[_msg_buff_data_start+1], sizeof(_model_name)-1); _got_model_name = true; - GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_text_prefix, (const char*)_model_name); + GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_text_prefix, _model_name); break; default: // unsupported control command @@ -870,7 +870,7 @@ void AP_Mount_Viewpro::send_camera_information(mavlink_channel_t chan) const static const uint8_t vendor_name[32] = "Viewpro"; uint8_t model_name[32] {}; if (_got_model_name) { - strncpy((char *)model_name, (const char*)_model_name, MIN(sizeof(model_name), sizeof(_model_name))); + strncpy((char *)model_name, _model_name, MIN(sizeof(model_name), sizeof(_model_name))); } const char cam_definition_uri[140] {}; @@ -887,7 +887,7 @@ void AP_Mount_Viewpro::send_camera_information(mavlink_channel_t chan) const chan, AP_HAL::millis(), // time_boot_ms vendor_name, // vendor_name uint8_t[32] - _model_name, // model_name uint8_t[32] + (const uint8_t*)_model_name, // model_name uint8_t[32] _firmware_version, // firmware version uint32_t NaNf, // sensor_size_h float (mm) NaNf, // sensor_size_v float (mm) diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.h b/libraries/AP_Mount/AP_Mount_Viewpro.h index 242f7ce0275e6a..5aa43b5eb81fdc 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.h +++ b/libraries/AP_Mount/AP_Mount_Viewpro.h @@ -407,7 +407,7 @@ class AP_Mount_Viewpro : public AP_Mount_Backend_Serial float _zoom_times; // zoom times received from gimbal uint32_t _firmware_version; // firmware version from gimbal bool _got_firmware_version; // true once we have received the firmware version - uint8_t _model_name[11] {}; // model name received from gimbal + char _model_name[11] {}; // model name received from gimbal bool _got_model_name; // true once we have received model name float _rangefinder_dist_m; // latest rangefinder distance (in meters) }; From da7172dae51b4e883843cf8960ca29a121689dbf Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sun, 22 Mar 2026 11:12:21 +1100 Subject: [PATCH 76/99] AP_Mount: avoid buffer-overread sending Viewpro camera_information we prepared a buffer on the stack of the correct length but didn't use it. The mavlink bindings memcpy out of this buffer according to the spec-defined length, so a buffer of at least that length should be supplied --- libraries/AP_Mount/AP_Mount_Viewpro.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.cpp b/libraries/AP_Mount/AP_Mount_Viewpro.cpp index d37d3af347bbd1..7c11ee54b96b71 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.cpp +++ b/libraries/AP_Mount/AP_Mount_Viewpro.cpp @@ -887,7 +887,7 @@ void AP_Mount_Viewpro::send_camera_information(mavlink_channel_t chan) const chan, AP_HAL::millis(), // time_boot_ms vendor_name, // vendor_name uint8_t[32] - (const uint8_t*)_model_name, // model_name uint8_t[32] + model_name, // model_name uint8_t[32] _firmware_version, // firmware version uint32_t NaNf, // sensor_size_h float (mm) NaNf, // sensor_size_v float (mm) From 92452505f5f47117bea8d8f6f596875eeb6fe02b Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sun, 22 Mar 2026 11:32:46 +1100 Subject: [PATCH 77/99] AP_Mount: use MAVLink field len defines and strncpy_noterm for camera_information strings Replace hardcoded buffer sizes (32, 140) with MAVLINK_MSG_CAMERA_INFORMATION_FIELD_*_LEN defines in Siyi, Viewpro, Topotek, and Xacti backends. Use strncpy_noterm (which omits the null terminator only when the string fills the field, as MAVLink allows) in place of strncpy for copies into vendor_name and model_name send buffers. Also cap the Topotek gimbal_model_name_analyse strncpy to sizeof(_model_name)-1 to ensure null termination, and document _model_name as always null-terminated in both Viewpro and Topotek headers. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount_Siyi.cpp | 8 ++++---- libraries/AP_Mount/AP_Mount_Topotek.cpp | 15 ++++++++++----- libraries/AP_Mount/AP_Mount_Topotek.h | 2 +- libraries/AP_Mount/AP_Mount_Viewpro.cpp | 8 ++++---- libraries/AP_Mount/AP_Mount_Viewpro.h | 2 +- libraries/AP_Mount/AP_Mount_Xacti.cpp | 6 +++--- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Siyi.cpp b/libraries/AP_Mount/AP_Mount_Siyi.cpp index 74578d272173a6..8a40fa8172f868 100644 --- a/libraries/AP_Mount/AP_Mount_Siyi.cpp +++ b/libraries/AP_Mount/AP_Mount_Siyi.cpp @@ -993,13 +993,13 @@ void AP_Mount_Siyi::send_camera_information(mavlink_channel_t chan) const return; } - static const uint8_t vendor_name[32] = "Siyi"; - static uint8_t model_name[32] {}; + static const uint8_t vendor_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_VENDOR_NAME_LEN] { "Siyi" }; + static uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; const uint32_t fw_version = _fw_version.camera.major | (_fw_version.camera.minor << 8) | (_fw_version.camera.patch << 16); - const char cam_definition_uri[140] {}; + const char cam_definition_uri[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_CAM_DEFINITION_URI_LEN] {}; // copy model name - strncpy((char *)model_name, get_model_name(), sizeof(model_name)-1); + strncpy_noterm((char *)model_name, get_model_name(), sizeof(model_name)); // focal length // To-Do: check these values are correct for A2, ZR30, ZT30 diff --git a/libraries/AP_Mount/AP_Mount_Topotek.cpp b/libraries/AP_Mount/AP_Mount_Topotek.cpp index f802f08e6089c3..63c9422e8ec999 100644 --- a/libraries/AP_Mount/AP_Mount_Topotek.cpp +++ b/libraries/AP_Mount/AP_Mount_Topotek.cpp @@ -453,13 +453,13 @@ void AP_Mount_Topotek::send_camera_information(mavlink_channel_t chan) const return; } - static const uint8_t vendor_name[32] = "Topotek"; - static uint8_t model_name[32] {}; - const char cam_definition_uri[140] {}; + static const uint8_t vendor_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_VENDOR_NAME_LEN] { "Topotek" }; + static uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; + const char cam_definition_uri[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_CAM_DEFINITION_URI_LEN] {}; // copy model name if available if (_got_gimbal_model_name) { - strncpy((char*)model_name, _model_name, ARRAY_SIZE(model_name)); + strncpy_noterm((char*)model_name, _model_name, ARRAY_SIZE(model_name)); } // capability flags @@ -1040,7 +1040,12 @@ void AP_Mount_Topotek::gimbal_version_analyse() // gimbal model name message analysis void AP_Mount_Topotek::gimbal_model_name_analyse() { - strncpy(_model_name, (const char *)_msg_buff + 10, char_to_hex(_msg_buff[5])); + const auto len = char_to_hex(_msg_buff[5]); + if (len == 255) { + // flag value indicating invalid character + return; + } + strncpy((char *)_model_name, (const char *)_msg_buff + 10, len); // display gimbal model name to user GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_message_prefix, _model_name); diff --git a/libraries/AP_Mount/AP_Mount_Topotek.h b/libraries/AP_Mount/AP_Mount_Topotek.h index 64e618e5e40962..c7e235e014ece2 100644 --- a/libraries/AP_Mount/AP_Mount_Topotek.h +++ b/libraries/AP_Mount/AP_Mount_Topotek.h @@ -254,7 +254,7 @@ class AP_Mount_Topotek : public AP_Mount_Backend_Serial bool _got_gimbal_model_name; // true if gimbal's model name has been received bool _last_zoom_stop; // true if zoom has been stopped (used to re-send in order to handle lost packets) bool _last_focus_stop; // true if focus has been stopped (used to re-sent in order to handle lost packets) - char _model_name[16]; // gimbal model name + char _model_name[16]; // gimbal model name, always null-terminated uint8_t _sent_time_count; // count of current time messages sent to gimbal uint32_t _firmware_ver; // firmware version Vector3f _current_angle_rad; // current angles in radians received from gimbal (x=roll, y=pitch, z=yaw) diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.cpp b/libraries/AP_Mount/AP_Mount_Viewpro.cpp index 7c11ee54b96b71..6603b2e7c6042b 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.cpp +++ b/libraries/AP_Mount/AP_Mount_Viewpro.cpp @@ -867,12 +867,12 @@ void AP_Mount_Viewpro::send_camera_information(mavlink_channel_t chan) const return; } - static const uint8_t vendor_name[32] = "Viewpro"; - uint8_t model_name[32] {}; + static const uint8_t vendor_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_VENDOR_NAME_LEN] { "Viewpro" }; + uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; if (_got_model_name) { - strncpy((char *)model_name, _model_name, MIN(sizeof(model_name), sizeof(_model_name))); + strncpy_noterm((char *)model_name, _model_name, sizeof(model_name)); } - const char cam_definition_uri[140] {}; + const char cam_definition_uri[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_CAM_DEFINITION_URI_LEN] {}; // capability flags const uint32_t flags = CAMERA_CAP_FLAGS_CAPTURE_VIDEO | diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.h b/libraries/AP_Mount/AP_Mount_Viewpro.h index 5aa43b5eb81fdc..7619843aa978f6 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.h +++ b/libraries/AP_Mount/AP_Mount_Viewpro.h @@ -407,7 +407,7 @@ class AP_Mount_Viewpro : public AP_Mount_Backend_Serial float _zoom_times; // zoom times received from gimbal uint32_t _firmware_version; // firmware version from gimbal bool _got_firmware_version; // true once we have received the firmware version - char _model_name[11] {}; // model name received from gimbal + char _model_name[11] {}; // model name received from gimbal, always null-terminated bool _got_model_name; // true once we have received model name float _rangefinder_dist_m; // latest rangefinder distance (in meters) }; diff --git a/libraries/AP_Mount/AP_Mount_Xacti.cpp b/libraries/AP_Mount/AP_Mount_Xacti.cpp index fdd2cfcede3bf0..8346ff53e42d14 100644 --- a/libraries/AP_Mount/AP_Mount_Xacti.cpp +++ b/libraries/AP_Mount/AP_Mount_Xacti.cpp @@ -289,9 +289,9 @@ void AP_Mount_Xacti::send_camera_information(mavlink_channel_t chan) const return; } - static const uint8_t vendor_name[32] = "Xacti"; - static uint8_t model_name[32] = "CX-GB100"; - const char cam_definition_uri[140] {}; + static const uint8_t vendor_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_VENDOR_NAME_LEN] { "Xacti" }; + static uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] { "CX-GB100" }; + const char cam_definition_uri[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_CAM_DEFINITION_URI_LEN] {}; // capability flags const uint32_t flags = CAMERA_CAP_FLAGS_CAPTURE_VIDEO | From 9932c9b8d4b3b66248c594e98c1be8c442c9433a Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sun, 22 Mar 2026 11:41:06 +1100 Subject: [PATCH 78/99] AP_Mount: remove static from model_name buffer in send_camera_information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static qualifier on model_name in Siyi and Topotek was wrong: static locals are shared across all class instances, so two gimbals of the same type would write into the same buffer. The {} initialiser was also misleading — on a static local it fires only once at program start, not on each call. Use a plain stack-allocated buffer (matching Viewpro) so it is correctly zero-initialised on every call and is instance-safe. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount_Siyi.cpp | 2 +- libraries/AP_Mount/AP_Mount_Topotek.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Siyi.cpp b/libraries/AP_Mount/AP_Mount_Siyi.cpp index 8a40fa8172f868..26d1ea4863872e 100644 --- a/libraries/AP_Mount/AP_Mount_Siyi.cpp +++ b/libraries/AP_Mount/AP_Mount_Siyi.cpp @@ -994,7 +994,7 @@ void AP_Mount_Siyi::send_camera_information(mavlink_channel_t chan) const } static const uint8_t vendor_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_VENDOR_NAME_LEN] { "Siyi" }; - static uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; + uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; const uint32_t fw_version = _fw_version.camera.major | (_fw_version.camera.minor << 8) | (_fw_version.camera.patch << 16); const char cam_definition_uri[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_CAM_DEFINITION_URI_LEN] {}; diff --git a/libraries/AP_Mount/AP_Mount_Topotek.cpp b/libraries/AP_Mount/AP_Mount_Topotek.cpp index 63c9422e8ec999..451db21137ab31 100644 --- a/libraries/AP_Mount/AP_Mount_Topotek.cpp +++ b/libraries/AP_Mount/AP_Mount_Topotek.cpp @@ -454,7 +454,7 @@ void AP_Mount_Topotek::send_camera_information(mavlink_channel_t chan) const } static const uint8_t vendor_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_VENDOR_NAME_LEN] { "Topotek" }; - static uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; + uint8_t model_name[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_MODEL_NAME_LEN] {}; const char cam_definition_uri[MAVLINK_MSG_CAMERA_INFORMATION_FIELD_CAM_DEFINITION_URI_LEN] {}; // copy model name if available From 20ac18da59f4eaa93c3df077d9bb8fdee94ad142 Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sat, 28 Mar 2026 15:49:53 +1100 Subject: [PATCH 79/99] AP_Mount: validate wire-data lengths before using as buffer/loop bounds Topotek gimbal_version_analyse: data_buf_len comes from char_to_hex(_msg_buff[5]), which returns 255 for an invalid hex byte. Return early on that value to reject the malformed packet; a valid length field is always a hex digit. Viewpro QUERY_MODEL handler: cap the memcpy to MIN(buffer, data_bytes_received-1) so a short response cannot cause stale bytes from a previous packet to appear in _model_name. Co-Authored-By: Claude Sonnet 4.6 --- libraries/AP_Mount/AP_Mount_Topotek.cpp | 9 ++++++++- libraries/AP_Mount/AP_Mount_Viewpro.cpp | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/AP_Mount/AP_Mount_Topotek.cpp b/libraries/AP_Mount/AP_Mount_Topotek.cpp index 451db21137ab31..7cf00a3c37fa5e 100644 --- a/libraries/AP_Mount/AP_Mount_Topotek.cpp +++ b/libraries/AP_Mount/AP_Mount_Topotek.cpp @@ -991,7 +991,13 @@ void AP_Mount_Topotek::gimbal_version_analyse() // extract firmware version // the version can be in the format "1.2.3" or "123" + // _msg_buff[5] holds the ASCII hex-encoded data length byte from the packet header; + // char_to_hex returns 255 for an invalid (non-hex) character, which we treat as a + // malformed packet rather than capping, since a valid length field is always a hex digit const uint8_t data_buf_len = char_to_hex(_msg_buff[5]); + if (data_buf_len == 255) { + return; + } // check for "." bool contains_period = false; @@ -1045,7 +1051,8 @@ void AP_Mount_Topotek::gimbal_model_name_analyse() // flag value indicating invalid character return; } - strncpy((char *)_model_name, (const char *)_msg_buff + 10, len); + memset(_model_name, 0, sizeof(_model_name)); + memcpy(_model_name, _msg_buff + 10, MIN((uint8_t)(sizeof(_model_name)-1), len)); // display gimbal model name to user GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_message_prefix, _model_name); diff --git a/libraries/AP_Mount/AP_Mount_Viewpro.cpp b/libraries/AP_Mount/AP_Mount_Viewpro.cpp index 6603b2e7c6042b..f1e60e445ebdfc 100644 --- a/libraries/AP_Mount/AP_Mount_Viewpro.cpp +++ b/libraries/AP_Mount/AP_Mount_Viewpro.cpp @@ -236,9 +236,12 @@ void AP_Mount_Viewpro::process_packet() break; } case CommConfigCmd::QUERY_MODEL: + if (_parsed_msg.data_bytes_received == 0) { + break; + } // gimbal model, length is 10 bytes memset(_model_name, '\0', sizeof(_model_name)); - memcpy(_model_name, &_msg_buff[_msg_buff_data_start+1], sizeof(_model_name)-1); + memcpy(_model_name, &_msg_buff[_msg_buff_data_start+1], MIN(sizeof(_model_name)-1, (size_t)(_parsed_msg.data_bytes_received-1))); _got_model_name = true; GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_text_prefix, _model_name); break; From c868e84a23e112d800585946707d5de0f7e4bf9b Mon Sep 17 00:00:00 2001 From: Peter Barker Date: Sat, 4 Apr 2026 11:30:58 +1100 Subject: [PATCH 80/99] SITL: correct response to Viewpro model query field is 10 bytes according to data sheet --- libraries/SITL/SIM_Viewpro.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/SITL/SIM_Viewpro.cpp b/libraries/SITL/SIM_Viewpro.cpp index c0f808d4fa665c..2984bcccca2627 100644 --- a/libraries/SITL/SIM_Viewpro.cpp +++ b/libraries/SITL/SIM_Viewpro.cpp @@ -311,6 +311,7 @@ void Viewpro::send_v_response(CommConfigCmd cmd) break; case CommConfigCmd::QUERY_MODEL: + // datasheet specifies a 10-byte model name field in the response databuff[1] = (uint8_t)cmd; databuff[2] = 'S'; databuff[3] = 'I'; @@ -318,7 +319,11 @@ void Viewpro::send_v_response(CommConfigCmd cmd) databuff[5] = '_'; databuff[6] = 'V'; databuff[7] = 'P'; - total_len = 8; + databuff[8] = 0; + databuff[9] = 0; + databuff[10] = 0; + databuff[11] = 0; + total_len = 12; break; default: From e56b6dd891b249615095ab66acd167b4427bb654 Mon Sep 17 00:00:00 2001 From: Oleksandr Ivashyna Date: Sun, 22 Feb 2026 11:16:13 +0200 Subject: [PATCH 81/99] AP_HAL_ChibiOS: update hwdef.dat for Atlas-Control - Add UART8 (SBUS/DSM port) to SERIAL_ORDER - Add PE0/PE1 pin definitions for UART8 RX/TX - Remove BIDIR from PWM AUX channels 1,3,5,7 --- .../AP_HAL_ChibiOS/hwdef/Atlas-Control/hwdef.dat | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libraries/AP_HAL_ChibiOS/hwdef/Atlas-Control/hwdef.dat b/libraries/AP_HAL_ChibiOS/hwdef/Atlas-Control/hwdef.dat index cc7b34d33762ab..da5ec7fba492b5 100644 --- a/libraries/AP_HAL_ChibiOS/hwdef/Atlas-Control/hwdef.dat +++ b/libraries/AP_HAL_ChibiOS/hwdef/Atlas-Control/hwdef.dat @@ -19,7 +19,7 @@ STM32_ST_USE_TIMER 2 FLASH_RESERVE_START_KB 128 # order of UARTs (and USB) -SERIAL_ORDER OTG1 USART2 USART6 USART1 UART4 EMPTY UART7 OTG2 +SERIAL_ORDER OTG1 USART2 USART6 USART1 UART4 UART8 UART7 OTG2 # now we define the pins that USB is connected on PA11 OTG_FS_DM OTG1 @@ -148,14 +148,18 @@ PG8 USART6_RTS USART6 PF6 UART7_RX UART7 NODMA PE8 UART7_TX UART7 NODMA +# SBUS, DSM port +PE0 UART8_RX UART8 +PE1 UART8_TX UART8 + # PWM AUX channels -PH10 TIM5_CH1 TIM5 PWM(1) GPIO(50) BIDIR +PH10 TIM5_CH1 TIM5 PWM(1) GPIO(50) PH11 TIM5_CH2 TIM5 PWM(2) GPIO(51) -PH12 TIM5_CH3 TIM5 PWM(3) GPIO(52) BIDIR +PH12 TIM5_CH3 TIM5 PWM(3) GPIO(52) PI0 TIM5_CH4 TIM5 PWM(4) GPIO(53) -PD12 TIM4_CH1 TIM4 PWM(5) GPIO(54) BIDIR +PD12 TIM4_CH1 TIM4 PWM(5) GPIO(54) PD13 TIM4_CH2 TIM4 PWM(6) GPIO(55) -PD14 TIM4_CH3 TIM4 PWM(7) GPIO(56) BIDIR +PD14 TIM4_CH3 TIM4 PWM(7) GPIO(56) PD15 TIM4_CH4 TIM4 PWM(8) GPIO(57) PE9 TIM1_CH1 TIM1 PWM(9) GPIO(58) From 36c5feeba7b62f21a3d11c00e2e73ee26c6707a2 Mon Sep 17 00:00:00 2001 From: Randy Mackay Date: Wed, 1 Apr 2026 14:08:09 +0900 Subject: [PATCH 82/99] AC_AttitudeControl: reduce INPUT_TC default multicopters reduced from 0.15 to 0.1 planes reduced from 0.2 to 0.15 --- libraries/AC_AttitudeControl/AC_AttitudeControl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/AC_AttitudeControl/AC_AttitudeControl.cpp b/libraries/AC_AttitudeControl/AC_AttitudeControl.cpp index b2cccb99119e6d..d0668767bd751c 100644 --- a/libraries/AC_AttitudeControl/AC_AttitudeControl.cpp +++ b/libraries/AC_AttitudeControl/AC_AttitudeControl.cpp @@ -8,12 +8,12 @@ extern const AP_HAL::HAL& hal; #if APM_BUILD_TYPE(APM_BUILD_ArduPlane) // default gains for Plane - # define AC_ATTITUDE_CONTROL_INPUT_TC_DEFAULT 0.2f // Soft + # define AC_ATTITUDE_CONTROL_INPUT_TC_DEFAULT 0.15f // Medium #define AC_ATTITUDE_CONTROL_ANGLE_LIMIT_MIN 5.0 // Min lean angle so that vehicle can maintain limited control #define AC_ATTITUDE_CONTROL_AFTER_RATE_CONTROL 0 #else // default gains for Copter and Sub - # define AC_ATTITUDE_CONTROL_INPUT_TC_DEFAULT 0.15f // Medium + # define AC_ATTITUDE_CONTROL_INPUT_TC_DEFAULT 0.10f // Crisp #define AC_ATTITUDE_CONTROL_ANGLE_LIMIT_MIN 10.0 // Min lean angle so that vehicle can maintain limited control #define AC_ATTITUDE_CONTROL_AFTER_RATE_CONTROL 1 #endif From 10a79320352decbbba21aa6f82f1b4a001ca88e1 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 6 Feb 2026 19:54:27 -0600 Subject: [PATCH 83/99] AP_NavEKF3: add missing constraints to FuseDragForces Unsure if this is a major issue since the next EKF loop would likely call some fusion function which would do the same things. But better safe than sorry, and consistency is good. --- libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp b/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp index 165d79c78dbb79..29fd29f38efdc7 100644 --- a/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp +++ b/libraries/AP_NavEKF3/AP_NavEKF3_AirDataFusion.cpp @@ -656,6 +656,10 @@ void NavEKF3_core::FuseDragForces() P[i][j] = P[i][j] - KHP[i][j]; } } + + // force the covariance matrix to be symmetrical and limit the variances to prevent ill-conditioning. + ForceSymmetry(); + ConstrainVariances(); } // record time of successful fusion From 1699f7dfc8eacbb68ac5bbfad922ae54f2623a71 Mon Sep 17 00:00:00 2001 From: Randy Mackay Date: Mon, 6 Apr 2026 16:25:23 +0900 Subject: [PATCH 84/99] Tools: extract features fix for batt watt max --- Tools/scripts/extract_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/scripts/extract_features.py b/Tools/scripts/extract_features.py index 17e59812b80ada..ff2daac996f80a 100755 --- a/Tools/scripts/extract_features.py +++ b/Tools/scripts/extract_features.py @@ -101,7 +101,7 @@ def __init__(self, filename, nm="arm-none-eabi-nm", strings="strings"): ('AP_BATTERY_{type}_ENABLED', r'AP_BattMonitor_(?P.*)::init\b',), ('AP_BATTERY_ESC_TELEM_OUTBOUND_ENABLED', r'AP_BattMonitor_Backend::update_esc_telem_outbound\b',), - ('AP_BATTERY_WATT_MAX_ENABLED', 'Plane::throttle_watt_limiter',), + ('AP_BATTERY_WATT_MAX_ENABLED', r'Plane::throttle_watt_limiter|AP_MotorsUGV::get_power_limit_max_throttle',), ('HAL_MOUNT_ENABLED', 'AP_Mount::AP_Mount',), ('HAL_MOUNT_{type}_ENABLED', r'AP_Mount_(?P.*)::update\b',), From 18c70699fef7e0e0541230792f42b62ef08a12f8 Mon Sep 17 00:00:00 2001 From: Randy Mackay Date: Mon, 6 Apr 2026 16:25:52 +0900 Subject: [PATCH 85/99] Tools: test-build-option fix for battery watt limiting --- Tools/autotest/test_build_options.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tools/autotest/test_build_options.py b/Tools/autotest/test_build_options.py index 3c19164b4fc104..f1b24609c688ab 100755 --- a/Tools/autotest/test_build_options.py +++ b/Tools/autotest/test_build_options.py @@ -324,7 +324,6 @@ def define_is_whitelisted_for_feature_in_code(self, target, define): feature_define_whitelist.add('QAUTOTUNE_ENABLED') feature_define_whitelist.add('AP_PLANE_OFFBOARD_GUIDED_SLEW_ENABLED') feature_define_whitelist.add('HAL_QUADPLANE_ENABLED') - feature_define_whitelist.add('AP_BATTERY_WATT_MAX_ENABLED') feature_define_whitelist.add('MODE_AUTOLAND_ENABLED') feature_define_whitelist.add('AP_PLANE_GLIDER_PULLUP_ENABLED') feature_define_whitelist.add('AP_QUICKTUNE_ENABLED') @@ -343,8 +342,12 @@ def define_is_whitelisted_for_feature_in_code(self, target, define): # required for these bindings: feature_define_whitelist.add('AP_SCRIPTING_BINDING_MOTORS_ENABLED') + if target.lower() not in ["plane", "rover"]: + # only Plane and Rover support battery watt limiting + feature_define_whitelist.add('AP_BATTERY_WATT_MAX_ENABLED') + if target.lower() not in ["rover", "copter"]: - # only Plane and Copter instantiate Beacon + # only Rover and Copter instantiate Beacon feature_define_whitelist.add('AP_BEACON_ENABLED') if target.lower() != "rover": From 367ebc6acb8ef6b64029a5ad646ec33096089021 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Sat, 21 Mar 2026 07:43:02 +0000 Subject: [PATCH 86/99] hwdef: MPU6000 version of TBS_LUCID_H7 --- libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/README.md | 2 +- libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/hwdef.dat | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/README.md b/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/README.md index e8a1c08fb67701..1d54699a875566 100644 --- a/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/README.md +++ b/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/README.md @@ -5,7 +5,7 @@ The TBS LUCID H7 is a flight controller produced by [TBS](https://www.team-black ## Features - MCU - STM32H743 32-bit processor running at 480 MHz -- IMU - Dual ICM42688 +- IMU - Dual ICM42688 (Dual MPU6000 on some v2 boards) - Barometer - DPS310 - OSD - AT7456E - microSD card slot diff --git a/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/hwdef.dat b/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/hwdef.dat index 1bc535c8431fc4..7369a9f0ce76e8 100644 --- a/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/hwdef.dat +++ b/libraries/AP_HAL_ChibiOS/hwdef/TBS_LUCID_H7/hwdef.dat @@ -193,6 +193,8 @@ STORAGE_FLASH_PAGE 14 # spi devices SPIDEV imu1 SPI1 DEVID1 IMU1_CS MODE3 2*MHZ 16*MHZ # Clock is 100Mhz so highest clock <= 24Mhz is 100Mhz/8 SPIDEV imu2 SPI4 DEVID1 IMU2_CS MODE3 2*MHZ 16*MHZ # Clock is 100Mhz so highest clock <= 24Mhz is 100Mhz/8 +SPIDEV mpu6000_1 SPI1 DEVID1 IMU1_CS MODE3 1*MHZ 4*MHZ +SPIDEV mpu6000_2 SPI4 DEVID1 IMU2_CS MODE3 1*MHZ 4*MHZ SPIDEV osd SPI2 DEVID4 MAX7456_CS MODE0 10*MHZ 10*MHZ # no built-in compass, but probe the i2c bus for all possible @@ -204,6 +206,8 @@ define HAL_COMPASS_AUTO_ROT_DEFAULT 2 IMU Invensensev3 SPI:imu1 ROTATION_YAW_270 IMU Invensensev3 SPI:imu2 ROTATION_YAW_180 +IMU Invensense SPI:mpu6000_1 ROTATION_ROLL_180 +IMU Invensense SPI:mpu6000_2 ROTATION_ROLL_180_YAW_270 define HAL_DEFAULT_INS_FAST_SAMPLE 3 # DPS310 integrated on I2C2 bus From 344ff8ce53fcd16faa1315fd845fac20a6c767fe Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Wed, 15 Apr 2026 11:38:14 +0200 Subject: [PATCH 87/99] mavlink: Update mavlink repo --- modules/mavlink | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mavlink b/modules/mavlink index 37d35a66317112..288b907c384a89 160000 --- a/modules/mavlink +++ b/modules/mavlink @@ -1 +1 @@ -Subproject commit 37d35a66317112bc88d97a557c22a1d15c2a0896 +Subproject commit 288b907c384a892c8519bfe271682424b1e1a3a0 From 1aa34b3be598eb9e7a8e68e6c216e3f5612c449c Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:06:33 +0200 Subject: [PATCH 88/99] Tracker: 4.7.0-beta3 release notes --- AntennaTracker/ReleaseNotes.txt | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/AntennaTracker/ReleaseNotes.txt b/AntennaTracker/ReleaseNotes.txt index 135db4c9a9af12..9c93be93dce76a 100644 --- a/AntennaTracker/ReleaseNotes.txt +++ b/AntennaTracker/ReleaseNotes.txt @@ -1,4 +1,66 @@ ArduPilot Antenna Tracker Release Notes: +------------------------------------------------------------------ +Release 4.7.0-beta3 10-Apr-2026 + +Changes from 4.7.0-beta2 + +1) Board specific changes + +- Atlas-Control has updated hwdef.dat file (@AltasFlight, PR:32268) +- HWH7 flight controller added to supported boards (@Arwayday, PR:32248) +- TBS_LUCID_H7 now has an MPU6000 version (@andyp1per, PR:32512) + +2) Driver changes + +- ZB25VQ128D flash chip now supported for onboard logging (@LoyalLinjy, PR:32408) +- Kebni SensAItion as External AHRS now supported in IMU and INS modes (@magnus-lindhe-kebni, PR:31775) +- IST8310 compass probing corrected on fmuv3-based boards (@peterbarker, PR:32462) + +3) AHRS specific changes + +- EKF3 flash space requirements reduced (@tpwrules, PRs:32213, 32618) +- EKF3 is fixed to no longer reset to external altitude when it is configured to another source (@snktshrma, PR:32508) +- AHRS recorded origin check will correctly use the GPS (@andyp1per, PR: 32469) +- AHRS external origin setting will be delayed until filter is initialised (@andyp1per, PR: 32505) +- EKF3 missing constraints to FuseDragForces added (@tpwrules, PR:32128) +- Fix replay involving body odometry data (@twprules, PR:32598) + +4) Camera and mount specific changes + +- Camera servo driver's CAMERA_INFORMATION flags fixed (@Davidsastresas, PR:32564) +- RunCam param metadata names are fixed to match code (@tpwrules, PR:32601) +- Mount fix to angular rates when using FPV mode (@KSimeonAVTA, PR:32540) +- Mount scripting driver accepts rate targets (@peterbarker, PR:32626) +- Fix wire data issues with various Mount backends (@peterbarker, PR:32630) + +5) Other libraries changes + +- Fence for Plane and Copter supports alt above terrain (@andyp1per, PR:31619) +- Ignore-receiver in RC_OPTIONS is now respected during RC loss (@peterbarker, PR:32488) +- ATC_INPUT_TC and Q_A_INPUT_TC param defaults reduced (@rmackay9, PR:32643) + +6) Copter/Tradheli specific changes + +- Spool state is logged (@Georacer, PR:31668) + +7) Plane specific changes + +- Rangefinder state is logged (@Georacer, PR:31579) +- AHRS log message rate not affected by full rate attitude logging (@IamPete1, PR:32623) + +8) Sub specific changes + +- Automatically deal with misconfigured lights/actuators (@Williangalvani, PR:31109) + +9) Rover specific changes + +- Battery watt limiter feature added to custom build server (@mackay9, PR:32687) + +10) Scripting changes + +- plane_follow.lua messages will no longer crash the script if there is no target vehicle (@timtuxworth, PR:32567) +- plane_follow.lua has a fix for crosstrack oscillation (@timtuxworth, PR:32575) + ------------------------------------------------------------------ Release 4.7.0-beta2 21-Mar-2026 From c26ddb6675498edde02770d4b27a7cc525ae46e6 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:12:14 +0200 Subject: [PATCH 89/99] Tracker: version to 4.7.0-beta3 --- AntennaTracker/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AntennaTracker/version.h b/AntennaTracker/version.h index eb69665fac2581..a8caa9771f72e7 100644 --- a/AntennaTracker/version.h +++ b/AntennaTracker/version.h @@ -6,10 +6,10 @@ #include "ap_version.h" -#define THISFIRMWARE "AntennaTracker V4.7.0-beta2" +#define THISFIRMWARE "AntennaTracker V4.7.0-beta3" // the following line is parsed by the autotest scripts -#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+1 +#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+2 #define FW_MAJOR 4 #define FW_MINOR 7 From 2f267efdc764063753d64da73c71745cf2272cbf Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:16:09 +0200 Subject: [PATCH 90/99] AP_Periph: 4.7.0-beta3 release notes --- Tools/AP_Periph/ReleaseNotes.txt | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Tools/AP_Periph/ReleaseNotes.txt b/Tools/AP_Periph/ReleaseNotes.txt index 13b27387563bbd..c74ee3ade8eec0 100644 --- a/Tools/AP_Periph/ReleaseNotes.txt +++ b/Tools/AP_Periph/ReleaseNotes.txt @@ -1,3 +1,67 @@ +ArduPilot AP_Periph Release Notes +------------------------------------------------------------------ +Release 4.7.0-beta3 10-Apr-2026 + +Changes from 4.7.0-beta2 + +1) Board specific changes + +- Atlas-Control has updated hwdef.dat file (@AltasFlight, PR:32268) +- HWH7 flight controller added to supported boards (@Arwayday, PR:32248) +- TBS_LUCID_H7 now has an MPU6000 version (@andyp1per, PR:32512) + +2) Driver changes + +- ZB25VQ128D flash chip now supported for onboard logging (@LoyalLinjy, PR:32408) +- Kebni SensAItion as External AHRS now supported in IMU and INS modes (@magnus-lindhe-kebni, PR:31775) +- IST8310 compass probing corrected on fmuv3-based boards (@peterbarker, PR:32462) + +3) AHRS specific changes + +- EKF3 flash space requirements reduced (@tpwrules, PRs:32213, 32618) +- EKF3 is fixed to no longer reset to external altitude when it is configured to another source (@snktshrma, PR:32508) +- AHRS recorded origin check will correctly use the GPS (@andyp1per, PR: 32469) +- AHRS external origin setting will be delayed until filter is initialised (@andyp1per, PR: 32505) +- EKF3 missing constraints to FuseDragForces added (@tpwrules, PR:32128) +- Fix replay involving body odometry data (@twprules, PR:32598) + +4) Camera and mount specific changes + +- Camera servo driver's CAMERA_INFORMATION flags fixed (@Davidsastresas, PR:32564) +- RunCam param metadata names are fixed to match code (@tpwrules, PR:32601) +- Mount fix to angular rates when using FPV mode (@KSimeonAVTA, PR:32540) +- Mount scripting driver accepts rate targets (@peterbarker, PR:32626) +- Fix wire data issues with various Mount backends (@peterbarker, PR:32630) + +5) Other libraries changes + +- Fence for Plane and Copter supports alt above terrain (@andyp1per, PR:31619) +- Ignore-receiver in RC_OPTIONS is now respected during RC loss (@peterbarker, PR:32488) +- ATC_INPUT_TC and Q_A_INPUT_TC param defaults reduced (@rmackay9, PR:32643) + +6) Copter/Tradheli specific changes + +- Spool state is logged (@Georacer, PR:31668) + +7) Plane specific changes + +- Rangefinder state is logged (@Georacer, PR:31579) +- AHRS log message rate not affected by full rate attitude logging (@IamPete1, PR:32623) + +8) Sub specific changes + +- Automatically deal with misconfigured lights/actuators (@Williangalvani, PR:31109) + +9) Rover specific changes + +- Battery watt limiter feature added to custom build server (@mackay9, PR:32687) + +10) Scripting changes + +- plane_follow.lua messages will no longer crash the script if there is no target vehicle (@timtuxworth, PR:32567) +- plane_follow.lua has a fix for crosstrack oscillation (@timtuxworth, PR:32575) + +------------------------------------------------------------------ Release 4.7.0-beta2 21-Mar-2026 Changes from 1.8.0 From 0b3df9a1f9cdc1263d9cb1332bc27a01d546612c Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:16:22 +0200 Subject: [PATCH 91/99] AP_Periph: version to 4.7.0-beta3 --- Tools/AP_Periph/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/AP_Periph/version.h b/Tools/AP_Periph/version.h index 383eaadae317ba..515703b143336d 100644 --- a/Tools/AP_Periph/version.h +++ b/Tools/AP_Periph/version.h @@ -7,7 +7,7 @@ #include "ap_version.h" #include -#define THISFIRMWARE "AP_Periph 4.7.0-beta2" +#define THISFIRMWARE "AP_Periph 4.7.0-beta3" // defines needed due to lack of GCS includes #ifndef HAVE_ENUM_FIRMWARE_VERSION_TYPE @@ -17,7 +17,7 @@ #endif // the following line is parsed by the autotest scripts -#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+1 +#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+2 #define FW_MAJOR 4 #define FW_MINOR 7 From 6ecec505dfd52964ddd25595ee44775483cabf3e Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:17:30 +0200 Subject: [PATCH 92/99] Sub: 4.7.0-beta3 release notes --- ArduSub/ReleaseNotes.txt | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/ArduSub/ReleaseNotes.txt b/ArduSub/ReleaseNotes.txt index c597e2e84bf571..e2b4688f514f14 100644 --- a/ArduSub/ReleaseNotes.txt +++ b/ArduSub/ReleaseNotes.txt @@ -1,4 +1,66 @@ ArduPilot Sub Release Notes: +------------------------------------------------------------------ +Release 4.7.0-beta3 10-Apr-2026 + +Changes from 4.7.0-beta2 + +1) Board specific changes + +- Atlas-Control has updated hwdef.dat file (@AltasFlight, PR:32268) +- HWH7 flight controller added to supported boards (@Arwayday, PR:32248) +- TBS_LUCID_H7 now has an MPU6000 version (@andyp1per, PR:32512) + +2) Driver changes + +- ZB25VQ128D flash chip now supported for onboard logging (@LoyalLinjy, PR:32408) +- Kebni SensAItion as External AHRS now supported in IMU and INS modes (@magnus-lindhe-kebni, PR:31775) +- IST8310 compass probing corrected on fmuv3-based boards (@peterbarker, PR:32462) + +3) AHRS specific changes + +- EKF3 flash space requirements reduced (@tpwrules, PRs:32213, 32618) +- EKF3 is fixed to no longer reset to external altitude when it is configured to another source (@snktshrma, PR:32508) +- AHRS recorded origin check will correctly use the GPS (@andyp1per, PR: 32469) +- AHRS external origin setting will be delayed until filter is initialised (@andyp1per, PR: 32505) +- EKF3 missing constraints to FuseDragForces added (@tpwrules, PR:32128) +- Fix replay involving body odometry data (@twprules, PR:32598) + +4) Camera and mount specific changes + +- Camera servo driver's CAMERA_INFORMATION flags fixed (@Davidsastresas, PR:32564) +- RunCam param metadata names are fixed to match code (@tpwrules, PR:32601) +- Mount fix to angular rates when using FPV mode (@KSimeonAVTA, PR:32540) +- Mount scripting driver accepts rate targets (@peterbarker, PR:32626) +- Fix wire data issues with various Mount backends (@peterbarker, PR:32630) + +5) Other libraries changes + +- Fence for Plane and Copter supports alt above terrain (@andyp1per, PR:31619) +- Ignore-receiver in RC_OPTIONS is now respected during RC loss (@peterbarker, PR:32488) +- ATC_INPUT_TC and Q_A_INPUT_TC param defaults reduced (@rmackay9, PR:32643) + +6) Copter/Tradheli specific changes + +- Spool state is logged (@Georacer, PR:31668) + +7) Plane specific changes + +- Rangefinder state is logged (@Georacer, PR:31579) +- AHRS log message rate not affected by full rate attitude logging (@IamPete1, PR:32623) + +8) Sub specific changes + +- Automatically deal with misconfigured lights/actuators (@Williangalvani, PR:31109) + +9) Rover specific changes + +- Battery watt limiter feature added to custom build server (@mackay9, PR:32687) + +10) Scripting changes + +- plane_follow.lua messages will no longer crash the script if there is no target vehicle (@timtuxworth, PR:32567) +- plane_follow.lua has a fix for crosstrack oscillation (@timtuxworth, PR:32575) + ------------------------------------------------------------------ Release 4.7.0-beta2 21-Mar-2026 From aaa21a038c7679b57857f5538f2c55f30e0cc0c6 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:17:42 +0200 Subject: [PATCH 93/99] Sub: version to 4.7.0-beta3 --- ArduSub/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArduSub/version.h b/ArduSub/version.h index d01d831afd0395..fed32e416307a9 100644 --- a/ArduSub/version.h +++ b/ArduSub/version.h @@ -6,10 +6,10 @@ #include "ap_version.h" -#define THISFIRMWARE "ArduSub V4.7.0-beta2" +#define THISFIRMWARE "ArduSub V4.7.0-beta3" // the following line is parsed by the autotest scripts -#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+1 +#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+2 #define FW_MAJOR 4 #define FW_MINOR 7 From 221f787b5f261fe8f860423ba25486462255fa45 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:18:42 +0200 Subject: [PATCH 94/99] Rover: 4.7.0-beta3 release notes --- Rover/ReleaseNotes.txt | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/Rover/ReleaseNotes.txt b/Rover/ReleaseNotes.txt index b455c251d48ade..2abc17e68b0d80 100644 --- a/Rover/ReleaseNotes.txt +++ b/Rover/ReleaseNotes.txt @@ -1,4 +1,66 @@ ArduPilot Rover Release Notes: +------------------------------------------------------------------ +Release 4.7.0-beta3 10-Apr-2026 + +Changes from 4.7.0-beta2 + +1) Board specific changes + +- Atlas-Control has updated hwdef.dat file (@AltasFlight, PR:32268) +- HWH7 flight controller added to supported boards (@Arwayday, PR:32248) +- TBS_LUCID_H7 now has an MPU6000 version (@andyp1per, PR:32512) + +2) Driver changes + +- ZB25VQ128D flash chip now supported for onboard logging (@LoyalLinjy, PR:32408) +- Kebni SensAItion as External AHRS now supported in IMU and INS modes (@magnus-lindhe-kebni, PR:31775) +- IST8310 compass probing corrected on fmuv3-based boards (@peterbarker, PR:32462) + +3) AHRS specific changes + +- EKF3 flash space requirements reduced (@tpwrules, PRs:32213, 32618) +- EKF3 is fixed to no longer reset to external altitude when it is configured to another source (@snktshrma, PR:32508) +- AHRS recorded origin check will correctly use the GPS (@andyp1per, PR: 32469) +- AHRS external origin setting will be delayed until filter is initialised (@andyp1per, PR: 32505) +- EKF3 missing constraints to FuseDragForces added (@tpwrules, PR:32128) +- Fix replay involving body odometry data (@twprules, PR:32598) + +4) Camera and mount specific changes + +- Camera servo driver's CAMERA_INFORMATION flags fixed (@Davidsastresas, PR:32564) +- RunCam param metadata names are fixed to match code (@tpwrules, PR:32601) +- Mount fix to angular rates when using FPV mode (@KSimeonAVTA, PR:32540) +- Mount scripting driver accepts rate targets (@peterbarker, PR:32626) +- Fix wire data issues with various Mount backends (@peterbarker, PR:32630) + +5) Other libraries changes + +- Fence for Plane and Copter supports alt above terrain (@andyp1per, PR:31619) +- Ignore-receiver in RC_OPTIONS is now respected during RC loss (@peterbarker, PR:32488) +- ATC_INPUT_TC and Q_A_INPUT_TC param defaults reduced (@rmackay9, PR:32643) + +6) Copter/Tradheli specific changes + +- Spool state is logged (@Georacer, PR:31668) + +7) Plane specific changes + +- Rangefinder state is logged (@Georacer, PR:31579) +- AHRS log message rate not affected by full rate attitude logging (@IamPete1, PR:32623) + +8) Sub specific changes + +- Automatically deal with misconfigured lights/actuators (@Williangalvani, PR:31109) + +9) Rover specific changes + +- Battery watt limiter feature added to custom build server (@mackay9, PR:32687) + +10) Scripting changes + +- plane_follow.lua messages will no longer crash the script if there is no target vehicle (@timtuxworth, PR:32567) +- plane_follow.lua has a fix for crosstrack oscillation (@timtuxworth, PR:32575) + ------------------------------------------------------------------ Release 4.7.0-beta2 21-Mar-2026 From 3f23ded9c87bd1508793c6d4d405e3e0e1161a0a Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:18:49 +0200 Subject: [PATCH 95/99] Rover: version to 4.7.0-beta3 --- Rover/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rover/version.h b/Rover/version.h index a7ad7d70d3c35d..558a92999d3e02 100644 --- a/Rover/version.h +++ b/Rover/version.h @@ -6,10 +6,10 @@ #include "ap_version.h" -#define THISFIRMWARE "ArduRover V4.7.0-beta2" +#define THISFIRMWARE "ArduRover V4.7.0-beta3" // the following line is parsed by the autotest scripts -#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+1 +#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+2 #define FW_MAJOR 4 #define FW_MINOR 7 From a8a95debf8a6a243ec2cbab567456bfd22b90076 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:20:10 +0200 Subject: [PATCH 96/99] Copter: 4.7.0-beta3 release notes --- ArduCopter/ReleaseNotes.txt | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/ArduCopter/ReleaseNotes.txt b/ArduCopter/ReleaseNotes.txt index 9fa9cd118b93ca..720c0ec97885a7 100644 --- a/ArduCopter/ReleaseNotes.txt +++ b/ArduCopter/ReleaseNotes.txt @@ -1,4 +1,66 @@ ArduPilot Copter Release Notes: +------------------------------------------------------------------ +Release 4.7.0-beta3 10-Apr-2026 + +Changes from 4.7.0-beta2 + +1) Board specific changes + +- Atlas-Control has updated hwdef.dat file (@AltasFlight, PR:32268) +- HWH7 flight controller added to supported boards (@Arwayday, PR:32248) +- TBS_LUCID_H7 now has an MPU6000 version (@andyp1per, PR:32512) + +2) Driver changes + +- ZB25VQ128D flash chip now supported for onboard logging (@LoyalLinjy, PR:32408) +- Kebni SensAItion as External AHRS now supported in IMU and INS modes (@magnus-lindhe-kebni, PR:31775) +- IST8310 compass probing corrected on fmuv3-based boards (@peterbarker, PR:32462) + +3) AHRS specific changes + +- EKF3 flash space requirements reduced (@tpwrules, PRs:32213, 32618) +- EKF3 is fixed to no longer reset to external altitude when it is configured to another source (@snktshrma, PR:32508) +- AHRS recorded origin check will correctly use the GPS (@andyp1per, PR: 32469) +- AHRS external origin setting will be delayed until filter is initialised (@andyp1per, PR: 32505) +- EKF3 missing constraints to FuseDragForces added (@tpwrules, PR:32128) +- Fix replay involving body odometry data (@twprules, PR:32598) + +4) Camera and mount specific changes + +- Camera servo driver's CAMERA_INFORMATION flags fixed (@Davidsastresas, PR:32564) +- RunCam param metadata names are fixed to match code (@tpwrules, PR:32601) +- Mount fix to angular rates when using FPV mode (@KSimeonAVTA, PR:32540) +- Mount scripting driver accepts rate targets (@peterbarker, PR:32626) +- Fix wire data issues with various Mount backends (@peterbarker, PR:32630) + +5) Other libraries changes + +- Fence for Plane and Copter supports alt above terrain (@andyp1per, PR:31619) +- Ignore-receiver in RC_OPTIONS is now respected during RC loss (@peterbarker, PR:32488) +- ATC_INPUT_TC and Q_A_INPUT_TC param defaults reduced (@rmackay9, PR:32643) + +6) Copter/Tradheli specific changes + +- Spool state is logged (@Georacer, PR:31668) + +7) Plane specific changes + +- Rangefinder state is logged (@Georacer, PR:31579) +- AHRS log message rate not affected by full rate attitude logging (@IamPete1, PR:32623) + +8) Sub specific changes + +- Automatically deal with misconfigured lights/actuators (@Williangalvani, PR:31109) + +9) Rover specific changes + +- Battery watt limiter feature added to custom build server (@mackay9, PR:32687) + +10) Scripting changes + +- plane_follow.lua messages will no longer crash the script if there is no target vehicle (@timtuxworth, PR:32567) +- plane_follow.lua has a fix for crosstrack oscillation (@timtuxworth, PR:32575) + ------------------------------------------------------------------ Release 4.7.0-beta2 21-Mar-2026 From cc438b9f840414d6ba20d6db966a1f60d338e4fc Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:20:44 +0200 Subject: [PATCH 97/99] Copter: version to 4.7.0-beta3 --- ArduCopter/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArduCopter/version.h b/ArduCopter/version.h index 21f65b2be68378..b57711e9fa0e07 100644 --- a/ArduCopter/version.h +++ b/ArduCopter/version.h @@ -6,10 +6,10 @@ #include "ap_version.h" -#define THISFIRMWARE "ArduCopter V4.7.0-beta2" +#define THISFIRMWARE "ArduCopter V4.7.0-beta3" // the following line is parsed by the autotest scripts -#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+1 +#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+2 #define FW_MAJOR 4 #define FW_MINOR 7 From 1ee50da8d760da7a26fd50fd36d365f301bd7e75 Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:21:26 +0200 Subject: [PATCH 98/99] Plane: 4.7.0-beta3 release notes --- ArduPlane/ReleaseNotes.txt | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/ArduPlane/ReleaseNotes.txt b/ArduPlane/ReleaseNotes.txt index 23a8958a46d9fa..2c8657e2461ae9 100644 --- a/ArduPlane/ReleaseNotes.txt +++ b/ArduPlane/ReleaseNotes.txt @@ -1,4 +1,66 @@ ArduPilot Plane Release Notes: +------------------------------------------------------------------ +Release 4.7.0-beta3 10-Apr-2026 + +Changes from 4.7.0-beta2 + +1) Board specific changes + +- Atlas-Control has updated hwdef.dat file (@AltasFlight, PR:32268) +- HWH7 flight controller added to supported boards (@Arwayday, PR:32248) +- TBS_LUCID_H7 now has an MPU6000 version (@andyp1per, PR:32512) + +2) Driver changes + +- ZB25VQ128D flash chip now supported for onboard logging (@LoyalLinjy, PR:32408) +- Kebni SensAItion as External AHRS now supported in IMU and INS modes (@magnus-lindhe-kebni, PR:31775) +- IST8310 compass probing corrected on fmuv3-based boards (@peterbarker, PR:32462) + +3) AHRS specific changes + +- EKF3 flash space requirements reduced (@tpwrules, PRs:32213, 32618) +- EKF3 is fixed to no longer reset to external altitude when it is configured to another source (@snktshrma, PR:32508) +- AHRS recorded origin check will correctly use the GPS (@andyp1per, PR: 32469) +- AHRS external origin setting will be delayed until filter is initialised (@andyp1per, PR: 32505) +- EKF3 missing constraints to FuseDragForces added (@tpwrules, PR:32128) +- Fix replay involving body odometry data (@twprules, PR:32598) + +4) Camera and mount specific changes + +- Camera servo driver's CAMERA_INFORMATION flags fixed (@Davidsastresas, PR:32564) +- RunCam param metadata names are fixed to match code (@tpwrules, PR:32601) +- Mount fix to angular rates when using FPV mode (@KSimeonAVTA, PR:32540) +- Mount scripting driver accepts rate targets (@peterbarker, PR:32626) +- Fix wire data issues with various Mount backends (@peterbarker, PR:32630) + +5) Other libraries changes + +- Fence for Plane and Copter supports alt above terrain (@andyp1per, PR:31619) +- Ignore-receiver in RC_OPTIONS is now respected during RC loss (@peterbarker, PR:32488) +- ATC_INPUT_TC and Q_A_INPUT_TC param defaults reduced (@rmackay9, PR:32643) + +6) Copter/Tradheli specific changes + +- Spool state is logged (@Georacer, PR:31668) + +7) Plane specific changes + +- Rangefinder state is logged (@Georacer, PR:31579) +- AHRS log message rate not affected by full rate attitude logging (@IamPete1, PR:32623) + +8) Sub specific changes + +- Automatically deal with misconfigured lights/actuators (@Williangalvani, PR:31109) + +9) Rover specific changes + +- Battery watt limiter feature added to custom build server (@mackay9, PR:32687) + +10) Scripting changes + +- plane_follow.lua messages will no longer crash the script if there is no target vehicle (@timtuxworth, PR:32567) +- plane_follow.lua has a fix for crosstrack oscillation (@timtuxworth, PR:32575) + ------------------------------------------------------------------ Release 4.7.0-beta2 21-Mar-2026 From 8660eca1950ce901b54786df9f07a56eac52633f Mon Sep 17 00:00:00 2001 From: George Zogopoulos Date: Fri, 10 Apr 2026 11:22:25 +0200 Subject: [PATCH 99/99] Plane: version to 4.7.0-beta3 --- ArduPlane/version.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArduPlane/version.h b/ArduPlane/version.h index 1fd131ccbacfe4..39d716cf2b4a9f 100644 --- a/ArduPlane/version.h +++ b/ArduPlane/version.h @@ -6,10 +6,10 @@ #include "ap_version.h" -#define THISFIRMWARE "ArduPlane V4.7.0-beta2" +#define THISFIRMWARE "ArduPlane V4.7.0-beta3" // the following line is parsed by the autotest scripts -#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+1 +#define FIRMWARE_VERSION 4,7,0,FIRMWARE_VERSION_TYPE_BETA+2 #define FW_MAJOR 4 #define FW_MINOR 7