Line data Source code
1 : #include "AP_BattMonitor_config.h"
2 :
3 : #if AP_BATTERY_UAVCAN_BATTERYINFO_ENABLED
4 :
5 : #include <AP_HAL/AP_HAL.h>
6 : #include "AP_BattMonitor.h"
7 : #include "AP_BattMonitor_DroneCAN.h"
8 :
9 : #include <AP_CANManager/AP_CANManager.h>
10 : #include <AP_Common/AP_Common.h>
11 : #include <GCS_MAVLink/GCS.h>
12 : #include <AP_Math/AP_Math.h>
13 : #include <AP_DroneCAN/AP_DroneCAN.h>
14 : #include <AP_BoardConfig/AP_BoardConfig.h>
15 :
16 : extern const AP_HAL::HAL& hal;
17 :
18 : const AP_Param::GroupInfo AP_BattMonitor_DroneCAN::var_info[] = {
19 :
20 : // @Param: CURR_MULT
21 : // @DisplayName: Scales reported power monitor current
22 : // @Description: Multiplier applied to all current related reports to allow for adjustment if no UAVCAN param access or current splitting applications
23 : // @Range: 0.1 10
24 : // @User: Advanced
25 : AP_GROUPINFO("CURR_MULT", 30, AP_BattMonitor_DroneCAN, _curr_mult, 1.0),
26 :
27 : // CHECK/UPDATE INDEX TABLE IN AP_BattMonitor_Backend.cpp WHEN CHANGING OR ADDING PARAMETERS
28 :
29 : AP_GROUPEND
30 : };
31 :
32 : /// Constructor
33 0 : AP_BattMonitor_DroneCAN::AP_BattMonitor_DroneCAN(AP_BattMonitor &mon, AP_BattMonitor::BattMonitor_State &mon_state, BattMonitor_DroneCAN_Type type, AP_BattMonitor_Params ¶ms) :
34 0 : AP_BattMonitor_Backend(mon, mon_state, params)
35 : {
36 0 : AP_Param::setup_object_defaults(this,var_info);
37 0 : _state.var_info = var_info;
38 :
39 : // starts with not healthy
40 0 : _state.healthy = false;
41 0 : }
42 :
43 5 : bool AP_BattMonitor_DroneCAN::subscribe_msgs(AP_DroneCAN* ap_dronecan)
44 : {
45 5 : const auto driver_index = ap_dronecan->get_driver_index();
46 :
47 5 : return (Canard::allocate_sub_arg_callback(ap_dronecan, &handle_battery_info_trampoline, driver_index) != nullptr)
48 5 : && (Canard::allocate_sub_arg_callback(ap_dronecan, &handle_battery_info_aux_trampoline, driver_index) != nullptr)
49 10 : && (Canard::allocate_sub_arg_callback(ap_dronecan, &handle_mppt_stream_trampoline, driver_index) != nullptr)
50 : ;
51 : }
52 :
53 : /*
54 : match a battery ID to driver serial number
55 : when serial number is negative, all batteries are accepted, otherwise it must match
56 : */
57 0 : bool AP_BattMonitor_DroneCAN::match_battery_id(uint8_t instance, uint8_t battery_id)
58 : {
59 0 : const auto serial_num = AP::battery().get_serial_number(instance);
60 0 : return serial_num < 0 || serial_num == (int32_t)battery_id;
61 : }
62 :
63 0 : AP_BattMonitor_DroneCAN* AP_BattMonitor_DroneCAN::get_dronecan_backend(AP_DroneCAN* ap_dronecan, uint8_t node_id, uint8_t battery_id)
64 : {
65 0 : if (ap_dronecan == nullptr) {
66 0 : return nullptr;
67 : }
68 0 : const auto &batt = AP::battery();
69 0 : for (uint8_t i = 0; i < batt._num_instances; i++) {
70 0 : if (batt.drivers[i] == nullptr ||
71 0 : batt.allocated_type(i) != AP_BattMonitor::Type::UAVCAN_BatteryInfo) {
72 0 : continue;
73 : }
74 0 : AP_BattMonitor_DroneCAN* driver = (AP_BattMonitor_DroneCAN*)batt.drivers[i];
75 0 : if (driver->_ap_dronecan == ap_dronecan && match_battery_id(i, battery_id)) {
76 0 : if (driver->_node_id == node_id) {
77 0 : return driver;
78 0 : } else if (!driver->_interim_state.healthy && driver->option_is_set(AP_BattMonitor_Params::Options::AllowDynamicNodeUpdate)) {
79 0 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Battery %u: Node change from %d to %d for Id %d",
80 : (unsigned)i+1, driver->_node_id, node_id, battery_id);
81 0 : driver->_node_id = node_id;
82 0 : driver->init();
83 0 : return driver;
84 : }
85 : }
86 : }
87 : // find empty uavcan driver
88 0 : for (uint8_t i = 0; i < batt._num_instances; i++) {
89 0 : if (batt.drivers[i] != nullptr &&
90 0 : batt.allocated_type(i) == AP_BattMonitor::Type::UAVCAN_BatteryInfo &&
91 0 : match_battery_id(i, battery_id)) {
92 :
93 0 : AP_BattMonitor_DroneCAN* batmon = (AP_BattMonitor_DroneCAN*)batt.drivers[i];
94 0 : if(batmon->_ap_dronecan != nullptr || batmon->_node_id != 0) {
95 0 : continue;
96 : }
97 0 : batmon->_ap_dronecan = ap_dronecan;
98 0 : batmon->_node_id = node_id;
99 0 : batmon->_instance = i;
100 0 : batmon->init();
101 0 : return batmon;
102 : }
103 : }
104 0 : return nullptr;
105 : }
106 :
107 0 : void AP_BattMonitor_DroneCAN::handle_battery_info(const uavcan_equipment_power_BatteryInfo &msg)
108 : {
109 0 : update_interim_state(msg.voltage, msg.current, msg.temperature, msg.state_of_charge_pct, msg.state_of_health_pct);
110 :
111 0 : WITH_SEMAPHORE(_sem_battmon);
112 0 : _remaining_capacity_wh = msg.remaining_capacity_wh;
113 0 : _full_charge_capacity_wh = msg.full_charge_capacity_wh;
114 :
115 : // consume state of health
116 0 : if (msg.state_of_health_pct != UAVCAN_EQUIPMENT_POWER_BATTERYINFO_STATE_OF_HEALTH_UNKNOWN) {
117 0 : _interim_state.state_of_health_pct = msg.state_of_health_pct;
118 0 : _interim_state.has_state_of_health_pct = true;
119 : }
120 0 : }
121 :
122 0 : void AP_BattMonitor_DroneCAN::update_interim_state(const float voltage, const float current, const float temperature_K, const uint8_t soc, uint8_t soh_pct)
123 : {
124 0 : WITH_SEMAPHORE(_sem_battmon);
125 :
126 0 : _interim_state.voltage = voltage;
127 0 : _interim_state.current_amps = _curr_mult * current;
128 0 : _soc = soc;
129 :
130 0 : if (!isnan(temperature_K) && temperature_K > 0) {
131 : // Temperature reported from battery in kelvin and stored internally in Celsius.
132 0 : _interim_state.temperature = KELVIN_TO_C(temperature_K);
133 0 : _interim_state.temperature_time = AP_HAL::millis();
134 : }
135 :
136 0 : const uint32_t tnow = AP_HAL::micros();
137 :
138 0 : if (!_has_battery_info_aux ||
139 0 : !use_CAN_SoC()) {
140 0 : const uint32_t dt_us = tnow - _interim_state.last_time_micros;
141 :
142 : // update total current drawn since startup
143 0 : update_consumed(_interim_state, dt_us);
144 : }
145 :
146 : // state of health
147 0 : if (soh_pct != UAVCAN_EQUIPMENT_POWER_BATTERYINFO_STATE_OF_HEALTH_UNKNOWN) {
148 0 : _interim_state.state_of_health_pct = soh_pct;
149 0 : _interim_state.has_state_of_health_pct = true;
150 : }
151 :
152 : // record time
153 0 : _interim_state.last_time_micros = tnow;
154 0 : _interim_state.healthy = true;
155 0 : }
156 :
157 0 : void AP_BattMonitor_DroneCAN::handle_battery_info_aux(const ardupilot_equipment_power_BatteryInfoAux &msg)
158 : {
159 0 : WITH_SEMAPHORE(_sem_battmon);
160 0 : uint8_t cell_count = MIN(ARRAY_SIZE(_interim_state.cell_voltages.cells), msg.voltage_cell.len);
161 :
162 0 : _cycle_count = msg.cycle_count;
163 0 : for (uint8_t i = 0; i < cell_count; i++) {
164 0 : _interim_state.cell_voltages.cells[i] = msg.voltage_cell.data[i] * 1000;
165 : }
166 0 : _interim_state.is_powering_off = msg.is_powering_off;
167 0 : if (!isnan(msg.nominal_voltage) && msg.nominal_voltage > 0) {
168 0 : float remaining_capacity_ah = _remaining_capacity_wh / msg.nominal_voltage;
169 0 : float full_charge_capacity_ah = _full_charge_capacity_wh / msg.nominal_voltage;
170 0 : _interim_state.consumed_mah = (full_charge_capacity_ah - remaining_capacity_ah) * 1000;
171 0 : _interim_state.consumed_wh = _full_charge_capacity_wh - _remaining_capacity_wh;
172 0 : _interim_state.time_remaining = is_zero(_interim_state.current_amps) ? 0 : (remaining_capacity_ah / _interim_state.current_amps * 3600);
173 0 : _interim_state.has_time_remaining = true;
174 : }
175 :
176 0 : _has_cell_voltages = true;
177 0 : _has_battery_info_aux = true;
178 0 : }
179 :
180 0 : void AP_BattMonitor_DroneCAN::handle_mppt_stream(const mppt_Stream &msg)
181 : {
182 0 : const bool use_input_value = option_is_set(AP_BattMonitor_Params::Options::MPPT_Use_Input_Value);
183 0 : const float voltage = use_input_value ? msg.input_voltage : msg.output_voltage;
184 0 : const float current = use_input_value ? msg.input_current : msg.output_current;
185 :
186 : // use an invalid soc so we use the library calculated one
187 0 : const uint8_t soc = 127;
188 :
189 : // convert C to Kelvin
190 0 : const float temperature_K = isnan(msg.temperature) ? 0 : C_TO_KELVIN(msg.temperature);
191 :
192 0 : update_interim_state(voltage, current, temperature_K, soc, UAVCAN_EQUIPMENT_POWER_BATTERYINFO_STATE_OF_HEALTH_UNKNOWN);
193 :
194 0 : if (!_mppt.is_detected) {
195 : // this is the first time the mppt message has been received
196 : // so set powered up state
197 0 : _mppt.is_detected = true;
198 :
199 : // Boot/Power-up event
200 0 : if (option_is_set(AP_BattMonitor_Params::Options::MPPT_Power_On_At_Boot)) {
201 0 : set_powered_state(true);
202 0 : } else if (option_is_set(AP_BattMonitor_Params::Options::MPPT_Power_Off_At_Boot)) {
203 0 : set_powered_state(false);
204 : }
205 : }
206 :
207 : #if AP_BATTMONITOR_UAVCAN_MPPT_DEBUG
208 : if (_mppt.fault_flags != msg.fault_flags) {
209 : mppt_report_faults(_instance, msg.fault_flags);
210 : }
211 : #endif
212 0 : _mppt.fault_flags = msg.fault_flags;
213 0 : }
214 :
215 0 : void AP_BattMonitor_DroneCAN::handle_battery_info_trampoline(AP_DroneCAN *ap_dronecan, const CanardRxTransfer& transfer, const uavcan_equipment_power_BatteryInfo &msg)
216 : {
217 0 : AP_BattMonitor_DroneCAN* driver = get_dronecan_backend(ap_dronecan, transfer.source_node_id, msg.battery_id);
218 0 : if (driver == nullptr) {
219 0 : return;
220 : }
221 0 : driver->handle_battery_info(msg);
222 : }
223 :
224 0 : void AP_BattMonitor_DroneCAN::handle_battery_info_aux_trampoline(AP_DroneCAN *ap_dronecan, const CanardRxTransfer& transfer, const ardupilot_equipment_power_BatteryInfoAux &msg)
225 : {
226 0 : const auto &batt = AP::battery();
227 0 : AP_BattMonitor_DroneCAN *driver = nullptr;
228 :
229 : /*
230 : check for a backend with AllowSplitAuxInfo set, allowing InfoAux
231 : from a different CAN node than the base battery information
232 : */
233 0 : for (uint8_t i = 0; i < batt._num_instances; i++) {
234 0 : const auto *drv = batt.drivers[i];
235 0 : if (drv != nullptr &&
236 0 : batt.allocated_type(i) == AP_BattMonitor::Type::UAVCAN_BatteryInfo &&
237 0 : drv->option_is_set(AP_BattMonitor_Params::Options::AllowSplitAuxInfo) &&
238 0 : batt.get_serial_number(i) == int32_t(msg.battery_id)) {
239 0 : driver = (AP_BattMonitor_DroneCAN *)batt.drivers[i];
240 0 : if (driver->_ap_dronecan == nullptr) {
241 : /* we have not received the main battery information
242 : yet. Discard InfoAux until we do so we can init the
243 : backend with the right node ID
244 : */
245 0 : return;
246 : }
247 0 : break;
248 : }
249 : }
250 0 : if (driver == nullptr) {
251 0 : driver = get_dronecan_backend(ap_dronecan, transfer.source_node_id, msg.battery_id);
252 : }
253 0 : if (driver == nullptr) {
254 0 : return;
255 : }
256 0 : driver->handle_battery_info_aux(msg);
257 : }
258 :
259 0 : void AP_BattMonitor_DroneCAN::handle_mppt_stream_trampoline(AP_DroneCAN *ap_dronecan, const CanardRxTransfer& transfer, const mppt_Stream &msg)
260 : {
261 0 : AP_BattMonitor_DroneCAN* driver = get_dronecan_backend(ap_dronecan, transfer.source_node_id, transfer.source_node_id);
262 0 : if (driver == nullptr) {
263 0 : return;
264 : }
265 0 : driver->handle_mppt_stream(msg);
266 : }
267 :
268 : // read - read the voltage and current
269 0 : void AP_BattMonitor_DroneCAN::read()
270 : {
271 0 : uint32_t tnow = AP_HAL::micros();
272 :
273 : // timeout after 5 seconds
274 0 : if ((tnow - _interim_state.last_time_micros) > AP_BATTMONITOR_UAVCAN_TIMEOUT_MICROS) {
275 0 : _interim_state.healthy = false;
276 : }
277 : // Copy over relevant states over to main state
278 0 : WITH_SEMAPHORE(_sem_battmon);
279 0 : _state.temperature = _interim_state.temperature;
280 0 : _state.temperature_time = _interim_state.temperature_time;
281 0 : _state.voltage = _interim_state.voltage;
282 0 : _state.current_amps = _interim_state.current_amps;
283 0 : _state.consumed_mah = _interim_state.consumed_mah;
284 0 : _state.consumed_wh = _interim_state.consumed_wh;
285 0 : _state.last_time_micros = _interim_state.last_time_micros;
286 0 : _state.healthy = _interim_state.healthy;
287 0 : _state.time_remaining = _interim_state.time_remaining;
288 0 : _state.has_time_remaining = _interim_state.has_time_remaining;
289 0 : _state.is_powering_off = _interim_state.is_powering_off;
290 0 : _state.state_of_health_pct = _interim_state.state_of_health_pct;
291 0 : _state.has_state_of_health_pct = _interim_state.has_state_of_health_pct;
292 0 : memcpy(_state.cell_voltages.cells, _interim_state.cell_voltages.cells, sizeof(_state.cell_voltages));
293 :
294 0 : _has_temperature = (AP_HAL::millis() - _state.temperature_time) <= AP_BATT_MONITOR_TIMEOUT;
295 :
296 : // check if MPPT should be powered on/off depending upon arming state
297 0 : if (_mppt.is_detected) {
298 0 : mppt_check_powered_state();
299 : }
300 0 : }
301 :
302 : // Return true if the DroneCAN state of charge should be used.
303 : // Return false if state of charge should be calculated locally by counting mah.
304 0 : bool AP_BattMonitor_DroneCAN::use_CAN_SoC() const
305 : {
306 : // a UAVCAN battery monitor may not be able to supply a state of charge. If it can't then
307 : // the user can set the option to use current integration in the backend instead.
308 : // SOC of 127 is used as an invalid SOC flag ie system configuration errors or SOC estimation unavailable
309 0 : return !(option_is_set(AP_BattMonitor_Params::Options::Ignore_UAVCAN_SoC) ||
310 0 : _mppt.is_detected ||
311 0 : (_soc == 127));
312 : }
313 :
314 : /// capacity_remaining_pct - returns true if the percentage is valid and writes to percentage argument
315 0 : bool AP_BattMonitor_DroneCAN::capacity_remaining_pct(uint8_t &percentage) const
316 : {
317 0 : if (!use_CAN_SoC()) {
318 0 : return AP_BattMonitor_Backend::capacity_remaining_pct(percentage);
319 : }
320 :
321 : // the monitor must have current readings in order to estimate consumed_mah and be healthy
322 0 : if (!has_current() || !_state.healthy) {
323 0 : return false;
324 : }
325 :
326 0 : percentage = _soc;
327 0 : return true;
328 : }
329 :
330 : // reset remaining percentage to given value
331 0 : bool AP_BattMonitor_DroneCAN::reset_remaining(float percentage)
332 : {
333 0 : if (use_CAN_SoC()) {
334 : // Cannot reset external state of charge
335 0 : return false;
336 : }
337 :
338 0 : WITH_SEMAPHORE(_sem_battmon);
339 :
340 0 : if (!AP_BattMonitor_Backend::reset_remaining(percentage)) {
341 : // Base class reset failed
342 0 : return false;
343 : }
344 :
345 : // Reset interim state that is used internally, this is then copied back to the main state in the read() call
346 0 : _interim_state.consumed_mah = _state.consumed_mah;
347 0 : _interim_state.consumed_wh = _state.consumed_wh;
348 0 : return true;
349 0 : }
350 :
351 : /// get_cycle_count - return true if cycle count can be provided and fills in cycles argument
352 0 : bool AP_BattMonitor_DroneCAN::get_cycle_count(uint16_t &cycles) const
353 : {
354 0 : if (_has_battery_info_aux) {
355 0 : cycles = _cycle_count;
356 0 : return true;
357 : }
358 0 : return false;
359 : }
360 :
361 : // request MPPT board to power on/off depending upon vehicle arming state as specified by BATT_OPTIONS
362 0 : void AP_BattMonitor_DroneCAN::mppt_check_powered_state()
363 : {
364 0 : if ((_mppt.powered_state_remote_ms != 0) && (AP_HAL::millis() - _mppt.powered_state_remote_ms >= 1000)) {
365 : // there's already a set attempt that didnt' respond. Retry at 1Hz
366 0 : set_powered_state(_mppt.powered_state);
367 : }
368 :
369 : // check if vehicle armed state has changed
370 0 : const bool vehicle_armed = hal.util->get_soft_armed();
371 0 : if ((!_mppt.vehicle_armed_last && vehicle_armed) && option_is_set(AP_BattMonitor_Params::Options::MPPT_Power_On_At_Arm)) {
372 : // arm event
373 0 : set_powered_state(true);
374 0 : } else if ((_mppt.vehicle_armed_last && !vehicle_armed) && option_is_set(AP_BattMonitor_Params::Options::MPPT_Power_Off_At_Disarm)) {
375 : // disarm event
376 0 : set_powered_state(false);
377 : }
378 0 : _mppt.vehicle_armed_last = vehicle_armed;
379 0 : }
380 :
381 : // request MPPT or BMS board to power on or off
382 : // power_on should be true to power on the MPPT, false to power off
383 : // force should be true to force sending the state change request to the MPPT
384 0 : void AP_BattMonitor_DroneCAN::set_powered_state(bool power_on)
385 : {
386 0 : if (_ap_dronecan == nullptr || !_mppt.is_detected) {
387 0 : return;
388 : }
389 :
390 0 : _mppt.powered_state = power_on;
391 :
392 0 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Battery %u: powering %s%s", (unsigned)_instance+1, _mppt.powered_state ? "ON" : "OFF",
393 : (_mppt.powered_state_remote_ms == 0) ? "" : " Retry");
394 :
395 : mppt_OutputEnableRequest request;
396 0 : request.enable = _mppt.powered_state;
397 0 : request.disable = !request.enable;
398 :
399 0 : if (mppt_outputenable_client == nullptr) {
400 0 : mppt_outputenable_client = NEW_NOTHROW Canard::Client<mppt_OutputEnableResponse>{_ap_dronecan->get_canard_iface(), mppt_outputenable_res_cb};
401 0 : if (mppt_outputenable_client == nullptr) {
402 0 : return;
403 : }
404 : }
405 0 : mppt_outputenable_client->request(_node_id, request);
406 : }
407 :
408 : // callback from outputEnable to verify it is enabled or disabled
409 0 : void AP_BattMonitor_DroneCAN::handle_outputEnable_response(const CanardRxTransfer& transfer, const mppt_OutputEnableResponse& response)
410 : {
411 0 : if (transfer.source_node_id != _node_id) {
412 : // this response is not from the node we are looking for
413 0 : return;
414 : }
415 :
416 0 : if (response.enabled == _mppt.powered_state) {
417 : // we got back what we expected it to be. We set it on, it now says it on (or vice versa).
418 : // Clear the timer so we don't re-request
419 0 : _mppt.powered_state_remote_ms = 0;
420 : }
421 : }
422 :
423 : #if AP_BATTMONITOR_UAVCAN_MPPT_DEBUG
424 : // report changes in MPPT faults
425 : void AP_BattMonitor_DroneCAN::mppt_report_faults(const uint8_t instance, const uint8_t fault_flags)
426 : {
427 : // handle recovery
428 : if (fault_flags == 0) {
429 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Battery %u: OK", (unsigned)instance+1);
430 : return;
431 : }
432 :
433 : // send battery faults via text messages
434 : for (uint8_t fault_bit=0x01; fault_bit <= 0x08; fault_bit <<= 1) {
435 : // this loop is to generate multiple messages if there are multiple concurrent faults, but also run once if there are no faults
436 : if ((fault_bit & fault_flags) != 0) {
437 : const MPPT_FaultFlags err = (MPPT_FaultFlags)fault_bit;
438 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Battery %u: %s", (unsigned)instance+1, mppt_fault_string(err));
439 : }
440 : }
441 : }
442 :
443 : // returns string description of MPPT fault bit. Only handles single bit faults
444 : const char* AP_BattMonitor_DroneCAN::mppt_fault_string(const MPPT_FaultFlags fault)
445 : {
446 : switch (fault) {
447 : case MPPT_FaultFlags::OVER_VOLTAGE:
448 : return "over voltage";
449 : case MPPT_FaultFlags::UNDER_VOLTAGE:
450 : return "under voltage";
451 : case MPPT_FaultFlags::OVER_CURRENT:
452 : return "over current";
453 : case MPPT_FaultFlags::OVER_TEMPERATURE:
454 : return "over temp";
455 : }
456 : return "unknown";
457 : }
458 : #endif
459 :
460 : // return mavlink fault bitmask (see MAV_BATTERY_FAULT enum)
461 0 : uint32_t AP_BattMonitor_DroneCAN::get_mavlink_fault_bitmask() const
462 : {
463 : // return immediately if not mppt or no faults
464 0 : if (!_mppt.is_detected || (_mppt.fault_flags == 0)) {
465 0 : return 0;
466 : }
467 :
468 : // convert mppt fault bitmask to mavlink fault bitmask
469 0 : uint32_t mav_fault_bitmask = 0;
470 0 : if ((_mppt.fault_flags & (uint8_t)MPPT_FaultFlags::OVER_VOLTAGE) || (_mppt.fault_flags & (uint8_t)MPPT_FaultFlags::UNDER_VOLTAGE)) {
471 0 : mav_fault_bitmask |= MAV_BATTERY_FAULT_INCOMPATIBLE_VOLTAGE;
472 : }
473 0 : if (_mppt.fault_flags & (uint8_t)MPPT_FaultFlags::OVER_CURRENT) {
474 0 : mav_fault_bitmask |= MAV_BATTERY_FAULT_OVER_CURRENT;
475 : }
476 0 : if (_mppt.fault_flags & (uint8_t)MPPT_FaultFlags::OVER_TEMPERATURE) {
477 0 : mav_fault_bitmask |= MAV_BATTERY_FAULT_OVER_TEMPERATURE;
478 : }
479 0 : return mav_fault_bitmask;
480 : }
481 :
482 : #endif // AP_BATTERY_UAVCAN_BATTERYINFO_ENABLED
|