Commit e8859752c993ff8ac3195e70f645eeaca37d4f3e
0 parents
Exists in
master
First commit
Showing 8 changed files with 434 additions and 0 deletions Inline Diff
MANIFEST.in
| File was created | 1 | include README | ||
| 2 | recursive-include eddsctrl_server *.py |
dist/eddsctrl_server-0.1.0.tar.gz
No preview for this file type
eddsctrl_server/__init__.py
| File was created | 1 | # -*- coding: utf-8 -*- | ||
| 2 | """ eddsctrl_server/__init__.py """ |
eddsctrl_server/constants.py
| File was created | 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 |
eddsctrl_server/eddsctrlserver
| File was created | 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, \ |
eddsctrl_server/version.py
| File was created | 1 | """ eddsctrl_server/version.py """ | ||
| 2 | __version__ = "0.1.0" | |||
| 3 |
setup.py
| File was created | 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', |