def test_current_point_is_not_reached_if_not_close_with_overstep_360(
         self, zero_point):
     assert not is_current_point_reached(
         pan_angle=300,
         tilt_angle=354,
         current_target=zero_point,
         next_target=PointOfMotion(pan_angle=100, tilt_angle=20, time=2))
 def test_current_point_is_reached_if_close(self):
     c = PointOfMotion(pan_angle=180,
                       pan_clockwise=True,
                       tilt_angle=30,
                       tilt_clockwise=True)
     n = PointOfMotion(pan_angle=270,
                       pan_clockwise=True,
                       tilt_angle=15,
                       tilt_clockwise=False)
     assert is_current_point_reached(pan_angle=179.96,
                                     tilt_angle=30.04,
                                     current_target=c,
                                     next_target=n)
     assert is_current_point_reached(pan_angle=179.96,
                                     tilt_angle=30.04,
                                     current_target=c,
                                     next_target=None)
 def test_current_point_is_not_reached_if_only_one_is_close(self):
     c = PointOfMotion(pan_angle=180,
                       pan_clockwise=True,
                       tilt_angle=30,
                       tilt_clockwise=True)
     n = PointOfMotion(pan_angle=270,
                       pan_clockwise=True,
                       tilt_angle=15,
                       tilt_clockwise=False)
     # tilt angle is reached before pan angle is reached
     assert not is_current_point_reached(pan_angle=179.91,
                                         tilt_angle=30.04,
                                         current_target=c,
                                         next_target=n)
     # pan angle is reached before tilt angle is reached
     assert not is_current_point_reached(pan_angle=179.96,
                                         tilt_angle=29.91,
                                         current_target=c,
                                         next_target=n)
 def test_current_point_is_not_reached_if_not_close_with_counter_clockwise_overstep_360(
         self, zero_point):
     assert not is_current_point_reached(pan_angle=300,
                                         tilt_angle=354,
                                         current_target=PointOfMotion(
                                             pan_angle=359.99,
                                             pan_clockwise=False,
                                             tilt_angle=359.99,
                                             tilt_clockwise=False),
                                         next_target=zero_point)
 def test_calculate_respects_max_speed(self, calculator, max_pan_speed,
                                       max_tilt_speed):
     target_speeds = calculator.calculate(state=CameraState(speeds=Mock(),
                                                            pan_angle=0,
                                                            tilt_angle=0),
                                          target=PointOfMotion(
                                              pan_angle=180,
                                              tilt_angle=90,
                                              time=1))
     assert max_pan_speed == target_speeds.pan_speed
     assert max_tilt_speed == target_speeds.tilt_speed
 def test_calculate(self, calculator):
     target_speeds = calculator.calculate(state=CameraState(speeds=Mock(),
                                                            pan_angle=0,
                                                            tilt_angle=0),
                                          target=PointOfMotion(
                                              pan_angle=180,
                                              tilt_angle=90,
                                              time=9))
     # gimbal has to pan 20°/s to get in 9s from 0° to 180°
     assert 20 == target_speeds.pan_speed
     # gimbal has to tilt 10°/s to get in 9s from 0° to 90°
     assert 10 == target_speeds.tilt_speed
 def test_calculate_overstep_360(self, calculator):
     target_speeds = calculator.calculate(state=CameraState(speeds=Mock(),
                                                            pan_angle=280,
                                                            tilt_angle=320),
                                          target=PointOfMotion(
                                              pan_angle=10,
                                              tilt_angle=20,
                                              time=6))
     # gimbal has to pan 15°/s counter-clockwise to get
     # in 6s from 280° to 10° (90° difference)
     assert 15 == target_speeds.pan_speed
     # gimbal has to tilt 10°/s counter-clockwise to get
     # in 6s from 320° to 20° (60° difference)
     assert 10 == target_speeds.tilt_speed
    def test_current_point_is_reached_if_one_is_close_after_the_other(self):
        c = PointOfMotion(pan_angle=180,
                          pan_clockwise=True,
                          tilt_angle=30,
                          tilt_clockwise=True)
        n = PointOfMotion(pan_angle=270,
                          pan_clockwise=True,
                          tilt_angle=45,
                          tilt_clockwise=True)
        # If the next point is in the same direction as the current point,
        # there is no stop at the intermediate point.

        # Tilt angle has been reached before pan angle is reached,
        # but tilting did not stop and tilt angle is not close anymore.
        assert is_current_point_reached(pan_angle=179.96,
                                        tilt_angle=30.14,
                                        current_target=c,
                                        next_target=n)
        # Pan angle has been reached before tilt angle is reached,
        # but panning did not stop and pan angle is not close anymore.
        assert is_current_point_reached(pan_angle=180.14,
                                        tilt_angle=29.96,
                                        current_target=c,
                                        next_target=n)
 def zero_point(self):
     return PointOfMotion(pan_angle=0, tilt_angle=0)
    def test_no_stop_at_intermediate_point_if_next_point_is_in_same_direction(
            self, controller, camera_speeds, gimbal, get_zero_angles,
            zero_point, rotate_speed_manager, tilt_speed_manager,
            target_speed_calculator, max_speeds, max_pan_speed,
            max_tilt_speed):
        gimbal.control = Mock()
        target_speed_calculator.calculate = Mock(return_value=max_speeds)
        rotate_speed_manager.acceleration_per_second = max_pan_speed / 2
        tilt_speed_manager.acceleration_per_second = max_tilt_speed / 2

        first_point = zero_point
        second_point = PointOfMotion(pan_angle=100, tilt_angle=20, time=2)
        controller.add_point(first_point)
        controller.add_point(second_point)
        controller.start()

        gimbal.get_angles = Mock(return_value=get_angles_in_cmd(
            pan_angle=300, pan_speed=0, tilt_angle=354, tilt_speed=0))
        controller.update(camera_speeds)
        # Tell gimbal to move with the speed of the first point to the second
        # point.
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=max_pan_speed / 2,
            yaw_angle=second_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=max_tilt_speed / 2,
            pitch_angle=second_point.tilt_angle)
        gimbal.control.reset_mock()

        assert rotate_speed_manager.current_speed < rotate_speed_manager.target_speed
        assert tilt_speed_manager.current_speed < tilt_speed_manager.target_speed
        # increase speed
        controller.update(camera_speeds)
        # Tell gimbal to move with the speed of the first point to the second
        # point.
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=max_pan_speed,
            yaw_angle=second_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=max_tilt_speed,
            pitch_angle=second_point.tilt_angle)
        gimbal.control.reset_mock()

        # First point is reached
        gimbal.get_angles = get_zero_angles
        # Continue moving to the second point, but with the speed of the second
        # point, i.e. speed has to be decreased from max speed to:
        # - pan 50°/s to get in 2s from 0° to 100°
        # - tilt 10°/s to get in 2s from 0° to 20°
        target_speed_calculator.calculate = Mock(
            return_value=CameraSpeeds(pan_speed=50, tilt_speed=10))
        controller.update(camera_speeds)
        target_speed_calculator.calculate.assert_called_once_with(
            CameraState(speeds=camera_speeds,
                        pan_angle=first_point.pan_angle,
                        tilt_angle=first_point.tilt_angle), second_point)
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=50,
            yaw_angle=second_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=10,
            pitch_angle=second_point.tilt_angle)
    def test_target_speeds_are_calculated_before_current_speeds_are_updated(
            self, controller, camera_speeds, gimbal, get_zero_angles,
            rotate_speed_manager, tilt_speed_manager, target_speed_calculator,
            max_speeds):
        rotate_speed_manager.acceleration_per_second = 2
        tilt_speed_manager.acceleration_per_second = 1

        first_point = PointOfMotion(pan_angle=0,
                                    pan_clockwise=False,
                                    tilt_angle=0,
                                    tilt_clockwise=False)
        last_point = PointOfMotion(pan_angle=21, tilt_angle=6, time=3)

        controller.add_point(first_point)
        controller.add_point(last_point)
        controller.start()

        # gimbal is not at first point yet
        gimbal.get_angles = Mock(return_value=get_angles_in_cmd(
            pan_angle=180, pan_speed=60, tilt_angle=90, tilt_speed=12))
        camera_speeds.pan_speed = 60
        camera_speeds.tilt_speed = 12
        camera_state = CameraState(speeds=camera_speeds,
                                   pan_angle=180,
                                   tilt_angle=90)
        # gimbal should move with maximum speed to first point
        target_speed_calculator.calculate = Mock(return_value=max_speeds)
        # Assert that target speeds are set on speed manager before updating
        # current speeds. Save update-method, replace it with assertion that
        # calls saved update-method.
        update_rotate_speed_manager = rotate_speed_manager.update
        update_tilt_speed_manager = tilt_speed_manager.update

        def assert_update_rotate_speed_manager():
            assert rotate_speed_manager.target_speed == max_speeds.pan_speed, \
                'target pan speed has to be set before updating current speed'
            return update_rotate_speed_manager()

        def assert_update_tilt_speed_manager():
            assert tilt_speed_manager.target_speed == max_speeds.tilt_speed, \
                'target tilt speed has to be set before updating current speed'
            return update_tilt_speed_manager()

        rotate_speed_manager.update = Mock(
            side_effect=assert_update_rotate_speed_manager)
        tilt_speed_manager.update = Mock(
            side_effect=assert_update_tilt_speed_manager)
        # TODO pass camera state to update (do not get angles in controller)
        controller.update(camera_speeds)
        # calculator has been used to calculate target speeds
        target_speed_calculator.calculate.assert_called_once_with(
            camera_state, first_point)
        # and target speeds have been set on speed managers
        assert rotate_speed_manager.target_speed == max_speeds.pan_speed
        assert tilt_speed_manager.target_speed == max_speeds.tilt_speed
        # and then current speeds have been updated
        rotate_speed_manager.update.assert_called_once()
        tilt_speed_manager.update.assert_called_once()
        # and used to control the gimbal
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=2,
            yaw_angle=first_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=1,
            pitch_angle=first_point.tilt_angle)
    def test_move_to_next_point_when_current_point_is_reached(
            self, controller, camera_speeds, gimbal, get_zero_angles,
            rotate_speed_manager, tilt_speed_manager, target_speed_calculator,
            max_speeds):
        gimbal.control = Mock()
        rotate_speed_manager.acceleration_per_second = 4
        rotate_speed_manager.target_speed = 60
        rotate_speed_manager.current_speed = rotate_speed_manager.target_speed
        tilt_speed_manager.acceleration_per_second = 1
        tilt_speed_manager.target_speed = 12
        tilt_speed_manager.current_speed = tilt_speed_manager.target_speed
        current_point = PointOfMotion(pan_angle=0,
                                      pan_clockwise=False,
                                      tilt_angle=0,
                                      tilt_clockwise=False)
        target_point = PointOfMotion(pan_angle=21, tilt_angle=6, time=3)

        controller.add_point(current_point)
        controller.add_point(target_point)
        controller.start()

        # gimbal is already moving
        gimbal.get_angles = Mock(return_value=get_angles_in_cmd(
            pan_angle=1, pan_speed=60, tilt_angle=1, tilt_speed=12))
        target_speed_calculator.calculate = Mock(return_value=max_speeds)
        # and should move to first point
        controller.update(camera_speeds)
        gimbal.control.reset_mock()

        # Gimbal still moves with target speed according to the speed manager
        assert rotate_speed_manager.current_speed == 60
        assert tilt_speed_manager.current_speed == 12
        # Old target speeds used to move to first point
        assert rotate_speed_manager.target_speed == 60
        assert tilt_speed_manager.target_speed == 12
        # First point is reached
        gimbal.get_angles = get_zero_angles
        camera_state = CameraState(speeds=camera_speeds,
                                   pan_angle=current_point.pan_angle,
                                   tilt_angle=current_point.tilt_angle)
        # New target speeds are calculated based on next point of motion.
        # Gimbal has to pan 7°/s to get in 3s from 0° to 21°.
        # Gimbal has to tilt 2°/s to get in 3s from 0° to 6°.
        target_speeds = CameraSpeeds(pan_speed=7, tilt_speed=2)
        target_speed_calculator.calculate = Mock(return_value=target_speeds)
        # TODO assert target speeds have been set before calculate is called

        controller.update(camera_speeds)
        assert controller.current_point() == target_point
        assert gimbal.get_angles.call_count == 1
        # calculator has been used to calculate target speeds
        target_speed_calculator.calculate.assert_called_once_with(
            camera_state, target_point)
        # and target speeds have been set on speed managers
        assert rotate_speed_manager.target_speed == target_speeds.pan_speed
        assert tilt_speed_manager.target_speed == target_speeds.tilt_speed
        # Gimbal stopped by itself, when it reached the last point.
        # The speed managers don't know that. Hence, the current speed
        # has to be set to zero and then updated.
        assert rotate_speed_manager.current_speed == 4
        assert tilt_speed_manager.current_speed == 1
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=rotate_speed_manager.current_speed,
            yaw_angle=target_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=tilt_speed_manager.current_speed,
            pitch_angle=target_point.tilt_angle)
    def test_move_with_increasing_speed_to_point(self, controller,
                                                 camera_speeds, gimbal,
                                                 get_zero_angles, zero_point,
                                                 rotate_speed_manager,
                                                 tilt_speed_manager,
                                                 target_speed_calculator):
        gimbal.get_angles = get_zero_angles
        gimbal.control = Mock()
        rotate_speed_manager.acceleration_per_second = 4
        tilt_speed_manager.acceleration_per_second = 1
        current_point = PointOfMotion(pan_angle=21, tilt_angle=6, time=3)
        next_point = PointOfMotion(pan_angle=0,
                                   pan_clockwise=False,
                                   tilt_angle=0,
                                   tilt_clockwise=False,
                                   time=3)

        controller.add_point(current_point)
        controller.add_point(next_point)

        controller.start()
        assert gimbal.get_angles.call_count == 0
        assert gimbal.control.call_count == 0

        # Gimbal is not moving
        assert rotate_speed_manager.current_speed == 0
        assert tilt_speed_manager.current_speed == 0
        # assert rotate_speed_manager.target_speed == 0
        # assert tilt_speed_manager.target_speed == 0
        # First point is not reached yet
        gimbal.get_angles = get_zero_angles
        camera_state = CameraState(speeds=camera_speeds,
                                   pan_angle=zero_point.pan_angle,
                                   tilt_angle=zero_point.tilt_angle)
        # New target speeds are calculated based on next point of motion.
        # Gimbal has to pan 7°/s to get in 3s from 21° to 0°.
        # Gimbal has to tilt 2°/s to get in 3s from 6° to 0°.
        target_speeds = CameraSpeeds(pan_speed=7, tilt_speed=2)
        target_speed_calculator.calculate = Mock(return_value=target_speeds)
        # TODO assert target speeds have been set before calculate is called

        # should move with speeds of speed managers to first point
        controller.update(camera_speeds)
        # calculator has been used to calculate target speeds
        target_speed_calculator.calculate.assert_called_once_with(
            camera_state, current_point)
        # and target speeds have been set on speed managers
        assert rotate_speed_manager.target_speed == target_speeds.pan_speed
        assert tilt_speed_manager.target_speed == target_speeds.tilt_speed
        assert rotate_speed_manager.current_speed == 4
        assert tilt_speed_manager.current_speed == 1
        assert gimbal.get_angles.call_count == 1
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=rotate_speed_manager.current_speed,
            yaw_angle=current_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=tilt_speed_manager.current_speed,
            pitch_angle=current_point.tilt_angle)
        assert not controller.is_target_speed_reached()

        gimbal.get_angles = Mock(return_value=get_angles_in_cmd(
            pan_angle=7, pan_speed=4, tilt_angle=2, tilt_speed=1))
        gimbal.control.reset_mock()
        # should increase speeds till target speeds are reached
        controller.update(camera_speeds)
        assert gimbal.get_angles.call_count == 1
        # Should not have been called again, since target didn't change.
        assert target_speed_calculator.calculate.call_count == 1
        # Target speeds are reached
        assert rotate_speed_manager.current_speed == 7
        assert tilt_speed_manager.current_speed == 2
        gimbal.control.assert_called_once_with(
            yaw_mode=ControlMode.angle,
            yaw_speed=rotate_speed_manager.current_speed,
            yaw_angle=current_point.pan_angle,
            pitch_mode=ControlMode.angle,
            pitch_speed=tilt_speed_manager.current_speed,
            pitch_angle=current_point.tilt_angle)
        assert controller.is_target_speed_reached()

        # reset mocks, since their call counts are checked later
        gimbal.control.reset_mock()
        rotate_speed_manager.update.reset_mock()
        rotate_speed_manager.reset.reset_mock()
        tilt_speed_manager.update.reset_mock()
        tilt_speed_manager.reset.reset_mock()

        gimbal.get_angles = Mock(return_value=get_angles_in_cmd(
            pan_angle=14, pan_speed=60, tilt_angle=4, tilt_speed=12))
        # target speeds are reached, but target is not reached yet
        controller.update(camera_speeds)
        # call to get_angles required to check if target is reached
        assert gimbal.get_angles.call_count == 1
        # Should not have been called again, since target didn't change.
        assert target_speed_calculator.calculate.call_count == 1
        # Target speed already reached => no control command needs to be sent
        assert gimbal.control.call_count == 0
        # but time of speed managers has to be updated
        assert (rotate_speed_manager.update.call_count == 1
                or rotate_speed_manager.reset.call_count == 1)
        assert (tilt_speed_manager.update.call_count == 1
                or tilt_speed_manager.reset.call_count == 1)
        assert controller.current_point() == current_point
 def non_zero_point(self):
     return PointOfMotion(pan_angle=180, tilt_angle=30)