From Scratch to Servo Control: Raspberry Pi and Flutter with snapp_cli

Moritz Theis
Snapp X
Published in
14 min readSep 3, 2024

In this tutorial, you’ll learn how to set up a Raspberry Pi from scratch, connect and control a servo motor using a PCA9685 PWM driver, and create a Flutter application to control the servo motor. You’ll also learn how to use snapp_cli to set up Flutter on the Raspberry Pi, utilize D-Bus for communication between a Python backend and your Flutter app, and perform remote debugging.

Key Learning Points:

  1. Setting up a Raspberry Pi with the latest Raspberry Pi OS.
  2. Connecting and controlling hardware (a servo motor) via I2C on a Raspberry Pi.
  3. Using snapp_cli to bootstrap and run Flutter apps with flutter-pi on Raspberry Pi.
  4. Implementing a Python backend to control hardware and interface with D-Bus.
  5. Creating a Flutter app that interacts with hardware using D-Bus communication.

What Do You Need / Prerequisites

Hardware:

  • Raspberry Pi 5 (or 4B) with power supply and SD card (16GB minimum recommended).
  • Touchscreen connected to the Raspberry Pi we will be using a Waveshare 11.6inch Capacitive Touch Screen.
  • PCA9685 PWM Driver with power supply for controlling the servo motor.
  • Continuous Rotation Servo Motor we will use a SpringRC SM-S4303R.
  • Jumper wires for connections.
  • Breadboard (optional, for easier wiring).
  • Your Flutter Development Machine we will be using a Macbook Pro, with Windows everything will also work, but some minor steps will differ.
  • SD-Card Reader we will be using the built in card reader of the Macbook.
  • Network with internet access for your Flutter Development Machine and the Raspberry Pi.

Software on your Flutter Development Machine:

  • The newest Flutter SDK(stable) installed on your Flutter Development Machine.
  • The newest version of Visual Studio Code with the extensions for Flutter, Dart and Remote — SSH
  • A SSH client (e.g., PuTTY on Windows, Terminal on macOS/Linux).

Step 1: Download & Install Raspberry Pi Imager

The Raspberry Pi is a powerful, compact computer, about the size of a credit card, that can run a complete operating system.

In this workshop, we’ll be installing a Linux-based operating system specifically designed for the Raspberry Pi, called Raspberry Pi OS. This OS will provide the foundation for all the projects you’ll be building.

To install Raspberry Pi OS, we’ll use a tool called Raspberry Pi Imager. This simple application allows you to easily download and install the OS onto your Raspberry Pi’s SD card.

Download the Raspberry Pi Imager from the official website: Raspberry Pi Imager(https://www.raspberrypi.com/software/) and install it on your Flutter Development Machine following the instructions for your operating system.

Step 2: Install Raspberry Pi OS

Connect your SD card, open the Raspberry Pi Imager and choose Raspberry Pi OS (64-bit) under the “Operating System” menu.

Choose your SD-Card from the Menu and click on next and then on “EDIT SETTINGS” to configure advanced settings:

  • Enable SSH (with password authentication) under the SERVICES tab.
  • Set up a unique hostname for your device we will use “raspi5–1234.local” (Important! You will need that hostname in the next step, so make sure you remember it.)
  • Set up Wi-Fi (if you’re not using Ethernet).
  • Set the Username and Password. (Important! You need to remember the username you set here as you will need it later in the setup process, if this is your first time we recommend leaving it with “pi”.)

Next write the OS to the SD card and wait for the process to complete, after that you can insert the SD card into your Raspberry Pi.

Step 3: Connect the Servo, Touchscreen and All Cables to the Raspberry Pi

Connect the PCA9685 to your Raspberry Pi via the I2C pins:

  • VCC to 3.3V.
  • GND to GND.
  • SDA to SDA (GPIO 2).
  • SCL to SCL (GPIO 3).

Connect the servo motor to PWM channel 0 on the PCA9685 and connect the Raspberry Pi to the touchscreen and power supply.

Made by “Kattni Rembor” under the CC BY-SA 3.0 License

Step 4: Power Up the Raspberry Pi

Power up your Raspberry Pi and wait until it is connected to your network.

Next we have to find out the IP-Adress of our device. To find out the IP-Adress of our Raspberry Pi we will ping it over its host name from our Flutter Development Machine. (For this step the devices have to be in the same network.)

ping raspi5–1234.local

If everything works as intended you will see a successful ping and the IP of your Pi. If it is not working please check your network and firewall settings.

PING raspi5-1234.local (192.168.188.21): 56 data bytes
64 bytes from 192.168.188.21: icmp_seq=0 ttl=64 time=25.663 ms
64 bytes from 192.168.188.21: icmp_seq=1 ttl=64 time=6.674 ms
64 bytes from 192.168.188.21: icmp_seq=2 ttl=64 time=22.191 ms
64 bytes from 192.168.188.21: icmp_seq=3 ttl=64 time=16.082 msping answer...

(Alternatively you could visit the web-interface of your router and check what IP the raspi got or you can plug in a mouse/keyboard into the Raspberry Pi and check it on the device.)

Step 5: Install snapp_cli

Before we dive into installing snapp_cli, let’s take a moment to understand why it’s so essential. Without snapp_cli, developing and deploying apps on the Raspberry Pi can be a tedious and time-consuming process. You’d have to manually install all the necessary components, set up the development environment, configure the IDE, and then start developing and running your app. This can be overwhelming and prone to errors.

With snapp_cli, however, everything is set up for you from scratch. It automatically installs all required components, configures the environment, and even sets up remote debugging, allowing you to focus on writing your code rather than worrying about the setup. This tool streamlines the entire process, making it faster and more efficient to develop apps on your Raspberry Pi.

Now first visit the snapp_cli page with your Flutter Development Machine on pub.dev and install it by running

dart pub global activate snapp_cli

from your terminal.

Step 6: Use snapp_cli to Bootstrap Flutter-Pi on the Raspberry Pi

On your Flutter Development Machine run:

snapp_cli bootstrap
  • Choose Express Mode
  • Choose the Device you are using, in our case: Raspberry Pi 5
  • Enter the device IP as received earlier, in our case: 192.168.188.21
  • Enter the username for the Raspberry-Pi as defined earlier, in our case: pi
  • Choose to create a ssh connection and enter the login-password for pi when prompted
  • Choose flutter-pi
  • Choose to install flutter-pi on the device for you
  • Get a coffee.. Depending on your internet connection this will take 5 to 15 minutes, snapp_cli will install and set up flutter-pi on your device for you including all needed dependencies. After the installation you will have to reboot to apply last changes and restart the Raspberry-Pi in CLI-Mode (needed for flutter-pi)

To reboot the device first ssh into it(replace the IP with the IP of your device) and then enter the reboot command.

ssh pi@192.168.188.21
sudo reboot

Additional Info: If your display needs to be rotated manually follow the instructions under: https://www.raspberrypi.com/documentation/computers/configuration.html#kernel-command-line-cmdline-txt to do so.

If you run into any troubles while installing flutter-pi via. snapp_cli it is often times useful to delete all ssh entries related to the Raspberry Pi on your system, to d0 so run(replace the IP with the IP of your device):

ssh-keygen -R 192.168.188.21

Step 7: Enable I2C Bus and Install all Dependencies on the Pi

In this step, we’ll be enabling the I2C bus and installing the necessary dependencies on your Raspberry Pi. But first, let’s break down what these terms mean and why they’re important.

What is I2C and Why Are We Using It?

I2C (Inter-Integrated Circuit) is a communication protocol that allows multiple devices, such as sensors, displays, and controllers, to communicate with your Raspberry Pi using just two wires — one for data (SDA) and one for clock (SCL). This makes I2C ideal for connecting various peripherals without needing a lot of pins, making it a versatile and efficient option for projects like controlling servos.

In our case, we’ll be using I2C to communicate with the PCA9685 controller, which will manage the servo motors. By enabling I2C on your Raspberry Pi, you allow it to interface with the PCA9685 and other I2C-compatible devices, enabling precise control over the connected components.

What Are the Dependencies and Why Are We Using Them?

To control the servo motors through the Raspberry Pi using the PCA9685 controller, we need specific Python libraries. These libraries provide the necessary tools and functions to communicate with and control the hardware.

First, we, again, need to ssh onto our Pi(replace the IP with the IP of your device)

ssh pi@192.168.188.21

Then enable the I2C Bus on the Pi via

sudo raspi-config nonint do_i2c 0

After that install all dependencies on the Pi we will need. (We don’t have to install Python3 as it is pre-installed on Raspberry Pi OS)

sudo pip install adafruit-circuitpython-pca9685 --break-system-packages
sudo pip install adafruit-circuitpython-servokit --break-system-packages

Step 8: Create a Python Script that Rotates the Servo for 3Seconds

(Important: For this step you will need the extension “Remote — SSH” installed in your VS-Code, if you haven’t done this yet, install it now)

Next we will create a python script that will drive the servo in one direction for 3 seconds.

To do so we create new file on the Raspberry Pi (again we will do this via. ssh)

mkdir -p ~/dev/python-be && touch ~/dev/python-be/turn_motor_script.py

and open that file afterwards with VS-Code and Remote-SSH from our Flutter Development Machine.

To do so we open VS-Code and first have to add the Pi to SSH Remotes by opening the Remote Explorer in VS-Code and clicking on the “+” to add a new remote Host.

Next we have to enter the command to ssh into our Pi, it is the same we also use in our terminal:

ssh pi@192.168.188.21

Now we refresh our remotes and we will be able to connect to our Raspberry Pi and open the Python file we created earlier trough the VS-Code file explorer.

(If you have any problems using the SSH-Extension it may be worth following this recommendation here: https://github.com/microsoft/vscode-remote-release/issues/9561#issuecomment-2017833353 and installing v0.107.1 of the Remote — SSH extension.)

In this file we will now first import all the dependencies we will need

import time  # Import time module to introduce delays in the program
import busio # Import busio module to set up I2C communication
from adafruit_motor import servo # Import servo module to control the servo motor
from adafruit_pca9685 import PCA9685 # Import PCA9685 module to interface with the PCA9685 PWM controller
from board import SCL, SDA # Import specific pins SCL and SDA for I2C communication

Followed by a simple python script which will initialize the I2C connection, the PCA9685 and the servo and then drive it for 3 seconds in one direction.

# Initialize I2C communication on the Raspberry Pi using the SCL and SDA pins.
i2c = busio.I2C(SCL, SDA)

# Create an instance of the PCA9685 class using the I2C communication.
# This instance will be used to control PWM signals, which are typically used for controlling servos.
pca = PCA9685(i2c)

# Set the PWM frequency to 50Hz, which is the standard frequency for controlling servo motors.
pca.frequency = 50

# Create a ContinuousServo object on channel 0 of the PCA9685.
# A continuous servo can rotate indefinitely in either direction based on the throttle value.
servo0 = servo.ContinuousServo(pca.channels[0])

# Set the throttle to full speed forward (1.0).
# Throttle value can range from -1.0 (full speed reverse) to 1.0 (full speed forward).
servo0.throttle = 1.0

# Keep the servo running at full speed for 3 seconds.
time.sleep(3)

# Stop the servo by setting the throttle to 0.0 (no movement).
servo0.throttle = 0.0

# Briefly sleep for 0.1 seconds to ensure the stop command is fully processed.
time.sleep(0.1)

# Deinitialize the PCA9685 to free up resources and reset the hardware.
pca.deinit()

Now we can save the file go back into our terminal and run the python script with

python3 ~/dev/python-be/turn_motor_script.py

You can find the finished file also under: https://github.com/Snapp-X/workshop_flutterfriends/blob/main/python_motor_controller_script/turn_motor_script.py

Step 9: Create a Python Backend that Connects to D-Bus and Drives the Servo

In this step, we’ll explore how to communicate with our servo motor to control it from our Flutter app. There are several methods we could use, such as setting up a Python backend or using MQTT for messaging. However, we’ve chosen to use D-Bus because it’s already present on most Linux-based systems, including Raspberry Pi OS, making it a convenient and efficient choice.

What is D-Bus?

D-Bus (Desktop Bus) is an inter-process communication (IPC) system that allows different programs to communicate with each other. It provides a simple way for software components to interact, even if they are written in different programming languages or run in different processes. D-Bus is widely used in Linux-based systems to allow various system services and applications to communicate seamlessly.

D-Bus Methodological picture

By using D-Bus, you can set up a communication channel between your Flutter app and the Raspberry Pi, enabling your app to control the servo motors remotely.

We’ll start by creating a Python server that sets up a D-Bus session. This server will listen for commands sent from the Flutter app and then drive the servos using the libraries we installed earlier (adafruit-circuitpython-pca9685 and adafruit-circuitpython-servokit). This server will act as the middleman, translating the app’s instructions into precise movements of the servo motors.

D-Bus Architecture Dart to Python

First we will create another file on the Pi to host our Python-Backend from our terminal, we do this by

touch ~/dev/python-be/servo_controller.py

and open that file in VS-Code over the ssh extension once again.

In the head of the file we import all the dependencies we will need

from __future__ import print_function
from datetime import datetime, timezone
from gi.repository import GLib
from board import SCL, SDA

import dbus
import dbus.service
import dbus.mainloop.glib
import time
import busio

from adafruit_motor import servo
from adafruit_servokit import ServoKit
from adafruit_pca9685 import PCA9685

And next we define a message that gets printed on the terminal when the server is started.

usage = """
ServoController DBus Service Started Successfully

Now we just need to wait for the client to call our methods :)
"""

Now we can define the actual ServoController as a dbusService. First we define the __init__ function in which we initialize the I2C connection and the servo, just like in our example before. Followed by the ThrottleMotor method trough which we open up a a method to control the servo with two variables, duration and throttle. Last but not least we define a exit function trough which the connections we opened up can be closed.

class ServoController(dbus.service.Object):
""" Python class to control the servo motors using PCA9685"""
def __init__(self, bus, path):
super(ServoController, self).__init__(bus, path)

self.i2c = busio.I2C(SCL, SDA)

self.pca = PCA9685(self.i2c)
self.pca.frequency = 50

self.servo = servo.ContinuousServo(self.pca.channels[0])

@dbus.service.method("de.snapp.ServoControllerInterface",
in_signature='dd', out_signature='b')
def ThrottleMotor(self, duration, throttle):
try:
print("ThrottleMotor request:", session_bus.get_unique_name())
print("Duration:", duration)
print("Throttle:", throttle)

# Set the throttle
self.servo.throttle = throttle

# Sleep for the specified duration
time.sleep(duration)

# Stop the motor
self.servo.throttle = 0.0

# Briefly sleep to ensure the stop command is processed
time.sleep(0.1)

return True
except Exception as e:
# Log the exception
print(f"Error in ThrottleMotor: {e}")

# Return False to indicate failure
return False

@dbus.service.method("de.snapp.ServoControllerInterface",
in_signature='', out_signature='')
def Exit(self):
self.pca.deinit()
mainloop.quit()

The only thing that is missing now is a main-function in which we start the dbus interface:

if __name__ == '__main__':
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

session_bus = dbus.SessionBus()
name = dbus.service.BusName("de.snapp.ServoControllerService", session_bus)
controller = ServoController(session_bus, '/ServoController')

# Run the main loop
mainloop = GLib.MainLoop()
print(usage)

mainloop.run()

And finally to start our service by:

ssh pi@192.168.188.21
python3 ~/dev/python-be/servo_controller.py

The full file can be found here: https://github.com/Snapp-X/workshop_flutterfriends/blob/main/python_dbus_server/servo_controller.py

Step 10: Controlling the Servo from our Flutter App

Now it’s time for the final step, to connect the Servo with our Flutter App. To do that we will first create a new Flutter app on your local machine and open it in Visual Studio Code.

If we do this via. the terminal this means

flutter create servo_app
cd servo_app
code .

Inside our Flutter App we will now first create the part which will connect our application with the dbus. Therefore our first action is to add dbus as a dependency to our pubspec.yaml.

Next we create a new file called dbus_repository.dart in our root lib folder

In this file we now first create a class we will call DBusDataSource trough which we define all the important specs to connect to our dbus.

import 'package:dbus/dbus.dart';

class DBusDataSource {
const DBusDataSource({required this.client, required this.remoteObject});

final DBusClient client;
final DBusRemoteObject remoteObject;

factory DBusDataSource.session() {
final dBusClient = DBusClient.session();

return DBusDataSource(
client: dBusClient,
remoteObject: DBusRemoteObject(
dBusClient,
name: 'de.snapp.ServoControllerService',
path: DBusObjectPath('/ServoController'),
),
);
}

/// The motor will be throttled for the specified duration
/// the [duration] is the duration in seconds
///
/// returns a [bool] indicating if the operation was successful
Future<DBusMethodSuccessResponse> throttleMotor(
double duration,
double speed,
) async {
final response = await remoteObject.callMethod(
'de.snapp.ServoControllerInterface',
'ThrottleMotor',
[
DBusDouble(duration),
DBusDouble(speed),
],
replySignature: DBusSignature('b'),
);

return response;
}

/// Close the DBus python session
Future<DBusMethodSuccessResponse> closeDBusSession() async {
final response = await remoteObject.callMethod(
'de.snapp.ServoControllerInterface',
'Exit',
[],
);

return response;
}
}

And in the next step we create a DBusRepository from which we will be able to interact with the dbus from our Flutter application


class DBusRepository {
const DBusRepository({required this.dataSource});

final DBusDataSource dataSource;

/// The motor will be throttled for the specified duration
/// the [duration] is the duration in seconds
///
/// returns a [bool] indicating if the operation was successful
Future<bool> throttleMotor(
{double duration = 0.5, double speed = 0.5}) async {
final response = await dataSource.throttleMotor(duration, speed);

final returnValue = response.returnValues[0];

final parsedResult = returnValue.toNative();

if (parsedResult is! bool) {
throw Exception('Invalid response type');
}

return parsedResult;
}
}

dbus_repository.dart on Github

The only thing left now is to rewrite our main.dart file to something more suitable for our needs like:

import 'package:flutter/material.dart';
import 'package:servo_app/dbus_repository.dart';

final dbusRepository = DBusRepository(dataSource: DBusDataSource.session());

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const Scaffold(
body: Center(
child: ElevatedButton(
onPressed: null, child: Text('Turn the Servo!')))),
);
}
}

So the only thing we have to do in here now is to wire the Elevated Button with the throttleMotor function of our DBusRepository.

onPressed: () {
dbusRepository.throttleMotor(duration: 1, speed: 0.5);
},

So in the end our main.dart looks like this

import 'package:flutter/material.dart';
import 'package:workshop_flutterfriends/dbus_repository.dart';

final dbusRepository = DBusRepository(dataSource: DBusDataSource.session());

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
dbusRepository.throttleMotor(duration: 1, speed: 0.5);
},
child: const Text('Turn the Servo!'),
),
),
),
);
}
}

main.dart on Github

Now all we have to do is to start the Flutter Application via remote debugging. All the setup was already done by the installation trough snapp_cli so all we have to do is to select the Pi as target debugging device and hit run.

Just start your app in debug mode, and… Congratulations, you have successfully created a Flutter app to control a servo! Just press the button and voila, the servo will rotate!

Now as this is most likely your first Flutter app running remotely on a Raspberry Pi using flutter-pi: You can use Devtools, Hot-Reload and everything else just how you know it! (Except the app won’t terminate when you stop the debugging, we are working on that…)

--

--

Moritz Theis
Snapp X
Editor for

Flutter Dev, Founder & CEO @ Snapp X, Founder @ Snapp Embedded, Co-Organizer of Flutter Munich