From: jimmy Date: Fri, 28 Jun 2024 01:12:07 +0000 (-0500) Subject: esp32Camera webserver X-Git-Url: https://vault307.fbx.one/gitweb/esp32Cam.git/commitdiff_plain/refs/heads/master?ds=sidebyside esp32Camera webserver --- da4ce9a145bc01403a2c299ce5a77c2f1beb1eb9 diff --git a/Wifi.py b/Wifi.py new file mode 100644 index 0000000..c3a2e7c --- /dev/null +++ b/Wifi.py @@ -0,0 +1,75 @@ + +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#----------------------------------------------------------------------------- + +#Basic WiFi configuration: + +from time import sleep +import network + +class Sta: + + AP = "SSID" # change to your SSID + PWD = "PASSWORD" # cjange to your password + + def __init__(my, ap='', pwd=''): + network.WLAN(network.AP_IF).active(False) # disable access point + my.wlan = network.WLAN(network.STA_IF) + my.wlan.active(True) + if ap == '': + my.ap = Sta.AP + my.pwd = Sta.PWD + else: + my.ap = ap + my.pwd = pwd + + def connect(my, ap='', pwd=''): + if ap != '': + my.ap = ap + my.pwd = pwd + + if not my.wlan.isconnected(): + my.wlan.connect(my.ap, my.pwd) + + def status(my): + if my.wlan.isconnected(): + return my.wlan.ifconfig() + else: + return () + + def wait(my): + cnt = 30 + while cnt > 0: + print("Waiting ..." ) + con(my.ap, my.pwd) # Connect to an AP + if my.wlan.isconnected(): + print("Connected to %s" % my.ap) + print('network config:', my.wlan.ifconfig()) + cnt = 0 + else: + sleep(5) + cnt -= 5 + return + + def scan(my): + return my.wlan.scan() # Scan for available access points diff --git a/config.py b/config.py new file mode 100644 index 0000000..e1e8089 --- /dev/null +++ b/config.py @@ -0,0 +1,226 @@ + +#-camera configuration int keys ** DONT EDIT **--------------- +PIN_PWDN = const(0) # power-down +PIN_RESET = const(1) # reset +PIN_XCLK = const(2) +PIN_SIOD = const(3) # SDA +PIN_SIOC = const(4) # SCL + +PIN_D7 = const(5) +PIN_D6 = const(6) +PIN_D5 = const(7) +PIN_D4 = const(8) +PIN_D3 = const(9) +PIN_D2 = const(10) +PIN_D1 = const(11) +PIN_D0 = const(12) +PIN_VSYNC = const(13) +PIN_HREF = const(14) +PIN_PCLK = const(15) + +XCLK_MHZ = const(16) # camera machine clock +PIXFORMAT = const(17) # pixel format +FRAMESIZE = const(18) # framesize +JPEG_QUALITY= const(19) +FB_COUNT = const(20) # framebuffer count > 1 continuous mode (JPEG only) + +#------------------------------------------------------------- + +# OV2640 Boards configuration below (can edit - add your board) + +# AI-Thinker esp32-cam board +ai_thinker = {PIN_PWDN:32, + PIN_RESET:-1, + PIN_XCLK:0, + PIN_SIOD:26, + PIN_SIOC:27, + PIN_D7:35, + PIN_D6:34, + PIN_D5:39, + PIN_D4:36, + PIN_D3:21, + PIN_D2:19, + PIN_D1:18, + PIN_D0:5, + PIN_VSYNC:25, + PIN_HREF:23, + PIN_PCLK:22, + XCLK_MHZ:16, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:10, + FB_COUNT:1, +} + +# New 2022 esp32vrover dev +esp_eye = {PIN_PWDN:-1, + PIN_RESET:-1, + PIN_XCLK:4, + PIN_SIOD:18, + PIN_SIOC:23, + PIN_D7:36, + PIN_D6:37, + PIN_D5:38, + PIN_D4:39, + PIN_D3:35, + PIN_D2:14, + PIN_D1:13, + PIN_D0:34, + PIN_VSYNC:5, + PIN_HREF:27, + PIN_PCLK:25, + XCLK_MHZ:16, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:10, + FB_COUNT:1, +} + +# esp32 wrover dev +wrover_dev = {PIN_PWDN:32, + PIN_RESET:-1, + PIN_XCLK:21, + PIN_SIOD:26, + PIN_SIOC:27, + PIN_D7:35, + PIN_D6:34, + PIN_D5:39, + PIN_D4:36, + PIN_D3:19, + PIN_D2:18, + PIN_D1:5, + PIN_D0:4, + PIN_VSYNC:25, + PIN_HREF:23, + PIN_PCLK:22, + XCLK_MHZ:12, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:10, + FB_COUNT:1, +} + +# Test +wrover_test = {PIN_PWDN:-1, + PIN_RESET:-1, + PIN_XCLK:21, + PIN_SIOD:26, + PIN_SIOC:27, + PIN_D7:35, + PIN_D6:34, + PIN_D5:39, + PIN_D4:36, + PIN_D3:19, + PIN_D2:18, + PIN_D1:5, + PIN_D0:4, + PIN_VSYNC:25, + PIN_HREF:23, + PIN_PCLK:22, + XCLK_MHZ:12, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:10, + FB_COUNT:1, +} + +# Red Board (has internal clock for camera set at 12Mhz) +red_board = {PIN_PWDN:32, # + PIN_RESET:-1, + PIN_XCLK:-1, # internal sensor clock + PIN_SIOD:26, + PIN_SIOC:27, + PIN_D7:35, + PIN_D6:34, + PIN_D5:39, + PIN_D4:36, + PIN_D3:21, + PIN_D2:19, + PIN_D1:18, + PIN_D0:5, + PIN_VSYNC:25, + PIN_HREF:23, + PIN_PCLK:22, + XCLK_MHZ:12, # the board has 12Mhz intrenal clock (MUST set to 12) + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:10, + FB_COUNT:1, +} + +# XIAO ESP32S3 Sense Camera +xiao_s3_sense = {PIN_PWDN:-1, + PIN_RESET:-1, + PIN_XCLK:10, + PIN_SIOD:40, + PIN_SIOC:39, + PIN_D7:48, + PIN_D6:11, + PIN_D5:12, + PIN_D4:14, + PIN_D3:16, + PIN_D2:18, + PIN_D1:17, + PIN_D0:15, + PIN_VSYNC:38, + PIN_HREF:47, + PIN_PCLK:13, + XCLK_MHZ:14, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:12, + FB_COUNT:1, +} + +# LILYGO T-Camera esp32s3 V1.6 +lilygo_t_camera = {PIN_PWDN:-1, + PIN_RESET:39, + PIN_XCLK:38, + PIN_SIOD:5, + PIN_SIOC:4, + PIN_D7:9, + PIN_D6:10, + PIN_D5:11, + PIN_D4:13, + PIN_D3:21, + PIN_D2:48, + PIN_D1:47, + PIN_D0:14, + PIN_VSYNC:8, + PIN_HREF:18, + PIN_PCLK:12, + XCLK_MHZ:14, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:12, + FB_COUNT:2, +} + +# FREENOVE esp32s3 WROOM FNK0085 A1B0 +freenove_fnk0085 = {PIN_PWDN:-1, + PIN_RESET:-1, + PIN_XCLK:15, + PIN_SIOD:4, + PIN_SIOC:5, + PIN_D7:16, + PIN_D6:17, + PIN_D5:18, + PIN_D4:12, + PIN_D3:10, + PIN_D2:8, + PIN_D1:9, + PIN_D0:11, + PIN_VSYNC:6, + PIN_HREF:7, + PIN_PCLK:13, + XCLK_MHZ:14, + PIXFORMAT:5, + FRAMESIZE:10, + JPEG_QUALITY:12, + FB_COUNT:2, +} + +def configure(cam, config): + for key, val in config.items(): + # print(key, val) + cam.conf(key, val) \ No newline at end of file diff --git a/esp32camNOTES.odt b/esp32camNOTES.odt new file mode 100644 index 0000000..630fd1c Binary files /dev/null and b/esp32camNOTES.odt differ diff --git a/espcamserver.py b/espcamserver.py new file mode 100644 index 0000000..c75732e --- /dev/null +++ b/espcamserver.py @@ -0,0 +1,51 @@ +import network, socket, time +from secrets import secrets + +ssid=secrets['ssid'] +password = secrets['pw'] + +def connect(): + wlan=network.WLAN(network.STA_IF) + wlan.active(True) + wlan.connect(ssid,password) + while wlan.isconnected() == False: + print('Waiting for connection...') + time.sleep(1) + ip=wlan.ifconfig()[0] + print(f'Connected on {ip}') + return ip + +def open_socket(ip): + address=(ip,80) + connection=socket.socket() + connection.bind(address) + connection.listen(1) + return connection + +def get_html(html_name): + with open(html_name, 'r') as file: + html=file.read() + return html + +def serve(connection): + + while True: + client=connection.accept()[0] + request=client.recv(1024) + request=str(request) + try: + response=get_html('index.html') + request=request.split()[1] + except IndexError: + pass + + client.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n') + client.send(response) + client.close() + +try: + ip=connect() + connection=open_socket(ip) + serve(connection) +except KeyboardInterrupt: + pass \ No newline at end of file diff --git a/firmware/ESP32_GENERIC-SPIRAM-20231005-v1.21.0.bin b/firmware/ESP32_GENERIC-SPIRAM-20231005-v1.21.0.bin new file mode 100644 index 0000000..179f90b Binary files /dev/null and b/firmware/ESP32_GENERIC-SPIRAM-20231005-v1.21.0.bin differ diff --git a/firmware/esp32camAIThinkerfirmware.bin b/firmware/esp32camAIThinkerfirmware.bin new file mode 100644 index 0000000..c6e0b63 Binary files /dev/null and b/firmware/esp32camAIThinkerfirmware.bin differ diff --git a/help.py b/help.py new file mode 100644 index 0000000..0c62b09 --- /dev/null +++ b/help.py @@ -0,0 +1,119 @@ +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +Setting = { + 'pixformat':0, # 0:JPEG, 1:Grayscale (2bytes/pixel), 2:RGB565 + 'framesize':11, # 1:96x96, 2:160x120, 3:176x144, 4:240x176, 5:240x240 + # 6:320x240, 7:400x296, 8:480x320, 9:640x480, 10:800x600 + # 11:1024x768, 12:1280x720, 13:1280x1024, 14:1600x1200 + # 15:1920x1080, 16:720x1280, 17:864x1536, 18:2048x1536 + + 'quality':11, # [0,63] lower number means higher quality + 'contrast':0, # [-2,2] higher number higher contrast + 'saturation':0, # [-2,2] higher number higher saturation. -2 grayscale + 'brightness':0, # [-2,2] higher number higher brightness. 2 brightest + 'speffect':0, # 0:,no effect 1:negative, 2:black and white, 3:reddish, + # 4:greenish, 5:blue, 6:retro + + 'whitebalance':0, # 0:default, 1:sunny, 2:cloudy, 3:office, 4:home + 'aelevels':0, # [-2,2] AE Level: Automatic exposure + 'aecvalue':0, # [0,1200] AEC Value: Automatic exposure control + 'agcgain':0, # [0,30] AGC Gain: Automatic Gain Control +} + +def help(server): + c=Setting + return f""" +Autentication: + Login to server with a one-time password. + http://{server}/login/ + After login, only the authenticated client can connect to server. + + Logout from server. + http://{server}/logout + The client is no longer authenticated. Create new one-time password. + The password is shown in REPL. + Any web browser can now login using the password. + + NB! The server is open to all if auth.on=False + +Help: + Show this page + http://{server} + If auth.on=True then you need to login first. + +Streaming: + Webcam live streaming + http://{server}/webcam + To stop streaming just go to other URL e.g. http://{server}/snap + +Still photo: + Take picture + http://{server}/snap + + Take picture with LED flash + http://{server}/blitz + +HTML image view port rotation: + Rotate the with transform:rotate() in style defination. + http://{server}/rot/n + n is one these value [0,90,180,270,360]. You can display the + image/video taken by the camera in portrait or landscape mode by + rotating the image view port. Depending on your camera sensor and + the orientation of your board, http://{server}/rot/90 may show + portrait mode image on your browser. + +Camera setting: + pixformat - http://{server}/fmt/n + framesize - http://{server}/pix/n + quality - http://{server}/qua/n + contrast - http://{server}/con/n + saturation - http://{server}/sat/n + brightness - http://{server}/bri/n + speffect - http://{server}/spe/n + whitebalance - http://{server}/wbl/n + aelevels - http://{server}/ael/n + aecvalue - http://{server}/aec/n + agcgain - http://{server}/agc/n + + NB! n is integer value (can be negative). + See listing below for appropriate value for each setting. + +Camera current setting: + pixformat={c['pixformat']} # 0:JPEG, 1:Grayscale (2bytes/pixel), 2:RGB565 + framesize={c['framesize']} # 1:96x96, 2:160x120, 3:176x144, 4:240x176, 5:240x240 + # 6:320x240, 7:400x296, 8:480x320, 9:640x480, 10:800x600 + # 11:1024x768, 12:1280x720, 13:1280x1024, 14:1600x1200 + # 15:1920x1080, 16:720x1280, 17:864x1536, 18:2048x1536 + quality={c['quality']} # [10,63] lower number means higher quality + contrast={c['contrast']} # [-2,2] higher number higher contrast + saturation={c['saturation']} # [-2,2] higher number higher saturation. -2 grayscale + brightness={c['brightness']} # [-2,2] higher number higher brightness. 2 brightest + speffect={c['speffect']} # 0:,no effect 1:negative, 2:black and white, 3:reddish, + # 4:greenish, 5:blue, 6:retro + whitebalance={c['whitebalance']} # 0:default, 1:sunny, 2:cloudy, 3:office, 4:home + aelevels={c['aelevels']} # [-2,2] AE Level: Automatic exposure + aecvalue={c['aecvalue']} # [0,1200] AEC Value: Automatic exposure control + agcgain={c['agcgain']} # [0,30] AGC Gain: Automatic Gain Control + +""" \ No newline at end of file diff --git a/html.py b/html.py new file mode 100644 index 0000000..09694a7 --- /dev/null +++ b/html.py @@ -0,0 +1,117 @@ +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#----------------------------------------------------------------------------- + +# html.py MVC - This is the view V of MVC + +pg = { + # URL: /webcam -> /live, /snap -> /foto, /blitz -> /boto + 'foto':''' + + +ESP32 Camera + + + +
+ +
+ + +''', + 'favicon':''' + + + + + + + +''', + 'err':'''Sorry, I can not do that. +''', + 'none':'''Sorry, nothing there. +''', + 'no': '''Sorry, unauthorized. +''', + 'OK':'''OK! +''', +} + +hdr = { + # start page for streaming + # URL: /webcam, /snap, /blitz + 'foto': """HTTP/1.1 200 OK +Content-Type: text/html; charset=utf-8 +Connection: Closed +Content-Length: %d""", + # live stream - + # URL: /live + 'stream': """HTTP/1.1 200 OK +Content-Type: multipart/x-mixed-replace; boundary=frame +Connection: keep-alive +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +Expires: Thu, Jan 01 1970 00:00:00 GMT +Pragma: no-cache""", + # live stream - + # URL: + 'frame': """--frame +Content-Type: image/jpeg""", + # still picture - + # URL: /foto + 'pix': """HTTP/1.1 200 OK +Content-Type: image/jpeg +Content-Length: %d""", + # + 'pic': """HTTP/1.1 200 OK +Content-Type: image/jpeg""", + # no content error + # URL: all the rest + 'none': """HTTP/1.1 400 Bad Request +Content-Type: text/plain; charset=utf-8 +Connection: Closed +Content-Length: %d""", + # URL: /favicon.ico + 'favicon': """HTTP/1.1 200 OK +Content-Type: text/html; charset=utf-8 +Connection: Closed +Cache-Control: max-age=2592000, public +Content-Length: %d""", + # bad request error + # URL: all the rest + 'err': """HTTP/1.1 400 Bad Request +Content-Type: text/plain; charset=utf-8 +Connection: Closed +Content-Length: %d""", + # OK + # URL: all the rest + 'OK': """HTTP/1.1 200 OK +Content-Type: text/plain; charset=utf-8 +Connection: Closed +Content-Length: %d""", + # NO + # URL: not authenticated + 'NO': """HTTP/1.1 401 Unauthorized +Content-Type: text/plain; charset=utf-8 +Connection: Closed +Content-Length: %d""", +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..28944ca --- /dev/null +++ b/index.html @@ -0,0 +1,627 @@ + + + + + + ESP32 OV2640 + + + + + + + +
+ +
+ +
+ +
+
+
+ + + + diff --git a/site.py b/site.py new file mode 100644 index 0000000..d3b79fa --- /dev/null +++ b/site.py @@ -0,0 +1,226 @@ + +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#----------------------------------------------------------------------------- + +# site.py MVC - This is the model M of MVC + +from uos import urandom as ran +from machine import Pin +from html import pg, hdr +from help import Setting as cam +from help import help + +# Init global variables +rot='0' +flash_light=Pin(04,Pin.OUT) + +class auth: pass + +server='' +client='' + +def pwd(size=8): + alfa = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890' + return ''.join('testCam') + +# These will be set by server script as site.ip and site.camera +ip='' +camera=None + +app={} +def route(p): + def w(g): + app[p]=g + return w + +def OK(cs): + p=pg['OK'] + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['OK']%ln, p)) + +def ERR(cs): + p=pg['err'] + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['err']%ln, p)) + +def NO(cs): + p=pg['no'] + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['err']%ln, p)) + +def NOP(cs): + p=pg['none'] + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['none']%ln, p)) + +def setting(cs,w,ok,cmd,v): + #print("setting:", w, ok) + if ok: + cmd(w); cam[v]=w + OK(cs) + else: + ERR(cs) + +@route('/') +def root(cs,v): + p=help(server) + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['OK']%ln, p)) + OK(cs) + +@route('/login') +def login(cs,v): + if auth.on: + if auth.ip=='': + if v==auth.pwd: + auth.ip=client + OK(cs) + +@route('/logout') +def logout(cs,v): + if auth.on: + auth.pwd=pwd() + auth.ip='' + print(f'New PWD: {auth.pwd}') + OK(cs) + +@route('/favicon.ico') +def fav(cs,v): + p=pg['favicon'] + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['favicon']%ln, p)) + +@route('/webcam') +def webcam(cs,v): + global ip,rot + p=pg['foto']%(f'http://{ip}/live',rot) # needed by opera, vivaldi, midori.. + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['foto']%ln, p)) + +@route('/live') +def live(cs,v): # live stream + cs.write(b'%s\r\n\r\n' % hdr['stream']) + cs.setblocking(True) + pic=camera.capture + put=cs.write + hr=hdr['frame'] + while True: + try: + put(b'%s\r\n\r\n' % hr) + put(pic()) + put(b'\r\n') # send and flush the send buffer + except Exception as e: + print(e) + break + +@route('/snap') +def snap(cs,v): + global ip,rot + p=pg['foto']%(f'http://{ip}/foto',rot) + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['foto']%ln, p)) + +@route('/blitz') +def blitz(cs,v): + global ip,rot + p=pg['foto']%(f'http://{ip}/boto',rot) + ln=len(p)+2 + cs.write(b'%s\r\n\r\n%s\r\n' % (hdr['foto']%ln, p)) + +@route('/foto') +def foto(cs,v): # still photo + #buf=camera.capture() + #ln=len(buf) + cs.setblocking(True) + cs.write(b'%s\r\n\r\n' % hdr['pic']) + cs.write(camera.capture()) + cs.write(b'\r\n') # send and flush the send buffer + #nc=cs.write(b'%s\r\n\r\n' % (hdr['pix']%ln)+buf) + +@route('/boto') +def boto(cs,v): # still photo blitz on + #buf=camera.capture() + #ln=len(buf) + cs.setblocking(True) + cs.write(b'%s\r\n\r\n' % hdr['pic']) + flash_light.on() + cs.write(camera.capture()) + flash_light.off() + cs.write(b'\r\n') + #nc=cs.write(b'%s\r\n\r\n' % (hdr['pix']%ln)+buf) + +@route('/rot') +def rotate(cs,v): + global rot + rot=v + OK(cs) + +@route('/flash') +def flash(cs,v): + if v==1: flash_light.on() + else: flash_light.off() + OK(cs) + +@route('/fmt') +def fmt(cs,w): + setting(cs,w,(w>=1 and w<=9),camera.pixformat,'pixformat') + +@route('/pix') +def pix(cs,w): + setting(cs,w,(w>0 and w<18),camera.framesize,'framesize') + +@route('/qua') +def qua(cs,w): + setting(cs,w,(w>9 and w<64),camera.quality,'quality') + +@route('/con') +def con(cs,w): + setting(cs,w,(w>-3 and w<3),camera.contrast,'contrast') + +@route('/sat') +def sat(cs,w): + setting(cs,w,(w>-3 and w<3),camera.saturation,'saturation') + +@route('/bri') +def bri(cs,w): + setting(cs,w,(w>-3 and w<3),camera.brightness,'brightness') + +@route('/ael') +def ael(cs,w): + setting(cs,w,(w>-3 and w<3),camera.aelevels,'aelevels') + +@route('/aec') +def aec(cs,w): + setting(cs,w,(w>=0 and w<=1200),camera.aecvalue,'aecvalue') + +@route('/agc') +def agc(cs,w): + setting(cs,w,(w>=0 and w<=30),camera.agcgain,'agcgain') + +@route('/spe') +def spe(cs,w): + setting(cs,w,(w>=0 and w<7),camera.speffect,'speffect') + +@route('/wbl') +def wbl(cs,w): + setting(cs,w,(w>=0 and w<5),camera.whitebalance,'whitebalance') diff --git a/streaming_server.py b/streaming_server.py new file mode 100644 index 0000000..e14940b --- /dev/null +++ b/streaming_server.py @@ -0,0 +1,119 @@ +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#----------------------------------------------------------------------------- + +# run this on ESP32 Camera + +import esp +# from bluetooth import BLE +from Wifi import Sta +import socket as soc +import camera +from time import sleep + +hdr = { + # live stream - + # URL: /live + 'stream': """HTTP/1.1 200 OK +Content-Type: multipart/x-mixed-replace; boundary=kaki5 +Connection: keep-alive +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +Expires: Thu, Jan 01 1970 00:00:00 GMT +Pragma: no-cache""", + # live stream - + # URL: + 'frame': """--kaki5 +Content-Type: image/jpeg"""} + +UID = const('espCam') # authentication user. Whatever you want +PWD = const('doesThisWork?') # authentication password. Whatever you want + +cam = camera.init() # Camera +print("Camera ready?: ", cam) + +# connect to access point +sta = Sta() # Station mode (i.e. need WiFi router) +# sta.wlan.disconnect() # disconnect from previous connection +AP = const('DEA Surveillance Van 3') # Your SSID +PW = const('B72E8Hitron') # Your password +sta.connect(AP, PW) # connet to dlink +sta.wait() + +# wait for WiFi +con = () +for i in range(5): + if sta.wlan.isconnected():con=sta.status();break + else: print("WIFI not ready. Wait...");sleep(2) +else: + print("WIFI not ready") + +if con and cam: # WiFi and camera are ready + if cam: + # set preffered camera setting + camera.framesize(10) # frame size 800X600 (1.33 espect ratio) + camera.contrast(2) # increase contrast + camera.speffect(2) # jpeg grayscale + if con: + # TCP server + port = 80 + addr = soc.getaddrinfo('0.0.0.0', port)[0][-1] + s = soc.socket(soc.AF_INET, soc.SOCK_STREAM) + s.setsockopt(soc.SOL_SOCKET, soc.SO_REUSEADDR, 1) + s.bind(addr) + s.listen(1) + # s.settimeout(5.0) + while True: + cs, ca = s.accept() # wait for client connect + print('Request from:', ca) + w = cs.recv(200) # blocking + (_, uid, pwd) = w.decode().split('\r\n')[0].split()[1].split('/') + # print(_, uid, pwd) + if not (uid==UID and pwd==PWD): + print('Not authenticated') + cs.close() + continue + # We are authenticated, so continue serving + cs.write(b'%s\r\n\r\n' % hdr['stream']) + pic=camera.capture + put=cs.write + hr=hdr['frame'] + while True: + # once connected and authenticated just send the jpg data + # client use HTTP protocol (not RTSP) + try: + put(b'%s\r\n\r\n' % hr) + put(pic()) + put(b'\r\n') # send and flush the send buffer + except Exception as e: + print('TCP send error', e) + cs.close() + break +else: + if not con: + print("WiFi not connected.") + if not cam: + print("Camera not ready.") + else: + camera.deinit() + print("System not ready. Please restart") + +print('System aborted') diff --git a/webcam.py b/webcam.py new file mode 100644 index 0000000..1767324 --- /dev/null +++ b/webcam.py @@ -0,0 +1,187 @@ +# +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#----------------------------------------------------------------------------- + +# webcam.py MVC - This is the controller C of MVC + +from machine import reset +from time import sleep +import usocket as soc +import gc +# +import camera +import config as K +from wifi import Sta +from help import Setting as cam_setting +import site + +gc.enable() # Enable automatic garbage collection + +auth=site.auth +pwd=site.pwd + +def clean_up(cs): + cs.close() # flash buffer and close socket + del cs + # gc.collect() + +def route(pm): + cs,rq=pm + pth='*NOP*' + rqp = rq.split('/') + rl=len(rqp) + if rl==1: # '' + pth='/';v=0 + elif rl==2: # '/', '/a' + pth=f'/{rqp[1]}';v=0 + else: # '/a/v' '/a/v/w/....' + pth=f'/{rqp[1]}' + if rqp[1]=='login': + v=rqp[2] + else: + try: + v=int(rqp[2]) + except: + v=0 + pth='*ERR*' + print('Not an integer value', rqp[2]) + if pth in site.app: + #print(pth, site.app[pth]) + site.app[pth](cs,v) + elif pth=='*NOP*': + site.NOP(cs) + else: + site.ERR(cs) + clean_up(cs) + +def server(pm): + p=pm[0] + ss=soc.socket(soc.AF_INET, soc.SOCK_STREAM) + ss.setsockopt(soc.SOL_SOCKET, soc.SO_REUSEADDR, 1) + sa = ('0.0.0.0', p) + ss.bind(sa) + ss.listen(1) # serve 1 client at a time + print("Start server", p) + if auth.on: + print(f"Try - http://{site.server}/login/{auth.pwd}") + else: + print(f"Try - http://{site.server}") + while True: + ms='';rq=[] + try: + cs, ca = ss.accept() + except: + pass + else: + r=b'';e='' + try: + r = cs.recv(1024) + except Exception as e: + print(f"EX:{e}") + clean_up(cs) + try: + ms = r.decode() + rq = ms.split(' ') + except Exception as e: + print(f"RQ:{ms} EX:{e}") + clean_up(cs) + else: + if len(rq)>=2: + print(ca, rq[:2]) + rv,ph=rq[:2] # GET /path + if not auth.on: + route((cs, ph)) + continue + elif auth.ip==ca[0]: # authenticated client + route((cs, ph)) + continue + elif ph.find('login/')>=0: # do login + site.client=ca[0] + route((cs, ph)) + continue + else: + # Unauthorized otherwise + site.NO(cs) + clean_up(cs) + +# set camera configuration +K.configure(camera, K.ai_thinker) # AI-Thinker PINs map - no need (default) +#camera.conf(K.XCLK_MHZ, 16) # 16Mhz xclk rate +camera.conf(K.XCLK_MHZ, 14) # 14Mhz xclk rate +#camera.conf(K.XCLK_MHZ, 13) # 14Mhz xclk rate +#camera.conf(K.XCLK_MHZ, 12) # 12Mhz xclk rate - to reduce "cam_hal: EV-EOF-OVF" + +# wait for camera ready +for i in range(5): + cam = camera.init() + print("Camera ready?: ", cam) + if cam: break + else: sleep(2) +else: + print('Timeout') + reset() + +if cam: + print("Camera ready") + # wait for wifi ready + w = Sta() + w.connect() + w.wait() + for i in range(5): + if w.wlan.isconnected(): break + else: print("WIFI not ready. Wait...");sleep(2) + else: + print("WIFI not ready. Can't continue!") + reset() + +# set auth +auth.on=True +#auth.on=False # False: no authentication needed + +if auth.on: + auth.pwd=pwd() + auth.ip='' + print(f'PWD: {auth.pwd}') + +# set preffered camera setting +camera.framesize(10) # frame size 800X600 (1.33 espect ratio) +#camera.framesize(11) +#camera.framesize(12) +camera.quality(5) +#camera.quality(10) +camera.brightness(3) +camera.contrast(2) # increase contrast +#camera.contrast(0) +camera.speffect(2) # jpeg grayscale + +cam_setting['framesize']=10 +cam_setting['quality']=5 +cam_setting['contrast']=0 +cam_setting['speffect']=2 +cam_setting['brightness']=3 + +site.ip=w.wlan.ifconfig()[0] +site.camera=camera + +server((80,)) # port 80 +reset() \ No newline at end of file diff --git a/wifi.py b/wifi.py new file mode 100644 index 0000000..9a617b9 --- /dev/null +++ b/wifi.py @@ -0,0 +1,76 @@ +# The MIT License (MIT) +# +# Copyright (c) Sharil Tumin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#----------------------------------------------------------------------------- + +#Basic WiFi configuration: + +from time import sleep +import network +import site + +class Sta: + + AP = "SSID" + PWD = "PASSWORD" + + def __init__(my, ap='', pwd=''): + network.WLAN(network.AP_IF).active(False) # disable access point + my.wlan = network.WLAN(network.STA_IF) + my.wlan.active(True) + if ap == '': + my.ap = Sta.AP + my.pwd = Sta.PWD + else: + my.ap = ap + my.pwd = pwd + + def connect(my, ap='', pwd=''): + if ap != '': + my.ap = ap + my.pwd = pwd + + if not my.wlan.isconnected(): + my.wlan.connect(my.ap, my.pwd) + + def status(my): + if my.wlan.isconnected(): + return my.wlan.ifconfig() + else: + return () + + def wait(my): + cnt = 30 + while cnt > 0: + print("Waiting ..." ) + # con(my.ap, my.pwd) # Connect to an AP + if my.wlan.isconnected(): + print("Connected to %s" % my.ap) + print('network config:', my.wlan.ifconfig()) + site.server=my.wlan.ifconfig()[0] + cnt = 0 + else: + sleep(5) + cnt -= 5 + return + + def scan(my): + return my.wlan.scan() # Scan for available access points