10.13. Téléverser vers le cloud les trames déclenchées¶
Lorsqu’un mouvement se déclenche, la caméra allume désormais le tableau de bord. C’est suffisant pour une utilisation en direct, mais le propriétaire souhaite aussi une archive permanente de chaque trame déclenchée, stockée quelque part en dehors de la caméra. Cela passe par un appel HTTP sortant – la caméra agissant en tant que client.
10.13.1. La caméra comme client¶
Le module requests est le client HTTP sortant de la caméra. Son interface reproduit délibérément le module requests de CPython – les mêmes fonctions de module nommées d’après les verbes, les mêmes arguments nommés files=, json=, headers=, auth=. Si vous avez déjà effectué des appels HTTP depuis CPython, vous connaissez déjà l’API :
import requests
import io
ARCHIVE_URL = 'https://api.backyard-cloud.com/frames'
ARCHIVE_TOKEN = load_archive_token()
async def archive_frame(jpeg, ts):
try:
r = requests.post(
ARCHIVE_URL,
files={'image': (
'frame-{}.jpg'.format(ts),
io.BytesIO(jpeg),
)},
headers={'Authorization': 'Bearer ' + ARCHIVE_TOKEN},
)
except OSError as e:
print('upload failed:', e)
return False
if r.status_code >= 400:
print('archive rejected:', r.status_code, r.reason)
return False
return True
requests.post() ouvre une connexion TCP, envoie la requête et renvoie une Response avec status_code, reason, headers, content, json() ainsi que le reste des propriétés familières.
files={...} construit un corps multipart/form-data. La valeur est un tuple (filename, file-like) ; requests.post() lit l’objet de type fichier par blocs afin que l’intégralité du JPEG n’ait pas à être recopiée en mémoire dans une chaîne au préalable. io.BytesIO enveloppe les octets JPEG déjà présents en mémoire pour qu’ils exposent une interface de lecture de type fichier.
headers={...} est un simple dictionnaire envoyé tel quel comme en-têtes de requête – ici, un jeton bearer à l’emplacement Authorization standard. Le fournisseur d’archivage documente le format de jeton qu’il attend ; l’exemple présente la forme la plus courante.
10.13.2. Intégration dans le détecteur de mouvement¶
La coroutine de détection de mouvement présentée plus tôt s’exécute déjà à chaque nouvelle trame et se déclenche lorsque change > state['threshold']. Ajoutez-y le téléversement, mais lancez-le comme tâche d”arrière-plan afin que le détecteur continue de surveiller pendant que le téléversement est en cours :
async def motion_detector():
global last_motion
prev = None
while True:
await new_frame.wait()
change = compute_change(prev, latest_jpeg)
if change > state['threshold']:
state['trigger_count'] += 1
ts = int(time.time())
last_motion = {'ts': ts,
'count': state['trigger_count'],
'change': change}
motion_event.set()
asyncio.create_task(archive_frame(latest_jpeg, ts))
prev = latest_jpeg
await asyncio.sleep_ms(50)
asyncio.create_task() planifie la coroutine de téléversement et retourne immédiatement. Le détecteur continue de capturer des trames ; le téléversement s’exécute en parallèle ; la caméra ne se bloque jamais.
10.13.3. Modes de défaillance¶
Le code réseau échoue. La caméra peut être hors ligne, l’archive peut être indisponible, le jeton bearer peut avoir expiré. Les catégories qu’il vaut la peine d’intercepter :
OSError– la connexion TCP n’a pas pu être ouverte ou a été fermée en plein transfert. Échec DNS, absence de route, réinitialisation de connexion.requestslève précisément cette exception.status_code >= 400– le serveur a reçu la requête et l’a rejetée. 401 pour un jeton expiré, 403 pour un jeton révoqué, 413 pour un corps trop volumineux, 5xx lorsque l’archive est en mauvaise santé.Délai d’expiration silencieux –
requestsutilise un délai d’expiration de socket par défaut (quelques secondes) ; au-delà, il lèveOSErroravecerrno.ETIMEDOUT.
Pour une archive qui compte vraiment, vous mettriez les trames rejetées en file d’attente dans /sdcard/pending/ et les réessaieriez selon une boucle plus lente – cela représente quelques lignes de plus par cas, en plus de ce qui est montré.
10.13.4. Ce que requests ne fait pas¶
Le portage MicroPython est délibérément réduit. Voici quelques choses que le requests de CPython fait et que celui-ci ne fait pas :
Le pooling de connexions. Chaque appel ouvre une nouvelle connexion TCP.
Les nouvelles tentatives automatiques en cas d’erreurs transitoires. Encadrez l’appel vous-même.
Les réponses en flux.
r.contentest lu intégralement en RAM ; il n’existe pas d’équivalent àstream=True.La décompression automatique des réponses gzippées. Définissez explicitement l’en-tête
Accept-Encodinguniquement si le serveur est configuré pour cela.
Consultez requests — Client HTTP pour la liste complète des méthodes et pour savoir ce qui entre ou non dans le périmètre.
Le HTTPS fonctionne d’emblée – le schéma de l’URL le déclenche, et le contexte SSL par défaut est créé à la volée. Pour vérifier le certificat de l’archive par rapport à un bundle de CA que vous avez chargé sur la caméra, consultez la section as a client de Vérifier un serveur public (caméra en tant que client).
L’application est entièrement livrée : aperçu en direct, détection de mouvement, tableau de bord avec authentification, HTTPS, CORS/CSRF, archive cloud.