Files
ardupilot/libraries/SITL/SIM_AIS.cpp
T
Andy Piper 318a2ec318 SITL: fix clang warnings
SIM_AIS: use abs() instead of fabsf() for integer argument
SIM_INA3221: fix unused message
SIM_JSON: guard debug field with preprocessor conditional
2026-02-21 08:13:00 +11:00

566 lines
17 KiB
C++

/*
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 <http://www.gnu.org/licenses/>.
*/
/*
Simulate virtual vessels
./Tools/autotest/sim_vehicle.py -v Rover -A --serial5=sim:AIS --map
param set SERIAL5_PROTOCOL 40
param set AIS_TYPE 1
reboot
param set SIM_AIS_COUNT 10
module load ais
Dump logged AIS data to the serial port
./Tools/autotest/sim_vehicle.py -v Rover -A --serial5=sim:AISReplay --custom-location 51.58689798356386,-3.9044570193067965,0,0 --map
param set SERIAL5_PROTOCOL 40
param set AIS_TYPE 1
reboot
module load ais
*/
#include "SIM_config.h"
#if AP_SIM_AIS_ENABLED
#include "SIM_AIS.h"
#include <SITL/SITL.h>
#define FORCE_VERSION_H_INCLUDE
#include "ap_version.h"
#include "SIM_Aircraft.h"
#include <AP_Common/NMEA.h>
extern const AP_HAL::HAL& hal;
using namespace SITL;
const AP_Param::GroupInfo AIS::var_info[] = {
// @Param: COUNT
// @DisplayName: Number of AIS vessels
// @Description: Total number of AIS simulated vessels
AP_GROUPINFO("COUNT", 1, AIS, vessel_count, -1),
// @Param: RADIUS
// @DisplayName: AIS radius stddev of vessels
// @Description: Simulated standard deviation of radius in AIS of a vessel
// @Units: m
AP_GROUPINFO("RADIUS", 2, AIS, radius_m, 10000),
AP_GROUPEND
};
AIS_Replay::AIS_Replay() : SerialDevice::SerialDevice()
{
char* file_path;
IGNORE_RETURN(asprintf(&file_path, AP_BUILD_ROOT "/libraries/SITL/SIM_AIS_data.txt"));
file = fopen(file_path,"r");
if (file == nullptr) {
AP_HAL::panic("AIS could not open data file");
}
// seek past the header line
char line[100];
IGNORE_RETURN(fgets(line, sizeof(line), file));
}
void AIS_Replay::update()
{
if (file == nullptr) {
AP_HAL::panic("AIS lost data file");
}
// just send a line of data at 1Hz:
const uint32_t now = AP_HAL::millis();
if (now - last_sent_ms < 1000) {
return;
}
last_sent_ms = now;
char line[100];
if (!fgets(line, sizeof(line), file)) {
// got to the end of the file, circle back
fseek(file,0,SEEK_SET);
if (!fgets(line, sizeof(line), file)) {
AP_HAL::panic("AIS lost data file");
}
return;
}
//hal.console->printf("%s",line);
write_to_autopilot(line, strlen(line));
}
AIS::AIS()
{
AP::sitl()->models.ais_ptr = this;
AP_Param::setup_object_defaults(this, var_info);
}
void AIS::update(const class Aircraft &aircraft)
{
const SIM *sitl = AP::sitl();
if (sitl == nullptr) {
return;
}
// Disabled
if (vessel_count <= 0) {
return;
}
// Calculate the time step to take
const uint32_t now_ms = AP_HAL::millis();
const float dt = (now_ms - last_sim_update_ms) * 0.001;
last_sim_update_ms = now_ms;
// Get current location to remove any vessel that is too far away
const Location &aircraft_loc = aircraft.get_location();
// Update all vessels, marking any that move too far from the aircraft as in-active
for (uint8_t i=0; i<ARRAY_SIZE(vessels); i++) {
update_simulated_vessel(vessels[i], dt, aircraft_loc, radius_m, now_ms);
}
// Add new vessels until we have the correct number
for (uint8_t i=0; i<ARRAY_SIZE(vessels); i++) {
// Disable any extra vessels over the set count
if (i >= vessel_count) {
vessels[i].active = false;
continue;
}
// Vessel is already active
if (vessels[i].active) {
continue;
}
// Generate new vessel
init_vessel(vessels[i], aircraft_loc, radius_m);
}
}
// Update a active vessel position and heading
void AIS::update_simulated_vessel(ais_vessel &vessel, const float dt, const Location &vehicle_loc, const float radius, const uint32_t now_ms)
{
// Check if this vessel is active
if (!vessel.active) {
return;
}
// Update heading from turn rate, both turn rate and heading are in cdeg
vessel.info.heading += vessel.info.turn_rate * dt;
// Extract location
Location loc { vessel.info.lat, vessel.info.lon, 0, Location::AltFrame::ABOVE_ORIGIN };
// Update location from heading and velocity
loc.offset_bearing(vessel.info.heading * 0.01, vessel.info.velocity * 0.01 * dt);
// Repopulate location
vessel.info.lat = loc.lat;
vessel.info.lon = loc.lng;
// Check if location is still valid
if (vehicle_loc.get_distance(loc) > radius) {
vessel.active = false;
return;
}
// Report, update rate depends on speed
uint32_t position_interval;
// Convert speed over ground from 0.1 knots to cm/s
if (vessel.info.velocity > 23.0 * 0.1 * KNOTS_TO_M_PER_SEC * 100.0) {
position_interval = 2000; // every 2 seconds if faster than 23 knots
} else if (vessel.info.velocity > 14.0 * 0.1 * KNOTS_TO_M_PER_SEC * 100.0) {
position_interval = 6000; // every 6 seconds if faster than 14 knots
} else if (vessel.info.velocity > 0.0) {
position_interval = 10000; // every 10 seconds if moving
} else {
position_interval = 3 * 60 * 1000; // every 3 mins if anchored or moored and moving at less than 3 knots
}
// Send position report at internal
if ((vessel.last_position_report_ms == 0) || (now_ms - vessel.last_position_report_ms > position_interval)) {
send_position_report(vessel.info);
vessel.last_position_report_ms = now_ms;
}
// Send static and voyage at internal
const uint32_t static_and_voyage_interval = 6 * 60 * 1000; // every 6 mins
if ((vessel.last_static_and_voyage_ms == 0) || (now_ms - vessel.last_static_and_voyage_ms > static_and_voyage_interval)) {
send_static_and_voyage(vessel.info);
vessel.last_static_and_voyage_ms = now_ms;
}
}
// Generate a new vessel
void AIS::init_vessel(ais_vessel &vessel, const Location &vehicle_loc, const float radius)
{
// Clear any existing data
memset(&vessel, 0, sizeof(vessel));
// Set flags for valid data
vessel.info.flags |= AIS_FLAGS_VALID_VELOCITY | AIS_FLAGS_VALID_TURN_RATE;
// Generate random data for static info, mask to 30 bit
vessel.info.MMSI = rand() & 0x3FFFFFFF;
// Set nav status and type
vessel.info.navigational_status = AIS_NAV_STATUS_UNDER_WAY;
vessel.info.type = AIS_TYPE_CARGO;
// 90% chance of having valid dimensions
if (rand() > RAND_MAX * 0.1) {
vessel.info.flags |= AIS_FLAGS_VALID_DIMENSIONS;
// Generate each dimension
vessel.info.dimension_bow = abs(Aircraft::rand_normal(0, 300.0));
if (vessel.info.dimension_bow > 511) {
vessel.info.dimension_bow = 511;
vessel.info.flags |= AIS_FLAGS_LARGE_BOW_DIMENSION;
}
vessel.info.dimension_stern = abs(Aircraft::rand_normal(0, 300.0));
if (vessel.info.dimension_stern > 511) {
vessel.info.dimension_stern = 511;
vessel.info.flags |= AIS_FLAGS_LARGE_STERN_DIMENSION;
}
vessel.info.dimension_port = abs(Aircraft::rand_normal(0, 30.0));
if (vessel.info.dimension_port > 63) {
vessel.info.dimension_port = 63;
vessel.info.flags |= AIS_FLAGS_LARGE_PORT_DIMENSION;
}
vessel.info.dimension_starboard = abs(Aircraft::rand_normal(0, 30.0));
if (vessel.info.dimension_starboard > 63) {
vessel.info.dimension_starboard = 63;
vessel.info.flags |= AIS_FLAGS_LARGE_STARBOARD_DIMENSION;
}
}
// 25% chance of turning
if (rand() > RAND_MAX * 0.75) {
vessel.info.turn_rate = Aircraft::rand_normal(0, 100.0);
}
// 95% chance of moving
if (rand() > RAND_MAX * 0.05) {
vessel.info.velocity = abs(Aircraft::rand_normal(0, 500.0));
// Speed threshold for high velocity flag
// Convert speed over ground from 0.1 knots to cm/s
const uint16_t high_velocity = 1022 * 0.1 * KNOTS_TO_M_PER_SEC * 100.0;
if (vessel.info.velocity >= high_velocity) {
vessel.info.velocity = high_velocity;
vessel.info.flags |= AIS_FLAGS_HIGH_VELOCITY;
}
}
// Generate a heading
vessel.info.heading = wrap_360_cd(rand());
// Generate a random location
Location loc = vehicle_loc;
loc.offset( Vector3p {
Aircraft::rand_normal(0, radius),
Aircraft::rand_normal(0, radius),
0
});
vessel.info.lat = loc.lat;
vessel.info.lon = loc.lng;
// mark active
vessel.active = true;
}
/*
formatted print of NMEA message, with checksum appended
*/
void AIS::nmea_printf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
char *s = nmea_vaprintf(fmt, ap);
va_end(ap);
if (s != nullptr) {
write_to_autopilot((const char*)s, strlen(s));
free(s);
}
}
// Convert to NMEA char
uint8_t AIS::encode_char(uint8_t payload) const
{
if (payload + 8 > 40) {
return payload + 8 + 48;
}
return payload + 48;
}
uint8_t AIS::get_bits(uint32_t value, const uint8_t start, const uint8_t len) const
{
value = value >> start;
const uint8_t mask = 0b111111 >> (6 - len);
return value & mask;
}
// set the specified bits in the payload with 6 bit chars
void AIS::set_bits(uint8_t *payload, const uint16_t low, const uint16_t high, uint32_t value)
{
// Bits are inclusive, so add one
const uint8_t bit_len = high - low + 1;
const uint32_t value_mask = 0xFFFFFFFF >> (32 - bit_len);
if ((value & ~value_mask) != 0) {
AP_HAL::panic("AIS value passed too big to fit (%u > %u bits)", value, bit_len);
}
const uint8_t char_low = low / 6;
const uint8_t bit_low = low % 6;
const uint8_t char_high = high / 6;
const uint8_t bit_high = (high % 6) + 1;
const uint8_t char_range = char_high - char_low;
uint8_t bits = bit_len;
for (uint8_t index = 0; index <= char_range; index++) {
if (bits == 0) {
AP_HAL::panic("AIS packing fail A");
}
if (index == char_range) {
// last char uses high bit
const uint8_t bits_needed = MIN(bit_high, bit_len);
bits -= bits_needed;
payload[char_low + index] |= get_bits(value, bits, bits_needed) << (6 - bit_high);
} else if (index == 0) {
// First char uses low bit
const uint8_t bits_needed = MIN(6 - bit_low, bit_len);
bits -= bits_needed;
payload[char_low + index] |= get_bits(value, bits, bits_needed);
} else {
// Others need full length
bits -= 6;
payload[char_low + index] |= get_bits(value, bits, 6);
}
}
if (bits != 0) {
AP_HAL::panic("AIS packing fail B");
}
}
void AIS::set_bits_signed(uint8_t *payload, const uint16_t low, const uint16_t high, int32_t value)
{
if (value >= 0) {
// Positive number is the same as unsigned
set_bits(payload, low, high, value);
return;
}
// Convert to unsigned
uint32_t val = value;
// Mask to correct length
const uint8_t bit_len = high - low + 1;
val &= 0xFFFFFFFF >> (32 - bit_len);
// Set sign bit
val |= 1 << (bit_len - 1);
set_bits(payload, low, high, val);
}
void AIS::send_position_report(const mavlink_ais_vessel_t &info) {
// Turn rate, default to not available
int8_t rot = -128;
if ((info.flags & AIS_FLAGS_VALID_TURN_RATE) != 0) {
if ((info.flags & AIS_FLAGS_TURN_RATE_SIGN_ONLY) != 0) {
// Only sign available
rot = info.turn_rate >= 0 ? 127 : -127;
} else {
// cdeg/s to deg/min, take abs to allow sqrt
const float turn_rate_deg_per_min = abs(info.turn_rate) * 60.0f * 0.01f;
// Apply scaling, recovering sign
rot = 4.733 * sqrtf(turn_rate_deg_per_min) * info.turn_rate >= 0 ? 1.0 : -1.0;
}
}
// Corse over ground, default to not available
uint16_t cog = 3600;
if ((info.flags & AIS_FLAGS_VALID_COG) != 0) {
// cdeg to 0.1 deg
cog = info.COG * 0.1;
}
// Accuracy bit from flag
const bool accuracy = (info.flags & AIS_FLAGS_POSITION_ACCURACY) != 0;
// Speed over ground, default to not available
uint16_t sog = 1023;
if ((info.flags & AIS_FLAGS_VALID_VELOCITY) != 0) {
if ((info.flags & AIS_FLAGS_HIGH_VELOCITY) != 0) {
sog = 1022;
} else {
sog = MIN((float(info.velocity) * 0.01 * M_PER_SEC_TO_KNOTS * 10), 1022);
}
}
// storage for payload
uint8_t payload[28] {};
set_bits(payload, 0, 5, 1); // type: Position Report Class A
set_bits(payload, 6, 7, 3); // repeat: Do not repeat
set_bits(payload, 8, 37, info.MMSI); // mmsi
set_bits(payload, 38, 41, info.navigational_status); // status
set_bits_signed(payload, 42, 49, rot); // rate of turn
set_bits(payload, 50, 59, sog); // speed
set_bits(payload, 60, 60, accuracy); // accuracy
set_bits_signed(payload, 61, 88, int32_t(double(info.lon) * 0.06)); // lon
set_bits_signed(payload, 89, 115, int32_t(double(info.lat) * 0.06)); // lat
set_bits(payload, 116, 127, cog); // course over ground
set_bits(payload, 128, 136, uint32_t(info.heading * 0.01)); // heading convert from deg to centi-deg
set_bits(payload, 137, 142, 60); // second: not available
set_bits(payload, 143, 144, 0); // maneuver: not available
// 145 - 147: spare
set_bits(payload, 148, 148, 0); // raim not used
set_bits(payload, 149, 167, 0); // Radio status
// Convert to chars
uint8_t encoded[sizeof(payload) + 1] {};
for (uint8_t i = 0; i < sizeof(payload); i++) {
encoded[i] = encode_char(payload[i]);
}
const uint8_t total_fragments = 1;
const uint8_t fragment = 1;
// sequence ID is not used for a single fragment message
const char channel_code = 'A';
const uint8_t fill_bits = 0;
// Send
nmea_printf("!AIVDM,%u,%u,,%c,%s,%u", total_fragments, fragment, channel_code, encoded, fill_bits);
}
void AIS::send_static_and_voyage(const mavlink_ais_vessel_t &info) {
// Fill in dimensions if valid
uint16_t bow_dim = 0;
uint16_t stern_dim = 0;
uint8_t port_dim = 0;
uint8_t star_dim = 0;
if ((info.flags & AIS_FLAGS_VALID_DIMENSIONS) != 0) {
if ((info.flags & AIS_FLAGS_LARGE_BOW_DIMENSION) != 0) {
bow_dim = 511;
} else {
bow_dim = MIN(info.dimension_bow, 511);
}
if ((info.flags & AIS_FLAGS_LARGE_STERN_DIMENSION) != 0) {
stern_dim = 511;
} else {
stern_dim = MIN(info.dimension_stern, 511);
}
if ((info.flags & AIS_FLAGS_LARGE_PORT_DIMENSION) != 0) {
port_dim = 63;
} else {
port_dim = MIN(info.dimension_port, 63);
}
if ((info.flags & AIS_FLAGS_LARGE_STARBOARD_DIMENSION) != 0) {
star_dim = 63;
} else {
star_dim = MIN(info.dimension_starboard, 63);
}
}
// storage for payload
uint8_t payload[71] {};
set_bits(payload, 0, 5, 5); // type: Static and Voyage Related Data
set_bits(payload, 6, 7, 3); // repeat: Do not repeat
set_bits(payload, 8, 37, info.MMSI); // mmsi
set_bits(payload, 38, 39, 1); // AIS version
set_bits(payload, 40, 69, 0); // imo number
set_bits(payload, 70, 111, '@'); // call_sign: empty
set_bits(payload, 112, 231, '@'); // name: empty
set_bits(payload, 232, 239, info.type);
set_bits(payload, 240, 248, bow_dim); // bow_dim
set_bits(payload, 249, 257, stern_dim); // stern_dim
set_bits(payload, 258, 263, port_dim); // port_dim
set_bits(payload, 264, 269, star_dim); // star_dim
set_bits(payload, 270, 273, 1); // fix: GPS
set_bits(payload, 274, 277, 0); // ETA month: not available
set_bits(payload, 278, 282, 0); // ETA day: not available
set_bits(payload, 283, 287, 0); // ETA hour: not available
set_bits(payload, 288, 293, 0); // ETA minute: not available
set_bits(payload, 294, 301, 0); // draught: 0 decimeters
set_bits(payload, 302, 421, '@'); // destination: empty
set_bits(payload, 422, 422, 1); // dte: Data terminal not ready
// 423: not used
// Split and encode
const uint8_t msg1_len = 50;
const uint8_t msg2_len = sizeof(payload) - msg1_len;
uint8_t msg1_encoded[msg1_len + 1] {};
for (uint8_t i = 0; i < msg1_len; i++) {
msg1_encoded[i] = encode_char(payload[i]);
}
uint8_t msg2_encoded[msg2_len + 1] {};
for (uint8_t i = 0; i < msg2_len; i++) {
msg2_encoded[i] = encode_char(payload[msg1_len + i]);
}
const uint8_t total_fragments = 2;
uint8_t fragment = 1;
const char channel_code = 'A';
const uint8_t fill_bits = 0;
// Send
nmea_printf("!AIVDM,%u,%u,%u,%c,%s,%u", total_fragments, fragment, sequence_ID, channel_code, msg1_encoded, fill_bits);
fragment++;
nmea_printf("!AIVDM,%u,%u,%u,%c,%s,%u", total_fragments, fragment, sequence_ID, channel_code, msg2_encoded, fill_bits);
sequence_ID++;
}
#endif // AP_SIM_AIS_ENABLED