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/>.
23 # This hack allows importing local modules also when
24 # __name__ == "__main__"
26 from .MorseDistanceModulator import MorseDistanceModulator
27 from .MorseTranslator import MorseTranslator
29 from MorseDistanceModulator import MorseDistanceModulator
30 from MorseTranslator import MorseTranslator
33 def log_symbol(distance, symbol, extra_info=""):
34 logging.info("distance: %.2f Received \"%s\"%s", distance, symbol,
38 class CallDistanceTransceiver(object):
39 """Transmit Morse messages using the distance between calls.
41 This is basically a pulse-distance modulation (PDM).
43 A RING is a pulse and the Morse symbols are encoded in the pause between
44 the first ring of the previous call and the first ring of a new call.
46 This strategy is very slow but it can even be used with ancient analog
47 modems which don't have call progress notifications for outgoing calls, and
48 make it also hard to count multiple rings in the same call on the receiving
49 side because there is no explicit notification on the receiving side of
50 when the caller ends a calls.
52 For GSM modems, which have a more sophisticate call report signalling,
53 a more efficient encoding can be used (for example using ring counts to
54 encode Morse symbols, i.e. a pulse-length modulation), but for
55 a proof-of-concept, a slow encoding covering the most general setup is
58 Plus, supporting communications between analog modems is cool :)
61 def __init__(self, modem,
62 call_setup_time_min=7, call_setup_time_max=16.5,
63 ring_time_min=4.8, ring_time_max=5.2,
64 add_inter_call_distance=True):
65 """Encode the Morse symbols using the distance between calls.
69 call_setup_time_min: the minimum time between when the transmitter
70 dials the number and the receiver reports RING notifications.
71 call_setup_time_max: the maximum time between when the transmitter
72 dials the number and the receiver reports RING notifications.
73 Waiting this time after dialing ensures that the receiver has
74 received _at_least_ one ring. The default chosen here have been
75 tested to work fine with old analog modems, provided that there
76 is some more distance between two calls, and with Android
78 ring_time_min: the minimum time between two consecutive RINGs
80 ring_time_max: the maximum time between two consecutive RINGs
81 in the same call, the standard interval is about 5 seconds, but
82 line and/or software delays can make it vary. This is needed
83 in order to ignore multiple ring in the same call when multiple
84 RING in the same call are notified, and then be able to
85 discriminate between two different calls.
86 add_inter_call_distance: specify if it is needed to wait an extra
87 fixed time between calls.
91 self.translator = MorseTranslator()
93 self.call_setup_time_max = call_setup_time_max
95 if add_inter_call_distance:
96 # Analog modems don't like to dial a new call immediately after
97 # they hung up the previous one; an extra delay of about one
98 # ring time makes them happy.
99 inter_symbol_distance = ring_time_max
101 inter_symbol_distance = 0.0
103 self.modulator = MorseDistanceModulator(call_setup_time_min,
107 inter_symbol_distance)
109 logging.debug("call setup time between %.2f and %.2f "
110 "--------- dot transmit time: %.2f + %.2f "
111 "receive time: between %.2f and %.2f",
112 self.modulator.period_min,
113 self.modulator.period_max,
114 self.modulator.period_max,
115 self.modulator.dot_time.dist,
116 self.modulator.dot_time.min,
117 self.modulator.dot_time.max)
118 logging.debug("call setup time between %.2f and %.2f "
119 "-------- dash transmit time: %.2f + %.2f "
120 "receive time: between %.2f and %.2f",
121 self.modulator.period_min,
122 self.modulator.period_max,
123 self.modulator.period_max,
124 self.modulator.dash_time.dist,
125 self.modulator.dash_time.min,
126 self.modulator.dash_time.max)
127 logging.debug("call setup time between %.2f and %.2f "
128 "- signalspace transmit time: %.2f + %.2f "
129 "receive time: between %.2f and %.2f",
130 self.modulator.period_min,
131 self.modulator.period_max,
132 self.modulator.period_max,
133 self.modulator.signalspace_time.dist,
134 self.modulator.signalspace_time.min,
135 self.modulator.signalspace_time.max)
136 logging.debug("call setup time between %.2f and %.2f "
137 "--- wordspace transmit time: %.2f + %.2f "
138 "receive time: between %.2f and %.2f",
139 self.modulator.period_min,
140 self.modulator.period_max,
141 self.modulator.period_max,
142 self.modulator.wordspace_time.dist,
143 self.modulator.wordspace_time.min,
144 self.modulator.wordspace_time.max)
145 logging.debug("call setup time between %.2f and %.2f "
146 "--------- EOM transmit time: %.2f + %.2f "
147 "receive time: between %.2f and inf",
148 self.modulator.period_min,
149 self.modulator.period_max,
150 self.modulator.period_max,
151 self.modulator.eom_time.dist,
152 self.modulator.eom_time.min)
154 self.previous_ring_time = -1
155 self.previous_call_time = -1
157 self.morse_message = ""
158 self.text_message = ""
160 self.end_of_message = False
162 def receive_symbol(self):
163 current_ring_time = time.time()
165 if self.previous_ring_time == -1:
166 self.previous_ring_time = current_ring_time
167 self.previous_call_time = current_ring_time
168 log_symbol(0, "", "(The very first ring)")
171 ring_distance = current_ring_time - self.previous_ring_time
172 logging.debug("RINGs distance: %.2f", ring_distance)
173 self.previous_ring_time = current_ring_time
175 # Ignore multiple rings in the same call
176 if self.modulator.is_same_period(ring_distance):
177 logging.debug("multiple rings in the same call, distance: %.2f",
181 call_distance = current_ring_time - self.previous_call_time
182 self.previous_call_time = current_ring_time
185 symbol = self.modulator.distance_to_symbol(call_distance)
186 except ValueError as err:
187 logging.error("%s", err)
188 logging.error("Check the transmitter and receiver parameters")
192 if symbol in [" ", "/", "EOM"]:
193 signal = self.morse_message.strip().split(' ')[-1]
194 character = self.translator.signal_to_character(signal)
195 extra_info = " got \"%s\"" % character
197 log_symbol(call_distance, symbol, extra_info)
200 # Add spaces around the wordspace symbol to make it easier to split
201 # the Morse message in symbols later on
204 self.morse_message += symbol
206 self.end_of_message = True
207 self.previous_ring_time = -1
208 self.previous_call_time = -1
210 def receive_loop(self):
211 while not self.end_of_message:
212 self.modem.get_response("RING")
213 self.receive_symbol()
214 logging.debug("Current message: %s", self.morse_message)
216 self.end_of_message = False
217 self.text_message = self.translator.morse_to_text(self.morse_message)
218 self.morse_message = ""
221 return self.text_message
223 def transmit_symbol(self, destination_number, symbol, sleep_time):
224 logging.info("Dial and wait %.2f = %.2f + %.2f seconds "
225 "(transmitting '%s')",
226 self.call_setup_time_max + sleep_time,
227 self.call_setup_time_max,
231 # Dial, then wait self.call_setup_time_max to make sure the receiver
232 # gets at least one RING, and then hangup and sleep the time needed to
234 time_before = time.time()
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_after = time.time()
241 # Account for possible delays in order to be as adherent as
242 # possible to the nominal total symbol transmission distance.
243 delay = (time_after - time_before) - self.call_setup_time_max
244 logging.debug("Delay %.2f", delay)
246 remaining_sleep_time = sleep_time - delay
247 if remaining_sleep_time < 0:
248 remaining_sleep_time = 0
250 logging.debug("Should sleep %.2f. Will sleep %.2f", sleep_time,
251 remaining_sleep_time)
252 time.sleep(remaining_sleep_time)
254 def transmit(self, message, destination_number):
255 morse_message = self.translator.text_to_morse(message)
256 distances = self.modulator.modulate(morse_message)
258 logging.debug("Starting the transmission")
259 for i, distance in enumerate(distances):
260 # Use 'None' for the last call
261 if i == len(distances) - 1:
264 total_sleep_time = self.call_setup_time_max + distance
265 symbol = self.modulator.distance_to_symbol(total_sleep_time)
267 self.transmit_symbol(destination_number, symbol, distance)
269 def estimate_transmit_duration(self, message):
270 morsemessage = self.translator.text_to_morse(message)
271 logging.debug(morsemessage)
273 distances = self.modulator.modulate(morsemessage)
275 transmitting_time = 0
276 for distance in distances:
277 transmitting_time += self.call_setup_time_max
278 transmitting_time += distance
280 logging.debug("Estimated transmitting time: %.2f seconds",
284 def test_transmit_receive():
285 logging.basicConfig(level=logging.DEBUG)
286 call_setup_time_min = 0
287 call_setup_time_max = 0.01
293 class DummyModem(object):
294 """Always receive a '.', a '/' and then EOM, which results in 'E '."""
299 # A transceiver will be used to get the symbol distance to fake in
300 # get_response, but it will only be assigned _after_ DummyModem has
303 # This placeholder here avoids an attribute-defined-outside-init
304 # warning from pylint.
305 self.transceiver = None
309 def send_command(self, command):
312 def get_response(self, response=None):
313 # pylint: disable=unused-argument
315 setup_time = random.uniform(self.transceiver.modulator.period_min,
316 self.transceiver.modulator.period_max)
318 if self.ring_count == 0:
321 elif self.ring_count == 1:
323 dot_time = self.transceiver.modulator.dot_time.dist
324 time.sleep(setup_time + dot_time)
325 elif self.ring_count == 2:
327 wordspace_time = self.transceiver.modulator.wordspace_time.dist
328 time.sleep(setup_time + wordspace_time)
331 eom_time = self.transceiver.modulator.eom_time.dist
332 time.sleep(setup_time + eom_time)
339 xcv = CallDistanceTransceiver(modem,
340 call_setup_time_min, call_setup_time_max,
341 ring_time_min, ring_time_max, True)
343 modem.transceiver = xcv
345 xcv.transmit("CODEX PARIS", "0")
351 print("Message received!")
352 print("\"%s\"" % xcv.get_text(), flush=True)
355 if __name__ == "__main__":
356 test_transmit_receive()