Skip to content

Commit 64b9177

Browse files
committed
Update examples for feedback
1 parent c4553ab commit 64b9177

File tree

4 files changed

+321
-102
lines changed

4 files changed

+321
-102
lines changed

README.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ Given a full path, returns the full contents of the file.
110110
The header is four fixed entries and a variable length path:
111111

112112
* Command: Single byte. Always `0x10`.
113-
* Chunk size: 32-bit number encoding the amount of data that the client can handle in the first reply.
114113
* Chunk offset: 32-bit number encoding the offset into the file to start the first chunk.
114+
* Chunk size: 32-bit number encoding the amount of data that the client can handle in the first reply.
115115
* Path length: 16-bit number encoding the encoded length of the path string.
116116
* Path: UTF-8 encoded string that is *not* null terminated. (We send the length instead.)
117117

@@ -126,8 +126,8 @@ The server will respond with:
126126
If the chunk length is smaller than the total length, then the client will request more data by sending:
127127
* Command: Single byte. Always `0x12`.
128128
* Status: Single byte. Always OK for now.
129-
* Chunk size: 32-bit number encoding the number of bytes to read. May be different than the original size. Does not need to be limited by the total size.
130129
* Chunk offset: 32-bit number encoding the offset into the file to start the next chunk.
130+
* Chunk size: 32-bit number encoding the number of bytes to read. May be different than the original size. Does not need to be limited by the total size.
131131

132132
The transaction is complete after the server has replied with all data. (No acknowledgement needed from the client.)
133133

@@ -136,6 +136,8 @@ The transaction is complete after the server has replied with all data. (No ackn
136136

137137
Writes the content to the given full path. If the file exists, it will be overwritten. Content may be written as received so an interrupted transfer may lead to a truncated file.
138138

139+
Offset larger than the existing file size will introduce zeros into the gap.
140+
139141
The header is four fixed entries and a variable length path:
140142

141143
* Command: Single byte. Always `0x20`.
@@ -157,7 +159,7 @@ The client will repeatedly respond until the total length has been transferred w
157159
* Data size: 32-bit number encoding the amount of data the client is sending.
158160
* Data
159161

160-
The transaction is complete after the server has received all data.
162+
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.
161163

162164

163165
`0x30` - Delete a file or directory
@@ -206,7 +208,7 @@ The server will reply with n+1 entries for a directory with n files:
206208
* Status: Single byte. `0x01` if the directory exists or `0x02` if it doesn't.
207209
* Entry number: 32-bit number encoding the entry number.
208210
* Total entries: 32-bit number encoding the total number of entries.
209-
* Flags: 8-bit number encoding data about the entries per-bit. Bit
211+
* Flags: 32-bit number encoding data about the entries.
210212
* Bit 0: Set when the entry is a directory
211213
* Bits 1-7: Reserved
212214
* File size: 32-bit number encoding the size of the file. Ignore for directories. Value may change.

adafruit_ble_file_transfer.py

Lines changed: 115 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
__version__ = "0.0.0-auto.0"
2525
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_File_Transfer.git"
2626

27+
CHUNK_SIZE = 490
28+
2729

2830
class FileTransferUUID(VendorUUID):
2931
"""UUIDs with the CircuitPython base UUID."""
@@ -59,7 +61,9 @@ def __init__(self):
5961
def bind(self, service):
6062
"""Binds the characteristic to the given Service."""
6163
bound_characteristic = super().bind(service)
62-
return _bleio.PacketBuffer(bound_characteristic, buffer_size=4)
64+
return _bleio.PacketBuffer(
65+
bound_characteristic, buffer_size=4, max_packet_size=512
66+
)
6367

6468

6569
class FileTransferService(Service):
@@ -79,23 +83,34 @@ class FileTransferService(Service):
7983

8084
# Commands
8185
INVALID = 0x00
82-
READ = 0x01
83-
WRITE = 0x02
84-
DELETE = 0x03
85-
MKDIR = 0x04
86-
LISTDIR = 0x05
87-
88-
# Statuses
86+
READ = 0x10
87+
READ_DATA = 0x11
88+
READ_PACING = 0x12
89+
WRITE = 0x20
90+
WRITE_PACING = 0x21
91+
WRITE_DATA = 0x22
92+
DELETE = 0x30
93+
DELETE_STATUS = 0x31
94+
MKDIR = 0x40
95+
MKDIR_STATUS = 0x41
96+
LISTDIR = 0x50
97+
LISTDIR_ENTRY = 0x51
98+
99+
# Responses
89100
# 0x00 is INVALID
90-
OK = 0x81 # pylint: disable=invalid-name
91-
ERROR = 0x82
92-
93-
ERROR_NO_FILE = 0xB0
101+
OK = 0x01 # pylint: disable=invalid-name
102+
ERROR = 0x02
103+
ERROR_NO_FILE = 0x03
104+
ERROR_PROTOCOL = 0x04
94105

95106
# Flags
96107
DIRECTORY = 0x01
97108

98109

110+
class ProtocolError(BaseException):
111+
"""Error thrown when expected bytes don't match"""
112+
113+
99114
class FileTransferClient:
100115
"""Helper class to communicating with a File Transfer server"""
101116

@@ -123,57 +138,108 @@ def _readinto(self, buffer):
123138
print("long packet", long_buffer[:read])
124139
return read
125140

126-
def read(self, path):
127-
"""Returns the contents of the file at the given path"""
141+
def read(self, path, *, offset=0):
142+
"""Returns the contents of the file at the given path starting at the given offset"""
128143
path = path.encode("utf-8")
129-
chunk_size = 10
144+
chunk_size = CHUNK_SIZE
130145
encoded = (
131-
struct.pack("<BIH", FileTransferService.READ, chunk_size, len(path)) + path
146+
struct.pack(
147+
"<BIIH", FileTransferService.READ, offset, chunk_size, len(path)
148+
)
149+
+ path
132150
)
133151
self._write(encoded)
134-
b = bytearray(struct.calcsize("<BBII") + chunk_size)
135-
contents_read = 0
152+
b = bytearray(struct.calcsize("<BBIII") + chunk_size)
153+
current_offset = offset
136154
content_length = None
137155
buf = None
138-
while content_length is None or contents_read < content_length:
156+
data_header_size = struct.calcsize("<BBIII")
157+
while content_length is None or current_offset < content_length:
139158
read = self._readinto(b)
140-
_, status, content_length, chunk_length = struct.unpack_from("<BBII", b)
159+
(
160+
cmd,
161+
status,
162+
current_offset,
163+
content_length,
164+
_,
165+
) = struct.unpack_from("<BBIII", b)
166+
if cmd != FileTransferService.READ_DATA:
167+
raise ProtocolError("Incorrect reply")
141168
if status != FileTransferService.OK:
142169
raise ValueError("Missing file")
143170
if buf is None:
144-
buf = bytearray(content_length)
145-
header_size = struct.calcsize("<BBII")
146-
buf[contents_read : contents_read + (read - header_size)] = b[
147-
header_size:read
171+
buf = bytearray(content_length - offset)
172+
out_offset = current_offset - offset
173+
buf[out_offset : out_offset + (read - data_header_size)] = b[
174+
data_header_size:read
148175
]
149-
if read - header_size > chunk_length:
150-
raise NotImplementedError("Chunk longer than a packet!")
151-
contents_read += read - header_size
152-
chunk_size = min(10, content_length - contents_read)
176+
current_offset += read - data_header_size
177+
chunk_size = min(CHUNK_SIZE, content_length - current_offset)
178+
if chunk_size == 0:
179+
break
153180
encoded = struct.pack(
154-
"<BBI", FileTransferService.READ, FileTransferService.OK, chunk_size
181+
"<BBII",
182+
FileTransferService.READ_PACING,
183+
FileTransferService.OK,
184+
current_offset,
185+
chunk_size,
155186
)
156187
self._write(encoded)
157188
return buf
158189

159-
def write(self, path, contents):
160-
"""Writes the given contents to the given path"""
190+
def write(self, path, contents, *, offset=0):
191+
"""Writes the given contents to the given path starting at the given offset.
192+
193+
If the file is shorter than the offset, zeros will be added in the gap."""
161194
path = path.encode("utf-8")
195+
total_length = len(contents) + offset
162196
encoded = (
163-
struct.pack("<BIH", FileTransferService.WRITE, len(contents), len(path))
197+
struct.pack(
198+
"<BIIH", FileTransferService.WRITE, offset, total_length, len(path)
199+
)
164200
+ path
165201
)
166202
self._write(encoded)
167-
b = bytearray(struct.calcsize("<BBI"))
203+
b = bytearray(struct.calcsize("<BBII"))
168204
written = 0
169205
while written < len(contents):
170206
self._readinto(b)
171-
_, status, free_space = struct.unpack("<BBI", b)
207+
cmd, status, current_offset, free_space = struct.unpack("<BBII", b)
172208
if status != FileTransferService.OK:
173-
raise ValueError("Invalid path")
174-
self._service.raw.write(contents[written : written + free_space])
209+
raise RuntimeError()
210+
if (
211+
cmd != FileTransferService.WRITE_PACING
212+
or current_offset != written + offset
213+
):
214+
self._write(
215+
struct.pack(
216+
"<BBII",
217+
FileTransferService.WRITE_DATA,
218+
FileTransferService.ERROR_PROTOCOL,
219+
0,
220+
0,
221+
)
222+
)
223+
raise ProtocolError()
224+
225+
self._write(
226+
struct.pack(
227+
"<BBII",
228+
FileTransferService.WRITE_DATA,
229+
FileTransferService.OK,
230+
current_offset,
231+
free_space,
232+
)
233+
)
234+
self._write(contents[written : written + free_space])
175235
written += free_space
176236

237+
# Wait for confirmation that everything was written ok.
238+
self._readinto(b)
239+
cmd, status, offset, free_space = struct.unpack("<BBII", b)
240+
if cmd != FileTransferService.WRITE_PACING or offset != total_length:
241+
raise ProtocolError()
242+
177243
def mkdir(self, path):
178244
"""Makes the directory and any missing parents"""
179245
path = path.encode("utf-8")
@@ -182,7 +248,9 @@ def mkdir(self, path):
182248

183249
b = bytearray(struct.calcsize("<BB"))
184250
self._readinto(b)
185-
_, status = struct.unpack("<BB", b)
251+
cmd, status = struct.unpack("<BB", b)
252+
if cmd != FileTransferService.MKDIR_STATUS:
253+
raise ProtocolError()
186254
if status != FileTransferService.OK:
187255
raise ValueError("Invalid path")
188256

@@ -195,22 +263,22 @@ def listdir(self, path):
195263
b = bytearray(self._service.raw.incoming_packet_length)
196264
i = 0
197265
total = 10 # starting value that will be replaced by the first response
198-
header_size = struct.calcsize("<BBIIBIH")
266+
header_size = struct.calcsize("<BBIIIIH")
199267
while i < total:
200268
read = self._readinto(b)
201269
offset = 0
202270
while offset < read:
203271
(
204-
_,
272+
cmd,
205273
status,
206274
i,
207275
total,
208276
flags,
209277
file_size,
210278
path_length,
211-
) = struct.unpack_from("<BBIIBIH", b, offset=offset)
212-
if status != FileTransferService.OK:
213-
raise ValueError("Invalid path")
279+
) = struct.unpack_from("<BBIIIIH", b, offset=offset)
280+
if cmd != FileTransferService.LISTDIR_ENTRY:
281+
raise ProtocolError()
214282
if i >= total:
215283
break
216284
path = str(
@@ -219,6 +287,8 @@ def listdir(self, path):
219287
)
220288
paths.append((path, file_size, flags))
221289
offset += header_size + path_length
290+
if status != FileTransferService.OK:
291+
break
222292
return paths
223293

224294
def delete(self, path):
@@ -229,6 +299,8 @@ def delete(self, path):
229299

230300
b = bytearray(struct.calcsize("<BB"))
231301
self._readinto(b)
232-
_, status = struct.unpack("<BB", b)
302+
cmd, status = struct.unpack("<BB", b)
303+
if cmd != FileTransferService.DELETE_STATUS:
304+
raise ProtocolError()
233305
if status != FileTransferService.OK:
234306
raise ValueError("Missing file")

0 commit comments

Comments
 (0)