Blame view

eddsctrl_server/eddsctrlserver 15.4 KB
e8859752c   bdubois   First commit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
  #!/usr/bin/python2.7
  # -*- coding: utf-8 -*-
  
  """package eddsctrl_server
  author    Benoit Dubois
  copyright FEMTO ENGINEERING
  license   GPL v3.0+
  brief     DDS controller, server part.
  details   This package provides interface to handle DDS package.
            This package implements server part of a client/server architecture.
            Code inspired from Python Module of the Week website (BSD licence):
            http://pymotw.com/2/select/
            for the logic and from:
            http://eli.thegreenplace.net/2011/08/02/length-prefix-framing-for-protocol-buffers/
            for the message using the length prefix technique.
  """
  
  import logging
  CONSOLE_LOG_LEVEL = logging.DEBUG
  FILE_LOG_LEVEL = logging.DEBUG
  
  import os
  try:
      import configparser
  except ImportError:
      import ConfigParser as configparser
  try:
      from queue import Queue, Empty
  except ImportError:
      from Queue import Queue, Empty
  from struct import pack, unpack
  from select import select
  from socket import socket, AF_INET, SOCK_STREAM
  
  from dds.ad9912dev import Ad9912Dev as DdsDevice    # for actual use
  #from dds.dds_emul import TestUsbDds as DdsDevice   # for test without DDS
  
  from eddsctrl_server.constants import APP_NAME, SERVER_PORT, SERVERHOST, \
      DEFAULT_IFREQ, DEFAULT_OFREQ, DEFAULT_AMP, DEFAULT_PHY, \
      CFG_SERVER_FILE, LOG_SERVER_FILE
  
  
  #==============================================================================
  class EDdsCtrlServer(object):
      """Class dedicated to interface socket client with DDS device.
      Provide a basic interface to handle client queries:
      Message is built using the length prefix technique: length is sent as a 
      packed 4-byte little-endian integer.
      Allow only 4 queries, changement of output frequency, input frequency,
      phase or amplitude value.
      Message structure is simple:
        - Length of message,
        - Exclamation mark
        - Character identifier,
        - Exclamation mark
        - Value
        - Exclamation mark
      There are 8 valid identifiers:
        - 'o[?]' for set/get output frequency
        - 'i[?]' for set/get input frequency
        - 'p[?]' for set/get phase
        - 'a[?]' for set/get amplitude
      """
  
      def __init__(self, settings_file=None):
          """Constructor: initialize the server.
          :param settings_file: file containing setting data value (str)
          :returns: None
          """
          # Retrieve parameter values from 'ini' file
          if settings_file is None:
              raise ValueError("Parameter settings_file missing.")
          self._settingsf = settings_file
          config = configparser.ConfigParser()
          config.read(settings_file)
          try:
              self._port = config.getint('dds_ctrl', 'server_port')
              ifreq = config.getfloat('dds_ctrl', 'ifreq')
              ofreq = config.getfloat('dds_ctrl', 'ofreq')
              phase = config.getfloat('dds_ctrl', 'phase')
              amp = config.getint('dds_ctrl', 'amp')
          except KeyError as ex:
              logging.critical("Correct or delete the configuration file.")
              raise KeyError("Key %s not found in configuration file:"% str(ex))
          # Init devices
          self._init_dds(ifreq, ofreq, phase, amp)
          self._init_server()
          # Start threaded server
          self._server_loop()
  
      def _init_dds(self, ifreq, ofreq, phase, amp):
          """Create and configure a DDS object.
          :param ifreq: dds input frequency (float)
          :param ofreq: dds output frequency (float)
          :param phase: dds output phase (float)
          :param amp: dds output amplitude (int)
          :returns: None
          """
          self._dds = DdsDevice()
          try:
              self._dds.set_ifreq(ifreq)
              self._dds.set_ofreq(ofreq)
              self._dds.set_phy(phase)
              self._dds.set_amp(amp)
              self._dds.set_hstl_output_state(False)
              self._dds.set_cmos_output_state(False)
          except Exception as ex:
              raise Exception("Unexpected error during DDS initialisation: %s" \
                              % str(ex))
          logging.debug("DDS inititialization done")
                  
      def _init_server(self):
          """Create and configure a basic server object.
          :returns: None
          """
          try:
              self.server = socket(AF_INET, SOCK_STREAM)
              self.server.setblocking(0)
              host = SERVERHOST    # Get local machine name
              self.server.bind((host, self._port))    # Bind to the port
              self.server.listen(3)    # Now wait for client connection
          except IOError as ex:
              raise IOError("EDDSCTRLSERVER: %s, have you closed properly the " \
                            "server? " % str(ex))
          except Exception as ex:
              raise Exception("EDDSCTRLSERVER: unexpected error during server " \
                              "initialisation: %s" % str(ex))
          # Sockets from which we expect to read
          self.inputs = [self.server]
          # Sockets to which we expect to write
          self.outputs = []
          # Outgoing message queues (socket:Queue)
          self.message_queues = {}
          logging.debug("Server initialization done")
  
      def _server_loop(self):
          """Main server loop: Handle connection, read and write on server socket.
          :returns: None
          """
          while self.inputs:
              # Await an event on a readable socket descriptor
              readable, writable, exceptional = select(self.inputs, \
                                                       self.outputs, \
                                                       self.inputs)
              # Handle inputs
              self._handle_readable(readable)
              # Handle outputs
              self._handle_writable(writable)
              # Handle exceptional condition
              self._handle_exceptional(exceptional)
  
      def _handle_readable(self, socket_list):
          """Read message from client.
          Message is built using the length prefix technique. See:
          http://eli.thegreenplace.net/2011/08/02/length-prefix-framing-for-protocol-buffers/
          :param socket_list: List of socket usable to receive data
          :returns: None
          """
          for sock in socket_list:
              # Received a connect to the server (listening) socket
              if sock is self.server:
                  self._accept_connect()
              # Established connection
              else:
                  header_data = self._recv_n_bytes(sock, 4)
                  if header_data == None:
                      logging.error("Message seems received but no data to read")
                      return
                  if len(header_data) == 4:
                      msg_len = unpack('<L', header_data)[0]
                      data = self._recv_n_bytes(sock, msg_len)
                      # Check message validity
                      if data == None:
                          logging.error("Message received but no data to read")
                          return
                      if len(data) != msg_len:
                          logging.error("Bad message length")
                          return
                      logging.debug("receive %s from %s", \
                                    data, sock.getpeername())
                      # Handle message
                      ret_data = self._input_msg_handler(data)
                      # Return data to client
                      self.message_queues[sock].put(ret_data)
                      # Add output channel for response
                      if sock not in self.outputs:
                          self.outputs.append(sock)
                  else:
                      # Interpret empty result as closed connection
                      logging.info("Closing connection after reading no data")
                      # Stop listening for input on the connection
                      if sock in self.outputs:
                          self.outputs.remove(sock)
                      self.inputs.remove(sock)
                      sock.close()
                      # Remove message queue
                      del self.message_queues[sock]
  
      def _handle_writable(self, socket_list):
          """Send message to client.
          :param socket_list: List of socket object usable to send data
          :returns: None
          """
          for sock in socket_list:
              try:
                  next_msg = self.message_queues[sock].get_nowait()
              except Empty:
                  self.outputs.remove(sock)
              else:
                  self._send_msg(sock, next_msg)
                  logging.debug("send %s to %s", next_msg, sock.getpeername())
  
      def _handle_exceptional(self, socket_list):
          """Handle error with socket by closing it.
          :param socket_list: List of socket object in exceptional condition
          :returns: None
          """
          try:
              for sock in socket_list:
                  logging.warning("handling exceptional condition for %s", \
                                  sock.getpeername())
                  # Stop listening for input on the connection
                  self.inputs.remove(sock)
                  if sock in outputs:
                      self.outputs.remove(sock)
                  sock.close()
                  # Remove message queue
                  del self.message_queues[sock]
          except Exception as ex:
              logging.error("Unexpected error: %s", ex)
      
      def _input_msg_handler(self, msg):
          """Handle message from clients. Message format is defined in
          :class:`eddsctrl.server.eddsctrlsserver.EDdsCtrlServer`.
          Message contains a command to update DDS device state.
          :param msg: A formated string message (str)
          :returns: Return actual value of parameter in DDS (float)
          """
          split_msg = msg.split("!")
          index = split_msg[1].strip('\0') # Remove extra binary NULL characters
          value = split_msg[2]
          config = configparser.ConfigParser()
          config.read(self._settingsf)
          if index == "o":
              retval = self._dds.set_ofreq(float(value))
              config.set('dds_ctrl', 'ofreq', str(self._dds.get_ofreq()))
          elif index == "p":
              retval = self._dds.set_phy(float(value))
              config.set('dds_ctrl', 'phase', str(self._dds.get_phy()))
          elif index == "a":
              retval = self._dds.set_amp(int(value))
              config.set('dds_ctrl', 'amp', str(self._dds.get_amp()))
          elif index == "i":
              retval = self._dds.set_ifreq(float(value))
              config.set('dds_ctrl', 'ifreq', str(self._dds.get_ifreq()))
          elif index == "o?":
              retval = self._dds.get_ofreq()
          elif index == "p?":
              retval = self._dds.get_phy()
          elif index == "a?":
              retval = self._dds.get_amp()
          elif index == "i?":
              retval = self._dds.get_ifreq()
          else: # Bad identifier, message not valid
              logging.error("Bad identifier," \
                            "expected o[?], p[?], a[?] or i[?]: ", \
                            index, " given")
              return
          # Write modification to setting file
          with open(self._settingsf, 'w') as fd:
              config.write(fd)
          # Note that value actually writed in DDS (retval) can be a bit different
          # than value sended to DDS (value).
  
          
          
          msg = "!" + index + "!" + str(retval) + "!"
          return msg
  
      @staticmethod
      def _send_msg(sock, msg):
          """Method for sending 'msg' to socket.
          Message is built using the length prefix technique. See:
          http://eli.thegreenplace.net/2011/08/02/length-prefix-framing-for-protocol-buffers/
          :param sock: a valid socket object
          :param msg: message to be send (str)
          :returns: None
          """
          header = pack('<L', len(msg))
          try:
              sock.sendall(header + msg)
          except IOError as ex:
              logging.error("Error during sending: %s", ex)
  
      @staticmethod
      def _recv_n_bytes(sock, n):
          """Convenience method for receiving exactly n bytes from socket
          (assuming it's open and connected).
          :param sock: socket object which receives the n bytes
          :param n: number of bytes to be received (int)
          :returns: data received (str)
          """
          data = ''
          while len(data) < n:
              try:
                  chunk = sock.recv(n - len(data))
                  if chunk == '':
                      break
                  data += chunk
              except IOError as ex:
                  logging.error("Socket error in _recv_n_bytes: %s", ex)
                  return
              except Exception as ex:
                  logging.error("Error in _recv_n_bytes: %s", ex)
                  return
          return data
  
      def _accept_connect(self):
          """Server accept connection from client.
          :returns: a new socket object related to client connection (socket)
          """
          # A "readable" server socket is ready to accept a connection
          connection, client_address = self.server.accept()
          logging.info("New connection from %s", client_address)
          connection.setblocking(0)
          self.inputs.append(connection)
          self.outputs.append(connection)
          # Give the connection a queue for data we want to send
          self.message_queues[connection] = Queue()
          return connection
  
  
  #==============================================================================
  def reset_settings(settings_file):
      """Resets the "settings" file with default values.
      :param settings_file: file containing setting data value (str)
      :returns: None
      """
      config = configparser.ConfigParser()
      config.add_section('dds_ctrl')
      config.set('dds_ctrl', 'server_port', str(SERVER_PORT))
      config.set('dds_ctrl', 'ofreq', str(DEFAULT_OFREQ))
      config.set('dds_ctrl', 'phase', str(DEFAULT_PHY))
      config.set('dds_ctrl', 'amp', str(DEFAULT_AMP))
      config.set('dds_ctrl', 'ifreq', str(DEFAULT_IFREQ))
      # Write modification to setting file
      with open(settings_file, 'w') as fd:
          fd.truncate(0) # Reset file contents
          config.write(fd)
      logging.info("Settings file reseted.")
  
  
  #==============================================================================
  def configure_logging():
      """Configures logs.
      """
      date_fmt = "%d/%m/%Y %H:%M:%S"
      log_format = "%(asctime)s %(levelname) -8s %(filename)s " + \
                   " %(funcName)s (%(lineno)d): %(message)s"
      logging.basicConfig(level=FILE_LOG_LEVEL, \
                          datefmt=date_fmt, \
                          format=log_format, \
                          filename=LOG_SERVER_FILE, \
                          filemode='w')
      console = logging.StreamHandler()
      # define a Handler which writes messages to the sys.stderr
      console.setLevel(CONSOLE_LOG_LEVEL)
      # set a format which is simpler for console use
      console_format = '%(levelname) -8s %(filename)s (%(lineno)d): %(message)s'
      formatter = logging.Formatter(console_format)
      # tell the handler to use this format
      console.setFormatter(formatter)
      # add the handler to the root logger
      logging.getLogger('').addHandler(console)
  
  
  #==============================================================================
  def eddsctrlserver(settings_file):
      configure_logging()
      if os.path.isfile(settings_file) is False:
          logging.error("Settings file missing: create one with default values.")
          reset_settings(settings_file)
      EDdsCtrlServer(settings_file)
  
  
  #==============================================================================
  if __name__ == '__main__':
      # Ctrl-c closes the application
      import signal
      signal.signal(signal.SIGINT, signal.SIG_DFL)
  
      eddsctrlserver(CFG_SERVER_FILE)