用樹莓派和Render構建一個物聯網安全攝像頭

用樹莓派和Render構建一個物聯網安全攝像頭

譯者 | 陳峻

如今,市場上的智慧監控攝像頭林林總總,它們往往對我們來說是一種看家護院的黑匣子,我們無法知曉其內部的工作機制。如果我們想一探究竟,則需要利用物聯網的相關知識,去自行搭建監控系統。下面,我將從客戶端、儀表板UI、以及伺服器端等方面,從硬體組裝和軟體部署入手,和您深入討論如何構建一個物聯網安全攝像頭。

1。構建的目標

我們希望新建立的家用智慧攝像頭監控系統,能夠實現如下四個方面:1。 透過運動檢測模組,系統會對檢測到的運動物體進行拍照。2。 可將影象儲存到遠端伺服器上。3。 透過訪問伺服器的儀表板,我們可以檢視所有事件,包括照片和時間戳。4。 以滑動視窗的形式,儲存最近20個事件,並清理所有舊的事件。

2。需要哪些元件?

硬體

一個Raspberry Pi(樹莓派) 4、一個運動檢測感測器、一個攝像頭模組,以及如下物料清單(BOM)中的各種小元件。

軟體

一個用於部署、儲存和顯示攝像頭影象的伺服器端Render。com帳戶。

Git、Python 3和一個程式碼編輯器。

3。配置Raspberry Pi

第1步:為Raspberry Pi插入可靠的電源。最好使用BOM中指定的官方版本。畢竟有訊息稱,一些舊的Raspberry Pi 4型號存在著一些USB-C電纜和電源的適配問題。

第2步:安裝Raspberry Pi OS(樹莓派作業系統)。從官網上獲取相關的指南和工具,包括如何使用SD卡等。

由於並不需要圖形化介面,因此我們可以安裝僅供專家使用的Raspberry Pi OS Lite版本。不過,如果您是首次使用Raspberry Pi進行開發的話,也可選用帶有桌面的64 位版本的Raspberry Pi OS。

第3步:測試PIR運動感測器,以檢測和捕捉房間中的運動物體。注意,感測器上有三根線,其中兩根用於電源(+5V和接地),第三根用於從感測器讀取數值,即:如果感測器檢測到移動,就讀取1,否則讀取0。請使用pinout命令,檢視Raspberry每個引腳的完整說明。

用樹莓派和Render構建一個物聯網安全攝像頭

在本例中,我們使用一根黑線將感測器的地線,連線到電路板的地線(PIN 6)上,一根紅線連線到+5V(PIN 2)上,並將訊號線連線到其中一個GPIO(PIN 11)上。下面的兩張影象展示了組裝的效果,當然,如果您不知道哪根線纜應當對應哪裡的話,請取下感測器上的蓋子,並仔細檢查PCB上的標籤。

用樹莓派和Render構建一個物聯網安全攝像頭

用樹莓派和Render構建一個物聯網安全攝像頭

4。檢測運動

為了檢測運動,我們需要透過軟體來讀取PIR的數值,併發送通知。GitHub上的 Python版本提供了針對此類應用的簡單版本,請參考如下程式碼段:

Python

from gpiozero import MotionSensorfrom datetime import datetimefrom signal import pausepir = MotionSensor(17)def capture():timestamp = datetime。now()。isoformat()print(‘%s Detected movement’ % timestamp)def not_moving():timestamp = datetime。now()。isoformat()print(‘%s All clear’ % timestamp)pir。when_motion = capturepir。when_no_motion = not_movingpause()1。2。3。4。5。6。7。8。9。10。11。12。13。

注意,由於我們的運動檢測器已插入GPIO17(儘管在物理板上,它對應的是引腳11),因此我們將17的值傳遞給MotionSensor(),並透過執行python pir_motion_sensor。py,來啟動之,以實現對PIR時間的調整。

為了避免過於頻繁地被運動觸發,內部計時器會阻止系統持續傳送運動訊號,因此感測器存在著雖然能夠每次檢測到運動,但可能不會去通知系統的風險。由於計時器的範圍是0-255秒(255是全部順時針方向,0為所有逆時針),因此根據我的經驗,只需將定時器配置在7-10秒之間,電位器便可以在幾乎水平的位置,逆時針地轉動。類似地,對於靈敏度電位器而言,順時針方向表示靈敏度更高。其對應的命令輸出會顯示如下:

Plain Textpi@raspberrypi:~/raspberry-pi-security-camera-client $ python pir_motion_sensor。py2022-04-21T15:35:35。275947 Detected movement2022-04-21T15:35:41。607265 All clear1。2。3。4。

5。新增攝像頭

在Raspberry Pi處於關閉狀態,以及斷開了與任何電源的連線時,我們將攝像頭安裝在右側。而在完成後,請重啟Raspberry Pi,並確保已擁有最新的攝像頭棧(camera stack)。

然後,請開啟控制檯並輸入如下內容:

Shell$ sudo raspi-config1。2。

請選擇“介面選項”選單。

用樹莓派和Render構建一個物聯網安全攝像頭

選擇“啟用/禁用傳統攝像頭支援”並確保將其已禁用。

用樹莓派和Render構建一個物聯網安全攝像頭

用樹莓派和Render構建一個物聯網安全攝像頭

最後,儲存並重新啟動。

6。Picamera2與Picamera

Picamera2是libcamera的新式Python埠。其對應的舊專案——Picamera雖然基於不同的系統,但是其接受度頗高。

7。測試攝像頭

為了測試攝像頭,我使用Picamera2建立了一個簡短的指令碼。鑑於Picamera2專案仍處於預覽階段,其安裝並不容易。下面,我們先執行example_picamera2。py指令碼,來驗證攝像頭是否已設定正確:

Shell$ python example_picamera2。py1。2。

而example_picamera2。py的具體內容如下:

Python

from gpiozeroimport MotionSensorfrompicamera2。picamera2 import *fromdatetime import datetimefromsignal import pausepir = MotionSensor(17)camera = Picamera2()camera。start_preview(Preview。NULL)config = camera。still_configuration()camera。configure(config)defcapture():camera。start()timestamp = datetime。now()。isoformat()print(‘%s Detected movement’ % timestamp)metadata = camera。capture_file(‘/home/pi/%s。jpg’ % timestamp)print(metadata)camera。stop()defnot_moving():timestamp = datetime。now()。isoformat()print(‘%s All clear’ % timestamp)pir。when_motion = capturepir。when_no_motion = not_movingpause()1。2。3。4。5。6。7。8。9。10。11。12。13。14。15。16。17。18。19。20。21。22。

每當移動檢測PIR感測器檢測到物體移動時,該檔案都會執行快照,並將影象放置在/home/pi目錄中,並保持檔名與攝像頭捕獲影象的時間相一致。下圖便是我的攝像頭所拍攝到的影象:

用樹莓派和Render構建一個物聯網安全攝像頭

至此,我們只完成了專案的一半,畢竟這些都是在本地實現的,並未透過物聯網進行遠端監控,更談不上防止有人訪問我們的Raspberry Pi、移除SD卡、並帶走監控記錄。

8。編寫客戶端程式碼並在本地進行測試

下面,我們準備在客戶端上實現以下軟體邏輯:

(1)使用Picamera2設定攝像頭。

(2)初始化運動感測器。

(3) 當檢測到物體運動時,讀取事件並呼叫以下函式:

a。 捕獲影象並將其儲存到本地檔案系統上的一個檔案中。

b。 將影象上傳到遠端伺服器上。

c。 如果上傳正確,則刪除本地檔案,以避免填滿Raspberry Pi上的所有空間。

(4) 當由於超時(在本例子中為6-7秒)而不再檢測到運動時,開始讀取事件,並打上帶有“All clear”訊息的時間戳。5。 等待下一個事件。

下面是對應的高級別(high-level)程式碼:

Python

def init(settings): camera = setup_camera() pir = MotionSensor(settings。get(‘PIR_GPIO’)) pir。when_motion = picture_when_motion(pir, camera, settings) pir。when_no_motion = not_movingpause()1。2。3。4。5。6。

其中,最複雜的函式是picture_when_motion。當裝置從非運動狀態變為運動狀態時,when_motion便會開始執行。我們可以設定為不接受其他引數,或僅接受單個強制引數。我將透過下面的程式碼,將其轉換為一個函式,並建立一個回撥(callback)來返回它。

Python

defpicture_when_motion(pir, camera, settings): setup_path(settings。get(‘IMG_PATH’)) def capture_and_upload_picture(): if camera: file_path = capture(camera, settings。get(‘IMG_PATH’)) server_settings = settings。get(‘SERVER’) uploaded = upload_picture(file_path, server_settings) if uploaded: cleanup(file_path) else: print(“Camera not defined”)return capture_and_upload_picture1。2。3。4。5。6。7。8。9。10。11。12。

上述程式碼中的捕獲函式類似於前面用於測試攝像頭的函式,而upload_picture函式是將軟體從本地轉換為物聯網應用的核心。下面讓我們來對其進行分析:Python

def upload_picture(file_path, server_settings):if server_settings。get(‘base_url’):url = urljoin(server_settings。get(‘base_url’), ‘upload’)if server_settings。get(‘user’) and server_settings。get(‘password’):user = server_settings。get(‘user’)password = server_settings。get(‘password’)files = {‘file’: open(file_path, ‘rb’)}print(‘Uploading file %s to URL: %s’ %(file_path, url))try:r = requests。post(url, files=files, auth=HTTPBasicAuth(user, password))image_path = r。json()。get(‘path’)except e:print(e)if not image_path or not r。ok:print(‘Error uploading image’)return Falseprint(‘Image available at: {}’。format(image_path))return True1。2。3。4。5。6。7。8。9。10。11。12。13。14。15。16。17。18。

理想情況下,我們讓伺服器使用使用者名稱和密碼的驗證方式,接受作為POST請求的負載檔案。其對應的命令為:

Shellcurl \-F “file=@/home/user/Desktop/test。jpg” \http://localhost:5000/upload1。2。3。4。

由於它們是使用開源的MIT許可證釋出的,因此您既可以隨意複製它們,也可以使用python main。py來執行之。

9。建立一個伺服器來儲存影象

針對儲存影象的伺服器,我們希望:

支援Python程式碼,特別是Flask。

無需RDBMS或複雜的資料庫,僅靠檔案系統來儲存影象。

提供如下簡單API的REST介面:

/upload 上傳影象。

/ 獲取所有影象的列表。

/cleanup 刪除舊的影象。

/download/下載單個影象。

安全的TLS連線。

在完成身份驗證的基礎上,允許Raspberry Pi上傳檔案。

自動部署。

支援安全的環境變數,可用於儲存使用者憑據。

低成本(不超過幾美元/月)。

GitHub上提供了在本地、或在伺服器上執行程式碼的相關說明。

10。Flask應用

Flask是一個簡單靈活的Python框架,可用於快速建立以REST API為主的Web應用。同時,我們可以將主要程式碼放在main。py檔案。首先,我們需要初始化Flask應用,並宣告身份驗證的方法。對此,我會宣告一個名為setup的函式,以讀取本地機器上的各種可用環境變數。同時,我也會建立一個包含了所有環境變數的。env檔案。接著,我聲明瞭一個verify_password函式,來驗證提供給伺服器的密碼是否正確。然後,我透過函式upload_file,來支援上傳新的檔案,並訪問/upload端點,將影象儲存在檔案系統中,其具體內容如下:

Python

defupload_file(): if request。method == ‘POST’: # check if the post request has the file part if ‘file’ not in request。files: flash(‘No file part’) return redirect(request。url) file = request。files[‘file’] # If the user does not select a file, the browser submits an # empty file without a filename。 if file。filename == ‘’: flash(‘No selected file’) return redirect(request。url) if file and allowed_file(file。filename): filename = secure_filename(file。filename) file。save(os。path。join(app。config[‘UPLOAD_FOLDER’], filename)) return jsonify(success=True, filename=filename, path=urljoin(request。host_url, url_for(‘download_file’, name=filename))) return ‘’‘ <!doctype html> Upload new File

Upload new File

’‘’1。2。3。4。5。6。7。8。9。10。11。12。13。14。15。16。17。18。19。20。21。22。23。24。25。

該函式在GET和POST模式下均可有效。其中,執行在POST中時,我們可以從文字客戶端、或其他應用程式處上傳檔案;而在GET模式下,我們則可以使用瀏覽器來實現。

用樹莓派和Render構建一個物聯網安全攝像頭

11。本地測試

雖然您可以直接在Raspberry Pi 4中測試伺服器,但是如果您有Linux或Mac系統,那麼配置和啟動它會更加容易。在本例中,我們首先需要建立一個。env檔案,並將其放在與應用程式相同的目錄中。。env檔案將會儲存伺服器如下所需的資訊:

用於管理Flask會話的金鑰

儲存影象的上傳資料夾

可接受影象的最大尺寸

用於身份驗證的使用者名稱和密碼

伺服器本身的端點URL

下面展示的是。env-example-local檔案的內容。您可以將其用作模板,複製、重新命名、並按需予以修改。

屬性檔案SECRET_KEY=‘change-this-to-something-unlikely-to-guess’UPLOAD_FOLDER = ‘。/img’MAX_CONTENT_LENGTH = 16000000USERNAME = ‘admin’PASSWORD = ‘change-this-to-your-unique-password’SERVER=‘http://127。0。0。0:5001/’1。2。3。4。5。6。7。

透過執行python main。py,伺服器將被啟動,並進入主動除錯模式,以便我們觀察到後臺發生的情況。

Shell$ python main。py* Serving Flask app ‘main’ (lazy loading)* Environment: productionWARNING: This is a development server。 Do not use it in a production deployment。Use a production WSGI server instead。* Debug mode: on* Running on all addresses (0。0。0。0)WARNING: This is a development server。 Do not use it in a production deployment。* Running on http://127。0。0。1:5001* Running on http://192。168。123。228:5001 (Press CTRL+C to quit)* Restarting with stat* Debugger is active!* Debugger PIN: 332-932-8291。2。3。4。5。6。7。8。9。10。11。12。13。14。

讓我們首先透過CURL的方式來測試上傳檔案。您也可以使用Postman之類的工具來進行測試。假設您想從路徑/Users/luca/Pictures/image。jpeg處上傳影象,請使用如下命令:

Shellcurl \-F “file=@/Users/luca/Pictures/image。jpeg” \-u ‘admin:password’ \‘http://127。0。0。1:5001/upload’{“filename”: “image。jpeg”,“path”: “http://127。0。0。1:5001/download/image。jpeg”,“success”: true}1。2。3。4。5。6。7。8。9。10。

下圖展示了上傳影象的請求已被成功受理。

用樹莓派和Render構建一個物聯網安全攝像頭

12。將伺服器部署到Render處

至此,我們可以將伺服器推送到一個真實、穩定且安全的環境中了。我們希望:

能夠在磁碟上直接儲存影象,而無需配置資料庫。

為了避免在伺服器上保留過多的影象,透過Cronjob執行某個API,以定期只保留最後20張影象。

簡單地使用基於。env檔案的金鑰和變數

13。註冊並配置首個服務

在註冊到平臺之前,我建議您透過單擊螢幕右上角的“分叉(fork)”按鈕,來分叉現有的GitHub儲存庫。

用樹莓派和Render構建一個物聯網安全攝像頭

接著,您可以在Github上完成註冊。

用樹莓派和Render構建一個物聯網安全攝像頭

然後,請從儀表板中選擇“新建Web服務”。

用樹莓派和Render構建一個物聯網安全攝像頭

並搜尋最近分叉的儲存庫(repo)。

用樹莓派和Render構建一個物聯網安全攝像頭

為了配置伺服器,您可以先選擇一個免費的入門計劃,並在後期按需選購永久性磁碟。其中會涉及到如下引數:

名稱:可自由選擇

環境:Python 3

地區:選擇離您最近的一個

分支:main

構建命令 pip install -r requirements。txt

啟動命令gunicorn main:app

用樹莓派和Render構建一個物聯網安全攝像頭

現在讓我們轉到介面的高階部分,以設定金鑰檔案。您可以將其命名為。env,並貼上以下的文字內容(您可以按需進行更改):

用樹莓派和Render構建一個物聯網安全攝像頭

用樹莓派和Render構建一個物聯網安全攝像頭

14。建立永久性磁碟

在Render上建立永久性磁碟並不難,我們完全可以使用介面來完成。您只需單擊左側的磁碟部分,為其選擇名稱和安裝路徑即可。例如:

名稱:影象

掛載路徑:/var/img

大小:1GB

用樹莓派和Render構建一個物聯網安全攝像頭

我們將會在“事件”選項卡中收到有關其狀態的通知。

用樹莓派和Render構建一個物聯網安全攝像頭

如果我們點選一個特定的事件,將能夠看到所有的細節。

用樹莓派和Render構建一個物聯網安全攝像頭

完成後,您將在頁面的頂部看到伺服器的URL。

現在,是時候開始從我們的Raspberry Pi客戶端處上傳一些真實的影象了。首先,我們需要更改Raspberry客戶端中的。env檔案。下面展示了其環境變數的資訊:

屬性檔案PIR_GPIO=17USERNAME=‘admin’PASSWORD=‘change-me-with-a-real-password-please’API_SERVER=‘https://your-api-address。onrender。com/’IMG_PATH=‘img’1。2。3。4。5。6。

接著,請使用Python 3啟動main。py的服務。

如果您在PIR感測器的區域內移動,攝像頭將會拍攝照片並將其上傳到伺服器上。我們可以透過獲取影象的URL,實現瀏覽器下載照片。

15。定期清理影象

為了避免在伺服器上儲存太多的影象,我人為地設定為最多保留20張。為此,我們需要建立一個額外的Cronjob服務,來定期呼叫API。

首先,我建立了一個名為/cleanup的伺服器路由,它會呼叫keep_last_images()函式。該函式的定義如下:

Shell$ curl-v -d ‘{“keep”: “20”}’ -H “Content-Type: application/json” -u ‘username:password’ -X POST http://127。0。0。1:5001/cleanup/1。2。

此函式會按照建立影象的時間對影象進行排序,並保留POST請求有效負載中所指示的X數量的影象。請使用如下命令測試CURL的執行效果:

Shell$ curl-v -d ‘{“keep”: “20”}’ -H “Content-Type: application/json” -u ‘username:password’ -X POST http://127。0。0。1:5001/cleanup/1。2。

透過定期(如每週)呼叫上述函式,我們將能夠清理所有比最近20張更舊的影象。

接著,我在Render的儀表板中建立了一個新的Cronjob服務。

用樹莓派和Render構建一個物聯網安全攝像頭

下面是針對Cronjob的設定:

名稱:清理舊檔案

地區:法蘭克福

時間表:4 5 * * 2(我使用https://crontab。guru/來建立正確的字串。)

命令:Python 3 auto_cleanup。py 20(最後一個引數是設定保留影象的數量)

構建命令:pip install -r requirements。txt(這是安裝所有依賴項所必需的)

分支:main

自動部署:是

Cronjob失敗通知:使用者帳戶的相關通知設定

用樹莓派和Render構建一個物聯網安全攝像頭

為了測試其效果,我們可以在Cronjob上手動觸發其執行,而無需等待真實的時間表,即:單擊頁面頂部的“觸發執行”按鈕即可。Render介面的儀表板會顯示如下資訊:

用樹莓派和Render構建一個物聯網安全攝像頭

16。建立事件的儀表板

為了實現對攝像頭的安全管理,我們可以使用list_files()函式查詢檔案系統,並按照建立日期列出所有的影象檔案。請參考如下程式碼段:

Python

# List endpoint, get an HTML page listing all the uploaded files link@app。route(‘/’)@auth。login_requireddef list_files():files = get_list_of_img_path(path=app。config[‘UPLOAD_FOLDER’], reverse=True)images_url = []for file in files:images_url。append(urljoin(request。host_url, url_for(‘download_file’, name=os。path。basename(file))))return render_template(‘imglist。html’, images_url=images_url)1。2。3。4。5。6。7。8。9。

上述函式會呼叫與作業系統相關的API,並返回按建立時間排序的檔案列表。接著,它會使用jinja模板,將資料返回到imglist。html檔案中。該檔案的基本部分為:

HTML

    {% for image in images_url %}
  • {{image}}
  • {% else %}
  • No images uploaded yet
  • {% endfor %}
1。2。3。4。5。6。7。8。

它會產生如下列表:

用樹莓派和Render構建一個物聯網安全攝像頭

17。在外出時檢視自己的儀表板

物聯網的好處不僅在於您可以安全地遠端儲存影象,而且能夠避免因有人竊取或損壞您的Raspberry Pi,而丟失資料。也就是說,您可以身處世界任何地方,透過使用伺服器。env檔案中記錄的使用者名稱和密碼,訪問並登入Render的完整URL,以檢視照片資料,並及時捕獲裝置前的運動事物。下面的一組照片來自我家的攝像頭。其中的最後一張記錄了我愛人對攝像頭進行測試的場景。

用樹莓派和Render構建一個物聯網安全攝像頭

用樹莓派和Render構建一個物聯網安全攝像頭

用樹莓派和Render構建一個物聯網安全攝像頭

18。小結

在上文中,我向您介紹瞭如何以端到端的方式,從硬體設定到伺服器部署,來建立一個廉價且實用的物聯網攝像頭應用。您可以使用Raspberry Pi 4、Python、Flask、Render等技術元件與服務,在短短几個小時內構建出具有遠端影象上傳功能的安全攝像頭。

原文連結:https://dzone。com/articles/iot-security-camera-with-rasbperry-and-render

譯者介紹

陳峻 (Julian Chen),51CTO社群編輯,具有十多年的IT專案實施經驗,善於對內外部資源與風險實施管控,專注傳播網路與資訊保安知識與經驗;持續以博文、專題和譯文等形式,分享前沿技術與新知;經常以線上、線下等方式,開展資訊保安類培訓與授課。