Skip to content

Commit f85f7e2

Browse files
committed
More updates
1 parent 038b554 commit f85f7e2

File tree

2 files changed

+81
-38
lines changed

2 files changed

+81
-38
lines changed

README.rst

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ The service has two characteristics:
9090
* version (``0x0100``) - Simple unsigned 32-bit integer version number. May be 1 or 2.
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

@@ -150,7 +159,7 @@ The header is four fixed entries and a variable length path:
150159
* Path length: 16-bit number encoding the encoded length of the path string.
151160
* Offset: 32-bit number encoding the starting offset to write.
152161
* Total size: 32-bit number encoding the total length of the file contents.
153-
* Current time: 64-bit number encoding nanoseconds since January 1st, 1970. Used as the file modification time. Not all system will s
162+
* 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.
154163
* Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
155164

156165
The server will repeatedly respond until the total length has been transferred with:
@@ -159,6 +168,7 @@ The server will repeatedly respond until the total length has been transferred w
159168
* 2 Bytes reserved for padding.
160169
* Offset: 32-bit number encoding the starting offset to write. (Should match the offset from the previous 0x20 or 0x22 message)
161170
* Free space: 32-bit number encoding the amount of data the client can send.
171+
* 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.
162172

163173
The client will repeatedly respond until the total length has been transferred with:
164174
* Command: Single byte. Always ``0x22``.
@@ -170,16 +180,7 @@ The client will repeatedly respond until the total length has been transferred w
170180

171181
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.
172182

173-
**NOTE**: Current time was added in version 2. The rest of the packets remained the same.
174-
175-
Time resolution
176-
~~~~~~~~~~~~~~~
177-
178-
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 Jan 1, 1970.
179-
180-
What do you want to do?
181-
* We could either return the stored time when doing a write.
182-
* We could add file system info command that has the smallest resolution in units of 64-bit nanoseconds. This command could to total filesystem size and free space as well.
183+
**NOTE**: Current time was added in version 3. The rest of the packets remained the same.
183184

184185

185186
``0x30`` - Delete a file or directory
@@ -210,11 +211,14 @@ The header is two fixed entries and a variable length path:
210211
* Command: Single byte. Always ``0x40``.
211212
* 1 Byte reserved for padding.
212213
* Path length: 16-bit number encoding the encoded length of the path string.
214+
* 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.
213215
* Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
214216

215217
The server will reply with:
216218
* Command: Single byte. Always ``0x41``.
217219
* Status: Single byte. ``0x01`` if the directory(s) were created or ``0x02`` if any parent of the path is an existing file.
220+
* 2 Bytes reserved for padding.
221+
* 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.
218222

219223
``0x50`` - List a directory
220224
+++++++++++++++++++++++++++
@@ -252,39 +256,37 @@ Moves a file or directory at a given path to a different path. Can be used to
252256
rename as well. The two paths are sent separately so that they are not limited
253257
by internal packet buffer sizes differently from other commands.
254258

255-
**TODO** Maybe we do pack two paths in one packet. That will limit path length
256-
but prevent any secondary storage internally.
257-
258259
The header is two fixed entries and a variable length path:
259260

260261
* Command: Single byte. Always ``0x60``.
261262
* 1 Byte reserved for padding.
262263
* Old Path length: 16-bit number encoding the encoded length of the path string.
263-
* Old Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
264-
265-
The server will reply with:
266-
* Command: Single byte. Always ``0x61``.
267-
* Status: Single byte. ``0x01`` on success or ``0x02`` if the old path is too long for internal buffers.
268-
269-
* Command: Single byte. Always ``0x62``.
270-
* 1 Byte reserved for padding.
271264
* New Path length: 16-bit number encoding the encoded length of the path string.
265+
* Old Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
272266
* New Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
273267

274268
The server will reply with:
275-
* Command: Single byte. Always ``0x63``.
269+
* Command: Single byte. Always ``0x61``.
276270
* Status: Single byte. ``0x01`` on success or ``0x02`` on error.
277271

272+
**NOTE**: This is added in version 4.
273+
278274
Versions
279275
=========
280276

281277
Version 2
282278
---------
279+
* Changes delete to delete contents of non-empty directories automatically.
283280

281+
Version 3
282+
---------
284283
* Adds modification time.
285284
* Adds current time to file write command.
285+
* Adds current time to make directory command.
286286
* Adds modification time to directory listing entries.
287-
* Changes delete to delete non-empty directories automatically.
287+
288+
Version 4
289+
---------
288290
* Adds move command.
289291

290292
Contributing

adafruit_ble_file_transfer.py

Lines changed: 55 additions & 14 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
@@ -204,24 +207,32 @@ def read(self, path, *, offset=0):
204207
self._write(encoded)
205208
return buf
206209

207-
def write(self, path, contents, *, offset=0):
210+
def write(self, path, contents, *, offset=0, modification_time=None):
208211
"""Writes the given contents to the given path starting at the given offset.
212+
Returns the trunctated modification time.
209213
210214
If the file is shorter than the offset, zeros will be added in the gap."""
211215
path = path.encode("utf-8")
212216
total_length = len(contents) + offset
217+
if modification_time is None:
218+
modification_time = int(time.time() * 1_000_000_000)
213219
encoded = (
214220
struct.pack(
215-
"<BxHII", FileTransferService.WRITE, len(path), offset, total_length
221+
"<BxHIIQ",
222+
FileTransferService.WRITE,
223+
len(path),
224+
offset,
225+
total_length,
226+
modification_time,
216227
)
217228
+ path
218229
)
219230
self._write(encoded)
220-
b = bytearray(struct.calcsize("<BBxxII"))
231+
b = bytearray(struct.calcsize("<BBxxIIQ"))
221232
written = 0
222233
while written < len(contents):
223234
self._readinto(b)
224-
cmd, status, current_offset, free_space = struct.unpack("<BBxxII", b)
235+
cmd, status, current_offset, free_space, _ = struct.unpack("<BBxxIIQ", b)
225236
if status != FileTransferService.OK:
226237
print("write error", status)
227238
raise RuntimeError()
@@ -254,23 +265,32 @@ def write(self, path, contents, *, offset=0):
254265

255266
# Wait for confirmation that everything was written ok.
256267
self._readinto(b)
257-
cmd, status, offset, free_space = struct.unpack("<BBxxII", b)
268+
cmd, status, offset, free_space, truncated_time = struct.unpack("<BBxxIIQ", b)
258269
if cmd != FileTransferService.WRITE_PACING or offset != total_length:
259270
raise ProtocolError()
271+
return truncated_time
260272

261-
def mkdir(self, path):
262-
"""Makes the directory and any missing parents"""
273+
def mkdir(self, path, modification_time=None):
274+
"""Makes the directory and any missing parents. Returns the truncated time"""
263275
path = path.encode("utf-8")
264-
encoded = struct.pack("<BxH", FileTransferService.MKDIR, len(path)) + path
276+
if modification_time is None:
277+
modification_time = int(time.time() * 1_000_000_000)
278+
encoded = (
279+
struct.pack(
280+
"<BxHQ", FileTransferService.MKDIR, len(path), modification_time
281+
)
282+
+ path
283+
)
265284
self._write(encoded)
266285

267286
b = bytearray(struct.calcsize("<BB"))
268287
self._readinto(b)
269-
cmd, status = struct.unpack("<BB", b)
288+
cmd, status, truncated_time = struct.unpack("<BBxxQ", b)
270289
if cmd != FileTransferService.MKDIR_STATUS:
271290
raise ProtocolError()
272291
if status != FileTransferService.OK:
273292
raise ValueError("Invalid path")
293+
return truncated_time
274294

275295
def listdir(self, path):
276296
"""Returns a list of tuples, one tuple for each file or directory in the given path"""
@@ -282,22 +302,23 @@ def listdir(self, path):
282302
b = bytearray(self._service.raw.incoming_packet_length)
283303
i = 0
284304
total = 10 # starting value that will be replaced by the first response
285-
header_size = struct.calcsize("<BBHIIII")
305+
header_size = struct.calcsize("<BBHIIIIQ")
286306
path_length = 0
287307
encoded_path = b""
288308
while i < total:
289309
read = self._readinto(b)
290310
offset = 0
291311
file_size = 0
292312
flags = 0
313+
modification_time = 0
293314
while offset < read:
294315
if len(encoded_path) == path_length:
295316
if path_length > 0:
296317
path = str(
297318
encoded_path,
298319
"utf-8",
299320
)
300-
paths.append((path, file_size, flags))
321+
paths.append((path, file_size, flags, modification_time))
301322
(
302323
cmd,
303324
status,
@@ -306,7 +327,8 @@ def listdir(self, path):
306327
total,
307328
flags,
308329
file_size,
309-
) = struct.unpack_from("<BBHIIII", b, offset=offset)
330+
modification_time,
331+
) = struct.unpack_from("<BBHIIIIQ", b, offset=offset)
310332
offset += header_size
311333
encoded_path = b""
312334
if cmd != FileTransferService.LISTDIR_ENTRY:
@@ -322,7 +344,7 @@ def listdir(self, path):
322344
return paths
323345

324346
def delete(self, path):
325-
"""Deletes the file or directory at the given path. Directories must be empty."""
347+
"""Deletes the file or directory at the given path."""
326348
path = path.encode("utf-8")
327349
encoded = struct.pack("<BxH", FileTransferService.DELETE, len(path)) + path
328350
self._write(encoded)
@@ -334,3 +356,22 @@ def delete(self, path):
334356
raise ProtocolError()
335357
if status != FileTransferService.OK:
336358
raise ValueError("Missing file")
359+
360+
def move(self, old_path, new_path):
361+
"""Moves the file or directory from old_path to new_path."""
362+
old_path = old_path.encode("utf-8")
363+
new_path = new_path.encode("utf-8")
364+
encoded = (
365+
struct.pack("<BxHH", FileTransferService.MOVE, len(old_path), len(new_path))
366+
+ old_path
367+
+ new_path
368+
)
369+
self._write(encoded)
370+
371+
b = bytearray(struct.calcsize("<BB"))
372+
self._readinto(b)
373+
cmd, status = struct.unpack("<BB", b)
374+
if cmd != FileTransferService.MOVE_STATUS:
375+
raise ProtocolError()
376+
if status != FileTransferService.OK:
377+
raise ValueError("Missing file")

0 commit comments

Comments
 (0)