Electronics Software Development

Controlling A Raspberry Pi From A Web Browser With Vapor 3

Raspberry Pi Web
Written by John Woolsey


In this tutorial you will learn how to access and control a Raspberry Pi development board from a web browser. I will show you how to control 3 LEDs, but this implementation can be easily extended to access and control much more. A basic understanding of electronics and programming is expected along with some familiarity with the Raspberry Pi platform. We will be using the Swift programming language along with the Vapor 3 server-side library for our implementation. If you are new to Swift, Vapor, or the Raspberry Pi platform or would just like to refresh your knowledge, check out our Hello World: Writing Your First Swift Program, Writing A Server-Side Program In Swift Using Vapor, and Blink: Making An LED Blink On A Raspberry Pi tutorials first before proceeding with this one. All resources used in this tutorial are available on GitHub for your reference.

What Is Needed

Background Information

Although the Swift language and the Vapor 3 library are not quite production ready on the Raspberry Pi, they are getting very close. For this reason, we will have to do a little finagling to get our code to compile and run properly. If you don’t yet have Swift installed on your system, check out the Swift 4.1.2 Updated For RaspberryPi 2/3 or A Big Update on Swift 4.1.2 For Raspberry Pi Zero/1/2/3 articles for installation information.

My development board is the Raspberry Pi 3 Model B running the Raspbian (November 2018 release) operating system. My version of Swift is 4.1.2. If you are using a different model or a different OS that is similar to Raspbian, the vast majority of this tutorial should still apply, however, some minor changes may be necessary.

If you need help with your specific setup, post a question in the comments section below and someone can try to help you.

Building The Circuit

Before connecting anything to your Raspberry Pi, shutdown and disconnect the Raspberry Pi from power. This avoids accidental damage during wiring. Build the circuit shown below to connect the LEDs to the Raspberry Pi.

Raspberry Pi LEDs Schematic

Attach the red LED to the breadboard by connecting the anode of the LED to one of the breadboard’s terminal strips and the cathode to the ground bus strip. The anode (positive terminal) of an LED is longer than the cathode (negative terminal). Connect one end of a 330 Ω resistor to the terminal strip containing the red LED’s anode and the other end of the resistor to an adjacent terminal strip.

Follow the same process as above for wiring the yellow and green LEDs as well.

Attach the male end of a jumper wire to the open end of the resistor connected to the red LED. Attach the other (female) end to GPIO21 (physical pin 40) on the Raspberry Pi header. In a similar fashion, connect jumper wires for the yellow and green LEDs to pins GPIO20 (38) and GPIO16 (36) respectively. Also attach a jumper wire from the ground bus strip on the breadboard to one of the ground (GND) pins on the Raspberry Pi header.

Once the circuit is completed, it should look like the photo below.

Raspberry Pi LEDs

Raspberry Pi LEDs setup with attached GPIO Breadboard Interface Board

You can now connect power to your Raspberry Pi and boot it up.

Setting Up The Vapor Server

If you haven’t done so already, this is the time to make sure that your Raspberry Pi is already configured to access your local network.

As stated previously, Swift and Vapor are are not quite ready for primetime. For instance, the Vapor Toolbox helper application, used for easily creating and managing Vapor based projects, is not yet available when running Raspbian on the Raspberry Pi. At least I was not able to figure out how to install it. I even tried to compile it locally, but it requires Swift 4.2, which itself is not yet available. Since we are not able to utilize the Toolbox just yet, we will have to add the boilerplate code ourselves, do some of the operations manually, and even “fix” a line of code in one of the sub-libraries used by Vapor.

Let’s now get Vapor up and running.

Create a new project directory named VaporLEDs and go into that directory.

% mkdir VaporLEDs
% cd VaporLEDs

Create a new Swift based project with the following Swift Package Manager command.

% swift package init --type executable

We now need to add the libraries we will use in this project as dependencies. Vapor is a server-side library, Leaf is an HTML templating library, and SwiftyGPIO is a library for controlling the GPIOs on the Raspberry Pi. Edit the Package.swift file and add the following dependencies to the package

.package(url: "https://github.com/vapor/vapor.git", .exact("3.1.0")),
.package(url: "https://github.com/vapor/leaf.git", .exact("3.0.2")),
.package(url: "https://github.com/uraimo/SwiftyGPIO.git", .exact("1.1.0")),

and the following to the VaporLEDs target.

dependencies: ["Vapor","Leaf","SwiftyGPIO"]),

The Package.swift file should now look like the following:

// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "VaporLEDs",
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/vapor/vapor.git", .exact("3.1.0")),
        .package(url: "https://github.com/vapor/leaf.git", .exact("3.0.2")),
        .package(url: "https://github.com/uraimo/SwiftyGPIO.git", .exact("1.1.0")),
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
            name: "VaporLEDs",
            dependencies: ["Vapor","Leaf","SwiftyGPIO"]),

Once your Package.swift file has been updated and saved, update (load) all of the packages (libraries) with the following command.

% swift package update

Next, we are going to utilize the Leaf templating language and create an HTML template for our home page. By default, the Leaf library looks for templates within the Resources/Views directory, so we need to create it first.

% mkdir Resources
% mkdir Resources/Views

Then create a new file named base.leaf within that directory with the following contents.

<!DOCTYPE html>

This file contains the base structure for an HTML file and will be used as the foundation for or any subsequent templates we create. The #get() construct allows us to pass data into the template through variables, title and body in this case.

Now create a new file named home.leaf, within the same directory as base.leaf, with the following contents.

#set("title") { Hello, from Raspberry Pi! }

#set("body") {
   Hello, from Raspberry Pi!


Here, we are embedding the base.leaf template, using the #embed() construct, and passing in the title and body variables, using the #set() construct. Once we have our server implemented, we will see the Raspberry Pi saying hello to us.

Now that our templates are set up, it is time to create our program. Replace the contents of the Sources/VaporLEDs/main.swift file with the following minimal Vapor based program.

import Leaf
import Vapor

// Vapor configuration
var config = Config.default()
config.prefer(LeafRenderer.self, for: ViewRenderer.self)
var environment = try Environment.detect()
var services = Services.default()
try services.register(LeafProvider())

// Routes
let router = EngineRouter.default()
router.get { req -> Future<View> in  // home page
   return try req.view().render("home")
services.register(router, as: Router.self)

// Main section
print("Press CTRL-C to exit.")
try Application(config: config, environment: environment, services: services).run()

Build (compile) the project with the following Swift Package Manager command. Make sure you are in the top level project directory (VaporLEDs) before running any Swift Package Manager commands or you may get erroneous results.

% swift build

The compile will take a little while, and once finished, we should see the following warning and error displayed.

OpenSSLHandler.swift:270:35: warning: 'changeCapacity(to:)' is deprecated: changeCapacity has been replaced by reserveCapacity
        receiveBuffer.changeCapacity(to: receiveBuffer.capacity + 1024)
SSLContext.swift:315:90: error: member 'init' in 'UnsafeMutableRawPointer!' produces result of type 'ImplicitlyUnwrappedOptional<Wrapped>', but context expects 'UnsafeMutableRawPointer!'
        guard 1 == SSL_CTX_ctrl(.make(optional: context), SSL_CTRL_EXTRA_CHAIN_CERT, 0, .init(X509_dup(.make(optional: cert.ref)))) else {

We can ignore the warning, but we need to fix the error so that our project will successfully compile. Normally, I would strongly discourage editing any of the source package files directly, but in this case we need to fix the error in order to proceed. The error is stating that the init member is expecting a result type of UnsafeMutableRawPointer!, but a type of ImplicitlyUnwrappedOptional<Wrapped> is received instead. Luckily we can fix this fairly easily by casting the result to the expected type.

Edit the SSLContext.swift file using the full path shown in the compilation output and replace the following on line 315

guard 1 == SSL_CTX_ctrl(.make(optional: context), SSL_CTRL_EXTRA_CHAIN_CERT, 0, .init(X509_dup(.make(optional: cert.ref)))) else {


guard 1 == SSL_CTX_ctrl(.make(optional: context), SSL_CTRL_EXTRA_CHAIN_CERT, 0, .init(UnsafeMutableRawPointer(X509_dup(.make(optional: cert.ref))))) else {

Run the build process again and it should compile successfully with only the warning this time.

% swift build

I don’t know about you, but I am ready to see this thing working. Run the following command to start the web server

% swift run VaporLEDs --hostname raspberrypi.local --port 8080

and it should display the following message. It will take a moment to start.

Server starting on http://raspberrypi.local:8080

Now comes the exciting part. Open your web browser, enter the http://raspberrypi.local:8080 URL, and voila, the Raspberry Pi is saying hello.

Hello, from Raspberry Pi!

We are now using Swift and Vapor to serve web pages from the Raspberry Pi! How cool is that? It’s amazing how we can get excited about some of the most simple things. When you are ready to exit the program, press CTRL-C to terminate it.

Integrating An LED Into The Program

Now let’s control an LED using our new website.

The first thing we need to do in incorporate the SwiftyGPIO library to access the onboard GPIO of the Raspberry Pi. Edit the main.swift file again and import the library just below the Leaf and Vapor imports at the beginning of the file.

import SwiftyGPIO

Add the SwiftyGPIO configuration, shown below, just after the Vapor configuration section.

// SwiftyGPIO configuration
let gpio = SwiftyGPIO.GPIOs(for: .RaspberryPi3)
guard let redLED = gpio[.P21] else {
   fatalError("Could not initialize redLED.")
redLED.direction = .OUT

Here, we are enabling GPIO control for the Raspberry Pi Rev3 and initializing access to the red LED on pin GPIO21.

Now add a new route below the home page route (router.get …) and before registering the router (services.register …), as shown below.

router.get("redLED", String.parameter) { req -> String in  // redLED
   let command = try req.parameters.next(String.self)
   switch command {
   case "on":
      redLED.value = 1
      redLED.value = 0
   return "redLED: \(redLED.value == 1 ? "On" : "Off")"

This route lets us send the URL commands /redLED/on and /redLED/off to turn on and off the red LED. It also returns the status of the red LED as determined by the Raspberry Pi for us to verify it is working as planned.

Once you have saved your main.swift file, let’s give it a try. Again from the top level project directory, build and run the program.

% swift build
% swift run VaporLEDs --hostname raspberrypi.local --port 8080

Enter the http://raspberrypi.local:8080/redLED/on and http://raspberrypi.local:8080/redLED/off URLs in your web browser. If all is working properly, you should not only see redLED: On and redLED: Off displayed on the web page, but you should also see the red LED on the board turning on and off too.

Getting even more exciting. Exit the program (CTRL-C) when finished.

Adding The Other LEDs

Now let’s add the rest of the LEDs.

Add the yellowLED and greenLED to the SwiftyGPIO configuration section, in main.swift, in the same fashion as we did for the redLED.

guard let yellowLED = gpio[.P20] else {
   fatalError("Could not initialize yellowLED.")
guard let greenLED = gpio[.P16] else {
   fatalError("Could not initialize greenLED.")
yellowLED.direction = .OUT
greenLED.direction = .OUT

And of course we need to add the routes as well.

router.get("yellowLED", String.parameter) { req -> String in  // yellowLED
   let command = try req.parameters.next(String.self)
   switch command {
   case "on":
      yellowLED.value = 1
      yellowLED.value = 0
   return "yellowLED: \(yellowLED.value == 1 ? "On" : "Off")"
router.get("greenLED", String.parameter) { req -> String in  // greenLED
   let command = try req.parameters.next(String.self)
   switch command {
   case "on":
      greenLED.value = 1
      greenLED.value = 0
   return "greenLED: \(greenLED.value == 1 ? "On" : "Off")"

Save, build, and run the program again. Test out the new http://raspberrypi.local:8080/yellowLED/on, http://raspberrypi.local:8080/yellowLED/off, http://raspberrypi.local:8080/greenLED/on, and http://raspberrypi.local:8080/greenLED/off routes to make sure they are working. Exit the program (CTRL-C) when finished.

We have made good progress. We are now able to turn on and off three LEDs from a web browser. That is all well and good, but I think we can do better.

Bringing It All Together

Wouldn’t it be nice if we had a single page showing us the status and allowing us to control all of the LEDs? I think so, so let’s make it happen.

Replace the contents of the home page Leaf template (Resources/Views/home.leaf) with the code shown below to add a nice looking table for this capability. The first column provides the component names: Red, Yellow, and Green LEDs. The second column provides the status of each LED: ON (in green color) or OFF (in red color). The third, and last, column provides us the ability to turn ON or OFF each LED.

#set("title") { Raspberry Pi Remote Control }

#set("body") {
   <h1>Raspberry Pi Remote Control</h1>
   <table border=1 style='text-align:center'>
      <td>Red LED</td>
      <td><font style='color:#(redLEDColor);'>#(redLEDState)</font></td>
      <td><a href='/redLED/on'>ON</a> / <a href='/redLED/off'>OFF</a></td>
      <td>Yellow LED</td>
      <td><font style='color:#(yellowLEDColor);'>#(yellowLEDState)</font></td>
      <td><a href='/yellowLED/on'>ON</a> / <a href='/yellowLED/off'>OFF</a></td>
      <td>Green LED</td>
      <td><font style='color:#(greenLEDColor);'>#(greenLEDState)</font></td>
      <td><a href='/greenLED/on'>ON</a> / <a href='/greenLED/off'>OFF</a></td>


Don’t forget to save the file when you are done. The /redLED/on, /redLED/off, etc. routes we used to control the LEDs still leave us on their own individual status web pages. Let’s instead make them redirect back to the home page for a cleaner user interface. To do this we need to change the return types from String to Response for the routes that control the LEDs along with adding the actual redirects themselves. For the redLED, this entails editing the main.swift file again and replacing the lines

router.get("redLED", String.parameter) { req -> String in  // redLED


router.get("redLED", String.parameter) { req -> Response in  // redLED

at the beginning of the route and

return "redLED: \(redLED.value == 1 ? "On" : "Off")"


return req.redirect(to: "/")

at the end of the route. Then do the same for the yellow and green LED routes as well.

Once you’re done making edits, save, build, and run the program again. Open the http://raspberrypi.local:8080 URL to view and test the new format. Looking quite nice isn’t it?

Vapor LEDs Webpage

Everything is looking pretty good, but we are not quite done yet. There is still one little problem that I would be remiss if I didn’t share and explain how to resolve it.

We have been exiting the program (CTRL-C) with the Raspberry Pi GPIO pins left in the same state as was used by the program. This could lead to potential issues with other programs or circuitry. We should be good citizens and reset the utilized pins back to their default states upon exiting.

Most programs run through various paths of the code and eventually exit once the main function has completed. We would, in that case, just put our GPIO cleanup code at the end of the main function. In this case, we find ourselves in an endless loop when using the Vapor library, hence, we have to use CTRL-C to exit the program. To solve this, we need to incorporate a signal interrupt handler that detects the CTRL-C operation and gives us the path to a clean exit.

Let’s first create a function, named resetGPIO(), that handles all of our GPIO cleanup duties. Then we need to create the signal interrupt handler and call our resetGPIO() function from within it. Add the following code just below the SwiftyGPIO configuration section and above the Routes section.

// Reset GPIO pins back to default states
func resetGPIO() {
   gpio[.P16]?.direction = .IN
   gpio[.P20]?.direction = .IN
   gpio[.P21]?.direction = .IN
   print("\nCompleted cleanup of GPIO resources.")

// Signal interrupt handler
signal(SIGINT) { signal in

Now when we exit the program, the pins will go back to their default states. Give it a shot; save, build, and run the program again. If one or more of the LEDs are turned on when exiting, you should now see all of them turn off.


In this tutorial we learned how to view the status and control three LEDs on a Raspberry Pi development board from a web browser. Although this may be a simple example, this approach can be extended to control a wide variety of operations and/or components. The endpoints (routes) used here can even be used within scripts, smartphone applications, etc. for even more functionality.

The complete project and the schematic are available on GitHub for your reference.

Thank you for joining me in this journey and I hope you enjoyed the experience. Please feel free to share your thoughts in the comments section below.

About the author

John Woolsey

John is an electrical engineer who loves science, math, and technology and teaching it to others even more.
He knew he wanted to work with electronics from an early age, building his first robot when he was in 8th grade. His first computer was a Timex/Sinclair 2068 followed by the Tandy 1000 TL (aka really old stuff).
He put himself through college (The University of Texas at Austin) by working at Motorola where he worked for many years afterward in the Semiconductor Products Sector in Research and Development.
John started developing mobile app software in 2010 for himself and for other companies. He has also taught programming to kids for summer school and enjoyed years of judging kids science projects at the Austin Energy Regional Science Festival.
Electronics, software, and teaching all culminate in his new venture to learn, make, and teach others via the Woolsey Workshop website.


  • I was stumbling through the exact same process (after giving up on Kitura because it was segfaulting), and found your post as the only place on the web that had this exact phrase:

    SSLContext.swift:315:90: error: member ‘init’ in ‘UnsafeMutableRawPointer!’ produces result of type ‘ImplicitlyUnwrappedOptional’, but context expects ‘UnsafeMutableRawPointer!’

    So thanks for posting this and posting it!

    While I’m temporarily fine with the hack of casting it to get it to compile, I’m wondering what the proper followup action is. Does swift-nio-ssl need to be updated for swift 4.1.1?

    • I really did not like putting in that hack as it can set the wrong precedence, but I thought it was at least worth sharing. I am glad you found the article useful. As for the proper follow up action, I believe we should just wait for newer versions of the dependency chain (Swift, Vapor, SwiftNIO). Perhaps by that time, we might even have a working Vapor Toolbox.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.