import asyncio from collections import defaultdict import aiomqtt import telnetlib3 import toml import sys import os #GLOBALS room: str barcoPosition: str barcoReached: bool lastState = defaultdict(lambda: None) cmdMap = { 'power': 'POWR', 'shutter': 'PMUT', '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): raw = raw[1:-1] # strip square brackets if raw.startswith("ERR"): print("ERROR:", raw) return None # TODO parse type error - "disabled control" is special case which shouldnt normally happen cmd, status = raw.split("!", maxsplit=2) cmd = reverseCmdMap[cmd] status = int(status) barcoReached = True return cmd, status 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}?]") async def barco_telnet_read_status(client, reader, select: str): """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() print("Received: " + raw_response + " from Barco (" + select + ')') 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: # 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}?]") await asyncio.sleep(2) # TODO find appropriate period async def main(): global room, barcoReached, barcoPosition if len(sys.argv) < 2: sys.exit("No position provided") else: barcoPosition = sys.argv[1] 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"]) barcoReached = False try: barcoReader, barcoWriter = await telnetlib3.open_connection(barcoIP, telnetPort) barcoReached = True except Exception as e: print("Connection failed: " + barcoIP + ": " + str(e)) barcoReached = False 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)) 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")) if __name__ == '__main__': asyncio.run(main())