Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ardupilot
GitHub Repository: Ardupilot/ardupilot
Path: blob/master/libraries/AP_AdvancedFailsafe/AP_AdvancedFailsafe.cpp
9448 views
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
16
/*
17
AP_AdvancedFailsafe.cpp
18
19
This is an advanced failsafe module originally modelled on the
20
failsafe rules of the Outback Challenge
21
*/
22
#include "AP_AdvancedFailsafe.h"
23
24
#if AP_ADVANCEDFAILSAFE_ENABLED
25
26
#include <AP_HAL/AP_HAL.h>
27
#include <RC_Channel/RC_Channel.h>
28
#include <SRV_Channel/SRV_Channel.h>
29
#include <GCS_MAVLink/GCS.h>
30
#include <AP_GPS/AP_GPS.h>
31
#include <AP_Baro/AP_Baro.h>
32
#include <AP_Mission/AP_Mission.h>
33
#include <AC_Fence/AC_Fence.h>
34
35
AP_AdvancedFailsafe *AP_AdvancedFailsafe::_singleton;
36
37
extern const AP_HAL::HAL& hal;
38
39
// table of user settable parameters
40
const AP_Param::GroupInfo AP_AdvancedFailsafe::var_info[] = {
41
// @Param: ENABLE
42
// @DisplayName: Enable Advanced Failsafe
43
// @Description: This enables the advanced failsafe system. If this is set to zero (disable) then all the other AFS options have no effect
44
// @User: Advanced
45
AP_GROUPINFO_FLAGS("ENABLE", 11, AP_AdvancedFailsafe, _enable, 0, AP_PARAM_FLAG_ENABLE),
46
47
// @Param: MAN_PIN
48
// @DisplayName: Manual Pin
49
// @Description: This sets a digital output pin to set high when in manual mode. See the Wiki's "GPIOs" page for how to determine the pin number for a given autopilot.
50
// @User: Advanced
51
AP_GROUPINFO("MAN_PIN", 0, AP_AdvancedFailsafe, _manual_pin, -1),
52
53
// @Param: HB_PIN
54
// @DisplayName: Heartbeat Pin
55
// @Description: This sets a digital output pin which is cycled at 10Hz when termination is not activated. Note that if a FS_TERM_PIN is set then the heartbeat pin will continue to cycle at 10Hz when termination is activated, to allow the termination board to distinguish between autopilot crash and termination. Some common values are given, but see the Wiki's "GPIOs" page for how to determine the pin number for a given autopilot.
56
// @User: Advanced
57
// @Values: -1:Disabled,49:BB Blue GP0 pin 4,50:AUXOUT1,51:AUXOUT2,52:AUXOUT3,53:AUXOUT4,54:AUXOUT5,55:AUXOUT6,57:BB Blue GP0 pin 3,113:BB Blue GP0 pin 6,116:BB Blue GP0 pin 5
58
// @Range: -1 127
59
AP_GROUPINFO("HB_PIN", 1, AP_AdvancedFailsafe, _heartbeat_pin, -1),
60
61
// @Param: WP_COMMS
62
// @DisplayName: Comms Waypoint
63
// @Description: Waypoint number to navigate to on comms loss
64
// @User: Advanced
65
AP_GROUPINFO("WP_COMMS", 2, AP_AdvancedFailsafe, _wp_comms_hold, 0),
66
67
// @Param: WP_GPS_LOSS
68
// @DisplayName: GPS Loss Waypoint
69
// @Description: Waypoint number to navigate to on GPS lock loss
70
// @User: Advanced
71
AP_GROUPINFO("WP_GPS_LOSS", 4, AP_AdvancedFailsafe, _wp_gps_loss, 0),
72
73
// @Param: TERMINATE
74
// @DisplayName: Force Terminate
75
// @Description: Can be set in flight to force termination of the heartbeat signal
76
// @User: Advanced
77
AP_GROUPINFO("TERMINATE", 5, AP_AdvancedFailsafe, _terminate, 0),
78
79
// @Param: TERM_ACTION
80
// @DisplayName: Terminate action
81
// @Description: This can be used to force an action on flight termination. Normally this is handled by an external failsafe board, but you can setup ArduPilot to handle it here. Please consult the wiki for more information on the possible values of the parameter
82
// @User: Advanced
83
AP_GROUPINFO("TERM_ACTION", 6, AP_AdvancedFailsafe, _terminate_action, 0),
84
85
// @Param: TERM_PIN
86
// @DisplayName: Terminate Pin
87
// @Description: This sets a digital output pin to set high on flight termination. Some common values are given, but see the Wiki's "GPIOs" page for how to determine the pin number for a given autopilot.
88
// @User: Advanced
89
// @Values: -1:Disabled,49:BB Blue GP0 pin 4,50:AUXOUT1,51:AUXOUT2,52:AUXOUT3,53:AUXOUT4,54:AUXOUT5,55:AUXOUT6,57:BB Blue GP0 pin 3,113:BB Blue GP0 pin 6,116:BB Blue GP0 pin 5
90
// @Range: -1 127
91
AP_GROUPINFO("TERM_PIN", 7, AP_AdvancedFailsafe, _terminate_pin, -1),
92
93
// @Param: AMSL_LIMIT
94
// @DisplayName: AMSL limit
95
// @Description: This sets the AMSL (above mean sea level) altitude limit. If the pressure altitude determined by QNH exceeds this limit then flight termination will be forced. Note that this limit is in meters, whereas pressure altitude limits are often quoted in feet. A value of zero disables the pressure altitude limit.
96
// @User: Advanced
97
// @Units: m
98
AP_GROUPINFO("AMSL_LIMIT", 8, AP_AdvancedFailsafe, _amsl_limit, 0),
99
100
// @Param: AMSL_ERR_GPS
101
// @DisplayName: Error margin for GPS based AMSL limit
102
// @Description: This sets margin for error in GPS derived altitude limit. This error margin is only used if the barometer has failed. If the barometer fails then the GPS will be used to enforce the AMSL_LIMIT, but this margin will be subtracted from the AMSL_LIMIT first, to ensure that even with the given amount of GPS altitude error the pressure altitude is not breached. OBC users should set this to comply with their D2 safety case. A value of -1 will mean that barometer failure will lead to immediate termination.
103
// @User: Advanced
104
// @Units: m
105
AP_GROUPINFO("AMSL_ERR_GPS", 9, AP_AdvancedFailsafe, _amsl_margin_gps, -1),
106
107
// @Param: QNH_PRESSURE
108
// @DisplayName: QNH pressure
109
// @Description: This sets the QNH pressure in millibars to be used for pressure altitude in the altitude limit. A value of zero disables the altitude limit.
110
// @Units: hPa
111
// @User: Advanced
112
AP_GROUPINFO("QNH_PRESSURE", 10, AP_AdvancedFailsafe, _qnh_pressure, 0),
113
114
// *NOTE* index 11 is "Enable" and is moved to the top to allow AP_PARAM_FLAG_ENABLE
115
116
// *NOTE* index 12 of AP_Int16 RC_FAIL_MS was depreciated. Replaced by RC_FAIL_TIME.
117
118
// @Param: MAX_GPS_LOSS
119
// @DisplayName: Maximum number of GPS loss events
120
// @Description: Maximum number of GPS loss events before the aircraft stops returning to mission on GPS recovery. Use zero to allow for any number of GPS loss events.
121
// @User: Advanced
122
AP_GROUPINFO("MAX_GPS_LOSS", 13, AP_AdvancedFailsafe, _max_gps_loss, 0),
123
124
// @Param: MAX_COM_LOSS
125
// @DisplayName: Maximum number of comms loss events
126
// @Description: Maximum number of comms loss events before the aircraft stops returning to mission on comms recovery. Use zero to allow for any number of comms loss events.
127
// @User: Advanced
128
AP_GROUPINFO("MAX_COM_LOSS", 14, AP_AdvancedFailsafe, _max_comms_loss, 0),
129
130
// @Param: GEOFENCE
131
// @DisplayName: Enable geofence Advanced Failsafe
132
// @Description: This enables the geofence part of the AFS. Will only be in effect if AFS_ENABLE is also 1
133
// @User: Advanced
134
AP_GROUPINFO("GEOFENCE", 15, AP_AdvancedFailsafe, _enable_geofence_fs, 1),
135
136
// @Param: RC
137
// @DisplayName: Enable RC Advanced Failsafe
138
// @Description: This enables the RC part of the AFS. Will only be in effect if AFS_ENABLE is also 1
139
// @User: Advanced
140
AP_GROUPINFO("RC", 16, AP_AdvancedFailsafe, _enable_RC_fs, 1),
141
142
// @Param: RC_MAN_ONLY
143
// @DisplayName: Enable RC Termination only in manual control modes
144
// @Description: If this parameter is set to 1, then an RC loss will only cause the plane to terminate in manual control modes. If it is 0, then the plane will terminate in any flight mode.
145
// @User: Advanced
146
AP_GROUPINFO("RC_MAN_ONLY", 17, AP_AdvancedFailsafe, _rc_term_manual_only, 1),
147
148
// @Param: DUAL_LOSS
149
// @DisplayName: Enable dual loss terminate due to failure of both GCS and GPS simultaneously
150
// @Description: This enables the dual loss termination part of the AFS system. If this parameter is 1 and both GPS and the ground control station fail simultaneously, this will be considered a "dual loss" and cause termination.
151
// @User: Advanced
152
AP_GROUPINFO("DUAL_LOSS", 18, AP_AdvancedFailsafe, _enable_dual_loss, 1),
153
154
// @Param: RC_FAIL_TIME
155
// @DisplayName: RC failure time
156
// @Description: This is the time in seconds in manual mode that failsafe termination will activate if RC input is lost. For the OBC rules this should be (1.5). Use 0 to disable.
157
// @User: Advanced
158
// @Units: s
159
AP_GROUPINFO("RC_FAIL_TIME", 19, AP_AdvancedFailsafe, _rc_fail_time_seconds, 0),
160
161
// @Param: MAX_RANGE
162
// @DisplayName: Max allowed range
163
// @Description: This is the maximum range of the vehicle in kilometers from first arming. If the vehicle goes beyond this range then the TERM_ACTION is performed. A value of zero disables this feature.
164
// @User: Advanced
165
// @Units: km
166
AP_GROUPINFO("MAX_RANGE", 20, AP_AdvancedFailsafe, _max_range_km, 0),
167
168
// @Param: OPTIONS
169
// @DisplayName: AFS options
170
// @Description: See description for each bitmask bit description
171
// @Bitmask: 0: Continue the mission even after comms are recovered (does not go to the mission item at the time comms were lost)
172
// @Bitmask: 1: Enable AFS for all autonomous modes (not just AUTO)
173
// @Bitmask: 2: Option to not jump to AFS_WP_COMMS if already in the return path
174
AP_GROUPINFO("OPTIONS", 21, AP_AdvancedFailsafe, options, 0),
175
176
// @Param: GCS_TIMEOUT
177
// @DisplayName: GCS timeout
178
// @Description: The time (in seconds) of persistent data link loss before GCS failsafe occurs.
179
// @User: Advanced
180
// @Units: s
181
AP_GROUPINFO("GCS_TIMEOUT", 22, AP_AdvancedFailsafe, _gcs_fail_time_seconds, 10),
182
183
AP_GROUPEND
184
};
185
186
// check for Failsafe conditions. This is called at 10Hz by the main
187
// ArduPlane code
188
void
189
AP_AdvancedFailsafe::check(uint32_t last_valid_rc_ms)
190
{
191
if (!_enable) {
192
return;
193
}
194
195
#if AP_FENCE_ENABLED
196
// we always check for fence breach
197
if(_enable_geofence_fs) {
198
const AC_Fence *ap_fence = AP::fence();
199
if ((ap_fence != nullptr && ap_fence->get_breaches() != 0) || check_altlimit()) {
200
if (!_terminate) {
201
GCS_SEND_TEXT(MAV_SEVERITY_CRITICAL, "Terminating due to fence breach");
202
_terminate.set_and_notify(1);
203
}
204
}
205
}
206
#endif
207
208
// update max range check
209
max_range_update();
210
211
enum control_mode mode = afs_mode();
212
213
// check if RC termination is enabled
214
// check for RC failure in manual mode or RC failure when AFS_RC_MANUAL is 0
215
if (_state != STATE_PREFLIGHT && !_terminate && _enable_RC_fs &&
216
(mode == AFS_MANUAL || mode == AFS_STABILIZED || !_rc_term_manual_only) &&
217
_rc_fail_time_seconds > 0 &&
218
(AP_HAL::millis() - last_valid_rc_ms) > (_rc_fail_time_seconds * 1000.0f)) {
219
GCS_SEND_TEXT(MAV_SEVERITY_CRITICAL, "Terminating due to RC failure");
220
_terminate.set_and_notify(1);
221
}
222
223
// tell the failsafe board if we are in manual control
224
// mode. This tells it to pass through controls from the
225
// receiver
226
if (_manual_pin != -1) {
227
hal.gpio->pinMode(_manual_pin, HAL_GPIO_OUTPUT);
228
hal.gpio->write(_manual_pin, mode==AFS_MANUAL);
229
}
230
231
const uint32_t last_heartbeat_ms = gcs().sysid_mygcs_last_seen_time_ms();
232
uint32_t now = AP_HAL::millis();
233
bool gcs_link_ok = ((now - last_heartbeat_ms) < (_gcs_fail_time_seconds*1000.0f));
234
bool gps_lock_ok = ((now - AP::gps().last_fix_time_ms()) < 3000);
235
236
AP_Mission *_mission = AP::mission();
237
if (_mission == nullptr) {
238
return;
239
}
240
AP_Mission &mission = *_mission;
241
242
switch (_state) {
243
case STATE_PREFLIGHT:
244
// we startup in preflight mode. This mode ends when
245
// we first enter auto.
246
if (mode == AFS_AUTO) {
247
GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "AFS State: AFS_AUTO");
248
_state = STATE_AUTO;
249
}
250
break;
251
252
case STATE_AUTO:
253
// this is the normal mode.
254
if (!gcs_link_ok) {
255
GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "AFS State: DATA_LINK_LOSS");
256
_state = STATE_DATA_LINK_LOSS;
257
258
if (should_use_comms_hold()) {
259
_saved_wp = mission.get_current_nav_cmd().index;
260
mission.set_current_cmd(_wp_comms_hold);
261
262
if (mode == AFS_AUTO && option_is_set(Option::GCS_FS_ALL_AUTONOMOUS_MODES)) {
263
set_mode_auto();
264
}
265
}
266
267
// if two events happen within 30s we consider it to be part of the same event
268
if (now - _last_comms_loss_ms > 30*1000UL) {
269
_comms_loss_count++;
270
_last_comms_loss_ms = now;
271
}
272
break;
273
}
274
if (!gps_lock_ok) {
275
GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "AFS State: GPS_LOSS");
276
_state = STATE_GPS_LOSS;
277
if (_wp_gps_loss) {
278
_saved_wp = mission.get_current_nav_cmd().index;
279
mission.set_current_cmd(_wp_gps_loss);
280
}
281
// if two events happen within 30s we consider it to be part of the same event
282
if (now - _last_gps_loss_ms > 30*1000UL) {
283
_gps_loss_count++;
284
_last_gps_loss_ms = now;
285
}
286
break;
287
}
288
break;
289
290
case STATE_DATA_LINK_LOSS:
291
if (!gps_lock_ok) {
292
// losing GPS lock when data link is lost
293
// leads to termination if AFS_DUAL_LOSS is 1
294
if(_enable_dual_loss) {
295
if (!_terminate) {
296
GCS_SEND_TEXT(MAV_SEVERITY_CRITICAL, "Terminating due to dual loss");
297
_terminate.set_and_notify(1);
298
}
299
}
300
} else if (gcs_link_ok) {
301
_state = STATE_AUTO;
302
GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "AFS State: AFS_AUTO, GCS now OK");
303
304
if (option_is_set(Option::CONTINUE_AFTER_RECOVERED)) {
305
break;
306
}
307
308
// we only return to the mission if we have not exceeded AFS_MAX_COM_LOSS
309
if (_saved_wp != 0 &&
310
(_max_comms_loss <= 0 ||
311
_comms_loss_count <= _max_comms_loss)) {
312
mission.set_current_cmd(_saved_wp);
313
_saved_wp = 0;
314
}
315
}
316
break;
317
318
case STATE_GPS_LOSS:
319
if (!gcs_link_ok) {
320
// losing GCS link when GPS lock lost
321
// leads to termination if AFS_DUAL_LOSS is 1
322
if (!_terminate && _enable_dual_loss) {
323
GCS_SEND_TEXT(MAV_SEVERITY_CRITICAL, "Terminating due to dual loss");
324
_terminate.set_and_notify(1);
325
}
326
} else if (gps_lock_ok) {
327
GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "AFS State: AFS_AUTO, GPS now OK");
328
_state = STATE_AUTO;
329
// we only return to the mission if we have not exceeded AFS_MAX_GPS_LOSS
330
if (_saved_wp != 0 &&
331
(_max_gps_loss <= 0 || _gps_loss_count <= _max_gps_loss)) {
332
mission.set_current_cmd(_saved_wp);
333
_saved_wp = 0;
334
}
335
}
336
break;
337
}
338
339
// if we are not terminating or if there is a separate terminate
340
// pin configured then toggle the heartbeat pin at 10Hz
341
heartbeat();
342
343
// set the terminate pin
344
if (_terminate_pin != -1) {
345
hal.gpio->pinMode(_terminate_pin, HAL_GPIO_OUTPUT);
346
hal.gpio->write(_terminate_pin, _terminate?1:0);
347
}
348
}
349
350
351
// send heartbeat messages during sensor calibration
352
void
353
AP_AdvancedFailsafe::heartbeat(void)
354
{
355
if (!_enable) {
356
return;
357
}
358
359
// if we are not terminating or if there is a separate terminate
360
// pin configured then toggle the heartbeat pin at 10Hz
361
if (_heartbeat_pin != -1 && (_terminate_pin != -1 || !_terminate)) {
362
_heartbeat_pin_value = !_heartbeat_pin_value;
363
hal.gpio->pinMode(_heartbeat_pin, HAL_GPIO_OUTPUT);
364
hal.gpio->write(_heartbeat_pin, _heartbeat_pin_value);
365
}
366
}
367
368
bool
369
AP_AdvancedFailsafe::gps_altitude_ok() const
370
{
371
if (_amsl_margin_gps == -1) {
372
return false;
373
}
374
const AP_GPS &gps = AP::gps();
375
if (gps.status() < AP_GPS::GPS_OK_FIX_3D) {
376
return false;
377
}
378
const auto &location = gps.location();
379
float alt;
380
if (!location.get_alt_m(Location::AltFrame::ABSOLUTE, alt)) {
381
return false;
382
}
383
if (alt > _amsl_limit - _amsl_margin_gps) {
384
return false;
385
}
386
return true;
387
}
388
389
// check for altitude limit breach
390
bool
391
AP_AdvancedFailsafe::check_altlimit(void)
392
{
393
if (!_enable) {
394
return false;
395
}
396
if (_amsl_limit == 0 || _qnh_pressure <= 0) {
397
// no limit set
398
return false;
399
}
400
401
// see if the barometer is dead
402
const AP_Baro &baro = AP::baro();
403
if (AP_HAL::millis() - baro.get_last_update() > 5000) {
404
// the barometer has been unresponsive for 5 seconds. See if we can switch to GPS
405
if (gps_altitude_ok()) {
406
// GPS based altitude OK
407
return false;
408
}
409
// no barometer - immediate termination
410
return true;
411
}
412
413
float alt_amsl = baro.get_altitude_difference(_qnh_pressure*100, baro.get_pressure());
414
if (alt_amsl > _amsl_limit) {
415
// pressure altitude breach
416
return true;
417
}
418
419
// all OK
420
return false;
421
}
422
423
/*
424
return true if we should jump to _wp_comms_hold
425
*/
426
bool AP_AdvancedFailsafe::should_use_comms_hold(void){
427
428
AP_Mission *_mission = AP::mission();
429
430
if (_mission == nullptr) {
431
return false;
432
}
433
434
if (_wp_comms_hold <= 0) {
435
return false;
436
}
437
438
if ((_mission->state() == AP_Mission::MISSION_RUNNING) && _mission->get_in_return_path_flag() && option_is_set(Option::CONTINUE_IF_ALREADY_IN_RETURN_PATH)) {
439
return false;
440
}
441
442
return true;
443
}
444
/*
445
return true if we should crash the vehicle
446
*/
447
bool AP_AdvancedFailsafe::should_crash_vehicle(void)
448
{
449
if (!_enable) {
450
return false;
451
}
452
// ensure failsafe values are setup for if FMU crashes on PX4/Pixhawk
453
if (!_failsafe_setup) {
454
_failsafe_setup = true;
455
setup_IO_failsafe();
456
}
457
458
// determine if the vehicle should be crashed
459
// only possible if FS_TERM_ACTION is 42 and _terminate is non zero
460
// _terminate may be set via parameters, or a mavlink command
461
if (_terminate &&
462
(_terminate_action == TERMINATE_ACTION_TERMINATE ||
463
_terminate_action == TERMINATE_ACTION_LAND)) {
464
// we are terminating
465
return true;
466
}
467
468
// continue flying
469
return false;
470
}
471
472
// update GCS based termination
473
// returns true if AFS is in the desired termination state
474
bool AP_AdvancedFailsafe::gcs_terminate(bool should_terminate, const char *reason) {
475
if (!_enable) {
476
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "AFS not enabled, can't terminate the vehicle");
477
return false;
478
}
479
480
_terminate.set_and_notify(should_terminate ? 1 : 0);
481
482
// evaluate if we will crash or not, as AFS must be enabled, and TERM_ACTION has to be correct
483
bool is_terminating = should_crash_vehicle();
484
485
if(should_terminate == is_terminating) {
486
if (is_terminating) {
487
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Terminating due to %s", reason);
488
} else {
489
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Aborting termination due to %s", reason);
490
}
491
return true;
492
} else if (should_terminate && _terminate_action != TERMINATE_ACTION_TERMINATE) {
493
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Unable to terminate, termination is not configured");
494
}
495
return false;
496
}
497
498
/*
499
update check of maximum range
500
*/
501
void AP_AdvancedFailsafe::max_range_update(void)
502
{
503
if (_max_range_km <= 0) {
504
return;
505
}
506
507
if (!_have_first_location) {
508
if (AP::gps().status() < AP_GPS::GPS_OK_FIX_3D) {
509
// wait for 3D fix
510
return;
511
}
512
if (!hal.util->get_soft_armed()) {
513
// wait for arming
514
return;
515
}
516
_first_location = AP::gps().location();
517
_have_first_location = true;
518
}
519
520
if (AP::gps().status() < AP_GPS::GPS_OK_FIX_2D) {
521
// don't trigger when dead-reckoning
522
return;
523
}
524
525
// check distance from first location
526
float distance_km = _first_location.get_distance(AP::gps().location()) * 0.001;
527
if (distance_km > _max_range_km) {
528
uint32_t now = AP_HAL::millis();
529
if (now - _term_range_notice_ms > 5000) {
530
GCS_SEND_TEXT(MAV_SEVERITY_CRITICAL, "Terminating due to range %.1fkm", distance_km);
531
_term_range_notice_ms = now;
532
}
533
_terminate.set_and_notify(1);
534
}
535
}
536
537
namespace AP {
538
539
AP_AdvancedFailsafe *advancedfailsafe()
540
{
541
return AP_AdvancedFailsafe::get_singleton();
542
}
543
544
};
545
546
#endif // AP_ADVANCEDFAILSAFE_ENABLED
547
548