]> vault307.fbx.one Git - esp32Cam.git/commitdiff
esp32Camera webserver master
authorjimmy <jimipunk88@gmail.com>
Fri, 28 Jun 2024 01:12:07 +0000 (20:12 -0500)
committerjimmy <jimipunk88@gmail.com>
Fri, 28 Jun 2024 01:12:07 +0000 (20:12 -0500)
13 files changed:
Wifi.py [new file with mode: 0644]
config.py [new file with mode: 0644]
esp32camNOTES.odt [new file with mode: 0644]
espcamserver.py [new file with mode: 0644]
firmware/ESP32_GENERIC-SPIRAM-20231005-v1.21.0.bin [new file with mode: 0644]
firmware/esp32camAIThinkerfirmware.bin [new file with mode: 0644]
help.py [new file with mode: 0644]
html.py [new file with mode: 0644]
index.html [new file with mode: 0644]
site.py [new file with mode: 0644]
streaming_server.py [new file with mode: 0644]
webcam.py [new file with mode: 0644]
wifi.py [new file with mode: 0644]

diff --git a/Wifi.py b/Wifi.py
new file mode 100644 (file)
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 (file)
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 (file)
index 0000000..630fd1c
Binary files /dev/null and b/esp32camNOTES.odt differ
diff --git a/espcamserver.py b/espcamserver.py
new file mode 100644 (file)
index 0000000..c75732e
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
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/<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
diff --git a/html.py b/html.py
new file mode 100644 (file)
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':'''<!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""",
+}
diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..28944ca
--- /dev/null
@@ -0,0 +1,627 @@
+<!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;">&#9776;&nbsp;&nbsp;Settings&nbsp;&nbsp;&nbsp;&nbsp;</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.&#013;&#013;Warning:&#013;Built-In lamps can be Very Bright! Avoid looking directly at LED&#013;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%;">&#9888;</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&#013;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&#013;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&#013;Increasing this will raise the camera framerate and capture speed&#013;&#013;Raising too far will result in visual artifacts and/or incomplete frames&#013;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&deg; (Right)</option>
+                  <option value="0" selected="selected">0&deg; (None)</option>
+                  <option value="-90">-90&deg; (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&#013;Higher settings reduce the frame rate&#013;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>
diff --git a/site.py b/site.py
new file mode 100644 (file)
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 (file)
index 0000000..e14940b
--- /dev/null
@@ -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 (file)
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 (file)
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