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', |