This tutorial demonstrates how to use MIT App Inventor to connect with an Adafruit Feather M0 Bluefruit LE and control up to 6 Output (GP-O) pins using a custom GATT service.
Once the Android app has connected with the Bluefruit LE module it will send command strings to the Bluefruit LE's Nordic nRF51822 chipset GATT serial link service and the Arduino code will interpret these command strings to switch any of the predefined pins high or low. The app also includes a GP-O latching option so that a pin will stay high once the button has been clicked, otherwise the default option is for the pin to remain high while the button on the app is pressed and returns low when the button has been released. For simplicity, this demo app does not receive any feedback from the Bluefruit LE module. This demonstration app is also designed to automate the selection of the required GATT service and the required characteristics in order to write command strings to the nRF51822 chipset.
An earlier tutorial was written up on Hackster.io demonstrating the use of both read and write commands to control an LED. This app allowed the user to manually select an appropriate GATT service. The link to this tutorial can be found here (BLE LED Controller).
This app uses a single screen view and will show and hide various sections within that screen view, depending on whether the app has successfully connected with a Bluefruit LE module, or not.
Designing the App LayoutThese instructions assume that you have prior experience and know-how on how to use the App Inventor IDE.
So, let's start by creating a new project. Then to design the app layout, go to the Designer view. Then within Screen1, insert 2 horizontal arrangements and 3 vertical arrangements in your screen layout, as follows:
All layout sections use "fill parent" for the width property. Then for heights, the following are used:
- Logo section: 10%
- Status section: 7%
- Scanning section: fill parent
- GPIO App section: fill parent
- Connect/Disconnect section: 16%
To facilitate the inclusion of all components in design view, make sure that the "Screen1" view is set to "scrollable" by clicking on the "Scrollable" checkbox property. Also make sure that the screen orientation property is set to "portrait" and to save space at runtime uncheck the "TitleVisible" check box. Note that the scrollable setting must then be changed back at runtime to "non-scrollable" to ensure that the app screen layout works properly. This will be set in the "Block View". Instructions on how to do this is shown below.
The Logo section is optional as contains no runtime functionality. The Status section contains a label, which is used to inform the user of runtime status, such as whether Bluetooth is enabled, whether scanning, whether connected etc.
The Scanning section contains the following:
- 2 buttons (Start Scan and Stop Scan)
- a ListView (used to show BLE devices detected during scan)
- a Vertical Arrangement with height and width properties set to "fill parent". This is a layout arrangement to ensure that the Connect/Disconnect Sections remains at the bottom of the screen when made visible.
The GPIO App section contains the following:
- An optional logo (image)
- A static label for title
- A 2-row x 3-column table containing 6 buttons (labelled Pins 1 - 6)
- A checkbox for handling latching of button press
Lastly, the Connect/Disconnect section just contains two buttons "Connect" and "Disconnect" within a horizontal arrangement.
The App also uses a number of hidden components:
- ActivityStarter (name shortened to AST1): This is used to turn on Bluetooth if it is not enabled on the phone (using code snippet provided by puravidaapps.com)
- Bluetooth Client (name shortened to BTC1): This is required to make the "is Bluetooth enabled" check on the phone
- Notifier: This is used to display toast alert messages on the phone
- BluetoothLE (name shortened to BLE1): This is the BLE component (imported as an extension)
Now let's create the logic for the app. Switch to the Blocks view.
To get things going we need some global variables:
- UARTServiceIndex -- this will store the index of the customer GATT service used for transmitting data, from the list of GATT services retrieved once connected.
- CharacteristicUUIds -- this will store all the characteristic UUIDs associated with the GATT service
- CharacteristicIndex -- this is a predefined index number of the correct Characteristic UUID (note that this index value was found manually using Nordic Scan BLE app and is used in this app to keep things simple.)
- btnClickLatchStatus -- this is a list containing the button state of each of the 6 buttons when the latched checkbox is checked by the user. A zero indicates that the button is in the "off state" while a 1 indicates that the button is in the "on state". Note that this is simply from the phone app perspective, as the actual pin state is never read from the Feather Bluefruit LE device.
- btnPinNames -- this is a double list containing the string commands used for each button for each state, which are then sent via BLE to the Feather Bluefruit LE device. For example, "A0" is used for pin1 in the off state and "A1" is used for pin1 in the on state. Similarly, "B0" is used for pin2 in the off state and "B1" is used for pin2 in the on state. etc. etc.
Now let's initialise the app using the "when Screen1.Initialize" event is triggered. Here two main activities are performed.
The first is initialising the app layout at runtime. The following is defined:
- The "Screen1.Scrollable" property is set to false.
- All buttons such as StartScan, StopScan, Connect and Disconnect are disabled.
- The Scan section (va_Scan) is set to "visible" while the GPIO App section (va_App) and the buttons within the Connect/Disconnect section (ha_connectButtons) is set to "hidden".
- The colours for the 6 buttons, which define the unpressed state are also defined.
The second is checking if Bluetooth is enabled on the phone and if so, then the StartScan button is enabled. If Bluetooth is not enabled the "ActivityStarter" method (AST1) is started using "call AST1.StartActivity". After the Bluetooth enable activity has been launched the Bluetooth check procedure (BTenabledChecker) is performed again.
When the StartScan button is clicked the "call BLE1.StartScanning" method is called. The status label (lbl_AppStatus) is then modified to tell the user that the app is now scanning for BLE devices. The StartScan button is then disabled and the StopScan button is enabled. The ListView for all BLE devices is also enabled (just in case it is set to false in design view). The Connect Buttons section is also set to hidden - this is required to cover the use case when scanning is repeated following a BLE connection and then disconnection.
Whenever a BLE device is found the "when BLE1.DeviceFound" method is triggered. Here the BLE_ListView is updated with the latest BLE Device List.
BLE device scanning continues until either the StopScan button is clicked or an element in the List View is picked by the user. The status label is then amended accordingly. Once scanning has stopped the StartScan button is enabled and the StopScan button is disabled. If a BLE device has been picked off the list of BLE devices, as shown in ListView, then the buttons within the Connect/Disconnect section are made visible, with the Connect button enabled.
When the Connect button is clicked, the "call BLE1.Connect" method is called using the selection index as reference for the BLE1.DeviceList.
When the selected BLE device has actually connected to your phone, the "when BLE1.Connected" event will be triggered. Within this event method, the status label is amended to inform that user than the phone is now connected with the Bluefruit LE device. The next step in the process is to validate or verify that the custom GATT service the app plans to use exists within the list of GATT services made available through the connection.
The GATT services list is obtained via the "call BLE1.SupportedServices" method. This list is then passed to a custom procedure called "parseServiceUUID" where the unique GATT service id "6e400001" is checked against the list of UUID's and if it is found the index number within this list is returned and stored in the global variable "UARTServiceIndex".
Then, if this index is non-zero, all the service characteristics associated with that GATT service are retrieved and then stored in the global variable "CharacteristicUUIDs" using the method "call BLE1.GetCharactertisticsForService" method.
The status label is then updated to show success in obtaining the correct services and service characteristic. The Scanning section is now hidden from view, by setting "va_Scan.Visible" to false, and the GPIO App section is made visible, by setting "va_App.Visible" to true.
The default button behaviour for the GPIO App section is controlled via the "TouchDown" and "TouchUp" events. When a "TouchDown" event is detected for a particular button, the "call BLE1.WriteStrings" method is called, using the GATT Services and Characteristic parameters stored within the respective global variables and the values used are obtained from the global list "btnPinNames".
Note that the "WriteStrings" method was chosen for readability purposes. A more efficient method to use would be the "WriteBytes" method (although at the time of writing, this method was known to cause problems when the app is compiled).
GP-O button behaviour is also controlled by the check box (chkbx_ToggleLatch) allowing the user to latch a button state. The control logic attached to the button "TouchDown" and "TouchUP" events only apply if "chkbx_ToggleLatch.Checked" is false.
The "when chkbx_ToggleLatch.Changed" event is also used to check when the checkbox changes from checked back to unchecked. When this condition occurs, all buttons are returned to the unchecked state and the colour settings returned to the default state.
Each button state, which is linked to a GP-O pin (see Arduino code below as to how this done), is then checked, using a for-each loop, to see if a button was found to be in an active state (i.e. a GP-O pin was set to HIGH). If found to be active, then a BLE command string is send to the Bluefruit LE device to return that GP-O pin back to state 0 (i.e. LOW). The global list "btnClickLatchStatus" is then also modified to return the appropriate list item value back to zero.
If the user has checked the "chkbx_ToggleLatch" checkbox, then the button "click" event is used to change and latch the button state.
Then, within the "when btn.Click" method, the "btnClickLatchStatus" value, for that button is checked and if it is zero, this value is changed to 1 in the "btnClickLatchStatus" global list.
The colour of the button that was clicked is then set to green using the "set btnPin.BackgroundColour" method. The "BLE1.WriteStrings" method is then called and the high value string command, as stored in the global list "btnPinNames" is then sent to the Bluefruit LE device.
Similarly, if the button state was found to high, or set to value 1, then this is changed to zero and the "BLE1.WriteStrings" method is called and the low value string command, as stored in the global list "btnPinNames" is sent to the Bluefruit LE device.
That covers the App essentials. Let's now look at the Adafruit Feather M0 Bluefruit LE device code. To learn how to set-up this device, please refer to Adafruit's excellent learning guide.
Modifying the Arduino Example CodeTo makes things easier, an existing Arduino code example (bleuart_datamode.ino), which comes with the Adafruit nRF51 BLE Library, is used as the starting point. Instructions on how to install this library are found in the Adafruit learning guide.
There are not many modifications to make using this example. The first change, is to declare which GP-O pin relates to which button in the app. This is achieved by creating a byte array "pinRef" in the code. There are 2 other global variables required for this application. The first is a boolean "cmdStr" which is triggered when a GP-O pin reference is received via the BLE uart (if a valid pin array reference is received then this is set to true). The second global is "pinNo" which relates to the actual pin reference as found in the "pinRef" byte array.
// -------------- DEMO APP GP-O PIN REFERENCE ---------------------------------------
// These are the pin reference values (if you do not want a pin set then enter a 0 in the array)
// MIT App button "Pin1" is 1st item in the array (refers to Feather MO pin-5), button "Pin2" is the 2nd (refers to Feather MO pin-6) etc.
// ----------------------------------------------------------------------------------
const byte pinRef[6] = {5, 6, 9, 10, 11, 13};
bool cmdStr = false; // A flag to inform which GP-O pin selected
byte pinNo = 0;
Then in the "void setup(void)" routine the pin modes are defined.
To facilitating testing of any actuators or LED's attached to the output, all are set HIGH initially and then after setup routine they are returned to LOW. In the demo shown below this helps to make sure that all LED's are in fact working and connected in the correct way.
void setup(void) {
// ------- some setup code -------
// Configure the pin modes
for (byte i = 0; i < 6; i++) {
pinMode(pinRef[i], OUTPUT);
digitalWrite(pinRef[i], HIGH);
}
// ------- some setup code -------
Serial.println(F("Waiting for connection"));
delay(500);
for (byte i = 0; i < 6; i++) {
digitalWrite(pinRef[i], LOW);
delay(500);
}
// ------- some setup code -------
}
Then in the "void loop(void)" routine, the following code is added to read incoming bytes, when the ble device is available.
The first check applies if the "cmdStr" boolean is false. Here, the byte is converted to a character and checked to see if it is an alpha character type, using the "isAlpha()" Arduino function. If it is an alpha character, the character is checked to see if it matches any of the six pin reference codes, A, B, C,... to F. If matching, the "pinNo" is extracted from the "pinRef" array. The boolean "cmdStr" is now set to true as the next incoming byte should then instruct whether to set the pin high or low. If the "pinRef" value for that array reference is set to zero then no action is taken with the next incoming byte.
The second check applies is the "cmdStr" boolean is true. Here, the byte is converted to a character and checked to see if it is a digit character type, using the "isDigit()" Arduino function. If it is a digit character, the character is checked to see if it is a "0" or a "1". If a zero is received then the pin as referenced by "pinNo" is set LOW using the "digitalWrite(pinNo, LOW);" function. If a one is received then the "pinNo" is set HIGH.
void loop(void)
{
if (ble.available()) {
byte c = ble.read();
if (!cmdStr) {
if (isAlpha((char)c)) {
switch ((char)c) {
case 'A': // pin 1
if (pinRef[0] != 0) {
pinNo = pinRef[0];
cmdStr = true;
}
break;
case 'B': // pin 2
if (pinRef[1] != 0) {
pinNo = pinRef[1];
cmdStr = true;
}
break;
case 'C': // pin 3
if (pinRef[2] != 0) {
pinNo = pinRef[2];
cmdStr = true;
}
break;
case 'D': // pin 4
if (pinRef[3] != 0) {
pinNo = pinRef[3];
cmdStr = true;
}
break;
case 'E': // pin 5
if (pinRef[4] != 0) {
pinNo = pinRef[4];
cmdStr = true;
}
break;
case 'F': // pin 6
if (pinRef[5] != 0) {
pinNo = pinRef[5];
cmdStr = true;
}
break;
default:
Serial.print((char)c);
Serial.println(F( " is not a valid GPIO pin reference"));
}
}
}
else {
if (isDigit((char)c)) {
if ((char)c == '0') {
if (pinNo) digitalWrite(pinNo, LOW);
}
else if ((char)c == '1') {
if (pinNo) digitalWrite(pinNo, HIGH);
}
else {
Serial.print((char)c);
Serial.print(F( " is not valid for GPIO pin: "));
Serial.println(pinNo);
}
}
cmdStr = false;
}
}
}
That is basically it. Feel free to amend as you want.
Once the code is uploaded onto the BluefruitLE device, the Serial Monitor can be opened to monitor progress.
The Demo (finished product)The demo circuit contains 5 x LED's and 5 x 100ohm resistors, with the 6th LED being the onboard LED, which is linked to pin13.
Comments