@@ -1310,52 +1310,186 @@ async function loadScripts() { await scriptEngine.load(); }
13101310
13111311class BTController {
13121312 constructor ( ) {
1313- this . _connected = false ;
1313+ this . _connected = false ;
1314+ this . _gamepadIdx = null ;
1315+ this . _prevBtns = { } ;
1316+ this . _driveActive = false ;
1317+ this . _domeActive = false ;
1318+ this . _lastDriveMs = 0 ;
1319+ this . _lastDomeMs = 0 ;
1320+ this . _DRIVE_HZ = 1000 / 30 ; // 30 req/s
1321+ this . _DOME_HZ = 1000 / 20 ; // 20 req/s
13141322 this . _loadMappings ( ) ;
1323+ this . _bind ( ) ;
13151324 }
13161325
1317- updateStatus ( data ) {
1318- if ( ! data ) return ;
1319- const connected = data . bt_connected || false ;
1320- const name = data . bt_name || '—' ;
1321- const pct = data . bt_battery || 0 ;
1326+ _bind ( ) {
1327+ window . addEventListener ( 'gamepadconnected' , e => this . _onConnect ( e . gamepad ) ) ;
1328+ window . addEventListener ( 'gamepaddisconnected' , e => this . _onDisconnect ( e . gamepad ) ) ;
1329+ const poll = ( ) => { this . _tick ( ) ; requestAnimationFrame ( poll ) ; } ;
1330+ requestAnimationFrame ( poll ) ;
1331+ }
1332+
1333+ _onConnect ( gp ) {
1334+ this . _gamepadIdx = gp . index ;
1335+ this . _connected = true ;
1336+ this . _prevBtns = { } ;
1337+ this . _setUI ( true , gp . id . split ( '(' ) [ 0 ] . trim ( ) . slice ( 0 , 24 ) ) ;
1338+ toast ( 'Manette BT connectée' , 'ok' ) ;
1339+ }
1340+
1341+ _onDisconnect ( gp ) {
1342+ if ( gp . index !== this . _gamepadIdx ) return ;
1343+ this . _gamepadIdx = null ;
1344+ this . _connected = false ;
1345+ this . _driveActive = false ;
1346+ this . _domeActive = false ;
1347+ this . _setUI ( false , '—' ) ;
1348+ api ( '/motion/stop' , 'POST' ) ;
1349+ api ( '/motion/dome/stop' , 'POST' ) ;
1350+ toast ( 'Manette BT déconnectée' , 'error' ) ;
1351+ }
1352+
1353+ _tick ( ) {
1354+ const pads = navigator . getGamepads ? navigator . getGamepads ( ) : [ ] ;
1355+
1356+ // Auto-détection si pas encore associée
1357+ if ( this . _gamepadIdx === null ) {
1358+ for ( let i = 0 ; i < pads . length ; i ++ ) {
1359+ if ( pads [ i ] ) { this . _onConnect ( pads [ i ] ) ; break ; }
1360+ }
1361+ }
1362+ if ( this . _gamepadIdx === null ) return ;
1363+
1364+ const gp = pads [ this . _gamepadIdx ] ;
1365+ if ( ! gp ) {
1366+ if ( this . _connected ) this . _onDisconnect ( { index : this . _gamepadIdx } ) ;
1367+ return ;
1368+ }
1369+
1370+ const m = this . _getMappings ( ) ;
1371+ const dz = ( parseInt ( m . deadzone ) || 8 ) / 100 ;
1372+ const now = performance . now ( ) ;
13221373
1323- this . _connected = connected ;
1374+ // ── PROPULSION — bloquée en Child Lock (mode 2) ────────────────
1375+ if ( ! lockMgr . isDriveLocked ( ) ) {
1376+ const tRaw = - this . _axis ( gp , m . throttle || 'L_STICK_Y' ) ;
1377+ const sRaw = this . _axis ( gp , m . steer || 'L_STICK_X' ) ;
1378+ const t = Math . abs ( tRaw ) > dz ? tRaw * _speedLimit : 0 ;
1379+ const s = Math . abs ( sRaw ) > dz ? sRaw * _speedLimit * 0.55 : 0 ;
1380+
1381+ if ( now - this . _lastDriveMs >= this . _DRIVE_HZ ) {
1382+ this . _lastDriveMs = now ;
1383+ if ( Math . abs ( t ) > 0.01 || Math . abs ( s ) > 0.01 ) {
1384+ api ( '/motion/arcade' , 'POST' , { throttle : t , steering : s } ) ;
1385+ this . _driveActive = true ;
1386+ } else if ( this . _driveActive ) {
1387+ api ( '/motion/stop' , 'POST' ) ;
1388+ this . _driveActive = false ;
1389+ }
1390+ }
1391+ } else if ( this . _driveActive ) {
1392+ // Child Lock activé en cours de conduite → arrêt immédiat
1393+ api ( '/motion/stop' , 'POST' ) ;
1394+ this . _driveActive = false ;
1395+ }
13241396
1325- const icon = document . querySelector ( '.gamepad-icon' ) ;
1397+ // ── DÔME ──────────────────────────────────────────────────────
1398+ const dRaw = this . _axis ( gp , m . dome || 'R_STICK_X' ) ;
1399+ if ( now - this . _lastDomeMs >= this . _DOME_HZ ) {
1400+ this . _lastDomeMs = now ;
1401+ if ( Math . abs ( dRaw ) > dz ) {
1402+ api ( '/motion/dome/turn' , 'POST' , { speed : dRaw * 0.85 } ) ;
1403+ this . _domeActive = true ;
1404+ } else if ( this . _domeActive ) {
1405+ api ( '/motion/dome/stop' , 'POST' ) ;
1406+ this . _domeActive = false ;
1407+ }
1408+ }
1409+
1410+ // ── BOUTONS — détection de front montant/descendant ───────────
1411+ const prev = this . _prevBtns ;
1412+
1413+ // Panneau dôme : ouvert tant qu'appuyé
1414+ const p1 = m . panel1 || 'SQUARE' ;
1415+ const p1v = this . _btn ( gp , p1 ) ;
1416+ if ( p1v && ! prev [ p1 ] ) api ( '/servo/dome/open_all' , 'POST' ) ;
1417+ if ( ! p1v && prev [ p1 ] ) api ( '/servo/dome/close_all' , 'POST' ) ;
1418+ prev [ p1 ] = p1v ;
1419+
1420+ // Panneau body : ouvert tant qu'appuyé
1421+ const p2 = m . panel2 || 'TRIANGLE' ;
1422+ const p2v = this . _btn ( gp , p2 ) ;
1423+ if ( p2v && ! prev [ p2 ] ) api ( '/servo/body/open_all' , 'POST' ) ;
1424+ if ( ! p2v && prev [ p2 ] ) api ( '/servo/body/close_all' , 'POST' ) ;
1425+ prev [ p2 ] = p2v ;
1426+
1427+ // Son aléatoire — front montant seulement
1428+ const au = m . audio || 'CIRCLE' ;
1429+ const auv = this . _btn ( gp , au ) ;
1430+ if ( auv && ! prev [ au ] ) api ( '/audio/random' , 'POST' , { category : 'happy' } ) ;
1431+ prev [ au ] = auv ;
1432+ }
1433+
1434+ // Lecture d'axe — retourne -1..1
1435+ _axis ( gp , name ) {
1436+ const axisMap = { L_STICK_X : 0 , L_STICK_Y : 1 , R_STICK_X : 2 , R_STICK_Y : 3 } ;
1437+ if ( name in axisMap ) return gp . axes [ axisMap [ name ] ] || 0 ;
1438+ const btnMap = { L2 : 6 , R2 : 7 } ;
1439+ if ( name in btnMap ) { const b = gp . buttons [ btnMap [ name ] ] ; return b ? b . value : 0 ; }
1440+ return 0 ;
1441+ }
1442+
1443+ // Lecture bouton — retourne bool
1444+ _btn ( gp , name ) {
1445+ const map = {
1446+ CROSS : 0 , CIRCLE : 1 , SQUARE : 2 , TRIANGLE : 3 ,
1447+ L1 : 4 , R1 : 5 , L2 : 6 , R2 : 7 , SELECT : 8 , START : 9 ,
1448+ L3 : 10 , R3 : 11 , DPAD_UP : 12 , DPAD_DOWN : 13 , DPAD_LEFT : 14 , DPAD_RIGHT : 15 ,
1449+ } ;
1450+ const idx = map [ name ] ;
1451+ return ( idx !== undefined && gp . buttons [ idx ] ) ? gp . buttons [ idx ] . pressed : false ;
1452+ }
1453+
1454+ _setUI ( connected , name ) {
1455+ const icon = document . querySelector ( '.gamepad-icon' ) ;
13261456 const statusText = el ( 'bt-status-text' ) ;
13271457 const deviceName = el ( 'bt-device-name' ) ;
13281458 const pillBt = el ( 'pill-bt' ) ;
1329- const fillEl = el ( 'bt-battery-fill' ) ;
1330- const pctEl = document . querySelector ( '#bt-battery-pct' ) || el ( 'bt-battery-pct' ) ;
1331-
13321459 if ( icon ) icon . classList . toggle ( 'connected' , connected ) ;
1333- if ( statusText ) {
1334- statusText . textContent = connected ? 'CONNECTED' : 'NOT CONNECTED' ;
1335- statusText . classList . toggle ( 'connected' , connected ) ;
1336- }
1337- if ( deviceName ) deviceName . textContent = name ;
1460+ if ( statusText ) { statusText . textContent = connected ? 'CONNECTED' : 'NOT CONNECTED' ; statusText . classList . toggle ( 'connected' , connected ) ; }
1461+ if ( deviceName ) deviceName . textContent = name || '—' ;
13381462 if ( pillBt ) pillBt . className = 'status-pill ' + ( connected ? 'ok' : '' ) ;
1463+ }
13391464
1340- if ( fillEl ) {
1465+ // Appelé par le poller status — le statut BT réel vient de la Gamepad API (local)
1466+ updateStatus ( data ) {
1467+ if ( ! data ) return ;
1468+ // Batterie si le serveur la connaît (futur)
1469+ const pct = data . bt_battery || 0 ;
1470+ if ( pct > 0 ) {
1471+ const fillEl = el ( 'bt-battery-fill' ) ;
1472+ const pctEl = el ( 'bt-battery-pct' ) ;
13411473 const bcolor = pct > 50 ? '#00cc66' : pct > 25 ? '#ff8800' : '#ff2244' ;
1342- fillEl . style . width = pct + '%' ;
1343- fillEl . style . background = bcolor ;
1474+ if ( fillEl ) { fillEl . style . width = pct + '%' ; fillEl . style . background = bcolor ; }
1475+ if ( pctEl ) pctEl . textContent = pct + '%' ;
13441476 }
1345- if ( pctEl ) pctEl . textContent = pct + '%' ;
1477+ }
1478+
1479+ _getMappings ( ) {
1480+ try { const s = localStorage . getItem ( 'r2d2-bt-mappings' ) ; return s ? JSON . parse ( s ) : { } ; }
1481+ catch { return { } ; }
13461482 }
13471483
13481484 _loadMappings ( ) {
13491485 try {
1350- const saved = localStorage . getItem ( 'r2d2-bt-mappings' ) ;
1351- if ( ! saved ) return ;
1352- const m = JSON . parse ( saved ) ;
1353- if ( m . throttle ) { const e = el ( 'bt-map-throttle' ) ; if ( e ) { for ( let o of e . options ) if ( o . value === m . throttle ) { o . selected = true ; break ; } } }
1354- if ( m . steer ) { const e = el ( 'bt-map-steer' ) ; if ( e ) { for ( let o of e . options ) if ( o . value === m . steer ) { o . selected = true ; break ; } } }
1355- if ( m . dome ) { const e = el ( 'bt-map-dome' ) ; if ( e ) { for ( let o of e . options ) if ( o . value === m . dome ) { o . selected = true ; break ; } } }
1486+ const m = this . _getMappings ( ) ;
1487+ if ( m . throttle ) { const e = el ( 'bt-map-throttle' ) ; if ( e ) { for ( const o of e . options ) if ( o . value === m . throttle ) { o . selected = true ; break ; } } }
1488+ if ( m . steer ) { const e = el ( 'bt-map-steer' ) ; if ( e ) { for ( const o of e . options ) if ( o . value === m . steer ) { o . selected = true ; break ; } } }
1489+ if ( m . dome ) { const e = el ( 'bt-map-dome' ) ; if ( e ) { for ( const o of e . options ) if ( o . value === m . dome ) { o . selected = true ; break ; } } }
13561490 const dz = el ( 'bt-deadzone' ) ;
1357- if ( dz && m . deadzone ) { dz . value = m . deadzone ; el ( 'bt-deadzone-val' ) . textContent = m . deadzone + '%' ; }
1358- } catch ( e ) { /* ignore */ }
1491+ if ( dz && m . deadzone ) { dz . value = m . deadzone ; const dzv = el ( 'bt-deadzone-val' ) ; if ( dzv ) dzv . textContent = m . deadzone + '%' ; }
1492+ } catch { /* ignore */ }
13591493 }
13601494
13611495 saveMappings ( ) {
0 commit comments