-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c5de4fb
commit 624d55f
Showing
3 changed files
with
660 additions
and
354 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,292 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>AmiiboLink WebBluetooth</title> | ||
<meta charset="UTF-8" /> | ||
</head> | ||
<body> | ||
<h1>AmiiboLink Management</h1> | ||
<p>This page uses WebBluetooth to manage the AmiiboLink.</p> | ||
<p><a id="connectDisconnect" href="#">Connect</a></p> | ||
<div id="connectedStuff" style="display: none;"> | ||
<ul> | ||
<li><a id="uploadTag" href="#">Upload tag data</a></li> | ||
<li><a id="clearTag" href="#">Write blank tag</a></li> | ||
</ul> | ||
</div> | ||
<script> | ||
var amiiboLink = null; | ||
|
||
/** | ||
* Prompts the user to select a file and returns its contents as a Uint8Array. | ||
* @returns {Promise<Uint8Array>} A promise that resolves with the file contents as a Uint8Array. | ||
* @throws {Error} If no file is selected or if there is an error reading the file. | ||
*/ | ||
function promptForFileAndGetContents() { | ||
return new Promise((resolve, reject) => { | ||
// Create an input element | ||
const input = document.createElement('input'); | ||
input.type = 'file'; | ||
|
||
// Handle file selection | ||
input.onchange = (event) => { | ||
var _a; | ||
const target = event.target; | ||
const file = (_a = target.files) === null || _a === void 0 ? void 0 : _a[0]; | ||
if (file) { | ||
const reader = new FileReader(); | ||
reader.onload = (loadEvent) => { | ||
var _a; | ||
const result = (_a = loadEvent.target) === null || _a === void 0 ? void 0 : _a.result; | ||
if (result instanceof ArrayBuffer) { | ||
resolve(new Uint8Array(result)); | ||
} else { | ||
reject(new Error('Failed to read file contents.')); | ||
} | ||
}; | ||
reader.onerror = () => { | ||
reject(new Error('Failed to read file.')); | ||
}; | ||
reader.readAsArrayBuffer(file); | ||
} else { | ||
reject(new Error('No file selected.')); | ||
} | ||
}; | ||
|
||
// Trigger file selection | ||
input.click(); | ||
}); | ||
} | ||
|
||
function processDumpToAmiiboLinkCommands(inputArray) { | ||
const writeCommands = []; | ||
|
||
// Ensure the working array is exactly 540 bytes | ||
const workingArray = new Uint8Array(540); | ||
workingArray.set(inputArray.slice(0, 540), 0); | ||
|
||
// Add initial byte arrays to the output | ||
writeCommands.push(Uint8Array.from([0xA0, 0xB0])); | ||
writeCommands.push(Uint8Array.from([0xAC, 0xAC, 0x00, 0x04, 0x00, 0x00, 0x02, 0x1C])); | ||
writeCommands.push(Uint8Array.from([0xAB, 0xAB, 0x02, 0x1C])); | ||
|
||
// Loop through the input array and slice 20 bytes at a time | ||
for (let i = 0; i < workingArray.length; i += 20) { | ||
const slice = workingArray.slice(i, i + 20); | ||
const iteration = Math.floor(i / 20); | ||
|
||
// Create temporary Uint8Array with required values | ||
const tempArray = Uint8Array.from([ | ||
0xDD, 0xAA, 0x00, 0x14, | ||
...slice, | ||
0x00, | ||
iteration | ||
]); | ||
|
||
// Add temporary array to the output | ||
writeCommands.push(tempArray); | ||
} | ||
|
||
// Add final byte arrays to the output | ||
writeCommands.push(Uint8Array.from([0xBC, 0xBC])); | ||
writeCommands.push(Uint8Array.from([0xCC, 0xDD])); | ||
|
||
return writeCommands; | ||
} | ||
|
||
/** | ||
* Converts an ArrayBuffer to a hexadecimal string. | ||
* @param {ArrayBuffer} buffer - The input ArrayBuffer to convert. | ||
* @returns {string} The hexadecimal representation of the input buffer. | ||
*/ | ||
function buf2hex(buffer) { | ||
buffer = new Uint8Array(buffer); | ||
|
||
let result = ''; | ||
|
||
for (let i = 0; i < buffer.length; i++) { | ||
const hex = buffer[i].toString(16).padStart(2, '0'); | ||
result += hex.toUpperCase() + ' '; | ||
|
||
if ((i + 1) % 8 === 0) { | ||
result += '\n'; | ||
} | ||
} | ||
|
||
return result.trim(); | ||
} | ||
|
||
async function connectToAmiiboLink(onDisconnect, deviceIdentifier) { | ||
try { | ||
let device; | ||
|
||
// Check if deviceIdentifier is provided and reconnect if available | ||
if (deviceIdentifier && navigator.bluetooth.getDevices) { | ||
const devices = await navigator.bluetooth.getDevices(); | ||
device = devices.find(d => d.id === deviceIdentifier); | ||
} | ||
|
||
// If device not found or deviceIdentifier not provided, perform a new scan | ||
if (!device) { | ||
device = await navigator.bluetooth.requestDevice({ | ||
filters: [{ | ||
name: 'amiibolink' | ||
}], | ||
optionalServices: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e'] | ||
}); | ||
} | ||
|
||
await device.gatt.disconnect(); | ||
|
||
const server = await device.gatt.connect(); | ||
const service = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e'); | ||
const characteristicTX = await service.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e'); | ||
const characteristicRX = await service.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e'); | ||
|
||
|
||
// Add disconnect event listener | ||
device.addEventListener('gattserverdisconnected', () => { | ||
if (typeof onDisconnect === 'function') { | ||
onDisconnect(); | ||
} | ||
}); | ||
|
||
characteristicRX.addEventListener('characteristicvaluechanged', function(event) { | ||
const data = event.target.value; | ||
// Handle received data here | ||
console.log(`\x1B[1mRead data\x1B[m\n${buf2hex(data.buffer)}`); | ||
}); | ||
|
||
|
||
await characteristicRX.startNotifications(); | ||
|
||
const characteristics = { | ||
device, | ||
service, | ||
server, | ||
characteristicTX, | ||
characteristicRX | ||
} | ||
|
||
return characteristics; | ||
} catch (error) { | ||
console.error('Error:', error); | ||
throw error; | ||
} | ||
} | ||
|
||
function sendAndWait(characteristics, data) { | ||
return new Promise(async (resolve, reject) => { | ||
try { | ||
const { characteristicTX, characteristicRX } = characteristics; | ||
|
||
function innerResolve(event) { | ||
characteristicRX.removeEventListener('characteristicvaluechanged', innerResolve) | ||
resolve(event.target.value) | ||
} | ||
|
||
characteristicRX.addEventListener('characteristicvaluechanged', innerResolve); | ||
|
||
await characteristicTX.writeValueWithResponse(data); | ||
} catch (error) { | ||
console.error('Error:', error); | ||
reject(error); | ||
} | ||
}) | ||
} | ||
|
||
async function sendDataToAmiboLink(characteristics, data, noWait) { | ||
try { | ||
const { characteristicTX, characteristicRX } = characteristics; | ||
|
||
for (const byteArray of data) { | ||
console.log(`\x1B[1mWrite data\x1B[m\n${buf2hex(byteArray)}`); | ||
|
||
if (noWait) { | ||
await characteristicTX.writeValueWithResponse(byteArray); | ||
} else { | ||
await sendAndWait(characteristics, byteArray); | ||
} | ||
} | ||
} catch (error) { | ||
console.error('Error:', error); | ||
throw error; | ||
} | ||
} | ||
|
||
/** | ||
* This function generates and returns a 572-byte array populated with the data of a blank NTAG215 with a random UID. | ||
* @returns An array with blank NTAG215 data. | ||
*/ | ||
function getBlankNtag() { | ||
const tag = new Uint8Array(572) | ||
|
||
tag[0] = 0x04 | ||
tag[1] = Math.round(Math.random() * 255) | ||
tag[2] = Math.round(Math.random() * 255) | ||
tag[3] = tag[0] ^ tag[1] ^ tag[2] ^ 0x88 | ||
tag[4] = Math.round(Math.random() * 255) | ||
tag[5] = Math.round(Math.random() * 255) | ||
tag[6] = Math.round(Math.random() * 255) | ||
tag[7] = Math.round(Math.random() * 255) | ||
tag[8] = tag[4] ^ tag[5] ^ tag[6] ^ tag[7] | ||
|
||
tag.set([0x48, 0x00, 0x00, 0xE1, 0x10, 0x3E, 0x00, 0x03, 0x00, 0xFE], 0x09) | ||
tag.set([0xBD, 0x04, 0x00, 0x00, 0xFF, 0x00, 0x05], 0x20B) | ||
|
||
return tag | ||
} | ||
|
||
function onConnected() { | ||
document.getElementById("connectedStuff").style.display = ""; | ||
} | ||
|
||
function onDisconnected() { | ||
document.getElementById("connectedStuff").style.display = "none"; | ||
} | ||
|
||
document.getElementById("connectDisconnect").addEventListener("click", async function(e) { | ||
e.preventDefault(); | ||
|
||
if (amiiboLink) { | ||
await amiiboLink.server.disconnect(); | ||
amiiboLink = null; | ||
|
||
return; | ||
} | ||
|
||
amiiboLink = await connectToAmiiboLink(() => { | ||
amiiboLink = null; | ||
this.innerText = "Connect"; | ||
onDisconnected(); | ||
}, this.dataset.previousDevice); | ||
this.innerText = "Disconnect" | ||
onConnected(); | ||
}); | ||
|
||
document.getElementById("uploadTag").addEventListener("click", async function(e) { | ||
e.preventDefault(); | ||
|
||
var fileData = await promptForFileAndGetContents(); | ||
var tagData = new Uint8Array(540); | ||
tagData.set(fileData.slice(0, Math.min(540, fileData.length)), 0); | ||
|
||
var chunkedData = processDumpToAmiiboLinkCommands(tagData); | ||
await sendDataToAmiboLink(amiiboLink, chunkedData); | ||
alert("Done"); | ||
}) | ||
|
||
document.getElementById("clearTag").addEventListener("click", async function(e) { | ||
e.preventDefault(); | ||
|
||
var tagData = new Uint8Array(540); | ||
tagData.set(getBlankNtag().slice(0, 540), 0); | ||
|
||
var chunkedData = processDumpToAmiiboLinkCommands(tagData); | ||
await sendDataToAmiboLink(amiiboLink, chunkedData); | ||
alert("Done"); | ||
}) | ||
|
||
</script> | ||
</body> | ||
</html> |
Oops, something went wrong.