diff --git a/dollseq/index.html b/dollseq/index.html new file mode 100644 index 0000000..aab3c7a --- /dev/null +++ b/dollseq/index.html @@ -0,0 +1,207 @@ + + + + + + + + + + + SAFETY LOCK + + + + ARM DOLL CONTROL + + + + Intensity Limit: 3 + + Pair + + + + Not Armed + + + + Safety ON (commands will not be sent) + + + + + + + + + + + + log + + + + + diff --git a/libs/shock.js b/libs/shock.js new file mode 100644 index 0000000..299aef3 --- /dev/null +++ b/libs/shock.js @@ -0,0 +1,174 @@ +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 setIntensityReadout = (value) => { + document.querySelector("#intensity-readout").innerHTML = value; +}; + +window.getIntensity = () => document.querySelector("#intensity").value; + +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 = (level) => { + if (window.__SAFETY__ || !window.__ARMED__) { + addLogMessage("SAFETY ON or NOT ARMED -- FIRE REJECTED."); + return; + } + + collar.runStatic(level); +}; + +window.shockInit = () => { + 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("#pair").addEventListener("click", () => { + // const pin = document.querySelector("#pin").value; + connectCollar(); + }); + + document.querySelector("#intensity").addEventListener("input", (event) => { + setIntensityReadout(event.target.value); + }); + + setIntensityReadout(getIntensity()); + + document.querySelector("#safety").checked = true; + document.querySelector("#arm").checked = false; + addLogMessage("Safety is on; shock commands will NOT be sent."); + addLogMessage("Ready"); +}; diff --git a/libs/shocktools.css b/libs/shocktools.css new file mode 100644 index 0000000..b44cd92 --- /dev/null +++ b/libs/shocktools.css @@ -0,0 +1,18 @@ +: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; +} +