quick & dirty phone (EBIP) simulator
Stefan Schuermans

Stefan Schuermans commited on 2019-06-15 20:15:54
Showing 4 changed files, with 750 additions and 0 deletions.

... ...
@@ -0,0 +1 @@
1
+/*~
... ...
@@ -0,0 +1,259 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!-- Generated with glade 3.20.0 -->
3
+<interface>
4
+  <requires lib="gtk+" version="3.6"/>
5
+  <object class="GtkWindow" id="CallWindow">
6
+    <property name="visible">True</property>
7
+    <property name="can_focus">False</property>
8
+    <signal name="destroy" handler="onDestroy" swapped="no"/>
9
+    <child>
10
+      <object class="GtkBox" id="MainVBox">
11
+        <property name="visible">True</property>
12
+        <property name="can_focus">False</property>
13
+        <property name="margin_left">5</property>
14
+        <property name="margin_right">5</property>
15
+        <property name="margin_top">5</property>
16
+        <property name="margin_bottom">5</property>
17
+        <property name="orientation">vertical</property>
18
+        <property name="spacing">5</property>
19
+        <child>
20
+          <object class="GtkLabel" id="PhoneNoLabel">
21
+            <property name="visible">True</property>
22
+            <property name="can_focus">False</property>
23
+            <property name="label" translatable="yes">Phone Number:</property>
24
+            <property name="xalign">0</property>
25
+          </object>
26
+          <packing>
27
+            <property name="expand">False</property>
28
+            <property name="fill">True</property>
29
+            <property name="position">0</property>
30
+          </packing>
31
+        </child>
32
+        <child>
33
+          <object class="GtkEntry" id="PhoneNoEntry">
34
+            <property name="visible">True</property>
35
+            <property name="can_focus">True</property>
36
+            <property name="editable">False</property>
37
+            <property name="max_length">32</property>
38
+            <property name="width_chars">40</property>
39
+            <property name="input_purpose">digits</property>
40
+          </object>
41
+          <packing>
42
+            <property name="expand">False</property>
43
+            <property name="fill">True</property>
44
+            <property name="position">1</property>
45
+          </packing>
46
+        </child>
47
+        <child>
48
+          <object class="GtkGrid" id="KeyGrid">
49
+            <property name="visible">True</property>
50
+            <property name="can_focus">False</property>
51
+            <property name="row_spacing">5</property>
52
+            <property name="column_spacing">5</property>
53
+            <property name="row_homogeneous">True</property>
54
+            <property name="column_homogeneous">True</property>
55
+            <child>
56
+              <object class="GtkButton" id="Button1">
57
+                <property name="label" translatable="yes">1</property>
58
+                <property name="visible">True</property>
59
+                <property name="sensitive">False</property>
60
+                <property name="can_focus">True</property>
61
+                <property name="receives_default">True</property>
62
+                <signal name="clicked" handler="onKey" swapped="no"/>
63
+              </object>
64
+              <packing>
65
+                <property name="left_attach">0</property>
66
+                <property name="top_attach">0</property>
67
+              </packing>
68
+            </child>
69
+            <child>
70
+              <object class="GtkButton" id="Button2">
71
+                <property name="label" translatable="yes">2</property>
72
+                <property name="visible">True</property>
73
+                <property name="sensitive">False</property>
74
+                <property name="can_focus">True</property>
75
+                <property name="receives_default">True</property>
76
+                <signal name="clicked" handler="onKey" swapped="no"/>
77
+              </object>
78
+              <packing>
79
+                <property name="left_attach">1</property>
80
+                <property name="top_attach">0</property>
81
+              </packing>
82
+            </child>
83
+            <child>
84
+              <object class="GtkButton" id="Button3">
85
+                <property name="label" translatable="yes">3</property>
86
+                <property name="visible">True</property>
87
+                <property name="sensitive">False</property>
88
+                <property name="can_focus">True</property>
89
+                <property name="receives_default">True</property>
90
+                <signal name="clicked" handler="onKey" swapped="no"/>
91
+              </object>
92
+              <packing>
93
+                <property name="left_attach">2</property>
94
+                <property name="top_attach">0</property>
95
+              </packing>
96
+            </child>
97
+            <child>
98
+              <object class="GtkButton" id="Button4">
99
+                <property name="label" translatable="yes">4</property>
100
+                <property name="visible">True</property>
101
+                <property name="sensitive">False</property>
102
+                <property name="can_focus">True</property>
103
+                <property name="receives_default">True</property>
104
+                <signal name="clicked" handler="onKey" swapped="no"/>
105
+              </object>
106
+              <packing>
107
+                <property name="left_attach">0</property>
108
+                <property name="top_attach">1</property>
109
+              </packing>
110
+            </child>
111
+            <child>
112
+              <object class="GtkButton" id="Button5">
113
+                <property name="label" translatable="yes">5</property>
114
+                <property name="visible">True</property>
115
+                <property name="sensitive">False</property>
116
+                <property name="can_focus">True</property>
117
+                <property name="receives_default">True</property>
118
+                <signal name="clicked" handler="onKey" swapped="no"/>
119
+              </object>
120
+              <packing>
121
+                <property name="left_attach">1</property>
122
+                <property name="top_attach">1</property>
123
+              </packing>
124
+            </child>
125
+            <child>
126
+              <object class="GtkButton" id="Button6">
127
+                <property name="label" translatable="yes">6</property>
128
+                <property name="visible">True</property>
129
+                <property name="sensitive">False</property>
130
+                <property name="can_focus">True</property>
131
+                <property name="receives_default">True</property>
132
+                <signal name="clicked" handler="onKey" swapped="no"/>
133
+              </object>
134
+              <packing>
135
+                <property name="left_attach">2</property>
136
+                <property name="top_attach">1</property>
137
+              </packing>
138
+            </child>
139
+            <child>
140
+              <object class="GtkButton" id="Button7">
141
+                <property name="label" translatable="yes">7</property>
142
+                <property name="visible">True</property>
143
+                <property name="sensitive">False</property>
144
+                <property name="can_focus">True</property>
145
+                <property name="receives_default">True</property>
146
+                <signal name="clicked" handler="onKey" swapped="no"/>
147
+              </object>
148
+              <packing>
149
+                <property name="left_attach">0</property>
150
+                <property name="top_attach">2</property>
151
+              </packing>
152
+            </child>
153
+            <child>
154
+              <object class="GtkButton" id="Button8">
155
+                <property name="label" translatable="yes">8</property>
156
+                <property name="visible">True</property>
157
+                <property name="sensitive">False</property>
158
+                <property name="can_focus">True</property>
159
+                <property name="receives_default">True</property>
160
+                <signal name="clicked" handler="onKey" swapped="no"/>
161
+              </object>
162
+              <packing>
163
+                <property name="left_attach">1</property>
164
+                <property name="top_attach">2</property>
165
+              </packing>
166
+            </child>
167
+            <child>
168
+              <object class="GtkButton" id="Button9">
169
+                <property name="label" translatable="yes">9</property>
170
+                <property name="visible">True</property>
171
+                <property name="sensitive">False</property>
172
+                <property name="can_focus">True</property>
173
+                <property name="receives_default">True</property>
174
+                <signal name="clicked" handler="onKey" swapped="no"/>
175
+              </object>
176
+              <packing>
177
+                <property name="left_attach">2</property>
178
+                <property name="top_attach">2</property>
179
+              </packing>
180
+            </child>
181
+            <child>
182
+              <object class="GtkButton" id="Button0">
183
+                <property name="label" translatable="yes">0</property>
184
+                <property name="visible">True</property>
185
+                <property name="sensitive">False</property>
186
+                <property name="can_focus">True</property>
187
+                <property name="receives_default">True</property>
188
+                <signal name="clicked" handler="onKey" swapped="no"/>
189
+              </object>
190
+              <packing>
191
+                <property name="left_attach">1</property>
192
+                <property name="top_attach">3</property>
193
+              </packing>
194
+            </child>
195
+            <child>
196
+              <object class="GtkButton" id="ButtonStar">
197
+                <property name="label" translatable="yes">*</property>
198
+                <property name="visible">True</property>
199
+                <property name="sensitive">False</property>
200
+                <property name="can_focus">True</property>
201
+                <property name="receives_default">True</property>
202
+                <signal name="clicked" handler="onKey" swapped="no"/>
203
+              </object>
204
+              <packing>
205
+                <property name="left_attach">0</property>
206
+                <property name="top_attach">3</property>
207
+              </packing>
208
+            </child>
209
+            <child>
210
+              <object class="GtkButton" id="ButtonHash">
211
+                <property name="label" translatable="yes">#</property>
212
+                <property name="visible">True</property>
213
+                <property name="sensitive">False</property>
214
+                <property name="can_focus">True</property>
215
+                <property name="receives_default">True</property>
216
+                <signal name="clicked" handler="onKey" swapped="no"/>
217
+              </object>
218
+              <packing>
219
+                <property name="left_attach">2</property>
220
+                <property name="top_attach">3</property>
221
+              </packing>
222
+            </child>
223
+          </object>
224
+          <packing>
225
+            <property name="expand">True</property>
226
+            <property name="fill">True</property>
227
+            <property name="position">2</property>
228
+          </packing>
229
+        </child>
230
+        <child>
231
+          <object class="GtkLabel" id="SoundLabel">
232
+            <property name="visible">True</property>
233
+            <property name="can_focus">False</property>
234
+            <property name="label" translatable="yes">Sound</property>
235
+            <property name="xalign">0</property>
236
+          </object>
237
+          <packing>
238
+            <property name="expand">False</property>
239
+            <property name="fill">True</property>
240
+            <property name="position">3</property>
241
+          </packing>
242
+        </child>
243
+        <child>
244
+          <object class="GtkEntry" id="SoundEntry">
245
+            <property name="visible">True</property>
246
+            <property name="can_focus">True</property>
247
+            <property name="editable">False</property>
248
+            <property name="width_chars">0</property>
249
+          </object>
250
+          <packing>
251
+            <property name="expand">False</property>
252
+            <property name="fill">True</property>
253
+            <property name="position">4</property>
254
+          </packing>
255
+        </child>
256
+      </object>
257
+    </child>
258
+  </object>
259
+</interface>
... ...
@@ -0,0 +1,91 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!-- Generated with glade 3.20.0 -->
3
+<interface>
4
+  <requires lib="gtk+" version="3.6"/>
5
+  <object class="GtkWindow" id="MasterWindow">
6
+    <property name="visible">True</property>
7
+    <property name="can_focus">False</property>
8
+    <property name="default_width">256</property>
9
+    <property name="default_height">256</property>
10
+    <signal name="destroy" handler="onDestroy" swapped="no"/>
11
+    <child>
12
+      <object class="GtkBox" id="MainVBox">
13
+        <property name="visible">True</property>
14
+        <property name="can_focus">False</property>
15
+        <property name="margin_left">5</property>
16
+        <property name="margin_right">5</property>
17
+        <property name="margin_top">5</property>
18
+        <property name="margin_bottom">5</property>
19
+        <property name="orientation">vertical</property>
20
+        <property name="spacing">5</property>
21
+        <child>
22
+          <object class="GtkLabel" id="PhoneNoLabel">
23
+            <property name="visible">True</property>
24
+            <property name="can_focus">False</property>
25
+            <property name="label" translatable="yes">Phone Number:</property>
26
+            <property name="xalign">0</property>
27
+          </object>
28
+          <packing>
29
+            <property name="expand">False</property>
30
+            <property name="fill">True</property>
31
+            <property name="position">0</property>
32
+          </packing>
33
+        </child>
34
+        <child>
35
+          <object class="GtkEntry" id="PhoneNoEntry">
36
+            <property name="visible">True</property>
37
+            <property name="can_focus">True</property>
38
+            <property name="max_length">32</property>
39
+            <property name="width_chars">40</property>
40
+            <property name="input_purpose">digits</property>
41
+          </object>
42
+          <packing>
43
+            <property name="expand">False</property>
44
+            <property name="fill">True</property>
45
+            <property name="position">1</property>
46
+          </packing>
47
+        </child>
48
+        <child>
49
+          <object class="GtkButton" id="CallButton">
50
+            <property name="label" translatable="yes">Call</property>
51
+            <property name="visible">True</property>
52
+            <property name="can_focus">True</property>
53
+            <property name="receives_default">True</property>
54
+            <signal name="clicked" handler="onCall" swapped="no"/>
55
+          </object>
56
+          <packing>
57
+            <property name="expand">True</property>
58
+            <property name="fill">True</property>
59
+            <property name="position">2</property>
60
+          </packing>
61
+        </child>
62
+        <child>
63
+          <object class="GtkLabel" id="ClientsLabel">
64
+            <property name="visible">True</property>
65
+            <property name="can_focus">False</property>
66
+            <property name="label" translatable="yes">Clients:</property>
67
+            <property name="xalign">0</property>
68
+          </object>
69
+          <packing>
70
+            <property name="expand">False</property>
71
+            <property name="fill">True</property>
72
+            <property name="position">3</property>
73
+          </packing>
74
+        </child>
75
+        <child>
76
+          <object class="GtkTextView" id="ClientsTextView">
77
+            <property name="visible">True</property>
78
+            <property name="can_focus">True</property>
79
+            <property name="editable">False</property>
80
+            <property name="accepts_tab">False</property>
81
+          </object>
82
+          <packing>
83
+            <property name="expand">True</property>
84
+            <property name="fill">True</property>
85
+            <property name="position">4</property>
86
+          </packing>
87
+        </child>
88
+      </object>
89
+    </child>
90
+  </object>
91
+</interface>
... ...
@@ -0,0 +1,399 @@
1
+#! /usr/bin/env python3
2
+"""phonesim - phone simulator for EBIP protocol
3
+Copyright 2019 Stefan Schuermans <stefan@schuermans.info>
4
+Copyleft: CC-BY-SA http://creativecommons.org/licenses/by-sa/3.0/"""
5
+
6
+import argparse
7
+import gi
8
+import os
9
+import socket
10
+import string
11
+import sys
12
+import time
13
+
14
+gi.require_version('Gtk', '3.0')
15
+from gi.repository import GObject
16
+from gi.repository import Gtk
17
+from gi.repository import Pango
18
+
19
+scriptdir = os.path.dirname(os.path.abspath(__file__))
20
+
21
+
22
+class Master(object):
23
+    """master window of phone simulator"""
24
+
25
+    class Client(object):
26
+        """EBIP client"""
27
+
28
+        def __init__(self, master, addr):
29
+            """initialize new client"""
30
+            super().__init__()
31
+            self.master = master
32
+            self.addr = addr
33
+            # initialize state
34
+            self.last_heartbeat_time = None
35
+            self.last_register_time = None
36
+
37
+        def heartbeat(self):
38
+            """client sent heartbeat"""
39
+            now = time.time()
40
+            self.last_heartbeat_time = now
41
+
42
+        def register(self):
43
+            """client registered"""
44
+            now = time.time()
45
+            self.last_register_time = now
46
+            self.last_heartbeat_time = now
47
+            self.master.output("client \"{:s}:{:d}\" registered".format(
48
+                self.addr[0], self.addr[1]))
49
+
50
+        def tick100ms(self, now):
51
+            """periodic 100ms tick"""
52
+            # expire client if no heartbeat is received
53
+            if now >= self.last_heartbeat_time + 60:
54
+                self.master.output("client \"{:s}:{:d}\" expired".format(
55
+                    self.addr[0], self.addr[1]))
56
+                # expire client
57
+                return False
58
+            # keep client
59
+            return True
60
+
61
+    def __init__(self, bind, verbose):
62
+        """construct the master window"""
63
+        super().__init__()
64
+        self.bind = bind
65
+        self.verbose = verbose
66
+        # state information
67
+        now = time.time()
68
+        self.clients = {}  # dict: addr -> Client
69
+        self.calls = {}  # dict: line number -> Call
70
+        self.max_line_no = 2
71
+        self.last_line_state = now
72
+        # build window
73
+        self.builder = Gtk.Builder()
74
+        self.builder.add_from_file(scriptdir + "/master.glade")
75
+        self.widPhoneNoEntry = self.builder.get_object("PhoneNoEntry")
76
+        self.widClientsTextView = self.builder.get_object("ClientsTextView")
77
+        handlers = {
78
+            "onCall": self.onCall,
79
+            "onDestroy": self.onDestroy,
80
+        }
81
+        self.builder.connect_signals(handlers)
82
+        # set up socket
83
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
84
+        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
85
+        self.sock.bind(self.bind)
86
+        self.sockInEvId = GObject.io_add_watch(self.sock.makefile(),
87
+                                               GObject.IO_IN, self.onRecv)
88
+        # start background actions
89
+        GObject.timeout_add(100, self.onTimer100ms)
90
+        self.output(
91
+            "started (bind=\"{:s}:{:d}\")".format(self.bind[0], self.bind[1]))
92
+
93
+    def __del__(self):
94
+        """cleanup"""
95
+        GObject.source_remove(self.sockInEvId)
96
+        self.sock.close()
97
+
98
+    def onCall(self, widget):
99
+        """call button clicked"""
100
+        phone_no_str = self.widPhoneNoEntry.get_text()
101
+        if len(phone_no_str) < 1:
102
+            self.output("not calling, no number")
103
+            return
104
+        if len(self.clients) < 1:
105
+            self.output("not calling, no clients")
106
+            return
107
+        self.output("calling {:s}".format(phone_no_str))
108
+        # find free line number
109
+        line_no = 1
110
+        while line_no in self.calls:
111
+            line_no += 1
112
+        self.max_line_no = max(self.max_line_no, line_no)
113
+        self.calls[line_no] = Call(self, line_no, phone_no_str)
114
+
115
+    def onDestroy(self, widget):
116
+        """window will be destroyed"""
117
+        destroy_calls = list(self.calls.values())
118
+        for call in destroy_calls:
119
+            call.destroy()
120
+        Gtk.main_quit()
121
+
122
+    def onRecv(self, source, condition):
123
+        """input received on socket"""
124
+        # read message
125
+        data, addr = self.sock.recvfrom(4096)
126
+        msg = data.decode("utf-8")
127
+        self.output("received \"{:s}\" from \"{:s}:{:d}\"".format(msg, addr[0],
128
+                                                                  addr[1]))
129
+        # process message
130
+        self.procMsg(msg, addr)
131
+        # request to be called on next event
132
+        return True
133
+
134
+    def onTimer100ms(self):
135
+        """timer callback, every 100ms"""
136
+        now = time.time()
137
+        # client tick, expire clients
138
+        expire = []
139
+        for addr, client in self.clients.items():
140
+            if not client.tick100ms(now):
141
+                expire.append(addr)
142
+        if len(expire) > 0:
143
+            for addr in expire:
144
+                del self.clients[addr]
145
+            self.updateClients()
146
+        # line state
147
+        if now >= self.last_line_state + 1:
148
+            for line_no in range(1, self.max_line_no + 1):
149
+                if line_no in self.calls:
150
+                    line_state = self.calls[line_no].get_line_state()
151
+                else:
152
+                    line_state = "onhook"
153
+                self.sendMsgToAll("{:d}:{:s}".format(line_no, line_state))
154
+            self.last_line_state = now
155
+        # request being called again
156
+        return True
157
+
158
+    def output(self, line):
159
+        """output a line of text if in verbose mode"""
160
+        if self.verbose:
161
+            print(
162
+                "{:s}: {:s}".format(time.strftime("%Y-%m-%d %H:%M:%S"), line))
163
+
164
+    def procMsg(self, msg, addr):
165
+        """process message"""
166
+        fields = msg.strip().split(":")
167
+        if len(fields) < 2:
168
+            return
169
+        try:
170
+            line_no = int(fields[0])
171
+        except ValueError:
172
+            return
173
+        if line_no < 0:
174
+            return
175
+        msgType = fields[1]
176
+        prms = fields[2:]
177
+        # general messages
178
+        if line_no == 0:
179
+            # new client messages
180
+            if msgType == "register" and len(prms) >= 1:
181
+                self.procMsgRegister(prms[0], addr)
182
+                return
183
+            # require client to be registered
184
+            if addr not in self.clients:
185
+                return
186
+            client = self.clients[addr]
187
+            # regular client messages
188
+            if msgType == "heartbeat":
189
+                client.heartbeat()
190
+                return
191
+            return
192
+        # require client to be registered
193
+        if addr not in self.clients:
194
+            return
195
+        client = self.clients[addr]
196
+        # require line to be open
197
+        if line_no not in self.calls:
198
+            return
199
+        call = self.calls[line_no]
200
+        # line messages
201
+        if msgType == "accept":
202
+            call.accept()
203
+            return
204
+        if msgType == "hangup":
205
+            call.hangup()
206
+            return
207
+        if (msgType == "play" or
208
+                msgType == "playbackground") and len(prms) >= 1:
209
+            call.play(prms[0])
210
+            return
211
+
212
+    def procMsgRegister(self, port_str, addr):
213
+        """process register message"""
214
+        try:
215
+            port = int(port_str)
216
+        except ValueError:
217
+            return
218
+        if port != addr[1]:
219
+            self.output("client does not know its port (real {:d} msg {:d})".
220
+                        format(addr[1], port))
221
+            return
222
+        if addr not in self.clients:
223
+            self.clients[addr] = self.Client(self, addr)
224
+            self.updateClients()
225
+        self.clients[addr].register()
226
+
227
+    def sendMsg(self, msg, addr):
228
+        """send message"""
229
+        self.output(
230
+            "sending \"{:s}\" to \"{:s}:{:d}\"".format(msg, addr[0], addr[1]))
231
+        self.sock.sendto(msg.encode("utf-8"), addr)
232
+
233
+    def sendMsgToAll(self, msg):
234
+        """send message to all clients"""
235
+        for addr in self.clients:
236
+            self.sendMsg(msg, addr)
237
+
238
+    def updateClients(self):
239
+        """update list of clients"""
240
+        txts = []
241
+        for addr in sorted(self.clients):
242
+            txts.append("{:s}:{:d}".format(addr[0], addr[1]))
243
+        txt = "\n".join(txts)
244
+        self.widClientsTextView.get_buffer().set_text(txt)
245
+
246
+    # end of class Master
247
+
248
+
249
+class Call(object):
250
+    """call windows of phone simulator"""
251
+
252
+    def __init__(self, master, line_no, phone_no_str):
253
+        """construct the call window"""
254
+        super().__init__()
255
+        self.master = master
256
+        self.line_no = line_no
257
+        self.phone_no_str = phone_no_str
258
+        # initialize state
259
+        self.connected = False
260
+        # build window
261
+        self.builder = Gtk.Builder()
262
+        self.builder.add_from_file(scriptdir + "/call.glade")
263
+        self.widCallWindow = self.builder.get_object("CallWindow")
264
+        self.widPhoneNoEntry = self.builder.get_object("PhoneNoEntry")
265
+        self.widButtons = {
266
+            "0": self.builder.get_object("Button0"),
267
+            "1": self.builder.get_object("Button1"),
268
+            "2": self.builder.get_object("Button2"),
269
+            "3": self.builder.get_object("Button3"),
270
+            "4": self.builder.get_object("Button4"),
271
+            "5": self.builder.get_object("Button5"),
272
+            "6": self.builder.get_object("Button6"),
273
+            "7": self.builder.get_object("Button7"),
274
+            "8": self.builder.get_object("Button8"),
275
+            "9": self.builder.get_object("Button9"),
276
+            "*": self.builder.get_object("ButtonStar"),
277
+            "#": self.builder.get_object("ButtonHash"),
278
+        }
279
+        self.widSoundEntry = self.builder.get_object("SoundEntry")
280
+        handlers = {
281
+            "onDestroy": self.onDestroy,
282
+            "onKey": self.onKey,
283
+        }
284
+        self.builder.connect_signals(handlers)
285
+        self.widPhoneNoEntry.set_text(self.phone_no_str)
286
+        # send connection request message
287
+        self.master.sendMsgToAll("{:d}:setup:{:d}:{:s}".format(
288
+            self.line_no, self.line_no, self.phone_no_str))
289
+
290
+    def accept(self):
291
+        """call has been accepted"""
292
+        self.master.output("call to \"{:s}\" accepted on line {:d}".format(
293
+            self.phone_no_str, self.line_no))
294
+        self.master.sendMsgToAll("{:d}:connected".format(self.line_no))
295
+        self.enableKeys(True)
296
+
297
+    def destroy(self):
298
+        """destroy window"""
299
+        self.widCallWindow.destroy()
300
+
301
+    def get_line_state(self):
302
+        return "offhook"
303
+
304
+    def hangup(self):
305
+        """call has been ended"""
306
+        self.master.output("call to \"{:s}\" hangup on line {:d}".format(
307
+            self.phone_no_str, self.line_no))
308
+        self.destroy()
309
+
310
+    def enableKeys(self, enabled):
311
+        """enable of disable phone keys"""
312
+        for key in self.widButtons.values():
313
+            key.set_sensitive(enabled)
314
+
315
+    def onDestroy(self, widget):
316
+        """window will be destroyed"""
317
+        self.master.output("call to \"{:s}\" ended on line {:d}".format(
318
+            self.phone_no_str, self.line_no))
319
+        self.master.sendMsgToAll("{:d}:onhook".format(self.line_no))
320
+        del self.master.calls[self.line_no]
321
+
322
+    def onKey(self, widget):
323
+        """phone key button clicked"""
324
+        dtmf = None
325
+        for d, button in self.widButtons.items():
326
+            if widget is button:
327
+                dtmf = d
328
+                break
329
+        else:
330
+            return  # button not found
331
+        self.master.output("phone key \"{:s}\" pressed on line {:d}".format(
332
+            dtmf, self.line_no))
333
+        # send DTMF message
334
+        self.master.sendMsgToAll("{:d}:dtmf:{:s}".format(self.line_no, dtmf))
335
+
336
+    def play(self, sound):
337
+        """play sound request"""
338
+        self.master.output("request to play sound \"{:s}\" on line {:d}".
339
+                           format(sound, self.line_no))
340
+        self.widSoundEntry.set_text(sound)
341
+
342
+    # end of class Call
343
+
344
+
345
+def parse_addr(addr_str):
346
+    """parse network address"""
347
+    fields = addr_str.split(":")
348
+    if len(fields) != 2:
349
+        raise ValueError("invalid address \"{:s}\"".format(addr_str))
350
+    host = fields[0]
351
+    port_str = fields[1]
352
+    try:
353
+        port = int(port_str)
354
+        if port < 1 or port > 65535:
355
+            raise ValueError("out of range 1..65535")
356
+    except ValueError as ve:
357
+        raise ValueError(
358
+            "invalid port \"{:s}\": {:s}".format(port_str, str(ve)))
359
+    return (host, port)
360
+
361
+
362
+def parse_args():
363
+    """parse command line arguments"""
364
+    parser = argparse.ArgumentParser(
365
+        "phonesim - phone simulator for EBIP protocol")
366
+    parser.add_argument(
367
+        "-b",
368
+        "--bind",
369
+        action="store",
370
+        dest="bind",
371
+        default="127.0.0.1:1234",
372
+        help="bind address")
373
+    parser.add_argument(
374
+        "-v",
375
+        "--verbose",
376
+        action="store_true",
377
+        dest="verbose",
378
+        default=False,
379
+        help="verbose mode")
380
+    args = parser.parse_args()
381
+    args.bind = parse_addr(args.bind)
382
+    return args
383
+
384
+
385
+def main():
386
+    """main program"""
387
+    # parse arguments
388
+    args = parse_args()
389
+    # create master window
390
+    master = Master(args.bind, args.verbose)
391
+    # run application
392
+    Gtk.main()
393
+    # done
394
+    return 0
395
+
396
+
397
+# main application entry point
398
+if __name__ == "__main__":
399
+    sys.exit(main())
0 400