Module relais USB HID : pilotage projecteurs + alim Canon + eclairage auto
Pilote backend/relais.py (hidapi/ctypes) pour module LCUS 5131:2007. Relais 1-2 = projecteurs (NO), relais 3 = alim Canon (NF). API : status, on/off par nom, power-cycle, config seuils. Eclairage auto : analyse luminosite visage → allume projecteurs selon seuils. Admin : onglet eclairage enrichi avec boutons relais + seuils configurables. Note : power-cycle Canon par relais ne redémarre pas le DSLR (contact capot batterie). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ from backend.destinations import distribuer_photo, detecter_usb, compteur_restan
|
|||||||
from backend.printer import lister_imprimantes, imprimer
|
from backend.printer import lister_imprimantes, imprimer
|
||||||
from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique, ajouter_au_spool, taille_spool, tache_spool_demarrage
|
from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique, ajouter_au_spool, taille_spool, tache_spool_demarrage
|
||||||
from backend.qrcode_gen import generer_qr, qr_galerie
|
from backend.qrcode_gen import generer_qr, qr_galerie
|
||||||
|
from backend import relais
|
||||||
from backend.evenements import (
|
from backend.evenements import (
|
||||||
lister_evenements, creer_evenement, obtenir_evenement,
|
lister_evenements, creer_evenement, obtenir_evenement,
|
||||||
modifier_evenement, supprimer_evenement, activer_evenement,
|
modifier_evenement, supprimer_evenement, activer_evenement,
|
||||||
@@ -122,6 +123,33 @@ async def _pusher_preview():
|
|||||||
eclairage = await loop.run_in_executor(None, analyser_eclairage, donnees)
|
eclairage = await loop.run_in_executor(None, analyser_eclairage, donnees)
|
||||||
if eclairage is not None:
|
if eclairage is not None:
|
||||||
await diffuser_ws({"type": "eclairage", **eclairage})
|
await diffuser_ws({"type": "eclairage", **eclairage})
|
||||||
|
_gerer_eclairage_auto(eclairage)
|
||||||
|
|
||||||
|
_proj_etat = {"proj1": False, "proj2": False}
|
||||||
|
|
||||||
|
def _gerer_eclairage_auto(eclairage: dict):
|
||||||
|
config = charger_config()
|
||||||
|
rcfg = config.get("relais", {})
|
||||||
|
if not rcfg.get("eclairage_auto") or not relais.est_connecte():
|
||||||
|
return
|
||||||
|
score = eclairage.get("score", -1)
|
||||||
|
if score < 0:
|
||||||
|
return
|
||||||
|
seuil_1 = rcfg.get("seuil_proj1", 45)
|
||||||
|
seuil_2 = rcfg.get("seuil_proj2", 30)
|
||||||
|
seuil_off = rcfg.get("seuil_off", 65)
|
||||||
|
if score >= seuil_off and (_proj_etat["proj1"] or _proj_etat["proj2"]):
|
||||||
|
relais.projecteurs(False)
|
||||||
|
_proj_etat["proj1"] = False
|
||||||
|
_proj_etat["proj2"] = False
|
||||||
|
elif score < seuil_2 and not _proj_etat["proj2"]:
|
||||||
|
relais.projecteurs(True)
|
||||||
|
_proj_etat["proj1"] = True
|
||||||
|
_proj_etat["proj2"] = True
|
||||||
|
elif score < seuil_1 and not _proj_etat["proj1"]:
|
||||||
|
relais.activer("projecteur_gauche")
|
||||||
|
_proj_etat["proj1"] = True
|
||||||
|
|
||||||
|
|
||||||
def _usb_reset_canon():
|
def _usb_reset_canon():
|
||||||
"""Reset USB ioctl du Canon EOS (vendor 04a9) pour débloquer un port stall."""
|
"""Reset USB ioctl du Canon EOS (vendor 04a9) pour débloquer un port stall."""
|
||||||
@@ -316,11 +344,13 @@ async def lifespan(app: FastAPI):
|
|||||||
task_push = asyncio.create_task(_pusher_preview())
|
task_push = asyncio.create_task(_pusher_preview())
|
||||||
task_spool = asyncio.create_task(tache_spool_demarrage())
|
task_spool = asyncio.create_task(tache_spool_demarrage())
|
||||||
task_watchdog = asyncio.create_task(_watchdog_systemd())
|
task_watchdog = asyncio.create_task(_watchdog_systemd())
|
||||||
|
relais.connecter()
|
||||||
yield
|
yield
|
||||||
task_dslr.cancel()
|
task_dslr.cancel()
|
||||||
task_push.cancel()
|
task_push.cancel()
|
||||||
task_spool.cancel()
|
task_spool.cancel()
|
||||||
task_watchdog.cancel()
|
task_watchdog.cancel()
|
||||||
|
relais.deconnecter()
|
||||||
log.info("Arret du photobooth")
|
log.info("Arret du photobooth")
|
||||||
camera.deconnecter()
|
camera.deconnecter()
|
||||||
|
|
||||||
@@ -609,6 +639,60 @@ async def api_eclairage():
|
|||||||
return resultat
|
return resultat
|
||||||
|
|
||||||
|
|
||||||
|
# --- API Relais ---
|
||||||
|
|
||||||
|
@app.get("/api/relais")
|
||||||
|
async def api_relais_status():
|
||||||
|
return relais.etat_complet()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/relais/{nom}/on")
|
||||||
|
async def api_relais_on(nom: str):
|
||||||
|
if nom == "projecteurs":
|
||||||
|
relais.projecteurs(True)
|
||||||
|
return {"ok": True}
|
||||||
|
if not relais.activer(nom):
|
||||||
|
return JSONResponse({"erreur": f"relais '{nom}' introuvable ou non connecté"}, 400)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/relais/{nom}/off")
|
||||||
|
async def api_relais_off(nom: str):
|
||||||
|
if nom == "projecteurs":
|
||||||
|
relais.projecteurs(False)
|
||||||
|
return {"ok": True}
|
||||||
|
if not relais.desactiver(nom):
|
||||||
|
return JSONResponse({"erreur": f"relais '{nom}' introuvable ou non connecté"}, 400)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/relais/canon/power-cycle")
|
||||||
|
async def api_power_cycle_canon(req: Request):
|
||||||
|
body = await req.json() if req.headers.get("content-type", "").startswith("application/json") else {}
|
||||||
|
duree = body.get("duree", 3.0)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, relais.power_cycle_canon, duree)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/relais/config")
|
||||||
|
async def api_relais_config():
|
||||||
|
config = charger_config()
|
||||||
|
return config.get("relais", {
|
||||||
|
"eclairage_auto": False,
|
||||||
|
"seuil_proj1": 45,
|
||||||
|
"seuil_proj2": 30,
|
||||||
|
"seuil_off": 65,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/relais/config")
|
||||||
|
async def api_relais_config_save(req: Request):
|
||||||
|
body = await req.json()
|
||||||
|
mettre_a_jour_config({"relais": body})
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/camera/debug")
|
@app.get("/api/camera/debug")
|
||||||
async def api_camera_debug():
|
async def api_camera_debug():
|
||||||
"""Diagnostic complet de la caméra (preview, viewfinder, erreurs gphoto2)."""
|
"""Diagnostic complet de la caméra (preview, viewfinder, erreurs gphoto2)."""
|
||||||
|
|||||||
166
backend/relais.py
Normal file
166
backend/relais.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""Pilotage module relais USB HID (LCUS 5131:2007).
|
||||||
|
|
||||||
|
4 canaux, protocole LCUS :
|
||||||
|
ON : 0x00 0xA0 <ch> 0x01 <checksum> 0x00 0x00 0x00 0x00
|
||||||
|
OFF : 0x00 0xA0 <ch> 0x00 <checksum> 0x00 0x00 0x00 0x00
|
||||||
|
Status : 0x00 0xD1 ...
|
||||||
|
|
||||||
|
Assignation :
|
||||||
|
1 = Projecteur gauche (NO)
|
||||||
|
2 = Projecteur droit (NO)
|
||||||
|
3 = Canon alim (NF — OFF=alimenté, ON=coupé)
|
||||||
|
4 = Libre
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
log = logging.getLogger("photobooth.relais")
|
||||||
|
|
||||||
|
VID = 0x5131
|
||||||
|
PID = 0x2007
|
||||||
|
|
||||||
|
CANAUX = {
|
||||||
|
"projecteur_gauche": {"relay": 1, "mode": "NO"},
|
||||||
|
"projecteur_droit": {"relay": 2, "mode": "NO"},
|
||||||
|
"canon": {"relay": 3, "mode": "NF"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_lib = None
|
||||||
|
_dev = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _charger_hidapi():
|
||||||
|
global _lib
|
||||||
|
if _lib is not None:
|
||||||
|
return _lib
|
||||||
|
for name in ("hidapi-hidraw", "hidapi-libusb", "hidapi"):
|
||||||
|
path = ctypes.util.find_library(name)
|
||||||
|
if path:
|
||||||
|
_lib = ctypes.CDLL(path)
|
||||||
|
_lib.hid_init()
|
||||||
|
_lib.hid_open.restype = ctypes.c_void_p
|
||||||
|
_lib.hid_read_timeout.restype = ctypes.c_int
|
||||||
|
log.info(f"hidapi chargé : {path}")
|
||||||
|
return _lib
|
||||||
|
try:
|
||||||
|
_lib = ctypes.CDLL("libhidapi-hidraw.so.0")
|
||||||
|
_lib.hid_init()
|
||||||
|
_lib.hid_open.restype = ctypes.c_void_p
|
||||||
|
_lib.hid_read_timeout.restype = ctypes.c_int
|
||||||
|
return _lib
|
||||||
|
except OSError:
|
||||||
|
log.warning("hidapi introuvable — relais désactivés")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def connecter():
|
||||||
|
global _dev
|
||||||
|
lib = _charger_hidapi()
|
||||||
|
if not lib:
|
||||||
|
return False
|
||||||
|
with _lock:
|
||||||
|
if _dev:
|
||||||
|
return True
|
||||||
|
_dev = lib.hid_open(VID, PID, None)
|
||||||
|
if not _dev:
|
||||||
|
log.warning("Module relais non trouvé (5131:2007)")
|
||||||
|
_dev = None
|
||||||
|
return False
|
||||||
|
log.info("Module relais connecté")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def deconnecter():
|
||||||
|
global _dev
|
||||||
|
with _lock:
|
||||||
|
if _dev and _lib:
|
||||||
|
_lib.hid_close(_dev)
|
||||||
|
_dev = None
|
||||||
|
|
||||||
|
|
||||||
|
def est_connecte():
|
||||||
|
return _dev is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _ecrire(data: list[int]):
|
||||||
|
if not _dev or not _lib:
|
||||||
|
return False
|
||||||
|
buf = (ctypes.c_ubyte * 9)(*data)
|
||||||
|
with _lock:
|
||||||
|
res = _lib.hid_write(_dev, buf, 9)
|
||||||
|
return res > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _relay_set(num: int, on: bool):
|
||||||
|
state = 0x01 if on else 0x00
|
||||||
|
checksum = (0xA0 + num + state) & 0xFF
|
||||||
|
ok = _ecrire([0x00, 0xA0, num, state, checksum, 0x00, 0x00, 0x00, 0x00])
|
||||||
|
if ok:
|
||||||
|
log.info(f"Relais {num} → {'ON' if on else 'OFF'}")
|
||||||
|
else:
|
||||||
|
log.error(f"Échec écriture relais {num}")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
if not _dev or not _lib:
|
||||||
|
return None
|
||||||
|
_ecrire([0x00, 0xD1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||||
|
time.sleep(0.1)
|
||||||
|
rbuf = (ctypes.c_ubyte * 64)()
|
||||||
|
with _lock:
|
||||||
|
n = _lib.hid_read_timeout(_dev, rbuf, 64, 500)
|
||||||
|
if n < 4:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"relay_1": rbuf[1] == 1,
|
||||||
|
"relay_2": rbuf[2] == 1,
|
||||||
|
"relay_3": rbuf[3] == 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def activer(nom: str):
|
||||||
|
cfg = CANAUX.get(nom)
|
||||||
|
if not cfg:
|
||||||
|
return False
|
||||||
|
return _relay_set(cfg["relay"], True)
|
||||||
|
|
||||||
|
|
||||||
|
def desactiver(nom: str):
|
||||||
|
cfg = CANAUX.get(nom)
|
||||||
|
if not cfg:
|
||||||
|
return False
|
||||||
|
return _relay_set(cfg["relay"], False)
|
||||||
|
|
||||||
|
|
||||||
|
def projecteurs(on: bool):
|
||||||
|
_relay_set(1, on)
|
||||||
|
_relay_set(2, on)
|
||||||
|
log.info(f"Projecteurs {'allumés' if on else 'éteints'}")
|
||||||
|
|
||||||
|
|
||||||
|
def power_cycle_canon(duree: float = 3.0):
|
||||||
|
log.info(f"Power-cycle Canon ({duree}s)")
|
||||||
|
_relay_set(3, True) # NF : ON = coupé
|
||||||
|
time.sleep(duree)
|
||||||
|
_relay_set(3, False) # NF : OFF = alimenté
|
||||||
|
log.info("Canon ré-alimenté")
|
||||||
|
|
||||||
|
|
||||||
|
def etat_complet():
|
||||||
|
raw = status()
|
||||||
|
if raw is None:
|
||||||
|
return {"connecte": False}
|
||||||
|
result = {"connecte": True}
|
||||||
|
for nom, cfg in CANAUX.items():
|
||||||
|
relay_on = raw.get(f"relay_{cfg['relay']}", False)
|
||||||
|
if cfg["mode"] == "NF":
|
||||||
|
result[nom] = not relay_on
|
||||||
|
else:
|
||||||
|
result[nom] = relay_on
|
||||||
|
return result
|
||||||
@@ -383,7 +383,7 @@
|
|||||||
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
||||||
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
||||||
<button class="onglet" data-onglet="videos-admin" onclick="chargerVideosAdmin()">Videos</button>
|
<button class="onglet" data-onglet="videos-admin" onclick="chargerVideosAdmin()">Videos</button>
|
||||||
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
|
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive();demarrerEclairageLiveExt()">Eclairage</button>
|
||||||
<button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button>
|
<button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button>
|
||||||
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -890,6 +890,33 @@
|
|||||||
<div id="eclairage-visages" style="font-size:.9rem;color:#aaa">--</div>
|
<div id="eclairage-visages" style="font-size:.9rem;color:#aaa">--</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 style="margin-top:1.5rem">Relais</h3>
|
||||||
|
<div id="relais-status" style="margin-bottom:1rem;padding:0.5rem;background:#111;border-radius:6px;font-size:0.85rem;color:#aaa">Non connecte</div>
|
||||||
|
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1rem">
|
||||||
|
<button class="btn-action btn-petit" onclick="relaisAction('projecteurs','on')">Projecteurs ON</button>
|
||||||
|
<button class="btn-secondaire btn-petit" onclick="relaisAction('projecteurs','off')">Projecteurs OFF</button>
|
||||||
|
<button class="btn-danger btn-petit" onclick="relaisAction('canon/power-cycle','on')">Power-cycle Canon</button>
|
||||||
|
</div>
|
||||||
|
<h4>Eclairage automatique</h4>
|
||||||
|
<div class="champ">
|
||||||
|
<label class="toggle"><input type="checkbox" id="tog-eclairage-auto"><span class="toggle-slider"></span> Allumer les projecteurs automatiquement selon la luminosite</label>
|
||||||
|
</div>
|
||||||
|
<div class="champ">
|
||||||
|
<label>Seuil projecteur 1 (score <)</label>
|
||||||
|
<input type="number" id="relais-seuil-proj1" min="0" max="100" value="45" step="5">
|
||||||
|
<span style="font-size:0.75rem;color:#888">1 seul projecteur s'allume</span>
|
||||||
|
</div>
|
||||||
|
<div class="champ">
|
||||||
|
<label>Seuil projecteur 2 (score <)</label>
|
||||||
|
<input type="number" id="relais-seuil-proj2" min="0" max="100" value="30" step="5">
|
||||||
|
<span style="font-size:0.75rem;color:#888">Les 2 projecteurs s'allument</span>
|
||||||
|
</div>
|
||||||
|
<div class="champ">
|
||||||
|
<label>Seuil extinction (score >)</label>
|
||||||
|
<input type="number" id="relais-seuil-off" min="0" max="100" value="65" step="5">
|
||||||
|
<span style="font-size:0.75rem;color:#888">Projecteurs s'eteignent</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-action" onclick="sauvegarderRelaisConfig()">Sauvegarder</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-panneau" id="panneau-infos">
|
<div class="admin-panneau" id="panneau-infos">
|
||||||
@@ -1057,6 +1084,6 @@
|
|||||||
<script src="/js/effects.js?v=4"></script>
|
<script src="/js/effects.js?v=4"></script>
|
||||||
<script src="/js/gallery.js?v=4"></script>
|
<script src="/js/gallery.js?v=4"></script>
|
||||||
<script src="/js/share.js?v=8"></script>
|
<script src="/js/share.js?v=8"></script>
|
||||||
<script src="/js/admin.js?v=12"></script>
|
<script src="/js/admin.js?v=13"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1823,3 +1823,73 @@ async function sauvegarderSurprise() {
|
|||||||
});
|
});
|
||||||
afficherStatut('Surprise sauvegardee', 'succes');
|
afficherStatut('Surprise sauvegardee', 'succes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === RELAIS ===
|
||||||
|
|
||||||
|
async function chargerRelaisStatus() {
|
||||||
|
try {
|
||||||
|
const data = await apiGet('/api/relais');
|
||||||
|
const el = document.getElementById('relais-status');
|
||||||
|
if (!el) return;
|
||||||
|
if (!data.connecte) {
|
||||||
|
el.textContent = 'Module relais non connecte';
|
||||||
|
el.style.color = '#f44';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const proj_g = data.projecteur_gauche ? '🟢' : '⚫';
|
||||||
|
const proj_d = data.projecteur_droit ? '🟢' : '⚫';
|
||||||
|
const canon = data.canon ? '🟢' : '🔴';
|
||||||
|
el.innerHTML = `Proj G: ${proj_g} | Proj D: ${proj_d} | Canon: ${canon} (alim)`;
|
||||||
|
el.style.color = '#4caf50';
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Erreur relais status:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chargerRelaisConfig() {
|
||||||
|
try {
|
||||||
|
const data = await apiGet('/api/relais/config');
|
||||||
|
const tog = document.getElementById('tog-eclairage-auto');
|
||||||
|
if (tog) tog.checked = !!data.eclairage_auto;
|
||||||
|
const s1 = document.getElementById('relais-seuil-proj1');
|
||||||
|
if (s1) s1.value = data.seuil_proj1 ?? 45;
|
||||||
|
const s2 = document.getElementById('relais-seuil-proj2');
|
||||||
|
if (s2) s2.value = data.seuil_proj2 ?? 30;
|
||||||
|
const soff = document.getElementById('relais-seuil-off');
|
||||||
|
if (soff) soff.value = data.seuil_off ?? 65;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Erreur relais config:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sauvegarderRelaisConfig() {
|
||||||
|
const eclairage_auto = document.getElementById('tog-eclairage-auto')?.checked || false;
|
||||||
|
const seuil_proj1 = parseInt(document.getElementById('relais-seuil-proj1')?.value) || 45;
|
||||||
|
const seuil_proj2 = parseInt(document.getElementById('relais-seuil-proj2')?.value) || 30;
|
||||||
|
const seuil_off = parseInt(document.getElementById('relais-seuil-off')?.value) || 65;
|
||||||
|
await apiPost('/api/relais/config', { eclairage_auto, seuil_proj1, seuil_proj2, seuil_off });
|
||||||
|
afficherStatut('Config relais sauvegardee', 'succes');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relaisAction(nom, action) {
|
||||||
|
try {
|
||||||
|
if (nom === 'canon/power-cycle') {
|
||||||
|
await fetch('/api/relais/canon/power-cycle', {
|
||||||
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({duree: 3})
|
||||||
|
});
|
||||||
|
afficherStatut('Power-cycle Canon lance (3s)', 'succes');
|
||||||
|
} else {
|
||||||
|
await fetch(`/api/relais/${nom}/${action}`, { method: 'POST' });
|
||||||
|
afficherStatut(`${nom} ${action}`, 'succes');
|
||||||
|
}
|
||||||
|
setTimeout(chargerRelaisStatus, 500);
|
||||||
|
} catch (e) {
|
||||||
|
afficherStatut('Erreur relais', 'erreur');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function demarrerEclairageLiveExt() {
|
||||||
|
chargerRelaisStatus();
|
||||||
|
chargerRelaisConfig();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user