3 # CallDistanceTransceiver - send and receive Morse using phone calls distance
5 # Copyright (C) 2015 Antonio Ospite <ao2@ao2.it>
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.
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.
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/>.
21 # This hack allows importing local modules also when
22 # __name__ == "__main__"
24 from .MorseDistanceModulator import MorseDistanceModulator
25 from .MorseTranslator import MorseTranslator
27 from MorseDistanceModulator import MorseDistanceModulator
28 from MorseTranslator import MorseTranslator
34 def log_symbol(distance, symbol, extra_info=""):
35 logging.info("distance: %.2f Received \"%s\"%s", distance, symbol,
39 class CallDistanceTransceiver(object):
40 """Transmit Morse messages using the distance between calls.
42 This is basically a pulse-distance modulation (PDM).
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.
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.
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
59 Plus, supporting communications between analog modems is cool :)
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.
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
79 ring_time_min: the minimum time between two consecutive RINGs
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.
92 self.translator = MorseTranslator()
94 self.call_setup_time_max = call_setup_time_max
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
102 inter_symbol_distance = 0
104 self.modulator = MorseDistanceModulator(call_setup_time_min,
108 inter_symbol_distance)
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)
155 self.previous_ring_time = -1
156 self.previous_call_time = -1
158 self.morse_message = ""
159 self.text_message = ""
161 self.end_of_message = False
163 def receive_symbol(self):
164 current_ring_time = time.time()
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)")
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
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",
182 call_distance = current_ring_time - self.previous_call_time
183 self.previous_call_time = current_ring_time
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")
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
198 log_symbol(call_distance, symbol, extra_info)
201 # Add spaces around the wordspace symbol to make it easier to split
202 # the Morse message in symbols later on
205 self.morse_message += symbol
207 self.end_of_message = True
208 self.previous_ring_time = -1
209 self.previous_call_time = -1
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)
217 self.end_of_message = False
218 self.text_message = self.translator.morse_to_text(self.morse_message)
219 self.morse_message = ""
222 return self.text_message
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,
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
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)
241 def transmit(self, message, destination_number):
242 morse_message = self.translator.text_to_morse(message)
243 distances = self.modulator.modulate(morse_message)
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:
251 total_sleep_time = self.call_setup_time_max + distance
252 symbol = self.modulator.distance_to_symbol(total_sleep_time)
254 self.transmit_symbol(destination_number, symbol, distance)
256 def estimate_transmit_duration(self, message):
257 morsemessage = self.translator.text_to_morse(message)
258 logging.debug(morsemessage)
260 distances = self.modulator.modulate(morsemessage)
262 transmitting_time = 0
263 for distance in distances:
264 transmitting_time += self.call_setup_time_max
265 transmitting_time += distance
267 logging.debug("Estimated transmitting time: %.2f seconds",
271 def test_transmit_receive():
272 logging.basicConfig(level=logging.DEBUG)
273 call_setup_time_min = 0
274 call_setup_time_max = 0.01
280 class DummyModem(object):
281 """Always receive a '.', a '/' and then EOM, which results in 'E '."""
286 # Take trasmission times from a transceiver
287 self.transceiver = None
291 def send_command(self, command):
294 def get_response(self, response=None):
295 # pylint: disable=unused-argument
297 setup_time = random.uniform(self.transceiver.modulator.period_min,
298 self.transceiver.modulator.period_max)
300 if self.ring_count == 0:
303 elif self.ring_count == 1:
305 dot_time = self.transceiver.modulator.dot_time.dist
306 time.sleep(setup_time + dot_time)
307 elif self.ring_count == 2:
309 wordspace_time = self.transceiver.modulator.wordspace_time.dist
310 time.sleep(setup_time + wordspace_time)
313 eom_time = self.transceiver.modulator.eom_time.dist
314 time.sleep(setup_time + eom_time)
321 xcv = CallDistanceTransceiver(modem,
322 call_setup_time_min, call_setup_time_max,
323 ring_time_min, ring_time_max, True)
325 modem.transceiver = xcv
327 xcv.transmit("CODEX PARIS", "0")
333 print("Message received!")
334 print("\"%s\"" % xcv.get_text(), flush=True)
337 if __name__ == "__main__":
338 test_transmit_receive()