Contents
Published: April 22, 2024
40 min read
In this article, you'll learn:
1
⚙️ BLE Hardware Details
2
🛠 How to Setup Bluetooth/BLE in React Native
3
🗂 Major Fitness Protocols: Similarities & Differences
4
📱 Top Things to Keep in Mind when Enabling Multiple Protocol Support
5
✅ How to Process Workout Responses
6
🛑 Protocol-Related Issues
7
🔄 Reverse Engineering: Getting BLE Logs Based on the Existing Apps
8
🔧 To Sum Up: How to Create an App for Fitness Device in React Native?
9
👂 Takeaways
The COVID-19 pandemic significantly changed our fitness habits, sparking interest in how to build a yoga mobile app, as now more people prefer exercising at home with their choice of equipment, including yoga, to maintain their physical and mental health.
In 2023, the worldwide revenue of the fitness app market was $5 billion, and it's projected to double this number by 2028. According to Statista, 833.3 million users worldwide have used fitness apps in 2023. This number is expected to grow to 1,048 million by 2028. This exponential growth is a compelling motivation to develop a complete React Native fitness app, don’t you think?
The fitness apps market continues to show a steady increase in users worldwide (image by Statista)
Today, more and more equipment for workouts has Bluetooth in place. This equipment can be used in various types of applications, including corporate fitness app development, to enhance the user experience and provide more personalized fitness tracking and guidance. Thus, you might be tempted to learn how to build a fitness tracking app that enables users to connect to those devices, track their progress, and control their fitness devices using a smartphone.
In this article, we’re going to take a look at how to create a wellness app with Bluetooth connectivity in React Native, find out the differences between major protocols, the ways to identify the protocols & the device itself, and, of course, to control it. Then, we’ll discuss the potential pitfalls each protocol has and how we can overcome them. Lastly, we’ll show you how we implemented a fitness app idea in React Native for our client.
We’re not going to have all the code written in this article because it’d take hours to read, and the actual ways you overcome the pitfalls might differ, depending on your tech stack and personal wishes.
This is a detailed guide for all developers involved in Bluetooth app development, particularly those connecting fitness devices to the React Native workout app or simply willing to learn more. Even though we write our code in React Native, most of the article code should be platform-agnostic, so you can skip the React Native part and get to the protocols themselves.
We've already described how Bluetooth works in our blog. Please take a look to revise your knowledge.
To keep it simple, we have a lot of different BLE devices. Each of them has unique services, descriptors, and characteristics to work with. Let’s take a closer look at this structure:
BLE device structure (shots from O’Reilly)
The device has a few important fields, which could help us working with it:
Name | You can show the name somewhere on your UI for the users. You might also be tempted to filter all the devices by name only, but it's not the best way, especially for fitness devices. In this industry, the devices are made by a lot of different manufacturers, and they might give them any name they want. Thus, be careful with it. |
ID | It is a unique ID between the phone and the fitness device. It doesn't match between two different phones. However, you can save it locally so that the next time you see a device with this ID, you understand it's the device you need. |
What is important for us to correctly manage the Bluetooth state is understanding how to work with Characteristic — the main gateway for communication between Client (our iOS and Android app) and Server (BLE device). The device might have a lot of different characteristics, and they contain a few important fields for us:
That's the stable UUID, that you can use to distinguish characteristics among others. It always has the very same form, and only 4 symbols ({XXXX}) differ in every characteristic: 0000{XXXX}-0000-1000-8000-00805f9b34fb
One of the most widely-used UUID is 0000fff1-0000-1000-8000-00805f9b34fb. However, keep in mind that if you find a BLE device, which has this characteristic UUID — it doesn't always mean it's the device you’re looking for. Thus, you might end up connecting to unnecessary devices just because they have the very same UUID.
This field determines whether we can send data without the response (characteristic wouldn't let us know when it processed the command). If true, we can call writeWithoutResponse
.
This field determines whether we can send data with the response(characteristic would let us know when it processed the command). If true, we can call writeWithResponse
.
In React Native, .writeWithResponse(data)
returns Promise<Characteristic>
This field determines when we can receive notifications from the characteristic (for instance, for some devices, we receive a real-time update that the workout is in progress). If isNotifying
is true
, we can subscribe to the monitor
function and pass our handler to it:
This field determines if we can read the characteristic's value. Some characteristics don’t notify us of any changes, however, we still want to get data. In this case, if isReadable
is true, we can at some point call .read()
It's probably the most important field for characteristics. It contains a base64
string, which could be parsed. Whenever we call any .write
, underneath the characteristic changes the value. Whenever we call the .read
, we receive the value itself.
It doesn't matter whether we write or read data, we work with Characteric's value which is a base64-string representation of Uint8Array.
To make transformations we use base64-js:
In case we need to write data for | In case we received a |
What byte array we send in, and how we parse the response — heavily depends on the exact device and protocol it works with.
Understanding the complexity of BLE hardware is crucial when estimating the cost to make a Fitbit or any similar fitness app, as device connectivity can add complexity to the fitness and workout app development process.
What about Expo? As of autumn 2024, Expo doesn’t offer you to work with BLE. To make sure your app can communicate with Bluetooth, you need to eject your workout app and continue as a bare React Native app.
As for now, we suggest using react-native-ble-plx as the best solution for making the connection between a React Native app and a BLE device. Please note that later on, the library might differ or we might have another competitor. Though, it shouldn’t change the logic of how we approach the task.
Please follow the installation guide for react-native-ble-plx:
When working with Bluetooth, we have a lot of things to cope with:
Due to the hardware limitations, we want scanning to run only once for our React Native fitness app. Because if we stop-start scanning in the application, we end up in a situation where we break the Bluetooth connection.
Additionally, we want our listeners to always be up-to-date. We also want to avoid possible race-conditions and issues with state, when updating it and having a new value only in the next render, which could break the app in this render.
Having these 2 ideas in mind, we understand that React doesn't really work well with it. States would lead to endless re-runs of the scanning process. Our listeners wouldn't be up-to-date, and there may be a lot of other possible issues.
We had some strict requirements for fitness device integration (image by Jordon Cheung)
Of course, we could "hack" React: either use the refs all the way or just make all the dependencies empty. However, such manipulations make the code very hard to read and difficult to maintain since any possible change could break the way we work with it.
The best solution we see is having a special class, which handles all the hardware computation and inner management, and takes a few dependencies to update React state. We also create a helper hook, which wraps the class, and then, we make sure we can work with the dependencies and have state changes.
Let's take a look at how we can do it:
We'll use basically the same approach across all modules we create in our fitness app React Native.
Now, as we've talked about some theoretical structural things, let's get to handling different important cases.
Permission request differs for both iOS and Android:
ACCESS_FINE_LOCATION
. Right now, they also need BLUETOOTH_SCAN
and BLUETOOTH_CONNECT
. For instance:
Sometimes it might be important to understand if the users actually have Bluetooth/location on, so you could prompt them to do so.
You can use the react-native-connectivity-status module for it.
Thus, our useBle code would look something like this:
Imagine a user got to your React Native gym app without location/Bluetooth turned on the mobile device. You show them an alert, saying they need to turn it on. Then we need to enable the scanning back and "unlock" the Bluetooth from the user.
We can use react-native-connectivity-status
for it as well:
Now that we've covered all the possible permissions and hardware issues, related to Bluetooth or location (on Android) being unavailable, let's move on to the next topic — device scanning.
To perform scanning, we first need to create a react-native-ble-plx
manager:
We need to create it only once, right at the start of the React Native fitness app development with BLE mobile app development services, because if we created it multiple times — Bluetooth wouldn't work well.
We've already discussed how we want to structure our scanning — we do it inside a separate class. The scanning will start only once, and all the outer dependencies will be replaced using refs. We believe it’s the best approach, however, if you'd like to keep it all inside the React hook (which we don't recommend doing), please take care of the following aspects:
startDeviceScan
(because, for instance, dependencies changed), we need to call .stopDeviceScan
. If we don't do it, we fall into a memory leak.If you're not careful with the dependencies, you might have too many dependencies and .startDeviceScan would be excessively triggered. You can't afford it, and you need to be careful with it in your React Native workout app to make sure your Bluetooth connection is stable.
Scanning can be tricky but we know how to handle it (image by Murat Gursoy)
What can you do?
state
and you need to update it — consider using setState(prevValue => newValue)
syntax because in this case, you don't need extra dependencies with useEffect
.useRef
. It doesn't lead to rerenders, and you don't need to add it to your useEffect
dependencies.Now, let's get back to how we could handle it as a separate class. We need to keep a few things in mind:
addItemToList
in the example is a custom function, which makes sure we have no duplicates.
Please note that these 2 functions (changeOnSyncDevices, changeOnBleError) are custom changeOn functions, which are updated within useEffect. We need it because the dependencies might change, and it won't affect the constructor. We need custom useEffects to make sure we can update the handlers and listeners as we progress inside the React Native fitness app.
You might've noticed that we pass the allowDuplicates here. We need it for a corner case. If we don't pass it, iOS wouldn't scan the same device if, for instance, you connected it, then disconnected, and then connected once again. iOS would consider it as the same device ➡️ it wouldn't let us know it's reconnected.
However, with this code, we might fall into the case when we connect to the same device a lot of times. To make sure it doesn't happen, we can add a field to the class, responsible for storing, whether a device is connected with it or not. Also, we need to handle device disconnections as well to make sure we don't store any non-available devices.
You can apply any additional logic you want regarding scanning/connection. Just one thing to keep in mind — avoid extending your dependencies.
Looks great!
Inside your React Native gym app, you'll often see a few screens that require Bluetooth connection and work with devices — and a few ones that don't, which could be an important factor in your IoT project cost estimation. Thus, you might be tempted to start all the scanning operations on specific screens because it eases the process of retrieving/showing the data.
We wouldn't advise doing it because, once again, multiple scan-stops might break scanning at all. Therefore, it'd be better if you wrapped all your code inside a context. Yet, you should somehow understand whether you should actively scan right now or not. You can use another isActive
field inside a class. To make sure it's up to date, you can use the listener updates within our hook.
Using this code above, on every screen you call useBleContext
, active
would be set to true, and all the scanning would process automatically. Once you leave this screen or go to another tab, active
is set back to false, and no scanning happens. For more information on managing Bluetooth connections in your app, check out our Web Bluetooth tutorial.
You can treat protocol as a special language for communicating between your complete React Native fitness app and BLE device. It determines a few things:
.write()
with some data).In our guide, we'll talk about 3 different protocols used when working with fitness devices and how they can influence the health and fitness app retention rate:
Let’s first talk about each of them in general, and then learn how we can seamlessly build them into the React Native workout app, avoiding extra pitfalls and headaches.
FTMS Protocol seems to be a protocol with the biggest number of available features. Here are some of the most important ones:
Few characteristics to write to Change level characteristic( | Receives workout changes via notification We monitor device characteristics and receive real-time updates. We don't need to do anything to receive a notification |
Has a characteristic to read from the device’s general data For example, whether it has an incline, resistance management, and how many levels are available ( | Enabled subscriptions Using a few characteristics you can subscribe to receive info about device/current resistance/whether the workout is started |
Big number of situations when we can send a request: 1️⃣ Establish a connection | Has a lot of characteristics to subscribe to receive workout info — each for every type of device Rower ( |
Last but not least — how we parse the received characteristic value. Imagine we received a notification from ROWER (2ad1
). As always, it's a base64 string which we can then parse to an array of numbers. However, what we receive in an array depends on the first two bytes:
[CONTROL_BYTE_0, CONTROL_BYTE_1, VALUE_BYTE_0, VALUE_BYTE_1, ....]
That's how it could look: [12, 0, 13, 6, 5, 3]
;
The important question for us is: how do we understand what [13, 6, 5, 3]
stands for?
It depends on the values we see in the first two control bytes. To work with it properly, we need to convert CONTROL_BYTE_0 and CONTROL_BYTE_1 to the binary system — [1100, 0]
. Then, we need to take a look at each bit and check its value. The way you can do it is described in the documentation. Here’s a quick example!
For instance, our protocol says that if on bit 0 we have value 1 — we have 2 bytes of speed:
Remember that we take a look at the binary string in reverse. So when we want to know bit 1 - we need to do "0011"[1]
instead of "1100"[1]
Let's sum up what we have for now:
| ➡️ | we don't have speed |
| we don't have distance | |
| we have 2 bytes of calories | |
| we have 2 bytes of energy |
Then we go step by step and take a look at our values — [13, 6, 5, 3]
. Because the first two bytes to take would be the "calorie" bytes — the calories are 13 + 6
. Then, we have 2 bytes of energy, and we start not from the beginning, but from the end of the previous take. It'd be 5+3
. Simply speaking, we shift our values.
RESPONSE: speed - not set, distance - not set, calories - 19, power - 8
However, if on CONTROL_BYTE_0 the value is 10 instead of 12 — we wouldn't have calories. If we turn 10 to the binary we receive 0101
, which means that instead of calories we have distance:
RESPONSE: speed - not set, distance - 19, calories - not set, power - 8
Note: that’s just a basic example. Most of the time, the output value is not a sum. You’d rather have bytes summed with some multipliers. Therefore, this example is simplified on purpose.
So what functionality do we expect from our protocol?
.write()
to different characteristics under different circumstances.
As for now, it seems to be a very niche protocol. You can access its documentation here. Despite offering a range of important features, their number is quite limited, yet, different from the FTMS one.
Let's take a look at the main capabilities of the protocol:
It has a single characteristic for both writes and monitors (fff1
). All the requests should be correctly prefixed so that we understand we work with Delightech.
Even though we have one characteristic for all Delightech operations, we have different goals for writing. We distinguish the needed goal by our payload:
64
as the first byte of the payload. This command includes information on whether the workout has started; connect, pause workout; user info (like weight), etc.32
as the first byte of the payload. We usually run this command every second — it means that every single second we’ll receive a notification. Yet, we can change this number to any other. For example, we can send this command every 10 seconds, and, thus, receive a notification every 10 seconds.Notification is sent only when we send the write command. Basically, instead of subscribing to a real notification, we ping our BLE device every second.
To sum up, there are only 3 possible situations when we send requests to a BLE device:
Each fitness app should show the relevant information about the workout (image by Nexique)
Let’s talk a bit about parsing the values. Instead of shifting our values, depending on the first bytes, we ALWAYS expect them in the same positions. For instance, if we expect calories — they'll always be sent as 6th byte, no matter whether we have distance or speed or any other field.
However, we have some exceptions here. Some fields might be empty for particular devices (for instance, when we run — we don't receive any rpm; or we won’t receive heart rate if we don’t use a heart rate sensor). We understand what fields are sent by taking a look at the "CONTROL_BYTE
". Imagine the following notification:
[CONTROL_BYTE, SPEED_HIGH, SPEED_LOW, CALORIES_HIGH, CALORIES_LOW, DISTANCE_HIGH, DISTANCE_LOW]
As you can see, we always receive the speed, calories, distance, etc. in the same position. However, if, for instance, bit 5 of CONTROL_BYTE is 1 — we omit CALORIES_HIGH
, CALORIES_LOW
. We don't take a look at them, at all.
The last thing — we can take a look at the notification we receive, and based on it, understand what type of device we work with (bike/treadmill/rower/etc). Some fields also depend on what type of device we work with.
.write()
to the same characteristic under different circumstances (with different values)..read()
from any fields..write()
to receive a notification. We don't shift any values, but read them, depending on CONTROL_BYTE
.
Documentation on this protocol can be found here. However, it’s in Chinese so there might be some issues understanding it. English simplified version can be found here. As before, let’s review some key points of the protocol.
First thing first, we have 2 different characteristics to work with. All the values we send/receive from characteristic should be prefixed with 2 first bytes — [0x02, 0x42-0x45]
. The second byte marks the exact command we want to send/parse.
Also, we have a special characteristic to write to (fff2
). Even though for all the writes we have a single characteristic, we have multiple possible occasions to send write:
The connection of the fitness app with the BLE workout device keeps track of the workout performance (image by Alexandr Fesun)
When using this protocol, we don’t have any characteristics to read and have 1 to subscribe to notifications.
Workout notification is sent only when we send the write command, just like in Delightech. Basically, instead of subscribing to a real notification, we ping our BLE device every second.
Value parsing also works the same way as Delightech: instead of shifting our values, depending on the first bytes, we ALWAYS expect them in the same positions. For instance, if we expect calories, they'll always be sent as the 6th byte, not taking a look at whether we have distanceб speed or any other field.
Yet, there are a few differences worth mentioning:
As you remember, we send 2 requests, and we receive 2 notifications regarding the workout. They're prefixed with different bytes. For instance, we'll always have speed
if the notification is prefixed with [0x02, 0x42, 0x02]
. No need to look at the specific bits.
We can't know the exact device. And the exact device doesn't change the response we receive.
.read()
from any fields..write()
to receive a notification. We don't shift any values, but read them, depending on a few first bytes.
Since now we know key points about each of the protocols, we want to make our fitness app React Native efficiently work with any of them. To make it happen, we need to work on a few crucial things:
We need to send specific write commands in specific situations, attaching maybe dynamic values (for instance, resistance, weight, or something else). These commands are mostly different for each protocol.
We need to parse the response and correctly understand what exactly our device has sent to us. It depends on the protocol because the responses have nothing in common.
Let’s take a closer look at these steps!
Each protocol has something unique, which could help us highlight it among the devices.
The protocol uses very unique identifiers. For instance, its bike- or rower-specific identifiers aren’t used anywhere else. Thus, if we receive a notification from a characteristic or read a characteristic with some FTMS identifiers — we can be sure it's FTMS.
Both of these protocols use the same identifier. Moreover, this UUID (fff1
) is widely used among other Bluetooth devices. Since we can't rely on the identifier itself, we should look at the value we receive — protocols’ responses are always prefixed with the same fields.
We can then basically write a protocol interface and basic implementation:
Here we state that the protocol might have an array of possible characteristic UUIDs, using which we can determine the specific protocol we work with. Also, the protocol might have an array of possible characteristic UUIDs or an array of prefixes for the protocol. Using either of them, we also can determine the protocol.
Also, we can specify protocol.helper.ts, which would take a Protocol
, and provide us with a way to understand the device is of Protocol:
Later on, we can use the ProtocolHelper
s inside our AppDevice
to know if the device is of Protocol
:
Having done this, we'll always have an available #currentProtocolHelper
inside our AppDevice
, which would make all the data parsing / .write()
sends. If we don't know the #currentProtocolHelper
— it means we weren't able to understand what protocol we work with. In this case, we can show some errors with a timeout.
First thing first, let's agree on what we mean by “particular commands”. In fact, it's just a write command:
A list of possible commands we might send consists of:
Each of these CommandType
is used at least in one or more protocols. Inside our AppDevice
, we specify our intention to send some information to the protocol.
However, that’s where the tricky part happens. To send a command, we need to understand what protocol to send it to. At the same time, some protocols (Delightech and Fitshow) don't respond until we send the CONNECT
command. How do we handle this chicken and egg situation?
Well, we could send the commands to all the possible protocols if their identifiers are available. So if we send the FTMS-like data to Delightech, they’d just ignore it. However, if we send Delightech data to Delightech — we receive a Delightech response and, thus, can use ProtocolHelper
afterward. Simple as that!
Let's firstly update our protocol to let us know what command it can handle:
Here, for a Protocol, we determine commands by creating a record, that contains all the CommandType
s we want to send to the protocol. We may not want to send one of the commands, that’s why it's Partial
. We also may send a few requests within one command (for instance, for Yeekang/Fitshow/Thinkfit every second we send 2 write requests to receive a workout notification).
Command
consists of the UUID of characteristic to send to (for instance, fff1
), and an array of values. Each value can have a few different fields:
Hardcoded fields | For instance, within the write command, we want to have 0x02 at the first byte, and 0x40 at the second byte |
Dynamic fields | Can be target speed, resistance, weight, height, whether a workout has started, etc. We'll talk a bit later on how to pass them |
Checksum fields | For some protocols, we want to have at some byte a sum of all the bytes before. That's why we have a separate field. We also pass the starting index because the checksum might not always start from the first byte (depends on the protocol) |
Here's an example of how it can look:
Then, we need to map these commands for every protocol to a definite array of numbers, which we could later use with Bluetooth:
And, finally, we can use it inside the AppDevice
. Remember that if we don’t know the protocol for sure and send a command — we basically send it to every protocol.
You should be careful and send a lot of events with a delay. Android doesn't handle multiple .write
s at a time well. The delay should be at least 200ms.
At this point, we have everything we need to work with different devices. We learned how to:
For now, there’s one last piece left to make it work (except for UI, state management, and all the unnecessary things 😉). Let’s figure out how to correctly process the workout response!
First of all, let's agree on our goals. The idea of this article is to teach you:
It's important to correctly parse notifications, considering all the differences and similarities (image by fajr fitr)
To correctly parse notifications, we need to understand what distinctions and similarities notifications of different protocols have:
Since we receive data under different characteristics, they have different IDs. Thus, we need to have it configured somehow.
Each field (speed, calories, distance, etc.) might take up to 4 bytes. The output value would be a sum of all these bytes with some multipliers. For instance, we can have 2 bytes for the distance — [1, 250]
. The first one will have a *1000
multiplier, and the second byte — a *1
multiplier. So to find how far we’ve got, we need to sum 1 * 1000 + 250 * 1 => 1250meters
. We need to be able to mark multipliers inside our code.
Different protocols can send data in either decimal or hexadecimal system. We need to make our React Native gym app handle the field.
The last important thing — where and when you should find the exact byte. As you remember, it differs a lot between Fitshow+Delightech and FTMS. In Fitshow+Delightech, we can always find the value on the same byte(s), if we have the required bytes/bits on the first few indices. In FTMS, the position of, for instance, distance, not only depends on the required bytes/bits for the distance, but also on the bytes/bits of the previous fields.
Talking about Fitshow/Delightech, the solution is straightforward here. We need to add an additional field, telling which bytes should satisfy the response before we save the new value. For instance, we can add a separate BytePosition interface. It might also take the array of bits within the byte we need to satisfy the rules:
The field might look something like this:
Here we state that we want to save a new speed, if we received a notification:
'fff1'
;Also, we want to have this field:
[10, 1]
.Let's take a look at the example of how it could look:
| We wouldn't save it because the 0x8 has the 0 on 2 index and 1 on 3 index |
| We would save it because all the rules match. Using the multipliers, the new speed would be 110 km/h. Pretty fast, huh? |
| We wouldn't save it because of the UUID mismatch |
It works just fine for Delightech and Fitshow as it allows to correctly understand the values. However, developers have identified a few flaws with the FTMS. As you might remember, we have value shifts for the FTMS. It means that, for instance, if we have speed, we will have the distance at byte 4. However, if we don't have speed, we will have the distance at byte 2. And it goes crazier the more combinations we have.
In React Native development, there are 2 possible solutions:
For instance, we pass the characteristic array, the UUID, and all the fields we have, and it goes one by one in an array (starting with the first value), increasing the startingByte
for all the next ones. Thus, we'll add a separate field sholudShift
or something like this. It's a working solution, that doesn’t have any serious downsides.
However, what I don’t like about it is that we lose the flexibility and divide our parsing into 2 different pipelines, with only 1 flag changing the behavior. In case we have another protocol with something in between — it might not work that well.
For instance:
And we'll have several rules like these for each field. Then, during the parsing, we'd just find the appropriate matchRule, and if we do find any — look at the startingByte and make the computations.
With this implementation, our code for Fitshow, Delightech, and FTMS looks basically the same. The only difference is that FTMS is much bigger 🙂
You can choose either of these solutions, but we used the second one, and didn't see any issues with it so far.
Let's take a look at how our mapper could look:
Here we have a simple switch on whether the field should be offset or not. If yes, we go through the positions, adding 1 rule to the existing matchRules and increasing the startingByte. Basically, the situation when we have ONLY this field (for instance, the speed at position 2) changes to the situation where we have every possible combination of fields. For instance, speed, distance, calories, speed, and RPM. In this case, speed is at position 10.
Let's take a look at how our Protocol might look:
Using the deviceFields
, you can understand what fields you can work with and save them once you understand the protocol. Let's finalize and see how it could work:
Here we introduce 2 new functions — getNewDeviceField
and getNewValue
. We use them to:
At this point, I think we have everything to make sure we are building a fitness app React Native that correctly processes all the protocols, saves the values, understands the protocols, and has no issues adding another protocol as it won’t change any line of the code. In fact, we could even bring the protocols to some outer API and load it on demand to lower our app bundle.
Still, there are quite a few protocol-related issues, which we'll briefly discuss in the next part.
However, the disconnection process is already in progress. It means that the device will anyway NOT respond to our calls and won’t let us know it's off. Instead, we'll just behave with the device as if it's turned on, meanwhile it's not. The solution here is to have a delay between disconnecting and accepting the same scanned device.
The solution here is to catch the paused behavior and store the values which need to be added up in a special state. Later on, we sum up the values before and after the pause. You can add a custom isAccumulative
field to DeviceField
to make it reusable.
You can automatically detect if the workout is paused. When the workout is paused, all the data is passed as 0. However, if in case of Delightech, when the workout resumes it stays at 0 and increases over time, for FTMS it’s brought back to the initial value when unpaused. It means that you should avoid these 0 values only when paused. As always, you can add an additional field to DeviceField
.
Even though we have a separate characteristic for each device type, not all the manufacturers follow this rule. You might have a few notifications from different characteristics within one device. For instance, if you exercise on a bike, you might receive values from bike, rower, and cross-trainer characteristics.
FTMS doc doesn't really share the characteristic UUID with us. You can take a look at them here.
There are many more interesting things regarding FTMS. If you want to dig into it, take a look at this article and check the comment section as well. It helped us a lot to sort out a lot of questions.
Don't send too many write requests to receive more real-time updates. If you send requests more often than once a second — your device ends up disconnected. One second is a preferred interval.
The only important difference from what we expected to see based on the docs — we noticed was the wrong data inside the protocol PDF file. To "obtain movement" (distance/calories/count/time) the protocol says the first bytes are [0x02, 0x43, 0x01]. However, it was [0x2c, 0x0b]. We're not sure if it's true for all the Fitshow/Thinkfit devices, but it might be useful to take a look at both prefixes and work with them.
Right now you have all the information you might need regarding the architecture, setup, and protocols when creating a React Native gym app for fitness devices. The last thing to cope with is making the correct mapping for the devices. You can use the protocol, although it might not always be easy for you to understand what command follows what response.
If you have a device in place, you can connect using any existing fitness-related app (or using Sportplus 🙂) to your device, and record all the Bluetooth logs. Here's a short guide on how to do it.
In case you want to know more about the integration of BLE devices into your React Native fitness app, you can read our article dedicated to this topic:
You can, later on, go through the logs using Wireshark and understand all the Bluetooth commands you need to send and receive. You need to have a fitness machine for that and an Android device (not available on iOS).
In case you don't have either of that, we would be glad to share with you a list of our logs here.
Now, we'd like to explain how our developers apply these skills in practice. So, we’ll share our expertise in making a complete React Native fitness app for SportPlus. Recognizing the growing demand among customers to connect their equipment to mobile devices, the client tasked us with the creation of an app compatible with a variety of fitness devices.
SportPlus app enables smooth connection with fitness equipment (image by Stormotion)
Building a React Native workout app for fitness devices is a journey that encompasses various stages, from protocol validation and UX wireframing to frontend development and quality assurance. Drawing from our experience with SportPlus, we’ll provide insights into each step of developing such an application.
We decided to start with protocol validation since the success of the app lies in its ability to communicate effectively with the supported equipment. Initially expecting a single protocol, we soon discovered that the reality was far more complex, with 3 distinct protocols in play. However, we swiftly came to an effective approach.
Our solution? A generic protocol handler. This system enables the fitness application to identify and adapt to the specific protocols used by different fitness devices. This involved parsing unique identifiers and special data within characteristic values to identify the protocol, ensuring that the app interacts only with compatible devices.
Upon receiving a notification from one of the protocols, our application expects the equipment to belong to that protocol and operates exclusively with it. However, in cases where no response is received within a certain timeframe, we deduce that the device is not a compatible training machine and initiate a disconnection.
The generic protocol handler helps the fitness app find the right device by checking either characteristic UUID or characteristicArray. Let's summarize and see how it works in our case:
So, what sets our generic protocol handler apart?
The emphasis on flexibility and adaptability. We understand the importance of future-proofing our application against evolving protocols and device standards. By doing so, we ensure that our application can seamlessly integrate with new workout equipment, without requiring significant redevelopment.
The focus on versatility and reliability. The generic protocol handler is equipped with commands tailored to each of the three protocols we encountered. This means that even in scenarios where the specific protocol in use is unknown, our application can transmit data to all possible characteristics in the format expected by each protocol.
So, a desire for simplicity and maintainability guided our decision to implement a generic protocol handler. Rather than navigating a complex maze of if statements tailored to each protocol, we opted for a streamlined approach that promotes reusability and scalability. This abstraction shields our application from the intricacies of individual protocols, allowing for smoother integration and easier maintenance in the long run.
In order to prepare the relevant design of the fitness app React Native, we began UX wireframing. This crucial step involves creating a blueprint of the application's layout and navigation flow, laying the groundwork for intuitive user interaction.
Since the success of a fitness device application relies on UX, we prioritize wireframing to ensure that every aspect of the interface serves a purpose. By mapping out the various screens and functionalities, we gain valuable insights into how users will navigate through the app and interact with its features.
UX-wireframing of the fitness app ensures a seamless user experience (image by Stormotion)
During the wireframing phase, our focus is on simplicity and clarity. We aim to create a clean and uncluttered layout that allows users to easily access the information they need. By establishing clear navigation paths and logical screen transitions, we ensure that users can navigate the app with minimal effort.
In essence, UX wireframing serves as the foundation of the design process, laying the groundwork for a user-friendly and intuitive application interface.
With the wireframe blueprint in place, we transition to UX / UI design, where we bring our concepts to life through visual aesthetics and interactive elements. This stage involves refining the wireframe layout and adding visual elements to create a polished and engaging user interface.
Understanding that technical functionality is just one piece of the puzzle, we prioritize UX / UI design to ensure that the application not only works well but also looks and feels appealing to users. Our goal is to create a seamless and immersive user experience that encourages users to engage with the app and achieve their fitness goals.
Central to our design philosophy is the concept of user empowerment. We aim to give users the tools they need to take control of their fitness journey, whether it's setting personal goals, customizing workout routines, or tracking performance metrics over time. Every aspect of the UI design is carefully crafted to improve usability and facilitate user interaction.
If you’d like to know how to create an engaging UI using React Native, you could read our article dedicated to this topic:
Also, we provide comprehensive services for building a personal training app from scratch, considering all your preferences.
With the foundation laid in protocol validation and UX / UI design, the next step was to bring our vision to life through frontend development. We transformed our design into functional components that seamlessly integrated with the underlying protocols and backend systems.
The frontend development of fitness apps is the culmination of the discovery stage and UX/UI design (image by Stormotion)
A key focus during frontend development was flexibility and scalability. We designed our fitness application architecture to accommodate future enhancements and changes, ensuring that it could adapt to evolving user needs and technological advancements.
By following best practices and leveraging the latest tools and frameworks, we were able to deliver a high-performance application that met the client’s' requirements.
No application is complete without thorough testing to ensure its reliability and performance across various devices and usage scenarios. Our QA process encompassed such testing types:
1. Functional testing.
We conducted extensive functional testing to identify and address any bugs or issues that could impact the user experience. It’s the main type of testing we used to verify functionally, from logging into the account to connecting the equipment via Bluetooth.
2. Non-functional testing, which includes such types:
Performance testing. Performance testing was another crucial aspect, ensuring that our application could handle the demands of real-world usage without slowdowns or glitches.
Compatibility testing. We tested our application against a range of fitness devices to ensure compatibility and consistency. And ensured our app functions work correctly across different devices and environments.
Usability testing. Usability testing aims to assess the user-friendliness and intuitiveness of software for its target users. This testing type is indeed crucial for ensuring that fitness applications meet user expectations and needs. This type of testing demands a significant level of experience from QA engineers who specialize in testing such applications.
Localization testing. We verified that the system functions work well when adapted to different languages, cultures, or regions, ensuring a consistent user experience across diverse audiences.
Please note that each fitness product requires a unique approach and testing types that are selected depending on customer requirements and specifications of the product.
At Stormotion, we're dedicated to finding solutions for every problem we encounter by exploring various approaches. For example, testing the active session flow of SportPlus app became more challenging due to the requirement of physical devices. However, we resolved this challenge by implementing mocked devices, enabling testers and customers to assess the features appearance and functionality, even without access to physical equipment.
Ostap Shtypuk, QA Engineer @ Stormotion
Certainly, except for the minor feature described above, we prioritized thoroughly testing the interaction between the equipment and the mobile application. In projects involving interactions between two devices, ensuring a stable connection and seamless operation is paramount. Additionally, it was crucial to test the connection of several pieces of equipment simultaneously. To verify such scenarios, we required multiple physical equipment or trainers to be present in one location.
The testing phase ensures a smooth connection between the fitness app and the BLE equipment (image by Stormotion)
Testing was crucial in SportPlus app development, like any other fitness app with connection equipment to mobile devices. Apart from confirming functionality, we prioritized ensuring Data Accuracy and Security. The data collected and displayed in the application (e.g., step count, calories burned, heart rate) must be accurate and reliable. Also, such apps often store users' personal data, such as name, address, physical metrics, health information, etc, so that's why we needed to identify potential vulnerabilities and ensure appropriate data protection measures are in place.
As a result, SportPlus has a user-friendly fitness app, and we’ve got one more happy client. So, with careful planning and diligent execution, any development team can create a complete React Native fitness app that resonates with users and drives business growth. Understanding the cost of fitness apps is also crucial to ensure the project's financial feasibility and long-term success.
To better understand what to expect, you can explore the price of Peloton app development, which ranges from $92,050 to $145,000 depending on features and platform choices.
As you see, React Native offers a powerful and efficient way to develop fitness mobile applications for BLE devices that work seamlessly across iOS and Android. Let’s summarize the key takeaways:
By understanding the core concepts of Bluetooth communication and integrating with relevant fitness device protocols, you can build a feature-rich React Native fitness app.
If you’re looking for a Tech Partner to integrate your application with a fitness device of your choice, drop us a line and we’ll see how we can help you!
Was it helpful?
Take a look at how we solve challenges to meet project requirements
We ensure code quality and maintainability in React Native projects through regular code reviews, automated testing, adherence to coding standards, and modular architecture design. Continuous integration and deployment pipelines also help maintain code quality by catching issues early and ensuring consistency across the codebase.
Cross-platform compatibility in React Native apps is achieved by utilizing platform-specific modules and APIs when necessary, employing responsive design principles, and testing on various devices and operating systems. Additionally, we rely on libraries like React Native Elements and React Native Paper to provide components that adapt to different platforms.
Integrating third-party services and APIs into React Native apps involves thorough research, understanding API documentation, and utilizing community-supported packages. We prioritize well-documented and well-supported APIs, ensure secure authentication mechanisms, and implement error handling and fallback strategies for robust integration.
We recommend incorporating features like personalized workout recommendations, social sharing functionalities for community engagement, and gamification elements such as challenges and rewards. Seamless integration with different BLE devices for comprehensive tracking will also help you stay ahead of the curve.
We meticulously follow platform-specific design guidelines and optimize performance through efficient rendering techniques and caching strategies. Extensive testing across a myriad of devices and OS versions ensures that our app performs flawlessly regardless of the user's setup.
We prioritize robust data encryption and security measures to safeguard user information. Obtaining explicit user consent for data collection and processing is a cornerstone of our privacy practices. Regular audits and updates to the privacy policies ensure ongoing compliance with stringent data protection laws such as GDPR and HIPAA.
Implementing secure payment processes for in-app purchases entails utilizing trusted payment gateways, encrypting sensitive user data during transactions, and adhering to PCI DSS standards for handling payment information. Additionally, implementing two-factor authentication and fraud detection mechanisms adds an extra layer of security.
Read also
Our clients say
When I was working with Stormotion, I forgot they were an external agency. They put such effort into my product it might as well have been their own. I’ve never worked with such a client-focused company before.
Alexander Wolff, CPO
Sjut