334 lines
9.3 KiB
HTML
334 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
|
<link rel="preconnect" href="https://fonts.bunny.net" />
|
|
<link
|
|
href="https://fonts.bunny.net/css?family=atkinson-hyperlegible:400,400i,700,700i"
|
|
rel="stylesheet"
|
|
/>
|
|
<style>
|
|
:root {
|
|
font-family: "Atkinson Hyperlegible", sans-serif;
|
|
background-color: black;
|
|
color: #efefef;
|
|
}
|
|
|
|
section {
|
|
padding: 2em;
|
|
}
|
|
|
|
#dot,
|
|
#safetyDot {
|
|
width: 7px;
|
|
height: 7px;
|
|
background-color: red;
|
|
display: inline-block;
|
|
}
|
|
</style>
|
|
|
|
<main>
|
|
<section>
|
|
<div>
|
|
<label for="safety">SAFETY LOCK</label>
|
|
<input type="checkbox" name="safety" id="safety" value="true" />
|
|
</div>
|
|
<div>
|
|
<label for="arm">ARM DOLL CONTROL</label>
|
|
<input type="checkbox" name="arm" id="arm" />
|
|
</div>
|
|
<div>
|
|
<label for="intensity">Intensity</label><br />
|
|
<input id="intensity" type="range" min="1" max="5" value="3" />
|
|
<button id="pair">Pair</button>
|
|
</div>
|
|
<div>
|
|
<label for="targets">Targets</label><br />
|
|
<textarea id="targets" width="50">
|
|
DollTR
|
|
DollNC
|
|
DollVS
|
|
WitchOfTheHighlands
|
|
41666
|
|
xenogender</textarea
|
|
>
|
|
</div>
|
|
<div>
|
|
<label for="targets">Attackers</label><br />
|
|
<textarea id="attackers">
|
|
Lyyti
|
|
LyytiTR
|
|
LyytiNC</textarea
|
|
>
|
|
</div>
|
|
<div>
|
|
<input type="submit" id="connect" value="Connect" />
|
|
</div>
|
|
<div>
|
|
<div id="dot"></div>
|
|
<span id="dotText">Not Armed</span>
|
|
</div>
|
|
<div>
|
|
<div id="safetyDot"></div>
|
|
<span id="safetyDotText">Safety ON (commands will not be sent)</span>
|
|
</div>
|
|
</section>
|
|
<section>
|
|
<h2>log</h2>
|
|
<pre id="log"></pre>
|
|
</section>
|
|
</main>
|
|
<script defer async>
|
|
window.__ARMED__ = false;
|
|
window.__SAFETY__ = true;
|
|
window.__CONNECTED__ = false;
|
|
|
|
const COLLAR_BT_NAME = "PetSafe Smart Dog Trainer";
|
|
const COLLAR_COMMAND_SERVICE = "0bd51666-e7cb-469b-8e4d-2742f1ba77cc";
|
|
const COLLAR_COMMAND_CHARACTERISTIC = "e7add780-b042-4876-aae1-112855353cc1";
|
|
const COLLAR_AUTH_CHARACTERISTIC = "0e7ad781-b043-4877-aae2-112855353cc2";
|
|
const COLLAR_TONE_COMMAND = [0x55, 0x36, 0x31, 0x31, 0x31, 0x30];
|
|
const COLLAR_VIBRATE_COMMAND = [0x55, 0x36, 0x31, 0x33, 0x33, 0x30];
|
|
// Last byte is command strength (0-15) + 0x48. We set it to 0 here.
|
|
const COLLAR_STATIC_COMMAND = [0x55, 0x36, 0x31, 0x32, 0x33, 0x30];
|
|
// Last four bytes are the ASCII values of the PIN digits
|
|
const COLLAR_AUTH_COMMAND = [0x55, 0x37, 0x37, 0x30, 0x30, 0x30, 0x30];
|
|
|
|
class PetsafeSmartDogTrainingCollar {
|
|
constructor() {
|
|
this.device = null;
|
|
this.service = null;
|
|
this.tx = null;
|
|
}
|
|
|
|
async connect() {
|
|
console.log("Starting connect");
|
|
|
|
if (!navigator.bluetooth) {
|
|
addLogMessage("No WebBluetooth support; bailing.");
|
|
throw new Error("No WebBluetooth; bailing.");
|
|
}
|
|
|
|
this.device = await navigator.bluetooth.requestDevice({
|
|
filters: [
|
|
{
|
|
name: COLLAR_BT_NAME,
|
|
},
|
|
],
|
|
optionalServices: [COLLAR_COMMAND_SERVICE],
|
|
});
|
|
let server = await this.device.gatt.connect();
|
|
this.service = await server.getPrimaryService(COLLAR_COMMAND_SERVICE);
|
|
this.tx = await this.service.getCharacteristic(
|
|
COLLAR_COMMAND_CHARACTERISTIC
|
|
);
|
|
this.auth = await this.service.getCharacteristic(
|
|
COLLAR_AUTH_CHARACTERISTIC
|
|
);
|
|
console.log("Connected");
|
|
}
|
|
|
|
async disconnect() {
|
|
await this.device.gatt.disconnect();
|
|
}
|
|
async authorize(d1, d2, d3, d4) {
|
|
console.log("Authenticating with ", d1, d2, d3, d4);
|
|
let r = new Uint8Array(COLLAR_AUTH_COMMAND);
|
|
r[3] += d1;
|
|
r[4] += d2;
|
|
r[5] += d3;
|
|
r[6] += d4;
|
|
console.log(r);
|
|
this.auth.writeValue(r);
|
|
}
|
|
async runTone() {
|
|
console.log("Running tone");
|
|
addLogMessage("[TONE]");
|
|
this.tx.writeValue(new Uint8Array(COLLAR_TONE_COMMAND));
|
|
}
|
|
|
|
async runVibrate() {
|
|
console.log("Running vibration");
|
|
addLogMessage("[VIBRATE]");
|
|
this.tx.writeValue(new Uint8Array(COLLAR_VIBRATE_COMMAND));
|
|
}
|
|
|
|
async runStatic(power) {
|
|
if (window.__SAFETY__) {
|
|
addLogMessage("SAFETY ON; runStatic REJECTED");
|
|
return;
|
|
}
|
|
|
|
console.log("Running static");
|
|
addLogMessage("[SHOCK] Power = " + power);
|
|
if (power < 0 || power > 15) {
|
|
throw Error("power not in range 0-15");
|
|
}
|
|
let r = new Uint8Array(COLLAR_STATIC_COMMAND);
|
|
r[5] += power;
|
|
console.log(r);
|
|
this.tx.writeValue(r);
|
|
}
|
|
}
|
|
|
|
const collar = new PetsafeSmartDogTrainingCollar();
|
|
|
|
const addLogMessage = (message) => {
|
|
const logEl = document.querySelector("#log");
|
|
logEl.innerHTML = `${new Date().toISOString()} :: ${message}\n${
|
|
logEl.innerHTML
|
|
}`;
|
|
};
|
|
|
|
const setStatus = (color, text) => {
|
|
document.querySelector("#dot").style.backgroundColor = color;
|
|
document.querySelector("#dotText").innerHTML = text;
|
|
};
|
|
|
|
const setSafety = (color, text) => {
|
|
document.querySelector("#safetyDot").style.backgroundColor = color;
|
|
document.querySelector("#safetyDotText").innerHTML = text;
|
|
};
|
|
|
|
const connectCollar = async (pin) => {
|
|
addLogMessage("Connecting to collar...");
|
|
await collar.connect();
|
|
addLogMessage("Collar connected; running vibration test.");
|
|
await collar.runVibrate();
|
|
};
|
|
|
|
const fire = () => {
|
|
if (window.__SAFETY__ || !window.__ARMED__) {
|
|
addLogMessage("SAFETY ON or NOT ARMED -- FIRE REJECTED.");
|
|
return;
|
|
}
|
|
|
|
const intensity = Number(document.querySelector("#intensity").value);
|
|
|
|
collar.runStatic(intensity || 0);
|
|
};
|
|
|
|
const resolveCharacters = async (list) => {
|
|
const url = `https://census.daybreakgames.com/s:saegd/get/ps2/character_name?name.first_lower=${list
|
|
.map((a) => a.toLowerCase())
|
|
.join(",")}&c:limit=${list.length}`;
|
|
|
|
const req = await fetch(url);
|
|
const data = await req.json();
|
|
|
|
return data.character_name_list.map((a) => a.character_id);
|
|
};
|
|
|
|
const connectToESS = (targets, attackers) => {
|
|
addLogMessage("Connecting to WSS...");
|
|
const ws = new WebSocket(
|
|
"wss://push.nanite-systems.net/streaming?environment=ps2&service-id=s:saegd"
|
|
);
|
|
|
|
ws.onopen = () => {
|
|
setTimeout(() => {
|
|
ws.send(
|
|
JSON.stringify({
|
|
service: "event",
|
|
action: "subscribe",
|
|
characters: targets,
|
|
eventNames: ["Death"],
|
|
worlds: ["all"],
|
|
logicalAndCharactersWithWorlds: true,
|
|
})
|
|
);
|
|
|
|
addLogMessage(`CONNECTED. Listening to ${targets}`);
|
|
setStatus("green", "CONNECTED");
|
|
|
|
addLogMessage(`will match on ${attackers}`);
|
|
}, 500);
|
|
};
|
|
|
|
ws.onerror = ws.onclose = (a) => {
|
|
console.log("ess fail", a);
|
|
addLogMessage("ESS failed to connect. Reload.");
|
|
};
|
|
|
|
ws.onmessage = (message) => {
|
|
const { payload } = JSON.parse(message.data);
|
|
|
|
if (payload.event_name === "Death") {
|
|
console.log("Death event", { payload });
|
|
if (attackers.includes(payload.attacker_character_id)) {
|
|
addLogMessage(
|
|
`<b>[TRIGGERED]</b> Death from ${payload.attacker_character_id} to ${payload.character_id}`
|
|
);
|
|
|
|
if (window.__SAFETY__ || !window.__ARMED__) {
|
|
addLogMessage("SAFETY ON or NOT ARMED -- NO FIRE");
|
|
return;
|
|
}
|
|
|
|
fire();
|
|
} else {
|
|
addLogMessage(
|
|
`Death event skipped, from ${payload.attacker_character_id} to ${payload.character_id}`
|
|
);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
document.querySelector("#arm").addEventListener("change", (event) => {
|
|
window.__ARMED__ = event.target.checked;
|
|
|
|
if (event.target.checked) {
|
|
addLogMessage("ARMED");
|
|
setStatus("yellow", "ARMED");
|
|
} else {
|
|
addLogMessage("Disarmed.");
|
|
setStatus("red", "Not Armed");
|
|
}
|
|
});
|
|
|
|
document.querySelector("#safety").addEventListener("change", (event) => {
|
|
window.__SAFETY__ = event.target.checked;
|
|
|
|
if (event.target.checked) {
|
|
addLogMessage("Safety is on; shock commands will NOT be sent.");
|
|
setSafety("red", "Safety ON (commands will not be sent)");
|
|
} else {
|
|
addLogMessage("Safety is off. Good luck, dolly.");
|
|
setSafety("green", "Safety OFF");
|
|
}
|
|
});
|
|
|
|
document
|
|
.querySelector("#connect")
|
|
.addEventListener("click", async (event) => {
|
|
if (window.__CONNECTED__) {
|
|
addLogMessage("Connection rejected; already connected.");
|
|
return;
|
|
}
|
|
if (!window.__ARMED__) {
|
|
addLogMessage("Connection rejected; arm the doll.");
|
|
return;
|
|
}
|
|
|
|
const targets = document.querySelector("#targets").value.split("\n");
|
|
const attackers = document.querySelector("#attackers").value.split("\n");
|
|
|
|
window.__CONNECTED__ = true;
|
|
|
|
setStatus("blue", "CONNECTING...");
|
|
addLogMessage("Fetching character IDs...");
|
|
const [targetIDs, attackerIDs] = await Promise.all([
|
|
resolveCharacters(targets),
|
|
resolveCharacters(attackers),
|
|
]);
|
|
connectToESS(targetIDs, attackerIDs);
|
|
});
|
|
|
|
document.querySelector("#pair").addEventListener("click", () => {
|
|
// const pin = document.querySelector("#pin").value;
|
|
connectCollar();
|
|
});
|
|
|
|
document.querySelector("#safety").checked = true;
|
|
document.querySelector("#arm").checked = false;
|
|
addLogMessage("Safety is on; shock commands will NOT be sent.");
|
|
addLogMessage("Ready");
|
|
</script>
|