ESP8266: Custom iOS App for LED Control

ESP8266: Custom iOS App for LED Control

by ACROBOTIC Industries


Time to complete: 10–15min; Level of difficulty: Beginner

This guide will show you how to program an ESP8266 Serial to Wi-Fi module to run a web application that will control two LEDs in response to requests over http. Then, we’ll use Xcode and Swift 2 to build a basic iOS App that will allow us to control the state of the LEDs.

List of Materials

  • 1 x ESP8266 (ESP-12E) Development Board
  • 1 x 5mm Color LED Set
  • 1 x Jumper Wires Male/Male - 10-pack
  • 1 x Half-Sized Solderless Breadboard

Step-by-step Video

We've made a video following the process described in this tutorial, if that's your preferred media for tutorials go right ahead:

Simple iOS App For Controlling The ESP8266!

Obtaining the Code

For this activity, we'll be loading a program to our ESP8266 Development Board, it's available in our Github repository:

We can open a new window in the Arduino environment and copy-paste the firmware.  We'll of course need to setup the Arduino IDE to program the ESP8266.  We'll also need to have Xcode 7 running on our system.

Wiring: ESP8266 LED Control

The wiring for this tutorial is fairly straight-forward.  The two anodes (+) of the LEDs shoud be connected to pins D1 and D2 on our ESP8266 Development Board, and the cathodes should be connected to a current-limiting resistor (330Ohm) and the resistor, in turn, should be connected to ground.

Wiring Diagram for iOS Wireless LED Control with ESP8266

Walkthrough: ESP8266 LED Control (firmware)

The firmware on the ESP8266 needs to do two things, control the low-level hardware (LEDs) and respond to requests sent to it over http.  Fortunately this can be accomplished very easily using the built-in libraries for the module.  These libraries will allow us to 1) run a webserver that will listen to http requests on a specific port (80), and 2) build a web application with an API (a set of URL paths) to execute control commands requested by a client.

As with most programs, we start our application code by including a few libraries and defining constants and variables that we'll be using later in our program.  Remember to change the values for the constants ssid and password so that they match your own network settings!


// Define the ID and password of your Wi-Fi network
const char* ssid = "Network ID";
const char* password = "Password";

// Instantiate the ESP8266WebServer class, passing the argument 80 for the
// port that the server will be listening.
ESP8266WebServer server(80);

// Define the variables with the pin values where the LEDs are connected
const int led1_pin = D1;
const int led2_pin = D2;

The next section of our program includes the definitions for the functions that will run when users access specific URLs of our web application.  Our minimalistic web application will only respond to the following http requests:

  • GET /
  • GET /setleds

Everything other request will return a 404 (Not Found) error, which we also define in one of our functions.  The first function handles all GET requests to our ESP8266 root-level path ('/').  This function simply returns a string describing how to use our web application using the send() method defined in the ESP8266WebServer class:

// User-defined function that will be called when a client accesses the root
// directory path of the ESP8266 host
void handleRoot() {
  // Simply sends an 'OK' (200) response to the client, and a plain text
  // string with usage.
  server.send(200, "text/plain", String("Hello from esp8266! Usage: navigate to") +
              String(" /setleds?led1=XX&led2=YY changing XX/YY to ON or OFF."));

The next function is the main one of our web application.  It handles the requests that attempt to change the status of the LEDs.  These requests will be hadnled throuth the path /setleds and the function expects that the requests include parameters specifying the desired state of the LEDs.  These parameters are appended to the path using the standard URL syntax ?parameter1=value1¶meter2=value2&...  We can access the parameter values with the arg() method of the ESP8266WebServer class.  Based on the values (ON or OFF) defined for the parameters we expect (led1 and led2), we use digitalWrite() to change the status of the LEDs.

// User-defined function that will be called when a client accesses the /setleds
// path of the ESP8266 host
void handleSetLeds() {
  // We can read the desired status of the LEDs from the expected parameters that
  // should be passed in the URL.  We expect two parameters "led1" and "led2".
  String led1_status = server.arg("led1");
  String led2_status = server.arg("led2");
  // Check if the URL include a change of the LED status
  bool url_check = false;
  if((led1_status == "ON")||(led1_status == "OFF")||(led2_status == "ON")||(led2_status == "OFF"))
    url_check = true;

  // It's not required to pass them both, so we check that they're exactly equal to
  // the strings ON or OFF by our design choice (this can be changed if you prefer
  // a different behavior)
  if (led1_status == "ON")
    digitalWrite(led1_pin, HIGH);
  else if (led1_status == "OFF")
    digitalWrite(led1_pin, LOW);
  if (led2_status == "ON")
    digitalWrite(led2_pin, HIGH);
  else if (led2_status == "OFF")
    digitalWrite(led2_pin, LOW);
  if (url_check)
    // If we've set the LEDs to the requested status, we have the webserver
    // return an "OK" (200) response.  We also include the number of milliseconds
    // since the program started running.
    // Note: This number will overflow (go back to zero), after approximately 50 days.
    server.send(200, "text/plain", "LEDs' status changed! (" + String(millis()) + ")");
    server.send(200, "text/plain", "LEDs' status unchanged! (" + String(millis()) + ")");

Note that both parameters need not be specified simultaneously on a request. Once the LEDs are set the server sends an OK (200) response notifying whether the LEDs' status was changed.  For this, we simply check to see if at least one of the four expected parameters were included in the request.

The next function includes our response to any request not sent to either of the URLs described above.  We simply construct a message that includes information that would be helpful for debugging (in case the request included a mistake) and send it as part of our 404 (Not Found) response:

// If the client requests any other URL than the root directory or the /setled path:
void handleNotFound() {
  // We construct a message to be returned to the client
  String message = "File Not Found\n\n";
  // which includes what URI was requested
  message += "URI: ";
  message += server.uri();
  // what method was used
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  // and what parameters were passed
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  // the response, as expected, is a "Not Found" (404) error
  server.send(404, "text/plain", message);

After these three user-defined functions, we define the standard setup() and loop() functions.  In setup() we initialize the pins as outputs and set their initial state to LOW.  We also connect our ESP8266 to our Wi-Fi network.  We start our webserver to listen for http requests.  And, we associate specific URLs with the 3 functions described above:

// In the setup function we initialize the different things
// that will be needed in our program, as well as set up the hardware
void setup(void) {
  // Set the LED pins to act as digital outputs and set them to a LOW 
  // state initially.
  pinMode(led1_pin, OUTPUT);
  pinMode(led2_pin, OUTPUT);
  digitalWrite(led1_pin, HIGH);
  digitalWrite(led2_pin, HIGH);

  // Start the Serial communication for debugging purposes
  //  Initialize the WiFi client and try to connect to our Wi-Fi network
  WiFi.begin(ssid, password);

  // Wait for a successful connection
  while (WiFi.status() != WL_CONNECTED) {
  // For debugging purposes print the network ID and the assigned IP address
  Serial.print("Connected to ");
  Serial.print("IP address: ");

  // Associate the URLs with the functions that will be handling the requests
  server.on("/", HTTP_GET, handleRoot);
  server.on("/setleds", HTTP_GET, handleSetLeds);

  // Start running the webserver
  Serial.println("HTTP server started");

Finally, our loop function only needs to call the handleClient() method of the ESP8266WebServer class:

// The loop function is straight-forward, simply handle any incoming requests to the
// our ESP8266 host!
void loop(void) {

Testing our ESP8266 LED Control Firmware

We can go ahead and upload the code to the ESP8266 using the Arduino IDE.  We can then open up a Serial Monitor window to view that the application runs as we'd expect.  In addition, we can get the IP Address assigned by our wireless router (or whatever device in our network is acting as a DHCP server).

Testing the ESP8266 LED Controller API from a Web Browser

Using the IP Address we can use our computers, or any other device connected to the same network to test the proper operation of the ESP8266.  In our case, the IP Address is, thus we use the browser in our laptop to navigate to:

And we verify that both LEDs, in fact, turn on.  After doing so, we proceed with opening Xcode and starting a new project to build our iOS app.

Setting Up Our New App in Xcode 7

To write our iOS App, we're going to start a new project in Xcode 7 File → New → Project... that is going to be a Single-View Application for iOS:

Xcode 7 New Project Single View Application

In the next configuration screen, you can set the organization name identifier to anything of your choosing (you can always use your name and "personal" for the identifier).  As we'll be using the Swift language (opposed to Objective-C) and developing our app for iPhones, we make sure those options are selected from the drop-down menus.  We name the project ESP8266LED:

New Project Options in Xcode 7

After clicking next our project is created and we're faced with several configuration options separated in different tabs.  The only changes we'll make to our app are in the General tab. Specifically, we select iPhone from the Devices drop-down menu, and Portrait from the Device Orientation options.

Xcode 7 Project Options for our iOS App

Note: if you're unfamiliar with Xcode 7, looking through the documentation on the Apple Developers site is a must(!).  We're going to open our main storyboard in the Interface Builder, and make the changes to the View Controller so that the Size is set to one of the iPhone options, and the Orientation is set to Portrait.

ESP8266 LED Control App: View Controller Options

Now that we have most things set up, we're ready to start adding some objects to our storyboard, and write some code to add the desired functionality to our app.

Walkthrough: ESP8266 LED Control (app)

Next, we're going to add a few objects to the View Controller's View.  Ensuring that the Object library is selected, we can use the search bar at the bottom of the utilities area to find our first object, a Web View, which we'll position at the bottom of the View Controller's View.  We then proceed to Ctrl+Click and drag to add an Outlet in our ViewController.swift file.  We name the Outlet for the Web View web, and leave the rest of the options set to their default values:

Adding a Web View Object to our View Controller's View

We can then proceed to add the first of four Button objects repeating the process we followed to add the Web View.  However, instead of adding an Outlet, we'll add an IBAction that we'll name led1on.  We'll modify the code of this IBAction in the ViewController.swift file with the function calls that will allow us to access the API of our web application running on the ESP8266.

To set led1 to ON we need to include the parameter led1=ON in the URL expected by the API (i.e., http://ip-address/setleds?).  To do this we can add:

    // URL paths including the ESP8266's IP address and arguments
    var led1on_URL = ""
    // IBAction connected to the "LED1 ON" button
    @IBAction func led1on(sender: AnyObject) {
        // Instantiate an NSURL object using the
        // led1on_URL string
        let requestURL = NSURL(string: led1on_URL)
        // Instantiate an NSURLRequest object using the
        // requestURL NSURL
        let request = NSURLRequest(URL: requestURL!)
        // Use the webview to send the request to the
        // request NSURLRequest

Leave a comment

  • Please note, comments must be approved before they are published

$1 Days
$2 Hours
$3 Minutes
$4 Second
{ "en":{ "general": { "field": { "required": "Required", "actions": "Actions", "top_btn": "Top" }, "accessibility": { "skip_to_content": "Skip to content", "close_modal": "Close (esc)" }, "meta": { "tags": "Tagged \"[[ tags ]]\"", "page": "Page [[ page ]]" }, "404": { "title": "404 Page Not Found", "subtext": "The page you requested does not exist.", "link": "Continue shopping" }, "pagination": { "previous": "Previous", "next": "Next", "current_page": "Page [[ current ]] of [[ total ]]" }, "password_page": { "opening_soon": "Opening Soon", "login_form_heading": "Enter store using password", "login_form_password_label": "Password", "login_form_password_placeholder": "Your password", "login_form_submit": "Enter", "signup_form_email_label": "Email", "signup_form_success": "We will send you an email right before we open!", "admin_link_html": "Are you the store owner? Log in here<\/a>", "password_link": "Enter using password", "powered_by_shopify_html": "This shop will be powered by [[ shopify ]]" }, "social": { "share_on_facebook": "Share", "share_on_twitter": "Tweet", "share_on_pinterest": "Pin it", "alt_text": { "share_on_facebook": "Share on Facebook", "share_on_twitter": "Tweet on Twitter", "share_on_pinterest": "Pin on Pinterest" } }, "search": { "no_results_html": "Your search for \"[[ terms ]]\" did not yield any results.", "results_with_count": { "one": "[[ count ]] result for \"[[ terms ]]\"", "other": "[[ count ]] results for \"[[ terms ]]\"" }, "title": "Search our site", "placeholder": "Search", "submit": "Submit", "close": "Close search" }, "newsletter_form": { "newsletter_email": "Join our mailing list", "email_placeholder": "Email address", "confirmation": "Thanks for subscribing", "submit": "Subscribe", "show_me_text": "Do not show me again" }, "filters": { "show_more": "Show More", "show_less": "Show Less" }, "breadcrumbs": { "home": "Home", "create_account": "Create account", "account": "Account", "addresses": "Addresses" }, "item": { "remove": "Remove Item" } }, "sections": { "header": { "top_header_login": "Login", "top_header_register": "Register", "top_header_wishlist": "Wish list", "register_dropdown": "No account? Create one here", "forgot": "Forgot password", "all_collection": "All Collections", "world_wide_delivery": "Worldwide delivery", "shipping_text": "Free UK Delivery on orders over £ 100", "hot_line": "Hot line" }, "menu": { "mobile_menu_tab": "Menu", "mobile_account_tab": "Account", "mobile_settings_tab": "Settings" }, "slideshow": { "next_slide": "Next slide", "previous_slide": "Previous slide", "pause_slideshow": "Pause slideshow", "play_slideshow": "Play slideshow", "play_video": "Play video", "close_video": "Close video" }, "map": { "get_directions": "Get directions", "address_error": "Error looking up that address", "address_no_results": "No results for that address", "address_query_limit_html": "You have exceeded the Google API usage limit. Consider upgrading to a Premium Plan<\/a>.", "auth_error_html": "There was a problem authenticating your Google Maps account. Create and enable the JavaScript API<\/a> and Geocoding API<\/a> permissions of your app." } }, "blogs": { "article": { "view_all": "View all", "all_topics": "All topics", "by_author": "by [[ author ]]", "posted_in": "Posted in", "read_more": "Read more", "back_to_blog": "Back to [[ title ]]" }, "comments": { "title": "Leave a comment", "name": "Name", "email": "Email", "message": "Message", "post": "Post comment", "moderated": "Please note, comments must be approved before they are published", "success_moderated": "Your comment was posted successfully. We will publish it in a little while, as our blog is moderated.", "success": "Your comment was posted successfully! Thank you!", "comments_with_count": { "one": "[[ count ]] comment", "other": "[[ count ]] comments" } } }, "cart": { "general": { "title": "Your cart", "note": "Add a note to your order", "remove": "Remove", "subtotal": "Subtotal", "savings": "You're saving", "shipping_at_checkout": "Shipping & taxes calculated at checkout", "update": "Update", "checkout": "Process Check out", "empty": "Your cart is currently empty.", "cookies_required": "Enable cookies to use the shopping cart", "edit": "Edit", "cancel": "Cancel", "continue_shopping": "Continue shopping", "recently_added_item": "Recently added item(s)", "remove_item": "Remove This Item", "view_and_edit_cart": "View and edit cart", "clear": "Clear cart", "empty_page_title": "Shopping Cart is Empty", "here": "here", "empty_continue_html": "Click here to continue shopping.", "processing": "Processing...", "items_count_label" : "[[ count ]] item(s) in your cart", "ok" : "Ok" }, "label": { "product": "Product", "price": "Price", "quantity": "Quantity", "total": "Total", "total_item": "Total item", "sub_total_top": "Cart Subtotal" } }, "collections": { "general": { "view_all": "View all", "clear_all": "Clear All", "no_matches": "Sorry, there are no products in this collection", "items_with_count": { "one": "[[ count ]] product", "other": "[[ count ]] products" }, "load_more": "Load More", "sidebar_btn": "Filter by" }, "sorting": { "title": "Sort by", "manual": "Featured", "best_selling": "Best Selling", "title_ascending": "Alphabetically, A-Z", "title_descending": "Alphabetically, Z-A", "price_ascending": "Price, low to high", "price_descending": "Price, high to low", "created_descending": "Date, new to old", "created_ascending": "Date, old to new" }, "filters": { "title_tags": "Filter", "all_tags": "All products", "categories": "categories", "title": "Filter", "color": "Color", "size": "Size", "brand": "Brand", "price": "Price", "green": "Green", "blue": "Blue", "red": "Red", "pink": "Pink", "black": "Black", "purple": "Purple", "white": "White", "orange": "Orange" }, "product_item": { "quick_shop": "Quick View", "compare": "Compare", "wishlist": "Add to Wishlist" } }, "contact": { "form": { "name": "Name", "email": "Email", "phone": "Phone Number", "message": "Message", "submit": "Submit", "post_success": "Thanks for contacting us. We'll get back to you as soon as possible.", "address": "Address", "telephone": "Telephone", "title": "Write us", "required": "Required" } }, "customer": { "account": { "title": "My Account", "details": "Account Details", "view_addresses": "View Addresses", "return": "Return to Account Details" }, "activate_account": { "title": "Activate Account", "subtext": "Create your password to activate your account.", "password": "Password", "password_confirm": "Confirm Password", "submit": "Activate Account", "cancel": "Decline Invitation" }, "addresses": { "title": "Your Addresses", "default": "Default", "add_new": "Add a New Address", "edit_address": "Edit address", "first_name": "First Name", "last_name": "Last Name", "company": "Company", "address1": "Address1", "address2": "Address2", "city": "City", "country": "Country", "province": "Province", "zip": "Postal\/Zip Code", "phone": "Phone", "set_default": "Set as default address", "add": "Add Address", "update": "Update Address", "cancel": "Cancel", "edit": "Edit", "delete": "Delete", "delete_confirm": "Are you sure you wish to delete this address?" }, "login": { "title": "Login", "desc": "If you have an account, sign in with your email address.", "email": "Email", "password": "Password", "forgot_password": "Forgot your password?", "sign_in": "Sign In", "guest_title": "Continue as a guest", "guest_continue": "Continue" }, "orders": { "title": "Order History", "order_number": "Order", "date": "Date", "payment_status": "Payment Status", "fulfillment_status": "Fulfillment Status", "total": "Total", "none": "You haven't placed any orders yet." }, "order": { "title": "Order [[ name ]]", "date": "Placed on [[ date ]]", "cancelled": "Order Cancelled on [[ date ]]", "cancelled_reason": "Reason: [[ reason ]]", "billing_address": "Billing Address", "payment_status": "Payment Status", "shipping_address": "Shipping Address", "fulfillment_status": "Fulfillment Status", "discount": "Discount", "shipping": "Shipping", "tax": "Tax", "product": "Product", "sku": "SKU", "price": "Price", "quantity": "Quantity", "total": "Total", "fulfilled_at": "Fulfilled [[ date ]]", "subtotal": "Subtotal" }, "recover_password": { "title": "Reset your password", "email": "Email", "submit": "Submit", "cancel": "Cancel", "subtext": "We will send you an email to reset your password.", "success": "We've sent you an email with a link to update your password." }, "reset_password": { "title": "Reset account password", "subtext": "Enter a new password for [[ email ]]", "password": "Password", "password_confirm": "Confirm Password", "submit": "Reset Password" }, "register": { "title": "Create Account", "first_name": "First Name", "last_name": "Last Name", "email": "Email", "password": "Password", "submit": "Create", "desc": "Creating an account is easy. Just fill in the form below." } }, "homepage": { "onboarding": { "product_title": "Your product's name", "product_description": "This area is used to describe your product’s details. Tell customers about the look, feel, and style of your product. Add details on color, materials used, sizing, and where it was made.", "collection_title": "Your collection's name", "blog_title": "Your post's title", "blog_excerpt": "Your store hasn’t published any blog posts yet. A blog can be used to talk about new product launches, tips, or other news you want to share with your customers. You can check out Shopify’s ecommerce blog for inspiration and advice for your own store and blog.", "blog_author": "Author name", "no_content": "This section doesn’t currently include any content. Add content to this section using the sidebar." } }, "layout": { "navigation": { "search": "Search", "toggle": "expand\/collapse", "expand": "expand", "collapse": "collapse", "all_categories": "All Categories" }, "cart": { "title": "Cart", "items_count": { "one": "item", "other": "items" } }, "customer": { "account": "Account", "log_out": "Log out", "logout": "Log out", "log_in": "Log in", "create_account": "Create account", "sign_up": "Sign up", "wishlist": "Wishlist" }, "footer": { "social_platform": "[[ name ]] on [[ platform ]]" }, "list_page": { "grid": "Grid", "list": "List" } }, "products": { "product": { "regular_price": "Regular price", "sold_out": "Sold out", "unavailable": "Unavailable", "on_sale": "Sale", "quantity": "Quantity", "add_to_cart": "Add to cart", "back_to_collection": "Back to [[ title ]]", "related_title": "Related Products", "qty_increase": "Increase", "qty_decrease": "Decrease", "deal_days": "Days", "deal_hours": "Hours", "deal_minutes": "Minutes", "deal_second": "Second", "select_option": "Select Option", "add_to_wishlist": "Add to Wishlist", "add_to_review": "Add to review", "compare_success_msg": "[[ product_title ]] has added to comparing box successful", "compare_exist_msg": "[[ product_title ]] is exist in comparing box", "compare_cart_msg": "[[ product_title ]] has added to shopping cart", "compare_remove_msg": "[[ product_title ]] has removed from comparing box", "compare_remove_msg": "[[ product_title ]] has removed from comparing box", "comparing_box": "Comparing box", "compare_no_items": "There is no items in comparing box", "wishlist_success_msg": "[[ product_title ]] has added to wishlist successful", "wishlist_exist_msg": "[[ product_title ]] is exist in wishlist", "wishlist_cart_msg": "[[ product_title ]] has added to shopping cart", "wishlist_box": "Wishlist", "wishlist_remove_msg": "[[ product_title ]] has removed from wishlist", "wislist_no_items": "There is no items in wishlist", "upsell_cart_msg": "\"[[ product_title ]]\" has added to shopping cart", "upsell_block_title": "Frequently bought with \"[[ product_title ]]\"", "upsell_cart_qty": "[[ count ]] item(s)", "upsell_product_page_title": "You may also like these products", "upsell_checkout_btn": "Checkout", "share": "Share product", "share_on_facebook": "Share on Facebook", "share_on_twitter": "Share on Twitter", "share_on_pinterest": "Share on Pinterest", "share_on_google": "Share on Google+", "share_on_linkedin": "Share on LinkedIn", "availability": "Availability", "in_stock": "In Stock", "out_of_stock": "Out of stock", "quick_overview": "Quick Overview", "details": "Details", "reviews": "Reviews", "first_review": "Be the first review", "tags": "Product Tags", "size_chart": "Size Chart", "options": "Options", "vendor": "Vendor", "features": "Features", "sale_left_text": "[[ sales ]] SOLD. HURRY! ONLY A FEW LEFT!", "checkout_text": "Secured and trusted checkout with" }, "upsell": { "recommend_text": "Someone purchased a", "minute_ago": "minutes ago" } }, "gift_cards": { "issued": { "title_html": "Here's your [[ value ]] gift card for [[ shop ]]!", "subtext": "Your gift card", "disabled": "Disabled", "expired": "Expired on [[ expiry ]]", "active": "Expires on [[ expiry ]]", "redeem_html": "Use this code at checkout to redeem your [[ value ]] gift card", "shop_link": "Start shopping", "print": "Print this gift card", "remaining_html": "[[ balance ]] left", "add_to_apple_wallet": "Add to Apple Wallet" } }, "date_formats": { "month_day_year": "%B %d, %Y" } } }