From 4ed6cdc9e94d9984dc5ac3da2a3b5184fd67977b Mon Sep 17 00:00:00 2001 From: WARIO2412 Date: Thu, 21 Aug 2025 01:46:28 +0200 Subject: [PATCH 1/2] fix LEAD mode for the pendant --- carveracontroller/addons/pendant/pendant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/carveracontroller/addons/pendant/pendant.py b/carveracontroller/addons/pendant/pendant.py index 0e7e1e9d..143c14d0 100644 --- a/carveracontroller/addons/pendant/pendant.py +++ b/carveracontroller/addons/pendant/pendant.py @@ -207,7 +207,7 @@ def _handle_jogging(self, daemon: whb04.Daemon, steps: int) -> None: self._controller.startContinuousJog(f"{axis}{distance}", None, f"S{daemon.step_size_value}") else: if daemon.step_size == whb04.StepSize.LEAD: - self._controller.jog(f"{axis}{round(steps * 0.1,3)}", round(abs(steps * 0.1 / 0.05) * 60 * 0.97), 3) + self._controller.jog(f"{axis}{round(steps * 0.1,3)}", round(abs(steps * 0.1 / 0.05) * 60 * 0.97, 3)) else: self._controller.jog(f"{axis}{round(distance, 3)}") From cda588739b408d902f1a7428bdcaea7a8ed4af25 Mon Sep 17 00:00:00 2001 From: WARIO2412 Date: Thu, 21 Aug 2025 01:43:36 +0200 Subject: [PATCH 2/2] Update to new protocol --- carveracontroller/Controller.py | 347 +++++++--- carveracontroller/WIFIStream.py | 38 +- carveracontroller/XMODEM.py | 1110 +++++++++++-------------------- carveracontroller/main.py | 158 +++-- 4 files changed, 781 insertions(+), 872 deletions(-) diff --git a/carveracontroller/Controller.py b/carveracontroller/Controller.py index 68799b1e..1b82f12a 100644 --- a/carveracontroller/Controller.py +++ b/carveracontroller/Controller.py @@ -22,7 +22,8 @@ from .CNC import CNC from .USBStream import USBStream from .WIFIStream import WIFIStream -from .XMODEM import EOT, CAN +from kivy.app import App +from kivy.utils import platform as kivy_platform STREAM_POLL = 0.2 # s DIAGNOSE_POLL = 0.5 # s @@ -70,6 +71,25 @@ CONN_USB = 0 CONN_WIFI = 1 +FRAME_HEADER = 34408 +FRAME_END = 21930 +PTYPE_CTRL_SINGLE = 161 +PTYPE_CTRL_MULTI = 162 +PTYPE_FILE_START = 176 +MAX_DATA_LEN = 1024 +PTYPE_STATUS_RES = 129 +PTYPE_DIAG_RES = 130 +PTYPE_LOAD_INFO = 131 +PTYPE_LOAD_FINISH = 132 +PTYPE_LOAD_ERROR = 133 +PTYPE_NORMAL_INFO = 144 +from enum import Enum, auto + +class RevPacketState(Enum): + WAIT_HEADER = auto() + READ_LENGTH = auto() + READ_DATA = auto() + CHECK_FOOTER = auto() # ============================================================================== # Controller class @@ -90,7 +110,10 @@ class Controller: connection_type = CONN_WIFI def __init__(self, cnc, callback): - self.usb_stream = USBStream() + if kivy_platform == 'ios': + self.usb_stream = None + else: + self.usb_stream = USBStream() self.wifi_stream = WIFIStream() # Reconnection properties @@ -154,6 +177,12 @@ def __init__(self, cnc, callback): self.pausing = False self.diagnosing = False + self.currentState = RevPacketState.WAIT_HEADER + self.packetData = bytearray() + self.headerBuffer = bytearray(2) + self.footerBuffer = bytearray(2) + self.bytesNeeded = 2 + self.expectedLength = 0 self.is_community_firmware = False @@ -181,7 +210,7 @@ def saveConfig(self): # ---------------------------------------------------------------------- def executeGcode(self, line): if isinstance(line, tuple) or \ - line[0] in ("$", "!", "~", "?", "(", "@") or GPAT.match(line): + (line and len(line) > 0 and line[0] in ("$", "!", "~", "?", "(", "@")) or GPAT.match(line): self.sendGCode(line) return True return False @@ -189,23 +218,66 @@ def executeGcode(self, line): # ---------------------------------------------------------------------- # Execute a single command # ---------------------------------------------------------------------- - def executeCommand(self, line): - #if self.sio_status != False or self.sio_diagnose != False: #wait for the ? or * command - # time.sleep(0.5) + def crc16_ccitt(self, data: bytes, length: int) -> int: + crc_table = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0] + crc = 0 + if length == 0: + for byte in data: + tmp = (crc >> 8 ^ byte) & 255 + crc = (crc << 8 ^ crc_table[tmp]) & 65535 + else: + for i in range(length): + tmp = (crc >> 8 ^ data[i]) & 255 + crc = (crc << 8 ^ crc_table[tmp]) & 65535 + return crc + + def executeSingleCharCommand(self, char: int): + data_length = 4 + crc_data = data_length.to_bytes(2, 'big') + PTYPE_CTRL_SINGLE.to_bytes(1, 'big') + char.to_bytes(1, 'big') + crc = self.crc16_ccitt(crc_data, 0) + packet = FRAME_HEADER.to_bytes(2, byteorder='big') + crc_data + crc.to_bytes(2, byteorder='big') + FRAME_END.to_bytes(2, byteorder='big') + self.stream.send(packet) + + def executeMultiCharCommand(self, data: bytes) -> bytes: + assert isinstance(data, bytes), 'data must be bytes type' + DATA_LENGTH = 1 + len(data) + 2 + crc_payload = DATA_LENGTH.to_bytes(2, 'big') + bytes([PTYPE_CTRL_MULTI]) + data + crc = self.crc16_ccitt(crc_payload, 0) + packet = FRAME_HEADER.to_bytes(2, 'big') + DATA_LENGTH.to_bytes(2, 'big') + bytes([PTYPE_CTRL_MULTI]) + data + crc.to_bytes(2, 'big') + FRAME_END.to_bytes(2, 'big') + self.stream.send(packet) + + def executeFileCommand(self, data: bytes) -> bytes: + assert isinstance(data, bytes), 'data must be bytes type' + DATA_LENGTH = 1 + len(data) + 2 + crc_payload = DATA_LENGTH.to_bytes(2, 'big') + bytes([PTYPE_FILE_START]) + data + crc = self.crc16_ccitt(crc_payload, 0) + packet = FRAME_HEADER.to_bytes(2, 'big') + DATA_LENGTH.to_bytes(2, 'big') + bytes([PTYPE_FILE_START]) + data + crc.to_bytes(2, 'big') + FRAME_END.to_bytes(2, 'big') + self.stream.send(packet) + + def executeCommand(self, line, nodisplay=False): + if self.stream and line: + try: + self.executeMultiCharCommand(line.encode()) + if self.execCallback and (not nodisplay): + self.execCallback(line) + except: + self.log.put((Controller.MSG_ERROR, str(sys.exc_info()[1]))) + + def executeTransfileCommand(self, line, nodisplay=False): if self.stream and line: try: - if line[-1] != '\n': - line += "\n" - self.stream.send(line.encode()) + # Check if line has content before accessing indices + if len(line) > 0 and line[(-1)]!= '\n': + line += '\n' + self.executeFileCommand(line.encode()) if self.execCallback: - # 检查文件名是否以 ".lz" 结尾 - if line.endswith(".lz\n"): - # 删除 ".lz" 后缀 - new_line = line[:-4] + "\n" - else: - # 如果没有 ".lz" 后缀,直接赋值 + print(f"line: {line}") + if line.endswith('.lz\n') and len(line) > 4: + new_line = line[:(-4)] + '\n' + else: # inserted new_line = line - self.execCallback(new_line) + if not nodisplay: + self.execCallback(new_line) except: self.log.put((Controller.MSG_ERROR, str(sys.exc_info()[1]))) @@ -529,13 +601,13 @@ def uploadCommand(self, filename): upload_command = "upload %s\n" % filename.replace(' ', '\x01') if '\\' in filename: upload_command = "upload %s\n" % '/'.join(filename.split('\\')).replace(' ', '\x01') - self.executeCommand(self.escape(upload_command)) + self.executeTransfileCommand(self.escape(upload_command)) def downloadCommand(self, filename): download_command = "download %s\n" % filename.replace(' ', '\x01') if '\\' in filename: download_command = "download %s\n" % '/'.join(filename.split('\\')).replace(' ', '\x01') - self.executeCommand(self.escape(download_command)) + self.executeTransfileCommand(self.escape(download_command)) def suspendCommand(self): self.executeCommand("suspend\n") @@ -554,27 +626,26 @@ def abortCommand(self): def feedholdCommand(self): if self.stream: - self.stream.send('!'.encode()) + self.executeSingleCharCommand(ord('!')) def toggleFeedholdCommand(self, holding): if self.stream: if holding: - self.stream.send('~'.encode()) + self.executeSingleCharCommand(ord('~')) else: - self.stream.send('!'.encode()) + self.executeSingleCharCommand(ord('!')) def cyclestartCommand(self): if self.stream: - self.stream.send('~'.encode()) + self.executeSingleCharCommand(ord('~')) def estopCommand(self): - self.continuous_jog_active = False # Stop continuous jog when emergency stop is triggered if self.stream: - self.stream.send(b'\x18') + self.executeSingleCharCommand(24) # ---------------------------------------------------------------------- def hardResetPre(self): - self.stream.send(b"reset\n") + self.executeMultiCharCommand(b"reset\n") def hardResetAfter(self): time.sleep(6) @@ -592,15 +663,21 @@ def parseBracketAngle(self, line,): CNC.vars["state"] = l[0] # strip of rest into a dict of name: [values,...,] - d = {a: [float(y) for y in b.split(',')] for a, b in [x.split(':') for x in l[1:]]} - if 'R' in d: + d = {} + try: + d = {a: [float(y) for y in b.split(',')] for a, b in [x.split(':') for x in l[1:]]} + except (ValueError, IndexError) as e: + logger.warning(f"Failed to parse status data: {e}") + return + + if 'R' in d and len(d['R']) > 0: CNC.vars["rotation_angle"] = float(d['R'][0]) CNC.can_rotate_wcs = True else: CNC.vars["rotation_angle"] = 0.0 - if 'G' in d: + if 'G' in d and len(d['G']) > 0: CNC.vars["active_coord_system"] = int(d['G'][0]) - if 'C' in d: + if 'C' in d and len(d['C']) > 3: CNC.vars['MachineModel'] = int(d['C'][0]) CNC.vars['FuncSetting'] = int(d['C'][1]) CNC.vars['inch_mode'] = int(d['C'][2]) @@ -726,6 +803,8 @@ def parseBigParentheses(self, line): CNC.vars["sw_air"] = int(d['R'][0]) if 'C' in d: CNC.vars["sw_wp_charge_pwr"] = int(d['C'][0]) + if 'RSSI' in d: + CNC.vars["RSSI"] = int(d['RSSI'][0]) if 'E' in d: CNC.vars["st_x_min"] = int(d['E'][0]) @@ -900,23 +979,39 @@ def sendGCode(self, cmd): # ---------------------------------------------------------------------- def sendHex(self, hexcode): if self.stream is None: return - self.stream.send(chr(int(hexcode, 16))) + self.executeMultiCharCommand(chr(int(hexcode, 16)).encode()) self.stream.flush() def viewStatusReport(self, sio_status): if self.loadNUM == 0 and self.sendNUM == 0: if self.continuous_jog_active: - self.stream.send(b"?1") + self.executeMultiCharCommand(b'?1') else: - self.stream.send(b"?") + self.executeSingleCharCommand(ord('?')) self.sio_status = sio_status def viewDiagnoseReport(self, sio_diagnose): if self.loadNUM == 0 and self.sendNUM == 0: - self.stream.send(b"diagnose\n") + self.executeMultiCharCommand(b"diagnose\n") self.sio_diagnose = sio_diagnose # ---------------------------------------------------------------------- + def stopProbe(self): + # Stop any active probing operations + pass + + def busy(self): + # Set busy state - can be overridden for UI updates + pass + + def notBusy(self): + # Clear busy state - can be overridden for UI updates + pass + + def openClose(self): + # Toggle connection state - can be overridden for specific behavior + pass + def hardReset(self): self.busy() if self.stream is not None: @@ -931,7 +1026,7 @@ def hardReset(self): def softReset(self, clearAlarm=True): if self.stream: - self.stream.send(b"\030") + self.executeSingleCharCommand(24) # Ctrl+X self.stopProbe() if clearAlarm: self._alarm = False CNC.vars["_OvChanged"] = True # force a feed change if any @@ -952,11 +1047,53 @@ def viewParameters(self): def viewWCS(self): self.sendGCode("get wcs") + def parseWCSParameters(self, line): + """Parse WCS parameters from machine response""" + # Parse format: [G54:-123.6800,-123.6800,-123.6800,-50,0.000,25.123] + # Extract all WCS entries from the line + + # parse the current WCS from the "get wcs" command + get_wcs_pattern = r'\[current WCS: (G5[4-9][.1-3]*)\]' + current_wcs_matches = re.findall(get_wcs_pattern, line) + + if current_wcs_matches: + # if not on community firmware or rotation angle is not set, + # the active coordinate system is tracked through the "get wcs" command + if not self.is_community_firmware or not CNC.can_rotate_wcs: + CNC.vars["active_coord_system"] = CNC.wcs_names.index(current_wcs_matches[0]) + return + + wcs_pattern = r'\[(G5[4-9][.1-3]*):([^]]+)\]' + matches = re.findall(wcs_pattern, line) + wcs_data = {} + for wcs_code, values_str in matches: + # Split the values by comma + values = values_str.split(',') + if len(values) >= 5: # X, Y, Z, A, B, Rotation + try: + x = float(values[0]) + y = float(values[1]) + z = float(values[2]) + a = float(values[3]) + b = float(values[4]) # B is always 0 + if self.is_community_firmware and CNC.can_rotate_wcs: + rotation = float(values[5]) + else: + rotation = 0.0 + wcs_data[wcs_code] = [x, y, z, a, b, rotation] # Store only X, Y, Z, A, B, Rotation + + except (ValueError, IndexError): + logger.error(f"Error parsing WCS values for {wcs_code}: {values_str}") + + # Send the parsed data to the WCS Settings popup if it's open + if hasattr(self, 'wcs_popup_callback') and self.wcs_popup_callback: + self.wcs_popup_callback(wcs_data) + def viewState(self): self.sendGCode("$G") def viewBuild(self): - self.stream.send(b"version\n") + self.executeMultiCharCommand(b"version\n") self.sendGCode("$I") def viewStartup(self): @@ -966,7 +1103,7 @@ def checkGcode(self): pass def grblHelp(self): - self.stream.send(b"help\n") + self.executeMultiCharCommand(b"help\n") def grblRestoreSettings(self): pass @@ -1011,10 +1148,9 @@ def stopContinuousJog(self): if self.jog_mode != Controller.JOG_MODE_CONTINUOUS: return - self.continuous_jog_active = False # Send Y^ (Ctrl+Y) to stop continuous jogging - if self.stream is not None: - self.stream.send(b"\031") + if self.stream is not None and self.continuous_jog_active: + self.executeSingleCharCommand(25) def jog(self, _dir, speed=None): if self.jog_mode == Controller.JOG_MODE_STEP: @@ -1104,14 +1240,18 @@ def setRotation(self, rotation): def feedHold(self, event=None): if event is not None and not self.acceptKey(True): return if self.stream is None: return - self.stream.send(b"!") + self.executeSingleCharCommand(ord('!')) self.stream.flush() self._pause = True + def acceptKey(self, key): + # Simple key acceptance method - can be overridden for more complex logic + return True + def resume(self, event=None): - if event is not None and not self.acceptKey(True): return + if event is not None and not self.acceptKey(1): return if self.stream is None: return - self.stream.send(b"~") + self.executeSingleCharCommand(ord('~')) self.stream.flush() self._alarm = False self._pause = False @@ -1127,22 +1267,25 @@ def pause(self, event=None): def parseLine(self, line): if not line: return True + # Check if line has at least one character before accessing line[0] + if len(line) == 0: + return True elif line[0] == "<": self.parseBracketAngle(line) self.sio_status = False elif line[0] == "{": - if not self.sio_diagnose: - self.log.put((self.MSG_NORMAL, line)) - else: - self.parseBigParentheses(line) - self.sio_diagnose = False - elif line[0] == "[" in line: + self.parseBigParentheses(line) + self.sio_diagnose = False + elif line[0] == "[": # Log raw WCS parameters before parsing self.log.put((self.MSG_NORMAL, line)) # Parse WCS parameters: [G54:-123.6800,-123.6800,-123.6800,-50,0.000,25.123] self.parseWCSParameters(line) elif line[0] == "#": self.log.put((self.MSG_INTERIOR, line)) + elif line[0] == "^": + if line[1] == "Y": + self.continuous_jog_active = False elif "error" in line.lower() or "alarm" in line.lower(): self.log.put((self.MSG_ERROR, line)) else: @@ -1173,6 +1316,20 @@ def resumeStream(self): self.paused = False self.pausing = False + def process_packet(self): + if len(self.packetData) < 2: + self.packetData.clear() + return + calcCRC = self.crc16_ccitt(self.packetData, len(self.packetData) - 2) + receivedCRC = self.packetData[-2] << 8 | self.packetData[-1] + if calcCRC == receivedCRC: + if len(self.packetData) >= 3: + return self.packetData[2] + self.packetData.clear() + return + self.packetData.clear() + return None + # ---------------------------------------------------------------------- # thread performing I/O on serial line # ---------------------------------------------------------------------- @@ -1183,7 +1340,6 @@ def streamIO(self): tr = td = time.time() line = b'' last_error = '' - while not self.stop.is_set(): if not self.stream or self.paused: time.sleep(1) @@ -1192,7 +1348,8 @@ def streamIO(self): # refresh machine position? running = self.sendNUM > 0 or self.loadNUM > 0 or self.pausing try: - if not running: + app = App.get_running_app() + if not running and app.root.echosended: if t - tr > STREAM_POLL: self.viewStatusReport(True) tr = t @@ -1202,39 +1359,72 @@ def streamIO(self): else: tr = t td = t - if self.stream.waiting_for_recv(): received = [bytes([b]) for b in self.stream.recv()] - for c in received: - if c == EOT or c == CAN: - # Ctrl + Z means transmission complete, Ctrl + D means transmission cancel or error - if len(line) > 0: - self.load_buffer.put(line.decode(errors='ignore')) - if self.loadNUM > 0: - self.load_buffer_size += len(line) - line = b'' - if c == EOT: - self.loadEOF = True - else: - self.loadERR = True - else: - if c == b'\n': - # (line.decode(errors='ignore')) - if self.loadNUM == 0 or '|MPos' in line.decode(errors='ignore'): - self.parseLine(line.decode(errors='ignore')) - else: - # 将字节串解码为字符串 + for byte in received: + byte = ord(byte) + if self.currentState == RevPacketState.WAIT_HEADER: + self.headerBuffer[0] = self.headerBuffer[1] + self.headerBuffer[1] = byte + checksum = self.headerBuffer[0] << 8 | self.headerBuffer[1] + if checksum == FRAME_HEADER: + self.currentState = RevPacketState.READ_LENGTH + self.bytesNeeded = 2 + self.packetData.clear() + elif self.currentState == RevPacketState.READ_LENGTH: + self.packetData.append(byte) + self.bytesNeeded -= 1 + if self.bytesNeeded == 0: + # Check if we have enough bytes to read length + if len(self.packetData) < 2: + logger.warning("Not enough bytes to read packet length") + self.currentState = RevPacketState.WAIT_HEADER + continue + self.expectedLength = self.packetData[0] << 8 | self.packetData[1] + self.currentState = RevPacketState.READ_DATA + self.bytesNeeded = self.expectedLength + elif self.currentState == RevPacketState.READ_DATA: + self.packetData.append(byte) + self.bytesNeeded -= 1 + if self.bytesNeeded == 0: + self.currentState = RevPacketState.CHECK_FOOTER + self.bytesNeeded = 2 + elif self.currentState == RevPacketState.CHECK_FOOTER: + self.footerBuffer[0] = self.footerBuffer[1] + self.footerBuffer[1] = byte + self.bytesNeeded -= 1 + if self.bytesNeeded == 0: + checksum = self.footerBuffer[0] << 8 | self.footerBuffer[1] + self.currentState = RevPacketState.WAIT_HEADER + if checksum == FRAME_END: + cmd = self.process_packet() + # Check if packetData has enough elements before slicing + if len(self.packetData) < 6: + logger.warning(f"Packet too short: {len(self.packetData)} bytes") + continue + + if cmd == PTYPE_STATUS_RES or cmd == PTYPE_DIAG_RES or cmd == PTYPE_NORMAL_INFO: + line = self.packetData[3:-3] + self.parseLine(line.decode(errors='ignore')) + continue + if cmd == PTYPE_LOAD_FINISH: + self.loadEOF = True + continue + if cmd == PTYPE_LOAD_ERROR: + self.loadERR = True + continue + line = self.packetData[3:-3] + if self.loadNUM == 0: + self.parseLine(line.decode(errors='ignore')) + continue decoded_line = line.decode(errors='ignore') - # 使用正则表达式去除以"<"开头,以">"结尾的部分 cleaned_line = re.sub(r'<.*?>', '', decoded_line) - # 去除多余的空格(如果需要) cleaned_line = cleaned_line.strip() if len(cleaned_line) != 0: - self.load_buffer.put(cleaned_line) - self.load_buffer_size += len(cleaned_line) + 1 - line = b'' - else: - line += c + split_lines = cleaned_line.replace('\r\n', '\n').split('\n') + for line2 in split_lines: + self.load_buffer.put(line2) + self.load_buffer_size += len(line2) + 1 dynamic_delay = 0 else: if self.sendNUM == 0 and self.loadNUM == 0: @@ -1243,8 +1433,9 @@ def streamIO(self): dynamic_delay = 0 except: + print(f"error: {sys.exc_info()[1]}") line = b'' - if last_error != str(sys.exc_info()[1]) : + if last_error != str(sys.exc_info()[1]): self.log.put((Controller.MSG_ERROR, str(sys.exc_info()[1]))) last_error = str(sys.exc_info()[1]) diff --git a/carveracontroller/WIFIStream.py b/carveracontroller/WIFIStream.py index c6bd0e29..15de5bc4 100644 --- a/carveracontroller/WIFIStream.py +++ b/carveracontroller/WIFIStream.py @@ -3,7 +3,8 @@ import time import socket import select - +import os +from . import Utils from .XMODEM import XMODEM import logging @@ -11,19 +12,21 @@ TCP_PORT = 2222 UDP_PORT = 3333 BUFFER_SIZE = 1024 -SOCKET_TIMEOUT = 0.3 # s +SOCKET_TIMEOUT = 5 # ============================================================================== # Machine Detector class # ============================================================================== class MachineDetector: + def __init__(self): self.machine_list = [] self.machine_name_list = [] self.sock = None self.t = None self.tr = None - + return + def is_machine_busy(self, addr): """Tries to connect to the machine, if machine is available returns true else false""" try: @@ -67,8 +70,6 @@ def check_for_responses(self): except: print(sys.exc_info()[1]) - - # ============================================================================== # WiFi stream class # ============================================================================== @@ -79,9 +80,7 @@ class WIFIStream: # ---------------------------------------------------------------------- def __init__(self): - - self.modem = XMODEM(self.getc, self.putc, 'xmodem8k') - + self.modem = XMODEM(self.getc, self.putc, 'wifiMode') handler = logging.StreamHandler(sys.stdout) handler.setLevel(logging.WARNING) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') @@ -98,17 +97,20 @@ def recv(self): # ---------------------------------------------------------------------- def open(self, address): - self.socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) - ip_port = address.split(':') - self.socket.settimeout(2) - self.socket.connect((address.split(':')[0], (int)(address.split(':')[1]) if len(ip_port) > 1 else TCP_PORT)) - self.socket.settimeout(SOCKET_TIMEOUT) - - return True + try: + self.socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) + ip_port = address.split(':') + self.socket.settimeout(3) + self.socket.connect((address.split(':')[0], int(address.split(':')[1]) if len(ip_port) > 1 else TCP_PORT)) + self.socket.settimeout(SOCKET_TIMEOUT) + return True + except socket.error as e: + return False # ---------------------------------------------------------------------- def close(self): - if self.socket is None: return + if self.socket is None: + return try: self.modem.clear_mode_set() self.socket.close() @@ -164,13 +166,13 @@ def putc(self, data, timeout = 0.5): def upload(self, filename, local_md5, callback): # do upload stream = open(filename, 'rb') - result = self.modem.send(stream, md5 = local_md5, retry = 10, callback = callback) + result = self.modem.send(stream, md5=local_md5, retry=50, callback=callback) stream.close() return result def download(self, filename, local_md5, callback): stream = open(filename, 'wb') - result = self.modem.recv(stream, md5 = local_md5, retry = 10, callback = callback) + result = self.modem.recv(stream, md5=local_md5, retry=50, callback=callback) stream.close() return result diff --git a/carveracontroller/XMODEM.py b/carveracontroller/XMODEM.py index ae7e6b7e..3f59d02b 100644 --- a/carveracontroller/XMODEM.py +++ b/carveracontroller/XMODEM.py @@ -1,718 +1,412 @@ -""" -=============================== - XMODEM file transfer protocol -=============================== - -XMODEM 128 byte blocks ----------------------- - -:: - - SENDER RECEIVER - - <-- NAK - SOH 01 FE 128 Data[128] CSUM --> - <-- ACK - SOH 02 FD 128 Data[128] CSUM --> - <-- ACK - SOH 03 FC 128 Data[128] CSUM --> - <-- ACK - SOH 04 FB 128 Data[128] CSUM --> - <-- ACK - SOH 05 FA 128 Data[100] CPMEOF[28] CSUM --> - <-- ACK - EOT --> - <-- ACK - -XMODEM-1k blocks, CRC mode --------------------------- - -:: - - SENDER RECEIVER - - <-- C - STX 01 FE Data[1024] CRC CRC --> - <-- ACK - STX 02 FD Data[1024] CRC CRC --> - <-- ACK - STX 03 FC Data[1000] CPMEOF[24] CRC CRC --> - <-- ACK - EOT --> - <-- ACK - -Mixed 1024 and 128 byte Blocks ------------------------------- - -:: - - SENDER RECEIVER - - <-- C - STX 01 FE Data[1024] CRC CRC --> - <-- ACK - STX 02 FD Data[1024] CRC CRC --> - <-- ACK - SOH 03 FC Data[128] CRC CRC --> - <-- ACK - SOH 04 FB Data[100] CPMEOF[28] CRC CRC --> - <-- ACK - EOT --> - <-- ACK - -YMODEM Batch Transmission Session (1 file) ------------------------------------------- - -:: - - SENDER RECEIVER - <-- C (command:rb) - SOH 00 FF foo.c NUL[123] CRC CRC --> - <-- ACK - <-- C - SOH 01 FE Data[128] CRC CRC --> - <-- ACK - SOH 02 FC Data[128] CRC CRC --> - <-- ACK - SOH 03 FB Data[100] CPMEOF[28] CRC CRC --> - <-- ACK - EOT --> - <-- NAK - EOT --> - <-- ACK - <-- C - SOH 00 FF NUL[128] CRC CRC --> - <-- ACK - - -""" from __future__ import division, print_function - -__author__ = 'Wijnand Modderman ' -__copyright__ = ['Copyright (c) 2010 Wijnand Modderman', - 'Copyright (c) 1981 Chuck Forsberg'] -__license__ = 'MIT' -__version__ = '0.4.5' - -import platform -import logging -import time -import sys +import platform, logging, time, sys, math, struct from functools import partial - -# Protocol bytes -SOH = b'\x01' -STX = b'\x02' -EOT = b'\x04' -ACK = b'\x06' -DLE = b'\x10' -NAK = b'\x15' -CAN = b'\x16' -CRC = b'C' +from enum import Enum, auto +FRAME_HEADER = 34408 +FRAME_END = 21930 +PTYPE_CTRL_SINGLE = 161 +PTYPE_CTRL_MULTI = 162 +PTYPE_FILE_MD5 = 177 +PTYPE_FILE_VIEW = 178 +PTYPE_FILE_DATA = 179 +PTYPE_FILE_END = 180 +PTYPE_FILE_CAN = 181 +PTYPE_FILE_RETRY = 182 + +class RevPacketState(Enum): + WAIT_HEADER = auto() + READ_LENGTH = auto() + READ_DATA = auto() + CHECK_FOOTER = auto() + + +class FileTransState(Enum): + WAIT_MD5 = auto() + WAIT_FILE_VIEW = auto() + READ_FILE_DATA = auto() class XMODEM(object): - ''' - XMODEM Protocol handler, expects two callables which encapsulate the read - and write operations on the underlying stream. - - Example functions for reading and writing to a serial line: - - >>> import serial - >>> from xmodem import XMODEM - >>> ser = serial.Serial('/dev/ttyUSB0', timeout=0) # or whatever you need - >>> def getc(size, timeout=0.5): - ... return ser.read(size) or None - ... - >>> def putc(data, timeout=0.5): - ... return ser.write(data) or None - ... - >>> modem = XMODEM(getc, putc) - - - :param getc: Function to retrieve bytes from a stream. The function takes - the number of bytes to read from the stream and a timeout in seconds as - parameters. It must return the bytes which were read, or ``None`` if a - timeout occured. - :type getc: callable - :param putc: Function to transmit bytes to a stream. The function takes the - bytes to be written and a timeout in seconds as parameters. It must - return the number of bytes written to the stream, or ``None`` in case of - a timeout. - :type putc: callable - :param mode: XMODEM protocol mode - :type mode: string - :param pad: Padding character to make the packets match the packet size - :type pad: char - - ''' - - # crctab calculated by Mark G. Mendel, Network Systems Corporation crctable = [ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, - ] - - def __init__(self, getc, putc, mode='xmodem8k', pad=b'\x1a'): + 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, + 33032, + 37161, 41290, 45419, 49548, 53677, 57806, 61935, + 4657, + 528, 12915, 8786, 21173, 17044, 29431, 25302, + 37689, + 33560, 45947, 41818, 54205, 50076, 62463, 58334, + 9314, + 13379, 1056, 5121, 25830, 29895, 17572, 21637, + 42346, + 46411, 34088, 38153, 58862, 62927, 50604, 54669, + 13907, + 9842, 5649, 1584, 30423, 26358, 22165, 18100, + 46939, + 42874, 38681, 34616, 63455, 59390, 55197, 51132, + 18628, + 22757, 26758, 30887, 2112, 6241, 10242, 14371, + 51660, + 55789, 59790, 63919, 35144, 39273, 43274, 47403, + 23285, + 19156, 31415, 27286, 6769, 2640, 14899, 10770, + 56317, + 52188, 64447, 60318, 39801, 35672, 47931, 43802, + 27814, + 31879, 19684, 23749, 11298, 15363, 3168, 7233, + 60846, + 64911, 52716, 56781, 44330, 48395, 36200, 40265, + 32407, + 28342, 24277, 20212, 15891, 11826, 7761, 3696, + 65439, + 61374, 57309, 53244, 48923, 44858, 40793, 36728, + 37256, + 33193, 45514, 41451, 53516, 49453, 61774, 57711, + 4224, + 161, 12482, 8419, 20484, 16421, 28742, 24679, + 33721, + 37784, 41979, 46042, 49981, 54044, 58239, 62302, + 689, + 4752, 8947, 13010, 16949, 21012, 25207, 29270, + 46570, + 42443, 38312, 34185, 62830, 58703, 54572, 50445, + 13538, + 9411, 5280, 1153, 29798, 25671, 21540, 17413, + 42971, + 47098, 34713, 38840, 59231, 63358, 50973, 55100, + 9939, + 14066, 1681, 5808, 26199, 30326, 17941, 22068, + 55628, + 51565, 63758, 59695, 39368, 35305, 47498, 43435, + 22596, + 18533, 30726, 26663, 6336, 2273, 14466, 10403, + 52093, + 56156, 60223, 64286, 35833, 39896, 43963, 48026, + 19061, + 23124, 27191, 31254, 2801, 6864, 10931, 14994, + 64814, + 60687, 56684, 52557, 48554, 44427, 40424, 36297, + 31782, + 27655, 23652, 19525, 15522, 11395, 7392, 3265, + 61215, + 65342, 53085, 57212, 44955, 49082, 36825, 40952, + 28183, + 32310, 20053, 24180, 11923, 16050, 3793, 7920] + + def __init__(self, getc, putc, mode='wifiMode', pad=b'\x1a'): self.getc = getc self.putc = putc self.mode = mode self.mode_set = False self.pad = pad - self.log = logging.getLogger('xmodem.XMODEM') + self.log = logging.getLogger("xmodem.XMODEM") self.canceled = False + self.currentState = RevPacketState.WAIT_HEADER + self.packetData = bytearray() + self.headerBuffer = bytearray(2) + self.footerBuffer = bytearray(2) + self.bytesNeeded = 2 + self.expectedLength = 0 + self.FileRcvState = FileTransState.WAIT_MD5 def clear_mode_set(self): self.mode_set = False def abort(self, count=2, timeout=60): - ''' - Send an abort sequence using CAN bytes. + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') - :param count: how many abort characters to send - :type count: int - :param timeout: timeout in seconds - :type timeout: int - ''' - for _ in range(count): - self.putc(CAN, timeout) + def crc16_ccitt(self, data: bytes, length: int) -> int: + crc = 0 + for i in range(length): + tmp = (crc >> 8 ^ data[i]) & 255 + crc = (crc << 8 ^ self.crctable[tmp]) & 65535 - def send(self, stream, md5, retry=16, timeout=5, quiet=False, callback=None): - ''' - Send a stream via the XMODEM protocol. - - >>> stream = open('/etc/issue', 'rb') - >>> print(modem.send(stream)) - True - - Returns ``True`` upon successful transmission or ``False`` in case of - failure or None incase of canceled. - - :param stream: The stream object to send data from. - :type stream: stream (file, etc.) - :param retry: The maximum number of times to try to resend a failed - packet before failing. - :type retry: int - :param timeout: The number of seconds to wait for a response before - timing out. - :type timeout: int - :param quiet: If True, write transfer information to stderr. - :type quiet: bool - :param callback: Reference to a callback function that has the - following signature. This is useful for - getting status updates while a xmodem - transfer is underway. - Expected callback signature: - def callback(total_packets, success_count, error_count) - :type callback: callable - ''' - - # initialize protocol - try: - packet_size = dict( - xmodem=128, - xmodem8k=8192, - )[self.mode] - except KeyError: - raise ValueError("Invalid mode specified: {self.mode!r}" - .format(self=self)) + return crc & 65535 - is_stx = 1 if packet_size > 255 else 0 - - self.log.debug('Begin start sequence, packet_size=%d', packet_size) - error_count = 0 - crc_mode = 0 - cancel = 0 + def recvPacket(self, timeout=0.5): + self.currentState = RevPacketState.WAIT_HEADER + tr = time.time() while True: - char = self.getc(1) - if char: - if char == NAK: - self.log.debug('standard checksum requested (NAK).') - crc_mode = 0 - break - elif char == CRC: - self.log.debug('16-bit CRC requested (CRC).') - crc_mode = 1 - break - elif char == CAN: - if not quiet: - print('received CAN', file=sys.stderr) - if cancel: - self.log.info('Transmission canceled: received 2xCAN ' - 'at start-sequence') + byte = self.getc(1, timeout) + if byte: + byte = ord(byte) + if self.currentState == RevPacketState.WAIT_HEADER: + self.headerBuffer[0] = self.headerBuffer[1] + self.headerBuffer[1] = byte + checksum = self.headerBuffer[0] << 8 | self.headerBuffer[1] + if checksum == FRAME_HEADER: + self.currentState = RevPacketState.READ_LENGTH + self.bytesNeeded = 2 + self.packetData.clear() + elif self.currentState == RevPacketState.READ_LENGTH: + self.packetData.append(byte) + self.bytesNeeded -= 1 + if self.bytesNeeded == 0: + self.expectedLength = self.packetData[0] << 8 | self.packetData[1] + self.currentState = RevPacketState.READ_DATA + self.bytesNeeded = self.expectedLength + elif self.currentState == RevPacketState.READ_DATA: + self.packetData.append(byte) + self.bytesNeeded -= 1 + while self.bytesNeeded > 0: + bytess = self.getc(self.bytesNeeded, timeout) + if bytess: + self.packetData.extend(bytess) + self.bytesNeeded = 0 + else: + return + if self.bytesNeeded == 0: + self.currentState = RevPacketState.CHECK_FOOTER + self.bytesNeeded = 2 + elif self.currentState == RevPacketState.CHECK_FOOTER: + self.footerBuffer[0] = self.footerBuffer[1] + self.footerBuffer[1] = byte + self.bytesNeeded -= 1 + if self.bytesNeeded == 0: + checksum = self.footerBuffer[0] << 8 | self.footerBuffer[1] + self.currentState = RevPacketState.WAIT_HEADER + if checksum == FRAME_END: + if self.process_packet(): + return 1 + return None return None - else: - self.log.debug('cancellation at start sequence.') - cancel = 1 - elif char == EOT: - self.log.info('Transmission canceled: received EOT ' - 'at start-sequence') - return False - else: - self.log.error('send error: expected NAK, CRC, EOT or CAN; ' - 'got %r', char) - - error_count += 1 - if error_count > retry: - self.log.info('send error: error_count reached %d, ' - 'aborting.', retry) - self.abort(timeout=timeout) - return False - - # send data - error_count = 0 - success_count = 0 - total_packets = 0 - sequence = 0 # 0 for md5 upload - md5_sent = False - - while True: - if self.canceled: - self.putc(CAN) - self.putc(CAN) - self.putc(CAN) - while self.getc(1, timeout): - pass - self.log.info('Transmission canceled by user.') - self.canceled = False - return None - - data = [] - if not md5_sent and sequence == 0: - data = md5.encode() - md5_sent = True - else: - data = stream.read(packet_size) - total_packets += 1 - if not data: - # end of stream - self.log.debug('send: at EOF') - break - - header = self._make_send_header(packet_size, sequence) - if is_stx == 0: - data = b''.join([bytes([len(data) & 0xff]), data.ljust(packet_size, self.pad)]) else: - data = b''.join([bytes([len(data) >> 8, len(data) & 0xff]), data.ljust(packet_size, self.pad)]) - checksum = self._make_send_checksum(crc_mode, data) - - # emit packet - while True: - self.log.debug('send: block %d', sequence) - self.putc(header + data + checksum) - char = self.getc(1, timeout) - if char == ACK: - success_count += 1 - if callable(callback): - callback(packet_size, total_packets, success_count, error_count) - error_count = 0 - break - elif char == CAN: - if cancel: - self.log.info('Transmission canceled: received 2xCAN.') - return False - else: - self.log.debug('Cancellation at Transmission.') - cancel = 1 - - self.log.info('send error: expected ACK; got %r for block %d', - char, sequence) - error_count += 1 - if callable(callback): - callback(packet_size, total_packets, success_count, error_count) - if error_count > retry: - # excessive amounts of retransmissions requested, - # abort transfer - self.log.error('send error: NAK received %d times, ' - 'aborting.', error_count) - self.abort(timeout=timeout) - return False - - # keep track of sequence - sequence = (sequence + 1) % 0x100 - - while True: - self.log.debug('sending EOT, awaiting ACK') - # end of transmission - self.putc(EOT) - - # An ACK should be returned - char = self.getc(1, timeout) - if char == ACK: - break - else: - self.log.error('send error: expected ACK; got %r', char) - error_count += 1 - if error_count > retry: - self.log.warn('EOT was not ACKd, aborting transfer') - self.abort(timeout=timeout) - return False - - self.log.info('Transmission successful (ACK received).') - return True - - def _make_send_header(self, packet_size, sequence): - assert packet_size in (128, 8192), packet_size - _bytes = [] - if packet_size == 128: - _bytes.append(ord(SOH)) - elif packet_size == 8192: - _bytes.append(ord(STX)) - _bytes.extend([sequence, 0xff - sequence]) - return bytearray(_bytes) - - def _make_send_checksum(self, crc_mode, data): - _bytes = [] - if crc_mode: - crc = self.calc_crc(data) - _bytes.extend([crc >> 8, crc & 0xff]) - else: - crc = self.calc_checksum(data) - _bytes.append(crc) - return bytearray(_bytes) - - def recv(self, stream, md5 = '', crc_mode=1, retry=16, timeout=1, delay=0.1, quiet=0, callback=None): - ''' - Receive a stream via the XMODEM protocol. - - >>> stream = open('/etc/issue', 'wb') - >>> print(modem.recv(stream)) - 2342 - - Returns the number of bytes received on success or ``None`` in case of - failure or -1 in case of canceled or 0 in case of md5 equal. - - :param stream: The stream object to write data to. - :type stream: stream (file, etc.) - :param crc_mode: XMODEM CRC mode - :type crc_mode: int - :param retry: The maximum number of times to try to resend a failed - packet before failing. - :type retry: int - :param timeout: The number of seconds to wait for a response before - timing out. - :type timeout: int - :param delay: The number of seconds to wait between resend attempts - :type delay: int - :param quiet: If ``True``, write transfer information to stderr. - :type quiet: bool - :param callback: Reference to a callback function that has the - following signature. This is useful for - getting status updates while a xmodem - transfer is underway. - Expected callback signature: - def callback(success_count, error_count) - :type callback: callable - - ''' - - # initiate protocol + return + + def process_packet(self): + if len(self.packetData) < 2: + self.packetData.clear() + return + calcCRC = self.crc16_ccitt(self.packetData, len(self.packetData) - 2) + receivedCRC = self.packetData[-2] << 8 | self.packetData[-1] + if calcCRC == receivedCRC: + if len(self.packetData) >= 3: + return 1 + self.packetData.clear() + return + self.packetData.clear() + return + + def SendFileTransCommand(self, Cmdstr, data: bytes) -> bytes: + if not isinstance(data, bytes): + raise TypeError("data 必须是 bytes 类型") + DATA_LENGTH = 1 + len(data) + 2 + crc_payload = DATA_LENGTH.to_bytes(2, "big") + bytes([Cmdstr]) + data + crc = self.crc16_ccitt(crc_payload, DATA_LENGTH) + packet = FRAME_HEADER.to_bytes(2, "big") + DATA_LENGTH.to_bytes(2, "big") + bytes([Cmdstr]) + data + crc.to_bytes(2, "big") + FRAME_END.to_bytes(2, "big") + self.putc(packet) + + def recv(self, stream, md5='', crc_mode=1, retry=5, timeout=5, delay=0.1, quiet=0, callback=None): success_count = 0 error_count = 0 - char = 0 - cancel = 0 - while True: - # first try CRC mode, if this fails, - # fall back to checksum mode - if error_count >= retry: - self.log.info('error_count reached %d, aborting.', retry) - self.abort(timeout=timeout) - return None - elif crc_mode and error_count < (retry // 2): - if not self.putc(CRC): - self.log.debug('recv error: putc failed, ' - 'sleeping for %d', delay) - time.sleep(0.1) #time.sleep(delay) - error_count += 1 - else: - crc_mode = 0 - if not self.putc(NAK): - self.log.debug('recv error: putc failed, ' - 'sleeping for %d', delay) - time.sleep(0.1) #time.sleep(delay) - error_count += 1 - - char = self.getc(1, timeout) - if char is None: - self.log.warn('recv error: getc timeout in start sequence') - error_count += 1 - continue - elif char == SOH: - if not self.mode_set: - self.mode = 'xmodem' - self.mode_set = True - self.log.debug('recv: SOH') - break - elif char == STX: - if not self.mode_set: - self.mode = 'xmodem8k' - self.mode_set = True - self.log.debug('recv: STX') - break - elif char == CAN: - if cancel: - self.log.info('Transmission canceled: received 2xCAN ' - 'at start-sequence') - return None + totalerr_count = 0 + total_packet = 0 + packet_size = 0 + sequence = 0 + income_size = 0 + while 1: + if self.canceled: + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') + self.log.info("Transmission canceled by user.") + self.canceled = False + return -1 + result = self.recvPacket(timeout) + if result: + cmdType = self.packetData[2] + if cmdType < PTYPE_FILE_MD5: + continue else: - self.log.debug('cancellation at start sequence.') - cancel = 1 + if cmdType == PTYPE_FILE_CAN: + self.log.info("Transmission canceled by Machine.") + self.FileRcvState = FileTransState.WAIT_MD5 + return + if self.FileRcvState == FileTransState.READ_FILE_DATA and self.packetData: + seq = self.packetData[3] << 24 | self.packetData[4] << 16 | self.packetData[5] << 8 | self.packetData[6] + if cmdType == PTYPE_FILE_DATA and sequence == seq: + data_len = (self.packetData[0] << 8 | self.packetData[1]) - 7 + income_size += data_len + stream.write(self.packetData[7:data_len + 7]) + if sequence < total_packet: + sequence += 1 + data = sequence.to_bytes(4, byteorder="big", signed=False) + self.SendFileTransCommand(PTYPE_FILE_DATA, data) + success_count = success_count + 1 + if callable(callback): + callback(seq, total_packet) + error_count = 0 + totalerr_count = 0 + if seq == total_packet: + self.SendFileTransCommand(PTYPE_FILE_END, b'') + self.FileRcvState = FileTransState.WAIT_MD5 + self.log.info("Transmission complete, %d bytes", income_size) + self.FileRcvState = FileTransState.WAIT_MD5 + return income_size + error_count += 1 + if error_count >= retry: + data = sequence.to_bytes(4, byteorder="big", signed=False) + self.SendFileTransCommand(PTYPE_FILE_DATA, data) + totalerr_count += 1 + if self.FileRcvState == FileTransState.WAIT_FILE_VIEW and self.packetData: + if cmdType == PTYPE_FILE_VIEW: + total_packet = self.packetData[3] << 24 | self.packetData[4] << 16 | self.packetData[5] << 8 | self.packetData[6] + packet_size = self.packetData[7] << 8 | self.packetData[8] + sequence = 1 + data = sequence.to_bytes(4, byteorder="big", signed=False) + self.SendFileTransCommand(PTYPE_FILE_DATA, data) + self.FileRcvState = FileTransState.READ_FILE_DATA + error_count = 0 + totalerr_count = 0 + else: + error_count += 1 + if error_count >= retry: + self.SendFileTransCommand(PTYPE_FILE_VIEW, b'') + totalerr_count += 1 + if self.FileRcvState == FileTransState.WAIT_MD5 and self.packetData: + if cmdType == PTYPE_FILE_MD5: + md5new = self.packetData[3:len(self.packetData) - 2] + if md5.encode() == md5new and (not md5 == ""): + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') + return 0 + self.SendFileTransCommand(PTYPE_FILE_VIEW, b'') + self.FileRcvState = FileTransState.WAIT_FILE_VIEW + error_count = 0 + totalerr_count = 0 + error_count += 1 + if error_count >= retry: + self.SendFileTransCommand(PTYPE_FILE_MD5, b'') + totalerr_count += 1 + if self.canceled: + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') + self.log.info("Transmission canceled by user.") + self.canceled = False + self.FileRcvState = FileTransState.WAIT_MD5 + return + self.packetData.clear() else: error_count += 1 + if error_count >= 1: + totalerr_count += 1 + self.SendFileTransCommand(PTYPE_FILE_RETRY, b'') + self.packetData.clear() + if totalerr_count >= retry: + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') + self.log.info("retry_count reached %d, aborting.", retry) + self.abort(timeout=timeout) + self.FileRcvState = FileTransState.WAIT_MD5 + return - # read data - error_count = 0 - income_size = 0 - # initialize protocol - - packet_size = 8192 + def send(self, stream, md5, retry=16, timeout=5, quiet=False, callback=None): + td = float() + lastseq = 0 + packetno = 0 try: - packet_size = dict( - xmodem=128, - xmodem8k=8192, - )[self.mode] + packet_size = dict(USBMode=128, + wifiMode=8192)[self.mode] except KeyError: - raise ValueError("Invalid mode specified: {self.mode!r}" - .format(self=self)) - is_stx = 1 if packet_size > 255 else 0 - - sequence = 0 - cancel = 0 - retrans = retry + 1 - md5_received = False + raise ValueError("Invalid mode specified: {self.mode!r}".format(self=self)) - while True: + data = md5.encode() + self.SendFileTransCommand(PTYPE_FILE_MD5, data) + lastcmd = PTYPE_FILE_MD5 + td = time.time() + while 1: if self.canceled: - self.putc(CAN) - self.putc(CAN) - self.putc(CAN) - while self.getc(1, timeout): - pass - self.log.info('Transmission canceled by user.') + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') + self.log.info("Transmission canceled by user.") self.canceled = False - return -1 - while True: - if char == SOH or char == STX: - break - elif char == EOT: - # We received an EOT, so send an ACK and return t - # he - # received data length. - self.putc(ACK) - self.log.info("Transmission complete, %d bytes", - income_size) - return income_size - elif char == CAN: - # cancel at two consecutive cancels - if cancel: - self.log.info('Transmission canceled: received 2xCAN ' - 'at block %d', sequence) - return None - else: - self.log.debug('cancellation at block %d', sequence) - cancel = 1 - elif char == None: - # no data avaliable - error_count += 1 - if error_count > retry: - self.log.error('error_count reached %d, aborting.', - retry) - self.abort() - return None - # get next start-of-header bytexs - char = self.getc(1, 0.5) #char = self.getc(1, timeout) + return + result = self.recvPacket(timeout * 8) + if result: + td = time.time() + cmdType = self.packetData[2] + if cmdType < PTYPE_FILE_MD5: continue - else: - err_msg = ('recv error: expected SOH, EOT; ' - 'got {0!r}'.format(char)) - if not quiet: - print(err_msg, file = sys.stderr) - self.log.warn(err_msg) - error_count += 1 - if error_count > retry: - self.abort() - return None + if cmdType == PTYPE_FILE_CAN: + self.log.info("Transmission canceled by Machine.") + self.FileRcvState = FileTransState.WAIT_MD5 + return + if cmdType == PTYPE_FILE_RETRY: + self.SendFileTransCommand(lastcmd, data) + if cmdType == PTYPE_FILE_MD5: + data = md5.encode() + self.SendFileTransCommand(PTYPE_FILE_MD5, data) + if cmdType == PTYPE_FILE_VIEW: + stream.seek(0, 2) + file_size = stream.tell() + stream.seek(0) + packetno = math.ceil(file_size / packet_size) + packetno_bytes = struct.pack(">I", packetno) + packetsize_bytes = struct.pack(">H", packet_size) + data = packetno_bytes + packetsize_bytes + self.SendFileTransCommand(PTYPE_FILE_VIEW, data) + lastcmd = PTYPE_FILE_VIEW + lastseq = 0 + if cmdType == PTYPE_FILE_DATA: + seq = self.packetData[3] << 24 | self.packetData[4] << 16 | self.packetData[5] << 8 | self.packetData[6] + if seq == lastseq: + self.SendFileTransCommand(PTYPE_FILE_DATA, data) + elif seq == lastseq + 1: + seq_bytes = struct.pack(">I", seq) + file_data = stream.read(packet_size) + data = seq_bytes + file_data + self.SendFileTransCommand(PTYPE_FILE_DATA, data) else: - while True: - if self.getc(1, timeout) == None: - break - self.putc(NAK) - char = self.getc(1, timeout) - continue - - # read sequence - error_count = 0 - cancel = 0 - self.log.debug('recv: data block %d', sequence) - seq1 = self.getc(1, timeout) - if seq1 is None: - self.log.warn('getc failed to get first sequence byte') - seq2 = None - else: - seq1 = ord(seq1) - seq2 = self.getc(1, timeout) - if seq2 is None: - self.log.warn('getc failed to get second sequence byte') - else: - # second byte is the same as first as 1's complement - seq2 = 0xff - ord(seq2) - - if not (seq1 == seq2 == sequence): - # consume data anyway ... even though we will discard it, - # it is not the sequence we expected! - self.log.error('expected sequence %d, ' - 'got (seq1=%r, seq2=%r), ' - 'receiving next block, will NAK.', - sequence, seq1, seq2) - self.getc(2 + packet_size + 1 + crc_mode) + seq_bytes = struct.pack(">I", seq) + stream.seek((seq - 1) * packet_size, 0) + file_data = stream.read(packet_size) + data = seq_bytes + file_data + self.SendFileTransCommand(PTYPE_FILE_DATA, data) + lastcmd = PTYPE_FILE_DATA + lastseq = seq + if callable(callback): + callback(packet_size, seq, 0, 0) + if cmdType == PTYPE_FILE_END: + self.log.info("Transmission successful (FILE end flag received).") + return True else: - # sequence is ok, read packet - # packet_size + checksum - # self.log.warn('Got sequence %d', sequence) - data = self.getc(1 + is_stx + packet_size + 1 + crc_mode, timeout) - if data is None: - self.log.warn('recv error: We got a data as None') - valid = None - else: - valid, data = self._verify_recv_checksum(crc_mode, data) - - - # valid data, append chunk - if valid: - retrans = retry + 1 - if sequence == 0 and not md5_received: - md5_received = True - if md5.encode() == data[1 + is_stx : 33 + is_stx]: - self.putc(CAN) - self.putc(CAN) - self.putc(CAN) - while self.getc(1, timeout): - pass - return 0 - else: - income_size += len(data) - 1 - is_stx - data_len = data[0] << 8 | data[1] if is_stx else data[0] - stream.write(data[1 + is_stx: (data_len + 1 + is_stx)]) - success_count = success_count + 1 - if callable(callback): - callback(packet_size, success_count, error_count) - self.putc(ACK) - sequence = (sequence + 1) % 0x100 - # get next start-of-header byte - char = self.getc(1, timeout) - continue - - # something went wrong, request retransmission - self.log.warn('recv error: purge, requesting retransmission (NAK)') - while True: - if self.getc(1, timeout) == None: - break - retrans = retrans - 1 - if retrans <= 0: - self.log.error('Download error: too many retry error!') - self.abort() - return None - # get next start-of-header byte - self.putc(NAK) - char = self.getc(1, timeout) - continue + t = time.time() + if t - td > 9: + self.SendFileTransCommand(PTYPE_FILE_CAN, b'') + self.log.info("Info: Controller receive data timeout!") + self.FileRcvState = FileTransState.WAIT_MD5 + return def _verify_recv_checksum(self, crc_mode, data): if crc_mode: - _checksum = bytearray(data[-2:]) + _checksum = bytearray(data[(-2):]) their_sum = (_checksum[0] << 8) + _checksum[1] data = data[:-2] - our_sum = self.calc_crc(data) valid = bool(their_sum == our_sum) if not valid: - self.log.warn('recv error: checksum fail ' - '(theirs=%04x, ours=%04x), ', - their_sum, our_sum) + self.log.warn("recv error: checksum fail (theirs=%04x, ours=%04x), ", their_sum, our_sum) else: _checksum = bytearray([data[-1]]) their_sum = _checksum[0] data = data[:-1] - our_sum = self.calc_checksum(data) valid = their_sum == our_sum if not valid: - self.log.warn('recv error: checksum fail ' - '(theirs=%02x, ours=%02x)', - their_sum, our_sum) - return valid, data + self.log.warn("recv error: checksum fail (theirs=%02x, ours=%02x)", their_sum, our_sum) + return (valid, data) def calc_checksum(self, data, checksum=0): - ''' - Calculate the checksum for a given block of data, can also be used to - update a checksum. - - >>> csum = modem.calc_checksum('hello') - >>> csum = modem.calc_checksum('world', csum) - >>> hex(csum) - '0x3c' - - ''' if platform.python_version_tuple() >= ('3', '0', '0'): return (sum(data) + checksum) % 256 - else: - return (sum(map(ord, data)) + checksum) % 256 + return (sum(map(ord, data)) + checksum) % 256 def calc_crc(self, data, crc=0): - ''' - Calculate the Cyclic Redundancy Check for a given block of data, can - also be used to update a CRC. - - >>> crc = modem.calc_crc('hello') - >>> crc = modem.calc_crc('world', crc) - >>> hex(crc) - '0x4ab3' - - ''' for char in bytearray(data): - crctbl_idx = ((crc >> 8) ^ char) & 0xff - crc = ((crc << 8) ^ self.crctable[crctbl_idx]) & 0xffff - return crc & 0xffff + crctbl_idx = (crc >> 8 ^ char) & 255 + crc = (crc << 8 ^ self.crctable[crctbl_idx]) & 65535 + + return crc & 65535 -def _send(mode='xmodem', filename=None, timeout=30): - '''Send a file (or stdin) using the selected mode.''' +def _send(mode='USBMode', filename=None, timeout=30): if filename is None: si = sys.stdin else: - si = open(filename, 'rb') - - # TODO(maze): make this configurable, serial out, etc. + si = open(filename, "rb") so = sys.stdout def _getc(size, timeout=timeout): @@ -738,81 +432,56 @@ def _putc(data, timeout=timeout): def run(): - '''Run the main entry point for sending and receiving files.''' - import argparse - import serial - import sys - + import argparse, serial, sys platform = sys.platform.lower() - - if platform.startswith('win'): - default_port = 'COM1' + if platform.startswith("win"): + default_port = "COM1" else: - default_port = '/dev/ttyS0' - + default_port = "/dev/ttyS0" parser = argparse.ArgumentParser() - parser.add_argument('-p', '--port', default=default_port, - help='serial port') - parser.add_argument('-r', '--rate', default=9600, type=int, - help='baud rate') - parser.add_argument('-b', '--bytesize', default=serial.EIGHTBITS, - help='serial port transfer byte size') - parser.add_argument('-P', '--parity', default=serial.PARITY_NONE, - help='serial port parity') - parser.add_argument('-S', '--stopbits', default=serial.STOPBITS_ONE, - help='serial port stop bits') - parser.add_argument('-m', '--mode', default='xmodem', - help='XMODEM mode (xmodem, xmodem8k)') - parser.add_argument('-t', '--timeout', default=30, type=int, - help='I/O timeout in seconds') - - subparsers = parser.add_subparsers(dest='subcommand') - send_parser = subparsers.add_parser('send') - send_parser.add_argument('filename', nargs='?', - help='filename to send, empty reads from stdin') - recv_parser = subparsers.add_parser('recv') - recv_parser.add_argument('filename', nargs='?', - help='filename to receive, empty sends to stdout') - + parser.add_argument("-p", "--port", default=default_port, help="serial port") + parser.add_argument("-r", "--rate", default=9600, type=int, help="baud rate") + parser.add_argument("-b", "--bytesize", default=(serial.EIGHTBITS), help="serial port transfer byte size") + parser.add_argument("-P", "--parity", default=(serial.PARITY_NONE), help="serial port parity") + parser.add_argument("-S", "--stopbits", default=(serial.STOPBITS_ONE), help="serial port stop bits") + parser.add_argument("-m", "--mode", default="USBMode", help="XMODEM mode (USBMode, wifiMode)") + parser.add_argument("-t", "--timeout", default=30, type=int, help="I/O timeout in seconds") + subparsers = parser.add_subparsers(dest="subcommand") + send_parser = subparsers.add_parser("send") + send_parser.add_argument("filename", nargs="?", help="filename to send, empty reads from stdin") + recv_parser = subparsers.add_parser("recv") + recv_parser.add_argument("filename", nargs="?", help="filename to receive, empty sends to stdout") options = parser.parse_args() - - if options.subcommand == 'send': + if options.subcommand == "send": return _send(options.mode, options.filename, options.timeout) - elif options.subcommand == 'recv': + if options.subcommand == "recv": return _recv(options.mode, options.filename, options.timeout) def runx(): - import optparse - import subprocess - - parser = optparse.OptionParser( - usage='%prog [] filename filename') - parser.add_option('-m', '--mode', default='xmodem', - help='XMODEM mode (xmodem, xmodem8k)') - + import optparse, subprocess + parser = optparse.OptionParser(usage="%prog [] filename filename") + parser.add_option("-m", "--mode", default="USBMode", help="XMODEM mode (USBMode, wifiMode)") options, args = parser.parse_args() if len(args) != 3: - parser.error('invalid arguments') + parser.error("invalid arguments") return 1 - - elif args[0] not in ('send', 'recv'): - parser.error('invalid mode') + if args[0] not in ('send', 'recv'): + parser.error("invalid mode") return 1 def _func(so, si): import select + print(("si", si)) + print(("so", so)) - print(('si', si)) - print(('so', so)) def getc(size, timeout=3): read_ready, _, _ = select.select([so], [], [], timeout) if read_ready: data = so.read(size) else: data = None - - print(('getc(', repr(data), ')')) + print(("getc(", repr(data), ")")) return data def putc(data, timeout=3): @@ -823,34 +492,29 @@ def putc(data, timeout=3): size = len(data) else: size = None - - print(('putc(', repr(data), repr(size), ')')) + print(("putc(", repr(data), repr(size), ")")) return size - return getc, putc + return (getc, putc) def _pipe(*command): - pipe = subprocess.Popen(command, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE) - return pipe.stdout, pipe.stdin - - if args[0] == 'recv': - getc, putc = _func(*_pipe('sz', '--xmodem', args[2])) - stream = open(args[1], 'wb') - xmodem = XMODEM(getc, putc, mode=options.mode) + pipe = subprocess.Popen(command, stdout=(subprocess.PIPE), + stdin=(subprocess.PIPE)) + return (pipe.stdout, pipe.stdin) + + if args[0] == "recv": + getc, putc = _func(*_pipe("sz", "--xmodem", args[2])) + stream = open(args[1], "wb") + xmodem = XMODEM(getc, putc, mode=(options.mode)) status = xmodem.recv(stream, retry=8) - assert status, ('Transfer failed, status is', False) stream.close() - - elif args[0] == 'send': - getc, putc = _func(*_pipe('rz', '--xmodem', args[2])) - stream = open(args[1], 'rb') - xmodem = XMODEM(getc, putc, mode=options.mode) + elif args[0] == "send": + getc, putc = _func(*_pipe("rz", "--xmodem", args[2])) + stream = open(args[1], "rb") + xmodem = XMODEM(getc, putc, mode=(options.mode)) sent = xmodem.send(stream, retry=8) - assert sent is not None, ('Transfer failed, sent is', sent) stream.close() -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(run()) diff --git a/carveracontroller/main.py b/carveracontroller/main.py index a6669a0b..ad170c74 100644 --- a/carveracontroller/main.py +++ b/carveracontroller/main.py @@ -2129,6 +2129,9 @@ class Makera(RelativeLayout): fw_version_checking = False fw_version_checked = False + echosended = False + echosending = False + filetype_support = 'nc' filetype = '' @@ -3001,13 +3004,13 @@ def monitorSerial(self): # Update Decompress status bar if self.decompstatus == True: if self.decompercent != self.decompercentlast: - self.updateCompressProgress(self.decompercent) + self.updateDeCompressProgress(self.decompercent) self.decompercentlast = self.decompercent self.decomptime = time.time() else: t = time.time() if t - self.decomptime > 8: - self.updateCompressProgress(self.fileCompressionBlocks) + self.updateDeCompressProgress(self.fileCompressionBlocks) # Update position if needed if self.controller.posUpdate: @@ -3294,7 +3297,12 @@ def check_and_download(self): remote_path = self.file_popup.remote_rv.curr_selected_file remote_size = self.file_popup.remote_rv.curr_selected_filesize remote_post_path = remote_path.replace('/sd/', '').replace('\\sd\\', '') - local_path = os.path.join(self.temp_dir, remote_post_path) + if kivy_platform == 'ios': + from os.path import expanduser + local_path = join(expanduser('~'), 'Documents') + local_path = join(local_path, remote_post_path) + else: # inserted + local_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), remote_post_path) app = App.get_running_app() app.selected_local_filename = local_path app.selected_remote_filename = remote_path @@ -3308,7 +3316,12 @@ def check_and_download(self): # ----------------------------------------------------------------------- def download_config_file(self): app = App.get_running_app() - app.selected_local_filename = os.path.join(self.temp_dir, 'config.txt') + if kivy_platform == 'ios': + import tempfile + tmp_file = tempfile.gettempdir() + app.selected_local_filename = os.path.join(tmp_file, 'config.txt') + else: # inserted + app.selected_local_filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.txt') self.downloading_file = '/sd/config.txt' self.downloading_size = 1024 * 5 self.downloading_config = True @@ -3318,8 +3331,12 @@ def download_config_file(self): def finishLoadConfig(self, success, *args): if success: self.setting_list.clear() - # caching config file - config_path = os.path.join(self.temp_dir, 'config.txt') + if kivy_platform == 'ios': + import tempfile + tmp_file = tempfile.gettempdir() + config_path = os.path.join(tmp_file, 'config.txt') + else: # inserted + config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.txt') with open(config_path, 'r') as f: config_string = '[dummy_section]\n' + f.read() # remove notes @@ -3370,7 +3387,7 @@ def doDownload(self): if os.path.exists(tmp_filename): md5 = Utils.md5(tmp_filename) self.controller.downloadCommand(self.downloading_file) - self.controller.pauseStream(0.2) + self.controller.pauseStream(0.0) download_result = self.controller.stream.download(tmp_filename, md5, self.downloadCallback) except: logger.error(sys.exc_info()[1]) @@ -3462,9 +3479,8 @@ def setUIForModel(self, model, *args): Clock.schedule_once(lambda dt: self.load_machine_config(), 0.1) # ----------------------------------------------------------------------- - def downloadCallback(self, packet_size, success_count, error_count): - packets = self.downloading_size / packet_size + (1 if self.downloading_size % packet_size > 0 else 0) - Clock.schedule_once(partial(self.progressUpdate, success_count * 100.0 / packets, tr._('Downloading') + ' \n%s' % self.downloading_file, False), 0) + def downloadCallback(self, seq_rev, totalpackets): + Clock.schedule_once(partial(self.progressUpdate, seq_rev * 100.0 / totalpackets, tr._('Downloading') + ' \n%s' % self.downloading_file, False), 0) # ----------------------------------------------------------------------- def cancelSelectFile(self): @@ -3629,13 +3645,17 @@ def show_message_popup(self, message, btn_disabled, *args): self.message_popup.open() # ----------------------------------------------------------------------- - def compress_file(self,input_filename): + def compress_file(self, input_filename): + self.qlzfilename = None + compercent = 0 try: # If the uploaded file is a firmware file, return the original filename without compression. if input_filename.find('.bin') != -1: + self.qlzfilename = input_filename + self.compstatus = False return input_filename - # Check if the filename.lz is writeable + #Check if the filename.lz is writeable can_write_in_lz = os.access(input_filename + '.lz', os.W_OK) if not can_write_in_lz: logger.warning(f"Compression failed: Cannot write to '{input_filename}.lz', using temp dir") @@ -3651,36 +3671,43 @@ def compress_file(self,input_filename): self.decompercent = 0 self.decompercentlast = 0 with open(input_filename, 'rb') as f_in, open(output_filename, 'wb') as f_out: - while True: - # Read block data - block = f_in.read(BLOCK_SIZE) - if not block: - break - # Calculate the sum - for byte in block: - sum += byte - # Compress the block data - compressed_block = quicklz.compress(block) - - # Calculate the size of the compressed data block - cmprs_size = len(compressed_block) - buffer_hdr = struct.pack('>I', cmprs_size) - # Write the length of the compressed data block to the output file - f_out.write(buffer_hdr) - # Write the compressed data block to the output file - f_out.write(compressed_block) - self.fileCompressionBlocks += 1 - # Write the checksum - sumdata = struct.pack('>H', sum & 0xffff) - f_out.write(sumdata) - + file_stats = os.stat(f_in.fileno()) + fileCompresssize = file_stats.st_size + Clock.schedule_once(partial(self.progressStart, tr._('compressing') + '\n%s' % input_filename, None), 0) + while True: + # Read block data + block = f_in.read(BLOCK_SIZE) + if not block: + break + compercent += len(block) + # Calculate the sum + for byte in block: + sum += byte + # Compress the block data + compressed_block = quicklz.compress(block) + # Calculate the size of the compressed data block + cmprs_size = len(compressed_block) + buffer_hdr = struct.pack('>I', cmprs_size) + # Write the length of the compressed data block to the output file + f_out.write(buffer_hdr) + # Write the compressed data block to the output file + f_out.write(compressed_block) + self.fileCompressionBlocks += 1 + Clock.schedule_once(partial(self.progressUpdate, compercent * 100.0 / fileCompresssize, '', True), 0) + # Write the checksum + sumdata = struct.pack('>H', sum & 0xffff) + f_out.write(sumdata) + Clock.schedule_once(self.progressFinish, 0.5) + self.compstatus = False logger.info(f"Compression completed. Compressed file saved as '{output_filename}'.") + self.qlzfilename = output_filename return output_filename except Exception as e: logger.error(f"Compression failed: {e}") if os.path.exists(output_filename): os.remove(output_filename) + self.compstatus = False return None # ----------------------------------------------------------------------- def decompress_file(self,input_filename,output_filename): @@ -3730,17 +3757,26 @@ def decompress_file(self,input_filename,output_filename): return False # ----------------------------------------------------------------------- def uploadLocalFile(self, filepath, callback=None): - self.controller.sendNUM = SEND_FILE self.uploading_file = filepath - self.original_upload_filepath = filepath # Store original path for recent directory tracking - if 'lz' in self.filetype: #如果固件支持的上传文件类型为.lz,则进行压缩 - qlzfilename = self.compress_file(filepath) - if qlzfilename: - self.uploading_file = qlzfilename - threading.Thread(target=self.doUpload,args=(callback,)).start() + try: + file_size = os.path.getsize(self.uploading_file) + except FileNotFoundError: + file_size = 0 + if 'lz' in self.filetype and file_size > BLOCK_SIZE: + self.compstatus = True + threading.Thread(target=self.compress_file, args=(filepath,)).start() + threading.Thread(target=self.doUpload).start() # ----------------------------------------------------------------------- - def doUpload(self, callback): + def doUpload(self): + while self.compstatus: + ts = time.time() + self.controller.sendNUM = SEND_FILE + ts = td = time.time() + while ts - td < 1: + ts = time.time() + if self.qlzfilename: + self.uploading_file = self.qlzfilename self.uploading_size = os.path.getsize(self.uploading_file) remotename = os.path.join(self.file_popup.remote_rv.curr_dir, os.path.basename(os.path.normpath(self.uploading_file))) if self.file_popup.firmware_mode: @@ -3757,6 +3793,7 @@ def doUpload(self, callback): #md5 = Utils.md5(self.uploading_file) md5 = Utils.md5(displayname) self.controller.uploadCommand(os.path.normpath(remotename)) + time.sleep(0.2) upload_result = self.controller.stream.upload(self.uploading_file, md5, self.uploadCallback) except: self.controller.log.put((Controller.MSG_ERROR, str(sys.exc_info()[1]))) @@ -3785,7 +3822,13 @@ def doUpload(self, callback): # copy file to application directory if needed remote_path = os.path.join(self.file_popup.remote_rv.curr_dir, os.path.basename(os.path.normpath(self.uploading_file))) remote_post_path = remote_path.replace('/sd/', '').replace('\\sd\\', '') - local_path = os.path.join(self.temp_dir, remote_post_path) + if kivy_platform == 'ios': + from os.path import expanduser + sandbox_documents_path = os.path.join(expanduser('~'), 'Documents') + curr_dir = os.path.join(sandbox_documents_path, 'gcodes') + local_path = os.path.join(os.path.dirname(curr_dir), remote_post_path) + else: # inserted + local_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), remote_post_path) if self.uploading_file != local_path and not self.file_popup.firmware_mode: if self.uploading_file.endswith('.lz'): #copy lz file to .lz dir @@ -3821,15 +3864,9 @@ def doUpload(self, callback): self.decompstatus = True os.remove(self.uploading_file) self.decomptime = time.time() - Clock.schedule_once(partial(self.progressStart, tr._('Decompressing') + '\n%s' % displayname, False), 0.2) + Clock.schedule_once(partial(self.progressStart, tr._('Decompressing') + '\n%s' % displayname, False), 0.5) self.controller.sendNUM = 0 - if upload_result and callback: # Only run callback if upload succeeded - if self.uploading_file.endswith('.lz'): - callback(remotename[:-3], origin_path) - else: - callback(remotename, local_path) - # For iOS we display the file list remotely only so we need to refresh it but on main thread if upload_result and not self.file_popup.firmware_mode and not self.uploading_file.endswith('.lz'): Clock.schedule_once(self.file_popup.remote_rv.current_dir, 0) @@ -3936,7 +3973,7 @@ def progressFinish(self, *args): self.progress_popup.dismiss() # --------------------------------------------------------------`--------- - def updateCompressProgress(self, value): + def updateDeCompressProgress(self, value): Clock.schedule_once(partial(self.progressUpdate, value * 100.0 / self.fileCompressionBlocks, '', True), 0) if value == self.fileCompressionBlocks: Clock.schedule_once(self.progressFinish, 0) @@ -3973,6 +4010,8 @@ def updateStatus(self, *args): self.config_loaded = False self.config_loading = False self.fw_version_checked = False + self.echosended = False + self.echosending = False # Check if we should show reconnection popup (only if not a manual disconnect and not already reconnecting) if not self.controller._manual_disconnect and not self.reconnection_popup._is_open: @@ -4025,7 +4064,20 @@ def updateStatus(self, *args): self.status_drop_down.btn_unlock.text = 'Reset' else: self.status_drop_down.btn_unlock.text = 'Unlock' - + if not app.playing and (not self.echosended) and (not self.echosending): + try: + if self.controller.stream: + self.echosending = True + self.controller.stream.send(b'echo echo\n') + echo = self.controller.stream.getc(10) + if echo == b'echo: echo': + self.message_popup.lb_content.text = tr._('Firmware version mismatch! \nPlease use a Controller with version V0.9.11 or earlier \nto upgrade the firmware to V1.0.4.') + self.message_popup.btn_ok.disabled = False + self.message_popup.open(self) + return + self.echosended = True + except: + print(sys.exc_info()[1]) # load config, only one time per connection if not app.playing and not self.config_loaded and not self.config_loading and app.state == "Idle": self.config_loading = True