easier instructions and connection for multi-computer over lan
This commit is contained in:
parent
d3f7f374ab
commit
cbd4b25bc6
112
README.md
112
README.md
@ -1,6 +1,63 @@
|
|||||||
# Bilateral Stimulation Simulator
|
# Bilateral Stimulation Simulator
|
||||||
|
|
||||||
Simple browser-based tool with an Admin panel and a full-screen Patient view. The Admin controls everything; the Patient view mirrors motion and audio.
|
Simple browser-based tool with an therapist panel and a full-screen Patient view. The therapist controls everything; the Patient view mirrors motion and audio.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
- Two pages:
|
||||||
|
- Therapist (Admin) page: where you set motion, colors, and sounds
|
||||||
|
- Patient page: fullscreen view that mirrors the Admin
|
||||||
|
- Two ways to show the Patient page:
|
||||||
|
- Same computer (extended monitor): plug a second display facing the patient, open the Patient page on that display, go fullscreen — no network setup needed
|
||||||
|
- Two computers on the same Wi‑Fi/LAN: start a tiny local server on the therapist’s computer so both can communicate, then the patient joins from their computer
|
||||||
|
- Audio note: most browsers need you to click on the page once to activate sound
|
||||||
|
- Go gently — adjust speed, colors, and sounds to what feels comfortable; pause whenever needed
|
||||||
|
|
||||||
|
## Super Simple Guide
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
- First‑Time Setup
|
||||||
|
- Double‑click `setup.bat`
|
||||||
|
- Allow the Windows Defender Firewall prompt (Private networks)
|
||||||
|
- Your browser opens at `http://<your‑IP>:8000/`; “Relay” turns green
|
||||||
|
- Every Session
|
||||||
|
- Double‑click `start-relay.bat`
|
||||||
|
- Click “Detect Local IP” in the app; the Patient link auto‑copies
|
||||||
|
- Stop Server
|
||||||
|
- Use “Stop Server” in the app, or press `Ctrl + C` in the relay window
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- First‑Time Setup
|
||||||
|
- Press `Cmd + Space` → type `Terminal` → press `Enter`
|
||||||
|
- `cd` into the folder where you unzipped the app
|
||||||
|
- Copy and paste: `chmod +x ./setup.sh && ./setup.sh`
|
||||||
|
- Allow the macOS firewall prompt (Private networks)
|
||||||
|
- Your browser opens at `http://<your‑IP>:8000/`; “Relay” turns green
|
||||||
|
- Every Session
|
||||||
|
- Press `Cmd + Space` → type `Terminal` → `Enter`
|
||||||
|
- `cd` into the app folder
|
||||||
|
- Copy and paste: `chmod +x ./start-relay.sh && ./start-relay.sh`
|
||||||
|
- Click “Detect Local IP”; the Patient link auto‑copies
|
||||||
|
- Stop Server
|
||||||
|
- Use “Stop Server” in the app, or press `Ctrl + C` in the relay Terminal
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
- First‑Time Setup
|
||||||
|
- Press `Ctrl + Alt + T` to open Terminal
|
||||||
|
- `cd` into the app folder
|
||||||
|
- Copy and paste: `chmod +x ./setup.sh && ./setup.sh`
|
||||||
|
- Your browser opens at `http://<your‑IP>:8000/`; “Relay” turns green
|
||||||
|
- Every Session
|
||||||
|
- Open Terminal → `cd` into the app folder
|
||||||
|
- Copy and paste: `chmod +x ./start-relay.sh && ./start-relay.sh`
|
||||||
|
- Click “Detect Local IP”; the Patient link auto‑copies
|
||||||
|
- Stop Server
|
||||||
|
- Use “Stop Server” in the app, or press `Ctrl + C` in the relay Terminal
|
||||||
|
|
||||||
|
### What You Should See
|
||||||
|
- “Relay:” shows a green dot and “Running”
|
||||||
|
- “Detect Local IP” fills `ws://<your‑IP>:8787` and copies the Patient link
|
||||||
|
- Admin page loads at `http://<your‑IP>:8000/`
|
||||||
|
|
||||||
## Admin Options
|
## Admin Options
|
||||||
|
|
||||||
@ -94,23 +151,48 @@ Simple browser-based tool with an Admin panel and a full-screen Patient view. Th
|
|||||||
- Admin connects to the relay with a Session ID and copies the Patient Join Link.
|
- 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.
|
- Patient opens the link on their computer and performs a quick gesture to unlock audio.
|
||||||
|
|
||||||
|
### Quick Start (Double‑Click or Copy‑Paste)
|
||||||
|
- macOS (double‑click): open the folder and double‑click `start-relay.command`.
|
||||||
|
- macOS (copy‑paste): `chmod +x ./start-relay.sh && ./start-relay.sh`
|
||||||
|
- Windows (double‑click): open the folder and double‑click `start-relay.bat`.
|
||||||
|
- Windows (copy‑paste): `start-relay.bat`
|
||||||
|
- These start scripts auto‑create a local Python virtual environment, install `websockets`, print your LAN relay URL, and run the relay on port `8787`.
|
||||||
|
|
||||||
|
### First‑Time Setup (Recommended)
|
||||||
|
- macOS/Linux:
|
||||||
|
- `chmod +x ./setup.sh && ./setup.sh`
|
||||||
|
- Windows:
|
||||||
|
- Double‑click `setup.bat`
|
||||||
|
- What it does:
|
||||||
|
- Creates a local virtual environment and installs `websockets`
|
||||||
|
- Starts the static site server on `http://<IP>:8000/`
|
||||||
|
- Starts the WebSocket relay on `ws://<IP>:8787`
|
||||||
|
- Opens the Admin page in your browser at `http://<IP>:8000/`
|
||||||
|
|
||||||
|
### Every Session (Start/Stop)
|
||||||
|
- Start Server:
|
||||||
|
- macOS/Linux: `chmod +x ./start-relay.sh && ./start-relay.sh`
|
||||||
|
- Windows: `start-relay.bat`
|
||||||
|
- Stop Server:
|
||||||
|
- Use the Admin page “Stop Server” button, or press `Ctrl + C` in the relay terminal
|
||||||
|
|
||||||
### One‑Time Install (WebSocket Relay)
|
### One‑Time Install (WebSocket Relay)
|
||||||
- Windows:
|
- Windows:
|
||||||
- Press `Windows` key → type `PowerShell` → `Enter`
|
- Press `Windows` key → type `PowerShell` → `Enter`
|
||||||
- Check Python: `python --version` (or `py --version`)
|
- Check Python: `python --version` (or `py --version`)
|
||||||
- Install package: `python -m pip install websockets` (or `py -m pip install websockets`)
|
- Quick option: double‑click `start-relay.bat` (or run it in PowerShell/CMD)
|
||||||
- Start relay: `python server.py` (or `py server.py`)
|
- Manual option: `python -m pip install websockets` (or `py -m pip install websockets`), then `python server.py` (or `py server.py`)
|
||||||
- If prompted by Windows Defender Firewall, click “Allow access”.
|
- If prompted by Windows Defender Firewall, click “Allow access”.
|
||||||
- macOS:
|
- macOS:
|
||||||
- Press `Cmd+Space` → type `Terminal` → `Enter`
|
- Press `Cmd+Space` → type `Terminal` → `Enter`
|
||||||
- Check Python: `python3 --version`
|
- Check Python: `python3 --version`
|
||||||
- Install package: `python3 -m pip install websockets`
|
- Quick option: double‑click `start-relay.command` (or run `chmod +x ./start-relay.sh && ./start-relay.sh`)
|
||||||
- Start relay: `python3 server.py`
|
- Manual option: `python3 -m pip install websockets`, then `python3 server.py`
|
||||||
- Linux (Ubuntu/Debian/Fedora etc.):
|
- Linux (Ubuntu/Debian/Fedora etc.):
|
||||||
- Press `Ctrl+Alt+T` to open Terminal
|
- Press `Ctrl+Alt+T` to open Terminal
|
||||||
- Check Python: `python3 --version`
|
- Check Python: `python3 --version`
|
||||||
- Install package: `python3 -m pip install websockets`
|
- Quick option: `chmod +x ./start-relay.sh && ./start-relay.sh`
|
||||||
- Start relay: `python3 server.py`
|
- Manual option: `python3 -m pip install websockets`, then `python3 server.py`
|
||||||
|
|
||||||
### Find Your Therapist Computer’s IP Address
|
### Find Your Therapist Computer’s IP Address
|
||||||
- Windows: run `ipconfig` → use the `IPv4 Address` on your active adapter (e.g., `192.168.1.5`).
|
- Windows: run `ipconfig` → use the `IPv4 Address` on your active adapter (e.g., `192.168.1.5`).
|
||||||
@ -135,6 +217,20 @@ Simple browser-based tool with an Admin panel and a full-screen Patient view. Th
|
|||||||
- On Windows, allow the relay through the firewall when prompted.
|
- On Windows, allow the relay through the firewall when prompted.
|
||||||
- You can disconnect Network Mode and continue Local Mode at any time.
|
- You can disconnect Network Mode and continue Local Mode at any time.
|
||||||
|
|
||||||
|
## Display Options
|
||||||
|
|
||||||
|
- Same Computer (Extended Display)
|
||||||
|
- Connect a second monitor facing the patient
|
||||||
|
- Open `patient.html` in a second browser window
|
||||||
|
- Drag the Patient window to the patient‑facing monitor and use browser fullscreen
|
||||||
|
- Audio unlock: have the patient do one quick gesture (mouse/key) on the Patient screen
|
||||||
|
- Pros: zero network setup, minimal friction
|
||||||
|
|
||||||
|
- Separate Computer (LAN WebSocket)
|
||||||
|
- Use the Network Mode steps above with the Python relay
|
||||||
|
- Share the short Patient link (e.g., `http://<therapist-ip>:8000/patient.html#room-1`)
|
||||||
|
- Audio unlock: one quick gesture on the Patient device; Programmatic audio is recommended
|
||||||
|
|
||||||
## Data Storage
|
## Data Storage
|
||||||
|
|
||||||
- Presets: `localStorage` key `bls_presets`
|
- Presets: `localStorage` key `bls_presets`
|
||||||
@ -147,4 +243,4 @@ Simple browser-based tool with an Admin panel and a full-screen Patient view. Th
|
|||||||
|
|
||||||
## ⚠️ Disclaimer
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
This tool is for research, education, and personal experimentation with bilateral stimulation. It is NOT a substitute for professional therapy, diagnosis, or treatment. Use at your own discretion.
|
This tool is for research, education, and personal experimentation with bilateral stimulation, or maybe the fancy lightbar broke down and you need a backup option, or maybe you sprained both your index fingers. It is NOT a substitute for professional therapy, diagnosis, or treatment. Use at your own discretion. You can freely copy or share this with blessings.
|
||||||
|
|||||||
56
index.html
56
index.html
@ -1,10 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Bilataral Stimulation Simulator Admin</title>
|
<title>Bilataral Stimulation Simulator Admin</title>
|
||||||
<style>
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%231f6feb'/%3E%3C/svg%3E">
|
||||||
|
<style>
|
||||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #f7f7f7; color: #111; }
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #f7f7f7; color: #111; }
|
||||||
header { padding: 16px 20px; background: #222; color: #fff; display: flex; align-items: center; justify-content: space-between; }
|
header { padding: 16px 20px; background: #222; color: #fff; display: flex; align-items: center; justify-content: space-between; }
|
||||||
header h1 { font-size: 18px; margin: 0; }
|
header h1 { font-size: 18px; margin: 0; }
|
||||||
@ -30,6 +31,8 @@
|
|||||||
#patientControls .danger { background: transparent; color: #d1242f; border: 1px solid #d1242f; padding: 6px 10px; border-radius: 6px; font-weight: 500; }
|
#patientControls .danger { background: transparent; color: #d1242f; border: 1px solid #d1242f; padding: 6px 10px; border-radius: 6px; font-weight: 500; }
|
||||||
.metrics { display: flex; gap: 12px; font-size: 13px; color: #444; }
|
.metrics { display: flex; gap: 12px; font-size: 13px; color: #444; }
|
||||||
.small { font-size: 12px; color: #666; display: inline-block; min-width: 60px; }
|
.small { font-size: 12px; color: #666; display: inline-block; min-width: 60px; }
|
||||||
|
.statusDot { display:inline-block; width:10px; height:10px; border-radius:50%; background:#d1242f; margin-right:6px; vertical-align:middle; }
|
||||||
|
.statusDot.on { background:#2da44e; }
|
||||||
.field { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
|
.field { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
|
||||||
.field label { min-width: 90px; }
|
.field label { min-width: 90px; }
|
||||||
#speed { width: 360px; }
|
#speed { width: 360px; }
|
||||||
@ -224,23 +227,7 @@
|
|||||||
<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">
|
||||||
@ -272,6 +259,31 @@
|
|||||||
<div id="sessionHistory" class="small" style="max-height:160px; overflow:auto; flex:1"></div>
|
<div id="sessionHistory" class="small" style="max-height:160px; overflow:auto; flex:1"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="small">This preview mirrors the patient's view.</div>
|
<div class="small">This preview mirrors the patient's view.</div>
|
||||||
|
<div class="panel" style="margin-top:8px">
|
||||||
|
<h2>Network</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>Relay:</label>
|
||||||
|
<span id="relayStatusDot" class="statusDot"></span>
|
||||||
|
<span id="relayStatusText" class="small">Not running</span>
|
||||||
|
<button id="startRelayBtn">Copy Start Command</button>
|
||||||
|
<button id="stopRelayBtn">Stop Server</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Step 3:</label>
|
||||||
|
<input type="text" id="wsUrl" placeholder="ws://192.168.x.x:8787" style="width:220px">
|
||||||
|
<input type="text" id="wsSession" placeholder="room-1" style="width:120px">
|
||||||
|
<button id="detectIP">Detect Local IP</button>
|
||||||
|
<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>Step 4:</label>
|
||||||
|
<input type="text" id="joinLink" readonly style="flex:1; min-width:260px">
|
||||||
|
<button id="copyJoinLink">Copy</button>
|
||||||
|
</div>
|
||||||
|
<div id="osHelp" class="small" style="margin-top:6px"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
94
js/admin.js
94
js/admin.js
@ -104,7 +104,13 @@ function updateTimerLabel(){ var m = Math.max(0, parseInt(timerMinutes.value||'0
|
|||||||
function refreshPatientUI(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); sessionCountEl.textContent = String(p.count||0); if(p.last){ var d = new Date(p.last.end); var dur = Math.round((p.last.durationSec||0)); lastSessionInfoEl.textContent = d.toLocaleString()+' • '+dur+'s' } else { lastSessionInfoEl.textContent = '—' } var host = document.getElementById('sessionHistory'); if(host){ if(!p.sessions || !p.sessions.length){ host.innerHTML = '<em>No sessions yet</em>' } else { var rows = p.sessions.slice().sort(function(a,b){ return a.start - b.start }).map(function(s, idx){ var start = new Date(s.start).toLocaleString(); var end = new Date(s.end).toLocaleString(); var dur = Math.round(s.durationSec||0); return '<div>'+String(idx+1)+'. '+start+' → '+end+' • '+dur+'s</div>' }); host.innerHTML = rows.join('') } } }
|
function refreshPatientUI(){ var n = (patientName.value||'').trim()||'Unnamed'; var p = getPatient(n); sessionCountEl.textContent = String(p.count||0); if(p.last){ var d = new Date(p.last.end); var dur = Math.round((p.last.durationSec||0)); lastSessionInfoEl.textContent = d.toLocaleString()+' • '+dur+'s' } else { lastSessionInfoEl.textContent = '—' } var host = document.getElementById('sessionHistory'); if(host){ if(!p.sessions || !p.sessions.length){ host.innerHTML = '<em>No sessions yet</em>' } else { var rows = p.sessions.slice().sort(function(a,b){ return a.start - b.start }).map(function(s, idx){ var start = new Date(s.start).toLocaleString(); var end = new Date(s.end).toLocaleString(); var dur = Math.round(s.durationSec||0); return '<div>'+String(idx+1)+'. '+start+' → '+end+' • '+dur+'s</div>' }); host.innerHTML = rows.join('') } } }
|
||||||
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) }
|
||||||
|
publishState()
|
||||||
|
}
|
||||||
function publishState(){ try{ if(wsConnected && ws){ ws.send(JSON.stringify({type:'state', sessionId: wsSid||'default', payload: BLS.loadState()})) } }catch(e){} }
|
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'); });
|
||||||
@ -162,7 +168,63 @@ function stopTimer(){ if(tickId){ clearInterval(tickId); tickId=null } recordSes
|
|||||||
wsDisconnectBtn.addEventListener('click', wsDisconnect);
|
wsDisconnectBtn.addEventListener('click', wsDisconnect);
|
||||||
wsUrl.addEventListener('input', updateJoinLink);
|
wsUrl.addEventListener('input', updateJoinLink);
|
||||||
wsSession.addEventListener('input', updateJoinLink);
|
wsSession.addEventListener('input', updateJoinLink);
|
||||||
copyJoinLink.addEventListener('click', function(){ if(joinLink.value){ navigator.clipboard && navigator.clipboard.writeText(joinLink.value) } });
|
function copyText(t, fallbackElem){
|
||||||
|
var ok=false;
|
||||||
|
if(navigator.clipboard && navigator.clipboard.writeText){
|
||||||
|
try{ navigator.clipboard.writeText(t); ok=true }catch(e){}
|
||||||
|
}
|
||||||
|
if(!ok && fallbackElem){
|
||||||
|
try{ fallbackElem.focus(); fallbackElem.select(); ok=document.execCommand('copy') }catch(e){}
|
||||||
|
}
|
||||||
|
if(!ok){
|
||||||
|
var ta=document.createElement('textarea');
|
||||||
|
ta.value=t; ta.setAttribute('readonly','');
|
||||||
|
ta.style.position='fixed'; ta.style.bottom='0'; ta.style.left='0'; ta.style.opacity='0.01';
|
||||||
|
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||||
|
try{ ok=document.execCommand('copy') }catch(e){}
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
}
|
||||||
|
if(!ok){ try{ alert(t) }catch(_){ } }
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
copyJoinLink.addEventListener('click', function(){ var v = joinLink.value||''; if(!v) return; var b=this; var ok=copyText(v, joinLink); var old=b.textContent; b.textContent = ok ? 'Copied!' : 'Shown'; setTimeout(function(){ b.textContent=old }, 1000) });
|
||||||
|
var detectIPBtn = document.getElementById('detectIP');
|
||||||
|
if(detectIPBtn){ detectIPBtn.addEventListener('click', function(){
|
||||||
|
function setUrl(ip){ if(!ip) return; wsUrl.value = 'ws://' + ip + ':8787'; if(!(wsSession.value||'').trim()){ wsSession.value = 'room-1' } updateJoinLink(); var v = joinLink.value||''; if(v){ var ok = copyText(v, joinLink); var b = document.getElementById('copyJoinLink'); if(b){ var old=b.textContent; b.textContent = ok? 'Copied!' : 'Copy'; setTimeout(function(){ b.textContent=old }, 1000) } } }
|
||||||
|
var host = location.hostname; if(host && host !== 'localhost' && host !== '127.0.0.1'){ setUrl(host); return }
|
||||||
|
// Attempt server-assisted detection
|
||||||
|
(function(){
|
||||||
|
var probeUrl = 'ws://' + (host || 'localhost') + ':8787'
|
||||||
|
try{
|
||||||
|
var temp = new WebSocket(probeUrl)
|
||||||
|
temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'detect_ip'})) }catch(e){} }
|
||||||
|
temp.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='detect_ip' && msg.ip){ setUrl(msg.ip); wsStatus.textContent = 'IP detected'; try{ temp.close() }catch(e){} return } }catch(e){} try{ temp.close() }catch(e){}
|
||||||
|
// Fallback to WebRTC if server didn't respond with IP
|
||||||
|
tryWebRTC()
|
||||||
|
}
|
||||||
|
temp.onerror = function(){ tryWebRTC() }
|
||||||
|
}catch(_){ tryWebRTC() }
|
||||||
|
})()
|
||||||
|
function tryWebRTC(){
|
||||||
|
var RTC = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection
|
||||||
|
if(!RTC){ wsStatus.textContent = 'Detect unavailable'; return }
|
||||||
|
try{
|
||||||
|
var pc = new RTC({iceServers: []}); pc.createDataChannel('x');
|
||||||
|
pc.onicecandidate = function(e){
|
||||||
|
if(!e || !e.candidate){ pc.close(); return }
|
||||||
|
var cand = e.candidate;
|
||||||
|
var ip = (cand && cand.address) || null;
|
||||||
|
if(!ip){
|
||||||
|
var s = cand.candidate || '';
|
||||||
|
var m = s.match(/(?:\s|\b)(\d{1,3}(?:\.\d{1,3}){3})(?:\b)/);
|
||||||
|
if(m) ip = m[1]
|
||||||
|
}
|
||||||
|
if(ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip) && !/^169\./.test(ip)){ setUrl(ip); wsStatus.textContent = 'IP detected'; pc.close() }
|
||||||
|
};
|
||||||
|
pc.createOffer().then(function(o){ return pc.setLocalDescription(o) }).catch(function(){ wsStatus.textContent = 'Detect failed' })
|
||||||
|
}catch(_){ wsStatus.textContent = 'Detect failed' }
|
||||||
|
}
|
||||||
|
}) }
|
||||||
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');
|
||||||
@ -197,7 +259,31 @@ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded
|
|||||||
var wsStatus = document.getElementById('wsStatus');
|
var wsStatus = document.getElementById('wsStatus');
|
||||||
var joinLink = document.getElementById('joinLink');
|
var joinLink = document.getElementById('joinLink');
|
||||||
var copyJoinLink = document.getElementById('copyJoinLink');
|
var copyJoinLink = document.getElementById('copyJoinLink');
|
||||||
|
var startRelayBtn = document.getElementById('startRelayBtn');
|
||||||
|
var stopRelayBtn = document.getElementById('stopRelayBtn');
|
||||||
|
var relayStatusDot = document.getElementById('relayStatusDot');
|
||||||
|
var relayStatusText = document.getElementById('relayStatusText');
|
||||||
var ws = null; var wsConnected = false; var wsSid = null;
|
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 publishState(){ try{ if(wsConnected && ws){ ws.send(JSON.stringify({type:'state', sessionId: wsSid||'default', payload: BLS.loadState()})) } }catch(e){} }
|
||||||
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 updateJoinLink(){ var sid = (wsSession.value||'').trim(); var base = location.origin + '/patient.html'; if(sid){ joinLink.value = base + '#' + encodeURIComponent(sid) } else { joinLink.value = '' } }
|
||||||
|
function renderOsHelp(){ var ua = navigator.userAgent||''; var isWin = /Windows/i.test(ua); var isMac = /Macintosh|Mac OS/i.test(ua); var isLinux = /Linux/i.test(ua) && !isWin && !isMac; var setup = (isWin? 'setup.bat' : 'chmod +x ./setup.sh && ./setup.sh'); var start = (isWin? 'start-relay.bat' : 'chmod +x ./start-relay.sh && ./start-relay.sh'); var open = (isWin? 'Open PowerShell' : isMac? 'Cmd+Space → Terminal' : 'Open Terminal'); return { setup: setup, start: start, open: open } }
|
||||||
|
(function(){ var el = document.getElementById('osHelp'); if(!el) return; var h = renderOsHelp(); var text = 'First time: ' + h.open + ', cd to the folder, run: ' + h.setup + ' • Every session: ' + h.start; el.innerText = text })();
|
||||||
|
function copyText(t){ var ok=false; if(navigator.clipboard && navigator.clipboard.writeText){ try{ navigator.clipboard.writeText(t); ok=true }catch(e){} } if(!ok){ var ta=document.createElement('textarea'); ta.value=t; ta.setAttribute('readonly',''); ta.style.position='fixed'; ta.style.bottom='0'; ta.style.left='0'; ta.style.opacity='0.01'; document.body.appendChild(ta); ta.focus(); ta.select(); try{ ok=document.execCommand('copy') }catch(e){} document.body.removeChild(ta) } if(!ok){ try{ alert(t) }catch(_){ } } return ok }
|
||||||
|
function setRelayStatus(on){ if(relayStatusDot){ relayStatusDot.classList.toggle('on', !!on) } if(relayStatusText){ relayStatusText.textContent = on? 'Running' : 'Not running' } }
|
||||||
|
(function(){ var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = true } })();
|
||||||
|
(function(){
|
||||||
|
var pollMs = 3000;
|
||||||
|
function schedule(){ setTimeout(run, pollMs) }
|
||||||
|
function run(){ checkRelayOnce().then(function(ok){ setRelayStatus(!!ok); var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = !ok } pollMs = ok ? 3000 : Math.min(Math.round(pollMs*1.5), 30000); schedule() }).catch(function(){ setRelayStatus(false); var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = true } pollMs = Math.min(Math.round(pollMs*1.5), 30000); schedule() }) }
|
||||||
|
async function checkRelayOnce(){ var host = location.hostname || 'localhost'; var url = 'ws://' + host + ':8787'; try{ return await new Promise(function(resolve){ var temp = new WebSocket(url); var done=false; var timer = setTimeout(function(){ if(!done){ resolve(false); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } } }, 1500);
|
||||||
|
temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'ping'})) }catch(e){} };
|
||||||
|
temp.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='pong'){ done=true; clearTimeout(timer); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } resolve(true); return } }catch(e){} resolve(false) };
|
||||||
|
temp.onerror = function(){ resolve(false) };
|
||||||
|
temp.onclose = function(){ if(!done){ resolve(false) } };
|
||||||
|
}) }catch(_){ return false } }
|
||||||
|
run()
|
||||||
|
})();
|
||||||
|
if(startRelayBtn){ startRelayBtn.addEventListener('click', function(){ var h = renderOsHelp(); var ok = copyText(h.start); var old = startRelayBtn.textContent; startRelayBtn.textContent = ok? 'Copied!' : 'Copy Start Command'; setTimeout(function(){ startRelayBtn.textContent = old }, 1200) }) }
|
||||||
|
if(stopRelayBtn){ stopRelayBtn.addEventListener('click', function(){ var host = location.hostname || 'localhost'; var url = 'ws://' + host + ':8787'; try{ var temp = new WebSocket(url); temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'shutdown'})) }catch(e){} }; setTimeout(function(){ setRelayStatus(false) }, 1500) }catch(e){} }) }
|
||||||
|
function wsConnect(){ var url = (wsUrl.value||'').trim(); var sid = (wsSession.value||'').trim()||'default'; var btn = document.getElementById('wsConnect'); if(btn && btn.disabled){ wsStatus.textContent = 'Relay not running'; return } if(ws){ try{ ws.close() }catch(e){} ws=null } if(!url){ wsStatus.textContent = 'Missing URL'; return } try{ ws = new WebSocket(url) }catch(e){ wsStatus.textContent = 'Error'; return } 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){} publishState() }; 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' }
|
function wsDisconnect(){ if(ws){ try{ ws.close() }catch(e){} } ws=null; wsConnected=false; wsStatus.textContent = 'Disconnected' }
|
||||||
|
|||||||
31
patient.html
31
patient.html
@ -25,11 +25,34 @@ BLS<!doctype html>
|
|||||||
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 qs = new URLSearchParams(location.search)
|
||||||
var wsUrl = qs.get('url')
|
var hashSid = (location.hash||'').replace('#','')
|
||||||
var wsSid = qs.get('sid') || 'default'
|
var wsSid = qs.get('sid') || hashSid || 'default'
|
||||||
|
var wsUrl = qs.get('url') || ('ws://' + (location.hostname||'localhost') + ':8787')
|
||||||
var ws = null
|
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 }
|
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){ stopAudio(); stopProgrammatic(); renderer.stop(); return }
|
||||||
|
if(st.audioEnabled){
|
||||||
|
if(st.audioMode==='file'){
|
||||||
|
(async function(){ if(!audioFolder){ await ensureFolder() } if(st.audioName){ await loadAudioByName(st.audioName); playBuffer() } })()
|
||||||
|
} else {
|
||||||
|
stopAudio(); if(!audioUnlocked) bindUnlock(); startProgrammatic(st)
|
||||||
|
}
|
||||||
|
} else { stopAudio(); stopProgrammatic() }
|
||||||
|
renderer.start()
|
||||||
|
}
|
||||||
|
}catch(e){}
|
||||||
|
};
|
||||||
|
ws.onclose = function(){ ws = null }
|
||||||
}
|
}
|
||||||
if(wsUrl){ wsConnect() }
|
if(wsUrl){ wsConnect() }
|
||||||
renderer.setState(BLS.loadState())
|
renderer.setState(BLS.loadState())
|
||||||
@ -37,7 +60,7 @@ BLS<!doctype html>
|
|||||||
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
|
||||||
var audioUnlocked = false
|
var audioUnlocked = false
|
||||||
var pendingProgramState = null
|
var pendingProgramState = null
|
||||||
function bindUnlock(){ ['pointerdown','keydown','touchstart','mousemove'].forEach(function(ev){ window.addEventListener(ev, function once(){ try{ AudioEngine.ensureCtx(); audioUnlocked = true }catch(e){} if(pendingProgramState){ var st = pendingProgramState; pendingProgramState = null; startProgrammatic(st) } try{ window.removeEventListener(ev, once) }catch(_){ } }, { once:true }) }) }
|
function bindUnlock(){ ['pointerdown','keydown','touchstart','mousemove'].forEach(function(ev){ window.addEventListener(ev, function once(){ try{ AudioEngine.ensureCtx(); audioUnlocked = true; AudioEngine.playPing(0) }catch(e){} if(pendingProgramState){ var st = pendingProgramState; pendingProgramState = null; startProgrammatic(st) } try{ window.removeEventListener(ev, once) }catch(_){ } }, { once:true }) }) }
|
||||||
bindUnlock()
|
bindUnlock()
|
||||||
var progRunning = false
|
var progRunning = false
|
||||||
function stopProgrammatic(){ AudioEngine.stopNoise(); progRunning=false }
|
function stopProgrammatic(){ AudioEngine.stopNoise(); progRunning=false }
|
||||||
|
|||||||
43
server.py
43
server.py
@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import websockets
|
import websockets
|
||||||
|
import socket
|
||||||
|
|
||||||
SESSIONS = {}
|
SESSIONS = {}
|
||||||
|
STOP = None
|
||||||
|
|
||||||
async def handler(ws):
|
async def handler(ws):
|
||||||
session_id = None
|
session_id = None
|
||||||
@ -26,6 +28,43 @@ async def handler(ws):
|
|||||||
await peer.send(json.dumps({'type':'state','payload':payload}))
|
await peer.send(json.dumps({'type':'state','payload':payload}))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
elif t in ('whoami','detect_ip'):
|
||||||
|
# Determine the host's primary LAN IP without external calls
|
||||||
|
ip = '127.0.0.1'
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(('8.8.8.8', 80))
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
except Exception:
|
||||||
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
try:
|
||||||
|
await ws.send(json.dumps({'type':'detect_ip','ip': ip}))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif t == 'shutdown':
|
||||||
|
try:
|
||||||
|
ra = ws.remote_address[0] if ws.remote_address else None
|
||||||
|
except Exception:
|
||||||
|
ra = None
|
||||||
|
local_ip = '127.0.0.1'
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(('8.8.8.8', 80))
|
||||||
|
local_ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
local_ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
except Exception:
|
||||||
|
local_ip = '127.0.0.1'
|
||||||
|
if ra in ('127.0.0.1', '::1', local_ip):
|
||||||
|
try:
|
||||||
|
await ws.send(json.dumps({'type':'shutdown_ack'}))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if STOP and not STOP.done():
|
||||||
|
STOP.set_result(True)
|
||||||
elif t == 'ping':
|
elif t == 'ping':
|
||||||
await ws.send(json.dumps({'type':'pong'}))
|
await ws.send(json.dumps({'type':'pong'}))
|
||||||
finally:
|
finally:
|
||||||
@ -33,9 +72,11 @@ async def handler(ws):
|
|||||||
SESSIONS[session_id].discard(ws)
|
SESSIONS[session_id].discard(ws)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
global STOP
|
||||||
|
STOP = asyncio.Future()
|
||||||
print('Starting BLS relay on ws://0.0.0.0:8787')
|
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):
|
async with websockets.serve(handler, '0.0.0.0', 8787, ping_interval=20, ping_timeout=20):
|
||||||
await asyncio.Future()
|
await STOP
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
25
setup.bat
Normal file
25
setup.bat
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set USEPY=
|
||||||
|
where py >nul 2>&1 && set USEPY=py
|
||||||
|
if not defined USEPY where python >nul 2>&1 && set USEPY=python
|
||||||
|
if not defined USEPY echo Python not found & exit /b 1
|
||||||
|
if exist ".venv" (
|
||||||
|
echo Using existing venv
|
||||||
|
) else (
|
||||||
|
if "%USEPY%"=="py" (
|
||||||
|
%USEPY% -3 -m venv .venv
|
||||||
|
) else (
|
||||||
|
%USEPY% -m venv .venv
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set VPY=.\.venv\Scripts\python
|
||||||
|
"%VPY%" -m pip install -U pip websockets
|
||||||
|
for /f "delims=" %%I in ('"%VPY%" -c "import socket;s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);s.connect(('8.8.8.8',80));print(s.getsockname()[0]);s.close()"') do set IP=%%I
|
||||||
|
if not defined IP set IP=localhost
|
||||||
|
rem Start static server if not running
|
||||||
|
netstat -ano | findstr ":8000" >nul || start /min cmd /c "%VPY% -m http.server 8000"
|
||||||
|
start "" http://%IP%:8000/
|
||||||
|
echo Relay URL: ws://%IP%:8787
|
||||||
|
"%VPY%" server.py
|
||||||
28
setup.sh
Normal file
28
setup.sh
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
if command -v python3 >/dev/null 2>&1; then PY=python3; elif command -v python >/dev/null 2>&1; then PY=python; else echo "Python not found"; exit 1; fi
|
||||||
|
if [ ! -d ".venv" ]; then "$PY" -m venv .venv; fi
|
||||||
|
VENV_PY="./.venv/bin/python"
|
||||||
|
"$VENV_PY" -m pip install -U pip websockets
|
||||||
|
IP="$($VENV_PY - <<'PYCODE'
|
||||||
|
import socket
|
||||||
|
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
s.connect(('8.8.8.8',80))
|
||||||
|
print(s.getsockname()[0])
|
||||||
|
except Exception:
|
||||||
|
print('localhost')
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
PYCODE
|
||||||
|
)"
|
||||||
|
# Start static server if not running
|
||||||
|
if ! lsof -ti:8000 >/dev/null 2>&1; then nohup "$VENV_PY" -m http.server 8000 >/dev/null 2>&1 & fi
|
||||||
|
URL="http://$IP:8000/"
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) open "$URL" ;;
|
||||||
|
Linux) command -v xdg-open >/dev/null 2>&1 && xdg-open "$URL" || true ;;
|
||||||
|
esac
|
||||||
|
echo "Relay URL: ws://$IP:8787"
|
||||||
|
exec "$VENV_PY" server.py
|
||||||
23
start-relay.bat
Normal file
23
start-relay.bat
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set USEPY=
|
||||||
|
where py >nul 2>&1 && set USEPY=py
|
||||||
|
if not defined USEPY where python >nul 2>&1 && set USEPY=python
|
||||||
|
if not defined USEPY echo Python not found & exit /b 1
|
||||||
|
if exist ".venv" (
|
||||||
|
echo Using existing venv
|
||||||
|
) else (
|
||||||
|
if "%USEPY%"=="py" (
|
||||||
|
%USEPY% -3 -m venv .venv
|
||||||
|
) else (
|
||||||
|
%USEPY% -m venv .venv
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set VPY=.\.venv\Scripts\python
|
||||||
|
"%VPY%" -m pip install -U pip websockets
|
||||||
|
for /f "delims=" %%I in ('"%VPY%" -c "import socket;s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);s.connect(('8.8.8.8',80));print(s.getsockname()[0]);s.close()"') do set IP=%%I
|
||||||
|
if not defined IP set IP=localhost
|
||||||
|
netstat -ano | findstr ":8000" >nul || start /min cmd /c "%VPY% -m http.server 8000"
|
||||||
|
echo Relay URL: ws://%IP%:8787
|
||||||
|
"%VPY%" server.py
|
||||||
4
start-relay.command
Normal file
4
start-relay.command
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
chmod +x ./start-relay.sh
|
||||||
|
./start-relay.sh
|
||||||
22
start-relay.sh
Executable file
22
start-relay.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
if command -v python3 >/dev/null 2>&1; then PY=python3; elif command -v python >/dev/null 2>&1; then PY=python; else echo "Python not found"; exit 1; fi
|
||||||
|
if [ ! -d ".venv" ]; then "$PY" -m venv .venv; fi
|
||||||
|
VENV_PY="./.venv/bin/python"
|
||||||
|
"$VENV_PY" -m pip install -U pip websockets
|
||||||
|
if ! lsof -ti:8000 >/dev/null 2>&1; then nohup "$VENV_PY" -m http.server 8000 >/dev/null 2>&1 & fi
|
||||||
|
IP="$($VENV_PY - <<'PYCODE'
|
||||||
|
import socket
|
||||||
|
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
s.connect(('8.8.8.8',80))
|
||||||
|
print(s.getsockname()[0])
|
||||||
|
except Exception:
|
||||||
|
print('localhost')
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
PYCODE
|
||||||
|
)"
|
||||||
|
echo "Relay URL: ws://$IP:8787"
|
||||||
|
exec "$VENV_PY" server.py
|
||||||
Loading…
Reference in New Issue
Block a user