Bluetooth Configuration - PushTracker/EvalApp GitHub Wiki

How the PushTracker Communicates


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]
  • 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.

Pairing

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).

Connecting

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.

Modes of Communication

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.

How the SmartDrive Communicates with the PushTracker


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.

Connecting

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.

Bluetooth Service Descriptors


SmartDrive Service Descriptor

<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>

PushTracker Service Descriptor

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>

Bluetooth Communications Description

#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;
};
⚠️ **GitHub.com Fallback** ⚠️