AP_IBus_Telem: Initial implementation

Allows telemetry data to be sent to receivers that support the
i-BUS protocol (FlySky and Turnigy).
This commit is contained in:
Nicole Ashley 2024-07-30 16:16:06 +12:00 committed by Peter Barker
parent 994e3edfb5
commit 8040e7c502
2 changed files with 918 additions and 0 deletions

View File

@ -0,0 +1,792 @@
/*
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/>.
i-BUS telemetry for FlySky/Turnigy receivers and other peripherals
(eg iA6B, iA10) by Nicole Ashley <nicole@ashley.kiwi>.
Originally based on work by Jan Verhulst:
https://github.com/ArduPilot/ardupilot/pull/16545
Libraries used for reference and inspiration:
* iBUStelemetry
https://github.com/Hrastovc/iBUStelemetry
* IBusBM
https://github.com/bmellink/IBusBM
* BetaFlight
https://github.com/betaflight/betaflight/blob/master/src/main/telemetry/ibus_shared.c
*/
#include <AP_IBus_Telem/AP_IBus_Telem.h>
#if AP_IBUS_TELEM_ENABLED
#include <AP_HAL/AP_HAL.h>
#include <AP_Vehicle/AP_Vehicle.h>
#include <AP_BattMonitor/AP_BattMonitor.h>
#include <AP_Arming/AP_Arming.h>
#include <AP_GPS/AP_GPS.h>
#include <AP_RPM/AP_RPM.h>
// 2-byte values
#define IBUS_SENSOR_TYPE_TEMPERATURE 0x01 // Temperature (in 0.1 degrees, where 0=-40'C)
#define IBUS_SENSOR_TYPE_RPM_FLYSKY 0x02 // FlySky-specific throttle value
#define IBUS_SENSOR_TYPE_EXTERNAL_VOLTAGE 0x03 // External voltage (in centivolts, so 1450 is 14.50V)
#define IBUS_SENSOR_TYPE_AVERAGE_CELL_VOLTAGE 0x04 // Avg cell voltage (in centivolts, so 1450 is 14.50V)
#define IBUS_SENSOR_TYPE_BATTERY_CURRENT 0x05 // Battery current (centi-amps)
#define IBUS_SENSOR_TYPE_FUEL 0x06 // Remaining battery percentage
#define IBUS_SENSOR_TYPE_RPM 0x07 // Throttle value (in 0.01, so 1200 is 12.00%)
#define IBUS_SENSOR_TYPE_COMPASS_HEADING 0x08 // Heading (0-360 degrees)
#define IBUS_SENSOR_TYPE_CLIMB_RATE 0x09 // Climb rate (cm/s)
#define IBUS_SENSOR_TYPE_COG 0x0a // Course over ground (centidegrees, so 27015 is 270.15 degrees)
#define IBUS_SENSOR_TYPE_GPS_STATUS 0x0b // GPS status (2 values: fix type, and number of satellites)
#define IBUS_SENSOR_TYPE_ACC_X 0x0c // Acc X (cm/s)
#define IBUS_SENSOR_TYPE_ACC_Y 0x0d // Acc Y (cm/s)
#define IBUS_SENSOR_TYPE_ACC_Z 0x0e // Acc Z (cm/s)
#define IBUS_SENSOR_TYPE_ROLL 0x0f // Roll (centidegrees)
#define IBUS_SENSOR_TYPE_PITCH 0x10 // Pitch (centidegrees)
#define IBUS_SENSOR_TYPE_YAW 0x11 // Yaw (centidegrees)
#define IBUS_SENSOR_TYPE_VERTICAL_SPEED 0x12 // Vertical speed (cm/s)
#define IBUS_SENSOR_TYPE_GROUND_SPEED 0x13 // Speed (cm/s)
#define IBUS_SENSOR_TYPE_GPS_DIST 0x14 // Distance from home (m)
#define IBUS_SENSOR_TYPE_ARMED 0x15 // Armed / unarmed (1 = armed, 0 = unarmed)
#define IBUS_SENSOR_TYPE_FLIGHT_MODE 0x16 // Flight mode
#define IBUS_SENSOR_TYPE_ODO1 0x7c // Odometer1
#define IBUS_SENSOR_TYPE_ODO2 0x7d // Odometer2
#define IBUS_SENSOR_TYPE_SPEED 0x7e // Speed km/h
#define IBUS_SENSOR_TYPE_ALT_FLYSKY 0xf9 // FlySky-specific altitude (metres)
// 4-byte values
#define IBUS_SENSOR_TYPE_TEMPERATURE_PRESSURE 0x41 // Combined temperature & pressure value
#define IBUS_SENSOR_TYPE_GPS_LAT 0x80 // WGS84 in degrees * 1E7
#define IBUS_SENSOR_TYPE_GPS_LNG 0x81 // WGS84 in degrees * 1E7
#define IBUS_SENSOR_TYPE_GPS_ALT 0x82 // GPS (cm)
#define IBUS_SENSOR_TYPE_ALT 0x83 // Alt (cm)
#define IBUS_SENSOR_TYPE_ALT_MAX 0x84 // MaxAlt (cm)
// i-BUS vehicle modes
#define IBUS_VEHICLE_MODE_STAB 0
#define IBUS_VEHICLE_MODE_ACRO 1
#define IBUS_VEHICLE_MODE_AHOLD 2
#define IBUS_VEHICLE_MODE_AUTO 3
#define IBUS_VEHICLE_MODE_GUIDED 4
#define IBUS_VEHICLE_MODE_LOITER 5
#define IBUS_VEHICLE_MODE_RTL 6
#define IBUS_VEHICLE_MODE_CIRCLE 7
#define IBUS_VEHICLE_MODE_PHOLD 8
#define IBUS_VEHICLE_MODE_LAND 9
#define IBUS_VEHICLE_MODE_UNKNOWN 255 // Must be positive and 0 is already used; out of range blanks the value
// All the sensors we can accurately provide are listed here.
// i-BUS will generally only query up to 15 sensors, so subjectively
// higher-value sensors are sorted to the top to make the most of a
// small telemetry window. In the future these could be configurable.
static const AP_IBus_Telem::SensorDefinition sensors[] {
#if AP_ARMING_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_ARMED, .sensor_length = 2},
#endif
{.sensor_type = IBUS_SENSOR_TYPE_FLIGHT_MODE, .sensor_length = 2},
#if AP_GPS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_GPS_STATUS, .sensor_length = 2},
#endif
#if AP_BATTERY_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_FUEL, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_EXTERNAL_VOLTAGE, .sensor_length = 2},
#endif
#if AP_BARO_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_ALT, .sensor_length = 4},
#endif
#if AP_AHRS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_GPS_DIST, .sensor_length = 2},
#endif
#if AP_BARO_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_CLIMB_RATE, .sensor_length = 2},
#endif
#if AP_GPS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_GROUND_SPEED, .sensor_length = 2},
#endif
#if AP_AHRS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_ROLL, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_PITCH, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_YAW, .sensor_length = 2},
#endif
#if AP_AIRSPEED_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_SPEED, .sensor_length = 2},
#endif
#if AP_BARO_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_TEMPERATURE_PRESSURE, .sensor_length = 4},
#endif
#if AP_RPM_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_RPM, .sensor_length = 2},
#endif
#if AP_BATTERY_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_BATTERY_CURRENT, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_AVERAGE_CELL_VOLTAGE, .sensor_length = 2},
#endif
#if AP_AHRS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_COMPASS_HEADING, .sensor_length = 2},
#endif
#if AP_GPS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_COG, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_GPS_LAT, .sensor_length = 4},
{.sensor_type = IBUS_SENSOR_TYPE_GPS_LNG, .sensor_length = 4},
{.sensor_type = IBUS_SENSOR_TYPE_GPS_ALT, .sensor_length = 4},
#endif
#if AP_AHRS_ENABLED
{.sensor_type = IBUS_SENSOR_TYPE_ACC_X, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_ACC_Y, .sensor_length = 2},
{.sensor_type = IBUS_SENSOR_TYPE_ACC_Z, .sensor_length = 2},
#endif
};
#if APM_BUILD_TYPE(APM_BUILD_Rover)
/* Rover modes:
MANUAL = 0
ACRO = 1
STEERING = 3
HOLD = 4
LOITER = 5
FOLLOW = 6
SIMPLE = 7
DOCK = 8
CIRCLE = 9
AUTO = 10
RTL = 11
SMART_RTL = 12
GUIDED = 15
INITIALISING = 16
*/
static const AP_IBus_Telem::ModeMap mode_map[] {
{.ap_mode = 1, .ibus_mode = IBUS_VEHICLE_MODE_ACRO},
{.ap_mode = 4, .ibus_mode = IBUS_VEHICLE_MODE_PHOLD},
{.ap_mode = 5, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
{.ap_mode = 9, .ibus_mode = IBUS_VEHICLE_MODE_CIRCLE},
{.ap_mode = 10, .ibus_mode = IBUS_VEHICLE_MODE_AUTO},
{.ap_mode = 11, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 12, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 15, .ibus_mode = IBUS_VEHICLE_MODE_GUIDED},
};
#elif APM_BUILD_COPTER_OR_HELI
/* Copter modes:
STABILIZE = 0
ACRO = 1
ALT_HOLD = 2
AUTO = 3
GUIDED = 4
LOITER = 5
RTL = 6
CIRCLE = 7
LAND = 9
DRIFT = 11
SPORT = 13
FLIP = 14
AUTOTUNE = 15
POSHOLD = 16
BRAKE = 17
THROW = 18
AVOID_ADSB = 19
GUIDED_NOGPS = 20
SMART_RTL = 21
FLOWHOLD = 22
FOLLOW = 23
ZIGZAG = 24
SYSTEMID = 25
AUTOROTATE = 26
AUTO_RTL = 27
TURTLE = 28
*/
static const AP_IBus_Telem::ModeMap mode_map[] {
{.ap_mode = 0, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 1, .ibus_mode = IBUS_VEHICLE_MODE_ACRO},
{.ap_mode = 2, .ibus_mode = IBUS_VEHICLE_MODE_AHOLD},
{.ap_mode = 3, .ibus_mode = IBUS_VEHICLE_MODE_AUTO},
{.ap_mode = 4, .ibus_mode = IBUS_VEHICLE_MODE_GUIDED},
{.ap_mode = 5, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
{.ap_mode = 6, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 7, .ibus_mode = IBUS_VEHICLE_MODE_CIRCLE},
{.ap_mode = 9, .ibus_mode = IBUS_VEHICLE_MODE_LAND},
{.ap_mode = 16, .ibus_mode = IBUS_VEHICLE_MODE_PHOLD},
{.ap_mode = 21, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 22, .ibus_mode = IBUS_VEHICLE_MODE_PHOLD},
{.ap_mode = 27, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
};
#elif APM_BUILD_TYPE(APM_BUILD_ArduPlane)
/* Plane modes:
MANUAL = 0
CIRCLE = 1
STABILIZE = 2
TRAINING = 3
ACRO = 4
FLY_BY_WIRE_A = 5
FLY_BY_WIRE_B = 6
CRUISE = 7
AUTOTUNE = 8
AUTO = 10
RTL = 11
LOITER = 12
TAKEOFF = 13
AVOID_ADSB = 14
GUIDED = 15
INITIALISING = 16
QSTABILIZE = 17
QHOVER = 18
QLOITER = 19
QLAND = 20
QRTL = 21
QAUTOTUNE = 22
QACRO = 23
THERMAL = 24
LOITER_ALT_QLAND = 25
*/
static const AP_IBus_Telem::ModeMap mode_map[] {
{.ap_mode = 1, .ibus_mode = IBUS_VEHICLE_MODE_CIRCLE},
{.ap_mode = 2, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 4, .ibus_mode = IBUS_VEHICLE_MODE_ACRO},
{.ap_mode = 5, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 6, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 7, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 10, .ibus_mode = IBUS_VEHICLE_MODE_AUTO},
{.ap_mode = 11, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 12, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
{.ap_mode = 13, .ibus_mode = IBUS_VEHICLE_MODE_AUTO},
{.ap_mode = 15, .ibus_mode = IBUS_VEHICLE_MODE_GUIDED},
{.ap_mode = 17, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 18, .ibus_mode = IBUS_VEHICLE_MODE_PHOLD},
{.ap_mode = 19, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
{.ap_mode = 20, .ibus_mode = IBUS_VEHICLE_MODE_LAND},
{.ap_mode = 21, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 23, .ibus_mode = IBUS_VEHICLE_MODE_ACRO},
{.ap_mode = 25, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
};
#elif APM_BUILD_TYPE(APM_BUILD_ArduSub)
/* Submarine modes:
STABILIZE = 0
ACRO = 1
ALT_HOLD = 2
AUTO = 3
GUIDED = 4
CIRCLE = 7
SURFACE = 9
POSHOLD = 16
MANUAL = 19
MOTOR_DETECT = 20
*/
static const AP_IBus_Telem::ModeMap mode_map[] {
{.ap_mode = 0, .ibus_mode = IBUS_VEHICLE_MODE_STAB},
{.ap_mode = 1, .ibus_mode = IBUS_VEHICLE_MODE_ACRO},
{.ap_mode = 2, .ibus_mode = IBUS_VEHICLE_MODE_AHOLD},
{.ap_mode = 3, .ibus_mode = IBUS_VEHICLE_MODE_AUTO},
{.ap_mode = 4, .ibus_mode = IBUS_VEHICLE_MODE_GUIDED},
{.ap_mode = 5, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
{.ap_mode = 6, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
{.ap_mode = 7, .ibus_mode = IBUS_VEHICLE_MODE_CIRCLE},
{.ap_mode = 8, .ibus_mode = IBUS_VEHICLE_MODE_PHOLD},
{.ap_mode = 9, .ibus_mode = IBUS_VEHICLE_MODE_LAND},
{.ap_mode = 16, .ibus_mode = IBUS_VEHICLE_MODE_PHOLD},
};
#elif APM_BUILD_TYPE(APM_BUILD_Blimp)
/* Blimp modes:
LAND = 0
MANUAL = 1
VELOCITY = 2
LOITER = 3
RTL = 4
*/
static const AP_IBus_Telem::ModeMap mode_map[] {
{.ap_mode = 0, .ibus_mode = IBUS_VEHICLE_MODE_LAND},
{.ap_mode = 3, .ibus_mode = IBUS_VEHICLE_MODE_LOITER},
{.ap_mode = 4, .ibus_mode = IBUS_VEHICLE_MODE_RTL},
};
#else
static const AP_IBus_Telem::ModeMap mode_map[] {};
#endif
void AP_IBus_Telem::init()
{
const AP_SerialManager &serial_manager = AP::serialmanager();
if ((port = serial_manager.find_serial(AP_SerialManager::SerialProtocol_IBUS_Telem, 0))) {
port->set_options(port->OPTION_HDPLEX);
hal.scheduler->register_timer_process(FUNCTOR_BIND_MEMBER(&AP_IBus_Telem::tick, void));
}
}
void AP_IBus_Telem::tick(void)
{
if (!initialized) {
port->begin(115200);
initialized = true;
}
const uint16_t available = MIN(port->available(), 1024U);
if (available == 0) {
return;
}
const uint32_t now = AP_HAL::millis();
if ((now - last_received_message_time_ms) < PROTOCOL_TIMEGAP) {
// Any bytes that arrive too soon since the last message should be discarded
port->discard_input();
return;
}
union {
CommandPacket incoming_message;
uint8_t buffer[sizeof(CommandPacket)];
} u;
if (available < sizeof(u.incoming_message)) {
// We've got here mid-message; come back in the next loop when it might be complete
return;
}
last_received_message_time_ms = now;
if (available > sizeof(u.incoming_message)) {
// We've received too many bytes, so probably a malformed message
port->discard_input();
return;
}
if (port->read(u.buffer, sizeof(u.buffer)) != sizeof(u.buffer)) {
// Despite all our checks this is still the wrong size; safest to wait until the next message
port->discard_input();
return;
}
if (u.incoming_message.message_length != PROTOCOL_INCOMING_MESSAGE_LENGTH) {
// Every message starts with this fixed message length; if not then it's not valid
return;
}
uint16_t calculated_checksum = 0xFFFF;
for (uint8_t i = 0; i < sizeof(u.buffer) - 2; i++) {
calculated_checksum -= u.buffer[i];
}
if (u.incoming_message.checksum != calculated_checksum) {
return;
}
handle_incoming_message(u.incoming_message);
}
void AP_IBus_Telem::handle_incoming_message(const CommandPacket &incoming)
{
// Sensor 0 is reserved for the transmitter, so if for some reason it's requested we should ignore it
if (incoming.sensor_id == 0) {
return;
}
// If we've reached the end of our sensor list, we shouldn't respond; this tells the receiver that there
// are no more sensors to discover.
if (incoming.sensor_id > ARRAY_SIZE(sensors)) {
return;
}
const auto &sensor_definition = sensors[incoming.sensor_id - 1];
switch (incoming.command << 4) {
case PROTOCOL_COMMAND_DISCOVER:
handle_discover_command(incoming);
break;
case PROTOCOL_COMMAND_TYPE:
handle_type_command(incoming, sensor_definition);
break;
case PROTOCOL_COMMAND_VALUE:
handle_value_command(incoming, sensor_definition);
break;
}
}
/* A discovery query has the following format:
* 0x04: Message length
* 0x81: 0x80 for discovery + 0x01 for sensor ID 1
* 0x7A: Checksum low byte
* 0xFF: Checksum high byte
Checksums are 0xFFFF minus the sum of the previous bytes
To acknowledge a discovery query, we echo the command back.
*/
void AP_IBus_Telem::handle_discover_command(const CommandPacket &incoming)
{
struct protocol_command_discover_response_t {
uint8_t command_length;
uint8_t address;
uint16_t checksum;
} packet {
PROTOCOL_FOUR_LENGTH,
(uint8_t)(PROTOCOL_COMMAND_DISCOVER | incoming.sensor_id)
};
populate_checksum((uint8_t*)&packet, sizeof(packet));
port->write((uint8_t*)&packet, sizeof(packet));
}
/* A type query has the following format:
* 0x04: Message length
* 0x91: 0x90 for type + 0x01 for sensor ID 1
* 0x6A: Checksum low byte
* 0xFF: Checksum high byte
Checksums are 0xFFFF minus the sum of the previous bytes
To respond to a type query, we send:
* 0x06: Message length
* 0x91: 0x90 for type + 0x01 for sensor ID 1
* 0x03: Sensor type, eg 0x03 for external voltage
* 0x02: Sensor length (2 or 4 bytes)
* 0x63: Checksum low byte
* 0xFF: Checksum high byte
Checksums are 0xFFFF minus the sum of the previous bytes
*/
void AP_IBus_Telem::handle_type_command(const CommandPacket &incoming, const SensorDefinition &sensor)
{
struct protocol_command_type_response_t {
uint8_t command_length;
uint8_t address;
uint8_t type;
uint8_t length;
uint16_t checksum;
} packet {
PROTOCOL_SIX_LENGTH,
(uint8_t)(PROTOCOL_COMMAND_TYPE | incoming.sensor_id),
sensor.sensor_type,
sensor.sensor_length
};
populate_checksum((uint8_t*)&packet, sizeof(packet));
port->write((uint8_t*)&packet, sizeof(packet));
}
/* A value query has the following format:
* 0x04: Message length
* 0xA1: 0xA0 for value + 0x01 for sensor ID 1
* 0x5A: Checksum low byte
* 0xFF: Checksum high byte
Checksums are 0xFFFF minus the sum of the previous bytes
To respond to a value query, we send:
* 0x06: Message length (or 0x08 for a 4-byte sensor)
* 0xA1: 0xA1 for value + 0x01 for sensor ID 1
* 0xD4: Value byte (value is 12,500 in this example)
* 0x30: Value byte
* 0x54: Checksum low byte
* 0xFE: Checksum high byte
Checksums are 0xFFFF minus the sum of the previous bytes
*/
void AP_IBus_Telem::handle_value_command(const CommandPacket &incoming, const SensorDefinition &sensor)
{
const SensorValue value = get_sensor_value(sensor.sensor_type);
if (sensor.sensor_length == 2) {
handle_2_byte_value_command(incoming, value);
} else {
handle_4_byte_value_command(incoming, value);
}
}
void AP_IBus_Telem::handle_2_byte_value_command(const CommandPacket &incoming, const SensorValue &value)
{
struct protocol_command_2byte_value_response_t {
uint8_t command_length;
uint8_t address;
uint8_t value[2];
uint16_t checksum;
} packet {
PROTOCOL_SIX_LENGTH,
(uint8_t)(PROTOCOL_COMMAND_VALUE | incoming.sensor_id),
{value.byte[0], value.byte[1]}
};
populate_checksum((uint8_t*)&packet, sizeof(packet));
port->write((uint8_t*)&packet, sizeof(packet));
}
void AP_IBus_Telem::handle_4_byte_value_command(const CommandPacket &incoming, const SensorValue &value)
{
struct protocol_command_4byte_value_response_t {
uint8_t command_length;
uint8_t address;
uint8_t value[4];
uint16_t checksum;
} packet {
PROTOCOL_EIGHT_LENGTH,
(uint8_t)(PROTOCOL_COMMAND_VALUE | incoming.sensor_id),
{value.byte[0], value.byte[1], value.byte[2], value.byte[3]}
};
populate_checksum((uint8_t*)&packet, sizeof(packet));
port->write((uint8_t*)&packet, sizeof(packet));
}
AP_IBus_Telem::SensorValue AP_IBus_Telem::get_sensor_value(const uint8_t sensor_type)
{
SensorValue value;
switch (sensor_type) {
#if AP_BATTERY_ENABLED
case IBUS_SENSOR_TYPE_EXTERNAL_VOLTAGE:
value.uint16 = AP::battery().voltage() * 100;
break;
case IBUS_SENSOR_TYPE_AVERAGE_CELL_VOLTAGE:
value.uint16 = get_average_cell_voltage_cV();
break;
case IBUS_SENSOR_TYPE_BATTERY_CURRENT:
value.uint16 = get_current_cAh();
break;
case IBUS_SENSOR_TYPE_FUEL:
value.uint16 = get_battery_or_fuel_level_pct();
break;
#endif // AP_BATTERY_ENABLED
#if AP_RPM_ENABLED
case IBUS_SENSOR_TYPE_RPM:
case IBUS_SENSOR_TYPE_RPM_FLYSKY:
value.uint16 = get_rpm();
break;
#endif
#if AP_AHRS_ENABLED
case IBUS_SENSOR_TYPE_COMPASS_HEADING:
value.uint16 = AP::ahrs().yaw_sensor * 0.01;
break;
#endif
#if AP_BARO_ENABLED
case IBUS_SENSOR_TYPE_CLIMB_RATE:
case IBUS_SENSOR_TYPE_VERTICAL_SPEED:
value.int16 = AP::baro().get_climb_rate() * 100;
break;
#endif
#if AP_GPS_ENABLED
case IBUS_SENSOR_TYPE_COG:
value.uint16 = AP::gps().ground_course() * 100;
break;
case IBUS_SENSOR_TYPE_GPS_STATUS:
value.byte[0] = get_gps_status();
value.byte[1] = AP::gps().num_sats();
break;
#endif // AP_GPS_ENABLED
#if AP_AHRS_ENABLED
case IBUS_SENSOR_TYPE_ACC_X:
value.int16 = (AP::ahrs().get_accel() - AP::ahrs().get_accel_bias()).x * 1000;
break;
case IBUS_SENSOR_TYPE_ACC_Y:
value.int16 = (AP::ahrs().get_accel() - AP::ahrs().get_accel_bias()).y * 1000;
break;
case IBUS_SENSOR_TYPE_ACC_Z:
value.int16 = (AP::ahrs().get_accel() - AP::ahrs().get_accel_bias()).z * 1000;
break;
case IBUS_SENSOR_TYPE_ROLL:
value.int16 = AP::ahrs().roll_sensor;
break;
case IBUS_SENSOR_TYPE_PITCH:
value.int16 = AP::ahrs().pitch_sensor;
break;
case IBUS_SENSOR_TYPE_YAW:
value.int16 = AP::ahrs().yaw_sensor;
break;
#endif // AP_AHRS_ENABLED
#if AP_GPS_ENABLED
case IBUS_SENSOR_TYPE_GROUND_SPEED:
value.uint16 = AP::gps().ground_speed_cm();
break;
#endif
#if AP_AHRS_ENABLED
case IBUS_SENSOR_TYPE_GPS_DIST:
value.uint16 = get_distance_from_home_m();
break;
#endif
#if AP_ARMING_ENABLED
case IBUS_SENSOR_TYPE_ARMED:
value.uint16 = AP::arming().is_armed();
break;
#endif
case IBUS_SENSOR_TYPE_FLIGHT_MODE:
value.uint16 = get_vehicle_mode();
break;
#if AP_AIRSPEED_ENABLED
case IBUS_SENSOR_TYPE_SPEED:
value.uint16 = AP::airspeed()->get_airspeed();
break;
#endif
#if AP_BARO_ENABLED
case IBUS_SENSOR_TYPE_TEMPERATURE_PRESSURE: {
const uint32_t pressure = AP::baro().get_pressure();
const uint32_t temperature = (AP::baro().get_temperature() + 40) * 10;
value.uint32 = pressure | (temperature << 19);
break;
}
#endif
#if AP_GPS_ENABLED
case IBUS_SENSOR_TYPE_GPS_LAT:
value.int32 = AP::gps().location().lat;
break;
case IBUS_SENSOR_TYPE_GPS_LNG:
value.int32 = AP::gps().location().lng;
break;
case IBUS_SENSOR_TYPE_GPS_ALT:
value.int32 = AP::gps().location().alt;
break;
#endif // AP_GPS_ENABLED
#if AP_BARO_ENABLED
case IBUS_SENSOR_TYPE_ALT:
value.int32 = AP::baro().get_altitude() * 100;
break;
#endif
default:
value.int32 = 0;
}
return value;
}
#if AP_BATTERY_ENABLED
uint16_t AP_IBus_Telem::get_average_cell_voltage_cV()
{
if (!AP::battery().has_cell_voltages()) {
return 0;
}
const auto &cell_voltages = AP::battery().get_cell_voltages();
const uint8_t number_of_cells = ARRAY_SIZE(cell_voltages.cells);
if (number_of_cells == 0) {
return 0;
}
uint32_t voltage_sum = 0;
for (auto i = 0; i < number_of_cells; i++) {
voltage_sum += cell_voltages.cells[i] * 0.001;
}
return voltage_sum / number_of_cells * 100;
}
uint16_t AP_IBus_Telem::get_current_cAh()
{
float current = 0;
IGNORE_RETURN(AP::battery().current_amps(current));
return current * 100;
}
uint8_t AP_IBus_Telem::get_battery_or_fuel_level_pct()
{
uint8_t percentage = 0;
IGNORE_RETURN(AP::battery().capacity_remaining_pct(percentage));
return percentage;
}
#endif // AP_BATTERY_ENABLED
#if AP_RPM_ENABLED
uint16_t AP_IBus_Telem::get_rpm()
{
const AP_RPM *rpm = AP::rpm();
float rpm_value;
if (rpm && rpm->get_rpm(0, rpm_value)) {
return rpm_value;
}
return 0;
}
#endif
#if AP_GPS_ENABLED
uint8_t AP_IBus_Telem::get_gps_status()
{
if (!AP::gps().is_healthy()) {
return 0;
}
const AP_GPS::GPS_Status gps_status = AP::gps().status();
if (gps_status >= AP_GPS::GPS_OK_FIX_3D) {
return 3;
} else if (gps_status >= AP_GPS::GPS_OK_FIX_2D) {
return 2;
} else if (gps_status == AP_GPS::NO_FIX) {
return 1;
} else {
return 0;
}
}
#endif // AP_GPS_ENABLED
#if AP_AHRS_ENABLED
uint16_t AP_IBus_Telem::get_distance_from_home_m()
{
Vector2f home;
if (AP::ahrs().get_relative_position_NE_home(home)) {
return home.length();
}
return 0;
}
#endif
uint16_t AP_IBus_Telem::get_vehicle_mode()
{
const uint8_t vehicle_mode = AP::vehicle()->get_mode();
for (uint8_t i = 0; i < ARRAY_SIZE(mode_map); i++) {
if (mode_map[i].ap_mode == vehicle_mode) {
return mode_map[i].ibus_mode;
}
}
return IBUS_VEHICLE_MODE_UNKNOWN;
}
// Populate the last two bytes of packet with the checksum of the preceding bytes
void AP_IBus_Telem::populate_checksum(uint8_t *packet, const uint16_t size)
{
uint16_t checksum = 0xFFFF;
for (int i=0; i<size-2; i++) {
checksum -= packet[i];
}
packet[size-2] = checksum & 0x0ff;
packet[size-1] = checksum >> 8;
}
#endif

View File

@ -0,0 +1,126 @@
/*
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/>.
i-BUS telemetry for FlySky/Turnigy receivers and other peripherals
(eg iA6B, iA10) by Nicole Ashley <nicole@ashley.kiwi>.
Originally based on work by Jan Verhulst:
https://github.com/ArduPilot/ardupilot/pull/16545
Libraries used for reference and inspiration:
* iBUStelemetry
https://github.com/Hrastovc/iBUStelemetry
* IBusBM
https://github.com/bmellink/IBusBM
* BetaFlight
https://github.com/betaflight/betaflight/blob/master/src/main/telemetry/ibus_shared.c
*/
#pragma once
#include <AP_HAL/AP_HAL_Boards.h>
#ifndef AP_IBUS_TELEM_ENABLED
#define AP_IBUS_TELEM_ENABLED BOARD_FLASH_SIZE > 2048
#endif
#if AP_IBUS_TELEM_ENABLED
#include <AP_HAL/AP_HAL.h>
#include <AP_Math/vector3.h>
#include <AP_AHRS/AP_AHRS_config.h>
#include <AP_BattMonitor/AP_BattMonitor_config.h>
#include <AP_GPS/AP_GPS_config.h>
#include <AP_RPM/AP_RPM_config.h>
class AP_IBus_Telem
{
public:
AP_IBus_Telem() {}
CLASS_NO_COPY(AP_IBus_Telem);
void init();
typedef struct {
uint8_t sensor_type; // Sensor type (IBUS_SENSOR_TYPE_* above)
uint8_t sensor_length; // Data length for defined sensor (can be 2 or 4 bytes)
} SensorDefinition;
typedef struct {
uint8_t ap_mode;
uint8_t ibus_mode;
} ModeMap;
private:
typedef union {
uint16_t uint16;
uint32_t uint32;
int16_t int16;
int32_t int32;
uint8_t byte[4];
} SensorValue;
struct PACKED CommandPacket {
uint8_t message_length;
uint8_t sensor_id : 4;
uint8_t command : 4;
uint16_t checksum;
};
void tick();
void handle_incoming_message(const CommandPacket &incoming);
void handle_discover_command(const CommandPacket &incoming);
void handle_type_command(const CommandPacket &incoming, const SensorDefinition &sensor);
void handle_value_command(const CommandPacket &incoming, const SensorDefinition &sensor);
void handle_2_byte_value_command(const CommandPacket &incoming, const SensorValue &value);
void handle_4_byte_value_command(const CommandPacket &incoming, const SensorValue &value);
SensorValue get_sensor_value(uint8_t sensor_id);
#if AP_BATTERY_ENABLED
uint16_t get_average_cell_voltage_cV();
uint16_t get_current_cAh();
uint8_t get_battery_or_fuel_level_pct();
#endif
#if AP_RPM_ENABLED
uint16_t get_rpm();
#endif
#if AP_GPS_ENABLED
uint8_t get_gps_status();
#endif
#if AP_AHRS_ENABLED
uint16_t get_distance_from_home_m();
#endif
uint16_t get_vehicle_mode();
void populate_checksum(uint8_t *packet, const uint16_t size);
static const uint8_t PROTOCOL_COMMAND_DISCOVER = 0x80; // Command to discover a sensor (lowest 4 bits are sensor)
static const uint8_t PROTOCOL_COMMAND_TYPE = 0x90; // Command to request a sensor type (lowest 4 bits are sensor)
static const uint8_t PROTOCOL_COMMAND_VALUE = 0xA0; // Command to request a sensor's value (lowest 4 bits are sensor)
static const uint8_t PROTOCOL_TIMEGAP = 3; // Packets are received very ~7ms so use ~half that for the gap
static const uint8_t PROTOCOL_FOUR_LENGTH = 0x04; // indicate that the message has 4 bytes
static const uint8_t PROTOCOL_SIX_LENGTH = 0x06; // indicate that the message has 6 bytes
static const uint8_t PROTOCOL_EIGHT_LENGTH = 0x08; // indicate that the message has 8 bytes
static const uint8_t PROTOCOL_INCOMING_MESSAGE_LENGTH = PROTOCOL_FOUR_LENGTH; // All incoming messages are the same length
AP_HAL::UARTDriver *port;
bool initialized;
uint32_t last_received_message_time_ms; // When the last message was received
};
#endif