You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
455 lines
17 KiB
HTML
455 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
|
|
<script src="https://unpkg.com/downloadjs@1.4.7"></script>
|
|
<script src="https://unpkg.com/tone@14.7.77/build/Tone.js" async></script>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
|
</head>
|
|
|
|
<body>
|
|
<div id="app">
|
|
<v-app>
|
|
<v-app-bar app color="blue darken-2 white--text">
|
|
<v-img src="birdytiger.png" max-width="48px" max-height="48px"></v-img>
|
|
<v-toolbar-title>
|
|
<b>Birdy Flash Tools 2.0</b>
|
|
<sub>by <a target="_blank" style="color: white;" href="https://t.me/spottychee">cheetah.cat</a> (<a style="color: white;" target="_blank" href="https://t.me/spottychee">telegram</a>)</sub>
|
|
</v-toolbar-title>
|
|
<v-spacer></v-spacer>
|
|
</v-app-bar>
|
|
<v-content>
|
|
<v-container v-if="global.step == 0">
|
|
<v-btn color="success" raised @click="openSerial()">Open Serial-Port</v-btn>
|
|
</v-container>
|
|
<v-container v-if="global.step == 1">
|
|
<v-row>
|
|
<v-col>
|
|
<pre>Selected Device: {{ global.selectedDevice.model }}
|
|
Hardware Version: {{ global.selectedDevice.hardwareVersion }}
|
|
Firmware Version: {{ global.selectedDevice.firmwareVersion }}
|
|
E2P Version: {{ global.selectedDevice.e2pVersion }}
|
|
Serial: {{ global.selectedDevice.deviceSerial }}
|
|
</pre>
|
|
</v-col>
|
|
<v-col>
|
|
<v-btn @click="writeE2P()">Write E2P</v-btn>
|
|
<v-btn @click="dumpE2P()">Dump E2P</v-btn>
|
|
|
|
|
|
<v-dialog v-model="changeSerial.dialog" width="auto">
|
|
<template v-slot:activator="{ on, attrs }">
|
|
<v-btn color="primary" v-bind="attrs" v-on="on">Change Serial</v-btn>
|
|
</template>
|
|
<v-card>
|
|
<v-card-text>
|
|
<v-text-field readonly v-model="changeSerial.oldSerial"></v-text-field>
|
|
<v-text-field v-model="changeSerial.newSerial"></v-text-field>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-btn color="primary" block @click="applyChangeSerial(); changeSerial.dialog = false">Close Dialog</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-col>
|
|
</v-row>
|
|
<v-col v-if="global.selectedDevice.model == 'BirdyIOT'">
|
|
<h2>Birdy Slim IoT : Features</h2>
|
|
<v-col cols="12" sm="6">
|
|
<v-text-field label="AppEUI" v-model="lora.AppEUI"></v-text-field>
|
|
<v-text-field label="DevEUI" v-model="lora.DevEUI"></v-text-field>
|
|
<v-text-field label="AppSKey" v-model="lora.AppSKey"></v-text-field>
|
|
<v-text-field label="NwkSKey" v-model="lora.NwkSKey"></v-text-field>
|
|
<v-text-field label="DevAddr" v-model="lora.DevAddr"></v-text-field>
|
|
<v-btn @click="writeLoRaWAN()" >Write LoRaWAN</v-btn>
|
|
</v-col>
|
|
<v-col class="d-flex" cols="12" sm="6">
|
|
<v-btn color="success" :disabled="step1.ringtoneSelection==null" raised @click="decodeRingtone()">Load</v-btn>
|
|
<v-btn color="warning" :disabled="step1.ringtoneSelection==null || !step1.dirty" raised @click="encodeRingtone()">Store</v-btn>
|
|
<v-btn color="error" :disabled="step1.ringtoneSelection==null" raised @click="cleanRingtone()">Clear</v-btn>
|
|
</v-col>
|
|
</v-col>
|
|
</v-container>
|
|
<!--
|
|
hello to anyone, who reads this :3
|
|
-->
|
|
</v-content>
|
|
<v-footer>
|
|
<v-card flat tile width="100%" class="red lighten-1 text-center">
|
|
<v-card-text class="white--text">
|
|
{{ new Date().getFullYear() }} - <strong>cheetah.cat</strong> - disclaimer.txt
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-footer>
|
|
</v-app>
|
|
</div>
|
|
|
|
<script>
|
|
// im working on a full version of this.
|
|
class TPL_BirdySlimConfig {
|
|
constructor() {
|
|
this.buffer = null
|
|
}
|
|
loadBuffer(buffer) {
|
|
this.buffer = buffer
|
|
return this
|
|
}
|
|
saveBuffer() {
|
|
return this.buffer
|
|
}
|
|
writeNotes(melody) {
|
|
console.log(melody)
|
|
let offset = 0x0AE0 + (melody.ringNumber * 90)
|
|
this.buffer[ offset++ ] = melody.melodyHeader
|
|
for(let i=0;i<45;i++) {
|
|
if (!(!!melody.notes[i])) {
|
|
this.buffer[ offset++ ] = 0x00
|
|
this.buffer[ offset++ ] = 0x00
|
|
} else {
|
|
let note = melody.notes[i] // 0note,1octave,2length
|
|
this.buffer[ offset++ ] = (note[ 1 ] << 4 & 0xf0) | (note[ 0 ] & 0xf)
|
|
this.buffer[ offset++ ] = note[ 2 ]
|
|
}
|
|
}
|
|
}
|
|
readNotesFromBuffer(ringNumber) {
|
|
const offset = 0x0AE0 + (ringNumber * 90)
|
|
let music = JSON.parse(JSON.stringify(this.buffer)).slice(offset,offset+1+(2 * 45))
|
|
//console.log(music.map(x=>x.toString(16).toUpperCase()).join(''))
|
|
/////if (typeof(Buffer)==='undefined') music = new Int8Array(music) // if in browser, we get arraybuffers
|
|
const notes = [], melodyHeader = music[0]
|
|
for (let i=1; i < music.length; i+=2) {
|
|
const note = music[i] >> 0 & 0xf,
|
|
octave = music[i] >> 4 & 0xf,
|
|
length = music[i+1] // 1 Byte for the Length
|
|
if (note == 0) break;
|
|
//console.log(`${ noteNames[note] } ${ octave } ${ length*10 }ms`)
|
|
notes.push([note, octave, length ])
|
|
}
|
|
return {
|
|
ringNumber,
|
|
melodyHeader,
|
|
notes,
|
|
}
|
|
}
|
|
}
|
|
function parseHexString(str) {
|
|
const result = []
|
|
while (str.length >= 2) {
|
|
result.push(parseInt(str.substring(0, 2), 16))
|
|
str = str.substring(2, str.length)
|
|
}
|
|
return result
|
|
}
|
|
|
|
|
|
class SerialPort {
|
|
constructor(device) {
|
|
this.device = device
|
|
this.writer = null
|
|
this.reader = null
|
|
}
|
|
connect(baudRate = 57600) {
|
|
let readLoop = async () => {
|
|
while (this.open && this.device?.readable) {
|
|
this.open = true
|
|
this.reader = this.device.readable.getReader()
|
|
console.log('starting read loop')
|
|
try {
|
|
while (this.open) {
|
|
console.log('reading...')
|
|
const { value, done } = await this.reader.read()
|
|
if (done) {
|
|
// |reader| has been canceled.
|
|
this.open = false
|
|
break;
|
|
}
|
|
console.log('read complete:', value, done)
|
|
}
|
|
} catch (error) {
|
|
console.error('reading error', error)
|
|
} finally {
|
|
this.reader.releaseLock()
|
|
}
|
|
}
|
|
}
|
|
return this.device.open({
|
|
baudRate: 57600,
|
|
stopBits: 1,
|
|
dataBits: 8,
|
|
bufferSize: 1,
|
|
parity: 'none',
|
|
flowControl: 'none'
|
|
})
|
|
.then(async _=> {
|
|
this.open = !!this.device?.readable
|
|
this.writer = this.device.writable.getWriter()
|
|
this.reader = this.device.readable.getReader()
|
|
//readLoop()
|
|
})
|
|
.catch(console.error)
|
|
}
|
|
disconnect() {
|
|
return this.device.close()
|
|
}
|
|
async send(data) {
|
|
await this.writer.write(data)
|
|
//this.writer.releaseLock()
|
|
return true
|
|
}
|
|
}
|
|
class TPLBirdy {
|
|
constructor(port) {
|
|
this.port = port
|
|
}
|
|
checksum(data) {
|
|
let cs = 0x00
|
|
for (let x of data)
|
|
cs = (cs + x % 256) & 0xFF
|
|
return cs & 0xFF
|
|
}
|
|
async sendCMD(data) {
|
|
let cmd = [0x02, data.length].concat(data, [0x03])
|
|
cmd.push(this.checksum(cmd))
|
|
cmd.push(0x04)
|
|
let bffr = new Uint8Array(cmd)
|
|
console.log('>', bffr.map(a=>a.toString(16)).join(' '))
|
|
await this.port.send(bffr)
|
|
}
|
|
responseToString(input) {
|
|
return input.map(a=>String.fromCharCode(a)).join('')
|
|
}
|
|
async wait4Response() {
|
|
{
|
|
console.log('requesting first read', this.port.reader)
|
|
let { value, done } = await this.port.reader.read()
|
|
console.log('read', value[0], done)
|
|
if (value[0] != 0x02) throw 'First byte isnt 0x02, something is off'
|
|
}
|
|
let dataLength = -1
|
|
{
|
|
let { value, done } = await this.port.reader.read()
|
|
dataLength = value[0]
|
|
}
|
|
console.log('<', dataLength, 'bytes')
|
|
let dataArray = []
|
|
{
|
|
for (let i = 0; i < dataLength; i++) {
|
|
let { value, done } = await this.port.reader.read()
|
|
dataArray.push(value[0])
|
|
}
|
|
}
|
|
{
|
|
let { value, done } = await this.port.reader.read()
|
|
if (value[0] != 0x03) throw 'Payload EOF byte isnt 0x03, something is off'
|
|
}
|
|
console.log('<', dataArray)
|
|
let dataChecksum = -1
|
|
{
|
|
let { value, done } = await this.port.reader.read()
|
|
dataChecksum = value[0]
|
|
}
|
|
const recvChecksumData = [0x02, dataLength].concat(dataArray, [0x03])
|
|
const recvChecksum = this.checksum(recvChecksumData)
|
|
{
|
|
let { value, done } = await this.port.reader.read()
|
|
if (value[0] != 0x04) throw 'Checksum EOF byte isnt 0x04, something is off'
|
|
}
|
|
if (dataChecksum != recvChecksum) throw 'Checksum mismatch'
|
|
return dataArray
|
|
}
|
|
async getDeviceInfo() {
|
|
await this.sendCMD([0x01,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff])
|
|
let deviceInfo = await this.wait4Response()
|
|
const devInfo = {
|
|
type: false,
|
|
firstIDK: null,
|
|
model: '',
|
|
firmwareVersion: '',
|
|
hardwareVersion: '',
|
|
e2pVersion: '',
|
|
idk: '',
|
|
deviceSerial: '',
|
|
}
|
|
let parts = this.responseToString(deviceInfo).substring(1).split(',')
|
|
console.log(parts)
|
|
switch (parts[0]) {
|
|
case "BirdyIOT":
|
|
devInfo.type = 'Slim'
|
|
devInfo.firstIDK = parts[0]
|
|
devInfo.model = parts[0]
|
|
devInfo.firmwareVersion = parts[1]
|
|
devInfo.hardwareVersion = parts[2]
|
|
devInfo.e2pVersion = parts[3]
|
|
devInfo.idk = parts[4]
|
|
devInfo.deviceSerial = parts[5]
|
|
break;
|
|
case "BirdyWP-128BOS":
|
|
case "BirdyWP-128r3BOS":
|
|
case "BirdyW":
|
|
devInfo.type = 'BirdyWP_Atmega1281'
|
|
devInfo.firstIDK = parts[0]
|
|
devInfo.model = parts[0]
|
|
devInfo.firmwareVersion = parts[1]
|
|
devInfo.hardwareVersion = parts[2]
|
|
devInfo.e2pVersion = parts[3]
|
|
devInfo.idk = parts[4]
|
|
devInfo.deviceSerial = parts[5]
|
|
break;
|
|
case "BirdyWP-r3":
|
|
devInfo.type = 'BirdyWP-r3'
|
|
devInfo.firstIDK = parts[0]
|
|
devInfo.model = parts[0]
|
|
devInfo.firmwareVersion = parts[1]
|
|
devInfo.hardwareVersion = parts[2]
|
|
devInfo.e2pVersion = parts[3]
|
|
devInfo.idk = parts[4]
|
|
devInfo.deviceSerial = parts[5]
|
|
break;
|
|
case "BirdyE":
|
|
devInfo.type = 'BirdyE'
|
|
devInfo.firstIDK = 0x00
|
|
devInfo.model = parts[0]
|
|
devInfo.firmwareVersion = parts[1]
|
|
devInfo.hardwareVersion = parts[2]
|
|
devInfo.e2pVersion = ''
|
|
devInfo.idk = ''
|
|
devInfo.deviceSerial = "-UNKNOWN-"
|
|
break;
|
|
}
|
|
this.deviceInfo = devInfo
|
|
return devInfo
|
|
}
|
|
async writeSerial(newSerial) {
|
|
switch (this.deviceInfo.type) {
|
|
case 'Slim':
|
|
if (newSerial.length != 13) throw 'insufficient serial length'
|
|
await this.sendCMD([ 0x13, ].concat(newSerial.split('').map(x => x.charCodeAt())))
|
|
console.log(this.responseToString(await this.wait4Response()))
|
|
break
|
|
case 'BirdyWP-r3': //note to myself, needs a 0 in front of the serial 1234 -> 01234
|
|
//if (newSerial.length != 13) throw 'insufficient serial length'
|
|
await this.sendCMD([ 0x13, ].concat(`0${ newSerial }`.split('').map(x => x.charCodeAt())))
|
|
console.log(this.responseToString(await this.wait4Response()))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
new Vue({
|
|
el: '#app',
|
|
vuetify: new Vuetify(),
|
|
data() {
|
|
return {
|
|
serial: {},
|
|
sPort: null,
|
|
bPager: null,
|
|
|
|
step1: {
|
|
birdyRingtoneSelection: [],
|
|
ringtoneSelection: null,
|
|
dirty: false,
|
|
},
|
|
global: {
|
|
step: 0,
|
|
selectedDevice: { type: null },
|
|
},
|
|
lora: {
|
|
AppEUI: '00 00 00 00 00 00 00 00',
|
|
DevEUI: '00 00 00 00 00 00 00 00',
|
|
AppSKey: '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00',
|
|
NwkSKey: '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00',
|
|
DevAddr: '00 00 00 00',
|
|
},
|
|
changeSerial: {
|
|
dialog: false,
|
|
oldSerial: '',
|
|
newSerial: ''
|
|
},
|
|
}
|
|
},
|
|
created() {
|
|
},
|
|
methods: {
|
|
// step0
|
|
openSerial() {
|
|
const filters = [
|
|
{ 'vendorId': 0x0403, 'productId': 0x6001 }, // Future Technology Devices International, Ltd FT232 Serial (UART) IC
|
|
]
|
|
const usbVendorId = '0x0403'
|
|
return navigator.serial.requestPort({ filters: [ { usbVendorId }] })
|
|
.then(device => {
|
|
this.sPort = new SerialPort(device)
|
|
this.sPort.connect(57600).then(_ => {
|
|
this.bPager = new TPLBirdy(this.sPort)
|
|
})
|
|
.then(async _ => {
|
|
let deviceInfo = await this.bPager.getDeviceInfo()
|
|
console.log('deviceInfo', deviceInfo)
|
|
if (deviceInfo.type === false) {
|
|
this.global.step = -1
|
|
alert('unknown pager detected')
|
|
} else {
|
|
this.global.step = 1
|
|
this.global.selectedDevice = deviceInfo
|
|
|
|
this.changeSerial.oldSerial = deviceInfo.deviceSerial
|
|
}
|
|
})
|
|
.catch(error => {
|
|
alert(error)
|
|
})
|
|
})
|
|
},
|
|
|
|
async rawHex(cmd) {
|
|
await this.bPager.sendCMD(parseHexString(cmd))
|
|
let resp = this.bPager.wait4Response()
|
|
console.log(resp)
|
|
},
|
|
async writeLoRaWAN() {
|
|
|
|
loraProvCMD = [ 0x2B, 0x00, 0x44, 0x00 ].concat(
|
|
parseHexString(this.lora.AppSKey.replaceAll(' ', '')),
|
|
parseHexString(this.lora.AppEUI.replaceAll(' ', '')),
|
|
parseHexString(this.lora.DevEUI.replaceAll(' ', '')),
|
|
parseHexString(this.lora.AppSKey.replaceAll(' ', '')),
|
|
parseHexString(this.lora.NwkSKey.replaceAll(' ', '')),
|
|
parseHexString(this.lora.DevAddr.replaceAll(' ', '')),
|
|
)
|
|
console.log(loraProvCMD)
|
|
await this.bPager.sendCMD(loraProvCMD)
|
|
let resp = this.bPager.wait4Response()
|
|
console.log(resp)
|
|
},
|
|
async applyChangeSerial() {
|
|
await this.bPager.writeSerial(this.changeSerial.newSerial)
|
|
let resp = this.bPager.wait4Response()
|
|
console.log(resp)
|
|
|
|
let deviceInfo = await this.bPager.getDeviceInfo()
|
|
console.log('deviceInfo', deviceInfo)
|
|
if (deviceInfo.type === false) {
|
|
this.global.step = -1
|
|
alert('unknown pager detected')
|
|
} else {
|
|
this.global.step = 1
|
|
this.global.selectedDevice = deviceInfo
|
|
|
|
this.changeSerial.oldSerial = deviceInfo.deviceSerial
|
|
}
|
|
}
|
|
}
|
|
|
|
})
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|