Exemple #1
0
    def __init__(self) -> None:
        self.mavlink_manager = MavlinkManager()
        available_mavlink_interfaces = self.mavlink_manager.available_interfaces(
        )
        if not available_mavlink_interfaces:
            raise RuntimeError("No MAVLink interface available.")

        # Create a folder for ArduPilotManager service
        self.settings_path = appdirs.user_config_dir(self.__class__.__name__)
        self.firmware_path = os.path.join(self.settings_path, "firmware")
        self.subprocess: Optional[Any] = None
        self.firmware_download = FirmwareDownload()

        # Create settings folder, ignore if already exists
        os.makedirs(self.firmware_path, exist_ok=True)
    def __init__(self) -> None:
        self.settings = Settings()
        self.settings.create_app_folders()
        self.mavlink_manager = MavlinkManager()
        self.mavlink_manager.set_logdir(self.settings.log_path)
        self._current_board: Optional[FlightController] = None
        self._current_sitl_frame: SITLFrame = SITLFrame.UNDEFINED

        # Load settings and do the initial configuration
        if self.settings.load():
            logger.info(f"Loaded settings from {self.settings.settings_file}.")
            logger.debug(self.settings.content)
        else:
            self.settings.create_settings_file()

        self.configuration = deepcopy(self.settings.content)
        self._load_endpoints()
        self.ardupilot_subprocess: Optional[Any] = None
        self.firmware_manager = FirmwareManager(self.settings.firmware_folder, self.settings.defaults_folder)
        self.vehicle_manager = VehicleManager()

        self.should_be_running = False
class ArduPilotManager(metaclass=Singleton):
    # pylint: disable=too-many-instance-attributes
    def __init__(self) -> None:
        self.settings = Settings()
        self.settings.create_app_folders()
        self.mavlink_manager = MavlinkManager()
        self.mavlink_manager.set_logdir(self.settings.log_path)
        self._current_platform: Platform = Platform.Undefined
        self._current_sitl_frame: SITLFrame = SITLFrame.UNDEFINED

        # Load settings and do the initial configuration
        if self.settings.load():
            logger.info(f"Loaded settings from {self.settings.settings_file}.")
            logger.debug(self.settings.content)
        else:
            self.settings.create_settings_file()

        self.configuration = deepcopy(self.settings.content)
        self._load_endpoints()
        self.ardupilot_subprocess: Optional[Any] = None
        self.firmware_manager = FirmwareManager(self.settings.firmware_folder,
                                                self.settings.defaults_folder)
        self.vehicle_manager = VehicleManager()

        self.should_be_running = False

    async def auto_restart_ardupilot(self) -> None:
        """Auto-restart Ardupilot when it's not running but was supposed to."""
        while True:
            process_not_running = (self.ardupilot_subprocess is not None
                                   and self.ardupilot_subprocess.poll()
                                   is not None) or len(
                                       self.running_ardupilot_processes()) == 0
            needs_restart = self.should_be_running and (
                (self.current_platform.type
                 in [PlatformType.SITL, PlatformType.Linux]
                 and process_not_running)
                or self.current_platform == Platform.Undefined)
            if needs_restart:
                logger.debug("Restarting ardupilot...")
                try:
                    await self.kill_ardupilot()
                except Exception as error:
                    logger.warning(f"Could not kill Ardupilot: {error}")
                try:
                    await self.start_ardupilot()
                except Exception as error:
                    logger.warning(f"Could not start Ardupilot: {error}")
                self.should_be_running = True
            await asyncio.sleep(5.0)

    async def start_mavlink_manager_watchdog(self) -> None:
        await self.mavlink_manager.auto_restart_router()

    def run_with_board(self) -> None:
        if not self.start_board(BoardDetector.detect()):
            logger.warning("Flight controller board not detected.")

    @staticmethod
    def check_running_as_root() -> None:
        if os.geteuid() != 0:
            raise RuntimeError(
                "ArduPilot manager needs to run with root privilege.")

    @property
    def current_platform(self) -> Platform:
        return self._current_platform

    @current_platform.setter
    def current_platform(self, platform: Platform) -> None:
        self._current_platform = platform
        logger.info(f"Setting {platform} as current platform.")

    @property
    def current_sitl_frame(self) -> SITLFrame:
        return self._current_sitl_frame

    @current_sitl_frame.setter
    def current_sitl_frame(self, frame: SITLFrame) -> None:
        self._current_sitl_frame = frame
        logger.info(f"Setting {frame.value} as frame for SITL.")

    def current_firmware_path(self) -> pathlib.Path:
        return self.firmware_manager.firmware_path(self.current_platform)

    def start_navigator(self, board: FlightController) -> None:
        self.current_platform = board.platform
        if not self.firmware_manager.is_firmware_installed(
                self.current_platform):
            if board.platform == Platform.NavigatorR3:
                self.firmware_manager.install_firmware_from_params(
                    Vehicle.Sub, self.current_platform)
            else:
                self.install_firmware_from_file(
                    pathlib.Path(
                        "/root/companion-files/ardupilot-manager/default/ardupilot_navigator_r4"
                    ))

        self.firmware_manager.validate_firmware(self.current_firmware_path(),
                                                self.current_platform)

        # ArduPilot process will connect as a client on the UDP server created by the mavlink router
        master_endpoint = Endpoint("Master",
                                   self.settings.app_name,
                                   EndpointType.UDPServer,
                                   "127.0.0.1",
                                   8852,
                                   protected=True)

        # Run ardupilot inside while loop to avoid exiting after reboot command
        ## Can be changed back to a simple command after https://github.com/ArduPilot/ardupilot/issues/17572
        ## gets fixed.
        # pylint: disable=consider-using-with
        #
        # The mapping of serial ports works as in the following table:
        #
        # |    ArduSub   |       Navigator         |
        # | -C = Serial1 | Serial1 => /dev/ttyS0   |
        # | -B = Serial3 | Serial3 => /dev/ttyAMA1 |
        # | -E = Serial4 | Serial4 => /dev/ttyAMA2 |
        # | -F = Serial5 | Serial5 => /dev/ttyAMA3 |
        #
        # The first column comes from https://ardupilot.org/dev/docs/sitl-serial-mapping.html

        self.ardupilot_subprocess = subprocess.Popen(
            f"{self.current_firmware_path()}"
            f" -A udp:{master_endpoint.place}:{master_endpoint.argument}"
            f" --log-directory {self.settings.firmware_folder}/logs/"
            f" --storage-directory {self.settings.firmware_folder}/storage/"
            f" -C /dev/ttyS0"
            f" -B /dev/ttyAMA1"
            f" -E /dev/ttyAMA2"
            f" -F /dev/ttyAMA3",
            shell=True,
            encoding="utf-8",
            errors="ignore",
        )

        self.start_mavlink_manager(master_endpoint)

    def start_serial(self, board: FlightController) -> None:
        if not board.path:
            raise ValueError(
                f"Could not find device path for board {board.name}.")
        self.current_platform = board.platform
        self.start_mavlink_manager(
            Endpoint("Master",
                     self.settings.app_name,
                     EndpointType.Serial,
                     board.path,
                     115200,
                     protected=True))

    def run_with_sitl(self, frame: SITLFrame = SITLFrame.VECTORED) -> None:
        self.current_platform = Platform.SITL
        if not self.firmware_manager.is_firmware_installed(
                self.current_platform):
            self.firmware_manager.install_firmware_from_params(
                Vehicle.Sub, self.current_platform)
        if frame == SITLFrame.UNDEFINED:
            frame = SITLFrame.VECTORED
            logger.warning(
                f"SITL frame is undefined. Setting {frame} as current frame.")
        self.current_sitl_frame = frame

        self.firmware_manager.validate_firmware(self.current_firmware_path(),
                                                self.current_platform)

        # ArduPilot SITL binary will bind TCP port 5760 (server) and the mavlink router will connect to it as a client
        master_endpoint = Endpoint("Master",
                                   self.settings.app_name,
                                   EndpointType.TCPServer,
                                   "127.0.0.1",
                                   5760,
                                   protected=True)
        # pylint: disable=consider-using-with
        self.ardupilot_subprocess = subprocess.Popen(
            [
                self.current_firmware_path(),
                "--model",
                self.current_sitl_frame.value,
                "--base-port",
                str(master_endpoint.argument),
                "--home",
                "-27.563,-48.459,0.0,270.0",
            ],
            shell=False,
            encoding="utf-8",
            errors="ignore",
        )

        self.start_mavlink_manager(master_endpoint)

    def start_mavlink_manager(self, device: Endpoint) -> None:
        default_endpoints = [
            Endpoint(
                "GCS Server Link",
                self.settings.app_name,
                EndpointType.UDPServer,
                "0.0.0.0",
                14550,
                persistent=True,
                enabled=False,
            ),
            Endpoint(
                "GCS Client Link",
                self.settings.app_name,
                EndpointType.UDPClient,
                "192.168.2.1",
                14550,
                persistent=True,
                enabled=True,
            ),
            Endpoint(
                "Internal Link",
                self.settings.app_name,
                EndpointType.UDPServer,
                "127.0.0.1",
                14001,
                persistent=True,
                protected=True,
            ),
        ]
        for endpoint in default_endpoints:
            try:
                self.mavlink_manager.add_endpoint(endpoint)
            except EndpointAlreadyExists:
                pass
            except Exception as error:
                logger.warning(str(error))
        self.mavlink_manager.set_master_endpoint(device)
        self.mavlink_manager.start()

    def start_board(self, boards: List[FlightController]) -> bool:
        if not boards:
            return False

        if len(boards) > 1:
            logger.warning(f"More than a single board detected: {boards}")

        # Sort by priority
        boards.sort(key=lambda flight_controller: flight_controller.platform)

        flight_controller = boards[0]
        logger.info(f"Board in use: {flight_controller}.")

        if flight_controller.platform in [
                Platform.NavigatorR3, Platform.NavigatorR5
        ]:
            self.start_navigator(flight_controller)
            return True
        if flight_controller.platform.type == PlatformType.Serial:
            self.start_serial(flight_controller)
            return True
        raise RuntimeError("Invalid board type: {boards}")

    def running_ardupilot_processes(self) -> List[psutil.Process]:
        """Return list of all Ardupilot process running on system."""
        def is_ardupilot_process(process: psutil.Process) -> bool:
            """Checks if given process is using Ardupilot's firmware file for current platform."""
            return str(self.current_firmware_path()) in " ".join(
                process.cmdline())

        return list(filter(is_ardupilot_process, psutil.process_iter()))

    async def terminate_ardupilot_subprocess(self) -> None:
        """Terminate Ardupilot subprocess."""
        if self.ardupilot_subprocess:
            self.ardupilot_subprocess.terminate()
            for _ in range(10):
                if self.ardupilot_subprocess.poll() is not None:
                    logger.info("Ardupilot subprocess terminated.")
                    return
                logger.debug("Waiting for process to die...")
                await asyncio.sleep(0.5)
            raise ArdupilotProcessKillFail(
                "Could not terminate Ardupilot subprocess.")
        logger.warning("Ardupilot subprocess already not running.")

    async def prune_ardupilot_processes(self) -> None:
        """Kill all system processes using Ardupilot's firmware file."""
        for process in self.running_ardupilot_processes():
            try:
                logger.debug(
                    f"Killing Ardupilot process {process.name()}::{process.pid}."
                )
                process.kill()
                await asyncio.sleep(0.5)
            except Exception as error:
                raise ArdupilotProcessKillFail(
                    f"Could not kill {process.name()}::{process.pid}."
                ) from error

    async def kill_ardupilot(self) -> None:
        self.should_be_running = False

        if self.current_platform != Platform.SITL:
            try:
                logger.info("Disarming vehicle.")
                self.vehicle_manager.disarm_vehicle()
                logger.info("Vehicle disarmed.")
            except Exception as error:
                logger.warning(
                    f"Could not disarm vehicle: {error}. Proceeding with kill."
                )

        # TODO: Add shutdown command on HAL_SITL and HAL_LINUX, changing terminate/prune
        # logic with a simple "self.vehicle_manager.shutdown_vehicle()"
        logger.info("Terminating Ardupilot subprocess.")
        await self.terminate_ardupilot_subprocess()
        logger.info("Ardupilot subprocess terminated.")
        logger.info("Pruning Ardupilot's system processes.")
        await self.prune_ardupilot_processes()
        logger.info("Ardupilot's system processes pruned.")

        logger.info("Stopping Mavlink manager.")
        self.mavlink_manager.stop()
        logger.info("Mavlink manager stopped.")

    async def start_ardupilot(self) -> None:
        try:
            if self.current_platform == Platform.SITL:
                self.run_with_sitl(self.current_sitl_frame)
                return
            self.run_with_board()
        except MavlinkRouterStartFail as error:
            logger.warning(f"Failed to start Mavlink router. {error}")
        finally:
            self.should_be_running = True

    async def restart_ardupilot(self) -> None:
        if self.current_platform.type in [
                PlatformType.SITL, PlatformType.Linux
        ]:
            await self.kill_ardupilot()
            await self.start_ardupilot()
            return
        self.vehicle_manager.reboot_vehicle()

    def _get_configuration_endpoints(self) -> Set[Endpoint]:
        return {
            Endpoint(**endpoint)
            for endpoint in self.configuration.get("endpoints") or []
        }

    def _save_endpoints_to_configuration(self,
                                         endpoints: Set[Endpoint]) -> None:
        self.configuration["endpoints"] = list(map(Endpoint.as_dict,
                                                   endpoints))

    def _load_endpoints(self) -> None:
        """Load endpoints from the configuration file to the mavlink manager."""
        for endpoint in self._get_configuration_endpoints():
            try:
                self.mavlink_manager.add_endpoint(endpoint)
            except Exception as error:
                logger.error(f"Could not load endpoint {endpoint}: {error}")

    def _save_current_endpoints(self) -> None:
        try:
            persistent_endpoints = set(
                filter(lambda endpoint: endpoint.persistent,
                       self.get_endpoints()))
            self._save_endpoints_to_configuration(persistent_endpoints)
            self.settings.save(self.configuration)
        except Exception as error:
            logger.error(f"Could not save endpoints. {error}")

    def get_endpoints(self) -> Set[Endpoint]:
        """Get all endpoints from the mavlink manager."""
        return self.mavlink_manager.endpoints()

    def add_new_endpoints(self, new_endpoints: Set[Endpoint]) -> None:
        """Add multiple endpoints to the mavlink manager and save them on the configuration file."""
        logger.info(
            f"Adding endpoints {[e.name for e in new_endpoints]} and updating settings file."
        )
        self.mavlink_manager.add_endpoints(new_endpoints)
        self._save_current_endpoints()
        self.mavlink_manager.restart()

    def remove_endpoints(self, endpoints_to_remove: Set[Endpoint]) -> None:
        """Remove multiple endpoints from the mavlink manager and save them on the configuration file."""
        logger.info(
            f"Removing endpoints {[e.name for e in endpoints_to_remove]} and updating settings file."
        )
        self.mavlink_manager.remove_endpoints(endpoints_to_remove)
        self._save_current_endpoints()
        self.mavlink_manager.restart()

    def update_endpoints(self, endpoints_to_update: Set[Endpoint]) -> None:
        """Update multiple endpoints from the mavlink manager and save them on the configuration file."""
        logger.info(
            f"Modifying endpoints {[e.name for e in endpoints_to_update]} and updating settings file."
        )
        self.mavlink_manager.update_endpoints(endpoints_to_update)
        self._save_current_endpoints()
        self.mavlink_manager.restart()

    def get_available_firmwares(self, vehicle: Vehicle) -> List[Firmware]:
        return self.firmware_manager.get_available_firmwares(
            vehicle, self.current_platform)

    def install_firmware_from_file(self, firmware_path: pathlib.Path) -> None:
        self.firmware_manager.install_firmware_from_file(
            firmware_path, self.current_platform)

    def install_firmware_from_url(self, url: str) -> None:
        self.firmware_manager.install_firmware_from_url(
            url, self.current_platform)

    def restore_default_firmware(self) -> None:
        self.firmware_manager.restore_default_firmware(self.current_platform)
Exemple #4
0
#!/bin/env python
import argparse
import sys
import time
from pathlib import Path

# Import local library
sys.path.append(str(Path(__file__).absolute().parent.parent))

from mavlink_proxy.AbstractRouter import AbstractRouter
from mavlink_proxy.Endpoint import Endpoint
from mavlink_proxy.Manager import Manager

if __name__ == "__main__":
    manager = Manager()
    interfaces = manager.available_interfaces()
    parser = argparse.ArgumentParser(
        description="Abstraction CLI for multiple mavlink routers")

    parser.add_argument(
        "--master",
        dest="master",
        type=Endpoint,
        required=True,
        help=
        "Master endpoint that follow the format: udp/udpout/tcp/serial:ip/device:port/baudrate",
    )

    parser.add_argument(
        "--out",
        dest="output",
class ArduPilotManager(metaclass=Singleton):
    # pylint: disable=too-many-instance-attributes
    def __init__(self) -> None:
        self.settings = Settings()
        self.settings.create_app_folders()
        self.mavlink_manager = MavlinkManager()
        self.mavlink_manager.set_logdir(self.settings.log_path)
        self._current_board: Optional[FlightController] = None
        self._current_sitl_frame: SITLFrame = SITLFrame.UNDEFINED

        # Load settings and do the initial configuration
        if self.settings.load():
            logger.info(f"Loaded settings from {self.settings.settings_file}.")
            logger.debug(self.settings.content)
        else:
            self.settings.create_settings_file()

        self.configuration = deepcopy(self.settings.content)
        self._load_endpoints()
        self.ardupilot_subprocess: Optional[Any] = None
        self.firmware_manager = FirmwareManager(self.settings.firmware_folder, self.settings.defaults_folder)
        self.vehicle_manager = VehicleManager()

        self.should_be_running = False

    async def auto_restart_ardupilot(self) -> None:
        """Auto-restart Ardupilot when it's not running but was supposed to."""
        while True:
            process_not_running = (
                self.ardupilot_subprocess is not None and self.ardupilot_subprocess.poll() is not None
            ) or len(self.running_ardupilot_processes()) == 0
            needs_restart = self.should_be_running and (
                self.current_board is None
                or (self.current_board.type in [PlatformType.SITL, PlatformType.Linux] and process_not_running)
            )
            if needs_restart:
                logger.debug("Restarting ardupilot...")
                try:
                    await self.kill_ardupilot()
                except Exception as error:
                    logger.warning(f"Could not kill Ardupilot: {error}")
                try:
                    await self.start_ardupilot()
                except Exception as error:
                    logger.warning(f"Could not start Ardupilot: {error}")
            await asyncio.sleep(5.0)

    async def start_mavlink_manager_watchdog(self) -> None:
        await self.mavlink_manager.auto_restart_router()

    @property
    def current_board(self) -> Optional[FlightController]:
        return self._current_board

    @property
    def current_sitl_frame(self) -> SITLFrame:
        return self._current_sitl_frame

    @current_sitl_frame.setter
    def current_sitl_frame(self, frame: SITLFrame) -> None:
        self._current_sitl_frame = frame
        logger.info(f"Setting {frame.value} as frame for SITL.")

    def start_navigator(self, board: FlightController) -> None:
        self._current_board = board
        if not self.firmware_manager.is_firmware_installed(self._current_board):
            if board.platform == Platform.Navigator:
                self.firmware_manager.install_firmware_from_file(
                    pathlib.Path("/root/blueos-files/ardupilot-manager/default/ardupilot_navigator"),
                    board,
                )

        firmware_path = self.firmware_manager.firmware_path(self._current_board.platform)
        self.firmware_manager.validate_firmware(firmware_path, self._current_board.platform)

        # ArduPilot process will connect as a client on the UDP server created by the mavlink router
        master_endpoint = Endpoint(
            "Master", self.settings.app_name, EndpointType.UDPServer, "127.0.0.1", 8852, protected=True
        )

        # Run ardupilot inside while loop to avoid exiting after reboot command
        ## Can be changed back to a simple command after https://github.com/ArduPilot/ardupilot/issues/17572
        ## gets fixed.
        # pylint: disable=consider-using-with
        #
        # The mapping of serial ports works as in the following table:
        #
        # |    ArduSub   |       Navigator         |
        # | -C = Serial1 | Serial1 => /dev/ttyS0   |
        # | -B = Serial3 | Serial3 => /dev/ttyAMA1 |
        # | -E = Serial4 | Serial4 => /dev/ttyAMA2 |
        # | -F = Serial5 | Serial5 => /dev/ttyAMA3 |
        #
        # The first column comes from https://ardupilot.org/dev/docs/sitl-serial-mapping.html

        self.ardupilot_subprocess = subprocess.Popen(
            f"{firmware_path}"
            f" -A udp:{master_endpoint.place}:{master_endpoint.argument}"
            f" --log-directory {self.settings.firmware_folder}/logs/"
            f" --storage-directory {self.settings.firmware_folder}/storage/"
            f" -C /dev/ttyS0"
            f" -B /dev/ttyAMA1"
            f" -E /dev/ttyAMA2"
            f" -F /dev/ttyAMA3",
            shell=True,
            encoding="utf-8",
            errors="ignore",
            cwd=self.settings.firmware_folder,
        )

        self.start_mavlink_manager(master_endpoint)

    def start_serial(self, board: FlightController) -> None:
        if not board.path:
            raise ValueError(f"Could not find device path for board {board.name}.")
        self._current_board = board
        self.start_mavlink_manager(
            Endpoint("Master", self.settings.app_name, EndpointType.Serial, board.path, 115200, protected=True)
        )

    def start_sitl(self, frame: SITLFrame = SITLFrame.VECTORED) -> None:
        self._current_board = BoardDetector.detect_sitl()
        if not self.firmware_manager.is_firmware_installed(self._current_board):
            self.firmware_manager.install_firmware_from_params(Vehicle.Sub, self._current_board)
        if frame == SITLFrame.UNDEFINED:
            frame = SITLFrame.VECTORED
            logger.warning(f"SITL frame is undefined. Setting {frame} as current frame.")
        self.current_sitl_frame = frame

        firmware_path = self.firmware_manager.firmware_path(self._current_board.platform)
        self.firmware_manager.validate_firmware(firmware_path, self._current_board.platform)

        # ArduPilot SITL binary will bind TCP port 5760 (server) and the mavlink router will connect to it as a client
        master_endpoint = Endpoint(
            "Master", self.settings.app_name, EndpointType.TCPServer, "127.0.0.1", 5760, protected=True
        )
        # pylint: disable=consider-using-with
        self.ardupilot_subprocess = subprocess.Popen(
            [
                firmware_path,
                "--model",
                self.current_sitl_frame.value,
                "--base-port",
                str(master_endpoint.argument),
                "--home",
                "-27.563,-48.459,0.0,270.0",
            ],
            shell=False,
            encoding="utf-8",
            errors="ignore",
            cwd=self.settings.firmware_folder,
        )

        self.start_mavlink_manager(master_endpoint)

    def start_mavlink_manager(self, device: Endpoint) -> None:
        default_endpoints = [
            Endpoint(
                "GCS Server Link",
                self.settings.app_name,
                EndpointType.UDPServer,
                "0.0.0.0",
                14550,
                persistent=True,
                enabled=False,
            ),
            Endpoint(
                "GCS Client Link",
                self.settings.app_name,
                EndpointType.UDPClient,
                "192.168.2.1",
                14550,
                persistent=True,
                enabled=True,
            ),
            Endpoint(
                "MAVLink2Rest",
                self.settings.app_name,
                EndpointType.UDPClient,
                "127.0.0.1",
                14000,
                persistent=True,
                protected=True,
            ),
            Endpoint(
                "Internal Link",
                self.settings.app_name,
                EndpointType.UDPServer,
                "127.0.0.1",
                14001,
                persistent=True,
                protected=True,
            ),
            Endpoint(
                "Ping360 Heading",
                self.settings.app_name,
                EndpointType.UDPServer,
                "0.0.0.0",
                14660,
                persistent=True,
                protected=True,
            ),
        ]
        for endpoint in default_endpoints:
            try:
                self.mavlink_manager.add_endpoint(endpoint)
            except EndpointAlreadyExists:
                pass
            except Exception as error:
                logger.warning(str(error))
        self.mavlink_manager.start(device)

    async def change_board(self, board: FlightController) -> None:
        logger.info(f"Trying to run with '{board.name}'.")
        if not board in BoardDetector.detect():
            raise ValueError(f"Cannot use '{board.name}'. Board not detected.")
        self.set_preferred_board(board)
        await self.kill_ardupilot()
        await self.start_ardupilot()

    def set_preferred_board(self, board: FlightController) -> None:
        logger.info(f"Setting {board.name} as preferred flight-controller.")
        self.configuration["preferred_board"] = board.dict(exclude={"path"})
        self.settings.save(self.configuration)

    def get_preferred_board(self) -> FlightController:
        preferred_board = self.configuration.get("preferred_board")
        if not preferred_board:
            raise NoPreferredBoardSet("Preferred board not set yet.")
        return FlightController(**preferred_board)

    def get_board_to_be_used(self, boards: List[FlightController]) -> FlightController:
        """Check if preferred board exists and is connected. If so, use it, otherwise, choose by priority."""
        try:
            preferred_board = self.get_preferred_board()
            logger.debug(f"Preferred flight-controller is {preferred_board.name}.")
            for board in boards:
                # Compare connected boards with saved board, excluding path (which can change between sessions)
                if preferred_board.dict(exclude={"path"}).items() <= board.dict().items():
                    return board
            logger.debug(f"Flight-controller {preferred_board.name} not connected.")
        except NoPreferredBoardSet as error:
            logger.warning(error)

        # SITL should only be used if explicitly set by user, in which case it's a preferred board and the
        # previous return logic will get it. We do this to prevent the user thinking that it's physical board
        # is correctly running when in fact it was SITL automatically starting.
        real_boards = [board for board in boards if board.type != PlatformType.SITL]
        if not real_boards:
            raise RuntimeError("Only available board is SITL, and it wasn't explicitly chosen.")
        real_boards.sort(key=lambda board: board.platform)
        return real_boards[0]

    def running_ardupilot_processes(self) -> List[psutil.Process]:
        """Return list of all Ardupilot process running on system."""

        def is_ardupilot_process(process: psutil.Process) -> bool:
            """Checks if given process is using a Ardupilot's firmware file, for any known platform."""
            for platform in Platform:
                firmware_path = self.firmware_manager.firmware_path(platform)
                if str(firmware_path) in " ".join(process.cmdline()):
                    return True
            return False

        return list(filter(is_ardupilot_process, psutil.process_iter()))

    async def terminate_ardupilot_subprocess(self) -> None:
        """Terminate Ardupilot subprocess."""
        if self.ardupilot_subprocess:
            self.ardupilot_subprocess.terminate()
            for _ in range(10):
                if self.ardupilot_subprocess.poll() is not None:
                    logger.info("Ardupilot subprocess terminated.")
                    return
                logger.debug("Waiting for process to die...")
                await asyncio.sleep(0.5)
            raise ArdupilotProcessKillFail("Could not terminate Ardupilot subprocess.")
        logger.warning("Ardupilot subprocess already not running.")

    async def prune_ardupilot_processes(self) -> None:
        """Kill all system processes using Ardupilot's firmware file."""
        for process in self.running_ardupilot_processes():
            try:
                logger.debug(f"Killing Ardupilot process {process.name()}::{process.pid}.")
                process.kill()
                await asyncio.sleep(0.5)
            except Exception as error:
                raise ArdupilotProcessKillFail(f"Could not kill {process.name()}::{process.pid}.") from error

    async def kill_ardupilot(self) -> None:
        if not self.current_board or self.current_board.platform != Platform.SITL:
            try:
                logger.info("Disarming vehicle.")
                await self.vehicle_manager.disarm_vehicle()
                logger.info("Vehicle disarmed.")
            except Exception as error:
                logger.warning(f"Could not disarm vehicle: {error}. Proceeding with kill.")

        try:
            # TODO: Add shutdown command on HAL_SITL and HAL_LINUX, changing terminate/prune
            # logic with a simple "self.vehicle_manager.shutdown_vehicle()"
            logger.info("Terminating Ardupilot subprocess.")
            await self.terminate_ardupilot_subprocess()
            logger.info("Ardupilot subprocess terminated.")
            logger.info("Pruning Ardupilot's system processes.")
            await self.prune_ardupilot_processes()
            logger.info("Ardupilot's system processes pruned.")

            logger.info("Stopping Mavlink manager.")
            self.mavlink_manager.stop()
            logger.info("Mavlink manager stopped.")
        finally:
            self.should_be_running = False

    async def start_ardupilot(self) -> None:
        try:
            available_boards = BoardDetector.detect()
            if not available_boards:
                raise RuntimeError("No boards available.")
            if len(available_boards) > 1:
                logger.warning(f"More than a single board detected: {available_boards}")

            flight_controller = self.get_board_to_be_used(available_boards)
            logger.info(f"Using {flight_controller.name} flight-controller.")

            if flight_controller.platform == Platform.Navigator:
                self.start_navigator(flight_controller)
            elif flight_controller.platform.type == PlatformType.Serial:
                self.start_serial(flight_controller)
            elif flight_controller.platform == Platform.SITL:
                self.start_sitl(self.current_sitl_frame)
            else:
                raise RuntimeError(f"Invalid board type: {flight_controller}")
        finally:
            self.should_be_running = True

    async def restart_ardupilot(self) -> None:
        if self.current_board is None or self.current_board.type in [PlatformType.SITL, PlatformType.Linux]:
            await self.kill_ardupilot()
            await self.start_ardupilot()
            return
        await self.vehicle_manager.reboot_vehicle()

    def _get_configuration_endpoints(self) -> Set[Endpoint]:
        return {Endpoint(**endpoint) for endpoint in self.configuration.get("endpoints") or []}

    def _save_endpoints_to_configuration(self, endpoints: Set[Endpoint]) -> None:
        self.configuration["endpoints"] = list(map(Endpoint.as_dict, endpoints))

    def _load_endpoints(self) -> None:
        """Load endpoints from the configuration file to the mavlink manager."""
        for endpoint in self._get_configuration_endpoints():
            try:
                self.mavlink_manager.add_endpoint(endpoint)
            except Exception as error:
                logger.error(f"Could not load endpoint {endpoint}: {error}")

    def _save_current_endpoints(self) -> None:
        try:
            persistent_endpoints = set(filter(lambda endpoint: endpoint.persistent, self.get_endpoints()))
            self._save_endpoints_to_configuration(persistent_endpoints)
            self.settings.save(self.configuration)
        except Exception as error:
            logger.error(f"Could not save endpoints. {error}")

    def get_endpoints(self) -> Set[Endpoint]:
        """Get all endpoints from the mavlink manager."""
        return self.mavlink_manager.endpoints()

    def add_new_endpoints(self, new_endpoints: Set[Endpoint]) -> None:
        """Add multiple endpoints to the mavlink manager and save them on the configuration file."""
        logger.info(f"Adding endpoints {[e.name for e in new_endpoints]} and updating settings file.")
        self.mavlink_manager.add_endpoints(new_endpoints)
        self._save_current_endpoints()
        self.mavlink_manager.restart()

    def remove_endpoints(self, endpoints_to_remove: Set[Endpoint]) -> None:
        """Remove multiple endpoints from the mavlink manager and save them on the configuration file."""
        logger.info(f"Removing endpoints {[e.name for e in endpoints_to_remove]} and updating settings file.")
        self.mavlink_manager.remove_endpoints(endpoints_to_remove)
        self._save_current_endpoints()
        self.mavlink_manager.restart()

    def update_endpoints(self, endpoints_to_update: Set[Endpoint]) -> None:
        """Update multiple endpoints from the mavlink manager and save them on the configuration file."""
        logger.info(f"Modifying endpoints {[e.name for e in endpoints_to_update]} and updating settings file.")
        self.mavlink_manager.update_endpoints(endpoints_to_update)
        self._save_current_endpoints()
        self.mavlink_manager.restart()

    def get_available_firmwares(self, vehicle: Vehicle, platform: Platform) -> List[Firmware]:
        return self.firmware_manager.get_available_firmwares(vehicle, platform)

    def install_firmware_from_file(self, firmware_path: pathlib.Path, board: FlightController) -> None:
        self.firmware_manager.install_firmware_from_file(firmware_path, board)

    def install_firmware_from_url(self, url: str, board: FlightController) -> None:
        self.firmware_manager.install_firmware_from_url(url, board)

    def restore_default_firmware(self, board: FlightController) -> None:
        self.firmware_manager.restore_default_firmware(board)
Exemple #6
0
#!/bin/env python
import argparse
import sys
import time
from pathlib import Path

from loguru import logger

# Import local library
sys.path.append(str(Path(__file__).absolute().parent.parent))

from mavlink_proxy.AbstractRouter import AbstractRouter
from mavlink_proxy.Manager import Manager

if __name__ == "__main__":
    manager = Manager()
    interfaces = manager.available_interfaces()
    parser = argparse.ArgumentParser(
        description="Abstraction CLI for multiple mavlink routers")

    parser.add_argument(
        "--master",
        dest="master",
        type=str,
        required=True,
        help=
        "Master endpoint that follow the format: udp/udpout/tcp/serial:ip/device:port/baudrate",
    )

    parser.add_argument(
        "--out",
Exemple #7
0
class ArduPilotManager:
    def __init__(self) -> None:
        self.mavlink_manager = MavlinkManager()
        available_mavlink_interfaces = self.mavlink_manager.available_interfaces(
        )
        if not available_mavlink_interfaces:
            raise RuntimeError("No MAVLink interface available.")

        # Create a folder for ArduPilotManager service
        self.settings_path = appdirs.user_config_dir(self.__class__.__name__)
        self.firmware_path = os.path.join(self.settings_path, "firmware")
        self.subprocess: Optional[Any] = None
        self.firmware_download = FirmwareDownload()

        # Create settings folder, ignore if already exists
        os.makedirs(self.firmware_path, exist_ok=True)

    def run(self) -> None:
        ArduPilotManager.check_running_as_root()

        while not self.start_board(BoardDetector.detect()):
            print("Flight controller board not detected, will try again.")
            time.sleep(2)

    @staticmethod
    def check_running_as_root() -> None:
        if os.geteuid() != 0:
            raise RuntimeError(
                "ArduPilot manager needs to run with root privilege.")

    def start_navigator(self) -> None:
        firmware = os.path.join(self.firmware_path, "ardusub")
        if not os.path.isfile(firmware):
            temporary_file = self.firmware_download.download(
                Vehicle.Sub, "Navigator")
            assert temporary_file, "Failed to download navigator binary."
            # Make the binary executable
            os.chmod(temporary_file, stat.S_IXOTH)
            os.rename(temporary_file, firmware)
        try:
            subprocess.check_output([firmware, "--help"])
        except Exception as error:
            raise RuntimeError(
                f"Failed to start navigator: {error}") from error

        local_endpoint = "tcp:0.0.0.0:5766"
        self.subprocess = subprocess.Popen(
            [
                firmware,
                "-A",
                local_endpoint,
                "--log-directory",
                f"{self.firmware_path}/logs/",
                "--storage-directory",
                f"{self.firmware_path}/storage/",
            ],
            shell=False,
            encoding="utf-8",
            errors="ignore",
        )

        # TODO: Fix ArduPilot UDP communication to use mavlink_manager
        # ArduPilot master is not working with UDP endpoints and mavlink-router
        # does not accept TCP master endpoints
        # self.start_mavlink_manager(Endpoint(local_endpoint))

        # Check if subprocess is running and wait until it finishes
        # Necessary since we don't have mavlink_manager running for navigator yet
        while self.subprocess.poll() is None:
            time.sleep(1)

    def start_serial(self, device: str) -> None:
        self.start_mavlink_manager(Endpoint(f"serial:{device}:115200"))

    def start_mavlink_manager(self, device: Endpoint) -> None:
        self.mavlink_manager.add_endpoints([Endpoint("udpin:0.0.0.0:14550")])
        self.mavlink_manager.set_master_endpoint(device)
        self.mavlink_manager.start()
        while self.mavlink_manager.is_running():
            time.sleep(1)

    def start_board(self, boards: List[Tuple[FlightControllerType,
                                             str]]) -> bool:
        if not boards:
            return False

        if len(boards) > 1:
            print(f"More than a single board detected: {boards}")

        # Sort by priority
        boards.sort(key=lambda tup: tup[0].value)

        flight_controller_type, place = boards[0]

        if FlightControllerType.Navigator == flight_controller_type:
            self.start_navigator()
        elif FlightControllerType.Serial == flight_controller_type:
            self.start_serial(place)
        else:
            raise RuntimeError("Invalid board type: {boards}")

        return False