added websocket for separate computers on same wifi/lan
This commit is contained in:
parent
3c9646cf0b
commit
d3f7f374ab
48
README.md
48
README.md
@ -87,6 +87,54 @@ Simple browser-based tool with an Admin panel and a full-screen Patient view. Th
|
|||||||
- Open `index.html` and use the Admin panel
|
- Open `index.html` and use the Admin panel
|
||||||
- Click “Open Patient View” to launch `patient.html` in a separate tab/window
|
- Click “Open Patient View” to launch `patient.html` in a separate tab/window
|
||||||
|
|
||||||
|
## Network Mode Setup (Two Computers on the Same Wi‑Fi/LAN)
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
- Therapist runs a lightweight relay (`server.py`) once on their computer.
|
||||||
|
- Admin connects to the relay with a Session ID and copies the Patient Join Link.
|
||||||
|
- Patient opens the link on their computer and performs a quick gesture to unlock audio.
|
||||||
|
|
||||||
|
### One‑Time Install (WebSocket Relay)
|
||||||
|
- Windows:
|
||||||
|
- Press `Windows` key → type `PowerShell` → `Enter`
|
||||||
|
- Check Python: `python --version` (or `py --version`)
|
||||||
|
- Install package: `python -m pip install websockets` (or `py -m pip install websockets`)
|
||||||
|
- Start relay: `python server.py` (or `py server.py`)
|
||||||
|
- If prompted by Windows Defender Firewall, click “Allow access”.
|
||||||
|
- macOS:
|
||||||
|
- Press `Cmd+Space` → type `Terminal` → `Enter`
|
||||||
|
- Check Python: `python3 --version`
|
||||||
|
- Install package: `python3 -m pip install websockets`
|
||||||
|
- Start relay: `python3 server.py`
|
||||||
|
- Linux (Ubuntu/Debian/Fedora etc.):
|
||||||
|
- Press `Ctrl+Alt+T` to open Terminal
|
||||||
|
- Check Python: `python3 --version`
|
||||||
|
- Install package: `python3 -m pip install websockets`
|
||||||
|
- Start relay: `python3 server.py`
|
||||||
|
|
||||||
|
### Find Your Therapist Computer’s IP Address
|
||||||
|
- Windows: run `ipconfig` → use the `IPv4 Address` on your active adapter (e.g., `192.168.1.5`).
|
||||||
|
- macOS: run `ipconfig getifaddr en0` (Wi‑Fi) or `ipconfig getifaddr en1` (alternate); or `ifconfig | grep "inet "`.
|
||||||
|
- Linux: run `hostname -I` and pick the local `192.168.x.x` address; or `ip addr show`.
|
||||||
|
|
||||||
|
### Connect Admin (Therapist Computer)
|
||||||
|
1) Open Admin in a browser: `http://localhost:8000/`
|
||||||
|
2) In “Network Mode”:
|
||||||
|
- Server URL: `ws://<THERAPIST_IP>:8787` (e.g., `ws://192.168.1.5:8787`)
|
||||||
|
- Session ID: any label you like (e.g., `room-1`)
|
||||||
|
- Click `Connect`
|
||||||
|
3) Copy the Patient Join Link shown below the connection fields.
|
||||||
|
|
||||||
|
### Connect Patient (Second Computer)
|
||||||
|
1) Open the copied Patient Join Link in a browser
|
||||||
|
2) Perform a quick gesture (mouse move or key press) to unlock audio (browser policy)
|
||||||
|
3) The Patient view will mirror Admin changes in real time
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
- If the Patient does not connect, verify both computers are on the same network and can reach `ws://<THERAPIST_IP>:8787`.
|
||||||
|
- On Windows, allow the relay through the firewall when prompted.
|
||||||
|
- You can disconnect Network Mode and continue Local Mode at any time.
|
||||||
|
|
||||||
## Data Storage
|
## Data Storage
|
||||||
|
|
||||||
- Presets: `localStorage` key `bls_presets`
|
- Presets: `localStorage` key `bls_presets`
|
||||||
|
|||||||
17
index.html
17
index.html
@ -224,6 +224,23 @@
|
|||||||
<button data-mult="3">3×</button>
|
<button data-mult="3">3×</button>
|
||||||
<button data-mult="4">4×</button>
|
<button data-mult="4">4×</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel" style="margin-top:8px">
|
||||||
|
<h2>Network Mode</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>Server URL:</label>
|
||||||
|
<input type="text" id="wsUrl" placeholder="ws://192.168.x.x:8787" style="width:220px">
|
||||||
|
<label>Session ID:</label>
|
||||||
|
<input type="text" id="wsSession" placeholder="room-1" style="width:120px">
|
||||||
|
<button id="wsConnect">Connect</button>
|
||||||
|
<button id="wsDisconnect">Disconnect</button>
|
||||||
|
<span class="small">Status: <span id="wsStatus">Disconnected</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Patient Join Link:</label>
|
||||||
|
<input type="text" id="joinLink" readonly style="flex:1; min-width:260px">
|
||||||
|
<button id="copyJoinLink">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
51
js/admin.js
51
js/admin.js
@ -105,6 +105,7 @@ function updateTimerLabel(){ var m = Math.max(0, parseInt(timerMinutes.value||'0
|
|||||||
function recordSession(){ var n = (patientName.value||'').trim()||'Unnamed'; var st = BLS.loadState(); if(!st.startedAt) return; var end = Date.now(); var durationSec = Math.floor((end - st.startedAt)/1000); var p = getPatient(n); p.sessions.push({ start: st.startedAt, end: end, durationSec: durationSec }); p.count = (p.count||0)+1; p.last = { start: st.startedAt, end: end, durationSec: durationSec }; setPatient(p); refreshPatientUI() }
|
function recordSession(){ var n = (patientName.value||'').trim()||'Unnamed'; var st = BLS.loadState(); if(!st.startedAt) return; var end = Date.now(); var durationSec = Math.floor((end - st.startedAt)/1000); var p = getPatient(n); p.sessions.push({ start: st.startedAt, end: end, durationSec: durationSec }); p.count = (p.count||0)+1; p.last = { start: st.startedAt, end: end, durationSec: durationSec }; setPatient(p); refreshPatientUI() }
|
||||||
function refreshPresetList(){ var names = BLS.listPresets(); var html=''; for(var i=0;i<names.length;i++){ var n=names[i]; html += '<option value="'+n+'">'+n+'</option>' } presetList.innerHTML = html }
|
function refreshPresetList(){ var names = BLS.listPresets(); var html=''; for(var i=0;i<names.length;i++){ var n=names[i]; html += '<option value="'+n+'">'+n+'</option>' } presetList.innerHTML = html }
|
||||||
function applyState(next, broadcast){ state = BLS.mergeState(state, next||{}); renderer.setState(state); if(state.running){ renderer.start() } else { renderer.stop() } if(broadcast){ chan.send(state) } }
|
function applyState(next, broadcast){ state = BLS.mergeState(state, next||{}); renderer.setState(state); if(state.running){ renderer.start() } else { renderer.stop() } if(broadcast){ chan.send(state) } }
|
||||||
|
function publishState(){ try{ if(wsConnected && ws){ ws.send(JSON.stringify({type:'state', sessionId: wsSid||'default', payload: BLS.loadState()})) } }catch(e){} }
|
||||||
shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.shape; if(!d) return; applyState({shape:d}, true); setActive(shapeControls,'shape','shape'); });
|
shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.shape; if(!d) return; applyState({shape:d}, true); setActive(shapeControls,'shape','shape'); });
|
||||||
directionControls.addEventListener('click', function(e){ var d = e.target.dataset.direction; if(!d) return; applyState({direction:d}, true); setActive(directionControls,'direction','direction'); });
|
directionControls.addEventListener('click', function(e){ var d = e.target.dataset.direction; if(!d) return; applyState({direction:d}, true); setActive(directionControls,'direction','direction'); });
|
||||||
easingControls.addEventListener('click', function(e){ var d = e.target.dataset.easing; if(!d) return; applyState({easingMode:d}, true); setActive(easingControls,'easing','easingMode'); });
|
easingControls.addEventListener('click', function(e){ var d = e.target.dataset.easing; if(!d) return; applyState({easingMode:d}, true); setActive(easingControls,'easing','easingMode'); });
|
||||||
@ -112,18 +113,18 @@ shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.sh
|
|||||||
bgColor.addEventListener('input', function(){ applyState({bgColor:bgColor.value}, true); });
|
bgColor.addEventListener('input', function(){ applyState({bgColor:bgColor.value}, true); });
|
||||||
shapePreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; shapeColor.value = c; applyState({shapeColor:c}, true); });
|
shapePreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; shapeColor.value = c; applyState({shapeColor:c}, true); });
|
||||||
bgPreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; bgColor.value = c; applyState({bgColor:c}, true); });
|
bgPreset.addEventListener('click', function(e){ var c = e.target.dataset.color; if(!c) return; bgColor.value = c; applyState({bgColor:c}, true); });
|
||||||
speed.addEventListener('input', function(){ updateSpeedLabel(); var m = (BLS.loadState().speedMultiplier||1); applyState({speed:parseInt(speed.value,10)*m}, true); });
|
speed.addEventListener('input', function(){ updateSpeedLabel(); var m = (BLS.loadState().speedMultiplier||1); applyState({speed:parseInt(speed.value,10)*m}, true); publishState() });
|
||||||
size.addEventListener('input', function(){ updateSizeLabel(); applyState({size:parseInt(size.value,10)}, true); });
|
size.addEventListener('input', function(){ updateSizeLabel(); applyState({size:parseInt(size.value,10)}, true); });
|
||||||
yPercent.addEventListener('input', function(){ updateYPercentLabel(); applyState({yPercent:parseInt(yPercent.value,10)}, true); });
|
yPercent.addEventListener('input', function(){ updateYPercentLabel(); applyState({yPercent:parseInt(yPercent.value,10)}, true); });
|
||||||
yReset.addEventListener('click', function(){ yPercent.value=50; updateYPercentLabel(); applyState({yPercent:50}, true); });
|
yReset.addEventListener('click', function(){ yPercent.value=50; updateYPercentLabel(); applyState({yPercent:50}, true); });
|
||||||
glowEnable.addEventListener('change', function(){ applyState({glowEnabled: glowEnable.checked}, true) });
|
glowEnable.addEventListener('change', function(){ applyState({glowEnabled: glowEnable.checked}, true); publishState() });
|
||||||
glowIntensity.addEventListener('input', function(){ updateGlowIntensityLabel(); var f = parseInt(glowIntensity.value,10)/100; applyState({glowIntensity:f}, true) });
|
glowIntensity.addEventListener('input', function(){ updateGlowIntensityLabel(); var f = parseInt(glowIntensity.value,10)/100; applyState({glowIntensity:f}, true); publishState() });
|
||||||
glowRate.addEventListener('input', function(){ updateGlowRateLabel(); applyState({glowRate: parseFloat(glowRate.value)}, true) });
|
glowRate.addEventListener('input', function(){ updateGlowRateLabel(); applyState({glowRate: parseFloat(glowRate.value)}, true); publishState() });
|
||||||
rampEnable.addEventListener('change', function(){ var st = BLS.loadState(); var next = {rampEnabled: rampEnable.checked}; if(!rampEnable.checked){ next.easingMode = 'linear' } if(st.running && rampEnable.checked){ next.startedAt = Date.now() } applyState(next, true) });
|
rampEnable.addEventListener('change', function(){ var st = BLS.loadState(); var next = {rampEnabled: rampEnable.checked}; if(!rampEnable.checked){ next.easingMode = 'linear' } if(st.running && rampEnable.checked){ next.startedAt = Date.now() } applyState(next, true); publishState() });
|
||||||
rampSeconds.addEventListener('input', function(){ updateRampLabel(); var st = BLS.loadState(); var next = {rampSeconds: parseInt(rampSeconds.value,10)}; if(st.running && st.rampEnabled){ next.startedAt = Date.now() } applyState(next, true) });
|
rampSeconds.addEventListener('input', function(){ updateRampLabel(); var st = BLS.loadState(); var next = {rampSeconds: parseInt(rampSeconds.value,10)}; if(st.running && st.rampEnabled){ next.startedAt = Date.now() } applyState(next, true); publishState() });
|
||||||
edgePause.addEventListener('input', function(){ updateEdgePauseLabel(); applyState({edgePauseMs: parseInt(edgePause.value,10)}, true) });
|
edgePause.addEventListener('input', function(){ updateEdgePauseLabel(); applyState({edgePauseMs: parseInt(edgePause.value,10)}, true); publishState() });
|
||||||
wiggleEnable.addEventListener('change', function(){ applyState({wiggleEnabled: wiggleEnable.checked}, true) });
|
wiggleEnable.addEventListener('change', function(){ applyState({wiggleEnabled: wiggleEnable.checked}, true); publishState() });
|
||||||
wiggleAmplitude.addEventListener('input', function(){ updateWiggleLabel(); applyState({wiggleAmplitude: parseInt(wiggleAmplitude.value,10)}, true) });
|
wiggleAmplitude.addEventListener('input', function(){ updateWiggleLabel(); applyState({wiggleAmplitude: parseInt(wiggleAmplitude.value,10)}, true); publishState() });
|
||||||
timerMinutes.addEventListener('input', function(){ updateTimerLabel(); });
|
timerMinutes.addEventListener('input', function(){ updateTimerLabel(); });
|
||||||
savePresetBtn.addEventListener('click', function(){ var n = (presetName.value||'').trim(); if(!n) return; BLS.savePreset(n, BLS.loadState()); refreshPresetList() })
|
savePresetBtn.addEventListener('click', function(){ var n = (presetName.value||'').trim(); if(!n) return; BLS.savePreset(n, BLS.loadState()); refreshPresetList() })
|
||||||
applyPresetBtn.addEventListener('click', function(){ var n = presetList.value; if(!n) return; var preset = BLS.loadPreset(n); if(!preset) return; applyState(preset, true); setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode') })
|
applyPresetBtn.addEventListener('click', function(){ var n = presetList.value; if(!n) return; var preset = BLS.loadPreset(n); if(!preset) return; applyState(preset, true); setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode') })
|
||||||
@ -136,11 +137,11 @@ shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.sh
|
|||||||
audioSelect.addEventListener('change', function(){ var name = audioSelect.value || null; applyState({audioName:name}, true) });
|
audioSelect.addEventListener('change', function(){ var name = audioSelect.value || null; applyState({audioName:name}, true) });
|
||||||
audioEnable.addEventListener('change', function(){ applyState({audioEnabled: audioEnable.checked}, true) });
|
audioEnable.addEventListener('change', function(){ applyState({audioEnabled: audioEnable.checked}, true) });
|
||||||
audioMode.addEventListener('change', function(){ refreshAudioMode(); applyState({audioMode: audioMode.value}, true) });
|
audioMode.addEventListener('change', function(){ refreshAudioMode(); applyState({audioMode: audioMode.value}, true) });
|
||||||
programPreset.addEventListener('change', function(){ applyState({programPreset: programPreset.value}, true) });
|
programPreset.addEventListener('change', function(){ applyState({programPreset: programPreset.value}, true); publishState() });
|
||||||
syncPan.addEventListener('change', function(){ applyState({syncPan: syncPan.checked}, true) });
|
syncPan.addEventListener('change', function(){ applyState({syncPan: syncPan.checked}, true); publishState() });
|
||||||
panRate.addEventListener('input', function(){ updatePanRateLabel(); applyState({panRate: parseFloat(panRate.value)}, true) });
|
panRate.addEventListener('input', function(){ updatePanRateLabel(); applyState({panRate: parseFloat(panRate.value)}, true); publishState() });
|
||||||
cueRate.addEventListener('input', function(){ updateCueRateLabel(); applyState({cueRate: parseFloat(cueRate.value)}, true) });
|
cueRate.addEventListener('input', function(){ updateCueRateLabel(); applyState({cueRate: parseFloat(cueRate.value)}, true); publishState() });
|
||||||
volume.addEventListener('input', function(){ updateVolumeLabel(); applyState({volume: parseInt(volume.value,10)/100}, true) });
|
volume.addEventListener('input', function(){ updateVolumeLabel(); applyState({volume: parseInt(volume.value,10)/100}, true); publishState() });
|
||||||
testSound.addEventListener('click', function(){ try{ var st = BLS.loadState(); if(st.audioMode==='programmatic'){ AudioEngine.setVolume(st.volume||0.6); var pan = 0; if(st.programPreset==='click'){ AudioEngine.playClick(pan) } else if(st.programPreset==='ping'){ AudioEngine.playPing(pan) } else if(st.programPreset==='woodblock'){ AudioEngine.playWoodblock(pan) } else if(st.programPreset==='bell'){ AudioEngine.playBell(pan) } else if(st.programPreset==='bass'){ AudioEngine.playBass(pan) } else if(st.programPreset==='kick'){ AudioEngine.playKick(pan) } else if(st.programPreset==='snare'){ AudioEngine.playSnare(pan) } else if(st.programPreset==='hihat'){ AudioEngine.playHiHat(pan) } else if(st.programPreset==='sweep'){ AudioEngine.playSweep(pan) } else if(st.programPreset==='zap'){ AudioEngine.playZap(pan) } else if(st.programPreset==='bubble'){ AudioEngine.playBubble(pan) } else if(st.programPreset==='pink'){ AudioEngine.startPinkNoisePan(st.panRate||0.5); setTimeout(function(){ AudioEngine.stopNoise() }, 800) } else if(st.programPreset==='hybrid'){ AudioEngine.startHybrid(st.cueRate||1.2); setTimeout(function(){ AudioEngine.stopNoise() }, 1000) } } }catch(e){} });
|
testSound.addEventListener('click', function(){ try{ var st = BLS.loadState(); if(st.audioMode==='programmatic'){ AudioEngine.setVolume(st.volume||0.6); var pan = 0; if(st.programPreset==='click'){ AudioEngine.playClick(pan) } else if(st.programPreset==='ping'){ AudioEngine.playPing(pan) } else if(st.programPreset==='woodblock'){ AudioEngine.playWoodblock(pan) } else if(st.programPreset==='bell'){ AudioEngine.playBell(pan) } else if(st.programPreset==='bass'){ AudioEngine.playBass(pan) } else if(st.programPreset==='kick'){ AudioEngine.playKick(pan) } else if(st.programPreset==='snare'){ AudioEngine.playSnare(pan) } else if(st.programPreset==='hihat'){ AudioEngine.playHiHat(pan) } else if(st.programPreset==='sweep'){ AudioEngine.playSweep(pan) } else if(st.programPreset==='zap'){ AudioEngine.playZap(pan) } else if(st.programPreset==='bubble'){ AudioEngine.playBubble(pan) } else if(st.programPreset==='pink'){ AudioEngine.startPinkNoisePan(st.panRate||0.5); setTimeout(function(){ AudioEngine.stopNoise() }, 800) } else if(st.programPreset==='hybrid'){ AudioEngine.startHybrid(st.cueRate||1.2); setTimeout(function(){ AudioEngine.stopNoise() }, 1000) } } }catch(e){} });
|
||||||
knobSens.addEventListener('input', function(){ updateKnobSensLabel() });
|
knobSens.addEventListener('input', function(){ updateKnobSensLabel() });
|
||||||
if(clearSessionsBtn){ clearSessionsBtn.addEventListener('click', function(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); p.sessions = []; p.count = 0; p.last = null; setPatient(p); refreshPatientUI() }) }
|
if(clearSessionsBtn){ clearSessionsBtn.addEventListener('click', function(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); p.sessions = []; p.count = 0; p.last = null; setPatient(p); refreshPatientUI() }) }
|
||||||
@ -155,8 +156,13 @@ shapeControls.addEventListener('click', function(e){ var d = e.target.dataset.sh
|
|||||||
var tickId = null;
|
var tickId = null;
|
||||||
function startTimer(){ if(tickId) clearInterval(tickId); var durationSec = Math.max(0, (parseInt(timerMinutes.value||'0',10)||0)*60); var startedAt = Date.now(); applyState({running:true, durationSec:durationSec, startedAt:startedAt}, true); statusText.textContent = 'Running'; tickId = setInterval(function(){ var st = BLS.loadState(); if(!st.running){ clearInterval(tickId); tickId=null; return } var elapsed = Math.floor((Date.now()-st.startedAt)/1000); var rem = Math.max(0, st.durationSec - elapsed); var em = Math.floor(elapsed/60), es = elapsed%60; elapsedText.textContent = String(em).padStart(2,'0')+':'+String(es).padStart(2,'0'); var rm = Math.floor(rem/60); timerDisplay.textContent = String(rm).padStart(2,'0')+':'+String(rem%60).padStart(2,'0'); if(rem<=0){ stopTimer() } }, 250); }
|
function startTimer(){ if(tickId) clearInterval(tickId); var durationSec = Math.max(0, (parseInt(timerMinutes.value||'0',10)||0)*60); var startedAt = Date.now(); applyState({running:true, durationSec:durationSec, startedAt:startedAt}, true); statusText.textContent = 'Running'; tickId = setInterval(function(){ var st = BLS.loadState(); if(!st.running){ clearInterval(tickId); tickId=null; return } var elapsed = Math.floor((Date.now()-st.startedAt)/1000); var rem = Math.max(0, st.durationSec - elapsed); var em = Math.floor(elapsed/60), es = elapsed%60; elapsedText.textContent = String(em).padStart(2,'0')+':'+String(es).padStart(2,'0'); var rm = Math.floor(rem/60); timerDisplay.textContent = String(rm).padStart(2,'0')+':'+String(rem%60).padStart(2,'0'); if(rem<=0){ stopTimer() } }, 250); }
|
||||||
function stopTimer(){ if(tickId){ clearInterval(tickId); tickId=null } recordSession(); applyState({running:false}, true); statusText.textContent = 'Idle'; refreshPatientUI() }
|
function stopTimer(){ if(tickId){ clearInterval(tickId); tickId=null } recordSession(); applyState({running:false}, true); statusText.textContent = 'Idle'; refreshPatientUI() }
|
||||||
startBtn.addEventListener('click', startTimer);
|
startBtn.addEventListener('click', function(){ startTimer(); publishState() });
|
||||||
endBtn.addEventListener('click', stopTimer);
|
endBtn.addEventListener('click', function(){ stopTimer(); publishState() });
|
||||||
|
wsConnectBtn.addEventListener('click', function(){ wsConnect(); updateJoinLink() });
|
||||||
|
wsDisconnectBtn.addEventListener('click', wsDisconnect);
|
||||||
|
wsUrl.addEventListener('input', updateJoinLink);
|
||||||
|
wsSession.addEventListener('input', updateJoinLink);
|
||||||
|
copyJoinLink.addEventListener('click', function(){ if(joinLink.value){ navigator.clipboard && navigator.clipboard.writeText(joinLink.value) } });
|
||||||
openPatient.addEventListener('click', function(){ window.open('patient.html','bls-patient'); });
|
openPatient.addEventListener('click', function(){ window.open('patient.html','bls-patient'); });
|
||||||
renderer.resize(360,220);
|
renderer.resize(360,220);
|
||||||
updateSpeedLabel(); updateSizeLabel(); updateYPercentLabel(); updateTimerLabel(); updateGlowIntensityLabel(); updateGlowRateLabel(); updateRampLabel(); updateEdgePauseLabel(); updateWiggleLabel(); updatePanRateLabel(); updateCueRateLabel(); updateVolumeLabel(); statusText.textContent = state.running? 'Running':'Idle'; setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode');
|
updateSpeedLabel(); updateSizeLabel(); updateYPercentLabel(); updateTimerLabel(); updateGlowIntensityLabel(); updateGlowRateLabel(); updateRampLabel(); updateEdgePauseLabel(); updateWiggleLabel(); updatePanRateLabel(); updateCueRateLabel(); updateVolumeLabel(); statusText.textContent = state.running? 'Running':'Idle'; setActive(shapeControls,'shape','shape'); setActive(directionControls,'direction','direction'); setActive(easingControls,'easing','easingMode');
|
||||||
@ -179,8 +185,19 @@ endBtn.addEventListener('click', stopTimer);
|
|||||||
updateKnobSensLabel();
|
updateKnobSensLabel();
|
||||||
yPercent.value = parseInt(state.yPercent||50,10);
|
yPercent.value = parseInt(state.yPercent||50,10);
|
||||||
updateYPercentLabel();
|
updateYPercentLabel();
|
||||||
refreshPresetList(); refreshPatientList();
|
refreshPresetList(); refreshPatientList(); updateJoinLink();
|
||||||
refreshPatientUI();
|
refreshPatientUI();
|
||||||
}
|
}
|
||||||
function waitForBLS(){ if(window.BLS){ initAdmin() } else { setTimeout(waitForBLS, 50) } }
|
function waitForBLS(){ if(window.BLS){ initAdmin() } else { setTimeout(waitForBLS, 50) } }
|
||||||
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', waitForBLS) } else { waitForBLS() }
|
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', waitForBLS) } else { waitForBLS() }
|
||||||
|
var wsUrl = document.getElementById('wsUrl');
|
||||||
|
var wsSession = document.getElementById('wsSession');
|
||||||
|
var wsConnectBtn = document.getElementById('wsConnect');
|
||||||
|
var wsDisconnectBtn = document.getElementById('wsDisconnect');
|
||||||
|
var wsStatus = document.getElementById('wsStatus');
|
||||||
|
var joinLink = document.getElementById('joinLink');
|
||||||
|
var copyJoinLink = document.getElementById('copyJoinLink');
|
||||||
|
var ws = null; var wsConnected = false; var wsSid = null;
|
||||||
|
function updateJoinLink(){ var url = (wsUrl.value||'').trim(); var sid = (wsSession.value||'').trim(); var base = location.origin + '/patient.html'; if(url && sid){ joinLink.value = base + '?url=' + encodeURIComponent(url) + '&sid=' + encodeURIComponent(sid) } else { joinLink.value = '' } }
|
||||||
|
function wsConnect(){ var url = (wsUrl.value||'').trim(); var sid = (wsSession.value||'').trim()||'default'; if(ws){ try{ ws.close() }catch(e){} ws=null } if(!url){ wsStatus.textContent = 'Missing URL'; return } ws = new WebSocket(url); wsSid = sid; wsStatus.textContent = 'Connecting...'; ws.onopen = function(){ wsConnected = true; wsStatus.textContent = 'Connected'; try{ ws.send(JSON.stringify({type:'join', sessionId:sid, role:'admin'})) }catch(e){} }; ws.onclose = function(){ wsConnected = false; wsStatus.textContent = 'Disconnected' }; ws.onerror = function(){ wsStatus.textContent = 'Error' }; ws.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='joined'){ wsStatus.textContent = 'Connected' } }catch(e){} } }
|
||||||
|
function wsDisconnect(){ if(ws){ try{ ws.close() }catch(e){} } ws=null; wsConnected=false; wsStatus.textContent = 'Disconnected' }
|
||||||
|
|||||||
@ -24,7 +24,14 @@ BLS<!doctype html>
|
|||||||
size(); window.addEventListener('resize', size)
|
size(); window.addEventListener('resize', size)
|
||||||
var renderer = BLS.Renderer(canvas)
|
var renderer = BLS.Renderer(canvas)
|
||||||
var chan = BLS.createChannel('bls-session')
|
var chan = BLS.createChannel('bls-session')
|
||||||
|
var qs = new URLSearchParams(location.search)
|
||||||
|
var wsUrl = qs.get('url')
|
||||||
|
var wsSid = qs.get('sid') || 'default'
|
||||||
|
var ws = null
|
||||||
chan.on(function(msg){ if(!msg || typeof msg !== 'object') return; var apply = Object.assign({}, msg); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(st.running){ renderer.start() } else { renderer.stop() } })
|
chan.on(function(msg){ if(!msg || typeof msg !== 'object') return; var apply = Object.assign({}, msg); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(st.running){ renderer.start() } else { renderer.stop() } })
|
||||||
|
function wsConnect(){ if(!wsUrl) return; try{ ws = new WebSocket(wsUrl) }catch(e){ return } ws.onopen = function(){ try{ ws.send(JSON.stringify({type:'join', sessionId: wsSid, role:'patient'})) }catch(e){} }; ws.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='state'){ var apply = Object.assign({}, msg.payload||{}); if(apply && apply.rampEnabled === false){ apply.easingMode = 'linear' } if(apply && apply.glowEnabled === false){ apply.glowIntensity = 0 } renderer.setState(apply); var st = renderer.getState(); if(st.running){ renderer.start() } else { renderer.stop() } } }catch(e){} }; ws.onclose = function(){ ws = null }
|
||||||
|
}
|
||||||
|
if(wsUrl){ wsConnect() }
|
||||||
renderer.setState(BLS.loadState())
|
renderer.setState(BLS.loadState())
|
||||||
var st = renderer.getState(); if(st.running){ renderer.start() }
|
var st = renderer.getState(); if(st.running){ renderer.start() }
|
||||||
var audioCtx = null, panner = null, gain = null, source = null, audioFolder = null, audioBuffer = null
|
var audioCtx = null, panner = null, gain = null, source = null, audioFolder = null, audioBuffer = null
|
||||||
|
|||||||
41
server.py
Normal file
41
server.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
SESSIONS = {}
|
||||||
|
|
||||||
|
async def handler(ws):
|
||||||
|
session_id = None
|
||||||
|
try:
|
||||||
|
async for msg in ws:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
t = data.get('type')
|
||||||
|
if t == 'join':
|
||||||
|
session_id = data.get('sessionId') or 'default'
|
||||||
|
SESSIONS.setdefault(session_id, set()).add(ws)
|
||||||
|
await ws.send(json.dumps({'type':'joined','sessionId':session_id}))
|
||||||
|
elif t == 'state':
|
||||||
|
sid = data.get('sessionId') or session_id or 'default'
|
||||||
|
payload = data.get('payload')
|
||||||
|
for peer in list(SESSIONS.get(sid, set())):
|
||||||
|
if peer is not ws:
|
||||||
|
try:
|
||||||
|
await peer.send(json.dumps({'type':'state','payload':payload}))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif t == 'ping':
|
||||||
|
await ws.send(json.dumps({'type':'pong'}))
|
||||||
|
finally:
|
||||||
|
if session_id and ws in SESSIONS.get(session_id, set()):
|
||||||
|
SESSIONS[session_id].discard(ws)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print('Starting BLS relay on ws://0.0.0.0:8787')
|
||||||
|
async with websockets.serve(handler, '0.0.0.0', 8787, ping_interval=20, ping_timeout=20):
|
||||||
|
await asyncio.Future()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
Loading…
Reference in New Issue
Block a user