diff --git a/README.md b/README.md index e69de29..15d2b0a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,35 @@ +# Multimedijski krmilnik FRI + +> *Also known as **PolžProjekt** or **KatjaKontroler*** + +## Arhitektura + +Sistem sestavljata dva kartična računalnika (trenutno Raspberry Pi 4) - *krmilnik* in *zaslon*. Na krmilniku je zagnan MQTT broker (mosquitto) in več *driverjev*, ki služijo kot mostovi med napravami v predavalnici in MQTT. Na zaslonu teče spletni brskalnik (chromium) z odprtim *frontendom* (VueJS aplikacija, servirana s krmilnika). Frontend komunicira neposredno z MQTT brokerjem preko WebSockets povezave. + +### Driverji + +- `tse_serial` - relejno polje *TSE ___*, ki krmili napajanje različnih komponent predavalnice, projekcijska platna in senčila +- `barco_telnet` - en projektor tipa *Barco G62* preko novega "Barco Telnet" protokola. Za več projektorjev je potrebnih več instanc driverja +- `projector_motors` - dvigala za projektorje preko dveh modulov *Grove I2C 4-channel SPDT relay* +- `lucke` - razsvetljava v predavalnici preko A-rosso sistema WebSCADA +- `extron_audio_matrix` - avdio matrika - **NI DOKONČAN** +- `barco_rlmw_{http,tcp}` - en projektor serije *Barco RLM W*. Obstajata dve različici (http in tcp) + +## Namestitev + +Namestitev poteka z orodjem *Ansible*, potrebna pa je tudi lokalna namestitev orodja `npm` (za izgradnjo frontenda). V mapi `ansible_deploy` sta ločeni mapi z playbooki za krmilnik in zaslon. + +Osnovni operacijski sistem za krmilnik je *RaspberryPi OS Lite* za zaslon pa *RaspberryPi OS Full*. Namestite ga lahko z orodjem *Raspberry Pi Imager* - pri tem bodite pozorni da nastavite uporabniško ime `pi` in geslo, ki je shranjeno v RC. + +Za namestitev mora imeti Raspberry Pi dostop do interneta, zato ga priključite v omrežje z DHCP. Njegov IP naslov se izpiše na HDMI izhodu, lahko ga pa najdete tudi na drug način. Ker se po prvem zagonu na vgrajeni mrežni kartici konfigurira statičen IP naslov, je priporočeno, da se za namestitev uporablja USB Ethernet adapter. + +Primer namestitve krmilnika za predavalnico P01 (ime v inventory datoteki je `p01_controller`), ki ima trenutno IP naslov `10.32.50.123` (ker se razlikuje od inventory datoteke ga je treba določiti z `-e ansible_host=`): + +```sh +cd ansible_deploy/controller +ansible-playbook playbook.yml -i inventory.yml -l p01_controller -e ansible_host=10.32.50.123 +``` + +## Posodobitev + +Posodobitev poteka enako kot namestitev. Če se izvaja v predavalnici je potrebo poskrbeti, da imajo ciljne naprave dostop do interneta. Za to ni dobrega načina, zato je priporočen enak pristop kot pri namestitvi (USB Ethernet adapter in omrežje z DHCP - lahko celo deljena povezava s prenosnika). diff --git a/ansible_deploy/controller_script.service.j2 b/ansible_deploy/controller_script.service.j2 index ccde8cb..b75861a 100644 --- a/ansible_deploy/controller_script.service.j2 +++ b/ansible_deploy/controller_script.service.j2 @@ -14,4 +14,3 @@ WorkingDirectory={{ INSTALL_BASE }} [Install] WantedBy=mm-controller.target -DefaultInstance=main diff --git a/ansible_deploy/inventory.yml b/ansible_deploy/inventory.yml index ee3854c..5930903 100644 --- a/ansible_deploy/inventory.yml +++ b/ansible_deploy/inventory.yml @@ -29,11 +29,11 @@ predavalnice_pi: - position: main model: barco_G62 port: 3023 - ip: 192.168.192.13 + ip: 192.168.192.12 - position: side model: barco_G62 port: 3023 - ip: 192.168.192.14 + ip: 192.168.192.13 tse_box: @@ -47,5 +47,5 @@ predavalnice_pi: lucke: url: http://192.168.190.90:8091 - roomId: 0 + roomId: 1 bearer_token: 0954afe1-4111-4f89-a123-fea08a55dc46 diff --git a/ansible_deploy/playbook.yaml b/ansible_deploy/playbook.yaml index 3ef361e..4b398cf 100644 --- a/ansible_deploy/playbook.yaml +++ b/ansible_deploy/playbook.yaml @@ -106,6 +106,11 @@ src: ./malinaConfig.toml.j2 dest: "{{INSTALL_BASE}}/malinaConfig.toml" + - name: Install python libraries + ansible.builtin.shell: + cmd: "poetry install" + chdir: "{{INSTALL_BASE}}" + # # INSTALL FRONTEND @@ -129,16 +134,11 @@ - name: Fix www root permission become: true ansible.builtin.file: - path: "/var/www/html/" + path: "/var/www/html" owner: pi group: pi recurse: true - mode: '0644' - - - name: Install python libraries - ansible.builtin.shell: - cmd: "poetry install" - chdir: "{{INSTALL_BASE}}/controller" + mode: 'u=rwX,g=rX,o=rX' # # SERVICES @@ -175,7 +175,7 @@ - barco@side.service # Lifti za projektorje (naši releji) - - name: template projector motors service + - name: template projector motors service when: projector_motors is defined block: - name: template service @@ -191,7 +191,7 @@ enabled: true state: restarted daemon_reload: true - + # Power, platna, etc. (TSE relay box) - name: template tse serial box service when: tse_box is defined diff --git a/barco.service b/barco.service deleted file mode 100644 index 653b8b8..0000000 --- a/barco.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Barco projector control -After=mqtt_init.service - - -[Service] -ExecStart=/usr/bin/python3 /home/rpi/barco_telnet_control.py -User=rpi -Group=rpi -Type=simple -Restart=always - - -[Install] -WantedBy=mqtt_init.service - diff --git a/barco@.service b/barco@.service deleted file mode 100644 index 99f9c39..0000000 --- a/barco@.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Barco projector control -After=multi-user.target - -[Service] -ExecStart=/usr/bin/poetry run python3 /home/kat/pyServices/fri-mm-maline/barco_G62_control.py %i -Type=simple -Restart=always -WorkingDirectory=/home/kat/pyServices/fri-mm-maline -User=kat -Group=kat -RestartSec=10 - -[Install] -WantedBy=multi-user.target -DefaultInstance=main \ No newline at end of file diff --git a/controller/Pipfile b/controller/Pipfile new file mode 100644 index 0000000..645a67e --- /dev/null +++ b/controller/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/controller/Pipfile.lock b/controller/Pipfile.lock new file mode 100644 index 0000000..b6df5da --- /dev/null +++ b/controller/Pipfile.lock @@ -0,0 +1,20 @@ +{ + "_meta": { + "hash": { + "sha256": "702ad05de9bc9de99a4807c8dde1686f31e0041d7b5f6f6b74861195a52110f5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": {} +} diff --git a/controller/barco_telnet/barco_G62_control.py b/controller/barco_telnet/barco_G62_control.py index 14031c1..b2d6976 100644 --- a/controller/barco_telnet/barco_G62_control.py +++ b/controller/barco_telnet/barco_G62_control.py @@ -1,114 +1,122 @@ import asyncio -import socket +from collections import defaultdict import aiomqtt import telnetlib3 import toml -import sys +import sys +import os -#GLOBALS +#GLOBALS room: str barcoPosition: str -barcoIP: str -telnetPort: int -mqttPort: int -mqttIP: str barcoReached: bool +lastState = defaultdict(lambda: None) + cmdMap = { 'power': 'POWR', 'shutter': 'PMUT', - 'freeze': 'FRZE' + 'freeze': 'FRZE', + #'test_pattern': 'TPRN', } reverseCmdMap = {v: k for k, v in cmdMap.items()} +# There needs to be a minimum time between writes. Since we have two "threads", we use a lock and a sleep in barco_send() to enforce it +lock = asyncio.Lock() def parse_barco_response(raw: str): - global room, barcoPosition, barcoReached raw = raw[1:-1] # strip square brackets - #print(raw) + if raw.startswith("ERR"): - print("Projector" + room + " " + barcoPosition + " returned" + raw) + print("ERROR:", raw) return None # TODO parse type error - "disabled control" is special case which shouldnt normally happen + cmd, status = raw.split("!", maxsplit=2) - #print(cmd + " " + status) + cmd = reverseCmdMap[cmd] - status = '1' if status == '01' else '0' + status = int(status) + barcoReached = True return cmd, status -async def barco_telnet_command(client, writer, select: str): - global room - onSub = f"{room}/projectors/{select}/#" - #print('TEST', onSub) - onMatch = f"{room}/projectors/{select}/command/+" #TODO should be set? - await client.subscribe(onSub) - #async with client.messages as msgs: - async for mesg in client.messages: - # print(mesg.topic.value) - # print(mesg.payload.decode()) - # print('on', select) +async def barco_send(writer, value): + async with lock: + writer.write(value + '\r\n') + print("Writing", value) + await asyncio.sleep(0.2) # sleep between writes necessary, otherwise it gets confused. + +async def barco_telnet_command(client, writer, select: str): + """Receive commands from MQTT and send them to the projector""" + await client.subscribe(f"{room}/projectors/{select}/#") + + async for mesg in client.messages: + + if mesg.topic.matches(f"{room}/projectors/{select}/set/+"): + cmd = mesg.topic.value.rsplit("/", maxsplit=1)[-1] + val = mesg.payload.decode() + + if val not in ("0", "1") or cmd not in cmdMap: + print("INVALID COMMAND OR VALUE:", cmd, val) + continue + + barcoCmd = cmdMap[cmd] + # Send command to projector + await barco_send(writer, f"[{barcoCmd}{val}]") + # Immediately ask for a status + await barco_send(writer, f"[{barcoCmd}?]") - if mesg.topic.matches(onMatch): - # print("test") - cmd = mesg.topic.value.split("/")[-1] - #val = '1' if mesg.payload.decode() == 'ON' else '0' - val = '1' if mesg.payload.decode() == '1' else '0' # refactor to direct 0 and 1 - barcoCmd = f"[{cmdMap[cmd]}{val}]" - print("Received: [" + mesg.topic.value + "] payload: [" + mesg.payload.decode() + "]") - print("Sending command to Barco: " + barcoCmd) - writer.write(barcoCmd) - async def barco_telnet_read_status(client, reader, select: str): - global room + """Read status reports (we trigger them in the polling task as well as whenver sending a command)""" while True: output = await reader.readuntil(b']') - raw_response: str = output.decode().strip() # strip not necessary? needed for local netcat testing though + raw_response: str = output.decode() print("Received: " + raw_response + " from Barco (" + select + ')') - parsed = parse_barco_response(raw_response) - if parsed is None: - continue #TODO alert for errors - print(f"Updating topic [{parsed[0]}] with value [{parsed[1]}]") - await client.publish(f"{room}/projectors/{select}/status/{parsed[0]}", payload=parsed[1]) + + try: + key, val = parse_barco_response(raw_response) + except: + print("NOT PARSED:", raw_response) + continue + + await client.publish(f"{room}/projectors/{select}/status/{key}", payload=val) async def barco_telnet_query_status(writer, select: str): + """Periodically ask the projector for its status""" while True: - for val in cmdMap.values(): - print(f"Querying Barco {select} with: [{val}?]") - writer.write(f"[{val}?]" + '\r\n') # TODO test if funny CRLF necessary (it probably gets ignored) - await asyncio.sleep(0.2) # sleep between writes necessary, otherwise it gets confused. - # simultaneous commands from control could break this? TODO fix later - await asyncio.sleep(10) # TODO find appropriate period + # Most queries only work when turned on, so if we're not sure, only query power + if lastState[cmdMap["power"]] == "01": + queries = cmdMap.values() + else: + queries = [cmdMap["power"]] + for val in queries: + await barco_send(writer, f"[{val}?]") -# async def shell(reader, writer): -# async with aiomqtt.Client('localhost', 1883) as client: -# task_status_query = asyncio.create_task(barco_telnet_query_status(writer)) -# task_status_reader = asyncio.create_task(barco_telnet_read_status(client, reader)) -# task_control = asyncio.create_task(barco_telnet_command(client, writer)) -# await asyncio.gather(task_status_query, task_status_reader, task_control) + await asyncio.sleep(2) # TODO find appropriate period async def main(): - global room, barcoReached - global barcoPosition, barcoIP, telnetPort, mqttIP, mqttPort + global room, barcoReached, barcoPosition if len(sys.argv) < 2: sys.exit("No position provided") else: barcoPosition = sys.argv[1] - conf = toml.load('./malinaConfig.toml') + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + g62Barcos = {k: v for k,v in conf["barco_G62"].items()} currentBarco = g62Barcos[barcoPosition] barcoIP = currentBarco['ip'] telnetPort = int(currentBarco["port"]) - room = conf["global"]["room"] - mqttPort = int(conf["global"]["mqttPort"]) - mqttIP = conf["global"]["mqttIp"] barcoReached = False try: barcoReader, barcoWriter = await telnetlib3.open_connection(barcoIP, telnetPort) @@ -116,8 +124,8 @@ async def main(): except Exception as e: print("Connection failed: " + barcoIP + ": " + str(e)) barcoReached = False - finally: - async with aiomqtt.Client(mqttIP, mqttPort) as client: + else: + async with aiomqtt.Client(mqttHost, mqttPort) as client: task_status_query_barco = asyncio.create_task(barco_telnet_query_status(barcoWriter, barcoPosition)) task_status_reader_barco = asyncio.create_task(barco_telnet_read_status(client, barcoReader, barcoPosition)) task_control_barco = asyncio.create_task(barco_telnet_command(client, barcoWriter, barcoPosition)) @@ -125,18 +133,6 @@ async def main(): await asyncio.gather(task_status_query_barco, task_status_reader_barco, task_control_barco) await client.publish(f"{room}/projectors/{barcoPosition}/error", payload=("UNREACHABLE" if not barcoReached else "OK")) -### fuj to, ne tk delat -# if __name__ == '__main__': - -# loop = asyncio.get_event_loop() -# coro = telnetlib3.open_connection(mainBarcoIP, 3023, shell=shell) -# coro = telnetlib3.open_connection(mainBarcoIP, 3023, shell=shell) -# # coro = telnetlib3.open_connection('localhost', 1234, shell=shell) -# reader, writer = loop.run_until_complete(coro) -# loop.run_until_complete(writer.protocol.waiter_closed) - -#mqttIP = sys.argv[1] -#barcoIP = sys.argv[2] - -asyncio.run(main()) \ No newline at end of file +if __name__ == '__main__': + asyncio.run(main()) diff --git a/controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py b/controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py index 52a8180..ae71d7e 100644 --- a/controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py +++ b/controller/extron_audio_matrix/extron_audio_matrix_telnet_control.py @@ -47,12 +47,17 @@ async def shell(reader, writer): async with aiomqtt.Client('localhost', 1883) as client: task_status_query = asyncio.create_task(extron_audio_telnet_status(client, writer, reader)) task_control = asyncio.create_task(extron_audio_telnet_control(client, writer)) - await asyncio.gather(task_control) + await asyncio.gather(task_status_query, task_control) if __name__ == '__main__': + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + loop = asyncio.get_event_loop() - #coro = telnetlib3.open_connection('localhost', 1234, shell=shell) coro = telnetlib3.open_connection('192.168.192.14', 23, shell=shell) reader, writer = loop.run_until_complete(coro) loop.run_until_complete(writer.protocol.waiter_closed) diff --git a/controller/lucke/luckeControl.py b/controller/lucke/luckeControl.py index 1d4a2f8..01a46da 100644 --- a/controller/lucke/luckeControl.py +++ b/controller/lucke/luckeControl.py @@ -4,47 +4,94 @@ import aiomqtt import asyncio import toml import aiohttp +import os -lucke_bearer_token = "" #TODO only set types -room = "" -url = "" -roomId = 0 +lucke_bearer_token: str +room: str +url: str +roomId: int|str async def sendSceneRequest(client, scene): - global roomId - endpoint = url.format(roomId=roomId, sceneId=scene) - # Content-Type needs to be JSON, but the content itself is ignored - async with aiohttp.request("POST", endpoint, headers={"Authorization": "Bearer " + lucke_bearer_token}, json={}) as resp: - #if 204 change was made - if resp.status != 204: + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/scenes/{scene}/activate" + async with aiohttp.request("GET", endpoint, headers={"Authorization": "Bearer " + lucke_bearer_token}) as resp: + if resp.status == 204: + await client.publish(f'{room}/lucke/preset/current', payload=scene, qos=1, retain=True) + else: print(resp.status, await resp.text()) - await client.publish(f'{room}/lucke/preset/current', payload=scene, qos=1, retain=True) +async def setLight(client, lightNum, intensity: int): + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/lights/{lightNum}" + async with aiohttp.request("PUT", endpoint, headers={"Authorization": "Bearer " + lucke_bearer_token}, json={ + "stateOn": intensity != 0, + "dimmValue": intensity + }) as resp: + if resp.status == 204: + await client.publish(f'{room}/lucke/brightness/{lightNum}', payload=intensity, qos=1, retain=True) + else: + print("setLight error:", resp.status, await resp.text()) async def task_luckeCommand(controlClient): - await controlClient.subscribe(f"{room}/lucke/preset/recall") - msgs = controlClient.messages - async for mesg in msgs: + await controlClient.subscribe(f"{room}/lucke/#") + + async for mesg in controlClient.messages: mesg: aiomqtt.Message - msgTopicArr = mesg.topic.value.split('/') - sceneNum = mesg.payload.decode() - print("Received: " + str(msgTopicArr) + " payload: [" + sceneNum + "]") - #print('sending request to endpoint..') - await sendSceneRequest(controlClient, sceneNum) + + if mesg.topic.matches(f"{room}/lucke/preset/recall"): + sceneNum = mesg.payload.decode() + print("Received: " + str(mesg.topic) + " payload: [" + sceneNum + "]") + await sendSceneRequest(controlClient, sceneNum) + + elif mesg.topic.matches(f"{room}/lucke/set/+"): + lightNum = mesg.topic.value.rsplit("/", maxsplit=1)[-1] + try: + intensity = int(mesg.payload.decode()) + assert 0 <= intensity <= 100 + except: + print("Invalid message:", mesg) + else: + await setLight(controlClient, lightNum, intensity) + await asyncio.sleep(0.01) +async def task_luckePoll(client): + """Polls the API and sends light brightness to the API""" + while True: + endpoint = url + f"/rest/v2/fri-fkkt/lecture-halls/{roomId}/lights/" + async with aiohttp.ClientSession() as session: + async with session.get(endpoint, headers={"Authorization": f"Bearer {lucke_bearer_token}"}) as response: + if response.status == 200: + lights = await response.json() + for light in lights: + await client.publish(f'{room}/lucke/is_dimmable/{light["id"]}', payload=light["dimmable"], qos=1, retain=True) + # TODO: find a better way to handle non-dimmable lights + if light["stateOn"]: + dimValue = light.get("dimmValue", 100) + else: + dimValue = 0 + await client.publish(f'{room}/lucke/brightness/{light["id"]}', payload=dimValue, qos=1, retain=True) + + else: + print(f"Failed to fetch lights: {response.status}", await response.text()) + await asyncio.sleep(.5) + async def main(): global room, lucke_bearer_token, url, roomId - conf = toml.load('./malinaConfig.toml') - room = conf.get("global")['room'] #TODO use brackets everywhere (also other files) - url = conf.get("lucke")['url'] - roomId = conf.get("lucke")['roomId'] - lucke_bearer_token = conf.get("lucke")['bearer_token'] - async with aiomqtt.Client('localhost', 1883) as client: + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + + url = conf["lucke"]['url'] + roomId = conf["lucke"]['roomId'] + lucke_bearer_token = conf["lucke"]['bearer_token'] + + async with aiomqtt.Client(mqttHost, mqttPort) as client: task_control = asyncio.create_task(task_luckeCommand(client)) + task_control = asyncio.create_task(task_luckePoll(client)) await asyncio.gather(task_control) if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/controller/malinaConfig.toml b/controller/malinaConfig.toml new file mode 100644 index 0000000..014f231 --- /dev/null +++ b/controller/malinaConfig.toml @@ -0,0 +1,17 @@ +[global] +mqttHost = '192.168.192.42' +mqttPort = 1883 +room = 'P01' + +[lucke] +url = 'http://192.168.190.90' +roomId = 1 +bearer_token = '0954afe1-4111-4f89-a123-fea08a55dc46' + +[barco_G62.main] +ip = '192.168.192.12' +port = 3023 + +[barco_G62.side] +ip = '192.168.192.13' +port = 3023 diff --git a/controller/projector_motors/projector_motors.py b/controller/projector_motors/projector_motors.py index 67ae7c8..4a4a48b 100644 --- a/controller/projector_motors/projector_motors.py +++ b/controller/projector_motors/projector_motors.py @@ -1,30 +1,23 @@ -#import gpiozero.pins.mock -#from gpiozero import * -#from grove.factory import Factory -#from grove.grove_relay import GroveRelay +MOCK = False import aiomqtt import asyncio -#from i2cpy import I2C from smbus2 import SMBus import toml -#import i2cpy -#i2cset -y 1 0x11 0x11 0x42 -#set i2c address from 0x11 to 0x42 +import os -# ONLY FOR TESTING ON NON RPI -#Device.pin_factory = gpiozero.pins.mock.MockFactory() - -#relays = [LED(17), LED(27), LED(22), LED(23), LED(5), LED(6), LED(24), LED(25)] -main_I2C_addr = 0 #default -side_I2C_addr = 0 #TODO get actual projector motor things from config (dont hardcode main/side) -room = "" -use_offset = False +room: str +use_offset = False """ 0 1 2 3 """ relayMasks = [0b0001, 0b0010, 0b0100, 0b1000] #probably ne rabim bus = SMBus(1) +i2c_map = { + 'main': -1, + 'side': -1, +} + relMapping = { 'service_down': 0, 'service_up': 1, @@ -38,36 +31,26 @@ currentState = { } async def msgRelayBoard(projSelect, command, state: bool): - #i2cAddr = relayBoardMain if projSelect == 'glavni' else relayBoardSide - #TODO this is not optimal, check for more crap - - # register 0x10 za releje - - I2CAddr = 0 #glavni - match projSelect: - case 'main': - I2CAddr = main_I2C_addr - case 'side': - I2CAddr = side_I2C_addr - #return #TODO TEMPORARY, REMOVE LATER# - case default: - return #ignore if unknown position + # Select the correct relay board + i2c_addr = i2c_map[projSelect] + # Get the relay position for the given command maskShift = relMapping[command] + # Set the corresponding bit mask = (1 << maskShift) if state: currentState[projSelect] = currentState[projSelect] | mask else: currentState[projSelect] = currentState[projSelect] & ~mask - - bus.write_byte_data(I2CAddr, 0x10, currentState[projSelect]) - print("Command sent") - #print('testirovano jako') + + # Write it to the I2C bus (0x10 is the register for relay states) + bus.write_byte_data(i2c_addr, 0x10, currentState[projSelect]) + print(projSelect, "{:04b}".format(currentState[projSelect])) """ SrvDwn SrvUp OpDwn OpUp -MAIN: 0x42 0001 0010 0100 1000 +MAIN: 0x42 0001 0010 0100 1000 SIDE: 0x43 0001 0010 0100 1000 """ @@ -81,45 +64,84 @@ SIDE: 4 5 6 7 #dej like bolš to podukumentiraj or smth async def task_command2relays(controlClient: aiomqtt.Client): - global room - #relayCtrl = lambda cmd, relay: relays[relay].on() if cmd == 1 else relays[relay].off() - #relayCtrl = lambda cmd, relay: [relays[r].on() if cmd == 1 and r == relay else relays[r].off for r in range(len(relays))] - - relayCtrl = lambda x, y: print(x, y) + """Read commands from MQTT and send them to the relays""" + await controlClient.subscribe(f"{room}/projectors/+/lift/#") - msgs = controlClient.messages - async for mesg in msgs: - mesg: aiomqtt.Message - if mesg.topic.matches(f'{room}/projectors/+/lift/move/+'): - msgTopicArr = mesg.topic.value.split('/') - state = mesg.payload.decode() - print("Received: " + str(msgTopicArr) + " payload: [" + state + "]") - #testCase = (msgTopicArr[2], msgTopicArr[4]) - projSel = msgTopicArr[2] #TODO projselect odzadaj indexed (just works tm) - if projSel != 'main' and projSel != 'side': - continue #TODO error hnadling? - command = msgTopicArr[5] #TODO same - await msgRelayBoard(projSel, command, state == '1') - await controlClient.publish(f'{room}/projectors/{projSel}/lift/status', payload="", qos=1, retain=True) + + async for mesg in controlClient.messages: + msgTopicArr = mesg.topic.value.split('/') + value = mesg.payload.decode() + + if mesg.topic.matches(f'{room}/projectors/+/lift/manual/+'): + command = msgTopicArr[-1] + projSel = msgTopicArr[-4] + + if projSel not in ("main", "side") or command not in relMapping.keys() or value not in ("0", "1"): + print("Invalid manual command:", projSel, command, value) + continue + + await msgRelayBoard(projSel, command, value == '1') + + # Service move + if "service" in command: + # Manual control makes the position unknown, so we clear it + if value == "1": + status = "MOVING" + else: + status = "STOPPED" + await controlClient.publish(f'{room}/projectors/{projSel}/lift/status', payload=status, qos=1, retain=True) + # Normal move + else: + # HACK: if the press is too short it doesn't register, so we sleep for a bit + if value == "1": + await asyncio.sleep(.2) + #print("Pushing \'off\' to other relays to prevent conflicts") - await asyncio.sleep(0.01) + + elif mesg.topic.matches(f'{room}/projectors/+/lift/goto'): + projSel = msgTopicArr[-3] + + if projSel not in ("main", "side") or value not in ("UP", "DOWN"): + print("Invalid goto command:", projSel, value) + continue + + # Clear manual control + await msgRelayBoard(projSel, "service_down", False) + await msgRelayBoard(projSel, "service_up", False) + + if value == "UP": + other = "down" + elif value == "DOWN": + other = "up" + await msgRelayBoard(projSel, other, False) + + # Click the buttom for a bit and release it, then publish that the lift has moved + await msgRelayBoard(projSel, value.lower(), True) + await asyncio.sleep(1) + await msgRelayBoard(projSel, value.lower(), False) + + await controlClient.publish(f'{room}/projectors/{projSel}/lift/status', payload=value, qos=1, retain=True) + + await asyncio.sleep(0.01) async def main(): - global main_I2C_addr, side_I2C_addr, room - conf = toml.load('./malinaConfig.toml') - projMotors = conf.get("projector_motors") - mainMotor = projMotors.get("main") - sideMotor = projMotors.get("side") - main_I2C_addr = mainMotor['i2c_address'] - side_I2C_addr = sideMotor['i2c_address'] #TODO spremen v dict + global i2c_map, room - room = conf["global"]["room"] + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) + room = conf['global']['room'] + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] - async with aiomqtt.Client('localhost', 1883) as client: + projMotors = conf["projector_motors"] + i2c_map['main'] = projMotors["main"]['i2c_address'] + i2c_map['side'] = projMotors["side"]['i2c_address'] + + async with aiomqtt.Client(mqttHost, mqttPort) as client: task_control = asyncio.create_task(task_command2relays(client)) await asyncio.gather(task_control) if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/controller/tse_serial/requirements.txt b/controller/tse_serial/requirements.txt deleted file mode 100644 index 1d9b897..0000000 --- a/controller/tse_serial/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -import asyncio -import serial -import aioserial -import aiomqtt \ No newline at end of file diff --git a/controller/tse_serial/tse_serial_controler.py b/controller/tse_serial/tse_serial_controler.py index e27cff2..c6dc014 100644 --- a/controller/tse_serial/tse_serial_controler.py +++ b/controller/tse_serial/tse_serial_controler.py @@ -4,14 +4,17 @@ import aioserial import aiomqtt from tse_serial_interpreter import * from dataclasses import dataclass -import os +import os import sys -import toml +import toml import serial.tools.list_ports -#GLOBALS - -room = 'undefined' #TODO make be do get fronm file of configuration +# GLOBALS +room: str +platnoBckgdMoving = { + 'main': False, + 'side': False, +} # serPath = '/dev/serial/by-id/' # devLst = os.listdir(serPath) @@ -20,11 +23,16 @@ room = 'undefined' #TODO make be do get fronm file of configuration # serDev = devLst[0] portList = serial.tools.list_ports.comports() -if len(portList) < 1: - sys.exit("No serial port found") -#TODO if multiple ports idk, shouldn't ever happen but still -(serport, serdesc, serhwid) = portList[0] #TODO CHECK FOR TTYUSB0 -#TODO if port provided from conf, set, otherwise autodetect magical thing just works +candidates = ('/dev/ttyUSB0', '/dev/ttyACM0', '/dev/ttyUSB1', '/dev/ttyACM1') +serport = '' +# Find first serial port that exists +for port in portList: + if port.device in candidates: + serport = port.device + break + else: + print(portList) + sys.exit("No serial port found") aser: aioserial.AioSerial = aioserial.AioSerial( port=serport, @@ -32,10 +40,8 @@ aser: aioserial.AioSerial = aioserial.AioSerial( parity=serial.PARITY_NONE, bytesize=serial.EIGHTBITS, stopbits=serial.STOPBITS_ONE - ) +) - -#TODO get this from file da ni hardcoded? # altho itak je ta script za tist specific tse box in so vsi isti mapping_toggles = { "master": 1, @@ -63,10 +69,10 @@ reverse_lookup = { 1: ("power", "master", ""), 2: ("power", "audio", ""), 3: ("power", "projectors", ""), - + 5: ("platno", "main", "MOVING_DOWN"), 6: ("platno", "main", "MOVING_UP"), - + 7: ("platno", "side", "MOVING_DOWN"), 8: ("platno", "side", "MOVING_UP"), @@ -113,7 +119,7 @@ async def executeAndPublish(mqttClient, pubTopic, pubPayload, relStat): await mqttClient.publish(pubTopic, payload=pubPayload) await asyncio.sleep(0.1) #TODO probably remove - + async def handleTsePower(client, sysSelect, cmd): rel = RelayState(mapping_toggles[sysSelect], cmd == '1') await executeAndPublish(client, f'{room}/power/{sysSelect}/status', cmd, rel) @@ -129,13 +135,8 @@ async def handleTseSencilo(client, cmd): await executeAndPublish(client, topicPub, "MOVING_DOWN", rel) else: await executeAndPublish(client, topicPub, "STOPPED", RelayState(shades_mapping['UP'], False)) - await executeAndPublish(client, topicPub, "STOPPED", RelayState(shades_mapping['DOWN'], False)) - + await executeAndPublish(client, topicPub, "STOPPED", RelayState(shades_mapping['DOWN'], False)) -platnoBckgdMoving = { - 'main': False, - 'side': False, -} # mucho importante variable prav zares dedoles async def platnoTimeout(mqttClient, pubTopic, pubPayload, relStat: RelayState, intent, select): global platnoBckgdMoving @@ -144,12 +145,10 @@ async def platnoTimeout(mqttClient, pubTopic, pubPayload, relStat: RelayState, i await executeAndPublish(mqttClient, pubTopic, intent, relStat) platnoBckgdMoving[select] = False #TODO properly document why this is here and what it does + async def handleTsePlatno(client, proj, cmdType, cmd): global platnoBckgdMoving - #topicSplit = tval.split('/') - # {room} {projectors} {[select]} {platno} {move/goto} - #projector = topicSplit[2] - #command = topicSplit[4] + pubTop = f'{room}/projectors/{proj}/platno/status' if not (proj == "main" or proj == "side"): return @@ -203,21 +202,13 @@ async def handleTsePlatno(client, proj, cmdType, cmd): print('unknown command') async def task_command2serial(controlClient: aiomqtt.Client): - #print('oge') - await controlClient.subscribe(f"{room}/#") - #print('ogee') - #async with controlClient.messages as msgs: - async for mesg in controlClient.messages: - #print('oge') - #print(mesg, mesg.topic) - #mesg: aiomqtt.Message + async for mesg in controlClient.messages: topicVal = mesg.topic.value msgTopic = mesg.topic.value.split('/') cmnd = mesg.payload.decode() - #print('Received on: ', topicVal, ' with:', cmnd) - #print('bfr') + if mesg.topic.matches(f'{room}/projectors/+/platno/move') or mesg.topic.matches(f'{room}/projectors/+/platno/goto'): proj = msgTopic[-3] cmdType = msgTopic[-1] # move / goto @@ -236,7 +227,7 @@ async def task_command2serial(controlClient: aiomqtt.Client): await handleTseSencilo(controlClient, cmnd) else: - continue + continue # code after if block doesnt execute in this case #print("after") await asyncio.sleep(0.01) #TODO do we need this? (probably) @@ -244,12 +235,17 @@ async def task_command2serial(controlClient: aiomqtt.Client): async def main(): global room - conf = toml.load('./malinaConfig.toml') + + config_file = os.getenv('MM_CONFIG_PATH', './malinaConfig.toml') + conf = toml.load(config_file) room = conf['global']['room'] - async with aiomqtt.Client('localhost', 1883) as client: #TODO omehčaj kodiranje + mqttHost = conf['global']['mqttHost'] + mqttPort = conf['global']['mqttPort'] + + async with aiomqtt.Client(mqttHost, mqttPort) as client: task_status = asyncio.create_task(task_status2mqtt(client)) task_control = asyncio.create_task(task_command2serial(client)) await asyncio.gather(task_status, task_control) if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/docs/Lučke.md b/docs/Lučke.md new file mode 100644 index 0000000..7d64ba5 --- /dev/null +++ b/docs/Lučke.md @@ -0,0 +1,15 @@ +Lučke v velikih predavalnicah +=== + +## Mapping po predavalnicah + +### P01 + + - 1 - Neonke tabla + - 2 - Neonke sredina + - 3 - Neonke vrata + - 4 - Okrogle 2 (vse razen ene stropne luči zgoraj) + - 5 - Neonke začetek + - 6 - Reflektorji 2 (ne dela) + - 7 - Okrogle 1 (ena stropna luč zgoraj) + - 8 - Stopnice diff --git a/eth0.notwork b/eth0.notwork deleted file mode 100644 index 7f7c756..0000000 --- a/eth0.notwork +++ /dev/null @@ -1,8 +0,0 @@ -[Match] - -Name=eth0 - -[Network] -Address=192.168.192.42 -Gateway=192.168.192.1 - diff --git a/extron_audio.service b/extron_audio.service deleted file mode 100644 index fb6ae80..0000000 --- a/extron_audio.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Extron audio matrix control -After=multi-user.target - -[Service] -ExecStart=/usr/bin/python3 /home/rpi/extron_audio_matrix_telnet_control.py -Type=simple -Restart=always -User=kat -Group=kat - -[Install] -WantedBy=multi-user.target diff --git a/frontend/vju_display/README.md b/frontend/vju_display/README.md index 3c39275..94a63a9 100644 --- a/frontend/vju_display/README.md +++ b/frontend/vju_display/README.md @@ -1,19 +1,3 @@ -# vju_display - -This template should help get you started developing with Vue 3 in Vite. - -## Recommended IDE Setup - -[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). - -## Type Support for `.vue` Imports in TS - -TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. - -## Customize configuration - -See [Vite Configuration Reference](https://vite.dev/config/). - ## Project Setup ```sh diff --git a/frontend/vju_display/index.html b/frontend/vju_display/index.html index a888544..3647951 100644 --- a/frontend/vju_display/index.html +++ b/frontend/vju_display/index.html @@ -4,7 +4,7 @@ -
?room=P01
to the URL