Library - Code
From RoboWiki
Return back to project page: Avalanche Cannon - Jakub Vojtek
Python code for the Web app library:
from flask import Flask, render_template, request, Response
from flask_socketio import SocketIO
from bs4 import BeautifulSoup
from blinker import signal
import cv2
app = Flask(__name__)
socketio = SocketIO(app)
button_click_signal = signal('button-click')
button_hold_signal = signal('button-hold')
image_click_signal = signal('image-click')
textfield_change_signal = signal('textfield-change')
file = None
video_feed_path = None
css = False
css_file_path = None
def run_app(html_file_path, host='0.0.0.0', port=5000):
global file
file = html_file_path
socketio.run(app, allow_unsafe_werkzeug=True, host=host, port=port, debug=True)
def read_html_file(file_path):
with open(file_path, 'r') as file:
html_content = file.read()
return html_content
def add_event_listeners(html_content):
soup = BeautifulSoup(html_content, 'html.parser')
buttons = soup.find_all('button', {'id': True})
for button in buttons:
button_id = button['id']
button['onmousedown'] = f"sendButtonHold('{button_id}')"
button['onmouseup'] = f"sendButtonClick('{button_id}')"
text_fields = soup.find_all('input', {'type': 'text', 'id': True})
for text_field in text_fields:
text_field_id = text_field['id']
text_field['onchange'] = f"sendTextFieldContent('{text_field_id}', this.value)"
images = soup.find_all('img', {'id': True})
for img in images:
img_id = img['id']
img['onclick'] = f"sendImageClick(event, '{img_id}')"
script_tag = soup.new_tag('script')
script_tag.string = """
function sendTextFieldContent(textFieldId, content) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/text_field_content', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({textFieldId: textFieldId, content: content}));
};
function sendButtonClick(buttonId) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/button_click', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({buttonId: buttonId}));
};
function sendButtonHold(buttonId) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/button_hold', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({buttonId: buttonId}));
};
function sendImageClick(event, imgId) {
var img = document.getElementById(imgId);
var rect = img.getBoundingClientRect();
var x = Math.round(event.clientX - rect.left);
var y = Math.round(event.clientY - rect.top);
var position = "(" + x + ", " + y + ")";
var xhr = new XMLHttpRequest();
xhr.open('POST', '/image_click', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({imgId: imgId, position: position}));
}
"""
soup.head.append(script_tag)
canvases = soup.find_all('canvas')
for canvas in canvases:
canvas_id = canvas['id']
container_div = soup.new_tag('div')
container_div['class'] = 'canvas-container'
canvas.insert_before(container_div)
container_div.append(canvas)
buttons_div = soup.new_tag('div')
buttons_div['class'] = 'canvas-buttons'
# create buttons for drawing functionalities
free_draw_btn = soup.new_tag('button')
free_draw_btn['id'] = f'{canvas_id}-freeDrawBtn'
free_draw_btn.string = 'Free Draw'
buttons_div.append(free_draw_btn)
line_draw_btn = soup.new_tag('button')
line_draw_btn['id'] = f'{canvas_id}-lineDrawBtn'
line_draw_btn.string = 'Draw Line'
buttons_div.append(line_draw_btn)
rectangle_draw_btn = soup.new_tag('button')
rectangle_draw_btn['id'] = f'{canvas_id}-rectangleDrawBtn'
rectangle_draw_btn.string = 'Draw Rectangle'
buttons_div.append(rectangle_draw_btn)
filled_rectangle_draw_btn = soup.new_tag('button')
filled_rectangle_draw_btn['id'] = f'{canvas_id}-filledRectangleDrawBtn'
filled_rectangle_draw_btn.string = 'Draw Filled Rectangle'
buttons_div.append(filled_rectangle_draw_btn)
oval_draw_btn = soup.new_tag('button')
oval_draw_btn['id'] = f'{canvas_id}-ovalDrawBtn'
oval_draw_btn.string = 'Draw Oval'
buttons_div.append(oval_draw_btn)
filled_oval_draw_btn = soup.new_tag('button')
filled_oval_draw_btn['id'] = f'{canvas_id}-filledOvalDrawBtn'
filled_oval_draw_btn.string = 'Draw Filled Oval'
buttons_div.append(filled_oval_draw_btn)
clear_btn = soup.new_tag('button')
clear_btn['id'] = f'{canvas_id}-clearBtn'
clear_btn.string = 'Clear'
buttons_div.append(clear_btn)
container_div.append(buttons_div)
socket_script_tag = soup.new_tag('script',
src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js",
integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==",
crossorigin="anonymous")
soup.head.append(socket_script_tag)
# javascript code for drawing functionalities
canvas_script = f"""
<script type="text/javascript" charset="utf-8">
var socket = io();
socket.on('connect', function() {{
console.log("socket connected");
}});
var canvas = document.getElementById("{canvas_id}");
var ctx_{canvas_id} = canvas.getContext("2d");
var selectedMode = "freeDraw";
var startPoint = null;
var isDrawing = false;
var lastX = 0;
var lastY = 0;
document.getElementById("{canvas_id}-freeDrawBtn").addEventListener("click", function() {{
selectedMode = "freeDraw";
startPoint = null;
}});
document.getElementById("{canvas_id}-lineDrawBtn").addEventListener("click", function() {{
selectedMode = "drawLine";
startPoint = null;
}});
document.getElementById("{canvas_id}-rectangleDrawBtn").addEventListener("click", function() {{
selectedMode = "drawRectangle";
startPoint = null;
}});
document.getElementById("{canvas_id}-filledRectangleDrawBtn").addEventListener("click", function() {{
selectedMode = "drawFilledRectangle";
startPoint = null;
}});
document.getElementById("{canvas_id}-ovalDrawBtn").addEventListener("click", function() {{
selectedMode = "drawOval";
startPoint = null;
}});
document.getElementById("{canvas_id}-filledOvalDrawBtn").addEventListener("click", function() {{
selectedMode = "drawFilledOval";
startPoint = null;
}});
document.getElementById("{canvas_id}-clearBtn").addEventListener("click", function() {{
var canvas = document.getElementById("{canvas_id}");
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
selectedMode = "freeDraw";
}});
canvas.addEventListener("mousedown", function(e) {{
if (selectedMode === "freeDraw") {{
isDrawing = true;
[lastX, lastY] = [e.offsetX, e.offsetY];
}}
}});
canvas.addEventListener("mousemove", function(e) {{
if (isDrawing && selectedMode === "freeDraw") {{
ctx_{canvas_id}.beginPath();
ctx_{canvas_id}.moveTo(lastX, lastY);
ctx_{canvas_id}.lineTo(e.offsetX, e.offsetY);
ctx_{canvas_id}.strokeStyle = "black";
ctx_{canvas_id}.lineWidth = 2;
ctx_{canvas_id}.stroke();
[lastX, lastY] = [e.offsetX, e.offsetY];
}}
}});
canvas.addEventListener("mouseup", function() {{
isDrawing = false;
}});
canvas.addEventListener("mouseout", function() {{
isDrawing = false;
}});
canvas.addEventListener("click", function(e) {{
if (selectedMode === "drawLine") {{
if (!startPoint) {{
startPoint = {{ x: e.offsetX, y: e.offsetY }};
}} else {{
drawLine(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
startPoint = null;
}}
}} else if (selectedMode === "drawRectangle") {{
if (!startPoint) {{
startPoint = {{ x: e.offsetX, y: e.offsetY }};
}} else {{
drawRectangle(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
startPoint = null;
}}
}} else if (selectedMode === "drawFilledRectangle") {{
if (!startPoint) {{
startPoint = {{ x: e.offsetX, y: e.offsetY }};
}} else {{
drawFilledRectangle(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
startPoint = null;
}}
}} else if (selectedMode === "drawOval") {{
if (!startPoint) {{
startPoint = {{ x: e.offsetX, y: e.offsetY }};
}} else {{
drawOval(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
startPoint = null;
}}
}} else if (selectedMode === "drawFilledOval") {{
if (!startPoint) {{
startPoint = {{ x: e.offsetX, y: e.offsetY }};
}} else {{
drawFilledOval(ctx_{canvas_id}, startPoint.x, startPoint.y, e.offsetX, e.offsetY);
startPoint = null;
}}
}}
}});
function drawLine(ctx, x1, y1, x2, y2, color = "black") {{
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
}}
function drawRectangle(ctx, x1, y1, x2, y2, color = "black") {{
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
}}
function drawFilledRectangle(ctx, x1, y1, x2, y2, color = "black") {{
ctx.fillStyle = color;
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
}}
function drawOval(ctx, x1, y1, x2, y2, color = "black") {{
var radiusX = Math.abs(x2 - x1) / 2;
var radiusY = Math.abs(y2 - y1) / 2;
var centerX = Math.min(x1, x2) + radiusX;
var centerY = Math.min(y1, y2) + radiusY;
ctx.beginPath();
ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
}}
function drawFilledOval(ctx, x1, y1, x2, y2, color = "black") {{
var radiusX = Math.abs(x2 - x1) / 2;
var radiusY = Math.abs(y2 - y1) / 2;
var centerX = Math.min(x1, x2) + radiusX;
var centerY = Math.min(y1, y2) + radiusY;
ctx.beginPath();
ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}}
socket.on('draw_line', function(data) {{
eval(data.script);
}});
socket.on('draw_oval', function(data) {{
eval(data.script);
}});
socket.on('draw_rectangle', function(data) {{
eval(data.script);
}});
socket.on('add_text', function(data) {{
eval(data.script);
}});
</script>
"""
soup.body.append(BeautifulSoup(canvas_script, 'html.parser'))
buttons_div.insert_after(script_tag)
link_tag = soup.new_tag('link', rel='stylesheet', type='text/css', href='/static/styles.css')
soup.head.append(link_tag)
socket_script_tag = soup.new_tag('script',
src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js",
integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==",
crossorigin="anonymous")
soup.head.append(socket_script_tag)
socket_script_tag = """
<script type="text/javascript" charset="utf-8">
var socket = io();
socket.on('connect', function() {
socket.emit('my event', {data: 'Im connected!'});
console.log("socket connected");
});
function changeLabelText(labelId, newText) {
var label = document.getElementById(labelId);
if (label) {
label.innerText = newText;
}
}
socket.on('change_label', function(data) {
eval(data.script); // Execute the provided script
});
</script>
"""
soup.head.append(BeautifulSoup(socket_script_tag, 'html.parser'))
return str(soup)
def set_img_src_to_camera(img_src, camera_index=0):
global video_feed_path
video_feed_path = img_src
if video_feed_path is not None:
@app.route(video_feed_path)
def video_feed():
print("Waiting for camera response")
return Response(generate_video_frame(camera_index), mimetype='multipart/x-mixed-replace; boundary=frame')
def generate_video_frame(camera_index):
print("Generating camera feed")
camera = cv2.VideoCapture(camera_index)
while True:
success, frame = camera.read()
if not success:
break
else:
ret, buffer = cv2.imencode('.jpg', frame)
frame = buffer.tobytes()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
camera.release()
cv2.destroyAllWindows()
def add_css_file(css_path):
global css_file_path
css_file_path = css_path
def add_css(html_content):
soup = BeautifulSoup(html_content, 'html.parser')
link_tag = soup.new_tag('link', rel='stylesheet', type='text/css', href=f'{css_file_path}')
soup.head.append(link_tag)
return str(soup)
@app.route('/')
def index():
if file is not None:
html_file_path = file
html_content = read_html_file(html_file_path)
if css_file_path is not None:
modified_html_content = add_css(html_content)
else:
modified_html_content = html_content
modified_html_content = add_event_listeners(modified_html_content)
return modified_html_content
else:
return "File path not provided"
@app.route('/button_click', methods=['POST'])
def button_click():
button_id = request.json['buttonId']
button_click_signal.send(app, button_id=button_id)
return "Button clicked"
@app.route('/button_hold', methods=['POST'])
def button_hold():
button_id = request.json['buttonId']
print(f"Button '{button_id}' is being held")
button_hold_signal.send(app, button_id=button_id)
return "Button hold event received"
@app.route('/text_field_content', methods=['POST'])
def text_field_content():
data = request.json
text_field_id = data['textFieldId']
content = data['content']
textfield_change_signal.send(app, text_field_id=text_field_id, content=content)
print(f"Content of text field with ID '{text_field_id}': {content}")
return "Text field content received"
@app.route('/image_click', methods=['POST'])
def image_click():
data = request.json
img_id = data['imgId']
position = data['position']
image_click_signal.send(app, img_id=img_id, position=position)
return "Image click data received"
@socketio.on('change_label_text')
def change_label_text(label_id, new_text):
script = f"changeLabelText('{label_id}', '{new_text}');"
socketio.emit('change_label', {'script': script})
@socketio.on('draw_line')
def draw_line(canvas_id, x1, y1, x2, y2, color='black'):
script = f"drawLine(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
socketio.emit('draw_line', {'script': script})
@socketio.on('draw_oval')
def draw_oval(canvas_id, x1, y1, x2, y2, filled=False, color='black'):
if filled:
script = f"drawOval(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
else:
script = f"drawFilledOval(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
socketio.emit('draw_oval', {'script': script})
@socketio.on('draw_rectangle')
def draw_rectangle(canvas_id, x1, y1, x2, y2, filled=False, color='black'):
if filled:
script = f"drawRectangle(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
else:
script = f"drawFilledRectangle(ctx_{canvas_id}, {x1}, {y1}, {x2}, {y2}, '{color}');"
socketio.emit('draw_rectangle', {'script': script})
@socketio.on('add_text')
def add_text(canvas_id, x, y, text, color='black', font='12px Arial'):
script = f"""
var canvas = document.getElementById("{canvas_id}");
var ctx = canvas.getContext("2d");
ctx.font = "{font}";
ctx.fillStyle = "{color}";
ctx.fillText("{text}", {x}, {y});
"""
socketio.emit('add_text', {'script': script})