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 |