|
| 1 | +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. |
| 2 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | +you may not use this file except in compliance with the License. |
| 4 | +You may obtain a copy of the License at |
| 5 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 6 | +Unless required by applicable law or agreed to in writing, software |
| 7 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 8 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 9 | +See the License for the specific language governing permissions and |
| 10 | +limitations under the License. |
| 11 | +==============================================================================*/ |
| 12 | + |
| 13 | +/* |
| 14 | + * Original here: https://github.com/petewarden/ble_file_transfer |
| 15 | + * Modified from original: |
| 16 | + * - added name to namespace |
| 17 | + * - moved to separate file with a header file |
| 18 | + * - moved BLEService outside of namespace |
| 19 | + */ |
| 20 | + |
| 21 | +#include "ble_file_transfer.h" |
| 22 | + |
| 23 | +// This is part of a simple demonstration of how to transfer small (tens of |
| 24 | +// kilobytes) files over BLE onto an Arduino Nano BLE Sense board. Most of this |
| 25 | +// sketch is internal implementation details of the protocol, but if you just |
| 26 | +// want to use it you can look at the bottom of this file. |
| 27 | +// The API is that you call setupBLEFileTransfer() in your setup() function to |
| 28 | +// open up communication with any clients that want to send you files, and then |
| 29 | +// onBLEFileReceived() is called when a file has been downloaded. |
| 30 | + |
| 31 | + |
| 32 | +// Comment this macro back in to log received data to the serial UART. |
| 33 | +//#define ENABLE_LOGGING |
| 34 | + |
| 35 | +namespace ble_file_transfer { |
| 36 | + |
| 37 | +// Controls how large a file the board can receive. We double-buffer the files |
| 38 | +// as they come in, so you'll need twice this amount of RAM. The default is set |
| 39 | +// to 50KB. |
| 40 | +constexpr int32_t file_maximum_byte_count = (50 * 1024); |
| 41 | + |
| 42 | +// Macro based on a master UUID that can be modified for each characteristic. |
| 43 | +#define FILE_TRANSFER_UUID(val) ("bf88b656-" val "-4a61-86e0-769c741026c0") |
| 44 | + |
| 45 | +// How big each transfer block can be. In theory this could be up to 512 bytes, but |
| 46 | +// in practice I've found that going over 128 affects reliability of the connection. |
| 47 | +constexpr int32_t file_block_byte_count = 128; |
| 48 | + |
| 49 | +// Where each data block is written to during the transfer. |
| 50 | +BLECharacteristic file_block_characteristic(FILE_TRANSFER_UUID("3000"), BLEWrite, file_block_byte_count); |
| 51 | + |
| 52 | +// Write the expected total length of the file in bytes to this characteristic |
| 53 | +// before sending the command to transfer a file. |
| 54 | +BLECharacteristic file_length_characteristic(FILE_TRANSFER_UUID("3001"), BLERead | BLEWrite, sizeof(uint32_t)); |
| 55 | + |
| 56 | +// Read-only attribute that defines how large a file the sketch can handle. |
| 57 | +BLECharacteristic file_maximum_length_characteristic(FILE_TRANSFER_UUID("3002"), BLERead, sizeof(uint32_t)); |
| 58 | + |
| 59 | +// Write the checksum that you expect for the file here before you trigger the transfer. |
| 60 | +BLECharacteristic file_checksum_characteristic(FILE_TRANSFER_UUID("3003"), BLERead | BLEWrite, sizeof(uint32_t)); |
| 61 | + |
| 62 | +// Writing a command of 1 starts a file transfer (the length and checksum characteristics should already have been set). |
| 63 | +// A command of 2 tries to cancel any pending file transfers. All other commands are undefined. |
| 64 | +BLECharacteristic command_characteristic(FILE_TRANSFER_UUID("3004"), BLEWrite, sizeof(uint32_t)); |
| 65 | + |
| 66 | +// A status set to 0 means a file transfer succeeded, 1 means there was an error, and 2 means a file transfer is |
| 67 | +// in progress. |
| 68 | +BLECharacteristic transfer_status_characteristic(FILE_TRANSFER_UUID("3005"), BLERead | BLENotify, sizeof(uint32_t)); |
| 69 | + |
| 70 | +// Informative text describing the most recent error, for user interface purposes. |
| 71 | +constexpr int32_t error_message_byte_count = 128; |
| 72 | +BLECharacteristic error_message_characteristic(FILE_TRANSFER_UUID("3006"), BLERead | BLENotify, error_message_byte_count); |
| 73 | + |
| 74 | +// Internal globals used for transferring the file. |
| 75 | +uint8_t file_buffers[2][file_maximum_byte_count]; |
| 76 | +int finished_file_buffer_index = -1; |
| 77 | +uint8_t* finished_file_buffer = nullptr; |
| 78 | +int32_t finished_file_buffer_byte_count = 0; |
| 79 | + |
| 80 | +uint8_t* in_progress_file_buffer = nullptr; |
| 81 | +int32_t in_progress_bytes_received = 0; |
| 82 | +int32_t in_progress_bytes_expected = 0; |
| 83 | +uint32_t in_progress_checksum = 0; |
| 84 | + |
| 85 | +void notifyError(const String& error_message) { |
| 86 | + Serial.println(error_message); |
| 87 | + constexpr int32_t error_status_code = 1; |
| 88 | + transfer_status_characteristic.notify32((int) error_status_code); |
| 89 | + |
| 90 | + const char* error_message_bytes = error_message.c_str(); |
| 91 | + uint8_t error_message_buffer[error_message_byte_count]; |
| 92 | + bool at_string_end = false; |
| 93 | + for (int i = 0; i < error_message_byte_count; ++i) { |
| 94 | + const bool at_last_byte = (i == (error_message_byte_count - 1)); |
| 95 | + if (!at_string_end && !at_last_byte) { |
| 96 | + const char current_char = error_message_bytes[i]; |
| 97 | + if (current_char == 0) { |
| 98 | + at_string_end = true; |
| 99 | + } else { |
| 100 | + error_message_buffer[i] = current_char; |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + if (at_string_end || at_last_byte) { |
| 105 | + error_message_buffer[i] = 0; |
| 106 | + } |
| 107 | + } |
| 108 | + error_message_characteristic.notify(error_message_buffer, error_message_byte_count); |
| 109 | +} |
| 110 | + |
| 111 | +void notifySuccess() { |
| 112 | + constexpr int32_t success_status_code = 0; |
| 113 | + transfer_status_characteristic.notify32( (int) success_status_code); |
| 114 | +} |
| 115 | + |
| 116 | +void notifyInProgress() { |
| 117 | + constexpr int32_t in_progress_status_code = 2; |
| 118 | + transfer_status_characteristic.notify32( (int) in_progress_status_code); |
| 119 | +} |
| 120 | + |
| 121 | +// See http://home.thep.lu.se/~bjorn/crc/ for more information on simple CRC32 calculations. |
| 122 | +uint32_t crc32_for_byte(uint32_t r) { |
| 123 | + for (int j = 0; j < 8; ++j) { |
| 124 | + r = (r & 1? 0: (uint32_t)0xedb88320L) ^ r >> 1; |
| 125 | + } |
| 126 | + return r ^ (uint32_t)0xff000000L; |
| 127 | +} |
| 128 | + |
| 129 | +uint32_t crc32(const uint8_t* data, size_t data_length) { |
| 130 | + constexpr int table_size = 256; |
| 131 | + static uint32_t table[table_size]; |
| 132 | + static bool is_table_initialized = false; |
| 133 | + if (!is_table_initialized) { |
| 134 | + for(size_t i = 0; i < table_size; ++i) { |
| 135 | + table[i] = crc32_for_byte(i); |
| 136 | + } |
| 137 | + is_table_initialized = true; |
| 138 | + } |
| 139 | + uint32_t crc = 0; |
| 140 | + for (size_t i = 0; i < data_length; ++i) { |
| 141 | + const uint8_t crc_low_byte = static_cast<uint8_t>(crc); |
| 142 | + const uint8_t data_byte = data[i]; |
| 143 | + const uint8_t table_index = crc_low_byte ^ data_byte; |
| 144 | + crc = table[table_index] ^ (crc >> 8); |
| 145 | + } |
| 146 | + return crc; |
| 147 | +} |
| 148 | + |
| 149 | +// This is a small test function for the CRC32 implementation, not normally called but left in |
| 150 | +// for debugging purposes. We know the expected CRC32 of [97, 98, 99, 100, 101] is 2240272485, |
| 151 | +// or 0x8587d865, so if anything else is output we know there's an error in the implementation. |
| 152 | +void testCrc32() { |
| 153 | + constexpr int test_array_length = 5; |
| 154 | + const uint8_t test_array[test_array_length] = {97, 98, 99, 100, 101}; |
| 155 | + const uint32_t test_array_crc32 = crc32(test_array, test_array_length); |
| 156 | + Serial.println(String("CRC32 for [97, 98, 99, 100, 101] is 0x") + String(test_array_crc32, 16) + |
| 157 | + String(" (") + String(test_array_crc32) + String(")")); |
| 158 | +} |
| 159 | + |
| 160 | +void onFileTransferComplete() { |
| 161 | + uint32_t computed_checksum = crc32(in_progress_file_buffer, in_progress_bytes_expected);; |
| 162 | + if (in_progress_checksum != computed_checksum) { |
| 163 | + notifyError(String("File transfer failed: Expected checksum 0x") + String(in_progress_checksum, 16) + |
| 164 | + String(" but received 0x") + String(computed_checksum, 16)); |
| 165 | + in_progress_file_buffer = nullptr; |
| 166 | + return; |
| 167 | + } |
| 168 | + |
| 169 | + if (finished_file_buffer_index == 0) { |
| 170 | + finished_file_buffer_index = 1; |
| 171 | + } else { |
| 172 | + finished_file_buffer_index = 0; |
| 173 | + } |
| 174 | + finished_file_buffer = &file_buffers[finished_file_buffer_index][0];; |
| 175 | + finished_file_buffer_byte_count = in_progress_bytes_expected; |
| 176 | + |
| 177 | + in_progress_file_buffer = nullptr; |
| 178 | + in_progress_bytes_received = 0; |
| 179 | + in_progress_bytes_expected = 0; |
| 180 | + |
| 181 | + notifySuccess(); |
| 182 | + |
| 183 | + onBLEFileReceived(finished_file_buffer, finished_file_buffer_byte_count); |
| 184 | +} |
| 185 | + |
| 186 | +void onFileBlockWritten(uint16_t conn_hdl, BLECharacteristic* characteristic, uint8_t* data, uint16_t len) { |
| 187 | + if (in_progress_file_buffer == nullptr) { |
| 188 | + notifyError("File block sent while no valid command is active"); |
| 189 | + return; |
| 190 | + } |
| 191 | + |
| 192 | + const int32_t file_block_length = len; |
| 193 | + if (file_block_length > file_block_byte_count) { |
| 194 | + notifyError(String("Too many bytes in block: Expected ") + String(file_block_byte_count) + |
| 195 | + String(" but received ") + String(file_block_length)); |
| 196 | + in_progress_file_buffer = nullptr; |
| 197 | + return; |
| 198 | + } |
| 199 | + |
| 200 | + const int32_t bytes_received_after_block = in_progress_bytes_received + file_block_length; |
| 201 | + if ((bytes_received_after_block > in_progress_bytes_expected) || |
| 202 | + (bytes_received_after_block > file_maximum_byte_count)) { |
| 203 | + notifyError(String("Too many bytes: Expected ") + String(in_progress_bytes_expected) + |
| 204 | + String(" but received ") + String(bytes_received_after_block)); |
| 205 | + in_progress_file_buffer = nullptr; |
| 206 | + return; |
| 207 | + } |
| 208 | + |
| 209 | + uint8_t* file_block_buffer = in_progress_file_buffer + in_progress_bytes_received; |
| 210 | + memcpy(file_block_buffer, data, file_block_length); |
| 211 | + |
| 212 | +// Enable this macro to show the data in the serial log. |
| 213 | +#ifdef ENABLE_LOGGING |
| 214 | + Serial.print("Data received: length = "); |
| 215 | + Serial.println(file_block_length); |
| 216 | + |
| 217 | + char string_buffer[file_block_byte_count + 1]; |
| 218 | + for (int i = 0; i < file_block_byte_count; ++i) { |
| 219 | + unsigned char value = file_block_buffer[i]; |
| 220 | + if (i < file_block_length) { |
| 221 | + string_buffer[i] = value; |
| 222 | + } else { |
| 223 | + string_buffer[i] = 0; |
| 224 | + } |
| 225 | + } |
| 226 | + string_buffer[file_block_byte_count] = 0; |
| 227 | + Serial.println(String(string_buffer)); |
| 228 | +#endif // ENABLE_LOGGING |
| 229 | + |
| 230 | + if (bytes_received_after_block == in_progress_bytes_expected) { |
| 231 | + onFileTransferComplete(); |
| 232 | + } else { |
| 233 | + in_progress_bytes_received = bytes_received_after_block; |
| 234 | + } |
| 235 | +} |
| 236 | + |
| 237 | +void startFileTransfer() { |
| 238 | + if (in_progress_file_buffer != nullptr) { |
| 239 | + notifyError("File transfer command received while previous transfer is still in progress"); |
| 240 | + return; |
| 241 | + } |
| 242 | + |
| 243 | + int32_t file_length_value = (int32_t) file_length_characteristic.read32(); |
| 244 | + if (file_length_value > file_maximum_byte_count) { |
| 245 | + notifyError( |
| 246 | + String("File too large: Maximum is ") + String(file_maximum_byte_count) + |
| 247 | + String(" bytes but request is ") + String(file_length_value) + String(" bytes")); |
| 248 | + return; |
| 249 | + } |
| 250 | + |
| 251 | + in_progress_checksum = file_checksum_characteristic.read32(); |
| 252 | + |
| 253 | + int in_progress_file_buffer_index; |
| 254 | + if (finished_file_buffer_index == 0) { |
| 255 | + in_progress_file_buffer_index = 1; |
| 256 | + } else { |
| 257 | + in_progress_file_buffer_index = 0; |
| 258 | + } |
| 259 | + |
| 260 | + in_progress_file_buffer = &file_buffers[in_progress_file_buffer_index][0]; |
| 261 | + in_progress_bytes_received = 0; |
| 262 | + in_progress_bytes_expected = file_length_value; |
| 263 | + |
| 264 | + notifyInProgress(); |
| 265 | +} |
| 266 | + |
| 267 | +void cancelFileTransfer() { |
| 268 | + if (in_progress_file_buffer != nullptr) { |
| 269 | + notifyError("File transfer cancelled"); |
| 270 | + in_progress_file_buffer = nullptr; |
| 271 | + } |
| 272 | +} |
| 273 | + |
| 274 | +void onCommandWritten(uint16_t conn_hdl, BLECharacteristic* characteristic, uint8_t* data, uint16_t len) { |
| 275 | + int32_t command_value = (int32_t) characteristic->read32(); |
| 276 | + |
| 277 | + if ((command_value != 1) && (command_value != 2)) { |
| 278 | + notifyError(String("Bad command value: Expected 1 or 2 but received ") + String(command_value)); |
| 279 | + return; |
| 280 | + } |
| 281 | + |
| 282 | + if (command_value == 1) { |
| 283 | + startFileTransfer(); |
| 284 | + } else if (command_value == 2) { |
| 285 | + cancelFileTransfer(); |
| 286 | + } |
| 287 | + |
| 288 | +} |
| 289 | + |
| 290 | +// Starts the BLE handling you need to support the file transfer. |
| 291 | +void setupBLEFileTransfer(BLEService service) { |
| 292 | + |
| 293 | + |
| 294 | + // Add in the characteristics we'll be making available. |
| 295 | + file_block_characteristic.setWriteCallback(onFileBlockWritten); |
| 296 | + file_block_characteristic.begin(); |
| 297 | + |
| 298 | + file_length_characteristic.begin(); |
| 299 | + |
| 300 | + file_maximum_length_characteristic.write32((int) file_maximum_byte_count); |
| 301 | + file_maximum_length_characteristic.begin(); |
| 302 | + |
| 303 | + file_checksum_characteristic.begin(); |
| 304 | + |
| 305 | + command_characteristic.setWriteCallback(onCommandWritten); |
| 306 | + command_characteristic.begin(); |
| 307 | + |
| 308 | + transfer_status_characteristic.begin(); |
| 309 | + error_message_characteristic.begin(); |
| 310 | + |
| 311 | + Serial.println("BLE setup and advertising"); |
| 312 | +} |
| 313 | + |
| 314 | + |
| 315 | +// Called in your loop function to handle BLE housekeeping. |
| 316 | +void updateBLEFileTransfer() { |
| 317 | +// BLEDevice central = BLE.central(); |
| 318 | +// static bool was_connected_last = false; |
| 319 | +// if (central && !was_connected_last) { |
| 320 | +// Serial.print("Connected to central: "); |
| 321 | +// Serial.println(central.address()); |
| 322 | +// } |
| 323 | +// was_connected_last = central; |
| 324 | +} |
| 325 | + |
| 326 | + |
| 327 | +bool isTransfering(){ |
| 328 | + return in_progress_file_buffer != nullptr; |
| 329 | +} |
| 330 | + |
| 331 | +} // namespace |
0 commit comments