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 |