Skip to content

Commit 75dad16

Browse files
Add simple RLE compression for ArduinoOTA
When uploading large, sparse filesystems, there are often many repeated, empty sectors. These take a long time to transmit, even over WiFi. Implement a basic RLE compression in espota.py (and only use it if it actually saves upload size), and a simple decompressor in ArduinoOTA. The actual flash update image is decompressed as received to the staging location, so the bootloader/etc. do not need to be aware of it (i.e. a 1MB filesystem still takes 1MB of flash in the update area, even if it only took 100KB to upload it). Fixes esp8266#4277
1 parent fb2cbe3 commit 75dad16

File tree

4 files changed

+214
-39
lines changed

4 files changed

+214
-39
lines changed

libraries/ArduinoOTA/ArduinoOTA.cpp

+124-2
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,114 @@ extern "C" {
3030
#endif
3131
#endif
3232

33+
34+
/* This class is only used in OTA and implements a dumb, low-memory decompression engine */
35+
class RLEDecompressor : public Stream {
36+
public:
37+
RLEDecompressor(WiFiClient client) {
38+
_client = client;
39+
_blockLen = 0;
40+
_blockIdx = 0;
41+
_block = new uint8_t[128];
42+
}
43+
44+
virtual ~RLEDecompressor() {
45+
delete[] _block;
46+
}
47+
48+
virtual int read() {
49+
int ret = -1; // Default to EOF
50+
if (_blockLen == _blockIdx) {
51+
_refill();
52+
}
53+
if (_blockIdx < _blockLen) {
54+
ret = _block[_blockIdx++];
55+
}
56+
return ret;
57+
}
58+
59+
virtual int peek() {
60+
return -1; // Not implemented, not needed for Updater
61+
}
62+
63+
size_t read(uint8_t*a, size_t&b) { return readBytes((char*)a, b); }
64+
65+
virtual size_t readBytes(char *buffer, size_t length) {
66+
if (_blockLen == _blockIdx) {
67+
_refill();
68+
}
69+
int toRead = std::min((int)(_blockLen - _blockIdx), (int)length);
70+
memcpy(buffer, _block + _blockIdx, toRead);
71+
_blockIdx += toRead;
72+
return toRead;
73+
}
74+
75+
virtual size_t write(uint8_t b) { return _client.write(b); }
76+
77+
virtual int available() {
78+
if (_blockLen == _blockIdx) {
79+
_refill();
80+
}
81+
return _blockLen - _blockIdx;
82+
}
83+
84+
85+
private:
86+
bool _refill() {
87+
int c = -1;
88+
while (_client.connected() && (c < 0)) {
89+
c = _client.read();
90+
yield();
91+
}
92+
if (c < 0) {
93+
return false;
94+
}
95+
if (c < 128) {
96+
int l = -1;
97+
while (_client.connected() && (l < 0)) {
98+
l = _client.read();
99+
yield();
100+
}
101+
if (l < 0) {
102+
return false;
103+
}
104+
memset(_block, l, c);
105+
_blockLen = c;
106+
_blockIdx = 0;
107+
return true;
108+
} else {
109+
c = c - 128;
110+
_blockLen = c;
111+
_blockIdx = 0;
112+
while (_client.connected() && c) {
113+
int ret = _client.readBytes(_block + _blockIdx, c);
114+
if (ret > 0) {
115+
c -= ret;
116+
_blockIdx += ret;
117+
} else {
118+
_blockIdx = _blockLen = 0;
119+
return false;
120+
}
121+
}
122+
_blockIdx = 0;
123+
return true;
124+
}
125+
}
126+
127+
private:
128+
WiFiClient _client;
129+
int _blockLeft;
130+
int _blockIdx;
131+
int _blockLen;
132+
uint8_t *_block;
133+
};
134+
135+
33136
ArduinoOTAClass::ArduinoOTAClass()
34137
: _port(0)
35138
, _udp_ota(0)
36139
, _initialized(false)
140+
, _useCompression(false)
37141
, _rebootOnSuccess(true)
38142
, _useMDNS(true)
39143
, _state(OTA_IDLE)
@@ -199,6 +303,15 @@ void ArduinoOTAClass::_onRx(){
199303
if(_md5.length() != 32)
200304
return;
201305

306+
String compress = readStringUntil('\n');
307+
compress.trim();
308+
if (compress == "COMPRESSRLE") {
309+
_useCompression = true;
310+
#ifdef OTA_DEBUG
311+
OTA_DEBUG.println("Compressed upload requested");
312+
#endif
313+
}
314+
202315
ota_ip = _ota_ip;
203316

204317
if (_password.length()){
@@ -273,7 +386,11 @@ void ArduinoOTAClass::_runUpdate() {
273386
_state = OTA_IDLE;
274387
return;
275388
}
276-
_udp_ota->append("OK", 2);
389+
if (_useCompression) {
390+
_udp_ota->append("COMPOK", 6);
391+
} else {
392+
_udp_ota->append("OK", 2);
393+
}
277394
_udp_ota->send(ota_ip, _ota_udp_port);
278395
delay(100);
279396

@@ -317,7 +434,12 @@ void ArduinoOTAClass::_runUpdate() {
317434
}
318435
_state = OTA_IDLE;
319436
}
320-
written = Update.write(client);
437+
if (_useCompression) {
438+
RLEDecompressor decomp(client);
439+
written = Update.write(decomp);
440+
} else {
441+
written = Update.write(client);
442+
}
321443
if (written > 0) {
322444
client.print(written, DEC);
323445
total += written;

libraries/ArduinoOTA/ArduinoOTA.h

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class ArduinoOTAClass
7575
String _nonce;
7676
UdpContext *_udp_ota;
7777
bool _initialized;
78+
bool _useCompression;
7879
bool _rebootOnSuccess;
7980
bool _useMDNS;
8081
ota_state_t _state;

tools/espota.py

+88-36
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,43 @@ def update_progress(progress):
6969
sys.stderr.write('.')
7070
sys.stderr.flush()
7171

72+
def rle(data):
73+
"RLE compress the input bitstream and return it as a list"
74+
buf = [0] * 128
75+
ret = []
76+
src = 0
77+
runlen = 0
78+
repeat = False
79+
while src < len(data):
80+
buf[runlen] = data[src]
81+
runlen = runlen + 1
82+
src = src + 1
83+
if runlen < 2:
84+
continue
85+
if repeat:
86+
if buf[runlen - 1] != buf[runlen - 2]:
87+
repeat = False
88+
if (not repeat) or (runlen == 128):
89+
ret += [runlen - 1] + [buf[0]]
90+
buf[0] = buf[runlen - 1]
91+
runlen = 1
92+
else:
93+
if buf[runlen - 1] == buf[runlen - 2]:
94+
repeat = True
95+
if runlen > 2:
96+
ret += [128 + runlen - 2] + buf[0:runlen - 2]
97+
buf[0] = buf[runlen - 1]
98+
buf[1] = buf[runlen - 1]
99+
runlen = 2
100+
continue
101+
if runlen == 128:
102+
ret += [128 + runlen - 1] + buf[0:runlen]
103+
runlen = 0
104+
if runlen:
105+
ret += [128 + runlen] + buf[0:runlen]
106+
return ret
107+
108+
72109
def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, command = FLASH):
73110
# Create a TCP/IP socket
74111
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -89,12 +126,18 @@ def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, comm
89126
sys.stderr.flush()
90127
logging.info(file_check_msg)
91128

92-
content_size = os.path.getsize(filename)
93-
f = open(filename,'rb')
94-
file_md5 = hashlib.md5(f.read()).hexdigest()
95-
f.close()
129+
with open(filename, "rb") as f:
130+
content = f.read()
131+
content_size = len(content)
132+
content_rle = rle(content)
133+
request_rle = len(content_rle) < content_size
134+
# request_rle = True
135+
file_md5 = hashlib.md5(content).hexdigest()
96136
logging.info('Upload size: %d', content_size)
97137
message = '%d %d %d %s\n' % (command, localPort, content_size, file_md5)
138+
if request_rle:
139+
# Add a request for compression, ignored on earlier ArduinoOTAs
140+
message += 'COMPRESSRLE\n'
98141

99142
# Wait for a connection
100143
logging.info('Sending invitation to: %s', remoteAddr)
@@ -103,42 +146,50 @@ def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, comm
103146
sent = sock2.sendto(message.encode(), remote_address)
104147
sock2.settimeout(10)
105148
try:
106-
data = sock2.recv(128).decode()
149+
data = sock2.recv(256).decode()
107150
except:
108151
logging.error('No Answer')
109152
sock2.close()
110153
return 1
111-
if (data != "OK"):
112-
if(data.startswith('AUTH')):
113-
nonce = data.split()[1]
114-
cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remoteAddr)
115-
cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
116-
passmd5 = hashlib.md5(password.encode()).hexdigest()
117-
result_text = '%s:%s:%s' % (passmd5 ,nonce, cnonce)
118-
result = hashlib.md5(result_text.encode()).hexdigest()
119-
sys.stderr.write('Authenticating...')
120-
sys.stderr.flush()
121-
message = '%d %s %s\n' % (AUTH, cnonce, result)
122-
sock2.sendto(message.encode(), remote_address)
123-
sock2.settimeout(10)
124-
try:
125-
data = sock2.recv(32).decode()
126-
except:
127-
sys.stderr.write('FAIL\n')
128-
logging.error('No Answer to our Authentication')
129-
sock2.close()
130-
return 1
131-
if (data != "OK"):
132-
sys.stderr.write('FAIL\n')
133-
logging.error('%s', data)
134-
sock2.close()
135-
sys.exit(1);
136-
return 1
137-
sys.stderr.write('OK\n')
154+
155+
if data == "COMPOK":
156+
compress = True
157+
elif data == "OK":
158+
compress = False
159+
elif data.startswith('AUTH'):
160+
nonce = data.split()[1]
161+
cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remoteAddr)
162+
cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
163+
passmd5 = hashlib.md5(password.encode()).hexdigest()
164+
result_text = '%s:%s:%s' % (passmd5 ,nonce, cnonce)
165+
result = hashlib.md5(result_text.encode()).hexdigest()
166+
sys.stderr.write('Authenticating...')
167+
sys.stderr.flush()
168+
message = '%d %s %s\n' % (AUTH, cnonce, result)
169+
sock2.sendto(message.encode(), remote_address)
170+
sock2.settimeout(10)
171+
try:
172+
data = sock2.recv(32).decode()
173+
except:
174+
sys.stderr.write('FAIL\n')
175+
logging.error('No Answer to our Authentication')
176+
sock2.close()
177+
return 1
178+
if data == "OK":
179+
compress = False
180+
elif data == "COMPOK":
181+
compress = True
138182
else:
139-
logging.error('Bad Answer: %s', data)
183+
sys.stderr.write('FAIL\n')
184+
logging.error('%s', data)
140185
sock2.close()
186+
sys.exit(1);
141187
return 1
188+
sys.stderr.write('OK\n')
189+
else:
190+
logging.error('Bad Answer: %s', data)
191+
sock2.close()
192+
return 1
142193
sock2.close()
143194

144195
logging.info('Waiting for device...')
@@ -155,16 +206,17 @@ def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, comm
155206
received_ok = False
156207

157208
try:
158-
f = open(filename, "rb")
159209
if (PROGRESS):
160210
update_progress(0)
161211
else:
162212
sys.stderr.write('Uploading')
163213
sys.stderr.flush()
164214
offset = 0
215+
chunk_size = 1460 # MTU-safe
165216
while True:
166-
chunk = f.read(1460)
167-
if not chunk: break
217+
if offset >= content_size:
218+
break
219+
chunk = content[offset:min(content_size, offset + chunk_size)]
168220
offset += len(chunk)
169221
update_progress(offset/float(content_size))
170222
connection.settimeout(10)

0 commit comments

Comments
 (0)