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 |