mirror of https://github.com/ArduPilot/ardupilot
554 lines
17 KiB
C++
554 lines
17 KiB
C++
#include "SIM_config.h"
|
|
|
|
#if AP_SIM_ADSB_SAGETECH_MXS_ENABLED
|
|
|
|
#include "SIM_ADSB_Sagetech_MXS.h"
|
|
|
|
#include <AP_Common/Location.h>
|
|
#include <GCS_MAVLink/GCS.h>
|
|
#include <AP_InternalError/AP_InternalError.h>
|
|
#include "SITL.h"
|
|
#include <ctype.h>
|
|
#include "SIM_Aircraft.h"
|
|
#include "SIM_ADSB.h"
|
|
|
|
#include <errno.h>
|
|
|
|
static const uint8_t FIRST_BYTE { 0xAA };
|
|
|
|
using namespace SITL;
|
|
|
|
void ADSB_Sagetech_MXS::move_preamble_in_buffer(uint8_t search_start_pos)
|
|
{
|
|
uint8_t i;
|
|
for (i=search_start_pos; i<buflen; i++) {
|
|
if ((uint8_t)msg.buffer[i] == FIRST_BYTE) {
|
|
break;
|
|
}
|
|
}
|
|
if (i == 0) {
|
|
return;
|
|
}
|
|
memmove(msg.buffer, &msg.buffer[i], buflen-i);
|
|
buflen = buflen - i;
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::update_serial_input()
|
|
{
|
|
const ssize_t n = read_from_autopilot(&msg.buffer[buflen], ARRAY_SIZE(msg.buffer) - buflen - 1);
|
|
if (n < 0) {
|
|
// TODO: do better here
|
|
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != 0) {
|
|
AP_HAL::panic("Failed to read from autopilot");
|
|
}
|
|
} else {
|
|
buflen += n;
|
|
}
|
|
|
|
|
|
// GCS_SEND_TEXT(MAV_SEVERITY_INFO, "read (%u) bytes", (unsigned)n);
|
|
|
|
switch (input_state) {
|
|
case InputState::WANT_START_BYTE:
|
|
move_preamble_in_buffer();
|
|
if (buflen == 0) {
|
|
return;
|
|
}
|
|
set_input_state(InputState::WANT_PREAMBLE);
|
|
FALLTHROUGH;
|
|
case InputState::WANT_PREAMBLE:
|
|
if (buflen < sizeof(msg.preamble)) {
|
|
return;
|
|
}
|
|
set_input_state(InputState::WANT_PAYLOAD);
|
|
FALLTHROUGH;
|
|
case InputState::WANT_PAYLOAD: {
|
|
// 1 preamble, 2 flags, 1 msg + payload
|
|
const uint8_t want_len = 4 + msg.preamble.payload_length;
|
|
if (buflen < want_len) {
|
|
return;
|
|
}
|
|
set_input_state(InputState::WANT_CHECKSUM);
|
|
FALLTHROUGH;
|
|
}
|
|
case InputState::WANT_CHECKSUM: {
|
|
// 1 start byte, 1 msg type, 1 msgid, 1 payload len
|
|
const uint8_t want_len = sizeof(msg.preamble) + msg.preamble.payload_length + 1;
|
|
if (buflen < want_len) {
|
|
return;
|
|
}
|
|
const uint8_t received_checksum = msg.buffer[want_len-1];
|
|
|
|
uint8_t calculated_checksum = 0;
|
|
for (uint8_t i=0; i<sizeof(msg.preamble)+msg.preamble.payload_length; i++) {
|
|
calculated_checksum += msg.buffer[i];
|
|
}
|
|
|
|
if (calculated_checksum == received_checksum) {
|
|
// GCS_SEND_TEXT(MAV_SEVERITY_INFO, "sim-MXS: Got one (%u)!", (unsigned)msg.preamble.msgtype);
|
|
handle_message();
|
|
move_preamble_in_buffer(want_len);
|
|
} else {
|
|
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "sim-MXS: Bad checksum");
|
|
move_preamble_in_buffer(1);
|
|
}
|
|
// consume these bytes
|
|
set_input_state(InputState::WANT_PREAMBLE);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ADSB_Sagetech_MXS::_handle_message()
|
|
{
|
|
switch (msg.preamble.msgtype) {
|
|
case MsgType::INSTALLATION:
|
|
return handle_message(msg.packed_installation.msg);
|
|
case MsgType::FLIGHTID:
|
|
return handle_message(msg.packed_flightidmessage.msg);
|
|
case MsgType::OPMSG:
|
|
return handle_message(msg.packed_operating.msg);
|
|
case MsgType::GPS:
|
|
return handle_message(msg.packed_gps.msg);
|
|
case MsgType::DATAREQ:
|
|
return handle_message(msg.packed_data_req.msg);
|
|
case MsgType::TARGETREQUEST:
|
|
return handle_message(msg.packed_target_req.msg);
|
|
|
|
case MsgType::ACK:
|
|
case MsgType::STATEVECTORREPORT:
|
|
case MsgType::MODESTATUSREPORT:
|
|
// we should never receive one of these?
|
|
INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
|
|
break;
|
|
}
|
|
|
|
// we should handle all messages:
|
|
INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
|
|
|
|
return false;
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::handle_message()
|
|
{
|
|
if (!_handle_message()) {
|
|
return;
|
|
}
|
|
|
|
// store ack info for sending an ack later:
|
|
ack_info.ackType = msg.preamble.msgtype;
|
|
ack_info.ackId = msg.preamble.msgid;
|
|
ack_info.send = true;
|
|
}
|
|
|
|
bool ADSB_Sagetech_MXS::handle_message(const SITL::ADSB_Sagetech_MXS::Installation& opmsg)
|
|
{
|
|
ASSERT_STORAGE_SIZE(Installation, 36);
|
|
|
|
if (operating_mode != OperatingMode::OFF &&
|
|
operating_mode != OperatingMode::MAINTENANCE) {
|
|
// see page 10 - ignored if not in one of those. We could
|
|
// return silently here if there are race conditions
|
|
INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
|
|
}
|
|
|
|
// do something!
|
|
|
|
return true;
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::assert_good_flight_id()
|
|
{
|
|
static_assert(sizeof(flight_id) == 8, "correct storage size");
|
|
|
|
bool space_seen = false;
|
|
for (uint8_t i=0; i<sizeof(flight_id); i++) {
|
|
// this doesn't seem right; they really allow any ASCII
|
|
// character? vertical tabs, for example?! The standard is
|
|
// ~US$750. Null (0x0) is a valid ascii character!
|
|
if (!isascii(flight_id[i])) {
|
|
INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
|
|
}
|
|
if (flight_id[i] == ' ') {
|
|
space_seen = true;
|
|
}
|
|
if (space_seen && flight_id[i] != ' ') {
|
|
INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ADSB_Sagetech_MXS::handle_message(const SITL::ADSB_Sagetech_MXS::FlightIDMessage& _msg)
|
|
{
|
|
ASSERT_STORAGE_SIZE(FlightIDMessage, 12);
|
|
|
|
// do something!
|
|
flight_id_set_time_ms = AP_HAL::millis();
|
|
memcpy(flight_id, _msg.flight_id, sizeof(flight_id));
|
|
assert_good_flight_id();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ADSB_Sagetech_MXS::handle_message(const SITL::ADSB_Sagetech_MXS::Operating& _msg)
|
|
{
|
|
ASSERT_STORAGE_SIZE(Operating, 12);
|
|
|
|
// do something!
|
|
|
|
return true;
|
|
}
|
|
|
|
#define ORD(x) (x - '0')
|
|
|
|
// buffer contains DDDMM.MMMMM (degrees, minutes including fraction)
|
|
double ADSB_Sagetech_MXS::lon_string_to_double(const uint8_t *str)
|
|
{
|
|
return ORD(str[0]) * 100 + lat_string_to_double(&str[1]);
|
|
}
|
|
|
|
// buffer contains DDMM.MMMMM (degrees, minutes including fraction)
|
|
double ADSB_Sagetech_MXS::lat_string_to_double(const uint8_t *str)
|
|
{
|
|
double degrees = ORD(str[0]) * 10 + ORD(str[1]);
|
|
double minutes = ORD(str[2]) * 10.0 + ORD(str[3]) + ORD(str[5])*0.1 + ORD(str[6])*0.01 + ORD(str[7]) * 0.001 + ORD(str[8]) * 0.0001 + ORD(str[9]) * 0.00001;
|
|
return degrees + minutes/60.0;
|
|
}
|
|
|
|
#undef ORD
|
|
|
|
bool ADSB_Sagetech_MXS::handle_message(const SITL::ADSB_Sagetech_MXS::GPS& _msg)
|
|
{
|
|
ASSERT_STORAGE_SIZE(GPS, 63);
|
|
|
|
// store data to transmit via ADSB
|
|
info_from_vehicle.gps.lat = lat_string_to_double(_msg.latitude);
|
|
info_from_vehicle.gps.lng = lon_string_to_double(_msg.longitude);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ADSB_Sagetech_MXS::handle_message(const SITL::ADSB_Sagetech_MXS::DataRequest& _msg)
|
|
{
|
|
ASSERT_STORAGE_SIZE(DataRequest, 4);
|
|
|
|
// handle request to send data to vehicle. Note that the
|
|
// specification says (on page 32) that the ack is sent before the
|
|
// data.
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ADSB_Sagetech_MXS::handle_message(const SITL::ADSB_Sagetech_MXS::TargetRequest& _msg)
|
|
{
|
|
ASSERT_STORAGE_SIZE(TargetRequest, 7);
|
|
|
|
// handle request to send adsb data to vehicle as it is received
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void ADSB_Sagetech_MXS::update_serial_output(const Aircraft *sitl_model)
|
|
{
|
|
// TODO: space checks here
|
|
|
|
if (ack_info.send) {
|
|
uint8_t alt[3] {};
|
|
uint8_t state = 0;
|
|
Ack ack{
|
|
ack_info.ackType,
|
|
ack_info.ackId,
|
|
state, // bitmask field
|
|
alt
|
|
};
|
|
const PackedMessage<Ack> packed { ack, MsgType::ACK, msgid++ };
|
|
write_to_autopilot((const char*)&packed, sizeof(packed));
|
|
ack_info.send = false;
|
|
}
|
|
|
|
update_serial_output_vehicles(sitl_model);
|
|
}
|
|
|
|
|
|
void ADSB_Sagetech_MXS::update_serial_output_vehicles(const SITL::Aircraft *sitl_model)
|
|
{
|
|
if (sitl_model->adsb == nullptr) {
|
|
return;
|
|
}
|
|
for (uint8_t i=0; i<sitl_model->adsb->num_vehicles; i++) {
|
|
const ADSB_Vehicle &vehicle = sitl_model->adsb->vehicles[i];
|
|
if (!vehicle.initialised) {
|
|
continue;
|
|
}
|
|
send_vehicle_message(vehicle);
|
|
}
|
|
}
|
|
|
|
// pack a floating-point latitude or longitude into three bytes
|
|
// according to Sagetech definitions:
|
|
void ADSB_Sagetech_MXS::pack_scaled_geocoord(uint8_t buf[3], float coord)
|
|
{
|
|
const int32_t scaled = coord * (1U<<23) / 180.0;
|
|
pack_int32_into_uint8_ts(scaled, buf);
|
|
}
|
|
|
|
// pack a floating-point altitude in metres into three bytes
|
|
// according to Sagetech definitions:
|
|
void ADSB_Sagetech_MXS::pack_scaled_alt(uint8_t buf[3], float alt_m)
|
|
{
|
|
const int32_t scaled = METRES_TO_FEET*alt_m * (1/0.015625);
|
|
pack_int32_into_uint8_ts(scaled, buf);
|
|
}
|
|
|
|
// discards the top 8 bits from source and shoves the rest into dest
|
|
void ADSB_Sagetech_MXS::pack_int32_into_uint8_ts(int32_t source, uint8_t dest[3])
|
|
{
|
|
// FIXME: cast &source instead?
|
|
dest[0] = source >> 16;
|
|
dest[1] = source >> 8;
|
|
dest[2] = source >> 0;
|
|
}
|
|
|
|
uint8_t ADSB_Sagetech_MXS::scaled_groundspeed(float speed_m_s) const
|
|
{
|
|
if (is_zero(speed_m_s)) {
|
|
return 0x01;
|
|
}
|
|
const float knots = M_PER_SEC_TO_KNOTS * speed_m_s;
|
|
if (knots < 0.125) {
|
|
return 0x02;
|
|
}
|
|
static const struct Threshold {
|
|
float min;
|
|
uint8_t code;
|
|
float increment;
|
|
} thresholds[] {
|
|
{ 0.125, 0x03, 0.146 },
|
|
{ 1, 0x09, 0.25 },
|
|
{ 2, 0x0D, 0.50 },
|
|
{ 15, 0x27, 1 },
|
|
{ 70, 0x5e, 2 },
|
|
{ 100, 0x6d, 5 },
|
|
{ 175, 0x7c, 0 },
|
|
};
|
|
auto *entry = &thresholds[0];
|
|
for (uint8_t i=1; i<ARRAY_SIZE(thresholds); i++) {
|
|
auto *next_entry = &thresholds[i];
|
|
if (knots > entry->min && knots < next_entry->min) {
|
|
const uint8_t code_delta = next_entry->code - entry->code;
|
|
return entry->code + uint8_t((knots - entry->min) / code_delta);
|
|
}
|
|
entry = next_entry;
|
|
}
|
|
return 0x7c;
|
|
}
|
|
void ADSB_Sagetech_MXS::pack_scaled_groundspeed(uint8_t dest[1], float speed_m_s) const
|
|
{
|
|
dest[0] = scaled_groundspeed(speed_m_s);
|
|
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "speed in: %f", speed_m_s);
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::pack_scaled_airspeed(uint8_t dest[2], float speed_m_s) const
|
|
{
|
|
const int16_t scaled = M_PER_SEC_TO_KNOTS * speed_m_s * 8;
|
|
dest[0] = scaled >> 8;
|
|
dest[1] = scaled >> 0;
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::pack_scaled_vertical_rate(uint8_t dest[2], float speed_m_s) const
|
|
{
|
|
const int16_t scaled = METRES_TO_FEET * speed_m_s;
|
|
dest[0] = scaled >> 8;
|
|
dest[1] = scaled >> 0;
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::send_vehicle_message(const SITL::ADSB_Vehicle &vehicle)
|
|
{
|
|
send_vehicle_message_state_vector(vehicle);
|
|
send_vehicle_message_status_report(vehicle);
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::send_vehicle_message_state_vector(const SITL::ADSB_Vehicle &vehicle)
|
|
{
|
|
enum class SVR_ValidityFlag : uint8_t {
|
|
LatitudeAndLongitude = (1U << 7),
|
|
Altitude_Geometric = (1U << 6),
|
|
NS_and_EW_Velocity = (1U << 5),
|
|
GroundSpeedWhileOnSurface = (1U << 4),
|
|
HeadingWhileOnSurface = (1U << 3),
|
|
Altitude_Barometric = (1U << 2),
|
|
VerticalRate_Geometric = (1U << 1),
|
|
VerticalRate_Barometric = (1U << 0),
|
|
};
|
|
enum class SVR_EstimateValidityFlag : uint8_t {
|
|
LatitudeAndLongitude = (1U << 7),
|
|
NS_and_EW_Velocity = (1U << 6),
|
|
};
|
|
|
|
// this struct-based approach may prove to be too inflexible
|
|
// if we want to change the shape of the report at runtime for
|
|
// some reason.
|
|
struct PACKED {
|
|
StateVectorReport_ReportStructure report_structure;
|
|
uint8_t validity_flags; // see SVR_ValidityFlag enum
|
|
uint8_t estimated_validity_flags; // see SVR_EstimateValidityFlag enum
|
|
uint8_t participant_address[3];
|
|
uint8_t address_qualifier;
|
|
uint16_t epos_toa;
|
|
uint16_t pos_toa;
|
|
uint16_t vel_toa;
|
|
uint8_t latitude[3];
|
|
uint8_t longitude[3];
|
|
uint8_t alt_geometric[3];
|
|
uint8_t ns_velocity[2];
|
|
uint8_t ew_velocity[2];
|
|
uint8_t up_velocity[2];
|
|
// uint8_t groundspeed;
|
|
// uint8_t ground_heading;
|
|
} my_report {};
|
|
|
|
my_report.report_structure.rs0.msn = 1;
|
|
|
|
// note that the Sagetech SDK parser requires all of these fields
|
|
// to be 1 to be an airborne vehicle::
|
|
my_report.report_structure.rs0.time_of_applicability_for_estimated_positon = 1;
|
|
my_report.report_structure.rs0.position_time_of_applicability = 1;
|
|
my_report.report_structure.rs0.velocity_time_of_applicability = 1;
|
|
|
|
my_report.report_structure.rs2.estimated_longitude = 0;
|
|
|
|
my_report.validity_flags = 0;
|
|
|
|
pack_int32_into_uint8_ts(vehicle.ICAO_address, my_report.participant_address);
|
|
my_report.address_qualifier = 0x02; // aircraft
|
|
|
|
Location loc = vehicle.get_location();
|
|
|
|
// pack in latitude/longitude:
|
|
pack_scaled_geocoord(my_report.latitude, loc.lat * 1e-7);
|
|
pack_scaled_geocoord(my_report.longitude, loc.lng * 1e-7);
|
|
my_report.report_structure.rs0.lat_and_lng = 1;
|
|
my_report.validity_flags |= (uint8_t)SVR_ValidityFlag::LatitudeAndLongitude;
|
|
|
|
// pack in altitude:
|
|
pack_scaled_alt(my_report.alt_geometric, loc.alt*0.01);
|
|
my_report.report_structure.rs1.altitude_geometric = 1;
|
|
my_report.validity_flags |= (uint8_t)SVR_ValidityFlag::Altitude_Geometric;
|
|
|
|
// pack in north/south and east/west velocity
|
|
pack_scaled_airspeed(my_report.ns_velocity, vehicle.velocity_ef.x);
|
|
pack_scaled_airspeed(my_report.ew_velocity, vehicle.velocity_ef.y);
|
|
my_report.report_structure.rs1.north_south_east_west_vel = 1;
|
|
my_report.validity_flags |= (uint8_t)SVR_ValidityFlag::NS_and_EW_Velocity;
|
|
|
|
// pack in surface speed and heading:
|
|
// pack_scaled_groundspeed(&my_report.groundspeed, vehicle.velocity_ef.length());
|
|
// my_report.report_structure.rs1.ground_speed_on_ground = 1;
|
|
// my_report.validity_flags |= (uint8_t)SVR_ValidityFlag::GroundSpeedWhileOnSurface;
|
|
// my_report.report_structure.rs1.heading_on_ground = 1;
|
|
// my_report.ground_heading = wrap_180(vehicle.velocity_ef.xy().angle()) / 1.40625;
|
|
// my_report.validity_flags |= (uint8_t)SVR_ValidityFlag::HeadingWhileOnSurface;
|
|
|
|
// pack in vertical rate
|
|
pack_scaled_vertical_rate(my_report.up_velocity, -vehicle.velocity_ef.z);
|
|
my_report.report_structure.rs1.vertical_rate_geometric_barometric = 1;
|
|
my_report.validity_flags |= (uint8_t)SVR_ValidityFlag::VerticalRate_Geometric;
|
|
|
|
StateVectorReportBuffer<sizeof(my_report)> report;
|
|
memcpy(report.buffer, &my_report, sizeof(report.buffer));
|
|
|
|
const PackedMessage<StateVectorReportBuffer<sizeof(my_report)>> packed {
|
|
report,
|
|
MsgType::STATEVECTORREPORT,
|
|
msgid++
|
|
};
|
|
write_to_autopilot((const char*)&packed, sizeof(packed));
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::send_vehicle_message_status_report(const SITL::ADSB_Vehicle &vehicle)
|
|
{
|
|
enum class RS0 : uint8_t {
|
|
TimeOfApplicability = (1U << 3),
|
|
ADSB_Version = (1U << 2),
|
|
CallSign = (1U << 1),
|
|
EmitterCategory = (1U << 0),
|
|
};
|
|
enum class RS1 : uint8_t {
|
|
AVLengthAndWidthCode = (1U << 7),
|
|
EmergencyPriorityStatus = (1U << 6),
|
|
CapabilityCodes = (1U << 5),
|
|
OperationalMode = (1U << 4),
|
|
SVQualityNACp = (1U << 3),
|
|
SVQualityNACv = (1U << 2),
|
|
SVQualitySIL = (1U << 1),
|
|
SQQualtity_GVA = (1U << 0),
|
|
};
|
|
enum class RS2 : uint8_t {
|
|
SVQualityNICbaro = (1U << 7),
|
|
TrueMagneticHeading = (1U << 6),
|
|
VerticalRateType = (1U << 5),
|
|
FlightModeSpecific = (1U << 4),
|
|
Reserved3 = (1U << 3),
|
|
Reserved2 = (1U << 2),
|
|
Reserved1 = (1U << 1),
|
|
Reserved0 = (1U << 0),
|
|
};
|
|
|
|
// this struct-based approach may prove to be too inflexible
|
|
// if we want to change the shape of the report at runtime for
|
|
// some reason.
|
|
struct PACKED {
|
|
uint32_t report_structure : 24;
|
|
uint8_t validity_flags; // see SVR_ValidityFlag enum
|
|
uint8_t participant_address[3];
|
|
uint8_t address_qualifier;
|
|
uint8_t callsign[8];
|
|
} my_report {};
|
|
|
|
my_report.report_structure = uint8_t(RS0::CallSign) | (0x2 << 4);
|
|
my_report.validity_flags = 0;
|
|
|
|
pack_int32_into_uint8_ts(vehicle.ICAO_address, my_report.participant_address);
|
|
my_report.address_qualifier = 0x02; // aircraft
|
|
|
|
memset(my_report.callsign, '\0', ARRAY_SIZE(my_report.callsign));
|
|
memcpy(my_report.callsign, vehicle.callsign, MIN(ARRAY_SIZE(vehicle.callsign), ARRAY_SIZE(my_report.callsign)));
|
|
|
|
ModeStatusReportBuffer<sizeof(my_report)> report;
|
|
memcpy(report.buffer, &my_report, sizeof(report.buffer));
|
|
|
|
const PackedMessage<ModeStatusReportBuffer<sizeof(my_report)>> packed {
|
|
report,
|
|
MsgType::MODESTATUSREPORT,
|
|
msgid++
|
|
};
|
|
write_to_autopilot((const char*)&packed, sizeof(packed));
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::update_rf_input()
|
|
{
|
|
// could selectivly copy from sitl->adsb here
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::update_rf_output()
|
|
{
|
|
// print a message here when we are transmitting
|
|
}
|
|
|
|
void ADSB_Sagetech_MXS::update(const SITL::Aircraft *sitl_model)
|
|
{
|
|
// see if we should do a report.
|
|
if ((AP::sitl()->adsb_types & (1U << (uint8_t)SIM::ADSBType::SageTechMXS)) == 0) {
|
|
return;
|
|
}
|
|
|
|
update_serial_input();
|
|
update_serial_output(sitl_model);
|
|
|
|
update_rf_input();
|
|
update_rf_output();
|
|
}
|
|
|
|
#endif // AP_SIM_ADSB_SAGETECH_MXS_ENABLED
|