sertools 0.1.0__tar.gz

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.
sertools-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Doar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: sertools
3
+ Version: 0.1.0
4
+ Summary: Interface for serial devices using pyserial.
5
+ Author-email: Jason Doar <jbdoar@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jbdoar/sertools
8
+ Project-URL: Issues, https://github.com/jbdoar/sertools/issues
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyserial
13
+ Requires-Dist: readchar
14
+ Dynamic: license-file
15
+
16
+ # sertools
17
+
18
+ Interface for serial devices using pyserial.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install sertools
24
+ ```
25
+
26
+ ## Features
27
+ - Query method with flexible configuration parameters for various response formats.
28
+ - Lightweight serial port terminal emulator.
29
+
30
+ ## Usage
31
+ The SerialDevice instance may be called with a command string to send to the serial port and optional readback/terminator parameters.
32
+
33
+ ```python
34
+ from sertools import SerialDevice
35
+ ser = SerialDevice()
36
+ ser('HELP')
37
+ ```
38
+
39
+ ## Development
40
+ ```bash
41
+ git clone https://github.com/jbdoar/sertools
42
+ cd sertools
43
+ pip install -e .
44
+ ```
@@ -0,0 +1,29 @@
1
+ # sertools
2
+
3
+ Interface for serial devices using pyserial.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install sertools
9
+ ```
10
+
11
+ ## Features
12
+ - Query method with flexible configuration parameters for various response formats.
13
+ - Lightweight serial port terminal emulator.
14
+
15
+ ## Usage
16
+ The SerialDevice instance may be called with a command string to send to the serial port and optional readback/terminator parameters.
17
+
18
+ ```python
19
+ from sertools import SerialDevice
20
+ ser = SerialDevice()
21
+ ser('HELP')
22
+ ```
23
+
24
+ ## Development
25
+ ```bash
26
+ git clone https://github.com/jbdoar/sertools
27
+ cd sertools
28
+ pip install -e .
29
+ ```
@@ -0,0 +1,31 @@
1
+ # pyproject.toml
2
+
3
+ [build-system]
4
+ requires = ["setuptools>=64", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "sertools"
9
+ version = "0.1.0"
10
+ description = "Interface for serial devices using pyserial."
11
+ readme = "README.md"
12
+ requires-python = ">=3.9"
13
+ authors = [{name = "Jason Doar", email = "jbdoar@gmail.com"}]
14
+ dependencies = ["pyserial", "readchar"]
15
+ license = "MIT"
16
+ license-files = ["LICENSE"]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/jbdoar/sertools"
20
+ Issues = "https://github.com/jbdoar/sertools/issues"
21
+
22
+ [project.scripts]
23
+ # serial terminal emulator
24
+ sterm = "sertools.scripts.sterm:main"
25
+
26
+
27
+ [tool.setuptools]
28
+ package-dir = {"" = "src"}
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .serial_device import SerialDevice
2
+
3
+ __all__ = ["SerialDevice"]
File without changes
@@ -0,0 +1,125 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ import threading
5
+
6
+ import readchar
7
+
8
+ import sertools
9
+
10
+
11
+ log = logging.getLogger('sertools')
12
+
13
+
14
+ def configure_logging(logfile=None):
15
+ """
16
+ """
17
+ log.setLevel(logging.INFO)
18
+ log.handlers.clear()
19
+ log.propagate = False
20
+
21
+ if logfile:
22
+ file_handler = logging.FileHandler(logfile, encoding='utf-8')
23
+ file_handler.setLevel(logging.INFO)
24
+ file_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M%S'))
25
+ log.addHandler(file_handler)
26
+
27
+
28
+ def reader(ser, stop_event):
29
+ """
30
+ """
31
+ buf = bytearray()
32
+
33
+ while not stop_event.is_set():
34
+ try:
35
+ n = ser.ser.in_waiting
36
+ data = ser.ser.read(n or 1)
37
+ except Exception:
38
+ break
39
+
40
+ if not data:
41
+ continue
42
+
43
+ text = data.decode(ser.encoding, errors='replace')
44
+ sys.stdout.write(text)
45
+ sys.stdout.flush()
46
+
47
+ buf.extend(data)
48
+
49
+ while True:
50
+ i_crlf = buf.find(b'\r\n')
51
+ i_cr = buf.find(b'\r')
52
+ i_lf = buf.find(b'\n')
53
+
54
+ matches = [(i, 2) for i in [i_crlf] if i != -1]
55
+ matches += [(i, 1) for i in [i_cr, i_lf] if i != -1]
56
+
57
+ if not matches:
58
+ break
59
+
60
+ i, nl_len = min(matches, key=lambda x: x[0])
61
+ line = bytes(buf[:i]).decode(ser.encoding, errors='replace')
62
+ del buf[:i + nl_len]
63
+
64
+ if line:
65
+ log.info('RX: %r', line)
66
+
67
+
68
+ def send_raw(ser, s):
69
+ """
70
+ """
71
+ sys.stdout.write(s)
72
+ sys.stdout.flush()
73
+ ser.write(s, append_newline=False)
74
+ log.info('TX: %r', s)
75
+
76
+
77
+
78
+ def terminal(ser):
79
+ stop_event = threading.Event()
80
+ threading.Thread(target=reader, args=(ser, stop_event), daemon=True).start()
81
+
82
+ try:
83
+ while True:
84
+ try:
85
+ key = readchar.readkey()
86
+ except KeyboardInterrupt:
87
+ break
88
+
89
+ if key == readchar.key.ESC:
90
+ send_raw(ser, '\x1b')
91
+ elif key == readchar.key.BACKSPACE:
92
+ send_raw(ser, '\x08')
93
+ elif key in (readchar.key.ENTER, readchar.key.CR, readchar.key.LF):
94
+ send_raw(ser, ser.newline_tx or '\r')
95
+ else:
96
+ send_raw(ser, key)
97
+ finally:
98
+ stop_event.set()
99
+
100
+
101
+ def main():
102
+ parser = argparse.ArgumentParser()
103
+ parser.add_argument('port')
104
+ parser.add_argument('baudrate', type=int)
105
+ parser.add_argument('--log', help='path to log file')
106
+ args = parser.parse_args()
107
+
108
+ configure_logging(args.log)
109
+
110
+ ser = sertools.SerialDevice(
111
+ port=args.port,
112
+ baudrate=args.baudrate,
113
+ newline_rx='\r',
114
+ terminator=None,
115
+ terminator_cmd=None,
116
+ )
117
+
118
+ try:
119
+ terminal(ser)
120
+ finally:
121
+ ser.close()
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -0,0 +1,303 @@
1
+ """sertools.py module, a thin pyserial wrapper"""
2
+
3
+ import logging
4
+ import time
5
+
6
+ import serial
7
+
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ class SerialDevice:
13
+ """Thin wrapper for pyserial.Serial class.
14
+ Provides a flexible query method for sending commands and receiving responses.
15
+ Responses are always either a str (single line) or list of str (multiline).
16
+
17
+ Parameters
18
+ ----------
19
+ port : str
20
+ baudrate : int
21
+ timeout : float
22
+ encoding : str
23
+ newline_tx : str
24
+ newline_rx : str
25
+ terminator : str
26
+ terminator_cmd : str
27
+
28
+ Examples
29
+ --------
30
+ >>> dut = SerialDevice(port='COM42',
31
+ baudrate=460800,
32
+ timeout=None,
33
+ newline_tx='\r',
34
+ newline_rx='\r\n',
35
+ terminator='Ok',
36
+ terminator_cmd='\r')
37
+ """
38
+ def __init__(self, *,
39
+ port: str | None = None,
40
+ baudrate: int = 9600,
41
+ timeout: float | None = None,
42
+ encoding: str = 'ascii',
43
+ newline_tx: str = '\r',
44
+ newline_rx: str = '\r',
45
+ terminator: str | None = None,
46
+ terminator_cmd: str | None = None):
47
+
48
+ self.port = port
49
+ self.baudrate = baudrate
50
+ self.timeout = timeout
51
+ self.encoding = encoding
52
+ self.newline_tx = newline_tx
53
+ self.newline_rx = newline_rx
54
+ self.terminator = terminator
55
+ self.terminator_cmd = terminator_cmd
56
+ self._rx_buffer = bytearray()
57
+
58
+ self.ser = serial.Serial(port=port,
59
+ baudrate=baudrate,
60
+ timeout=timeout)
61
+
62
+
63
+ def __repr__(self):
64
+ return f"{self.__class__.__name__}(port={self.port!r}, baudrate={self.baudrate})"
65
+
66
+
67
+ def __str__(self):
68
+ return f"{self.port} @ {self.baudrate}"
69
+
70
+
71
+ def __call__(self, command: str, **kwargs) -> str | list[str]:
72
+ """Passes `command` and other params along to `self.query`."""
73
+
74
+ response = self.query(command, **kwargs)
75
+ return response
76
+
77
+
78
+ def open(self) -> None:
79
+ """Open port."""
80
+ self.ser.open()
81
+
82
+
83
+ def close(self) -> None:
84
+ """Close port."""
85
+ self.ser.close()
86
+
87
+
88
+ def flush(self) -> None:
89
+ """Reset input and output buffers."""
90
+ self.ser.reset_input_buffer()
91
+ self.ser.reset_output_buffer()
92
+
93
+
94
+ def write(self, command: str,
95
+ newline_tx: str | None = None,
96
+ append_newline: bool = True) -> int:
97
+ """Write command to port.
98
+
99
+ Parameters
100
+ ----------
101
+ command : str
102
+ Command sent to device.
103
+ newline_tx : str, optional
104
+ Newline character denoting end of line sent.
105
+ Defaults to `self.newline_tx'
106
+ append_newline : bool, optional
107
+ Optionally append newline_tx to command if not already present.
108
+ Defaults to True.
109
+
110
+ Returns
111
+ -------
112
+ int
113
+ Number of bytes sent to port.
114
+
115
+ Examples
116
+ --------
117
+ >>> TBD
118
+ """
119
+ newline_tx = self.newline_tx if newline_tx is None else newline_tx
120
+
121
+ if append_newline and not command.endswith(newline_tx):
122
+ command += newline_tx
123
+
124
+ log.info("TX: %r", command)
125
+ tx = command.encode(self.encoding)
126
+ return self.ser.write(tx)
127
+
128
+
129
+ def readline(self, newline_rx: str | None = None,
130
+ timeout: float | None = None) -> str:
131
+ """Read a line from device.
132
+
133
+ Parameters
134
+ ----------
135
+ newline_rx : str, optional
136
+ Newline character denoting end of line read.
137
+ Defaults to self.newline_rx.
138
+ timeout : float, optional
139
+ Timeout.
140
+ Defaults to self.timeout.
141
+
142
+ Returns
143
+ -------
144
+ line : str
145
+
146
+ Examples
147
+ --------
148
+ >>> TBD
149
+ """
150
+ newline_rx = self.newline_rx if newline_rx is None else newline_rx
151
+ timeout = self.timeout if timeout is None else timeout
152
+
153
+ nl = newline_rx.encode(self.encoding)
154
+ t0 = time.monotonic()
155
+ line_bytes = None
156
+
157
+ while True:
158
+ i = self._rx_buffer.find(nl)
159
+ if i != -1:
160
+ line_bytes = self._rx_buffer[:i]
161
+ del self._rx_buffer[:i + len(nl)]
162
+ break
163
+
164
+ if timeout is not None and time.monotonic() - t0 >= timeout:
165
+ if self._rx_buffer:
166
+ line_bytes = bytes(self._rx_buffer)
167
+ self._rx_buffer.clear()
168
+ else:
169
+ line_bytes = b''
170
+ break
171
+
172
+ n = self.ser.in_waiting
173
+ if n:
174
+ self._rx_buffer.extend(self.ser.read(n))
175
+ else:
176
+ time.sleep(0.005)
177
+
178
+ line = line_bytes.decode(self.encoding, errors='replace').strip()
179
+ if line:
180
+ log.info("RX: %r", line)
181
+ return line
182
+
183
+
184
+ def query(self, command: str, **kwargs) -> str | list[str]:
185
+ """Primary method for sending command and receiving response.
186
+
187
+ Parameters
188
+ ----------
189
+ command : str
190
+ Command sent to device.
191
+
192
+ **kwargs
193
+ Additional read options.
194
+ May override instance defaults such as `newline_tx`, `newline_rx`, etc.
195
+
196
+ newline_tx : str, optional
197
+ Newline character for `self.write`.
198
+ Defaults to `self.newline_tx`.
199
+ newline_rx : str, optional
200
+ Newline character for `self.readline`.
201
+ Defaults to `self.newline_rx`.
202
+ timeout : float, optional
203
+ Read timeout for whole query, separate from `self.ser.timeout`.
204
+ Defaults to `self.timeout`.
205
+ terminator : str, optional
206
+ Ends readline loop when `terminator` in `line`.
207
+ Defaults to `self.terminator`.
208
+ terminator_cmd : str, optional
209
+ Command string sent to port that triggers device to emit `terminator`.
210
+ Defaults to `self.terminator_cmd`.
211
+ strip_terminator : bool, optional
212
+ Remove `terminator` line from end of `response`.
213
+ Defaults to `True`.
214
+ terminator_delay : float, optional
215
+ Wait `terminator_delay` seconds before sending `terminator_cmd`.
216
+ Defaults to 0.
217
+ num_lines : int, optional
218
+ Stops read when `len(response) == num_lines`.
219
+ Defaults to None.
220
+
221
+ Returns
222
+ -------
223
+ response : str or list[str]
224
+
225
+ Examples
226
+ --------
227
+ >>> TBD
228
+ """
229
+
230
+ newline_tx = kwargs.get('newline_tx', self.newline_tx)
231
+ newline_rx = kwargs.get('newline_rx', self.newline_rx)
232
+ timeout = kwargs.get('timeout', self.timeout)
233
+ terminator = kwargs.get('terminator', self.terminator)
234
+ terminator_cmd = kwargs.get('terminator_cmd', self.terminator_cmd)
235
+ strip_terminator = kwargs.get('strip_terminator', True)
236
+ terminator_delay = kwargs.get('terminator_delay', 0)
237
+ num_lines = kwargs.get('num_lines', None)
238
+
239
+ # do we want to check if stuff is getting received?
240
+
241
+ self.flush()
242
+
243
+ self.write(command, newline_tx=newline_tx, append_newline=True)
244
+
245
+ response = []
246
+ t0 = time.monotonic()
247
+ sent_terminator = False
248
+ saw_terminator = False
249
+
250
+ while True:
251
+ # End read if timeout
252
+ if timeout is None:
253
+ remaining = None
254
+ else:
255
+ remaining = timeout - (time.monotonic() - t0)
256
+ if remaining <= 0:
257
+ break
258
+
259
+ # Send terminator_cmd after terminator_delay.
260
+ # Toggle sent_terminator to True so it only sends it once.
261
+ if (terminator_cmd is not None
262
+ and terminator_delay >= 0
263
+ and not sent_terminator
264
+ and time.monotonic() - t0 >= terminator_delay
265
+ ):
266
+ self.write(terminator_cmd, newline_tx=newline_tx, append_newline=False)
267
+ sent_terminator = True
268
+
269
+ # Read line and append to response if it's not empty
270
+ line = self.readline(newline_rx=newline_rx, timeout=remaining)
271
+
272
+ if line:
273
+ response.append(line)
274
+
275
+ # Optionally end read at num_lines
276
+ if num_lines is not None and len(response) >= num_lines:
277
+ break
278
+
279
+ # Optionally end read at terminator
280
+ if (terminator is not None
281
+ and line
282
+ and terminator in line
283
+ and sent_terminator):
284
+ saw_terminator = True
285
+ break
286
+
287
+ # Optionally remove terminator from response.
288
+ if strip_terminator and response and terminator and saw_terminator and terminator in response[-1]:
289
+ response = response[:-1]
290
+
291
+ # If response is single line, return as str
292
+ if len(response) == 1:
293
+ response = response[0]
294
+
295
+ # clear 'self._rx_buffer'
296
+ self._rx_buffer = bytearray()
297
+
298
+ # flush
299
+ if self.ser.in_waiting != 0:
300
+ self.flush()
301
+
302
+ return response
303
+
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: sertools
3
+ Version: 0.1.0
4
+ Summary: Interface for serial devices using pyserial.
5
+ Author-email: Jason Doar <jbdoar@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jbdoar/sertools
8
+ Project-URL: Issues, https://github.com/jbdoar/sertools/issues
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyserial
13
+ Requires-Dist: readchar
14
+ Dynamic: license-file
15
+
16
+ # sertools
17
+
18
+ Interface for serial devices using pyserial.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install sertools
24
+ ```
25
+
26
+ ## Features
27
+ - Query method with flexible configuration parameters for various response formats.
28
+ - Lightweight serial port terminal emulator.
29
+
30
+ ## Usage
31
+ The SerialDevice instance may be called with a command string to send to the serial port and optional readback/terminator parameters.
32
+
33
+ ```python
34
+ from sertools import SerialDevice
35
+ ser = SerialDevice()
36
+ ser('HELP')
37
+ ```
38
+
39
+ ## Development
40
+ ```bash
41
+ git clone https://github.com/jbdoar/sertools
42
+ cd sertools
43
+ pip install -e .
44
+ ```
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/sertools/__init__.py
5
+ src/sertools/serial_device.py
6
+ src/sertools.egg-info/PKG-INFO
7
+ src/sertools.egg-info/SOURCES.txt
8
+ src/sertools.egg-info/dependency_links.txt
9
+ src/sertools.egg-info/entry_points.txt
10
+ src/sertools.egg-info/requires.txt
11
+ src/sertools.egg-info/top_level.txt
12
+ src/sertools/scripts/__init__.py
13
+ src/sertools/scripts/sterm.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sterm = sertools.scripts.sterm:main
@@ -0,0 +1,2 @@
1
+ pyserial
2
+ readchar
@@ -0,0 +1 @@
1
+ sertools