Skip to content

Commit da515dc

Browse files
committed
Improved codec: coquelet
1 parent 8233c10 commit da515dc

4 files changed

Lines changed: 87 additions & 47 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
405405

406406
#### [Frequency](https://python-codext.readthedocs.io/en/latest/enc/frequency.html)
407407

408-
- [X] `coquelet`: Coquelet 13, radio teleprinter system sending each ITA-2 character as two audio tones (gives their frequencies in Hz)
408+
- [X] `coquelet`: Coquelet 13 and 8 (aka Mk 1 and Mk 2), radio teleprinter systems sending each ITA-2 character as two audio tones (gives their frequencies in Hz)
409409
- [X] `piccolo`: Piccolo I, II (aka Mk 6) and III, radio teleprinter systems sending each ITA-2 character as one or two audio tones (gives their frequencies in Hz)
410410

411411
#### [Hashing](https://python-codext.readthedocs.io/en/latest/enc/hashing.html)

‎docs/pages/enc/frequency.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,25 @@
66

77
### Coquelet
88

9-
[Coquelet](https://www.cryptomuseum.com/ref/coquelet/index.htm) is an MFSK system developed by ACEC in Belgium in the 1950s, used by the armies and police services of Belgium, France, Algeria and other countries. With Coquelet 13 (Mk 1), each character of the ITA-2 (Baudot) code is sent as two tones of 75 ms: the first one, out of the 8 tones of Group I (812 to 1022 Hz, every 30 Hz), gives its bits 3 to 5, while the second one, out of the 4 tones of Group II (1082 to 1172 Hz), gives its bits 1 and 2. A 1052 Hz tone is sent when idle. As with ITA-2, the LTR and FIG codes switch between the letters and the figures shifts.
9+
[Coquelet](https://www.cryptomuseum.com/ref/coquelet/index.htm) is an MFSK system developed by ACEC in Belgium in the 1950s, used by the armies and police services of Belgium, France, Algeria and other countries. With Coquelet 13 (Mk 1), each character of the ITA-2 (Baudot) code is sent as two tones of 75 ms: the first one, out of the 8 tones of Group I (812 to 1022 Hz, every 30 Hz), gives its bits 3 to 5, while the second one, out of the 4 tones of Group II (1082 to 1172 Hz), gives its bits 1 and 2. A 1052 Hz tone is sent when idle. As with ITA-2, the LTR and FIG codes switch between the letters and the figures shifts. With Coquelet 8 (Mk 2), a synchronous system mostly used in Algeria, the characters are sent the same way, but with 8 tones from 773 to 960 Hz (every 26.67 Hz), Group II being made of the last 4 of them, each tone lasting 37.5 or 75 ms ; tones 1 and 8 are sent alternately when idle.
1010

1111
**Codec** | **Conversions** | **Aliases** | **Comment**
1212
:---: | :---: | --- | ---
13-
`coquelet` | text <-> Coquelet 13 tones (Hz) | `coquelet13`, `coquelet-13`, `coquelet-mk1` | letters uppercased ; letters shift assumed at the start ; idle tones ignored when decoding
13+
`coquelet` | text <-> Coquelet tones (Hz) | `coquelet13`, `coquelet-8`, `coquelet_mk1`, `coquelet-mk2`, ... | parametrized ; *variant* belongs to {13 (Mk1), 8 (Mk2)} (default: 13) ; letters uppercased ; letters shift assumed at the start ; idle tones of Coquelet 13 ignored when decoding
1414

1515
!!! note "Character set"
1616

17-
The figures shift follows ITA-2, except for `;` instead of `=` (as with US teleprinters). Coquelet 8 (Mk 2) is not implemented, as its character table is not documented.
17+
The figures shift follows ITA-2, except for `;` instead of `=` (as with US teleprinters). The frequencies of Coquelet 8 are those of [Wavecom's table](https://www.wavecom.ch/content/ext/DecoderOnlineHelp/worddocuments/coquelet8.htm), rounded to the hertz.
18+
19+
!!! warning "Coquelet 8 idle sequence"
20+
21+
The idle sequence of Coquelet 8 (tones 1 and 8 alternately) is made of the same tones as `M`, which receivers tell apart by the timing of this synchronous system: as the text holds no timing, it is decoded as a series of `M`.
1822

1923
```python
2024
>>> codext.encode("SOS 123", "coquelet")
2125
'902 1112 842 1172 902 1112 902 1172 842 1082 932 1082 872 1082 1022 1112'
26+
>>> codext.encode("SOS 123", "coquelet-8")
27+
'853 907 800 960 853 907 853 960 800 880 880 880 826 880 960 907'
2228
>>> codext.decode("1052 872 1172 1052 1022 1112", "coquelet")
2329
'TE'
2430
```

‎src/codext/frequency/coquelet.py‎

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,97 @@
22
"""Coquelet Codec - Coquelet content encoding.
33
44
Coquelet is a Multi Frequency-Shift Keying (MFSK) radio teleprinter system developed by ACEC in Belgium in the 1950s,
5-
used by the armies and police services of Belgium, France, Algeria and other countries. With Coquelet 13 (Mk 1), each
6-
character of the ITA-2 (Baudot) code is sent as a sequence of two tones of 75 ms: the first one, out of the 8 tones of
7-
Group I (812 to 1022 Hz, every 30 Hz), gives its bits 3 to 5, while the second one, out of the 4 tones of Group II
8-
(1082 to 1172 Hz), gives its bits 1 and 2. A 1052 Hz tone is sent when idle. As with ITA-2, the LTR and FIG codes
5+
used by the armies and police services of Belgium, France, Algeria and other countries. Each character of the ITA-2
6+
(Baudot) code is sent as a sequence of two tones: the first one, out of the 8 tones of Group I, gives its bits 3 to 5,
7+
while the second one, out of the 4 tones of Group II, gives its bits 1 and 2. As with ITA-2, the LTR and FIG codes
98
switch between the letters and the figures shifts.
9+
- With Coquelet 13 (Mk 1), the tones of Group I range from 812 to 1022 Hz (every 30 Hz) and those of Group II from 1082
10+
to 1172 Hz, each tone lasting 75 ms ; a 1052 Hz tone is sent when idle.
11+
- With Coquelet 8 (Mk 2), a synchronous system, the tones of Group I are 8 tones from 773 to 960 Hz (every 26.67 Hz),
12+
Group II being made of the last 4 of them, each tone lasting 37.5 or 75 ms ; tones 1 and 8 are sent alternately when
13+
idle.
1014
11-
Encoding gives the frequencies of the tones in Hz, separated by spaces (letters being uppercased and the letters shift
12-
being assumed at the start), while decoding ignores idle tones.
15+
The variant is given with the codec name (e.g. "coquelet-8") ; it defaults to Coquelet 13. Encoding gives the
16+
frequencies of the tones in Hz, separated by spaces (letters being uppercased and the letters shift being assumed at
17+
the start), while decoding ignores the idle tones of Coquelet 13 (the idle sequence of Coquelet 8 being read as "M",
18+
whose tones are the same).
1319
1420
This codec:
1521
- en/decodes strings from str to str
1622
- en/decodes strings from bytes to bytes
1723
- decodes file content to str (read)
1824
- encodes file content from str to bytes (write)
1925
20-
Reference: https://www.cryptomuseum.com/ref/coquelet/index.htm
26+
References: https://www.cryptomuseum.com/ref/coquelet/index.htm (Coquelet 13)
27+
https://www.wavecom.ch/content/ext/DecoderOnlineHelp/worddocuments/coquelet8.htm (Coquelet 8)
2128
"""
2229
from ._ita2 import ita2_decode, ita2_encode
2330
from ..__common__ import *
2431

2532

2633
__examples__ = {
34+
'enc(coquelet-12|coquelet-mk3)': None,
2735
'enc(coquelet|coquelet13|coquelet-13|coquelet_mk1)': {'TEST': "872 1172 1022 1112 902 1112 872 1172"},
36+
'enc(coquelet8|coquelet-8|coquelet_mk2)': {'TEST': "826 960 960 907 853 907 826 960"},
2837
'enc(coquelet)': {'A1': "1022 1082 842 1082 932 1082", '1A': "842 1082 932 1082 812 1082 1022 1082",
2938
'a\x00\n\r': "1022 1082 1022 1172 1022 1142 992 1172", '#': None},
3039
'dec(coquelet)': {'1052 872 1172 1052 1052 1022 1112': "TE", '872 1172 1022': None, '1082 872': None,
3140
'872 1000': None},
32-
'enc-dec(coquelet)': ["", "HELLO WORLD", "COQUELET 13, MK 1 (1950'S): $5 + 3; 8?!", "LINE 1\r\nLINE 2\x07\x05"],
41+
'dec(coquelet8)': {'826 960 960 907': "TE", '773 960 773 960': "MM", '960 773 960': None, '826 773': None},
42+
'enc-dec(coquelet|coquelet8)': ["", "HELLO WORLD", "COQUELET 13, MK 1 (1950'S): $5 + 3; 8?!",
43+
"LINE 1\r\nLINE 2\x07\x05"],
3344
}
34-
__guess__ = ["coquelet"]
45+
__guess__ = ["coquelet", "coquelet8"]
3546

3647

3748
# frequencies of the tones of Group I by bits 3 to 5 of the codes (code >> 2) and of Group II by bits 1 and 2
38-
# (code & 3), that is, tones 8, 4, 7, 6, 3, 5, 2, 1 and 12, 10, 11, 9 of Crypto Museum's table
39-
COQUELET_GROUP1 = [812 + 30 * (t - 1) for t in [8, 4, 7, 6, 3, 5, 2, 1]]
40-
COQUELET_GROUP2 = [1082 + 30 * (t - 9) for t in [12, 10, 11, 9]]
41-
COQUELET_IDLE = 1052
42-
COQUELET_PAIRS = {(str(COQUELET_GROUP1[c >> 2]), str(COQUELET_GROUP2[c & 3])): c for c in range(32)}
43-
44-
45-
def coquelet_encode(text, errors="strict"):
46-
codes = ita2_encode("coquelet", ensure_str(text), errors)
47-
tones = [c if isinstance(c, str) else f"{COQUELET_GROUP1[c >> 2]} {COQUELET_GROUP2[c & 3]}" for c in codes]
48-
return " ".join(tones), len(text)
49-
50-
51-
def coquelet_decode(text, errors="strict"):
52-
_h, r, shift = handle_error("coquelet", errors, decode=True, kind="tone"), "", "letters"
53-
tones = [(i, t) for i, t in enumerate(ensure_str(text).split()) if t != str(COQUELET_IDLE)]
54-
j = 0
55-
while j < len(tones):
56-
(i, t1), t2 = tones[j], tones[j+1][1] if j + 1 < len(tones) else None
57-
# a tone of Group I must be followed by a tone of Group II ; otherwise, skip the first tone
58-
if (t1, t2) not in COQUELET_PAIRS:
59-
r += _h(t1, i, r)
60-
j += 1
61-
continue
62-
c, shift = ita2_decode(COQUELET_PAIRS[t1, t2], shift)
63-
r, j = r + c, j + 2
64-
return r, len(text)
65-
66-
67-
add("coquelet", coquelet_encode, coquelet_decode, r"^coquelet(?:[-_]?(?:13|mk[-_]?1))?$", examples=__examples__,
49+
# (code & 3), that is, tones 8, 4, 7, 6, 3, 5, 2, 1 and 12, 10, 11, 9 of Crypto Museum's table for Coquelet 13, and
50+
# tones 8, 4, 7, 6, 3, 5, 2, 1 and 8, 6, 7, 5 of Wavecom's table for Coquelet 8 (with its rounded frequencies)
51+
COQUELET8_TONES = [773, 800, 826, 853, 880, 907, 933, 960]
52+
COQUELET_VARIANTS = {
53+
13: ([812 + 30 * (t - 1) for t in [8, 4, 7, 6, 3, 5, 2, 1]], [1082 + 30 * (t - 9) for t in [12, 10, 11, 9]], 1052),
54+
8: ([COQUELET8_TONES[t - 1] for t in [8, 4, 7, 6, 3, 5, 2, 1]], [COQUELET8_TONES[t - 1] for t in [8, 6, 7, 5]],
55+
None),
56+
}
57+
COQUELET_NAMES = {'13': 13, 'mk1': 13, '8': 8, 'mk2': 8}
58+
59+
60+
def _variant(variant):
61+
# a bad variant is only reported when coding, as the lookup of the bare codec name must succeed (e.g. when listing
62+
# the encodings)
63+
v = COQUELET_NAMES.get(str(variant or 13).lower().replace("-", "").replace("_", ""))
64+
if v is None:
65+
raise LookupError("Bad parameter for encoding 'coquelet': variant must be 13 (Mk1) or 8 (Mk2)")
66+
g1, g2, idle = COQUELET_VARIANTS[v]
67+
return g1, g2, idle, {(str(g1[c >> 2]), str(g2[c & 3])): c for c in range(32)}
68+
69+
70+
def coquelet_encode(variant=None):
71+
def encode(text, errors="strict"):
72+
g1, g2, _, _ = _variant(variant)
73+
codes = ita2_encode("coquelet", ensure_str(text), errors)
74+
return " ".join(c if isinstance(c, str) else f"{g1[c >> 2]} {g2[c & 3]}" for c in codes), len(text)
75+
return encode
76+
77+
78+
def coquelet_decode(variant=None):
79+
def decode(text, errors="strict"):
80+
_, _, idle, pairs = _variant(variant)
81+
_h, r, shift = handle_error("coquelet", errors, decode=True, kind="tone"), "", "letters"
82+
tones = [(i, t) for i, t in enumerate(ensure_str(text).split()) if t != str(idle)]
83+
j = 0
84+
while j < len(tones):
85+
(i, t1), t2 = tones[j], tones[j+1][1] if j + 1 < len(tones) else None
86+
# a tone of Group I must be followed by a tone of Group II ; otherwise, skip the first tone
87+
if (t1, t2) not in pairs:
88+
r += _h(t1, i, r)
89+
j += 1
90+
continue
91+
c, shift = ita2_decode(pairs[t1, t2], shift)
92+
r, j = r + c, j + 2
93+
return r, len(text)
94+
return decode
95+
96+
97+
add("coquelet", coquelet_encode, coquelet_decode, r"^coquelet(?:[-_]?(\d+|mk[-_]?\d))?$", examples=__examples__,
6898
guess=__guess__, printables_rate=1.)

‎tests/test_manual.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,15 @@ def test_codec_coquelet(self):
4747
self.assertEqual(codecs.decode("872 1172 1000 1022 1112 1082", "coquelet", e), r)
4848
self.assertRaises(ValueError, codecs.decode, "872 1172 1000", "coquelet")
4949
self.assertRaises(ValueError, codecs.decode, "872 1172 1000", "coquelet", "BAD_ERRORS")
50-
# every character of both shifts, with the shifts inserted only when needed
50+
# every character of both shifts with each variant, with the shifts inserted only when needed
5151
from codext.frequency._ita2 import CHARS
52-
for chars in CHARS.values():
53-
text = "".join(c for c in chars if c is not None)
54-
self.assertEqual(codecs.decode(codecs.encode(text, "coquelet"), "coquelet"), text)
52+
for v in ["coquelet", "coquelet-8"]:
53+
for chars in CHARS.values():
54+
text = "".join(c for c in chars if c is not None)
55+
self.assertEqual(codecs.decode(codecs.encode(text, v), v), text)
56+
# the idle sequence of Coquelet 8 has the tones of "M", while a misaligned one is resynchronised
57+
self.assertEqual(codecs.decode("773 960 773 960 826 960", "coquelet8"), "MMT")
58+
self.assertEqual(codecs.decode("960 773 960 826 960", "coquelet8", "ignore"), "MT")
5559
self.assertEqual(len(codecs.encode("A1B2", "coquelet").split()), 2 * 7)
5660

5761
def test_codec_cow(self):

0 commit comments

Comments
 (0)