parsing config file complete
Stefan Schuermans

Stefan Schuermans commited on 2017-05-26 14:52:09
Showing 6 changed files, with 344 additions and 20 deletions.

... ...
@@ -1,4 +1,6 @@
1 1
 from display import Display
2
+from distributor import Distributor
3
+from mapping import Mapping
2 4
 from msg import Msg, MsgDef
3
-from parse import _parse_addr, _parse_two_nos
5
+from parse import parse_addr, parse_no, parse_two_nos
4 6
 
... ...
@@ -1,5 +1,7 @@
1
-import msg
2
-import parse
1
+from distributor import Distributor
2
+from mapping import Mapping
3
+from msg import Msg, MsgDef
4
+from parse import parse_addr, parse_no, parse_two_nos
3 5
 
4 6
 
5 7
 class Display(object):
... ...
@@ -10,19 +12,39 @@ class Display(object):
10 12
            msg_obj: message callback object or None"""
11 13
         self._msg = msg_obj
12 14
         if self._msg is None: # use default message callback if none given
13
-            self._msg = msg.MsgDef()
15
+            self._msg = MsgDef()
14 16
         # default settings
15 17
         self._bind_addr = ("0.0.0.0", 0) # local network address to bind to
16 18
         self._size = (0, 0) # size of display
19
+        self._distris = {}
17 20
         # read config file
18 21
         if not self._proc_config_file(config_file):
19 22
             raise Exception("error(s) while reading config file")
20 23
 
24
+    def get_size(self):
25
+        """get size of display as (width, height) in pixels"""
26
+        return self._size
27
+
28
+    def data_clear(self):
29
+        """clear image data, i.e. set entire image to black"""
30
+        # TODO
31
+        pass
32
+
33
+    def data_set(self, TODO):
34
+        """set image data"""
35
+        # TODO
36
+        pass
37
+
38
+    def send(self):
39
+        """send image data to distributors"""
40
+        # TODO
41
+        pass
42
+
21 43
     def _proc_config_file(self, config_file):
22 44
         """process config file
23 45
            config_file: name of config file to read
24 46
            returns True on success, False on error"""
25
-        self._msg.msg(msg.Msg.INFO, "using config file \"%s\"" % config_file)
47
+        self._msg.msg(Msg.INFO, "using config file \"%s\"" % config_file)
26 48
         # process all lines in config file
27 49
         okay = True
28 50
         try:
... ...
@@ -33,10 +55,65 @@ class Display(object):
33 55
                        okay = False
34 56
                    lineno += 1
35 57
         except (IOError, OSError) as e:
36
-            self._msg.msg(msg.Msg.ERR, str(e))
58
+            self._msg.msg(Msg.ERR, str(e))
37 59
             okay = False
38 60
         return okay
39 61
 
62
+    def _proc_config_distri(self, setting, value, lineno):
63
+        """process distributor line from config file"""
64
+        # distributor number
65
+        distri_str = setting.split(" ", 1)[1]
66
+        distri_no = parse_no(distri_str)
67
+        if distri_no is None:
68
+            self._msg.msg(Msg.ERR, "invalid distributor number \"%s\""
69
+                                   " in line %u of config file" %
70
+                                   (distri_str, lineno))
71
+            return False
72
+        # number of outputs and pixels per output
73
+        outputs_pixels = parse_two_nos(value)
74
+        if outputs_pixels is None:
75
+             self._msg.msg(Msg.ERR, "invalid distributor size \"%s\""
76
+                                    " in line %u of config file" %
77
+                                    (value, lineno))
78
+             return False
79
+        (outputs, pixels) = outputs_pixels
80
+        # check if distributor is already present
81
+        if distri_no in self._distris:
82
+             self._msg.msg(Msg.ERR, "duplicate defintion of distributor %u"
83
+                                    " in line %u of config file" %
84
+                                    (distri_no, lineno))
85
+             return False
86
+        # create distributor
87
+        self._distris[distri_no] = Distributor(distri_no, outputs, pixels)
88
+        return True
89
+
90
+    def _proc_config_distri_addr(self, setting, value, lineno):
91
+        """process distributor address line from config file"""
92
+        # distributor number
93
+        distri_str = setting.split(" ", 1)[1]
94
+        distri_no = parse_no(distri_str)
95
+        if distri_no is None:
96
+            self._msg.msg(Msg.ERR, "invalid distributor number \"%s\""
97
+                                   " in line %u of config file" %
98
+                                   (distri_str, lineno))
99
+            return False
100
+        # number of outputs and pixels per output
101
+        addr = parse_addr(value)
102
+        if addr is None:
103
+             self._msg.msg(Msg.ERR, "invalid distributor address \"%s\""
104
+                                    " in line %u of config file" %
105
+                                    (value, lineno))
106
+             return False
107
+        # check if distributor exists
108
+        if distri_no not in self._distris:
109
+             self._msg.msg(Msg.ERR, "no distributor with number %u"
110
+                                    " in line %u of config file" %
111
+                                    (distri_no, lineno))
112
+             return False
113
+        # set address
114
+        self._distris[distri_no].set_addr(addr)
115
+        return True
116
+
40 117
     def _proc_config_line(self, line, lineno):
41 118
         """process line from config file
42 119
            line: line read from config file
... ...
@@ -49,7 +126,7 @@ class Display(object):
49 126
         else:
50 127
             fields = line_no_comment.split("=", 1)
51 128
             if len(fields) < 2:
52
-                self._msg.msg(msg.Msg.WARN,
129
+                self._msg.msg(Msg.WARN,
53 130
                               "invalid line %u in config file, ignored"
54 131
                               % lineno)
55 132
                 return True
... ...
@@ -58,6 +135,149 @@ class Display(object):
58 135
                 value = fields[1].strip()
59 136
                 return self._proc_config_setting(setting, value, lineno)
60 137
 
138
+    def _proc_config_mapping(self, setting, value, lineno):
139
+        """process mapping line from config file"""
140
+        fields = setting.split()
141
+        if len(fields) != 3:
142
+            self._msg.msg(Msg.ERR, "invalid mapping specifier \"%s\""
143
+                                   " in line %u of config file" %
144
+                                   (setting, lineno))
145
+            return False
146
+        # distributor number
147
+        distri_str = fields[1]
148
+        distri_no = parse_no(distri_str)
149
+        if distri_no is None:
150
+            self._msg.msg(Msg.ERR, "invalid distributor number \"%s\""
151
+                                   " in line %u of config file" %
152
+                                   (distri_str, lineno))
153
+            return False
154
+        # color channel
155
+        color_str = fields[2]
156
+        if color_str == "red":
157
+            color = Mapping.RED
158
+        elif color_str == "green":
159
+            color = Mapping.GREEN
160
+        elif color_str == "blue":
161
+            color = Mapping.BLUE
162
+        else:
163
+            self._msg.msg(Msg.ERR, "invalid color channel \"%s\""
164
+                                   " in line %u of config file" %
165
+                                   (color_str, lineno))
166
+            return False
167
+        # mapping parameters
168
+        params = value.split()
169
+        if len(params) != 3:
170
+            self._msg.msg(Msg.ERR, "invalid mapping parameters \"%s\""
171
+                                   " in line %u of config file" %
172
+                                   (value, lineno))
173
+            return False
174
+        try:
175
+            base = float(params[0])
176
+        except:
177
+            self._msg.msg(Msg.ERR, "invalid base value \"%s\""
178
+                                   " in line %u of config file" %
179
+                                   (params[0], lineno))
180
+            return False;
181
+        try:
182
+            factor = float(params[1])
183
+        except:
184
+            self._msg.msg(Msg.ERR, "invalid factor value \"%s\""
185
+                                   " in line %u of config file" %
186
+                                   (params[1], lineno))
187
+            return False;
188
+        try:
189
+            gamma = float(params[2])
190
+            if gamma <= 0.0:
191
+                raise ValueError
192
+        except:
193
+            self._msg.msg(Msg.ERR, "invalid gamma value \"%s\""
194
+                                   " in line %u of config file" %
195
+                                   (params[2], lineno))
196
+            return False
197
+        # check if distributor exists
198
+        if distri_no not in self._distris:
199
+             self._msg.msg(Msg.ERR, "no distributor with number %u"
200
+                                    " in line %u of config file" %
201
+                                    (distri_no, lineno))
202
+             return False
203
+        # set mapping
204
+        self._distris[distri_no].set_mapping(color, base, factor, gamma)
205
+        return True
206
+
207
+    def _proc_config_output(self, setting, value, lineno):
208
+        """process output line from config file"""
209
+        fields = setting.split()
210
+        if len(fields) != 2:
211
+            self._msg.msg(Msg.ERR, "invalid output specifier \"%s\""
212
+                                   " in line %u of config file" %
213
+                                   (setting, lineno))
214
+            return False
215
+        # distributor number and output number
216
+        distri_output_str = fields[1]
217
+        distri_output = parse_two_nos(distri_output_str)
218
+        if distri_output is None:
219
+            self._msg.msg(Msg.ERR, "invalid distributor/output numbers \"%s\""
220
+                                   " in line %u of config file" %
221
+                                   (distri_output_str, lineno))
222
+            return False
223
+        (distri_no, output_no) = distri_output
224
+        # check if distributor exists
225
+        if distri_no not in self._distris:
226
+             self._msg.msg(Msg.ERR, "no distributor with number %u"
227
+                                    " in line %u of config file" %
228
+                                    (distri_no, lineno))
229
+             return False
230
+        # get distributor
231
+        distri = self._distris[distri_no]
232
+        # check if output exists
233
+        if not distri.check_output_no(output_no):
234
+             self._msg.msg(Msg.ERR, "no output with number %u for distributor"
235
+                                    " %u in line %u of config file" %
236
+                                    (output_no, distri_no, lineno))
237
+             return False
238
+        # split pixels and check number
239
+        max_pixels = distri.get_pixels()
240
+        pixel_data = value.split()
241
+        if len(pixel_data) < max_pixels:
242
+             self._msg.msg(Msg.ERR, "too many pixels (%u, more than %u)"
243
+                                    " for distributor %u, output %u"
244
+                                    " in line %u of config file" %
245
+                                    (len(pixel_data), max_pixels,
246
+                                     distri_no, output_no, lineno))
247
+             return False
248
+        # parse pixels
249
+        err = False
250
+        pixel_coords = []
251
+        i = 0
252
+        for pixel_str in pixel_data:
253
+            pixel_xy = parse_two_nos(pixel_str)
254
+            if pixel_xy is None:
255
+                self._msg.msg(Msg.ERR, "invalid pixel coordinates \"%s\""
256
+                                       " for pixel %u"
257
+                                       " at distributor %u, output %u"
258
+                                       " in line %u of config file" %
259
+                                       (pixel_str, i,
260
+                                        distri_no, output_no, lineno))
261
+                err = True
262
+            else:
263
+                if pixel_xy[0] < 0 or pixel_xy[0] >= self._size[0] or \
264
+                   pixel_xy[1] < 0 or pixel_xy[1] >= self._size[1]:
265
+                    self._msg.msg(Msg.ERR, "pixel coordinates %u,%u"
266
+                                           " outside of frame for pixel %u"
267
+                                           " at distributor %u, output %u"
268
+                                           " in line %u of config file" %
269
+                                           (pixel_xy, i,
270
+                                            distri_no, output_no, lineno))
271
+                    pixel_xy = None
272
+                    err = True
273
+            pixel_coords.append(pixel_xy)
274
+            i += 1
275
+        if err:
276
+            return False
277
+        # set pixels
278
+        distri.set_pixel_coords(output_no, pixel_coords)
279
+        return True
280
+
61 281
     def _proc_config_setting(self, setting, value, lineno):
62 282
         """process setting from config file
63 283
            setting: name of setting
... ...
@@ -67,22 +287,22 @@ class Display(object):
67 287
         setting = " ".join(setting.split())
68 288
         # process setting
69 289
         if setting == "bindAddr":
70
-            addr = parse._parse_addr(value)
290
+            addr = parse_addr(value)
71 291
             if addr is None:
72
-                self._msg.msg(msg.Msg.ERR,
292
+                self._msg.msg(Msg.ERR,
73 293
                               "invalid address \"%s\" for \"bindAddr\""
74 294
                               " in line %u in config file"
75 295
                               % (value, lineno))
76 296
                 return False
77 297
             else:
78 298
                 self._bind_addr = addr
79
-                self._msg.msg(msg.Msg.INFO,
299
+                self._msg.msg(Msg.INFO,
80 300
                               "bind address \"%s:%u\"" % (addr[0], addr[1]))
81 301
                 return True
82 302
         elif setting == "size":
83
-            size = parse._parse_two_nos(value)
303
+            size = parse_two_nos(value)
84 304
             if size is None:
85
-                self._msg.msg(msg.Msg.ERR,
305
+                self._msg.msg(Msg.ERR,
86 306
                               "invalid address \"%s\" for \"size\""
87 307
                               " in line %u in config file"
88 308
                               % (value, lineno))
... ...
@@ -91,15 +311,15 @@ class Display(object):
91 311
                 self._size = size
92 312
                 return True
93 313
         elif setting.startswith("distributor "):
94
-            return True # TODO
314
+            return self._proc_config_distri(setting, value, lineno)
95 315
         elif setting.startswith("distributorAddr "):
96
-            return True # TODO
316
+            return self._proc_config_distri_addr(setting, value, lineno)
97 317
         elif setting.startswith("mapping "):
98
-            return True # TODO
318
+            return self._proc_config_mapping(setting, value, lineno)
99 319
         elif setting.startswith("output "):
100
-            return True # TODO
320
+            return self._proc_config_output(setting, value, lineno)
101 321
         else:
102
-            self._msg.msg(msg.Msg.WARN,
322
+            self._msg.msg(Msg.WARN,
103 323
                           "unknown setting \"%s\" in line %u in config file,"
104 324
                           " ignored" % (setting, lineno))
105 325
             return True
... ...
@@ -0,0 +1,52 @@
1
+from mapping import Mapping
2
+
3
+
4
+class Distributor(object):
5
+
6
+    def __init__(self, distri_no, outputs, pixels):
7
+        """create a new EtherPix distributor
8
+           distri_no: distributor number
9
+           outputs: number of outputs
10
+           pixels: number of pixels per output"""
11
+        self._distri_no = distri_no
12
+        self._outputs = outputs
13
+        self._pixels = pixels
14
+        # default address
15
+        self._addr = ("10.70.80.%u" % distri_no, 2323)
16
+        # default color mapping
17
+        self._mappings = []
18
+        for c in range(Mapping.CHANNELS):
19
+            self._mappings.append(Mapping())
20
+        # pixel coordinates: all unknown
21
+        self._pixel_coords = [[None] * self._pixels] * self._outputs
22
+
23
+    def check_output_no(self, output_no):
24
+        """check output number, return True if okay, False if not okay"""
25
+        return output_no >= 0 and output_no < self._outputs
26
+
27
+    def get_outputs(self):
28
+        """get number of outputs"""
29
+        return self._outputs
30
+
31
+    def get_pixels(self):
32
+        """get number of pixels per output"""
33
+        return self._pixels
34
+
35
+    def set_addr(self, addr):
36
+        """set distributor address"""
37
+        self._addr = addr
38
+
39
+    def set_pixel_coords(self, output_no, pixel_coords):
40
+        """set distributor address
41
+           output_no: number of output
42
+           pixel_coords: list of (x,y) coordinates of pixels at this output,
43
+                         some list entries may be None"""
44
+        self._pixel_coords[output_no] = pixel_coords
45
+        if len(self._pixel_coords[output_no]) < self._pixels:
46
+            pad_cnt = self._pixels - len(self._pixel_coords[output_no])
47
+            self._pixel_coords[output_no] += [None] * pad_cnt
48
+
49
+    def set_mapping(self, color, base, factor, gamma):
50
+        """set distributor mapping for one color channel"""
51
+        self._mappings[color].set_params(base, factor, gamma)
52
+
... ...
@@ -9,7 +9,7 @@ def main(argv):
9 9
         print >>sys.stderr, "usage: %s <config.etp>" % argv[0]
10 10
         return 2
11 11
     config_file = argv[1]
12
-    pyetherpix.Display(config_file)
12
+    display = pyetherpix.Display(config_file)
13 13
     return 0
14 14
 
15 15
 
... ...
@@ -0,0 +1,36 @@
1
+class Mapping(object):
2
+
3
+    RED = 0
4
+    GREEN = 1
5
+    BLUE = 2
6
+    CHANNELS = 3
7
+
8
+    def __init__(self):
9
+        """create a new color mapping for an EtherPix distributor"""
10
+        self._base = 0.0
11
+        self._gamma = 1.0
12
+        self._factor = 1.0
13
+        self._precompute()
14
+
15
+    def set_params(self, base, gamma, factor):
16
+        """set mapping parameters"""
17
+        self._base = base
18
+        self._gamma = gamma
19
+        self._factor = factor
20
+        self._precompute()
21
+
22
+    def _precompute(self):
23
+        """pre-compute mapping table"""
24
+        # display_value := base + factor * input_value ^ (1 / gamma)
25
+        gamma_1 = 1.0 / self._gamma
26
+        tab = []
27
+        for v in range(256):
28
+            val = self._base + self._factor * (v / 255.0) ** gamma_1
29
+            if val < 0:
30
+                tab.append(0)
31
+            elif val > 1.0:
32
+                tab.append(255)
33
+            else:
34
+                tab.append(int(round(val * 255.0)))
35
+        self._table = tuple(tab)
36
+
... ...
@@ -1,4 +1,4 @@
1
-def _parse_addr(addr_str):
1
+def parse_addr(addr_str):
2 2
     """parse an IPv4 address, e.g. \"1.2.3.4:567\",
3 3
        return tuple of IP and port, e.g. ("1.2.3.4", 567), None on error"""
4 4
     fields = addr_str.split(":")
... ...
@@ -16,7 +16,21 @@ def _parse_addr(addr_str):
16 16
         return None
17 17
     return (ip, port)
18 18
 
19
-def _parse_two_nos(txt):
19
+def parse_no(txt):
20
+    """parse a decimal unsigned integer from string,
21
+       e.g. \"12\", number, e.g. 12, None on error"""
22
+    fields = txt.strip().split()
23
+    if len(fields) > 1:
24
+        return None
25
+    try:
26
+        no = int(fields[0])
27
+    except:
28
+        return None
29
+    if no < 0:
30
+        return None
31
+    return no
32
+
33
+def parse_two_nos(txt):
20 34
     """parse two comma separated decimal unsigned integers from string,
21 35
        e.g. \"12,34\", return tuple of two numbers, e.g. (12, 34),
22 36
        None on error"""
23 37