diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a602cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*~ + diff --git a/cassini.py b/cassini.py index f42e6e3..1b75040 100755 --- a/cassini.py +++ b/cassini.py @@ -9,6 +9,7 @@ import asyncio import logging import random from simple_mqtt_server import SimpleMQTTServer +from simple_http_server import SimpleHTTPServer logging.basicConfig( level=logging.INFO, @@ -18,11 +19,12 @@ logging.basicConfig( SATURN_BROADCAST_PORT = 3000 +SATURN_STATUS_PRINTING = 4 SATURN_STATUS_COMPLETE = 16 # ?? SATURN_CMD_0 = 0 # null data SATURN_CMD_1 = 1 # null data -SATURN_CMD_SET_REPORT_TIME_PERIOD = 512 +SATURN_CMD_SET_MYSTERY_TIME_PERIOD = 512 # "TimePeriod": 5000 SATURN_CMD_START_PRINTING = 128 # "Filename": "X", "StartLayer": 0 SATURN_CMD_UPLOAD_FILE = 256 # "Check": 0, "CleanCache": 1, "Compress": 0, "FileSize": 3541068, "Filename": "_ResinXP2-ValidationMatrix_v2.goo", "MD5": "205abc8fab0762ad2b0ee1f6b63b1750", "URL": "http://${ipaddr}:58883/f60c0718c8144b0db48b7149d4d85390.goo" }, SATURN_CMD_DISCONNECT = 64 # Maybe disconnect? @@ -36,115 +38,118 @@ def handle_exception(loop, context): name = context.get("future").get_coro().__name__ logging.error(f"Caught exception from {name}: {msg}") -def find_printers_on(iface): - #netaddr = netifaces.ifaddresses(iface) - #if not netaddr.has_key(netifaces.AF_INET): - # print('No IPv4 address found for interface {}'.format(iface)) - # sys.exit(1) - #broadcast = netaddr[netifaces.AF_INET][0]['broadcast'] +class SaturnPrinter: + def __init__(self, addr, desc): + self.addr = addr + self.desc = desc - # create UDP socket and send broadcast - printers = [] - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with sock: - sock.settimeout(PRINTER_SEARCH_TIMEOUT) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.sendto(b'M99999', ('', SATURN_BROADCAST_PORT)) + # Class method: UDP broadcast search for all printers + def find_printers(timeout=1): + printers = [] + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + with sock: + sock.settimeout(PRINTER_SEARCH_TIMEOUT) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, timeout) + sock.sendto(b'M99999', ('', SATURN_BROADCAST_PORT)) - now = time.time() - while True: - if time.time() - now > PRINTER_SEARCH_TIMEOUT: - break - try: - data, addr = sock.recvfrom(1024) - except socket.timeout: - continue - else: - print(f'Found printer at {addr}') - pdata = json.loads(data.decode('utf-8')) - pdata['addr'] = addr - printers.append(pdata) - return printers + now = time.time() + while True: + if time.time() - now > timeout: + break + try: + data, addr = sock.recvfrom(1024) + except socket.timeout: + continue + else: + print(f'Found printer at {addr}') + pdata = json.loads(data.decode('utf-8')) + printers.append(SaturnPrinter(addr, pdata)) + return printers + + # Tell this printer to connect to the given mqtt server + def connect(self, mqtt, http): + self.mqtt = mqtt + self.http = http + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + with sock: + sock.sendto(b'M66666 ' + str(mqtt.port).encode('utf-8'), self.addr) + + def describe(self): + attrs = self.desc['Data']['Attributes'] + return f"{attrs['Name']} ({attrs['MachineName']})" -def connect_printer(printer, srvport): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with sock: - sock.sendto(b'M66666 ' + str(srvport).encode('utf-8'), printer['addr']) + def send_command(self, cmdid, data=None): + # generate 16-byte random identifier as a hex string + hexstr = '%032x' % random.getrandbits(128) + timestamp = int(time.time() * 1000) + mainboard = self.desc['Data']['Attributes']['MainboardID'] + cmd_data = { + "Data": { + "Cmd": cmdid, + "Data": data, + "From": 0, + "MainboardID": mainboard, + "RequestID": hexstr, + "TimeStamp": timestamp + }, + "Id": self.desc['Id'] + } + print("SENDING REQUEST: " + json.dumps(cmd_data)) + self.mqtt.outgoing_messages.put_nowait({'topic': '/sdcp/request/' + mainboard, 'payload': json.dumps(cmd_data)}) -async def create_server(): +async def create_mqtt_server(): mqtt = SimpleMQTTServer('0.0.0.0', 0) await mqtt.start() - logging.info(f"Server created, port {mqtt.port}") - return mqtt + logging.info(f"MQTT Server created, port {mqtt.port}") + mqtt_server_task = asyncio.create_task(mqtt.serve_forever()) + return mqtt, mqtt.port, mqtt_server_task -async def serve_forever(mqtt): - loop = asyncio.get_running_loop() - loop.set_exception_handler(handle_exception) - await mqtt.serve_forever() - -async def find_printers(): - printers = find_printers_on('en0') - if len(printers) == 0: - print('No printers found') - sys.exit(1) - for i, p in enumerate(printers): - attrs = p['Data']['Attributes'] - #print('{}: {} ({})'.format(i, attrs['Name'], attrs['MachineName'])) - return printers - -async def printer_setup(mqtt_port): - PRINTERS = await find_printers() - printer = PRINTERS[0] - connect_printer(printer, mqtt_port) - return printer +async def create_http_server(): + http = SimpleHTTPServer('0.0.0.0', 0) + await http.start() + logging.info(f"HTTP Server created, port {http.port}") + http_server_task = asyncio.create_task(http.serve_forever()) + return http, http.port, http_server_task def print_printer_status(printers): for i, p in enumerate(printers): - attrs = p['Data']['Attributes'] - status = p['Data']['Status'] + attrs = p.desc['Data']['Attributes'] + status = p.desc['Data']['Status'] printInfo = status['PrintInfo'] print(f"{i}: {attrs['Name']} ({attrs['MachineName']})") print(f" Status: {printInfo['Status']} Layers: {printInfo['CurrentLayer']}/{printInfo['TotalLayer']}") -def send_printer_command(mqtt, printer, cmdid, data=None): - # generate 16-byte random identifier as a hex string - hexstr = '%032x' % random.getrandbits(128) - timestamp = int(time.time() * 1000) - mainboard = printer['Data']['Attributes']['MainboardID'] - cmd_data = { - "Data": { - "Cmd": cmdid, - "Data": json.dumps(data) if data is not None else "null", - "From": 0, - "MainboardID": mainboard, - "RequestID": hexstr, - "TimeStamp": timestamp - }, - "Id": printer['Id'] - } - print(json.dumps(cmd_data)) - mqtt.outgoing_messages.put_nowait({'topic': '/sdcp/request/' + mainboard, 'payload': json.dumps(cmd_data)}) - async def main(): cmd = None + printers = SaturnPrinter.find_printers() + + if len(printers) == 0: + print("No printers found") + return + if len(sys.argv) > 1: cmd = sys.argv[1] + if cmd == 'status': - printers = await find_printers() print_printer_status(printers) - else: - mqtt = await create_server() - server_task = asyncio.create_task(serve_forever(mqtt)) - printer = await printer_setup(mqtt.port) - await asyncio.sleep(3) - send_printer_command(mqtt, printer, 0) - await asyncio.sleep(1) - send_printer_command(mqtt, printer, 1) - await asyncio.sleep(1) - send_printer_command(mqtt, printer, 512, { "TimePeriod": 5000 }) + return + + # Spin up our private mqtt server + mqtt, mqtt_port, mqtt_task = await create_mqtt_server() + http, http_port, http_task = await create_http_server() + + printer = printers[0] + printer.connect(mqtt, http) + + await asyncio.sleep(3) + printer.send_command(SATURN_CMD_0) + await asyncio.sleep(1) + printer.send_command(SATURN_CMD_1) + await asyncio.sleep(1) + printer.send_command(SATURN_CMD_SET_MYSTERY_TIME_PERIOD, { 'TimePeriod': 5 }) + await asyncio.sleep(1000) #printer_task = asyncio.create_task(printer_setup(mqtt.port)) - await asyncio.sleep(1000) #while True: # if server_task is not None and server_task.done(): # print("Server task done") diff --git a/simple_http_server.py b/simple_http_server.py new file mode 100644 index 0000000..59a6228 --- /dev/null +++ b/simple_http_server.py @@ -0,0 +1,67 @@ +import asyncio +import os +import hashlib + +class SimpleHTTPServer: + def __init__(self, host="127.0.0.1", port=0): + self.host = host + self.port = port + self.server = None + self.routes = {} + + def register_file_route(self, path, filename): + size = os.path.getsize(filename) + md5 = hashlib.md5() + with open(filename, 'rb') as f: + while True: + data = f.read(1024) + if not data: + break + md5.update(data) + route = { 'file': filename, 'size': size, 'md5': md5.hexdigest() } + self.routes[path] = route + return route + + async def start(self): + self.server = await asyncio.start_server(self.handle_client, self.host, self.port) + self.port = self.server.sockets[0].getsockname()[1] + + async def serve_forever(self): + await self.server.serve_forever() + + async def handle_client(self, reader, writer): + data = b'' + while True: + data += await reader.read(1024) + if b'\r\n\r\n' in data: + break + + request_line = data.decode().splitlines()[0] + method, path, _ = request_line.split() + + if path not in self.routes: + writer.write("HTTP/1.1 404 Not Found\r\n".encode()) + writer.close() + return + + route = self.routes[path] + header = f"HTTP/1.1 200 OK\r\n" + header += f"Content-Type: application/octet-stream\r\n" + header += f"Etag: {route['md5']}\r\n" + header += f"Content-Length: {path['size']}\r\n" + + writer.write(header.encode()) + + if method == "GET": + writer.write(b'\r\n') + with open(route['file'], 'rb') as f: + while True: + data = f.read(8192) + if not data: + break + writer.write(data) + + await writer.drain() + writer.close() + await writer.wait_closed() + diff --git a/simple_mqtt_server.py b/simple_mqtt_server.py index 0041cf7..165e4de 100644 --- a/simple_mqtt_server.py +++ b/simple_mqtt_server.py @@ -181,6 +181,3 @@ class SimpleMQTTServer: self.next_pack_id_value += 1 return pack_id -if __name__ == "__main__": - server = SimpleMQTTServer() - asyncio.run(server.run())