|
2 | 2 | """Coquelet Codec - Coquelet content encoding. |
3 | 3 |
|
4 | 4 | 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 |
9 | 8 | 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. |
10 | 14 |
|
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). |
13 | 19 |
|
14 | 20 | This codec: |
15 | 21 | - en/decodes strings from str to str |
16 | 22 | - en/decodes strings from bytes to bytes |
17 | 23 | - decodes file content to str (read) |
18 | 24 | - encodes file content from str to bytes (write) |
19 | 25 |
|
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) |
21 | 28 | """ |
22 | 29 | from ._ita2 import ita2_decode, ita2_encode |
23 | 30 | from ..__common__ import * |
24 | 31 |
|
25 | 32 |
|
26 | 33 | __examples__ = { |
| 34 | + 'enc(coquelet-12|coquelet-mk3)': None, |
27 | 35 | '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"}, |
28 | 37 | 'enc(coquelet)': {'A1': "1022 1082 842 1082 932 1082", '1A': "842 1082 932 1082 812 1082 1022 1082", |
29 | 38 | 'a\x00\n\r': "1022 1082 1022 1172 1022 1142 992 1172", '#': None}, |
30 | 39 | 'dec(coquelet)': {'1052 872 1172 1052 1052 1022 1112': "TE", '872 1172 1022': None, '1082 872': None, |
31 | 40 | '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"], |
33 | 44 | } |
34 | | -__guess__ = ["coquelet"] |
| 45 | +__guess__ = ["coquelet", "coquelet8"] |
35 | 46 |
|
36 | 47 |
|
37 | 48 | # 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__, |
68 | 98 | guess=__guess__, printables_rate=1.) |
0 commit comments