Commit e8859752c993ff8ac3195e70f645eeaca37d4f3e

Authored by bdubois
0 parents
Exists in master

First commit

Showing 8 changed files with 434 additions and 0 deletions Inline Diff

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