mpytool 2.0.0__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mpytool/conn.py CHANGED
@@ -63,6 +63,23 @@ class Conn():
63
63
  return self._read_available()
64
64
  return None
65
65
 
66
+ def read_bytes(self, count, timeout=1):
67
+ """Read exactly count bytes from device"""
68
+ start_time = _time.time()
69
+ while len(self._buffer) < count:
70
+ if self._read_to_buffer(wait_timeout=0.001):
71
+ start_time = _time.time()
72
+ if timeout is not None and start_time + timeout < _time.time():
73
+ if self._buffer:
74
+ raise Timeout(
75
+ f"During timeout received: {bytes(self._buffer)}")
76
+ raise Timeout("No data received")
77
+ data = bytes(self._buffer[:count])
78
+ del self._buffer[:count]
79
+ if self._log:
80
+ self._log.debug("rd: %s", data)
81
+ return data
82
+
66
83
  def write(self, data):
67
84
  """Write data to device"""
68
85
  if self._log:
@@ -98,3 +115,15 @@ class Conn():
98
115
  """Read single line"""
99
116
  line = self.read_until(b'\n', timeout)
100
117
  return line.strip(b'\r')
118
+
119
+ def hard_reset(self):
120
+ """Hardware reset (only available on serial connections)"""
121
+ raise NotImplementedError("Hardware reset not available on this connection")
122
+
123
+ def reset_to_bootloader(self):
124
+ """Reset into bootloader mode (only available on serial connections)"""
125
+ raise NotImplementedError("Reset to bootloader not available on this connection")
126
+
127
+ def reconnect(self, timeout=None):
128
+ """Reconnect after device reset (only available on serial connections)"""
129
+ raise NotImplementedError("Reconnect not available on this connection")
mpytool/conn_serial.py CHANGED
@@ -1,12 +1,16 @@
1
1
  """MicroPython tool: serial connector"""
2
2
 
3
+ import time as _time
3
4
  import serial as _serial
4
5
  import mpytool.conn as _conn
5
6
 
6
7
 
7
8
  class ConnSerial(_conn.Conn):
9
+ RECONNECT_TIMEOUT = 10 # seconds
10
+
8
11
  def __init__(self, log=None, **serial_config):
9
12
  super().__init__(log)
13
+ self._serial_config = serial_config
10
14
  try:
11
15
  self._serial = _serial.Serial(**serial_config)
12
16
  except _serial.serialutil.SerialException as err:
@@ -24,11 +28,94 @@ class ConnSerial(_conn.Conn):
24
28
 
25
29
  def _read_available(self):
26
30
  """Read available data from serial port"""
27
- in_waiting = self._serial.in_waiting
28
- if in_waiting > 0:
29
- return self._serial.read(in_waiting)
30
- return None
31
+ if self._serial is None:
32
+ raise _conn.ConnError("Not connected")
33
+ try:
34
+ in_waiting = self._serial.in_waiting
35
+ if in_waiting > 0:
36
+ return self._serial.read(in_waiting)
37
+ return None
38
+ except OSError as err:
39
+ raise _conn.ConnError(f"Connection lost: {err}") from err
31
40
 
32
41
  def _write_raw(self, data):
33
42
  """Write data to serial port"""
34
- return self._serial.write(data)
43
+ if self._serial is None:
44
+ raise _conn.ConnError("Not connected")
45
+ try:
46
+ return self._serial.write(data)
47
+ except OSError as err:
48
+ raise _conn.ConnError(f"Connection lost: {err}") from err
49
+
50
+ def _is_usb_cdc(self):
51
+ """Check if this is a USB-CDC port (native USB on ESP32-S/C)"""
52
+ port = self._serial.port or ''
53
+ return 'ACM' in port or 'usbmodem' in port
54
+
55
+ def hard_reset(self):
56
+ """Hardware reset using DTR/RTS signals"""
57
+ self._serial.setDTR(False) # GPIO0 high (normal boot)
58
+ self._serial.setRTS(True) # Assert reset
59
+ _time.sleep(0.1)
60
+ self._serial.setRTS(False) # Release reset
61
+
62
+ def reconnect(self, timeout=None):
63
+ """Close and reopen serial port (for USB-CDC reconnect after reset)"""
64
+ if timeout is None:
65
+ timeout = self.RECONNECT_TIMEOUT
66
+ port = self._serial_config.get('port', 'unknown')
67
+ if self._serial:
68
+ try:
69
+ self._serial.close()
70
+ except (OSError, _serial.serialutil.SerialException):
71
+ pass # Port may already be gone
72
+ self._serial = None
73
+ start = _time.time()
74
+ while _time.time() - start < timeout:
75
+ try:
76
+ self._serial = _serial.Serial(**self._serial_config)
77
+ del self._buffer[:] # Clear any stale data
78
+ _time.sleep(0.5) # Wait for device to stabilize
79
+ # Verify connection is stable
80
+ _ = self._serial.in_waiting
81
+ return True
82
+ except (_serial.serialutil.SerialException, OSError):
83
+ if self._serial:
84
+ try:
85
+ self._serial.close()
86
+ except (OSError, _serial.serialutil.SerialException):
87
+ pass
88
+ self._serial = None
89
+ _time.sleep(0.1)
90
+ raise _conn.ConnError(f"Could not reconnect to {port} within {timeout}s")
91
+
92
+ def reset_to_bootloader(self):
93
+ """Reset into bootloader mode (auto-detects USB-CDC vs USB-UART)"""
94
+ if self._is_usb_cdc():
95
+ self._reset_to_bootloader_usb_jtag()
96
+ else:
97
+ self._reset_to_bootloader_classic()
98
+
99
+ def _reset_to_bootloader_usb_jtag(self):
100
+ """Bootloader reset for USB-JTAG-Serial (esptool sequence)"""
101
+ self._serial.setRTS(False)
102
+ self._serial.setDTR(False)
103
+ _time.sleep(0.1)
104
+ self._serial.setDTR(True)
105
+ self._serial.setRTS(True)
106
+ _time.sleep(0.1)
107
+ self._serial.setDTR(False)
108
+ self._serial.setRTS(True)
109
+ _time.sleep(0.1)
110
+ self._serial.setDTR(False)
111
+ self._serial.setRTS(False)
112
+
113
+ def _reset_to_bootloader_classic(self):
114
+ """Bootloader reset for USB-UART (classic DTR/RTS circuit)"""
115
+ self._serial.setDTR(False) # GPIO0 high
116
+ self._serial.setRTS(True) # Assert reset
117
+ _time.sleep(0.1)
118
+ self._serial.setDTR(True) # GPIO0 low (bootloader)
119
+ self._serial.setRTS(False) # Release reset
120
+ _time.sleep(0.05)
121
+ self._serial.setDTR(False) # GPIO0 high