Stefan Schuermans commited on 2017-05-26 14:52:09
Showing 6 changed files, with 344 additions and 20 deletions.
... | ... |
@@ -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 |
+ |
... | ... |
@@ -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 |