顯示具有 javascript 標籤的文章。 顯示所有文章
顯示具有 javascript 標籤的文章。 顯示所有文章

2024年2月17日 星期六

Raspberry Pi + Webcam 居家遠端監看

 



預計達成的目標是使用者透過手機或電腦上的瀏覽器連線到網站就可以遙控遠端的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 下載。

2014年7月8日 星期二

[Web] 自製 HTML5 2D Game & Engine


上面這個 demo 遊戲的網址在 這裡 還有另一個疊疊樂遊戲在 這裡
是由自製的 HTML5 2D Game Engine - KIKI 製作的
其實現在有不少好用的 HTML5 graphics library 或 game library 可以用
不過這是2011年左右的作品, 當時還沒有也還不知道這些 library
只是一頭熱的鑽下去研發了這個 HTML5 2D Game Engine

KIKI 是由 box2d for javascript 及 processingjs 為基礎進行開發
主要分成 遊戲引擊、關卡編輯器物件編輯器 三個部份

物件編輯器 主要負責遊戲物件動畫及碰撞範圍的編輯
由物件編輯器生成的遊戲物件檔就可以放入關卡編輯器裡


關卡編輯器 主要負責遊戲物件位置的擺放及圖層設定等等動作
還可以設定觸發事件,例如 demo game 裡的踩按鈕
除了 KIKI 內建幾個簡單的 trigger也可以為個別遊戲外掛 trigger
也支援 run-time mode 編輯完後可以按個
play 按鈕直接玩玩看

關卡編輯器
物件編輯器

 KIKI 雖然可以開發出現像 demo game 這樣的 platform game 不過功能還很陽春
若有人有興趣合作開發 web game 請與我聯絡,我還是願意開發下去。


2012年10月19日 星期五

[Web] jquery + processing.js

用 JQuery + JQuery UI + javascript + PHP + Processing.js

製作這個網站 - drawall



讓網友可以隨手塗鴉, 分享彼此的大作

也可以以四格連環漫畫的方式創作

主要的繪圖介面當然是以 Processing.js 來作



現在 chrome, firefox 對 HTML 5 支援度已經作得滿不錯了

其它的 UI 部份全部由 JQuery + JQuery UI 製作



以 JQuery 建構網是滿方便快速, 但可能有 performance issue

不過一樣的呈現方式, 以 javascript 來寫可能也會滿複雜的

用 JQuery 撰寫時間真的縮短許多

2012年3月12日 星期一

javascript read server-side file

使用 jQuery 是最簡單的方法
jQuery 是一套強大的 javascript library
以下簡單幾行就可以讀取 server-side text file

$.get('foo.txt', function(data) {
var myvar = data;
alert(data);
});

其實 jQuery 是用 ajax 發出 HTTP Request 向 web server
要求 HTTP Response, 所以如果 foo.txt 的內容是
    Hello
    World
則 myvar 內容只有 "Hello", 如果改寫為 foo.php
    <?php echo "Hello\nWorld"; ?>
就可以得到有斷行的 data 了

以下是 sample code

<html>
<head>
<meta charset="utf-8">
<title>Demo</title>
</head>
<body>
<a href="http://jquery.com/">jQuery</a>
<script src="jquery-1.7.1.min.js"></script>
<script>
$(document).ready(function(){
$.get('foo.php', function(data) {
var myvar = data;
alert(data);
});
});

</script>
</body>
</html>

BlueTea螢幕錄影程式

  螢幕錄影新選擇:簡單、方便、免費 現在的螢幕錄影工具多樣,但安裝麻煩、操作複雜讓人卻步。我們推出了一款全新的螢幕錄影程式,專為追求簡單和效率的你設計。 1. 免安裝 無需安裝程式。解壓縮後點兩下就可以開始使用 2. 可選取錄影範圍 自由選擇全螢幕、特定視窗或自定義區域,靈活應...