Skip to content

Commit 8673cc2

Browse files
Merge pull request #12 from tannewt/v4
Add versions 3 and 4
2 parents d6d99fd + d35bdb0 commit 8673cc2

File tree

5 files changed

+344
-65
lines changed

5 files changed

+344
-65
lines changed

README.rst

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,18 @@ The base UUID used in characteristics is ``ADAFxxxx-4669-6C65-5472-616E73666572`
8787

8888
The service has two characteristics:
8989

90-
* version (``0x0100``) - Simple unsigned 32-bit integer version number. Always 1.
90+
* version (``0x0100``) - Simple unsigned 32-bit integer version number. May be 1 - 4.
9191
* raw transfer (``0x0200``) - Bidirectional link with a custom protocol. The client does WRITE_NO_RESPONSE to the characteristic and then server replies via NOTIFY. (This is similar to the Nordic UART Service but on a single characteristic rather than two.) The commands over the transfer characteristic are idempotent and stateless. A disconnect during a command will reset the state.
9292

93+
Time resolution
94+
---------------
95+
96+
Time resolution varies based filesystem type. FATFS can only get down to the 2 second bound after 1980. Littlefs can do 64-bit nanoseconds after January 1st, 1970.
97+
98+
To account for this, the protocol has time in 64-bit nanoseconds after January 1st, 1970. However, the server will respond with a potentially truncated version that is the value stored.
99+
100+
Also note that devices serving the file transfer protocol may not have it's own clock so do not rely on time ordering. Any internal writes may set the time incorrectly. So, we only recommend using the value as a cache key.
101+
93102
Commands
94103
---------
95104

@@ -151,6 +160,7 @@ The header is four fixed entries and a variable length path:
151160
* 1 Byte reserved for padding.
152161
* Path length: 16-bit number encoding the encoded length of the path string.
153162
* Offset: 32-bit number encoding the starting offset to write.
163+
* Current time: 64-bit number encoding nanoseconds since January 1st, 1970. Used as the file modification time. Not all system will support the full resolution. Use the truncated time response value for caching.
154164
* Total size: 32-bit number encoding the total length of the file contents.
155165
* Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
156166

@@ -160,6 +170,7 @@ The server will repeatedly respond until the total length has been transferred w
160170
* Status: Single byte. ``0x01`` if OK. ``0x02`` if any parent directory is missing or a file.
161171
* 2 Bytes reserved for padding.
162172
* Offset: 32-bit number encoding the starting offset to write. (Should match the offset from the previous 0x20 or 0x22 message)
173+
* Truncated time: 64-bit number encoding nanoseconds since January 1st, 1970 as stored by the file system. The resolution may be less that the protocol. It is sent back for use in caching on the host side.
163174
* Free space: 32-bit number encoding the amount of data the client can send.
164175

165176
The client will repeatedly respond until the total length has been transferred with:
@@ -173,11 +184,13 @@ The client will repeatedly respond until the total length has been transferred w
173184

174185
The transaction is complete after the server has received all data and replied with a status with 0 free space and offset set to the content length.
175186

187+
**NOTE**: Current time was added in version 3. The rest of the packets remained the same.
188+
176189

177190
``0x30`` - Delete a file or directory
178191
+++++++++++++++++++++++++++++++++++++
179192

180-
Deletes the file or directory at the given full path. Directories must be empty to be deleted.
193+
Deletes the file or directory at the given full path. Non-empty directories will have their contents deleted as well.
181194

182195
The header is two fixed entries and a variable length path:
183196

@@ -189,7 +202,9 @@ The header is two fixed entries and a variable length path:
189202
The server will reply with:
190203

191204
* Command: Single byte. Always ``0x31``.
192-
* Status: Single byte. ``0x01`` if the file or directory was deleted or ``0x02`` if the path is a non-empty directory or non-existent.
205+
* Status: Single byte. ``0x01`` if the file or directory was deleted or ``0x02`` if the path is non-existent.
206+
207+
**NOTE**: In version 2, this command now deletes contents of a directory as well. It won't error.
193208

194209
``0x40`` - Make a directory
195210
+++++++++++++++++++++++++++
@@ -201,12 +216,16 @@ The header is two fixed entries and a variable length path:
201216
* Command: Single byte. Always ``0x40``.
202217
* 1 Byte reserved for padding.
203218
* Path length: 16-bit number encoding the encoded length of the path string.
219+
* 4 Bytes reserved for padding.
220+
* Current time: 64-bit number encoding nanoseconds since January 1st, 1970. Used as the file modification time. Not all system will support the full resolution. Use the truncated time response value for caching.
204221
* Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
205222

206223
The server will reply with:
207224

208225
* Command: Single byte. Always ``0x41``.
209226
* Status: Single byte. ``0x01`` if the directory(s) were created or ``0x02`` if any parent of the path is an existing file.
227+
* 6 Bytes reserved for padding.
228+
* Truncated time: 64-bit number encoding nanoseconds since January 1st, 1970 as stored by the file system. The resolution may be less that the protocol. It is sent back for use in caching on the host side.
210229

211230
``0x50`` - List a directory
212231
+++++++++++++++++++++++++++
@@ -232,11 +251,53 @@ The server will reply with n+1 entries for a directory with n files:
232251
- Bit 0: Set when the entry is a directory
233252
- Bits 1-7: Reserved
234253

254+
* Modification time: 64-bit number of nanoseconds since January 1st, 1970. *However*, files modifiers may not have an accurate clock so do *not* assume it is correct. Instead, only use it to determine cacheability vs a local copy.
235255
* File size: 32-bit number encoding the size of the file. Ignore for directories. Value may change.
236256
* Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.) These paths are relative so they won't contain ``/`` at all.
237257

238258
The transaction is complete when the final entry is sent from the server. It will have entry number == total entries and zeros for flags, file size and path length.
239259

260+
``0x60`` - Move a file or directory
261+
+++++++++++++++++++++++++++++++++++
262+
263+
Moves a file or directory at a given path to a different path. Can be used to
264+
rename as well. The two paths are sent separated by a byte so that the server
265+
may null-terminate the string itself. The client may send anything there.
266+
267+
The header is two fixed entries and a variable length path:
268+
269+
* Command: Single byte. Always ``0x60``.
270+
* 1 Byte reserved for padding.
271+
* Old Path length: 16-bit number encoding the encoded length of the path string.
272+
* New Path length: 16-bit number encoding the encoded length of the path string.
273+
* Old Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
274+
* One padding byte. This can be used to null terminate the old path string.
275+
* New Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
276+
277+
The server will reply with:
278+
* Command: Single byte. Always ``0x61``.
279+
* Status: Single byte. ``0x01`` on success or ``0x02`` on error.
280+
281+
**NOTE**: This is added in version 4.
282+
283+
Versions
284+
=========
285+
286+
Version 2
287+
---------
288+
* Changes delete to delete contents of non-empty directories automatically.
289+
290+
Version 3
291+
---------
292+
* Adds modification time.
293+
* Adds current time to file write command.
294+
* Adds current time to make directory command.
295+
* Adds modification time to directory listing entries.
296+
297+
Version 4
298+
---------
299+
* Adds move command.
300+
240301
Contributing
241302
============
242303

adafruit_ble_file_transfer.py

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""
1414

1515
import struct
16+
import time
1617
import _bleio
1718

1819
from adafruit_ble.attributes import Attribute
@@ -75,7 +76,7 @@ class FileTransferService(Service):
7576
# pylint: disable=too-few-public-methods
7677

7778
uuid = StandardUUID(0xFEBB)
78-
version = Uint32Characteristic(uuid=FileTransferUUID(0x0100))
79+
version = Uint32Characteristic(uuid=FileTransferUUID(0x0100), initial_value=4)
7980
raw = _TransferCharacteristic()
8081
# _raw gets shadowed for each MIDIService instance by a PacketBuffer. PyLint doesn't know this
8182
# so it complains about missing members.
@@ -95,6 +96,8 @@ class FileTransferService(Service):
9596
MKDIR_STATUS = 0x41
9697
LISTDIR = 0x50
9798
LISTDIR_ENTRY = 0x51
99+
MOVE = 0x60
100+
MOVE_STATUS = 0x61
98101

99102
# Responses
100103
# 0x00 is INVALID
@@ -117,6 +120,9 @@ class FileTransferClient:
117120
def __init__(self, service):
118121
self._service = service
119122

123+
if service.version < 3:
124+
raise RuntimeError("Service on other device too old")
125+
120126
def _write(self, buffer):
121127
# print("write", binascii.hexlify(buffer))
122128
sent = 0
@@ -135,7 +141,7 @@ def _readinto(self, buffer):
135141
read = self._service.raw.readinto(buffer)
136142
except ValueError:
137143
read = self._service.raw.readinto(long_buffer)
138-
buffer[:read] = long_buffer[:read]
144+
buffer[:read] = long_buffer[:read]
139145
return read
140146

141147
def read(self, path, *, offset=0):
@@ -204,24 +210,32 @@ def read(self, path, *, offset=0):
204210
self._write(encoded)
205211
return buf
206212

207-
def write(self, path, contents, *, offset=0):
213+
def write(self, path, contents, *, offset=0, modification_time=None):
208214
"""Writes the given contents to the given path starting at the given offset.
215+
Returns the trunctated modification time.
209216
210217
If the file is shorter than the offset, zeros will be added in the gap."""
211218
path = path.encode("utf-8")
212219
total_length = len(contents) + offset
220+
if modification_time is None:
221+
modification_time = int(time.time() * 1_000_000_000)
213222
encoded = (
214223
struct.pack(
215-
"<BxHII", FileTransferService.WRITE, len(path), offset, total_length
224+
"<BxHIQI",
225+
FileTransferService.WRITE,
226+
len(path),
227+
offset,
228+
modification_time,
229+
total_length,
216230
)
217231
+ path
218232
)
219233
self._write(encoded)
220-
b = bytearray(struct.calcsize("<BBxxII"))
234+
b = bytearray(struct.calcsize("<BBxxIQI"))
221235
written = 0
222236
while written < len(contents):
223237
self._readinto(b)
224-
cmd, status, current_offset, free_space = struct.unpack("<BBxxII", b)
238+
cmd, status, current_offset, _, free_space = struct.unpack("<BBxxIQI", b)
225239
if status != FileTransferService.OK:
226240
print("write error", status)
227241
raise RuntimeError()
@@ -254,23 +268,32 @@ def write(self, path, contents, *, offset=0):
254268

255269
# Wait for confirmation that everything was written ok.
256270
self._readinto(b)
257-
cmd, status, offset, free_space = struct.unpack("<BBxxII", b)
271+
cmd, status, offset, truncated_time, free_space = struct.unpack("<BBxxIQI", b)
258272
if cmd != FileTransferService.WRITE_PACING or offset != total_length:
259273
raise ProtocolError()
274+
return truncated_time
260275

261-
def mkdir(self, path):
262-
"""Makes the directory and any missing parents"""
276+
def mkdir(self, path, modification_time=None):
277+
"""Makes the directory and any missing parents. Returns the truncated time"""
263278
path = path.encode("utf-8")
264-
encoded = struct.pack("<BxH", FileTransferService.MKDIR, len(path)) + path
279+
if modification_time is None:
280+
modification_time = int(time.time() * 1_000_000_000)
281+
encoded = (
282+
struct.pack(
283+
"<BxHxxxxQ", FileTransferService.MKDIR, len(path), modification_time
284+
)
285+
+ path
286+
)
265287
self._write(encoded)
266288

267-
b = bytearray(struct.calcsize("<BB"))
289+
b = bytearray(struct.calcsize("<BBxxxxxxQ"))
268290
self._readinto(b)
269-
cmd, status = struct.unpack("<BB", b)
291+
cmd, status, truncated_time = struct.unpack("<BBxxxxxxQ", b)
270292
if cmd != FileTransferService.MKDIR_STATUS:
271293
raise ProtocolError()
272294
if status != FileTransferService.OK:
273295
raise ValueError("Invalid path")
296+
return truncated_time
274297

275298
def listdir(self, path):
276299
"""Returns a list of tuples, one tuple for each file or directory in the given path"""
@@ -282,31 +305,33 @@ def listdir(self, path):
282305
b = bytearray(self._service.raw.incoming_packet_length)
283306
i = 0
284307
total = 10 # starting value that will be replaced by the first response
285-
header_size = struct.calcsize("<BBHIIII")
308+
header_size = struct.calcsize("<BBHIIIQI")
286309
path_length = 0
287310
encoded_path = b""
311+
file_size = 0
312+
flags = 0
313+
modification_time = 0
288314
while i < total:
289315
read = self._readinto(b)
290316
offset = 0
291-
file_size = 0
292-
flags = 0
293317
while offset < read:
294318
if len(encoded_path) == path_length:
295319
if path_length > 0:
296320
path = str(
297321
encoded_path,
298322
"utf-8",
299323
)
300-
paths.append((path, file_size, flags))
324+
paths.append((path, file_size, flags, modification_time))
301325
(
302326
cmd,
303327
status,
304328
path_length,
305329
i,
306330
total,
307331
flags,
332+
modification_time,
308333
file_size,
309-
) = struct.unpack_from("<BBHIIII", b, offset=offset)
334+
) = struct.unpack_from("<BBHIIIQI", b, offset=offset)
310335
offset += header_size
311336
encoded_path = b""
312337
if cmd != FileTransferService.LISTDIR_ENTRY:
@@ -322,7 +347,7 @@ def listdir(self, path):
322347
return paths
323348

324349
def delete(self, path):
325-
"""Deletes the file or directory at the given path. Directories must be empty."""
350+
"""Deletes the file or directory at the given path."""
326351
path = path.encode("utf-8")
327352
encoded = struct.pack("<BxH", FileTransferService.DELETE, len(path)) + path
328353
self._write(encoded)
@@ -334,3 +359,25 @@ def delete(self, path):
334359
raise ProtocolError()
335360
if status != FileTransferService.OK:
336361
raise ValueError("Missing file")
362+
363+
def move(self, old_path, new_path):
364+
"""Moves the file or directory from old_path to new_path."""
365+
if self._service.version < 4:
366+
raise RuntimeError("Service on other device too old")
367+
old_path = old_path.encode("utf-8")
368+
new_path = new_path.encode("utf-8")
369+
encoded = (
370+
struct.pack("<BxHH", FileTransferService.MOVE, len(old_path), len(new_path))
371+
+ old_path
372+
+ b" "
373+
+ new_path
374+
)
375+
self._write(encoded)
376+
377+
b = bytearray(struct.calcsize("<BB"))
378+
self._readinto(b)
379+
cmd, status = struct.unpack("<BB", b)
380+
if cmd != FileTransferService.MOVE_STATUS:
381+
raise ProtocolError()
382+
if status != FileTransferService.OK:
383+
raise ValueError("Missing file")
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""
5+
Used with ble_uart_echo_test.py. Transmits "echo" to the UARTService and receives it back.
6+
"""
7+
8+
import sys
9+
10+
from adafruit_ble import BLERadio
11+
from adafruit_ble.advertising.standard import (
12+
ProvideServicesAdvertisement,
13+
Advertisement,
14+
)
15+
import adafruit_ble_file_transfer
16+
17+
# Connect to a file transfer device
18+
ble = BLERadio()
19+
connection = None
20+
print("disconnected, scanning")
21+
for advertisement in ble.start_scan(
22+
ProvideServicesAdvertisement, Advertisement, timeout=1
23+
):
24+
# print(advertisement.address, advertisement.address.type)
25+
if (
26+
not hasattr(advertisement, "services")
27+
or adafruit_ble_file_transfer.FileTransferService not in advertisement.services
28+
):
29+
continue
30+
connection = ble.connect(advertisement)
31+
peer_address = advertisement.address
32+
print("connected to", advertisement.address)
33+
break
34+
ble.stop_scan()
35+
36+
if not connection:
37+
print("No advertisement found")
38+
sys.exit(1)
39+
40+
# Prep the connection
41+
if adafruit_ble_file_transfer.FileTransferService not in connection:
42+
print("Connected device missing file transfer service")
43+
sys.exit(1)
44+
if not connection.paired:
45+
print("pairing")
46+
connection.pair()
47+
print("paired")
48+
print()
49+
service = connection[adafruit_ble_file_transfer.FileTransferService]
50+
client = adafruit_ble_file_transfer.FileTransferClient(service)
51+
52+
# Do the file operations
53+
print(client.listdir("/"))
54+
print(client.listdir("/lib/"))

0 commit comments

Comments
 (0)