1# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
2# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
3#
4# SPDX-License-Identifier: GPL-2.0-or-later
5
6import errno
7import os
8import struct
9import time
10
11from .util import FatalError, PrintOnce
12
13# Used for resetting into bootloader on Unix-like systems
14if os.name != "nt":
15    import fcntl
16    import termios
17
18    # Constants used for terminal status lines reading/setting.
19    # Taken from pySerial's backend for IO:
20    # https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
21    TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
22    TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
23    TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
24    TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
25
26DEFAULT_RESET_DELAY = 0.05  # default time to wait before releasing boot pin after reset
27
28
29class ResetStrategy(object):
30    print_once = PrintOnce()
31
32    def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
33        self.port = port
34        self.reset_delay = reset_delay
35
36    def __call__(self):
37        """
38        On targets with USB modes, the reset process can cause the port to
39        disconnect / reconnect during reset.
40        This will retry reconnections on ports that
41        drop out during the reset sequence.
42        """
43        for retry in reversed(range(3)):
44            try:
45                if not self.port.isOpen():
46                    self.port.open()
47                self.reset()
48                break
49            except OSError as e:
50                # ENOTTY for TIOCMSET; EINVAL for TIOCMGET
51                if e.errno in [errno.ENOTTY, errno.EINVAL]:
52                    self.print_once(
53                        "WARNING: Chip was NOT reset. Setting RTS/DTR lines is not "
54                        f"supported for port '{self.port.name}'. Set --before and --after "
55                        "arguments to 'no_reset' and switch to bootloader manually to "
56                        "avoid this warning."
57                    )
58                    break
59                elif not retry:
60                    raise
61                self.port.close()
62                time.sleep(0.5)
63
64    def reset(self):
65        pass
66
67    def _setDTR(self, state):
68        self.port.setDTR(state)
69
70    def _setRTS(self, state):
71        self.port.setRTS(state)
72        # Work-around for adapters on Windows using the usbser.sys driver:
73        # generate a dummy change to DTR so that the set-control-line-state
74        # request is sent with the updated RTS state and the same DTR state
75        self.port.setDTR(self.port.dtr)
76
77    def _setDTRandRTS(self, dtr=False, rts=False):
78        status = struct.unpack(
79            "I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
80        )[0]
81        if dtr:
82            status |= TIOCM_DTR
83        else:
84            status &= ~TIOCM_DTR
85        if rts:
86            status |= TIOCM_RTS
87        else:
88            status &= ~TIOCM_RTS
89        fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))
90
91
92class ClassicReset(ResetStrategy):
93    """
94    Classic reset sequence, sets DTR and RTS lines sequentially.
95    """
96
97    def reset(self):
98        self._setDTR(False)  # IO0=HIGH
99        self._setRTS(True)  # EN=LOW, chip in reset
100        time.sleep(0.1)
101        self._setDTR(True)  # IO0=LOW
102        self._setRTS(False)  # EN=HIGH, chip out of reset
103        time.sleep(self.reset_delay)
104        self._setDTR(False)  # IO0=HIGH, done
105
106
107class UnixTightReset(ResetStrategy):
108    """
109    UNIX-only reset sequence with custom implementation,
110    which allows setting DTR and RTS lines at the same time.
111    """
112
113    def reset(self):
114        self._setDTRandRTS(False, False)
115        self._setDTRandRTS(True, True)
116        self._setDTRandRTS(False, True)  # IO0=HIGH & EN=LOW, chip in reset
117        time.sleep(0.1)
118        self._setDTRandRTS(True, False)  # IO0=LOW & EN=HIGH, chip out of reset
119        time.sleep(self.reset_delay)
120        self._setDTRandRTS(False, False)  # IO0=HIGH, done
121        self._setDTR(False)  # Needed in some environments to ensure IO0=HIGH
122
123
124class USBJTAGSerialReset(ResetStrategy):
125    """
126    Custom reset sequence, which is required when the device
127    is connecting via its USB-JTAG-Serial peripheral.
128    """
129
130    def reset(self):
131        self._setRTS(False)
132        self._setDTR(False)  # Idle
133        time.sleep(0.1)
134        self._setDTR(True)  # Set IO0
135        self._setRTS(False)
136        time.sleep(0.1)
137        self._setRTS(True)  # Reset. Calls inverted to go through (1,1) instead of (0,0)
138        self._setDTR(False)
139        self._setRTS(True)  # RTS set as Windows only propagates DTR on RTS setting
140        time.sleep(0.1)
141        self._setDTR(False)
142        self._setRTS(False)  # Chip out of reset
143
144
145class HardReset(ResetStrategy):
146    """
147    Reset sequence for hard resetting the chip.
148    Can be used to reset out of the bootloader or to restart a running app.
149    """
150
151    def __init__(self, port, uses_usb=False):
152        super().__init__(port)
153        self.uses_usb = uses_usb
154
155    def reset(self):
156        self._setRTS(True)  # EN->LOW
157        if self.uses_usb:
158            # Give the chip some time to come out of reset,
159            # to be able to handle further DTR/RTS transitions
160            time.sleep(0.2)
161            self._setRTS(False)
162            time.sleep(0.2)
163        else:
164            time.sleep(0.1)
165            self._setRTS(False)
166
167
168class CustomReset(ResetStrategy):
169    """
170    Custom reset strategy defined with a string.
171
172    CustomReset object is created as "rst = CustomReset(port, seq_str)"
173    and can be later executed simply with "rst()"
174
175    The seq_str input string consists of individual commands divided by "|".
176    Commands (e.g. R0) are defined by a code (R) and an argument (0).
177
178    The commands are:
179    D: setDTR - 1=True / 0=False
180    R: setRTS - 1=True / 0=False
181    U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
182    W: Wait (time delay) - positive float number
183
184    e.g.
185    "D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
186    "U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
187    """
188
189    format_dict = {
190        "D": "self.port.setDTR({})",
191        "R": "self.port.setRTS({})",
192        "W": "time.sleep({})",
193        "U": "self._setDTRandRTS({})",
194    }
195
196    def reset(self):
197        exec(self.constructed_strategy)
198
199    def __init__(self, port, seq_str):
200        super().__init__(port)
201        self.constructed_strategy = self._parse_string_to_seq(seq_str)
202
203    def _parse_string_to_seq(self, seq_str):
204        try:
205            cmds = seq_str.split("|")
206            fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
207        except Exception as e:
208            raise FatalError(f'Invalid "custom_reset_sequence" option format: {e}')
209        return "\n".join(fn_calls_list)
210