Development Tools for CFclient

This page contains generic information about various topics that might be interesting while developing for the Crazyflie Python client. The same kind of information is available here for the Crazyflie Python API.

Here's a quick overview:

  • The GUI is made in QT6 (using QTDesigner and loading the .ui files at runtime)
  • It uses the SDL2 to read input devices on Windows/Mac OSX and raw jsdevs on Linux. It also supports custom input from LeapMotion and ZMQ.

Architecture

input arch mux


Input devices

The architecture for the input devices in the client strives to give as much flexibility as possible and to make cross platform compatibility smooth. It combines raw readings from input devices with input device mappings to create control values for the Crazyflie and the application. It's also possible to input control values directly.

Below is a walk though of every step of the process, from reading the device to sending the control values to the Crazyflie.

InputDevice

There are two ways to get input into the client: Input readers and input interfaces. On startup the modules lib/cfclient/utils/inputreaders and lib/cfclient/utils/inputinterfaces are initialized and these directories are scanned for implementations that can be used. Each python file in these directories represent a "backend" that handles input. Each backend can have zero, one or multiple devices that it can control. The inputreaders module is used to read normal joysticks/gamepads while the inputinterfaces module is used to read any custom interface that's not a joystick/gamepad.

Once the backends are found the client tries to initialize each backend. If successful it is scanned for devices, otherwise it's quietly discarded (only printing a message to the console). A structure is build where the dependency is reversed (backend->device to device->backend) and a list of devices (with connected backends) is passed on.

The client can now open any device in the list and read it. If the device is from the inputreaders module a mapping has to be supplied as described below.

Input readers

Currently there's two types of inputreaders: SDL2 and Linux. The Linux backend is used on Linux and SDL2 on all other platforms. In order to use the devices connected to the backend a mapping has to be supplied to translate the raw axis/buttons indexes (0, 1, 2..) to usable values (roll/pitch/yaw/thrust..).

Input interfaces

The input interfaces don't use any mapping, the devices itself directly generate useful values (like roll/pitch/yaw/thrust). Currently there's two implementations: LeapMotion and ZMQ. Values are read the same way as from normal gamepads/joysticks, at 100Hz. For more information on how the ZMQ interface works read here.


Files

To support the application there's a number of files around it, such as configuration and caching. All these use JSON to store information. All of the user configuration files are stored in the /conf directory. Most of the files have default versions in the /lib/configs directory that are either copied at the first start up or used in parallel as read-only copies to complement what ever is stored in the user configuration directory.

User configuration file

To save the configuration between runs of the application there's a configuration file (/conf/config.json).The file is updated while the application runs and settings change. Below is an example of the configuration file.

{
  "link_uri": "radio://0/100/250K",
  "input_device": "Sony PLAYSTATION(R)3 Controller",
  "slew_limit": 45,
  "max_rp": 30,
  "ui_update_period": 100,
  "trim_pitch": 0.0,
  "device_config_mapping": {
    "Leapmotion": "LeapMotion",
    "Sony PLAYSTATION(R)3 Controller": "PS3_Mode_1_Split-Yaw_Linux",
    "PLAYSTATION(R)3 Controller (34:C7:31:8E:CF:0E)": "PS3_Mode_1",
    "Microsoft X-Box 360 pad": "xbox360_mode1_linux"
  },
  "slew_rate": 30,
  "auto_reconnect": false,
  "max_yaw": 200,
  "flightmode": "Advanced",
  "open_tabs": "Flight Control,Parameters,Console",
  "input_device_blacklist": "(VirtualBox|VMware)",
  "trim_roll": 0.0,
  "max_thrust": 80.0,
  "min_thrust": 25.0
}
Field Format Comments
link_uri string The last successfully connected Crazyflie URI. This is used to fill in the address in the top bar at startup
auto_reconnect boolean Set's if auto-reconnect is enabled or not
ui_update_period int The minimum time (in ms) between UI updates for logging values
open_tabs string A comma-separated list of the open tabs (using the tab.tabName attribute)
input_device string The readable name of the last used input device
device_config_mapping dict A dictionary where the keys are readable input device names and the values are the last used mapping for the device
input_device_blacklist string A regexp that will sort out input devices while scanning. This is to avoid detecting virtual joysticks while using a VM
flight_mode string The name of the last used flightmode (either Advanced or ?)
slew_limit int The limit (in %) where the slew-tate limiting kicks in, only applicable in Advanced mode
slew_rate int The slew rate in %/s that will limit the lowering of the thrust, only applicable in Advanced mode
trim_pitch float The pitch trim (degrees)
trim_roll float The roll trim (degrees)
max_thrust float Max allowed thrust, only applicable in Advanced mode
min_thrust float Min allowed thrust, only applicable in Advanced mode
max_yaw float Max allowed yaw rate (degrees/s), only applicable in Advanced mode
max_rp float Max allowed roll/pitch (degrees), only applicable in Advanced mode

Default configuration file

The source code contains a default configuration file (/lib/cfclient/configs/config.json). The file contains two parts: The default writable part and the default read-only part. When the application is started for the first time (and /conf/ doesn't exists) the writable part of this configuration file is copied to the /conf/config.json file to create the default values. The read-only part is used for settings that cannot be changed, but shouldn't be hardcoded in the code. When the application starts and both the user config in /conf/config.json and the read-only part of /lib/cfclient/configs/config.json is merged so they can all be accessed in the application.

{
  "writable" : {
    "input_device": "",
    "link_uri": "",
    "flightmode": "Normal",
    "open_tabs": "Flight Control",
    "trim_pitch": 0.0,
    "slew_limit": 45,
    "slew_rate": 30,
    "trim_roll": 0.0,
    "max_thrust": 80,
    "min_thrust": 25,
    "max_yaw": 200,
    "max_rp": 30,
    "auto_reconnect": false,
    "device_config_mapping": {},
    "input_device_blacklist": "(VirtualBox|VMware)",
    "ui_update_period": 100
  },
  "read-only" : {
    "normal_slew_limit": 45,
    "normal_slew_rate": 30,
    "normal_max_thrust": 80,
    "normal_min_thrust": 25,
    "normal_max_yaw": 200,
    "normal_max_rp": 30,
    "default_cf_channel": 10,
    "default_cf_speed": 0,
    "default_cf_trim": 0
  }
}

TOC cache files

In order to speed up the connection procedure for the Crazyflie the TOCs are cached (more info on logging/parameter frameworks and TOC ). The writable part of the TOC cache is located in /conf/cache where each cache is saved in a file named after the CRC32 (in hex) of the TOC CRC32 (for example 1CB41680.json). There's also a read-only part of the TOC cache that's located in /lib/cglib/cache and contains the caches for official builds. When the application connects to a Crazyflie the CRC32 of the log and param TOC is requested. When the client receives it will check if a file with the correct name exists (in both the RW and the RO TOC cache). If it does it will load the cached TOC, if not it will start requesting the TOC from the Crazyflie and when it's done it will save it in the cache.

The TOC cache files are organized in a hierarchical manner after the group.name concept. In the examples below you first see the group acc which contains the variables y,x,z,zw and mag2. Each of these variables have a set of attributes that are described below.

Field Format Comments
ident int The TOC id of the variable
group string The group the variable belongs to
name string The name of the variable
prototype string The Python unpack string of the variable used when unpacking the binary data
[class]{.underline} string The name of the class that can hold this variable (either LogTocElement or ParamTocElement)
ctype string The variable type in the firmware
access int The access restrictions mask for the variable (only applicable for parameters). 0 = RW, 1 = RO

Below is an example of part of the log TOC cache:

{
  "acc": {
    "y": {
      "ident": 8,
      "group": "acc",
      "name": "y",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    },
    "x": {
      "ident": 7,
      "group": "acc",
      "name": "x",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    },
    "z": {
      "ident": 9,
      "group": "acc",
      "name": "z",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    },
    "zw": {
      "ident": 10,
      "group": "acc",
      "name": "zw",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    },
    "mag2": {
      "ident": 11,
      "group": "acc",
      "name": "mag2",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    }
  },
  "mag": {
    "y": {
      "ident": 39,
      "group": "mag",
      "name": "y",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    },
    "x": {
      "ident": 38,
      "group": "mag",
      "name": "x",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    },
    "z": {
      "ident": 40,
      "group": "mag",
      "name": "z",
      "pytype": "<f",
      "__class__": "LogTocElement",
      "ctype": "float",
      "access": 0
    }
  },
  "stabilizer": {
      ....
  }
}

Below is an example of part of the param TOC cache:

{
  "imu_sensors": {
    "AK8963": {
      "ident": 0,
      "group": "imu_sensors",
      "name": "AK8963",
      "pytype": "<B",
      "__class__": "ParamTocElement",
      "ctype": "uint8_t",
      "access": 1
    },
    "LPS25H": {
      "ident": 1,
      "group": "imu_sensors",
      "name": "LPS25H",
      "pytype": "<B",
      "__class__": "ParamTocElement",
      "ctype": "uint8_t",
      "access": 1
    }
  },
  "sensorfusion6": {
    "ki": {
      "ident": 30,
      "group": "sensorfusion6",
      "name": "ki",
      "pytype": "<f",
      "__class__": "ParamTocElement",
      "ctype": "float",
      "access": 0
    },
    "kp": {
      "ident": 29,
      "group": "sensorfusion6",
      "name": "kp",
      "pytype": "<f",
      "__class__": "ParamTocElement",
      "ctype": "float",
      "access": 0
    }
  },
  "flightmode": {
    "althold": {
      "ident": 10,
      "group": "flightmode",
      "name": "althold",
      "pytype": "<B",
      "__class__": "ParamTocElement",
      "ctype": "uint8_t",
      "access": 0
    }
  },
  "firmware": {
    "revision0": {
      "ident": 57,
      "group": "firmware",
      "name": "revision0",
      "pytype": "<L",
      "__class__": "ParamTocElement",
      "ctype": "uint32_t",
      "access": 1
    },
    "revision1": {
      "ident": 58,
      "group": "firmware",
      "name": "revision1",
      "pytype": "<H",
      "__class__": "ParamTocElement",
      "ctype": "uint16_t",
      "access": 1
    },
    "modified": {
      "ident": 59,
      "group": "firmware",
      "name": "modified",
      "pytype": "<B",
      "__class__": "ParamTocElement",
      "ctype": "uint8_t",
      "access": 1
    }
  },
  "cpu": {
     ....
  }
}

Input device configuration

Input device configurations are used to map raw axis (integers) to values such as roll/pitch/yaw/thrust (more info above). The configurations are stored in /conf/input, one file for each configuration. The default configurations are stored in /lib/cfclient/configs. The first time the configuration starts up (if /conf/input doesn't exist) the default configurations are copied into this directory and can then be used.

A raw axis can be mapped to one or more values, that way it's possible to split up values on multiple axis. An example of this is using the bumper buttons to control the yaw, where the left one controls CW rotation and the right one controls CCW rotation.

Field Format Comments
inputconfig dict Contains one input device
inputdevice dict Contains a configuration for an input device
updateperiod int Specifies how often the device is read (not used)
name string Readable name of the configuration
axis list A list of every axis that is mapped
scale float A scale that should be applied to the axis value (will be divided with the scale). Negative values can be used to invert the axis
offset float An offset that should be applied to the axis value
type string Either Input.AXIS or Input.BUTTON depending on if it's an axis or a button that id or ids refer to
id int The driver id of the axis (used for single axis mapping)
ids list of ints The driver ids of the axis (used for split axis configuration). The first one will be the negative part and the second one the positive part
key string This string is used inside the application to determine what value should be updated using this axis
name string Readable name of the axis (not used)
{
  "inputconfig": {
    "inputdevice": {
      "updateperiod": 10,
      "name": "PS3_Mode_1_Split-Yaw_Linux",
      "axis": [
        {
          "scale": -1.0,
          "type": "Input.AXIS",
          "id": 3,
          "key": "thrust",
          "name": "thrust",
          "offset": 1.0,
        },
        {
          "scale": 1.0,
          "type": "Input.AXIS",
          "ids": [
            12,
            13
          ],
          "key": "yaw",
          "name": "yaw"
        },
        {
          "scale": 1.0,
          "type": "Input.AXIS",
          "id": 0,
          "key": "roll",
          "name": "roll"
        },
        {
          "scale": -1.0,
          "type": "Input.AXIS",
          "id": 1,
          "key": "pitch",
          "name": "pitch"
        },
        {
          "scale": -1.0,
          "type": "Input.BUTTON",
          "id": 6,
          "key": "pitchcal",
          "name": "pitchNeg"
        },
        {
          "scale": 1.0,
          "type": "Input.BUTTON",
          "id": 4,
          "key": "pitchcal",
          "name": "pitchPos"
        },
        {
          "scale": 1.0,
          "type": "Input.BUTTON",
          "id": -1,
          "key": "estop",
          "name": "killswitch"
        },
        {
          "scale": -1.0,
          "type": "Input.BUTTON",
          "id": 7,
          "key": "rollcal",
          "name": "rollNeg"
        },
        {
          "scale": 1.0,
          "type": "Input.BUTTON",
          "id": 5,
          "key": "rollcal",
          "name": "rollPos"
        },
        {
          "scale": 1.0,
          "type": "Input.BUTTON",
          "id": 14,
          "key": "althold",
          "name": "althold"
        },
        {
          "scale": 1.0,
          "type": "Input.BUTTON",
          "id": 12,
          "key": "exit",
          "name": "exitapp"
        }
      ]
    }
  }
}

Log configuration files

The user can configure custom logging configurations from the UI (more information on logging/parameter frameworks (/doc/crazyflie/dev/arch/logparam) ). These will be saved in the /conf/log directory, one file for each configuration. Default logging configurations are stored in the /lib/cfclient/configs/log and are copied into the user configuration directory on the first status (if /conf/log doesn't exist).

Field Format Comments  
logconfig dict Contains a logging configuration  
logblock dict A logging configuration  
name string A readable name of the configuration that will be shown in the UI  
period int The period the logging data should be requested in. Minimum resolution is 10th of ms
variables list A list of dictionaries, one for each variable in the configuration  
name string The full name of the variable in the group.name format  
type string Could be either TOC or Memory, currently only TOC is implemented  
stored_as string The format (as C type) that the variable is stored as in the firmware  
fetch_as string The format (as C type) that the variable should be logged as  

Below is an example of a log configuration file:

{
  "logconfig": {
    "logblock":
    {"name": "Stabilizer", "period":20,
     "variables": [
          {"name":"stabilizer.roll", "type":"TOC", "stored_as":"float", "fetch_as":"float"},
          {"name":"stabilizer.pitch", "type":"TOC", "stored_as":"float", "fetch_as":"float"},
          {"name":"stabilizer.yaw", "type":"TOC", "stored_as":"float", "fetch_as":"float"}
     ]}
  }
}