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 ---------> transmit time: %.2f "
106 "receive time: between %.2f and %.2f",
108 (self.dot_time - self.symbol_uncertainty),
109 (self.dot_time + self.symbol_uncertainty))
110 logging.debug("dash --------> transmit time: %.2f "
111 "receive time: between %.2f and %.2f",
113 (self.dash_time - self.symbol_uncertainty),
114 (self.dash_time + self.symbol_uncertainty))
115 logging.debug("singalspace -> transmit time: %.2f "
116 "receive time: between %.2f and %.2f",
117 self.signalspace_time,
118 (self.signalspace_time - self.symbol_uncertainty),
119 (self.signalspace_time + self.symbol_uncertainty))
120 logging.debug("wordspace ---> transmit time: %.2f "
121 "receive time: between %.2f and %.2f",
123 (self.wordspace_time - self.symbol_uncertainty),
124 (self.wordspace_time + self.symbol_uncertainty))
125 logging.debug("EOM ---------> transmit time: %.2f "
126 "receive time: between %.2f and inf",
128 (self.eom_time - self.symbol_uncertainty))
130 self.previous_ring_time = -1
131 self.previous_call_time = -1
133 self.morse_message = ""
134 self.text_message = ""
136 self.end_of_message = False
138 def log_symbol(self, distance, symbol, extra_info=""):
139 logging.info("distance: %.2f Received \"%s\" %s", distance, symbol,
142 def receive_character(self):
143 current_ring_time = time.time()
145 if self.previous_ring_time == -1:
146 self.previous_ring_time = current_ring_time
147 self.previous_call_time = current_ring_time
148 self.log_symbol(0, "", "(The very first ring)")
151 ring_distance = current_ring_time - self.previous_ring_time
152 logging.debug("RINGs distance: %.2f", ring_distance)
153 self.previous_ring_time = current_ring_time
155 # Ignore multiple rings in the same call
156 if abs(ring_distance - self.rings_distance) < self.ring_uncertainty:
157 logging.debug("multiple rings in the same call, distance: %.2f",
161 call_distance = current_ring_time - self.previous_call_time
162 self.previous_call_time = current_ring_time
164 if abs(call_distance - self.dot_time) < self.symbol_uncertainty:
165 self.log_symbol(call_distance, '.')
166 self.morse_message += "."
169 if abs(call_distance - self.dash_time) < self.symbol_uncertainty:
170 self.log_symbol(call_distance, '-')
171 self.morse_message += "-"
174 if abs(call_distance - self.signalspace_time) < self.symbol_uncertainty:
175 signal = self.morse_message.strip().split(' ')[-1]
176 character = self.translator.signal_to_character(signal)
177 self.log_symbol(call_distance, ' ', "got \"%s\"" % character)
178 self.morse_message += " "
181 if abs(call_distance - self.wordspace_time) < self.symbol_uncertainty:
182 signal = self.morse_message.strip().split(' ')[-1]
183 character = self.translator.signal_to_character(signal)
184 self.log_symbol(call_distance, '/', "got \"%s\"" % character)
185 self.morse_message += " / "
188 if call_distance >= self.eom_time - self.symbol_uncertainty:
189 signal = self.morse_message.strip().split(' ')[-1]
190 character = self.translator.signal_to_character(signal)
191 self.log_symbol(call_distance, 'EOM', "got \"%s\"" % character)
192 self.end_of_message = True
193 self.previous_ring_time = -1
194 self.previous_call_time = -1
197 # if the code made it up to here, something fishy is going on
198 logging.error("Unexpected distance: %.2f", call_distance)
199 logging.error("Check the transmitter and receiver parameters")
201 def receive_loop(self):
202 while not self.end_of_message:
203 self.modem.get_response("RING")
204 self.receive_character()
205 logging.debug("Current message: %s", self.morse_message)
207 self.end_of_message = False
208 self.text_message = self.translator.morse_to_text(self.morse_message)
209 self.morse_message = ""
212 return self.text_message
214 def transmit_symbol(self, symbol):
216 sleep_time = self.dot_time
218 sleep_time = self.dash_time
220 sleep_time = self.signalspace_time
222 sleep_time = self.wordspace_time
223 elif symbol == "EOM":
224 sleep_time = self.eom_time
226 # To terminate the transmission just call and hangup, with no extra
228 sleep_time = self.call_setup_time
230 logging.info("Dial and wait %.2f seconds (transmitting '%s')",
233 # Dial, then wait self.call_setup_time to make sure the receiver gets
234 # at least one RING, and then hangup and sleep the remaining time
235 self.modem.send_command("ATDT" + self.destination_number + ";")
236 time.sleep(self.call_setup_time)
237 self.modem.send_command("ATH")
238 self.modem.get_response()
239 time.sleep(sleep_time - self.call_setup_time)
241 def transmit_signal(self, signal):
242 logging.debug("Transmitting signal: %s", signal)
243 for symbol in signal:
244 self.transmit_symbol(symbol)
246 def transmit(self, message, destination_number):
247 self.destination_number = destination_number
249 morse_message = self.translator.text_to_morse(message)
250 signals = morse_message.split()
253 logging.debug("Starting the transmission")
254 for i, signal in enumerate(signals):
255 logging.debug("Transmitting '%s' as '%s'", message[i], signal)
256 self.transmit_signal(signal)
258 # Transmit a signal separator only when strictly necessary:
259 # - after the last symbol, we are going to transmit an EOM
260 # anyway, and that will mark the end of the last symbol.
261 # - between words the word separator act as a symbol separator
263 if i != len(signals) - 1 and signals[i + 1] != "/" and signal != "/":
264 self.transmit_symbol(" ")
266 self.transmit_symbol("EOM")
268 # Since the Morse signals are encoded in the distance between calls, an
269 # extra call is needed in order for receiver actually get the EOM and
270 # see that the transmission has terminated.
271 self.transmit_symbol(None)
273 def estimate_transmit_duration(self, message):
274 morsemessage = self.translator.text_to_morse(message)
275 signals = morsemessage.split()
277 logging.debug(signals)
279 transmitting_time = 0
280 for i, signal in enumerate(signals):
281 logging.debug("signal: %s", signal)
283 for symbol in signal:
285 transmitting_time += self.dot_time
287 transmitting_time += self.dash_time
289 transmitting_time += self.wordspace_time
291 if i != len(signals) - 1 and signals[i + 1] != "/" and symbol != "/":
292 transmitting_time += self.signalspace_time
294 transmitting_time += self.eom_time
296 logging.debug("Estimated transmitting time: %.2f seconds",
300 def test_send_receive():
301 logging.basicConfig(level=logging.DEBUG)
303 call_setup_uncertainty = 0.4
305 ring_uncertainty = 0.3
307 class DummyModem(object):
308 """Always receive a '.' and then a '/', which result in 'E '."""
313 def send_command(self, command):
316 def get_response(self, response):
317 if self.ring_count % 2:
319 time.sleep(call_setup_time + (ring_time + ring_uncertainty))
322 time.sleep(call_setup_time + (ring_time + ring_uncertainty) * 4)
326 xcv = CallDistanceTransceiver(DummyModem(), call_setup_time,
327 call_setup_uncertainty, ring_time,
332 if __name__ == "__main__":