@ing.jorgeu/wargaming-overlay 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/device_connection.py +113 -0
- package/diagnostics.js +7 -1
- package/game-store.js +1 -1
- package/package.json +3 -2
- package/public/control.js +1 -1
- package/reader.py +9 -18
package/README.md
CHANGED
|
@@ -625,3 +625,11 @@ npm run facts:coverage -- --require-four-hours
|
|
|
625
625
|
```
|
|
626
626
|
|
|
627
627
|
Al ampliar `data/faction-facts.json`, usa el nombre exacto del registro, conserva los IDs existentes para respetar el historial y asigna un ID estable a cada nueva curiosidad. Incluye un enlace específico, título de fuente y fecha de revisión. No añadas reformulaciones del mismo dato para aumentar artificialmente la duración.
|
|
628
|
+
|
|
629
|
+
### Continuidad entre USB y Wi-Fi
|
|
630
|
+
|
|
631
|
+
El lector recuerda el teléfono por su identificador físico en `phone-connection.json`, dentro de la carpeta de datos. Si ADB presenta USB y Wi-Fi para ese mismo teléfono, el overlay prefiere USB. Al retirar el cable, utiliza la conexión inalámbrica autorizada; al reconectarlo, vuelve a USB. Nunca elige otro teléfono por compartir el modelo o una dirección IP anterior.
|
|
632
|
+
|
|
633
|
+
Activa la depuración inalámbrica y empareja el computador con el teléfono en Android. Tener activada la opción no equivale a estar emparejado. ADB debe poder descubrir el teléfono en la red. Si el descubrimiento no funciona, conecta previamente con `adb connect IP:PUERTO` usando la dirección que Android muestra en Depuración inalámbrica. El overlay puede recuperar direcciones conocidas y servicios inalámbricos que anuncien el identificador del teléfono guardado; no activa puertos de depuración ni empareja dispositivos desconocidos automáticamente.
|
|
634
|
+
|
|
635
|
+
Para elegir otro teléfono, inicia con `--serial SERIAL` usando su identificador de `adb devices -l`. Si hay varios teléfonos diferentes y ninguno seleccionado, el lector pide seleccionar uno. El panel distingue conexión USB, Wi-Fi y espera de una ronda legible. Al enviar Tabletop Battles al fondo se conserva el último marcador; al volver a la ronda, la lectura se reanuda. Durante una reconexión puede haber una pausa de unos segundos.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Track one physical Android device across authorized ADB transports."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_adb(*args):
|
|
11
|
+
return subprocess.run([os.environ.get('OVERLAY_ADB', 'adb'), *args],
|
|
12
|
+
capture_output=True, text=True, encoding='utf-8',
|
|
13
|
+
errors='replace', check=True, timeout=5).stdout
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def devices(output):
|
|
17
|
+
rows = []
|
|
18
|
+
for line in output.splitlines():
|
|
19
|
+
fields = line.split()
|
|
20
|
+
if len(fields) >= 2 and not line.startswith(('List of', '*')):
|
|
21
|
+
serial = fields[0]
|
|
22
|
+
wireless = ':' in serial or '._adb-tls-connect._tcp' in serial
|
|
23
|
+
rows.append({'serial': serial, 'state': fields[1],
|
|
24
|
+
'transport': 'wifi' if wireless else 'usb'})
|
|
25
|
+
return rows
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeviceConnection:
|
|
29
|
+
def __init__(self, serial='', path=None, run=run_adb, now=time.monotonic):
|
|
30
|
+
self.run, self.now, self.explicit = run, now, serial
|
|
31
|
+
self.path = Path(path) if path else None
|
|
32
|
+
self.identity, self.aliases = None, set()
|
|
33
|
+
self.retry_at = 0
|
|
34
|
+
if self.path:
|
|
35
|
+
try:
|
|
36
|
+
saved = json.loads(self.path.read_text())
|
|
37
|
+
if not serial or serial in saved.get('aliases', []):
|
|
38
|
+
self.identity = saved.get('identity')
|
|
39
|
+
self.aliases = set(saved.get('aliases', []))
|
|
40
|
+
except (OSError, ValueError, TypeError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def identify(self, serial):
|
|
44
|
+
for prop in ('ro.serialno', 'ro.boot.serialno'):
|
|
45
|
+
try:
|
|
46
|
+
value = self.run('-s', serial, 'shell', 'getprop', prop).strip()
|
|
47
|
+
if value and value.lower() not in ('unknown', 'null'):
|
|
48
|
+
return value
|
|
49
|
+
except (OSError, subprocess.SubprocessError):
|
|
50
|
+
pass
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def remember(self, matching):
|
|
54
|
+
self.aliases.update(d['serial'] for d in matching)
|
|
55
|
+
if self.path:
|
|
56
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
temp = self.path.with_suffix('.tmp')
|
|
58
|
+
temp.write_text(json.dumps({'identity': self.identity, 'aliases': sorted(self.aliases)}))
|
|
59
|
+
temp.replace(self.path)
|
|
60
|
+
|
|
61
|
+
def reconnect(self):
|
|
62
|
+
if not self.identity or self.now() < self.retry_at:
|
|
63
|
+
return
|
|
64
|
+
self.retry_at = self.now() + 15
|
|
65
|
+
targets = {a for a in self.aliases if ':' in a and not a.startswith('adb-')}
|
|
66
|
+
# Only discover services advertising the remembered device, never arbitrary LAN phones.
|
|
67
|
+
try:
|
|
68
|
+
for line in self.run('mdns', 'services').splitlines():
|
|
69
|
+
fields = line.split()
|
|
70
|
+
if (len(fields) >= 3 and fields[0].startswith('adb-' + self.identity + '-')
|
|
71
|
+
and '_adb-tls-connect._tcp' in fields[1]):
|
|
72
|
+
targets.add(fields[2])
|
|
73
|
+
except (OSError, subprocess.SubprocessError):
|
|
74
|
+
pass
|
|
75
|
+
for target in sorted(targets)[:4]:
|
|
76
|
+
if re.fullmatch(r'[A-Za-z0-9.\-\[\]:]+:\d+', target):
|
|
77
|
+
try:
|
|
78
|
+
self.run('connect', target)
|
|
79
|
+
except (OSError, subprocess.SubprocessError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def select(self):
|
|
83
|
+
rows = devices(self.run('devices', '-l'))
|
|
84
|
+
ready = [d for d in rows if d['state'] == 'device']
|
|
85
|
+
# Verify the physical identity even for remembered IP addresses: DHCP can reuse them.
|
|
86
|
+
for d in ready:
|
|
87
|
+
d['identity'] = self.identify(d['serial'])
|
|
88
|
+
if self.identity:
|
|
89
|
+
matching = [d for d in ready if d['identity'] == self.identity]
|
|
90
|
+
elif self.explicit:
|
|
91
|
+
matching = [d for d in ready if d['serial'] == self.explicit]
|
|
92
|
+
if matching:
|
|
93
|
+
self.identity = matching[0]['identity']
|
|
94
|
+
if self.identity:
|
|
95
|
+
matching = [d for d in ready if d['identity'] == self.identity]
|
|
96
|
+
else:
|
|
97
|
+
identities = {d['identity'] or d['serial'] for d in ready}
|
|
98
|
+
if len(identities) > 1:
|
|
99
|
+
raise ValueError('Hay varios teléfonos diferentes. Selecciona uno con --serial SERIAL.')
|
|
100
|
+
matching = ready
|
|
101
|
+
if matching:
|
|
102
|
+
self.identity = matching[0]['identity']
|
|
103
|
+
if not matching:
|
|
104
|
+
self.reconnect()
|
|
105
|
+
if any(d['state'] == 'unauthorized' for d in rows):
|
|
106
|
+
raise ValueError('Autoriza este computador en el teléfono; para Wi-Fi completa el emparejamiento de depuración inalámbrica.')
|
|
107
|
+
raise ValueError('Reconectando al teléfono seleccionado por USB o Wi-Fi. Comprueba la conexión y la depuración inalámbrica.')
|
|
108
|
+
# If identity is unavailable, stick to the chosen transport, never guess another phone.
|
|
109
|
+
chosen = sorted(matching, key=lambda d: d['transport'] != 'usb')[0]
|
|
110
|
+
if not self.identity:
|
|
111
|
+
self.explicit = chosen['serial']
|
|
112
|
+
self.remember(matching)
|
|
113
|
+
return chosen
|
package/diagnostics.js
CHANGED
|
@@ -33,6 +33,12 @@ function check(options={}){
|
|
|
33
33
|
if(py.status!==0)return {ok:false,code:'PYTHON_VERSION',message:'Se necesita Python 3.8 o posterior. Usa --python /ruta/a/python3.',adb,python};
|
|
34
34
|
const result=spawnSync(adb,['devices','-l'],{encoding:'utf8',timeout:15000});
|
|
35
35
|
if(result.error||result.status!==0)return {ok:false,code:'ADB_ERROR',message:'ADB no pudo consultar el teléfono: '+(result.error?.message||(result.stderr||'').trim()).slice(0,600),adb,python};
|
|
36
|
-
|
|
36
|
+
let status=deviceStatus(result.stdout,options.serial);
|
|
37
|
+
// USB and wireless transports can be the same physical phone.
|
|
38
|
+
if(status.code==='MULTIPLE_DEVICES'){
|
|
39
|
+
const probe=spawnSync(python,['-c', 'import json; from device_connection import DeviceConnection; print(json.dumps(DeviceConnection().select()))'],{cwd:__dirname,env:{...process.env,OVERLAY_ADB:adb},encoding:'utf8',timeout:20000,windowsHide:true});
|
|
40
|
+
if(probe.status===0){try{const d=JSON.parse(probe.stdout);status={ok:true,code:'READY',message:'Teléfono conectado por '+(d.transport==='wifi'?'Wi-Fi.':'USB.'),serial:d.serial};}catch{}}
|
|
41
|
+
}
|
|
42
|
+
return {...status,adb,python};
|
|
37
43
|
}
|
|
38
44
|
module.exports={locate,deviceStatus,check,findPython};
|
package/game-store.js
CHANGED
|
@@ -2,7 +2,7 @@ const {inferTurn}=require('./turn-tracker');
|
|
|
2
2
|
const emptyGame=()=>({players:[],connected:false,updatedAt:null,error:null});
|
|
3
3
|
function mergeGame(previous,incoming){
|
|
4
4
|
if(!incoming.connected || !Array.isArray(incoming.players) || incoming.players.length!==2)
|
|
5
|
-
return {...previous,connected:false,error:incoming.error||'Sin lectura válida'};
|
|
5
|
+
return {...previous,connected:false,phoneConnected:incoming.phoneConnected===true,transport:incoming.transport||null,error:incoming.error||'Sin lectura válida'};
|
|
6
6
|
const same=previous.round===incoming.round && previous.players?.length===2 && incoming.players.every(p=>previous.players.some(old=>old.name===p.name));
|
|
7
7
|
const players=incoming.players.map(p=>{
|
|
8
8
|
const old=same?previous.players.find(old=>old.name===p.name):null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ing.jorgeu/wargaming-overlay",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Overlay local para OBS con lectura Android por ADB",
|
|
5
5
|
"bin": {
|
|
6
6
|
"wargaming-overlay": "cli.js"
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"announcements.js",
|
|
33
33
|
"data/faction-facts.json",
|
|
34
34
|
"factions.js",
|
|
35
|
-
"scripts/fact-coverage.js"
|
|
35
|
+
"scripts/fact-coverage.js",
|
|
36
|
+
"device_connection.py"
|
|
36
37
|
],
|
|
37
38
|
"license": "0BSD",
|
|
38
39
|
"publishConfig": {
|
package/public/control.js
CHANGED
|
@@ -47,7 +47,7 @@ document.querySelector('#reset-game').addEventListener('click',async()=>{
|
|
|
47
47
|
catch {document.querySelector('#reset-status').textContent='No se pudo restablecer. Comprueba que el servidor esté actualizado y funcionando.';}
|
|
48
48
|
finally{button.disabled=false}
|
|
49
49
|
});
|
|
50
|
-
async function phoneStatus(){try{const r=await fetch('/game-state.json',{cache:'no-store'});const g=await r.json();document.querySelector('#turn-status').textContent=(g.half==='top'?'TOP · Primer turno. ':g.half==='bottom'?'BOTTOM · Segundo turno. ':'Turno automático: ')+(g.turn?.reason||'Esperando actividad de puntuación.');document.querySelector('#phone-status').textContent=g.connected?'Teléfono conectado.':(g.error||'
|
|
50
|
+
async function phoneStatus(){try{const r=await fetch('/game-state.json',{cache:'no-store'});const g=await r.json();document.querySelector('#turn-status').textContent=(g.half==='top'?'TOP · Primer turno. ':g.half==='bottom'?'BOTTOM · Segundo turno. ':'Turno automático: ')+(g.turn?.reason||'Esperando actividad de puntuación.');document.querySelector('#phone-status').textContent=g.connected?`Teléfono conectado por ${g.transport==='wifi'?'Wi-Fi':'USB'}.`:(g.phoneConnected?`Teléfono conectado por ${g.transport==='wifi'?'Wi-Fi':'USB'}. `:'')+(g.error||'Reconectando…')+(g.players?.length?' Se conserva la última lectura.':'');}catch{document.querySelector('#phone-status').textContent='Servidor no disponible.'}finally{setTimeout(phoneStatus,3000)}}phoneStatus();
|
|
51
51
|
|
|
52
52
|
document.querySelector('#obs-url').textContent=location.origin+'/overlay-art.html';
|
|
53
53
|
|
package/reader.py
CHANGED
|
@@ -5,32 +5,23 @@ from battle_reader import parse, capture
|
|
|
5
5
|
ADB = os.environ.get('OVERLAY_ADB', 'adb')
|
|
6
6
|
SERIAL = os.environ.get('OVERLAY_SERIAL', '')
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if not devices:
|
|
11
|
-
raise ValueError('ADB no detecta un teléfono. Conecta un cable de datos, desbloquea Android y activa Opciones de desarrollador → Depuración USB. No se puede distinguir cable desconectado de depuración desactivada.')
|
|
12
|
-
if not SERIAL and len(devices) > 1:
|
|
13
|
-
raise ValueError('Hay varios dispositivos. Reinicia con --serial SERIAL: ' + ', '.join(d[0] for d in devices))
|
|
14
|
-
device = next((d for d in devices if d[0] == SERIAL), None) if SERIAL else devices[0]
|
|
15
|
-
if not device: raise ValueError('El dispositivo seleccionado no está conectado: ' + SERIAL)
|
|
16
|
-
serial, status = device
|
|
17
|
-
if status == 'unauthorized': raise ValueError('Teléfono sin autorizar. Desbloquéalo y acepta «Permitir depuración USB» para este equipo. Si no aparece, reconecta el cable.')
|
|
18
|
-
if status == 'no': raise ValueError('Sin permisos USB. Revisa reglas udev y grupos del usuario; en Ubuntu, android-sdk-platform-tools-common y plugdev. Vuelve a iniciar sesión después de cambiar grupos.')
|
|
19
|
-
if status == 'offline': raise ValueError('Teléfono offline. Desbloquéalo y reconecta el cable USB.')
|
|
20
|
-
if status != 'device': raise ValueError('Dispositivo no disponible: ' + status)
|
|
21
|
-
return serial
|
|
8
|
+
from device_connection import DeviceConnection
|
|
9
|
+
from pathlib import Path
|
|
22
10
|
|
|
23
11
|
if __name__ == '__main__':
|
|
12
|
+
data_dir = os.environ.get('OVERLAY_DATA_DIR')
|
|
13
|
+
connection = DeviceConnection(SERIAL, Path(data_dir) / 'phone-connection.json' if data_dir else None)
|
|
24
14
|
while True:
|
|
15
|
+
device = None
|
|
25
16
|
try:
|
|
26
|
-
|
|
27
|
-
serial =
|
|
17
|
+
device = connection.select()
|
|
18
|
+
serial = device['serial']
|
|
28
19
|
state = parse(capture(serial))
|
|
29
|
-
print(json.dumps(dict(state, connected=True, updatedAt=time.time(), error=None)), flush=True)
|
|
20
|
+
print(json.dumps(dict(state, connected=True, phoneConnected=True, transport=device["transport"], deviceSerial=serial, updatedAt=time.time(), error=None)), flush=True)
|
|
30
21
|
except Exception as e:
|
|
31
22
|
message = str(e)
|
|
32
23
|
if isinstance(e, subprocess.TimeoutExpired): message = 'El teléfono no respondió a tiempo. Desbloquéalo y abre la ronda; volveré a intentar la lectura.'
|
|
33
24
|
elif isinstance(e, subprocess.CalledProcessError): message = 'No se pudo leer Android. Abre la ronda y cierra otros lectores UIAutomator. Se reintentará. ' + (e.stderr or '').strip()[:300]
|
|
34
25
|
elif isinstance(e, FileNotFoundError): message = 'ADB no está disponible. Ejecuta wargaming-overlay setup --install-adb.'
|
|
35
|
-
print(json.dumps({'connected':False, 'error':message}), flush=True)
|
|
26
|
+
print(json.dumps({'connected':False, 'phoneConnected':device is not None and isinstance(e, ValueError), 'transport':device['transport'] if device is not None else None, 'error':message}), flush=True)
|
|
36
27
|
time.sleep(float(os.environ.get('OVERLAY_INTERVAL', '3')))
|