Initial import
authorAntonio Ospite <ao2@ao2.it>
Thu, 10 Dec 2015 21:31:04 +0000 (22:31 +0100)
committerAntonio Ospite <ao2@ao2.it>
Fri, 11 Dec 2015 14:48:25 +0000 (15:48 +0100)
21 files changed:
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
TODO [new file with mode: 0644]
logs/codex_receive.log [new file with mode: 0644]
logs/codex_transmit.log [new file with mode: 0644]
logs/measure_call_setup_time_PSTN-to-GSM.log [new file with mode: 0644]
logs/measure_ring_distance_PSTN-to-GSM.log [new file with mode: 0644]
plot/call_distance_modulation.gnuplot [new file with mode: 0755]
plot/codex_receive.log [new file with mode: 0644]
plot/codex_transmit.log [new file with mode: 0644]
plot/savemysugar_receive.log [new file with mode: 0644]
plot/savemysugar_transmit.log [new file with mode: 0644]
src/measure_call_setup_time.py [new file with mode: 0755]
src/measure_ring_distance.py [new file with mode: 0755]
src/receive.py [new file with mode: 0755]
src/savemysugar/CallDistanceTransceiver.py [new file with mode: 0755]
src/savemysugar/Modem.py [new file with mode: 0755]
src/savemysugar/MorseTranslator.py [new file with mode: 0755]
src/savemysugar/__init__.py [new file with mode: 0755]
src/savemysugar/cumulative_average.py [new file with mode: 0755]
src/transmit.py [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b1854c9
--- /dev/null
@@ -0,0 +1,2 @@
+__pycache__
+_test*.sh
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..947bd19
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+SaveMySugar
+===========
+
+SaveMySugar is an experiment about transmitting and receiving messages for free
+using phone call rings to encode Morse code.
+
+The name SaveMySugar is a joke on the price per bit of SMSs (Short Message
+Service) which is quite high, and the distress signal Save Our Soul used in
+Morse communications.
+
+See [http://savemysugar.ao2.it](http://savemysugar.ao2.it) for further details.
+
+Tutorial
+--------
+
+The "measure" pass in 1. and 2. assumes that there are two serial modems
+attached to the same host connected to two independent phone lines, so that
+a single program can control both the transmitter and the receiver device.
+
+A possible setup is to have two cell phones providing serial ports over which
+the programs can send AT commands and receive RING notifications.
+
+1. Measure the call setup time using `src/measure_call_setup_time.py` and take
+   note of `Max call setup time` and `Call setup time uncertainty`.
+
+2. Measure the distance between rings in the same call using
+   `src/measure_ring_distance.py` and take note of `max_distance` and
+   `uncerainty`.
+
+3. Adjust the parameters in `src/savemysugar/CallDistanceTransceiver.py` using
+   the values measured before, possibly approximated by excess.
+
+4. Transmit and/or receive a message using `src/transmit.py` or `src/receive.py`
+
+The sender and the receiver must use the same parameters.
+
+In a future version it should be possible to pass the parameters as command line
+arguments to `transmit.py` and `receive.py`, but for now these values have to be
+hardcoded.
+
+Dependencies
+------------
+
+The only dependencies are:
+
+* python3
+* python3-serial
+
+Authors
+-------
+
+Written by Antonio Ospite <ao2@ao2.it> starting from an idea by Corrado Rubera.
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..9f26f6d
--- /dev/null
+++ b/TODO
@@ -0,0 +1,7 @@
+- Consider making Modem.py and MorseTranslator.py independent packages.
+- Add a better command line argument parsing for the transmit.py and
+  receive.py programs.
+- Do not use Modem.py directly in the transceiver class but instead use
+  a generic transmitting/receiving back-end in order to support other
+  transmission mechanisms or telephony frameworks with python bindings (e.g.
+  freesmartphone.org).
diff --git a/logs/codex_receive.log b/logs/codex_receive.log
new file mode 100644 (file)
index 0000000..bdc87a7
--- /dev/null
@@ -0,0 +1,72 @@
+1449670918.188733 DEBUG:send_command AT command: 'ATZ\r\n'
+1449670918.198112 DEBUG:send_command AT command: 'ATX3\r\n'
+1449670918.201892 DEBUG:__init__ dot time:         transmit: 14.70 receive: (13.20, 16.20)
+1449670918.203712 DEBUG:__init__ dash time:        transmit: 19.90 receive: (18.40, 21.40)
+1449670918.203933 DEBUG:__init__ signalspace time: transmit: 25.10 receive: (23.60, 26.60)
+1449670918.204020 DEBUG:__init__ wordspace time:   transmit: 30.30 receive: (28.80, 31.80)
+1449670918.204102 DEBUG:__init__ EOM time:         transmit: 35.50 receive: (34.00, +inf)
+1449670994.957390 INFO:log_symbol distance: 0.00 Received "" (The very first ring)
+1449670994.957713 DEBUG:receive_loop Current message: 
+1449671014.664847 DEBUG:receive_character RINGs distance: 19.71
+1449671014.665521 INFO:log_symbol distance: 19.71 Received "-" 
+1449671014.665703 DEBUG:receive_loop Current message: -
+1449671030.670222 DEBUG:receive_character RINGs distance: 16.01
+1449671030.670410 INFO:log_symbol distance: 16.01 Received "." 
+1449671030.670524 DEBUG:receive_loop Current message: -.
+1449671050.785801 DEBUG:receive_character RINGs distance: 20.12
+1449671050.785999 INFO:log_symbol distance: 20.12 Received "-" 
+1449671050.786084 DEBUG:receive_loop Current message: -.-
+1449671066.069670 DEBUG:receive_character RINGs distance: 15.28
+1449671066.070046 INFO:log_symbol distance: 15.28 Received "." 
+1449671066.070279 DEBUG:receive_loop Current message: -.-.
+1449671091.500801 DEBUG:receive_character RINGs distance: 25.43
+1449671091.500978 INFO:log_symbol distance: 25.43 Received " " got "c"
+1449671091.501050 DEBUG:receive_loop Current message: -.-. 
+1449671112.640585 DEBUG:receive_character RINGs distance: 21.14
+1449671112.640955 INFO:log_symbol distance: 21.14 Received "-" 
+1449671112.641135 DEBUG:receive_loop Current message: -.-. -
+1449671132.990194 DEBUG:receive_character RINGs distance: 20.35
+1449671132.990636 INFO:log_symbol distance: 20.35 Received "-" 
+1449671132.990819 DEBUG:receive_loop Current message: -.-. --
+1449671153.426853 DEBUG:receive_character RINGs distance: 20.44
+1449671153.427221 INFO:log_symbol distance: 20.44 Received "-" 
+1449671153.427400 DEBUG:receive_loop Current message: -.-. ---
+1449671179.055193 DEBUG:receive_character RINGs distance: 25.63
+1449671179.055582 INFO:log_symbol distance: 25.63 Received " " got "o"
+1449671179.055764 DEBUG:receive_loop Current message: -.-. --- 
+1449671199.486396 DEBUG:receive_character RINGs distance: 20.43
+1449671199.486596 INFO:log_symbol distance: 20.43 Received "-" 
+1449671199.486665 DEBUG:receive_loop Current message: -.-. --- -
+1449671215.024084 DEBUG:receive_character RINGs distance: 15.54
+1449671215.024292 INFO:log_symbol distance: 15.54 Received "." 
+1449671215.024386 DEBUG:receive_loop Current message: -.-. --- -.
+1449671230.094768 DEBUG:receive_character RINGs distance: 15.07
+1449671230.094973 INFO:log_symbol distance: 15.07 Received "." 
+1449671230.095073 DEBUG:receive_loop Current message: -.-. --- -..
+1449671255.788444 DEBUG:receive_character RINGs distance: 25.69
+1449671255.788843 INFO:log_symbol distance: 25.69 Received " " got "d"
+1449671255.789009 DEBUG:receive_loop Current message: -.-. --- -.. 
+1449671271.266149 DEBUG:receive_character RINGs distance: 15.48
+1449671271.266586 INFO:log_symbol distance: 15.48 Received "." 
+1449671271.266758 DEBUG:receive_loop Current message: -.-. --- -.. .
+1449671296.885521 DEBUG:receive_character RINGs distance: 25.62
+1449671296.885915 INFO:log_symbol distance: 25.62 Received " " got "e"
+1449671296.886115 DEBUG:receive_loop Current message: -.-. --- -.. . 
+1449671316.965795 DEBUG:receive_character RINGs distance: 20.08
+1449671316.966002 INFO:log_symbol distance: 20.08 Received "-" 
+1449671316.966103 DEBUG:receive_loop Current message: -.-. --- -.. . -
+1449671332.817696 DEBUG:receive_character RINGs distance: 15.85
+1449671332.818059 INFO:log_symbol distance: 15.85 Received "." 
+1449671332.818241 DEBUG:receive_loop Current message: -.-. --- -.. . -.
+1449671347.603125 DEBUG:receive_character RINGs distance: 14.79
+1449671347.603330 INFO:log_symbol distance: 14.79 Received "." 
+1449671347.603422 DEBUG:receive_loop Current message: -.-. --- -.. . -..
+1449671368.286672 DEBUG:receive_character RINGs distance: 20.68
+1449671368.286869 INFO:log_symbol distance: 20.68 Received "-" 
+1449671368.286952 DEBUG:receive_loop Current message: -.-. --- -.. . -..-
+1449671404.531024 DEBUG:receive_character RINGs distance: 36.24
+1449671404.531392 INFO:log_symbol distance: 36.24 Received "EOM" got "x"
+1449671404.531572 DEBUG:receive_loop Current message: -.-. --- -.. . -..-
+
+Message received!
+codex
diff --git a/logs/codex_transmit.log b/logs/codex_transmit.log
new file mode 100644 (file)
index 0000000..f52bed2
--- /dev/null
@@ -0,0 +1,96 @@
+Script started on mer 09 dic 2015 15:23:05 CET
++ OUTGOING_PORT=/dev/ttyS0
++ DESTINATION_NUMBER=555402402
++ MESSAGE=CODEX
++ ./src/transmit.py /dev/ttyS0 555402402 CODEX
+Available hardware serial ports:
+  - /dev/ttyS0
+
+1449670985.587252 DEBUG:send_command AT command: 'ATZ\r\n'
+1449670986.534742 DEBUG:send_command AT command: 'ATX3\r\n'
+1449670986.553606 DEBUG:__init__ dot time:         transmit: 14.70 receive: (13.20, 16.20)
+1449670986.553924 DEBUG:__init__ dash time:        transmit: 19.90 receive: (18.40, 21.40)
+1449670986.554089 DEBUG:__init__ signalspace time: transmit: 25.10 receive: (23.60, 26.60)
+1449670986.554257 DEBUG:__init__ wordspace time:   transmit: 30.30 receive: (28.80, 31.80)
+1449670986.554419 DEBUG:__init__ EOM time:         transmit: 35.50 receive: (34.00, +inf)
+1449670986.555578 DEBUG:estimate_transmit_duration ['-.-.', '---', '-..', '.', '-..-']
+1449670986.555808 DEBUG:estimate_transmit_duration signal: -.-.
+1449670986.556016 DEBUG:estimate_transmit_duration signal: ---
+1449670986.556195 DEBUG:estimate_transmit_duration signal: -..
+1449670986.556372 DEBUG:estimate_transmit_duration signal: .
+1449670986.556543 DEBUG:estimate_transmit_duration signal: -..-
+1449670986.556720 DEBUG:estimate_transmit_duration Estimated transmitting time: 573 seconds
+1449670986.557297 DEBUG:transmit Starting the trasmission
+1449670986.557462 DEBUG:transmit Transmitting 'C' as '-.-.'
+1449670986.557620 DEBUG:transmit_signal Transmitting signal: -.-.
+1449670986.557800 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449670986.558138 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449670996.064090 DEBUG:send_command AT command: 'ATH\r\n'
+1449671007.066112 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671007.066549 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671016.574705 DEBUG:send_command AT command: 'ATH\r\n'
+1449671022.369606 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671022.369991 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671031.873174 DEBUG:send_command AT command: 'ATH\r\n'
+1449671042.877515 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671042.877715 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671052.378261 DEBUG:send_command AT command: 'ATH\r\n'
+1449671058.174628 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671058.175008 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671067.682544 DEBUG:send_command AT command: 'ATH\r\n'
+1449671083.885474 DEBUG:transmit Transmitting 'O' as '---'
+1449671083.885724 DEBUG:transmit_signal Transmitting signal: ---
+1449671083.885830 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671083.885926 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671093.394557 DEBUG:send_command AT command: 'ATH\r\n'
+1449671104.398561 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671104.398755 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671113.908509 DEBUG:send_command AT command: 'ATH\r\n'
+1449671124.910546 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671124.910772 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671134.420646 DEBUG:send_command AT command: 'ATH\r\n'
+1449671145.422644 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671145.423032 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671154.930423 DEBUG:send_command AT command: 'ATH\r\n'
+1449671171.138666 DEBUG:transmit Transmitting 'D' as '-..'
+1449671171.139023 DEBUG:transmit_signal Transmitting signal: -..
+1449671171.139232 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671171.139414 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671180.645330 DEBUG:send_command AT command: 'ATH\r\n'
+1449671191.649726 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671191.650113 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671201.159920 DEBUG:send_command AT command: 'ATH\r\n'
+1449671206.955485 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671206.955890 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671216.465874 DEBUG:send_command AT command: 'ATH\r\n'
+1449671222.262631 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671222.263016 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671231.770671 DEBUG:send_command AT command: 'ATH\r\n'
+1449671247.979372 DEBUG:transmit Transmitting 'E' as '.'
+1449671247.979575 DEBUG:transmit_signal Transmitting signal: .
+1449671247.979692 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671247.979795 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671257.488091 DEBUG:send_command AT command: 'ATH\r\n'
+1449671263.281866 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671263.282084 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671272.782675 DEBUG:send_command AT command: 'ATH\r\n'
+1449671288.990560 DEBUG:transmit Transmitting 'X' as '-..-'
+1449671288.990759 DEBUG:transmit_signal Transmitting signal: -..-
+1449671288.990876 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671288.990978 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671298.499601 DEBUG:send_command AT command: 'ATH\r\n'
+1449671309.502568 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671309.502792 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671319.010559 DEBUG:send_command AT command: 'ATH\r\n'
+1449671324.806638 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671324.807027 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671334.314691 DEBUG:send_command AT command: 'ATH\r\n'
+1449671340.113764 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671340.113983 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671349.622691 DEBUG:send_command AT command: 'ATH\r\n'
+1449671360.626544 INFO:transmit_symbol Dial and wait 35.50 seconds (transmitting 'EOM')
+1449671360.626760 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671370.136553 DEBUG:send_command AT command: 'ATH\r\n'
+1449671396.756532 INFO:transmit_symbol Dial and wait 9.50 seconds (transmitting 'None')
+1449671396.756752 DEBUG:send_command AT command: 'ATDT555402402\r\n'
+1449671406.261547 DEBUG:send_command AT command: 'ATH\r\n'
diff --git a/logs/measure_call_setup_time_PSTN-to-GSM.log b/logs/measure_call_setup_time_PSTN-to-GSM.log
new file mode 100644 (file)
index 0000000..8a9cc81
--- /dev/null
@@ -0,0 +1,68 @@
+Attempt: 1
+First ring
+After hangup sleeping 5.20 sec...
+Call setup time: 8.495060
+Min call setup time: 8.495060
+Max call setup time: 8.495060
+Average call setup time: 8.495060
+Call setup time uncertainty: 0.000000
+
+Total call time: 9.088812
+Min call time: 9.088812
+Max call time: 9.088812
+
+
+Attempt: 2
+First ring
+After hangup sleeping 5.20 sec...
+Call setup time: 7.734905
+Min call setup time: 7.734905
+Max call setup time: 8.495060
+Average call setup time: 8.114982
+Call setup time uncertainty: 0.380077
+
+Total call time: 8.328592
+Min call time: 8.328592
+Max call time: 9.088812
+
+
+Attempt: 3
+First ring
+After hangup sleeping 5.20 sec...
+Call setup time: 7.631182
+Min call setup time: 7.631182
+Max call setup time: 8.495060
+Average call setup time: 7.953716
+Call setup time uncertainty: 0.431939
+
+Total call time: 8.224887
+Min call time: 8.224887
+Max call time: 9.088812
+
+
+Attempt: 4
+First ring
+After hangup sleeping 5.20 sec...
+Call setup time: 8.416898
+Min call setup time: 7.631182
+Max call setup time: 8.495060
+Average call setup time: 8.069511
+Call setup time uncertainty: 0.431939
+
+Total call time: 9.010716
+Min call time: 8.224887
+Max call time: 9.088812
+
+
+Attempt: 5
+First ring
+After hangup sleeping 5.20 sec...
+Call setup time: 7.746990
+Min call setup time: 7.631182
+Max call setup time: 8.495060
+Average call setup time: 8.005007
+Call setup time uncertainty: 0.431939
+
+Total call time: 8.340551
+Min call time: 8.224887
+Max call time: 9.088812
diff --git a/logs/measure_ring_distance_PSTN-to-GSM.log b/logs/measure_ring_distance_PSTN-to-GSM.log
new file mode 100644 (file)
index 0000000..c4878e5
--- /dev/null
@@ -0,0 +1,64 @@
+First ring
+
+Other ring
+distance: 5.011862
+min_distance: 5.011862
+max_distance: 5.011862
+average_distance: 5.011862
+uncertainty: 0.000000
+
+Other ring
+distance: 4.978777
+min_distance: 4.978777
+max_distance: 5.011862
+average_distance: 4.995319
+uncertainty: 0.016542
+
+Other ring
+distance: 5.181837
+min_distance: 4.978777
+max_distance: 5.181837
+average_distance: 5.057492
+uncertainty: 0.101530
+
+Other ring
+distance: 4.829713
+min_distance: 4.829713
+max_distance: 5.181837
+average_distance: 5.000547
+uncertainty: 0.176062
+
+Other ring
+distance: 5.144908
+min_distance: 4.829713
+max_distance: 5.181837
+average_distance: 5.029419
+uncertainty: 0.176062
+
+Other ring
+distance: 4.868851
+min_distance: 4.829713
+max_distance: 5.181837
+average_distance: 5.002658
+uncertainty: 0.176062
+
+Other ring
+distance: 5.055723
+min_distance: 4.829713
+max_distance: 5.181837
+average_distance: 5.010239
+uncertainty: 0.176062
+
+Other ring
+distance: 4.964905
+min_distance: 4.829713
+max_distance: 5.181837
+average_distance: 5.004572
+uncertainty: 0.176062
+
+Other ring
+distance: 5.034827
+min_distance: 4.829713
+max_distance: 5.181837
+average_distance: 5.007934
+uncertainty: 0.176062
diff --git a/plot/call_distance_modulation.gnuplot b/plot/call_distance_modulation.gnuplot
new file mode 100755 (executable)
index 0000000..207eedb
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/gnuplot -persist
+#
+#    
+#      G N U P L O T
+#      Version 5.0 patchlevel 1    last modified 2015-06-07 
+#    
+#      Copyright (C) 1986-1993, 1998, 2004, 2007-2015
+#      Thomas Williams, Colin Kelley and many others
+#    
+#      gnuplot home:     http://www.gnuplot.info
+#      faq, bugs, etc:   type "help FAQ"
+#      immediate help:   type "help"  (plot window: hit 'h')
+
+set border 1
+set nogrid
+set key autotitles nobox reverse Left enhanced vert left at graph 0, screen 0.98
+
+set title "Morse code sent using phone rings and pulse-distance modulation"
+set xlabel "time since the first call (seconds)"
+
+set noytics
+
+set xtics 20 out nomirror rotate by -45
+
+set yrange[0:0.7]
+set size ratio 0.3
+
+set style arrow 1 heads size 2,90 front ls 201 lc 'gray'
+
+_w = 1280
+_h = _w * 0.39
+
+# a helper function
+min(a, b) = (a < b) ? a : b
+
+# The data files
+transmit_data="codex_transmit.log"
+receive_data="codex_receive.log"
+#transmit_data="savemysugar_transmit.log"
+#receive_data="savemysugar_receive.log"
+
+set terminal unknown
+
+plot transmit_data using 1:(1)
+sender_t0 = GPVAL_DATA_X_MIN
+
+plot receive_data using 1:(1)
+receiver_t0 = GPVAL_DATA_X_MIN
+
+t0 = min(sender_t0, receiver_t0)
+
+call_setup_time = receiver_t0 - sender_t0
+call_time_error = 1.2
+call_time = call_setup_time + call_time_error
+
+morse_symbol(symbol) = symbol eq " " ? "sep" : symbol eq "/" ? "SEP" : symbol
+end_of_signal(symbol) = (symbol eq " ") || (symbol eq "/") || (symbol eq "EOM")
+text_character(symbol, character) = symbol eq "/" ? character . "␣" : character
+
+set terminal qt 0 enhanced font "Georgia,14" size _w,_h
+#set terminal pngcairo notransparent  nocrop truecolor rounded enhanced font "Georgia,14" fontscale 1.0 size _w,_h
+#set output 'pulse_distance_modulation.png'
+
+plot \
+  transmit_data using (0):(0):($1 - t0):($1 - t0 + call_time):(0):(0.5) with boxxy \
+    fs solid noborder lc "#90caf9" title 'outgoing calls', \
+  receive_data using ($1 - t0):(0.5) with impulses lw 2 lc '#ff3d00' title 'ingoing rings', \
+  receive_data using ($4 > 0 ? $1 - t0 : 1/0):(0.53):(-$4):(0) with vectors arrowstyle 1 notitle, \
+  receive_data using ($4 > 0 ? ($1 - t0) - ($4 / 2) : 1/0):(0.56):(morse_symbol(strcol(6))) with labels notitle, \
+  receive_data using (end_of_signal(strcol(6)) ? ($1 - t0) : 1/0):(0.61):(text_character(strcol(6), strcol(8))) with labels font "Courier,14" notitle, \
diff --git a/plot/codex_receive.log b/plot/codex_receive.log
new file mode 100644 (file)
index 0000000..05db781
--- /dev/null
@@ -0,0 +1,21 @@
+1449670994.957390 INFO:log_symbol distance: 0.00 Received "" (The very first ring)
+1449671014.665521 INFO:log_symbol distance: 19.71 Received "-" 
+1449671030.670410 INFO:log_symbol distance: 16.01 Received "." 
+1449671050.785999 INFO:log_symbol distance: 20.12 Received "-" 
+1449671066.070046 INFO:log_symbol distance: 15.28 Received "." 
+1449671091.500978 INFO:log_symbol distance: 25.43 Received " " got "c"
+1449671112.640955 INFO:log_symbol distance: 21.14 Received "-" 
+1449671132.990636 INFO:log_symbol distance: 20.35 Received "-" 
+1449671153.427221 INFO:log_symbol distance: 20.44 Received "-" 
+1449671179.055582 INFO:log_symbol distance: 25.63 Received " " got "o"
+1449671199.486596 INFO:log_symbol distance: 20.43 Received "-" 
+1449671215.024292 INFO:log_symbol distance: 15.54 Received "." 
+1449671230.094973 INFO:log_symbol distance: 15.07 Received "." 
+1449671255.788843 INFO:log_symbol distance: 25.69 Received " " got "d"
+1449671271.266586 INFO:log_symbol distance: 15.48 Received "." 
+1449671296.885915 INFO:log_symbol distance: 25.62 Received " " got "e"
+1449671316.966002 INFO:log_symbol distance: 20.08 Received "-" 
+1449671332.818059 INFO:log_symbol distance: 15.85 Received "." 
+1449671347.603330 INFO:log_symbol distance: 14.79 Received "." 
+1449671368.286869 INFO:log_symbol distance: 20.68 Received "-" 
+1449671404.531392 INFO:log_symbol distance: 36.24 Received "EOM" got "x"
diff --git a/plot/codex_transmit.log b/plot/codex_transmit.log
new file mode 100644 (file)
index 0000000..254c1cf
--- /dev/null
@@ -0,0 +1,21 @@
+1449670986.557800 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671007.066112 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671022.369606 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671042.877515 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671058.174628 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671083.885830 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671104.398561 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671124.910546 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671145.422644 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671171.139232 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671191.649726 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671206.955485 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671222.262631 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671247.979692 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671263.281866 INFO:transmit_symbol Dial and wait 25.10 seconds (transmitting ' ')
+1449671288.990876 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671309.502568 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671324.806638 INFO:transmit_symbol Dial and wait 14.70 seconds (transmitting '.')
+1449671340.113764 INFO:transmit_symbol Dial and wait 19.90 seconds (transmitting '-')
+1449671360.626544 INFO:transmit_symbol Dial and wait 35.50 seconds (transmitting 'EOM')
+1449671396.756532 INFO:transmit_symbol Dial and wait 9.50 seconds (transmitting 'None')
diff --git a/plot/savemysugar_receive.log b/plot/savemysugar_receive.log
new file mode 100644 (file)
index 0000000..18e7843
--- /dev/null
@@ -0,0 +1,42 @@
+1449767078.298480 INFO:log_symbol distance: 0.00 Received "" (The very first ring)
+1449767093.801842 INFO:log_symbol distance: 15.50 Received "." 
+1449767109.398245 INFO:log_symbol distance: 15.60 Received "." 
+1449767124.998858 INFO:log_symbol distance: 15.60 Received "." 
+1449767151.817434 INFO:log_symbol distance: 26.82 Received " " got "s"
+1449767167.063156 INFO:log_symbol distance: 15.25 Received "." 
+1449767188.460764 INFO:log_symbol distance: 21.40 Received "-" 
+1449767214.667315 INFO:log_symbol distance: 26.21 Received " " got "a"
+1449767229.979030 INFO:log_symbol distance: 15.31 Received "." 
+1449767246.120717 INFO:log_symbol distance: 16.14 Received "." 
+1449767262.340340 INFO:log_symbol distance: 16.22 Received "." 
+1449767283.399982 INFO:log_symbol distance: 21.06 Received "-" 
+1449767309.184571 INFO:log_symbol distance: 25.78 Received " " got "v"
+1449767325.035770 INFO:log_symbol distance: 15.85 Received "." 
+1449767357.800701 INFO:log_symbol distance: 32.76 Received "/" got "e"
+1449767377.463256 INFO:log_symbol distance: 19.66 Received "-" 
+1449767398.644859 INFO:log_symbol distance: 21.18 Received "-" 
+1449767424.584395 INFO:log_symbol distance: 25.94 Received " " got "m"
+1449767446.529010 INFO:log_symbol distance: 21.94 Received "-" 
+1449767461.247696 INFO:log_symbol distance: 14.72 Received "." 
+1449767482.329433 INFO:log_symbol distance: 21.08 Received "-" 
+1449767503.725013 INFO:log_symbol distance: 21.40 Received "-" 
+1449767535.224446 INFO:log_symbol distance: 31.50 Received "/" got "y"
+1449767551.420112 INFO:log_symbol distance: 16.20 Received "." 
+1449767566.244822 INFO:log_symbol distance: 14.82 Received "." 
+1449767583.211545 INFO:log_symbol distance: 16.97 Received "." 
+1449767608.362101 INFO:log_symbol distance: 25.15 Received " " got "s"
+1449767625.392787 INFO:log_symbol distance: 17.03 Received "." 
+1449767640.819488 INFO:log_symbol distance: 15.43 Received "." 
+1449767660.902081 INFO:log_symbol distance: 20.08 Received "-" 
+1449767688.683652 INFO:log_symbol distance: 27.78 Received " " got "u"
+1449767709.581140 INFO:log_symbol distance: 20.90 Received "-" 
+1449767730.097842 INFO:log_symbol distance: 20.52 Received "-" 
+1449767745.651483 INFO:log_symbol distance: 15.55 Received "." 
+1449767771.709129 INFO:log_symbol distance: 26.06 Received " " got "g"
+1449767788.010725 INFO:log_symbol distance: 16.30 Received "." 
+1449767808.022360 INFO:log_symbol distance: 20.01 Received "-" 
+1449767834.800986 INFO:log_symbol distance: 26.78 Received " " got "a"
+1449767850.402655 INFO:log_symbol distance: 15.60 Received "." 
+1449767871.284281 INFO:log_symbol distance: 20.88 Received "-" 
+1449767887.303974 INFO:log_symbol distance: 16.02 Received "." 
+1449767924.244262 INFO:log_symbol distance: 36.94 Received "EOM" got "r"
diff --git a/plot/savemysugar_transmit.log b/plot/savemysugar_transmit.log
new file mode 100644 (file)
index 0000000..6f2e32b
--- /dev/null
@@ -0,0 +1,42 @@
+1449767070.136624 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767085.946654 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767101.756635 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767117.564704 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767143.780804 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767159.588935 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767180.604049 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767206.825262 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767222.635230 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767238.445194 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767254.255166 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767275.269486 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767301.482446 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767317.292965 INFO:transmit_symbol Dial and wait 30.80 seconds (transmitting '/')
+1449767348.716438 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767369.731551 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767390.746612 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767416.952798 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767437.967617 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767453.773552 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767474.785972 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767495.794459 INFO:transmit_symbol Dial and wait 30.80 seconds (transmitting '/')
+1449767527.220392 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767543.029056 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767558.839412 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767574.639571 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767600.857857 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767616.664918 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767632.470437 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767653.485939 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767679.703824 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767700.718621 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767721.733658 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767737.538787 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767763.753572 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767779.563234 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767800.575923 INFO:transmit_symbol Dial and wait 25.60 seconds (transmitting ' ')
+1449767826.789688 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767842.599629 INFO:transmit_symbol Dial and wait 20.40 seconds (transmitting '-')
+1449767863.614820 INFO:transmit_symbol Dial and wait 15.20 seconds (transmitting '.')
+1449767879.418044 INFO:transmit_symbol Dial and wait 36.00 seconds (transmitting 'EOM')
+1449767916.043168 INFO:transmit_symbol Dial and wait 10.00 seconds (transmitting 'None')
diff --git a/src/measure_call_setup_time.py b/src/measure_call_setup_time.py
new file mode 100755 (executable)
index 0000000..6268145
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+#
+# measure_call_setup_time - measure time between dialing out and ringing in
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from savemysugar.cumulative_average import cumulative_average
+from savemysugar.Modem import Modem
+import time
+
+
+class Ring(object):
+    time = -1
+
+
+def on_ring():
+    if Ring.time == -1:
+        print("First ring")
+        Ring.time = time.time()
+        raise StopIteration("We just want the first ring")
+
+
+def measure_call_setup_time(ingoing_port, outgoing_port, destination_number):
+    ingoing_modem = Modem(ingoing_port)
+    ingoing_modem.register_callback("RING", on_ring)
+
+    outgoing_modem = Modem(outgoing_port)
+
+    min_call_time = 10000
+    max_call_time = -1
+    min_call_setup_time = 10000
+    max_call_setup_time = -1
+
+    avg_call_setup_time = 0
+
+    for i in range(5):
+        print()
+        print("Attempt: %s" % (i + 1))
+        outgoing_modem.send_command("ATDT" + destination_number)
+        dial_time = time.time()
+
+        # Wait for the receiver to get a ring before terminating the call
+        try:
+            ingoing_modem.get_response_loop()
+        except (StopIteration, KeyboardInterrupt):
+            outgoing_modem.send_command("ATH")
+            outgoing_modem.get_response()
+
+        hangup_time = time.time()
+
+        # When dialing with an analog modem I noticed that if calls are
+        # separated one from another they take less time to set up, this
+        # may be due to how an analog modem works: getting it on-hook and
+        # off-hook takes quite some time.
+        inter_call_sleep = 5.2
+        print("After hangup sleeping %.2f sec..." % inter_call_sleep)
+        time.sleep(inter_call_sleep)
+
+        call_setup_time = Ring.time - dial_time
+        min_call_setup_time = min(min_call_setup_time, call_setup_time)
+        max_call_setup_time = max(max_call_setup_time, call_setup_time)
+        avg_call_setup_time = cumulative_average(avg_call_setup_time,
+                                                 i + 1,
+                                                 call_setup_time)
+        uncertainty = (max_call_setup_time - min_call_setup_time) / 2.
+        print("Call setup time: %f" % call_setup_time)
+        print("Min call setup time: %f" % min_call_setup_time)
+        print("Max call setup time: %f" % max_call_setup_time)
+        print("Average call setup time: %f" % avg_call_setup_time)
+        print("Call setup time uncertainty: %f" % uncertainty)
+        print()
+
+        call_time = hangup_time - dial_time
+        min_call_time = min(min_call_time, call_time)
+        max_call_time = max(max_call_time, call_time)
+        print("Total call time: %f" % call_time)
+        print("Min call time: %f" % min_call_time)
+        print("Max call time: %f" % max_call_time)
+        print()
+        Ring.time = -1
+
+
+def main():
+    import sys
+    if len(sys.argv) != 4:
+        print("usage: %s" % sys.argv[0],
+              "<ingoing serial port> <outgoing serial port>",
+              "<destination number>")
+        sys.exit(1)
+
+    measure_call_setup_time(sys.argv[1], sys.argv[2], sys.argv[3])
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/measure_ring_distance.py b/src/measure_ring_distance.py
new file mode 100755 (executable)
index 0000000..87f6da3
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+#
+# measure_ring_distance - measure the time between two rings in the same call
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from savemysugar.cumulative_average import cumulative_average
+from savemysugar.Modem import Modem
+import time
+
+
+class Rings(object):
+    previous_time = -1
+    average_distance = 0
+    min_distance = 10000
+    max_distance = -1
+    count = 0
+
+
+def on_ring():
+    Rings.count += 1
+
+    if Rings.previous_time == -1:
+        print("First ring")
+        Rings.previous_time = time.time()
+    else:
+        new_time = time.time()
+        distance = new_time - Rings.previous_time
+        Rings.previous_time = new_time
+        Rings.min_distance = min(Rings.min_distance, distance)
+        Rings.max_distance = max(Rings.max_distance, distance)
+        Rings.average_distance = cumulative_average(Rings.average_distance,
+                                                    Rings.count - 1, distance)
+        print()
+        print("Other ring")
+        print("distance: %f" % distance)
+        print("min_distance: %f" % Rings.min_distance)
+        print("max_distance: %f" % Rings.max_distance)
+        print("average_distance: %f" % Rings.average_distance)
+        uncertainty = (Rings.max_distance - Rings.min_distance) / 2.
+        print("uncertainty: %f" % uncertainty)
+
+
+def measure_ring_distance(ingoing_port, outgoing_port, destination_number):
+    ingoing_modem = Modem(ingoing_port)
+    ingoing_modem.register_callback("RING", on_ring)
+    outgoing_modem = Modem(outgoing_port)
+
+    outgoing_modem.send_command("ATDT" + destination_number)
+
+    try:
+        ingoing_modem.get_response_loop()
+    except KeyboardInterrupt:
+        outgoing_modem.send_command("ATH")
+        outgoing_modem.get_response()
+
+
+def main():
+    import sys
+    if len(sys.argv) != 4:
+        print("usage: %s" % sys.argv[0],
+              "<ingoing serial port> <outgoing serial port>",
+              "<destination number>")
+        sys.exit(1)
+
+    measure_ring_distance(sys.argv[1], sys.argv[2], sys.argv[3])
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/receive.py b/src/receive.py
new file mode 100755 (executable)
index 0000000..042ec63
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# SaveMySugar - receive messages using Morse code via phone rings
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from savemysugar.CallDistanceTransceiver import CallDistanceTransceiver
+from savemysugar.Modem import Modem
+import logging
+
+# TODO: set the logging level from the command line
+FORMAT = "%(created)f %(levelname)s:%(funcName)s %(message)s"
+logging.basicConfig(level=logging.DEBUG, format=FORMAT)
+
+
+def receive(port):
+    modem = Modem(port)
+
+    receiver = CallDistanceTransceiver(modem)
+
+    while True:
+        receiver.receive_loop()
+        print()
+        print("Message received!")
+        print((receiver.get_text()))
+
+
+def main():
+    print("Available hardware serial ports:")
+    for port in Modem.scan():
+        print(("  - " + port))
+    print()
+
+    import sys
+    if len(sys.argv) != 2:
+        print("usage: %s <serial port>" % sys.argv[0])
+        sys.exit(1)
+
+    receive(sys.argv[1])
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/savemysugar/CallDistanceTransceiver.py b/src/savemysugar/CallDistanceTransceiver.py
new file mode 100755 (executable)
index 0000000..fd60a1f
--- /dev/null
@@ -0,0 +1,329 @@
+#!/usr/bin/env python3
+#
+# CallDistanceTransceiver - send and receive Morse using phone calls distance
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+
+# This hack allows MorseTranslator to be imported also when
+# __name__ == "__main__"
+try:
+    from .MorseTranslator import MorseTranslator
+except SystemError:
+    from MorseTranslator import MorseTranslator
+
+import logging
+import time
+
+
+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_average=8.5, call_setup_uncertainty=1.5,
+                 ring_distance_average=5, ring_uncertainty=0.2):
+        """Encode the Morse symbols using the distance between calls.
+
+        Args:
+            call_setup_average: the time between when the transmitter dials
+                the number and the receiver reports RING notifications. This is
+                needed in order to be sure than the receiver has received
+                _at_least_ one ring. The default chosen here should work fine
+                even with old analog modems, provided that there is some more
+                distance between two calls.
+            call_setup_uncertainty: uncertainty of call_setup_average,
+                a tolerance value
+            ring_distance_average: the time between two consecutive RINGs
+                in the same call, the standard interval is about 5 seconds, but
+                some tolerance is needed to account for line delays.
+                This is needed in order to ignore rings in the same call, and
+                differentiate two different calls.
+            ring_uncertainty: uncertainty of ring_distance_average, a tolerance
+                value
+        """
+
+        self.modem = modem
+        self.translator = MorseTranslator()
+
+        self.destination_number = ""
+
+        self.call_setup_time = call_setup_average + call_setup_uncertainty
+        self.rings_distance = ring_distance_average + ring_uncertainty
+
+        # In theory the symbol distance, the distance between calls which
+        # represent symbols, can be arbitrary, but in practice it's better to
+        # wait at least the duration of a ring between terminating one call and
+        # initiating the next call, as pick-up and hang-up can take some time
+        # with old analog modems.
+        symbol_distance = self.rings_distance
+
+        def symbol_time(multiplier):
+            return self.call_setup_time + symbol_distance * multiplier
+
+        self.dot_time = symbol_time(1)
+        self.dash_time = symbol_time(2)
+        self.signalspace_time = symbol_time(3)
+        self.wordspace_time = symbol_time(4)
+        self.eom_time = symbol_time(5)
+
+        self.ring_uncertainty = ring_uncertainty
+        self.symbol_uncertainty = symbol_distance / 2.
+
+        logging.debug("dot time:         transmit: %.2f receive: (%.2f, %.2f)",
+                      self.dot_time,
+                      (self.dot_time - self.symbol_uncertainty),
+                      (self.dot_time + self.symbol_uncertainty))
+        logging.debug("dash time:        transmit: %.2f receive: (%.2f, %.2f)",
+                      self.dash_time,
+                      (self.dash_time - self.symbol_uncertainty),
+                      (self.dash_time + self.symbol_uncertainty))
+        logging.debug("signalspace time: transmit: %.2f receive: (%.2f, %.2f)",
+                      self.signalspace_time,
+                      (self.signalspace_time - self.symbol_uncertainty),
+                      (self.signalspace_time + self.symbol_uncertainty))
+        logging.debug("wordspace time:   transmit: %.2f receive: (%.2f, %.2f)",
+                      self.wordspace_time,
+                      (self.wordspace_time - self.symbol_uncertainty),
+                      (self.wordspace_time + self.symbol_uncertainty))
+        logging.debug("EOM time:         transmit: %.2f receive: (%.2f, +inf)",
+                      self.eom_time,
+                      (self.eom_time - self.symbol_uncertainty))
+
+        self.previous_ring_time = -1
+        self.previous_call_time = -1
+
+        self.morse_message = ""
+        self.text_message = ""
+
+        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):
+        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)")
+            return
+        else:
+            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 abs(ring_distance - self.rings_distance) < self.ring_uncertainty:
+            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
+
+        if abs(call_distance - self.dot_time) < self.symbol_uncertainty:
+            self.log_symbol(call_distance, '.')
+            self.morse_message += "."
+            return
+
+        if abs(call_distance - self.dash_time) < self.symbol_uncertainty:
+            self.log_symbol(call_distance, '-')
+            self.morse_message += "-"
+            return
+
+        if abs(call_distance - self.signalspace_time) < self.symbol_uncertainty:
+            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
+
+        if abs(call_distance - self.wordspace_time) < self.symbol_uncertainty:
+            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
+
+        if call_distance >= self.eom_time - self.symbol_uncertainty:
+            signal = self.morse_message.strip().split(' ')[-1]
+            character = self.translator.signal_to_character(signal)
+            self.log_symbol(call_distance, 'EOM', "got \"%s\"" % character)
+            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", ring_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()
+            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, symbol):
+        if symbol == ".":
+            sleep_time = self.dot_time
+        elif symbol == "-":
+            sleep_time = self.dash_time
+        elif symbol == " ":
+            sleep_time = self.signalspace_time
+        elif symbol == "/":
+            sleep_time = self.wordspace_time
+        elif symbol == "EOM":
+            sleep_time = self.eom_time
+        elif symbol is None:
+            # To terminate the transmission just call and hangup, with no extra
+            # distance
+            sleep_time = self.call_setup_time
+
+        logging.info("Dial and wait %.2f seconds (transmitting '%s')",
+                     sleep_time, symbol)
+
+        # Dial, then wait self.call_setup_time to make sure the receiver gets
+        # at least one RING, and then hangup and sleep the remaining time
+        self.modem.send_command("ATDT" + self.destination_number)
+        time.sleep(self.call_setup_time)
+        self.modem.send_command("ATH")
+        self.modem.get_response()
+        time.sleep(sleep_time - self.call_setup_time)
+
+    def transmit_signal(self, signal):
+        logging.debug("Transmitting signal: %s", signal)
+        for symbol in signal:
+            self.transmit_symbol(symbol)
+
+    def transmit(self, message, destination_number):
+        self.destination_number = destination_number
+
+        morse_message = self.translator.text_to_morse(message)
+        signals = morse_message.split()
+        print(signals)
+
+        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")
+
+        # 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)
+
+    def estimate_transmit_duration(self, message):
+        morsemessage = self.translator.text_to_morse(message)
+        signals = morsemessage.split()
+
+        logging.debug(signals)
+
+        transmitting_time = 0
+        for i, signal in enumerate(signals):
+            logging.debug("signal: %s", signal)
+
+            for symbol in signal:
+                if symbol == ".":
+                    transmitting_time += self.dot_time
+                elif symbol == "-":
+                    transmitting_time += self.dash_time
+                elif symbol == "/":
+                    transmitting_time += self.wordspace_time
+
+                if i != len(signals) - 1 and signals[i + 1] != "/" and symbol != "/":
+                    transmitting_time += self.signalspace_time
+
+        transmitting_time += self.eom_time
+
+        logging.debug("Estimated transmitting time: %d seconds",
+                      transmitting_time)
+
+
+def test_send_receive():
+    logging.basicConfig(level=logging.DEBUG)
+    call_setup_time = 2
+    call_setup_uncertainty = 0.4
+    ring_time = 1
+    ring_uncertainty = 0.3
+
+    class DummyModem(object):
+        """Always receive a '.' and then a '/', which result in 'E '."""
+
+        def __init__(self):
+            self.ring_count = 0
+
+        def send_command(self, command):
+            pass
+
+        def get_response(self, response):
+            if self.ring_count % 2:
+                # received a '.'
+                time.sleep(call_setup_time + (ring_time + ring_uncertainty))
+            else:
+                # received a '/'
+                time.sleep(call_setup_time + (ring_time + ring_uncertainty) * 4)
+
+            self.ring_count += 1
+
+    xcv = CallDistanceTransceiver(DummyModem(), call_setup_time,
+                                  call_setup_uncertainty, ring_time,
+                                  ring_uncertainty)
+    xcv.receive_loop()
+
+
+if __name__ == "__main__":
+    test_send_receive()
diff --git a/src/savemysugar/Modem.py b/src/savemysugar/Modem.py
new file mode 100755 (executable)
index 0000000..e510382
--- /dev/null
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+#
+# Modem - encapsulate serial modem functionalities
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import logging
+import re
+import serial
+
+
+class Modem(object):
+    def __init__(self, serialport, baudrate=9600):
+        self.timeout = 0.1
+        self.init_commands = ["Z", "X3"]
+
+        self.callbacks = {}
+
+        self.open(serialport, baudrate)
+
+    @staticmethod
+    def scan():
+        """Scan for available hardware serial ports.
+
+        The code does not scan for virtual serial ports (e.g. /dev/ttyACM*,
+        /dev/ttyUSB* on linux).
+
+        Return:
+            a dict {portname: num}
+        """
+        available = {}
+        for i in range(256):
+            try:
+                port = serial.Serial(i)
+                available[port.portstr] = i
+                port.close()
+            except (OSError, serial.SerialException):
+                pass
+        return available
+
+    def open(self, port, baudrate):
+        self.serial = serial.Serial(port, baudrate=baudrate,
+                                    timeout=self.timeout,
+                                    rtscts=0, xonxoff=0)
+        self.reset()
+
+    def close(self):
+        self.reset()
+        self.serial.close()
+
+    def reset(self):
+        for command in self.init_commands:
+            self.send_command(command)
+            self.get_response("OK")
+
+    def flush(self):
+        self.serial.flushInput()
+        self.serial.flushOutput()
+
+    def convert_cr_lf(self, command):
+        if command.endswith("\r\n"):
+            pass
+        elif command.endswith("\n"):
+            command = command[:-1] + "\r\n"
+        elif command.endswith("\r"):
+            command += "\n"
+        else:
+            command += "\r\n"
+
+        return command
+
+    def send_command(self, command):
+        if command.upper().startswith("AT"):
+            command = command[2:]
+
+        command = "AT" + command
+        command = self.convert_cr_lf(command)
+        logging.debug("AT command: %s", repr(command))
+
+        self.flush()
+        self.serial.write(bytes(command, 'UTF-8'))
+
+    def get_response(self, expected=None):
+        std_responses = ["OK", "RING", "ERROR", "BUSY", "NO CARRIER",
+                         "NO DIALTONE"]
+
+        resultlist = []
+        line = ""
+        while 1:
+            data = self.serial.read(1)
+            if len(data):
+                line += data.decode('UTF-8')
+                if line.endswith("\r\n"):
+                    # strip '\r', '\n' and other spaces
+                    line = re.sub(r"\s+$", "", line)
+                    line = re.sub(r"^\s+", "", line)
+
+                    if len(line):
+                        resultlist.append(line)
+
+                        # Only react on std_responses when NOT asked explicitly
+                        # for an expected response
+                        if (expected and (line == expected)) or \
+                                (not expected and (line in std_responses)):
+                            if line in self.callbacks:
+                                func = self.callbacks[line]
+                                func()
+                            return resultlist
+
+                        line = ""
+
+    def get_response_loop(self):
+        while 1:
+            self.get_response()
+
+    def register_callback(self, response, callback_func):
+        self.callbacks[response] = callback_func
+
+
+def test(port, number):
+    logging.basicConfig(level=logging.DEBUG)
+
+    def on_ok():
+        print("Received an OK")
+
+    def on_ring():
+        print("Phone Ringing")
+
+    import time
+
+    def test_send(port, number):
+        modem = Modem(port)
+
+        modem.register_callback("OK", on_ok)
+
+        time1 = 0
+        time2 = 0
+
+        for i in range(3):
+            time_difference = time2 - time1
+            print("time difference: %.2f" % time_difference)
+            time1 = time.time()
+            modem.send_command("ATDT" + number)
+
+            call_setup_time = 9
+            inter_call_sleep = 5
+
+            print("sleep %.2f" % call_setup_time)
+            time.sleep(call_setup_time)
+
+            modem.send_command("ATH")
+            modem.get_response()
+
+            print("sleep %.2f" % inter_call_sleep)
+            time.sleep(inter_call_sleep)
+
+            time2 = time.time()
+
+    def test_receive(port):
+        modem = Modem(port)
+
+        modem.register_callback("OK", on_ok)
+
+        modem.send_command("I1")
+        response = modem.get_response()
+        print("response:", response)
+
+        modem.register_callback("RING", on_ring)
+        modem.get_response_loop()
+
+    test_send(port, number)
+    test_receive(port)
+
+
+if __name__ == "__main__":
+    import sys
+    print(Modem.scan())
+    if len(sys.argv) != 3:
+        print("usage: %s <serial port> <destination number>" % sys.argv[0])
+        sys.exit(1)
+    test(sys.argv[1], sys.argv[2])
diff --git a/src/savemysugar/MorseTranslator.py b/src/savemysugar/MorseTranslator.py
new file mode 100755 (executable)
index 0000000..8184829
--- /dev/null
@@ -0,0 +1,227 @@
+#!/usr/bin/env python3
+#
+# MorseTranslator - translate to and from Morse code
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+import re
+
+
+class MorseTranslator(object):
+    """International Morse Code translator.
+
+    The specification of the International Morse Code is in ITU-R M.1677-1
+    (10/2009), Annex 1.
+
+    The terminology used here may differ from the one used in some other
+    places, so here is some nomenclature:
+
+        symbol: one of . (dot), - (dash), ' ' (signal separator),
+            '/' (word separator)
+
+        character: a letter of the alphabet, a number, a punctuation mark, or
+            a ' ' (text word separator)
+
+        signal: a sequence of . and - symbols which encode a character,
+            or a '/' (Morse word separator)
+
+        word: a sequence of characters not containing a ' ', or
+            a sequence of signals not containing a '/'
+
+        text: a sequence of characters
+
+        morse: a sequence of signals separated by ' '
+
+    NOTE:
+    signals are separated by a ' ' (signal separator), characters are not
+    separated one from the other.
+
+    This class defines a subset of the signals in Section 1 of the
+    aforementioned specification, plus a word space, and it does not make
+    assumptions about their actual transmission.
+    """
+
+    def __init__(self):
+        self.signals_table = {}
+        self.characters_table = {}
+
+        # XXX the current code only handles single characters,
+        # so prosigns are not added to the tables below
+
+        # Letters
+        self.signals_table['a'] = ".-"
+        self.signals_table['b'] = "-..."
+        self.signals_table['c'] = "-.-."
+        self.signals_table['d'] = "-.."
+        self.signals_table['e'] = "."
+        self.signals_table['f'] = "..-."
+        self.signals_table['g'] = "--."
+        self.signals_table['h'] = "...."
+        self.signals_table['i'] = ".."
+        self.signals_table['j'] = ".---"
+        self.signals_table['k'] = "-.-"
+        self.signals_table['l'] = ".-.."
+        self.signals_table['m'] = "--"
+        self.signals_table['n'] = "-."
+        self.signals_table['o'] = "---"
+        self.signals_table['p'] = ".--."
+        self.signals_table['q'] = "--.-"
+        self.signals_table['r'] = ".-."
+        self.signals_table['s'] = "..."
+        self.signals_table['t'] = "-"
+        self.signals_table['u'] = "..-"
+        self.signals_table['v'] = "...-"
+        self.signals_table['w'] = ".--"
+        self.signals_table['x'] = "-..-"
+        self.signals_table['y'] = "-.--"
+        self.signals_table['z'] = "--.."
+        # Figures
+        self.signals_table['1'] = ".----"
+        self.signals_table['2'] = "..---"
+        self.signals_table['3'] = "...--"
+        self.signals_table['4'] = "....-"
+        self.signals_table['5'] = "....."
+        self.signals_table['6'] = "-...."
+        self.signals_table['7'] = "--..."
+        self.signals_table['8'] = "---.."
+        self.signals_table['9'] = "----."
+        self.signals_table['0'] = "-----"
+        # Punctuation marks and miscellaneous signs
+        self.signals_table['.'] = ".-.-.-"
+        self.signals_table[','] = "--..--"
+        self.signals_table[':'] = "---..."
+        self.signals_table['?'] = "..--.."
+        self.signals_table['\''] = ".----."
+        self.signals_table['-'] = "-....-"
+        self.signals_table['/'] = "-..-."
+        self.signals_table['('] = "-.--."
+        self.signals_table[')'] = "-.--.-"
+        self.signals_table['"'] = ".-..-."
+        self.signals_table['='] = "-...-"
+        self.signals_table['+'] = ".-.-."
+        self.signals_table['x'] = "-..-"
+        self.signals_table['@'] = ".--.-."
+
+        # Represent the word space as a signal with only one "/" symbol
+        self.signals_table[' '] = "/"
+
+        for key, value in self.signals_table.items():
+            self.characters_table[value] = key
+
+    def stats(self):
+        signal_length_sum = 0
+        for signal in self.signals_table.values():
+            signal_length_sum += len(signal)
+
+        average_signal_length = signal_length_sum / len(self.signals_table)
+
+        character_length_sum = 0
+        for character in self.characters_table.values():
+            character_length_sum += len(character)
+
+        average_char_length = character_length_sum / len(self.characters_table)
+
+        return average_signal_length, average_char_length
+
+    def sanitize_text(self, text):
+        sanitized = text.lower()
+        sanitized = re.sub(r"[^a-z0-9.,?\'\"/() \-=\+@]", "", sanitized)
+        sanitized = re.sub(r"\s+", " ", sanitized)
+        sanitized = re.sub(r"^\s+", "", sanitized)
+        sanitized = re.sub(r"\s+$", "", sanitized)
+        return sanitized
+
+    def char_to_signal(self, character):
+        char = character.lower()
+        if char in self.signals_table:
+            return self.signals_table[char]
+        else:
+            return ""
+
+    def text_to_morse(self, text, sanitize=True):
+        if sanitize:
+            text = self.sanitize_text(text)
+
+        signal = [self.char_to_signal(c) for c in text]
+        return str(" ").join(signal)
+
+    def sanitize_morse(self, morse):
+        sanitized = re.sub("_", "-", morse)
+        sanitized = re.sub(r"[^\-\.\/]", " ", sanitized)
+        sanitized = re.sub(r"\|", "/", sanitized)
+        sanitized = re.sub(r"\s+", " ", sanitized)
+        sanitized = re.sub(r"( ?/ ?)+", " / ", sanitized)
+        sanitized = re.sub(r"^[ /]+", "", sanitized)
+        sanitized = re.sub(r"[ /]+$", "", sanitized)
+        return sanitized
+
+    def signal_to_character(self, signal):
+        if signal in self.characters_table:
+            return self.characters_table[signal]
+        else:
+            return '*'
+
+    def morse_to_text(self, morse, sanitize=True):
+        if sanitize:
+            morse = self.sanitize_morse(morse)
+
+        signals = morse.split()
+        characters = [self.signal_to_character(signal) for signal in signals]
+        return str('').join(characters)
+
+
+def test():
+    translator = MorseTranslator()
+    avg_signal_length, avg_character_length = translator.stats()
+    print("Average signal length:", avg_signal_length)
+    print("Average character length:", avg_character_length)
+
+    text = "Hello, I am just some text."
+
+    print(text)
+
+    morse = translator.text_to_morse(text)
+    print(morse)
+
+    text = translator.morse_to_text(morse)
+    print(text)
+
+    print("\n\nTesting sanitizing functions")
+
+    print()
+    dirty_text = '   <      >Hello::##        this is dirty^^%%  text!     '
+    print(dirty_text)
+    print(translator.sanitize_text(dirty_text))
+    print(translator.text_to_morse(translator.sanitize_text(dirty_text)))
+
+    print()
+    dirty_morse = ' 009 .... . ._.. .-.. --- /34// / // // - .... .. ...' + \
+        '    /    .. ... / -.. .. .-. - -.-- / - . -..- _   '
+    print(dirty_morse)
+    print(translator.sanitize_morse(dirty_morse))
+    print(translator.morse_to_text(translator.sanitize_morse(dirty_morse)))
+
+    print("\n\nTesting conversion on unsanitized strings")
+    print(dirty_text)
+    print(translator.text_to_morse(dirty_text))
+    print(translator.morse_to_text(translator.text_to_morse(dirty_text)))
+
+    print(dirty_morse)
+    print(translator.morse_to_text(dirty_morse))
+
+
+if __name__ == "__main__":
+    test()
diff --git a/src/savemysugar/__init__.py b/src/savemysugar/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/src/savemysugar/cumulative_average.py b/src/savemysugar/cumulative_average.py
new file mode 100755 (executable)
index 0000000..7200eb0
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+#
+# cumulative_average - compute the cumulative average
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+
+def cumulative_average(old_average, n_samples, new_sample):
+    return ((old_average * (n_samples - 1)) + new_sample) / n_samples
+
+
+def test():
+    values = [1, 2, 3, 4, 5, 6]
+    average = 0
+    for i, value in enumerate(values):
+        average = cumulative_average(average, i + 1, value)
+        print(average)
+
+if __name__ == "__main__":
+    test()
diff --git a/src/transmit.py b/src/transmit.py
new file mode 100755 (executable)
index 0000000..2e8ef4c
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# SaveMySugar - transmit messages using Morse code via phone rings
+#
+# Copyright (C) 2015  Antonio Ospite <ao2@ao2.it>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+from savemysugar.CallDistanceTransceiver import CallDistanceTransceiver
+from savemysugar.Modem import Modem
+import logging
+
+# TODO: set the logging level from the command line
+FORMAT = "%(created)f %(levelname)s:%(funcName)s %(message)s"
+logging.basicConfig(level=logging.DEBUG, format=FORMAT)
+
+
+def transmit(port, number, message):
+    modem = Modem(port)
+
+    transmitter = CallDistanceTransceiver(modem)
+    transmitter.estimate_transmit_duration(message)
+    transmitter.transmit(message, number)
+
+
+def main():
+    print("Available hardware serial ports:")
+    for port in Modem.scan():
+        print("  - " + port)
+    print()
+
+    import sys
+    if len(sys.argv) != 4:
+        print("usage: %s" % sys.argv[0],
+              "<serial port> <destination number> <message>")
+        sys.exit(1)
+    transmit(sys.argv[1], sys.argv[2], sys.argv[3])
+
+
+if __name__ == "__main__":
+    main()