├── .DS_Store
├── .gitignore
├── .project
├── .pydevproject
├── .vscode
├── launch.json
└── settings.json
├── Makefile
├── README.md
├── bin
├── .DS_Store
├── .swp
├── QLC.py
├── config.yml
├── expand_fixture_features.py
├── generate_fixtures.py
├── map_fixtures.yml
├── surface_map.yml
└── sync_mitti.py
├── docs
├── Chase.png
├── Collection.png
├── Collections.png
├── Colors.png
├── CueList.png
├── ExpandFixtureFeatures.md
├── ExpandRGBLayouts.md
├── FixtureDefinition.png
├── FixtureDefinitions.png
├── FixtureScenes.png
├── ShowfileDesign.md
└── XLSFixtureGroup-Spots.png
├── fixtures
└── fixtures.yml
├── lib
└── qlcplus_bin.py
├── requirements.txt
├── showfiles
└── blank.qxw
└── sourceme
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv
2 | *.pyc
3 |
4 | *.qxw
5 | *.xls
6 | .vscode/settings.json
7 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | qlcplus-tools
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 | Default
4 | python interpreter
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 | {
9 | "name": "Python: generate_fixtures.py",
10 | "type": "python",
11 | "request": "launch",
12 | "program": "bin/generate_fixtures.py",
13 | "console": "integratedTerminal",
14 | "args": [ "-c", "bin/config.yml", "bin/blank.qxw", "-d" , "-D", "-V"]
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "/Users/branson/git/qlcplus-tools/venv/bin/python3"
3 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile to generate working environment for Ops
2 |
3 | # used to determine arch for downloads
4 | UNAME := $(shell uname | tr '[:upper:]' '[:lower:]')
5 |
6 | ACTIVATE_BIN := venv/bin/activate
7 |
8 | all: $(ACTIVATE_BIN) pip_requirements
9 |
10 | clean:
11 | $(RM) -r venv
12 | find . -name "*.pyc" -exec $(RM) -rf {} \;
13 |
14 | $(ACTIVATE_BIN):
15 | python3 -m venv venv
16 | chmod +x $@
17 |
18 | pip_requirements: $(ACTIVATE_BIN) requirements.txt
19 | . venv/bin/activate; PYTHONWARNINGS='ignore:DEPRECATION' pip3 install -r requirements.txt
20 |
21 | freeze: requirements.txt
22 | pip3 freeze > requirements.txt
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qlcplus-tools
2 |
3 | Tools for managing and expanding QLC+ Files
4 |
5 | This repo is a collection of tools I use to manage QLC showfiles. I will work to
6 | add functionality as I go. Much of this is predicated on my use case.. which is:
7 |
8 | 1. configure fixtures and other DMX targets for the stage
9 | 1. create fixture groups based on show design
10 | 1. create sets of scenes by fixture features (tedious for every base combination)
11 | 1. create RGBMatrix and EFX as needed
12 | 1. combine scenes, matrix, EFX in collections as cue points
13 | 1. assemble collections into chaser by points in the show with timing and fading
14 | 1. cue the show
15 |
16 | You can see more details in [ShowfileDesign.md](./docs/ShowfileDesign.md)
17 |
18 | ## Naming Conventions
19 | This is predicated on what's been easy for me to teach to students and easy to find when assembling scenes into collections for a show
20 |
21 | - each FixtureBasedScene Name uses the full path in the storage structure to allow clean identification of the scene
22 | - each Collection is named based on thier use case
23 | - for cuepoints in the show I use {song}-{section}.{cue}-{name} to allow easy identification
24 |
25 |
26 | # Getting Started
27 |
28 | This tooling is setup to run in a venv locally and cleanly, to get things setup
29 |
30 | ```
31 | git clone git@github.com:sandinak/qlcplus-tools.git
32 | . sourceme
33 | make
34 | ```
35 |
36 | - All the programs have a --help option
37 | - All the programs are designed to be non-destructive .. they should not touch any existing configuration
38 |
39 | ## How to use these tools:
40 |
41 | 1. Create a new show file with fixtures mapped to universes as usual
42 | 2. Create FixtureGroups of lights you'd like to address simultaniously, these can be of the same fixture type or different types
43 | 3. Save the file
44 | 4. Run the programs against the file. There is an --output option, however the programs are designed to be non-destructive and will only create things.
45 | 5. Read the file back into QLC+
46 | 6. Profit!
47 |
48 | ## NOTES
49 |
50 | - So you can run these programs as many times as you'd like .. they're idempotent
51 | - If you add/change/update FixtureGroups .. you can just re-run the programs on the files.
52 | - As noted .. these are non-destructive.. so if you remove FixtureGroups .. this will NOT remove the corresponding scenes for those Groups
53 |
54 | # Programs
55 |
56 | ## expand_fixture_features
57 |
58 | This tool has two major functions
59 |
60 | - [Expand Fixture Features](./docs/ExpandFixtureFeatures.md) - generate scenes for fixture features.
61 | - [Expand RGB Matrix Layouts](./docs/ExpandRGBLayouts.md) - export/import .xls files with layouts
62 |
63 | ## sync_mitti
64 | I use the Mitti Video tool to display basic video on screens as part of our show. Given the number of potential cues in Mitti that must match up to the cues in QLC; I wanted a way to syncronise these two so when a 'cue' is placed in QLC for a collection, it will be consistently activated on Mitti when the cue is activated on QLC.
65 |
66 | So it creates global and cue functions that do not require values, each as Script entries using the sendosc command.
67 |
68 | ## generate_fixtures
69 |
70 | This was a one-off program that generates large sets of fixtures. It's mostly deprecated and here as a framework if needed again.
71 |
72 |
--------------------------------------------------------------------------------
/bin/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/bin/.DS_Store
--------------------------------------------------------------------------------
/bin/.swp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/bin/.swp
--------------------------------------------------------------------------------
/bin/QLC.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | '''
4 | ============================================================================
5 | Title: manage qlc showfile
6 | Description:
7 | QLC Libraries
8 | ============================================================================
9 | NOTES/TODO:
10 | - need to refine the searches to be attribute aware instead of defining them discretely.
11 | - need to setup add/delete for things (Functions, Heads, etc)
12 | - allow import of colors for creation?
13 | - handle 'light' colors for heads that dont' have a white LED
14 | - add 'preset' position creation for all fixtures.
15 | '''
16 |
17 | import logging
18 | import xml.etree.ElementTree as ET
19 | from xml.dom import XML_NAMESPACE, minidom
20 | import sys
21 | import copy
22 | import pprint
23 | import urllib.parse
24 | import re
25 | import os
26 | import xlsxwriter
27 | import pandas as pd
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 | # colors
32 | # http://www.webriti.com/wp-content/uploads/2012/01/rgb-color-wheel-lg.jpg
33 | # colors for white.
34 | # TODO: Make a set w/o white called rgb
35 | FIXTURE_PATHS = [
36 | '/Applications/QLC+.app/Contents/Resources/Fixtures',
37 | '~/Library/Application Support/QLC+/Fixtures'
38 | ]
39 | rgbw = { #shortname R G B W
40 | 'Red': { 'n':'R', 'rgbw': [255, 0, 0, 0], },
41 | 'Green': { 'n':'G', 'rgbw': [ 0, 255, 0, 0], },
42 | 'Blue': { 'n':'B', 'rgbw': [ 0, 0, 255, 0], },
43 | 'Yellow': { 'n':'Y', 'rgbw': [255, 255, 0, 0], },
44 | 'Cyan': { 'n':'C', 'rgbw': [ 0, 255, 255, 0], },
45 | 'Magenta': { 'n':'M', 'rgbw': [255, 0, 255, 0], },
46 | 'Violet': { 'n':'V', 'rgbw': [128, 0, 255, 0], },
47 | 'All': { 'n':'A', 'rgbw': [255, 255, 255, 255], },
48 | 'Rasp': { 'n':'Ra', 'rgbw': [255, 0, 128, 0], },
49 | 'Orange': { 'n':'Or', 'rgbw': [255, 128, 0, 0], },
50 | 'Ocean': { 'n':'Oc', 'rgbw': [ 0, 128, 128, 0], },
51 | 'Turqoise': { 'n':'T', 'rgbw': [ 0, 255, 128, 0], },
52 | 'Purple': { 'n':'P', 'rgbw': [128, 0, 255, 0], },
53 | 'Pink': { 'n':'Pk', 'rgbw': [255, 0, 128, 0], },
54 | 'White': { 'n':'W', 'rgbw': [ 0, 0, 0, 255], },
55 | 'Flesh': { 'n':'F', 'rgbw': [245, 204, 176, 0], },
56 | 'IntOnly': { 'n':'IO', 'rgbw': [ 0, 0, 0, 0], },
57 |
58 | # with white
59 | 'Red+W': { 'n':'RW', 'rgbw': [255, 0, 0, 255], },
60 | 'Green+W': { 'n':'GW', 'rgbw': [0, 255, 0, 255], },
61 | 'Blue+W': { 'n':'BW', 'rgbw': [0, 0, 255, 255], },
62 | 'Yellow+W': { 'n':'YW', 'rgbw': [255, 255, 0, 255], },
63 | 'Cyan+W': { 'n':'CW', 'rgbw': [0, 255, 255, 255], },
64 | 'Red+W': { 'n':'MW', 'rgbw': [255, 0, 255, 255], },
65 | 'Purple+W': { 'n':'PW', 'rgbw': [128, 0, 255, 255], },
66 | 'Rasp+W': { 'n':'RaW', 'rgbw': [255, 0, 128, 255], },
67 | 'Orange+W': { 'n':'OrW', 'rgbw': [255, 128, 0, 255], },
68 | 'Ocean+W': { 'n':'OcW', 'rgbw': [ 0, 128, 128, 255], },
69 | 'Turqoise+W': { 'n':'TW', 'rgbw': [ 0, 255, 128, 255], },
70 | 'Purple+W': { 'n':'PW', 'rgbw': [128, 0, 255, 255], },
71 | 'Pink+W': { 'n':'PkW', 'rgbw': [255, 0, 128, 255], },
72 | 'Flesh+W': { 'n':'FW', 'rgbw': [245, 204, 176, 255], },
73 | }
74 | # because names are such random things
75 | RGB_ALTERNATES = {
76 | 'Cyan': 'Light Blue'
77 | }
78 |
79 | SPOT_ON_CHANNELS = [
80 | 'master dimmer',
81 | 'spot-master dimmer',
82 | 'strobe/shutter',
83 | 'shutter',
84 | 'dimmer',
85 | 'intensity',
86 | 'dim/strobe'
87 | ]
88 |
89 | # NOTE: this is case sensitive.
90 | SPOT_COLORWHEEL_NAMES = [
91 | 'Colour',
92 | 'Color',
93 | 'Color wheel',
94 | 'Color Wheel',
95 | 'Spot-Color Wheel',
96 | ]
97 |
98 |
99 | MOVEMENT_CHANNEL_NAMES = [
100 | 'Pan',
101 | 'Level',
102 | 'Horizontal',
103 | 'Tilt',
104 | 'Vertical'
105 | ]
106 |
107 | # there's really only 1 here as we don't know the relative
108 | # horizontal positions, nor the total vertical deflection;
109 | # however having the preset to start with will help minimize
110 | # movements from this position .. rather than starting with 0,0
111 | # TODO: eventually it'd be good to pre-compute some positions by the
112 | # head PAN Max and Tilt Max .. however we dont' know which way
113 | # the heads turn or starting position .. so V is more reliable
114 | # than H... be nice to incporporate some of the work from
115 | # dmx_followspot which describes position, height, etc.
116 | BASE_POSITIONS = {
117 | # H V DMX Values, not angle.
118 | 'Base': [ 128, 128],
119 | '0': [ 0, 0],
120 | }
121 |
122 | # regexes
123 | size_re = re.compile(r'X:(\d+) Y:(\d+)')
124 | fixture_re = re.compile(r'^(.*) H:(\d+)') # A:(\d+) U:(\d+)$')
125 | # fixture_re = re.compile(r'^(.*)\sH:(\d+)')
126 |
127 |
128 | def match(items,item):
129 | for i in items:
130 | if i == item:
131 | return i
132 |
133 | def _strip(elem):
134 | for elem in elem.iter():
135 | if(elem.text):
136 | elem.text = str(elem.text).strip()
137 | if(elem.tail):
138 | elem.tail = elem.tail.strip()
139 |
140 | def intersection(lst1, lst2):
141 | return list(set(lst1) & set(lst2))
142 |
143 |
144 | BLANK_WORKSPACE = '''
145 |
146 |
147 |
148 |
149 | Q Light Controller Plus
150 | 4.12.3
151 | Branson Matheson
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | None
165 | Default
166 | Default
167 | None
168 | Default
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | EOF
181 | '''.strip()
182 |
183 | ENCODING = 'UTF-8'
184 | DOCTYPE='''
185 |
186 |
187 | '''.strip()
188 |
189 | class QLC():
190 | def __init__(self, file=None):
191 | ''' setup a working QLC space '''
192 | self.fixture_definitions = FixtureDefinitions(FIXTURE_PATHS)
193 | self.workspace = Workspace(file)
194 | self.output_ips = dict()
195 |
196 | def expand_fixture_group_capabilities(self):
197 | for fg in self.workspace.engine.fixture_groups.items:
198 | Name = fg.name
199 | logging.info(f'processing fixture group: {Name}')
200 | if len(fg.heads.items) == 0:
201 | continue
202 |
203 | self.generate_color_scenes(fg)
204 | self.generate_capability_scenes(fg)
205 |
206 | def generate_universes(self, universes):
207 | us = self.workspace.engine.inputoutputmap.universes
208 | # these are mapped as shown
209 | for u in universes:
210 | id = u.get('id')
211 | this_u = us.find_by_id(id)
212 | if this_u:
213 | this_u.update(u)
214 | else:
215 | us.add(u)
216 |
217 |
218 | def export_fixture_groups(self, path):
219 | fgs = self.workspace.engine.fixture_groups
220 | sheets = []
221 | workbook = xlsxwriter.Workbook(path)
222 | bold = workbook.add_format({'bold': True})
223 | bold_c = workbook.add_format({'bold': True, 'align': 'center'})
224 | for fg in fgs:
225 | worksheet = workbook.add_worksheet(fg.name)
226 | worksheet.write(0,0,f'Fixture Group: {fg.name}', bold)
227 |
228 | # generate the matrix for this sheet
229 | # m: y,x
230 | size = fg.size
231 | # NOTE: y and x are REVERSED coming from QLC,
232 | # so if this seems odd it's to handle that strange case.
233 | s_x = int(size.x or "0") + 1
234 | s_y = int(size.y or "0") + 1
235 |
236 | # test heads to make sure we have the right size
237 | for head in fg.heads:
238 | if int(head.y) > s_y:
239 | s_y = int(head.y)
240 | if int(head.x) > s_x:
241 | s_x = int(head.x)
242 |
243 | worksheet.write(1,0,f'X:{s_x} Y:{s_y}', bold)
244 | logger.info(f"initalizing table %s: {s_y},{s_x}" % fg.name)
245 | m = []
246 | # setup the grid
247 | for y in range(0, s_y+2):
248 | worksheet.write(y+2, 0, y+1, bold_c)
249 | m.append(['' for _ in range(0,s_x+2)])
250 |
251 | for x in range(0, s_x+1):
252 | worksheet.write(1, x+1, x+1, bold_c )
253 | for y in range(0, s_y+1):
254 | worksheet.write(x, y, '', )
255 |
256 | max_w = 0
257 | for head in fg.heads:
258 | # TODO do we need the universe and such or just the name here.
259 | # fname = "%s: u:%d a: %d" % (head.fixture.name, head.fixture.universe, head.fixture.channels)
260 | logger.debug(f"looking for fixture id {head.fixture}")
261 | fixture = self.workspace.engine.fixtures.find_by_id(head.fixture)
262 | if fixture:
263 | fname = fixture.name
264 | logger.info(f"storing fixture {fname} ({head.id}) at {head.y},{head.x} in {s_y},{s_x}")
265 | # qlc plus displays relative to 1 vs 0
266 | h_id = int(head.id) + 1
267 | f_addr = int(fixture.address) + 1
268 | f_uni = int(fixture.universe) + 1
269 | m_name =f"{fname} H:{h_id}"
270 | if len(m_name) > max_w:
271 | max_w = len(m_name)
272 | m[int(head.y)][int(head.x)] = f"{m_name}" # \n{m_addr}"
273 |
274 | # set widths
275 | for c in range(0, s_x+1):
276 | worksheet.set_column(c, c, max_w+1)
277 |
278 | worksheet.add_table(2, 1, 2+s_y, 1+s_x, { 'header_row': False, 'data': m})
279 | workbook.close()
280 |
281 |
282 | def extract_fixture_from_cell(self, data):
283 | if isinstance(data, str):
284 | data = data.replace('\n',' ')
285 | r = fixture_re.search(data)
286 | if not r:
287 | logger.error(f"unlable to match fixture data\n===\n{data}\n===")
288 | sys.exit(1)
289 | name, head = r.groups()
290 | f = self.workspace.engine.fixtures.find_by_name(name)
291 | if not f:
292 | logger.error(f"unable to match fixture to {name}")
293 | sys.exit(1)
294 | return f, head
295 | return False, None
296 |
297 |
298 | def extract_size_from_cell(self,data):
299 | r = size_re.search(data)
300 | if not r:
301 | logger.error(f"unlable to match size data {data}")
302 | sys.exit(1)
303 |
304 | s_x, s_y = r.groups()
305 | return int(s_y), int(s_x)
306 |
307 | def extract_fg_from_sheet(self, sheet):
308 | s_x, s_y = self.extract_size_from_cell(sheet.iat[0,0])
309 | logger.debug(f"extracting sheet size {s_x},{s_y}")
310 | heads = []
311 | for x in range(0, s_x):
312 | for y in range(0, s_y):
313 | try:
314 | data = sheet.iat[x+1,y+1]
315 | except:
316 | continue
317 | logger.debug(f"extracting {x},{y}: {data}")
318 | f, head = self.extract_fixture_from_cell(data)
319 | if f:
320 | # generate head xml, remember reversed
321 | heads.append({
322 | 'x': str(int(y)),
323 | 'y': str(int(x)),
324 | 'f_id': f.id,
325 | 'head': str(int(head) -1),
326 | })
327 | return s_x, s_y, heads
328 |
329 | def import_fixture_groups(self, path):
330 | worksheet = pd.ExcelFile(path)
331 | # each sheet name represents a fixture group
332 | for sheet_name in worksheet.sheet_names:
333 | sheet = worksheet.parse(sheet_name)
334 | s_x, s_y, heads = self.extract_fg_from_sheet(sheet)
335 | fg = {
336 | 'name': sheet_name,
337 | 'size_x': s_x,
338 | 'size_y': s_y,
339 | 'heads': heads
340 | }
341 | self.update_fixture_group(fg)
342 |
343 | def update_fixture_group(self, fg):
344 | fgs = self.workspace.engine.fixture_groups
345 | Name = fg.get('name')
346 | efg = fgs.find_by_name(Name)
347 | # if it exists, we use that fgid and delete the old one
348 | if efg:
349 | ID = str(efg.id)
350 | fgs.root.remove(efg.root)
351 | else:
352 | # else we generate a new one
353 | ID = str(fgs.next_id())
354 |
355 | this_fg = ET.SubElement(fgs.root, "FixtureGroup")
356 | logger.info(f'generating fg {Name} {ID}')
357 | this_name = ET.SubElement(this_fg, 'Name')
358 | this_name.text = Name
359 | this_fg.set('ID',str(ID))
360 | this_size = ET.SubElement(this_fg, 'Size')
361 | this_size.set('X', str(fg.get('size_y')))
362 | this_size.set('Y', str(fg.get('size_x')))
363 | for head in fg.get('heads'):
364 | this_head = ET.SubElement(this_fg, 'Head')
365 | this_head.set('X',str(head.get('x')))
366 | this_head.set('Y',str(head.get('y')))
367 | this_head.set('Fixture',str(head.get('f_id')))
368 | this_head.text = head.get('head')
369 |
370 | def generate_fixtures(self,fixtures):
371 | fs = self.workspace.engine.fixtures
372 | for fixture in fixtures:
373 | logger.debug('generatng fixture %s ' % pprint.pformat(fixture))
374 |
375 | # find or create fixture
376 | ef = fs.find_by_name(fixture.get('name'))
377 | if not ef:
378 | this_f = ET.SubElement(fs.root, "Fixture")
379 | ID = str(fs.next_id())
380 | ET.SubElement(this_f, "ID").text = ID
381 | else:
382 | this_f = ef.root
383 | ET.SubElement(this_f, "Name").text = fixture.get('name')
384 |
385 | # find fixture definition and set Manufacturer
386 | this_f_model = fixture.get('model')
387 | this_fd = self.fixture_definitions.find_by_model(this_f_model)[0]
388 | if not this_fd:
389 | raise Exception(f'Unable to find fixture definition for {this_f_model}')
390 | ET.SubElement(this_f, "Manufacturer").text = this_fd.manufacturer
391 | ET.SubElement(this_f, "Model").text = this_fd.model
392 | ET.SubElement(this_f, "Channels").text = str(len(this_fd.channels.items))
393 | ET.SubElement(this_f, "Address").text = str(fixture.get('a'))
394 |
395 | # find mode, required
396 | if fixture.get('mode'):
397 | this_f_mode = fixture.get('mode')
398 | fd_mode = this_fd.modes.find_by_name(this_f_mode)
399 | if not Mode:
400 | raise Exception(f'Unable to find mode {this_f_mode} in fixture def: {this_f_model}')
401 | elif len(this_fd.modes.items) == 1:
402 | fd_mode = this_fd.modes.items[0]
403 | else:
404 | raise Exception(f'No mode defined for fixture definition {this_f_model}')
405 | ET.SubElement(this_f, "Mode").text = fd_mode.name
406 |
407 | # TODO eventually make this smart
408 | u = fixture.get('u')
409 | ET.SubElement(this_f, "Universe").text = u
410 |
411 |
412 | def _sort_fval(self,fval):
413 | i = iter(fval.split(','))
414 | d = dict(zip(i, i))
415 | l = []
416 | for k in sorted(d, key=lambda item: int(item)):
417 | l.append(k)
418 | l.append(d[k])
419 | r = ','.join(l)
420 | return r
421 |
422 | def generate_color_scenes(self, fg):
423 | ''' this is now fixture independent... which means that groups
424 | can have more than one fixture type and it will try still do the
425 | right thing finding the intersections of colors across all the
426 | fixtures '''
427 | Type = 'Scene'
428 | Path = '/'.join(['Colors', fg.name])
429 | for color_name, color_data in rgbw.items():
430 | Name = '/'.join([Path, color_name])
431 | # generate fval by head
432 | fvals = {}
433 | fixtures = {}
434 | for head in fg.heads:
435 | # get fixture, if we can't find it.. skip
436 | fixture = self.workspace.engine.fixtures.find_by_id(head.fixture_id)
437 | if not fixture:
438 | print(f" FAIL: no fixture found for {head.fixture_id}")
439 | if fixture and fixture.mode == None:
440 | print(f" FAIL: no fixture mode for {fixture.name}")
441 | continue
442 | fixtures[fixture.id] = True
443 |
444 | # we skip undefined fixtures or pixel fixtures
445 | fixture_definition = self.fixture_definitions.find_by_fixture(fixture)
446 | if fixture_definition == None or 'Pixels' in fixture_definition.type:
447 | continue
448 | fixture_mode = fixture_definition.modes.find_by_name(fixture.mode)
449 | if not fixture_mode:
450 | logger.info(f" SkIP: no fixture mode for {fixture.name}")
451 | continue
452 |
453 | if not fixture_mode.heads:
454 | logger.info(f" SkIP: no heads for {fixture.name} {fixture_mode.name}")
455 | continue
456 |
457 | if fixture_mode.heads.count and 'mode channels' in dir(fixture_mode.heads.find_by_id(head.id)):
458 | # if we have more than one head .. find the channels for that head
459 | mode_channels = fixture_mode.heads.find_by_id(head.id).mode_channels
460 | else:
461 | # if not use all the channels for the
462 | mode_channels = fixture_mode.mode_channels
463 |
464 |
465 | fval = []
466 | if any('Red' in s for s in mode_channels.names):
467 | # this is an RGB Head
468 | red, green, blue, white = color_data.get('rgbw')
469 | for mode_channel in mode_channels:
470 | channel = mode_channel.channel
471 | channel_name = channel.name.lower()
472 | # if multi head .. wash must be in the name
473 | if ( 'intensity' in channel_name or 'dimmer' in channel_name):
474 | fval.append(f'{mode_channel.number},255')
475 | elif 'red' in channel_name:
476 | fval.append(f'{mode_channel.number},{red}')
477 | elif 'green' in channel_name:
478 | fval.append(f'{mode_channel.number},{green}')
479 | elif 'blue' in channel_name:
480 | fval.append(f'{mode_channel.number},{blue}')
481 | elif 'white' in channel_name:
482 | fval.append(f'{mode_channel.number},{white}')
483 |
484 | if color_channels := intersection(mode_channels.names, SPOT_COLORWHEEL_NAMES):
485 | # this has a color wheel head
486 | color_channel = fixture_definition.channels.find_by_name(color_channels[0])
487 | # see if we can match to a predefined color
488 | if color_name in RGB_ALTERNATES:
489 | color_name = RGB_ALTERNATES[color_name]
490 |
491 | if capability := color_channel.capabilities.find_by_name(color_name):
492 | cval = capability.min
493 | for mode_channel in mode_channels:
494 | channel = mode_channel.channel
495 | channel_name = channel.name.lower()
496 | color_channel_name = color_channel.name.lower()
497 | # if multi-head .. 'spot' must be in the name
498 | if ( 'intensity' in channel_name or 'dimmer' in channel_name
499 | or channel_name in SPOT_ON_CHANNELS):
500 | fval.append(f'{mode_channel.number},255')
501 | elif channel_name == color_channel_name:
502 | fval.append(f'{mode_channel.number},{cval}')
503 |
504 | # assemble the fvals for this head
505 | if len(fval) > 0:
506 | if fixture.id in fvals:
507 | fvals[fixture.id] = self._sort_fval((fvals[fixture.id] + ',%s' % ','.join(fval)))
508 | else:
509 | fvals[fixture.id] = ','.join(fval)
510 |
511 | # create the scene if all heads in the group are accounted for
512 | if len(fixtures) == len(fvals):
513 | print(f" adding {Name}")
514 | self.function(
515 | Type = Type,
516 | Path = Path,
517 | Name = Name,
518 | FixtureVals = fvals)
519 |
520 | def generate_capability_scenes(self, fg):
521 | ''' take each expandable channel and make scenes for the capabilities
522 | NOTE: this one does NOT work across heads.. so is only gonna work when the
523 | heads are all the same. '''
524 |
525 | # first lets determine if this FG has all the same definition and mode
526 | fixture_definition = None
527 | mode = None
528 | capabilities = {}
529 | moving_head = False
530 | fixtures = dict()
531 | for head in fg.heads:
532 |
533 | # get fixture, if we can't find it.. skip
534 | fixture = self.workspace.engine.fixtures.find_by_id(head.fixture_id)
535 | if not fixture:
536 | print(f" FAIL: no fixture found for {head.fixture_id}")
537 | if fixture and fixture.mode == None:
538 | print(f" FAIL: no fixture mode for {fixture.name}")
539 | continue
540 | fixtures[fixture.id] = True
541 |
542 | # we skip undefined fixtures or pixel fixtures
543 | fixture_definition = self.fixture_definitions.find_by_fixture(fixture)
544 | if fixture_definition == None or 'Pixels' in fixture_definition.type:
545 | continue
546 | mode = fixture_definition.modes.find_by_name(fixture.mode)
547 | if 'heads' not in dir(mode):
548 | continue
549 | if mode.heads.count and 'mode channels' in dir(mode.heads.find_by_id(head.id)):
550 | mode_channels = mode.heads.find_by_id(head.id).mode_channels
551 | else:
552 | mode_channels = fixture_definition.modes.find_by_name(fixture.mode).mode_channels
553 |
554 | # now get the capability channels to expand for this fixture mode.
555 | # we identify the right ones by the channel groups we support.
556 | for mode_channel in mode_channels:
557 | channel = mode_channel.channel
558 | # normalize the name so it doesn't become part of the path
559 | # for any channel with capabilities, we make scenes
560 | if channel.capabilities:
561 | channel_name = channel.name.replace('/','+')
562 | Path = '/'.join(['Capabilties', fg.name, channel_name])
563 | for capability in channel.capabilities:
564 | if capability.name:
565 | fval=[]
566 | # set the normalized scene name and capability value
567 | Name = '/'.join([Path, capability.name.replace('/','+') ])
568 | fval.append(f'{mode_channel.number},{capability.min}')
569 | if Name not in capabilities:
570 | logger.info(f'adding {Name} to capabilities')
571 | capabilities[Name] = { 'fvals': {} }
572 | capabilities[Name]['Path'] = Path
573 | capabilities[Name]['fvals'][fixture.id] = ','.join(fval)
574 |
575 |
576 | # if moving head, generate base positions for this fixture
577 | if fixture_definition.type == 'Moving Head':
578 | Path = '/'.join(['Positions', fg.name])
579 |
580 | for name, pos in BASE_POSITIONS.items():
581 | h,v = pos
582 | Name = '/'.join([Path, name ])
583 | fval = []
584 | for mode_channel in mode_channels:
585 | channel = mode_channel.channel
586 | if channel.group == 'Pan' or channel.name == 'Pan':
587 | fval.append(f'{mode_channel.number},{h}')
588 | elif channel.group == 'Tilt' or channel.name == 'Tilt':
589 | fval.append(f'{mode_channel.number},{v}')
590 | if fval:
591 | if Name not in capabilities:
592 | logger.info(f'adding movement {Name} to capabilities')
593 | capabilities[Name] = { 'fvals' : {} }
594 | capabilities[Name]['Path'] = Path
595 | logger.debug(f"adding {head.fixture} {fval} to {Name}")
596 | capabilities[Name]['fvals'][fixture.id] = ','.join(fval)
597 |
598 | # generate scenes
599 | for Name,c in capabilities.items():
600 | self.function(
601 | Type = 'Scene',
602 | Name = Name,
603 | Path = c['Path'],
604 | FixtureVals = c['fvals'])
605 |
606 | # TODO: extrapolate this into the actual classes
607 | def function(self, **kwargs):
608 | functions = self.workspace.engine.functions
609 |
610 | Name = kwargs.get('Name')
611 | ID = kwargs.get('ID')
612 | Type = kwargs.get('Type')
613 | Path = kwargs.get('Path')
614 |
615 | logger.debug(f'generating function {Name} at {Path}')
616 |
617 | # go find if we can, ID is most significant
618 | func = None
619 | if ID:
620 | func = functions.find_by_id(ID)
621 | if not func and Name:
622 | func = functions.find_by_name(Name)
623 |
624 | # if not create root element
625 | if not func:
626 | this_func = ET.SubElement(functions.root, "Function")
627 | ID = str(functions.next_id())
628 | this_func.set('ID', ID)
629 | else:
630 | ID = func.id
631 | this_func = func.root
632 |
633 | # set/update primatives
634 | this_func.set('Name', Name)
635 | this_func.set('Type', Type)
636 |
637 | # optional
638 | if Path:
639 | this_func.set('Path', Path)
640 |
641 | if Type == 'Script':
642 | self.f_script(this_func, **kwargs)
643 | elif Type == 'Scene':
644 | fvals = kwargs.get('FixtureVals')
645 | self.f_scene(this_func, **kwargs)
646 |
647 | def f_script(self, f, **kwargs):
648 |
649 | self.fattr_speed(f, **kwargs)
650 | default_direction = 'Forward'
651 | if direction := kwargs.get('Direction') or default_direction:
652 | d = f.find('{%s}Direction' % Workspace.xmlns)
653 | if d == None:
654 | d = ET.SubElement(f, 'Direction')
655 | d.text = direction
656 |
657 | default_runorder = 'Loop'
658 | if runorder := kwargs.get('RunOrder') or default_runorder:
659 | r = f.find('{%s}RunOrder' % Workspace.xmlns)
660 | if r == None:
661 | r = ET.SubElement(f, 'RunOrder')
662 | r.text = runorder
663 |
664 | if kwargs.get('Command'):
665 | c = f.find('{%s}Command' % Workspace.xmlns)
666 | if c == None:
667 | c = ET.SubElement(f, 'Command')
668 | command = urllib.parse.quote(kwargs.get('Command'), safe='')
669 | c.text = command
670 |
671 | # create a scene
672 | def f_scene(self, f, **kwargs):
673 | fvals = kwargs.get('FixtureVals')
674 | self.fattr_speed(f, **kwargs)
675 |
676 | # set fixture vals
677 | fvs = f.findall('{%s}FixtureVal' % Workspace.xmlns)
678 | existing_fv_ids = []
679 | for fv in fvs:
680 | fid = fv.get('ID')
681 | existing_fv_ids.append(fid)
682 | # remove it if not part of the group, else
683 | # update value
684 | if not fid in fvals:
685 | f.remove(fv)
686 | else:
687 | fv.text = fvals[fid]
688 |
689 | # update it if it's not there
690 | for fid, cstr in fvals.items():
691 | if fid not in existing_fv_ids:
692 | this_fv = ET.SubElement(f,'FixtureVal')
693 | this_fv.set('ID', fid)
694 | this_fv.text = cstr
695 |
696 |
697 | def fattr_speed(self,f,**kwargs):
698 | default_speed = {
699 | 'FadeIn': '0',
700 | 'FadeOout': '0',
701 | 'Duration': '0'
702 | }
703 | if speed := kwargs.get('Speed') or default_speed:
704 | s = f.find('{%s}Speed' % Workspace.xmlns)
705 | if s == None:
706 | s = ET.SubElement(f, 'Speed')
707 | for k, v in speed.items():
708 | s.set(k, v)
709 |
710 |
711 | class Workspace(QLC):
712 | xmlns = 'http://www.qlcplus.org/Workspace'
713 | def __init__(self, file):
714 | ''' setup class '''
715 | # expand common targets
716 | ET.register_namespace('', self.xmlns)
717 |
718 | self.file = file
719 | logger.debug(f'loading {file}')
720 |
721 | if self.file:
722 | self.tree = ET.parse(self.file)
723 | self.root = self.tree.getroot()
724 | else:
725 | self.root = ET.fromstring(BLANK_WORKSPACE)
726 |
727 | self.creator = Creator(self.root)
728 | self.engine = Engine(self.root)
729 |
730 | def _gen_text(self):
731 | _strip(self.root)
732 | rough_bytes = ET.tostring(self.root) #, 'utf-8')
733 | # fix DOCTYPE
734 | reparsed = minidom.parseString(rough_bytes)
735 | xml = reparsed.toprettyxml(indent=' ')
736 | xml = xml.replace('', DOCTYPE)
737 | return xml
738 |
739 | def dump(self):
740 | self.text = self._gen_text()
741 | print(self.text)
742 |
743 | def write(self, file=None):
744 | out = file or self.file
745 | self.text = self._gen_text()
746 | f = open(out, 'w')
747 | f.write(self.text)
748 | f.close()
749 |
750 | class Creator(Workspace):
751 | def __init__(self, root):
752 | self.root = root.find('{%s}%s' % (Workspace.xmlns, type(self).__name__))
753 | self.name = self.root.find('{%s}Name' % Workspace.xmlns).text
754 | self.version = self.root.find('{%s}Version' % Workspace.xmlns).text
755 | self.author = self.root.find('{%s}Author' % Workspace.xmlns).text
756 |
757 | class Engine(Workspace):
758 | def __init__(self, root):
759 | self.root = root.find('{%s}%s' % (Workspace.xmlns, type(self).__name__))
760 | if not self.root:
761 | self.root = ET.SubElement(root, type(self).__name__)
762 | self.inputoutputmap = InputOutputMap(self.root)
763 | self.fixtures = Fixtures(self.root)
764 | self.fixture_groups = FixtureGroups(self.root)
765 | self.functions = Functions(self.root)
766 |
767 |
768 | class InputOutputMap(Engine):
769 | def __init__(self, root):
770 | self.root = root.find('{%s}%s' % (Workspace.xmlns, type(self).__name__))
771 | if not self.root:
772 | self.root = ET.SubElement(root, type(self).__name__)
773 | self.universes = Universes(self.root)
774 |
775 | class Universes(InputOutputMap):
776 | def __init__(self, root):
777 | self.root = root
778 | self.items = []
779 | for item in root.findall('{%s}Universe' % Workspace.xmlns):
780 | self.items.append(Universe(item))
781 |
782 | self._next_id = 0
783 | self.names = list(map(lambda x: x.name, self.items))
784 | self.ids = list(map(lambda x: x.id, self.items))
785 | for id in self.ids:
786 | if int(id) > self._next_id:
787 | self._next_id = int(id)
788 |
789 | # map addresses
790 | self.u_by_output_ip = dict()
791 | for u in self.items:
792 | if u.output and u.output.output_ip:
793 | self.u_by_output_ip[u.output.output_ip] = u
794 |
795 | def add(self, u):
796 | self.items.append(Universe(self.root, u))
797 |
798 |
799 | def find_by_output(self, output_ip, output_uni=None):
800 | u = self.u_by_ouput_ip.get(output_ip)
801 | if u:
802 | for o_ip_u in self.u_by_output_ip[output_ip]:
803 | if o_ip_u.output_uni == output_uni:
804 | return o_ip_u
805 |
806 |
807 | def find_by_name(self, name):
808 | for i in self.items:
809 | if i.name == name:
810 | return i
811 |
812 | def find_by_id(self, fid):
813 | for i in self.items:
814 | if int(i.id) == int(fid):
815 | return i
816 |
817 | def next_id(self):
818 | self._next_id +=1
819 | return self._next_id
820 |
821 | def __iter__(self):
822 | return iter(self.items)
823 |
824 | class Universe(Universes):
825 | def __init__(self, root, u=None):
826 | self.channels = [None] * 512
827 | self.input = None
828 | self.output = None
829 | if u:
830 | self.output_ip = u.get('ipaddr')
831 | self.output_u = u.get('artnet_u')
832 | self.id = str(u.get('id'))
833 | self.name = u.get('name')
834 | logger.debug('generatng universe %s' % pprint.pformat(u))
835 | self.root = ET.SubElement(root, "{%s}Universe" % Workspace.xmlns)
836 | self.root.set('ID', self.id)
837 | self.root.set('Name',self.name)
838 | if self.output_ip:
839 | self.output = Output(self.root, u)
840 | else:
841 | # read
842 | self.root = root
843 | self.name = root.get('Name')
844 | self.id = root.get('ID')
845 | if root.find('{%s}Input' % Workspace.xmlns):
846 | self.input = Input(self.root, u)
847 | if root.find('{%s}Output' % Workspace.xmlns):
848 | self.output = Output(self.root, u)
849 |
850 | def update(self, u):
851 | self.root.set('Name',u.get('name'))
852 | if u.get('ipaddr') and u.get('artnet_u') and self.output:
853 | self.output.update(u)
854 |
855 | def allocate(self, channel, f_id):
856 | self.channels[channel] = f_id
857 |
858 |
859 | class Input(Universe):
860 | def __init__(self,root, u=None):
861 | self.root = root.find('{%s}Input' % Workspace.xmlns)
862 | self.plugin = self.root.get('Plugin')
863 | self.line = self.root.get('Line')
864 |
865 |
866 | # TODO: this all assumes artnet, make agnostic
867 | class Output(Universe):
868 | def __init__(self, root, u=None):
869 | self.root = root.find('{%s}Output' % Workspace.xmlns)
870 | self.output_ip = None
871 | if not self.root and u:
872 | # generate
873 | self.root = ET.SubElement(root,"Output")
874 | self.update(u)
875 | self.output_ip = u.get('ipaddr')
876 | else:
877 | self.parameters = self.root.find('{%s}PluginParameters' % Workspace.xmlns)
878 | if self.parameters:
879 | self.output_u = self.parameters.get('outputUni')
880 | self.output_ip = self.parameters.get('outputIP')
881 | if 'ArtNet' in self.plugin:
882 | self.artnet_ip = self.output_ip
883 | self.plugin = self.root.get('Plugin')
884 | self.line = self.root.get('Line')
885 |
886 | def update(self, u):
887 | if u.get('ipaddr') and u.get('artnet_u') != None:
888 | self.root.set('Plugin', 'ArtNet')
889 | # TODO: map this by known IP?
890 | self.root.set('Line', u.get('Line') or "2")
891 |
892 | pps = self.root.findall('{%s}PluginParameters' % Workspace.xmlns)
893 | if len(pps) > 1:
894 | # delete em all
895 | for pp in pps:
896 | self.root.remove(pp)
897 | pps = []
898 | if not pps:
899 | # create one
900 | print(ET.tostring(self.root))
901 | print('oops')
902 | self.parameters = ET.SubElement(self.root, "{%s}PluginParameters" % Workspace.xmlns)
903 | self.parameters.set('outputIP', u.get('ipaddr'))
904 | self.parameters.set('outputUni', str(u.get('artnet_u')))
905 |
906 |
907 | class Fixtures(Engine):
908 | def __init__(self, root):
909 | # no Fixtures sub
910 | self.root = root
911 | self.items = []
912 | for item in self.root.findall('{%s}Fixture' % Workspace.xmlns):
913 | self.items.append(Fixture(item))
914 |
915 | self.names = list(map(lambda x: x.name, self.items))
916 | self.ids = list(map(lambda x: int(x.id), self.items))
917 | self._next_id = 0
918 | for id in self.ids:
919 | if int(id) > self._next_id:
920 | self._next_id = int(id)
921 |
922 |
923 | def next_id(self):
924 | self._next_id += 1
925 | return self._next_id
926 |
927 |
928 | def find_by_name(self, name):
929 | for i in self.items:
930 | if i.name == name:
931 | return i
932 |
933 | def find_by_id(self, fid):
934 | for i in self.items:
935 | if i.id == fid:
936 | return i
937 |
938 | def __iter__(self):
939 | return iter(self.items)
940 |
941 |
942 | class Fixture(Fixtures):
943 | def __init__(self, root):
944 | self.root = root
945 | self.manufacturer = root.find('{%s}Manufacturer' % Workspace.xmlns).text
946 | self.model = root.find('{%s}Model' % Workspace.xmlns).text
947 | self.name = f'{self.manufacturer} - {self.model}'
948 | self.mode = root.find('{%s}Mode' % Workspace.xmlns).text
949 | self.id = root.find('{%s}ID' % Workspace.xmlns).text
950 | self.name = root.find('{%s}Name' % Workspace.xmlns).text
951 | self.universe = root.find('{%s}Universe' % Workspace.xmlns).text
952 | self.address = root.find('{%s}Address' % Workspace.xmlns).text
953 | self.channels = root.find('{%s}Channels' % Workspace.xmlns).text
954 |
955 |
956 | class FixtureGroups(Engine):
957 | def __init__(self, root):
958 | # no FixtureGrops sub
959 | self.root = root
960 | self.items = []
961 | for item in root.findall('{%s}FixtureGroup' % Workspace.xmlns):
962 | self.items.append(FixtureGroup(item))
963 |
964 | self.names = list(map(lambda x: x.name, self.items))
965 | self.ids = list(map(lambda x: x.id, self.items))
966 | self._next_id = 0
967 | for id in self.ids:
968 | if int(id) > self._next_id:
969 | self._next_id = int(id)
970 |
971 | def add(self, ref):
972 | self.items.append(FixtureGroup(self.root, ref))
973 |
974 | def delete(self, fg):
975 | self.items.remove(fg)
976 |
977 | def next_id(self):
978 | self._next_id += 1
979 | return self._next_id
980 |
981 | def find_by_name(self, name):
982 | for i in self.items:
983 | if i.name == name:
984 | return i
985 |
986 | def find_by_id(self, fid):
987 | for i in self.items:
988 | if i.id == fid:
989 | return i
990 |
991 | def __iter__(self):
992 | return iter(self.items)
993 |
994 |
995 | class FixtureGroup(FixtureGroups):
996 | def __init__(self, root):
997 | self.root = root
998 | self.name = self.root.find('{%s}Name' % Workspace.xmlns).text
999 | self.id = root.get('ID')
1000 | self.size = Size(root)
1001 | self.heads = Heads(root)
1002 |
1003 | class ChannelsGroup(Engine):
1004 | def __init__(self, root, cg=None):
1005 | self.root = root
1006 | if cg != None:
1007 | self.root.set('ID', cg.get('id'))
1008 | self.root.set('Name', cg.get('name'))
1009 | self.root.set('Value', cg.get('value'))
1010 | self.root.set('InputUniverse', cg.get('InputUniverse'))
1011 | self.root.set('InputChannel', cg.get('InputChannel'))
1012 | self.id = root.get('ID')
1013 | self.name = root.get('Name')
1014 | self.value = root.get('Value')
1015 | self.input_universe = root.get('InputUniverse')
1016 | self.input_channel = root.get('InputChannel')
1017 | self.mapping = root.text
1018 |
1019 | def update(self, cg):
1020 | for k,v in data.items():
1021 | self.root.set(k,v)
1022 |
1023 |
1024 | class Size(FixtureGroup):
1025 | def __init__(self, root, fg=None):
1026 | self.root = root
1027 | self.root = root.find('{%s}Size' % Workspace.xmlns)
1028 | self.x = self.root.get('X')
1029 | self.y = self.root.get('Y')
1030 |
1031 |
1032 | class Heads(FixtureGroup):
1033 | def __init__(self, root, fg=None):
1034 | self.root = root
1035 | self.items = []
1036 | if fg != None:
1037 | for head in fg.get('heads'):
1038 | self.items.append(Head(root, head))
1039 | else:
1040 | for item in root.findall('{%s}Head' % Workspace.xmlns):
1041 | self.items.append(Head(item))
1042 |
1043 | def update(self, heads):
1044 | for head in heads:
1045 | e_head = heads.find_by_id(head.get('id'))
1046 | if e_head:
1047 | e_head.update(head)
1048 | else:
1049 | self.items.append(Head(self.root, head))
1050 |
1051 |
1052 | class Head(Heads):
1053 | def __init__(self, root, head=None):
1054 | self.root = root
1055 | if head != None:
1056 | self.root.set('X', head.get('X'))
1057 | self.root.set('Y', head.get('Y'))
1058 | self.root.text = head.get('id')
1059 | self.root.set('Fixture', head.get('Fixture'))
1060 | self.x = root.get('X')
1061 | self.y = root.get('Y')
1062 | self.id = root.text
1063 | self.fixture = root.get('Fixture')
1064 | self.fixture_id = root.get('Fixture')
1065 |
1066 | def update(self, head):
1067 | for k,v in data.items():
1068 | self.root.set(k,v)
1069 |
1070 | class Functions(Engine):
1071 | def __init__(self, root):
1072 | # no Functions sub group
1073 | self.root = root
1074 | self.items = []
1075 | for item in root.findall('{%s}Function' % Workspace.xmlns):
1076 | self.items.append(Function(item))
1077 |
1078 | self.names = list(map(lambda x: x.name, self.items))
1079 |
1080 | self.last_id = 0
1081 | for item in self.items:
1082 | if int(item.id) > self.last_id:
1083 | self.last_id = int(item.id)
1084 |
1085 | def find_by_name(self, name):
1086 | for i in self.items:
1087 | if i.name == name:
1088 | return i
1089 |
1090 | def find_by_path(self, path):
1091 | for i in self.items:
1092 | if i.path == path:
1093 | return i
1094 |
1095 | def find_by_id(self, id):
1096 | for i in self.items:
1097 | if i.id == id:
1098 | return i
1099 |
1100 | def next_id(self):
1101 | self.last_id += 1
1102 | return self.last_id
1103 |
1104 | def __iter__(self):
1105 | return iter(self.items)
1106 |
1107 | class Function(Functions):
1108 | def __init__(self, root):
1109 | self.root = root
1110 | self.id = root.get('ID')
1111 | self.type = root.get('Type')
1112 | self.name = root.get('Name')
1113 | self.path = root.get('Path')
1114 | self.speed = Speed(root)
1115 | if 'Scene' in self.type:
1116 | self.config = Scene(root)
1117 |
1118 | class Scene(Function):
1119 | def __init__(self, root):
1120 | self.fixture_vals = FixtureVals(root)
1121 | self.speed = Speed(root)
1122 |
1123 | class FixtureVals(Scene):
1124 | def __init__(self,root):
1125 | self.root = root
1126 | self.items = []
1127 | for item in root.findall('{%s}FixtureVal' % Workspace.xmlns):
1128 | self.items.append(FixtureVal(item))
1129 |
1130 | def __iter__(self):
1131 | return iter(self.items)
1132 |
1133 | class FixtureVal(FixtureVals):
1134 | def __init__(self, root):
1135 | self.id = root.get('ID')
1136 | self.data = root.text
1137 |
1138 | class Speed(Function):
1139 | def __init__(self, root):
1140 | self.root = root.find('{%s}Speed' % Workspace.xmlns)
1141 | if self.root:
1142 | self.fadein = self.root.get('FadeIn')
1143 | self.fadeout = self.root.get('FadeOut')
1144 | self.duration = self.root.get('Duration')
1145 | else:
1146 | self.fadein = '0'
1147 | self.fadeout = '0'
1148 | self.duration = '0'
1149 |
1150 | class FixtureDefinitions(QLC):
1151 | def __init__(self, paths):
1152 | self.paths = paths
1153 | self.items = dict()
1154 | self.fd_by_model = dict()
1155 | for path in paths:
1156 | self.read_fixture_dir(path)
1157 |
1158 | def read_fixture_dir(self,path):
1159 | realpath = os.path.expanduser(path)
1160 | entries = os.scandir(realpath)
1161 | for entry in entries:
1162 | t_path = f'{realpath}/{entry.name}'
1163 | if os.path.isdir(entry):
1164 | self.read_fixture_dir(t_path)
1165 | continue
1166 | elif entry.name.startswith('.'):
1167 | continue
1168 | elif '.qxf' in entry.name:
1169 | try:
1170 | fd = FixtureDefinition(entry)
1171 | if manufacturer := fd.manufacturer:
1172 | model = fd.model
1173 | else:
1174 | continue
1175 | # list by manufacturer and model
1176 | if not manufacturer in self.items:
1177 | self.items[manufacturer] = {}
1178 | self.items[manufacturer][model] = fd
1179 | # list by model
1180 | if not model in self.fd_by_model:
1181 | self.fd_by_model[model] = []
1182 | self.fd_by_model[model].append(fd)
1183 | except Exception as e:
1184 | logger.error(f'Error loading {entry.name}: {e}')
1185 | continue
1186 |
1187 | def find_by_manufacturer_model(self, manufacturer, model):
1188 | if ( manufacturer in self.items and model in self.items[manufacturer] ):
1189 | return self.items[manufacturer][model]
1190 |
1191 | def find_by_fixture(self, fixture):
1192 | if fixture:
1193 | return self.find_by_manufacturer_model(fixture.manufacturer, fixture.model)
1194 |
1195 | def find_by_model(self, model):
1196 | return self.fd_by_model.get(model)
1197 |
1198 | def __iter__(self):
1199 | return iter(self.items)
1200 |
1201 | class FixtureDefinition(FixtureDefinitions):
1202 | # for all fixture definitions..
1203 | xmlns = 'http://www.qlcplus.org/FixtureDefinition'
1204 |
1205 | def __init__(self, path):
1206 | ''' read the fixture xml into an dict indexed by manuf and model'''
1207 | # read/parse the file
1208 | ET.register_namespace('', FixtureDefinition.xmlns)
1209 |
1210 | try:
1211 | tree = ET.parse(path)
1212 | except Exception:
1213 | self.manufacturer = None
1214 | return None
1215 | self.root = tree.getroot()
1216 |
1217 | # set accessors
1218 | self.manufacturer = self.root.find('{%s}Manufacturer' % FixtureDefinition.xmlns).text
1219 | self.model = self.root.find('{%s}Model' % FixtureDefinition.xmlns).text
1220 | self.type = self.root.find('{%s}Type' % FixtureDefinition.xmlns).text
1221 |
1222 | logger.info(f'loading {self.manufacturer} {self.model}')
1223 |
1224 | self.channels = Channels(self.root)
1225 | self.modes = Modes(self.root, self.channels)
1226 | self.physical = Physical(self.root)
1227 |
1228 | class Physical(FixtureDefinition):
1229 | ''' extract channels and return a list '''
1230 | def __init__(self, root):
1231 | self.item = root.findall('{%s}Physical' % FixtureDefinition.xmlns)
1232 | # get layout
1233 | if self.item:
1234 | layout = self.item[0].find('{%s}Layout' % FixtureDefinition.xmlns)
1235 | if layout:
1236 | self.layout.width = layout.get('Width')
1237 | self.layout.height = layout.get('Height')
1238 |
1239 | # get dimensions
1240 | dimensions = self.item[0].find('{%s}Dimensions' % FixtureDefinition.xmlns)
1241 | if dimensions:
1242 | self.dimensions.width = dimensions.get('Width')
1243 | self.dimensions.height = dimensions.get('Height')
1244 | self.dimensions.depth = dimensions.get('Depth')
1245 | self.dimensions.weight = dimensions.get('weight')
1246 |
1247 |
1248 | class Channels(FixtureDefinition):
1249 | ''' extract channels and return a list '''
1250 | def __init__(self, root):
1251 | self.items = []
1252 | for item in root.findall('{%s}Channel' % FixtureDefinition.xmlns):
1253 | self.items.append(Channel(item))
1254 |
1255 | self.names = list(map(lambda x: x.name, self.items))
1256 |
1257 | def find_by_name(self, name):
1258 | for i in self.items:
1259 | if i.name == name:
1260 | return i
1261 |
1262 | def find(self, name):
1263 | return self.find_by_name(name)
1264 |
1265 | def __iter__(self):
1266 | return iter(self.items)
1267 |
1268 | class Channel(Channels):
1269 | def __init__(self, root):
1270 | self.root = root
1271 | # create accessors
1272 | self.name = root.get('Name')
1273 | self.capabilities = Capabilities(root)
1274 | group = root.find('{%s}Group' % FixtureDefinition.xmlns)
1275 | if group != None:
1276 | self.group = group.text
1277 | else:
1278 | self.group = None
1279 |
1280 | class Capabilities(Channel):
1281 | def __init__(self, root):
1282 | self.items = []
1283 | for item in root.findall('{%s}Capability' % FixtureDefinition.xmlns):
1284 | self.items.append(Capability(item))
1285 | self.names = list(map(lambda x: x.name, self.items))
1286 |
1287 | def find_by_name(self, name):
1288 | for i in self.items:
1289 | if i.name == name:
1290 | return i
1291 |
1292 | def find(self, name):
1293 | self.find_by_name(name)
1294 |
1295 | def __iter__(self):
1296 | return iter(self.items)
1297 |
1298 | class Capability(Capabilities):
1299 | def __init__(self, root):
1300 | self.root = root
1301 | self.name = root.text
1302 | self.min = root.get('Min')
1303 | self.max = root.get('Max')
1304 |
1305 | class Modes(Channel):
1306 | def __init__(self, root, channels):
1307 | self.items = []
1308 | for item in root.findall('{%s}Mode' % FixtureDefinition.xmlns):
1309 | try:
1310 | self.items.append(Mode(item, channels))
1311 | except Exception as e:
1312 | raise Exception(f'Error loading Mode {item.get("Name")}: {e}')
1313 | self.names = list(map(lambda x: x.name, self.items))
1314 |
1315 |
1316 | def find_by_name(self, name):
1317 | for i in self.items:
1318 | if i.name == name:
1319 | return i
1320 |
1321 | def find(self, name):
1322 | return self.find_by_name(name)
1323 |
1324 | def __iter__(self):
1325 | return iter(self.items)
1326 |
1327 | class Mode(Modes):
1328 | def __init__(self, root, channels):
1329 | self.root = root
1330 | self.name = root.get('Name')
1331 | self.mode_channels = ModeChannels(root, channels)
1332 | self.channels = self.mode_channels.channels
1333 | self.heads = ModeHeads(root, self.mode_channels)
1334 |
1335 | class ModeChannels(Mode):
1336 | def __init__(self, root, channels):
1337 | self.root = root
1338 | self.items = []
1339 | self.channels = []
1340 | try:
1341 | for item in root.findall('{%s}Channel' % FixtureDefinition.xmlns):
1342 | mc = ModeChannel(item, channels)
1343 | self.items.append(mc)
1344 | self.channels.append(mc.channel)
1345 | self.names = list(map(lambda x: x.name, self.items))
1346 | except Exception as e:
1347 | print('foo')
1348 | raise Exception(f'Error loading {self.name}: {e}')
1349 |
1350 | def find_by_name(self, name):
1351 | for i in self.items:
1352 | if i.name == name:
1353 | return i
1354 |
1355 | def find_by_number(self, number):
1356 | for i in self.items:
1357 | if i.number == number:
1358 | return i
1359 |
1360 | def find(self, name):
1361 | return self.find_by_name(name)
1362 |
1363 | def __iter__(self):
1364 | return iter(self.items)
1365 |
1366 | class ModeChannel(ModeChannels):
1367 | def __init__(self, root, channels):
1368 | self.root = root
1369 | self.name = root.text
1370 | self.number = root.get('Number')
1371 | self.channel = channels.find_by_name(self.name)
1372 |
1373 | class ModeHeads(Mode):
1374 |
1375 | def __init__(self, root, mode_channels):
1376 | self.root = root
1377 | self.items = []
1378 | self.count = 0
1379 | for item in root.findall('{%s}Head' % FixtureDefinition.xmlns):
1380 | self.count += 1
1381 | mh = ModeHead(item, mode_channels, self.count)
1382 | self.items.append(mh)
1383 |
1384 | def find_by_id(self, id):
1385 | if int(id) < len(self.items):
1386 | return self.items[int(id)]
1387 |
1388 | class ModeHead(Mode):
1389 | def __init__(self, root, mode_channels, count):
1390 | self.root = root
1391 | self.id = count
1392 | self.mode_channels = ModeHeadChannels(root, mode_channels)
1393 |
1394 | class ModeHeadChannels(ModeHead):
1395 | def __init__(self, root, mode_channels):
1396 | self.root = root
1397 | self.items = []
1398 | for hc in root.findall('{%s}Channel' % FixtureDefinition.xmlns):
1399 | self.items.append(ModeHeadChannel(hc, mode_channels))
1400 | self.names = list(map(lambda x: x.channel.name, self.items))
1401 |
1402 | class ModeHeadChannel(ModeHeadChannels):
1403 | def __init__(self, root, mode_channels):
1404 | self.root = root
1405 | self.number = self.root.text
1406 | self.mode_channel = mode_channels.find_by_number(self.number)
1407 | if not self.mode_channel:
1408 | raise Exception(f'No mode channel found for Head Channel {self.number}')
1409 | self.channel = self.mode_channel.channel
1410 |
--------------------------------------------------------------------------------
/bin/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | universes:
3 | - name: Direct
4 | id: 0
5 | - name: DS-1-12
6 | id: 1
7 | ipaddr: 172.1.1.64
8 | artnet_u: 0
9 | - name: DS-1-34
10 | id: 2
11 | ipaddr: 172.1.1.64
12 | artnet_u: 1
13 | - name: DS-1-56
14 | id: 3
15 | ipaddr: 172.1.1.64
16 | artnet_u: 2
17 | - name: DS-1-78
18 | id: 4
19 | ipaddr: 172.1.1.64
20 | artnet_u: 3
21 |
22 | - name: DS-2-12
23 | id: 5
24 | ipaddr: 172.1.1.65
25 | artnet_u: 0
26 | - name: DS-2-34
27 | id: 6
28 | ipaddr: 172.1.1.65
29 | artnet_u: 1
30 | - name: DS-2-56
31 | id: 7
32 | ipaddr: 172.1.1.65
33 | artnet_u: 2
34 | - name: DS-2-78
35 | id: 8
36 | ipaddr: 172.1.1.65
37 | artnet_u: 3
38 |
39 | - name: DS-3-12
40 | id: 9
41 | ipaddr: 172.1.1.66
42 | artnet_u: 0
43 | - name: DS-3-34
44 | id: 10
45 | ipaddr: 172.1.1.66
46 | artnet_u: 1
47 | - name: DS-3-56
48 | id: 11
49 | ipaddr: 172.1.1.66
50 | artnet_u: 2
51 | - name: DS-3-78
52 | id: 12
53 | ipaddr: 172.1.1.66
54 | artnet_u: 3
55 |
56 | - name: DS-4-12
57 | id: 13
58 | ipaddr: 172.1.1.67
59 | artnet_u: 0
60 | - name: DS-4-34
61 | id: 14
62 | ipaddr: 172.1.1.67
63 | artnet_u: 1
64 | - name: DS-4-56
65 | id: 15
66 | ipaddr: 172.1.1.67
67 | artnet_u: 2
68 | - name: DS-4-78
69 | id: 16
70 | ipaddr: 172.1.1.67
71 | artnet_u: 3
72 |
73 | fixtures:
74 | - name: ds-1-1
75 | model: Step Row 64 Heads
76 | u: 1
77 | a: 0
78 | - name: ds-1-2
79 | model: Step Row 64 Heads
80 | u: 1
81 | a: 192
82 | - name: ds-1-3
83 | model: Step Row 64 Heads
84 | u: 2
85 | a: 0
86 | - name: ds-1-4
87 | model: Step Row 64 Heads
88 | u: 2
89 | a: 192
90 | - name: ds-1-5
91 | model: Step Row 64 Heads
92 | u: 3
93 | a: 0
94 | - name: ds-1-6
95 | model: Step Row 64 Heads
96 | u: 3
97 | a: 192
98 | - name: ds-1-7
99 | model: Step Row 64 Heads
100 | u: 4
101 | a: 0
102 | - name: ds-1-8
103 | model: Step Row 64 Heads
104 | u: 4
105 | a: 192
106 |
107 | - name: ds-2-1
108 | model: Step Row 64 Heads
109 | u: 5
110 | a: 0
111 | - name: ds-2-2
112 | model: Step Row 64 Heads
113 | u: 5
114 | a: 192
115 | - name: ds-2-3
116 | model: Step Row 64 Heads
117 | u: 6
118 | a: 0
119 | - name: ds-2-4
120 | model: Step Row 64 Heads
121 | u: 6
122 | a: 192
123 | - name: ds-2-5
124 | model: Step Row 64 Heads
125 | u: 7
126 | a: 0
127 | - name: ds-2-6
128 | model: Step Row 64 Heads
129 | u: 7
130 | a: 192
131 | - name: ds-2-7
132 | model: Step Row 64 Heads
133 | u: 8
134 | a: 0
135 | - name: ds-2-8
136 | model: Step Row 64 Heads
137 | u: 8
138 | a: 192
139 |
140 | - name: ds-3-1
141 | model: Step Row 64 Heads
142 | u: 9
143 | a: 0
144 | - name: ds-3-2
145 | model: Step Row 64 Heads
146 | u: 9
147 | a: 192
148 | - name: ds-3-3
149 | model: Step Row 64 Heads
150 | u: 10
151 | a: 0
152 | - name: ds-3-4
153 | model: Step Row 64 Heads
154 | u: 10
155 | a: 192
156 | - name: ds-3-5
157 | model: Step Row 64 Heads
158 | u: 11
159 | a: 0
160 | - name: ds-3-6
161 | model: Step Row 64 Heads
162 | u: 11
163 | a: 192
164 | - name: ds-3-7
165 | model: Step Row 64 Heads
166 | u: 12
167 | a: 0
168 | - name: ds-3-8
169 | model: Step Row 64 Heads
170 | u: 12
171 | a: 192
172 |
173 | - name: ds-4-1
174 | model: Step Row 64 Heads
175 | u: 13
176 | a: 0
177 | - name: ds-4-2
178 | model: Step Row 64 Heads
179 | u: 13
180 | a: 192
181 | - name: ds-4-3
182 | model: Step Row 64 Heads
183 | u: 14
184 | a: 0
185 | - name: ds-4-4
186 | model: Step Row 64 Heads
187 | u: 14
188 | a: 192
189 | - name: ds-4-5
190 | model: Step Row 64 Heads
191 | u: 15
192 | a: 0
193 | - name: ds-4-6
194 | model: Step Row 64 Heads
195 | u: 15
196 | a: 192
197 | - name: ds-4-7
198 | model: Step Row 64 Heads
199 | u: 16
200 | a: 0
201 | - name: ds-4-8
202 | model: Step Row 64 Heads
203 | u: 16
204 | a: 192
205 |
206 | fixture_groups:
207 | - type: step
208 | name: ds-1
209 | rows: 8
210 | cols: 64
211 | fixtures:
212 | - name: ds-1-1
213 | - name: ds-1-2
214 | - name: ds-1-3
215 | - name: ds-1-4
216 | - name: ds-1-5
217 | - name: ds-1-6
218 | - name: ds-1-7
219 | - name: ds-1-8
220 |
221 | # # - type: step
222 | # # name: us-2
223 | # # rows: 8
224 | # # cols: 64
225 | # # fixtures: step_row
226 | # # # ip_address: 172.18.1.202
227 |
228 | # # - type: step
229 | # # name: us-3
230 | # # rows: 8
231 | # # cols: 64
232 | # # fixtures: step_row
233 | # # # ip_address: 172.18.1.203
234 |
235 | # # - type: step
236 | # # name: us-4
237 | # # rows: 8
238 | # # cols: 64
239 | # # fixtures: step_row
240 | # # ip_address: 172.18.1.204
241 |
242 | # # - name: us
243 | # # type: matrix
244 | # # rows: 8
245 | # # cols: 256
246 | # # fixtures:
247 | # # - us-1
248 | # # - us-2
249 | # # - us-3
250 | # # - us-4
251 |
252 |
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------
/bin/expand_fixture_features.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | '''
4 | ============================================================================
5 | Title: expand_fixture_features
6 | Description:
7 | Tool to pull osc config from Mitti and push script entries for QLC+
8 | ============================================================================
9 | '''
10 | # -*- coding: utf-8 -*-
11 |
12 | import argparse
13 | import sys
14 | from os import path
15 | import logging
16 |
17 |
18 | logger = logging.getLogger(__name__)
19 |
20 | from QLC import QLC
21 |
22 | DESC = '''
23 | This tool will:
24 | - ingest a showfile
25 | - ingest known fixtures based on normal QLC pathing
26 | - expand fixture capabilities by fixture group into scenes
27 | - edit in place or (over) write a new file
28 | '''
29 |
30 | def setup_logging(args):
31 | """ setup logging """
32 | global debug, verbose
33 | llevel = logging.ERROR
34 | if args.debug:
35 | llevel = logging.DEBUG
36 | print("logging debug")
37 | elif args.verbose:
38 | llevel = logging.INFO
39 | print("logging verbose")
40 |
41 | if args.logfile:
42 | if not os.access(args.logfile, os.W_OK):
43 | print("ERROR: Unable to write to %s")
44 | sys.exit(1)
45 | logging.basicConfig(filename=args.logfile, level=llevel)
46 | else:
47 | logging.basicConfig(level=llevel)
48 |
49 | def get_args():
50 | ''' read arguments from C/L '''
51 | parser = argparse.ArgumentParser(
52 | description=DESC,
53 | formatter_class=argparse.RawTextHelpFormatter)
54 | parser.set_defaults(help=False)
55 | parser.set_defaults(overwrite=False)
56 | parser.set_defaults(inplace=False)
57 | parser.set_defaults(import_fixture_groups=False)
58 | parser.set_defaults(export_fixture_groups=False)
59 | parser.set_defaults(expand_fixtures=False)
60 | parser.set_defaults(dump=False)
61 | parser.set_defaults(verbose=False)
62 | parser.set_defaults(debug=False)
63 | parser.add_argument("-V", "--verbose",
64 | help='enable verbose output',
65 | action="store_true")
66 | parser.add_argument("-D", "--debug",
67 | help='enable debugging output',
68 | action="store_true")
69 | parser.add_argument("--logfile", help="Send output to a file.")
70 | parser.add_argument("-d", "--dump",
71 | help='Dump modifications to STDOUT',
72 | action="store_true")
73 |
74 | xls = parser.add_argument_group("XLS Import/Export")
75 | xls.add_argument("--import-fixture-groups", "-I", help="Import Fixture Groups from XLSX file", action='store_true')
76 | xls.add_argument("--export-fixture-groups", "-E", help="Export Fixture Groups to XLSX file", action='store_true')
77 | xls.add_argument("--xls-file", "-x", help="Export Fixture Groups to XLSX file")
78 |
79 | fmgmt = parser.add_argument_group("File IN/Out")
80 | fmgmt.add_argument("-o", "--outputfile",
81 | type=str, default=None,
82 | help="Write to output file")
83 | fmgmt.add_argument("-F", "--overwrite", action="store_true",
84 | help="overwrite existing output file")
85 | fmgmt.add_argument("-i", "--inplace",
86 | help='Overwrite the showfile in place.',
87 | action="store_true")
88 | parser.add_argument("-X", "--expand-fixtures", help="Expand Fixture Features into Scenes", action='store_true')
89 |
90 | parser.add_argument('showfile', type=str,
91 | help='Showfile to extend')
92 |
93 | args = parser.parse_args()
94 | if args.help or not args.showfile:
95 | parser.print_help()
96 | sys.exit(0)
97 |
98 | if args.inplace and args.outputfile:
99 | print("can only write output file or in-place not both.")
100 | sys.exit(1)
101 |
102 | if ( args.export_fixture_groups or args.import_fixture_groups) and not args.xls_file:
103 | print("xls file required to import or export fixture groups.")
104 | sys.exit(1)
105 |
106 | return args
107 |
108 |
109 | def main():
110 | # load a showfile
111 | args = get_args()
112 | setup_logging(args)
113 |
114 | # load the file and expand
115 | q = QLC(args.showfile)
116 |
117 | if args.export_fixture_groups and args.xls_file:
118 | q.export_fixture_groups(args.xls_file)
119 |
120 | elif args.import_fixture_groups and args.xls_file:
121 | if (not (args.inplace or args.outputfile)):
122 | print("importing fixture groups requires output option.")
123 | sys.exit(1)
124 | q.import_fixture_groups(args.xls_file)
125 | if args.expand_fixtures:
126 | q.expand_fixture_group_capabilities()
127 |
128 | # handle output
129 | if args.dump:
130 | q.workspace.dump()
131 |
132 | if args.inplace:
133 | if not args.overwrite:
134 | yn = input(f'Overwrite {args.showfile} [yN] ?')
135 | if 'n' in yn.lower():
136 | sys.exit(0)
137 | q.workspace.write(args.showfile)
138 |
139 | elif args.outputfile:
140 | logger.debug(f'writing {args.outputfile}...')
141 | if path.exists(args.outputfile):
142 | if not args.overwrite:
143 | yn = input(f'Overwrite {args.outputfile} [yN] ?')
144 | if 'n' in yn.lower():
145 | sys.exit(0)
146 | q.workspace.write(args.outputfile)
147 |
148 |
149 | if __name__ == '__main__':
150 | main()
151 |
--------------------------------------------------------------------------------
/bin/generate_fixtures.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | '''
4 | ============================================================================
5 | Title: generate fixtures based on definitions
6 | Description:
7 | Pulls in config from yaml and generates fixtures and layouts.
8 | ============================================================================
9 | '''
10 | # -*- coding: utf-8 -*-
11 |
12 | import argparse
13 | import sys
14 | import yaml
15 | import logging
16 | import pprint
17 | import os
18 |
19 | from QLC import QLC
20 |
21 | logger = logging.getLogger(__name__)
22 |
23 | DESC = '''
24 | This tool will:
25 | - ingest a yaml config
26 | - ingest a showfile
27 | - ingest known fixtures based on normal QLC pathing
28 | - generate/update fixture definitions based on config
29 | - generate/update fixtures based on config
30 | - generate/update fixture groups based on config
31 | - edit in place or (over) write a new showfile
32 | '''
33 |
34 | def setup_logging(args):
35 | """ setup logging """
36 | global debug, verbose
37 | llevel = logging.ERROR
38 | if args.debug:
39 | llevel = logging.DEBUG
40 | print("logging debug")
41 | elif args.verbose:
42 | llevel = logging.INFO
43 | print("logging verbose")
44 |
45 | if args.logfile:
46 | if not os.access(args.logfile, os.W_OK):
47 | print("ERROR: Unable to write to %s")
48 | sys.exit(1)
49 | logging.basicConfig(filename=args.logfile, level=llevel)
50 | else:
51 | logging.basicConfig(level=llevel)
52 |
53 | def get_args():
54 | ''' read arguments from C/L '''
55 | parser = argparse.ArgumentParser(
56 | description=DESC,
57 | formatter_class=argparse.RawTextHelpFormatter)
58 | parser.set_defaults(help=False)
59 | parser.set_defaults(overwrite=False)
60 | parser.set_defaults(inplace=False)
61 | parser.set_defaults(dump=False)
62 | parser.set_defaults(verbose=False)
63 | parser.set_defaults(debug=False)
64 | parser.add_argument("-V", "--verbose",
65 | help='enable verbose output',
66 | action="store_true")
67 | parser.add_argument("-D", "--debug",
68 | help='enable debugging output',
69 | action="store_true")
70 | parser.add_argument("--logfile", help="Send output to a file.")
71 | parser.add_argument("-i", "--inplace",
72 | help='Overwrite the showfile in place.',
73 | action="store_true")
74 | parser.add_argument("-d", "--dump",
75 | help='Dump modifications to STDOUT',
76 | action="store_true")
77 | parser.add_argument("-c", "--config_file",
78 | type=str, default=None,
79 | help="config file with definitions")
80 | parser.add_argument("-o", "--output_file",
81 | type=str, default=None,
82 | help="Write to output file")
83 | parser.add_argument("-F", "--overwrite", action="store_true",
84 | help="overwrite existing output file")
85 | parser.add_argument('showfile', type=str,
86 | help='Showfile to extend')
87 |
88 | args = parser.parse_args()
89 | if args.help or not args.showfile:
90 | parser.print_help()
91 | sys.exit(0)
92 |
93 | return args
94 |
95 |
96 | def main():
97 | # load a showfile
98 | args = get_args()
99 | setup_logging(args)
100 |
101 | # load the config
102 | with open(args.config_file) as file:
103 | cfg = yaml.load(file,Loader=yaml.FullLoader)
104 |
105 | # load the file and expand
106 | q = QLC(args.showfile)
107 |
108 | universes = cfg.get('universes', [])
109 | if universes:
110 | logger.info('generating %d universes' % len(universes))
111 | q.generate_universes(universes)
112 | q.workspace.dump()
113 |
114 | fixtures = cfg.get('fixtures', [])
115 | if fixtures:
116 | logger.info('generating %d fixtures' % len(fixtures))
117 | q.generate_fixtures(cfg.get('fixtures'))
118 |
119 | fixture_groups = cfg.get('fixture_groups', [])
120 | if fixture_groups:
121 | logger.info('generating %d fixture_groups' % len(fixture_groups))
122 | q.generate_fixture_groups(fixture_groups)
123 |
124 | # q.expand_fixture_group_capabilities()
125 |
126 | # handle output
127 | if args.dump:
128 | logger.info('dumping workspace')
129 | q.workspace.dump()
130 |
131 | elif not args.output_file:
132 | if not args.inplace:
133 | yn = input(f'Overwrite {args.showfile} [yN] ?')
134 | if not 'y' in yn:
135 | sys.exit(0)
136 |
137 | q.workspace.write(args.showfile)
138 |
139 | else:
140 | if os.path.exists(args.output_file):
141 | if not args.overwrite:
142 | yn = input(f'Overwrite {args.output_file} [yN] ?')
143 | if 'y' in yn.lower():
144 | q.workspace.write(args.outputfle)
145 | else:
146 | q.workspace.write(args.outputfle)
147 |
148 |
149 | if __name__ == '__main__':
150 | main()
151 |
--------------------------------------------------------------------------------
/bin/map_fixtures.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | # default network for ArtNet in CIDR
4 | artnet_network: 172.19.0.0/24
5 |
6 | # NOTE: These are ordered
7 | universes:
8 | - name: Truss
9 | ip: 172.19.0.100
10 | - name: Wireless
11 | ip: 172.19.0.101
12 |
13 | fixtures:
14 | - name: DS1-1
15 | definition: Step Row 64 Heads
16 | mode: All
17 | address: 0
18 | ip: 172.19.0.201
19 | artnet_u: 1
20 | channel: 1
21 |
22 | - name: DS1-2
23 | model: Step Row 64 Heads
24 | mode: All
25 | address: 192
26 | ip: 172.19.0.201
27 | arnet_u: 1
28 | channel: 192
29 |
30 | - name: DS1-3
31 | model: Step Row 64 Heads
32 | mode: All
33 | universe: 3
34 | address: 0
35 | ip: 172.19.0.201
36 | arnet_u: 2
37 |
38 | - name: DS1-4
39 | model: Step Row 64 Heads
40 | mode: All
41 | universe: 3
42 | address: 192
43 | channels: 192
44 |
45 | - name: DS1-5
46 | model: Step Row 64 Heads
47 | mode: All
48 | universe: 4
49 | address: 0
50 | channels: 192
51 |
52 | - name: DS1-6
53 | model: Step Row 64 Heads
54 | mode: All
55 | universe: 4
56 | address: 192
57 | channels: 192
58 |
59 | - name: DS1-7
60 | model: Step Row 64 Heads
61 | mode: All
62 | universe: 5
63 | address: 0
64 | channels: 192
65 |
66 | - name: DS1-8
67 | model: Step Row 64 Heads
68 | mode: All
69 | universe: 5
70 | address: 192
71 | channels: 192
72 |
73 |
74 | fixture_groups:
75 | - name: All Steps
76 | x: 256
77 | y: 24
78 | fixtures:
79 | - name: ds1-1
80 | x: 0
81 | y:
82 |
83 |
--------------------------------------------------------------------------------
/bin/surface_map.yml:
--------------------------------------------------------------------------------
1 | ---
2 | surface_maps:
3 | APC40 mkII:
4 | Intensity: Slider 1
5 | Red: Slider 2
6 | Green: Slider 3
7 | Blue: Slider 4
8 | White: Slider 5
9 | Amber: Slider 6
10 | UV: Slider 7
11 | Strobe: Slider 8
12 | Speed: Slider 9
13 | Pan:
14 |
15 |
16 |
--------------------------------------------------------------------------------
/bin/sync_mitti.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | '''
4 | ============================================================================
5 | Title: sync_osc
6 | Description:
7 | Tool to pull osc config from Mitti and push script entries for QLC+
8 | ============================================================================
9 | '''
10 | # -*- coding: utf-8 -*-
11 |
12 | import argparse
13 | import urllib.request
14 | import sys
15 | from os import path
16 | from urllib.parse import urlparse
17 | from html.parser import HTMLParser
18 | from QLC import QLC
19 |
20 | SENDOSC = '/usr/local/bin/sendosc'
21 |
22 | # TODO:
23 | EXPORTED_FUNCTIONS = [
24 | 'audioOff',
25 | 'audioOn',
26 | 'fullscreenOff',
27 | 'fullscreenOn',
28 | 'goto10',
29 | 'goto20',
30 | 'goto30',
31 | 'jump',
32 | 'loopOn',
33 | 'loopOff',
34 | 'panic',
35 | 'pause',
36 | 'play',
37 | 'rewind',
38 | 'toggleFullscreen',
39 | 'toggleLoop',
40 | 'togglePlay',
41 | 'toggleTransitionOnPlay',
42 | 'transitiionOnPlayOff',
43 | 'transitiionOnPlayOn',
44 | ]
45 |
46 | # default looking at localhost.
47 | URL='http://127.0.0.1:51000'
48 |
49 | DESC = '''
50 | This tool will:
51 | - ingest a showfile
52 | - connect to a Mitti OSC server
53 | - expand each Mitti Cues into a callable 'script' scene
54 | - edit in place or (over) write a new file
55 | '''
56 |
57 | def _match(list, item):
58 | for i in list:
59 | if item == i:
60 | return item
61 | return None
62 |
63 |
64 | class OSC_HTMLParser(HTMLParser):
65 | ''' We rip all the headings out as those are the valid
66 | commands.
67 | NOTE: interestingly cue's start at 1 not zero .. so to keep things
68 | consistent .. I am using a dict of lists '''
69 | def __init__(self):
70 | HTMLParser.__init__(self)
71 |
72 | def read(self, data):
73 | ''' read the input and return a list of cues '''
74 | # clear output and reset parsers state
75 | self.cues = {}
76 | self.functions = []
77 | self.last_cf = ''
78 | self.text = {}
79 | self.reset()
80 |
81 | # parse the data
82 | self.feed(data)
83 | return self.functions, self.cues, self.text
84 |
85 | def handle_starttag(self, tag, attrs):
86 | ''' all the cue's are in Tags '''
87 | if tag == 'strong':
88 | self.valid_data = 1
89 |
90 | def handle_endtag(self, tag):
91 | self.valid_data = 0
92 |
93 | def handle_data(self, data):
94 | ''' split the cues' out .. eventually we will want
95 | to merge this with config so we can default some
96 | of the actions that take values, right now we only
97 | operate on functions that require no value '''
98 | if self.valid_data:
99 | # this is weird because some functions have subconfig
100 | # NOTE: leading / means idx 0 is None
101 | pdata = data.split('/')
102 |
103 | # handle per-cue functions
104 | if len(pdata) >= 4:
105 | cue_id = pdata[2]
106 | function = pdata[3]
107 | self.last_cf = '%s/%s' % (cue_id,function)
108 | if not self.cues.get(cue_id):
109 | self.cues[cue_id] = []
110 | self.cues[cue_id].append(function)
111 |
112 | # handle global functions
113 | elif len(pdata) == 3:
114 | function = pdata[2]
115 | self.last_cf = '%s' % (function)
116 | self.functions.append(function)
117 | else:
118 | if self.last_cf not in self.text:
119 | info = data.split('.')[0]
120 | self.text[self.last_cf] = info
121 |
122 | def sync_mitti(args, q, url):
123 |
124 | # break out URL
125 | u = urlparse(args.url)
126 |
127 | # Grab current OSC config
128 | try:
129 | with urllib.request.urlopen(args.url) as response:
130 | html = response.read().decode('utf-8')
131 | except Exception as e:
132 | print('unable to connect to %s: %s' % ( url, str(e)))
133 | sys.exit(1)
134 |
135 | if not html:
136 | print('no data from %s:' % url )
137 | sys.exit(1)
138 | osc = OSC_HTMLParser()
139 | g_functions, cues, text = osc.read(html)
140 |
141 | # create scripts for all functions.
142 | # NOTE: these are all using NAME as significant as we can't predict ID
143 | # NOTE: have asked Mitti Developer to add name to the OSC server.
144 | # build the command
145 | syscmd = [
146 | 'systemcommand:%s' % args.sendosc,
147 | 'arg:%s' % u.hostname,
148 | 'arg:%d' % u.port or 51000 ]
149 | cmd = ' '.join( syscmd )
150 |
151 | # create the script scenes
152 | for cue, functions in cues.items():
153 | for function in functions:
154 | this_cmd = cmd
155 | cf = '%s/%s' % (cue, function)
156 | if _match(EXPORTED_FUNCTIONS, function):
157 | if cf in text:
158 | this_cmd = '//Mitti command: %s %s\n' % (cf, text.get(cf)) + cmd
159 | q.function(
160 | Type = 'Script',
161 | Path = '/Mitti/%s' % cue,
162 | Name = '/Mitti/%s' % cf,
163 | Command = this_cmd + ' arg:/mitti/%s/%s' % (cue, function)
164 | )
165 |
166 | for function in g_functions:
167 | if _match(EXPORTED_FUNCTIONS, function):
168 | this_cmd = cmd
169 | if function in text:
170 | this_cmd = '//Mitti command: %s %s\n' % (function, text.get(function)) + cmd
171 | q.function(
172 | Type = 'Script',
173 | Path = '/Mitti',
174 | Name = '/Mitti/%s' % (function),
175 | Command = this_cmd + ' arg:/mitti/%s' % function
176 | )
177 |
178 |
179 | def get_args():
180 | ''' read arguments from C/L '''
181 | parser = argparse.ArgumentParser(
182 | description=DESC,
183 | formatter_class=argparse.RawTextHelpFormatter)
184 | parser.set_defaults(help=False)
185 | parser.set_defaults(overwrite=False)
186 | parser.set_defaults(inplace=False)
187 | parser.set_defaults(dump=False)
188 | parser.add_argument("-i", "--inplace",
189 | help='Overwrite the showfile in place.',
190 | action="store_true")
191 | parser.add_argument("-d", "--dump",
192 | help='Dump modifications to STDOUT',
193 | action="store_true")
194 | parser.add_argument("-o", "--outputfile",
195 | type=str, default=None,
196 | help="Write to output file")
197 | parser.add_argument("-s", "--sendosc",
198 | type=str, default=SENDOSC,
199 | help="Path to the sendosc executable.")
200 | parser.add_argument("-u", "--url",
201 | type=str, default=URL,
202 | help="URL for the Mitti OSC Service")
203 | parser.add_argument("-F", "--overwrite", action="store_true",
204 | help="overwrite existing output file")
205 | parser.add_argument('showfile', type=str,
206 | help='Showfile to extend')
207 |
208 | args = parser.parse_args()
209 | if args.help or not args.showfile:
210 | parser.print_help()
211 | sys.exit(0)
212 |
213 | return args
214 |
215 | def main():
216 | # load a showfile
217 | args = get_args()
218 |
219 | if not args.outputfile and not args.inplace:
220 | print('ERR: must specify -i or an outputfile.')
221 | sys.exit(1)
222 |
223 | if not path.exists(args.sendosc):
224 | print('WARN: sendosc not found at %s\n please install from: https://github.com/yoggy/sendosc or brew ')
225 |
226 | # load a showfile
227 | q = QLC(file='../showfiles/blank.qxw')
228 | sync_mitti(args, q, URL)
229 |
230 | # handle output
231 | if args.dump:
232 | q.workspace.dump()
233 |
234 | elif args.inplace:
235 | # this assumes overwrite
236 | q.workspace.write(args.showfile)
237 |
238 | elif args.outputfile:
239 | if path.exists(args.outputfile):
240 | if not args.overwrite:
241 | yn = input(f'Overwrite {args.outputfile} [yN] ?')
242 | if 'y' in yn.lower():
243 | q.workspace.write(args.outputfle)
244 | else:
245 | q.workspace.write(args.outputfle)
246 |
247 | if __name__ == '__main__':
248 | main()
249 |
250 |
--------------------------------------------------------------------------------
/docs/Chase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/Chase.png
--------------------------------------------------------------------------------
/docs/Collection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/Collection.png
--------------------------------------------------------------------------------
/docs/Collections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/Collections.png
--------------------------------------------------------------------------------
/docs/Colors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/Colors.png
--------------------------------------------------------------------------------
/docs/CueList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/CueList.png
--------------------------------------------------------------------------------
/docs/ExpandFixtureFeatures.md:
--------------------------------------------------------------------------------
1 | # Expand Fixture Features
2 |
3 | The tool ingests a showfile and configuration; and then writes a new showfile ( or in place ) with the configuration data expanded into scene folders based on the fixture groups defined. It's designed to be fixture agnostic, so you can mix "color wheel" heads and "RGB(W)" heads in your Fixture Groups and still get a color scene for that group that works for both sets.
4 |
5 | ## Usage
6 |
7 | 1. setup your show file as per [this document](Showfile_Design.md) with Fixture Groups that would be used in scenes in the show
8 | 1. setup this tool and make sure it's ready to operate using the help
9 | ```
10 | git clone git@github.com:sandinak/qlcplus-tools.git
11 | make
12 | . sourceme
13 | ./bin/expand_fixture_features.py --help
14 | ```
15 | 1. Run the tool against your file to generate the scenes
16 | ```
17 | ./bin/expand_fixture_features.py -d -v -o newfile.qxw showfile.qxw
18 | ```
19 | 1. run QLC+ on the file and review the new scenes.
20 |
21 | ## Advanced
22 |
23 | - if you want other colors/intensities than the defaults.. you can edit the QLC.py file to add/extend the color pallet. I am considering making this an external config file if there's interest
24 | - once you're comfortable using the tool, you can just edit the showfile in place. The tool is designed to be NON-Destructive against existing config in the file; and only add new config if needed ( eg .. it's idempotent )
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/ExpandRGBLayouts.md:
--------------------------------------------------------------------------------
1 | # Expand RGB Layouts
2 |
3 | This part of the tool exports the fixture group layouts into a spreadsheet to more easily edit the locations of the assets in the group to the design you want.
4 |
5 | ## Export
6 |
7 | ```
8 | ./bin/expand_fixture_features.py -o examples/showfile-after.qxw ./examples/showfile-before.qxw -E -x ./examples/showfile.xls
9 | ```
10 |
11 | This will write out a file you can then open with your favorite spreadsheet editor, I like LibreOffice. In the file you'll find tabs that represent each Fixture Group with cells that match the instances of the fixture at that position
12 |
13 |
14 |
15 | ### Existing Fixture Groups
16 | You can edit the layout of fixtures here by:
17 |
18 | - moving the cells to the locations you want, just maintain the format of the cell name
19 | - adding more rows and columns, just make sure you change the X and Y settings at A2
20 |
21 |
22 | ### New FixtureGroups
23 |
24 | You can create new groups by:
25 |
26 | 1. duplicating an existing tab and naming the group in the tab name
27 | 1. changing the name in the header on the new page
28 | 1. updating the X: Y: settings in A2
29 | 1. copy/pasting from this or other Tabs to create the layout you want
30 |
31 | ## Import
32 | ```
33 | ./bin/expand_fixture_features.py ./examples/showfile-before.qxw -I -x ./examples/showfile.xls -o ./examples/showfile-after.qxw
34 | ```
35 |
36 | This will create the new file with a merge of the existing file and the new Fixture Groups and Layouts.
37 |
38 |
--------------------------------------------------------------------------------
/docs/FixtureDefinition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/FixtureDefinition.png
--------------------------------------------------------------------------------
/docs/FixtureDefinitions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/FixtureDefinitions.png
--------------------------------------------------------------------------------
/docs/FixtureScenes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/FixtureScenes.png
--------------------------------------------------------------------------------
/docs/ShowfileDesign.md:
--------------------------------------------------------------------------------
1 | # Showfile Design
2 |
3 | The premise of operation is that instead of making large monolithic scenes in the tool that require point editing to adapt or update, we assemble the show via combinations of settings. In this way.. making a change to a visual scene is as simple as changing the groupings of scenes. This also allows for easier global changes in the system.
4 |
5 | We use "folders" in the system to organize the layout of the data, and the tool will express the entire "path" to an object in the name so that when viewing the information in the system; it's clear what's being selected at that point.
6 |
7 | ## Fixture Groups
8 |
9 | We use this as the basic grouping of elements on the stage. Typically this is fixutres of same location or type or combinations; which are used together during scenes to light the show. In QLC+ are base set of groups created automatically when generating RGBMatrix features, however any seriues of fixtures anywhere on the stage can be grouped as desired.
10 |
11 |
12 |
13 |
14 | If you're gonna be using these fixture sets in a chase or RGB Matrix, remember to lay them out correctly relative to their position on the stage.
15 |
16 | ## Fixture Based Scenes (FBS)
17 | We leverage this capability to assign "feature based scenes" to the groups so that when assembling cue-points in "chase" you can easily change the behavior of the fixtures by replacing the sets. This also means where you're using a common feature based scene, such as a position for a head; if you need to alter that for the entire show you can make the change in 1 place and it will apply to all instances of use of the scene.
18 |
19 | For the desigin of the software, we assume the sets needed are Capabilities (gobo, etc..), Colors, and Positions. Specials I use as a catch all for effects or other that are DMX driven such as Relays, Fog/Haze, etc.
20 |
21 |
22 |
23 | To create these scenes, we define some basic settings in the tool, load the existing showfile, and the tool will then automatically generate the fixture based scenes as needed.
24 |
25 | ### Organization
26 |
27 | Sets of capablities for each fixture group are organized in to scenes that can then be used in collections to assemble the view
28 |
29 |
30 |
31 | The path is preserved in the name to avoid collisions and allow easy reference in when viewing an entire collection.
32 |
33 |
34 | ## Collections
35 | We leverage collections to assemble the FBS into a "view" of the stage. Collections can include scenes, matrixes, EFX, chases, sequences, scripts and other objects. We leverage this capability to take a single collection and assemble a set of FBSs and such to establish a view of the stage.
36 |
37 |
38 |
39 | In this example it's the "preset" for the show so we have all the lights on and pointed in an expected configuration so that technicians can visually verify everything is working before the show starts.
40 |
41 | ### Organization
42 |
43 | We organize collections by their use-case and timing in the show. I use a cue naming format that allows easy expansion/extenssion {song}-{part}-{cue}; and song 0 is before-curtain.
44 |
45 |
46 |
47 |
48 | ## Chase
49 |
50 | The Show is then setup to be run via cue'ing using a "Chase". Each cue is added with timings so that it can then be used as needed. To do this a hold time of ∞ is generally used so that the show can be cue'd via the virtual Console Cue List. Fade in/out timings can be set here as well .. and/or using a defined holdtime and duration can "auto-cue" where it makes sense.
51 |
52 |
53 |
54 |
55 | ## Cuelist
56 |
57 | Once you've assembled the parts, A Cuelist is used in the Virtual Console to allow the show to be cued. Once added to the Virutal console, in the configuration we define a "Next Cue" button ( we use M ), and a "Previous Cue" button to allow the operator to move the show through time.
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/docs/XLSFixtureGroup-Spots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sandinak/qlcplus-tools/1850a22452fb0326902e3babaec89f6efbb8afee/docs/XLSFixtureGroup-Spots.png
--------------------------------------------------------------------------------
/fixtures/fixtures.yml:
--------------------------------------------------------------------------------
1 | ---
2 | output_dir: ~/Library/Application Support/QLC+/Fixtures/
3 |
4 | default:
5 | Creator:
6 | Name: Q Light Controller Plus
7 | Version: 4.12.2
8 | Author: Branson Matheson
9 |
10 | defs:
11 | - Manufacturer: PHS Chorus
12 | Model: Step Row 64 Heads
13 | Type: LED Bar (Pixels)
14 | Colors: RGB
15 | Heads: 64
16 | Physical:
17 | Bulb:
18 | Type: LED
19 | Lumens: 10
20 | ColourTemperature: 0
21 | Dimensions:
22 | Weight: 80
23 | Width: 10000
24 | Height: 203
25 | Depth: 228
26 | Lens:
27 | Name: Other
28 | DegreesMin: 180
29 | DegreesMax: 180
30 | Focus:
31 | Type: Fixed
32 | PanMax: 0
33 | TiltMax: 1
34 | Layout:
35 | Width: 64
36 | Height: 1
37 | Technical:
38 | PowerConsumption: 0
39 | DmxConnector: Other
40 |
41 | # - Manufacturer: PHS Chorus
42 | # Model: Step Row 128 Heads
43 | # Type: LED Bar (Pixels)
44 | # Colors: RGB
45 | # Heads: 128
46 | # Physical:
47 | # Bulb:
48 | # Type: LED
49 | # Lumens: 10
50 | # ColourTemperature: 0
51 | # Dimensions:
52 | # Weight: 80
53 | # Width: 10000
54 | # Height: 203
55 | # Depth: 228
56 | # Lens:
57 | # Name: Other
58 | # DegreesMin: 180
59 | # DegreesMax: 180
60 | # Focus:
61 | # Type: Fixed
62 | # PanMax: 0
63 | # TiltMax: 1
64 | # Layout:
65 | # Width: 64
66 | # Height: 1
67 | # Technical:
68 | # PowerConsumption: 0
69 | # DmxConnector: Other
70 | #
71 |
--------------------------------------------------------------------------------
/lib/qlcplus_bin.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | '''
4 | ============================================================================
5 | Title: discover qlc version
6 | Description:
7 | create fixtures from basic config
8 | ============================================================================
9 | '''
10 |
11 | import platform
12 | import os
13 |
14 | BINPATH = {
15 | 'Darwin': '/Applications/QLC+.app/Contents/MacOS/qlcplus',
16 | }
17 |
18 | class qlc_bin:
19 | def __init__(self, **kwargs)
20 |
21 | ''' setup class '''
22 | self.__dict__.update(kwargs)
23 | system = platform.system()
24 |
25 | _bin = self._find_bin()
26 | help = os.system(_bin + ' --help')
27 |
28 | version = self._parse_help(help)
29 |
30 |
31 | def _find_bin(self):
32 | if
33 |
34 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pyaml
2 | XlsxWriter
3 | pandas
4 | openpyxl
5 |
--------------------------------------------------------------------------------
/showfiles/blank.qxw:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Q Light Controller Plus
6 | 4.12.3
7 | Branson Matheson
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | None
21 | Default
22 | Default
23 | None
24 | Default
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/sourceme:
--------------------------------------------------------------------------------
1 |
2 | # source to setup shell and environment
3 |
4 | export GIT_NAME=$(basename ${PWD})
5 | echo "activating $GIT_NAME"
6 |
7 | # activate virtualenv
8 | . venv/bin/activate
9 |
10 | # set prompt
11 | COLOR_PROMPT=${GIT_NAME}
12 |
13 | # extend path
14 | export PATH="${PWD}/bin:${PATH}"
15 |
16 | # add local dir for libs
17 | export PYTHONPATH="${PATH}/lib:${PYTHONPATH}"
18 |
19 |
20 |
--------------------------------------------------------------------------------