mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
feat: implement WebRTC signaling and UDP-based video streaming and control service
This commit is contained in:
@@ -79,6 +79,35 @@
|
||||
<div class="wheel">
|
||||
<div class="wheel-disc" id="wheelDisc"></div>
|
||||
</div>
|
||||
|
||||
<div class="control-extra" style="display: flex; gap: 10px; margin-top: 15px; justify-content: space-around; border-top: 1px solid #333; padding-top: 12px;">
|
||||
<div class="gear-indicator" style="text-align: center;">
|
||||
<div style="font-size: 0.8rem; color: #aaa;">GEAR</div>
|
||||
<div style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
|
||||
<button id="btnGearDown" class="btn" style="padding: 2px 8px; font-size: 0.8rem;">-</button>
|
||||
<b id="lblGear" style="font-size: 1.3rem; min-width: 15px; display: inline-block;">N</b>
|
||||
<button id="btnGearUp" class="btn" style="padding: 2px 8px; font-size: 0.8rem;">+</button>
|
||||
</div>
|
||||
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(Z / X)</div>
|
||||
</div>
|
||||
<div class="sys-toggle" style="text-align: center;">
|
||||
<div style="font-size: 0.8rem; color: #aaa;">DRS</div>
|
||||
<button id="btnDrs" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px;">Toggle</button>
|
||||
<div id="lblDrs" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ff3b30;">OFF</div>
|
||||
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(Q)</div>
|
||||
</div>
|
||||
<div class="sys-toggle" style="text-align: center;">
|
||||
<div style="font-size: 0.8rem; color: #aaa;">PIT LIMIT</div>
|
||||
<button id="btnPit" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px;">Toggle</button>
|
||||
<div id="lblPit" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ff3b30;">OFF</div>
|
||||
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(E)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; border-top: 1px solid #333; padding-top: 12px;">
|
||||
<span style="font-size: 0.8rem; color: #aaa;">Пакет управления (JSON):</span>
|
||||
<pre id="ctrlJson" style="font-family: monospace; font-size: 0.75rem; background: #111; border: 1px solid #222; padding: 8px; border-radius: 4px; color: #00ff66; margin: 6px 0 0 0; overflow-x: auto; max-height: 250px; text-align: left; line-height: 1.2;">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
+100
-1
@@ -35,6 +35,14 @@
|
||||
tPos: $('tPos'),
|
||||
tTsm: $('tTsm'),
|
||||
tCars: $('tCars'),
|
||||
lblGear: $('lblGear'),
|
||||
lblDrs: $('lblDrs'),
|
||||
lblPit: $('lblPit'),
|
||||
btnDrs: $('btnDrs'),
|
||||
btnPit: $('btnPit'),
|
||||
btnGearUp: $('btnGearUp'),
|
||||
btnGearDown: $('btnGearDown'),
|
||||
ctrlJson: $('ctrlJson'),
|
||||
};
|
||||
|
||||
// -------- State --------
|
||||
@@ -53,7 +61,7 @@
|
||||
snapshotCount: 0,
|
||||
snapshotWindow: [],
|
||||
keys: new Set(),
|
||||
input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0 },
|
||||
input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0, gear: 0, drs: false, pitLimiter: false },
|
||||
pingTimer: null,
|
||||
inputTimer: null,
|
||||
slotHint: 0, // сервер выдаёт слот; 0 по умолчанию
|
||||
@@ -113,6 +121,12 @@
|
||||
state.input.steer = steer;
|
||||
state.input.handbrake = handbrake;
|
||||
|
||||
// Calculate buttons bitmask: bit 0 (DRS), bit 1 (Pit Limiter)
|
||||
let buttons = 0;
|
||||
if (state.input.drs) buttons |= 1;
|
||||
if (state.input.pitLimiter) buttons |= 2;
|
||||
state.input.buttons = buttons;
|
||||
|
||||
// Update UI bars
|
||||
ui.lblThrottle.textContent = throttle.toFixed(2);
|
||||
ui.lblBrake.textContent = brake.toFixed(2);
|
||||
@@ -129,6 +143,48 @@
|
||||
ui.barSteer.style.width = '2%';
|
||||
|
||||
ui.wheelDisc.style.transform = `rotate(${steer * 90}deg)`;
|
||||
|
||||
// Update Gear, DRS, Pit Limiter UI text & class if elements exist
|
||||
if (ui.lblGear) {
|
||||
let gName = 'N';
|
||||
if (state.input.gear === -1) gName = 'R';
|
||||
if (state.input.gear === 1) gName = 'D';
|
||||
ui.lblGear.textContent = gName;
|
||||
}
|
||||
if (ui.lblDrs) {
|
||||
ui.lblDrs.textContent = state.input.drs ? 'ON' : 'OFF';
|
||||
ui.lblDrs.className = state.input.drs ? 'val active' : 'val';
|
||||
}
|
||||
if (ui.lblPit) {
|
||||
ui.lblPit.textContent = state.input.pitLimiter ? 'ON' : 'OFF';
|
||||
ui.lblPit.className = state.input.pitLimiter ? 'val active' : 'val';
|
||||
}
|
||||
|
||||
// Format control packet as JSON and show in UI
|
||||
if (ui.ctrlJson) {
|
||||
const steerByte = Math.round((steer + 1.0) * 100);
|
||||
const throttleByte = Math.round(throttle * 100);
|
||||
const brakeByte = Math.round(brake * 100);
|
||||
const gearByte = state.input.gear === -1 ? 0 : (state.input.gear === 1 ? 2 : 1);
|
||||
|
||||
const jsonStr = JSON.stringify({
|
||||
sent_json: {
|
||||
steering: steer,
|
||||
throttle: throttle,
|
||||
brake: brake,
|
||||
gear: state.input.gear,
|
||||
buttons: buttons
|
||||
},
|
||||
binary_packet_bytes: {
|
||||
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
|
||||
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
|
||||
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
|
||||
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
|
||||
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${state.input.drs ? 1 : 0}, Pit=${state.input.pitLimiter ? 1 : 0})`
|
||||
}
|
||||
}, null, 2);
|
||||
ui.ctrlJson.textContent = jsonStr;
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Send helpers --------
|
||||
@@ -450,6 +506,24 @@
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
||||
if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
|
||||
state.keys.add(k);
|
||||
|
||||
// Toggle DRS, Pit Limiter, and shift gears
|
||||
if (k === 'q') {
|
||||
state.input.drs = !state.input.drs;
|
||||
pollInput();
|
||||
}
|
||||
if (k === 'e') {
|
||||
state.input.pitLimiter = !state.input.pitLimiter;
|
||||
pollInput();
|
||||
}
|
||||
if (k === 'z') {
|
||||
state.input.gear = Math.max(-1, state.input.gear - 1);
|
||||
pollInput();
|
||||
}
|
||||
if (k === 'x') {
|
||||
state.input.gear = Math.min(1, state.input.gear + 1);
|
||||
pollInput();
|
||||
}
|
||||
});
|
||||
window.addEventListener('keyup', (e) => {
|
||||
state.keys.delete(keyName(e));
|
||||
@@ -462,6 +536,31 @@
|
||||
ui.btnJoin.addEventListener('click', joinRace);
|
||||
ui.btnLeave.addEventListener('click', leaveRace);
|
||||
|
||||
if (ui.btnDrs) {
|
||||
ui.btnDrs.addEventListener('click', () => {
|
||||
state.input.drs = !state.input.drs;
|
||||
pollInput();
|
||||
});
|
||||
}
|
||||
if (ui.btnPit) {
|
||||
ui.btnPit.addEventListener('click', () => {
|
||||
state.input.pitLimiter = !state.input.pitLimiter;
|
||||
pollInput();
|
||||
});
|
||||
}
|
||||
if (ui.btnGearUp) {
|
||||
ui.btnGearUp.addEventListener('click', () => {
|
||||
state.input.gear = Math.min(1, state.input.gear + 1);
|
||||
pollInput();
|
||||
});
|
||||
}
|
||||
if (ui.btnGearDown) {
|
||||
ui.btnGearDown.addEventListener('click', () => {
|
||||
state.input.gear = Math.max(-1, state.input.gear - 1);
|
||||
pollInput();
|
||||
});
|
||||
}
|
||||
|
||||
// -------- Boot --------
|
||||
setConn('disconnected', '');
|
||||
log('x0gp PoC client ready. Press Connect.', 'meta');
|
||||
|
||||
+118
-2
@@ -408,6 +408,35 @@
|
||||
<div id="keyS" class="key-btn">S</div>
|
||||
<div id="keyD" class="key-btn">D</div>
|
||||
</div>
|
||||
|
||||
<div class="control-extra" style="display: flex; gap: 10px; margin-top: 15px; justify-content: space-around; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; width: 100%;">
|
||||
<div class="gear-indicator" style="text-align: center;">
|
||||
<div style="font-size: 0.8rem; color: #8a8d9a;">GEAR</div>
|
||||
<div style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
|
||||
<button id="btnGearDown" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">-</button>
|
||||
<b id="lblGear" style="font-size: 1.3rem; min-width: 15px; display: inline-block;">N</b>
|
||||
<button id="btnGearUp" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">+</button>
|
||||
</div>
|
||||
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(Z / X)</div>
|
||||
</div>
|
||||
<div class="sys-toggle" style="text-align: center;">
|
||||
<div style="font-size: 0.8rem; color: #8a8d9a;">DRS</div>
|
||||
<button id="btnDrs" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">Toggle</button>
|
||||
<div id="lblDrs" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ef4444;">OFF</div>
|
||||
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(Q)</div>
|
||||
</div>
|
||||
<div class="sys-toggle" style="text-align: center;">
|
||||
<div style="font-size: 0.8rem; color: #8a8d9a;">PIT LIMIT</div>
|
||||
<button id="btnPit" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">Toggle</button>
|
||||
<div id="lblPit" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ef4444;">OFF</div>
|
||||
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(E)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; width: 100%;">
|
||||
<span style="font-size: 0.8rem; color: #8a8d9a;">Пакет управления (JSON):</span>
|
||||
<pre id="ctrlJson" style="font-family: monospace; font-size: 0.75rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.05); padding: 8px; border-radius: 4px; color: #00ff66; margin: 6px 0 0 0; overflow-x: auto; max-height: 250px; text-align: left; line-height: 1.2;">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -489,6 +518,10 @@
|
||||
let currentSteer = 0; // -1 to 1
|
||||
let currentThrottle = 0; // 0 to 1
|
||||
let currentBrake = 0; // 0 to 1
|
||||
let currentGear = 0; // 0: N, -1: R, 1: D
|
||||
let drs = false;
|
||||
let pitLimiter = false;
|
||||
let buttons = 0;
|
||||
let pingsReceived = 0;
|
||||
let latencyStart = 0;
|
||||
|
||||
@@ -513,6 +546,22 @@
|
||||
activeKeys[key] = true;
|
||||
updateKeyVisuals();
|
||||
}
|
||||
if (key === 'q') {
|
||||
drs = !drs;
|
||||
updateKeyVisuals();
|
||||
}
|
||||
if (key === 'e') {
|
||||
pitLimiter = !pitLimiter;
|
||||
updateKeyVisuals();
|
||||
}
|
||||
if (key === 'z') {
|
||||
currentGear = Math.max(-1, currentGear - 1);
|
||||
updateKeyVisuals();
|
||||
}
|
||||
if (key === 'x') {
|
||||
currentGear = Math.min(1, currentGear + 1);
|
||||
updateKeyVisuals();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
@@ -542,10 +591,76 @@
|
||||
if (activeKeys.a) currentSteer = -1.0;
|
||||
if (activeKeys.d) currentSteer = 1.0;
|
||||
|
||||
buttons = 0;
|
||||
if (drs) buttons |= 1;
|
||||
if (pitLimiter) buttons |= 2;
|
||||
|
||||
// Update UI Gear/DRS/Pit if elements exist
|
||||
const lblGear = document.getElementById('lblGear');
|
||||
if (lblGear) {
|
||||
let gName = 'N';
|
||||
if (currentGear === -1) gName = 'R';
|
||||
if (currentGear === 1) gName = 'D';
|
||||
lblGear.innerText = gName;
|
||||
}
|
||||
const lblDrs = document.getElementById('lblDrs');
|
||||
if (lblDrs) {
|
||||
lblDrs.innerText = drs ? 'ON' : 'OFF';
|
||||
lblDrs.style.color = drs ? '#00ff66' : '#ef4444';
|
||||
}
|
||||
const lblPit = document.getElementById('lblPit');
|
||||
if (lblPit) {
|
||||
lblPit.innerText = pitLimiter ? 'ON' : 'OFF';
|
||||
lblPit.style.color = pitLimiter ? '#00ff66' : '#ef4444';
|
||||
}
|
||||
|
||||
const ctrlJson = document.getElementById('ctrlJson');
|
||||
if (ctrlJson) {
|
||||
const steerByte = Math.round((currentSteer + 1.0) * 100);
|
||||
const throttleByte = Math.round(currentThrottle * 100);
|
||||
const brakeByte = Math.round(currentBrake * 100);
|
||||
const gearByte = currentGear === -1 ? 0 : (currentGear === 1 ? 2 : 1);
|
||||
|
||||
ctrlJson.innerText = JSON.stringify({
|
||||
sent_json: {
|
||||
steering: currentSteer,
|
||||
throttle: currentThrottle,
|
||||
brake: currentBrake,
|
||||
gear: currentGear,
|
||||
buttons: buttons
|
||||
},
|
||||
binary_packet_bytes: {
|
||||
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
|
||||
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
|
||||
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
|
||||
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
|
||||
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${drs ? 1 : 0}, Pit=${pitLimiter ? 1 : 0})`
|
||||
}
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
// Log control changes to UI console
|
||||
log(`Controls updated: throttle=${currentThrottle}, steering=${currentSteer}, brake=${currentBrake}`, 'out');
|
||||
log(`Controls updated: throttle=${currentThrottle}, steering=${currentSteer}, brake=${currentBrake}, gear=${currentGear}, buttons=${buttons}`, 'out');
|
||||
}
|
||||
|
||||
// Click listeners for the custom controls
|
||||
document.getElementById('btnGearDown').addEventListener('click', () => {
|
||||
currentGear = Math.max(-1, currentGear - 1);
|
||||
updateKeyVisuals();
|
||||
});
|
||||
document.getElementById('btnGearUp').addEventListener('click', () => {
|
||||
currentGear = Math.min(1, currentGear + 1);
|
||||
updateKeyVisuals();
|
||||
});
|
||||
document.getElementById('btnDrs').addEventListener('click', () => {
|
||||
drs = !drs;
|
||||
updateKeyVisuals();
|
||||
});
|
||||
document.getElementById('btnPit').addEventListener('click', () => {
|
||||
pitLimiter = !pitLimiter;
|
||||
updateKeyVisuals();
|
||||
});
|
||||
|
||||
connectBtn.addEventListener('click', () => {
|
||||
if (pc) {
|
||||
disconnectCockpit();
|
||||
@@ -742,7 +857,8 @@
|
||||
throttle: currentThrottle,
|
||||
steering: currentSteer,
|
||||
brake: currentBrake,
|
||||
gear: 1,
|
||||
gear: currentGear,
|
||||
buttons: buttons,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
dataChannel.send(JSON.stringify(payload));
|
||||
|
||||
Reference in New Issue
Block a user