--- /dev/null
+
+# 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
--- /dev/null
+
+#-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
--- /dev/null
+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
--- /dev/null
+# 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/<PWD>
+ 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 <img> 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
--- /dev/null
+# 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':'''<!DOCTYPE html>
+<html>
+<head>
+<title>ESP32 Camera</title>
+<link rel="icon" href="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">
+</head>
+<body>
+ <div style="display:flex; margin-top: 15%%; justify-content:center; align-items:center; height:600px;">
+ <img src="%s" style="height:100%%; transform:rotate(%sdeg);"/>
+ </div>
+</body>
+</html>
+''',
+ 'favicon':'''<!DOCTYPE html>
+<html>
+<head>
+<link rel="icon" href="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">
+</head>
+<body>
+</body>
+</html>
+''',
+ '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""",
+}
--- /dev/null
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <title>ESP32 OV2640</title>
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
+ <link rel="stylesheet" type="text/css" href="/style.css">
+ <style>
+ @media (min-width: 800px) and (orientation:landscape) {
+ #content {
+ display:flex;
+ flex-wrap: nowrap;
+ align-items: stretch
+ }
+ }
+ </style>
+ </head>
+
+ <body>
+ <section class="main">
+ <div id="logo">
+ <label for="nav-toggle-cb" id="nav-toggle" style="float:left;">☰ Settings </label>
+ <button id="swap-viewer" style="float:left;" title="Swap to simple viewer">Simple</button>
+ <button id="get-still" style="float:left;">Get Still</button>
+ <button id="toggle-stream" style="float:left;" class="hidden">Start Stream</button>
+ <div id="wait-settings" style="float:left;" class="loader" title="Waiting for camera settings to load"></div>
+ </div>
+ <div id="content">
+ <div class="hidden" id="sidebar">
+ <input type="checkbox" id="nav-toggle-cb" checked="checked">
+ <nav id="menu">
+ <div class="input-group hidden" id="lamp-group" title="Flashlight LED.

Warning:
Built-In lamps can be Very Bright! Avoid looking directly at LED
Can draw a lot of power and may cause visual artifacts, affect WiFi or even brownout the camera on high settings">
+ <label for="lamp">Light</label>
+ <div class="range-min">Off</div>
+ <input type="range" id="lamp" min="0" max="100" value="0" class="default-action">
+ <div class="range-max"><span style="font-size: 125%;">⚠</span>Full</div>
+ </div>
+ <div class="input-group hidden" id="autolamp-group" title="When enabled the lamp will only turn on while the camera is active">
+ <label for="autolamp">Auto Lamp</label>
+ <div class="switch">
+ <input id="autolamp" type="checkbox" class="default-action">
+ <label class="slider" for="autolamp"></label>
+ </div>
+ </div>
+
+ <div class="input-group" id="framesize-group" title="Camera resolution
Higher resolutions will result in lower framerates">
+ <label for="framesize">Resolution</label>
+ <select id="framesize" class="default-action">
+ <option value="13">UXGA (1600x1200)</option>
+ <option value="12">SXGA (1280x1024)</option>
+ <option value="11">HD (1280x720)</option>
+ <option value="10">XGA (1024x768)</option>
+ <option value="9">SVGA (800x600)</option>
+ <option value="8">VGA (640x480)</option>
+ <option value="7">HVGA (480x320)</option>
+ <option value="6">CIF (400x296)</option>
+ <option value="5">QVGA (320x240)</option>
+ <option value="3">HQVGA (240x176)</option>
+ <option value="1">QQVGA (160x120)</option>
+ <option value="0">THUMB (96x96)</option>
+ </select>
+ </div>
+ <div class="input-group" id="quality-group" title="Camera Image and Stream quality factor
Higher settings will result in lower framerates">
+ <label for="quality">Quality</label>
+ <div class="range-min">Low</div>
+ <!-- Note; the following element is 'flipped' in CSS so that it slides from High to Low
+ As a result the 'min' and 'max' values are reversed here too -->
+ <input type="range" id="quality" min="6" max="63" value="10" class="default-action">
+ <div class="range-max">High</div>
+ </div>
+ <div class="input-group" id="set-xclk-group" title="Camera Bus Clock Frequency
Increasing this will raise the camera framerate and capture speed

Raising too far will result in visual artifacts and/or incomplete frames
This setting can vary a lot between boards, budget boards typically need lower values">
+ <label for="set-xclk">XCLK</label>
+ <div class="text">
+ <input id="xclk" type="number" min="2" max="32" size="3" step="1" class="default-action">
+ <div class="range-max">MHz</div>
+ </div>
+ </div>
+ <div class="input-group" id="brightness-group">
+ <label for="brightness">Brightness</label>
+ <div class="range-min">-2</div>
+ <input type="range" id="brightness" min="-2" max="2" value="0" class="default-action">
+ <div class="range-max">2</div>
+ </div>
+ <div class="input-group" id="contrast-group">
+ <label for="contrast">Contrast</label>
+ <div class="range-min">-2</div>
+ <input type="range" id="contrast" min="-2" max="2" value="0" class="default-action">
+ <div class="range-max">2</div>
+ </div>
+ <div class="input-group" id="saturation-group">
+ <label for="saturation">Saturation</label>
+ <div class="range-min">-2</div>
+ <input type="range" id="saturation" min="-2" max="2" value="0" class="default-action">
+ <div class="range-max">2</div>
+ </div>
+ <div class="input-group" id="special_effect-group">
+ <label for="special_effect">Special Effect</label>
+ <select id="special_effect" class="default-action">
+ <option value="0" selected="selected">No Effect</option>
+ <option value="1">Negative</option>
+ <option value="2">Grayscale</option>
+ <option value="3">Red Tint</option>
+ <option value="4">Green Tint</option>
+ <option value="5">Blue Tint</option>
+ <option value="6">Sepia</option>
+ </select>
+ </div>
+ <div class="input-group" id="awb-group">
+ <label for="awb">AWB Enable</label>
+ <div class="switch">
+ <input id="awb" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="awb"></label>
+ </div>
+ </div>
+ <div class="input-group" id="awb_gain-group">
+ <label for="awb_gain">Manual AWB Gain</label>
+ <div class="switch">
+ <input id="awb_gain" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="awb_gain"></label>
+ </div>
+ </div>
+ <div class="input-group" id="wb_mode-group">
+ <label for="wb_mode">WB Mode</label>
+ <select id="wb_mode" class="default-action">
+ <option value="0" selected="selected">Auto</option>
+ <option value="1">Sunny</option>
+ <option value="2">Cloudy</option>
+ <option value="3">Office</option>
+ <option value="4">Home</option>
+ </select>
+ </div>
+ <div class="input-group" id="aec-group">
+ <label for="aec">AEC Sensor Enable</label>
+ <div class="switch">
+ <input id="aec" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="aec"></label>
+ </div>
+ </div>
+ <div class="input-group" id="aec2-group">
+ <label for="aec2">AEC DSP</label>
+ <div class="switch">
+ <input id="aec2" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="aec2"></label>
+ </div>
+ </div>
+ <div class="input-group" id="ae_level-group">
+ <label for="ae_level">AE Level</label>
+ <div class="range-min">-2</div>
+ <input type="range" id="ae_level" min="-2" max="2" value="0" class="default-action">
+ <div class="range-max">2</div>
+ </div>
+ <div class="input-group" id="aec_value-group">
+ <label for="aec_value">Exposure</label>
+ <div class="range-min">0</div>
+ <input type="range" id="aec_value" min="0" max="1200" value="204" class="default-action">
+ <div class="range-max">1200</div>
+ </div>
+ <div class="input-group" id="agc-group">
+ <label for="agc">AGC</label>
+ <div class="switch">
+ <input id="agc" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="agc"></label>
+ </div>
+ </div>
+ <div class="input-group hidden" id="agc_gain-group">
+ <label for="agc_gain">Gain</label>
+ <div class="range-min">1x</div>
+ <input type="range" id="agc_gain" min="0" max="30" value="5" class="default-action">
+ <div class="range-max">31x</div>
+ </div>
+ <div class="input-group" id="gainceiling-group">
+ <label for="gainceiling">Gain Ceiling</label>
+ <div class="range-min">2x</div>
+ <input type="range" id="gainceiling" min="0" max="6" value="0" class="default-action">
+ <div class="range-max">128x</div>
+ </div>
+ <div class="input-group" id="bpc-group">
+ <label for="bpc">BPC</label>
+ <div class="switch">
+ <input id="bpc" type="checkbox" class="default-action">
+ <label class="slider" for="bpc"></label>
+ </div>
+ </div>
+ <div class="input-group" id="wpc-group">
+ <label for="wpc">WPC</label>
+ <div class="switch">
+ <input id="wpc" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="wpc"></label>
+ </div>
+ </div>
+ <div class="input-group" id="raw_gma-group">
+ <label for="raw_gma">Raw GMA Enable</label>
+ <div class="switch">
+ <input id="raw_gma" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="raw_gma"></label>
+ </div>
+ </div>
+ <div class="input-group" id="lenc-group">
+ <label for="lenc">Lens Correction</label>
+ <div class="switch">
+ <input id="lenc" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="lenc"></label>
+ </div>
+ </div>
+ <div class="input-group" id="hmirror-group">
+ <label for="hmirror">H-Mirror Stream</label>
+ <div class="switch">
+ <input id="hmirror" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="hmirror"></label>
+ </div>
+ </div>
+ <div class="input-group" id="vflip-group">
+ <label for="vflip">V-Flip Stream</label>
+ <div class="switch">
+ <input id="vflip" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="vflip"></label>
+ </div>
+ </div>
+ <div class="input-group" id="rotate-group">
+ <label for="rotate">Rotate in Browser</label>
+ <select id="rotate" class="default-action">
+ <option value="90">90° (Right)</option>
+ <option value="0" selected="selected">0° (None)</option>
+ <option value="-90">-90° (Left)</option>
+ </select>
+ </div>
+ <div class="input-group" id="dcw-group">
+ <label for="dcw">DCW (Downsize EN)</label>
+ <div class="switch">
+ <input id="dcw" type="checkbox" class="default-action" checked="checked">
+ <label class="slider" for="dcw"></label>
+ </div>
+ </div>
+ <div class="input-group" id="colorbar-group">
+ <label for="colorbar">Test Pattern</label>
+ <div class="switch">
+ <input id="colorbar" type="checkbox" class="default-action">
+ <label class="slider" for="colorbar"></label>
+ </div>
+ </div>
+ <div class="input-group" id="min_frame_time-group" title="Minimum frame time
Higher settings reduce the frame rate
Use this for a smoother stream and to reduce load on the WiFi and browser">
+ <label for="min_frame_time">Frame Duration Limit</label>
+ <select id="min_frame_time" class="default-action">
+ <option value="3333">3.3s (0.3fps)</option>
+ <option value="2000">2s (0.5fps)</option>
+ <option value="1000">1s (1fps)</option>
+ <option value="500">500ms (2fps)</option>
+ <option value="333">333ms (3fps)</option>
+ <option value="200">200ms (5fps)</option>
+ <option value="100">100ms (10fps)</option>
+ <option value="50">50ms (20fps)</option>
+ <option value="0" selected="selected">Disabled</option>
+ </select>
+ </div>
+ <div class="input-group" id="preferences-group">
+ <label for="prefs" style="line-height: 2em;">Preferences</label>
+ <button id="reboot" title="Reboot the camera module">Reboot</button>
+ <button id="save_prefs" title="Save Preferences on camera module">Save</button>
+ <button id="clear_prefs" title="Erase saved Preferences on camera module">Erase</button>
+ </div>
+ <div class="input-group" id="cam_name-group">
+ <label for="cam_name">
+ <a href="/dump" title="System Info" target="_blank">Name</a></label>
+ <div id="cam_name" class="default-action"></div>
+ </div>
+ <div class="input-group" id="code_ver-group">
+ <label for="code_ver">
+ <a href="https://github.com/easytarget/esp32-cam-webserver"
+ title="ESP32 Cam Webserver on GitHub" target="_blank">Firmware</a></label>
+ <div id="code_ver" class="default-action"></div>
+ </div>
+ <div class="input-group hidden" id="stream-group">
+ <label for="stream_url" id="stream_link">Stream</label>
+ <div id="stream_url" class="default-action">Unknown</div>
+ </div>
+ </nav>
+ </div>
+ <figure>
+ <div id="stream-container" class="image-container hidden">
+ <div class="close close-rot-none" id="close-stream">×</div>
+ <img id="stream" src="">
+ </div>
+ </figure>
+ </div>
+ </section>
+ </body>
+
+ <script>
+ document.addEventListener('DOMContentLoaded', function (event) {
+ var baseHost = document.location.origin;
+ var streamURL = 'Undefined';
+ var viewerURL = 'Undefined';
+
+ const header = document.getElementById('logo')
+ const settings = document.getElementById('sidebar')
+ const waitSettings = document.getElementById('wait-settings')
+ const lampGroup = document.getElementById('lamp-group')
+ const autolampGroup = document.getElementById('autolamp-group')
+ const streamGroup = document.getElementById('stream-group')
+ const camName = document.getElementById('cam_name')
+ const codeVer = document.getElementById('code_ver')
+ const rotate = document.getElementById('rotate')
+ const view = document.getElementById('stream')
+ const viewContainer = document.getElementById('stream-container')
+ const stillButton = document.getElementById('get-still')
+ const streamButton = document.getElementById('toggle-stream')
+ const closeButton = document.getElementById('close-stream')
+ const streamLink = document.getElementById('stream_link')
+ const framesize = document.getElementById('framesize')
+ const xclk = document.getElementById('xclk')
+ const swapButton = document.getElementById('swap-viewer')
+ const savePrefsButton = document.getElementById('save_prefs')
+ const clearPrefsButton = document.getElementById('clear_prefs')
+ const rebootButton = document.getElementById('reboot')
+ const minFrameTime = document.getElementById('min_frame_time')
+
+ const hide = el => {
+ el.classList.add('hidden')
+ }
+ const show = el => {
+ el.classList.remove('hidden')
+ }
+
+ const disable = el => {
+ el.classList.add('disabled')
+ el.disabled = true
+ }
+
+ const enable = el => {
+ el.classList.remove('disabled')
+ el.disabled = false
+ }
+
+ const updateValue = (el, value, updateRemote) => {
+ updateRemote = updateRemote == null ? true : updateRemote
+ let initialValue
+ if (el.type === 'checkbox') {
+ initialValue = el.checked
+ value = !!value
+ el.checked = value
+ } else {
+ initialValue = el.value
+ el.value = value
+ }
+
+ if (updateRemote && initialValue !== value) {
+ updateConfig(el);
+ } else if(!updateRemote){
+ if(el.id === "aec"){
+ value ? hide(exposure) : show(exposure)
+ } else if(el.id === "agc"){
+ if (value) {
+ show(gainCeiling)
+ hide(agcGain)
+ } else {
+ hide(gainCeiling)
+ show(agcGain)
+ }
+ } else if(el.id === "awb_gain"){
+ value ? show(wb) : hide(wb)
+ } else if(el.id === "lamp"){
+ if (value == -1) {
+ hide(lampGroup)
+ hide(autolampGroup)
+ } else {
+ show(lampGroup)
+ show(autolampGroup)
+ }
+ } else if(el.id === "cam_name"){
+ camName.innerHTML = value;
+ window.document.title = value;
+ console.log('Name set to: ' + value);
+ } else if(el.id === "code_ver"){
+ codeVer.innerHTML = value;
+ console.log('Firmware Build: ' + value);
+ } else if(el.id === "rotate"){
+ rotate.value = value;
+ applyRotation();
+ } else if(el.id === "min_frame_time"){
+ min_frame_time.value = value;
+ } else if(el.id === "stream_url"){
+ streamURL = value;
+ viewerURL = value + 'view';
+ stream_url.innerHTML = value;
+ stream_link.setAttribute("title", `Open the standalone stream viewer :: ${viewerURL}`);
+ stream_link.style.textDecoration = "underline";
+ stream_link.style.cursor = "pointer";
+ streamButton.setAttribute("title", `Start the stream :: ${streamURL}`);
+ show(streamGroup)
+ console.log('Stream URL set to: ' + streamURL);
+ console.log('Stream Viewer URL set to: ' + viewerURL);
+ }
+ }
+ }
+
+ var rangeUpdateScheduled = false
+ var latestRangeConfig
+
+ function updateRangeConfig (el) {
+ latestRangeConfig = el
+ if (!rangeUpdateScheduled) {
+ rangeUpdateScheduled = true;
+ setTimeout(function(){
+ rangeUpdateScheduled = false
+ updateConfig(latestRangeConfig)
+ }, 150);
+ }
+ }
+
+ function updateConfig (el) {
+ let value
+ switch (el.type) {
+ case 'checkbox':
+ value = el.checked ? 1 : 0
+ break
+ case 'range':
+ case 'number':
+ case 'select-one':
+ value = el.value
+ break
+ case 'button':
+ case 'submit':
+ value = '1'
+ break
+ default:
+ return
+ }
+
+ const query = `${baseHost}/control?var=${el.id}&val=${value}`
+
+ fetch(query)
+ .then(response => {
+ console.log(`request to ${query} finished, status: ${response.status}`)
+ })
+ }
+
+ document
+ .querySelectorAll('.close')
+ .forEach(el => {
+ el.onclick = () => {
+ hide(el.parentNode)
+ }
+ })
+
+ // read initial values
+ fetch(`${baseHost}/status`)
+ .then(function (response) {
+ return response.json()
+ })
+ .then(function (state) {
+ document
+ .querySelectorAll('.default-action')
+ .forEach(el => {
+ updateValue(el, state[el.id], false)
+ })
+ hide(waitSettings);
+ show(settings);
+ show(streamButton);
+ //startStream();
+ })
+
+ // Put some helpful text on the 'Still' button
+ stillButton.setAttribute("title", `Capture a still image :: ${baseHost}/capture`);
+
+ const stopStream = () => {
+ window.stop();
+ streamButton.innerHTML = 'Start Stream';
+ streamButton.setAttribute("title", `Start the stream :: ${streamURL}`);
+ hide(viewContainer);
+ }
+
+ const startStream = () => {
+ view.src = streamURL;
+ view.scrollIntoView(false);
+ streamButton.innerHTML = 'Stop Stream';
+ streamButton.setAttribute("title", `Stop the stream`);
+ show(viewContainer);
+ }
+
+ const applyRotation = () => {
+ rot = rotate.value;
+ if (rot == -90) {
+ viewContainer.style.transform = `rotate(-90deg) translate(-100%)`;
+ closeButton.classList.remove('close-rot-none');
+ closeButton.classList.remove('close-rot-right');
+ closeButton.classList.add('close-rot-left');
+ } else if (rot == 90) {
+ viewContainer.style.transform = `rotate(90deg) translate(0, -100%)`;
+ closeButton.classList.remove('close-rot-left');
+ closeButton.classList.remove('close-rot-none');
+ closeButton.classList.add('close-rot-right');
+ } else {
+ viewContainer.style.transform = `rotate(0deg)`;
+ closeButton.classList.remove('close-rot-left');
+ closeButton.classList.remove('close-rot-right');
+ closeButton.classList.add('close-rot-none');
+ }
+ console.log('Rotation ' + rot + ' applied');
+ }
+
+ // Attach actions to controls
+
+ streamLink.onclick = () => {
+ stopStream();
+ window.open(viewerURL, "_blank");
+ }
+
+ stillButton.onclick = () => {
+ stopStream();
+ view.src = `${baseHost}/capture?_cb=${Date.now()}`;
+ view.scrollIntoView(false);
+ show(viewContainer);
+ }
+
+ closeButton.onclick = () => {
+ stopStream();
+ hide(viewContainer);
+ }
+
+ streamButton.onclick = () => {
+ const streamEnabled = streamButton.innerHTML === 'Stop Stream'
+ if (streamEnabled) {
+ stopStream();
+ } else {
+ startStream();
+ }
+ }
+
+ // Attach default on change action
+ document
+ .querySelectorAll('.default-action')
+ .forEach(el => {
+ el.onchange = () => updateConfig(el)
+ })
+
+ // Update range sliders as they are being moved
+ document
+ .querySelectorAll('input[type="range"]')
+ .forEach(el => {
+ el.oninput = () => updateRangeConfig(el)
+ })
+
+ // Custom actions
+ // Gain
+ const agc = document.getElementById('agc')
+ const agcGain = document.getElementById('agc_gain-group')
+ const gainCeiling = document.getElementById('gainceiling-group')
+ agc.onchange = () => {
+ updateConfig(agc)
+ if (agc.checked) {
+ show(gainCeiling)
+ hide(agcGain)
+ } else {
+ hide(gainCeiling)
+ show(agcGain)
+ }
+ }
+
+ // Exposure
+ const aec = document.getElementById('aec')
+ const exposure = document.getElementById('aec_value-group')
+ aec.onchange = () => {
+ updateConfig(aec)
+ aec.checked ? hide(exposure) : show(exposure)
+ }
+
+ // AWB
+ const awb = document.getElementById('awb_gain')
+ const wb = document.getElementById('wb_mode-group')
+ awb.onchange = () => {
+ updateConfig(awb)
+ awb.checked ? show(wb) : hide(wb)
+ }
+
+ // Detection and framesize
+ rotate.onchange = () => {
+ applyRotation();
+ updateConfig(rotate);
+ }
+
+ framesize.onchange = () => {
+ updateConfig(framesize)
+ }
+
+ minFrameTime.onchange = () => {
+ updateConfig(minFrameTime)
+ }
+
+ xclk.onchange = () => {
+ console.log("xclk:" , xclk);
+ updateConfig(xclk)
+ }
+
+ swapButton.onclick = () => {
+ window.open('/?view=simple','_self');
+ }
+
+ savePrefsButton.onclick = () => {
+ if (confirm("Save the current preferences?")) {
+ updateConfig(savePrefsButton);
+ }
+ }
+
+ clearPrefsButton.onclick = () => {
+ if (confirm("Remove the saved preferences?")) {
+ updateConfig(clearPrefsButton);
+ }
+ }
+
+ rebootButton.onclick = () => {
+ if (confirm("Reboot the Camera Module?")) {
+ updateConfig(rebootButton);
+ // Some sort of countdown here?
+ hide(settings);
+ hide(viewContainer);
+ header.innerHTML = '<h1>Rebooting!</h1><hr>Page will reload after 30 seconds.';
+ setTimeout(function() {
+ location.replace(document.URL);
+ }, 30000);
+ }
+ }
+
+ })
+ </script>
+</html>
--- /dev/null
+
+# 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')
--- /dev/null
+# 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')
--- /dev/null
+#
+# 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
--- /dev/null
+# 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