qrss-ei 0.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.
- qrss_ei-0.0.1/LICENSE +20 -0
- qrss_ei-0.0.1/PKG-INFO +116 -0
- qrss_ei-0.0.1/README.md +95 -0
- qrss_ei-0.0.1/pyproject.toml +42 -0
- qrss_ei-0.0.1/setup.cfg +4 -0
- qrss_ei-0.0.1/src/qrss_ei/__init__.py +202 -0
- qrss_ei-0.0.1/src/qrss_ei/qrss_paddle.py +69 -0
- qrss_ei-0.0.1/src/qrss_ei/tx.py +93 -0
- qrss_ei-0.0.1/src/qrss_ei/waiting.py +36 -0
- qrss_ei-0.0.1/src/qrss_ei.egg-info/PKG-INFO +116 -0
- qrss_ei-0.0.1/src/qrss_ei.egg-info/SOURCES.txt +15 -0
- qrss_ei-0.0.1/src/qrss_ei.egg-info/dependency_links.txt +1 -0
- qrss_ei-0.0.1/src/qrss_ei.egg-info/entry_points.txt +2 -0
- qrss_ei-0.0.1/src/qrss_ei.egg-info/requires.txt +1 -0
- qrss_ei-0.0.1/src/qrss_ei.egg-info/top_level.txt +1 -0
- qrss_ei-0.0.1/tests/test_arguments.py +98 -0
- qrss_ei-0.0.1/tests/test_counting.py +15 -0
qrss_ei-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 Dr. Andreas Krüger, DJ3EI
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
“Software”), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
qrss_ei-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qrss-ei
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A simple QRSS CW (very slow Morse code) transmit software.
|
|
5
|
+
Author-email: "Dr. Andreas Krüger, DJ3EI" <dj3ei@famsik.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://codeberg.org/omwb/qrss-ei
|
|
8
|
+
Project-URL: Repository, https://codeberg.org/omwb/qrss-ei
|
|
9
|
+
Project-URL: Issues, https://codeberg.org/omwb/qrss-ei/issues
|
|
10
|
+
Keywords: ham,amateur radio
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: morse2sound~=1.0.7
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# qrss-ei: Software to transmit QRSS CW signals
|
|
23
|
+
|
|
24
|
+
## Introduction
|
|
25
|
+
|
|
26
|
+
### What is QRSS CW?
|
|
27
|
+
|
|
28
|
+
Very slow Morse code. In normal Morse code, the beginner's slow speed
|
|
29
|
+
is 60 cpm (characters per minute), which is the same as 12 wpm (words
|
|
30
|
+
per minute). At that speed, the fundamental unit, the "dot length",
|
|
31
|
+
is 100 ms. In QRSS, the dot length is much longer, commonly 3 seconds
|
|
32
|
+
or more. In other words, QRSS is Morse code with a speed of 2 cpm =
|
|
33
|
+
0.4 wpm or less.
|
|
34
|
+
|
|
35
|
+
Normal CW is on-off-keying. QRSS is commonly sent as
|
|
36
|
+
frequency-shift-keying, with the "key engaged" frequency typically
|
|
37
|
+
being 5 Hz above the "key released" frequency.
|
|
38
|
+
|
|
39
|
+
Finally, typical QRSS receiving software assumes that the QRSS
|
|
40
|
+
transmission is repeated a few times each 10 minutes, preferably with
|
|
41
|
+
exactly the same timing. This is used to "integrate" serveral
|
|
42
|
+
transmissions so they can be fished out of the noise better.
|
|
43
|
+
|
|
44
|
+
QRSS is also very much a QRP mode. Typical transceivers put out a few
|
|
45
|
+
100 mW or so, some even less.
|
|
46
|
+
|
|
47
|
+
The waterholes for people interested in this mode seems to be
|
|
48
|
+
|
|
49
|
+
- To read up on it: [Scott Harden + Andy
|
|
50
|
+
resources](https://swharden.com/qrss/plus/)
|
|
51
|
+
- To meet others doing it (and read up some more): [QRSS Knights
|
|
52
|
+
mailing list](https://groups.io/g/qrssknights)
|
|
53
|
+
|
|
54
|
+
### What is this project?
|
|
55
|
+
|
|
56
|
+
This software enables me to transmit QRSS signals.
|
|
57
|
+
|
|
58
|
+
Management summary: This is a programm that talks to
|
|
59
|
+
[rigctld](https://hamlib.sourceforge.net/html/rigctld.1.html) from the
|
|
60
|
+
well-known [hamlib](https://hamlib.github.io/) software package. The
|
|
61
|
+
rig should be set to RTTY mode, which will cause a carrier to be sent
|
|
62
|
+
whenever PTT is pressed. Alternatively, some other such mode can be
|
|
63
|
+
tried (AM and FM would work as well).
|
|
64
|
+
|
|
65
|
+
No actual characters are ever emitted to be sent via RTTY. Also, this
|
|
66
|
+
uses the "data" variety of "PTT", and as now data comes in, AM and FM
|
|
67
|
+
should also send a bare, unmodulated carrier.
|
|
68
|
+
|
|
69
|
+
On each frequency shift, the PTT is briefly released, the frequency
|
|
70
|
+
shifted 5 Hz up or down, and the PTT again pressed.
|
|
71
|
+
|
|
72
|
+
This is strictly a command-line utility. It needs only plain Python
|
|
73
|
+
without any modules that need compilation. It does make use of the
|
|
74
|
+
[morse2sound](https://gitlab.com/4ham/morse2sound) software by the
|
|
75
|
+
same author which already has the code to convert text into Morse code
|
|
76
|
+
characters. But morse2sound is plain Python as well.
|
|
77
|
+
|
|
78
|
+
### What do I use to *receive* QRSS CW?
|
|
79
|
+
|
|
80
|
+
This software does not help you to receive QRSS CW. To do that, I
|
|
81
|
+
presently use [Lopora](https://www.qsl.net/pa2ohh/11lop.htm).
|
|
82
|
+
|
|
83
|
+
#### Installation hints on that:
|
|
84
|
+
|
|
85
|
+
The Lopora instructions address unconventional installation targets.
|
|
86
|
+
On my plain Python installation on my plain Linux Debian Laptop, I
|
|
87
|
+
found I just needed an active [virtual
|
|
88
|
+
environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments)
|
|
89
|
+
with this pre-installation:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pip install numpy pillow pyaudio
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
On my Debian Linux Trixie box, I'm using 16 (pipewire) as the "audio
|
|
96
|
+
device for input". I set the input device actually used via the
|
|
97
|
+
standard Gnome configuration.
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
### Plain Python
|
|
102
|
+
|
|
103
|
+
- Construct a plain Python [virtual
|
|
104
|
+
environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments)
|
|
105
|
+
- activate it
|
|
106
|
+
- `pip install qrss-ei
|
|
107
|
+
|
|
108
|
+
### uv
|
|
109
|
+
|
|
110
|
+
If you use the [uv](https://docs.astral.sh/uv/) Python dependency
|
|
111
|
+
manager:
|
|
112
|
+
|
|
113
|
+
- Construct a uv virtual environment with `uv venv`
|
|
114
|
+
- activate it
|
|
115
|
+
- `uv pip install qrss-ei`
|
|
116
|
+
|
qrss_ei-0.0.1/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# qrss-ei: Software to transmit QRSS CW signals
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
### What is QRSS CW?
|
|
6
|
+
|
|
7
|
+
Very slow Morse code. In normal Morse code, the beginner's slow speed
|
|
8
|
+
is 60 cpm (characters per minute), which is the same as 12 wpm (words
|
|
9
|
+
per minute). At that speed, the fundamental unit, the "dot length",
|
|
10
|
+
is 100 ms. In QRSS, the dot length is much longer, commonly 3 seconds
|
|
11
|
+
or more. In other words, QRSS is Morse code with a speed of 2 cpm =
|
|
12
|
+
0.4 wpm or less.
|
|
13
|
+
|
|
14
|
+
Normal CW is on-off-keying. QRSS is commonly sent as
|
|
15
|
+
frequency-shift-keying, with the "key engaged" frequency typically
|
|
16
|
+
being 5 Hz above the "key released" frequency.
|
|
17
|
+
|
|
18
|
+
Finally, typical QRSS receiving software assumes that the QRSS
|
|
19
|
+
transmission is repeated a few times each 10 minutes, preferably with
|
|
20
|
+
exactly the same timing. This is used to "integrate" serveral
|
|
21
|
+
transmissions so they can be fished out of the noise better.
|
|
22
|
+
|
|
23
|
+
QRSS is also very much a QRP mode. Typical transceivers put out a few
|
|
24
|
+
100 mW or so, some even less.
|
|
25
|
+
|
|
26
|
+
The waterholes for people interested in this mode seems to be
|
|
27
|
+
|
|
28
|
+
- To read up on it: [Scott Harden + Andy
|
|
29
|
+
resources](https://swharden.com/qrss/plus/)
|
|
30
|
+
- To meet others doing it (and read up some more): [QRSS Knights
|
|
31
|
+
mailing list](https://groups.io/g/qrssknights)
|
|
32
|
+
|
|
33
|
+
### What is this project?
|
|
34
|
+
|
|
35
|
+
This software enables me to transmit QRSS signals.
|
|
36
|
+
|
|
37
|
+
Management summary: This is a programm that talks to
|
|
38
|
+
[rigctld](https://hamlib.sourceforge.net/html/rigctld.1.html) from the
|
|
39
|
+
well-known [hamlib](https://hamlib.github.io/) software package. The
|
|
40
|
+
rig should be set to RTTY mode, which will cause a carrier to be sent
|
|
41
|
+
whenever PTT is pressed. Alternatively, some other such mode can be
|
|
42
|
+
tried (AM and FM would work as well).
|
|
43
|
+
|
|
44
|
+
No actual characters are ever emitted to be sent via RTTY. Also, this
|
|
45
|
+
uses the "data" variety of "PTT", and as now data comes in, AM and FM
|
|
46
|
+
should also send a bare, unmodulated carrier.
|
|
47
|
+
|
|
48
|
+
On each frequency shift, the PTT is briefly released, the frequency
|
|
49
|
+
shifted 5 Hz up or down, and the PTT again pressed.
|
|
50
|
+
|
|
51
|
+
This is strictly a command-line utility. It needs only plain Python
|
|
52
|
+
without any modules that need compilation. It does make use of the
|
|
53
|
+
[morse2sound](https://gitlab.com/4ham/morse2sound) software by the
|
|
54
|
+
same author which already has the code to convert text into Morse code
|
|
55
|
+
characters. But morse2sound is plain Python as well.
|
|
56
|
+
|
|
57
|
+
### What do I use to *receive* QRSS CW?
|
|
58
|
+
|
|
59
|
+
This software does not help you to receive QRSS CW. To do that, I
|
|
60
|
+
presently use [Lopora](https://www.qsl.net/pa2ohh/11lop.htm).
|
|
61
|
+
|
|
62
|
+
#### Installation hints on that:
|
|
63
|
+
|
|
64
|
+
The Lopora instructions address unconventional installation targets.
|
|
65
|
+
On my plain Python installation on my plain Linux Debian Laptop, I
|
|
66
|
+
found I just needed an active [virtual
|
|
67
|
+
environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments)
|
|
68
|
+
with this pre-installation:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
pip install numpy pillow pyaudio
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
On my Debian Linux Trixie box, I'm using 16 (pipewire) as the "audio
|
|
75
|
+
device for input". I set the input device actually used via the
|
|
76
|
+
standard Gnome configuration.
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
### Plain Python
|
|
81
|
+
|
|
82
|
+
- Construct a plain Python [virtual
|
|
83
|
+
environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments)
|
|
84
|
+
- activate it
|
|
85
|
+
- `pip install qrss-ei
|
|
86
|
+
|
|
87
|
+
### uv
|
|
88
|
+
|
|
89
|
+
If you use the [uv](https://docs.astral.sh/uv/) Python dependency
|
|
90
|
+
manager:
|
|
91
|
+
|
|
92
|
+
- Construct a uv virtual environment with `uv venv`
|
|
93
|
+
- activate it
|
|
94
|
+
- `uv pip install qrss-ei`
|
|
95
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# I find this documentation helpful when editing this file:
|
|
2
|
+
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
3
|
+
# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
|
4
|
+
|
|
5
|
+
[build-system]
|
|
6
|
+
requires = ["setuptools"]
|
|
7
|
+
build-backend = "setuptools.build_meta"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Dr. Andreas Krüger, DJ3EI", email = "dj3ei@famsik.de"},
|
|
12
|
+
]
|
|
13
|
+
name = "qrss-ei"
|
|
14
|
+
description = "A simple QRSS CW (very slow Morse code) transmit software."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
# If we change this, we can also move up the python version in lint's call to black:
|
|
17
|
+
requires-python = ">=3.12"
|
|
18
|
+
keywords = ["ham", "amateur radio"]
|
|
19
|
+
license = "MIT"
|
|
20
|
+
# https://pypi.org/classifiers/
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
"Topic :: Communications :: Ham Radio"
|
|
27
|
+
]
|
|
28
|
+
version = "0.0.1"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"morse2sound ~= 1.0.7"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
qrss_ei = "qrss_ei:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://codeberg.org/omwb/qrss-ei"
|
|
38
|
+
Repository = "https://codeberg.org/omwb/qrss-ei"
|
|
39
|
+
Issues = "https://codeberg.org/omwb/qrss-ei/issues"
|
|
40
|
+
|
|
41
|
+
[tool.isort]
|
|
42
|
+
profile = "black"
|
qrss_ei-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import collections.abc
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from cw_enc.cw_coder import CWCoder, raise_on_notmorseable_char
|
|
9
|
+
from cw_enc.paddle_samplecount import PaddleSampleCount
|
|
10
|
+
|
|
11
|
+
from qrss_ei.qrss_paddle import QRSS_Paddle
|
|
12
|
+
from qrss_ei.tx import AbstractConnectedTx, connect_to_rigctld_factory
|
|
13
|
+
from qrss_ei.waiting import AbstractWaiter, Waiter
|
|
14
|
+
|
|
15
|
+
QRSS_EI_VERSION = "0.0.1"
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def count_dot_times(intext: str) -> int:
|
|
21
|
+
with PaddleSampleCount(
|
|
22
|
+
dit_samples=2, dah_samples=4, ci_silence_samples=2, iw_silence_samples=4
|
|
23
|
+
) as dot_counter:
|
|
24
|
+
coder = CWCoder(paddle=dot_counter, on_missing=raise_on_notmorseable_char)
|
|
25
|
+
for c in intext:
|
|
26
|
+
coder.code(c)
|
|
27
|
+
return typing.cast(PaddleSampleCount, dot_counter).count()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def inner_inner(
|
|
31
|
+
intext: str,
|
|
32
|
+
dot_length: float,
|
|
33
|
+
period_s: float,
|
|
34
|
+
repeats: int,
|
|
35
|
+
qrg: float,
|
|
36
|
+
deltaf: float,
|
|
37
|
+
connect_to_rigctld: typing.Callable[[], typing.ContextManager[AbstractConnectedTx]],
|
|
38
|
+
waiter: AbstractWaiter,
|
|
39
|
+
duration: float,
|
|
40
|
+
) -> None:
|
|
41
|
+
if period_s < duration:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"A {duration} seconds message does not fit into {period_s} s period."
|
|
44
|
+
)
|
|
45
|
+
start_of_everything_µs = waiter.get_now()
|
|
46
|
+
with connect_to_rigctld() as connected_tx:
|
|
47
|
+
start_for_loop_iteration_µs = waiter.get_now()
|
|
48
|
+
logger.debug(
|
|
49
|
+
"Starting to send at %.2f s into hour",
|
|
50
|
+
start_for_loop_iteration_µs % (3600 * 1000000) * 1e-6,
|
|
51
|
+
)
|
|
52
|
+
for loop_count in range(0, repeats):
|
|
53
|
+
with QRSS_Paddle(
|
|
54
|
+
dot_length, qrg, qrg + deltaf, connected_tx, waiter
|
|
55
|
+
) as paddle:
|
|
56
|
+
coder = CWCoder(paddle=paddle, on_missing=raise_on_notmorseable_char)
|
|
57
|
+
for c in intext:
|
|
58
|
+
coder.code(c)
|
|
59
|
+
if loop_count + 1 < repeats:
|
|
60
|
+
start_for_loop_iteration_µs += round(period_s * 1e6)
|
|
61
|
+
logger.debug(
|
|
62
|
+
"Want to wait until %.2f s into hour",
|
|
63
|
+
start_of_everything_µs % (3600 * 1000000) * 1e-6,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
waiter.wait_until(start_for_loop_iteration_µs)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main_inner(
|
|
70
|
+
intext: str,
|
|
71
|
+
dot_length: float,
|
|
72
|
+
count_only: bool,
|
|
73
|
+
period_seconds: float,
|
|
74
|
+
repeats: int,
|
|
75
|
+
qrg: float,
|
|
76
|
+
deltaf: float,
|
|
77
|
+
host: str,
|
|
78
|
+
port: int,
|
|
79
|
+
) -> None:
|
|
80
|
+
dot_count = count_dot_times(intext)
|
|
81
|
+
duration = dot_count * dot_length
|
|
82
|
+
logger.info(
|
|
83
|
+
'Message "%s" takes %d dot lengths %.1f s to tx.', intext, dot_count, duration
|
|
84
|
+
)
|
|
85
|
+
if count_only:
|
|
86
|
+
pass
|
|
87
|
+
else:
|
|
88
|
+
connect_to_rigctld = connect_to_rigctld_factory(host, port)
|
|
89
|
+
inner_inner(
|
|
90
|
+
intext,
|
|
91
|
+
dot_length,
|
|
92
|
+
period_seconds,
|
|
93
|
+
repeats,
|
|
94
|
+
qrg,
|
|
95
|
+
deltaf,
|
|
96
|
+
connect_to_rigctld,
|
|
97
|
+
Waiter(),
|
|
98
|
+
duration,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def setup_logging(verbose: bool) -> None:
|
|
103
|
+
logging.Formatter.converter = time.gmtime
|
|
104
|
+
logging.basicConfig(
|
|
105
|
+
format="%(asctime)sZ %(levelname)5s: %(message)s",
|
|
106
|
+
level=logging.DEBUG if verbose else logging.INFO,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(
|
|
111
|
+
parse_us: list[str] = sys.argv[1:],
|
|
112
|
+
inner: collections.abc.Callable[
|
|
113
|
+
[str, float, bool, float, int, float, float, str, int],
|
|
114
|
+
None,
|
|
115
|
+
] = main_inner,
|
|
116
|
+
) -> None:
|
|
117
|
+
argp = argparse.ArgumentParser(
|
|
118
|
+
prog="qrss_ei",
|
|
119
|
+
description="Simple software to send QRSS CW transmissions, "
|
|
120
|
+
"at home at https://codeberg.org/omwb/qrss-ei",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
argp.add_argument(
|
|
124
|
+
"--intext", type=str, required=True, help="The text to be sent. Required."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
argp.add_argument(
|
|
128
|
+
"--dotlength",
|
|
129
|
+
type=float,
|
|
130
|
+
default=3.0,
|
|
131
|
+
help="The dot length in seconds, float, default 3.0.",
|
|
132
|
+
)
|
|
133
|
+
argp.add_argument(
|
|
134
|
+
"--count",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="Only analyze the text length, without actually sending anything.",
|
|
137
|
+
)
|
|
138
|
+
argp.add_argument(
|
|
139
|
+
"--period",
|
|
140
|
+
type=float,
|
|
141
|
+
default=10,
|
|
142
|
+
help="After how many minutes to repeat the transmission (if at all), default 10.",
|
|
143
|
+
)
|
|
144
|
+
argp.add_argument(
|
|
145
|
+
"--repeats",
|
|
146
|
+
type=int,
|
|
147
|
+
default=1,
|
|
148
|
+
help="How often to transmit the same text, default 1.",
|
|
149
|
+
)
|
|
150
|
+
argp.add_argument(
|
|
151
|
+
"--qrg",
|
|
152
|
+
type=float,
|
|
153
|
+
default=10139842,
|
|
154
|
+
help="Lower (off) frequency on which you want to transmit, in Hz, default 10139842",
|
|
155
|
+
)
|
|
156
|
+
argp.add_argument(
|
|
157
|
+
"--deltaf",
|
|
158
|
+
type=float,
|
|
159
|
+
default=5,
|
|
160
|
+
help="Delta between off and on frequency, in Hz, default 5",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
argp.add_argument(
|
|
164
|
+
"--host",
|
|
165
|
+
type=str,
|
|
166
|
+
help="Host running rigctld, default 127.0.0.1",
|
|
167
|
+
default="127.0.0.1",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
argp.add_argument(
|
|
171
|
+
"--port",
|
|
172
|
+
type=int,
|
|
173
|
+
help="Port running rigctld, default 4532",
|
|
174
|
+
default=4532,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
argp.add_argument(
|
|
178
|
+
"-v",
|
|
179
|
+
"--verbose",
|
|
180
|
+
action="store_true",
|
|
181
|
+
help="Produce detailed, verbose output telling you what's being done.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
args = argp.parse_args(parse_us)
|
|
185
|
+
|
|
186
|
+
setup_logging(args.verbose)
|
|
187
|
+
|
|
188
|
+
inner(
|
|
189
|
+
args.intext,
|
|
190
|
+
args.dotlength,
|
|
191
|
+
args.count,
|
|
192
|
+
args.period * 60,
|
|
193
|
+
args.repeats,
|
|
194
|
+
args.qrg,
|
|
195
|
+
args.deltaf,
|
|
196
|
+
args.host,
|
|
197
|
+
args.port,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from types import TracebackType
|
|
3
|
+
from typing import Literal, Optional, Type
|
|
4
|
+
|
|
5
|
+
from cw_enc.paddle import Paddle
|
|
6
|
+
|
|
7
|
+
from qrss_ei.tx import AbstractConnectedTx
|
|
8
|
+
from qrss_ei.waiting import AbstractWaiter
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QRSS_Paddle(Paddle):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
dot_length: float,
|
|
17
|
+
qrg_up: float,
|
|
18
|
+
qrg_down: float,
|
|
19
|
+
tx: AbstractConnectedTx,
|
|
20
|
+
waiter: AbstractWaiter,
|
|
21
|
+
):
|
|
22
|
+
self._dot_length_µs = round(dot_length * 1e6)
|
|
23
|
+
self._qrg_down = qrg_down
|
|
24
|
+
self._qrg_up = qrg_up
|
|
25
|
+
self._tx = tx
|
|
26
|
+
self._waiter = waiter
|
|
27
|
+
self._next_end_of_signal_µs = self._waiter.get_now()
|
|
28
|
+
self._start_of_everything = self._next_end_of_signal_µs
|
|
29
|
+
|
|
30
|
+
def _signal(self, µs_down: int, µs_up: int) -> None:
|
|
31
|
+
self._waiter.wait_until(self._next_end_of_signal_µs)
|
|
32
|
+
if 0 < µs_down:
|
|
33
|
+
self._tx.ptt_up()
|
|
34
|
+
self._tx.set_qrg(self._qrg_down)
|
|
35
|
+
self._tx.ptt_down()
|
|
36
|
+
self._waiter.wait_until(self._next_end_of_signal_µs + µs_down)
|
|
37
|
+
self._tx.ptt_up()
|
|
38
|
+
self._tx.set_qrg(self._qrg_up)
|
|
39
|
+
self._tx.ptt_down()
|
|
40
|
+
self._next_end_of_signal_µs += µs_down + µs_up
|
|
41
|
+
|
|
42
|
+
def dit(self) -> None:
|
|
43
|
+
logger.debug(
|
|
44
|
+
"Dit to start at %.2f s into hour",
|
|
45
|
+
(self._next_end_of_signal_µs % (3600 * 1000000)) * 1e-6,
|
|
46
|
+
)
|
|
47
|
+
self._signal(self._dot_length_µs, self._dot_length_µs)
|
|
48
|
+
|
|
49
|
+
def dah(self) -> None:
|
|
50
|
+
logger.debug(
|
|
51
|
+
"Dah to start at %.2f s into hour",
|
|
52
|
+
(self._next_end_of_signal_µs % (3600 * 1000000)) * 1e-6,
|
|
53
|
+
)
|
|
54
|
+
self._signal(self._dot_length_µs * 3, self._dot_length_µs)
|
|
55
|
+
|
|
56
|
+
def csilence(self) -> None:
|
|
57
|
+
self._signal(0, 2 * self._dot_length_µs)
|
|
58
|
+
|
|
59
|
+
def iwsilence(self) -> None:
|
|
60
|
+
self._signal(0, 4 * self._dot_length_µs)
|
|
61
|
+
|
|
62
|
+
def __exit__(
|
|
63
|
+
self,
|
|
64
|
+
exc_type: Optional[Type[BaseException]],
|
|
65
|
+
exc_val: Optional[BaseException],
|
|
66
|
+
exc_tb: Optional[TracebackType],
|
|
67
|
+
) -> Literal[False]:
|
|
68
|
+
self._waiter.wait_until(self._next_end_of_signal_µs)
|
|
69
|
+
return False
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import contextlib
|
|
3
|
+
import socket
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AbstractConnectedTx(abc.ABC):
|
|
8
|
+
@abc.abstractmethod
|
|
9
|
+
def ptt_down(self) -> None:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abc.abstractmethod
|
|
13
|
+
def ptt_up(self) -> None:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abc.abstractmethod
|
|
17
|
+
def set_qrg(self, qrg: float) -> None:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConnectedSocket(typing.Protocol):
|
|
22
|
+
def recv(self, bufsize: int) -> bytes:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def sendall(self, b: bytes) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommunicationWithRigctldError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConnectedTx(AbstractConnectedTx):
|
|
34
|
+
def __init__(self, rigctld_socket: ConnectedSocket) -> None:
|
|
35
|
+
self._rigctld_socket = rigctld_socket
|
|
36
|
+
|
|
37
|
+
def _ptt(self, digit: bytes) -> None:
|
|
38
|
+
self._rigctld_socket.sendall(b";\\set_ptt " + digit + b"\n")
|
|
39
|
+
answer = self._rigctld_socket.recv(1024)
|
|
40
|
+
answer_parts = answer.split(b";")
|
|
41
|
+
if len(answer_parts) != 2:
|
|
42
|
+
raise CommunicationWithRigctldError(
|
|
43
|
+
f'Rigctld did not send expected two ";"-separated parts, found {answer!r}'
|
|
44
|
+
)
|
|
45
|
+
if answer_parts[0] != b"set_ptt: " + digit:
|
|
46
|
+
raise CommunicationWithRigctldError(
|
|
47
|
+
f'Rigctld did not send expected initial "set_ptt: {str(digit, encoding="UTF-8")}", found {answer!r}'
|
|
48
|
+
)
|
|
49
|
+
if answer_parts[1] != b"RPRT 0\n":
|
|
50
|
+
raise CommunicationWithRigctldError(
|
|
51
|
+
f'Rigctld answer did not end with "RPRT 0\\n" as expected, found {answer!r}'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def ptt_down(self) -> None:
|
|
55
|
+
# Set PTT to data (3), so no accidental noise is transmitted.
|
|
56
|
+
self._ptt(b"3")
|
|
57
|
+
|
|
58
|
+
def ptt_up(self) -> None:
|
|
59
|
+
self._ptt(b"0")
|
|
60
|
+
|
|
61
|
+
def set_qrg(self, qrg: float) -> None:
|
|
62
|
+
freq_b = f"{qrg}".encode("UTF-8")
|
|
63
|
+
self._rigctld_socket.sendall(b";\\set_freq " + freq_b + b"\n")
|
|
64
|
+
answer = self._rigctld_socket.recv(1024)
|
|
65
|
+
answer_parts = answer.split(b";")
|
|
66
|
+
if len(answer_parts) != 2:
|
|
67
|
+
raise CommunicationWithRigctldError(
|
|
68
|
+
f'Rigctld did not send expected two ";"-separated parts, found {answer!r}'
|
|
69
|
+
)
|
|
70
|
+
if answer_parts[0] != b"set_freq: " + freq_b:
|
|
71
|
+
raise CommunicationWithRigctldError(
|
|
72
|
+
f'Rigctld did not send expected initial "set_freq {str(freq_b, encoding="UTF-8")}", found {answer!r}'
|
|
73
|
+
)
|
|
74
|
+
if answer_parts[1] != b"RPRT 0\n":
|
|
75
|
+
raise CommunicationWithRigctldError(
|
|
76
|
+
f'Rigctld answer did not end with "RPRT 0\\n" as expected, found {answer!r}'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def connect_to_rigctld_factory(
|
|
81
|
+
host: str, port: int
|
|
82
|
+
) -> typing.Callable[[], typing.ContextManager[ConnectedTx]]:
|
|
83
|
+
|
|
84
|
+
@contextlib.contextmanager
|
|
85
|
+
def connect_to_rigctld() -> typing.Iterator[ConnectedTx]:
|
|
86
|
+
with socket.create_connection((host, port)) as rigctld_connected_socket:
|
|
87
|
+
connected_tx = ConnectedTx(rigctld_connected_socket)
|
|
88
|
+
try:
|
|
89
|
+
yield connected_tx
|
|
90
|
+
finally:
|
|
91
|
+
connected_tx.ptt_up()
|
|
92
|
+
|
|
93
|
+
return connect_to_rigctld
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractWaiter(abc.ABC):
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def get_now(self) -> int:
|
|
11
|
+
"""Return present time in µseconds since epoch"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def wait_until(self, µseconds_since_epoch: int) -> None:
|
|
16
|
+
"""Wait until"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Waiter(AbstractWaiter):
|
|
21
|
+
|
|
22
|
+
def get_now(self) -> int:
|
|
23
|
+
return (time.monotonic_ns() + 500) // 1000
|
|
24
|
+
|
|
25
|
+
def wait_until(self, µseconds_since_epoch: int) -> None:
|
|
26
|
+
now = self.get_now()
|
|
27
|
+
while now < µseconds_since_epoch:
|
|
28
|
+
need_to_sleep = (µseconds_since_epoch - now) * 1e-6
|
|
29
|
+
# logger.debug(
|
|
30
|
+
# "Need to sleep for %.3f seconds, now %d, wake %d",
|
|
31
|
+
# need_to_sleep,
|
|
32
|
+
# now,
|
|
33
|
+
# µseconds_since_epoch,
|
|
34
|
+
# )
|
|
35
|
+
time.sleep(need_to_sleep)
|
|
36
|
+
now = self.get_now()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qrss-ei
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A simple QRSS CW (very slow Morse code) transmit software.
|
|
5
|
+
Author-email: "Dr. Andreas Krüger, DJ3EI" <dj3ei@famsik.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://codeberg.org/omwb/qrss-ei
|
|
8
|
+
Project-URL: Repository, https://codeberg.org/omwb/qrss-ei
|
|
9
|
+
Project-URL: Issues, https://codeberg.org/omwb/qrss-ei/issues
|
|
10
|
+
Keywords: ham,amateur radio
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: morse2sound~=1.0.7
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# qrss-ei: Software to transmit QRSS CW signals
|
|
23
|
+
|
|
24
|
+
## Introduction
|
|
25
|
+
|
|
26
|
+
### What is QRSS CW?
|
|
27
|
+
|
|
28
|
+
Very slow Morse code. In normal Morse code, the beginner's slow speed
|
|
29
|
+
is 60 cpm (characters per minute), which is the same as 12 wpm (words
|
|
30
|
+
per minute). At that speed, the fundamental unit, the "dot length",
|
|
31
|
+
is 100 ms. In QRSS, the dot length is much longer, commonly 3 seconds
|
|
32
|
+
or more. In other words, QRSS is Morse code with a speed of 2 cpm =
|
|
33
|
+
0.4 wpm or less.
|
|
34
|
+
|
|
35
|
+
Normal CW is on-off-keying. QRSS is commonly sent as
|
|
36
|
+
frequency-shift-keying, with the "key engaged" frequency typically
|
|
37
|
+
being 5 Hz above the "key released" frequency.
|
|
38
|
+
|
|
39
|
+
Finally, typical QRSS receiving software assumes that the QRSS
|
|
40
|
+
transmission is repeated a few times each 10 minutes, preferably with
|
|
41
|
+
exactly the same timing. This is used to "integrate" serveral
|
|
42
|
+
transmissions so they can be fished out of the noise better.
|
|
43
|
+
|
|
44
|
+
QRSS is also very much a QRP mode. Typical transceivers put out a few
|
|
45
|
+
100 mW or so, some even less.
|
|
46
|
+
|
|
47
|
+
The waterholes for people interested in this mode seems to be
|
|
48
|
+
|
|
49
|
+
- To read up on it: [Scott Harden + Andy
|
|
50
|
+
resources](https://swharden.com/qrss/plus/)
|
|
51
|
+
- To meet others doing it (and read up some more): [QRSS Knights
|
|
52
|
+
mailing list](https://groups.io/g/qrssknights)
|
|
53
|
+
|
|
54
|
+
### What is this project?
|
|
55
|
+
|
|
56
|
+
This software enables me to transmit QRSS signals.
|
|
57
|
+
|
|
58
|
+
Management summary: This is a programm that talks to
|
|
59
|
+
[rigctld](https://hamlib.sourceforge.net/html/rigctld.1.html) from the
|
|
60
|
+
well-known [hamlib](https://hamlib.github.io/) software package. The
|
|
61
|
+
rig should be set to RTTY mode, which will cause a carrier to be sent
|
|
62
|
+
whenever PTT is pressed. Alternatively, some other such mode can be
|
|
63
|
+
tried (AM and FM would work as well).
|
|
64
|
+
|
|
65
|
+
No actual characters are ever emitted to be sent via RTTY. Also, this
|
|
66
|
+
uses the "data" variety of "PTT", and as now data comes in, AM and FM
|
|
67
|
+
should also send a bare, unmodulated carrier.
|
|
68
|
+
|
|
69
|
+
On each frequency shift, the PTT is briefly released, the frequency
|
|
70
|
+
shifted 5 Hz up or down, and the PTT again pressed.
|
|
71
|
+
|
|
72
|
+
This is strictly a command-line utility. It needs only plain Python
|
|
73
|
+
without any modules that need compilation. It does make use of the
|
|
74
|
+
[morse2sound](https://gitlab.com/4ham/morse2sound) software by the
|
|
75
|
+
same author which already has the code to convert text into Morse code
|
|
76
|
+
characters. But morse2sound is plain Python as well.
|
|
77
|
+
|
|
78
|
+
### What do I use to *receive* QRSS CW?
|
|
79
|
+
|
|
80
|
+
This software does not help you to receive QRSS CW. To do that, I
|
|
81
|
+
presently use [Lopora](https://www.qsl.net/pa2ohh/11lop.htm).
|
|
82
|
+
|
|
83
|
+
#### Installation hints on that:
|
|
84
|
+
|
|
85
|
+
The Lopora instructions address unconventional installation targets.
|
|
86
|
+
On my plain Python installation on my plain Linux Debian Laptop, I
|
|
87
|
+
found I just needed an active [virtual
|
|
88
|
+
environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments)
|
|
89
|
+
with this pre-installation:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pip install numpy pillow pyaudio
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
On my Debian Linux Trixie box, I'm using 16 (pipewire) as the "audio
|
|
96
|
+
device for input". I set the input device actually used via the
|
|
97
|
+
standard Gnome configuration.
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
### Plain Python
|
|
102
|
+
|
|
103
|
+
- Construct a plain Python [virtual
|
|
104
|
+
environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments)
|
|
105
|
+
- activate it
|
|
106
|
+
- `pip install qrss-ei
|
|
107
|
+
|
|
108
|
+
### uv
|
|
109
|
+
|
|
110
|
+
If you use the [uv](https://docs.astral.sh/uv/) Python dependency
|
|
111
|
+
manager:
|
|
112
|
+
|
|
113
|
+
- Construct a uv virtual environment with `uv venv`
|
|
114
|
+
- activate it
|
|
115
|
+
- `uv pip install qrss-ei`
|
|
116
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/qrss_ei/__init__.py
|
|
5
|
+
src/qrss_ei/qrss_paddle.py
|
|
6
|
+
src/qrss_ei/tx.py
|
|
7
|
+
src/qrss_ei/waiting.py
|
|
8
|
+
src/qrss_ei.egg-info/PKG-INFO
|
|
9
|
+
src/qrss_ei.egg-info/SOURCES.txt
|
|
10
|
+
src/qrss_ei.egg-info/dependency_links.txt
|
|
11
|
+
src/qrss_ei.egg-info/entry_points.txt
|
|
12
|
+
src/qrss_ei.egg-info/requires.txt
|
|
13
|
+
src/qrss_ei.egg-info/top_level.txt
|
|
14
|
+
tests/test_arguments.py
|
|
15
|
+
tests/test_counting.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
morse2sound~=1.0.7
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qrss_ei
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import qrss_ei
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_minimal_arguments() -> None:
|
|
7
|
+
def inner_mock(
|
|
8
|
+
intext: str,
|
|
9
|
+
dot_length: float,
|
|
10
|
+
count_only: bool,
|
|
11
|
+
period_seconds: float,
|
|
12
|
+
repeats: int,
|
|
13
|
+
qrg: float,
|
|
14
|
+
deltaf: float,
|
|
15
|
+
host: str,
|
|
16
|
+
port: int,
|
|
17
|
+
) -> None:
|
|
18
|
+
assert intext == "lorem ipsum"
|
|
19
|
+
assert dot_length == 3.0
|
|
20
|
+
assert count_only is False
|
|
21
|
+
assert period_seconds == 600
|
|
22
|
+
assert repeats == 1
|
|
23
|
+
assert qrg == 10139842
|
|
24
|
+
assert deltaf == 5
|
|
25
|
+
|
|
26
|
+
qrss_ei.main(["--intext", "lorem ipsum"], inner_mock)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_many_arguments() -> None:
|
|
30
|
+
def inner_mock(
|
|
31
|
+
intext: str,
|
|
32
|
+
dot_length: float,
|
|
33
|
+
count_only: bool,
|
|
34
|
+
period_minutes: float,
|
|
35
|
+
repeats: int,
|
|
36
|
+
qrg: float,
|
|
37
|
+
deltaf: float,
|
|
38
|
+
host: str,
|
|
39
|
+
port: int,
|
|
40
|
+
) -> None:
|
|
41
|
+
assert intext == "lorem ipsum"
|
|
42
|
+
assert 3.125 == pytest.approx(dot_length)
|
|
43
|
+
assert count_only is True
|
|
44
|
+
assert period_minutes == 17 * 60
|
|
45
|
+
assert repeats == 56
|
|
46
|
+
assert qrg == 10140156
|
|
47
|
+
assert deltaf == 7
|
|
48
|
+
assert host == "globalhost"
|
|
49
|
+
assert port == 42
|
|
50
|
+
|
|
51
|
+
qrss_ei.main(
|
|
52
|
+
[
|
|
53
|
+
"--intext",
|
|
54
|
+
"lorem ipsum",
|
|
55
|
+
"--dotlength",
|
|
56
|
+
"3.125",
|
|
57
|
+
"--count",
|
|
58
|
+
"--host",
|
|
59
|
+
"globalhost",
|
|
60
|
+
"--port",
|
|
61
|
+
"42",
|
|
62
|
+
"--period",
|
|
63
|
+
"17",
|
|
64
|
+
"--repeats",
|
|
65
|
+
"56",
|
|
66
|
+
"--deltaf",
|
|
67
|
+
"7",
|
|
68
|
+
"--qrg",
|
|
69
|
+
"10140156",
|
|
70
|
+
],
|
|
71
|
+
inner_mock,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def dont_call_me(
|
|
76
|
+
intext: str,
|
|
77
|
+
dot_length: float,
|
|
78
|
+
count_only: bool,
|
|
79
|
+
period_minutes: float,
|
|
80
|
+
repeats: int,
|
|
81
|
+
qrg: float,
|
|
82
|
+
deltaf: float,
|
|
83
|
+
host: str,
|
|
84
|
+
port: int,
|
|
85
|
+
) -> None:
|
|
86
|
+
pytest.fail("This should not be called.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_complain_missing_arguments() -> None:
|
|
90
|
+
|
|
91
|
+
with pytest.raises(SystemExit, check=lambda ex: ex.code == 2):
|
|
92
|
+
qrss_ei.main([], dont_call_me)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_help() -> None:
|
|
96
|
+
|
|
97
|
+
with pytest.raises(SystemExit, check=lambda ex: ex.code == 0):
|
|
98
|
+
qrss_ei.main(["--help"], dont_call_me)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import qrss_ei
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_paris_length() -> None:
|
|
7
|
+
assert qrss_ei.count_dot_times("paris ") == 50
|
|
8
|
+
assert qrss_ei.count_dot_times("paris " * 12) == 600
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_cant_express_in_morse_code() -> None:
|
|
12
|
+
with pytest.raises(
|
|
13
|
+
KeyError, match='Character "!" not representable in Morse code.'
|
|
14
|
+
):
|
|
15
|
+
qrss_ei.count_dot_times("!")
|