aprs.py
  1. # -*- coding: UTF-8 -*-
  2. # Copyright (C) 2008, Jack Zielke <jack@linuxcoffee.com>
  3. # Copyright (C) 2008, One Laptop Per Child
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  18.  
  19. import gtk
  20. import pango
  21. import time
  22. import socket
  23. import random
  24. import gobject
  25.  
  26. from gettext import gettext as _
  27.  
  28. from sugar import profile
  29. from sugar.activity import activity
  30. from sugar.bundle.activitybundle import ActivityBundle
  31. from sugar.graphics.menuitem import MenuItem
  32. from sugar.graphics.toolbutton import ToolButton
  33.  
  34. TESTING = False
  35.  
  36. HOST = 'rotate.aprs2.net'
  37. #HOST = '192.168.50.6'
  38. PORT = 14580
  39. RECV_BUFFER = 4096
  40.  
  41. MAXLINES = 50
  42. MAXRETRIES = 15
  43.  
  44. bundle = ActivityBundle(activity.get_bundle_path())
  45. VERSION = bundle.get_activity_version()
  46. del bundle
  47.  
  48. class APRSActivity(activity.Activity):
  49.     def __init__(self, handle):
  50.         activity.Activity.__init__(self, handle)
  51.         self.set_title(_('APRS-XO Activity'))
  52.  
  53.         self.sock = None
  54.         self.location = "home"
  55.         self.site = None
  56.         self.sent_acks = {}
  57.         self.recv_acks = {}
  58.         self.timers = []
  59.         self.bulletins = ["AIR", "DGPS", "QST", "TEL", "ALL", "DRILL", "QTH", "TEST", "AP", "DX", "RTCM", "TLM", "BEACON", "ID", "SKY", "WX", "CQ", "JAVA", "SPACE", "ZIP", "GPS", "MAIL", "SPC", "DF", "MICE", "SYM", "BLN", "NWS", "NTS"]
  60.         self.validating = False
  61.         self.help = True
  62.         self.messagebox = False
  63.         self.sequence = random.randrange(0, 7000)
  64.  
  65.         titlefont = pango.FontDescription('Sans bold 8')
  66.         mediumfont = pango.FontDescription('Sans 6.5')
  67.         smallfont = pango.FontDescription('Sans 6')
  68.         verysmallfont = pango.FontDescription('Sans 4')
  69.         firstName = profile.get_nick_name().split(None, 1)[0].capitalize()
  70.  
  71.         toolbox = activity.ActivityToolbox(self)
  72.         self.set_toolbox(toolbox)
  73.  
  74.         activity_toolbar = toolbox.get_activity_toolbar()
  75.         activity_toolbar.share.props.visible = False
  76.         activity_toolbar.keep.props.visible = False
  77.  
  78.         toolbox.show()
  79.  
  80.         win = gtk.HBox(False, 10)
  81.         self.set_canvas(win)
  82.  
  83.         leftwin = gtk.VBox(False, 10)
  84.  
  85.         # Top 'about' box
  86.         aboutbox = gtk.VBox(False, 11)
  87.         aboutbox.set_border_width(10)
  88.  
  89.         topaboutbox = gtk.VBox(False, 0)
  90.  
  91.         titlebox = gtk.HBox(False, 0)
  92.         titlelabel = gtk.Label("APRS-XO:")
  93.         titlelabel.set_alignment(0, 0)
  94.         titlelabel.modify_font(titlefont)
  95.         titlebox.pack_start(titlelabel, False, False, 0)
  96.         titlelabel.show()
  97.         aboutlabel1 = gtk.Label("This amateur radio program will update your")
  98.         aboutlabel1.set_alignment(0, 0.8)
  99. #        aboutlabel1.modify_font(mediumfont)
  100.         titlebox.pack_start(aboutlabel1, False, False, 0)
  101.         aboutlabel1.show()
  102.         topaboutbox.pack_start(titlebox, False, False, 0)
  103.         titlebox.show()
  104.         aboutlabel2 = gtk.Label("positon & status on all of the global APRS web pages once\nevery 10 minutes.")
  105.         aboutlabel2.set_alignment(0, 0)
  106. #        aboutlabel2.modify_font(mediumfont)
  107.         topaboutbox.pack_start(aboutlabel2, False, False, 0)
  108.         aboutlabel2.show()
  109.         aboutbox.pack_start(topaboutbox, False, False, 0)
  110.         topaboutbox.show()
  111.  
  112.         sitebox = gtk.HBox(False, 10)
  113.         sitelabel = gtk.Label("Select an APRS site:")
  114.         sitelabel.set_alignment(0, 0.4)
  115.         sitebox.pack_start(sitelabel, False, False, 0)
  116.         sitelabel.show()
  117.  
  118.         findubutton = gtk.Button()
  119.         findubutton.set_label("FINDU.COM")
  120.         findubutton.connect("clicked", self.set_site, "http://www.findu.com/cgi-bin/symbol.cgi?icon=XA&limit=200")
  121.         sitebox.pack_start(findubutton, False, False, 0)
  122.         findubutton.show()
  123.  
  124.         aprsworldbutton = gtk.Button()
  125.         aprsworldbutton.set_label("APRSworld")
  126.         aprsworldbutton.connect("clicked", self.set_site, "http://aprsworld.net/")
  127.         sitebox.pack_start(aprsworldbutton, False, False, 0)
  128.         aprsworldbutton.show()
  129.  
  130.         otherbutton = gtk.Button()
  131.         otherbutton.set_label("About")
  132.         otherbutton.connect("clicked", self.set_site, "http://aprs.org/")
  133.         sitebox.pack_start(otherbutton, False, False, 0)
  134.         otherbutton.show()
  135.  
  136.         aboutbox.pack_start(sitebox, False, False, 0)
  137.         sitebox.show()
  138.  
  139. #        infobox = gtk.VBox(False, 4)
  140.  
  141. ##        infolabel = gtk.Label("With a ham license you can communicate worldwide with wireless and radio.\nWithout a ham license you can only communicate with other XO's on the\nInternet.")
  142. #        infolabel = gtk.Label("With a ham license you can communicate worldwide with wireless and radio.")
  143. #        infolabel.set_alignment(0, 0)
  144. #        infolabel.modify_font(smallfont)
  145. #        infobox.pack_start(infolabel, False, False, 0)
  146. #        infolabel.show()
  147.  
  148. #        aboutbox.pack_start(infobox, False, False, 0)
  149. #        infobox.show()
  150.  
  151.         leftwin.pack_start(aboutbox, False, False, 0)
  152.         aboutbox.show()
  153.  
  154.         separator = gtk.HSeparator()
  155.         leftwin.pack_start(separator, False, False, 0)
  156.         separator.show()
  157.  
  158.         # identifiers box
  159.         identbox = gtk.VBox(False, 4)
  160.         identbox.set_border_width(10)
  161.  
  162.         identlabel = gtk.Label("Identifiers")
  163.         identlabel.set_alignment(0, 0)
  164.         identlabel.modify_font(titlefont)
  165.         identbox.pack_start(identlabel, False, False, 0)
  166.         identlabel.show()
  167.  
  168.         bottomidentbox = gtk.HBox(False, 10)
  169.  
  170.         calllabel1 = gtk.Label("Callsign: ")
  171.         calllabel1.set_alignment(0, 0.5)
  172.         bottomidentbox.pack_start(calllabel1, False, False, 0)
  173.         calllabel1.show()
  174.  
  175.         self.calltext = gtk.Entry()
  176.         self.calltext.set_max_length(9)
  177.         self.calltext.set_width_chars(9)
  178.         self.calltext.connect("changed", self.disable_beacon)
  179.         bottomidentbox.pack_start(self.calltext, False, False, 0)
  180.         self.calltext.show()
  181.  
  182.         passlabel1 = gtk.Label("Password: ")
  183.         passlabel1.set_alignment(0, 0.5)
  184.         bottomidentbox.pack_start(passlabel1, False, False, 0)
  185.         passlabel1.show()
  186.  
  187.         self.passtext = gtk.Entry()
  188.         self.passtext.set_max_length(5)
  189.         self.passtext.set_width_chars(5)
  190.         self.passtext.set_invisible_char("x")
  191.         self.passtext.set_visibility(False)
  192.         bottomidentbox.pack_start(self.passtext, False, False, 0)
  193.         self.passtext.show()
  194.  
  195.         identbox.pack_start(bottomidentbox, False, False, 0)
  196.         bottomidentbox.show()
  197.  
  198.         passlabel2 = gtk.Label("optional")
  199.         passlabel2.set_alignment(0.74, 0)
  200.         passlabel2.modify_font(smallfont)
  201.         identbox.pack_start(passlabel2, False, False, 0)
  202.         passlabel2.show()
  203.  
  204.         leftwin.pack_start(identbox, False, False, 0)
  205.         identbox.show()
  206.  
  207.         separator = gtk.HSeparator()
  208.         leftwin.pack_start(separator, False, False, 0)
  209.         separator.show()
  210.  
  211.         # station box
  212.         stationbox = gtk.VBox(False, 11)
  213.         stationbox.set_border_width(10)
  214.  
  215.         stationlabel = gtk.Label("Station Comment")
  216.         stationlabel.set_alignment(0, 0)
  217.         stationlabel.modify_font(titlefont)
  218.         stationbox.pack_start(stationlabel, False, False, 0)
  219.         stationlabel.show()
  220.  
  221.         # so the text box does not fill all Horizontal space
  222.         stationtextbox = gtk.HBox()
  223.  
  224.         self.stationtext = gtk.Entry()
  225.         self.stationtext.set_max_length(43)
  226.         self.stationtext.set_width_chars(43)
  227.         self.stationtext.set_text("%s's XO at home." % firstName)
  228.         self.stationtext.connect("changed", self.disable_beacon)
  229.         stationtextbox.pack_start(self.stationtext, False, False, 0)
  230.         self.stationtext.show()
  231.         stationbox.pack_start(stationtextbox, False, False, 0)
  232.         stationtextbox.show()
  233.  
  234.         stationhelp = gtk.Label("Optional description of current position, status,\nor destination.  Enter up to 43 characters, most\nimportant first since some displays can only see\nthe first 20 or 28.")
  235.         stationhelp.set_alignment(0, 0)
  236. #        stationhelp.modify_font(mediumfont)
  237.         stationbox.pack_start(stationhelp, False, False, 0)
  238.         stationhelp.show()
  239.  
  240.         leftwin.pack_start(stationbox, False, False, 0)
  241.         stationbox.show()
  242.  
  243.         separator = gtk.HSeparator()
  244.         leftwin.pack_start(separator, False, False, 0)
  245.         separator.show()
  246.  
  247.         # position box
  248.         positbox = gtk.VBox(False, 11)
  249.         positbox.set_border_width(10)
  250.  
  251.         toppositbox = gtk.HBox(False, 0)
  252.  
  253.         topleftpositbox = gtk.VBox(False, 4)
  254.  
  255.         positlabel1 = gtk.Label("Position Data")
  256.         positlabel1.set_alignment(0, 0)
  257.         positlabel1.modify_font(titlefont)
  258.         topleftpositbox.pack_start(positlabel1, False, False, 0)
  259.         positlabel1.show()
  260.  
  261.         latpositbox = gtk.HBox(False, 4)
  262.         latlabel1 = gtk.Label("Latitude:    ")
  263.         latlabel1.set_alignment(0, 0.5)
  264.         latpositbox.pack_start(latlabel1, False, False, 0)
  265.         latlabel1.show()
  266.  
  267.         self.latDDtext = gtk.Entry()
  268.         self.latDDtext.set_max_length(2)
  269.         self.latDDtext.set_width_chars(4)
  270.         self.latDDtext.set_text("DD")
  271.         self.latDDtext.select_region(1,2)
  272.         self.latDDtext.connect("changed", self.disable_beacon)
  273.         latpositbox.pack_start(self.latDDtext, False, False, 0)
  274.         self.latDDtext.show()
  275.  
  276.         self.latMMtext = gtk.Entry()
  277.         self.latMMtext.set_max_length(2)
  278.         self.latMMtext.set_width_chars(3)
  279.         self.latMMtext.set_text("MM")
  280.         self.latMMtext.select_region(1,2)
  281.         self.latMMtext.connect("changed", self.disable_beacon)
  282.         latpositbox.pack_start(self.latMMtext, False, False, 0)
  283.         self.latMMtext.show()
  284.  
  285.         latlabel2 = gtk.Label(".")
  286.         latlabel2.set_alignment(0, 1)
  287.         latpositbox.pack_start(latlabel2, False, False, 0)
  288.         latlabel2.show()
  289.  
  290.         self.latmmtext = gtk.Entry()
  291.         self.latmmtext.set_max_length(2)
  292.         self.latmmtext.set_width_chars(3)
  293.         self.latmmtext.set_text("mm")
  294.         self.latmmtext.select_region(1,2)
  295.         self.latmmtext.connect("changed", self.disable_beacon)
  296.         latpositbox.pack_start(self.latmmtext, False, False, 0)
  297.         self.latmmtext.show()
  298.  
  299.         self.latcombo = gtk.combo_box_new_text()
  300.         self.latcombo.append_text("N")
  301.         self.latcombo.append_text("S")
  302.         self.latcombo.set_active(0)
  303.         latpositbox.pack_start(self.latcombo, False, False, 0)
  304.         self.latcombo.show()
  305.  
  306.         topleftpositbox.pack_start(latpositbox, False, False, 0)
  307.         latpositbox.show()
  308.  
  309.         lonpositbox = gtk.HBox(False, 4)
  310.         lonlabel1 = gtk.Label("Longitude: ")
  311.         lonlabel1.set_alignment(0, 0.5)
  312.         lonpositbox.pack_start(lonlabel1, False, False, 0)
  313.         lonlabel1.show()
  314.  
  315.         self.lonDDDtext = gtk.Entry()
  316.         self.lonDDDtext.set_max_length(3)
  317.         self.lonDDDtext.set_width_chars(4)
  318.         self.lonDDDtext.set_text("DDD")
  319.         self.lonDDDtext.select_region(1,2)
  320.         self.lonDDDtext.connect("changed", self.disable_beacon)
  321.         lonpositbox.pack_start(self.lonDDDtext, False, False, 0)
  322.         self.lonDDDtext.show()
  323.  
  324.         self.lonMMtext = gtk.Entry()
  325.         self.lonMMtext.set_max_length(2)
  326.         self.lonMMtext.set_width_chars(3)
  327.         self.lonMMtext.set_text("MM")
  328.         self.lonMMtext.select_region(1,2)
  329.         self.lonMMtext.connect("changed", self.disable_beacon)
  330.         lonpositbox.pack_start(self.lonMMtext, False, False, 0)
  331.         self.lonMMtext.show()
  332.  
  333.         lonlabel2 = gtk.Label(".")
  334.         lonlabel2.set_alignment(0, 1)
  335.         lonpositbox.pack_start(lonlabel2, False, False, 0)
  336.         lonlabel2.show()
  337.  
  338.         self.lonmmtext = gtk.Entry()
  339.         self.lonmmtext.set_max_length(2)
  340.         self.lonmmtext.set_width_chars(3)
  341.         self.lonmmtext.set_text("mm")
  342.         self.lonmmtext.select_region(1,2)
  343.         self.lonmmtext.connect("changed", self.disable_beacon)
  344.         lonpositbox.pack_start(self.lonmmtext, False, False, 0)
  345.         self.lonmmtext.show()
  346.  
  347.         self.loncombo = gtk.combo_box_new_text()
  348.         self.loncombo.append_text("W")
  349.         self.loncombo.append_text("E")
  350.         self.loncombo.set_active(0)
  351.         lonpositbox.pack_start(self.loncombo, False, False, 0)
  352.         self.loncombo.show()
  353.  
  354.         topleftpositbox.pack_start(lonpositbox, False, False, 0)
  355.         lonpositbox.show()
  356.  
  357.         toppositbox.pack_start(topleftpositbox, False, False, 0)
  358.         topleftpositbox.show()
  359.  
  360.         toprightpositbox = gtk.VBox(False, 4)
  361.         toprightpositbox.set_border_width(10)
  362.  
  363.         loclabel = gtk.Label("-OR- Zip Code:")
  364.         loclabel.set_alignment(0.3, 0)
  365.         toprightpositbox.pack_start(loclabel, False, False, 0)
  366.         loclabel.show()
  367.  
  368.         self.ziptext = gtk.Entry()
  369.         self.ziptext.set_max_length(5)
  370.         self.ziptext.set_width_chars(5)
  371.         self.ziptext.connect("changed", self.disable_beacon)
  372.         toprightpositbox.pack_start(self.ziptext, False, False, 0)
  373.         self.ziptext.show()
  374.  
  375.         toppositbox.pack_start(toprightpositbox, False, False, 0)
  376.         toprightpositbox.show()
  377.  
  378.         positbox.pack_start(toppositbox, False, False, 0)
  379.         toppositbox.show()
  380.  
  381.         positlabel2 = gtk.Label("If you do not know your LAT/LONG then your zip code will\nbe used to place you on the map.")
  382.         positlabel2.set_alignment(0, 0)
  383. #        positlabel2.modify_font(mediumfont)
  384.         positbox.pack_start(positlabel2, False, False, 0)
  385.         positlabel2.show()
  386.  
  387.         leftwin.pack_start(positbox, False, False, 0)
  388.         positbox.show()
  389.  
  390.         separator = gtk.HSeparator()
  391.         leftwin.pack_start(separator, False, False, 0)
  392.         separator.show()
  393.  
  394.         # defined here so clear and connect buttons have access
  395.         self.statusview = gtk.TextView()
  396.         self.statusbuffer = self.statusview.get_buffer()
  397.         self.messageview = gtk.TextView()
  398.         self.messagebuffer = self.messageview.get_buffer()
  399.  
  400.         connectbutton = gtk.Button()
  401.         connectbutton.set_label("Connect")
  402.         connectbutton.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#00b20d"))
  403.         connectbutton.connect("clicked", self.connect_aprs)
  404.         leftwin.pack_start(connectbutton, False, False, 12)
  405.         connectbutton.show()
  406.  
  407.         win.pack_start(leftwin, False, False, 0)
  408.         leftwin.show()
  409.  
  410.         rightwin = gtk.VBox(False, 4)
  411.         rightwin.set_border_width(4)
  412.  
  413.         rightwintopbox = gtk.HBox(False, 4)
  414.  
  415.         clearbutton = gtk.Button()
  416.         clearbutton.set_label("  Clear  ")
  417.         clearbutton.connect("clicked", self.clear_message)
  418.         rightwintopbox.pack_start(clearbutton, False, False, 20)
  419.         clearbutton.show()
  420.  
  421.         self.beaconbutton = gtk.CheckButton("Beacon every 10 minutes")
  422.         self.beaconbutton.set_active(True)
  423.         self.beaconbutton.connect("toggled", self.enable_beacon, "beacon")
  424.         rightwintopbox.pack_start(self.beaconbutton, False, False, 20)
  425.         self.beaconbutton.show()
  426.  
  427.         rightwin.pack_start(rightwintopbox, False, False, 0)
  428.         rightwintopbox.show()
  429.  
  430.         self.messageview.set_editable(False)
  431.         self.messageview.set_cursor_visible(True)
  432.         self.messageview.set_wrap_mode(gtk.WRAP_CHAR)
  433.         self.messageview.set_justification(gtk.JUSTIFY_LEFT)
  434.         self.messageview.modify_font(smallfont)
  435.  
  436.         self.messagebuffer.set_text("Welcome to APRS-XO.\n\nThis program sends your position information to a server that will\nthen display your location on a webpage.  This program requires an\nactive Internet connection to work.\n\nSelect an APRS Site\nSelecting a button will copy the URI to the clipboard.\n\nIndentifiers\nEnter your callsign and optionally an aprsd password.\n\nStation Comment\nData in the Station Comment field will appear after your location\ninformation on the website.")
  437.  
  438.         self.messagewindow = gtk.ScrolledWindow()
  439.         self.messagewindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
  440.         self.messagewindow.add_with_viewport(self.messageview)
  441.         self.messageview.show()
  442.         rightwin.pack_start(self.messagewindow, True, True, 0)
  443.         self.messagewindow.show()
  444.  
  445.         messagebox = gtk.HBox(False, 4)
  446.  
  447.         self.messagecombo = gtk.combo_box_entry_new_text()
  448.         self.messagecombo.append_text("ALL")
  449.         self.messagecombo.append_text("BEACON")
  450.         self.messagecombo.append_text("CQ")
  451.         self.messagecombo.append_text("QST")
  452.         self.messagecombo.set_active(-1)
  453.         self.messagedest = self.messagecombo.get_child()
  454.         self.messagedest.set_max_length(9)
  455.         self.messagedest.set_width_chars(5)
  456.         self.messagedest.modify_font(smallfont)
  457.         messagebox.pack_start(self.messagecombo, False, False, 0)
  458.         self.messagecombo.show()
  459.  
  460.         self.messagetext = gtk.Entry()
  461.         # TODO should this be lower with acks?
  462.         self.messagetext.set_max_length(67)
  463.         self.messagetext.set_width_chars(32)
  464.         self.messagetext.modify_font(smallfont)
  465.         self.messagetext.connect("activate", self.send_message, self.messagetext)
  466.         messagebox.pack_start(self.messagetext, False, False, 0)
  467.         self.messagetext.show()
  468.  
  469.         messagebutton = gtk.Button()
  470.         messagebutton.set_label("Send")
  471.         messagebutton.connect("clicked", self.send_message, "message")
  472.         messagebox.pack_start(messagebutton, False, False, 0)
  473.         messagebutton.show()
  474.  
  475.         rightwin.pack_start(messagebox, False, False, 0)
  476.         messagebox.show()
  477.  
  478.         self.statusview.set_editable(False)
  479.         self.statusview.set_cursor_visible(True)
  480.         self.statusview.set_wrap_mode(gtk.WRAP_CHAR)
  481.         self.statusview.set_justification(gtk.JUSTIFY_LEFT)
  482.         self.statusview.modify_font(smallfont)
  483.  
  484.         self.statusbuffer.set_text("Position\nEnter your lat/long.  You may leave the decimal minutes (mm)\nblank for position ambiguity.  If you do not know your lat/long,\nenter your 5 digit zip code instead.\n\nBeacon\nWhen selected, sends location data every 10 minutes.  The\nbeacon will automatically stop when you edit personal\ninformation.  Must be manually reselected after edit completion.\n\nThis message will self destruct when you press Connect.\n\nAPRS Copyright (c) Bob Bruninga WB4APR\n\n")
  485.  
  486.         self.statuswindow = gtk.ScrolledWindow()
  487.         self.statuswindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
  488.         self.statuswindow.add_with_viewport(self.statusview)
  489.         self.statusview.show()
  490.         rightwin.pack_start(self.statuswindow, True, True, 0)
  491.         self.statuswindow.show()
  492.  
  493.         self.rawbox = gtk.HBox(False, 6)
  494.  
  495.         self.rawtext = gtk.Entry()
  496.         self.rawtext.set_max_length(128)
  497.         self.rawtext.set_width_chars(43)
  498.         self.rawtext.modify_font(smallfont)
  499.         self.rawtext.connect("activate", self.raw_send)
  500.         self.rawbox.pack_start(self.rawtext, False, False, 0)
  501.         self.rawtext.show()
  502.  
  503.         rawbutton = gtk.Button()
  504.         rawbutton.set_label("Send")
  505.         rawbutton.connect("clicked", self.raw_send)
  506.         self.rawbox.pack_start(rawbutton, False, False, 0)
  507.         rawbutton.show()
  508.  
  509.         rightwin.pack_start(self.rawbox, False, False, 0)
  510.         self.rawbox.show()
  511.  
  512.         win.pack_start(rightwin, False, False, 0)
  513.         rightwin.show()
  514.  
  515.         # for quick testing
  516.         if (TESTING):
  517.             self.calltext.set_text("kg4gjy")
  518.             self.passtext.set_text("18107")
  519.             self.latDDtext.set_text("35")
  520.             self.latMMtext.set_text("7")
  521.             self.latmmtext.set_text("42")
  522.             self.latcombo.set_active(0)
  523.             self.lonDDDtext.set_text("85")
  524.             self.lonMMtext.set_text("6")
  525.             self.lonmmtext.set_text("93")
  526.             self.loncombo.set_active(0)
  527. #            self.stationtext.set_text("")
  528.             self.ziptext.set_text("")
  529.             self.beaconbutton.set_active(True)
  530.  
  531.         win.show()
  532.         self.calltext.grab_focus()
  533.  
  534.     def clear_status(self, button=None):
  535. #        self.statusbuffer.delete(self.statusbuffer.get_start_iter(), self.statusbuffer.get_end_iter())
  536.         self.statusbuffer.set_text("")
  537.  
  538.     def clear_message(self, button=None):
  539. #        self.messagebuffer.delete(self.messagebuffer.get_start_iter(), self.messagebuffer.get_end_iter())
  540.         self.messagebuffer.set_text("")
  541.  
  542.     def connect_aprs(self, button):
  543.         if (self.sock == None):
  544.  
  545.             if (self.help):
  546.                 self.clear_status()
  547.                 self.messagebuffer.set_text("Message Window")
  548.                 self.messagebox = True
  549.                 self.help = False
  550.  
  551.             if (self.validate_data() == False):
  552.                 return False
  553.             self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  554.             self.status_write("Connecting ")
  555.             iplist = socket.gethostbyname_ex(HOST)[2]
  556.             server = random.choice(iplist)
  557.             self.status_write("to %s\n" % server)
  558.             try:
  559.                 self.sock.connect((server, PORT))
  560.             except socket.error, msg:
  561.                 self.status_write(msg[1])
  562.                 self.sock = None
  563.                 return
  564.  
  565.             self.status_write("Connected\n")
  566.             button.set_label("Disconnect")
  567.             button.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#e6000a"))
  568.        
  569.             response = self.sock.recv(RECV_BUFFER)
  570.             self.status_write("%s" % response)
  571.             if (response.find("javAPRSSrvr") == -1):
  572.                 self.status_write("invalid response.\n")
  573.                 self.disconnect_aprs(button)
  574.  
  575.             if (response.find("Port Full") != -1):
  576.                 self.status_write("Port Full.\n")
  577.                 self.disconnect_aprs(button)
  578.  
  579.             if (self.calltext.get_text() != "" and self.passtext.get_text() != ""):
  580.                 sendme = "user %s pass %s vers aprs_xo %d filter m/300\n" % (self.calltext.get_text(), self.passtext.get_text(), VERSION)
  581.             else:
  582.                 sendme = "user %s vers aprs_xo %d\n" % (self.calltext.get_text(), VERSION)
  583.             self.sock.sendall(sendme)
  584.             self.status_write("%s" % sendme)
  585.  
  586.             response = self.sock.recv(RECV_BUFFER)
  587.             self.status_write("%s" % response)
  588.             if (response.find("# logresp") == -1):
  589.                 self.status_write("invalid response.\n")
  590.                 self.disconnect_aprs(button)
  591. #            if (response.find("unverified") == -1):
  592. #                self.rawbox.show()
  593.  
  594.             self.status_write("\n")
  595.  
  596.             self.input_watch = gobject.io_add_watch(self.sock, gobject.IO_IN, self.recv_data)
  597.  
  598.             # send banner
  599.             sendme = "%s>APRS-XO v%s\n" % (self.calltext.get_text(), VERSION)
  600.             self.sock.sendall(sendme)
  601.             self.status_write("%s>\n" % self.calltext.get_text())
  602.             self.status_write("APRS-XO v%s\n\n" % VERSION)
  603.  
  604.             self.send_beacon()
  605.             self.output_watch = gobject.timeout_add(10 * 60 * 1000, self.send_beacon)
  606.         else:
  607.             self.disconnect_aprs(button)
  608.             gobject.source_remove(self.input_watch)
  609.             gobject.source_remove(self.output_watch)
  610.  
  611.     def disconnect(self):
  612.         # for test server only
  613.         if(HOST == "192.168.50.6"):
  614.             self.sock.sendall("q")
  615.  
  616.         self.sock.close()
  617.         self.sock = None
  618.  
  619.     def disconnect_aprs(self, button):
  620.         self.disconnect()
  621.  
  622.         self.status_write("Disconnected\n")
  623.         button.set_label("Connect")
  624.         button.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#00b20d"))
  625. #        self.rawbox.hide()
  626.  
  627.     def recv_data(self, sock, condition):
  628.         while 1:
  629.             try:
  630.                 recv_data = sock.recv(RECV_BUFFER)
  631.             except:
  632.                 self.status_write("Server closed connection.\n")
  633.                 return False
  634.             if not recv_data:
  635.                 self.status_write("Server closed connection.\n")
  636.                 return False
  637.             else:
  638.                 # find uri's http or else www
  639.                 webaddy = recv_data.lower().find("http")
  640.                 if (webaddy != -1):
  641.                     recv_data = ("%s\n%s" % (recv_data[:webaddy], recv_data[webaddy:]))
  642.                 else:
  643.                     webaddy = recv_data.lower().find("www")
  644.                     if (webaddy != -1):
  645.                         recv_data = ("%s\n%s" % (recv_data[:webaddy], recv_data[webaddy:]))
  646.                 if (recv_data[0] == "#"):
  647.                     self.status_write("%s\n\n" % recv_data)
  648.                 else:
  649.                     cuthere = recv_data.find(":")
  650.                     self.status_write("%s\n" % recv_data[:cuthere+1])
  651.  
  652.                     # some incoming lines end in \n and some do not
  653.                     if (recv_data[-1:] == "\n"):
  654.                         self.status_write("%s\n" % recv_data[cuthere+1:])
  655.                     else:
  656.                         self.status_write("%s\n\n" % recv_data[cuthere+1:])
  657.                     self.msg_check(recv_data)
  658.                 return True
  659.  
  660.     def send_beacon(self):
  661.         if (self.beaconbutton.get_active()):
  662.             beacon = "=%s%s.%s%sX%s%s.%s%sA%s" % (self.latDDtext.get_text(), self.latMMtext.get_text(), self.latmmtext.get_text(), self.latcombo.get_active_text(), self.lonDDDtext.get_text(), self.lonMMtext.get_text(), self.lonmmtext.get_text(), self.loncombo.get_active_text(), self.stationtext.get_text())
  663.             if (self.send_data(beacon)):
  664.                 return True
  665.             else:
  666.                 self.status_write("\nProblem sending beacon - STOPPED")
  667.                 return False
  668.  
  669.     def send_data(self, msg):
  670.         path = "%s>APOLPC:" % self.calltext.get_text()
  671.         try:
  672.             self.sock.sendall("%s%s\n" % (path, msg))
  673.         except:
  674.             self.status_write("Problem sending packet\n")
  675.             self.sock = None
  676.             return False
  677.         self.status_write("%s\n" % path)
  678.         self.status_write("%s\n\n" % msg)
  679.         return True
  680.  
  681.     def send_ack(self, msg):
  682.         self.send_data(msg)
  683.         return False
  684.  
  685.     def status_write(self, text):
  686.         self.statusbuffer.insert(self.statusbuffer.get_end_iter(), text)
  687.         statuslines = self.statusbuffer.get_line_count()
  688.         if (statuslines > MAXLINES):
  689.             deletehere = self.statusbuffer.get_iter_at_line(statuslines - MAXLINES)
  690.             self.statusbuffer.delete(self.statusbuffer.get_start_iter(), deletehere)
  691.         if (not self.statusview.is_focus()):
  692.             adjustment = self.statuswindow.get_vadjustment()
  693.             adjustment.set_value(adjustment.upper)
  694.  
  695.     def message_write(self, text):
  696.         if (self.messagebox):
  697.             self.clear_message()
  698.             self.messagebox = False
  699.         self.messagebuffer.insert(self.messagebuffer.get_end_iter(), text)
  700.         if (not self.messageview.is_focus()):
  701.             adjustment = self.messagewindow.get_vadjustment()
  702.             adjustment.set_value(adjustment.upper)
  703.  
  704.     def set_location(self, widget, data=None):
  705.         self.location = data
  706.  
  707.     def set_site(self, widget, data=None):
  708.         self.site = data
  709.         self.clipboard()
  710.  
  711.     def validate_data(self):
  712.         stop_here = False
  713.         self.validating = True
  714.         beaconchecked = self.beaconbutton.get_active()
  715.  
  716.         if (self.calltext.get_text() == "" and self.ziptext.get_text() == ""):
  717.             self.status_write("A callsign or zip code must be provided.\n")
  718.             return False
  719.  
  720.         # convert zip to position
  721.         if (self.ziptext.get_text() != ""):
  722.             self.status_write("Attempting to set location from zip code\n")
  723.             self.ziptext.set_text(self.ziptext.get_text().zfill(5))
  724.             try:
  725.                 zipsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  726.             except socket.error, msg:
  727.                 self.status_write("[ERROR] %s\n" % msg[1])
  728.                 self.validating = False
  729.                 return False
  730.             try:
  731.                 zipsock.connect(('geocoder.us', 80))
  732.             except socket.error, msg:
  733.                 self.status_write("[ERROR] %s\n" % msg[1])
  734.                 self.validating = False
  735.                 return False
  736.             try:
  737.                 zipsock.sendall("GET /service/csv/geocode?zip=%s\n" % self.ziptext.get_text())
  738.             except socket.error, msg:
  739.                 self.status_write("[ERROR] %s\n" % msg[1])
  740.                 self.validating = False
  741.                 return False
  742.             zipsock.shutdown(1)
  743.             try:
  744.                 response = zipsock.recv(RECV_BUFFER)
  745.             except socket.error, msg:
  746.                 self.status_write("[ERROR] %s\n" % msg[1])
  747.                 self.validating = False
  748.                 return False
  749.             A = random.randrange(0, 9)
  750.             O = random.randrange(0, 9)
  751.             if (self.calltext.get_text() == ""):
  752.                 self.calltext.set_text("X%s-%d%d" % (self.ziptext.get_text(), A, O))
  753.             try:
  754.                 (lat, lon, junk) = response.split(',', 2)
  755.             except:
  756.                 self.status_write("%s\n" % response)
  757.                 lat = "0"
  758.                 lon = "0"
  759.             lat = float(lat.strip())
  760.             lon = float(lon.strip())
  761.             if (lat < 0):
  762.                 self.latcombo.set_active(1)     # S
  763.             else:
  764.                 self.latcombo.set_active(0)     # N
  765.             lat = abs(lat)
  766.             if (lon < 0):
  767.                 self.loncombo.set_active(0)     # W
  768.             else:
  769.                 self.loncombo.set_active(1)     # E
  770.             lon = abs(lon)
  771.             self.latDDtext.set_text("%02d" % int(lat))
  772.             self.latMMtext.set_text("%02d" % int((lat - int(lat)) * 60 + 0.5))
  773.             self.latmmtext.set_text("%d " % A)
  774.             self.lonDDDtext.set_text("%03d" % int(lon))
  775.             self.lonMMtext.set_text("%02d" % int((lon - int(lon)) * 60 + 0.5))
  776.             self.lonmmtext.set_text("%d " % O)
  777.             self.ziptext.set_text("")
  778.         # end set loc by zip code
  779.  
  780.         if (self.latDDtext.get_text() == "DD" or self.latDDtext.get_text() == ""):
  781.             self.status_write("Latitude Degrees are required.\n")
  782.             stop_here = True
  783.         if (self.latMMtext.get_text() == "MM" or self.latMMtext.get_text() == ""):
  784.             self.status_write("Latitude Minutes are required.\n")
  785.             stop_here = True
  786.         if (self.lonDDDtext.get_text() == "DDD" or self.lonDDDtext.get_text() == ""):
  787.             self.status_write("Longitude Degrees are required.\n")
  788.             stop_here = True
  789.         if (self.lonMMtext.get_text() == "MM" or self.lonMMtext.get_text() == ""):
  790.             self.status_write("Longitude Minutes are required.\n")
  791.             stop_here = True
  792.  
  793.         if (stop_here):
  794.             self.status_write("Latitude and Longitude must be complete.\nFor position ambiguity omit decimal Minutes (mm).\nIf you do not know your lat/long, leave the letters\n(DD, MM, etc) and provide your zip code.\n")
  795.             self.validating = False
  796.             return False
  797.  
  798.         if (not self.latDDtext.get_text().isdigit()):
  799.             self.status_write("Latitude Degrees must be a number.\n")
  800.             stop_here = True
  801.         if (not self.latMMtext.get_text().isdigit()):
  802.             self.status_write("Latitude Minutes must be a number.\n")
  803.             stop_here = True
  804.         if (not self.lonDDDtext.get_text().isdigit()):
  805.             self.status_write("Longitude Degrees must be a number.\n")
  806.             stop_here = True
  807.         if (not self.lonMMtext.get_text().isdigit()):
  808.             self.status_write("Longitude Minutes must be a number.\n")
  809.             stop_here = True
  810.  
  811.         if (stop_here):
  812.             self.status_write("Invalid Position.\n")
  813.             self.validating = False
  814.             return False
  815.  
  816.         if (int(self.latDDtext.get_text()) < 0  or int(self.latDDtext.get_text()) > 90):
  817.             self.status_write("Latitude Degrees must be between 0 and 90.\n")
  818.             stop_here = True
  819.         if (int(self.latMMtext.get_text()) < 0  or int(self.latMMtext.get_text()) > 60):
  820.             self.status_write("Latitude Minutes must be between 0 and 60.\n")
  821.             stop_here = True
  822.         if (int(self.lonDDDtext.get_text()) < 0  or int(self.lonDDDtext.get_text()) > 180):
  823.             self.status_write("Longitude Degrees must be between 0 and 180.\n")
  824.             stop_here = True
  825.         if (int(self.lonMMtext.get_text()) < 0  or int(self.lonMMtext.get_text()) > 60):
  826.             self.status_write("Longitude Minutes must be between 0 and 60.\n")
  827.             stop_here = True
  828.  
  829.         if (stop_here):
  830.             self.status_write("Invalid Position.\n")
  831.             self.validating = False
  832.             return False
  833.  
  834.         # clean up entries
  835.         self.latDDtext.set_text(self.latDDtext.get_text().zfill(2))
  836.         self.latMMtext.set_text(self.latMMtext.get_text().zfill(2))
  837.         self.lonDDDtext.set_text(self.lonDDDtext.get_text().zfill(3))
  838.         self.lonMMtext.set_text(self.lonMMtext.get_text().zfill(2))
  839.         self.latmmtext.set_text(self.latmmtext.get_text().ljust(2))
  840.         self.lonmmtext.set_text(self.lonmmtext.get_text().ljust(2))
  841.         if (self.latmmtext.get_text() == "mm"):
  842.             self.latmmtext.set_text("  ")
  843.         if (self.lonmmtext.get_text() == "mm"):
  844.             self.lonmmtext.set_text("  ")
  845.         self.calltext.set_text(self.calltext.get_text().upper())
  846.         self.beaconbutton.set_active(beaconchecked)
  847.         self.validating = False
  848.  
  849.     def close( self ):
  850.         self.hide()
  851.         if (self.sock != None):
  852.             self.disconnect()
  853.         activity.Activity.close( self )
  854.  
  855.     def write_file_broken(self, filename):
  856.         # does not appear to run
  857.         # from self.save() returns:
  858.         # TypeError: object of type 'dbus.Int32' has no len()
  859.  
  860.         self.metadata['mime_type'] = 'text/plain'
  861.  
  862.         self.metadata['callsign'] = self.calltext.get_text()
  863.         self.metadata['password'] = self.passtext.get_text()
  864.         self.metadata['latDD'] = self.latDDtext.get_text()
  865.         self.metadata['latMM'] = self.latMMtext.get_text()
  866.         self.metadata['latmm'] = self.latmmtext.get_text()
  867.         self.metadata['lat'] = self.latcombo.get_active()
  868.         self.metadata['lonDDD'] = self.lonDDDtext.get_text()
  869.         self.metadata['lonMM'] = self.lonMMtext.get_text()
  870.         self.metadata['lonmm'] = self.lonmmtext.get_text()
  871.         self.metadata['lon'] = self.loncombo.get_active()
  872. #        self.metadata['nick_name'] = self.nametext.get_text()
  873. #        self.metadata['location'] = self.location
  874.         self.metadata['stationtext'] = self.stationtext.get_text()
  875. #        self.metadata['zip'] = self.ziptext.get_text()
  876.         self.metadata['beacon'] = self.beaconbutton.get_active()
  877.         # should save list of callsigns used in outgoing messages
  878.  
  879.     def read_file_broken(self, filename):
  880.         # does not appear to run
  881.         self.calltext.set_text(self.metadata.get('callsign', ""))
  882.         self.passtext.set_text(self.metadata.get('password', ""))
  883.         self.latDDtext.set_text(self.metadata.get('latDD', "00"))
  884.         self.latMMtext.set_text(self.metadata.get('latMM', "00"))
  885.         self.latmmtext.set_text(self.metadata.get('latmm', "00"))
  886. #        self.latcombo.set_active(self.metadata.get('lat', ""))
  887.         self.lonDDDtext.set_text(self.metadata.get('lonDDD', "000"))
  888.         self.lonMMtext.set_text(self.metadata.get('lonMM', "00"))
  889.         self.lonmmtext.set_text(self.metadata.get('lonmm', "00"))
  890. #        self.loncombo.set_active(self.metadata.get('lon', ""))
  891. #        self.nametext.set_text(self.metadata.get('nick_name', "Joe"))
  892. #        self.locbutton.set_active(self.metadata.get('location', ""))
  893.         self.stationtext.set_text(self.metadata.get('stationtext', "test text"))
  894.         self.ziptext.set_text(self.metadata.get('zip', ""))
  895. #        self.beaconbutton.set_active(self.metadata.get('beacon', ""))
  896.  
  897.     def msg_check(self, data):
  898.         # a better message decoder
  899.         firstcheck = data.find("::")
  900.         secondcheck = data[firstcheck+11:firstcheck+12]
  901.         thirdcheck = data.find("{")
  902.         if (firstcheck != -1 and secondcheck == ":"):
  903.             tocall = data[firstcheck+2:firstcheck+11].upper()
  904.             strippedtocall = tocall.strip()
  905.             fromcall = data[:data.find(">")].upper()
  906.             isbulletin = self.bulletin_check(strippedtocall)
  907.             if (isbulletin):
  908.                 message = data[firstcheck+12:-1]
  909.                 # TODO add BLN, etc to tocall dropdown list?
  910. #                self.add_callsign(fromcall, False)
  911.                 # for now, display them every time
  912.                 self.message_write("%s %s.%s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), fromcall, strippedtocall, message))
  913.             else:
  914.                 if (strippedtocall == self.calltext.get_text()):
  915.                     self.add_callsign(fromcall, False)
  916.                     if (thirdcheck != -1):
  917.                         sequence_end = data.find("}")
  918.                         if (sequence_end == -1):
  919.                             sequence = data[thirdcheck+1:-1]
  920.                         else:
  921.                             sequence = data[thirdcheck+1:sequence_end]
  922.                             replyack = data[sequence_end+1:-1]
  923.                             if (replyack[0:3] == "ack"):
  924.                                 self.recv_acks["%s-%s" % (fromcall, replyack[3:])] = 1
  925.                         message = data[firstcheck+12:thirdcheck]
  926.                         ackmessage = ":%s:ack%s" % (fromcall, sequence)
  927.                         self.send_data(ackmessage)
  928.                         id = "%s-%s" % (fromcall, sequence)
  929.                         if (id in self.sent_acks):
  930.                             self.timers.append(gobject.timeout_add(30 * 1000, self.send_ack, ackmessage))
  931.                             self.timers.append(gobject.timeout_add(60 * 1000, self.send_ack, ackmessage))
  932.                             self.timers.append(gobject.timeout_add(120 * 1000, self.send_ack, ackmessage))
  933.                         else:
  934.                             self.message_write("%s %s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), fromcall, message))
  935.                         self.sent_acks[id] = time.time() # to help a cleanup thread later
  936.                         self.sent_acks[fromcall] = sequence # to help with reply acks later
  937.                     else:
  938.                         message = data[firstcheck+12:-1]
  939.                         if (message[0:3] == "ack"):
  940.                             self.recv_acks["%s-%s" % (fromcall, message[3:])] = 1
  941.                         else:
  942.                             self.message_write("%s %s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), fromcall, message))
  943.  
  944.     def msg_check_old(self, data):
  945.         # a quick "does it look like a message?" check
  946.         # this check will only find messages that require acks
  947.         firstcheck = data.find("::")
  948.         secondcheck = data.find("{")
  949.         # FIX this will ignore BLN, ALL, QST, CQ, etc with { in message
  950.         # FIXME actually a BLN with a { freezes the status window..
  951.         if (firstcheck != -1 and secondcheck != -1):
  952.             # great, it looks kinda like a message, is it to me?
  953.             tocall = data[firstcheck+2:firstcheck+11].upper()
  954.             strippedtocall = tocall.strip()
  955.             if (strippedtocall == self.calltext.get_text()):
  956.                 sequence_end = data.find("}")
  957.                 if (sequence_end == -1):
  958.                     sequence = data[secondcheck+1:-1]
  959.                 else:
  960.                     sequence = data[secondcheck+1:sequence_end]
  961.                 fromcall = data[:data.find(">")].upper()
  962.                 message = data[firstcheck+12:secondcheck]
  963.                 self.add_callsign(fromcall, False)
  964.                 ackmessage = ":%s:ack%s" % (fromcall, sequence)
  965.                 self.send_data(ackmessage)
  966.                 id = "%s-%s" % (fromcall, sequence)
  967.                 if (id in self.sent_acks):
  968.                     self.timers.append(gobject.timeout_add(30 * 1000, self.send_ack, ackmessage))
  969.                     self.timers.append(gobject.timeout_add(60 * 1000, self.send_ack, ackmessage))
  970.                     self.timers.append(gobject.timeout_add(120 * 1000, self.send_ack, ackmessage))
  971.                 else:
  972.                     self.message_write("%s %s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), fromcall, message))
  973.  
  974.                 self.sent_acks[id] = time.time() # to help a cleanup thread later
  975.                 self.sent_acks[fromcall] = sequence # to help with reply acks later
  976.         else:
  977.             # is it a message that does not get acked?
  978.             if (firstcheck != -1):
  979.                 tocall = data[firstcheck+2:firstcheck+11].upper()
  980.                 strippedtocall = tocall.strip()
  981.                 fromcall = data[:data.find(">")].upper()
  982.                 message = data[firstcheck+12:-1]
  983.                 if (strippedtocall == "ALL" or strippedtocall == "QST" or strippedtocall == "CQ" or strippedtocall[:3] == "BLN" or strippedtocall[:3] == "NWS"):
  984.                     # for now, display them every time
  985.                     self.message_write("%s %s:%s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), fromcall, strippedtocall, message))
  986.                 else:
  987.                     # is it an ack (to me)?
  988.                     if (strippedtocall == self.calltext.get_text()):
  989.                         if (message[0:3] == "ack"):
  990.                             # record the message as acked to stop repeating it
  991.                             self.recv_acks["%s-%s" % (fromcall, message[3:])] = 1
  992.                         else:
  993.                             # no ack and addressed to me.
  994.                             self.message_write("%s %s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), fromcall, message))
  995.  
  996.     def disable_beacon(self, widget, data=None):
  997.         if (self.sock != None):
  998.             self.beaconbutton.set_active(False)
  999.  
  1000.     def enable_beacon(self, widget, data=None):
  1001.         if (not self.validating and widget.get_active()):
  1002.             self.validate_data()
  1003.  
  1004.     def raw_send(self, widget, data=None):
  1005.         msg = "%s\n" % self.rawtext.get_text()
  1006.         try:
  1007.             self.sock.sendall(msg)
  1008.         except:
  1009.             self.status_write("Problem sending message\n")
  1010.             self.sock = None
  1011.             return False
  1012.         self.status_write("%s\n" % msg)
  1013.         self.rawtext.set_text("")
  1014.         return True
  1015.  
  1016.     def send_message(self, widget, data=None):
  1017.         tocall = self.messagedest.get_text().upper()
  1018.         message = self.messagetext.get_text()
  1019.  
  1020.         isbulletin = self.bulletin_check(tocall)
  1021.  
  1022.         # check for illegal characters before clearing message
  1023.         if (isbulletin):
  1024.             if (message.find("|") != -1 or message.find("~") != -1):
  1025.                 self.message_write("Bulletins can not contain the following characters: | ~")
  1026.                 return False
  1027.         else:
  1028.             if (message.find("|") != -1 or message.find("~") != -1 or message.find("{") != -1):
  1029.                 self.message_write("Messages can not contain the following characters: | ~ {")
  1030.                 return False
  1031.  
  1032.         self.messagetext.set_text("")
  1033.  
  1034.         # add callsign to list if just entered
  1035.         if (self.messagecombo.get_active() == -1):
  1036.             self.add_callsign(tocall, True)
  1037.  
  1038.         # get a sequence number
  1039.         if (isbulletin):
  1040.             # don't waste a number on a bulletin
  1041.             sequence = ""
  1042.         else:
  1043.             sequence = "%s" % self.b90()
  1044.  
  1045.         # TODO
  1046.         # check to see if there is an unack-ed message to same callsign
  1047.         # too many to the same call? give an error instead
  1048.         # too many in general? give an error instead
  1049.         # print message + QUEUED add to the queue...
  1050.         # cancel message option - menu popup
  1051.  
  1052.         # TODO save location for repeat/ack counter, queued and cancelled locations too
  1053.         # put the message in the message window
  1054. #        self.message_write("%s %s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), self.calltext.get_text(), message))
  1055.         self.message_write("%s To:%s> %s\n" % (time.strftime("%m/%d %H:%M", time.localtime()), tocall, message))
  1056.  
  1057.         # send the message
  1058.         if (isbulletin):
  1059.             self.send_data(":%s:%s" % (tocall.ljust(9), message))
  1060.             # TODO what are the correct timings for bulletins?
  1061.             gobject.timeout_add(600 * 1000, self.msg_timer, tocall, message, "", 2, 600)
  1062.         else:
  1063.             replyack = self.replyack(tocall)
  1064.             self.send_data(":%s:%s{%s}%s" % (tocall.ljust(9), message, sequence, replyack))
  1065.             self.recv_acks["%s-%s" % (tocall, sequence)] = 0
  1066.             gobject.timeout_add(7 * 1000, self.msg_timer, tocall, message, sequence, 2, 7)
  1067.  
  1068.     def b90(self):
  1069.         if (self.sequence > 8099):
  1070.             self.sequence = 0
  1071.         first = ((self.sequence / 90) % 90) + 33
  1072.         second = (self.sequence % 90) + 33
  1073.         output = "%c%c" % (first, second)
  1074.         self.sequence += 1
  1075.         return output
  1076.  
  1077.     def msg_timer(self, tocall, message, sequence, count, delay):
  1078.         if (self.recv_acks["%s-%s" % (tocall, sequence)] == 1):
  1079.             return False
  1080.         else:
  1081.             replyack = self.replyack(tocall)
  1082.             self.send_data(":%s:%s{%s}%s" % (tocall.ljust(9), message, sequence, replyack))
  1083.  
  1084.             # start the next timer
  1085.             count += 1
  1086.             if (count > MAXRETRIES):
  1087.                 return False
  1088.             delay *=  2
  1089.             if (delay > 600):
  1090.                 delay = 600
  1091.             gobject.timeout_add(delay * 1000, self.msg_timer, tocall, message, sequence, count, delay)
  1092.  
  1093.             # and stop this timer
  1094.             return False
  1095.  
  1096.     def clipboard(self):
  1097.         clipboard = gtk.clipboard_get()
  1098.         target = [("text/uri-list", 0, 0)]
  1099.         clipboard.set_with_data(target, self.clipboard_get, self.clipboard_clear, (self.site))
  1100.  
  1101.     def clipboard_get(self, clipboard, selection, info, data):
  1102.         selection.set_uris([data])
  1103.  
  1104.     def clipboard_clear(self, clipboard, data):
  1105.         pass
  1106.  
  1107.     def add_callsign(self, callsign, activate):
  1108.         model = self.messagecombo.get_model()
  1109.         notfound = True
  1110.         iter = model.get_iter_first()
  1111.         while iter:
  1112.             currentcall = model.get(iter, 0)[0]
  1113.             if (currentcall == callsign):
  1114.                 notfound = False
  1115.                 break
  1116.             iter = model.iter_next(iter)
  1117.         if (notfound):
  1118.             self.messagecombo.prepend_text(callsign)
  1119.             if (activate):
  1120.                 self.messagecombo.set_active(0)
  1121.  
  1122.     def bulletin_check(self, callsign):
  1123.         for currentcall in self.bulletins:
  1124.             length = len(currentcall)
  1125.             if (currentcall == callsign[0:length]):
  1126.                 return True
  1127.         return False
  1128.  
  1129.     def replyack(self, tocall):
  1130.         replyack = ""
  1131.         if (tocall in self.sent_acks):
  1132.             id = "%s-%s" % (tocall, self.sent_acks[tocall])
  1133.             if (time.time() - self.sent_acks[id] < 5400):
  1134.                 # less than 90 minutes ago
  1135.                 replyack = "ack%s" % self.sent_acks[tocall]
  1136.         return replyack
  1137.