401 lines
15 KiB
Python
401 lines
15 KiB
Python
|
#!/usr/bin/python3
|
||
|
# -*- coding: utf-8 -*-
|
||
|
# -*- mode: Python -*-
|
||
|
|
||
|
'''
|
||
|
|
||
|
Tracer v0.8.2
|
||
|
|
||
|
Etherdream DACs handler on network via Redis
|
||
|
|
||
|
LICENCE : CC
|
||
|
Sam Neurohack, pclf
|
||
|
|
||
|
Includes live conversion in etherdream coordinates, geometric corrections, color balance change, intensity limitation, grid display,...
|
||
|
|
||
|
One tracer process is launched per requested laser by LJ. Lasers parameters in LJ.conf.
|
||
|
Live I/O based on redis keys : inputs (Pointlists to draw,...) and outputs (DAC state, errors,..).
|
||
|
Keys are mostly read and set at each main loop.
|
||
|
This tracer include an enhanced version (support for several lasers) of the etherdream python library from j4cDAC.
|
||
|
|
||
|
|
||
|
* Redis keys reference *
|
||
|
|
||
|
- Drawing things :
|
||
|
|
||
|
/pl/Scene/lasernumber [(x,y,color),(x1,y1,color),...] The live list of drawn pygame points. Tracer continously ask redis for key /clientkey+lasernumber
|
||
|
/resampler/lasernumber [(1.0,8), (0.25,3),(0.75,3),(1.0,10)] : a string for resampling rules.
|
||
|
the first tuple (1.0,8) is for short line < 4000 in etherdream space
|
||
|
(0.25,3),(0.75,3),(1.0,10) for long line > 4000
|
||
|
i.e (0.25,3) means go at 25% position on the line, send 3 times this position to etherdream
|
||
|
/clientkey "/pl/SceneNumber/" What Scene to retrieve from redis
|
||
|
/EDH/lasernumber
|
||
|
|
||
|
- Tracer control :
|
||
|
|
||
|
/order 0-8 Set redis key with new value then issue the order number
|
||
|
|
||
|
0 : Draw Normal point list
|
||
|
1 : Get the new EDH = reread redis key /EDH/lasernumber
|
||
|
2 : Draw BLACK point list
|
||
|
3 : Draw GRID point list
|
||
|
4 : Resampler Change (longs and shorts lsteps)
|
||
|
5 : Client Key Change = reread redis key /clientkey
|
||
|
6 : Max Intensity Change = reread redis key /intensity
|
||
|
7 : kpps change = reread redis key /kpps
|
||
|
8 : color balance change = reread redis keys /red /green /blue
|
||
|
|
||
|
|
||
|
- Managing Etherdream DACs :
|
||
|
|
||
|
Discrete drawing values
|
||
|
|
||
|
/kpps 0- DAC output speed to laser, then order 7. Depends of actual angle
|
||
|
/intensity 0-255 Laser output power, then order 6 (for alignement,...)
|
||
|
/red 0-100 % of full red, then order 8
|
||
|
/green 0-100 % of full green, then order 8
|
||
|
/blue 0-100 % of full blue, then order 8
|
||
|
|
||
|
DAC status report
|
||
|
|
||
|
/lstt/lasernumber etherdream last_status.playback_state (0: idle 1: prepare 2: playing)
|
||
|
/cap/lasernumber number of empty points sent to fill etherdream buffer (up to 1799)
|
||
|
/lack/lasernumber "a": ACK "F": Full "I": invalid. 64 or 35 for no connection.
|
||
|
|
||
|
|
||
|
Geometric corrections
|
||
|
|
||
|
Doctodo
|
||
|
|
||
|
|
||
|
'''
|
||
|
|
||
|
import socket
|
||
|
import time
|
||
|
import struct
|
||
|
# from gstt import debug
|
||
|
from libs3 import gstt, log
|
||
|
import math
|
||
|
import ast
|
||
|
|
||
|
from libs3 import homographyp
|
||
|
import numpy as np
|
||
|
from .tracer_common import *
|
||
|
|
||
|
# @todo this needs normallization
|
||
|
ackstate = {'61': 'ACK', '46': 'FULL', '49': "INVALID", '21': 'STOP', '64': "NO CONNECTION ?",
|
||
|
'35': "NO CONNECTION ?",
|
||
|
'97': 'ACK', '70': 'FULL', '73': "INVALID", '33': 'STOP', '100': "NOCONNECTION", '48': "NOCONNECTION",
|
||
|
'a': 'ACK', 'F': 'FULL', 'I': "INVALID", '!': 'STOP', 'd': "NO CONNECTION ?", '0': "NO CONNECTION ?"}
|
||
|
black_points = [(278.0, 225.0, 0), (562.0, 279.0, 0), (401.0, 375.0, 0), (296.0, 454.0, 0), (298.0, 165.0, 0)]
|
||
|
grid_points = [(300.0, 200.0, 0), (500.0, 200.0, 65280), (500.0, 400.0, 65280), (300.0, 400.0, 65280),
|
||
|
(300.0, 200.0, 65280), (300.0, 200.0, 0), (200.0, 100.0, 0), (600.0, 100.0, 65280),
|
||
|
(600.0, 500.0, 65280), (200.0, 500.0, 65280), (200.0, 100.0, 65280)]
|
||
|
|
||
|
|
||
|
class TracerEtherdream(Tracer):
|
||
|
"""A connection to a DAC."""
|
||
|
|
||
|
# "Laser point List" Point generator
|
||
|
# each points is yielded : Getpoints() call n times OnePoint()
|
||
|
|
||
|
def __init__(self, mylaser, PL, redis, port=7765):
|
||
|
"""Connect to the DAC over TCP."""
|
||
|
socket.setdefaulttimeout(2)
|
||
|
self.redis = redis
|
||
|
self.mylaser = mylaser
|
||
|
self.clientkey = self.redis.get("/clientkey").decode('ascii')
|
||
|
|
||
|
# log.info("Tracer "+str(self.mylaser)+" connecting to "+ gstt.lasersIPS[mylaser])
|
||
|
# print("DAC", self.mylaser, "Handler process, connecting to", gstt.lasersIPS[mylaser] )
|
||
|
self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
|
self.connstatus = self.conn.connect_ex((gstt.lasersIPS[mylaser], port))
|
||
|
if self.connstatus == 35 or self.connstatus == 64:
|
||
|
log.err(
|
||
|
"Tracer " + str(self.mylaser) + " (" + gstt.lasersIPS[mylaser] + "): " + ackstate[str(self.connstatus)])
|
||
|
else:
|
||
|
print(
|
||
|
"Tracer " + str(self.mylaser) + " (" + gstt.lasersIPS[mylaser] + "): " + ackstate[str(self.connstatus)])
|
||
|
|
||
|
# ipconn state is -1 at startup (see gstt) and modified here
|
||
|
self.redis.set('/lack/' + str(self.mylaser), self.connstatus)
|
||
|
gstt.lstt_ipconn[self.mylaser] = self.connstatus
|
||
|
|
||
|
self.buf = b''
|
||
|
# Upper case PL is the Point List number
|
||
|
self.PL = PL
|
||
|
|
||
|
# Lower case pl is the actual point list coordinates
|
||
|
|
||
|
# pdb.set_trace()
|
||
|
self.pl = ast.literal_eval(self.redis.get(self.clientkey + str(self.mylaser)).decode('ascii'))
|
||
|
if self.redis.get('/EDH/' + str(self.mylaser)) == None:
|
||
|
# print("Laser",self.mylaser,"NO EDH !! Computing one...")
|
||
|
homographyp.newEDH(self.mylaser)
|
||
|
else:
|
||
|
|
||
|
gstt.EDH[self.mylaser] = np.array(ast.literal_eval(self.redis.get('/EDH/' + str(self.mylaser)).decode('ascii')))
|
||
|
# print("Laser",self.mylaser,"found its EDH in redis")
|
||
|
# print gstt.EDH[self.mylaser]
|
||
|
|
||
|
self.xyrgb = self.xyrgb_prev = (0, 0, 0, 0, 0)
|
||
|
self.intensity = 65280
|
||
|
self.intred = 100
|
||
|
self.intgreen = 100
|
||
|
self.intblue = 100
|
||
|
self.newstream = self.OnePoint()
|
||
|
self.prev_x = 0
|
||
|
self.prev_y = 0
|
||
|
|
||
|
if gstt.debug > 0:
|
||
|
print("Tracer", self.mylaser, "init asked for ckey", self.clientkey + str(self.mylaser))
|
||
|
if self.connstatus != 0:
|
||
|
# print(""
|
||
|
log.err("Connection ERROR " + str(self.connstatus) + " with laser " + str(mylaser) + " : " + str(
|
||
|
gstt.lasersIPS[mylaser]))
|
||
|
# print("first 10 points in PL",self.PL, self.GetPoints(10)
|
||
|
else:
|
||
|
print("Connection status for DAC " + str(self.mylaser) + " : " + str(self.connstatus))
|
||
|
|
||
|
# Reference points
|
||
|
# Read the "hello" message
|
||
|
first_status = self.readresp("?")
|
||
|
first_status.dump()
|
||
|
position = []
|
||
|
|
||
|
def before_loop(self):
|
||
|
|
||
|
# print("laser", self.laser_id, "Pb : ",self.last_status.playback_state)
|
||
|
# error if DAC is already playing state (from other source)
|
||
|
if self.last_status.playback_state == 2:
|
||
|
raise Exception("already playing?!")
|
||
|
|
||
|
# if idle go to prepare state
|
||
|
elif self.last_status.playback_state == 0:
|
||
|
self.prepare()
|
||
|
|
||
|
def get_points_capacity(self):
|
||
|
cap = 1799 - self.last_status.fullness
|
||
|
if cap < 100:
|
||
|
time.sleep(0.001)
|
||
|
cap += 150
|
||
|
return
|
||
|
|
||
|
def read(self, l):
|
||
|
"""Read exactly length bytes from the connection."""
|
||
|
while l > len(self.buf):
|
||
|
self.buf += self.conn.recv(4096)
|
||
|
|
||
|
obuf = self.buf
|
||
|
self.buf = obuf[l:]
|
||
|
return obuf[:l]
|
||
|
|
||
|
def readresp(self, cmd):
|
||
|
"""Read a response from the DAC."""
|
||
|
|
||
|
data = self.read(22)
|
||
|
response = data[0]
|
||
|
gstt.lstt_dacanswers[self.mylaser] = response
|
||
|
cmdR = chr(data[1])
|
||
|
status = Status(data[2:])
|
||
|
|
||
|
self.redis.set('/lack/' + str(self.mylaser), response)
|
||
|
|
||
|
if cmdR != cmd:
|
||
|
raise ProtocolError("expected resp for %r, got %r"
|
||
|
% (cmd, cmdR))
|
||
|
|
||
|
if response != ord('a'):
|
||
|
raise ProtocolError("expected ACK, got %r"
|
||
|
% (response,))
|
||
|
|
||
|
self.last_status = status
|
||
|
return status
|
||
|
|
||
|
def begin(self, lwm, rate):
|
||
|
cmd = struct.pack("<cHI", b'b', lwm, rate)
|
||
|
print("Tracer", str(self.mylaser), "begin with PL : ", str(self.PL))
|
||
|
self.conn.sendall(cmd)
|
||
|
return self.readresp("b")
|
||
|
|
||
|
def update(self, lwm, rate):
|
||
|
print(("update", lwm, rate))
|
||
|
cmd = struct.pack("<cHI", b'u', lwm, rate)
|
||
|
self.conn.sendall(cmd)
|
||
|
return self.readresp("u")
|
||
|
|
||
|
def encode_point(self, point):
|
||
|
return pack_point(self.mylaser, self.intensity, *point)
|
||
|
|
||
|
def get_capacity(self):
|
||
|
""" How much points can we send next / are free in etherdream's buffer?"""
|
||
|
cap = 1799 - self.last_status.fullness
|
||
|
return cap
|
||
|
|
||
|
def write(self, points):
|
||
|
epoints = list(map(self.encode_point, points))
|
||
|
cmd = struct.pack("<cH", b'd', len(epoints))
|
||
|
self.conn.sendall(cmd + b''.join(epoints))
|
||
|
return self.readresp('d')
|
||
|
|
||
|
def get_warped_point(self, x, y ):
|
||
|
# Etherpoint all transform in one matrix, with warp !!
|
||
|
# xy : x y
|
||
|
|
||
|
# gstt.EDH[self.mylaser]= np.array(ast.literal_eval(self.redis.get('/EDH/'+str(self.mylaser))))
|
||
|
position = homographyp.apply(gstt.EDH[self.laser_id], np.array([(x, y)]))
|
||
|
|
||
|
# print("etherdream point",position[0][0], position[0][1], ((c >> 16) & 0xFF) << 8, ((c >> 8) & 0xFF) << 8, (c & 0xFF) << 8)
|
||
|
|
||
|
return position[0][0], position[0][1]
|
||
|
|
||
|
def prepare(self):
|
||
|
self.conn.sendall(b'p')
|
||
|
return self.readresp('p')
|
||
|
|
||
|
def stop(self):
|
||
|
self.conn.sendall('s')
|
||
|
return self.readresp('s')
|
||
|
|
||
|
def estop(self):
|
||
|
self.conn.sendall("\xFF")
|
||
|
return self.readresp("\xFF")
|
||
|
|
||
|
def clear_estop(self):
|
||
|
self.conn.sendall("c")
|
||
|
return self.readresp("c")
|
||
|
|
||
|
def ping(self):
|
||
|
self.conn.sendall('?')
|
||
|
return self.readresp('?')
|
||
|
|
||
|
def play_stream(self):
|
||
|
|
||
|
# print("laser", self.mylaser, "Pb : ",self.last_status.playback_state)
|
||
|
|
||
|
# error if etherdream is already playing state (from other source)
|
||
|
if self.last_status.playback_state == 2:
|
||
|
raise Exception("already playing?!")
|
||
|
|
||
|
# if idle go to prepare state
|
||
|
elif self.last_status.playback_state == 0:
|
||
|
self.prepare()
|
||
|
|
||
|
started = 0
|
||
|
|
||
|
while True:
|
||
|
|
||
|
# print("laser", self.mylaser, "Pb : ",self.last_status.playback_state)
|
||
|
|
||
|
order = int(self.redis.get('/order/' + str(self.mylaser)).decode('ascii'))
|
||
|
# print("tracer", str(self.mylaser),"order", order, type(order)
|
||
|
|
||
|
if order == 0:
|
||
|
|
||
|
# USER point list
|
||
|
|
||
|
# self.pl = ast.literal_eval(self.redis.get(self.clientkey+str(self.mylaser)).decode('ascii'))
|
||
|
# print("Tracer : laser", self.mylaser, " order 0 : pl : ",len(self.pl))
|
||
|
|
||
|
# si la clef est vide utiliser les points noirs ? -> syntax error -> black points.
|
||
|
|
||
|
try:
|
||
|
|
||
|
# newpl = ""
|
||
|
# newpl = self.redis.get(self.clientkey+str(self.mylaser))
|
||
|
# self.pl = ast.literal_eval(newpl.decode('ascii'))
|
||
|
self.pl = ast.literal_eval(self.redis.get(self.clientkey + str(self.mylaser)).decode('ascii'))
|
||
|
|
||
|
except SyntaxError:
|
||
|
print("BAD POINTLIST on Tracer : laser", self.mylaser, " order 0 : pl : ", self.pl)
|
||
|
self.pl = black_points
|
||
|
|
||
|
# print("Tracer : laser", self.mylaser, " order 0 : pl : ",len(self.pl))
|
||
|
|
||
|
else:
|
||
|
|
||
|
# Get the new EDH
|
||
|
if order == 1:
|
||
|
print("Tracer", self.mylaser, "new EDH ORDER in redis")
|
||
|
gstt.EDH[self.mylaser] = np.array(
|
||
|
ast.literal_eval(self.redis.get('/EDH/' + str(self.mylaser)).decode('ascii')))
|
||
|
# Back to user point list
|
||
|
self.redis.set('/order/' + str(self.mylaser), 0)
|
||
|
|
||
|
# BLACK point list
|
||
|
if order == 2:
|
||
|
print("Tracer", self.mylaser, "BLACK ORDER in redis")
|
||
|
self.pl = black_points
|
||
|
|
||
|
# GRID point list
|
||
|
if order == 3:
|
||
|
print("Tracer", self.mylaser, "GRID ORDER in redis")
|
||
|
self.pl = grid_points
|
||
|
|
||
|
# Resampler Change
|
||
|
if order == 4:
|
||
|
self.resampler = ast.literal_eval(self.redis.get('/resampler/' + str(self.mylaser)).decode('ascii'))
|
||
|
print("Tracer", self.mylaser, " : resetting lsteps for", self.resampler)
|
||
|
gstt.stepshortline = self.resampler[0]
|
||
|
gstt.stepslongline[0] = self.resampler[1]
|
||
|
gstt.stepslongline[1] = self.resampler[2]
|
||
|
gstt.stepslongline[2] = self.resampler[3]
|
||
|
# Back to user point list order
|
||
|
self.redis.set('/order/' + str(self.mylaser), 0)
|
||
|
|
||
|
# Client Key change
|
||
|
if order == 5:
|
||
|
print("Tracer", self.mylaser, "new clientkey")
|
||
|
self.clientkey = self.redis.get('/clientkey')
|
||
|
# Back to user point list order
|
||
|
self.redis.set('/order/' + str(self.mylaser), 0)
|
||
|
|
||
|
# Intensity change
|
||
|
if order == 6:
|
||
|
self.intensity = int(self.redis.get('/intensity/' + str(self.mylaser)).decode('ascii')) << 8
|
||
|
print("Tracer", self.mylaser, "new Intensity", self.intensity)
|
||
|
gstt.intensity[self.mylaser] = self.intensity
|
||
|
self.redis.set('/order/' + str(self.mylaser), "0")
|
||
|
|
||
|
# kpps change
|
||
|
if order == 7:
|
||
|
gstt.kpps[self.mylaser] = int(self.redis.get('/kpps/' + str(self.mylaser)).decode('ascii'))
|
||
|
print("Tracer", self.mylaser, "new kpps", gstt.kpps[self.mylaser])
|
||
|
self.update(0, gstt.kpps[self.mylaser])
|
||
|
self.redis.set('/order/' + str(self.mylaser), "0")
|
||
|
|
||
|
# color balance change
|
||
|
if order == 8:
|
||
|
self.intred = int(self.redis.get('/red/' + str(self.mylaser)).decode('ascii'))
|
||
|
self.intgreen = int(self.redis.get('/green/' + str(self.mylaser)).decode('ascii'))
|
||
|
self.intblue = int(self.redis.get('/blue/' + str(self.mylaser)).decode('ascii'))
|
||
|
print("Tracer", self.mylaser, "new color balance", self.intred, "% ", self.intgreen, "% ",
|
||
|
self.intblue, "% ")
|
||
|
self.redis.set('/order/' + str(self.mylaser), "0")
|
||
|
|
||
|
self.redis.set('/lstt/' + str(self.mylaser), self.last_status.playback_state)
|
||
|
# pdb.set_trace()
|
||
|
# How much room?
|
||
|
|
||
|
cap = 1799 - self.last_status.fullness
|
||
|
points = self.GetPoints(cap)
|
||
|
|
||
|
self.redis.set('/cap/' + str(self.mylaser), cap)
|
||
|
|
||
|
if cap < 100:
|
||
|
time.sleep(0.001)
|
||
|
cap += 150
|
||
|
|
||
|
# print("Writing %d points" % (cap, ))
|
||
|
# t0 = time.time()
|
||
|
# if self.mylaser == 2:
|
||
|
# print(points)
|
||
|
self.write(points)
|
||
|
# t1 = time.time()
|
||
|
# print("Took %f" % (t1 - t0, )
|
||
|
|
||
|
if not started:
|
||
|
print("Tracer", self.mylaser, "starting with", gstt.kpps[self.mylaser], "kpps")
|
||
|
self.begin(0, gstt.kpps[self.mylaser])
|
||
|
started = 1
|