In this section of the tutorial we will discuss the implementation of the
Bluetooth Low Energy functionality. This will allow the CLUE board to determine
the current time by communicating with an Android smartphone app. The BLE
spec contains a lot of details, so we will just focus on what's needed to get
this part of the project working. BLE allows a device to set up what is
essentially a shared "chalkboard" between two devices. Devices may advertise
their existence, pair, and read and write to the shared chalkboard.
BLE devices may advertise multiple services, which can contain multiple
characteristics. There is a list of common service and characteristic
identifiers published in the BLE standards. In our project, the CLUE board
will start one BLE service: SVC_CURRENT_TIME, under which we will have two
characteristics: CHR_DAY_DATE_TIME and CHR_DAY_OF_WEEK.
module Ble
include("<bluefruit.h>", "<bluefruit_common.h>")
typeservicet = service(ptr)
typecharacterstict = characterstic(ptr)
typeadvertisingFlagt = advertisingFlag(uint8)
typeappearancet = appearance(uint16)
typesecureModet = secureMode(uint16)
typepropertiest = properties(uint8)
We begin by creating a new module in a file called Ble.jun. The library
that we are wrapping is called bluefruit, so we will import the required
C/C++ header files. We start by defining a wrapper type for a characteristic
and service. These will hold a ptr, which is compiled to
void *. Inside of our CWatch.jun we will define
different services and characteristics as global C++ objects and wrap
them in these types. We also define wrapper types for other BLE configuration
options. Note that we could have made a value constructor for every
configuration option instead of wrapping around an integer, however
the bluefruit library expects these numbers.
// Wrap #define constants into a type
// The full Ble.jun wrapper contains many of these entries:
We now define a few functions for starting services and characteristics and
configuring them. Note that we use pattern matching inside the let expressions
to pull out the ptrs, then cast them
appropriately. The readGeneric function is
particularly notable as it is polymorphic in its return type. By constraining
the return type, we can change how many bytes are read into the variable and
what is therefore returned by the function.
funbeginService(s) = {
let service(p) = s
// p is a ptr aka a void *
// Inside of our inline C++, we cast it back to a BLEService *
We now add a few more functions for controlling the top level BLE device on
the CLUE board. These functions will things like advertising names, intervals,
and power levels.
We now return to the CWatch.jun file and add some
inline C++ in the top level module. We define the service object, characteristics
and handlers for when the CLUE board receives data from the smartphone. We also
define two packed record types. These types are packed, which means that no members
of the record will be padded. On the Android app side, we will ensure that the
data we will write is packed as well. When Juniper transpiles to C++, these
record types will be defined as packed structs.
We now define a few more global constants, namely the wrappers around the
services and characteristics. Recall that in Juniper, curly braces containing
expressions separated by newlines are sequence expressions, and the return
result of a sequence expression is determined by the final expression in the
sequence. We also add a bunch of BLE initialization code to the
setup function. This includes setting the
characteristics to be writable, disabling security/encryption, setting the size
of the characteristics via the sizeof expression, and
setting up the advertising information.
We are now ready to write a function that will update the clockState
global variable. Recall that clockState is a mutable record.
This means that we are allowed to mutate the fields of the record via the record field
access operator ..
funprocessBluetoothUpdates() = {
letmut hasNewDayDateTime = false
letmut hasNewDayOfWeek = false
#
// rawHasNewDayDateTime and rawHasNewDayOfWeek are C/C++ global
// variables that we defined previously. These variables are set
// to true when the smartphone has finished writing data
hasNewDayDateTime = rawHasNewDayDateTime;
rawHasNewDayDateTime =false;
hasNewDayOfWeek = rawHasNewDayOfWeek;
rawHasNewDayOfWeek =false;
#
if hasNewDayDateTime {
let bleData : dayDateTimeBLE = Ble:readGeneric(dayDateTimeCharacterstic)
clockState.month =
match bleData.month {
0 => january()
1 => february()
2 => march()
3 => april()
4 => may()
5 => june()
6 => july()
7 => august()
8 => september()
9 => october()
10 => november()
11 => december()
_ => january()
}
clockState.day = bleData.day
clockState.year = bleData.year
clockState.hours = bleData.hours
clockState.minutes = bleData.minutes
clockState.seconds = bleData.seconds
}
if hasNewDayOfWeek {
let bleData : dayOfWeekBLE = Ble:readGeneric(dayOfWeekCharacterstic)
clockState.dayOfWeek =
match bleData.dayOfWeek {
0 => sunday()
1 => monday()
2 => tuesday()
3 => wednesday()
4 => thursday()
5 => friday()
6 => saturday()
_ => sunday()
}
}
}
The only thing left to do is call the processBluetoothUpdates
function inside of loop. This will potentially mutate
the global clock state just before we start updating it and drawing the time.