Every time I travel somewhere, I am concerned that my luggage might exceed the weight limit. With RAD Studio XE7 and the Wahoo Bluetooth Scale, I built a luggage scale application that allows me to weigh my luggage and store data about my luggage contents in the cloud.
The app UI consists of a TTabControl with 3 tabs:
- WeightScale - for weighing my luggage and saving the weight and associated notes
- Logged Data - for retrieving existing luggage data and displaying it in a list
- Settings - for displaying my bluetooth data
In addition to the various UI components, the app also contains a TBluetoothLE component, a TStyleBook component (for loading and assigning my custom Jet style) and the following BaaS components:
- TKinveyProvider (AppKey, AppSecret and MasterSecret match my Kinvey.com credentials)
- TBackendQuery (connected to TKinveyProvider)
- TBackendStorage (connected to TKinveyProvider)
- TRestResponseDataSetAdapter
- TFDMemTable
The scale I am working with is the Wahoo Balance scale.
Here is the Object Pascal code for my application:
unit WeightScaleForm1; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Ani, FMX.StdCtrls, System.Bluetooth, FMX.Layouts, FMX.Memo, FMX.Controls.Presentation, FMX.Edit, FMX.Objects, IPPeerClient, IPPeerServer, System.Tether.Manager, System.Bluetooth.Components, FMX.TabControl, REST.Backend.ServiceTypes, REST.Backend.MetaTypes, System.JSON, REST.Backend.KinveyServices, REST.OpenSSL, REST.Backend.KinveyProvider, REST.Backend.Providers, REST.Backend.ServiceComponents, FMX.ListView.Types, Data.Bind.Components, Data.Bind.ObjectScope, REST.Backend.BindSource, FMX.ListView, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf, REST.Response.Adapter, Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client, System.Rtti, System.Bindings.Outputs, Fmx.Bind.Editors, Data.Bind.EngExt, Fmx.Bind.DBEngExt, Data.Bind.DBScope; type TSensorContactStatus = (NonSupported, NonDetected, Detected); THRMFlags = record HRValue16bits: boolean; SensorContactStatus: TSensorContactStatus; EnergyExpended: boolean; RRInterval: boolean; end; TfrmWeightMonitor = class(TForm) BluetoothLE1: TBluetoothLE; TabControl1: TTabControl; WeightScale: TTabItem; History: TTabItem; Panel1: TPanel; Memo1: TMemo; Panel2: TPanel; btnConnect: TButton; lblDevice: TLabel; lblWeight: TLabel; btnDisconnect: TButton; Settings: TTabItem; ToolBar1: TToolBar; Label1: TLabel; ToolBar2: TToolBar; ToolBar3: TToolBar; Label3: TLabel; btnSave: TButton; KinveyProvider1: TKinveyProvider; BackendStorage1: TBackendStorage; AppTitle: TLabel; title: TLabel; lbs: TLabel; ListView1: TListView; BackendQuery1: TBackendQuery; FDMemTable1: TFDMemTable; RESTResponseDataSetAdapter1: TRESTResponseDataSetAdapter; FDMemTable1_id: TWideStringField; FDMemTable1Weight: TWideStringField; FDMemTable1_acl: TWideStringField; FDMemTable1_kmd: TWideStringField; BindSourceDB1: TBindSourceDB; BindingsList1: TBindingsList; LinkListControlToField1: TLinkListControlToField; FDMemTable1LuggageDetails: TWideStringField; ImageControl1: TImageControl; StyleBook1: TStyleBook; Image1: TImage; procedure btnConnectClick(Sender: TObject); procedure BluetoothLE1EndDiscoverDevices(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList); procedure BluetoothLE1CharacteristicRead(const Sender: TObject; const ACharacteristic: TBluetoothGattCharacteristic; AGattStatus: TBluetoothGattStatus); procedure btnDisconnectClick(Sender: TObject); procedure btnSaveClick(Sender: TObject); procedure LinkListControlToField1FilledListItem(Sender: TObject; const AEditor: IBindListEditorItem); procedure FormCreate(Sender: TObject); procedure HistoryClick(Sender: TObject); private { Private declarations } FBLEDevice: TBluetoothLEDevice; FWeightGattService: TBluetoothGattService; FWeightMeasurementGattCharacteristic: TBluetoothGattCharacteristic; procedure GetServiceAndCharacteristics; public { Public declarations } end; const ScaleDeviceName = 'Wahoo Scale'; ServiceUUID = ''; CharactUUID = ''; // 0x1901 is the weight service // 0x2B01 is the live weight characteristic readings 0x84ae0000 0001 - 38 lbs 17kg // 0b 10000100101011100000000000000000 Weight_Device: TBluetoothUUID = '{00001901-0000-1000-8000-00805F9B34FB}'; Weight_Service: TBluetoothUUID = '{00001901-0000-1000-8000-00805F9B34FB}'; Weight_Characteristic: TBluetoothUUID = '{00002B01-0000-1000-8000-00805F9B34FB}'; var frmWeightMonitor: TfrmWeightMonitor; implementation {$R *.fmx} {$R *.iPhone4in.fmx IOS} procedure TfrmWeightMonitor.btnConnectClick(Sender: TObject); begin lblWeight.Text := '0'; lblDevice.Text := ''; Memo1.Lines.Clear; BluetoothLE1.DiscoverDevices(3500, [Weight_Device]); end; procedure TfrmWeightMonitor.btnDisconnectClick(Sender: TObject); begin If FBLEDevice <> nil then BluetoothLE1.UnSubscribeToCharacteristic(FBLEDevice, FWeightMeasurementGattCharacteristic); btnDisconnect.Enabled := false; // disable disconnect from subscribe scale button btnConnect.Enabled := true; // enable connect to scale button end; //store luggage notes in the cloud procedure TfrmWeightMonitor.btnSaveClick(Sender: TObject); var LJSON : TJSONObject; ACreatedObject: TBackendEntityValue; luggagedetails : string; begin LJSON := TJSONObject.Create; if InputQuery('Luggage BaaS Cloud Store', 'Enter Notes about your Luggage', luggagedetails) then LJSON.AddPair('Weight', lblWeight.Text); LJSON.AddPair('LuggageDetails', luggagedetails); BackendStorage1.Storage.CreateObject('luggagecloud', LJSON, ACreatedObject); ShowMessage('Luggage weight and notes stored'); end; procedure TfrmWeightMonitor.FormCreate(Sender: TObject); begin BackendQuery1.Execute; end; procedure TfrmWeightMonitor.BluetoothLE1EndDiscoverDevices(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList); var I: Integer; begin // log Memo1.Lines.Add(ADeviceList.Count.ToString + ' devices discovered:'); for I := 0 to ADeviceList.Count - 1 do Memo1.Lines.Add(ADeviceList[I].DeviceName); if BluetoothLE1.DiscoveredDevices.Count > 0 then begin FBLEDevice := BluetoothLE1.DiscoveredDevices.First; ShowMessage('Connected to Wahoo Scale'); if BluetoothLE1.GetServices(FBLEDevice).Count = 0 then begin Memo1.Lines.Add('No Weight services found!'); lblWeight.Text := 'No Weight services found!'; end else begin Memo1.Lines.Add('Wahoo Services Found: '+ IntToStr(BluetoothLE1.GetServices(FBLEDevice).Count)); btnDisconnect.Enabled := true; // enable disconnect from subscribe scale button btnConnect.Enabled := false; //disable connect to scale button // get Weight Service and Characteristic GetServiceAndCharacteristics; end; end else lblDevice.Text := 'Weight Device not found'; end; procedure TfrmWeightMonitor.GetServiceAndCharacteristics; begin FWeightGattService := nil; FWeightMeasurementGattCharacteristic := nil; // get Weight Service by UUID FWeightGattService := BluetoothLE1.GetService(FBLEDevice, Weight_SERVICE); if FWeightGattService <> nil then begin memo1.Lines.Add('Service: '+FWeightGattService.UUID.ToString); // get Weight Characteristic Memo1.Lines.Add('Looking Char: '+Weight_CHARACTERISTIC.ToString); BluetoothLE1.GetCharacteristics(FWeightGattService); FWeightMeasurementGattCharacteristic := BluetoothLE1.GetCharacteristic(FWeightGattService,Weight_CHARACTERISTIC); if FWeightMeasurementGattCharacteristic <> nil then begin Memo1.Lines.Add('Char: '+FWeightMeasurementGattCharacteristic.UUID.ToString); // subscribe to the weight service BluetoothLE1.SubscribeToCharacteristic(FBLEDevice, FWeightMeasurementGattCharacteristic); Memo1.Lines.Add('Subscribed to Weight Measurement Characteristic'); end else begin Memo1.Lines.Add('Weight Char not found'); lblWeight.Text := 'Weight Char not found'; end end else begin Memo1.Lines.Add('Weight Service not found'); lblWeight.Text := 'Weight Service not found'; end; end; //query BaaS provider for existing luggage data procedure TfrmWeightMonitor.HistoryClick(Sender: TObject); begin BackendQuery1.Execute; end; //display weight icon next to each row in TListview procedure TfrmWeightMonitor.LinkListControlToField1FilledListItem( Sender: TObject; const AEditor: IBindListEditorItem); var LItem: TListViewItem; begin LItem := AEditor.CurrentObject As TListViewItem; LItem.BitmapRef := ImageControl1.Bitmap; end; procedure TfrmWeightMonitor.BluetoothLE1CharacteristicRead(const Sender: TObject; const ACharacteristic: TBluetoothGattCharacteristic; AGattStatus: TBluetoothGattStatus); var LSValue: string; HexValue: string; WeightPounds : double; begin if AGattStatus <> TBluetoothGattStatus.Success then Memo1.Lines.Add('Error reading Characteristic ' + ACharacteristic.UUIDName + ': ' + Ord(AGattStatus).ToString) else begin LSValue := IntToStr(ACharacteristic.GetValueAsInteger); Memo1.Lines.Add(ACharacteristic.UUID.ToString + ' String: ' + LSValue); // calculate weight - characteristic is in hectograms WeightPounds := (ACharacteristic.GetValueAsInteger shr* 0.2205; Memo1.Lines.Add('Weight (pounds): '+Format('%8.2f',[WeightPounds])); lblWeight.Text := Format('%8.2f',[WeightPounds]); end; end; end.