Commit e8859752c993ff8ac3195e70f645eeaca37d4f3e

Authored by bdubois
0 parents
Exists in master

First commit

Showing 8 changed files with 434 additions and 0 deletions Side-by-side Diff

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