diff --git a/umod/__init__.py b/umod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/umod/uaio_http_server.py b/umod/uaio_http_server.py new file mode 100644 index 0000000..544ed33 --- /dev/null +++ b/umod/uaio_http_server.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- +# uaio_http_server.py +# GNU GENERAL PUBLIC LICENSE: (c) DD miraceti.net +import uasyncio as asyncio # @UnresolvedImport +import hashlib # @UnresolvedImport +import ubinascii # @UnresolvedImport +import os +''' +AsyncHttpServer est un serveur HTTP asynchrone 100 % MicroPython. +Autonome et sans dépendances externes, conçu pour ESP32. +Il gère le parsing HTTP, le routage GET/POST, le service de fichiers statiques en streaming et les WebSockets. +Respecte les contraintes mémoire et appuie uniquement sur uasyncio et les bibliothèques natives (hashlib, ubinascii, ujson). +''' + +MIME = { + '.txt': 'text/plain', + '.html': 'text/html; charset=utf-8', + '.css': 'text/css', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.json': 'application/json', + '.js': 'application/javascript', +} + +EPOCH_2000_UNIX_TIME = 946684800 + +def toUnixTimestamp(totalseconds): + return totalseconds + EPOCH_2000_UNIX_TIME + + +def to2000Timestamp(totalseconds): + return totalseconds - EPOCH_2000_UNIX_TIME + + +def base64_encode(data): + return ubinascii.b2a_base64(data).strip() + + +def base64_decode(data): + return ubinascii.a2b_base64(data) + + +class AsyncHttpServer: + ''' + -------------------------- + Exemple minimal d’usage : + -------------------------- + server = AsyncHttpServer() + + @server.get("/hello") + async def hello(server, reader, writer): + await server.send_response(writer, b"Hello GET") + + @server.post("/echo") + async def echo(server, reader, writer, body): + await server.send_response(writer, body, mime="application/json") + + async def on_ws_message(server, msg, writer): + await server.ws_send_text(writer, msg) + + server.ws_handler = on_ws_message + server.run() + + ------------------------------------------------------------------- + Client HTML + JS (pour tester WebSocket + API + fichiers statiques) + ------------------------------------------------------------------- + + + +

Serveur Micropython 100% autonome

+ + + + + +

+    
+    
+    
+    
+    '''   
+    def __init__(self, host="0.0.0.0", port=80, www="www"):
+        self.host = host
+        self.port = port
+        self.www = www
+        self.routes_get = {}
+        self.routes_post = {}
+        self.ws_routes = {}
+        self.ws_clients = []
+        self._server = None
+
+    # -------------------------
+    # Routing API
+    # -------------------------
+    def get(self, path):
+        def decorator(func):
+            self.routes_get[path] = func
+            return func
+        return decorator
+
+
+    def post(self, path):
+        def decorator(func):
+            self.routes_post[path] = func
+            return func
+        return decorator
+
+
+    def websocket(self, path):
+        def decorator(func):
+            self.ws_routes[path] = func
+            return func
+        return decorator
+
+
+    # -------------------------
+    # Serveur principal
+    # -------------------------
+    async def start(self):
+        self._server = await asyncio.start_server(self.handle_client, self.host, self.port)
+        print(f"HTTP Serveur {self.host}:{self.port} vient de démarrer!...")
+        
+        while True:
+            await asyncio.sleep(1)
+
+
+    # -------------------------
+    # Lecture HTTP minimaliste
+    # -------------------------
+    def ensure_bytes(self, body):
+        if body is None:
+            return b""
+        if isinstance(body, bytes):
+            return body
+        if isinstance(body, str):
+            return body.encode("utf-8")
+        # Si c'est un dict, liste, JSON…
+        return str(body).encode("utf-8")
+    
+    
+    def get_mime(self, path):
+        
+        for ext, mt in MIME.items():
+            if path.lower().endswith(ext):
+                return mt
+        return 'application/octet-stream'
+    
+    
+    def file_exists(self, path):
+        try:
+            os.stat(path)
+            return True
+        except:
+            return False
+
+
+    async def read_headers(self, reader):
+        headers = {}
+        while True:
+            line = await reader.readline()
+            if not line:
+                break
+            line = line.decode().strip()
+            if line == "":
+                break  # fin des headers           
+            if ":" in line:
+                k, v = line.split(":", 1)
+                headers[k.lower()] = v.strip()
+        return headers
+                
+
+    async def read_body(self, reader, headers):
+        length = int(headers.get("content-length", "0"))
+        if length > 0:
+            return await reader.read(length)
+        return b""
+
+
+    # -------------------------
+    # Réponse HTTP standard
+    # -------------------------   
+    async def send_response(self, writer,  body=b'', status="200 OK", content_type="text/plain"):  
+        size = len(body)
+        await writer.awrite(
+            "HTTP/1.1 " + status + "\r\n"
+            "Content-Type: " + content_type + "\r\n"
+            "Content-Length: " + str(size) + "\r\n"
+            "Connection: close\r\n"
+            "\r\n"
+        )
+        await writer.drain()        
+        
+        if body:
+            body = self.ensure_bytes(body)
+            await writer.awrite(body)
+            await writer.drain()
+
+        
+    # -------------------------
+    # Client handler dispatcher
+    # -------------------------
+    
+    async def handle_client(self, reader, writer):
+        try:
+            first = await reader.readline()
+            if not first:
+                await writer.aclose()
+                return
+
+            request_line = first.decode().strip()
+            method, path, proto = request_line.split()  # @UnusedVariable
+            headers = await self.read_headers(reader)
+            
+            # POST ?
+            if method == "POST" and path in self.routes_post:
+                body = await self.read_body(reader, headers)
+                await self.routes_post[path](self, writer, headers, body)
+            # GET API ?
+            elif method == "GET" and path in self.routes_get:
+                await self.routes_get[path](self, writer, headers)
+            # websocket ?  
+            elif headers.get('upgrade', '').lower() == 'websocket' and path in self.ws_routes:    
+                await self.handle_websocket(reader, writer, headers, path)                       
+            else:
+                await self.serve_static(writer, path)
+
+        except Exception as e:
+            print("Server error:", e)
+            await self.send_response(writer, "500 Internal Server Error")
+            
+        finally:
+            try:
+                await writer.aclose()
+            except:
+                pass            
+
+    # ---------------------------
+    # Fichiers statiques resolver
+    # ---------------------------
+    
+    async def serve_static(self, writer, path):
+        if path == "/":
+            path = "/index.html"
+        full = self.www + path
+        print("serve static =>", full)
+        
+        try:
+            if not self.file_exists(full):
+                raise Exception("File Not found")
+            size = os.stat(full)[6]
+            mime = self.get_mime(full)
+
+            header = (
+                "HTTP/1.1 200 OK\r\n"
+                "Content-Type: " + mime + "\r\n"
+                "Content-Length: " + str(size) + "\r\n"
+                #"Cache-Control: no-cache, no-store, must-revalidate\r\n"
+                #"Pragma: no-cache\r\n"
+                #"Expires: 0\r\n"  
+                "Connection: close\r\n"
+                "\r\n"
+            )
+            await writer.awrite(header)
+            await writer.drain()
+            
+            with open(full, "rb") as f:
+                while True:
+                    chunk = f.read(1024)
+                    if not chunk:
+                        break
+                    await writer.awrite(chunk)
+                    await writer.drain() 
+
+        except Exception as e:
+            print("serve_static error =>", full, e)
+            await self.send_response(writer, "404 File Not found")
+
+
+    # --- compute Sec-WebSocket-Accept ---
+    def ws_accept_key(self, key):
+        GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".encode()
+        key_bytes = key.encode()
+        sha1 = hashlib.sha1(key_bytes + GUID).digest()
+        ws_key = ubinascii.b2a_base64(sha1).strip().decode()
+        return ws_key
+
+
+    # --- send one WebSocket TEXT frame ---
+    async def ws_send_text(self, writer, text):
+        payload = text.encode()
+        await writer.awrite(b"\x81")  # FIN + opcode TEXT
+
+        length = len(payload)
+        if length < 126:
+            await writer.awrite(bytes([length]))
+        else:
+            await writer.awrite(b"\x7E" + bytes([(length >> 8) & 0xFF, length & 0xFF]))
+
+        await writer.awrite(payload)
+        await writer.drain()
+
+
+    # --- receive a single frame ---
+    async def ws_recv(self, reader):
+        header = await reader.readexactly(2)
+        if not header:
+            return None
+
+        opcode = header[0] & 0x0F
+        masked = header[1] & 0x80
+        length = header[1] & 0x7F
+        if opcode == 0x8:
+            return None
+
+        if length == 126:
+            ext = await reader.readexactly(2)
+            length = (ext[0] << 8) | ext[1]
+        elif length == 127:
+            return None
+
+        mask = await reader.readexactly(4) if masked else None
+        data = await reader.readexactly(length)
+        if masked:
+            data = bytes(b ^ mask[i % 4] for i, b in enumerate(data))
+        return data.decode()    
+    
+     
+    # -------------------------
+    # Websocket
+    # -------------------------
+    
+    async def handle_websocket(self, reader, writer, headers, path):
+        try:
+            ws_key = headers["sec-websocket-key"]
+            if not ws_key:
+                await writer.awrite("HTTP/1.1 400 Bad Request\r\n\r\n")
+                await writer.drain()
+                await writer.aclose()
+                return
+            
+            accept = self.ws_accept_key(ws_key)
+            response = (
+                "HTTP/1.1 101 Switching Protocols\r\n"
+                "Upgrade: websocket\r\n"
+                "Connection: Upgrade\r\n"
+                "Sec-WebSocket-Accept: " + accept +"\r\n"
+                "\r\n"
+            )            
+            await writer.awrite(response)
+            await writer.drain()
+            #print(response)
+            
+            # Register this client
+            self.ws_clients.append(writer)
+
+            # Main recv loop
+            while True:
+                try:
+                    msg = await self.ws_recv(reader)
+                    if msg is None:
+                        break
+                    callback = self.ws_routes[path]       
+                    if callback:
+                        # APPEL DU CALLBACK UTILISATEUR (si défini)
+                        # ws_routes[path] doit être une coroutine ou fonction acceptant (server, msg, writer)
+                        asyncio.create_task(callback(self, msg, writer))
+                    else:
+                        await self.ws_send_text(writer, msg)
+                except:
+                    pass
+        except Exception:
+            pass
+        finally:
+            # retirer le client s'il est présent
+            try:
+                if writer in self.ws_clients:
+                    self.ws_clients.remove(writer)
+            except:
+                pass
+            try:
+                await writer.aclose()
+            except:
+                pass
+        
diff --git a/umod/wifi_manager.py b/umod/wifi_manager.py
new file mode 100644
index 0000000..e41de88
--- /dev/null
+++ b/umod/wifi_manager.py
@@ -0,0 +1,201 @@
+## -*- coding: utf-8 -*-
+# wifi_manager.py
+# GNU GENERAL PUBLIC LICENSE: (c) DD miraceti.net
+
+import utime as time    # @UnresolvedImport
+import ubinascii        # @UnresolvedImport
+import network          # @UnresolvedImport
+'''
+    Management de la WiFi pour ESP32 (MicroPython).
+    Usage:
+
+    SSID = "ssid_8chars"
+    PASSWORD = "ChangeMeSoon"  
+        
+    if __name__ == "__main__":
+        try:
+            WiFi = WifiManager(mode='AP')
+            
+            wifi = WiFi(SSID, PASSWORD)
+            wifi.start()
+            n=0
+            while True:
+                time.sleep(5.0)
+                print(n, wifi.status())
+                n += 1
+        except KeyboardInterrupt:
+            pass
+        finally:
+            wifi.stop()
+            print("Arrêt")   
+'''
+
+class WifiBase:
+    ssid = None
+    password = None
+    channel = 1
+    hidden = False
+    max_clients = 4
+    authmode = 3
+    _iface= None
+    _mode='AP'
+    wifi_delay = 1.5
+
+    def start(self):
+        pass
+    
+    def stop(self):
+        pass
+    
+    def status(self):
+        pass
+    
+    def restart(self):
+        if_was = self._iface.active()
+        self._iface.active(False)
+        time.sleep(self.wifi_delay)
+        if if_was:
+            self._iface.active(True)
+        return self.status()
+    
+    def get_ip(self):
+        return self._iface.ifconfig()[0] if self._iface.active() else None
+    
+    def get_ssid(self):
+        return self._iface.config('essid') if self._iface.active() else None
+    
+    def get_rssi(self):
+        return self._iface.status('rssi') if self._iface.isconnected() else None
+    
+    def get_iface(self):
+        return self._mode
+
+
+class APManager(WifiBase):
+    """
+    Gestion WiFi pour ESP32 (MicroPython).
+    Arguments (tous optionnels selon le mode) :
+      ssid (str) : nom du réseau AP ou SSID à rejoindre
+      password (str) : mot de passe AP ou STA (min 8 chars si authmode != 0)
+      channel (int) : canal pour AP (1-13)
+      hidden (bool) : AP caché ou non
+      max_clients (int) : nombre max de clients en AP
+      authmode (int) : 0=OPEN,1=WEP,2=WPAPSK,3=WPAPSK/SAE,4=SAEs
+      timeout_connect: délai pour connecter
+    """     
+    def __init__(self, ssid=None, password=None, channel=1, hidden=False,
+                                max_clients=4, authmode=3, timeout_connect=15):
+        self.ssid = ssid or b"ESP32-" + ubinascii.hexlify(self._iface.config('mac')[-3:]).decode()
+        self.password = password or ''
+        self.channel = channel if 1 <= channel <= 13 else 1
+        self.hidden = bool(hidden)
+        self.max_clients = max_clients if max_clients > 0 else 4
+        self.authmode = authmode
+        self.timeout_connect = timeout_connect    
+        self._iface= network.WLAN(network.AP_IF)
+        self._mode = 'AP'
+
+        
+    def start(self):
+        if not self._iface.active():
+            self._iface.active(True)
+        cfg_auth = 0 if not self.password else self.authmode
+        try:
+            self._iface.config(essid=self.ssid, password=self.password, authmode=cfg_auth,
+                    channel=self.channel, hidden=self.hidden, max_clients=self.max_clients)
+        except Exception as e:
+            print("start error", e)
+            return False
+        time.sleep(self.wifi_delay)
+        print("Démarrage de la wifi:", self.status())
+        return True
+
+
+    def stop(self):
+        if self._iface.active():
+            self._iface.active(False)
+        print("Déconnexion de la wifi:", self.status())
+
+
+    def status(self):
+        return {
+            'iface': self.get_iface(),
+            'active': self._iface.active(),
+            'ssid': self.get_ssid(),
+            'ip': self.get_ip(),
+        }
+
+
+class STAManager(WifiBase):
+    """
+    Gestion WiFi pour ESP32 (MicroPython).
+    Arguments (tous optionnels selon le mode) :
+      ssid (str) : nom du réseau AP ou SSID à rejoindre
+      password (str) : mot de passe AP ou STA (min 8 chars si authmode != 0)
+      authmode (int) : 0=OPEN,1=WEP,2=WPAPSK,3=WPAPSK/SAE,4=SAEs
+      timeout_connect: délai pour connecter
+    """       
+    def __init__(self, ssid=None, password=None, authmode=3, timeout_connect=10):
+        self.ssid = ssid
+        self.password = password
+        self.authmode = authmode
+        self.timeout_connect = timeout_connect
+        self._iface= network.WLAN(network.STA_IF)
+        self._mode = 'STA'
+
+    def start(self):
+        try:
+            if not self.ssid:
+                raise ValueError("SSID requis pour le mode STA")
+            if self.password and len(self.password) < 8:
+                raise ValueError("Mot de passe WPA/WPA2 doit faire >=8 caractères")
+            
+            #self._iface.disconnect()
+            self._iface.active(False)
+            time.sleep(self.wifi_delay)
+            
+            self._iface.active(True)
+            self._iface.connect(self.ssid, self.password)
+
+            t0 = time.time()
+            wait, n = 0.1, 0
+            
+            print('Tentative de connexion au réseau...')
+            while not self._iface.isconnected():
+                time.sleep(wait)
+                wait = min(0.5, wait + 0.05)
+                n+=1
+                print(n, '> Essai de connexion...', 'status>', self._iface.status())
+                if (time.time() - t0) > self.timeout_connect:
+                    raise Exception('Timeout connexion atteint')  # @UndefinedVariable
+                
+            time.sleep(self.wifi_delay)
+            print("Démarrage de la wifi:", self.status())
+            return True
+        except Exception as e:
+            print(f"STA connect() raised: {e}")
+        return False
+    
+    
+    def stop(self):
+        if self._iface.isconnected():
+            try:
+                self._iface.disconnect()
+            except Exception:
+                pass
+        print("Déconnexion de la wifi", self.status())
+
+
+    def status(self):
+        return {
+            'iface': self.get_iface(),
+            'active': self._iface.active(),
+            'connected': self._iface.isconnected(),
+            'ip': self.get_ip(),
+            'rssi': self.get_rssi()
+        }
+
+
+def WifiManager(mode='AP'):
+    return APManager if mode == 'AP' else STAManager
+
diff --git a/www/favicon.ico b/www/favicon.ico
new file mode 100644
index 0000000..9f304ea
Binary files /dev/null and b/www/favicon.ico differ
diff --git a/www/index.html b/www/index.html
new file mode 100644
index 0000000..9a383d4
--- /dev/null
+++ b/www/index.html
@@ -0,0 +1,71 @@
+
+
+	
+		
+		
+		Uart controller for esp32
+		
+		
+	
+	
+		
+
+
+ +
+ + + + diff --git a/www/tools.js b/www/tools.js new file mode 100644 index 0000000..70d0772 --- /dev/null +++ b/www/tools.js @@ -0,0 +1,8 @@ +/* +** tools.js +** GNU GENERAL PUBLIC LICENSE: (c) DD miraceti.net +*/ +let s = function(sel) { return document.querySelector(sel); }; +let sId = function(sel) { return document.getElementById(sel); }; +let isJsonObject = function(value) { return value !== undefined && value !== null && typeof value === 'object'; } +let to_date = function(unixtimestamp) { return new Date(parseInt(unixtimestamp)*1000).toLocaleString(); }