CallDistanceTransceiver.py: consider possible delays in transmit_symbol()
[SaveMySugar/python3-savemysugar.git] / src / savemysugar / CallDistanceTransceiver.py
index 29c3fb8..15fcdae 100755 (executable)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-# This hack allows MorseTranslator to be imported also when
+# 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
 
 
-class SymbolTime(object):
-    """
-    In theory the symbol distance (the distance to discriminate symbols) can
-    be arbitrary, and it's only bounded below by the stability of the pulse
-    time, but in practice it can be necessary to wait a predefined minimum
-    amount of time between pulses because of technological limits of the
-    transmitting devices, we call this time the "minimum inter-symbol
-    distance".
-    """
-
-    # pylint: disable=too-few-public-methods
-    def __init__(self, pulse_min, pulse_max, multiplier,
-                 min_inter_symbol_distance=0.0):
-        assert multiplier >= 0
-        if (pulse_min == pulse_max) and (min_inter_symbol_distance == 0):
-            raise ValueError("If (pulse_min == pulse_max) a non-zero",
-                             "inter-symbol distance MUST be specified")
-
-        symbol_distance = 2 * (pulse_max - pulse_min)
-        if symbol_distance == 0:
-            symbol_distance = min_inter_symbol_distance
-
-        # The time the transmitter has to wait to disambiguate between this
-        # symbol and a different one.
-        self.dist = min_inter_symbol_distance + symbol_distance * multiplier
-
-        # The minimum time which represents the symbol at the receiving end.
-        self.min = min_inter_symbol_distance + pulse_min + \
-            symbol_distance * multiplier
-
-        # The maximum time which represents the symbol at the receiving end
-        self.max = min_inter_symbol_distance + pulse_min + \
-            symbol_distance * (multiplier + 1)
+def log_symbol(distance, symbol, extra_info=""):
+    logging.info("distance: %.2f Received \"%s\"%s", distance, symbol,
+                 extra_info)
 
 
 class CallDistanceTransceiver(object):
@@ -88,7 +60,7 @@ class CallDistanceTransceiver(object):
     """
 
     def __init__(self, modem,
-                 call_setup_time_min=7, call_setup_time_max=15,
+                 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.
@@ -119,14 +91,8 @@ class CallDistanceTransceiver(object):
         self.modem = modem
         self.translator = MorseTranslator()
 
-        self.destination_number = ""
-
-        self.call_setup_time_min = call_setup_time_min
         self.call_setup_time_max = call_setup_time_max
 
-        self.ring_time_min = ring_time_min
-        self.ring_time_max = ring_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
@@ -135,66 +101,56 @@ class CallDistanceTransceiver(object):
         else:
             inter_symbol_distance = 0
 
-        self.dot_time = SymbolTime(call_setup_time_min,
-                                   call_setup_time_max, 0,
-                                   inter_symbol_distance)
-        self.dash_time = SymbolTime(call_setup_time_min,
-                                    call_setup_time_max, 1,
-                                    inter_symbol_distance)
-        self.signalspace_time = SymbolTime(call_setup_time_min,
-                                           call_setup_time_max, 2,
-                                           inter_symbol_distance)
-        self.wordspace_time = SymbolTime(call_setup_time_min,
-                                         call_setup_time_max, 3,
-                                         inter_symbol_distance)
-        self.eom_time = SymbolTime(call_setup_time_min,
-                                   call_setup_time_max, 4,
-                                   inter_symbol_distance)
+        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.call_setup_time_min,
-                      self.call_setup_time_max,
-                      self.call_setup_time_max,
-                      self.dot_time.dist,
-                      self.dot_time.min,
-                      self.dot_time.max)
+                      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.call_setup_time_min,
-                      self.call_setup_time_max,
-                      self.call_setup_time_max,
-                      self.dash_time.dist,
-                      self.dash_time.min,
-                      self.dash_time.max)
+                      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.call_setup_time_min,
-                      self.call_setup_time_max,
-                      self.call_setup_time_max,
-                      self.signalspace_time.dist,
-                      self.signalspace_time.min,
-                      self.signalspace_time.max)
+                      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.call_setup_time_min,
-                      self.call_setup_time_max,
-                      self.call_setup_time_max,
-                      self.wordspace_time.dist,
-                      self.wordspace_time.min,
-                      self.wordspace_time.max)
+                      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.call_setup_time_min,
-                      self.call_setup_time_max,
-                      self.call_setup_time_max,
-                      self.eom_time.dist,
-                      self.eom_time.min)
+                      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
@@ -204,17 +160,13 @@ class CallDistanceTransceiver(object):
 
         self.end_of_message = False
 
-    def log_symbol(self, distance, symbol, extra_info=""):
-        logging.info("distance: %.2f Received \"%s\"%s", distance, symbol,
-                     extra_info)
-
-    def receive_character(self):
+    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
-            self.log_symbol(0, "", "(The very first ring)")
+            log_symbol(0, "", "(The very first ring)")
             return
 
         ring_distance = current_ring_time - self.previous_ring_time
@@ -222,8 +174,7 @@ class CallDistanceTransceiver(object):
         self.previous_ring_time = current_ring_time
 
         # Ignore multiple rings in the same call
-        if ring_distance > self.ring_time_min and \
-           ring_distance <= self.ring_time_max:
+        if self.modulator.is_same_period(ring_distance):
             logging.debug("multiple rings in the same call, distance: %.2f",
                           ring_distance)
             return
@@ -231,51 +182,36 @@ class CallDistanceTransceiver(object):
         call_distance = current_ring_time - self.previous_call_time
         self.previous_call_time = current_ring_time
 
-        if call_distance > self.dot_time.min and \
-           call_distance <= self.dot_time.max:
-            self.log_symbol(call_distance, '.')
-            self.morse_message += "."
-            return
-
-        if call_distance > self.dash_time.min and \
-           call_distance <= self.dash_time.max:
-            self.log_symbol(call_distance, '-')
-            self.morse_message += "-"
+        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
 
-        if call_distance > self.signalspace_time.min and \
-           call_distance <= self.signalspace_time.max:
+        extra_info = ""
+        if symbol in [" ", "/", "EOM"]:
             signal = self.morse_message.strip().split(' ')[-1]
             character = self.translator.signal_to_character(signal)
-            self.log_symbol(call_distance, ' ', " got \"%s\"" % character)
-            self.morse_message += " "
-            return
+            extra_info = " got \"%s\"" % character
 
-        if call_distance > self.wordspace_time.min and \
-           call_distance <= self.wordspace_time.max:
-            signal = self.morse_message.strip().split(' ')[-1]
-            character = self.translator.signal_to_character(signal)
-            self.log_symbol(call_distance, '/', " got \"%s\"" % character)
-            self.morse_message += " / "
-            return
+        log_symbol(call_distance, symbol, extra_info)
 
-        if call_distance > self.eom_time.min:
-            signal = self.morse_message.strip().split(' ')[-1]
-            character = self.translator.signal_to_character(signal)
-            self.log_symbol(call_distance, 'EOM', " got \"%s\"" % character)
+        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
-            return
-
-        # if the code made it up to here, something fishy is going on
-        logging.error("Unexpected distance: %.2f", call_distance)
-        logging.error("Check the transmitter and receiver parameters")
 
     def receive_loop(self):
         while not self.end_of_message:
             self.modem.get_response("RING")
-            self.receive_character()
+            self.receive_symbol()
             logging.debug("Current message: %s", self.morse_message)
 
         self.end_of_message = False
@@ -285,22 +221,7 @@ class CallDistanceTransceiver(object):
     def get_text(self):
         return self.text_message
 
-    def transmit_symbol(self, symbol):
-        if symbol == ".":
-            sleep_time = self.dot_time.dist
-        elif symbol == "-":
-            sleep_time = self.dash_time.dist
-        elif symbol == " ":
-            sleep_time = self.signalspace_time.dist
-        elif symbol == "/":
-            sleep_time = self.wordspace_time.dist
-        elif symbol == "EOM":
-            sleep_time = self.eom_time.dist
-        elif symbol is None:
-            # To terminate the transmission just call and hangup, with no extra
-            # distance
-            sleep_time = 0
-
+    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,
@@ -311,77 +232,57 @@ class CallDistanceTransceiver(object):
         # 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.
-        self.modem.send_command("ATDT" + self.destination_number + ";")
+        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.sleep(sleep_time)
+        time_after = time.time()
 
-    def transmit_signal(self, signal):
-        logging.debug("Transmitting signal: %s", signal)
-        for symbol in signal:
-            self.transmit_symbol(symbol)
+        # 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)
 
-    def transmit(self, message, destination_number):
-        self.destination_number = destination_number
+        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)
-        signals = morse_message.split()
+        distances = self.modulator.modulate(morse_message)
 
         logging.debug("Starting the transmission")
-        for i, signal in enumerate(signals):
-            logging.debug("Transmitting '%s' as '%s'", message[i], signal)
-            self.transmit_signal(signal)
-
-            # Transmit a signal separator only when strictly necessary:
-            #  - after the last symbol, we are going to transmit an EOM
-            #    anyway, and that will mark the end of the last symbol.
-            #  - between words the word separator act as a symbol separator
-            #    too.
-            if i != len(signals) - 1 and signals[i + 1] != "/" and signal != "/":
-                self.transmit_symbol(" ")
-
-        self.transmit_symbol("EOM")
+        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)
 
-        # Since the Morse signals are encoded in the distance between calls, an
-        # extra call is needed in order for receiver actually get the EOM and
-        # see that the transmission has terminated.
-        self.transmit_symbol(None)
+            self.transmit_symbol(destination_number, symbol, distance)
 
     def estimate_transmit_duration(self, message):
         morsemessage = self.translator.text_to_morse(message)
-        signals = morsemessage.split()
+        logging.debug(morsemessage)
 
-        logging.debug(signals)
+        distances = self.modulator.modulate(morsemessage)
 
         transmitting_time = 0
-        for i, signal in enumerate(signals):
-            logging.debug("signal: %s", signal)
-
-            for symbol in signal:
-                transmitting_time += self.call_setup_time_max
-                if symbol == ".":
-                    transmitting_time += self.dot_time.dist
-                elif symbol == "-":
-                    transmitting_time += self.dash_time.dist
-                elif symbol == "/":
-                    transmitting_time += self.wordspace_time.dist
-
-                if i != len(signals) - 1 and signals[i + 1] != "/" and symbol != "/":
-                    transmitting_time += self.call_setup_time_max
-                    transmitting_time += self.signalspace_time.dist
-
-        transmitting_time += self.call_setup_time_max
-        transmitting_time += self.eom_time.dist
-
-        # The final call needed for the receiver to get the EOM
-        transmitting_time += self.call_setup_time_max
+        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_send_receive():
+def test_transmit_receive():
     logging.basicConfig(level=logging.DEBUG)
     call_setup_time_min = 0
     call_setup_time_max = 0.01
@@ -404,24 +305,27 @@ def test_send_receive():
         def send_command(self, command):
             pass
 
-        def get_response(self, response):
+        def get_response(self, response=None):
             # pylint: disable=unused-argument
 
-            setup_time = random.uniform(self.transceiver.call_setup_time_min,
-                                        self.transceiver.call_setup_time_max)
+            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 '.'
-                time.sleep(setup_time + self.transceiver.dot_time.dist)
+                dot_time = self.transceiver.modulator.dot_time.dist
+                time.sleep(setup_time + dot_time)
             elif self.ring_count == 2:
                 # received a '/'
-                time.sleep(setup_time + self.transceiver.wordspace_time.dist)
+                wordspace_time = self.transceiver.modulator.wordspace_time.dist
+                time.sleep(setup_time + wordspace_time)
             else:
                 # received an 'EOM'
-                time.sleep(setup_time + self.transceiver.eom_time.dist)
+                eom_time = self.transceiver.modulator.eom_time.dist
+                time.sleep(setup_time + eom_time)
 
             self.ring_count += 1
             self.ring_count %= 4
@@ -434,13 +338,15 @@ def test_send_receive():
 
     modem.transceiver = xcv
 
+    xcv.transmit("CODEX PARIS", "0")
+
     while True:
-        xcv.receive_loop()
         modem.ring_count = 0
+        xcv.receive_loop()
         print()
         print("Message received!")
         print("\"%s\"" % xcv.get_text(), flush=True)
 
 
 if __name__ == "__main__":
-    test_send_receive()
+    test_transmit_receive()