862 lines
29 KiB
Python
Executable File

#!/usr/bin/env python3
from alive_progress import alive_bar
import get_specs
import traceback
#import logging
import yaml
from multiprocessing import Process, Manager, Pool, TimeoutError, active_children, log_to_stderr, Pipe, Queue
from multiprocessing.pool import Pool
import multiprocessing
from time import sleep
from util import fprint
from util import run_cmd
import sys
import ur5_control
from ur5_control import Rob
import os
import signal
import socket
from flask import Flask, render_template, request
import requests
from led_control import LEDSystem
import server
import asyncio
import json
import process_video
import search
from search import JukeboxSearch
#multiprocessing.set_start_method('spawn', True)
from pyModbusTCP.client import ModbusClient
from uptime import uptime
# set to false to run without real hardware for development
real = False
skip_scanning = True
mbconn = None
config = None
keeprunning = True
arm_ready = False
led_ready = False
camera_ready = False
sensor_ready = False
vm_ready = False
cable_search_ready = False
killme = None
#pool = None
serverproc = None
camera = None
ledsys = None
arm = None
to_server_queue = Queue()
from_server_queue = Queue()
mainloop_get = Queue()
mode = "Startup"
oldmode = "Startup"
counter = 0
jbs = None
scan_value = None
arm_state = None
cable_list = list()
parse_res = None
cable_list_state = list()
just_placed = -1
ring_animation = None
led_set_mode = None
sensors = [0,0,0,0]
websocket_process = None
def arm_start_callback(res):
fprint("Arm action complete.")
global arm_ready
arm_ready = True
def led_start_callback(res):
global led_ready
led_ready = True
global ledsys
ledsys = res
def camera_start_callback(res):
global camera_ready
camera_ready = True
global scan_value
scan_value = res
def sensor_start_callback(res):
global sensor_ready
sensor_ready = True
def vm_start_callback(res):
global vm_ready
vm_ready = True
def cable_search_callback(res):
global cable_search_ready
cable_search_ready = True
global parse_res
parse_res = res
def wait_for(val, name):
#global val
if val is False:
fprint("waiting for " + name + " to complete...")
while val is False:
sleep(0.1)
def send_data(type, call, data, client_id="*"):
out = dict()
out["type"] = type
out["call"] = call
out["data"] = data
to_server_queue.put((client_id, json.dumps(out)))
def check_server():
#print("HI")
global cable_list
global to_server_queue
global from_server_queue
global jbs
if True:
# Handeling Server Requests Loop, will run forever
if not from_server_queue.empty():
client_id, message = from_server_queue.get()
fprint(f"Message from client {client_id}: {message}")
# Message handler
try:
decoded = json.loads(message)
except:
fprint("Non-JSON message recieved")
return
if "type" not in decoded:
fprint("Missing \"type\" field.")
return
if "call" not in decoded:
fprint("Missing \"call\" field.")
return
if "data" not in decoded:
fprint("Missing \"data\" field.")
return
# if we get here, we have a "valid" data packet
data = decoded["data"]
call = decoded["call"]
try:
match decoded["type"]:
case "log":
fprint("log message")
if call == "send":
fprint("webapp: " + str(data), sendqueue=to_server_queue)
elif call == "request":
pass
case "cable_map":
fprint("cable_map message")
if call == "send":
pass
elif call == "request":
tmp = list()
for idx in range(len(cable_list)):
if cable_list[idx] is not False:
tmp1 = {"part_number": cable_list[idx], "position": idx, "name": cable_list[idx], "brand": "Belden", "description": "Blah", "short_description": "Bla"}
tmp.append(tmp1)
out = {"map": tmp}
fprint(out)
send_data(decoded["type"], "send", out, client_id)
case "ping":
fprint("Pong!!!")
# Lucas' notes
# Add a ping pong :) response/handler
# Add a get cable response/handler
# this will tell the robot arm to move
# Call for turning off everything
# TODO Helper for converting Python Dictionaries to JSON
# make function: pythonData --> { { "type": "...", "call": "...", "data": pythonData } }
# to send: to_server_queue.put(("*", "JSON STRING HERE")) # replace * with UUID of client to send to one specific location
case "cable_details":
fprint("cable_details message")
if call == "send":
pass
elif call == "request":
dataout = dict()
dataout["cables"] = list()
print(data)
if "part_number" in data:
for part in data["part_number"]:
#print(part)
#print(jbs.get_partnum(part))
dataout["cables"].append(jbs.get_partnum(part)["fullspecs"])
if "position" in data:
for pos in data["position"]:
#print(pos)
#print(jbs.get_position(str(pos)))
dataout["cables"].append(jbs.get_position(str(pos))["fullspecs"])
send_data(decoded["type"], "send", dataout, client_id)
case "cable_search":
fprint("cable_search message")
if call == "send":
pass
elif call == "request":
results = jbs.search(data["string"])["hits"]
dataout = dict()
dataout["cables"] = list()
for result in results:
dataout["cables"].append(result["fullspecs"])
send_data(decoded["type"], "send", dataout, client_id)
case "keyboard":
fprint("keyboard message")
if call == "send":
pass
elif call == "request":
if data["enabled"] == True:
# todo : send this to client
p = Process(target=run_cmd, args=("./keyboard-up.ps1",))
p.start()
elif data["enabled"] == False:
p = Process(target=run_cmd, args=("./keyboard-down.ps1",))
p.start()
case "machine_settings":
fprint("machine_settings message")
if call == "send":
pass
elif call == "request":
pass
case "cable_get":
fprint("cable_get message")
if call == "send":
global mainloop_get
if "part_number" in data:
for cableidx in range(len(cable_list)):
cable = cable_list[cableidx]
if cable == data["part_number"]:
fprint("Adding cable to dispense queue")
mainloop_get.put(("pickup", cableidx))
elif "position" in data:
fprint("Adding cable to dispense queue")
mainloop_get.put(("pickup", data["position"]))
else:
fprint("Invalid data.")
case _:
fprint("Unknown/unimplemented data type: " + decoded["type"])
except Exception as e:
fprint(traceback.format_exc())
fprint(e)
sleep(0.001) # Sleep to prevent tight loop
def start_client_socket():
app = Flask(__name__)
@app.route('/control_client', methods=['POST'])
def message_from_server():
# Handle message from server
data = request.json
fprint(f"Message from server: {data.get('message')}")
return "Message received", 200
app.run(host='0.0.0.0', port=6000)
def check_server_online(serverip, clientip):
def send_ip_to_server(server_url, client_ip):
try:
response = requests.post(server_url, json={'ip': client_ip}, timeout=1)
fprint(f"Server response: {response.text}")
return True
except requests.exceptions.RequestException as e:
fprint(f"Error sending IP to server: {e}")
return False
server_url = 'http://' + serverip + ':5000/report_ip'
while not send_ip_to_server(server_url, clientip):
sleep(1)
fprint("Successfully connected to server.")
return True
def setup_server(pool):
# linux server setup
global config
global counter
global sensor_ready
global camera_ready
global led_ready
global arm_ready
global serverproc
global camera
global arm
global jbs
global to_server_queue
global from_server_queue
global websocket_process
arm = Rob(config)
if real:
pool.apply_async(ur5_control.powerup_arm, (arm,), callback=arm_start_callback, error_callback=handle_error)
else:
arm_ready = True
global ledsys
if real:
ledsys = LEDSystem()
#pool.apply_async(ledsys.init, callback=led_start_callback)
#pool.apply_async(sensor_control.init, callback=sensor_start_callback)
jbs = JukeboxSearch()
if led_ready is False:
fprint("waiting for " + "LED controller initialization" + " to complete...", sendqueue=to_server_queue)
if real:
ledsys.init()
led_ready = True
fprint("LED controllers initialized.", sendqueue=to_server_queue)
if sensor_ready is False:
fprint("waiting for " + "Sensor Initialization" + " to complete...", sendqueue=to_server_queue)
global mbconn
mbconn = ModbusClient(host="192.168.1.20", port=502, auto_open=True, auto_close=True)
get_sensors()
fprint("Sensors initialized.", sendqueue=to_server_queue)
if camera_ready is False:
fprint("waiting for " + "Camera initilization" + " to complete...", sendqueue=to_server_queue)
camera = process_video.qr_reader(config["cameras"]["banner"]["ip"], int(config["cameras"]["banner"]["port"]))
fprint("Camera initialized.", sendqueue=to_server_queue)
#arm_ready = True
if arm_ready is False:
fprint("waiting for " + "UR5 powerup" + " to complete...", sendqueue=to_server_queue)
while arm_ready is False:
sleep(0.1)
if real:
ur5_control.init_arm(arm)
fprint("Arm initialized.", sendqueue=to_server_queue)
fprint("Starting websocket server...", sendqueue=to_server_queue)
websocket_process = server.start_websocket_server(to_server_queue, from_server_queue)
return True
def handle_error(error):
print(error, flush=True)
def get_sensors():
global mbconn
global sensors
oldsens = sensors
#print("Reading sensors")
#mbconn.open()
"""
port 1: 256
port 2: 272
port 3: 288
port 4: 304
port 5: 320
port 6: 336
port 7: 352
port 8: 368
"""
out = list()
for reg in [352, 288, 304, 368]:
val = mbconn.read_holding_registers(reg)[0]
if val == 1:
out.append(1)
else:
out.append(0)
sensors = out
#fprint("Values: " + str(sensors))
#mbconn.close()
for x in range(len(oldsens)):
if oldsens[x] == 0 and out[x] == 1:
# cable newly detected on tray
fprint("Precense detected: slot " + str(x))
return x
return -1
def get_open_spot(sensordata):
for x in range(len(sensordata)):
sens = sensordata[x]
if not sens:
return x
# if we get here, every spot is full
return False
def mainloop_server(pool):
# NON-blocking loop
global real
global ring_animation
global led_set_mode
global just_placed
global config
global counter
global killme
global mode
global jbs
global arm
global ledsys
global camera
global arm_ready
global arm_state
global camera_ready
global cable_search_ready
global cable_list
global mainloop_get
global cable_list_state
global scan_value
global oldmode
if mode != oldmode:
print(" ***** Running mode:", mode, "***** ")
oldmode = mode
if killme.value > 0:
killall()
# check for messages
check_server()
# do every loop!
if ring_animation is not None and ledsys.mode != "idle" and real:
ledsys.mainloop(None, ring_animation)
elif ring_animation is not None and real:
ledsys.mainloop(led_set_mode, ring_animation)
led_set_mode = None
else:
pass
#fprint("Not triggering LED loop: no ring animation")
if mode == "Startup":
if not real or skip_scanning:
counter = 54
if counter < 54:
# scanning cables
if arm_state is None:
#pool.apply_async(arm_start_callback, ("",))
arm_ready = False
pool.apply_async(ur5_control.holder_to_camera, (arm,counter), callback=arm_start_callback, error_callback=handle_error)
fprint("Getting cable index " + str(counter) + " and scanning...")
arm_state = "GET"
ring_animation = counter
led_set_mode = "GrabA"
#ur5_control.to_camera(arm, counter)
#arm_ready = True
elif arm_ready and arm_state == "GET":
fprint("Looking for QR code...")
pool.apply_async(camera.read_qr, (10,), callback=camera_start_callback, error_callback=handle_error)
arm_ready = False
elif camera_ready:
ring_animation = counter
led_set_mode = "GrabC"
fprint("Adding cable to list...")
global scan_value
if scan_value is False:
cable_list.append(scan_value)
elif scan_value.find("bldn.app/") > -1:
scan_value = scan_value[scan_value.find("bldn.app/")+9:]
else:
cable_list.append(scan_value)
fprint(scan_value)
pool.apply_async(ur5_control.camera_to_holder, (arm,counter), callback=arm_start_callback, error_callback=handle_error)
#ur5_control.return_camera(arm, counter)
#arm_ready = True
arm_state = "RETURN"
camera_ready = False
elif arm_ready and arm_state == "RETURN":
counter += 1
arm_state = None
else:
# just wait til arm/camera is ready
pass
else:
# scanned everything
ring_animation = None
led_set_mode == "idle"
tmp = [
# Actual cables in Jukebox
"BLTF-1LF-006-RS5",
"BLTF-SD9-006-RI5",
"BLTT-SLG-024-HTN",
"BLFISX012W0",
"BLFI4X012W0",
"BLSPE101",
"BLSPE102",
"BL7922A",
"BL7958A",
"BLIOP6U",
"BL10GXW13",
"BL10GXW53",
"BL29501F",
"BL29512",
"BL3106A",
"BL9841",
"BL3105A",
"BL3092A",
"BL8760",
"BL6300UE",
"BL6300FE",
"BLRA500P",
"AW86104CY",
"AW3050",
"AW6714",
"AW1172C",
"AWFIT-221-1_4"
]
while len(tmp) < 54:
tmp.append(False) # must have 54 entries
if not real or skip_scanning:
cable_list = tmp # comment out for real demo
for idx in range(len(cable_list)):
cable_list_state.append(True)
pool.apply_async(get_specs.get_multi, (cable_list, 0.3), callback=cable_search_callback, error_callback=handle_error)
mode = "Parsing"
fprint("All cables scanned. Finding & parsing datasheets...")
if mode == "Parsing":
# waiting for search & parse to complete
#cable_search_ready = True
if cable_search_ready is False:
pass
else:
# done
global parse_res
success, partnums = parse_res
for idx in range(len(partnums)):
if partnums[idx] is not False:
cable_list[idx] = partnums[idx][0].replace("/", "_")
else:
cable_list[idx] = False
print(partnums)
if success:
# easy mode
fprint("All cables inventoried and parsed.")
fprint("Adding to database...")
for idx in range(len(cable_list)):
partnum = cable_list[idx]
if partnum is not False:
with open("cables/" + partnum + "/search.json", "rb") as f:
searchdata = json.load(f)
searchdata["position"] = idx
with open("cables/" + partnum + "/specs.json", "rb") as f:
specs = json.load(f)
searchdata["fullspecs"] = specs
searchdata["fullspecs"]["position"] = idx
jbs.add_document(searchdata)
#sleep(0.5)
#print(jbs.get_position("1"))
fprint("All cables added to database.")
mode = "Idle"
else:
# TODO: manual input
pass
if mode == "Idle":
# do nothing
if arm_ready is False:
pass
else:
global mainloop_get
#print("Checking sensors..")
newtube = get_sensors()
if newtube >= 0 and newtube != just_placed:
# need to return a cable
mainloop_get.put(("return", newtube))
just_placed = -1
if not mainloop_get.empty():
fprint("Movement requested. Keep clear of the machine!")
action, get_cable = mainloop_get.get()
if get_cable > -1:
global sensors
if action == "pickup":
spot = get_open_spot(sensors)
if spot is not False:
arm_ready = False
if real:
pool.apply_async(ur5_control.holder_to_tray, (arm, get_cable, spot), callback=arm_start_callback, error_callback=handle_error)
else:
arm_ready = True
fprint("Getting cable at position " + str(get_cable))
mode = "Pickup"
cable_list_state[get_cable] = False # mark as removed
get_sensors()
if action == "return":
arm_ready = False
fprint("Returning cable from tray position " + str(get_cable))
if real:
pool.apply_async(ur5_control.tray_to_camera, (arm, get_cable), callback=arm_start_callback, error_callback=handle_error)
else:
arm_ready = True
mode = "ReturnC"
else:
# LED idle anim
pass
if mode == "Pickup":
# complete
if arm_ready == True:
mode = "Idle"
else:
# getting cable and bringing to tray
# led animation
pass
if mode == "ReturnC":
# complete
if arm_ready == True:
mode = "Scan"
arm_ready = False
camera_ready = False
if real:
pool.apply_async(camera.read_qr, (10,), callback=camera_start_callback, error_callback=handle_error)
else:
camera_ready = True
scan_value = "10GXS13"
else:
# getting cable from and bringing to camera
# led animation
pass
if mode == "Scan":
if camera_ready == True:
if scan_value is False:
# unable to scan ???? not good
fprint("Unable to scan cable. Gonna retry.")
camera_ready = False
pool.apply_async(camera.read_qr, (10,), callback=camera_start_callback, error_callback=handle_error)
pass
elif scan_value.find("bldn.app/") > -1:
scan_value = scan_value[scan_value.find("bldn.app/")+9:]
fprint("Got cable: " + str(scan_value))
if scan_value[0:2] == "BL" or scan_value[0:2] == "AW":
scan_value = scan_value[2:]
for idx in range(len(cable_list)):
cable = cable_list[idx]
if cable == scan_value and cable_list_state[idx] == False:
cable_list_state[idx] = True # mark cable as returned
arm_ready = False
if real:
pool.apply_async(ur5_control.camera_to_holder, (arm, idx), callback=arm_start_callback, error_callback=handle_error)
else:
arm_ready = True
mode = "Return"
break
elif cable == scan_value and cable_list_state[idx] == True:
fprint("WARNING: Holder still marked as occupied!")
arm_ready = False
if real:
pool.apply_async(ur5_control.camera_to_holder, (arm, idx), callback=arm_start_callback, error_callback=handle_error)
else:
arm_ready = True
mode = "Return"
break
if mode == "Return":
if arm_ready == True:
mode = "Idle"
#arm_ready = False
# movement finished
else:
# cable going from camera to holder
# led animation
pass
def run_loading_app():
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
app.run(debug=True, use_reloader=False, port=7000)
def setup_client(pool):
# Windows client setup
fprint("Opening browser...")
firefox = webdriver.Firefox()
firefox.fullscreen_window()
global config
global vm_ready
global serverproc
# Open loading wepage
p = Process(target=run_loading_app)
p.start()
firefox.get('http://localhost:7000')
# start Linux server VM
if config["core"]["server"] == "Hyper-V":
run_cmd("Start-VM -Name Jukebox*") # any and all VMs starting with "Jukebox"
# Wait for VM to start and be reachable over the network
serverproc = Process(target=start_client_socket)
serverproc.start()
pool.apply_async(check_server_online, (config["core"]["serverip"],config["core"]["clientip"]), callback=vm_start_callback)
#wait_for(vm_ready, "VM Startup")
#global vm_ready
if vm_ready is False:
fprint("waiting for " + "VM Startup" + " to complete...")
while vm_ready is False:
sleep(0.1)
p.terminate()
firefox.get("http://" + config["core"]["serverip"] + ":8000")
return True
def mainloop_client(pool):
sleep(0.1)
# listen for & act on commands from VM, if needed
# mainly just shut down, possibly connect to wifi or something
"""class Logger(object):
def __init__(self, filename="output.log"):
self.log = open(filename, "a")
self.terminal = sys.stdout
def write(self, message):
self.log.write(message)
#close(filename)
#self.log = open(filename, "a")
try:
self.terminal.write(message)
except:
sleep(0)
def flush(self):
print("",end="")"""
def killall():
procs = active_children()
for proc in procs:
proc.kill()
fprint("All child processes killed")
os.kill(os.getpid(), 9) # dirty kill of self
def killall_signal(a, b):
global config
if config["core"]["server"] == "Hyper-V":
run_cmd("Stop-VM -Name Jukebox*") # any and all VMs starting with "Jukebox"
killall()
def error(msg, *args):
return multiprocessing.get_logger().error(msg, *args)
class LogExceptions(object):
def __init__(self, callable):
self.__callable = callable
def __call__(self, *args, **kwargs):
try:
result = self.__callable(*args, **kwargs)
except Exception as e:
# Here we add some debugging help. If multiprocessing's
# debugging is on, it will arrange to log the traceback
error(traceback.format_exc())
# Re-raise the original exception so the Pool worker can
# clean up
raise
# It was fine, give a normal answer
return result
class LoggingPool(Pool):
def apply_async(self, func, args=(), kwds={}, callback=None, error_callback=None):
return Pool.apply_async(self, LogExceptions(func), args, kwds, callback, error_callback)
if __name__ == "__main__":
#sys.stdout = Logger(filename="output.log")
#sys.stderr = Logger(filename="output.log")
#log_to_stderr(logging.DEBUG)
fprint("Starting Jukebox control system...")
with open('config.yml', 'r') as fileread:
#global config
config = yaml.safe_load(fileread)
fprint("Config loaded.")
with Manager() as manager:
fprint("Spawning threads...")
pool = LoggingPool(processes=10)
counter = 0
killme = manager.Value('d', 0)
signal.signal(signal.SIGINT, killall_signal)
if config["core"]["mode"] == "winclient":
fprint("Starting in client mode.")
from selenium import webdriver
if setup_client(pool):
fprint("Entering main loop...")
while(keeprunning):
mainloop_client(pool)
elif config["core"]["mode"] == "linuxserver":
fprint("Starting in server mode.")
if setup_server(pool):
fprint("Entering main loop...")
start = 0
speed = config["core"]["loopspeed"]
while(keeprunning):
start = uptime()
mainloop_server(pool)
#sleep(0.01)
# limit to certain "framerate"
#print(start, start + 1.0/speed, uptime())
while start + 1.0/speed > uptime():
sleep(0.001)
else:
fprint("Mode unspecified - quitting")