Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../global.css">
<link rel="stylesheet" href="preference.css">
<script type="module">
import SimpleBLEClient from './ble-client.mjs'
document.addEventListener('DOMContentLoaded', () => {
const FIELD_DEFINITIONS = [
{ id: 'wifi.ssid', type: 'string' },
{ id: 'wifi.password', type: 'string' },
{ id: 'renderer.type', type: 'string' },
{ id: 'driver.type', type: 'string' },
{ id: 'driver.panId', type: 'number' },
{ id: 'driver.tiltId', type: 'number' },
{ id: 'driver.pwmPan', type: 'number' },
{ id: 'driver.pwmTilt', type: 'number' },
{ id: 'driver.baud', type: 'number' },
{ id: 'driver.offsetPan', type: 'number' },
{ id: 'driver.offsetTilt', type: 'number' },
{ id: 'tts.type', type: 'string' },
{ id: 'tts.host', type: 'string' },
{ id: 'tts.port', type: 'number' },
{ id: 'tts.token', type: 'string' },
{ id: 'tts.voice', type: 'string' },
{ id: 'tts.speakerId', type: 'number' },
{ id: 'tts.model', type: 'string' },
{ id: 'tts.speed', type: 'number' },
{ id: 'tts.instructions', type: 'string' },
{ id: 'tts.volume', type: 'number' },
{ id: 'ai.token', type: 'string' },
{ id: 'ai.context', type: 'string' },
]
const settingsForm = document.getElementById('settings-form')
const bleConnectButton = document.getElementById('ble-connect-button')
const submitButton = document.getElementById('submit-button')
const toast = document.getElementById('toast')
toast.addEventListener('animationend', () => {
toast.className = 'toast'
})
const showToast = (message, type = 'success') => {
toast.innerText = message
toast.className = `toast ${type} visible`
}
const updateView = () => {
const connected = client.isConnected()
settingsForm.classList.toggle('active', connected)
bleConnectButton.classList.toggle('active', !connected)
submitButton.disabled = !connected
}
const setVisible = (selector, visible) => {
document.querySelectorAll(selector).forEach((element) => {
element.classList.toggle('hidden', !visible)
})
}
const updateConditionalFields = () => {
const ttsType = document.getElementById('tts.type').value
const driverType = document.getElementById('driver.type').value
const needsHostPort = ttsType === 'remote' || ttsType === 'voicevox'
const needsToken = ttsType === 'elevenlabs' || ttsType === 'openai' || ttsType === 'voicevox-web'
const needsVoice = ttsType === 'elevenlabs' || ttsType === 'openai'
const needsSpeakerId = ttsType === 'voicevox' || ttsType === 'voicevox-web'
const needsModel = ttsType === 'elevenlabs' || ttsType === 'openai'
const needsOpenAIOnly = ttsType === 'openai'
setVisible('[data-tts="host-port"]', needsHostPort)
setVisible('[data-tts="token"]', needsToken)
setVisible('[data-tts="voice"]', needsVoice)
setVisible('[data-tts="speaker-id"]', needsSpeakerId)
setVisible('[data-tts="model"]', needsModel)
setVisible('[data-tts="openai-only"]', needsOpenAIOnly)
const isSerialDriver = driverType === 'scservo' || driverType === 'dynamixel' || driverType === 'rs30x'
setVisible('[data-driver="ids"]', isSerialDriver)
setVisible('[data-driver="pwm"]', driverType === 'pwm')
setVisible('[data-driver="baud"]', driverType === 'dynamixel')
setVisible('[data-driver="offset"]', driverType !== 'none')
}
const collectPayload = () => {
const payload = {}
for (const field of FIELD_DEFINITIONS) {
const element = document.getElementById(field.id)
if (element == null) {
continue
}
const raw = element.value.trim()
if (raw.length === 0) {
continue
}
if (field.type === 'number') {
const parsed = Number(raw)
if (!Number.isFinite(parsed)) {
throw new Error(`Invalid number: ${field.id}`)
}
payload[field.id] = parsed
continue
}
payload[field.id] = raw
}
return payload
}
const handleConnectButtonClick = async () => {
try {
await client.connect()
updateView()
showToast('Connected to Stack-chan [・_・]')
} catch (error) {
console.error(error)
showToast(`Connect failed: ${String(error)}`, 'error')
}
}
const handleDisconnectButtonClick = async () => {
await client.disconnect()
updateView()
showToast('Disconnected', 'info')
}
const handlePreferenceSubmit = async (event) => {
event.preventDefault()
if (!client.isConnected()) {
showToast('Not connected', 'error')
return
}
try {
const payload = collectPayload()
if (Object.keys(payload).length === 0) {
showToast('No values to submit', 'info')
return
}
submitButton.disabled = true
await client.send({ _batch: payload })
showToast('Preference set [・_・]')
} catch (error) {
console.error(error)
showToast(`Submit failed: ${String(error)}`, 'error')
} finally {
submitButton.disabled = false
}
}
const handleCharacteristicWritten = ({ prop, value }) => {
const elem = document.getElementById(prop) ?? document.querySelector(`[name="${prop}"]`)
if (elem != null) {
elem.value = value
if (prop === 'tts.type' || prop === 'driver.type') {
updateConditionalFields()
}
}
}
const client = new SimpleBLEClient({
deviceName: 'STK',
onCharacteristicValueChanged: handleCharacteristicWritten
})
client.onDisconnected = () => {
updateView()
showToast('Disconnected', 'info')
}
document.getElementById('ble-connect-button').addEventListener('click', handleConnectButtonClick)
document.getElementById('ble-disconnect-button').addEventListener('click', handleDisconnectButtonClick)
document.getElementById('form-preference').addEventListener('submit', handlePreferenceSubmit)
document.getElementById('tts.type').addEventListener('change', updateConditionalFields)
document.getElementById('driver.type').addEventListener('change', updateConditionalFields)
updateView()
updateConditionalFields()
})
</script>
<title>Stack-chan Preference Editor</title>
</head>
<body>
<div class="app">
<div>
<button class="accordion active" id="ble-connect-button">
Connect Stack-chan with BLE [・_・]
</button>
</div>
<div class="accordion card" id="settings-form">
<h2>Stack-chan Preferences</h2>
<p class="description">
Only non-empty values are sent. Leave a field blank if you do not want to update it.
</p>
<div class="ble-disconnect-button" id="ble-disconnect-button" title="Disconnect">×</div>
<form class="form" id="form-preference">
<section class="form-section">
<h3>Wi-Fi</h3>
<div class="form-group">
<label for="wifi.ssid">SSID</label>
<input type="text" name="wifi.ssid" id="wifi.ssid" autocomplete="off" placeholder="MyWiFi">
</div>
<div class="form-group">
<label for="wifi.password">Password</label>
<input type="password" name="wifi.password" id="wifi.password" autocomplete="off" placeholder="********">
</div>
</section>
<section class="form-section">
<h3>Renderer</h3>
<div class="form-group">
<label for="renderer.type">Type</label>
<select name="renderer.type" id="renderer.type">
<option value="simple">Simple</option>
<option value="dog">Dog</option>
<option value="small-face">Small Face (M5AtomS3)</option>
</select>
</div>
</section>
<section class="form-section">
<h3>Driver</h3>
<div class="form-group">
<label for="driver.type">Type</label>
<select name="driver.type" id="driver.type">
<option value="scservo">SCServo</option>
<option value="dynamixel">Dynamixel (Protocol v2)</option>
<option value="rs30x">RS30X</option>
<option value="pwm">PWM (SG-90)</option>
<option value="none">None</option>
</select>
</div>
<div class="form-group" data-driver="ids">
<label for="driver.panId">Pan Servo ID</label>
<input type="number" name="driver.panId" id="driver.panId" min="1" max="254" placeholder="1">
</div>
<div class="form-group" data-driver="ids">
<label for="driver.tiltId">Tilt Servo ID</label>
<input type="number" name="driver.tiltId" id="driver.tiltId" min="1" max="254" placeholder="2">
</div>
<div class="form-group" data-driver="baud">
<label for="driver.baud">Baud Rate</label>
<input type="number" name="driver.baud" id="driver.baud" placeholder="1000000">
</div>
<div class="form-group" data-driver="pwm">
<label for="driver.pwmPan">PWM Pin (Pan)</label>
<input type="number" name="driver.pwmPan" id="driver.pwmPan" placeholder="5">
</div>
<div class="form-group" data-driver="pwm">
<label for="driver.pwmTilt">PWM Pin (Tilt)</label>
<input type="number" name="driver.pwmTilt" id="driver.pwmTilt" placeholder="2">
</div>
<div class="form-group" data-driver="offset">
<label for="driver.offsetPan">Offset Pan</label>
<input type="number" name="driver.offsetPan" id="driver.offsetPan" placeholder="0">
</div>
<div class="form-group" data-driver="offset">
<label for="driver.offsetTilt">Offset Tilt</label>
<input type="number" name="driver.offsetTilt" id="driver.offsetTilt" placeholder="0">
</div>
</section>
<section class="form-section">
<h3>TTS</h3>
<div class="form-group">
<label for="tts.type">Type</label>
<select name="tts.type" id="tts.type">
<option value="local">Local</option>
<option value="remote">Remote WAV</option>
<option value="voicevox">VOICEVOX</option>
<option value="voicevox-web">VOICEVOX Web</option>
<option value="elevenlabs">ElevenLabs</option>
<option value="openai">OpenAI</option>
</select>
</div>
<div class="form-group" data-tts="host-port">
<label for="tts.host">Host</label>
<input type="text" name="tts.host" id="tts.host" placeholder="my-tts-host.local">
</div>
<div class="form-group" data-tts="host-port">
<label for="tts.port">Port</label>
<input type="number" name="tts.port" id="tts.port" placeholder="50021">
</div>
<div class="form-group" data-tts="token">
<label for="tts.token">API Token</label>
<input type="text" name="tts.token" id="tts.token" autocomplete="off">
</div>
<div class="form-group" data-tts="voice">
<label for="tts.voice">Voice</label>
<input type="text" name="tts.voice" id="tts.voice" placeholder="alloy / AZnzlk...">
</div>
<div class="form-group" data-tts="speaker-id">
<label for="tts.speakerId">Speaker ID</label>
<input type="number" name="tts.speakerId" id="tts.speakerId" placeholder="1">
</div>
<div class="form-group" data-tts="model">
<label for="tts.model">Model</label>
<input type="text" name="tts.model" id="tts.model" placeholder="tts-1 / eleven_monolingual_v1">
</div>
<div class="form-group" data-tts="openai-only">
<label for="tts.speed">Speed</label>
<input type="number" name="tts.speed" id="tts.speed" step="0.1" min="0.25" max="4" placeholder="1.0">
</div>
<div class="form-group" data-tts="openai-only">
<label for="tts.instructions">Instructions</label>
<textarea name="tts.instructions" id="tts.instructions" rows="3"
placeholder="Speak in a cheerful tone."></textarea>
</div>
<div class="form-group">
<label for="tts.volume">Volume</label>
<input type="number" name="tts.volume" id="tts.volume" step="0.1" min="0" max="1" placeholder="0.5">
</div>
</section>
<section class="form-section">
<h3>AI (for chat mods)</h3>
<div class="form-group">
<label for="ai.token">API Token</label>
<input type="text" name="ai.token" id="ai.token" autocomplete="off">
</div>
<div class="form-group">
<label for="ai.context">System Prompt</label>
<textarea name="ai.context" id="ai.context" rows="4"
placeholder="You are Stack-chan, the palm-sized super kawaii companion robot."></textarea>
</div>
</section>
<hr>
<button type="submit" id="submit-button">Save Preferences</button>
</form>
</div>
</div>
<div id="toast" class="toast"></div>
</body>
</html>
d9ca604d5This file is pinned to the selected release. Download the original to open it in your design tool.