Bluetooth Configuration - PushTracker/EvalApp GitHub Wiki
The PushTracker is a Bluetooth Central device - just like a phone or computer. This means that it does not advertise services like a peripheral does but instead scans for peripherals which are advertising certain services (with associated characteristics / descriptors). The two types of services that it searches for are
-
The App Service [
9358ac8f-6343-4a31-b4e0-4b13a2b45d86
] - this service has these characteristic UUIDs:58daaa15-f2b2-4cd9-b827-5807b267dae1, 68208ebf-f655-4a2d-98f4-20d7d860c471, // send all PT data (including OTA) to this 9272e309-cd33-4d83-a959-b54cc7a54d1f, 8489625f-6c73-4fc0-8bcc-735bb173a920, 5177fda8-1003-4254-aeb9-7f9edb3cc9cf
These characteristics should all have
-
properties
read, write, notify
, -
permissions
read, write
-
default value of
0
(UINT8
) -
WRITE_TYPE_DEFAULT
-
These descriptors:
2900 2902
where each descriptor should have:
-
permissions
read, write
- default value
[0x00, 0x00]
-
permissions
-
-
The SmartDrive Service [
0cd51666-e7cb-469b-8e4d-2742f1ba7723
]- this service has these characteristic UUIDs:e7add780-b042-4876-aae1-112855353cc1, // send SDBT OTA DATA e8add780-b042-4876-aae1-112855353cc1, e9add780-b042-4876-aae1-112855353cc1, // send smart drive packets (incl SDUC OTA) eaadd780-b042-4876-aae1-112855353cc1, ebadd780-b042-4876-aae1-112855353cc1 // send SDBT OTA Start (6) / Stop (3) commands
For a full description of the service please see SmartDrive Service Descriptor.
If the phone is advertising the App
service etc. mentioned above,
then the PushTracker will be able to find it when it scans for
available devices advertising a service with that UUID. When the user
requests the PushTracker to pair to the phone (from within the
PushTracker's settings menu) it will scan for the App and send a
pairing request to the first device that it finds. If the user
accepts the pairing request on the phone then the PushTracker will be
notified that the bonding was successful and will save the bonding
information on the PushTracker. From that point on the user can
simply press the right button to initiate a connection to the bonded
phone.
Note: on Android, the app has to listen for the bonding event so that after bonding succeeds it can delete the bonding information on the phone's side. This must be done because the PushTracker does not support encrypted connections - and Android will automatically encrypt connections to the PushTracker if the bonding information is saved on the phone (which it is by default).
Since the PushTracker is the Central / Master, the phone cannot initiate a connection to a PushTracker - and must advertise the App service and wait for the user to initiate a connection on the PushTracker. When the PushTracker connects, it will subscribe to all the characteristics of the App after which it will send
-
VersionInfo
that contain all firmware version for PT, SDUC and SDBT - the current day's
ErrorInfo
-
TotalDistance
that the SmartDrive driven - its current
Settings
- all
DailyInfo
data for days that have not been sent to a phone before (after which they will be deleted from the PushTracker) - the current day's
DailyInfo
While connected the App can send a SetTime
command to synchronize
the time on PushTracker. The App can also send a Wake
command to see
if the PushTracker is still connected - the PushTracker will respond
with a Ready
packet.
The app will be in one of these modes:
- Neither paired nor connected to a PushTracker
- Paired to a PushTracker but not connected to a PushTracker
- Paired and connected to a PushTracker
In all of these modes - the app will be advertising its services so that a PushTracker may find and pair or connect to the App.
The SmartDrive is a regular Bluetooth peripheral, meaning that all Bluetooth devices support communication with it and the PushTracker or App controls the connections.
Note: The SmartDrive does not require pairing / bonding to communicate with the phone - the phone can simply initiate a connection to the SmartDrive. The same is true for the PushTracker - we only pair to the SmartDrive to make it simpler to remember which SmartDrive we want to connect to.
Since the SmartDrive is a Bluetooth peripheral, the PushTracker completely controls the connection to the SmartDrive. If the SmartDrive is not connected to any other devices (including PushTrackers or Apps), then it will advertise the SmartDrive services described above. Therefore, all the PushTracker needs to do is to scan for devices advertising the SmartDrive service and then choose one of those devices to connect to.
Once the PushTracker connects, the PushTracker should subscribe to all characteristics advertised by the SmartDrive service. Once it has subscribed, the SmartDrive will then send
-
A
DeviceInfo
packet from the SmartDrive Bluetooth chip - this is sent once and lets the app know the version of the SmartDrive Bluetooth firmware -
A
MotorInfo
packet ten times a second which contains (among other things) the state of the motor and the version of the SmartDrive microcontroller firmware. -
A
MotorDistance
packet once a second which contains the cumulative distance (in ticks) that the motor case has rolled, and that the motor has driven.For a full description of the packet format please see Bluetooth Communications Description.
<service uuid="0cd51666-e7cb-469b-8e4d-2742f1ba7723" advertise="true">
<description>SmartDrive Data service</description>
<characteristic uuid="e7add780-b042-4876-aae1-112855353cc1" id="ota_data_fromWB">
<description>Data</description>
<properties write="true" indicate="true" read="true" />
<value variable_length="true" length="20" type="user" />
</characteristic>
<characteristic uuid="e8add780-b042-4876-aae1-112855353cc1" id="xgatt_speed">
<description>Data</description>
<properties write="true" indicate="true" read="true"/>
<value variable_length="true" length="20" type="user" />
</characteristic>
<characteristic uuid="e9add780-b042-4876-aae1-112855353cc1" id="xgatt_data">
<description>Data</description>
<properties write="true" indicate="true" read="true" />
<value variable_length="true" length="20" type="user" />
</characteristic>
<characteristic uuid="eaadd780-b042-4876-aae1-112855353cc1" id="OTA_data_fromDG">
<description>Data</description>
<properties write_no_response="true" />
<value variable_length="true" length="20" type="user" />
</characteristic>
<characteristic uuid="ebadd780-b042-4876-aae1-112855353cc1" id="data_control">
<description>Data</description>
<properties write="true" indicate="true" read="true" />
<value variable_length="true" length="20" type="user" />
</characteristic>
</service>
NOTE: THIS SERVICE DESCRIPTOR IS NOT USED BY THE SMARTDRIVE OR THE APP
<service uuid="1d14d6ee-fd63-4fa1-bfa4-8f47b42119f0">
<description>Bluegiga OTA</description>
<characteristic uuid="f7bf3564-fb6d-4e53-88a4-5e37e0326063" id="ota_control">
<properties write="true" />
<value length="1" type="user" />
</characteristic>
<characteristic uuid="984227f3-34fc-4045-a5d0-2c581f81a153" id="ota_data">
<properties write_no_response="true" />
<value length="20" />
</characteristic>
</service>
#include <string>
#include <vector>
#include <iostream>
/*** Motor ***/
class Motor {
public:
enum class State : uint8_t {
Off,
On,
Error
};
};
/*** Smart Drive ***/
class SmartDrive {
public:
enum class Error : uint8_t;
enum class ControlMode : uint8_t;
enum class Units : uint8_t;
enum class AttendantMode : uint8_t;
enum class Error: uint8_t {
NoError,
BatteryVoltage,
MotorPhases,
OverCurrent,
OverTemperature,
GyroRange,
OTAUnavailable,
BLEDisconnect
};
enum class ControlMode : uint8_t {
Beginner,
Intermediate,
Advanced,
Off
};
enum class Units : uint8_t {
English,
Metric
};
enum class AttendantMode : uint8_t {
Off,
Inactive,
OnePressed,
TwoPressed
};
// settings flags values are the bit numbers
enum class BoolSettingFlag : uint8_t { EZMODE = 0 };
struct Settings {
ControlMode controlMode;
Units units;
uint8_t settingsFlags1; /** Bitmask of boolean settings. */
uint8_t padding;
float tapSensitivity; /** Slider setting, range: [0.1, 1.0] */
float acceleration; /** Slider setting, range: [0.1, 1.0] */
float maxSpeed; /** Slider setting, range: [0.1, 1.0] */
};
static const int settingsLength = sizeof(Settings);
static bool getBoolSetting ( const Settings* s, BoolSettingFlag boolSetting ) {
return (bool)((s->settingsFlags1 >> (uint8_t)boolSetting) & 0x01);
}
};
/*** Packet ***/
/**
* @brief Payloads sent between the Drive Unit and the Wristband for
* transferring data, commands, errors, and over-the-air
* (OTA) update data.
*
* @details The packet structures sent on bluetooth have only:
*
* | Field | Size (B) |
* |:--------:|:--------:|
* | Type | 1 |
* | SubType | 1 |
* | Data | 0-16 |
*
* and therefore fit within the 20 byte bluetooth packet limit so
* do not need length bytes. Since Bluetooth handles reliable
* communications and start/stop conditions, no start, stop, or
* CRC bytes are required. For the communication between the
* SmartDrive bluetooth and the SmartDrive MCU (through UART),
* start, stop, length, and CRC bytes are added to the
* packet. Therefore the structure of packets sent between the
* SmartDrive bluetooth and the SmartDrive MCU is
*
* | Field | Size (B) |
* |:--------:|:--------:|
* | Start | 1 |
* | Type | 1 |
* | SubType | 1 |
* | Data | 0-16 |
* | Length | 1 |
* | CRC | 1 |
* | Stop | 1 |
*
*
* Note: Structures must be 4-byte aligned for the processors, so
* there are some padding bytes between data, for instance in
* settings, motorInfo.
*
*/
class Packet {
public:
// Main Type of the packet
enum class Type : uint8_t {
None,
Data,
Command,
Error,
OTA
};
// Subtypes of the packet
enum class Data : uint8_t;
enum class Command : uint8_t;
enum class OTA : uint8_t;
enum class Data : uint8_t {
MotorDistance,
Speed,
CoastTime,
Pushes,
MotorState,
BatteryLevel,
VersionInfo,
DailyInfo,
JourneyInfo,
MotorInfo,
DeviceInfo,
Ready,
ErrorInfo
};
enum class Command : uint8_t {
SetAcceleration,
SetMaxSpeed,
Tap, DoubleTap,
SetControlMode,
SetSettings,
TurnOffMotor,
StartJourney,
StopJourney,
PauseJourney,
SetTime,
StartOTA,
StopOTA,
OTAReady,
CancelOTA,
Wake,
DistanceRequest,
StartGame,
StopGame,
ConnectMPGame,
DisconnectMPGame,
SetLEDColor
};
enum class OTA : uint8_t {
SmartDrive,
SmartDriveBluetooth,
PushTracker
};
enum class Device : uint8_t {
SmartDrive,
SmartDriveBluetooth,
PushTracker
};
enum class Game : uint8_t {
Mario,
Snake,
Sonic,
Tunnel,
FlappyBird,
Pong
};
static const int numTypeBytes = 1;
static const int numSubTypeBytes = 1;
static const int maxDataLength = 16;
static const int minDataLength = 0;
static const int maxSize = numTypeBytes + numSubTypeBytes + maxDataLength;
static const int minSize = numTypeBytes + numSubTypeBytes + minDataLength;
// Version Info
struct VersionInfo {
uint8_t pushTracker; /** Major.Minor version as the MAJOR and MINOR nibbles of the byte. **/
uint8_t smartDrive; /** Major.Minor version as the MAJOR and MINOR nibbles of the byte. **/
uint8_t smartDriveBluetooth; /** Major.Minor version as the MAJOR and MINOR nibbles of the byte. **/
};
// Daily Info
struct DailyInfo {
uint16_t year;
uint8_t month;
uint8_t day;
uint16_t pushesWith; /** Raw integer number of pushes. */
uint16_t pushesWithout; /** Raw integer number of pushes. */
uint16_t coastWith; /** Coast Time (s) * 100. */
uint16_t coastWithout; /** Coast Time Without (s) * 100. */
uint8_t distance; /** Distance (mi) * 10. */
uint8_t speed; /** Speed (mph) * 10. */
uint8_t ptBattery; /** Percent, [0, 100]. */
uint8_t sdBattery; /** Percent, [0, 100]. */
};
// Journey Info
struct JourneyInfo {
uint16_t pushes; /** Raw integer number of pushes. */
uint8_t distance; /** Distance (mi) * 10. */
uint8_t speed; /** Speed (mph) * 10. */
};
// Motor Info
struct MotorInfo {
Motor::State state;
uint8_t batteryLevel; /** [0,100] integer percent. */
uint8_t version; /** Major.Minor version as the MAJOR and MINOR nibbles of the byte. **/
uint8_t padding;
float distance;
float speed;
float driveTime;
};
// Time Info (for SetTime)
struct TimeInfo {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hours;
uint8_t minutes;
uint8_t seconds;
};
// Used for just sending device info between devices
struct DeviceInfo {
Device device; /** Which Device is this about? **/
uint8_t version; /** Major.Minor version as the MAJOR and MINOR nibbles of the byte. **/
};
// Error Info (sent by the PT to the app)
struct ErrorInfo {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
SmartDrive::Error mostRecentError; /** Type of the most recent error, associated with the timeStamp. **/
uint8_t numBatteryVoltageErrors;
uint8_t numOverCurrentErrors;
uint8_t numMotorPhaseErrors;
uint8_t numGyroRangeErrors;
uint8_t numOverTemperatureErrors;
uint8_t numBLEDisconnectErrors;
};
// BatteryInfo: Used for keeping track of battery and last time
// battery was updated between PT and App, so the app knows the
// PT and SD battery and when the SD battery data was last
// received (as a time stamp). Note that the SmartDrive battery
// will only be non-zero if the PushTracker has connected to it
// today, so the only timestamp we need is HH:MM:SS
struct BatteryInfo {
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t smartDriveBatteryLevel; /** [0,100] */
uint8_t pushTrackerBatteryLevel; /** [0,100] */
};
// DistanceInfo: How far have the motor and case gone?
struct DistanceInfo {
uint64_t motorDistance; /** Cumulative Drive distance in ticks. **/
uint64_t caseDistance; /** Cumulative Case distance in ticks. **/
};
// The length of data bytes contained in the packet
int dataLength;
// The actual type contained in the packet
Type type;
// The actual subtype contained in the packet
union {
Data data;
Command command;
SmartDrive::Error error;
OTA ota;
};
// The actual data contained in the packet
union {
SmartDrive::Settings settings;
VersionInfo versionInfo;
DailyInfo dailyInfo;
JourneyInfo journeyInfo;
MotorInfo motorInfo;
TimeInfo timeInfo;
DeviceInfo deviceInfo;
ErrorInfo errorInfo;
BatteryInfo batteryInfo;
DistanceInfo distanceInfo;
/**
* Used with StartOTA / StopOTA commands, tells which device the
* subsequent OTA data is for. The command should be relayed to
* that device.
*/
OTA otaDevice;
Game game;
SmartDrive::ControlMode controlMode;
SmartDrive::Units units;
Motor::State motorState;
uint64_t motorDistance; /** Cumulative Drive distance in ticks for MotorDistance packet. **/
float motorSpeed;
float coastTime;
uint16_t pushes;
uint8_t batteryLevel;
float acceleration;
float maxSpeed;
uint64_t errorId; /** Unique ID when an Error Packet is sent. **/
uint8_t bytes[maxDataLength];
};
Packet() {
newPacket();
}
~Packet() {}
Packet& operator=(const Packet& rhs) {
if (this == &rhs)
return *this;
_valid = rhs._valid;
type = rhs.type;
for (int i=0; i<Packet::maxDataLength; i++) {
bytes[i] = rhs.bytes[i];
}
return *this;
}
bool valid (void) {
return _valid;
}
bool processPacket(std::string rawData) {
int size = rawData.size();
//std::cout << "Packet size: " << size << std::endl;
if (size < Packet::minSize || size > Packet::maxSize)
return false;
int dataIndex = 2;
int subTypeIndex = 1;
int typeIndex = 0;
type = (Packet::Type) rawData[typeIndex];
switch (type) {
case Packet::Type::Data:
data = (Packet::Data)rawData[subTypeIndex];
break;
case Packet::Type::Command:
command = (Packet::Command)rawData[subTypeIndex];
break;
case Packet::Type::Error:
error = (SmartDrive::Error)rawData[subTypeIndex];
break;
case Packet::Type::OTA:
break;
default:
return false;
break;
}
dataLength = size - 2;
for (int i=0; i<dataLength; i++) {
bytes[i] = rawData[dataIndex + i];
}
return true;
}
void newPacket (void) {
dataLength = 0;
_valid = false;
type = Packet::Type::None;
}
std::vector<uint8_t> format () {
std::vector<uint8_t> output;
int numBytes = Packet::minSize;
output.push_back((uint8_t)type);
//std::cout << (uint8_t)type << std::endl;
int dataLen = 0;
switch (type) {
case Packet::Type::Data:
output.push_back((uint8_t)data);
switch (data) {
case Packet::Data::MotorDistance:
dataLen = sizeof(distanceInfo);
break;
case Packet::Data::Speed:
dataLen = sizeof(motorSpeed);
break;
case Packet::Data::CoastTime:
dataLen = sizeof(coastTime);
break;
case Packet::Data::Pushes:
dataLen = sizeof(pushes);
break;
case Packet::Data::MotorState:
dataLen = sizeof(motorState);
break;
case Packet::Data::BatteryLevel:
dataLen = sizeof(batteryLevel);
break;
case Packet::Data::VersionInfo:
dataLen = sizeof(versionInfo);
break;
case Packet::Data::DailyInfo:
dataLen = sizeof(dailyInfo);
break;
case Packet::Data::JourneyInfo:
dataLen = sizeof(journeyInfo);
break;
case Packet::Data::MotorInfo:
dataLen = sizeof(motorInfo);
break;
case Packet::Data::DeviceInfo:
dataLen = sizeof(deviceInfo);
break;
case Packet::Data::Ready:
dataLen = 0;
break;
case Packet::Data::ErrorInfo:
dataLen = sizeof(errorInfo);
break;
default:
break;
}
break;
case Packet::Type::Command:
output.push_back((uint8_t)command);
switch (command) {
case Packet::Command::StartOTA:
dataLen = sizeof(otaDevice);
break;
case Packet::Command::StopOTA:
dataLen = sizeof(otaDevice);
break;
case Packet::Command::SetSettings:
dataLen = sizeof(settings);
break;
case Packet::Command::TurnOffMotor:
break;
case Packet::Command::Tap:
break;
case Packet::Command::DoubleTap:
break;
default:
break;
}
break;
case Packet::Type::Error:
output.push_back((uint8_t)error);
// errors are sent with a unique ID
dataLen = sizeof(errorId);
break;
case Packet::Type::OTA:
output.push_back((uint8_t)ota);
dataLen = dataLength;
/*
for (int i=0; i<dataLen; i++) {
printf("0x%.2X ", bytes[i]);
}
printf("\n");
*/
break;
default:
break;
}
for (int i=0; i<dataLen; i++) {
output.push_back(bytes[i]);
}
numBytes += dataLen;
output.shrink_to_fit();
return output;
}
std::vector<uint8_t> getBytes() const {
std::vector<uint8_t> b;
for (int i=0; i<maxDataLength; i++)
b.push_back((uint8_t)bytes[i]);
return b;
}
void setBytes(std::vector<uint8_t> b) {
for (int i=0; i<b.size(); i++) {
bytes[i] = (uint8_t)b[i];
}
}
void setMotorDistance(int d) {
distanceInfo.motorDistance = (uint64_t)d;
}
int getMotorDistance() const {
return (int)distanceInfo.motorDistance;
}
void setCaseDistance(int d) {
distanceInfo.caseDistance = (uint64_t)d;
}
int getCaseDistance() const {
return (int)distanceInfo.caseDistance;
}
private:
bool _valid;
};