Skip to content

Commit

Permalink
Refactor ByteArray from std::string alias to class implementation (#319)
Browse files Browse the repository at this point in the history
* Refactor ByteArray from string alias to class implementation

- Converted ByteArray from an alias of std::string to a class implementation.
- Added multiple constructors to support initialization from various sources:
  - std::vector<uint8_t>
  - uint8_t pointer with size
  - std::string
  - C-string with and without null termination
- Implemented static methods fromHex() to construct from hex strings.
- Implemented c_str() method to return a C-string representation.
- Overloaded the stream operator to support pretty printing ByteArrays.

* Use an internal std::vector instead of inheriting from it

* Access directly to internal bytes of ByteArray

* Remove c_str methods

* Add slicing methods to ByteArray

* Fix data slicing in Windows backend

* Simplify ByteArray::fromHex

* Rename ByteArray::toHexString to toHex

so as to be consistent with fromHex

* Share ByteArray with simplebluez using common directory

* Remove unnecessary cast from ByteArray to string

* Extend use of ByteArray in simplebluez

* Revert changes in build script

* Remove subdirectory and rename namespace

* Allow hex prefix in 'fromHex' and clarify documentation

* Explicitly build ByteArray from py::bytes in python wrapper

* Add ByteArray to documentation

* Rename common dir to external

And rename kvn::ByteArray to kvn::bytearray

* Fix include path and format

* Use preprocessor guards instead of pragma
  • Loading branch information
tlifschitz authored Jul 27, 2024
1 parent 79ac0c9 commit 39cc358
Show file tree
Hide file tree
Showing 25 changed files with 532 additions and 67 deletions.
2 changes: 1 addition & 1 deletion docs/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.

INPUT = ../simpleble/include
INPUT = ../simpleble/include ../external/include

# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
Expand Down
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
html_static_path = ['_static']

# -- Extension configuration -------------------------------------------------
breathe_projects = { 'simpleble': '_doxygen/xml' }
breathe_projects = { 'simpleble': '_doxygen/xml',
'external': '_doxygen/xml'
}

autosummary_generate = True
11 changes: 11 additions & 0 deletions docs/simpleble/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Standard API
:members:
:undoc-members:

.. doxygentypedef:: SimpleBLE::ByteArray
:project: simpleble

Safe API
========

Expand All @@ -55,3 +58,11 @@ C API

.. doxygenfile:: peripheral.h
:project: simpleble

External API
============

.. doxygenclass:: kvn::bytearray
:project: external
:members:
:undoc-members:
7 changes: 0 additions & 7 deletions examples/simpleble/cpp/common/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
#include <iomanip>
#include <iostream>

void Utils::print_byte_array(const SimpleBLE::ByteArray& bytes) {
for (auto b : bytes) {
std::cout << std::hex << std::setfill('0') << std::setw(2) << (uint32_t)((uint8_t)b) << " ";
}
std::cout << std::endl;
}

std::optional<std::size_t> Utils::getUserInputInt(const std::string& line, std::size_t max) {
std::size_t ret;

Expand Down
4 changes: 0 additions & 4 deletions examples/simpleble/cpp/common/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ std::optional<SimpleBLE::Adapter> getAdapter();
*/
std::optional<std::size_t> getUserInputInt(const std::string& line, std::size_t max);

/**
* @brief Pretty print a ByteArray
*/
void print_byte_array(const SimpleBLE::ByteArray& bytes);
} // namespace Utils

#endif
6 changes: 2 additions & 4 deletions examples/simpleble/cpp/notify/notify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,8 @@ int main() {
}

// Subscribe to the characteristic.
peripheral.notify(uuids[selection.value()].first, uuids[selection.value()].second, [&](SimpleBLE::ByteArray bytes) {
std::cout << "Received: ";
Utils::print_byte_array(bytes);
});
peripheral.notify(uuids[selection.value()].first, uuids[selection.value()].second,
[&](SimpleBLE::ByteArray bytes) { std::cout << "Received: " << bytes << std::endl; });

std::this_thread::sleep_for(5s);

Expand Down
3 changes: 1 addition & 2 deletions examples/simpleble/cpp/notify_multi/notify_multi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ int main() {
peripherals[iter].notify(uuids[selection.value()].first, uuids[selection.value()].second,
[&, iter](SimpleBLE::ByteArray bytes) {
if (print_allowed) {
std::cout << "Peripheral " << iter << " received: ";
Utils::print_byte_array(bytes);
std::cout << "Peripheral " << iter << " received: " << bytes << std::endl;
}
});
}
Expand Down
3 changes: 1 addition & 2 deletions examples/simpleble/cpp/read/read.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ int main() {
// Attempt to read the characteristic 5 times in 5 seconds.
for (size_t i = 0; i < 5; i++) {
SimpleBLE::ByteArray rx_data = peripheral.read(uuids[selection.value()].first, uuids[selection.value()].second);
std::cout << "Characteristic content is: ";
Utils::print_byte_array(rx_data);
std::cout << "Characteristic content is: " << rx_data << std::endl;
std::this_thread::sleep_for(1s);
}

Expand Down
6 changes: 2 additions & 4 deletions examples/simpleble/cpp/scan/scan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,13 @@ int main() {
std::vector<SimpleBLE::Service> services = peripherals[i].services();
for (auto& service : services) {
std::cout << " Service UUID: " << service.uuid() << std::endl;
std::cout << " Service data: ";
Utils::print_byte_array(service.data());
std::cout << " Service data: " << service.data() << std::endl;
}

std::map<uint16_t, SimpleBLE::ByteArray> manufacturer_data = peripherals[i].manufacturer_data();
for (auto& [manufacturer_id, data] : manufacturer_data) {
std::cout << " Manufacturer ID: " << manufacturer_id << std::endl;
std::cout << " Manufacturer data: ";
Utils::print_byte_array(data);
std::cout << " Manufacturer data: " << data << std::endl;
}
}
return EXIT_SUCCESS;
Expand Down
4 changes: 3 additions & 1 deletion examples/simpleble/cpp/write/write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ int main() {
return EXIT_FAILURE;
}

SimpleBLE::ByteArray bytes = SimpleBLE::ByteArray::fromHex(contents);

// NOTE: Alternatively, `write_command` can be used to write to a characteristic too.
// `write_request` is for unacknowledged writes.
// `write_command` is for acknowledged writes.
peripheral.write_request(uuids[selection.value()].first, uuids[selection.value()].second, contents);
peripheral.write_request(uuids[selection.value()].first, uuids[selection.value()].second, bytes);

peripheral.disconnect();
return EXIT_SUCCESS;
Expand Down
200 changes: 200 additions & 0 deletions external/include/external/kvn_bytearray.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#ifndef KVN_BYTEARRAY_H
#define KVN_BYTEARRAY_H

#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <vector>

namespace kvn {

/**
* @class bytearray
* @brief A class to handle byte arrays and their conversion from/to hex strings.
*/
class bytearray {
public:
/**
* @brief Default constructor.
*/
bytearray() = default;

/**
* @brief Constructs byte array from a vector of uint8_t.
* @param vec A vector of uint8_t.
*/
bytearray(const std::vector<uint8_t>& vec) : data_(vec) {}

/**
* @brief Constructs byte array from a raw pointer and size.
* @param ptr A pointer to uint8_t data.
* @param size The size of the data.
*/
bytearray(const uint8_t* ptr, size_t size) : data_(ptr, ptr + size) {}

/**
* @brief Constructs byte array from a std::string.
* @param byteArr A string containing byte data.
*/
bytearray(const std::string& byteArr) : data_(byteArr.begin(), byteArr.end()) {}

/**
* @brief Constructs byte array from a C-style string and size.
* @param byteArr A C-style string.
* @param size The size of the string.
*/
bytearray(const char* byteArr, size_t size) : bytearray(std::string(byteArr, size)) {}

/**
* @brief Constructs byte array from a C-style string.
* @param byteArr A C-style string.
*/
bytearray(const char* byteArr) : bytearray(std::string(byteArr)) {}

/**
* @brief Creates a ByteArray from a hex string.
*
* Case is ignored and the string may have a '0x' hex prefix or not.
*
* @param hexStr A string containing hex data.
* @return A ByteArray object.
* @throws std::invalid_argument If the hex string contains non-hexadecimal characters.
* @throws std::length_error If the hex string length is not even.
*/
static bytearray fromHex(const std::string& hexStr) {
std::string cleanString(hexStr);

// Check and skip the '0x' prefix if present
if (cleanString.size() >= 2 && cleanString.substr(0, 2) == "0x") {
cleanString = cleanString.substr(2);
}

size_t size = cleanString.size();
if (size % 2 != 0) {
throw std::length_error("Hex string length must be even.");
}

bytearray byteArray;
byteArray.data_.reserve(size / 2);

for (size_t i = 0; i < size; i += 2) {
uint8_t byte = static_cast<uint8_t>(std::stoi(cleanString.substr(i, 2), nullptr, 16));
byteArray.data_.push_back(byte);
}

return byteArray;
}

/**
* @overload
*/
static bytearray fromHex(const char* byteArr) { return fromHex(std::string(byteArr)); }

/**
* @overload
*/
static bytearray fromHex(const char* byteArr, size_t size) { return fromHex(std::string(byteArr, size)); }

/**
* @brief Converts the byte array to a lowercase hex string without '0x' prefix.
* @param spacing Whether to include spaces between bytes.
*
* @return A hex string representation of the byte array.
*/
std::string toHex(bool spacing = false) const {
std::ostringstream oss;
for (auto byte : data_) {
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
if (spacing) {
oss << " ";
}
}
return oss.str();
}

/**
* @brief Slices the byte array from a specified start index to an end index.
*
* This method creates a new byte array containing bytes from the specified range.
* The start index is inclusive, while the end index is exclusive.
*
* @param start The starting index from which to begin slicing.
* @param end The ending index up to which to slice (exclusive).
* @return byte array A new byte array containing the sliced segment.
* @throws std::out_of_range If the start index is greater than the end index or if the end index is out of bounds.
*/
bytearray slice(size_t start, size_t end) const {
if (start > end || end > data_.size()) {
throw std::out_of_range("Invalid slice range");
}
return bytearray(std::vector<uint8_t>(data_.begin() + start, data_.begin() + end));
}

/**
* @brief Slices the byte array from a specified start index to the end of the array.
*
* This method creates a new byte array containing all bytes from the specified start index to the end of the
* byte array.
*
* @param start The starting index from which to begin slicing.
* @return byte array A new byte array containing the sliced segment from the start index to the end.
* @throws std::out_of_range If the start index is out of the bounds of the byte array.
*/
bytearray slice_from(size_t start) const { return slice(start, data_.size()); }

/**
* @brief Slices the byte array from the beginning to a specified end index.
*
* This method creates a new byte array containing all bytes from the beginning of the byte array to the specified
* end index.
*
* @param end The ending index up to which to slice (exclusive).
* @return byte array A new byte array containing the sliced segment from the beginning to the end index.
* @throws std::out_of_range If the end index is out of the bounds of the byte array.
*/
bytearray slice_to(size_t end) const { return slice(0, end); }

/**
* @brief Overloaded stream insertion operator for byte array.
* @param os The output stream.
* @param byteArray The byte array object.
* @return The output stream.
*/
friend std::ostream& operator<<(std::ostream& os, const bytearray& byteArray) {
os << byteArray.toHex(true);
return os;
}

/**
* @brief Conversion operator to convert byte array to std::string.
*
* @note This is provided to support code that relies on byte array
* being representd as a string.
* @return String containing the raw bytes of the byte array
*/
operator std::string() const { return std::string(data_.begin(), data_.end()); }

//! @cond Doxygen_Suppress
// Expose vector-like functionality
size_t size() const { return data_.size(); }
const uint8_t* data() const { return data_.data(); }
bool empty() const { return data_.empty(); }
void clear() { data_.clear(); }
uint8_t& operator[](size_t index) { return data_[index]; }
const uint8_t& operator[](size_t index) const { return data_[index]; }
void push_back(uint8_t byte) { data_.push_back(byte); }
auto begin() const { return data_.begin(); }
auto end() const { return data_.end(); }
//! @endcond

private:
std::vector<uint8_t> data_;
};

} // namespace kvn

#endif // KVN_BYTEARRAY_H
9 changes: 8 additions & 1 deletion simpleble/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ set(SIMPLEBLE_PRIVATE_INCLUDES
${CMAKE_CURRENT_SOURCE_DIR}/src/builders
${CMAKE_CURRENT_SOURCE_DIR}/src/external
${CMAKE_CURRENT_SOURCE_DIR}/src/backends/common
${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/safe)
${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/safe
${CMAKE_CURRENT_SOURCE_DIR}/../external/include)

set(SIMPLEBLE_SRC
${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/base/Adapter.cpp
Expand Down Expand Up @@ -122,6 +123,7 @@ target_include_directories(simpleble-c PRIVATE ${SIMPLEBLE_PRIVATE_INCLUDES})

target_include_directories(simpleble INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../external/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
target_include_directories(simpleble-c INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
Expand Down Expand Up @@ -347,6 +349,10 @@ install(
DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/simpleble/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble)

install(
DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../external/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble)

install(
DIRECTORY ${PROJECT_BINARY_DIR}/export/simpleble/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble)
Expand All @@ -366,6 +372,7 @@ if(SIMPLEBLE_TEST)
add_executable(simpleble_test
${CMAKE_CURRENT_SOURCE_DIR}/test/src/main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_bytearray.cpp
)

set_target_properties(simpleble_test PROPERTIES
Expand Down
14 changes: 11 additions & 3 deletions simpleble/include/simpleble/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
#include <cstdint>
#include <string>
#include <vector>
#include "external/kvn_bytearray.h"

/**
* @file Types.h
* @brief Defines types and enumerations used throughout the SimpleBLE library.
*/

namespace SimpleBLE {

Expand All @@ -12,9 +18,11 @@ using BluetoothAddress = std::string;
// returns the same string, but provides a homogeneous interface.
using BluetoothUUID = std::string;

// IDEA: Extend ByteArray to be constructed by a vector of bytes
// and pointers to uint8_t.
using ByteArray = std::string;
/**
* @typedef ByteArray
* @brief Represents a byte array using kvn::bytearray from the external library.
*/
using ByteArray = kvn::bytearray;

enum class OperatingSystem {
WINDOWS,
Expand Down
Loading

0 comments on commit 39cc358

Please sign in to comment.