tsapython 3.0.0__tar.gz → 3.0.1__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.
@@ -1,13 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsapython
3
- Version: 3.0.0
3
+ Version: 3.0.1
4
4
  Summary: An UNOFFICIAL Python API for the tinySA series of devices
5
+ Keywords: tinysa,spectrum analyzer,rf,sdr,serial,ultra
5
6
  Author: LC-Linkous
6
7
  Author-email: LC-Linkous <L.C.Linkous@gmail.com>
7
8
  License-Expression: GPL-2.0-only
8
9
  License-File: LICENSE
9
10
  Classifier: Programming Language :: Python :: 3
10
11
  Classifier: Operating System :: OS Independent
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Classifier: Environment :: Console
11
16
  Requires-Dist: numpy>=2.2.6
12
17
  Requires-Dist: pyserial>=3.5
13
18
  Requires-Dist: matplotlib>=3.10.5 ; extra == 'plotting'
@@ -15,6 +20,8 @@ Requires-Dist: pyqt5 ; sys_platform == 'linux' and extra == 'plotting'
15
20
  Requires-Dist: pytest>=8.0 ; extra == 'test'
16
21
  Requires-Dist: pytest-cov>=5.0 ; extra == 'test'
17
22
  Requires-Python: >=3.10
23
+ Project-URL: Changelog, https://github.com/LC-Linkous/tinySA_python/blob/main/CHANGELOG.md
24
+ Project-URL: Homepage, https://github.com/LC-Linkous/tinySA_python
18
25
  Project-URL: Issues, https://github.com/LC-Linkous/tinySA_python/issues
19
26
  Project-URL: Repository, https://github.com/LC-Linkous/tinySA_python
20
27
  Provides-Extra: plotting
@@ -0,0 +1,97 @@
1
+ [project]
2
+ name = "tsapython"
3
+ version = "3.0.1"
4
+ authors = [
5
+ { name = "LC-Linkous", email = "L.C.Linkous@gmail.com" }
6
+ ]
7
+ description = "An UNOFFICIAL Python API for the tinySA series of devices"
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ keywords = ["tinysa", "spectrum analyzer", "rf", "sdr", "serial", "ultra"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Operating System :: OS Independent",
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Scientific/Engineering",
17
+ "Environment :: Console",
18
+ ]
19
+ dependencies = [
20
+ "numpy>=2.2.6",
21
+ "pyserial>=3.5",
22
+ ]
23
+ license = "GPL-2.0-only"
24
+ license-files = ["LICENSE*"]
25
+
26
+ [project.optional-dependencies]
27
+ plotting = [
28
+ "matplotlib>=3.10.5",
29
+ 'pyqt5 ; platform_system == "Linux"',
30
+ ]
31
+ test = [
32
+ "pytest>=8.0",
33
+ "pytest-cov>=5.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ "Homepage" = "https://github.com/LC-Linkous/tinySA_python"
38
+ "Repository" = "https://github.com/LC-Linkous/tinySA_python"
39
+ "Issues" = "https://github.com/LC-Linkous/tinySA_python/issues"
40
+ "Changelog" = "https://github.com/LC-Linkous/tinySA_python/blob/main/CHANGELOG.md"
41
+
42
+ [build-system]
43
+ requires = ["uv_build>=0.8.11,<0.9.0"]
44
+ build-backend = "uv_build"
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ python_files = ["test_*.py"]
49
+ markers = [
50
+ "hardware: tests that require a physically connected tinySA device (deselect with -m 'not hardware')",
51
+ ]
52
+ # PEP 735 dependency group used by `uv sync` and CI. The [project.optional-dependencies]
53
+ # test extra above is kept for `pip install -e ".[test]"` compatibility.
54
+ [dependency-groups]
55
+ dev = [
56
+ "pytest>=8.0",
57
+ "pytest-cov>=5.0",
58
+ "ruff>=0.6",
59
+ "mypy>=1.11",
60
+ ]
61
+
62
+ [tool.ruff]
63
+ line-length = 100
64
+ target-version = "py310"
65
+
66
+ [tool.ruff.lint]
67
+ # Blocking ruleset: F (pyflakes: real bugs -- unused imports, undefined
68
+ # names), E9 (syntax errors), and W (whitespace hygiene; the pre-existing
69
+ # findings were autofixed when W was enabled). Full E remains a deferred
70
+ # cleanup pass: ~70 findings, mostly line-length (E501) and None/bool
71
+ # comparison style (E711/E712), which touch code lines and deserve their
72
+ # own reviewed commit rather than riding along here.
73
+ select = ["E9", "F", "W"]
74
+
75
+ [tool.mypy]
76
+ # No python_version pin: numpy's bundled stubs use PEP 695 `type` statements,
77
+ # which mypy rejects when the analysis version is pinned below 3.12. mypy runs
78
+ # on CI's 3.12 environment; actual 3.10/3.11 compatibility is enforced by the
79
+ # test matrix, which runs the real interpreter versions.
80
+ files = ["src/tsapython"]
81
+ # The package ships a py.typed marker, which promises downstream type-checkers
82
+ # that the inline annotations are real. These settings are what make that
83
+ # promise true -- every def in the shipped package must be annotated, or CI
84
+ # fails. Tests and examples are deliberately not gated.
85
+ disallow_untyped_defs = true
86
+ disallow_incomplete_defs = true
87
+ check_untyped_defs = true
88
+ no_implicit_optional = true
89
+ warn_redundant_casts = true
90
+ warn_unused_ignores = true
91
+
92
+ [[tool.mypy.overrides]]
93
+ # pyserial ships no type stubs; treat its interface as dynamically typed rather
94
+ # than failing the import.
95
+ module = "serial.*"
96
+ ignore_missing_imports = true
97
+
@@ -7,4 +7,4 @@ __version__ = "2.0.0"
7
7
  # In __init__.py
8
8
  from .core import tinySA
9
9
 
10
- __all__ = ["tinySA"]
10
+ __all__ = ["tinySA"]
@@ -0,0 +1 @@
1
+ # tsapython command mixins (internal). Not a public API.
@@ -11,57 +11,61 @@
11
11
  # Author(s): Lauren Linkous
12
12
  ##--------------------------------------------------------------------------------------------------\
13
13
 
14
- class AcquisitionMixin:
15
- def data(self, val=0):
16
- # dumps the trace data.
14
+ from collections.abc import Iterator
15
+
16
+ from .._host import MixinHost
17
+
18
+ class AcquisitionMixin(MixinHost):
19
+ def data(self, val: int = 0) -> bytearray | None:
20
+ # dumps the trace data.
17
21
  # usage: data [0-2]
18
22
  # 0=temp value, 1=stored trace, 2=measurement
19
23
  # example return: bytearray(b'-8.671875e+01\r\n... -8.337500e+01\r\n-8.237500e+01\r')
20
-
24
+
21
25
  #explicitly allowed vals
22
26
  accepted_vals = [0,1,2]
23
27
  #check input
24
28
  if val in accepted_vals:
25
29
  writebyte = 'data '+str(val)+'\r\n'
26
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
30
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
27
31
  if val == 0:
28
- self.print_message("returning temp value data")
32
+ self.print_message("returning temp value data")
29
33
  elif val == 1:
30
- self.print_message("returning stored trace data")
34
+ self.print_message("returning stored trace data")
31
35
  elif val == 2:
32
- self.print_message("returning measurement data")
36
+ self.print_message("returning measurement data")
33
37
  else:
34
38
  self.print_message("ERROR: data() takes vals [0-2]")
35
39
  msgbytes = self.error_byte_return()
36
40
  return msgbytes
37
41
 
38
- def get_temporary_data(self):
42
+ def get_temporary_data(self) -> bytearray | None:
39
43
  # alias func for data()
40
44
  return self.data(val=0)
41
45
 
42
- def get_stored_trace_data(self):
46
+ def get_stored_trace_data(self) -> bytearray | None:
43
47
  # alias func for data()
44
48
  return self.data(val=1)
45
49
 
46
- def dump_measurement_data(self):
50
+ def dump_measurement_data(self) -> bytearray | None:
47
51
  # alias func for data()
48
52
  return self.data(val=2)
49
53
 
50
- def frequencies(self):
54
+ def frequencies(self) -> bytearray | None:
51
55
  # gets the frequencies used by the last sweep
52
56
  # usage: frequencies
53
57
  # example return: bytearray(b'1500000000\r\n... \r\n3000000000\r')
54
58
 
55
59
  writebyte = 'frequencies\r\n'
56
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
60
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
57
61
  self.print_message("getting frequencies from the last sweep")
58
62
  return msgbytes
59
63
 
60
- def get_last_freqs(self):
64
+ def get_last_freqs(self) -> bytearray | None:
61
65
  # get frequencies of last sweep
62
66
  return self.frequencies()
63
67
 
64
- def hop(self, start, stop, inc, outmask=None):
68
+ def hop(self, start: int, stop: int, inc: int, outmask: int | None = None) -> bytearray | None:
65
69
  # this is a measurement, maybe a sample measurement. format looks like hop freqval integer
66
70
  # usage: hop {start(Hz)} {stop(Hz)} {step(Hz) | points} [outmask]
67
71
  # outmask: 1 is frequency, 2 is level
@@ -74,40 +78,40 @@ class AcquisitionMixin:
74
78
  elif outmask ==None:
75
79
  writebyte = 'hop ' + str(start) + ' ' + str(stop) + ' ' + str(inc) + '\r\n'
76
80
 
77
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
81
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
78
82
  self.print_message("sampling over frequency range")
79
83
  return msgbytes
80
84
 
81
- else:
85
+ else:
82
86
  self.print_message("hop() takes arguments start=Int, stop=Int, inc=Int, outmask=None|Int")
83
87
 
84
88
  return None
85
89
 
86
- def get_sample_pts(self, start, stop, pts):
90
+ def get_sample_pts(self, start: int, stop: int, pts: int) -> bytearray | None:
87
91
  # alias for hop()
88
92
  return self.hop(start, stop, pts, outmask=1)
89
93
 
90
- def pause(self):
94
+ def pause(self) -> bytearray | None:
91
95
  # pauses the sweeping in either input or output mode
92
96
  # usage: pause
93
97
  # example return: ''
94
98
 
95
99
  writebyte = 'pause\r\n'
96
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
100
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
97
101
  self.print_message("pausing tinySA device")
98
- return msgbytes
102
+ return msgbytes
99
103
 
100
- def resume(self):
104
+ def resume(self) -> bytearray | None:
101
105
  # resumes the sweeping in either input or output mode
102
106
  # usage: resume
103
107
  # example return: ''
104
108
 
105
109
  writebyte = 'resume\r\n'
106
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
110
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
107
111
  self.print_message("resuming sweep")
108
- return msgbytes
112
+ return msgbytes
109
113
 
110
- def scan(self, start, stop, pts=250, outmask=None):
114
+ def scan(self, start: int, stop: int, pts: int = 250, outmask: int | None = None) -> bytearray | None:
111
115
  # Performs a scan and optionally outputs the measured data.
112
116
  # usage: scan {start(Hz)} {stop(Hz)} [points] [outmask]
113
117
  # where the outmask is a binary OR of:
@@ -117,21 +121,21 @@ class AcquisitionMixin:
117
121
  if (0<=start) and (start < stop) and (pts <= self.maxPoints):
118
122
  if outmask == None:
119
123
  writebyte = 'scan '+str(start)+' '+str(stop)+' '+str(pts)+'\r\n'
120
- else:
124
+ else:
121
125
  writebyte = 'scan '+str(start)+' '+str(stop)+' '+str(pts)+ ' '+str(outmask)+'\r\n'
122
126
  msgbytes = self.tinySA_serial(writebyte, printBool=False)
123
- self.print_message("scanning...")
127
+ self.print_message("scanning...")
124
128
  else:
125
129
  self.print_message("ERROR: scan takes START STOP PTS OUTMASK as args. Check doc for format and limits")
126
130
  msgbytes = self.error_byte_return()
127
131
  return msgbytes
128
132
 
129
- def scan_raw(self, start, stop, pts=250, unbuf=1):
130
- # performs a scan of unlimited amount of points
133
+ def scan_raw(self, start: int, stop: int, pts: int = 250, unbuf: int = 1) -> bytearray | None:
134
+ # performs a scan of unlimited amount of points
131
135
  # and sends the data in binary form
132
136
  # usage: scanraw {start(Hz)} {stop(Hz)} [points] [unbuffered]
133
137
  # The measured data is sent as:
134
- # '{' ('x' MSB LSB)*points '}'
138
+ # '{' ('x' MSB LSB)*points '}'
135
139
  # where the 16 bit data is scaled by 32 & shifted based on device.
136
140
  # the README has examples for processing
137
141
 
@@ -140,7 +144,7 @@ class AcquisitionMixin:
140
144
  writebyte = 'scanraw '+str(start)+' '+str(stop)+' '+str(pts)+ ' '+str(unbuf)+'\r\n'
141
145
 
142
146
  # write out to serial, get message back, clean up, return
143
- self.print_message("scanning...")
147
+ self.print_message("scanning...")
144
148
  msgbytes = self.tinySA_serial(writebyte, printBool=False, pts=pts) #pts added for error checking
145
149
  return msgbytes
146
150
  else:
@@ -151,7 +155,7 @@ class AcquisitionMixin:
151
155
  msgbytes = self.error_byte_return()
152
156
  return msgbytes
153
157
 
154
- def continuous_scanraw(self, start, stop, pts=250, unbuf=1, count=None):
158
+ def continuous_scanraw(self, start: int, stop: int, pts: int = 250, unbuf: int = 1, count: int | None = None) -> "Iterator[bytearray | None]":
155
159
  # Continuous SCANRAW acquisition.
156
160
  #
157
161
  # The tinySA returns exactly ONE binary frame per scanraw call (it does
@@ -189,26 +193,26 @@ class AcquisitionMixin:
189
193
  yield frame
190
194
  emitted += 1
191
195
 
192
- def config_sweep(self, argName=None, val=None):
196
+ def config_sweep(self, argName: int | float | str | None = None, val: int | float | str | None = None) -> bytearray | None:
193
197
  # split call for SWEEP
194
198
  # Set sweep boundaries.
195
- # Sweep without arguments lists the current sweep
196
- # settings. The frequencies specified should be
197
- # within the permissible range. The sweep commands
198
- # apply both to input and output modes
199
- # usage:
200
- # sweep [(start|stop|center|span|cw {frequency}) |
199
+ # Sweep without arguments lists the current sweep
200
+ # settings. The frequencies specified should be
201
+ # within the permissible range. The sweep commands
202
+ # apply both to input and output modes
203
+ # usage:
204
+ # sweep [(start|stop|center|span|cw {frequency}) |
201
205
  # ({start(Hz)} {stop(Hz)} [0..290])]
202
206
  # EXAMPLES:
203
207
  # sweep start {frequency}: sets the start frequency of the sweep.
204
208
  # sweep stop {frequency}: sets the stop frequency of the sweep.
205
209
  # sweep center {frequency}: sets the center frequency of the sweep.
206
210
  # sweep span {frequency}: sets the span of the sweep.
207
- # sweep cw {frequency}: sets the continuous wave frequency (zero span sweep).
208
- # # example return: b''
211
+ # sweep cw {frequency}: sets the continuous wave frequency (zero span sweep).
212
+ # # example return: b''
209
213
 
210
214
  # explicitly allowed vals
211
- accepted_table_args = ["start", "stop", "center",
215
+ accepted_table_args = ["start", "stop", "center",
212
216
  "span", "cw"]
213
217
 
214
218
  if (argName==None) and (val==None):
@@ -216,7 +220,7 @@ class AcquisitionMixin:
216
220
  writebyte = 'sweep\r\n'
217
221
  msgbytes = self.tinySA_serial(writebyte, printBool=False)
218
222
 
219
- elif (argName in accepted_table_args):
223
+ elif (argName in accepted_table_args):
220
224
  if val == None:
221
225
  #error
222
226
  self.print_message("ERROR: sweep " + str(argName) + " needs a value")
@@ -233,40 +237,41 @@ class AcquisitionMixin:
233
237
 
234
238
  return msgbytes
235
239
 
236
- def get_sweep_params(self):
237
- # alias for config_sweep()
240
+ def get_sweep_params(self) -> bytearray | None:
241
+ # alias for config_sweep()
238
242
  return self.config_sweep()
239
243
 
240
- def set_sweep_start(self, val):
241
- # alias for config_sweep()
244
+ def set_sweep_start(self, val: int | float | str) -> bytearray | None:
245
+ # alias for config_sweep()
242
246
  return self.config_sweep("start", val)
243
247
 
244
- def set_sweep_stop(self, val):
245
- # alias for config_sweep()
248
+ def set_sweep_stop(self, val: int | float | str) -> bytearray | None:
249
+ # alias for config_sweep()
246
250
  return self.config_sweep("stop", val)
247
251
 
248
- def set_sweep_center(self, val):
249
- # alias for config_sweep()
252
+ def set_sweep_center(self, val: int | float | str) -> bytearray | None:
253
+ # alias for config_sweep()
250
254
  return self.config_sweep("center", val)
251
255
 
252
- def set_sweep_span(self, val):
253
- # alias for config_sweep()
256
+ def set_sweep_span(self, val: int | float | str) -> bytearray | None:
257
+ # alias for config_sweep()
254
258
  return self.config_sweep("span", val)
255
259
 
256
- def set_sweep_cw(self, val):
257
- # alias for config_sweep()
258
- return self.config_sweep("cw", val)
260
+ def set_sweep_cw(self, val: int | float | str) -> bytearray | None:
261
+ # alias for config_sweep()
262
+ return self.config_sweep("cw", val)
259
263
 
260
- def run_sweep(self, startVal=None, stopVal=None, pts=250):
264
+ def run_sweep(self, startVal: int | float | str | None = None, stopVal: int | float | str | None = None, pts: int | None = 250) -> bytearray | None:
261
265
  # split call for SWEEP
262
266
  # Execute sweep.
263
- # The frequencies specified should be
264
- # within the permissible range. The sweep commands
265
- # apply both to input and output modes
266
- # usage:
267
- # sweep [(start|stop|center|span|cw {frequency}) |
267
+ # The frequencies specified should be
268
+ # within the permissible range. The sweep commands
269
+ # apply both to input and output modes
270
+ # usage:
271
+ # sweep [(start|stop|center|span|cw {frequency}) |
268
272
  # ({start(Hz)} {stop(Hz)} [0..290])]
269
- # # example return:
273
+ # # example return:
274
+ msgbytes: bytearray | None
270
275
  if (startVal==None) or (stopVal==None):
271
276
  self.print_message("ERROR: sweep start and stop need non-empty values")
272
277
  msgbytes = self.error_byte_return()
@@ -279,56 +284,56 @@ class AcquisitionMixin:
279
284
  writebyte = 'sweep '+str(startVal)+' '+str(stopVal)+' '+str(pts)+'\r\n'
280
285
  msgbytes = self.tinySA_serial(writebyte, printBool=False)
281
286
 
282
- return msgbytes
287
+ return msgbytes
283
288
 
284
- def sweep_time(self, val):
289
+ def sweep_time(self, val: int | float | str) -> bytearray | None:
285
290
  # sets the sweeptime
286
291
  # usage: sweep {time(Seconds)}the time
287
292
  # specified may end in a letter where
288
293
  # m=mili and u=micro
289
294
  # example return: b''
290
-
291
-
295
+
296
+
292
297
  # needs some error checking
293
298
 
294
299
  writebyte = 'sweeptime '+str(val)+'\r\n'
295
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
300
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
296
301
  self.print_message("sweeptime set to " + str(val))
297
302
  return msgbytes
298
303
 
299
- def trigger(self, val, freq=None):
304
+ def trigger(self, val: int | float | str | None, freq: int | float | str | None = None) -> bytearray | None:
300
305
  # sets the trigger type or level
301
306
  # usage: trigger auto|normal|single|{level(dBm)}
302
307
  # the trigger level is always set in dBm and is the only numerical input
303
- # example return:
308
+ # example return:
304
309
  # #explicitly allowed vals
305
- accepted_vals = ["auto", "normal", "single"]
310
+ accepted_vals = ["auto", "normal", "single"]
306
311
 
307
312
  if str(val) in accepted_vals:
308
313
  writebyte = 'trigger ' + str(val) +'\r\n'
309
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
314
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
310
315
  self.print_message("setting trigger to " + str(val))
311
316
  elif val==None and isinstance(freq,int):
312
317
  writebyte = 'trigger ' + str(freq) +'\r\n'
313
- msgbytes = self.tinySA_serial(writebyte, printBool=False)
318
+ msgbytes = self.tinySA_serial(writebyte, printBool=False)
314
319
  self.print_message("setting trigger level (dBm) to " + str(freq))
315
320
  else:
316
321
  self.print_message("ERROR: trigger takes inputs auto|normal|single|{level(dBm)}")
317
322
  msgbytes = self.error_byte_return()
318
323
  return msgbytes
319
324
 
320
- def trigger_auto(self):
325
+ def trigger_auto(self) -> bytearray | None:
321
326
  # alias for trigger
322
327
  return self.trigger("auto")
323
328
 
324
- def trigger_normal(self):
329
+ def trigger_normal(self) -> bytearray | None:
325
330
  # alias for trigger
326
- return self.trigger("normal")
331
+ return self.trigger("normal")
327
332
 
328
- def trigger_single(self):
333
+ def trigger_single(self) -> bytearray | None:
329
334
  # alias for trigger
330
335
  return self.trigger("single")
331
336
 
332
- def trigger_level(self, val):
337
+ def trigger_level(self, val: int | float | str) -> bytearray | None:
333
338
  # alias for trigger
334
- return self.trigger(None, val)
339
+ return self.trigger(None, val)