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 :)
56 def __init__(self, modem,
57 call_setup_average=8.5, call_setup_uncertainty=1.5,
58 ring_distance_average=5, ring_uncertainty=0.2):
59 """Encode the Morse symbols using the distance between calls.
62 call_setup_average: the time between when the transmitter dials
63 the number and the receiver reports RING notifications. This is
64 needed in order to be sure than the receiver has received
65 _at_least_ one ring. The default chosen here should work fine
66 even with old analog modems, provided that there is some more
67 distance between two calls.
68 call_setup_uncertainty: uncertainty of call_setup_average,
70 ring_distance_average: the time between two consecutive RINGs
71 in the same call, the standard interval is about 5 seconds, but
72 some tolerance is needed to account for line delays.
73 This is needed in order to ignore rings in the same call, and
74 differentiate two different calls.
75 ring_uncertainty: uncertainty of ring_distance_average, a tolerance
80 self.translator = MorseTranslator()
82 self.destination_number = ""
84 self.call_setup_time = call_setup_average + call_setup_uncertainty
85 self.rings_distance = ring_distance_average + ring_uncertainty
87 # In theory the symbol distance, the distance between calls which
88 # represent symbols, can be arbitrary, but in practice it's better to
89 # wait at least the duration of a ring between terminating one call and
90 # initiating the next call, as pick-up and hang-up can take some time
91 # with old analog modems.
92 symbol_distance = self.rings_distance
94 def symbol_time(multiplier):
95 return self.call_setup_time + symbol_distance * multiplier
97 self.dot_time = symbol_time(1)
98 self.dash_time = symbol_time(2)
99 self.signalspace_time = symbol_time(3)
100 self.wordspace_time = symbol_time(4)
101 self.eom_time = symbol_time(5)
103 self.ring_uncertainty = ring_uncertainty
104 self.symbol_uncertainty = symbol_distance / 2.
106 logging.debug("dot time: transmit: %.2f receive: (%.2f, %.2f)",
108 (self.dot_time - self.symbol_uncertainty),
109 (self.dot_time + self.symbol_uncertainty))
110 logging.debug("dash time: transmit: %.2f receive: (%.2f, %.2f)",
112 (self.dash_time - self.symbol_uncertainty),
113 (self.dash_time + self.symbol_uncertainty))
114 logging.debug("signalspace time: transmit: %.2f receive: (%.2f, %.2f)",
115 self.signalspace_time,
116 (self.signalspace_time - self.symbol_uncertainty),
117 (self.signalspace_time + self.symbol_uncertainty))
118 logging.debug("wordspace time: transmit: %.2f receive: (%.2f, %.2f)",
120 (self.wordspace_time - self.symbol_uncertainty),
121 (self.wordspace_time + self.symbol_uncertainty))
122 logging.debug("EOM time: transmit: %.2f receive: (%.2f, +inf)",
124 (self.eom_time - self.symbol_uncertainty))
126 self.previous_ring_time = -1
127 self.previous_call_time = -1
129 self.morse_message = ""
130 self.text_message = ""
132 self.end_of_message = False
134 def log_symbol(self, distance, symbol, extra_info=""):
135 logging.info("distance: %.2f Received \"%s\" %s", distance, symbol,
138 def receive_character(self):
139 current_ring_time = time.time()
141 if self.previous_ring_time == -1:
142 self.previous_ring_time = current_ring_time
143 self.previous_call_time = current_ring_time
144 self.log_symbol(0, "", "(The very first ring)")
147 ring_distance = current_ring_time - self.previous_ring_time
148 logging.debug("RINGs distance: %.2f", ring_distance)
149 self.previous_ring_time = current_ring_time
151 # Ignore multiple rings in the same call
152 if abs(ring_distance - self.rings_distance) < self.ring_uncertainty:
153 logging.debug("multiple rings in the same call, distance: %.2f",
157 call_distance = current_ring_time - self.previous_call_time
158 self.previous_call_time = current_ring_time
160 if abs(call_distance - self.dot_time) < self.symbol_uncertainty:
161 self.log_symbol(call_distance, '.')
162 self.morse_message += "."
165 if abs(call_distance - self.dash_time) < self.symbol_uncertainty:
166 self.log_symbol(call_distance, '-')
167 self.morse_message += "-"
170 if abs(call_distance - self.signalspace_time) < self.symbol_uncertainty:
171 signal = self.morse_message.strip().split(' ')[-1]
172 character = self.translator.signal_to_character(signal)
173 self.log_symbol(call_distance, ' ', "got \"%s\"" % character)
174 self.morse_message += " "
177 if abs(call_distance - self.wordspace_time) < self.symbol_uncertainty:
178 signal = self.morse_message.strip().split(' ')[-1]
179 character = self.translator.signal_to_character(signal)
180 self.log_symbol(call_distance, '/', "got \"%s\"" % character)
181 self.morse_message += " / "
184 if call_distance >= self.eom_time - self.symbol_uncertainty:
185 signal = self.morse_message.strip().split(' ')[-1]
186 character = self.translator.signal_to_character(signal)
187 self.log_symbol(call_distance, 'EOM', "got \"%s\"" % character)
188 self.end_of_message = True
189 self.previous_ring_time = -1
190 self.previous_call_time = -1
193 # if the code made it up to here, something fishy is going on
194 logging.error("Unexpected distance: %.2f", ring_distance)
195 logging.error("Check the transmitter and receiver parameters")
197 def receive_loop(self):
198 while not self.end_of_message:
199 self.modem.get_response("RING")
200 self.receive_character()
201 logging.debug("Current message: %s", self.morse_message)
203 self.end_of_message = False
204 self.text_message = self.translator.morse_to_text(self.morse_message)
205 self.morse_message = ""
208 return self.text_message
210 def transmit_symbol(self, symbol):
212 sleep_time = self.dot_time
214 sleep_time = self.dash_time
216 sleep_time = self.signalspace_time
218 sleep_time = self.wordspace_time
219 elif symbol == "EOM":
220 sleep_time = self.eom_time
222 # To terminate the transmission just call and hangup, with no extra
224 sleep_time = self.call_setup_time
226 logging.info("Dial and wait %.2f seconds (transmitting '%s')",
229 # Dial, then wait self.call_setup_time to make sure the receiver gets
230 # at least one RING, and then hangup and sleep the remaining time
231 self.modem.send_command("ATDT" + self.destination_number)
232 time.sleep(self.call_setup_time)
233 self.modem.send_command("ATH")
234 self.modem.get_response()
235 time.sleep(sleep_time - self.call_setup_time)
237 def transmit_signal(self, signal):
238 logging.debug("Transmitting signal: %s", signal)
239 for symbol in signal:
240 self.transmit_symbol(symbol)
242 def transmit(self, message, destination_number):
243 self.destination_number = destination_number
245 morse_message = self.translator.text_to_morse(message)
246 signals = morse_message.split()
249 logging.debug("Starting the transmission")
250 for i, signal in enumerate(signals):
251 logging.debug("Transmitting '%s' as '%s'", message[i], signal)
252 self.transmit_signal(signal)
254 # Transmit a signal separator only when strictly necessary:
255 # - after the last symbol, we are going to transmit an EOM
256 # anyway, and that will mark the end of the last symbol.
257 # - between words the word separator act as a symbol separator
259 if i != len(signals) - 1 and signals[i + 1] != "/" and signal != "/":
260 self.transmit_symbol(" ")
262 self.transmit_symbol("EOM")
264 # Since the Morse signals are encoded in the distance between calls, an
265 # extra call is needed in order for receiver actually get the EOM and
266 # see that the transmission has terminated.
267 self.transmit_symbol(None)
269 def estimate_transmit_duration(self, message):
270 morsemessage = self.translator.text_to_morse(message)
271 signals = morsemessage.split()
273 logging.debug(signals)
275 transmitting_time = 0
276 for i, signal in enumerate(signals):
277 logging.debug("signal: %s", signal)
279 for symbol in signal:
281 transmitting_time += self.dot_time
283 transmitting_time += self.dash_time
285 transmitting_time += self.wordspace_time
287 if i != len(signals) - 1 and signals[i + 1] != "/" and symbol != "/":
288 transmitting_time += self.signalspace_time
290 transmitting_time += self.eom_time
292 logging.debug("Estimated transmitting time: %d seconds",
296 def test_send_receive():
297 logging.basicConfig(level=logging.DEBUG)
299 call_setup_uncertainty = 0.4
301 ring_uncertainty = 0.3
303 class DummyModem(object):
304 """Always receive a '.' and then a '/', which result in 'E '."""
309 def send_command(self, command):
312 def get_response(self, response):
313 if self.ring_count % 2:
315 time.sleep(call_setup_time + (ring_time + ring_uncertainty))
318 time.sleep(call_setup_time + (ring_time + ring_uncertainty) * 4)
322 xcv = CallDistanceTransceiver(DummyModem(), call_setup_time,
323 call_setup_uncertainty, ring_time,
328 if __name__ == "__main__":