Commit e8859752c993ff8ac3195e70f645eeaca37d4f3e
0 parents
Exists in
master
First commit
Showing 8 changed files with 434 additions and 0 deletions Side-by-side Diff
MANIFEST.in
dist/eddsctrl_server-0.1.0.tar.gz
No preview for this file type
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
setup.py
| ... | ... | @@ -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"]) |