Better docs

This commit is contained in:
leduc 2025-04-27 15:55:38 +02:00
parent 356755c486
commit cd93efa04f
12 changed files with 560 additions and 29 deletions

129
README.md
View File

@ -0,0 +1,129 @@
# LJnano
A lightweight, stripped-down version of LJ specifically designed to run the chain of clitools (generator + filters + exports) for pointlist processing.
## Overview
LJnano provides a local laser simulator that connects to a browser interface via WebSockets. It's designed to be minimal and focused on running the bundled clitools chain efficiently. LJnano comes with its own set of clitools in the `clitools/` directory, which includes generators, filters, and exporters for processing point lists.
## Installation
### Requirements
LJnano requires the following Python packages:
- websocket-client: For WebSocket client functionality
- websocket-server: For WebSocket server functionality
- redis: For Redis database interaction
- pyOSC3: For OSC protocol support (used by some generators)
### Quick Install
A `requirements.txt` file is provided for easy installation of all dependencies:
```bash
pip3 install -r requirements.txt
```
### Manual Install
Alternatively, you can install dependencies individually:
```bash
pip3 install websocket-client websocket-server redis pyOSC3
```
## Usage
### Starting the Server
```bash
python3 nano.py
```
Options:
- `-v, --verbose`: Enable verbose output
- `-s, --server`: WS server IP (default: 127.0.0.1)
- `-p, --port`: WS port to bind to (default: 9001)
- `-k, --key`: Redis key to update
### Browser Interface
Open `www/simulocal.html` in a browser to view the laser simulation.
### Running the Clitools Chain
LJnano is designed to work with its bundled clitools chain located in the `clitools/` directory:
1. **Generators** (`clitools/generators/`): Create point lists
2. **Filters** (`clitools/filters/`): Process and modify point lists
3. **Exporters** (`clitools/exports/`): Output point lists to various formats
These tools can be chained together using Unix pipes. For example:
```bash
python3 clitools/generators/dummy.py | python3 clitools/filters/kaleidoscope.py | python3 clitools/exports/tonano.py
```
For detailed chain operations and examples, see `clitools/README.md`
### Using LJnano Output in Browser
To use LJnano output in a browser, use the `tonano.py` exporter located in `clitools/exports/`:
```bash
# Example usage of tonano.py exporter
python3 clitools/exports/tonano.py
```
Options for tonano.py:
- `-v, --verbose`: Enable verbose output
- `-s, --server`: WS server IP (default: 127.0.0.1)
- `-p, --port`: WS port to bind to (default: 9001)
- `-k, --key`: Redis key to update (default: /pl/0/0)
- `-o, --old`: Use old school 0-800 coordinate space
## Architecture
LJnano uses WebSockets on port 9001 by default to communicate between the server and the browser interface. The system allows for real-time visualization of laser point lists.
## Changelog
### v0.1b (Current Version)
#### Core Features
- WebSocket server on port 9001 for real-time communication
- Browser-based visualization interface (www/simulocal.html)
- Redis integration for storing and retrieving point lists
- Support for multiple laser simulations
- Status indicators for laser state and connections
#### Bundled Clitools
**Generators** (in `clitools/generators/`):
- dummy.py: Basic point list generator
- audio.py: Audio-reactive point generation
- turtle1.py: Turtle graphics based generator
- blank.py: Empty template for creating new generators
- audiogen3.py: Advanced audio-reactive generator
- Support for NetLogo integration via file-based input
**Filters** (in `clitools/filters/`):
- kaleidoscope.py: Mirrors points based on a pivot
- anaglyph.py: Creates 3D anaglyph effects
- colorcycle.py: Cycles through colors for points
- redilysis.py: Redis-based analysis and modification
**Exporters** (in `clitools/exports/`):
- tonano.py: Sends point lists to LJnano for visualization
- tonano800.py: Sends point lists in 0-800 coordinate space
- toRedis.py: Exports point lists to Redis
- toUDP.py: Sends point lists via UDP
- toWS.py: Sends point lists via WebSockets
- toNull.py: Discards point lists (for testing)
#### Browser Interface
- Real-time point list visualization
- Status indicators for connections
- Support for multiple laser displays
- Canvas-based rendering of laser points

View File

@ -9,7 +9,7 @@ BOOM | WIIIIIZ :: PHHHHHRACKRACKRACK ~~ WOOP ~~###~~ WIIT
## The basic loop
```
python3 generators/dummy.py -f 2 | python3 filters/kaleidoscope.py | python3 exports/toRedis.py -v
python3 generators/dummy.py -f 2 | python3 filters/kaleidoscope.py | python3 exports/tonano.py -v
------------------------------ --------------------- -------------------
\/ \/ \/
Generator Filter Export
@ -55,7 +55,11 @@ These do listen and read on STDIN and do the same print'n'flush on STDOUT.
### Export
Read from STDIN and send to redis mostly
Read from STDIN and send to LJnano.
* tonano.py
When your chain is ready and tested with LJnano going with real lasers needs LJ running and you simply change the export with :
* toRedis.py : provide a key, server IP, etc.

Binary file not shown.

View File

@ -26,7 +26,7 @@ import time
argsparser = argparse.ArgumentParser(description="Redis exporter LJ")
argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str)
argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str)
argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str)
argsparser.add_argument("-k","--key",help="Redis key to update",default="/pl/0/0",type=str)
argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose")
args = argsparser.parse_args()

View File

@ -4,9 +4,20 @@
'''
tonano
input space for X & Y : -1500,+1500
exporter to LJ nano
v0.1b
a la place de ast.literal_eval(line) : ?
>>> a = "[[111.121, 45.8783, 0.0],[110.936, 44.8368, 0.0],[374.849, 673.228, 230.536]]"
>>> import json
>>> b = json.loads(a)
>>> b
[[111.121, 45.8783, 0.0], [110.936, 44.8368, 0.0], [374.849, 673.228, 230.536]]
>>> b[0]
[111.121, 45.8783, 0.0]
'''
from __future__ import print_function
import websocket
@ -17,6 +28,7 @@ import sys
import random
from websocket_server import WebsocketServer
from socket import *
#import ast
try:
import thread
@ -35,7 +47,8 @@ argsparser = argparse.ArgumentParser(description="tonano v0.1b help mode")
argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output")
argsparser.add_argument("-s","--server",help="WS server IP (127.0.0.1 by default)", type=str)
argsparser.add_argument("-p","--port",help="WS port to bind to (9001 by default)", type=str)
argsparser.add_argument("-k","--key",help="Redis key to update",default="0",type=str)
argsparser.add_argument("-k","--key",help="Redis key to update",default="/pl/0/0",type=str)
argsparser.add_argument("-o","--old",help="Coordinates in old school 0-800 space",action="store_true")
args = argsparser.parse_args()
key = args.key
@ -55,6 +68,15 @@ if args.port:
else:
wsPORT = 9001
if args.old:
inspace = [0,800]
else:
inspace = [-1500,1500]
outspace = [-1500,1500]
zoom = (outspace[1]-outspace[0])/(inspace[1]-inspace[0])
debug("")
debug("tonano v0.1b")
@ -74,7 +96,7 @@ def sendbroadcast():
cs = socket(AF_INET, SOCK_DGRAM)
cs.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
cs.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
cs.sendto("LJ tonano 0.1".encode(), ("255.255.255.255", 54545))
cs.sendto("LJ tonano v0.1".encode(), ("255.255.255.255", 54545))
#
@ -109,10 +131,18 @@ def on_open(ws):
line = line.replace("]",')')
#debug(line)
line = "[{}]".format(line)
if zoom != 1.0:
shape = []
pointsList = ast.literal_eval(line)
for point in pointsList:
shape.append(((point[0]*zoom)+outspace[0],(point[1]*zoom)+outspace[0], point[2]))
line = str(shape)
debug("CLI proccess sending : /simul" +" "+ line)
#sendWSall("/simul" +" "+ str(points[laserid].decode('ascii')))
ws.send("/simul "+line)
#debug("exports::tosimuCLIent "+str(key)+" "+line)
except EOFError:
debug("tonano break")# no more information

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,359 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -*- mode: Python -*-
'''
fromild
v0.1.0
Read/display once an .ild animation file and quit ??
LICENCE : CC
by cocoa and Sam Neurohack
Heavy u-se of :
ILDA.py
Python module for dealing with the ILDA Image Data Transfer Format,
an interchange format for laser image frames.
Copyright (c) 2008 Micah Dowty
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
from __future__ import print_function
import time
import struct
import argparse
import sys
import random
name="generator::fromild"
def debug(*args, **kwargs):
if( verbose == False ):
return
print(*args, file=sys.stderr, **kwargs)
argsparser = argparse.ArgumentParser(description=".ild file frame generator")
argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int)
argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output")
argsparser.add_argument("-i","--ild",help=".ild file",default="book2.ild",type=str)
args = argsparser.parse_args()
fps=args.fps
verbose=args.verbose
optimal_looptime = 1 / fps
debug(name+" optimal looptime "+str(optimal_looptime))
# Format codes
FORMAT_3D = 0
FORMAT_2D = 1
FORMAT_COLOR_TABLE = 2
# Mapping from FORMAT_* codes to struct format strings
formatTable = (
'>hhhH',
'>hhH',
'>BBB',
)
# Header values
HEADER_MAGIC = b"ILDA\0\0\0"
HEADER_RESERVED = 0
HEADER_FORMAT = ">7sB16sHHHBB"
HEADER_LEN = struct.calcsize(HEADER_FORMAT)
class Table(object):
"""Container object for one ILDA table: either a frame (table of points)
or a palette (table of colors).
The 'items' list contains the data within this table. Each item
is a tuple, corresponding to the raw values within that row of the
table.
2D frame: (x, y, status)
3D frame: (x, y, z, status)
Color: (r, g, b)
"""
def __init__(self, format=FORMAT_2D, name="",
length=0, number=0, total=0, scanHead=0):
self.__dict__.update(locals())
self.items = []
self.itemsproducer = None
def __repr__(self):
return ("<ILDA.Table format=%d name=%r "
"length=%d number=%d total=%d scanHead=%d>" %
(self.format, self.name, self.length, self.number,
self.total, self.scanHead))
def unpackHeader(self, data):
magic, self.format, self.name, self.length, \
self.number, self.total, self.scanHead, \
reserved = struct.unpack(HEADER_FORMAT, data)
print(magic, HEADER_MAGIC)
if magic != HEADER_MAGIC:
raise ValueError("Bad ILDA header magic. Not an ILDA file?")
if reserved != HEADER_RESERVED:
raise ValueError("Reserved ILDA field is not zero.")
def packHeader(self):
return struct.pack(HEADER_FORMAT, HEADER_MAGIC, self.format,
self.name, self.length, self.number,
self.total, self.scanHead, HEADER_RESERVED)
def readHeader(self, stream):
self.unpackHeader(stream.read(HEADER_LEN))
def writeHeader(self, stream):
stream.write(self.packHeader())
def _getItemFormat(self):
try:
return formatTable[self.format]
except IndexError:
raise ValueError("Unsupported format code")
def read_stream(self, stream):
"""Read the header, then read all items in this table."""
self.readHeader(stream)
if self.length:
fmt = self._getItemFormat()
itemSize = struct.calcsize(fmt)
self.items = [struct.unpack(fmt, stream.read(itemSize))
for i in range(self.length)]
self.itemsproducer = self.produce()
def write(self, stream):
"""Write the header, then write all items in this table."""
self.writeHeader(stream)
if self.length:
fmt = self._getItemFormat()
itemSize = struct.calcsize(fmt)
stream.write(''.join([struct.pack(fmt, *item)
for item in self.items]))
def iterPoints(self):
"""Iterate over Point instances for each item in this table.
Only makes sense if this is a 2D or 3D point table.
"""
for item in self.items:
p = Point()
p.decode(item)
yield p
def produce(self):
"""Iterate over Point instances for each item in this table.
Only makes sense if this is a 2D or 3D point table.
"""
while True:
for item in self.items:
p = Point()
p.decode(item)
yield p.encode()
#yield (p.x, p.y, p.z, p.color, p.blanking)
def read(self, cap):
"""yields what dac.play_stream() needs (x, y, z, ?, ?)
"""
return [next(self.itemsproducer) for i in range(cap)]
class Point:
"""Abstraction for one vector point. The Table object, for
completeness and efficiency, stores raw tuples for each
point. This is a higher level interface that decodes the status
bits and represents coordinates in floating point.
"""
def __init__(self, x=0.0, y=0.0, z=0.0, color=0, blanking=False):
self.__dict__.update(locals())
def __repr__(self):
return "%s, %s, %s, %s, %s" % (
self.x, self.y, self.z, self.color, self.blanking)
#return "<ILDA.Point (%s, %s, %s) color=%s blanking=%s>" % (
# self.x, self.y, self.z, self.color, self.blanking)
def encode(self):
status = self.color & 0xFF
if self.blanking:
status |= 1 << 14
return (
int( min(0x7FFF, max(-0x7FFF, self.x * 0x7FFF)) ),
int( min(0x7FFF, max(-0x7FFF, self.y * 0x7FFF)) ),
int( min(0x7FFF, max(-0x7FFF, self.z * 0x7FFF)) ),
int( min(0x7FFF, max(-0x7FFF, self.color * 0x7FFF)) ),
int( min(0x7FFF, max(-0x7FFF, self.blanking * 0x7FFF)) )
)
def decode(self, t):
#print "~~ Decoding, t of len "+ str(len(t)) +" is: " + str(t)
self.x = t[0] / 0x7FFF
self.y = t[1] / 0x7FFF
if len(t) > 3:
self.z = t[2] / 0x7FFF
# self.color = t[3] & 0xFF
# self.blanking = (t[3] & (1 << 14)) != 0
else:
self.z = 0.0
self.color = t[-1] & 0xFF
self.blanking = (t[-1] & (1 << 14)) != 0
def read(stream):
"""Read ILDA data from a stream until we hit the
end-of-stream marker. Yields a sequence of Table objects.
"""
while True:
t = Table()
t.read_stream(stream)
if not t.length:
# End-of-stream
break
yield t
def write(stream, tables):
"""Write a sequence of tables in ILDA format,
terminated by an end-of-stream marker.
"""
for t in tables:
t.write(stream)
Table().write(stream)
def readFrames(stream):
"""Read ILDA data from a stream, and ignore
all non-frame tables. Yields only 2D or 3D
point tables.
"""
for t in read(stream):
if t.format in (FORMAT_2D, FORMAT_3D):
yield t
def readFirstFrame(stream):
"""Read only a single frame from an ILDA stream."""
for frame in readFrames(stream):
return frame
'''
#LD = LaserDisplay()
LD = LaserDisplay({"server":"localhost","port": 50000})
LD.set_scan_rate(37000)
LD.set_blanking_delay(0)
'''
WIDTH=700
HEIGHT=700
ilda_file = open(args.ild, 'rb')
ilda_frames = readFrames(ilda_file)
frames = []
for myframe in ilda_frames:
frame = []
debug("Frame", myframe.number, "/",myframe.total, "length", myframe.length)
for mypoint in myframe.iterPoints():
frame.append([WIDTH/2 + (WIDTH/2)*mypoint.x, HEIGHT/2 + (HEIGHT/2)*mypoint.y])
#debug(frame)
frames.append(frame)
if myframe.number +1 == myframe.total:
debug("last frame", myframe.number, myframe.total)
break
ilda_file.close()
debug(len(frames))
#debug(frame)
#LD.set_color(YELLOW)
'''
for frame in frames:
for _ in range(2):
shape =[]
for point in frame:
#LD.set_color(p.color)
if random.random()<=0.5:
shape.append([point[0], point[1],0])
#debug(shape)
#shape =[]
'''
'''
while True:
LD.messageBuffer = m
LD.show_frame()
f = open(args.ild, 'rb')
myframe = readFirstFrame(f)
while myframe.number +1< myframe.total:
start = time.time()
shape =[]
if myframe is None:
f.close()
sys.exit() # does not quit ????
debug(name,"Frame", myframe.number, "/",myframe.total, "length", myframe.length)
for p in myframe.iterPoints():
p2 = str(p)
point = p2.split(',')
x = float(point[0])
y = float(point[1])
z = float(point[2])
color = int(point[3])
blanking = point[4][1:]
if blanking == "True":
shape.append([300+(x*300),300+(-y*300),0])
else:
shape.append([300+(x*300),300+(-y*300),65535])
print(shape, flush=True);
#debug(shape)
myframe = readFirstFrame(f)
looptime = time.time() - start
if( looptime < optimal_looptime ):
time.sleep( optimal_looptime - looptime)
debug(name+" micro sleep:"+str( optimal_looptime - looptime))
f.close()
'''

View File

@ -32,6 +32,7 @@ from HersheyFonts import HersheyFonts
name="generator::text"
Position = [-1500,0]
def debug(*args, **kwargs):
if( verbose == False ):
@ -42,7 +43,7 @@ def debug(*args, **kwargs):
argsparser = argparse.ArgumentParser(description="Text generator")
argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int)
argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output")
argsparser.add_argument("-t","--text",help="Text to display",default="hello",type=str)
argsparser.add_argument("-t","--text",help="Text to display",default="proton",type=str)
argsparser.add_argument("-p","--police",help="Herschey font to use",default="futural",type=str)
args = argsparser.parse_args()
@ -73,14 +74,18 @@ Allfonts = ['futural', 'astrology', 'cursive', 'cyrilc_1', 'cyrillic', 'futuram'
thefont = HersheyFonts()
#thefont.load_default_font()
thefont.load_default_font(fontname)
thefont.normalize_rendering(120)
thefont.normalize_rendering(300)
for (x1, y1), (x2, y2) in thefont.lines_for_text(text):
shape.append([x1, -y1+400, color])
shape.append([x2 ,-y2+400, color])
shape.append([Position[0]+x1, Position[1]-y1, color])
shape.append([Position[0]+x2, Position[1]-y2, color])
'''
shape.append([x1+ScreenX[0]+Position[0], -y1+ScreenY[0]+Position[1], color])
shape.append([x2+ScreenX[0]+Position[0] ,-y2+ScreenY[0]+Position[1], color])
'''
while True:
debug(shape)
start = time.time()
print(shape, flush=True);

18
nano.py
View File

@ -62,11 +62,11 @@ if args.port:
else:
wsPORT = 9001
debug("")
debug("LJnano v0.1b")
print("")
print("LJnano v0.1b")
points0 = "[(150.0, 230.0, 65280), (170.0, 170.0, 65280), (230.0, 170.0, 65280), (210.0, 230.0, 65280), (150.0, 230.0, 65280)]"
points1 = "[(180.0, 230.0, 65280), (200.0, 170.0, 65280), (180.0, 230.0, 65280)]"
points0 = "[(-150.0, 230.0, 65280), (-70.0, -170.0, 65280), (310.0, -170.0, 65280), (210.0, 230.0, 65280), (-150.0, 230.0, 65280)]"
points1 = "[(200.0, 170.0, 65280), (-180.0, -230.0, 65280)]"
points2 = "[(170.0, 190.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (230.0, 200.0, 65280), (170.0, 230.0, 65280), (230.0, 230.0, 65280)]"
points3 = "[(170.0, 170.0, 65280), (200.0, 170.0, 65280), (230.0, 190.0, 65280), (200.0, 200.0, 65280), (230.0, 210.0, 65280), (200.0, 230.0, 65280), (170.0, 230.0, 65280)]"
points = [points0, points1, points2, points3]
@ -157,7 +157,6 @@ def sendbroadcast():
# Websocket server
#
def client_list():
clients = []
@ -171,7 +170,7 @@ def new_client(client, wserver):
debug("WS server got new client connected and was given id %d" % client['id'])
toKey('WSclients', client_list())
sendWSall("/status Hello " + str(client['id']))
sendWSall("/status nano:Hello " + str(client['id']))
sendWSall("/laser "+str(0))
sendWSall("/lack/" + str(0) + " 3")
sendWSall("/lstt/" + str(0) + " 3")
@ -237,13 +236,12 @@ def LaunchServer(*args):
# Websocket server
wserver = WebsocketServer(wsPORT,host=serverIP)
debug("Launching Websocket server...")
debug("at", serverIP, "port",wsPORT)
print("Launching Websocket server at", serverIP, "port",wsPORT)
wserver.set_fn_new_client(new_client)
wserver.set_fn_client_left(client_left)
wserver.set_fn_message_received(message_received)
debug("LJ local server running...")
debug("")
print("Nano is running. Open/reload www/simulocal.html in a browser !")
print()
# websocket server loop
wserver.run_forever()

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
websocket-client>=1.3.3
websocket-server>=0.4
redis>=4.3.4
pyOSC3>=0.0.7

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Simu Rack</title>
<title>LJnano</title>
<meta charset="utf-8" />
<meta name="apple-mobile-web-app-title" content="LJ">
<meta name="apple-mobile-web-app-capable" content="yes">
@ -452,7 +452,7 @@
var ctx = canvas.getContext("2d");
var lastpoint = { x: 0, y: 0, color: 0};
ctx.clearRect(0,0,400,400);
var zoom = 0.5;
var zoom = 0.1333;
//ctx.save
// Draws every segment received, except black colored target ones
@ -461,18 +461,19 @@
{
ctx.clearRect(0,0,400,400);
lastpoint = {
x:pl2[0],
y:pl2[1],
x:pl2[0]+1500,
y:pl2[1]+1500,
color:pl2[2]
}
for (var i = 0; i <= pl2.length; i+=3)
for (var i = 0; i <= pl2.length-1; i+=3)
{
point = {
x:pl2[i],
y:pl2[i+1],
x:pl2[i]+1500,
y:pl2[i+1]+1500,
color:pl2[i+2]
}
// console.log(lastpoint,point)
//console.log(point)
//console.log(point.x * zoom, point.y * zoom);
// if the target is black, skip drawing
if( point.color != 0){
ctx.beginPath()
@ -480,6 +481,7 @@
//ctx.shadowOffsetY = 0;
ctx.shadowBlur = 5;
ctx.shadowColor = 'rgba(255, 255, 255, 1)';
///ctx.shadowColor = 'rgba(255, 255, 255, 1)';
ctx.lineWidth = 2;
ctx.strokeStyle = "#"+(point.color + Math.pow(16, 6)).toString(16).slice(-6);
ctx.moveTo(lastpoint.x * zoom, lastpoint.y * zoom);