boatd documentation

Introduction

Boatd is designed to be the manager for a boat control system, granting graceful startup, telemetry, logging and a built in simulator.

There are two main components of a system written using boatd:

  • the driver interfaces with the particular set of hardware in the boat.
  • the behaviour performs a set of actions to make the boat do a particular task. The API available for these scripts is supposed to be declarative, with the idea that for any boat with a driver written, any behaviour script will work.
_images/boatd-arch.png

Installing

Boatd is currently tested and supported on Python 2.7 and 3.4. Support for python 2 may be dropped in the near future.

Installing with Docker

$ docker build -t boatd .

Installing for development

First, clone the repository and change to the directory:

$ git clone https://github.com/boatd/boatd.git
$ cd boatd

Create a new virtualenv:

$ virtualenv boatd-dev-env

Activate this virtualenv:

$ source boatd-dev-env/bin/activate

Install boatd in editable mode from the local copy:

$ pip install --editable .

Installing when you don’t care and live life on the edge (system wide installation)

First install dependencies:

On any Debian based distribution (Debian, Ubuntu, Mint etc):

$ apt-get install python-yaml

On Red Hat systems (Fedora, CentOS, etc):

$ dnf install PyYAML

Then clone the repository and change to the directory:

$ git clone https://github.com/boatd/boatd.git
$ cd boatd

Run the installer:

$ sudo python setup.py install

Running boatd

Running with Docker:

Assumming you have built the docker image locally:

Quick-start:

$ docker run -d -p 2222:2222 boatd
$ curl localhost:2222
{"boatd": {"version": 1.3}}

By default, the image uses the example configuration, drivers and behaviours.

There are three major ways to develop using this Docker image.

  1. Modify the configuration and mount the custom configuration.
  2. Simply mount a directory (e.g. for plugins, drivers or behaviours).
  3. A combination of the above.

For example, to use a custom driver:

Running locally:

$ boatd --help
usage: boatd [-h] [--version] [CONFIG FILE]

Experimental robotic sailing boat daemon.

positional arguments:
  CONFIG FILE  a path to a configuration file

optional arguments:
  -h, --help   show this help message and exit
  --version    show program's version number and exit

After you have installed boatd, it can be run with $ boatd.

You will need to create a configuration file. It should look something like:

boatd:
  port: 2222
  interface: 127.0.0.1

plugin_directory: null

plugins:
  - logger:
    period: 10
    filename: logs/gps_trace

    driver:
            file: example/basic_driver.py

    behaviours:
            - example:
                    file: example/basic_behaviour.py

The example config file (boatd-config.yaml.example) can be modified for your boat.

Output will be similar to:

$ boatd
[15:43:55] loaded function heading as "heading"
[15:43:55] loaded function get_wind as "wind_direction"
[15:43:55] loaded function get_wind_speed as "wind_speed"
[15:43:55] loaded function position as "position"
[15:43:55] loaded function rudder as "rudder"
[15:43:55] loaded function sail as "sail"
[15:43:55] loaded driver from example/basic_driver.py

If you would like to use a different config file in a different location, pass the path as an argument to boatd. For example, $ boatd /etc/boatd/fancy-conf.yaml.

Using the boatd API

Boatd’s main method of interaction is via the JSON API.

/

  • GET

    Returns the current status and version of boatd. Example output:

    {
       "boatd": {
         "version": 1.1
       }
    }
    

/boat

  • GET

    Returns attributes about the current state of the boat. Example output:

    {
      "active": false,
      "position": [2.343443, null],
      "heading": 2.43,
      "wind": {
        "direction": 8.42,
        "speed": 25
      }
    }
    

/wind

  • GET

    Returns properties of the wind. Example output:

    {
      "direction": 8.42,
      "speed": 25
    }
    

/waypoints

  • GET

    Returns the active set of waypoints

    {
      "current": [1.0, 1.0],
      "home": [0.0, 0.0],
      "waypoints": [
        [0.0, 0.0],
        [1.0, 1.0],
        [2.0, 2.0]
      ]
    }
    
  • POST

    Add to the current set of waypoints

    {
      "waypoints": [
        [0.0, 0.0],
        [1.0, 1.0],
        [2.0, 2.0]
      ]
    }
    

/behaviours

  • GET

    Returns data about available and current behaviours. Example output:

    {
      "current": null,
      "behaviours": {
        "basic": {
          "filename": "example/basic_behaviour.py",
          "running": false
        }
      }
    }
    
  • POST

    Change the currently running behaviour. Setting the current behaviour to null will cause no behaviour to be run.

    Examples:

    {
      "current": null
    }
    
    {
      "current": "basic"
    }
    

Drivers

Driver basics

Boatd drivers are implemented as a simple user defined class in a loadable python module. When a behaviour script requires information about the current state of the boat or needs to send a command to some hardware, boatd runs one of the methods in the driver.

To write a driver, a python module should be created that contains an object named driver. This object must be an instance of a class inheriting from and implementing the interface defined in BaseBoatdDriver:

class boatd.BaseBoatdDriver[source]
absolute_wind_direction()[source]

Must return the direction the wind is blowing in degrees, relative to the world.

Return type:float between 0 and 360
heading()[source]

Must return the heading of the boat in degrees, relative to the world.

Return type:float between 0 and 360
position()[source]

Must return a tuple containing the current latitude and longitude of the boat, in that order.

Return type:tuple of two floats - (float, float)
reconnect()[source]

Reconnect the driver to boat devices. It is recommended that initial connections are made using this function by calling it in the __init__ method. If the driver does not require any persistent connections, this method may be empty.

rudder(angle)[source]

Set the boat’s rudder to angle degrees relative to the boat.

Parameters:angle (float between -90 and 90) – target number of degrees
sail(angle)[source]

Set the sail to angle degrees relative to the boat.

Parameters:angle (float between -90 and 90) – target number of degrees
wind_speed()[source]

Must return the speed the wind is blowing in knots.

Return type:float

Note that the driver instance must be named driver, otherwise boatd won’t know where to find it.

Example driver

An example:

import boatd

class MyFancyBoatDriver(boatd.BaseBoatdDriver):
    def __init__(self):
        # initialize some things here
        pass

    def heading(self):
        return 30.0

    def wind_direction(self):
        return 45.0

    def wind_speed(self):
        return 4.0

    def position(self):
        return (0, 0)

    def rudder(self, angle):
        print('moving rudder to', angle)

    def sail(self, angle):
        print('moving sail to', angle)

    def reconnect(self):
            pass
# create an instance of the driver class
driver = MyFancyBoatDriver()

Configuring boatd to use a driver

Once you’ve written a driver, you can tell boatd to load it as the active driver by setting scripts.driver in your configuration file. Eg:

scripts:
    driver: example/driver.py

This can be a relative path, as with the example above. It can also be absolute. boatd will also expand ~ to your home directory:

scripts:
    driver: ~/git/sails-boatd-driver/driver.py

Plugins

Plugins are loadable python modules that run in a separate thread inside boatd. They have access to the current data about the boat.

Plugins are enabled with the main boatd configuration file. Each plugin may have a few extra parameters, but all have the enabled parameter to enable or disable it.

Example:

plugins:
  - some_plugin_name:
    enabled: true

Bundled plugins

Boatd comes with a few plugins preinstalled. These are:

  • logger

    This logs data about the current state of the boat to a file periodically.

    Configuration parameters:

    • period - the time in seconds between each logged line
    • filename - the path to the file logs will be written to

    Example:

    plugins:
      - logger:
        enabled: true
        period: 10
        filename: logs/log_trace
    
  • gpx_logger

    This logs data about the current state of the boat to a GPX formatted file periodically.

    Configuration parameters:

    • period - the time in seconds between each logged line
    • filename - the path to the file logs will be written to, the filename

    will be appended with a timestamp

    Example:

    plugins:
      - gpx_logger:
        enabled: true
        period: 1
        filename: logs/gpx_log
    
  • mavlink

    This allows boatd to communicate using a subset of the mavlink protocol.

    Configuration parameters:

    • device - the serial port to use
    • baud - baud rate to use with the serial port

    Example:

    plugins:
      - mavlink:
        enabled: true
        device: /dev/ttyUSB0
        baud: 115200
    

Writing new plugins

To implement a plugin, a class must be implemented that conforms to a certain interface (similar to how drivers are defined). The interface is simple:

class boatd.BasePlugin(config, boatd)[source]
main()[source]

The main method for a plugin. This should contain a loop if the plugin is intended to be long-running.

An example implementation would be:

from boatd import BasePlugin

class ExamplePlugin(BasePlugin):
    def main(self):
        while self.running:
            position = self.boatd.boat.position()
            print('logging some stuff ', position)

plugin = LoggerPlugin

Some things to note:

  • You automatically get access to an object called self.boatd. This contains a boat attribute which you can use to interact with the live boat.
  • self.running can be used to check if the plugin should end. When the plugin is started by boatd, this will be set to True. When boatd is about to quit or plugins need to be stopped for some other reason, it will be set to False.

python-boatdclient

Boatd has a client library written for python. It contains a python wrapper module and a command line client.

You can install python-boatdclient from PyPi by running:

$ pip install python-boatdclient

Boatdclient includes the following user facing classes:

class boatdclient.Boat(boatd=None, auto_update=True)[source]

A boat controlled by boatd

Parameters:auto_update – automatically update properties when they are requested.
heading

Return the current heading of the boat in degrees.

Returns:current bearing
Return type:Bearing
position

Return the current position of the boat.

Returns:current position
Return type:Point
set_rudder(angle)[source]

Set the angle of the rudder to be angle degrees.

Parameters:angle (float between -90 and 90) – rudder angle
set_sail(angle)[source]

Set the angle of the sail to angle degrees

Parameters:angle (float between -90 and 90) – sail angle
target_rudder_angle

Return the current target rudder angle in degrees.

Returns:rudder angle
Return type:float
target_sail_angle

Return the current target sail angle in degrees.

Returns:sail angle
Return type:float
wind

Return the direction of the wind in degrees.

Returns:wind object containing direction bearing and speed
Return type:Wind
class boatdclient.Behaviour(boatd=None)[source]
list()[source]

Return a list of the available behaviours to run.

start(name)[source]

End the current behaviour and run a named behaviour.

Parameters:name (str) – the name of the behaviour to run
stop()[source]

Stop the current behaviour.

Boat returns and uses special classes for bearings and latitude longitude points. These contain some common functionality.

class boatdclient.Point(latitude, longitude)[source]

A point on the face of the earth

bearing_to(point)[source]

Return the bearing to another point.

Parameters:point (Point) – Point to measure bearing to
Returns:The bearing to the other point
Return type:Bearing
cross_track_distance(start_point, end_point)[source]

Return the cross track distance from this point to the line between two points:

           * end_point
          /
         /
        /   * this point
       /
      /
     *
start_point
Parameters:
  • start_point (Point) – First point on the line
  • end_point (Point) – Second point on the line
Returns:

The perpendicular distance to the line between start_point and end_point, where distance on the right of start_point is positive and distance on the left is negative

Return type:

float

distance_to(point)[source]

Return the distance between this point and another point in meters.

Parameters:point (Point) – Point to measure distance to
Returns:The distance to the other point
Return type:float
classmethod from_radians(lat_radians, long_radians)[source]

Return a new instance of Point from a pair of coordinates in radians.

lat

Return the latitude in degrees

lat_radians

Return the latitude in radians

long

Return the longitude in degrees

long_radians

Return the longitude in radians

relative_point(bearing_to_point, distance)[source]

Return a waypoint at a location described relative to the current point

Parameters:
  • bearing_to_point (Bearing) – Relative bearing from the current waypoint
  • distance (float) – Distance from the current waypoint
Returns:

The point described by the parameters

class boatdclient.Bearing(degrees)[source]

An angle between 0 and 360 degrees

Examples:

>>> Bearing(100)
<Bearing (100.00 degrees clockwise from north) at 0x7f25e22b3710>
>>> Bearing(100) + Bearing(100)
<Bearing (200.00 degrees clockwise from north) at 0x7f25e22b3940>
>>> Bearing(100) + Bearing(300)
<Bearing (40.00 degrees clockwise from north) at 0x7f25e22b37b8>
>>> Bearing(0) - Bearing(100)
<Bearing (260.00 degrees clockwise from north) at 0x7f25e22b3940>
>>> import math
>>> Bearing.from_radians(math.pi)
<Bearing (180.00 degrees clockwise from north) at 0x7f25e22b3828>
>>> int(Bearing(120.4))
120
>>> float(Bearing(120.4))
120.4
delta(other)[source]

Return the error between this and another bearing. This will be an angle in degrees, positive or negative depending on the direction of the error.

self other
/
/
__/
/ <- angle will be +ve
other self
/
/
__/
/ <- angle will be -ve
Parameters:other (Bearing) – bearing to compare to
Returns:error angle
Return type:float

Testing

To run tests, install tox

$ pip install tox

and run tox. If all the tests pass, the output should be similar to:

$ tox
GLOB sdist-make: /home/louis/git/boatd/setup.py
py27 inst-nodeps: /home/louis/git/boatd/.tox/dist/boatd-1.1.3.zip
py27 installed: boatd==1.1.3,coverage==4.0.2,coveralls==1.1,docopt==0.6.2,p
luggy==0.3.1,py==1.4.30,pytest==2.8.2,pytest-cov==2.2.0,PyYAML==3.11,reques
ts==2.8.1,tox==2.2.1,virtualenv==13.1.2,wheel==0.24.0
py27 runtests: PYTHONHASHSEED='2985615961'
py27 runtests: commands[0] | py.test -v --cov boatd boatd
========================= test session starts ==========================
platform linux2 -- Python 2.7.10, pytest-2.8.2, py-1.4.30, pluggy-0.3.1 --
/home/louis/git/boatd/.tox/py27/bin/python2.7
cachedir: .cache
rootdir: /home/louis/git/boatd, inifile:
plugins: cov-2.2.0
collected 50 items

boatd/tests/test_api.py::TestAPI::test_GET PASSED
boatd/tests/test_api.py::TestAPI::test_content_type PASSED

... snipped

====================== 50 passed in 1.39 seconds =======================
_______________________________ summary ________________________________
  py27: commands succeeded
  py34: commands succeeded
  pypy: commands succeeded
  flake8: commands succeeded
  congratulations :)

This will run all test environments. To run an individual environment, run tox -e py27, or more generally tox -e <env>, replacing env with py27, py34, pypy or flake8 (style checks).

The current test results from the head of the master branch can be found here.