預計達成的目標是使用者透過手機或電腦上的瀏覽器連線到網站就可以遙控遠端的Webcam 拍攝一張照片回傳到網頁上觀看。完整原始碼請到 Github 下載。 現在 open source 社群非常發達,不管是軟體還是硬體上,開發的平台或工具越來越多樣,像這樣的系統架構只要靠著各站點約一百行左右的程式碼就可以完成,真的是非常方便。這次實驗將會用到的技術包括:
Python, Node.js, HTML and Javascript.
首先,要能在 Raspberry Pi 3 上擷取一張照片,使用的 webcam 當然要支援 v4l2 驅動及 UVC 相容,這樣我們就可以很方便的用 opencv-python 模組來抓影像。
# snapshot.py
import cv2
def capture():
cap = cv2.VideoCapture(0)
ret, frame = cap.read()
cv2.imwrite("snap.jpg", frame)
cap.release()
capture()
接下來,我們需要一些網際網路及 TCP/IP 的觀念。
我們的 Raspberry Pi 在家中使用 WiFi 上網,屬於 Private Network,身在Public Network 的網站是無法直接透過 IP 位址找到 Raspberry Pi。所以我們要在Raspberry Pi 和網站中間保持一條 TCP 連線,當使用者按下拍照鈕後,網站可以透過這條連線通知 Raspberry Pi 進行拍照及上傳。
我採用 Node.js 開發 camera client 程式,當然也可以用 python 來實作,不過用Node.js 的話,程式會更加簡短。
// camera_client.js
const net = require('net');
const exec = require('child_process').exec;
const request = require('request');
const fs = require('fs');
const PORT = 18080; // web server tcp port
const HOST = '<web server ip>';
var client = new net.Socket();
client.connect(POST, HOST, function() {
console.log('Connected');
});
client.on('data', function(msg) {
if (msg == "snapshot") {
console.log('Snaphsot');
}
});
client.on('close', function() {
console.log('Connection closed');
client = null;
});
當 camera_client.js 收到 ‘snapshot’ 訊息後,就要執行 snapshot.py 來拍照並存成 snap.jpg 檔。
exec('python3 snapshot.py', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log('Snaphsot...done');
});
存檔完成後,我們可以用 request 模組,以 HTTP 協定的方式上傳 snap.jpg 檔。
var req = request.post(HOST + '/fileupload', function (err, resp, body) {
if (err) {
console.log('Error');
}
});
var form = req.form();
form.append('filetoupload', fs.createReadStream('snap.jpg'), {
name: 'snap.jpg'
});
以上在 Raspberry Pi 上的開發就完成了。
網站端的開發我一樣採用 Node.js,它可以很快速的建立一個簡單的 web server 提供使用者一個簡易的網頁,同時也可以接收 camera_client.js 的 TCP 連線。
首先我們建立一個 index.html 網頁提供使用者檢視照片及一個拍照按鈕。
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript">
...
function snapshot() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
setTimeout(reloadSnapshot, 1000);
}
};
xhttp.open("GET", "snapshot", true);
xhttp.send();
}
</script>
<body>
<img id="snap-img" src="snap.jpg" style="max-width:100%;height:auto;"/><br/>
<input id="snap-btn" type="button" value="Snapshot" onclick="snapshot()" />
</body>
</html>
這個網頁會向 web server 下載 snap.jpg 然後當使用者下載拍照鈕後,會用 AJAX發出 GET request 到 web server。接下來我們實作 camera_server.js 來處理及回應這兩個動作。
// camera_server.js
const net = require('net');
const http = require('http');
const fs = require('fs');
const formidable = require('formidable');
const HOST = '0.0.0.0';
const requestListener = function (req, res) {
if (req.url == '/') {
var html = fs.readFileSync('index.html');
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(html);
}
else if (req.url.startsWith('/snap.jpg')) {
if (!fs.existsSync('snap.jpg')) {
res.setHeader("Content-Type", "image/jpeg");
res.writeHead(400);
res.end();
return;
}
var img = fs.readFileSync('snap.jpg');
res.setHeader("Content-Type", "image/jpeg");
res.writeHead(200);
res.end(img);
}
...
}
const web = http.createServer(requestListener);
web.listen(8080, HOST, () => {
console.log('Web Server is running');
});
Node.js 強大的 http 模組讓我們一秒變身簡易 web server 我們可以建立 Request Listener 來處理所以網頁的 request。以 req.url 來判斷要回應 index.html 或 snap.jpg。
前面提到過 camera_client.js 要跟 camera_server.js 保持 TCP 連線,所以現在在 camera_server.js 加入一個 socket server 並開啟 TCP_PORT 等待 camera_client.js 連入。
var cameraClient = null;
const TCP_PORT = 8080;
var server = net.createServer();
server.on('connection', handleConnection);
server.listen(TCP_PORT, HOST, function() {
console.log('TCP Server is running');
});
function handleConnection(conn) {
cameraClient = conn;
var remoteAddress = conn.remoteAddress + ':' + conn.remotePort;
console.log('new client connection from %s', remoteAddress);
conn.on('data', onReceiveData);
conn.once('close', onConnClose);
conn.on('error', onConnError);
function onReceiveData(msg) {
};
function onConnClose() {
console.log('connection from %s closed', remoteAddress);
cameraClient = null;
};
function onConnError(err) {
console.log('connection from %s error', remoteAddress);
cameraClient = null;
};
}
接下來我們在 camera_server.js 的 Request Listener 收到 snapshot request 後,我們就可以送出 ‘snapshot’ 訊息到 camera_client.js。然後我用 formidable 模組處理 camera_client.js 上傳檔案的動作。
...
else if (req.url == '/snapshot') {
// send 'snapshot' message to camera_client.js
cameraClient.write('snapshot');
if (fs.existsSync('snap.jpg'))
fs.unlinkSync('snap.jpg');
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end();
}
else if (req.url == '/fileupload') {
var form = new formidable.IncomingForm();
form.parse(req, function (err, fields, files) {
var oldpath = files.filetoupload.path;
var newpath = './' + files.filetoupload.name;
fs.rename(oldpath, newpath, function (err) {
if (err) throw err;
res.write('File uploaded');
res.end();
});
});
}
...
最後比較麻煩的是,前端網頁在送出 snapshot GET request 後要如何知道新的影像己經上傳完成?我們可以在 index.html 中送出 ‘snapshot’ 訊息後設定一個 timer 固定一小段時間就去詢問 camera_server.js 上的 snap.jpg 是否更新了。
...
function reloadSnapshot() {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "reload", false);
xhttp.send();
if (xhttp.status != 200) {
setTimeout(reloadSnapshot, 1000);
return;
}
document.getElementById('snap-img').src = "snap.jpg?random="+new
Date().getTime();
}
...
我們用 setTimeout() 每秒送出 reload 訊息去詢問 camera_server.js 是否有更新影像,若有的話,這裡有個小技巧,用 snap.jpg?random=… 的方式強迫瀏覽器更新影像。
最後在 camera_server.js 的 Request Listener加入 reload 訊息處理。如果 snap.jpg 檔案不存在則回應 404。
...
else if (req.url == '/reload') {
if (!fs.existsSync('snap.jpg')) {
res.setHeader("Content-Type", "text/html");
res.writeHead(404);
res.end();
return;
}
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end();
}
...
到這裡,我們就用一百行左右的程式碼完成了簡易的遠端監看系統。在 Raspberry Pi 跟 web server 上分別啟動程式。
Web-Server# node camera-server.js
RPi# node camera-client.js
用瀏覽器連線到 http://<host_ip>:<web_port>/ 就可以開始使用囉。
完整原始碼請到 Github 下載。