LCOV - code coverage report
Current view: top level - AP_ExternalAHRS - AP_ExternalAHRS_Aeron_plx.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 417 495 84.2 %
Date: 2026-09-09 11:02:08 Functions: 23 24 95.8 %

          Line data    Source code
       1             : /*
       2             :    This program is free software: you can redistribute it and/or modify
       3             :    it under the terms of the GNU General Public License as published by
       4             :    the Free Software Foundation, either version 3 of the License, or
       5             :    (at your option) any later version.
       6             : 
       7             :    This program is distributed in the hope that it will be useful,
       8             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
       9             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      10             :    GNU General Public License for more details.
      11             : 
      12             :    You should have received a copy of the GNU General Public License
      13             :    along with this program.  If not, see <http://www.gnu.org/licenses/>.
      14             : 
      15             :    ATH- Vinayak Pundhir (VP)
      16             :  */
      17             : 
      18             : /*
      19             :   Aeron Systems PLX series INS: External AHRS backend.
      20             :   See AP_ExternalAHRS_Aeron_plx.h for protocol overview and usage.
      21             :  */
      22             : 
      23             : #define AP_MATH_ALLOW_DOUBLE_FUNCTIONS 1
      24             : 
      25             : #include "AP_ExternalAHRS_config.h"
      26             : 
      27             : #if AP_EXTERNAL_AHRS_AERON_PLX_ENABLED
      28             : 
      29             : #include "AP_ExternalAHRS_Aeron_plx.h"
      30             : 
      31             : #include <AP_Baro/AP_Baro.h>
      32             : #include <AP_Compass/AP_Compass.h>
      33             : #include <AP_GPS/AP_GPS.h>
      34             : #include <AP_HAL/AP_HAL.h>
      35             : #include <AP_HAL/utility/sparse-endian.h>
      36             : #include <AP_InertialSensor/AP_InertialSensor.h>
      37             : #include <AP_Logger/AP_Logger.h>
      38             : #include <AP_Math/AP_Math.h>
      39             : #include <AP_Math/crc.h>
      40             : #include <AP_SerialManager/AP_SerialManager.h>
      41             : #include <GCS_MAVLink/GCS.h>
      42             : 
      43             : // Local debug macro - no-op in release builds.
      44             : #ifndef AERON_DEBUG
      45             : #define AERON_DEBUG 0
      46             : #endif  // AERON_DEBUG
      47             : #if AERON_DEBUG
      48             : #define debug(fmt, args ...) GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "Aeron: " fmt, ##args)
      49             : #else
      50             : #define debug(...) do {} while (0)
      51             : #endif  // AERON_DEBUG
      52             : 
      53             : extern const AP_HAL::HAL &hal;
      54             : 
      55          11 : AP_ExternalAHRS_Aeron_plx::AP_ExternalAHRS_Aeron_plx(AP_ExternalAHRS *_frontend,
      56          11 :                                                      AP_ExternalAHRS::state_t &_state)
      57          11 :     : AP_ExternalAHRS_backend(_frontend, _state)
      58             : {
      59          11 :     const auto &sm = AP::serialmanager();
      60          11 :     uart     = sm.find_serial(AP_SerialManager::SerialProtocol_AHRS, 0);
      61          11 :     baudrate = sm.find_baudrate(AP_SerialManager::SerialProtocol_AHRS, 0);
      62          11 :     port_num = sm.find_portnum(AP_SerialManager::SerialProtocol_AHRS, 0);
      63             : 
      64          11 :     if (uart == nullptr) {
      65           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "Aeron: Serial port not found");
      66           0 :         return;
      67             :     }
      68             : 
      69             :     // Not offering IMU by default; rates below 400 Hz are too slow for
      70             :     // multicopter platforms. Users running SENS_PARA at >= 400 Hz can
      71             :     // opt into IMU via EAHRS_SENSORS.
      72          11 :     set_default_sensors(uint16_t(AP_ExternalAHRS::AvailableSensor::GPS) |
      73             :                         uint16_t(AP_ExternalAHRS::AvailableSensor::BARO) |
      74             :                         uint16_t(AP_ExternalAHRS::AvailableSensor::COMPASS));
      75             : 
      76          11 :     if (!hal.scheduler->thread_create(
      77             :             FUNCTOR_BIND_MEMBER(&AP_ExternalAHRS_Aeron_plx::update_thread, void),
      78             :             "Aeron",
      79             :             2048,
      80             :             AP_HAL::Scheduler::PRIORITY_SPI,
      81             :             0)) {
      82           0 :         AP_HAL::panic("Aeron: failed to create update thread");
      83             :     }
      84             : 
      85          11 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO,
      86             :                   "Aeron: Initialised on SERIAL%d @ %lu baud",
      87             :                   port_num,
      88             :                   (unsigned long)baudrate);
      89             : }
      90             : 
      91          11 : void AP_ExternalAHRS_Aeron_plx::update_thread()
      92             : {
      93          11 :     uart->begin(baudrate, 2048, 256);
      94          11 :     setup_complete = true;
      95             : 
      96             :     while (true) {
      97       43967 :         check_and_decode();
      98       43967 :         report_hw_status();
      99       43967 :         hal.scheduler->delay_microseconds(1000);
     100             :     }
     101             : }
     102             : 
     103             : // Core pipeline: read -> parse -> publish -> deferred messages.
     104       43967 : void AP_ExternalAHRS_Aeron_plx::check_and_decode()
     105             : {
     106       43967 :     if (uart == nullptr) {
     107       13794 :         return;
     108             :     }
     109             : 
     110             :     // Drain buffer, stack-local. 256 B is ~3x the max bytes that arrive
     111             :     // in one 1 ms tick at 921600 baud (~92 B), and still clears a full
     112             :     // UART RX backlog within a few ticks if the thread is briefly
     113             :     // preempted.
     114             :     uint8_t chunk_buf[256];
     115       43967 :     const uint16_t avail = MIN(uart->available(), uint16_t(sizeof(chunk_buf)));
     116       43967 :     if (avail == 0) {
     117       13794 :         return;
     118             :     }
     119             : 
     120       30173 :     const auto n_read = uart->read(chunk_buf, avail);
     121             : 
     122       30173 :     if (n_read <= 0) {
     123           0 :         return;
     124             :     }
     125             : 
     126             :     // msgs collects GCS notifications raised mid-parse (CRC failures,
     127             :     // unknown packets, ext-sensor replies) so they are emitted once per
     128             :     // drain instead of from inside the byte loop.
     129       30173 :     AeronDeferredMsgs msgs{};
     130             : 
     131     2670018 :     for (auto index = 0; index < n_read; index++) {
     132     2639845 :         parse_byte(chunk_buf[index], msgs);
     133             :     }
     134             : 
     135       30173 :     send_deferred_messages(msgs);
     136             : 
     137       30173 :     check_for_query_ext_sensors();
     138             : }
     139             : 
     140             : // State-machine parser for the Aeron binary protocol.  On a CRC-valid
     141             : // frame, dispatches via handle_chunk_buf_packet() rather than leaving
     142             : // the caller to read implicitly-set member state.  CRC failures and
     143             : // unknown packets are flagged in msgs for the caller to surface as
     144             : // throttled GCS warnings.
     145     2639845 : void AP_ExternalAHRS_Aeron_plx::parse_byte(uint8_t byte, AeronDeferredMsgs &msgs)
     146             : {
     147     2639845 :     switch (parse_state) {
     148       35055 :     case ParseState::RESET:
     149       35055 :         write_idx   = 0;
     150       35055 :         pkt_length  = 0;
     151       35055 :         sync_count  = 0;
     152       35055 :         parse_state = ParseState::SYNC;
     153             :         FALLTHROUGH;
     154             : 
     155      140220 :     case ParseState::SYNC:
     156      140220 :         if (byte == SYNC_BYTE) {
     157      140220 :             rx_buf[write_idx++] = byte;
     158      140220 :             if (++sync_count == SYNC_REQUIRED) {
     159       35055 :                 parse_state = ParseState::LEN_HIGH;
     160             :             }
     161             :         } else {
     162           0 :             parse_state = ParseState::RESET;
     163             :         }
     164      140220 :         return;
     165             : 
     166       35055 :     case ParseState::LEN_HIGH:
     167       35055 :         pkt_length          = byte;
     168       35055 :         rx_buf[write_idx++] = byte;
     169       35055 :         parse_state         = ParseState::LEN_LOW;
     170       35055 :         return;
     171             : 
     172       35055 :     case ParseState::LEN_LOW:
     173       35055 :         rx_buf[write_idx++] = byte;
     174       35055 :         pkt_length = (pkt_length << 8) | byte;
     175       35055 :         if (pkt_length >= PKT_LEN_MIN && pkt_length <= PKT_LEN_MAX) {
     176       35055 :             parse_state = ParseState::PAYLOAD;
     177             :         } else {
     178           0 :             parse_state = ParseState::RESET;
     179             :         }
     180       35055 :         return;
     181             : 
     182     2394460 :     case ParseState::PAYLOAD:
     183     2394460 :         rx_buf[write_idx++] = byte;
     184     2394460 :         if (write_idx >= pkt_length) {
     185       35055 :             parse_state = ParseState::CRC_BYTE;
     186             :         }
     187     2394460 :         return;
     188             : 
     189       35055 :     case ParseState::CRC_BYTE: {
     190       35055 :         rx_buf[write_idx++] = byte;
     191       35055 :         const bool valid = (byte == crc_xor_of_bytes(rx_buf, pkt_length));
     192       35055 :         if (valid) {
     193       35055 :             const uint16_t prp_id      = be16toh_ptr(&rx_buf[6]);
     194       35055 :             const uint16_t payload_len = (pkt_length > 8) ? (pkt_length - 8) : 0;
     195       35055 :             handle_chunk_buf_packet(prp_id, &rx_buf[8], payload_len, msgs);
     196             :         } else {
     197           0 :             msgs.crc_fail = true;
     198             :             debug("CRC-ID=0x%04X",
     199             :                   (uint16_t(rx_buf[6]) << 8) | rx_buf[7]);
     200             :         }
     201       35055 :         parse_state = ParseState::RESET;
     202       35055 :         return;
     203             :     }
     204             :     }
     205             : 
     206             :     // Unreachable - all enum values handled above.
     207           0 :     parse_state = ParseState::RESET;
     208             : }
     209             : 
     210             : // Dispatch a CRC-valid frame.  The frame sits in rx_buf[0 .. pkt_length-1];
     211             : // PRP_ID is at offset 6..7 (big-endian) and the payload starts at offset 8.
     212             : // Called from inside parse_byte's CRC state so the data hasn't yet been
     213             : // overwritten by the next incoming byte.
     214       35055 : void AP_ExternalAHRS_Aeron_plx::handle_chunk_buf_packet(uint16_t prp_id,
     215             :                                                         const uint8_t *payload,
     216             :                                                         uint16_t payload_len,
     217             :                                                         AeronDeferredMsgs &msgs)
     218             : {
     219       35055 :     switch (AeronPacketID(prp_id)) {
     220       10294 :     case AeronPacketID::NAV_PARA1:
     221       10294 :         if (payload_len < sizeof(NavPara1Payload)) {
     222           0 :             msgs.bad_length = true;
     223           0 :             return;
     224             :         }
     225       10294 :         publish_nav_para1(*reinterpret_cast<const NavPara1Payload *>(payload));
     226       10294 :         return;
     227             : 
     228       10305 :     case AeronPacketID::NAV_PARA2:
     229       10305 :         if (payload_len < sizeof(NavPara2Payload)) {
     230           0 :             msgs.bad_length = true;
     231           0 :             return;
     232             :         }
     233       10305 :         publish_nav_para2(*reinterpret_cast<const NavPara2Payload *>(payload));
     234       10305 :         return;
     235             : 
     236       10302 :     case AeronPacketID::SENS_PARA:
     237       10302 :         if (payload_len < sizeof(SensParaPayload)) {
     238           0 :             msgs.bad_length = true;
     239           0 :             return;
     240             :         }
     241       10302 :         publish_sens_para(*reinterpret_cast<const SensParaPayload *>(payload));
     242       10302 :         return;
     243             : 
     244        2066 :     case AeronPacketID::GPS_PARA:
     245        2066 :         if (payload_len < sizeof(GpsParaPayload)) {
     246           0 :             msgs.bad_length = true;
     247           0 :             return;
     248             :         }
     249        2066 :         publish_gps_para(*reinterpret_cast<const GpsParaPayload *>(payload), msgs);
     250        2066 :         return;
     251             : 
     252        2066 :     case AeronPacketID::EXTD_GNSS:
     253        2066 :         if (payload_len < sizeof(ExtdGnssPayload)) {
     254           0 :             msgs.bad_length = true;
     255           0 :             return;
     256             :         }
     257        2066 :         publish_extd_gnss(*reinterpret_cast<const ExtdGnssPayload *>(payload));
     258        2066 :         return;
     259             : 
     260          11 :     case AeronPacketID::EXT_SENSORS_MAG: {
     261          11 :         if (payload_len < 1) {
     262           0 :             msgs.bad_length = true;
     263           0 :             return;
     264             :         }
     265          11 :         WITH_SEMAPHORE(arn_sem);
     266          11 :         shared.ext_mag_present = payload[0];
     267          11 :         shared.ext_mag_checked = true;
     268          11 :         msgs.ext_mag_checked   = true;
     269          11 :         return;
     270          11 :     }
     271             : 
     272          11 :     case AeronPacketID::EXT_SENSORS_ASP: {
     273          11 :         if (payload_len < 1) {
     274           0 :             msgs.bad_length = true;
     275           0 :             return;
     276             :         }
     277          11 :         WITH_SEMAPHORE(arn_sem);
     278          11 :         shared.ext_asp_present = payload[0];
     279          11 :         shared.ext_asp_checked = true;
     280          11 :         msgs.ext_asp_checked   = true;
     281          11 :         return;
     282          11 :     }
     283             : 
     284           0 :     case AeronPacketID::HEADING_S:
     285             :     case AeronPacketID::H_SPEED_S:
     286             :     case AeronPacketID::V_SPEED_S:
     287             :     case AeronPacketID::POSITION_S:
     288             :     case AeronPacketID::ALTITUDE_S:
     289             :     case AeronPacketID::DEV_INFO:
     290             :     case AeronPacketID::GNSS_PKT:
     291             :     case AeronPacketID::GNSS_FIX_STATUS:
     292             :         // Recognised but unused.
     293           0 :         return;
     294             :     }
     295             : 
     296             :     // Unknown packet ID - flag as garbage.
     297           0 :     msgs.unknown_pkt = true;
     298           0 :     msgs.unknown_id = prp_id;
     299             : }
     300             : 
     301             : // NAV_PARA1: velocity, position, euler angles, course
     302       10294 : void AP_ExternalAHRS_Aeron_plx::publish_nav_para1(const NavPara1Payload &data)
     303             : {
     304       10294 :     last_nav1_ms = AP_HAL::millis();
     305             : 
     306             :     float undulation_m;
     307             :     bool  undulation_valid;
     308             :     {
     309       10294 :         WITH_SEMAPHORE(arn_sem);
     310       10294 :         undulation_m         = shared.gps_undulation_m;
     311             :         // undulation (geoid separation) only becomes valid once GPS_PARA
     312             :         // has been received; before that it is zero and would corrupt the
     313             :         // MSL altitude by the geoid height (tens of metres).
     314       10294 :         undulation_valid     = (last_gnss_ms != 0);
     315       10294 :     }
     316             : 
     317             :     {
     318       10294 :         WITH_SEMAPHORE(state.sem);
     319       20588 :         state.velocity = {
     320       10294 :             data.velocity_ned[0],
     321       10294 :             data.velocity_ned[1],
     322       10294 :             data.velocity_ned[2]
     323             :         };
     324       10294 :         state.have_velocity = true;
     325             : 
     326             :         // Only publish an absolute location once we can compute a correct
     327             :         // MSL altitude (i.e. undulation is valid). Velocity above has no
     328             :         // such dependency and is always published.
     329       10294 :         if (undulation_valid) {
     330       20588 :             state.location = Location {
     331       10294 :                 int32_t(data.position[0] * 1.0e7),
     332       10294 :                 int32_t(data.position[1] * 1.0e7),
     333       10294 :                 int32_t((data.height_abv_ellip - undulation_m) * 1.0e2),
     334             :                 Location::AltFrame::ABSOLUTE
     335             :             };
     336       10294 :             state.have_location           = true;
     337       10294 :             state.last_location_update_us = AP_HAL::micros();
     338             :         }
     339       10294 :     }
     340             : 
     341             :     // Logging is done outside the lock. We must not hold state.sem across that.
     342             : #if HAL_LOGGING_ENABLED
     343       10294 :     const uint32_t now_ms = AP_HAL::millis();
     344       10294 :     const int16_t log_rate = AP::externalAHRS().log_rate.get();
     345       10294 :     if (log_rate > 0 && now_ms - last_aern_log_ms >= uint32_t(1000U / log_rate)) {
     346        1873 :         last_aern_log_ms = now_ms;
     347             :         // @LoggerMessage: AERN
     348             :         // @Description: Aeron PLX fused navigation solution (NAV_PARA1)
     349             :         // @Field: TimeUS: Time since system startup
     350             :         // @Field: Epoch: Unix epoch seconds at sample time
     351             :         // @Field: Lat: Latitude
     352             :         // @Field: Lng: Longitude
     353             :         // @Field: Alt: Height above WGS84 ellipsoid
     354             :         // @Field: VN: Velocity north
     355             :         // @Field: VE: Velocity east
     356             :         // @Field: VD: Velocity down
     357             :         // @Field: Crs: Course over ground
     358             :         // @Field: Roll: Roll angle
     359             :         // @Field: Pitch: Pitch angle
     360             :         // @Field: Yaw: Yaw angle
     361             :         // @Field: InsSt: PLX INS filter-source status word
     362             :         // @Field: HwSt: PLX hardware status word
     363        3746 :         AP::logger().WriteStreaming(
     364             :             "AERN",
     365             :             "TimeUS," "Epoch," "Lat," "Lng," "Alt," "VN," "VE," "VD," "Crs," "Roll," "Pitch," "Yaw," "InsSt," "HwSt",
     366             :             "s"       "s"      "D"    "U"    "m"    "n"   "n"   "n"   "h"    "r"     "r"      "r"    "-"      "-",
     367             :             "F"       "-"      "-"    "-"    "-"    "-"   "-"   "-"   "-"    "-"     "-"      "-"    "-"      "-",
     368             :             "Q"       "I"      "d"    "d"    "f"    "f"   "f"   "f"   "f"    "f"     "f"      "f"    "I"      "I",
     369             :             AP_HAL::micros64(),
     370        1873 :             data.epoch_time,
     371        1873 :             data.position[0], data.position[1], double(data.height_abv_ellip),
     372        1873 :             data.velocity_ned[0], data.velocity_ned[1], data.velocity_ned[2],
     373        1873 :             data.course,
     374        1873 :             data.euler[0], data.euler[1], data.euler[2],
     375        1873 :             data.ins_status,
     376        1873 :             data.hw_status
     377             :         );
     378             :     }
     379             : #endif  // HAL_LOGGING_ENABLED
     380       10294 : }
     381             : 
     382             : // NAV_PARA2: quaternion, body velocity, ECEF, hw_status
     383       10305 : void AP_ExternalAHRS_Aeron_plx::publish_nav_para2(const NavPara2Payload &data)
     384             : {
     385       10305 :     last_nav2_ms = AP_HAL::millis();
     386             : 
     387             :     {
     388       10305 :         WITH_SEMAPHORE(arn_sem);
     389       10305 :         shared.nav2_hw_status = data.hw_status;
     390       10305 :     }
     391             : 
     392       10305 :     WITH_SEMAPHORE(state.sem);
     393       20610 :     state.quat = {
     394       10305 :         data.quat[0],
     395       10305 :         data.quat[1],
     396       10305 :         data.quat[2],
     397       10305 :         data.quat[3]
     398             :     };
     399       10305 :     state.have_quaternion = true;
     400       10305 : }
     401             : 
     402             : // SENS_PARA: gyro/accel/mag/baro
     403       10302 : void AP_ExternalAHRS_Aeron_plx::publish_sens_para(const SensParaPayload &data)
     404             : {
     405       10302 :     last_sens_ms = AP_HAL::millis();
     406             : 
     407             :     // A sensor the PLX has declared non-functional (manual 7.1.2.9 Table 15:
     408             :     // "non-functional" or "communication failure") must not have its samples
     409             :     // republished into ArduPilot's own INS, baro or compass, where they would
     410             :     // reach EKF3 with no health signal attached. BARO and COMPASS are
     411             :     // advertised by default, so this is the default path rather than an
     412             :     // EAHRS_SENSORS opt-in. Dropping the call lets the consuming library time
     413             :     // out, which is the machine-readable signal a GCS warning alone cannot be.
     414             :     uint32_t hw;
     415             :     {
     416       10302 :         WITH_SEMAPHORE(arn_sem);
     417       10302 :         hw = shared.nav2_hw_status;
     418       10302 :     }
     419       10302 :     const bool imu_ok  = (hw & ((1U << uint8_t(AeronHwStatus::ACC)) |
     420             :                                 (1U << uint8_t(AeronHwStatus::GYR)))) == 0;
     421       10302 :     const bool baro_ok = (hw & (1U << uint8_t(AeronHwStatus::BARO))) == 0;
     422       10302 :     const bool mag_ok  = (hw & (1U << uint8_t(AeronHwStatus::MAG)))  == 0;
     423             : 
     424             :     const Vector3f accel {
     425       10302 :         data.accel[0],
     426       10302 :         data.accel[1],
     427       10302 :         data.accel[2]
     428       10302 :     };
     429             :     const Vector3f gyro {
     430       10302 :         data.gyro[0] * DEG_TO_RAD,
     431       10302 :         data.gyro[1] * DEG_TO_RAD,
     432       10302 :         data.gyro[2] * DEG_TO_RAD
     433       10302 :     };
     434             : 
     435             :     {
     436       10302 :         WITH_SEMAPHORE(state.sem);
     437       10302 :         state.accel = accel;
     438       10302 :         state.gyro  = gyro;
     439       10302 :     }
     440             : 
     441       10302 :     if (imu_ok) {
     442             :         const AP_ExternalAHRS::ins_data_message_t ins {
     443             :             accel,
     444             :             gyro,
     445       10302 :             data.temperature
     446       10302 :         };
     447       10302 :         AP::ins().handle_external(ins);
     448             :     }
     449             : 
     450             : #if AP_BARO_EXTERNALAHRS_ENABLED
     451       10302 :     if (baro_ok) {
     452       10302 :         const AP_ExternalAHRS::baro_data_message_t baro {
     453             :             0,                      // instance
     454       10302 :             data.baro_pressure,
     455       10302 :             data.baro_temperature
     456       10302 :         };
     457       10302 :         AP::baro().handle_external(baro);
     458             :     }
     459             : #endif  // AP_BARO_EXTERNALAHRS_ENABLED
     460             : 
     461             : #if AP_COMPASS_EXTERNALAHRS_ENABLED
     462       10302 :     if (mag_ok) {
     463             :         const AP_ExternalAHRS::mag_data_message_t mag {
     464             :             Vector3f {
     465       10302 :                 data.mag[0],
     466       10302 :                 data.mag[1],
     467       10302 :                 data.mag[2]
     468             :             }
     469       10302 :         };
     470       10302 :         AP::compass().handle_external(mag);
     471             :     }
     472             : #endif  // AP_COMPASS_EXTERNALAHRS_ENABLED
     473             : 
     474             : #if HAL_LOGGING_ENABLED
     475       10302 :     const uint32_t now_ms = AP_HAL::millis();
     476       10302 :     const int16_t log_rate = AP::externalAHRS().log_rate.get();
     477       10302 :     if (log_rate > 0 && now_ms - last_aers_log_ms >= uint32_t(1000U / log_rate)) {
     478        1959 :         last_aers_log_ms = now_ms;
     479             :         // @LoggerMessage: AERS
     480             :         // @Description: Aeron PLX raw sensor data (SENS_PARA)
     481             :         // @Field: TimeUS: Time since system startup
     482             :         // @Field: Temp: Sensor temperature
     483             :         // @Field: GX: Gyro X
     484             :         // @Field: GY: Gyro Y
     485             :         // @Field: GZ: Gyro Z
     486             :         // @Field: AX: Accel X
     487             :         // @Field: AY: Accel Y
     488             :         // @Field: AZ: Accel Z
     489             :         // @Field: MX: Mag X
     490             :         // @Field: MY: Mag Y
     491             :         // @Field: MZ: Mag Z
     492             :         // @Field: Pres: Barometric pressure
     493             :         // @Field: BTmp: Barometer temperature
     494             :         // @Field: BAlt: Barometric altitude
     495        3918 :         AP::logger().WriteStreaming(
     496             :             "AERS",
     497             :             "TimeUS," "Temp," "GX," "GY," "GZ," "AX," "AY," "AZ," "MX," "MY," "MZ," "Pres," "BTmp," "BAlt",
     498             :             "s"       "O"     "k"   "k"   "k"   "o"   "o"   "o"   "G"   "G"   "G"   "P"     "O"     "m",
     499             :             "F"       "-"     "-"   "-"   "-"   "-"   "-"   "-"   "C"   "C"   "C"   "-"     "-"     "-",
     500             :             "Q"       "f"     "f"   "f"   "f"   "f"   "f"   "f"   "f"   "f"   "f"   "f"     "f"     "f",
     501             :             AP_HAL::micros64(),
     502        1959 :             data.temperature,
     503        1959 :             data.gyro[0], data.gyro[1], data.gyro[2],
     504        1959 :             data.accel[0], data.accel[1], data.accel[2],
     505        1959 :             data.mag[0], data.mag[1], data.mag[2],
     506        1959 :             data.baro_pressure,
     507        1959 :             data.baro_temperature,
     508        1959 :             data.baro_altitude
     509             :         );
     510             :     }
     511             : #endif  // HAL_LOGGING_ENABLED
     512       10302 : }
     513             : 
     514             : // GPS_PARA: GNSS status, position, DOP
     515        2066 : void AP_ExternalAHRS_Aeron_plx::publish_gps_para(const GpsParaPayload &data, AeronDeferredMsgs &msgs)
     516             : {
     517        2066 :     last_gnss_ms = AP_HAL::millis();
     518             : 
     519             :     {
     520        2066 :         WITH_SEMAPHORE(arn_sem);
     521        2066 :         shared.gps_epoch_s          = data.epoch_time;
     522        2066 :         shared.gps_microseconds     = data.microseconds;
     523        2066 :         shared.gps_status           = data.gps_status;
     524        2066 :         shared.gps_undulation_m     = data.gps_undulation;
     525        2066 :         shared.gps_hdop             = data.hdop;
     526        2066 :         shared.gps_position[0]      = data.gps_position[0];
     527        2066 :         shared.gps_position[1]      = data.gps_position[1];
     528        2066 :         shared.gps_height_ellip_m   = data.gps_height_abv_ellip;
     529        2066 :     }
     530             : 
     531             :     // Push the gps data in ardupilot gps backend.
     532        2066 :     format_and_push_gps(msgs);
     533        2066 : }
     534             : 
     535             : // EXTD_GNSS: accuracy metrics + GNSS NED velocity
     536        2066 : void AP_ExternalAHRS_Aeron_plx::publish_extd_gnss(const ExtdGnssPayload &data)
     537             : {
     538        2066 :     WITH_SEMAPHORE(arn_sem);
     539             : 
     540        2066 :     shared.cust_hpa_m   = data.hpa;
     541        2066 :     shared.cust_vpa_m   = data.vpa;
     542        2066 :     shared.cust_hva_mps = data.hva;
     543        2066 :     shared.cust_vdop    = data.vdop;
     544        2066 :     memcpy(shared.cust_gnss_vel_ned_mps, data.gnss_vned, sizeof(data.gnss_vned));
     545             : 
     546        2066 :     last_extd_ms = AP_HAL::millis();
     547        2066 : }
     548             : 
     549             : /*
     550             :   EXTD_GNSS freshness. Caller must hold arn_sem.
     551             :  */
     552       11825 : bool AP_ExternalAHRS_Aeron_plx::extd_fresh_locked() const
     553             : {
     554       23639 :     return last_extd_ms != 0 &&
     555       23639 :            (AP_HAL::millis() - last_extd_ms) <= EXTD_VALIDITY_MS;
     556             : }
     557             : 
     558             : /*
     559             :   Decode the PLX GPS status word into an ArduPilot fix type.
     560             : 
     561             :   Bit 10 is the PLX's authoritative "GNSS fix present" flag (manual
     562             :   7.1.2.10, Table 16); bits 0-9 are *visible* satellites, which is not by
     563             :   itself a fix. Both the AP_GPS publication path and get_filter_status()
     564             :   decode through here so they cannot reach different conclusions from the
     565             :   same status word.
     566             :  */
     567       11825 : AP_GPS_FixType AP_ExternalAHRS_Aeron_plx::fix_type_from_status(uint32_t status)
     568             : {
     569       11825 :     if ((status & GPS_STATUS_FIX_BIT) == 0 ||
     570       11825 :         (status & GPS_STATUS_NUMSAT_MASK) == 0) {
     571           0 :         return AP_GPS_FixType::NONE;
     572             :     }
     573             : 
     574       11825 :     switch (AeronGnssFix((status & GPS_STATUS_FIXTYPE_MASK) >> GPS_STATUS_FIXTYPE_SHIFT)) {
     575       11825 :     case AeronGnssFix::GNSS_FIX:
     576       11825 :         return AP_GPS_FixType::FIX_3D;
     577           0 :     case AeronGnssFix::SBAS_FIX:
     578           0 :         return AP_GPS_FixType::DGPS;
     579           0 :     case AeronGnssFix::RTK_FIX:
     580           0 :         return AP_GPS_FixType::RTK_FIXED;
     581           0 :     case AeronGnssFix::RTK_FLOAT:
     582           0 :         return AP_GPS_FixType::RTK_FLOAT;
     583           0 :     case AeronGnssFix::NO_FIX:
     584             :     default:
     585             :         // No fix, or an unrecognised PLX fix code: alive but untrusted.
     586           0 :         return AP_GPS_FixType::NONE;
     587             :     }
     588             : }
     589             : 
     590             : /*
     591             :   Build the ArduPilot GPS message from GPS_PARA + EXTD_GNSS fields.
     592             :  */
     593        2066 : void AP_ExternalAHRS_Aeron_plx::format_and_push_gps(AeronDeferredMsgs &msgs)
     594             : {
     595             :     // One coherent snapshot of the cross-thread cache, taken under
     596             :     // arn_sem and released immediately. Everything below works off the
     597             :     // local copy, so no shared.* field is read while unlocked.
     598             :     SharedData snap;
     599             :     bool extd_stale;
     600             :     {
     601        2066 :         WITH_SEMAPHORE(arn_sem);
     602        2066 :         snap       = shared;
     603        2066 :         extd_stale = !extd_fresh_locked();
     604        2066 :     }
     605             : 
     606        2066 :     const uint32_t st          = snap.gps_status;
     607        2066 :     const uint16_t num_sats    = st & GPS_STATUS_NUMSAT_MASK;
     608        2066 :     const uint8_t  jam         = (st & 0x00030000U) >> 16;
     609        2066 :     const uint8_t  spf         = (st & 0x000C0000U) >> 18;
     610             : 
     611             :     // throttled jamming/spoofing warnings - 0.2 Hz
     612        2066 :     const uint32_t now = AP_HAL::millis();
     613        2066 :     if (now - last_jam_warn_ms > WARN_THROTTLE_MS) {
     614          38 :         last_jam_warn_ms = now;
     615          38 :         if (jam >= uint8_t(AeronJamSpoof::WARNING)) {
     616           0 :             GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Aeron: GNSS jamming level %u", jam);
     617             :         }
     618          38 :         if (spf >= uint8_t(AeronJamSpoof::WARNING)) {
     619           0 :             GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Aeron: GNSS spoofing level %u", spf);
     620             :         }
     621             :     }
     622             : 
     623        2066 :     AP_ExternalAHRS::gps_data_message_t gps_msg{};
     624             : 
     625             :     // Convert the PLX POSIX epoch to GPS week / ms-of-week using
     626             :     // AP_GPS.h own constants. UNIX_OFFSET_MSEC already
     627             :     // folds in the leap-second offset, so no separate leap term is added
     628             :     // here - and the leap count tracks AP_GPS.h global value rather
     629             :     // than a private copy.
     630        2066 :     const uint64_t epoch_ms = uint64_t(snap.gps_epoch_s) * 1000ULL +
     631        2066 :                               (snap.gps_microseconds / 1000ULL);
     632        2066 :     if (epoch_ms >= UNIX_OFFSET_MSEC) {
     633        2066 :         const uint64_t gps_ms = epoch_ms - UNIX_OFFSET_MSEC;
     634        2066 :         gps_msg.gps_week = uint16_t(gps_ms / AP_MSEC_PER_WEEK);
     635        2066 :         gps_msg.ms_tow   = uint32_t(gps_ms % AP_MSEC_PER_WEEK);
     636             :     }
     637             : 
     638             :     // satellites_in_view is uint8_t; the PLX field is 10-bit.
     639        2066 :     gps_msg.satellites_in_view = MIN(num_sats, uint16_t(255));
     640             : 
     641             :     // We have received a GPS_PARA packet, so the receiver is present; no
     642             :     // fix, no satellites, or an unrecognised fix code decodes as NONE
     643             :     // (receiver alive, no position fix).
     644        2066 :     gps_msg.fix_type = fix_type_from_status(st);
     645             : 
     646             :     // accuracy metrics from EXTD_GNSS (fresh - checked above)
     647        2066 :     gps_msg.horizontal_pos_accuracy = snap.cust_hpa_m;
     648        2066 :     gps_msg.vertical_pos_accuracy   = snap.cust_vpa_m;
     649        2066 :     gps_msg.horizontal_vel_accuracy = snap.cust_hva_mps;
     650        2066 :     gps_msg.hdop = snap.gps_hdop  * 1.0e2;
     651        2066 :     gps_msg.vdop = snap.cust_vdop * 1.0e2;
     652             : 
     653             :     // position from GPS_PARA, kept in double end-to-end
     654        2066 :     gps_msg.latitude     = snap.gps_position[0] * 1.0e7;
     655        2066 :     gps_msg.longitude    = snap.gps_position[1] * 1.0e7;
     656        2066 :     gps_msg.msl_altitude = (snap.gps_height_ellip_m - snap.gps_undulation_m) * 1.0e2;
     657             : 
     658             :     // GNSS velocity from EXTD_GNSS (fresh)
     659        2066 :     gps_msg.ned_vel_north = snap.cust_gnss_vel_ned_mps[0];
     660        2066 :     gps_msg.ned_vel_east  = snap.cust_gnss_vel_ned_mps[1];
     661        2066 :     gps_msg.ned_vel_down  = snap.cust_gnss_vel_ned_mps[2];
     662             : 
     663             :     // Set origin on the first 3D fix. The PLX only declares a 3D fix
     664             :     // after its own internal quality checks, so no extra gating here.
     665        2066 :     if (gps_msg.fix_type >= AP_GPS_FixType::FIX_3D) {
     666        2066 :         WITH_SEMAPHORE(state.sem);
     667        2066 :         if (!state.have_origin) {
     668          22 :             state.origin = Location {
     669          11 :                 int32_t(snap.gps_position[0] * 1.0e7),
     670          11 :                 int32_t(snap.gps_position[1] * 1.0e7),
     671          11 :                 int32_t((snap.gps_height_ellip_m - snap.gps_undulation_m) * 1.0e2),
     672             :                 Location::AltFrame::ABSOLUTE
     673             :             };
     674          11 :             state.have_origin = true;
     675             :         }
     676        2066 :     }
     677             : 
     678             :     // If EXTD_GNSS is stale we have no fresh accuracy or GNSS velocity to
     679             :     // attach. Rather than publish a GPS message with fabricated accuracy
     680             :     // and zeroed velocity - which raw-data consumers (DCM, vehicle code)
     681             :     // would read as real - we skip publication entirely and let AP_GPS
     682             :     // time out naturally, which every consumer handles. The
     683             :     // fused INS solution still reaches ArduPilot via state.* (NAV_PARA1).
     684             :     // This will happen in very rare conditions, where device itself is compromised,
     685             :     // as it's hard configured to send these data.
     686        2066 :     if (extd_stale) {
     687             :         // Shout on telemetry that rare condition has occurred.
     688          11 :         msgs.stale_data = true;
     689          11 :         return;
     690             :     }
     691             : 
     692             :     uint8_t instance;
     693        2055 :     if (AP::gps().get_first_external_instance(instance)) {
     694        1700 :         AP::gps().handle_external(gps_msg, instance);
     695             :     }
     696             : 
     697             : #if HAL_LOGGING_ENABLED
     698             :     // AERG follows the GNSS rate (5-10 Hz) so it is logged per-packet.
     699             :     // @LoggerMessage: AERG
     700             :     // @Description: Aeron PLX GNSS status and accuracy (GPS_PARA + EXTD_GNSS)
     701             :     // @Field: TimeUS: Time since system startup
     702             :     // @Field: GMS: Time of week
     703             :     // @Field: GWk: GPS week
     704             :     // @Field: NSat: Satellites in view
     705             :     // @Field: FixT: GPS fix type as reported to AP_GPS
     706             :     // @Field: Jam: Jamming detection level (1=none,2=warn,3=critical)
     707             :     // @Field: Spf: Spoofing detection level (1=none,2=warn,3=critical)
     708             :     // @Field: HPA: Horizontal position accuracy
     709             :     // @Field: VPA: Vertical position accuracy
     710             :     // @Field: HVA: Horizontal velocity accuracy
     711             :     // @Field: HDOP: Horizontal dilution of precision
     712             :     // @Field: VDOP: Vertical dilution of precision
     713        4110 :     AP::logger().WriteStreaming(
     714             :         "AERG",
     715             :         "TimeUS," "GMS," "GWk," "NSat," "FixT," "Jam," "Spf," "HPA," "VPA," "HVA," "HDOP," "VDOP",
     716             :         "s"       "-"    "-"    "-"     "-"     "-"    "-"    "-"    "-"    "-"    "-"     "-",
     717             :         "F"       "-"    "-"    "-"     "-"     "-"    "-"    "-"    "-"    "-"    "-"     "-",
     718             :         "Q"       "I"    "H"    "H"     "B"     "B"    "B"    "f"    "f"    "f"    "f"     "f",
     719             :         AP_HAL::micros64(),
     720             :         gps_msg.ms_tow,
     721        2055 :         gps_msg.gps_week,
     722             :         num_sats,
     723        2055 :         uint8_t(gps_msg.fix_type),
     724             :         jam,
     725             :         spf,
     726        2055 :         snap.cust_hpa_m,
     727        2055 :         snap.cust_vpa_m,
     728        2055 :         snap.cust_hva_mps,
     729        2055 :         snap.gps_hdop,
     730        2055 :         snap.cust_vdop
     731             :     );
     732             : #endif  // HAL_LOGGING_ENABLED
     733             : }
     734             : 
     735       30173 : void AP_ExternalAHRS_Aeron_plx::send_deferred_messages(const AeronDeferredMsgs &msgs)
     736             : {
     737       30173 :     const uint32_t now = AP_HAL::millis();
     738             : 
     739       30173 :     if (msgs.crc_fail && (now - last_crc_warn_ms) >= WARN_THROTTLE_MS) {
     740           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "Aeron: CRC mismatch");
     741           0 :         last_crc_warn_ms = now;
     742             :     }
     743             : 
     744             :     // Malformed-packet warnings share one throttle. A recognised ID with
     745             :     // a short payload is reported distinctly from a truly unknown ID, so
     746             :     // the latter isn't misreported for the former.
     747       30173 :     if ((msgs.bad_length || msgs.unknown_pkt) &&
     748           0 :         (now - last_garbage_warn_ms) >= WARN_THROTTLE_MS) {
     749           0 :         if (msgs.unknown_pkt) {
     750           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "Aeron: Unknown packet 0x%04X", msgs.unknown_id);
     751             :         } else {
     752           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "Aeron: Bad packet length");
     753             :         }
     754           0 :         last_garbage_warn_ms = now;
     755             :     }
     756             : 
     757       30173 :     if (msgs.stale_data && (now - last_stale_data_ms) >= WARN_THROTTLE_MS) {
     758           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "Aeron: EXTD_GNSS Stale data, check packet!");
     759           0 :         last_stale_data_ms = now;
     760             :     }
     761             : 
     762       30173 :     if (msgs.ext_mag_checked) {
     763             :         uint8_t present;
     764             :         {
     765          11 :             WITH_SEMAPHORE(arn_sem);
     766          11 :             present = shared.ext_mag_present;
     767          11 :         }
     768          11 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Aeron: Ext mag present=%u", present);
     769             :     }
     770       30173 :     if (msgs.ext_asp_checked) {
     771             :         uint8_t present;
     772             :         {
     773          11 :             WITH_SEMAPHORE(arn_sem);
     774          11 :             present = shared.ext_asp_present;
     775          11 :         }
     776          11 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Aeron: Ext airspeed present=%u", present);
     777             :     }
     778       30173 : }
     779             : 
     780             : // Fire the external-sensor presence queries once NAV data confirms the
     781             : // PLX is up and producing a fused solution. If NAV data lapses (a device
     782             : // reset or link dropout) and then resumes, re-query so the presence flags
     783             : // reflect the re-initialised device rather than the pre-reset state.
     784       30173 : void AP_ExternalAHRS_Aeron_plx::check_for_query_ext_sensors()
     785             : {
     786             :     // Either NAV packet proves the device is producing a solution; use
     787             :     // whichever arrived most recently as the "device up" timestamp.
     788       30173 :     const uint32_t nav_ms = MAX(last_nav1_ms, last_nav2_ms);
     789             : 
     790             :     // nav_ms == 0 means either no NAV yet, or the timestamp landed exactly
     791             :     // on the millis() rollover (every ~50 days). Both are benign: we
     792             :     // just skip this cycle and resume on the next NAV packet (~20 ms
     793             :     // later). The only cost is at most one deferred re-query check.
     794       30173 :     if (nav_ms == 0) {
     795           0 :         return;
     796             :     }
     797             : 
     798             :     // A jump in the NAV timestamp larger than the reset threshold means
     799             :     // NAV stopped and later resumed - treat as a fresh boot and re-query.
     800             :     // (A reset landing exactly on the 50-day millis() wrap could be
     801             :     // missed here, but that coincidence is very unlikely and
     802             :     // the only consequence is not re-querying optional-sensor presence.)
     803             :     // Presence information gathered before the reset describes the pre-reset
     804             :     // device, so drop it rather than carry it across. report_hw_status() then
     805             :     // keeps the EXT_* flags unreportable until the re-query is answered.
     806       30173 :     if (prev_nav_ms != 0 && (nav_ms - prev_nav_ms) > DEVICE_RESET_GAP_MS) {
     807           0 :         WITH_SEMAPHORE(arn_sem);
     808           0 :         shared.ext_mag_present = 0;
     809           0 :         shared.ext_asp_present = 0;
     810           0 :         shared.ext_mag_checked = false;
     811           0 :         shared.ext_asp_checked = false;
     812           0 :         ext_query_tries        = 0;
     813           0 :         last_ext_query_ms      = 0;
     814           0 :     }
     815       30173 :     prev_nav_ms = nav_ms;
     816             : 
     817             :     bool mag_checked;
     818             :     bool asp_checked;
     819             :     {
     820       30173 :         WITH_SEMAPHORE(arn_sem);
     821       30173 :         mag_checked = shared.ext_mag_checked;
     822       30173 :         asp_checked = shared.ext_asp_checked;
     823       30173 :     }
     824             : 
     825             :     // Nothing outstanding, or we have exhausted the retry budget.
     826       30173 :     if ((mag_checked && asp_checked) || ext_query_tries >= EXT_QUERY_MAX_TRIES) {
     827       30162 :         return;
     828             :     }
     829             : 
     830          11 :     const uint32_t now = AP_HAL::millis();
     831          11 :     if (last_ext_query_ms != 0 && (now - last_ext_query_ms) < EXT_QUERY_RETRY_MS) {
     832           0 :         return;
     833             :     }
     834             : 
     835             :     // Announce once - the retries are silent so a device that never answers
     836             :     // does not spam the GCS.
     837          11 :     if (ext_query_tries == 0) {
     838          11 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Aeron: External sensor query");
     839             :     }
     840             : 
     841             :     // Counted even when the write is short, so a persistently backed-up port
     842             :     // cannot retry forever.
     843          11 :     last_ext_query_ms = now;
     844          11 :     ext_query_tries++;
     845          11 :     if (!query_ext_sensors(!mag_checked, !asp_checked)) {
     846           0 :         GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Aeron: Ext sensor query write failed");
     847             :     }
     848             : }
     849             : 
     850          11 : bool AP_ExternalAHRS_Aeron_plx::query_ext_sensors(bool want_mag, bool want_asp)
     851             : {
     852             :     // External sensor presence queries - fixed packets sent once at startup
     853             :     static const uint8_t cmd_query_ext_mag[] = {
     854             :         0x05, 0x05, 0x05, 0x05, 0x00, 0x0A, 0x00, 0xA1, 0x01, 0x17, 0xBD
     855             :     };
     856             : 
     857             :     static const uint8_t cmd_query_ext_asp[] = {
     858             :         0x05, 0x05, 0x05, 0x05, 0x00, 0x0A, 0x00, 0xA1, 0x03, 0x62, 0xCA
     859             :     };
     860             : 
     861             :     // Only the outstanding queries are re-sent, so a sensor that has already
     862             :     // answered is not asked again.
     863          11 :     bool all_sent = true;
     864             : 
     865          22 :     if (want_mag &&
     866          11 :         uart->write(cmd_query_ext_mag, sizeof(cmd_query_ext_mag)) != sizeof(cmd_query_ext_mag)) {
     867           0 :         all_sent = false;
     868             :     }
     869          22 :     if (want_asp &&
     870          11 :         uart->write(cmd_query_ext_asp, sizeof(cmd_query_ext_asp)) != sizeof(cmd_query_ext_asp)) {
     871           0 :         all_sent = false;
     872             :     }
     873             : 
     874          11 :     return all_sent;
     875             : }
     876             : 
     877           0 : const char *AP_ExternalAHRS_Aeron_plx::get_name() const
     878             : {
     879           0 :     return "Aeron-PLX3";
     880             : }
     881             : 
     882          33 : int8_t AP_ExternalAHRS_Aeron_plx::get_port() const
     883             : {
     884          33 :     return uart ? port_num : -1;
     885             : }
     886             : 
     887       35894 : bool AP_ExternalAHRS_Aeron_plx::initialised() const
     888             : {
     889       35894 :     if (!setup_complete) {
     890           0 :         return false;
     891             :     }
     892             :     // EXTD_GNSS is required, not optional: without it format_and_push_gps()
     893             :     // publishes no GPS message at all, so reporting initialised before the
     894             :     // first one arrives would claim readiness while AP_GPS has never had a
     895             :     // single sample.
     896             :     uint32_t extd_ms;
     897             :     {
     898       35894 :         WITH_SEMAPHORE(arn_sem);
     899       35894 :         extd_ms = last_extd_ms;
     900       35894 :     }
     901             : 
     902       35894 :     return last_sens_ms  != 0
     903       35894 :         && last_nav1_ms  != 0
     904       35894 :         && last_nav2_ms  != 0
     905       35894 :         && last_gnss_ms  != 0
     906       71788 :         && extd_ms       != 0;
     907             : }
     908             : 
     909             : // hw_status reporting table.  Order is not significant - report_hw_status
     910             : // walks every row to compute both the effective error mask and the
     911             : // fault / recovery messages.
     912             : const AP_ExternalAHRS_Aeron_plx::HwStatusEntry
     913             : AP_ExternalAHRS_Aeron_plx::hw_status_table[] = {
     914             :     { AeronHwStatus::GNSS,     MAV_SEVERITY_NOTICE,    "GNSS unhealthy",             "GNSS okay" },
     915             :     { AeronHwStatus::ACC,      MAV_SEVERITY_ERROR,     "Accelerometer unhealthy",    "Accel okay" },
     916             :     { AeronHwStatus::GYR,      MAV_SEVERITY_ERROR,     "Gyroscope unhealthy",        "Gyro okay" },
     917             :     { AeronHwStatus::MAG,      MAV_SEVERITY_ERROR,     "Magnetometer unhealthy",     "Mag okay" },
     918             :     { AeronHwStatus::BARO,     MAV_SEVERITY_ERROR,     "Barometer unhealthy",        "Baro okay" },
     919             :     { AeronHwStatus::SUP_VLTG, MAV_SEVERITY_WARNING,   "Supply Voltage Error",       "Supply voltage okay" },
     920             :     { AeronHwStatus::CM_PRT,   MAV_SEVERITY_WARNING,   "Comport Overrun",            "Comport okay" },
     921             :     { AeronHwStatus::RAM,      MAV_SEVERITY_EMERGENCY, "RAM Failure",                nullptr },
     922             :     { AeronHwStatus::FIRMWARE, MAV_SEVERITY_EMERGENCY, "Firmware Failure",           nullptr },
     923             :     { AeronHwStatus::CONFIG,   MAV_SEVERITY_EMERGENCY, "Config Memory Failure",      nullptr },
     924             :     { AeronHwStatus::EXT_MAG,  MAV_SEVERITY_WARNING,   "Ext Mag not connected",      "Ext Mag connected" },
     925             :     { AeronHwStatus::EXT_ASP,  MAV_SEVERITY_WARNING,   "Ext Airspeed not connected", "Ext Airspeed connected" },
     926             : };
     927             : 
     928             : /*
     929             :   Hardware-status reporting. Emits GCS warnings for any error flag in
     930             :   NAV_PARA2.hw_status, with rising-edge + per-flag-rate-limited
     931             :   semantics.
     932             : 
     933             :   Reporting strategy:
     934             :     - Rising edge (flag 0 -> 1):       report immediately, once.
     935             :     - Persistent fault (flag stays 1): repeat every HEALTH_REPEAT_INTERVAL_MS.
     936             :     - Falling edge (flag 1 -> 0):      log "okay" (if a clear_msg is set)
     937             :                                        and stop reporting.
     938             : 
     939             :   Most hw_status flags do NOT fail healthy() - the PLX continues
     940             :   navigation with degraded sensor inputs, so this routine only emits
     941             :   informational/warning GCS messages for them. There are two exceptions:
     942             :   the flags in HW_STATUS_FATAL_MASK (RAM, firmware, config memory, accel,
     943             :   gyro), which healthy() rejects outright, and MAG/BARO, which do not fail
     944             :   health but do stop their raw samples being forwarded to ArduPilot's own
     945             :   estimators (see publish_sens_para).
     946             :  */
     947       43967 : void AP_ExternalAHRS_Aeron_plx::report_hw_status()
     948             : {
     949             :     // last_warned_ms is sized HW_STATUS_COUNT and indexed by table row, so
     950             :     // the two must stay in lockstep. Adding a table entry without bumping
     951             :     // the constant (or vice versa) trips this at compile time.
     952             :     static_assert(ARRAY_SIZE(hw_status_table) == HW_STATUS_COUNT,
     953             :                     "HW_STATUS_COUNT must equal the number of hw_status_table rows");
     954             : 
     955       43967 :     const uint32_t now = AP_HAL::millis();
     956             : 
     957             :     // Snapshot the shared status fields under the lock so a publish
     958             :     // from the same thread (publish_nav_para2) can't tear us. Same
     959             :     // thread, but the lock is conventional protection that documents
     960             :     // the contract.
     961             :     uint32_t hw;
     962             :     uint8_t  ext_mag_present;
     963             :     uint8_t  ext_asp_present;
     964             :     bool     ext_mag_checked;
     965             :     bool     ext_asp_checked;
     966             :     {
     967       43967 :         WITH_SEMAPHORE(arn_sem);
     968       43967 :         hw              = shared.nav2_hw_status;
     969       43967 :         ext_mag_present = shared.ext_mag_present;
     970       43967 :         ext_asp_present = shared.ext_asp_present;
     971       43967 :         ext_mag_checked = shared.ext_mag_checked;
     972       43967 :         ext_asp_checked = shared.ext_asp_checked;
     973       43967 :     }
     974             : 
     975             :     // Build the effective error mask from the table, then drop the
     976             :     // EXT_* flags unless the user actually has those sensors configured.
     977       43967 :     uint32_t effective_mask = 0;
     978      571571 :     for (const auto &entry : hw_status_table) {
     979      527604 :         effective_mask |= 1U << uint8_t(entry.status);
     980             :     }
     981       43967 :     if (!(ext_mag_checked && ext_mag_present)) {
     982          31 :         effective_mask &= ~(1U << uint8_t(AeronHwStatus::EXT_MAG));
     983             :     }
     984       43967 :     if (!(ext_asp_checked && ext_asp_present)) {
     985          31 :         effective_mask &= ~(1U << uint8_t(AeronHwStatus::EXT_ASP));
     986             :     }
     987             : 
     988       43967 :     const uint32_t errors      = hw & effective_mask;
     989       43967 :     const uint32_t newly_set   = errors & ~last_warned_stat;
     990       43967 :     const uint32_t newly_clear = last_warned_stat & ~errors;
     991             : 
     992      571571 :     for (uint8_t row = 0; row < ARRAY_SIZE(hw_status_table); row++) {
     993      527604 :         const HwStatusEntry &entry = hw_status_table[row];
     994      527604 :         const uint32_t mask        = 1U << uint8_t(entry.status);
     995             : 
     996             :         // Recovery message on falling edge.
     997      527604 :         if ((newly_clear & mask) != 0U && entry.clear_msg != nullptr) {
     998           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Aeron: %s", entry.clear_msg);
     999             :         }
    1000             : 
    1001      527604 :         if ((errors & mask) == 0U) {
    1002      527604 :             continue;
    1003             :         }
    1004             : 
    1005             :         // Currently in fault: emit on rising edge or after the repeat
    1006             :         // interval has elapsed. last_warned_ms is indexed by table row so
    1007             :         // its size tracks the number of entries, not the enum bit range.
    1008           0 :         const bool rising  = (newly_set & mask) != 0U;
    1009           0 :         const bool overdue = (now - last_warned_ms[row]) >= HEALTH_REPEAT_INTERVAL_MS;
    1010           0 :         if (!rising && !overdue) {
    1011           0 :             continue;
    1012             :         }
    1013             : 
    1014           0 :         GCS_SEND_TEXT(MAV_SEVERITY(entry.severity), "Aeron: %s", entry.fault_msg);
    1015           0 :         last_warned_ms[row] = now;
    1016             :     }
    1017             : 
    1018       43967 :     last_warned_stat = errors;
    1019       43967 : }
    1020             : 
    1021             : /*
    1022             :   Health check. Pure stale-timestamp test.
    1023             :  */
    1024       17972 : bool AP_ExternalAHRS_Aeron_plx::healthy() const
    1025             : {
    1026       17972 :     if (!initialised()) {
    1027           0 :         return false;
    1028             :     }
    1029             : 
    1030             :     // A fault in HW_STATUS_FATAL_MASK means the PLX's own solution is
    1031             :     // untrustworthy, so it fails health rather than only raising a GCS
    1032             :     // warning. See the mask definition for which flags qualify and why.
    1033             :     // The remaining flags stay informational because the PLX continues
    1034             :     // navigating with degraded sensor inputs.
    1035             :     uint32_t hw;
    1036             :     {
    1037       17972 :         WITH_SEMAPHORE(arn_sem);
    1038       17972 :         hw = shared.nav2_hw_status;
    1039       17972 :     }
    1040       17972 :     if ((hw & HW_STATUS_FATAL_MASK) != 0) {
    1041           0 :         return false;
    1042             :     }
    1043             : 
    1044       17972 :     const uint32_t now = AP_HAL::millis();
    1045             :     // Both NAV streams are checked independently: NAV_PARA1 carries
    1046             :     // position/velocity/attitude and NAV_PARA2 the quaternion + hw_status.
    1047             :     // Losing either must trip unhealthy, otherwise stale data from the
    1048             :     // lost stream would keep publishing at arbitrary age.
    1049       17972 :     return (now - last_sens_ms)  < SENS_TIMEOUT_MS
    1050       17972 :         && (now - last_nav1_ms)  < NAV_TIMEOUT_MS
    1051       17972 :         && (now - last_nav2_ms)  < NAV_TIMEOUT_MS
    1052       35944 :         && (now - last_gnss_ms)  < GNSS_TIMEOUT_MS;
    1053             : }
    1054             : 
    1055             : /*
    1056             :   Pre-arm check.
    1057             : 
    1058             :   GNSS-related warnings (no lock, jamming, spoofing) are surfaced via
    1059             :   throttled GCS_SEND_TEXT in format_and_push_gps(), because the PLX
    1060             :   can navigate in GNSS-denied mode and we want those warnings to be
    1061             :   informational rather than blocking.
    1062             :  */
    1063          50 : bool AP_ExternalAHRS_Aeron_plx::pre_arm_check(char *failure_msg, uint8_t failure_msg_len) const
    1064             : {
    1065          50 :     if (!setup_complete) {
    1066           0 :         hal.util->snprintf(failure_msg, failure_msg_len, "Aeron Not Initialised");
    1067           0 :         return false;
    1068             :     }
    1069          50 :     if (!healthy()) {
    1070           0 :         hal.util->snprintf(failure_msg, failure_msg_len, "Aeron Unhealthy");
    1071           0 :         return false;
    1072             :     }
    1073          50 :     return true;
    1074             : }
    1075             : 
    1076             : /*
    1077             :   Filter status.
    1078             : 
    1079             :   Reaching the body below means healthy() returned true, which already
    1080             :   guarantees NAV_PARA1, NAV_PARA2, SENS and GNSS are all within their
    1081             :   staleness deadlines. So the PLX's fused INS solution is currently
    1082             :   valid, and every estimate it continuously produces - attitude, 3D
    1083             :   velocity, and relative position (by inertial integration and aidings) - is
    1084             :   available. Those flags are therefore set whenever healthy, not
    1085             :   unconditionally.
    1086             : 
    1087             :   horiz_pos_abs / pred_horiz_pos_abs are the fused absolute position,
    1088             :   which the INS holds through short GNSS outages by dead reckoning, so
    1089             :   they follow the (healthy) NAV solution rather than the GNSS fix.
    1090             :   using_gps / gps_quality_good track the raw GNSS fix bit specifically.
    1091             :  */
    1092        9759 : void AP_ExternalAHRS_Aeron_plx::get_filter_status(nav_filter_status &status) const
    1093             : {
    1094        9759 :     memset(&status, 0, sizeof(status));
    1095             : 
    1096        9759 :     if (!initialised()) {
    1097           0 :         return;
    1098             :     }
    1099             : 
    1100        9759 :     status.flags.initalized = true;
    1101             : 
    1102        9759 :     if (!healthy()) {
    1103           0 :         return;
    1104             :     }
    1105             : 
    1106             :     bool fix;
    1107             :     {
    1108        9759 :         WITH_SEMAPHORE(arn_sem);
    1109             :         // EXTD_GNSS gone stale means format_and_push_gps() publishes nothing,
    1110             :         // so ArduPilot is not receiving GPS regardless of what the PLX's own
    1111             :         // status word says. The status word itself is decoded by the same
    1112             :         // helper the AP_GPS path uses, so the two cannot disagree.
    1113       19518 :         fix = extd_fresh_locked() &&
    1114        9759 :               fix_type_from_status(shared.gps_status) >= AP_GPS_FixType::FIX_3D;
    1115        9759 :     }
    1116             : 
    1117        9759 :     status.flags.attitude            = true;
    1118        9759 :     status.flags.horiz_vel           = true;
    1119        9759 :     status.flags.vert_vel            = true;
    1120        9759 :     status.flags.horiz_pos_rel       = true;
    1121        9759 :     status.flags.vert_pos            = true;
    1122        9759 :     status.flags.pred_horiz_pos_rel  = true;
    1123        9759 :     status.flags.using_gps           = fix;
    1124        9759 :     status.flags.gps_quality_good    = fix;
    1125             : 
    1126             :     // healthy() guarantees NAV is fresh, so the fused absolute position
    1127             :     // is available now (held through GNSS outages by dead reckoning).
    1128        9759 :     status.flags.horiz_pos_abs      = true;
    1129        9759 :     status.flags.pred_horiz_pos_abs = true;
    1130             : }
    1131             : 
    1132             : /*
    1133             :   Variance estimates from EXTD_GNSS accuracy metrics. Returns false
    1134             :   until the first EXTD_GNSS packet arrives so ArduPilot does not
    1135             :   over-trust the solution during initialisation when accuracy fields
    1136             :   are still zero.
    1137             : 
    1138             :   The PLX3-N INS does not expose EKF innovation variances, returning nothing
    1139             :   leaves the report unpopulated. We therefore map the PLX3's own GNSS accuracy
    1140             :   estimates onto variance fields as a best effort health signal.
    1141             : 
    1142             :   These are accuracy-derived values, not filter innovations.
    1143             :  */
    1144        9759 : bool AP_ExternalAHRS_Aeron_plx::get_variances(float &velVar, float &posVar,
    1145             :                                               float &hgtVar, Vector3f &magVar,
    1146             :                                               float &tasVar) const
    1147             : {
    1148             :     float hpa;
    1149             :     float vpa;
    1150             :     float hva;
    1151             :     {
    1152        9759 :         WITH_SEMAPHORE(arn_sem);
    1153             : 
    1154             :         // Report only while EXTD_GNSS is fresh: before the first packet
    1155             :         // (last_extd_ms == 0) or once it has gone stale, the accuracy
    1156             :         // values are meaningless and we return false rather than feed the
    1157             :         // EKF a stale variance.
    1158       19518 :         if (last_extd_ms == 0 ||
    1159        9759 :             (AP_HAL::millis() - last_extd_ms) > EXTD_VALIDITY_MS) {
    1160           0 :             return false;
    1161             :         }
    1162             : 
    1163        9759 :         hpa = shared.cust_hpa_m;
    1164        9759 :         vpa = shared.cust_vpa_m;
    1165        9759 :         hva = shared.cust_hva_mps;
    1166        9759 :     }
    1167             : 
    1168        9759 :     velVar = hva * vel_gate_scale;
    1169        9759 :     posVar = hpa * pos_gate_scale;
    1170        9759 :     hgtVar = vpa * hgt_gate_scale;
    1171        9759 :     magVar.zero();
    1172        9759 :     tasVar = 0;
    1173        9759 :     return true;
    1174             : }
    1175             : 
    1176             : #endif  // AP_EXTERNAL_AHRS_AERON_PLX_ENABLED

Generated by: LCOV version 1.14