Cleanup
This commit is contained in:
97
cassini.py
97
cassini.py
@@ -1,5 +1,12 @@
|
|||||||
#!env python3
|
#!env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Cassini
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 Vladimir Vukicevic
|
||||||
|
# License: MIT
|
||||||
|
#
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
@@ -10,93 +17,14 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from simple_mqtt_server import SimpleMQTTServer
|
from simple_mqtt_server import SimpleMQTTServer
|
||||||
from simple_http_server import SimpleHTTPServer
|
from simple_http_server import SimpleHTTPServer
|
||||||
|
from saturn_printer import SaturnPrinter
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.DEBUG, # .INFO
|
||||||
format="%(asctime)s,%(msecs)d %(levelname)s: %(message)s",
|
format="%(asctime)s,%(msecs)d %(levelname)s: %(message)s",
|
||||||
datefmt="%H:%M:%S",
|
datefmt="%H:%M:%S",
|
||||||
)
|
)
|
||||||
|
|
||||||
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_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?
|
|
||||||
|
|
||||||
PRINTERS = []
|
|
||||||
|
|
||||||
PRINTER_SEARCH_TIMEOUT = 1
|
|
||||||
|
|
||||||
def handle_exception(loop, context):
|
|
||||||
msg = context.get("exception", context["message"])
|
|
||||||
name = context.get("future").get_coro().__name__
|
|
||||||
logging.error(f"Caught exception from {name}: {msg}")
|
|
||||||
|
|
||||||
class SaturnPrinter:
|
|
||||||
def __init__(self, addr, desc):
|
|
||||||
self.addr = addr
|
|
||||||
self.desc = desc
|
|
||||||
|
|
||||||
# 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', ('<broadcast>', SATURN_BROADCAST_PORT))
|
|
||||||
|
|
||||||
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 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_mqtt_server():
|
async def create_mqtt_server():
|
||||||
mqtt = SimpleMQTTServer('0.0.0.0', 0)
|
mqtt = SimpleMQTTServer('0.0.0.0', 0)
|
||||||
await mqtt.start()
|
await mqtt.start()
|
||||||
@@ -127,6 +55,11 @@ async def main():
|
|||||||
print("No printers found")
|
print("No printers found")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if len(printers) > 1:
|
||||||
|
print("More than 1 printer found.")
|
||||||
|
print("Usage --printer argument to specify the ID. [TODO]")
|
||||||
|
return
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
cmd = sys.argv[1]
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
@@ -134,7 +67,7 @@ async def main():
|
|||||||
print_printer_status(printers)
|
print_printer_status(printers)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Spin up our private mqtt server
|
# Spin up our private servers
|
||||||
mqtt, mqtt_port, mqtt_task = await create_mqtt_server()
|
mqtt, mqtt_port, mqtt_task = await create_mqtt_server()
|
||||||
http, http_port, http_task = await create_http_server()
|
http, http_port, http_task = await create_http_server()
|
||||||
|
|
||||||
|
115
saturn_printer.py
Normal file
115
saturn_printer.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#
|
||||||
|
# Cassini
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 Vladimir Vukicevic
|
||||||
|
# License: MIT
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
|
||||||
|
SATURN_BROADCAST_PORT = 3000
|
||||||
|
|
||||||
|
SATURN_STATUS_EXPOSURE = 2 # TODO: double check tese
|
||||||
|
SATURN_STATUS_RETRACTING = 3
|
||||||
|
SATURN_STATUS_LOWERING = 4
|
||||||
|
SATURN_STATUS_COMPLETE = 16 # ??
|
||||||
|
|
||||||
|
STATUS_NAMES = {
|
||||||
|
SATURN_STATUS_EXPOSURE: "Exposure",
|
||||||
|
SATURN_STATUS_RETRACTING: "Retracting",
|
||||||
|
SATURN_STATUS_LOWERING: "Lowering",
|
||||||
|
SATURN_STATUS_COMPLETE: "Complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
SATURN_CMD_0 = 0 # null data
|
||||||
|
SATURN_CMD_1 = 1 # null data
|
||||||
|
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?
|
||||||
|
|
||||||
|
class SaturnPrinter:
|
||||||
|
def __init__(self, addr, desc):
|
||||||
|
self.addr = addr
|
||||||
|
self.desc = desc
|
||||||
|
|
||||||
|
# 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(timeout)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, timeout)
|
||||||
|
sock.sendto(b'M99999', ('<broadcast>', SATURN_BROADCAST_PORT))
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
while True:
|
||||||
|
if time.time() - now > timeout:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
data, addr = sock.recvfrom(1024)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
logging.debug(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
|
||||||
|
|
||||||
|
mainboard = self.desc['Data']['Attributes']['MainboardID']
|
||||||
|
mqtt.add_handler("/sdcp/saturn/" + mainboard, self.incoming_data)
|
||||||
|
mqtt.add_handler("/sdcp/response/" + mainboard, self.incoming_data)
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
with sock:
|
||||||
|
sock.sendto(b'M66666 ' + str(mqtt.port).encode('utf-8'), self.addr)
|
||||||
|
|
||||||
|
def incoming_data(self, topic, payload):
|
||||||
|
if topic.startswith("/sdcp/status/"):
|
||||||
|
self.incoming_status(payload['Data']['Status'])
|
||||||
|
elif topic.startswith("/sdcp/attributes/"):
|
||||||
|
# don't think I care about attributes
|
||||||
|
pass
|
||||||
|
elif topic.startswith("/sdcp/response/"):
|
||||||
|
self.incoming_response(payload['Data']['RequestID'], payload['Data']['Cmd'], payload['Data']['Data'])
|
||||||
|
|
||||||
|
def incoming_status(self, status):
|
||||||
|
logging.info(f"STATUS: {status}")
|
||||||
|
|
||||||
|
def incoming_response(self, id, cmd, data):
|
||||||
|
logging.info(f"RESPONSE: {id} -- {cmd}: {data}")
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
attrs = self.desc['Data']['Attributes']
|
||||||
|
return f"{attrs['Name']} ({attrs['MachineName']})"
|
||||||
|
|
||||||
|
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)})
|
@@ -1,3 +1,10 @@
|
|||||||
|
#
|
||||||
|
# Cassini
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 Vladimir Vukicevic
|
||||||
|
# License: MIT
|
||||||
|
#
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@@ -1,3 +1,11 @@
|
|||||||
|
#
|
||||||
|
# Cassini
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023 Vladimir Vukicevic
|
||||||
|
# License: MIT
|
||||||
|
#
|
||||||
|
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
@@ -17,18 +25,24 @@ class SimpleMQTTServer:
|
|||||||
self.incoming_messages = asyncio.Queue()
|
self.incoming_messages = asyncio.Queue()
|
||||||
self.outgoing_messages = asyncio.Queue()
|
self.outgoing_messages = asyncio.Queue()
|
||||||
self.next_pack_id_value = 1
|
self.next_pack_id_value = 1
|
||||||
|
self.handlers = {}
|
||||||
|
|
||||||
|
def add_handler(self, topic, handler):
|
||||||
|
if topic not in self.handlers:
|
||||||
|
self.handlers[topic] = []
|
||||||
|
self.handlers[topic].append(handler)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
self.server = await asyncio.start_server(self.handle_client, self.host, self.port)
|
self.server = await asyncio.start_server(self.handle_client, self.host, self.port)
|
||||||
self.port = self.server.sockets[0].getsockname()[1]
|
self.port = self.server.sockets[0].getsockname()[1]
|
||||||
print(f'Listening on {self.server.sockets[0].getsockname()}')
|
logging.debug(f'Listening on {self.server.sockets[0].getsockname()}')
|
||||||
|
|
||||||
async def serve_forever(self):
|
async def serve_forever(self):
|
||||||
await self.server.serve_forever()
|
await self.server.serve_forever()
|
||||||
|
|
||||||
async def handle_client(self, reader, writer):
|
async def handle_client(self, reader, writer):
|
||||||
addr = writer.get_extra_info('peername')
|
addr = writer.get_extra_info('peername')
|
||||||
print(f'Socket connected from {addr}')
|
logging.debug(f'Socket connected from {addr}')
|
||||||
data = b''
|
data = b''
|
||||||
|
|
||||||
read_future = asyncio.ensure_future(reader.read(1024))
|
read_future = asyncio.ensure_future(reader.read(1024))
|
||||||
@@ -53,7 +67,7 @@ class SimpleMQTTServer:
|
|||||||
qos = subscribed_topics[topic]
|
qos = subscribed_topics[topic]
|
||||||
await self.send_msg(writer, MQTT_PUBLISH, payload=self.encode_publish(topic, payload, self.next_pack_id()))
|
await self.send_msg(writer, MQTT_PUBLISH, payload=self.encode_publish(topic, payload, self.next_pack_id()))
|
||||||
else:
|
else:
|
||||||
print(f'SEND: NOT SUBSCRIBED {topic}: {payload}')
|
logging.debug(f'SEND: NOT SUBSCRIBED {topic}: {payload}')
|
||||||
#msg = (MQTT_PUBLISH, 0, topic.encode('utf-8') + payload.encode('utf-8'))
|
#msg = (MQTT_PUBLISH, 0, topic.encode('utf-8') + payload.encode('utf-8'))
|
||||||
#await self.send_msg(writer, *msg)
|
#await self.send_msg(writer, *msg)
|
||||||
outgoing_messages_future = asyncio.ensure_future(self.outgoing_messages.get())
|
outgoing_messages_future = asyncio.ensure_future(self.outgoing_messages.get())
|
||||||
@@ -79,12 +93,12 @@ class SimpleMQTTServer:
|
|||||||
# TODO -- we could maybe not have enough bytes to decode the length, but assume
|
# TODO -- we could maybe not have enough bytes to decode the length, but assume
|
||||||
# that won't happen
|
# that won't happen
|
||||||
msg_length, len_bytes_consumed = self.decode_length(data[1:])
|
msg_length, len_bytes_consumed = self.decode_length(data[1:])
|
||||||
print(f" in msg_type: {msg_type} flags: {msg_flags} msg_length {msg_length} bytes_consumed for msg_length {len_bytes_consumed}")
|
logging.debug(f" in msg_type: {msg_type} flags: {msg_flags} msg_length {msg_length} bytes_consumed for msg_length {len_bytes_consumed}")
|
||||||
|
|
||||||
# is there enough to process the message?
|
# is there enough to process the message?
|
||||||
head_len = len_bytes_consumed + 1
|
head_len = len_bytes_consumed + 1
|
||||||
if msg_length + head_len > len(data):
|
if msg_length + head_len > len(data):
|
||||||
print("Not enough")
|
logging.debug("Not enough")
|
||||||
break
|
break
|
||||||
|
|
||||||
# pull the message payload out, and move data to next packet
|
# pull the message payload out, and move data to next packet
|
||||||
@@ -93,12 +107,16 @@ class SimpleMQTTServer:
|
|||||||
|
|
||||||
if msg_type == MQTT_CONNECT:
|
if msg_type == MQTT_CONNECT:
|
||||||
# ignore the contents of the message, should maybe check for 'MQTT' identifier at least
|
# ignore the contents of the message, should maybe check for 'MQTT' identifier at least
|
||||||
print(f"Client {addr} connected")
|
logging.info(f"Client {addr} connected")
|
||||||
await self.send_msg(writer, MQTT_CONNACK, payload=b'\x00\x00')
|
await self.send_msg(writer, MQTT_CONNACK, payload=b'\x00\x00')
|
||||||
elif msg_type == MQTT_PUBLISH:
|
elif msg_type == MQTT_PUBLISH:
|
||||||
qos = (msg_flags >> 1) & 0x3
|
qos = (msg_flags >> 1) & 0x3
|
||||||
topic, packid, content = self.parse_publish(message)
|
topic, packid, content = self.parse_publish(message)
|
||||||
print(f"{topic}: {content}")
|
|
||||||
|
logging.info(f"Got DATA on: {topic}")
|
||||||
|
if topic in self.handlers:
|
||||||
|
for handler in self.handlers[topic]:
|
||||||
|
handler(topic, content)
|
||||||
if qos > 0:
|
if qos > 0:
|
||||||
await self.send_msg(writer, MQTT_PUBACK, packet_ident=packid)
|
await self.send_msg(writer, MQTT_PUBACK, packet_ident=packid)
|
||||||
elif msg_type == MQTT_SUBSCRIBE:
|
elif msg_type == MQTT_SUBSCRIBE:
|
||||||
@@ -106,11 +124,11 @@ class SimpleMQTTServer:
|
|||||||
packid = message[0] << 8 | message[1]
|
packid = message[0] << 8 | message[1]
|
||||||
message = message[2:]
|
message = message[2:]
|
||||||
topic = self.parse_subscribe(message)
|
topic = self.parse_subscribe(message)
|
||||||
print(f"Client {addr} subscribed to topic '{topic}', QoS {qos}")
|
logging.info(f"Client {addr} subscribed to topic '{topic}', QoS {qos}")
|
||||||
subscribed_topics[topic] = qos
|
subscribed_topics[topic] = qos
|
||||||
await self.send_msg(writer, MQTT_SUBACK, packet_ident=packid, payload=bytes([qos]))
|
await self.send_msg(writer, MQTT_SUBACK, packet_ident=packid, payload=bytes([qos]))
|
||||||
elif msg_type == MQTT_DISCONNECT:
|
elif msg_type == MQTT_DISCONNECT:
|
||||||
print(f"Client {addr} disconnected")
|
logging.info(f"Client {addr} disconnected")
|
||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
return
|
return
|
||||||
@@ -124,7 +142,7 @@ class SimpleMQTTServer:
|
|||||||
if packet_ident > 0:
|
if packet_ident > 0:
|
||||||
head += bytes([packet_ident >> 8, packet_ident & 0xff])
|
head += bytes([packet_ident >> 8, packet_ident & 0xff])
|
||||||
data = head + payload
|
data = head + payload
|
||||||
print(f" writing {len(data)} bytes: {data}")
|
logging.debug(f" writing {len(data)} bytes: {data}")
|
||||||
writer.write(data)
|
writer.write(data)
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user