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"]) |