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 MorseTranslator to be imported also when
22 # __name__ == "__main__"
24 from .MorseTranslator import MorseTranslator
26 from MorseTranslator import MorseTranslator
32 class CallDistanceTransceiver(object):
33 """Transmit Morse messages using the distance between calls.
35 This is basically a pulse-distance modulation (PDM).
37 A RING is a pulse and the Morse symbols are encoded in the pause between
38 the first ring of the previous call and the first ring of a new call.
40 This strategy is very slow but it can even be used with ancient analog
41 modems which don't have call progress notifications for outgoing calls, and
42 make it also hard to count multiple rings in the same call on the receiving
43 side because there is no explicit notification on the receiving side of
44 when the caller ends a calls.
46 For GSM modems, which have a more sophisticate call report signalling,
47 a more efficient encoding can be used (for example using ring counts to
48 encode Morse symbols, i.e. a pulse-length modulation), but for
49 a proof-of-concept, a slow encoding covering the most general setup is
52 Plus, supporting communications between analog modems is cool :)
55 def __init__(self, modem,
56 call_setup_average=8.5, call_setup_uncertainty=1.5,
57 ring_distance_average=5, ring_uncertainty=0.2):
58 """Encode the Morse symbols using the distance between calls.
61 call_setup_average: the time between when the transmitter dials
62 the number and the receiver reports RING notifications. This is
63 needed in order to be sure than the receiver has received
64 _at_least_ one ring. The default chosen here should work fine
65 even with old analog modems, provided that there is some more
66 distance between two calls.
67 call_setup_uncertainty: uncertainty of call_setup_average,
69 ring_distance_average: the time between two consecutive RINGs
70 in the same call, the standard interval is about 5 seconds, but
71 some tolerance is needed to account for line delays.
72 This is needed in order to ignore rings in the same call, and
73 differentiate two different calls.
74 ring_uncertainty: uncertainty of ring_distance_average, a tolerance
79 self.translator = MorseTranslator()
81 self.destination_number = ""
83 self.call_setup_time = call_setup_average + call_setup_uncertainty
84 self.rings_distance = ring_distance_average + ring_uncertainty
86 # In theory the symbol distance, the distance between calls which
87 # represent symbols, can be arbitrary, but in practice it's better to
88 # wait at least the duration of a ring between terminating one call and
89 # initiating the next call, as pick-up and hang-up can take some time
90 # with old analog modems.
91 symbol_distance = self.rings_distance
93 def symbol_time(multiplier):
94 return self.call_setup_time + symbol_distance * multiplier
96 self.dot_time = symbol_time(1)
97 self.dash_time = symbol_time(2)
98 self.signalspace_time = symbol_time(3)
99 self.wordspace_time = symbol_time(4)
100 self.eom_time = symbol_time(5)
102 self.ring_uncertainty = ring_uncertainty
103 self.symbol_uncertainty = symbol_distance / 2.
105 logging.debug("dot time: transmit: %.2f receive: (%.2f, %.2f)",
107 (self.dot_time - self.symbol_uncertainty),
108 (self.dot_time + self.symbol_uncertainty))
109 logging.debug("dash time: transmit: %.2f receive: (%.2f, %.2f)",
111 (self.dash_time - self.symbol_uncertainty),
112 (self.dash_time + self.symbol_uncertainty))
113 logging.debug("signalspace time: transmit: %.2f receive: (%.2f, %.2f)",
114 self.signalspace_time,
115 (self.signalspace_time - self.symbol_uncertainty),
116 (self.signalspace_time + self.symbol_uncertainty))
117 logging.debug("wordspace time: transmit: %.2f receive: (%.2f, %.2f)",
119 (self.wordspace_time - self.symbol_uncertainty),
120 (self.wordspace_time + self.symbol_uncertainty))
121 logging.debug("EOM time: transmit: %.2f receive: (%.2f, +inf)",
123 (self.eom_time - self.symbol_uncertainty))
125 self.previous_ring_time = -1
126 self.previous_call_time = -1
128 self.morse_message = ""
129 self.text_message = ""
131 self.end_of_message = False
133 def log_symbol(self, distance, symbol, extra_info=""):
134 logging.info("distance: %.2f Received \"%s\" %s", distance, symbol,
137 def receive_character(self):
138 current_ring_time = time.time()
140 if self.previous_ring_time == -1:
141 self.previous_ring_time = current_ring_time
142 self.previous_call_time = current_ring_time
143 self.log_symbol(0, "", "(The very first ring)")
146 ring_distance = current_ring_time - self.previous_ring_time
147 logging.debug("RINGs distance: %.2f", ring_distance)
148 self.previous_ring_time = current_ring_time
150 # Ignore multiple rings in the same call
151 if abs(ring_distance - self.rings_distance) < self.ring_uncertainty:
152 logging.debug("multiple rings in the same call, distance: %.2f",
156 call_distance = current_ring_time - self.previous_call_time
157 self.previous_call_time = current_ring_time
159 if abs(call_distance - self.dot_time) < self.symbol_uncertainty:
160 self.log_symbol(call_distance, '.')
161 self.morse_message += "."
164 if abs(call_distance - self.dash_time) < self.symbol_uncertainty:
165 self.log_symbol(call_distance, '-')
166 self.morse_message += "-"
169 if abs(call_distance - self.signalspace_time) < self.symbol_uncertainty:
170 signal = self.morse_message.strip().split(' ')[-1]
171 character = self.translator.signal_to_character(signal)
172 self.log_symbol(call_distance, ' ', "got \"%s\"" % character)
173 self.morse_message += " "
176 if abs(call_distance - self.wordspace_time) < self.symbol_uncertainty:
177 signal = self.morse_message.strip().split(' ')[-1]
178 character = self.translator.signal_to_character(signal)
179 self.log_symbol(call_distance, '/', "got \"%s\"" % character)
180 self.morse_message += " / "
183 if call_distance >= self.eom_time - self.symbol_uncertainty:
184 signal = self.morse_message.strip().split(' ')[-1]
185 character = self.translator.signal_to_character(signal)
186 self.log_symbol(call_distance, 'EOM', "got \"%s\"" % character)
187 self.end_of_message = True
188 self.previous_ring_time = -1
189 self.previous_call_time = -1
192 # if the code made it up to here, something fishy is going on
193 logging.error("Unexpected distance: %.2f", call_distance)
194 logging.error("Check the transmitter and receiver parameters")
196 def receive_loop(self):
197 while not self.end_of_message:
198 self.modem.get_response("RING")
199 self.receive_character()
200 logging.debug("Current message: %s", self.morse_message)
202 self.end_of_message = False
203 self.text_message = self.translator.morse_to_text(self.morse_message)
204 self.morse_message = ""
207 return self.text_message
209 def transmit_symbol(self, symbol):
211 sleep_time = self.dot_time
213 sleep_time = self.dash_time
215 sleep_time = self.signalspace_time
217 sleep_time = self.wordspace_time
218 elif symbol == "EOM":
219 sleep_time = self.eom_time
221 # To terminate the transmission just call and hangup, with no extra
223 sleep_time = self.call_setup_time
225 logging.info("Dial and wait %.2f seconds (transmitting '%s')",
228 # Dial, then wait self.call_setup_time to make sure the receiver gets
229 # at least one RING, and then hangup and sleep the remaining time
230 self.modem.send_command("ATDT" + self.destination_number + ";")
231 time.sleep(self.call_setup_time)
232 self.modem.send_command("ATH")
233 self.modem.get_response()
234 time.sleep(sleep_time - self.call_setup_time)
236 def transmit_signal(self, signal):
237 logging.debug("Transmitting signal: %s", signal)
238 for symbol in signal:
239 self.transmit_symbol(symbol)
241 def transmit(self, message, destination_number):
242 self.destination_number = destination_number
244 morse_message = self.translator.text_to_morse(message)
245 signals = morse_message.split()
248 logging.debug("Starting the transmission")
249 for i, signal in enumerate(signals):
250 logging.debug("Transmitting '%s' as '%s'", message[i], signal)
251 self.transmit_signal(signal)
253 # Transmit a signal separator only when strictly necessary:
254 # - after the last symbol, we are going to transmit an EOM
255 # anyway, and that will mark the end of the last symbol.
256 # - between words the word separator act as a symbol separator
258 if i != len(signals) - 1 and signals[i + 1] != "/" and signal != "/":
259 self.transmit_symbol(" ")
261 self.transmit_symbol("EOM")
263 # Since the Morse signals are encoded in the distance between calls, an
264 # extra call is needed in order for receiver actually get the EOM and
265 # see that the transmission has terminated.
266 self.transmit_symbol(None)
268 def estimate_transmit_duration(self, message):
269 morsemessage = self.translator.text_to_morse(message)
270 signals = morsemessage.split()
272 logging.debug(signals)
274 transmitting_time = 0
275 for i, signal in enumerate(signals):
276 logging.debug("signal: %s", signal)
278 for symbol in signal:
280 transmitting_time += self.dot_time
282 transmitting_time += self.dash_time
284 transmitting_time += self.wordspace_time
286 if i != len(signals) - 1 and signals[i + 1] != "/" and symbol != "/":
287 transmitting_time += self.signalspace_time
289 transmitting_time += self.eom_time
291 logging.debug("Estimated transmitting time: %.2f seconds",
295 def test_send_receive():
296 logging.basicConfig(level=logging.DEBUG)
298 call_setup_uncertainty = 0.4
300 ring_uncertainty = 0.3
302 class DummyModem(object):
303 """Always receive a '.' and then a '/', which result in 'E '."""
308 def send_command(self, command):
311 def get_response(self, response):
312 if self.ring_count % 2:
314 time.sleep(call_setup_time + (ring_time + ring_uncertainty))
317 time.sleep(call_setup_time + (ring_time + ring_uncertainty) * 4)
321 xcv = CallDistanceTransceiver(DummyModem(), call_setup_time,
322 call_setup_uncertainty, ring_time,
327 if __name__ == "__main__":