test_commands.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import math
  2. import unittest
  3. from motian_bridge import Bridge
  4. from motian_bridge.dialects.v20 import VKFly
  5. from motian_bridge.telemetry import Telemetry
  6. from test_bridge import FakeMqtt, FakeSerial, command, config
  7. def heartbeat(system=1, component=1):
  8. mav = VKFly.MAVLink(None, srcSystem=system, srcComponent=component)
  9. return mav.heartbeat_encode(
  10. VKFly.MAV_TYPE_QUADROTOR,
  11. VKFly.MAV_AUTOPILOT_GENERIC,
  12. 0,
  13. VKFly.VKFLY_CUSTOM_MODE_POSHOLD,
  14. VKFly.MAV_STATE_ACTIVE,
  15. ).pack(mav)
  16. def decode(frame):
  17. parser = VKFly.MAVLink(None)
  18. result = None
  19. for byte in frame:
  20. message = parser.parse_char(bytes((byte,)))
  21. if message is not None:
  22. result = message
  23. return result
  24. class CommandHandlerTests(unittest.TestCase):
  25. def setUp(self):
  26. self.cfg = config()
  27. self.mqtt = FakeMqtt()
  28. self.serial = FakeSerial()
  29. self.bridge = Bridge(self.cfg, self.mqtt, lambda *args, **kwargs: self.serial)
  30. self.bridge._serial = self.serial
  31. self.bridge._on_connect(self.mqtt, None, None, 0, None)
  32. self.bridge._telemetry.feed(heartbeat(42, 1), now=1)
  33. def deliver(self, data_type, data):
  34. self.bridge._on_message(self.mqtt, None, type("Message", (), {
  35. "topic": self.cfg.obtain_topic,
  36. "retain": False,
  37. "payload": command(data_type, data),
  38. })())
  39. def last(self):
  40. return decode(self.serial.written[-1])
  41. def test_get_all_params(self):
  42. self.deliver(281, None)
  43. message = self.last()
  44. self.assertEqual(message.get_type(), "PARAM_REQUEST_LIST")
  45. self.assertEqual((message.target_system, message.target_component), (42, 1))
  46. def test_direct_parameter_mappings(self):
  47. cases = [
  48. ("battery_low_threshold", 30, "VCAP1_LOW_VAL", VKFly.MAV_PARAM_TYPE_UINT8, 30),
  49. ("battery_critical_threshold", 20, "VCAP2_LOW_VAL", VKFly.MAV_PARAM_TYPE_UINT8, 20),
  50. ("height_limit", 120, "ALT_LIM_UP1", VKFly.MAV_PARAM_TYPE_UINT16, 120),
  51. ("distance_limit", 5000, "MAX_HOR_DIST", VKFly.MAV_PARAM_TYPE_UINT32, 5000),
  52. ("return_height", 80, "RTL_ALT_M", VKFly.MAV_PARAM_TYPE_REAL32, 80),
  53. ("return_speed", 8, "MC_XY_CRUISE", VKFly.MAV_PARAM_TYPE_REAL32, 8),
  54. ("obstacle_avoidance_distance", 5, "OBAVOID_DIST",
  55. VKFly.MAV_PARAM_TYPE_REAL32, 5),
  56. ("obstacle_avoidance_enable", 1, "OBAVOID_ACT",
  57. VKFly.MAV_PARAM_TYPE_UINT8, 1),
  58. ("obstacle_avoidance_switch", 0, "OBAVOID_ACT",
  59. VKFly.MAV_PARAM_TYPE_UINT8, 0),
  60. ]
  61. for name, value, expected_id, expected_type, expected_value in cases:
  62. with self.subTest(name=name):
  63. self.serial.written.clear()
  64. self.deliver(282, {"name": name, "type": 1, "value": value})
  65. message = self.last()
  66. self.assertEqual(message.get_type(), "PARAM_SET")
  67. self.assertEqual(message.param_id, expected_id)
  68. self.assertEqual(message.param_type, expected_type)
  69. self.assertEqual(message.param_value, expected_value)
  70. def test_string_parameter_mappings(self):
  71. self.deliver(282, {"name": "return_type", "type": 2, "value": "MissionReverse"})
  72. self.assertEqual(self.last().param_id, "RTL_EXEC_MODE")
  73. self.assertEqual(self.last().param_value, 2)
  74. self.deliver(282, {"name": "precision_land_mode", "type": 2,
  75. "value": "AprilTag"})
  76. self.assertEqual(self.last().param_id, "RTL_PLD_EN")
  77. self.assertEqual(self.last().param_value, 1)
  78. def test_todo_and_third_threshold_do_not_write(self):
  79. for name in ("battery_emergency_threshold", "geofence_action", "rtk_switch",
  80. "visual_sensor_enable", "unknown"):
  81. with self.subTest(name=name):
  82. before = len(self.serial.written)
  83. self.deliver(282, {"name": name, "type": 1, "value": 1})
  84. self.assertEqual(len(self.serial.written), before)
  85. def test_low_battery_action_preserves_other_bits(self):
  86. self.bridge._telemetry.params["FS_CONF_A"] = float(0xAB00)
  87. self.deliver(282, {"name": "low_battery_action", "type": 2,
  88. "value": "ReturnHome"})
  89. message = self.last()
  90. self.assertEqual(message.param_id, "FS_CONF_A")
  91. self.assertEqual(message.param_type, VKFly.MAV_PARAM_TYPE_UINT32)
  92. self.assertEqual(message.param_value, 0xAB22)
  93. def test_low_battery_action_requires_parameter_download(self):
  94. before = len(self.serial.written)
  95. self.deliver(282, {"name": "low_battery_action", "type": 2,
  96. "value": "ReturnHome"})
  97. self.assertEqual(len(self.serial.written), before)
  98. def test_param_value_is_cached(self):
  99. mav = VKFly.MAVLink(None, srcSystem=42, srcComponent=1)
  100. frame = mav.param_value_encode(
  101. b"FS_CONF_A", 4660, VKFly.MAV_PARAM_TYPE_UINT32, 1, 0
  102. ).pack(mav)
  103. self.bridge._telemetry.feed(frame, now=2)
  104. self.assertEqual(self.bridge.mavlink_param("FS_CONF_A"), 4660)
  105. def test_2050_false_arms_and_true_is_todo(self):
  106. self.deliver(2050, False)
  107. message = self.last()
  108. self.assertEqual(message.get_type(), "COMMAND_LONG")
  109. self.assertEqual(message.command, VKFly.MAV_CMD_COMPONENT_ARM_DISARM)
  110. self.assertEqual(message.param1, 1)
  111. before = len(self.serial.written)
  112. self.deliver(2050, True)
  113. self.assertEqual(len(self.serial.written), before)
  114. def test_takeoff_and_land(self):
  115. self.deliver(2054, None)
  116. takeoff = self.last()
  117. self.assertEqual(takeoff.command, VKFly.MAV_CMD_NAV_TAKEOFF)
  118. self.assertTrue(math.isnan(takeoff.param7))
  119. self.deliver(2055, None)
  120. land = self.last()
  121. self.assertEqual(land.command, VKFly.MAV_CMD_NAV_LAND)
  122. def test_reposition_command_int(self):
  123. self.deliver(2070, {
  124. "latitude": 33.3180863,
  125. "longitude": 120.0978796,
  126. "altitude": 61.52,
  127. "speed": 5.0,
  128. "heading": 30,
  129. })
  130. message = self.last()
  131. self.assertEqual(message.get_type(), "COMMAND_INT")
  132. self.assertEqual(message.command, VKFly.MAV_CMD_DO_REPOSITION)
  133. self.assertEqual(message.frame, VKFly.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT)
  134. self.assertEqual(message.param2, VKFly.MAV_DO_REPOSITION_FLAGS_CHANGE_MODE)
  135. self.assertEqual(message.x, 333180863)
  136. self.assertEqual(message.y, 1200978796)
  137. self.assertAlmostEqual(message.z, 61.52, places=4)
  138. def test_commands_wait_for_heartbeat(self):
  139. bridge = Bridge(self.cfg, self.mqtt, lambda *args, **kwargs: self.serial)
  140. bridge._serial = self.serial
  141. bridge._on_connect(self.mqtt, None, None, 0, None)
  142. before = len(self.serial.written)
  143. bridge._on_message(self.mqtt, None, type("Message", (), {
  144. "topic": self.cfg.obtain_topic,
  145. "retain": False,
  146. "payload": command(2054, None),
  147. })())
  148. self.assertEqual(len(self.serial.written), before)
  149. class ParameterSyncTests(unittest.TestCase):
  150. def setUp(self):
  151. self.cfg = config(param_request_timeout=1.0, param_request_max_attempts=2)
  152. self.mqtt = FakeMqtt()
  153. self.serial = FakeSerial()
  154. self.bridge = Bridge(self.cfg, self.mqtt, lambda *args, **kwargs: self.serial)
  155. self.bridge._serial = self.serial
  156. self.bridge._on_connect(self.mqtt, None, None, 0, None)
  157. self.bridge._telemetry.feed(heartbeat(42, 1), now=0)
  158. def request_lists(self):
  159. messages = [decode(frame) for frame in self.serial.written]
  160. return [message for message in messages if message.get_type() == "PARAM_REQUEST_LIST"]
  161. def test_auto_request_once_targets_locked_controller(self):
  162. self.bridge._sync_parameters(now=0)
  163. frames = self.request_lists()
  164. self.assertEqual(len(frames), 1)
  165. self.assertEqual((frames[0].target_system, frames[0].target_component), (42, 1))
  166. self.bridge._sync_parameters(now=0.1)
  167. self.assertEqual(len(self.serial.written), 1)
  168. def test_disabled_config_never_requests(self):
  169. bridge = Bridge(config(request_params_on_start=False), self.mqtt,
  170. lambda *args, **kwargs: self.serial)
  171. bridge._serial = self.serial
  172. bridge._telemetry.feed(heartbeat(42, 1), now=0)
  173. bridge._sync_parameters(now=0)
  174. self.assertEqual(self.serial.written, [])
  175. def test_retry_until_max_attempts_then_give_up(self):
  176. self.bridge._sync_parameters(now=0)
  177. mav = VKFly.MAVLink(None, srcSystem=42, srcComponent=1)
  178. self.bridge._telemetry.feed(mav.param_value_encode(
  179. b"P", 1.0, VKFly.MAV_PARAM_TYPE_REAL32, 3, 0).pack(mav), now=0.1)
  180. self.bridge._sync_parameters(now=0.5) # timeout not reached yet
  181. self.assertEqual(len(self.serial.written), 1)
  182. self.bridge._sync_parameters(now=1.0) # second attempt
  183. self.assertEqual(len(self.serial.written), 2)
  184. self.bridge._sync_parameters(now=2.0) # attempts exhausted
  185. self.assertEqual(len(self.serial.written), 2)
  186. def test_complete_table_stops_retry(self):
  187. self.bridge._sync_parameters(now=0)
  188. mav = VKFly.MAVLink(None, srcSystem=42, srcComponent=1)
  189. self.bridge._telemetry.feed(mav.param_value_encode(
  190. b"P", 1.0, VKFly.MAV_PARAM_TYPE_REAL32, 1, 0).pack(mav), now=0.1)
  191. self.assertTrue(self.bridge._telemetry.aircraft.params.complete())
  192. self.bridge._sync_parameters(now=99.0)
  193. self.assertEqual(len(self.serial.written), 1)
  194. def test_new_aircraft_requests_again(self):
  195. self.bridge._sync_parameters(now=0)
  196. self.assertEqual(len(self.serial.written), 1)
  197. self.bridge._telemetry = Telemetry(self.cfg)
  198. self.bridge._telemetry.feed(heartbeat(7, 1), now=0)
  199. self.bridge._sync_parameters(now=0)
  200. self.assertEqual(len(self.serial.written), 2)
  201. self.assertEqual(decode(self.serial.written[-1]).target_system, 7)
  202. if __name__ == "__main__":
  203. unittest.main()