add webui
This commit is contained in:
240
webui/app.py
Normal file
240
webui/app.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Flask web UI for HPGL plotter.
|
||||
Run from project root so Command, Program, Plotter, hpgl_svg, config can be imported.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from flask import Flask, request, jsonify, session, send_from_directory, render_template
|
||||
|
||||
# Run from project root (parent of webui)
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from Command import Command
|
||||
from Program import Program
|
||||
from Plotter import Plotter
|
||||
from hpgl_svg import program_to_svg
|
||||
|
||||
try:
|
||||
from config import webui_secret_key, webui_max_upload_mb
|
||||
except ImportError:
|
||||
webui_secret_key = lambda: os.environ.get('SECRET_KEY', 'change-me-in-production')
|
||||
webui_max_upload_mb = lambda: 16
|
||||
|
||||
app = Flask(__name__, static_folder='static', template_folder='templates')
|
||||
app.secret_key = webui_secret_key()
|
||||
app.config['MAX_CONTENT_LENGTH'] = webui_max_upload_mb() * 1024 * 1024
|
||||
UPLOAD_DIR = os.path.join(ROOT, 'webui', 'uploads')
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def _get_program():
|
||||
path = session.get('upload_path')
|
||||
if not path or not os.path.isfile(path):
|
||||
return None
|
||||
try:
|
||||
return Program.parsefile(path)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _save_program(program, path=None):
|
||||
path = path or session.get('upload_path')
|
||||
if not path:
|
||||
return None
|
||||
with open(path, 'w') as f:
|
||||
f.write(str(program))
|
||||
return path
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/api/upload', methods=['POST'])
|
||||
def upload():
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': 'No file'}), 400
|
||||
f = request.files['file']
|
||||
if not f.filename or not (f.filename.lower().endswith('.hpgl') or f.filename.lower().endswith('.plt')):
|
||||
return jsonify({'error': 'Need an .hpgl or .plt file'}), 400
|
||||
try:
|
||||
content = f.read().decode('utf-8', errors='replace')
|
||||
Program.parse(content) # validate
|
||||
except Exception as e:
|
||||
return jsonify({'error': 'Invalid HPGL: {}'.format(str(e))}), 400
|
||||
name = str(uuid.uuid4()) + '.hpgl'
|
||||
path = os.path.join(UPLOAD_DIR, name)
|
||||
with open(path, 'w') as out:
|
||||
out.write(content)
|
||||
session['upload_path'] = path
|
||||
session['upload_filename'] = f.filename
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'filename': f.filename,
|
||||
'preview_url': '/api/svg'
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/dimensions')
|
||||
def dimensions():
|
||||
"""Return current program size in plotter units (e.g. 0.025 mm per unit)."""
|
||||
p = _get_program()
|
||||
if p is None:
|
||||
return jsonify({'error': 'No file loaded'}), 404
|
||||
w, h = p.winsize
|
||||
return jsonify({
|
||||
'width': round(w, 1),
|
||||
'height': round(h, 1),
|
||||
'width_mm': round(w * 0.025, 2),
|
||||
'height_mm': round(h * 0.025, 2),
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/svg')
|
||||
def get_svg():
|
||||
p = _get_program()
|
||||
if p is None:
|
||||
return jsonify({'error': 'No file loaded'}), 404
|
||||
width = request.args.get('width', 800, type=int)
|
||||
height = request.args.get('height', 600, type=int)
|
||||
show_scale = request.args.get('scale', '1').lower() in ('1', 'true', 'yes')
|
||||
svg = program_to_svg(p, width=width, height=height, show_scale_bar=show_scale)
|
||||
return svg, 200, {'Content-Type': 'image/svg+xml; charset=utf-8'}
|
||||
|
||||
|
||||
@app.route('/api/transform', methods=['POST'])
|
||||
def transform():
|
||||
p = _get_program()
|
||||
if p is None:
|
||||
return jsonify({'error': 'No file loaded'}), 404
|
||||
data = request.get_json() or {}
|
||||
action = data.get('action')
|
||||
if action == 'flip':
|
||||
p = p.flip()
|
||||
elif action == 'rotate':
|
||||
angle = data.get('angle', 90)
|
||||
p = p.rotate(angle)
|
||||
elif action == 'scale':
|
||||
factor = data.get('factor', 1.5)
|
||||
p = p * factor
|
||||
elif action == 'centralize':
|
||||
p = p - p.center
|
||||
elif action == 'scale_to_bounds':
|
||||
xmin = data.get('xmin')
|
||||
ymin = data.get('ymin')
|
||||
xmax = data.get('xmax')
|
||||
ymax = data.get('ymax')
|
||||
if xmin is not None and ymin is not None and xmax is not None and ymax is not None:
|
||||
if xmax <= xmin or ymax <= ymin:
|
||||
return jsonify({'error': 'Invalid bounds: xmax > xmin and ymax > ymin required.'}), 400
|
||||
p = p.fitin((xmin, ymin, xmax, ymax), (0.5, 0.5))
|
||||
else:
|
||||
try:
|
||||
plt = Plotter()
|
||||
if getattr(plt, 'boundaries', None) is None:
|
||||
return jsonify({'error': 'Plotter not connected. Connect plotter or send bounds (xmin, ymin, xmax, ymax).'}), 400
|
||||
p = plt.full(p)
|
||||
except (OSError, AttributeError, TypeError):
|
||||
return jsonify({'error': 'Plotter not connected. Connect plotter or send bounds (xmin, ymin, xmax, ymax).'}), 400
|
||||
else:
|
||||
return jsonify({'error': 'Unknown action'}), 400
|
||||
_save_program(p)
|
||||
return jsonify({'ok': True, 'preview_url': '/api/svg'})
|
||||
|
||||
|
||||
@app.route('/api/print', methods=['POST'])
|
||||
def print_to_plotter():
|
||||
p = _get_program()
|
||||
if p is None:
|
||||
return jsonify({'error': 'No file loaded'}), 404
|
||||
try:
|
||||
plt = Plotter()
|
||||
if getattr(plt, 'ser', None) is None:
|
||||
return jsonify({'error': 'Plotter not connected. Connect plotter and try again.'}), 503
|
||||
p = plt.full(p)
|
||||
p = plt.centralize(p)
|
||||
plt.write(p)
|
||||
return jsonify({'ok': True})
|
||||
except OSError as e:
|
||||
return jsonify({'error': 'Plotter not connected: {}'.format(str(e))}), 503
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/status')
|
||||
def status():
|
||||
has_file = bool(session.get('upload_path')) and os.path.isfile(session.get('upload_path'))
|
||||
plotter_ready = False
|
||||
try:
|
||||
plt = Plotter()
|
||||
if getattr(plt, 'ser', None) is not None and getattr(plt, 'boundaries', None) is not None:
|
||||
plotter_ready = plt.ready
|
||||
except (OSError, AttributeError, TypeError):
|
||||
pass
|
||||
return jsonify({
|
||||
'has_file': has_file,
|
||||
'filename': session.get('upload_filename'),
|
||||
'plotter_ready': plotter_ready
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/plotter_info')
|
||||
def plotter_info():
|
||||
"""Return plotter connection and media dimensions for display under the print button."""
|
||||
try:
|
||||
from config import plotter_port, plotter_baudrate
|
||||
except ImportError:
|
||||
plotter_port = lambda: '/dev/ttyUSB0'
|
||||
plotter_baudrate = lambda: 9600
|
||||
port = plotter_port()
|
||||
baudrate = plotter_baudrate()
|
||||
connected = False
|
||||
ready = False
|
||||
boundaries = None
|
||||
winsize_units = None
|
||||
winsize_mm = None
|
||||
try:
|
||||
plt = Plotter()
|
||||
if getattr(plt, 'ser', None) is not None and getattr(plt, 'boundaries', None) is not None:
|
||||
connected = True
|
||||
ready = plt.ready
|
||||
try:
|
||||
boundaries = {
|
||||
'xmin': plt.xmin,
|
||||
'ymin': plt.ymin,
|
||||
'xmax': plt.xmax,
|
||||
'ymax': plt.ymax,
|
||||
}
|
||||
w, h = plt.winsize
|
||||
winsize_units = {'width': round(w, 1), 'height': round(h, 1)}
|
||||
winsize_mm = {'width': round(w * 0.025, 1), 'height': round(h * 0.025, 1)}
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
except (OSError, AttributeError, TypeError):
|
||||
pass
|
||||
return jsonify({
|
||||
'port': port,
|
||||
'baudrate': baudrate,
|
||||
'connected': connected,
|
||||
'ready': ready,
|
||||
'boundaries': boundaries,
|
||||
'winsize_units': winsize_units,
|
||||
'winsize_mm': winsize_mm,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
from config import webui_host, webui_port
|
||||
except ImportError:
|
||||
webui_host = lambda: '0.0.0.0'
|
||||
webui_port = lambda: 5000
|
||||
app.run(
|
||||
host=webui_host(),
|
||||
port=webui_port(),
|
||||
debug=os.environ.get('FLASK_DEBUG', '0') == '1'
|
||||
)
|
||||
Reference in New Issue
Block a user