LCOV - code coverage report
Current view: top level - SITL - SIM_Aeron.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 175 182 96.2 %
Date: 2026-09-09 11:02:08 Functions: 13 13 100.0 %

          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             :   SITL simulation of Aeron Systems PLX3 series INS
      20             : */
      21             : 
      22             : #include "SIM_Aeron.h"
      23             : 
      24             : #if AP_SIM_AERON_ENABLED
      25             : 
      26             : #include <AP_Baro/AP_Baro.h>
      27             : #include <AP_Math/AP_Math.h>
      28             : #include <AP_Math/crc.h>
      29             : #include <GCS_MAVLink/GCS.h>
      30             : 
      31             : #include <stdio.h>
      32             : #include <string.h>
      33             : #include <sys/time.h>
      34             : 
      35             : using namespace SITL;
      36             : 
      37          53 : Aeron::Aeron() :
      38          53 :     SerialDevice::SerialDevice(2048, 512)
      39             : {
      40          53 : }
      41             : 
      42             : /*
      43             :   produce a timeval that advances with the simulation clock
      44             :  */
      45      111409 : void Aeron::simulation_timeval(struct timeval *tv)
      46             : {
      47      111409 :     const uint64_t now = AP_HAL::micros64();
      48             :     static uint64_t first_usec;
      49             :     static struct timeval first_tv;
      50      111409 :     if (first_usec == 0) {
      51          53 :         first_usec = now;
      52          53 :         first_tv.tv_sec = AP::sitl()->start_time_UTC;
      53             :     }
      54      111409 :     *tv = first_tv;
      55      111409 :     tv->tv_sec += now / 1000000ULL;
      56      111409 :     const uint64_t new_usec = tv->tv_usec + (now % 1000000ULL);
      57      111409 :     tv->tv_sec += new_usec / 1000000ULL;
      58      111409 :     tv->tv_usec = new_usec % 1000000ULL;
      59      111409 : }
      60             : 
      61             : /*
      62             :   pack the GPS status word consumed by AP_ExternalAHRS_Aeron::format_and_push_gps()
      63             : 
      64             :       bits  0..9  : satellites in use         (mask 0x000003FF)
      65             :       bit  10     : fix present               (mask 0x00000400)
      66             :       bits 11..14 : raw fix type              (mask 0x00007800)
      67             :       bits 16..17 : jamming detection level   (mask 0x00030000)
      68             :       bits 18..19 : spoofing detection level  (mask 0x000C0000)
      69             :  */
      70        6987 : uint32_t Aeron::build_gps_status(uint16_t num_sats, bool fix_present,
      71             :                                  uint8_t fix_raw, uint8_t jam, uint8_t spoof)
      72             : {
      73        6987 :     uint32_t status = uint32_t(num_sats) & 0x3FFu;
      74        6987 :     if (fix_present) {
      75        6987 :         status |= (1u << 10);
      76             :     }
      77        6987 :     status |= (uint32_t(fix_raw)  & 0x0Fu) << 11;
      78        6987 :     status |= (uint32_t(jam)      & 0x03u) << 16;
      79        6987 :     status |= (uint32_t(spoof)    & 0x03u) << 18;
      80        6987 :     return status;
      81             : }
      82             : 
      83             : // Build a complete Aeron frame in a stack buffer and emit it as one
      84             : // contiguous write_to_autopilot() call.
      85      118418 : void Aeron::send_aeron_packet(PktID prp_id, const void *payload,
      86             :                               uint16_t payload_len)
      87             : {
      88             :     // 4 sync + 2 length + 2 PRP_ID + payload + 1 CRC
      89             :     uint8_t buf[8 + sizeof(NavPara2Payload) + 1];
      90      118418 :     const uint16_t total_len = 8 + payload_len;          // length field (excludes CRC)
      91      118418 :     const uint16_t on_wire   = total_len + 1;            // bytes actually transmitted
      92             : 
      93             :     // sanity guard against a future enum getting out of step with the
      94             :     // structs.  Panic rather than silently dropping
      95      118418 :     if (on_wire > sizeof(buf)) {
      96           0 :         AP_HAL::panic("Aeron SIM: packet too large for tx buffer (%u > %u)",
      97             :                       unsigned(on_wire), unsigned(sizeof(buf)));
      98             :     }
      99             : 
     100             :     // sync bytes
     101      592090 :     for (uint8_t index = 0; index < SYNC_COUNT; index++) {
     102      473672 :         buf[index] = SYNC_BYTE;
     103             :     }
     104             : 
     105             :     // length (BE)
     106      118418 :     buf[4] = uint8_t(total_len >> 8);
     107      118418 :     buf[5] = uint8_t(total_len & 0xFF);
     108             : 
     109             :     // PRP_ID (BE)
     110      118418 :     const uint16_t id = uint16_t(prp_id);
     111      118418 :     buf[6] = uint8_t(id >> 8);
     112      118418 :     buf[7] = uint8_t(id & 0xFF);
     113             : 
     114             :     // payload (LE on both SITL and the MCU)
     115      118418 :     if (payload_len > 0) {
     116      118418 :         memcpy(&buf[8], payload, payload_len);
     117             :     }
     118             : 
     119             :     // CRC = XOR over [0 .. total_len-1]
     120      118418 :     buf[total_len] = crc_xor_of_bytes(buf, total_len);
     121             : 
     122      118418 :     write_to_autopilot((const char *)buf, on_wire);
     123      118418 : }
     124             : 
     125             : // NAV_PARA1: fused euler/course/velocity/position.
     126       34835 : void Aeron::send_nav_para1()
     127             : {
     128       34835 :     const auto &fdm = _sitl->state;
     129             : 
     130             :     struct timeval tv;
     131       34835 :     simulation_timeval(&tv);
     132             : 
     133             :     // course over ground [deg], wrapped 0..360
     134       34835 :     const float track_deg = wrap_360(degrees(atan2f(fdm.speedE, fdm.speedN)));
     135             : 
     136       34835 :     const NavPara1Payload p {
     137       34835 :         uint32_t(tv.tv_sec),
     138       34835 :         uint32_t(tv.tv_usec),
     139             :         0,                                  // ins_status
     140             :         0,                                  // hw_status
     141      139340 :         { float(radians(fdm.rollDeg)), float(radians(fdm.pitchDeg)), float(radians(fdm.yawDeg)) },
     142             :         track_deg,
     143       34835 :         { float(fdm.speedN), float(fdm.speedE), float(fdm.speedD) },
     144       34835 :         { fdm.latitude, fdm.longitude },
     145             :         // FDM altitude is MSL; PLX3 reports height-above-ellipsoid. With no
     146             :         // simulated geoid model we treat undulation = 0 (GPS_PARA matches).
     147       34835 :         float(fdm.altitude)
     148       34835 :     };
     149             : 
     150       34835 :     send_aeron_packet(PktID::NAV_PARA1, &p, sizeof(p));
     151       34835 : }
     152             : 
     153             : // NAV_PARA2: quaternion, body-frame velocity, ECEF position.
     154       34794 : void Aeron::send_nav_para2()
     155             : {
     156       34794 :     const auto &fdm = _sitl->state;
     157             : 
     158             :     struct timeval tv;
     159       34794 :     simulation_timeval(&tv);
     160             : 
     161             :     // rotate NED velocity into body frame
     162       34794 :     Vector3f v_body { float(fdm.speedN), float(fdm.speedE), float(fdm.speedD) };
     163       34794 :     fdm.quaternion.earth_to_body(v_body);
     164             : 
     165             :     // ECEF via the AP_Math helper
     166       34794 :     const Vector3d llh { radians(fdm.latitude), radians(fdm.longitude), double(fdm.altitude) };
     167       34794 :     Vector3d ecef_v;
     168       34794 :     wgsllh2ecef(llh, ecef_v);
     169             : 
     170       34794 :     const NavPara2Payload p {
     171       34794 :         uint32_t(tv.tv_sec),
     172       34794 :         uint32_t(tv.tv_usec),
     173             :         0,                                  // ins_status
     174             :         0,                                  // hw_status
     175             :         // quaternion (w, x, y, z)
     176       34794 :         { fdm.quaternion.q1, fdm.quaternion.q2, fdm.quaternion.q3, fdm.quaternion.q4 },
     177       34794 :         { v_body.x, v_body.y, v_body.z },
     178       34794 :         { ecef_v.x, ecef_v.y, ecef_v.z },
     179       34794 :         float(fdm.altitude)
     180       34794 :     };
     181             : 
     182       34794 :     send_aeron_packet(PktID::NAV_PARA2, &p, sizeof(p));
     183       34794 : }
     184             : 
     185             : // SENS_PARA: raw IMU / mag / baro. Rate-critical for EAHRS_RATE.
     186       34793 : void Aeron::send_sens_para()
     187             : {
     188       34793 :     const auto &fdm = _sitl->state;
     189             : 
     190             :     struct timeval tv;
     191       34793 :     simulation_timeval(&tv);
     192             : 
     193             :     // synthesise pressure & temperature from altitude
     194       34793 :     float pressure_pa = 0.0f;
     195       34793 :     float temp_K      = 0.0f;
     196       34793 :     AP_Baro::get_pressure_temperature_for_alt_amsl(fdm.altitude, pressure_pa, temp_K);
     197       34793 :     const float temp_C = KELVIN_TO_C(temp_K);
     198             : 
     199             :     const SensParaPayload p {
     200       34793 :         uint32_t(tv.tv_sec),
     201       34793 :         uint32_t(tv.tv_usec),
     202             :         temp_C,
     203             :         // gyro [deg/s], body frame (FDM rates already deg/s)
     204       34793 :         { float(fdm.rollRate), float(fdm.pitchRate), float(fdm.yawRate) },
     205             :         // accel [m/s^2], body frame
     206       34793 :         { float(fdm.xAccel), float(fdm.yAccel), float(fdm.zAccel) },
     207             :         // mag [mGauss], body frame (bodyMagField already mGauss)
     208       34793 :         { float(fdm.bodyMagField.x), float(fdm.bodyMagField.y), float(fdm.bodyMagField.z) },
     209             :         // euler_rates: small-angle approx; driver ignores these
     210       34793 :         { float(fdm.rollRate), float(fdm.pitchRate), float(fdm.yawRate) },
     211       69586 :         pressure_pa + rand_float() * 0.01f,
     212       69586 :         temp_C + rand_float() * 0.001f,
     213       34793 :         float(fdm.altitude)
     214       34793 :     };
     215             : 
     216       34793 :     send_aeron_packet(PktID::SENS_PARA, &p, sizeof(p));
     217       34793 : }
     218             : 
     219             : /*
     220             :   GPS_PARA — GNSS solution, status, DOP
     221             :  */
     222        6987 : void Aeron::send_gps_para()
     223             : {
     224        6987 :     const auto &fdm = _sitl->state;
     225             : 
     226             :     struct timeval tv;
     227        6987 :     simulation_timeval(&tv);
     228             : 
     229        6987 :     const GpsParaPayload p {
     230        6987 :         uint32_t(tv.tv_sec),
     231        6987 :         uint32_t(tv.tv_usec),
     232        6987 :         build_gps_status(SIM_NUM_SATS, /*fix_present=*/true,
     233             :                          SIM_FIX_RAW, SIM_JAM_STATUS, SIM_SPOOF_LEVEL),
     234        6987 :         { fdm.latitude, fdm.longitude },
     235        6987 :         float(fdm.altitude),
     236             :         0.0f,                               // undulation
     237             :         SIM_PDOP,
     238             :         SIM_HDOP,
     239             :         SIM_GDOP
     240        6987 :     };
     241             : 
     242        6987 :     send_aeron_packet(PktID::GPS_PARA, &p, sizeof(p));
     243        6987 : }
     244             : 
     245             : // EXTD_GNSS: accuracy metrics + GNSS NED velocity. Drives the accuracy
     246             : // fields and GNSS velocity in the constructed GPS message.
     247        6987 : void Aeron::send_extd_gnss()
     248             : {
     249        6987 :     const auto &fdm = _sitl->state;
     250             : 
     251        6987 :     const ExtdGnssPayload p {
     252             :         SIM_HPA_M,
     253             :         SIM_VPA_M,
     254             :         SIM_HVA_MPS,
     255             :         SIM_VDOP,
     256        6987 :         { fdm.speedN, fdm.speedE, fdm.speedD }
     257        6987 :     };
     258             : 
     259        6987 :     send_aeron_packet(PktID::EXTD_GNSS, &p, sizeof(p));
     260        6987 : }
     261             : 
     262             : /*
     263             :   Send Aeron data — drained at every SIMState step.
     264             :  */
     265      835635 : void Aeron::update(void)
     266             : {
     267      835635 :     if (!init_sitl_pointer()) {
     268           0 :         return;
     269             :     }
     270             : 
     271             :     // 1 kHz tick gate
     272      835635 :     const uint64_t tick_now = (AP_HAL::micros64() + 500) / 1000;
     273      835635 :     if (tick_now <= _tick) {
     274      139585 :         return;
     275             :     }
     276      696050 :     _tick = tick_now;
     277             : 
     278             :     // Drive the inbound parser.  The driver sends EXT_SENSORS_* query
     279             :     // commands a few seconds after boot; handle_inbound_packet replies
     280             :     // with the matching presence frame so the driver's response path
     281             :     // gets exercised in SITL.
     282      696050 :     process_inbound();
     283             : 
     284             :     // Phased emission.
     285      696050 :     if ((_tick % SENS_PERIOD_MS) == SENS_PHASE_MS) {
     286       34793 :         send_sens_para();
     287      661257 :     } else if ((_tick % NAV1_PERIOD_MS) == NAV1_PHASE_MS) {
     288       34835 :         send_nav_para1();
     289      626422 :     } else if ((_tick % NAV2_PERIOD_MS) == NAV2_PHASE_MS) {
     290       34794 :         send_nav_para2();
     291      591628 :     } else if ((_tick % GPS_PERIOD_MS) == GPS_PHASE_MS) {
     292        6987 :         send_gps_para();
     293      584641 :     } else if ((_tick % EXTD_PERIOD_MS) == EXTD_PHASE_MS) {
     294        6987 :         send_extd_gnss();
     295             :     }
     296             : }
     297             : 
     298             : // Drain whatever the driver has sent us this tick.  Each byte feeds a
     299             : // minimal state-machine that mirrors the on-wire framing handled by
     300             : // AP_ExternalAHRS_Aeron_plx::parse_byte.
     301      696050 : void Aeron::process_inbound()
     302             : {
     303             :     char buf[64];
     304             :     ssize_t num;
     305      696062 :     while ((num = read_from_autopilot(buf, sizeof(buf))) > 0) {
     306         254 :         for (ssize_t index = 0; index < num; index++) {
     307         242 :             handle_inbound_byte(uint8_t(buf[index]));
     308             :         }
     309             :     }
     310      696050 : }
     311             : 
     312         242 : void Aeron::handle_inbound_byte(uint8_t byte)
     313             : {
     314         242 :     switch (rx_state) {
     315          22 :     case RxParseState::RESET:
     316          22 :         rx_write_idx    = 0;
     317          22 :         rx_pkt_len      = 0;
     318          22 :         rx_sync_count   = 0;
     319          22 :         rx_state        = RxParseState::SYNC;
     320             :         FALLTHROUGH;
     321             : 
     322          88 :     case RxParseState::SYNC:
     323          88 :         if (byte == SYNC_BYTE) {
     324          88 :             rx_buf[rx_write_idx++] = byte;
     325          88 :             if (++rx_sync_count == SYNC_COUNT) {
     326          22 :                 rx_state      = RxParseState::LEN_HIGH;
     327             :             }
     328             :         } else {
     329           0 :             rx_state = RxParseState::RESET;
     330             :         }
     331          88 :         return;
     332             : 
     333          22 :     case RxParseState::LEN_HIGH:
     334          22 :         rx_pkt_len              = byte;
     335          22 :         rx_buf[rx_write_idx++]  = byte;
     336          22 :         rx_state                = RxParseState::LEN_LOW;
     337          22 :         return;
     338             : 
     339          22 :     case RxParseState::LEN_LOW:
     340          22 :         rx_buf[rx_write_idx++] = byte;
     341          22 :         rx_pkt_len = (rx_pkt_len << 8) | byte;
     342          22 :         if (rx_pkt_len >= 8 && rx_pkt_len < sizeof(rx_buf)) {
     343          22 :             rx_state = RxParseState::PAYLOAD;
     344             :         } else {
     345           0 :             rx_state = RxParseState::RESET;
     346             :         }
     347          22 :         return;
     348             : 
     349          88 :     case RxParseState::PAYLOAD:
     350          88 :         rx_buf[rx_write_idx++] = byte;
     351          88 :         if (rx_write_idx >= rx_pkt_len) {
     352          22 :             rx_state = RxParseState::CRC_BYTE;
     353             :         }
     354          88 :         return;
     355             : 
     356          22 :     case RxParseState::CRC_BYTE: {
     357          22 :         rx_buf[rx_write_idx++] = byte;
     358          22 :         const bool valid = (byte == crc_xor_of_bytes(rx_buf, rx_pkt_len));
     359          22 :         if (valid) {
     360          22 :             const uint16_t prp = (uint16_t(rx_buf[6]) << 8) | rx_buf[7];
     361          22 :             handle_inbound_packet(prp, &rx_buf[8], rx_pkt_len - 8);
     362             :         }
     363          22 :         rx_state = RxParseState::RESET;
     364          22 :         return;
     365             :     }
     366             :     }
     367             : 
     368           0 :     rx_state = RxParseState::RESET;
     369           0 :     return ;
     370             : }
     371             : 
     372             : // Dispatch a CRC-valid inbound command.  The PLX query format puts the
     373             : // target packet ID in the 2-byte payload (big-endian) of a 0x00A1
     374             : // command frame; we reply with the matching response packet carrying a
     375             : // single byte of presence info.
     376          22 : void Aeron::handle_inbound_packet(uint16_t prp_id, const uint8_t *payload, uint16_t payload_len)
     377             : {
     378          22 :     if (prp_id != QUERY_PRP_ID || payload_len < 2) {
     379           0 :         return;
     380             :     }
     381             : 
     382          22 :     const uint16_t target = (uint16_t(payload[0]) << 8) | payload[1];
     383          22 :     const uint8_t  presence = 1;   // SIM always reports the sensor as present
     384             : 
     385          22 :     if (target == uint16_t(PktID::EXT_SENSORS_MAG)) {
     386          11 :         send_aeron_packet(PktID::EXT_SENSORS_MAG, &presence, sizeof(presence));
     387          11 :     } else if (target == uint16_t(PktID::EXT_SENSORS_ASP)) {
     388          11 :         send_aeron_packet(PktID::EXT_SENSORS_ASP, &presence, sizeof(presence));
     389             :     }
     390             : }
     391             : 
     392             : #endif // AP_SIM_AERON_ENABLED

Generated by: LCOV version 1.14