Browse Source

✨ Firmware Upload via Binary Transfer (#23462)

GHGiampy 2 years ago
parent
commit
1363b43946
No account linked to committer's email address

+ 5
- 0
Marlin/Configuration_adv.h View File

@@ -1621,6 +1621,11 @@
1621 1621
   // Add an optimized binary file transfer mode, initiated with 'M28 B1'
1622 1622
   //#define BINARY_FILE_TRANSFER
1623 1623
 
1624
+  #if ENABLED(BINARY_FILE_TRANSFER)
1625
+    // Include extra facilities (e.g., 'M20 F') supporting firmware upload via BINARY_FILE_TRANSFER
1626
+    //#define CUSTOM_FIRMWARE_UPLOAD
1627
+  #endif
1628
+
1624 1629
   /**
1625 1630
    * Set this option to one of the following (or the board's defaults apply):
1626 1631
    *

+ 7
- 1
Marlin/src/gcode/sd/M20.cpp View File

@@ -33,7 +33,13 @@
33 33
 void GcodeSuite::M20() {
34 34
   if (card.flag.mounted) {
35 35
     SERIAL_ECHOLNPGM(STR_BEGIN_FILE_LIST);
36
-    card.ls(TERN_(LONG_FILENAME_HOST_SUPPORT, parser.boolval('L')));
36
+    card.ls(
37
+      TERN_(CUSTOM_FIRMWARE_UPLOAD, parser.boolval('F'))
38
+      #if BOTH(CUSTOM_FIRMWARE_UPLOAD, LONG_FILENAME_HOST_SUPPORT)
39
+        ,
40
+      #endif
41
+      TERN_(LONG_FILENAME_HOST_SUPPORT, parser.boolval('L'))
42
+    );
37 43
     SERIAL_ECHOLNPGM(STR_END_FILE_LIST);
38 44
   }
39 45
   else

+ 8
- 8
Marlin/src/pins/pins.h View File

@@ -558,21 +558,21 @@
558 558
 #elif MB(CHITU3D_V9)
559 559
   #include "stm32f1/pins_CHITU3D_V9.h"          // STM32F1                                env:chitu_f103 env:chitu_f103_maple
560 560
 #elif MB(CREALITY_V4)
561
-  #include "stm32f1/pins_CREALITY_V4.h"         // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
561
+  #include "stm32f1/pins_CREALITY_V4.h"         // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
562 562
 #elif MB(CREALITY_V4210)
563
-  #include "stm32f1/pins_CREALITY_V4210.h"      // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
563
+  #include "stm32f1/pins_CREALITY_V4210.h"      // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
564 564
 #elif MB(CREALITY_V423)
565
-  #include "stm32f1/pins_CREALITY_V423.h"       // STM32F1                                env:STM32F103RET6_creality
565
+  #include "stm32f1/pins_CREALITY_V423.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer
566 566
 #elif MB(CREALITY_V427)
567
-  #include "stm32f1/pins_CREALITY_V427.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
567
+  #include "stm32f1/pins_CREALITY_V427.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
568 568
 #elif MB(CREALITY_V431, CREALITY_V431_A, CREALITY_V431_B, CREALITY_V431_C, CREALITY_V431_D)
569
-  #include "stm32f1/pins_CREALITY_V431.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
569
+  #include "stm32f1/pins_CREALITY_V431.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
570 570
 #elif MB(CREALITY_V452)
571
-  #include "stm32f1/pins_CREALITY_V452.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
571
+  #include "stm32f1/pins_CREALITY_V452.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
572 572
 #elif MB(CREALITY_V453)
573
-  #include "stm32f1/pins_CREALITY_V453.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
573
+  #include "stm32f1/pins_CREALITY_V453.h"       // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
574 574
 #elif MB(CREALITY_V24S1)
575
-  #include "stm32f1/pins_CREALITY_V24S1.h"      // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_maple
575
+  #include "stm32f1/pins_CREALITY_V24S1.h"      // STM32F1                                env:STM32F103RET6_creality env:STM32F103RET6_creality_xfer env:STM32F103RET6_creality_maple
576 576
 #elif MB(TRIGORILLA_PRO)
577 577
   #include "stm32f1/pins_TRIGORILLA_PRO.h"      // STM32F1                                env:trigorilla_pro env:trigorilla_pro_maple
578 578
 #elif MB(FLY_MINI)

+ 27
- 12
Marlin/src/sd/cardreader.cpp View File

@@ -195,11 +195,15 @@ char *createFilename(char * const buffer, const dir_t &p) {
195 195
 }
196 196
 
197 197
 //
198
-// Return 'true' if the item is a folder or G-code file
198
+// Return 'true' if the item is something Marlin can read
199 199
 //
200
-bool CardReader::is_dir_or_gcode(const dir_t &p) {
200
+bool CardReader::is_visible_entity(const dir_t &p OPTARG(CUSTOM_FIRMWARE_UPLOAD, bool onlyBin/*=false*/)) {
201 201
   //uint8_t pn0 = p.name[0];
202 202
 
203
+  #if DISABLED(CUSTOM_FIRMWARE_UPLOAD)
204
+    constexpr bool onlyBin = false;
205
+  #endif
206
+
203 207
   if ( (p.attributes & DIR_ATT_HIDDEN)                  // Hidden by attribute
204 208
     // When readDir() > 0 these must be false:
205 209
     //|| pn0 == DIR_NAME_FREE || pn0 == DIR_NAME_DELETED  // Clear or Deleted entry
@@ -211,7 +215,11 @@ bool CardReader::is_dir_or_gcode(const dir_t &p) {
211 215
 
212 216
   return (
213 217
     flag.filenameIsDir                                  // All Directories are ok
214
-    || (p.name[8] == 'G' && p.name[9] != '~')           // Non-backup *.G* files are accepted
218
+    || (!onlyBin && p.name[8] == 'G'
219
+                 && p.name[9] != '~')                   // Non-backup *.G* files are accepted
220
+    || ( onlyBin && p.name[8]  == 'B'
221
+                 && p.name[9]  == 'I'
222
+                 && p.name[10] == 'N')                  // BIN files are accepted
215 223
   );
216 224
 }
217 225
 
@@ -222,7 +230,7 @@ int CardReader::countItems(SdFile dir) {
222 230
   dir_t p;
223 231
   int c = 0;
224 232
   while (dir.readDir(&p, longFilename) > 0)
225
-    c += is_dir_or_gcode(p);
233
+    c += is_visible_entity(p);
226 234
 
227 235
   #if ALL(SDCARD_SORT_ALPHA, SDSORT_USES_RAM, SDSORT_CACHE_NAMES)
228 236
     nrFiles = c;
@@ -237,7 +245,7 @@ int CardReader::countItems(SdFile dir) {
237 245
 void CardReader::selectByIndex(SdFile dir, const uint8_t index) {
238 246
   dir_t p;
239 247
   for (uint8_t cnt = 0; dir.readDir(&p, longFilename) > 0;) {
240
-    if (is_dir_or_gcode(p)) {
248
+    if (is_visible_entity(p)) {
241 249
       if (cnt == index) {
242 250
         createFilename(filename, p);
243 251
         return;  // 0 based index
@@ -253,7 +261,7 @@ void CardReader::selectByIndex(SdFile dir, const uint8_t index) {
253 261
 void CardReader::selectByName(SdFile dir, const char * const match) {
254 262
   dir_t p;
255 263
   for (uint8_t cnt = 0; dir.readDir(&p, longFilename) > 0; cnt++) {
256
-    if (is_dir_or_gcode(p)) {
264
+    if (is_visible_entity(p)) {
257 265
       createFilename(filename, p);
258 266
       if (strcasecmp(match, filename) == 0) return;
259 267
     }
@@ -272,6 +280,7 @@ void CardReader::selectByName(SdFile dir, const char * const match) {
272 280
  */
273 281
 void CardReader::printListing(
274 282
   SdFile parent, const char * const prepend
283
+  OPTARG(CUSTOM_FIRMWARE_UPLOAD, bool onlyBin/*=false*/)
275 284
   OPTARG(LONG_FILENAME_HOST_SUPPORT, const bool includeLongNames/*=false*/)
276 285
   OPTARG(LONG_FILENAME_HOST_SUPPORT, const char * const prependLong/*=nullptr*/)
277 286
 ) {
@@ -297,12 +306,12 @@ void CardReader::printListing(
297 306
             char pathLong[lenPrependLong + strlen(longFilename) + 1];
298 307
             if (prependLong) { strcpy(pathLong, prependLong); pathLong[lenPrependLong - 1] = '/'; }
299 308
             strcpy(pathLong + lenPrependLong, longFilename);
300
-            printListing(child, path, true, pathLong);
309
+            printListing(child, path OPTARG(CUSTOM_FIRMWARE_UPLOAD, onlyBin), true, pathLong);
301 310
           }
302 311
           else
303
-            printListing(child, path);
312
+            printListing(child, path OPTARG(CUSTOM_FIRMWARE_UPLOAD, onlyBin));
304 313
         #else
305
-          printListing(child, path);
314
+          printListing(child, path OPTARG(CUSTOM_FIRMWARE_UPLOAD, onlyBin));
306 315
         #endif
307 316
       }
308 317
       else {
@@ -310,7 +319,7 @@ void CardReader::printListing(
310 319
         return;
311 320
       }
312 321
     }
313
-    else if (is_dir_or_gcode(p)) {
322
+    else if (is_visible_entity(p OPTARG(CUSTOM_FIRMWARE_UPLOAD, onlyBin))) {
314 323
       if (prepend) { SERIAL_ECHO(prepend); SERIAL_CHAR('/'); }
315 324
       SERIAL_ECHO(createFilename(filename, p));
316 325
       SERIAL_CHAR(' ');
@@ -330,10 +339,16 @@ void CardReader::printListing(
330 339
 //
331 340
 // List all files on the SD card
332 341
 //
333
-void CardReader::ls(TERN_(LONG_FILENAME_HOST_SUPPORT, bool includeLongNames/*=false*/)) {
342
+void CardReader::ls(
343
+  TERN_(CUSTOM_FIRMWARE_UPLOAD, const bool onlyBin/*=false*/)
344
+  #if BOTH(CUSTOM_FIRMWARE_UPLOAD, LONG_FILENAME_HOST_SUPPORT)
345
+    ,
346
+  #endif
347
+  TERN_(LONG_FILENAME_HOST_SUPPORT, const bool includeLongNames/*=false*/)
348
+) {
334 349
   if (flag.mounted) {
335 350
     root.rewind();
336
-    printListing(root, nullptr OPTARG(LONG_FILENAME_HOST_SUPPORT, includeLongNames));
351
+    printListing(root, nullptr OPTARG(CUSTOM_FIRMWARE_UPLOAD, onlyBin) OPTARG(LONG_FILENAME_HOST_SUPPORT, includeLongNames));
337 352
   }
338 353
 }
339 354
 

+ 9
- 2
Marlin/src/sd/cardreader.h View File

@@ -204,7 +204,13 @@ public:
204 204
     FORCE_INLINE static void getfilename_sorted(const uint16_t nr) { selectFileByIndex(nr); }
205 205
   #endif
206 206
 
207
-  static void ls(TERN_(LONG_FILENAME_HOST_SUPPORT, bool includeLongNames=false));
207
+  static void ls(
208
+    TERN_(CUSTOM_FIRMWARE_UPLOAD, const bool onlyBin=false)
209
+    #if BOTH(CUSTOM_FIRMWARE_UPLOAD, LONG_FILENAME_HOST_SUPPORT)
210
+      ,
211
+    #endif
212
+    TERN_(LONG_FILENAME_HOST_SUPPORT, const bool includeLongNames=false)
213
+  );
208 214
 
209 215
   #if ENABLED(POWER_LOSS_RECOVERY)
210 216
     static bool jobRecoverFileExists();
@@ -331,12 +337,13 @@ private:
331 337
   //
332 338
   // Directory items
333 339
   //
334
-  static bool is_dir_or_gcode(const dir_t &p);
340
+  static bool is_visible_entity(const dir_t &p OPTARG(CUSTOM_FIRMWARE_UPLOAD, const bool onlyBin=false));
335 341
   static int countItems(SdFile dir);
336 342
   static void selectByIndex(SdFile dir, const uint8_t index);
337 343
   static void selectByName(SdFile dir, const char * const match);
338 344
   static void printListing(
339 345
     SdFile parent, const char * const prepend
346
+    OPTARG(CUSTOM_FIRMWARE_UPLOAD, const bool onlyBin=false)
340 347
     OPTARG(LONG_FILENAME_HOST_SUPPORT, const bool includeLongNames=false)
341 348
     OPTARG(LONG_FILENAME_HOST_SUPPORT, const char * const prependLong=nullptr)
342 349
   );

+ 434
- 0
buildroot/share/scripts/MarlinBinaryProtocol.py View File

@@ -0,0 +1,434 @@
1
+#
2
+# MarlinBinaryProtocol.py
3
+# Supporting Firmware upload via USB/Serial, saving to the attached media.
4
+#
5
+import serial
6
+import math
7
+import time
8
+from collections import deque
9
+import threading
10
+import sys
11
+import datetime
12
+import random
13
+try:
14
+    import heatshrink
15
+    heatshrink_exists = True
16
+except ImportError:
17
+    heatshrink_exists = False
18
+
19
+
20
+def millis():
21
+    return time.perf_counter() * 1000
22
+
23
+class TimeOut(object):
24
+    def __init__(self, milliseconds):
25
+        self.duration = milliseconds
26
+        self.reset()
27
+
28
+    def reset(self):
29
+        self.endtime = millis() + self.duration
30
+
31
+    def timedout(self):
32
+        return millis() > self.endtime
33
+
34
+class ReadTimeout(Exception):
35
+    pass
36
+class FatalError(Exception):
37
+    pass
38
+class SycronisationError(Exception):
39
+    pass
40
+class PayloadOverflow(Exception):
41
+    pass
42
+class ConnectionLost(Exception):
43
+    pass
44
+
45
+class Protocol(object):
46
+    device = None
47
+    baud = None
48
+    max_block_size = 0
49
+    port = None
50
+    block_size = 0
51
+
52
+    packet_transit = None
53
+    packet_status = None
54
+    packet_ping = None
55
+
56
+    errors = 0
57
+    packet_buffer = None
58
+    simulate_errors = 0
59
+    sync = 0
60
+    connected = False
61
+    syncronised = False
62
+    worker_thread = None
63
+
64
+    response_timeout = 1000
65
+
66
+    applications = []
67
+    responses = deque()
68
+
69
+    def __init__(self, device, baud, bsize, simerr, timeout):
70
+        print("pySerial Version:", serial.VERSION)
71
+        self.port = serial.Serial(device, baudrate = baud, write_timeout = 0, timeout = 1)
72
+        self.device = device
73
+        self.baud = baud
74
+        self.block_size = int(bsize)
75
+        self.simulate_errors = max(min(simerr, 1.0), 0.0);
76
+        self.connected = True
77
+        self.response_timeout = timeout
78
+
79
+        self.register(['ok', 'rs', 'ss', 'fe'], self.process_input)
80
+
81
+        self.worker_thread = threading.Thread(target=Protocol.receive_worker, args=(self,))
82
+        self.worker_thread.start()
83
+
84
+    def receive_worker(self):
85
+        while self.port.in_waiting:
86
+            self.port.reset_input_buffer()
87
+
88
+        def dispatch(data):
89
+            for tokens, callback in self.applications:
90
+                for token in tokens:
91
+                    if token == data[:len(token)]:
92
+                        callback((token, data[len(token):]))
93
+                        return
94
+
95
+        def reconnect():
96
+            print("Reconnecting..")
97
+            self.port.close()
98
+            for x in range(10):
99
+                try:
100
+                    if self.connected:
101
+                        self.port = serial.Serial(self.device, baudrate = self.baud, write_timeout = 0, timeout = 1)
102
+                        return
103
+                    else:
104
+                        print("Connection closed")
105
+                        return
106
+                except:
107
+                    time.sleep(1)
108
+            raise ConnectionLost()
109
+
110
+        while self.connected:
111
+            try:
112
+                data = self.port.readline().decode('utf8').rstrip()
113
+                if len(data):
114
+                    #print(data)
115
+                    dispatch(data)
116
+            except OSError:
117
+                reconnect()
118
+            except UnicodeDecodeError:
119
+                # dodgy client output or datastream corruption
120
+                self.port.reset_input_buffer()
121
+
122
+    def shutdown(self):
123
+        self.connected = False
124
+        self.worker_thread.join()
125
+        self.port.close()
126
+
127
+    def process_input(self, data):
128
+        #print(data)
129
+        self.responses.append(data)
130
+
131
+    def register(self, tokens, callback):
132
+        self.applications.append((tokens, callback))
133
+
134
+    def send(self, protocol, packet_type, data = bytearray()):
135
+        self.packet_transit = self.build_packet(protocol, packet_type, data)
136
+        self.packet_status = 0
137
+        self.transmit_attempt = 0
138
+
139
+        timeout = TimeOut(self.response_timeout * 20)
140
+        while self.packet_status == 0:
141
+            try:
142
+                if timeout.timedout():
143
+                    raise ConnectionLost()
144
+                self.transmit_packet(self.packet_transit)
145
+                self.await_response()
146
+            except ReadTimeout:
147
+                self.errors += 1
148
+                #print("Packetloss detected..")
149
+        self.packet_transit = None
150
+
151
+    def await_response(self):
152
+        timeout = TimeOut(self.response_timeout)
153
+        while not len(self.responses):
154
+            time.sleep(0.00001)
155
+            if timeout.timedout():
156
+                raise ReadTimeout()
157
+
158
+        while len(self.responses):
159
+            token, data = self.responses.popleft()
160
+            switch = {'ok' : self.response_ok, 'rs': self.response_resend, 'ss' : self.response_stream_sync, 'fe' : self.response_fatal_error}
161
+            switch[token](data)
162
+
163
+    def send_ascii(self, data, send_and_forget = False):
164
+        self.packet_transit = bytearray(data, "utf8") + b'\n'
165
+        self.packet_status = 0
166
+        self.transmit_attempt = 0
167
+
168
+        timeout = TimeOut(self.response_timeout * 20)
169
+        while self.packet_status == 0:
170
+            try:
171
+                if timeout.timedout():
172
+                    return
173
+                self.port.write(self.packet_transit)
174
+                if send_and_forget:
175
+                    self.packet_status = 1
176
+                else:
177
+                    self.await_response_ascii()
178
+            except ReadTimeout:
179
+                self.errors += 1
180
+                #print("Packetloss detected..")
181
+            except serial.serialutil.SerialException:
182
+                return
183
+        self.packet_transit = None
184
+
185
+    def await_response_ascii(self):
186
+        timeout = TimeOut(self.response_timeout)
187
+        while not len(self.responses):
188
+            time.sleep(0.00001)
189
+            if timeout.timedout():
190
+                raise ReadTimeout()
191
+        token, data = self.responses.popleft()
192
+        self.packet_status = 1
193
+
194
+    def corrupt_array(self, data):
195
+        rid = random.randint(0, len(data) - 1)
196
+        data[rid] ^= 0xAA
197
+        return data
198
+
199
+    def transmit_packet(self, packet):
200
+        packet = bytearray(packet)
201
+        if(self.simulate_errors > 0 and random.random() > (1.0 - self.simulate_errors)):
202
+            if random.random() > 0.9:
203
+                #random data drop
204
+                start = random.randint(0, len(packet))
205
+                end = start + random.randint(1, 10)
206
+                packet = packet[:start] + packet[end:]
207
+                #print("Dropping {0} bytes".format(end - start))
208
+            else:
209
+                #random corruption
210
+                packet = self.corrupt_array(packet)
211
+                #print("Single byte corruption")
212
+        self.port.write(packet)
213
+        self.transmit_attempt += 1
214
+
215
+    def build_packet(self, protocol, packet_type, data = bytearray()):
216
+        PACKET_TOKEN = 0xB5AD
217
+
218
+        if len(data) > self.max_block_size:
219
+            raise PayloadOverflow()
220
+
221
+        packet_buffer = bytearray()
222
+
223
+        packet_buffer += self.pack_int8(self.sync)                           # 8bit sync id
224
+        packet_buffer += self.pack_int4_2(protocol, packet_type)             # 4 bit protocol id, 4 bit packet type
225
+        packet_buffer += self.pack_int16(len(data))                          # 16bit packet length
226
+        packet_buffer += self.pack_int16(self.build_checksum(packet_buffer)) # 16bit header checksum
227
+
228
+        if len(data):
229
+            packet_buffer += data
230
+            packet_buffer += self.pack_int16(self.build_checksum(packet_buffer))
231
+
232
+        packet_buffer =  self.pack_int16(PACKET_TOKEN) + packet_buffer       # 16bit start token, not included in checksum
233
+        return packet_buffer
234
+
235
+    # checksum 16 fletchers
236
+    def checksum(self, cs, value):
237
+        cs_low = (((cs & 0xFF) + value) % 255);
238
+        return ((((cs >> 8) + cs_low) % 255) << 8) | cs_low;
239
+
240
+    def build_checksum(self, buffer):
241
+        cs = 0
242
+        for b in buffer:
243
+            cs = self.checksum(cs, b)
244
+        return cs
245
+
246
+    def pack_int32(self, value):
247
+        return value.to_bytes(4, byteorder='little')
248
+
249
+    def pack_int16(self, value):
250
+        return value.to_bytes(2, byteorder='little')
251
+
252
+    def pack_int8(self, value):
253
+        return value.to_bytes(1, byteorder='little')
254
+
255
+    def pack_int4_2(self, vh, vl):
256
+        value = ((vh & 0xF) << 4) | (vl & 0xF)
257
+        return value.to_bytes(1, byteorder='little')
258
+
259
+    def connect(self):
260
+        print("Connecting: Switching Marlin to Binary Protocol...")
261
+        self.send_ascii("M28B1")
262
+        self.send(0, 1)
263
+
264
+    def disconnect(self):
265
+        self.send(0, 2)
266
+        self.syncronised = False
267
+
268
+    def response_ok(self, data):
269
+        try:
270
+            packet_id = int(data);
271
+        except ValueError:
272
+            return
273
+        if packet_id != self.sync:
274
+            raise SycronisationError()
275
+        self.sync = (self.sync + 1) % 256
276
+        self.packet_status = 1
277
+
278
+    def response_resend(self, data):
279
+        packet_id = int(data);
280
+        self.errors += 1
281
+        if not self.syncronised:
282
+            print("Retrying syncronisation")
283
+        elif packet_id != self.sync:
284
+            raise SycronisationError()
285
+
286
+    def response_stream_sync(self, data):
287
+        sync, max_block_size, protocol_version = data.split(',')
288
+        self.sync = int(sync)
289
+        self.max_block_size = int(max_block_size)
290
+        self.block_size = self.max_block_size if self.max_block_size < self.block_size else self.block_size
291
+        self.protocol_version = protocol_version
292
+        self.packet_status = 1
293
+        self.syncronised = True
294
+        print("Connection synced [{0}], binary protocol version {1}, {2} byte payload buffer".format(self.sync, self.protocol_version, self.max_block_size))
295
+
296
+    def response_fatal_error(self, data):
297
+        raise FatalError()
298
+
299
+
300
+class FileTransferProtocol(object):
301
+    protocol_id = 1
302
+
303
+    class Packet(object):
304
+        QUERY = 0
305
+        OPEN  = 1
306
+        CLOSE = 2
307
+        WRITE = 3
308
+        ABORT = 4
309
+
310
+    responses = deque()
311
+    def __init__(self, protocol, timeout = None):
312
+        protocol.register(['PFT:success', 'PFT:version:', 'PFT:fail', 'PFT:busy', 'PFT:ioerror', 'PTF:invalid'], self.process_input)
313
+        self.protocol = protocol
314
+        self.response_timeout = timeout or protocol.response_timeout
315
+
316
+    def process_input(self, data):
317
+        #print(data)
318
+        self.responses.append(data)
319
+
320
+    def await_response(self, timeout = None):
321
+        timeout = TimeOut(timeout or self.response_timeout)
322
+        while not len(self.responses):
323
+            time.sleep(0.0001)
324
+            if timeout.timedout():
325
+                raise ReadTimeout()
326
+
327
+        return self.responses.popleft()
328
+
329
+    def connect(self):
330
+        self.protocol.send(FileTransferProtocol.protocol_id, FileTransferProtocol.Packet.QUERY);
331
+
332
+        token, data = self.await_response()
333
+        if token != 'PFT:version:':
334
+            return False
335
+
336
+        self.version, _, compression = data.split(':')
337
+        if compression != 'none':
338
+            algorithm, window, lookahead = compression.split(',')
339
+            self.compression = {'algorithm': algorithm, 'window': int(window), 'lookahead': int(lookahead)}
340
+        else:
341
+            self.compression = {'algorithm': 'none'}
342
+
343
+        print("File Transfer version: {0}, compression: {1}".format(self.version, self.compression['algorithm']))
344
+
345
+    def open(self, filename, compression, dummy):
346
+        payload =  b'\1' if dummy else b'\0'          # dummy transfer
347
+        payload += b'\1' if compression else b'\0'    # payload compression
348
+        payload += bytearray(filename, 'utf8') + b'\0'# target filename + null terminator
349
+
350
+        timeout = TimeOut(5000)
351
+        token = None
352
+        self.protocol.send(FileTransferProtocol.protocol_id, FileTransferProtocol.Packet.OPEN, payload);
353
+        while token != 'PFT:success' and not timeout.timedout():
354
+            try:
355
+                token, data = self.await_response(1000)
356
+                if token == 'PFT:success':
357
+                    print(filename,"opened")
358
+                    return
359
+                elif token == 'PFT:busy':
360
+                    print("Broken transfer detected, purging")
361
+                    self.abort()
362
+                    time.sleep(0.1)
363
+                    self.protocol.send(FileTransferProtocol.protocol_id, FileTransferProtocol.Packet.OPEN, payload);
364
+                    timeout.reset()
365
+                elif token == 'PFT:fail':
366
+                    raise Exception("Can not open file on client")
367
+            except ReadTimeout:
368
+                pass
369
+        raise ReadTimeout()
370
+
371
+    def write(self, data):
372
+        self.protocol.send(FileTransferProtocol.protocol_id, FileTransferProtocol.Packet.WRITE, data);
373
+
374
+    def close(self):
375
+        self.protocol.send(FileTransferProtocol.protocol_id, FileTransferProtocol.Packet.CLOSE);
376
+        token, data = self.await_response(1000)
377
+        if token == 'PFT:success':
378
+            print("File closed")
379
+            return
380
+        elif token == 'PFT:ioerror':
381
+            print("Client storage device IO error")
382
+        elif token == 'PFT:invalid':
383
+            print("No open file")
384
+
385
+    def abort(self):
386
+        self.protocol.send(FileTransferProtocol.protocol_id, FileTransferProtocol.Packet.ABORT);
387
+        token, data = self.await_response()
388
+        if token == 'PFT:success':
389
+            print("Transfer Aborted")
390
+
391
+    def copy(self, filename, dest_filename, compression, dummy):
392
+        self.connect()
393
+
394
+        compression_support = heatshrink_exists and self.compression['algorithm'] == 'heatshrink' and compression
395
+        if compression and (not heatshrink_exists or not self.compression['algorithm'] == 'heatshrink'):
396
+            print("Compression not supported by client")
397
+        #compression_support = False
398
+
399
+        data = open(filename, "rb").read()
400
+        filesize = len(data)
401
+
402
+        self.open(dest_filename, compression_support, dummy)
403
+
404
+        block_size = self.protocol.block_size
405
+        if compression_support:
406
+            data = heatshrink.encode(data, window_sz2=self.compression['window'], lookahead_sz2=self.compression['lookahead'])
407
+
408
+        cratio = filesize / len(data)
409
+
410
+        blocks = math.floor((len(data) + block_size - 1) / block_size)
411
+        kibs = 0
412
+        dump_pctg = 0
413
+        start_time = millis()
414
+        for i in range(blocks):
415
+            start = block_size * i
416
+            end = start + block_size
417
+            self.write(data[start:end])
418
+            kibs = (( (i+1) * block_size) / 1024) / (millis() + 1 - start_time) * 1000
419
+            if (i / blocks) >= dump_pctg:
420
+                print("\r{0:2.2f}% {1:4.2f}KiB/s {2} Errors: {3}".format((i / blocks) * 100, kibs, "[{0:4.2f}KiB/s]".format(kibs * cratio) if compression_support else "", self.protocol.errors), end='')
421
+                dump_pctg += 0.1
422
+        print("\r{0:2.2f}% {1:4.2f}KiB/s {2} Errors: {3}".format(100, kibs, "[{0:4.2f}KiB/s]".format(kibs * cratio) if compression_support else "", self.protocol.errors)) # no one likes transfers finishing at 99.8%
423
+
424
+        self.close()
425
+        print("Transfer complete")
426
+
427
+
428
+class EchoProtocol(object):
429
+    def __init__(self, protocol):
430
+        protocol.register(['echo:'], self.process_input)
431
+        self.protocol = protocol
432
+
433
+    def process_input(self, data):
434
+        print(data)

+ 274
- 0
buildroot/share/scripts/upload.py View File

@@ -0,0 +1,274 @@
1
+import argparse
2
+import sys
3
+import os
4
+import time
5
+import random
6
+import serial
7
+
8
+Import("env")
9
+
10
+# Needed (only) for compression, but there are problems with pip install heatshrink
11
+#try:
12
+#    import heatshrink
13
+#except ImportError:
14
+#    # Install heatshrink
15
+#    print("Installing 'heatshrink' python module...")
16
+#    env.Execute(env.subst("$PYTHONEXE -m pip install heatshrink"))
17
+# 
18
+# Not tested: If it's safe to install python libraries in PIO python try:
19
+#    env.Execute(env.subst("$PYTHONEXE -m pip install https://github.com/p3p/pyheatshrink/releases/download/0.3.3/pyheatshrink-pip.zip"))
20
+
21
+import MarlinBinaryProtocol
22
+
23
+# Internal debug flag
24
+Debug = False
25
+
26
+#-----------------#
27
+# Upload Callback #
28
+#-----------------#
29
+def Upload(source, target, env):
30
+
31
+    #------------------#
32
+    # Marlin functions #
33
+    #------------------#
34
+    def _GetMarlinEnv(marlinEnv, feature):
35
+        if not marlinEnv: return None
36
+        return marlinEnv[feature] if feature in marlinEnv else None
37
+
38
+    #----------------#
39
+    # Port functions #
40
+    #----------------#
41
+    def _GetUploadPort(env):
42
+        if Debug: print('Autodetecting upload port...')
43
+        env.AutodetectUploadPort(env)
44
+        port = env.subst('$UPLOAD_PORT')
45
+        if not port:
46
+            raise Exception('Error detecting the upload port.')
47
+        if Debug: print('OK')
48
+        return port
49
+
50
+    #-------------------------#
51
+    # Simple serial functions #
52
+    #-------------------------#
53
+    def _Send(data):
54
+        if Debug: print(f'>> {data}')
55
+        strdata = bytearray(data, 'utf8') + b'\n'
56
+        port.write(strdata)
57
+        time.sleep(0.010)
58
+
59
+    def _Recv():
60
+        clean_responses = []
61
+        responses = port.readlines()
62
+        for Resp in responses:
63
+            # Test: suppress invaid chars (coming from debug info)
64
+            try:
65
+                clean_response = Resp.decode('utf8').rstrip().lstrip()
66
+                clean_responses.append(clean_response)
67
+            except:
68
+                pass
69
+            if Debug: print(f'<< {clean_response}')
70
+        return clean_responses
71
+
72
+    #------------------#
73
+    # SDCard functions #
74
+    #------------------#
75
+    def _CheckSDCard():
76
+        if Debug: print('Checking SD card...')
77
+        _Send('M21')
78
+        Responses = _Recv()
79
+        if len(Responses) < 1 or not any('SD card ok' in r for r in Responses):
80
+            raise Exception('Error accessing SD card')
81
+        if Debug: print('SD Card OK')
82
+        return True
83
+
84
+    #----------------#
85
+    # File functions #
86
+    #----------------#
87
+    def _GetFirmwareFiles():
88
+        if Debug: print('Get firmware files...')
89
+        _Send('M20 F')
90
+        Responses = _Recv()
91
+        if len(Responses) < 3 or not any('file list' in r for r in Responses):
92
+            raise Exception('Error getting firmware files')
93
+        if Debug: print('OK')
94
+        return Responses
95
+        
96
+    def _FilterFirmwareFiles(FirmwareList):
97
+        Firmwares = []
98
+        for FWFile in FirmwareList:
99
+            if not '/' in FWFile and '.BIN' in FWFile:
100
+                idx = FWFile.index('.BIN')
101
+                Firmwares.append(FWFile[:idx+4])
102
+        return Firmwares
103
+
104
+    def _RemoveFirmwareFile(FirmwareFile):
105
+        _Send(f'M30 /{FirmwareFile}')
106
+        Responses = _Recv()
107
+        Removed = len(Responses) >= 1 and any('File deleted' in r for r in Responses)
108
+        if not Removed:
109
+            raise Exception(f"Firmware file '{FirmwareFile}' not removed")
110
+        return Removed
111
+
112
+
113
+    #---------------------#
114
+    # Callback Entrypoint #
115
+    #---------------------#
116
+    port = None
117
+    protocol = None
118
+    filetransfer = None
119
+
120
+    # Get Marlin evironment vars
121
+    MarlinEnv = env['MARLIN_FEATURES']
122
+    marlin_pioenv = _GetMarlinEnv(MarlinEnv, 'PIOENV')
123
+    marlin_motherboard = _GetMarlinEnv(MarlinEnv, 'MOTHERBOARD')
124
+    marlin_board_info_name = _GetMarlinEnv(MarlinEnv, 'BOARD_INFO_NAME')
125
+    marlin_board_custom_build_flags = _GetMarlinEnv(MarlinEnv, 'BOARD_CUSTOM_BUILD_FLAGS')
126
+    marlin_firmware_bin = _GetMarlinEnv(MarlinEnv, 'FIRMWARE_BIN')
127
+    marlin_custom_firmware_upload = _GetMarlinEnv(MarlinEnv, 'CUSTOM_FIRMWARE_UPLOAD') is not None
128
+    marlin_short_build_version = _GetMarlinEnv(MarlinEnv, 'SHORT_BUILD_VERSION')
129
+    marlin_string_config_h_author = _GetMarlinEnv(MarlinEnv, 'STRING_CONFIG_H_AUTHOR')
130
+
131
+    # Get firmware upload params
132
+    upload_firmware_source_name = str(source[0])    # Source firmware filename
133
+    upload_speed = env['UPLOAD_SPEED'] if 'UPLOAD_SPEED' in env else 115200
134
+                                                    # baud rate of serial connection
135
+    upload_port = _GetUploadPort(env)               # Serial port to use
136
+
137
+    # Set local upload params
138
+    upload_firmware_target_name = os.path.basename(upload_firmware_source_name)     # WARNING! Need rework on "binary_stream" to allow filename > 8.3
139
+                                                    # Target firmware filename
140
+    upload_timeout = 1000                           # Communication timout, lossy/slow connections need higher values
141
+    upload_blocksize = 512                          # Transfer block size. 512 = Autodetect
142
+    upload_compression = True                       # Enable compression
143
+    upload_error_ratio = 0                          # Simulated corruption ratio
144
+    upload_test = False                             # Benchmark the serial link without storing the file
145
+    upload_reset = True                             # Trigger a soft reset for firmware update after the upload
146
+
147
+    # Set local upload params based on board type to change script behavior
148
+    # "upload_delete_old_bins": delete all *.bin files in the root of SD Card
149
+    upload_delete_old_bins = marlin_motherboard in ['BOARD_CREALITY_V4',   'BOARD_CREALITY_V4210', 'BOARD_CREALITY_V423', 'BOARD_CREALITY_V427',
150
+                                                    'BOARD_CREALITY_V431', 'BOARD_CREALITY_V452',  'BOARD_CREALITY_V453', 'BOARD_CREALITY_V24S1']
151
+    try:
152
+
153
+        # Start upload job
154
+        print(f"Uploading firmware '{os.path.basename(upload_firmware_target_name)}' to '{marlin_motherboard}' via '{upload_port}'")
155
+
156
+        # Dump some debug info
157
+        if Debug:
158
+            print('Upload using:')
159
+            print('---- Marlin --------------------')
160
+            print(f' PIOENV                 : {marlin_pioenv}')
161
+            print(f' SHORT_BUILD_VERSION    : {marlin_short_build_version}')
162
+            print(f' STRING_CONFIG_H_AUTHOR : {marlin_string_config_h_author}')
163
+            print(f' MOTHERBOARD            : {marlin_motherboard}')
164
+            print(f' BOARD_INFO_NAME        : {marlin_board_info_name}')
165
+            print(f' CUSTOM_BUILD_FLAGS     : {marlin_board_custom_build_flags}')
166
+            print(f' FIRMWARE_BIN           : {marlin_firmware_bin}')
167
+            print(f' CUSTOM_FIRMWARE_UPLOAD : {marlin_custom_firmware_upload}')
168
+            print('---- Upload parameters ---------')
169
+            print(f' Source      : {upload_firmware_source_name}')
170
+            print(f' Target      : {upload_firmware_target_name}')
171
+            print(f' Port        : {upload_port} @ {upload_speed} baudrate')
172
+            print(f' Timeout     : {upload_timeout}')
173
+            print(f' Block size  : {upload_blocksize}')
174
+            print(f' Compression : {upload_compression}')
175
+            print(f' Error ratio : {upload_error_ratio}')
176
+            print(f' Test        : {upload_test}')
177
+            print(f' Reset       : {upload_reset}')
178
+            print('--------------------------------')
179
+
180
+        # Custom implementations based on board parameters
181
+
182
+        # Delete all *.bin files on the root of SD Card (if flagged)
183
+        if upload_delete_old_bins:
184
+            # CUSTOM_FIRMWARE_UPLOAD is needed for this feature
185
+            if not marlin_custom_firmware_upload:
186
+                raise Exception(f"CUSTOM_FIRMWARE_UPLOAD must be enabled in 'Configuration_adv.h' for '{marlin_motherboard}'")
187
+
188
+            # Generate a new 8.3 random filename 
189
+            # This board remember the last firmware filename and doesn't allow to flash from that filename
190
+            upload_firmware_target_name = f"fw-{''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=5))}.BIN"
191
+            print(f"Board {marlin_motherboard}: Overriding firmware filename to '{upload_firmware_target_name}'")
192
+
193
+            # Init serial port
194
+            port = serial.Serial(upload_port, baudrate = upload_speed, write_timeout = 0, timeout = 0.1)
195
+            port.reset_input_buffer()
196
+
197
+            # Check SD card status
198
+            _CheckSDCard()
199
+            
200
+            # Get firmware files
201
+            FirmwareFiles = _GetFirmwareFiles()
202
+            if Debug:
203
+                for FirmwareFile in FirmwareFiles:
204
+                    print(f'Found: {FirmwareFile}')
205
+                
206
+            # Get all 1st level firmware files (to remove)
207
+            OldFirmwareFiles = _FilterFirmwareFiles(FirmwareFiles[1:len(FirmwareFiles)-2])   # Skip header and footers of list
208
+            if len(OldFirmwareFiles) == 0:
209
+                print('No old firmware files to delete')
210
+            else:
211
+                print(f"Remove {len(OldFirmwareFiles)} old firmware file{'s' if len(OldFirmwareFiles) != 1 else ''}:")
212
+                for OldFirmwareFile in OldFirmwareFiles:
213
+                    print(f" -Removing- '{OldFirmwareFile}'...")
214
+                    print(' OK' if _RemoveFirmwareFile(OldFirmwareFile) else ' Error!')
215
+
216
+            # Close serial
217
+            port.close()
218
+
219
+            # Cleanup completed
220
+            if Debug: print('Cleanup completed')
221
+
222
+        # WARNING! The serial port must be closed here because the serial transfer that follow needs it!
223
+    
224
+        # Upload firmware file
225
+        if Debug: print(f"Copy '{upload_firmware_source_name}' --> '{upload_firmware_target_name}'")
226
+        protocol = MarlinBinaryProtocol.Protocol(upload_port, upload_speed, upload_blocksize, float(upload_error_ratio), int(upload_timeout))
227
+        #echologger = MarlinBinaryProtocol.EchoProtocol(protocol)
228
+        protocol.connect()
229
+        filetransfer = MarlinBinaryProtocol.FileTransferProtocol(protocol)
230
+        filetransfer.copy(upload_firmware_source_name, upload_firmware_target_name, upload_compression, upload_test)
231
+        protocol.disconnect()
232
+
233
+        # Notify upload completed
234
+        protocol.send_ascii('M117 Firmware uploaded')
235
+
236
+        # Remount SD card
237
+        print('Wait for SD card release...')
238
+        time.sleep(1)
239
+        print('Remount SD card')
240
+        protocol.send_ascii('M21')
241
+
242
+        # Trigger firmware update
243
+        if upload_reset:
244
+            print('Trigger firmware update...')
245
+            protocol.send_ascii('M997', True)
246
+
247
+        protocol: protocol.shutdown()
248
+        print('Firmware update completed')
249
+
250
+    except KeyboardInterrupt:
251
+        if port: port.close()
252
+        if filetransfer: filetransfer.abort()
253
+        if protocol: protocol.shutdown()
254
+        raise
255
+
256
+    except serial.SerialException as se:
257
+        if port: port.close()
258
+        print(f'Serial excepion: {se}')
259
+        raise Exception(se)
260
+
261
+    except MarlinBinaryProtocol.FatalError:
262
+        if port: port.close()
263
+        if protocol: protocol.shutdown()
264
+        print('Too many retries, Abort')
265
+        raise
266
+
267
+    except:
268
+        if port: port.close()
269
+        if protocol: protocol.shutdown()
270
+        print('Firmware not updated')
271
+        raise
272
+
273
+# Attach custom upload callback
274
+env.Replace(UPLOADCMD=Upload)

+ 6
- 1
ini/stm32f1.ini View File

@@ -100,7 +100,6 @@ build_flags                 = ${common_STM32F103RC_variant.build_flags}
100 100
                               -DTIMER_SERVO=TIM5 -DDEFAULT_SPI=3
101 101
 build_unflags               = ${common_STM32F103RC_variant.build_unflags}
102 102
                               -DUSBCON -DUSBD_USE_CDC
103
-monitor_speed               = 115200
104 103
 debug_tool                  = stlink
105 104
 
106 105
 #
@@ -124,6 +123,12 @@ monitor_speed               = 115200
124 123
 debug_tool                  = jlink
125 124
 upload_protocol             = jlink
126 125
 
126
+[env:STM32F103RET6_creality_xfer]
127
+extends         = env:STM32F103RET6_creality
128
+extra_scripts   = ${env:STM32F103RET6_creality.extra_scripts}
129
+                  pre:buildroot/share/scripts/upload.py
130
+upload_protocol = custom
131
+
127 132
 #
128 133
 # BigTree SKR Mini E3 V2.0 & DIP / SKR CR6 (STM32F103RET6 ARM Cortex-M3)
129 134
 #

Loading…
Cancel
Save