#!/usr/bin/env python3 # # CallDistanceTransceiver - send and receive Morse using phone calls distance # # Copyright (C) 2015 Antonio Ospite # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # This hack allows importing local modules also when # __name__ == "__main__" try: from .MorseDistanceModulator import MorseDistanceModulator from .MorseTranslator import MorseTranslator except SystemError: from MorseDistanceModulator import MorseDistanceModulator from MorseTranslator import MorseTranslator import logging import time def log_symbol(distance, symbol, extra_info=""): logging.info("distance: %.2f Received \"%s\"%s", distance, symbol, extra_info) class CallDistanceTransceiver(object): """Transmit Morse messages using the distance between calls. This is basically a pulse-distance modulation (PDM). A RING is a pulse and the Morse symbols are encoded in the pause between the first ring of the previous call and the first ring of a new call. This strategy is very slow but it can even be used with ancient analog modems which don't have call progress notifications for outgoing calls, and make it also hard to count multiple rings in the same call on the receiving side because there is no explicit notification on the receiving side of when the caller ends a calls. For GSM modems, which have a more sophisticate call report signalling, a more efficient encoding can be used (for example using ring counts to encode Morse symbols, i.e. a pulse-length modulation), but for a proof-of-concept, a slow encoding covering the most general setup is fine. Plus, supporting communications between analog modems is cool :) """ def __init__(self, modem, call_setup_time_min=7, call_setup_time_max=16.5, ring_time_min=4.8, ring_time_max=5.2, add_inter_call_distance=True): """Encode the Morse symbols using the distance between calls. Args: call_setup_time_min: the minimum time between when the transmitter dials the number and the receiver reports RING notifications. call_setup_time_max: the maximum time between when the transmitter dials the number and the receiver reports RING notifications. Waiting this time after dialing ensures that the receiver has received _at_least_ one ring. The default chosen here have been tested to work fine with old analog modems, provided that there is some more distance between two calls, and with Android receivers. ring_time_min: the minimum time between two consecutive RINGs in the same call. ring_time_max: the maximum time between two consecutive RINGs in the same call, the standard interval is about 5 seconds, but line and/or software delays can make it vary. This is needed in order to ignore multiple ring in the same call when multiple RING in the same call are notified, and then be able to discriminate between two different calls. add_inter_call_distance: specify if it is needed to wait an extra fixed time between calls. """ self.modem = modem self.translator = MorseTranslator() self.call_setup_time_max = call_setup_time_max if add_inter_call_distance: # Analog modems don't like to dial a new call immediately after # they hung up the previous one; an extra delay of about one # ring time makes them happy. inter_symbol_distance = ring_time_max else: inter_symbol_distance = 0 self.modulator = MorseDistanceModulator(call_setup_time_min, call_setup_time_max, ring_time_min, ring_time_max, inter_symbol_distance) logging.debug("call setup time between %.2f and %.2f " "--------- dot transmit time: %.2f + %.2f " "receive time: between %.2f and %.2f", self.modulator.period_min, self.modulator.period_max, self.modulator.period_max, self.modulator.dot_time.dist, self.modulator.dot_time.min, self.modulator.dot_time.max) logging.debug("call setup time between %.2f and %.2f " "-------- dash transmit time: %.2f + %.2f " "receive time: between %.2f and %.2f", self.modulator.period_min, self.modulator.period_max, self.modulator.period_max, self.modulator.dash_time.dist, self.modulator.dash_time.min, self.modulator.dash_time.max) logging.debug("call setup time between %.2f and %.2f " "- signalspace transmit time: %.2f + %.2f " "receive time: between %.2f and %.2f", self.modulator.period_min, self.modulator.period_max, self.modulator.period_max, self.modulator.signalspace_time.dist, self.modulator.signalspace_time.min, self.modulator.signalspace_time.max) logging.debug("call setup time between %.2f and %.2f " "--- wordspace transmit time: %.2f + %.2f " "receive time: between %.2f and %.2f", self.modulator.period_min, self.modulator.period_max, self.modulator.period_max, self.modulator.wordspace_time.dist, self.modulator.wordspace_time.min, self.modulator.wordspace_time.max) logging.debug("call setup time between %.2f and %.2f " "--------- EOM transmit time: %.2f + %.2f " "receive time: between %.2f and inf", self.modulator.period_min, self.modulator.period_max, self.modulator.period_max, self.modulator.eom_time.dist, self.modulator.eom_time.min) self.previous_ring_time = -1 self.previous_call_time = -1 self.morse_message = "" self.text_message = "" self.end_of_message = False def receive_symbol(self): current_ring_time = time.time() if self.previous_ring_time == -1: self.previous_ring_time = current_ring_time self.previous_call_time = current_ring_time log_symbol(0, "", "(The very first ring)") return ring_distance = current_ring_time - self.previous_ring_time logging.debug("RINGs distance: %.2f", ring_distance) self.previous_ring_time = current_ring_time # Ignore multiple rings in the same call if self.modulator.is_same_period(ring_distance): logging.debug("multiple rings in the same call, distance: %.2f", ring_distance) return call_distance = current_ring_time - self.previous_call_time self.previous_call_time = current_ring_time try: symbol = self.modulator.distance_to_symbol(call_distance) except ValueError as err: logging.error("%s", err) logging.error("Check the transmitter and receiver parameters") return extra_info = "" if symbol in [" ", "/", "EOM"]: signal = self.morse_message.strip().split(' ')[-1] character = self.translator.signal_to_character(signal) extra_info = " got \"%s\"" % character log_symbol(call_distance, symbol, extra_info) if symbol != "EOM": # Add spaces around the wordspace symbol to make it easier to split # the Morse message in symbols later on if symbol == "/": symbol = " / " self.morse_message += symbol else: self.end_of_message = True self.previous_ring_time = -1 self.previous_call_time = -1 def receive_loop(self): while not self.end_of_message: self.modem.get_response("RING") self.receive_symbol() logging.debug("Current message: %s", self.morse_message) self.end_of_message = False self.text_message = self.translator.morse_to_text(self.morse_message) self.morse_message = "" def get_text(self): return self.text_message def transmit_symbol(self, destination_number, symbol, sleep_time): logging.info("Dial and wait %.2f = %.2f + %.2f seconds " "(transmitting '%s')", self.call_setup_time_max + sleep_time, self.call_setup_time_max, sleep_time, symbol) # Dial, then wait self.call_setup_time_max to make sure the receiver # gets at least one RING, and then hangup and sleep the time needed to # transmit a symbol. time_before = time.time() self.modem.send_command("ATDT" + destination_number + ";") time.sleep(self.call_setup_time_max) self.modem.send_command("ATH") self.modem.get_response() time_after = time.time() # Account for possible delays in order to be as adherent as # possible to the nominal total symbol transmission distance. delay = (time_after - time_before) - self.call_setup_time_max logging.debug("Delay %.2f", delay) remaining_sleep_time = sleep_time - delay if remaining_sleep_time < 0: remaining_sleep_time = 0 logging.debug("Should sleep %.2f. Will sleep %.2f", sleep_time, remaining_sleep_time) time.sleep(remaining_sleep_time) def transmit(self, message, destination_number): morse_message = self.translator.text_to_morse(message) distances = self.modulator.modulate(morse_message) logging.debug("Starting the transmission") for i, distance in enumerate(distances): # Use 'None' for the last call if i == len(distances) - 1: symbol = None else: total_sleep_time = self.call_setup_time_max + distance symbol = self.modulator.distance_to_symbol(total_sleep_time) self.transmit_symbol(destination_number, symbol, distance) def estimate_transmit_duration(self, message): morsemessage = self.translator.text_to_morse(message) logging.debug(morsemessage) distances = self.modulator.modulate(morsemessage) transmitting_time = 0 for distance in distances: transmitting_time += self.call_setup_time_max transmitting_time += distance logging.debug("Estimated transmitting time: %.2f seconds", transmitting_time) def test_transmit_receive(): logging.basicConfig(level=logging.DEBUG) call_setup_time_min = 0 call_setup_time_max = 0.01 ring_time_min = 0 ring_time_max = 0 import random class DummyModem(object): """Always receive a '.', a '/' and then EOM, which results in 'E '.""" def __init__(self): self.ring_count = 0 # Take trasmission times from a transceiver self.transceiver = None random.seed(None) def send_command(self, command): pass def get_response(self, response=None): # pylint: disable=unused-argument setup_time = random.uniform(self.transceiver.modulator.period_min, self.transceiver.modulator.period_max) if self.ring_count == 0: # dummy ring pass elif self.ring_count == 1: # received a '.' dot_time = self.transceiver.modulator.dot_time.dist time.sleep(setup_time + dot_time) elif self.ring_count == 2: # received a '/' wordspace_time = self.transceiver.modulator.wordspace_time.dist time.sleep(setup_time + wordspace_time) else: # received an 'EOM' eom_time = self.transceiver.modulator.eom_time.dist time.sleep(setup_time + eom_time) self.ring_count += 1 self.ring_count %= 4 modem = DummyModem() xcv = CallDistanceTransceiver(modem, call_setup_time_min, call_setup_time_max, ring_time_min, ring_time_max, True) modem.transceiver = xcv xcv.transmit("CODEX PARIS", "0") while True: modem.ring_count = 0 xcv.receive_loop() print() print("Message received!") print("\"%s\"" % xcv.get_text(), flush=True) if __name__ == "__main__": test_transmit_receive()