dollseq
This commit is contained in:
parent
0697e5f55b
commit
095cf7f9b7
3 changed files with 399 additions and 0 deletions
207
dollseq/index.html
Normal file
207
dollseq/index.html
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
<!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"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="/libs/shocktools.css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.drawer {
|
||||||
|
display: flex;
|
||||||
|
background-color: #2f382f;
|
||||||
|
max-width: 960px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seqItem {
|
||||||
|
/* allow for 16 seqments before wrapping */
|
||||||
|
flex: 1 8 100px;
|
||||||
|
|
||||||
|
max-width: 100px;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 100px;
|
||||||
|
background-color: #6f916f;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.seqItemInput {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 95%;
|
||||||
|
position: relative;
|
||||||
|
left: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seqItemValue {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #42424288;
|
||||||
|
font-size: 45px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
--progress-color: #e2d34a;
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb,
|
||||||
|
input[type="range"]::-moz-range-track {
|
||||||
|
background-color: transparent;
|
||||||
|
appearance: none;
|
||||||
|
opacity: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
background-color: var(--progress-color);
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
input[type="range"]::-moz-range-progress {
|
||||||
|
background-color: var(--progress-color);
|
||||||
|
appearance: none;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section id="safetysection">
|
||||||
|
<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 Limit: <span id="intensity-readout">3</span></label
|
||||||
|
><br />
|
||||||
|
<input id="intensity" type="range" min="1" max="9" value="3" />
|
||||||
|
<button id="pair">Pair</button>
|
||||||
|
</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 id="app">
|
||||||
|
<div id="drawer" class="drawer">
|
||||||
|
<div class="seqItem">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="9"
|
||||||
|
step="1"
|
||||||
|
class="seqItemInput"
|
||||||
|
value="0"
|
||||||
|
oninput="handleNoteChange"
|
||||||
|
onchange="handleNoteChange"
|
||||||
|
/>
|
||||||
|
<div class="seqItemValue"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="logsection">
|
||||||
|
<h2>log</h2>
|
||||||
|
<pre id="log"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script src="/libs/shock.js"></script>
|
||||||
|
<script defer async>
|
||||||
|
window.shockInit();
|
||||||
|
|
||||||
|
const safeLevel = (input, userMax, rangeMax = 9) =>
|
||||||
|
Math.min(Math.round((input / rangeMax) * userMax), userMax);
|
||||||
|
|
||||||
|
const handleNoteChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const parent = event.target.parentNode;
|
||||||
|
const display = parent.querySelector(".seqItemValue");
|
||||||
|
const safeValue = safeLevel(value, window.getIntensity());
|
||||||
|
|
||||||
|
console.log({ value, safeValue });
|
||||||
|
|
||||||
|
if (safeValue === 0) {
|
||||||
|
display.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.innerHTML = `${safeValue}`;
|
||||||
|
|
||||||
|
location.hash = generateURLSequence();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupSequencerDrawer = (sequenceItems, urlSeed) => {
|
||||||
|
const seqItem = document.querySelector(".seqItem");
|
||||||
|
const drawer = document.querySelector("#drawer");
|
||||||
|
|
||||||
|
seqItem.value = 0;
|
||||||
|
drawer.innerHTML = "";
|
||||||
|
|
||||||
|
if (urlSeed !== null) {
|
||||||
|
sequenceItems = urlSeed.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < sequenceItems; i++) {
|
||||||
|
drawer.innerHTML += seqItem.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachInput((input, _, idx) => {
|
||||||
|
input.addEventListener("input", handleNoteChange);
|
||||||
|
input.addEventListener("change", handleNoteChange);
|
||||||
|
input.value = urlSeed[idx] || 0;
|
||||||
|
handleNoteChange({ target: input });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const forEachInput = (predicate) => {
|
||||||
|
const allInputs = drawer.querySelectorAll(".seqItemInput");
|
||||||
|
allInputs.forEach((input, idx) => predicate(input, input.value, idx));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateURLSequence = () => {
|
||||||
|
let seq = "";
|
||||||
|
forEachInput((_, value) => (seq += value));
|
||||||
|
|
||||||
|
return seq;
|
||||||
|
};
|
||||||
|
|
||||||
|
let urlSeed = null;
|
||||||
|
|
||||||
|
if (location.hash) {
|
||||||
|
urlSeed = location.hash.split("").slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSequencerDrawer(16, urlSeed);
|
||||||
|
|
||||||
|
document.querySelector("#intensity").addEventListener("input", (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
forEachInput((input) => {
|
||||||
|
handleNoteChange({ target: input });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
174
libs/shock.js
Normal file
174
libs/shock.js
Normal file
|
@ -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");
|
||||||
|
};
|
18
libs/shocktools.css
Normal file
18
libs/shocktools.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue