-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6c962a7
commit 3d6584e
Showing
6 changed files
with
222 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,153 +1,23 @@ | ||
import base64 | ||
from flask import Flask, abort, render_template, make_response, Response | ||
import threading | ||
from flask import Flask | ||
from flask_limiter import Limiter | ||
from flask_limiter.util import get_remote_address | ||
import redis | ||
from flask_socketio import SocketIO, emit | ||
from flask_socketio import SocketIO | ||
from flask_cors import CORS | ||
from typing import List, Tuple | ||
|
||
# Connect to Redis | ||
r = redis.Redis(host='localhost', port=6379, db=0) | ||
|
||
app = Flask(__name__) | ||
CORS(app) # Allow cross-origin requests from any domain | ||
socketio = SocketIO(app, cors_allowed_origins="*") # Allow all origins for WebSocket connections | ||
limiter = Limiter(key_func=get_remote_address) | ||
|
||
CACHE_DURATION = 60 # Cache duration in seconds | ||
from .routes import register_routes | ||
from .sockets import register_sockets | ||
|
||
def get_labels() -> List[str]: | ||
""" | ||
Retrieve and decode all unique labels from Redis keys. | ||
Returns: | ||
list: Sorted list of unique labels. | ||
""" | ||
cursor, keys = r.scan() | ||
decoded_keys = [key.decode('utf-8') for key in keys] | ||
labels = {key.split('_')[0] for key in decoded_keys if key.count('_') == 1 and not key.startswith('_') and not key.endswith('_')} | ||
return sorted(labels) | ||
|
||
def get_image_data(label: str) -> List[Tuple[str, str]]: | ||
""" | ||
Retrieve and process image data for a specific label. | ||
Args: | ||
label (str): The label/category of the images. | ||
Returns: | ||
list: List of tuples containing base64 encoded images and their names. | ||
""" | ||
cursor, keys = r.scan(match=f'{label}_*') | ||
image_data = [(base64.b64encode(r.get(key)).decode('utf-8'), key.decode('utf-8').split('_')[1]) for key in keys] | ||
return sorted(image_data, key=lambda x: x[1]) | ||
|
||
@app.route('/') | ||
@limiter.limit("60 per minute") | ||
def index() -> str: | ||
""" | ||
Serve the index page with a dynamically generated list of labels from Redis. | ||
Returns: | ||
str: The rendered 'index.html' page. | ||
""" | ||
labels = get_labels() | ||
return render_template('index.html', labels=labels) | ||
|
||
@app.route('/label/<label>', methods=['GET']) | ||
@limiter.limit("60 per minute") | ||
def label_page(label: str) -> str: | ||
""" | ||
Serve a page for each label, displaying images under that label. | ||
Args: | ||
label (str): The label/category of the images. | ||
Returns: | ||
str: The rendered 'label.html' page for the specific label. | ||
""" | ||
image_data = get_image_data(label) | ||
return render_template('label.html', label=label, image_data=image_data) | ||
|
||
@socketio.on('connect') | ||
def handle_connect() -> None: | ||
""" | ||
Handle client connection to the WebSocket. | ||
""" | ||
emit('message', {'data': 'Connected'}) | ||
socketio.start_background_task(update_images) | ||
|
||
@socketio.on('disconnect') | ||
def handle_disconnect() -> None: | ||
""" | ||
Handle client disconnection from the WebSocket. | ||
""" | ||
print('Client disconnected') | ||
|
||
@socketio.on('error') | ||
def handle_error(e: Exception) -> None: | ||
""" | ||
Handle errors in WebSocket communication. | ||
Args: | ||
e (Exception): The exception that occurred. | ||
""" | ||
print(f'Error: {str(e)}') | ||
|
||
def update_images() -> None: | ||
""" | ||
Update images every 10 seconds by scanning Redis and emitting updates to clients. | ||
""" | ||
while True: | ||
socketio.sleep(10) # Execute every 20 seconds | ||
labels = get_labels() | ||
for label in labels: | ||
image_data = get_image_data(label) | ||
socketio.emit('update', {'label': label, 'images': [img for img, _ in image_data], 'image_names': [name for _, name in image_data]}) | ||
|
||
@app.route('/image/<label>/<filename>.png') | ||
@limiter.limit("60 per minute") | ||
def image(label: str, filename: str) -> Response: | ||
""" | ||
Serve an image file from Redis. | ||
Args: | ||
label (str): The label/category of the image. | ||
filename (str): The filename of the image. | ||
Returns: | ||
Response: The image file as a response. | ||
""" | ||
redis_key = f"{label}_{filename}" | ||
img_encoded = r.get(redis_key) | ||
|
||
if img_encoded is None: | ||
abort(404, description="Resource not found") | ||
|
||
response = make_response(img_encoded) | ||
response.headers.set('Content-Type', 'image/png') | ||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' | ||
response.headers['Pragma'] = 'no-cache' | ||
response.headers['Expires'] = '0' | ||
|
||
return response | ||
|
||
@app.route('/camera/<label>/<camera_id>') | ||
@limiter.limit("60 per minute") | ||
def camera_page(label: str, camera_id: str) -> str: | ||
""" | ||
Serve a page for viewing the camera stream. | ||
Args: | ||
label (str): The label/category of the camera. | ||
camera_id (str): The specific ID of the camera to view. | ||
Returns: | ||
str: The rendered 'camera.html' page for the specific camera. | ||
""" | ||
return render_template('camera.html', label=label, camera_id=camera_id) | ||
register_routes(app, limiter, r) | ||
register_sockets(socketio, r) | ||
|
||
if __name__ == '__main__': | ||
socketio.start_background_task(target=update_images) | ||
socketio.run(app, host='127.0.0.1', port=8000, debug=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from flask import Flask, render_template, abort, make_response, Response | ||
from flask_limiter import Limiter | ||
from .utils import get_labels, get_image_data | ||
|
||
def register_routes(app: Flask, limiter: Limiter, r) -> None: | ||
@app.route('/') | ||
@limiter.limit("60 per minute") | ||
def index() -> str: | ||
""" | ||
Serve the index page with a dynamically generated list of labels from Redis. | ||
Returns: | ||
str: The rendered 'index.html' page. | ||
""" | ||
labels = get_labels(r) | ||
return render_template('index.html', labels=labels) | ||
|
||
@app.route('/label/<label>', methods=['GET']) | ||
@limiter.limit("60 per minute") | ||
def label_page(label: str) -> str: | ||
""" | ||
Serve a page for each label, displaying images under that label. | ||
Args: | ||
label (str): The label/category of the images. | ||
Returns: | ||
str: The rendered 'label.html' page for the specific label. | ||
""" | ||
image_data = get_image_data(r, label) | ||
return render_template('label.html', label=label, image_data=image_data) | ||
|
||
@app.route('/image/<label>/<filename>.png') | ||
@limiter.limit("60 per minute") | ||
def image(label: str, filename: str) -> Response: | ||
""" | ||
Serve an image file from Redis. | ||
Args: | ||
label (str): The label/category of the image. | ||
filename (str): The filename of the image. | ||
Returns: | ||
Response: The image file as a response. | ||
""" | ||
redis_key = f"{label}_{filename}" | ||
img_encoded = r.get(redis_key) | ||
|
||
if img_encoded is None: | ||
abort(404, description="Resource not found") | ||
|
||
response = make_response(img_encoded) | ||
response.headers.set('Content-Type', 'image/png') | ||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' | ||
response.headers['Pragma'] = 'no-cache' | ||
response.headers['Expires'] = '0' | ||
|
||
return response | ||
|
||
@app.route('/camera/<label>/<camera_id>') | ||
@limiter.limit("60 per minute") | ||
def camera_page(label: str, camera_id: str) -> str: | ||
""" | ||
Serve a page for viewing the camera stream. | ||
Args: | ||
label (str): The label/category of the camera. | ||
camera_id (str): The specific ID of the camera to view. | ||
Returns: | ||
str: The rendered 'camera.html' page for the specific camera. | ||
""" | ||
return render_template('camera.html', label=label, camera_id=camera_id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from flask_socketio import SocketIO, emit | ||
from typing import Any | ||
from .utils import get_labels, get_image_data | ||
|
||
def register_sockets(socketio: SocketIO, r: Any) -> None: | ||
@socketio.on('connect') | ||
def handle_connect() -> None: | ||
""" | ||
Handle client connection to the WebSocket. | ||
""" | ||
emit('message', {'data': 'Connected'}) | ||
socketio.start_background_task(update_images, socketio, r) | ||
|
||
@socketio.on('disconnect') | ||
def handle_disconnect() -> None: | ||
""" | ||
Handle client disconnection from the WebSocket. | ||
""" | ||
print('Client disconnected') | ||
|
||
@socketio.on('error') | ||
def handle_error(e: Exception) -> None: | ||
""" | ||
Handle errors in WebSocket communication. | ||
Args: | ||
e (Exception): The exception that occurred. | ||
""" | ||
print(f'Error: {str(e)}') | ||
|
||
def update_images(socketio: SocketIO, r: Any) -> None: | ||
""" | ||
Update images every 10 seconds by scanning Redis and emitting updates to clients. | ||
Args: | ||
socketio (SocketIO): The SocketIO instance. | ||
r (Any): The Redis connection. | ||
""" | ||
while True: | ||
socketio.sleep(10) | ||
labels = get_labels(r) | ||
for label in labels: | ||
image_data = get_image_data(r, label) | ||
socketio.emit('update', {'label': label, 'images': [img for img, _ in image_data], 'image_names': [name for _, name in image_data]}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
$(document).ready(() => { | ||
// 自动检测当前页面协议,以决定 ws 还是 wss | ||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; | ||
// 创建 WebSocket 连接,并配置重连策略 | ||
const socket = io.connect(protocol + document.domain + ':' + location.port, { | ||
transports: ['websocket'], | ||
reconnectionAttempts: 5, // 最多重连尝试 5 次 | ||
reconnectionDelay: 2000 // 重连间隔为 2000 毫秒 | ||
}); | ||
|
||
// 获取当前页面的标签名 | ||
const currentPageLabel = $('h1').text(); // 假设页面的 <h1> 标签包含了当前的标签名称 | ||
|
||
socket.on('connect', () => { | ||
console.log('WebSocket connected!'); | ||
}); | ||
|
||
socket.on('connect_error', (error) => { | ||
console.error('WebSocket connection error:', error); | ||
}); | ||
|
||
socket.on('reconnect_attempt', () => { | ||
console.log('Attempting to reconnect...'); | ||
}); | ||
|
||
socket.on('update', (data) => { | ||
// 检查接收到的数据是否适用于当前页面的标签 | ||
if (data.label === currentPageLabel) { | ||
console.log('Received update for current label:', data.label); | ||
const fragment = document.createDocumentFragment(); | ||
data.images.forEach((image, index) => { | ||
const cameraDiv = $('<div>').addClass('camera'); | ||
const title = $('<h2>').text(data.image_names[index]); | ||
const img = $('<img>').attr('src', `data:image/png;base64,${image}`).attr('alt', `${data.label} image`); | ||
cameraDiv.append(title).append(img); | ||
fragment.appendChild(cameraDiv[0]); | ||
}); | ||
$('.camera-grid').empty().append(fragment); | ||
} else { | ||
console.log('Received update for different label:', data.label); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>{{ label }}</title> | ||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> | ||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.1.3/socket.io.min.js"></script> | ||
<script src="{{ url_for('static', filename='js/label.js') }}"></script> | ||
</head> | ||
<body> | ||
<h1>{{ label }}</h1> | ||
<div class="camera-grid"> | ||
{% for image, image_name in image_data %} | ||
<div class="camera"> | ||
<h2>{{ image_name }}</h2> | ||
<img src="data:image/png;base64,{{ image }}" alt="{{ label }} image"> | ||
</div> | ||
{% endfor %} | ||
</div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import base64 | ||
import redis | ||
from typing import List, Tuple | ||
|
||
def get_labels(r: redis.Redis) -> List[str]: | ||
""" | ||
Retrieve and decode all unique labels from Redis keys, excluding the label 'test'. | ||
Args: | ||
r (redis.Redis): The Redis connection. | ||
Returns: | ||
list: Sorted list of unique labels. | ||
""" | ||
cursor, keys = r.scan() | ||
decoded_keys = [key.decode('utf-8') for key in keys] | ||
labels = {key.split('_')[0] for key in decoded_keys if key.count('_') == 1 and not key.startswith('_') and not key.endswith('_') and key.split('_')[0] != 'test'} | ||
return sorted(labels) | ||
|
||
def get_image_data(r: redis.Redis, label: str) -> List[Tuple[str, str]]: | ||
""" | ||
Retrieve and process image data for a specific label. | ||
Args: | ||
r (redis.Redis): The Redis connection. | ||
label (str): The label/category of the images. | ||
Returns: | ||
list: List of tuples containing base64 encoded images and their names. | ||
""" | ||
cursor, keys = r.scan(match=f'{label}_*') | ||
image_data = [(base64.b64encode(r.get(key)).decode('utf-8'), key.decode('utf-8').split('_')[1]) for key in keys] | ||
return sorted(image_data, key=lambda x: x[1]) |