libs.zazac.link
Libs/capy/wifi

capy.wifi v0.5.4

capy entre appareils du même wifi, sans connaître les IP

Inclus dans capy : installe capy pour l'avoir.

import capy.wifi · PyPI : capyy · Python 3.9+ · aucune dépendance

Guide

capy entre appareils du même wifi, sans connaître les IP. Le serveur s'annonce sous un nom ("maison", "salon"…), le client l'appelle par ce nom. Pratique pour un Raspberry Pi, un 2e ordi, une manette faite maison, un jeu en réseau local…

Démarrage#

  1. Sur l'appareil qui sert : crée un capy.wifi.Server("un-nom") et ajoute des actions, comme avec capy.
  2. Lance-le : il affiche son nom et son IP sur le wifi.
  3. Sur n'importe quel appareil du même wifi (ou le même) : capy.wifi.call("un-nom", "action").
serveur.py
import capy.wifi

api = capy.wifi.Server("maison")   # le port est choisi tout seul

@api.action("temperature")
def temperature():
    return {"salon": 20.5}

api.run()
résultat
Visible sur le wifi sous le nom 'maison' (192.168.1.42)
API en écoute sur le port 51234, chiffrée (TLS) · empreinte : 9dfa7b98c1e0…
client.py
import capy
import capy.wifi

try:
    reponse = capy.wifi.call("maison", "temperature")
except ConnectionError as e:
    exit(str(e))

print(capy.get(reponse, "salon"), "°C")
résultat
capy.wifi : nouveau serveur wifi:maison, empreinte retenue 9dfa7b98c1e0…
20.5 °C

Comment le client trouve le serveur#

Pas de magie : au 1er appel, le client crie sur tout le wifi « il y a des serveurs capy ? » (un petit message UDP sur le port 38790). Chaque serveur répond avec son nom, son port et s'il est chiffré. Le client retient la réponse : les appels suivants vont directement au bon endroit, sans redemander.

échanges
client  → tout le wifi : capy?
maison  → client       : {"name": "maison", "port": 51234, "tls": true}
client  → maison       : {"action": "temperature"}   (chiffré, en TCP, comme capy)

Le serveur a redémarré ?

Il a sûrement changé de port. Pas grave : si l'appel échoue, call refait un scan et retrouve le serveur tout seul.

Avec un fichier capy.config.json

{"name": "maison", "mode": "wifi"} dans un capy.config.json, puis capy.project().server() et capy.project().call("temperature") : le nom n'est écrit qu'à un seul endroit.

Voir qui est là#

capy.wifi.scan() liste tous les serveurs du wifi en 1 seconde :

python
import capy.wifi

for nom, (ip, port) in capy.wifi.scan().items():
    print(f"{nom:<12} {ip}:{port}")
résultat
maison       192.168.1.42:51234
salon        192.168.1.50:49152
imprimante   127.0.0.1:50110
  • capy.wifi.find("maison") donne juste (ip, port) d'un serveur, ou une ConnectionError s'il n'est pas là.
  • Un serveur qui tourne sur le même appareil apparaît avec 127.0.0.1 : c'est 20 fois plus rapide que de passer par le wifi.

Depuis le terminal#

terminal
capy wifi scan
résultat
  maison               192.168.1.42:51234
  salon                127.0.0.1:50110  (cet appareil)
terminal
capy wifi call maison temperature
résultat
{
  "salon": 20.5
}
  • Paramètres en nom=valeur (capy wifi call maison regler temperature=21), --no-tls pour un serveur en clair.
  • capy wifi scan --timeout 3 attend plus longtemps les réponses (wifi lent).
  • Dans un projet en mode wifi, cy call temperature fait pareil, avec le nom de capy.config.json.

Les erreurs#

SituationCe qui se passe
personne ne porte ce nomConnectionError: capy.wifi : aucun serveur 'maison' sur le wifi (trouvés : salon)
le serveur est en clair et pas le client (ou l'inverse)ConnectionError qui dit quoi changer : mets tls=False ou enlève tls=False
l'empreinte du serveur a changécapy.SecurityError
l'action refuse ou planteune réponse {"erreur": "..."}, comme avec capy

Chiffrement#

Comme capy : tout est chiffré, et l'empreinte d'un serveur est retenue sous le nom wifi:<nom> dans ~/.capy/known_hosts.json. Un serveur en clair se crée avec tls=False, et s'appelle aussi avec tls=False :

python
api = capy.wifi.Server("maison", tls=False)
capy.wifi.call("maison", "temperature", tls=False)

Astuces et pièges#

Tout le wifi peut appeler tes actions

N'importe quel appareil connecté au même wifi peut trouver ton serveur et appeler ses actions. Rien de dangereux sans mot de passe !

« aucun serveur sur le wifi » alors qu'il tourne

  • Vérifie que les deux appareils sont sur le même wifi (pas l'un en 4G, ou sur le wifi invité).
  • Beaucoup de wifi publics (école, hôtel, gare) isolent les appareils entre eux : capy.wifi ne peut rien y faire.
  • Sur Mac, au 1er lancement, macOS demande d'accepter les connexions entrantes pour Python : réponds oui.
  • Un pare-feu peut bloquer le port UDP 38790 : il doit laisser passer les scans.

Toujours le même port

capy.wifi.Server("maison", port=4000) : pratique si tu veux aussi l'appeler avec capy.call(ip, 4000, ...).

Plusieurs serveurs sur le même appareil

Aucun problème : chacun a son nom et son port, et ils répondent tous aux scans.

Et pour joindre ma maison depuis l'extérieur ?

capy.wifi ne sort pas du wifi. Pour passer par internet, utilise capy.network avec un relais.

Exemples

capy.wifi : se trouver par son nom sur le même wifi

Pas besoin de connaître l'IP ni le port : le serveur s'annonce sous un nom. Lance serveur.py sur un appareil, client.py sur un autre du même wifi (ou le même).

serveur.py
import capy.wifi

api = capy.wifi.Server("salon")  # Port choisi tout seul


@api.action("lumiere")
def lumiere(allumer=True):
    etat = "allumée" if allumer else "éteinte"
    return {"lumiere": etat}


api.run()

Lister les serveurs du wifi

scan() demande à tout le wifi « il y a des serveurs capy ? » et attend les réponses pendant 1 seconde. Lance quelques capy.wifi.Server ailleurs (ou l'exemple 5) pour voir quelque chose.

scanner_le_wifi.py
import capy.wifi

serveurs = capy.wifi.scan(timeout=1)

if not serveurs:
    print("Aucun serveur capy.wifi trouvé (même wifi ? wifi public qui isole les appareils ?)")

for nom, (ip, port) in sorted(serveurs.items()):
    ici = " (cet appareil)" if ip == "127.0.0.1" else ""
    print(f"{nom:<15} {ip}:{port}{ici}")
# maison          192.168.1.42:51234
# salon           127.0.0.1:50110 (cet appareil)

Un projet complet : capy.config.json

On décrit le projet une fois (nom, version, port, adresse, chiffrement, fichiers…) ; serveur et client le lisent avec capy.project(), et cy lance ses scripts et ses fichiers (comme npm).

1. Créer le projet (modes : local, wifi ou network), puis tout lancer d'un coup

creer_le_projet.sh
capy new mon-jeu
cd mon-jeu
cy          # Voir ce que capy a compris
# mon-jeu v1.0
#   mode     local (localhost:4000)
#   tls      oui (chiffré)
#   server   server.py  ✓
#   client   client.py  ✓

cy run      # Serveur en fond + client, puis arrêt du serveur
# server │ API en écoute sur le port 4000, chiffrée (TLS) · empreinte : …
# Bonjour toi !

cy server   # Ou chacun dans son terminal
cy client

Référence

Classes

class Server(name, port=0, tls=True)

Serveur visible sur le wifi sous un nom (port choisi tout seul si on n'en donne pas)

Exapi = capy.wifi.Server("maison") puis api.run()

Hérite de capy.Server et de toutes ses méthodes.

Server.run(port=None)

Lancer le serveur, et répondre aux scans du wifi pour être trouvé

Voir le code
python
def run(self, port=None):
threading.Thread(target=self._announce, daemon=True).start()
print(f"Visible sur le wifi sous le nom {self.name!r} ({_local_ip()})")
super().run(port)

Fonctions

scan(timeout=1)

Tous les serveurs capy.wifi du réseau

Exscan() -> {"maison": ("192.168.1.42", 51234), "salon": ("192.168.1.50", 3879)}

Voir le code
python
def scan(timeout=1, _wanted=None):
    found, me = {}, _local_ip()
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        end = time.monotonic() + timeout
        next_ask = 0
        while time.monotonic() < end:
            # On redemande 3 fois : en wifi, un message UDP peut se perdre
            if time.monotonic() >= next_ask:
                for target in ("255.255.255.255", "127.0.0.1"):  # Tout le wifi + cet appareil (même sans wifi)
                    try:
                        s.sendto(b"capy?", (target, DISCOVERY_PORT))
                    except OSError:
                        pass
                next_ask = time.monotonic() + timeout / 3
            s.settimeout(max(min(end, next_ask) - time.monotonic(), 0.001))
            try:
                data, (ip, _) = s.recvfrom(1024)
                info = jsonlib.loads(data)
                name, port = info["name"], int(info["port"])
            except socket.timeout:
                continue
            except (OSError, ValueError, KeyError, TypeError):
                continue
            # Serveur sur cet appareil -> 127.0.0.1 (20x plus rapide que passer par l'IP du wifi)
            found.setdefault(name, ("127.0.0.1" if ip == me else ip, port))
            _secure.setdefault(name, info.get("tls") is True)
            if name == _wanted:  # find() : on a ce qu'on cherche, inutile d'attendre
                break
    _found.update(found)
    return found

find(name, timeout=1)

Trouver un serveur par son nom -> (ip, port)

Exfind("maison") -> ("192.168.1.42", 51234)

Voir le code
python
def find(name, timeout=1):
    if name not in _found:
        found = scan(timeout, _wanted=name)
        if name not in found:
            raise ConnectionError(f"capy.wifi : aucun serveur {name!r} sur le wifi (trouvés : {', '.join(found) or 'aucun'})")
    return _found[name]

call(name, action, /, tls=True, **params) -> Any

Appeler une action d'un serveur par son nom (comme capy.call, sans IP ni port)

tls=False : pour un serveur pas chiffré (Server(..., tls=False))

Excapy.wifi.call("maison", "test") -> {"text": "Hello"}

Voir le code
python
def call(name, action, /, tls=True, **params) -> Any:
    for attempt in range(2):
        host, port = find(name)
        if _secure.get(name, True) != tls:
            state = "n'est pas chiffré : mets tls=False" if tls else "est chiffré : enlève tls=False"
            raise ConnectionError(f"capy.wifi : le serveur {name!r} {state} dans capy.wifi.call()")
        try:
            return capy._call(host, port, action, params, tls, label=f"wifi:{name}", who="capy.wifi")
        except ConnectionError as e:
            if attempt or "chiffr" in str(e):
                raise
            _found.pop(name, None)  # Serveur relancé (autre port, autre IP) -> on le recherche une fois
            _secure.pop(name, None)

Constantes

DISCOVERY_PORT

DISCOVERY_PORT = 38790  # Port UDP où les serveurs répondent "je suis là"