CallDistanceTransceiver.py: use a more conservative call_setup_time_max default
[SaveMySugar/python3-savemysugar.git] / src / savemysugar / MorseDistanceModulator.py
1 #!/usr/bin/env python3
2 #
3 # MorseDistanceModulator - represent Morse symbols using Distance Modulation
4 #
5 # Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21 class SymbolTime(object):
22     """
23     In theory the symbol distance (the distance to discriminate symbols) can
24     be arbitrary, and it's only bounded below by the stability of the period
25     time, but in practice it can be necessary to wait a predefined minimum
26     amount of time between periods because of technological limits of the
27     transmitting devices, we call this time the "minimum inter-symbol
28     distance".
29     """
30
31     # pylint: disable=too-few-public-methods
32     def __init__(self, period_min, period_max, multiplier,
33                  min_inter_symbol_distance=0.0):
34         assert multiplier >= 0
35         if (period_min == period_max) and (min_inter_symbol_distance == 0):
36             raise ValueError("If (period_min == period_max) a non-zero",
37                              "min_inter_symbol_distance MUST be specified")
38
39         symbol_distance = 2 * (period_max - period_min)
40         if symbol_distance == 0:
41             symbol_distance = min_inter_symbol_distance
42
43         # The time the transmitter has to wait to disambiguate between this
44         # symbol and a different one.
45         self.dist = min_inter_symbol_distance + symbol_distance * multiplier
46
47         # The minimum time which represents the symbol at the receiving end.
48         self.min = min_inter_symbol_distance + period_min + \
49             symbol_distance * multiplier
50
51         # The maximum time which represents the symbol at the receiving end
52         self.max = min_inter_symbol_distance + period_min + \
53             symbol_distance * (multiplier + 1)
54
55
56 class MorseDistanceModulator(object):
57     def __init__(self, period_min, period_max, pulse_min, pulse_max,
58                  inter_symbol_distance):
59         self.set_parameters(period_min, period_max,
60                             pulse_min, pulse_max,
61                             inter_symbol_distance)
62
63     def set_parameters(self, period_min, period_max, pulse_min, pulse_max,
64                        inter_symbol_distance):
65         self.period_min = period_min
66         self.period_max = period_max
67         self.pulse_min = pulse_min
68         self.pulse_max = pulse_max
69         self.inter_symbol_distance = inter_symbol_distance
70
71         self.dot_time = SymbolTime(period_min,
72                                    period_max, 0,
73                                    inter_symbol_distance)
74         self.dash_time = SymbolTime(period_min,
75                                     period_max, 1,
76                                     inter_symbol_distance)
77         self.signalspace_time = SymbolTime(period_min,
78                                            period_max, 2,
79                                            inter_symbol_distance)
80         self.wordspace_time = SymbolTime(period_min,
81                                          period_max, 3,
82                                          inter_symbol_distance)
83         self.eom_time = SymbolTime(period_min,
84                                    period_max, 4,
85                                    inter_symbol_distance)
86
87     def symbol_to_distance(self, symbol):
88         if symbol == ".":
89             return self.dot_time.dist
90         elif symbol == "-":
91             return self.dash_time.dist
92         elif symbol == " ":
93             return self.signalspace_time.dist
94         elif symbol == "/" or symbol == " / ":
95             return self.wordspace_time.dist
96         elif symbol == "EOM":
97             return self.eom_time.dist
98
99         raise ValueError("Unexpected symbol %s" % symbol)
100
101     def is_same_period(self, distance):
102         return distance > self.pulse_min and distance <= self.pulse_max
103
104     def distance_to_symbol(self, distance):
105         if distance > self.dot_time.min and \
106            distance <= self.dot_time.max:
107             return "."
108
109         if distance > self.dash_time.min and \
110            distance <= self.dash_time.max:
111             return "-"
112
113         if distance > self.signalspace_time.min and \
114            distance <= self.signalspace_time.max:
115             return " "
116
117         if distance > self.wordspace_time.min and \
118            distance <= self.wordspace_time.max:
119             return "/"
120
121         if distance > self.eom_time.min:
122             return "EOM"
123
124         raise ValueError("Unexpected distance %.2f" % distance)
125
126     def modulate(self, morse):
127         signals = morse.split(' ')
128         distances = []
129         for i, signal in enumerate(signals):
130             for symbol in signal:
131                 distances.append(self.symbol_to_distance(symbol))
132
133             # Transmit a signal separator only when strictly necessary.
134             #
135             # Avoid it in these cases:
136             #  - after the last symbol, because EOM is going to ne transmitted
137             #    anyway and that will mark the end of the last symbol.
138             #  - between words, because the word separator act as a symbol
139             #    separator too.
140             if i != len(signals) - 1 and signals[i + 1] != "/" and signal != "/":
141                 distances.append(self.symbol_to_distance(" "))
142
143         distances.append(self.symbol_to_distance("EOM"))
144
145         # Since the Morse symbols are encoded in the distance between periods,
146         # an extra pulse is needed in order for the receiver to actually get
147         # the EOM and see that the transmission has terminated.
148         distances.append(0)
149
150         return distances
151
152
153 def test_modulate():
154     period_min = 7
155     period_max = 15
156     pulse_min = 4.9
157     pulse_max = 5.2
158     min_inter_symbol_distance = pulse_max
159     morse = "... -- ..."
160     modulator = MorseDistanceModulator(period_min, period_max,
161                                        pulse_min, pulse_max,
162                                        min_inter_symbol_distance)
163
164     print(modulator.modulate(morse))
165
166 if __name__ == "__main__":
167     test_modulate()