10.6. Xây dựng bảng điều khiển¶
API JSON của camera tốt cho curl, nhưng chủ nhân muốn trỏ điện thoại vào một URL và nhìn thấy thứ gì đó. Điều đó có nghĩa là phục vụ HTML, JS, và CSS từ camera cùng với API.
10.6.1. File tĩnh từ hệ thống file¶
microdot.Response.send_file() đọc một file từ hệ thống file của camera và ghi lại với Content-Type đúng được suy ra từ phần mở rộng. Một route bắt tất cả ở cuối bảng định tuyến gửi mọi thứ không khớp với route API:
@app.get('/<path:filename>')
async def static(request, filename):
if '..' in filename or filename.startswith('/'):
abort(403)
return Response.send_file('/sdcard/static/' + filename)
Bộ chuyển đổi <path:filename> từ trang trước khớp với các phân đoạn có chứa dấu gạch chéo -- app.js và css/site.css đều đi qua. Kiểm tra độ tỉnh táo hai dòng từ chối bất kỳ đường dẫn nào cố gắng thoát khỏi /sdcard/static/ bằng .. hoặc đường dẫn tuyệt đối -- nếu không một client tò mò có thể đọc ../../boot.py hoặc /flash/secrets.txt. send_file() bản thân nó không thực hiện bất kỳ việc làm sạch nào: nó mở bất kỳ đường dẫn nào bạn đưa cho nó.
10.6.2. Các file bảng điều khiển¶
Ba file trên camera trong /sdcard/static/:
<!-- /sdcard/static/index.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Backyard cam</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Backyard cam</h1>
<img id="stream" src="/stream.jpg">
<label>Threshold:
<input id="threshold" type="range" min="0" max="100">
<span id="threshold-value"></span>
</label>
<h2>Recent events</h2>
<ul id="events"></ul>
<script src="app.js"></script>
</body>
</html>
// /sdcard/static/app.js
const slider = document.getElementById('threshold');
const sliderValue = document.getElementById('threshold-value');
async function loadConfig() {
const r = await fetch('/config');
const cfg = await r.json();
slider.value = cfg.threshold;
sliderValue.textContent = cfg.threshold;
}
loadConfig();
slider.addEventListener('change', async () => {
sliderValue.textContent = slider.value;
await fetch('/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({threshold: parseInt(slider.value)}),
});
});
/* /sdcard/static/style.css */
body { font-family: sans-serif; max-width: 720px; margin: 1em auto; }
img#stream { width: 100%; }
HTML tải luồng MJPEG trực tiếp vào một <img> -- trình duyệt xử lý phản hồi multipart một cách trong suốt. Thanh trượt đọc ngưỡng từ /config khi tải trang và gửi giá trị mới lại mỗi khi thay đổi.
10.6.3. Kiểu MIME và nén¶
send_file() đọc phần mở rộng của file và chọn Content-Type từ microdot.Response.types_map. .html trở thành text/html, .js trở thành text/javascript, .css trở thành text/css, .jpg trở thành image/jpeg. Các phần mở rộng bạn đã thêm vào bảng điều khiển (.svg, .ico) đã được đăng ký. Bất kỳ thứ gì không xác định mặc định là application/octet-stream -- để thêm một ánh xạ mới, mở rộng Response.types_map một lần khi khởi động.
Đối với các tài nguyên bạn đã nén trước (style.css.gz, app.js.gz), hãy truyền compressed=True và microdot phục vụ file .gz với Content-Encoding: gzip. Trình duyệt giải nén trong suốt và bạn tiết kiệm một vài KB cho mỗi lần tải trang.
Chủ nhân bây giờ mở http://yard-cam.local/index.html và thấy nguồn cấp trực tiếp, ngưỡng hiện tại, và nhật ký sự kiện trống.