morse: fix for #1278
[libsigrokdecode.git] / decoders / morse / pd.py
1 ##
2 ## This file is part of the libsigrokdecode project.
3 ##
4 ## Copyright (C) 2017 Christoph Rackwitz <christoph.rackwitz@rwth-aachen.de>
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10 ##
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 ## GNU General Public License for more details.
15 ##
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, see <http://www.gnu.org/licenses/>.
18 ##
19
20 import sigrokdecode as srd
21
22 def decode_ditdah(s):
23     return tuple({'-': 3, '.': 1}[c] for c in s)
24
25 def encode_ditdah(tpl):
26     return ''.join({1: '.', 3: '-'}[c] for c in tpl)
27
28 # https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1677-1-200910-I!!PDF-E.pdf
29 # Recommendation ITU-R M.1677-1
30 # (10/2009)
31 # International Morse code
32 alphabet = {
33     # 1.1.1 Letters
34     '.-':       'a',
35     '-...':     'b',
36     '-.-.':     'c',
37     '-..':      'd',
38     '.':        'e',
39     '..-..':    'é', # "accented"
40     '..-.':     'f',
41     '--.':      'g',
42     '....':     'h',
43     '..':       'i',
44     '.---':     'j',
45     '-.-':      'k',
46     '.-..':     'l',
47     '--':       'm',
48     '-.':       'n',
49     '---':      'o',
50     '.--.':     'p',
51     '--.-':     'q',
52     '.-.':      'r',
53     '...':      's',
54     '-':        't',
55     '..-':      'u',
56     '...-':     'v',
57     '.--':      'w',
58     '-..-':     'x',
59     '-.--':     'y',
60     '--..':     'z',
61
62     # 1.1.2 Figures
63     '.----':    '1',
64     '..---':    '2',
65     '...--':    '3',
66     '....-':    '4',
67     '.....':    '5',
68     '-....':    '6',
69     '--...':    '7',
70     '---..':    '8',
71     '----.':    '9',
72     '-----':    '0',
73
74     # 1.1.3 Punctuation marks and miscellaneous signs
75     '.-.-.-':   '.',          # Full stop (period)
76     '--..--':   ',',          # Comma
77     '---...':   ':',          # Colon or division sign
78     '..--..':   '?',          # Question mark (note of interrogation or request for repetition of a transmission not understood)
79     '.----.':   '’',          # Apostrophe
80     '-....-':   '-',          # Hyphen or dash or subtraction sign
81     '-..-.':    '/',          # Fraction bar or division sign
82     '-.--.':    '(',          # Left-hand bracket (parenthesis)
83     '-.--.-':   ')',          # Right-hand bracket (parenthesis)
84     '.-..-.':   '“ ”',        # Inverted commas (quotation marks) (before and after the words)
85     '-...-':    '=',          # Double hyphen
86     '...-.':    'UNDERSTOOD', # Understood
87     '........': 'ERROR',      # Error (eight dots)
88     '.-.-.':    '+',          # Cross or addition sign
89     '.-...':    'WAIT',       # Wait
90     '...-.-':   'EOW',        # End of work
91     '-.-.-':    'START',      # Starting signal (to precede every transmission)
92     '.--.-.':   '@',          # Commercial at
93
94     #'-.-':      'ITT',        # K: Invitation to transmit
95
96     # 3.2.1 For the multiplication sign, the signal corresponding to the letter X shall be transmitted.
97     #'-..-':     '×',          # Multiplication sign
98 }
99
100 alphabet = {decode_ditdah(k): v for k, v in alphabet.items()}
101
102 # 2 Spacing and length of the signals (right side is just for printing).
103 symbols = { # level, time units
104     # 2.1 A dash is equal to three dots.
105     (1, 1): '*',
106     (1, 3): '===',
107     # 2.2 The space between the signals forming the same letter is equal to one dot.
108     (0, 1): '_',
109     # 2.3 The space between two letters is equal to three dots.
110     (0, 3): '__',
111     # 2.4 The space between two words is equal to seven dots.
112     (0, 7): '___',
113 }
114
115 class Decoder(srd.Decoder):
116     api_version = 3
117     id = 'morse'
118     name = 'Morse'
119     longname = 'Morse code'
120     desc = 'Demodulated morse code protocol.'
121     license = 'gplv2+'
122     inputs = ['logic']
123     outputs = ['morse']
124     channels = (
125         {'id': 'data', 'name': 'Data', 'desc': 'Data line'},
126     )
127     options = (
128         {'id': 'timeunit', 'desc': 'Time unit (guess)', 'default': 0.1},
129     )
130     annotations = (
131         ('time', 'Time'),
132         ('units', 'Units'),
133         ('symbol', 'Symbol'),
134         ('letter', 'Letter'),
135         ('word', 'Word'),
136     )
137     annotation_rows = tuple((u, v, (i,)) for i, (u, v) in enumerate(annotations))
138
139     def __init__(self):
140         self.reset()
141
142     def reset(self):
143         self.samplerate = None
144
145     def metadata(self, key, value):
146         if key == srd.SRD_CONF_SAMPLERATE:
147             self.samplerate = value
148
149     def start(self):
150         self.out_ann = self.register(srd.OUTPUT_ANN)
151         self.out_binary = self.register(srd.OUTPUT_BINARY)
152
153     def decode_symbols(self):
154         # Annotate symbols, emit symbols, handle timeout via token.
155
156         timeunit = self.options['timeunit']
157
158         self.wait({0: 'r'})
159         prevtime = self.samplenum # Time of an actual edge.
160
161         while True:
162             (val,) = self.wait([{0: 'e'}, {'skip': int(5 * self.samplerate * timeunit)}])
163
164             pval = 1 - val
165             curtime = self.samplenum
166             dt = (curtime - prevtime) / self.samplerate
167             units = dt / timeunit
168             iunits = int(max(1, round(units)))
169             error = abs(units - iunits)
170
171             symbol = (pval, iunits)
172
173             if self.matched[1]:
174                 yield None # Flush word.
175                 continue
176
177             self.put(prevtime, curtime, self.out_ann, [0, ['{:.3g}'.format(dt)]])
178
179             if symbol in symbols:
180                 self.put(prevtime, curtime, self.out_ann, [1, ['{:.1f}*{:.3g}'.format(units, timeunit)]])
181                 yield (prevtime, curtime, symbol)
182             else:
183                 self.put(prevtime, curtime, self.out_ann, [1, ['!! {:.1f}*{:.3g} !!'.format(units, timeunit)]])
184
185             prevtime = curtime
186
187             thisunit = dt / iunits
188             timeunit += (thisunit - timeunit) * 0.2 * max(0, 1 - 2*error) # Adapt.
189
190     def decode_morse(self):
191         # Group symbols into letters.
192         sequence = ()
193         s0 = s1 = None
194
195         for item in self.decode_symbols():
196             do_yield = False
197             if item is not None: # Level + width.
198                 (t0, t1, symbol) = item
199                 (sval, sunits) = symbol
200                 if sval == 1:
201                     if s0 is None:
202                         s0 = t0
203                     s1 = t1
204                     sequence += (sunits,)
205                 else:
206                     # Generate "flush" for end of letter, end of word.
207                     if sunits >= 3:
208                         do_yield = True
209             else:
210                 do_yield = True
211             if do_yield:
212                 if sequence:
213                     yield (s0, s1, alphabet.get(sequence, encode_ditdah(sequence)))
214                     sequence = ()
215                     s0 = s1 = None
216             if item is None:
217                 yield None # Pass through flush of 5+ space.
218
219     def decode(self):
220
221         # Strictly speaking there is no point in running this decoder
222         # when the sample rate is unknown or zero. But the previous
223         # implementation already fell back to a rate of 1 in that case.
224         # We stick with this approach, to not introduce new constraints
225         # for existing use scenarios.
226         if not self.samplerate:
227             self.samplerate = 1.0
228
229         # Annotate letters, group into words.
230         s0 = s1 = None
231         word = ''
232         for item in self.decode_morse():
233             do_yield = False
234
235             if item is not None: # Append letter.
236                 (t0, t1, letter) = item
237                 self.put(t0, t1, self.out_ann, [3, [letter]])
238                 if s0 is None:
239                     s0 = t0
240                 s1 = t1
241                 word += letter
242             else:
243                 do_yield = True
244
245             if do_yield: # Flush of word.
246                 if word:
247                     self.put(s0, s1, self.out_ann, [4, [word]])
248                     word = ''
249                     s0 = s1 = None