9c53ba257a210dc35820a45f8cebb95810ab42c6
[SaveMySugar/python3-savemysugar.git] / src / savemysugar / CallDistanceTransceiver.py
1 #!/usr/bin/env python3
2 #
3 # CallDistanceTransceiver - send and receive Morse using phone calls distance
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 # This hack allows importing local modules also when
22 # __name__ == "__main__"
23 try:
24     from .MorseDistanceModulator import MorseDistanceModulator
25     from .MorseTranslator import MorseTranslator
26 except SystemError:
27     from MorseDistanceModulator import MorseDistanceModulator
28     from MorseTranslator import MorseTranslator
29
30 import logging
31 import time
32
33
34 def log_symbol(distance, symbol, extra_info=""):
35     logging.info("distance: %.2f Received \"%s\"%s", distance, symbol,
36                  extra_info)
37
38
39 class CallDistanceTransceiver(object):
40     """Transmit Morse messages using the distance between calls.
41
42     This is basically a pulse-distance modulation (PDM).
43
44     A RING is a pulse and the Morse symbols are encoded in the pause between
45     the first ring of the previous call and the first ring of a new call.
46
47     This strategy is very slow but it can even be used with ancient analog
48     modems which don't have call progress notifications for outgoing calls, and
49     make it also hard to count multiple rings in the same call on the receiving
50     side because there is no explicit notification on the receiving side of
51     when the caller ends a calls.
52
53     For GSM modems, which have a more sophisticate call report signalling,
54     a more efficient encoding can be used (for example using ring counts to
55     encode Morse symbols, i.e. a pulse-length modulation), but for
56     a proof-of-concept, a slow encoding covering the most general setup is
57     fine.
58
59     Plus, supporting communications between analog modems is cool :)
60     """
61
62     def __init__(self, modem,
63                  call_setup_time_min=7, call_setup_time_max=16.5,
64                  ring_time_min=4.8, ring_time_max=5.2,
65                  add_inter_call_distance=True):
66         """Encode the Morse symbols using the distance between calls.
67
68         Args:
69
70             call_setup_time_min: the minimum time between  when the transmitter
71                 dials the number and the receiver reports RING notifications.
72             call_setup_time_max: the maximum time between when the transmitter
73                 dials the number and the receiver reports RING notifications.
74                 Waiting this time after dialing ensures that the receiver has
75                 received _at_least_ one ring. The default chosen here have been
76                 tested to work fine with old analog modems, provided that there
77                 is some more distance between two calls, and with Android
78                 receivers.
79             ring_time_min: the minimum time between two consecutive RINGs
80                 in the same call.
81             ring_time_max: the maximum time between two consecutive RINGs
82                 in the same call, the standard interval is about 5 seconds, but
83                 line and/or software delays can make it vary.  This is needed
84                 in order to ignore multiple ring in the same call when multiple
85                 RING in the same call are notified, and then be able to
86                 discriminate between two different calls.
87             add_inter_call_distance: specify if it is needed to wait an extra
88                 fixed time between calls.
89             """
90
91         self.modem = modem
92         self.translator = MorseTranslator()
93
94         self.call_setup_time_max = call_setup_time_max
95
96         if add_inter_call_distance:
97             # Analog modems don't like to dial a new call immediately after
98             # they hung up the previous one; an extra delay of about one
99             # ring time makes them happy.
100             inter_symbol_distance = ring_time_max
101         else:
102             inter_symbol_distance = 0
103
104         self.modulator = MorseDistanceModulator(call_setup_time_min,
105                                                 call_setup_time_max,
106                                                 ring_time_min,
107                                                 ring_time_max,
108                                                 inter_symbol_distance)
109
110         logging.debug("call setup time between %.2f and %.2f "
111                       "--------- dot transmit time: %.2f + %.2f "
112                       "receive time: between %.2f and %.2f",
113                       self.modulator.period_min,
114                       self.modulator.period_max,
115                       self.modulator.period_max,
116                       self.modulator.dot_time.dist,
117                       self.modulator.dot_time.min,
118                       self.modulator.dot_time.max)
119         logging.debug("call setup time between %.2f and %.2f "
120                       "-------- dash transmit time: %.2f + %.2f "
121                       "receive time: between %.2f and %.2f",
122                       self.modulator.period_min,
123                       self.modulator.period_max,
124                       self.modulator.period_max,
125                       self.modulator.dash_time.dist,
126                       self.modulator.dash_time.min,
127                       self.modulator.dash_time.max)
128         logging.debug("call setup time between %.2f and %.2f "
129                       "- signalspace transmit time: %.2f + %.2f "
130                       "receive time: between %.2f and %.2f",
131                       self.modulator.period_min,
132                       self.modulator.period_max,
133                       self.modulator.period_max,
134                       self.modulator.signalspace_time.dist,
135                       self.modulator.signalspace_time.min,
136                       self.modulator.signalspace_time.max)
137         logging.debug("call setup time between %.2f and %.2f "
138                       "--- wordspace transmit time: %.2f + %.2f "
139                       "receive time: between %.2f and %.2f",
140                       self.modulator.period_min,
141                       self.modulator.period_max,
142                       self.modulator.period_max,
143                       self.modulator.wordspace_time.dist,
144                       self.modulator.wordspace_time.min,
145                       self.modulator.wordspace_time.max)
146         logging.debug("call setup time between %.2f and %.2f "
147                       "--------- EOM transmit time: %.2f + %.2f "
148                       "receive time: between %.2f and inf",
149                       self.modulator.period_min,
150                       self.modulator.period_max,
151                       self.modulator.period_max,
152                       self.modulator.eom_time.dist,
153                       self.modulator.eom_time.min)
154
155         self.previous_ring_time = -1
156         self.previous_call_time = -1
157
158         self.morse_message = ""
159         self.text_message = ""
160
161         self.end_of_message = False
162
163     def receive_symbol(self):
164         current_ring_time = time.time()
165
166         if self.previous_ring_time == -1:
167             self.previous_ring_time = current_ring_time
168             self.previous_call_time = current_ring_time
169             log_symbol(0, "", "(The very first ring)")
170             return
171
172         ring_distance = current_ring_time - self.previous_ring_time
173         logging.debug("RINGs distance: %.2f", ring_distance)
174         self.previous_ring_time = current_ring_time
175
176         # Ignore multiple rings in the same call
177         if self.modulator.is_same_period(ring_distance):
178             logging.debug("multiple rings in the same call, distance: %.2f",
179                           ring_distance)
180             return
181
182         call_distance = current_ring_time - self.previous_call_time
183         self.previous_call_time = current_ring_time
184
185         try:
186             symbol = self.modulator.distance_to_symbol(call_distance)
187         except ValueError as err:
188             logging.error("%s", err)
189             logging.error("Check the transmitter and receiver parameters")
190             return
191
192         extra_info = ""
193         if symbol in [" ", "/", "EOM"]:
194             signal = self.morse_message.strip().split(' ')[-1]
195             character = self.translator.signal_to_character(signal)
196             extra_info = " got \"%s\"" % character
197
198         log_symbol(call_distance, symbol, extra_info)
199
200         if symbol != "EOM":
201             # Add spaces around the wordspace symbol to make it easier to split
202             # the Morse message in symbols later on
203             if symbol == "/":
204                 symbol = " / "
205             self.morse_message += symbol
206         else:
207             self.end_of_message = True
208             self.previous_ring_time = -1
209             self.previous_call_time = -1
210
211     def receive_loop(self):
212         while not self.end_of_message:
213             self.modem.get_response("RING")
214             self.receive_symbol()
215             logging.debug("Current message: %s", self.morse_message)
216
217         self.end_of_message = False
218         self.text_message = self.translator.morse_to_text(self.morse_message)
219         self.morse_message = ""
220
221     def get_text(self):
222         return self.text_message
223
224     def transmit_symbol(self, destination_number, symbol, sleep_time):
225         logging.info("Dial and wait %.2f = %.2f + %.2f seconds "
226                      "(transmitting '%s')",
227                      self.call_setup_time_max + sleep_time,
228                      self.call_setup_time_max,
229                      sleep_time,
230                      symbol)
231
232         # Dial, then wait self.call_setup_time_max to make sure the receiver
233         # gets at least one RING, and then hangup and sleep the time needed to
234         # transmit a symbol.
235         self.modem.send_command("ATDT" + destination_number + ";")
236         time.sleep(self.call_setup_time_max)
237         self.modem.send_command("ATH")
238         self.modem.get_response()
239         time.sleep(sleep_time)
240
241     def transmit(self, message, destination_number):
242         morse_message = self.translator.text_to_morse(message)
243         distances = self.modulator.modulate(morse_message)
244
245         logging.debug("Starting the transmission")
246         for i, distance in enumerate(distances):
247             # Use 'None' for the last call
248             if i == len(distances) - 1:
249                 symbol = None
250             else:
251                 total_sleep_time = self.call_setup_time_max + distance
252                 symbol = self.modulator.distance_to_symbol(total_sleep_time)
253
254             self.transmit_symbol(destination_number, symbol, distance)
255
256     def estimate_transmit_duration(self, message):
257         morsemessage = self.translator.text_to_morse(message)
258         logging.debug(morsemessage)
259
260         distances = self.modulator.modulate(morsemessage)
261
262         transmitting_time = 0
263         for distance in distances:
264             transmitting_time += self.call_setup_time_max
265             transmitting_time += distance
266
267         logging.debug("Estimated transmitting time: %.2f seconds",
268                       transmitting_time)
269
270
271 def test_transmit_receive():
272     logging.basicConfig(level=logging.DEBUG)
273     call_setup_time_min = 0
274     call_setup_time_max = 0.01
275     ring_time_min = 0
276     ring_time_max = 0
277
278     import random
279
280     class DummyModem(object):
281         """Always receive a '.', a '/' and then EOM, which results in 'E '."""
282
283         def __init__(self):
284             self.ring_count = 0
285
286             # Take trasmission times from a transceiver
287             self.transceiver = None
288
289             random.seed(None)
290
291         def send_command(self, command):
292             pass
293
294         def get_response(self, response=None):
295             # pylint: disable=unused-argument
296
297             setup_time = random.uniform(self.transceiver.modulator.period_min,
298                                         self.transceiver.modulator.period_max)
299
300             if self.ring_count == 0:
301                 # dummy ring
302                 pass
303             elif self.ring_count == 1:
304                 # received a '.'
305                 dot_time = self.transceiver.modulator.dot_time.dist
306                 time.sleep(setup_time + dot_time)
307             elif self.ring_count == 2:
308                 # received a '/'
309                 wordspace_time = self.transceiver.modulator.wordspace_time.dist
310                 time.sleep(setup_time + wordspace_time)
311             else:
312                 # received an 'EOM'
313                 eom_time = self.transceiver.modulator.eom_time.dist
314                 time.sleep(setup_time + eom_time)
315
316             self.ring_count += 1
317             self.ring_count %= 4
318
319     modem = DummyModem()
320
321     xcv = CallDistanceTransceiver(modem,
322                                   call_setup_time_min, call_setup_time_max,
323                                   ring_time_min, ring_time_max, True)
324
325     modem.transceiver = xcv
326
327     xcv.transmit("CODEX PARIS", "0")
328
329     while True:
330         modem.ring_count = 0
331         xcv.receive_loop()
332         print()
333         print("Message received!")
334         print("\"%s\"" % xcv.get_text(), flush=True)
335
336
337 if __name__ == "__main__":
338     test_transmit_receive()