new features: Mute, and different sounds

This commit is contained in:
Jakob Gegeniger committed 2026-09-09 22:42:06 +02:00
1 parent 81ddf048ca
commit cbd7d02ad8
6 files changed
+505 -47

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
serve:
npm run dev
+6
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue' import Home from '../views/Home.vue'
import Maintenance from '../views/Maintenance.vue' import Maintenance from '../views/Maintenance.vue'
import Setup from '../views/Setup.vue'
import Players from '../views/Players.vue' import Players from '../views/Players.vue'
import Start from '../views/Start.vue' import Start from '../views/Start.vue'
import Text from '../views/Text.vue' import Text from '../views/Text.vue'
@@ -28,6 +29,11 @@ const routes: any[] = [
name: 'TextDirect', name: 'TextDirect',
component: Text component: Text
}, },
{
path: '/setup',
name: 'Setup',
component: Setup
},
{ {
path: '/maintenance', path: '/maintenance',
name: 'Maintenance', name: 'Maintenance',
+51 -19
View File
@@ -30,7 +30,7 @@ export function getMqttClient(): any {
export function publishStartHit(): void { export function publishStartHit(): void {
const tempClient = getClient() const tempClient = getClient()
const mqttTopic = mqttTopicRoot + 'run' const mqttTopic = mqttTopicRoot + 'set'
tempClient.on('connect', () => { tempClient.on('connect', () => {
tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => { tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => {
@@ -71,28 +71,28 @@ export function subscribeToIntTopic(callback: (response: IntTopicResponse) => vo
} }
interface IntTopicResponse { interface IntTopicResponse {
version: string
mode: number mode: number
sE: boolean sE: boolean
sS: boolean sS: boolean
sP: boolean sP: boolean
lE: boolean lE: boolean
snd: boolean
vol: number
delay: number
} }
export function parseIntMessage(message: string): IntTopicResponse { export function parseIntMessage(message: string): IntTopicResponse {
const result: IntTopicResponse = { const result: IntTopicResponse = {
version: '',
mode: 0, mode: 0,
sE: false, // EndPoint sE: false, // EndPoint
sS: false, // StartPoint sS: false, // StartPoint
sP: false, // PullSensor sP: false, // PullSensor
lE: false // LatchEnable lE: false, // LatchEnable
snd: false, // LatchEnable
vol: 0, // Volumen
delay: 0, // Delay
} }
const versionMatch = message.match(/T([\d.]+)/)
if (versionMatch) {
result.version = versionMatch[1]
}
const modeMatch = message.match(/Mode=(\d+)/) const modeMatch = message.match(/Mode=(\d+)/)
if (modeMatch) { if (modeMatch) {
result.mode = parseInt(modeMatch[1], 10) result.mode = parseInt(modeMatch[1], 10)
@@ -113,19 +113,33 @@ export function parseIntMessage(message: string): IntTopicResponse {
if (lEMatch) { if (lEMatch) {
result.lE = lEMatch[1] === 'true' result.lE = lEMatch[1] === 'true'
} }
const soundMute = message.match(/snd:(true|false)/)
if (soundMute) {
result.snd = soundMute[1] === 'true'
}
const volMatch = message.match(/v:(\d+)/)
if (volMatch) {
result.vol = Number(volMatch[1])
}
const delayMatch = message.match(/pD:(\d+)/)
if (delayMatch) {
result.delay = Number(delayMatch[1])
}
return result return result
} }
export function enterMaintananceMode( export function enterMaintananceMode(
callback: (sE: number, sS: number, sP: number, lE: number) => void) statCallback: (snd:number, sE: number, sS: number, sP: number, lE: number, vol: number, delay: number) => void,
verCallback: (ver: string) => void)
: mqtt.MqttClient { : mqtt.MqttClient {
const client = getClient() const client = getClient()
const mqttTopic = mqttTopicRoot + 'stat' const statMqttTopic = mqttTopicRoot + 'stat'
const verMqttTopic = mqttTopicRoot + 'ver'
client.on('connect', () => { client.on('connect', () => {
publicMaintananceCmd(client, "SetMaintanance") publishCmd(client, "SetMaintanance")
client.subscribe(mqttTopic, { qos: 1 }, (error) => { client.subscribe(statMqttTopic, { qos: 1 }, (error) => {
if (error) { if (error) {
console.error('MQTT subscribe failed:', error) console.error('MQTT subscribe failed:', error)
} }
@@ -137,14 +151,22 @@ export function enterMaintananceMode(
}) })
client.on('message', (topic: string, message: Buffer) => { client.on('message', (topic: string, message: Buffer) => {
if (topic === mqttTopic) { console.log(`MQTT: Got ${topic}: ${message}`)
if (topic === statMqttTopic) {
const messageStr = message.toString() const messageStr = message.toString()
const status = parseIntMessage(message.toString()) const status = parseIntMessage(messageStr)
const snd = status.snd ? 1 : 0
const sE = status.sE ? 1 : 0 const sE = status.sE ? 1 : 0
const sS = status.sS ? 1 : 0 const sS = status.sS ? 1 : 0
const sP = status.sP ? 1 : 0 const sP = status.sP ? 1 : 0
const lE = status.lE ? 1 : 0 const lE = status.lE ? 1 : 0
callback(sE, sS, sP, lE) const vol = status.vol
const delay = status.delay
statCallback(snd, sE, sS, sP, lE, vol, delay)
}
if (topic === verMqttTopic) {
const messageStr = message.toString()
verCallback(messageStr)
} }
}) })
@@ -152,7 +174,7 @@ export function enterMaintananceMode(
} }
export function exitMaintananceMode(client: mqtt.MqttClient): void { export function exitMaintananceMode(client: mqtt.MqttClient): void {
publicMaintananceCmd(client, "Exit") publishCmd(client, "Exit")
const mqttTopic = mqttTopicRoot + 'stat' const mqttTopic = mqttTopicRoot + 'stat'
client.unsubscribe(mqttTopic, (error) => { client.unsubscribe(mqttTopic, (error) => {
if (error) { if (error) {
@@ -162,10 +184,20 @@ export function exitMaintananceMode(client: mqtt.MqttClient): void {
client.end() client.end()
} }
export function publicMaintananceCmd(client: mqtt.MqttClient, message : string): void { export function publishCmd(client: mqtt.MqttClient, message: string): void {
const mqttTopic = mqttTopicRoot + 'run' const mqttTopic = mqttTopicRoot + 'cfg'
console.log(`MQTT: Publish ${mqttTopic}: ${message}`) console.log(`MQTT: Publish ${mqttTopic}: ${message}`)
client.publish(mqttTopic, message, { qos: 1 }, (error) => { client.publish(mqttTopic, message, { qos: 2 }, (error) => {
if (error) {
console.error('MQTT publish failed:', error)
}
})
}
export function publishQueryVersionCmd(client: mqtt.MqttClient) {
const mqttTopic = mqttTopicRoot + 'ver'
console.log(`MQTT: Publish ${mqttTopic}`)
client.publish(mqttTopic, "", { qos: 1 }, (error) => {
if (error) { if (error) {
console.error('MQTT publish failed:', error) console.error('MQTT publish failed:', error)
} }
+45 -5
View File
@@ -7,19 +7,34 @@
<div class="info-section"> <div class="info-section">
<div class="info-box"> <div class="info-box">
<h3>Version</h3> <h3>Version</h3>
<p id="version-number">1.0.0</p> <p id="version-number">0.8.0</p>
</div> </div>
<div class="info-box"> <div class="info-box">
<h3>Service Hotline</h3> <h3>Service Hotline</h3>
<p id="service-hotline">+1 800-123-4567</p> <p id="service-hotline">+49 176 64036128</p>
</div> </div>
</div> </div>
<nav class="home-nav"> <div class="status-container">
<span class="status-label">Sword</span>
<div class="status-frame setup-frame">
<router-link to="/setup" class="nav-button">Setup</router-link>
<router-link to="/maintenance" class="nav-button">Maintenance</router-link>
</div>
</div>
<div class="status-container">
<span class="status-label">Throne</span>
<div class="status-frame setup-frame">
</div>
</div>
<div class="status-container">
<span class="status-label">Game</span>
<div class="status-frame setup-frame">
<router-link to="/players" class="nav-button">Players</router-link> <router-link to="/players" class="nav-button">Players</router-link>
<router-link to="/start" class="nav-button">Start</router-link> <router-link to="/start" class="nav-button">Start</router-link>
<router-link to="/text" class="nav-button">Text</router-link> <router-link to="/text" class="nav-button">Text</router-link>
<router-link to="/maintenance" class="nav-button">Maintenance</router-link> </div>
</nav> </div>
</div> </div>
</section> </section>
</main> </main>
@@ -27,3 +42,28 @@
<script setup lang="ts"> <script setup lang="ts">
</script> </script>
<style scoped>
.status-container {
margin-bottom: 20px;
}
.status-frame {
border: 2px solid #d1d5db;
border-radius: 8px;
padding: 16px;
background: #f9fafb;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.status-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.8;
}
</style>
+142 -23
View File
@@ -4,16 +4,17 @@
<h2>Sword Status</h2> <h2>Sword Status</h2>
<div class="status-container"> <div class="status-container">
<h3>Inputs</h3> <h3>Inputs</h3>
<div class="status-frame"> <div class="status-frame">
<div class="status-item" :class="{ active: endSensStatus === 1 }">
<span class="status-label">EndSens:</span>
<span class="status-value">{{ endSensStatus }}</span>
</div>
<div class="status-item" :class="{ active: startSensStatus === 1 }"> <div class="status-item" :class="{ active: startSensStatus === 1 }">
<span class="status-label">StartSens:</span> <span class="status-label">StartSens:</span>
<span class="status-value">{{ startSensStatus }}</span> <span class="status-value">{{ startSensStatus }}</span>
</div> </div>
<div class="status-item" :class="{ active: endSensStatus === 1 }">
<span class="status-label">EndSens:</span>
<span class="status-value">{{ endSensStatus }}</span>
</div>
<div class="status-item" :class="{ active: pullSensStatus === 1 }"> <div class="status-item" :class="{ active: pullSensStatus === 1 }">
<span class="status-label">PullSens:</span> <span class="status-label">PullSens:</span>
<span class="status-value">{{ pullSensStatus }}</span> <span class="status-value">{{ pullSensStatus }}</span>
@@ -29,14 +30,61 @@
<div class="control-buttons"> <div class="control-buttons">
<button @click="handleLock" class="btn-lock">Lock</button> <button @click="handleLock" class="btn-lock">Lock</button>
<button @click="handleUnlock" class="btn-unlock">Unlock</button> <button @click="handleUnlock" class="btn-unlock">Unlock</button>
</div>
</div>
<h2>Sound</h2>
<div class="status-frame">
<div class="status-item" :class="{ active: soundStatus === 1 }">
<span class="status-label">Sound Active:</span>
<span class="status-value">{{ soundStatus }}</span>
</div>
</div>
<div class="status-frame">
<div class="control-buttons">
<button @click="handleMute" class="btn-lock">Mute</button>
<button @click="handleUnMute" class="btn-unlock">Un-Mute</button>
</div>
</div>
<h3>Sounds</h3>
<div class="status-frame">
<div class="control-buttons">
<button @click="handlePlayX(1)" class="btn-unlock">Play sound 1</button> <button @click="handlePlayX(1)" class="btn-unlock">Play sound 1</button>
<button @click="handlePlayX(2)" class="btn-unlock">Play sound 2</button> <button @click="handlePlayX(2)" class="btn-unlock">Play sound 2</button>
<button @click="handlePlayX(3)" class="btn-unlock">Play sound 3</button> <button @click="handlePlayX(3)" class="btn-unlock">Play sound 3</button>
</div> </div>
</div> </div>
<h3>Status</h3> <h2>Setup</h2>
<div class="status-container">
<div class="status-frame setup-frame">
<div class="slider-item">
<div class="slider-header">
<span class="slider-label">Volume</span>
<span class="slider-value">{{ volume }}</span>
</div>
<input class="slider-input" type="range" min="0" max="100" step="1" v-model.number="volume"
@input="handleVolume" />
</div>
<div class="slider-item">
<div class="slider-header">
<span class="slider-label">Promo Delay</span>
<span class="slider-value">{{ promoDelay }}</span>
</div>
<input class="slider-input" type="range" min="30" max="300" step="1" v-model.number="promoDelay"
@input="handlePromoDelay" />
</div>
</div>
</div>
<h3>Info</h3>
<div class="status-container">
<div class="status-frame">
<span class="status-label">Version:</span>
<span class="status-value">{{ version }}</span>
</div>
</div>
</div> </div>
<h2>Game Views</h2> <h2>Game Views</h2>
@@ -55,61 +103,97 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { enterMaintananceMode, exitMaintananceMode, publicMaintananceCmd } from '../services/mqtt' import { enterMaintananceMode, exitMaintananceMode, publishCmd, publishQueryVersionCmd } from '../services/mqtt'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import type mqtt from 'mqtt' import type mqtt from 'mqtt'
const route = useRoute() const route = useRoute()
const version = ref<string>()
const endSensStatus = ref<number>(0) const endSensStatus = ref<number>(0)
const startSensStatus = ref<number>(0) const startSensStatus = ref<number>(0)
const pullSensStatus = ref<number>(0) const pullSensStatus = ref<number>(0)
const lockStatus = ref<number>(0) const lockStatus = ref<number>(0)
const soundStatus = ref<number>(0)
const volume = ref<number>(50)
const promoDelay = ref<number>(180)
let statTopicClient: mqtt.MqttClient | null = null let mqttClient: mqtt.MqttClient | null = null
function isActive(path: string): boolean { function isActive(path: string): boolean {
return route.path === path return route.path === path
} }
function cleanup(): void { function cleanup(): void {
if (statTopicClient) { if (mqttClient) {
exitMaintananceMode(statTopicClient) exitMaintananceMode(mqttClient)
} }
} }
function handleLock(): void { function handleLock(): void {
if (statTopicClient) { if (mqttClient) {
publicMaintananceCmd(statTopicClient, "ServiceClose") publishCmd(mqttClient, "closeLock")
} }
} }
function handleUnlock(): void { function handleUnlock(): void {
if (statTopicClient) { if (mqttClient) {
publicMaintananceCmd(statTopicClient, "ServiceOpen") publishCmd(mqttClient, "openLock")
}
}
function handleMute(): void {
if (mqttClient) {
publishCmd(mqttClient, "mute")
}
}
function handleUnMute(): void {
if (mqttClient) {
publishCmd(mqttClient, "unmute")
}
}
function handleVolume(): void {
if (mqttClient) {
publishCmd(mqttClient, `volume:${volume.value}`)
}
}
function handlePromoDelay(): void {
if (mqttClient) {
publishCmd(mqttClient, `promoDelay:${promoDelay.value}`)
} }
} }
function handlePlayX(sound: number): void { function handlePlayX(sound: number): void {
if (statTopicClient) { if (mqttClient) {
if (sound === 1) { if (sound === 1) {
publicMaintananceCmd(statTopicClient, "ServicePlay1") publishCmd(mqttClient, "playSound:Promo")
} else if (sound === 2) { } else if (sound === 2) {
publicMaintananceCmd(statTopicClient, "ServicePlay2") publishCmd(mqttClient, "playSound:Deny")
} else if (sound === 3) { } else if (sound === 3) {
publicMaintananceCmd(statTopicClient, "ServicePlay3") publishCmd(mqttClient, "playSound:Chosen")
} }
} }
} }
onMounted(() => { onMounted(() => {
statTopicClient = enterMaintananceMode((endSens, startSens, pullSens, lock) => { mqttClient = enterMaintananceMode(
endSensStatus.value = endSens (sound, endSens, startSens, pullSens, lock, vol, delay) => {
startSensStatus.value = startSens endSensStatus.value = endSens
pullSensStatus.value = pullSens startSensStatus.value = startSens
lockStatus.value = lock pullSensStatus.value = pullSens
}) lockStatus.value = lock
soundStatus.value = sound
volume.value = vol
promoDelay.value = delay
},
(ver) => {
version.value = ver
}
)
window.addEventListener('beforeunload', cleanup) window.addEventListener('beforeunload', cleanup)
}) })
@@ -135,6 +219,41 @@ onUnmounted(() => {
gap: 12px; gap: 12px;
} }
.setup-frame {
grid-template-columns: 1fr;
gap: 20px;
}
.slider-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.slider-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.slider-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
}
.slider-value {
font-size: 18px;
font-weight: 600;
color: #111827;
}
.slider-input {
width: 100%;
cursor: pointer;
}
.status-item { .status-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+259
View File
@@ -0,0 +1,259 @@
<template>
<main class="app">
<h1>Setup View</h1>
<h2>Setup</h2>
<div class="status-container">
<div class="status-frame setup-frame">
<div class="slider-item">
<div class="slider-header">
<span class="slider-label">Volume</span>
<span class="slider-value">{{ volume }}</span>
</div>
<input class="slider-input" type="range" min="0" max="100" step="1" v-model.number="volume"
@input="handleVolume" />
</div>
<div class="slider-item">
<div class="slider-header">
<span class="slider-label">Promo Delay</span>
<span class="slider-value">{{ promoDelay }}</span>
</div>
<input class="slider-input" type="range" min="30" max="300" step="1" v-model.number="promoDelay"
@input="handlePromoDelay" />
</div>
</div>
</div>
<h2>Sound</h2>
<div class="status-frame">
<div class="status-item" :class="{ active: soundStatus === 1 }">
<span class="status-label">Sound Active:</span>
<span class="status-value">{{ soundStatus }}</span>
</div>
</div>
<div class="status-frame">
<div class="control-buttons">
<button @click="handleMute" class="btn-lock">Mute</button>
<button @click="handleUnMute" class="btn-unlock">Un-Mute</button>
</div>
</div>
<h2>Sword Status</h2>
<div class="status-container">
<div class="status-frame">
<div class="status-item" :class="{ active: endSensStatus === 1 }">
<span class="status-label">Sword Present:</span>
<span class="status-value">{{ endSensStatus }}</span>
</div>
<div class="status-item" :class="{ active: lockStatus === 1 }">
<span class="status-label">Lock Active:</span>
<span class="status-value">{{ lockStatus }}</span>
</div>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import { enterMaintananceMode, exitMaintananceMode, publishCmd } from '../services/mqtt'
import { useRoute } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue'
import type mqtt from 'mqtt'
const route = useRoute()
const version = ref<string>()
const endSensStatus = ref<number>(0)
const startSensStatus = ref<number>(0)
const pullSensStatus = ref<number>(0)
const lockStatus = ref<number>(0)
const soundStatus = ref<number>(0)
const volume = ref<number>(50)
const promoDelay = ref<number>(180)
let mqttClient: mqtt.MqttClient | null = null
function cleanup(): void {
if (mqttClient) {
exitMaintananceMode(mqttClient)
}
}
function handleMute(): void {
if (mqttClient) {
publishCmd(mqttClient, "mute")
}
}
function handleUnMute(): void {
if (mqttClient) {
publishCmd(mqttClient, "unmute")
}
}
function handleVolume(): void {
if (mqttClient) {
publishCmd(mqttClient, `volume:${volume.value}`)
}
}
function handlePromoDelay(): void {
if (mqttClient) {
publishCmd(mqttClient, `promoDelay:${promoDelay.value}`)
}
}
function handlePlayX(sound: number): void {
if (mqttClient) {
if (sound === 1) {
publishCmd(mqttClient, "playSound:Promo")
} else if (sound === 2) {
publishCmd(mqttClient, "playSound:Deny")
} else if (sound === 3) {
publishCmd(mqttClient, "playSound:Chosen")
}
}
}
onMounted(() => {
mqttClient = enterMaintananceMode(
(sound, endSens, startSens, pullSens, lock, vol, delay) => {
endSensStatus.value = endSens
startSensStatus.value = startSens
pullSensStatus.value = pullSens
lockStatus.value = lock
soundStatus.value = sound
volume.value = vol
promoDelay.value = delay
},
(ver) => {})
window.addEventListener('beforeunload', cleanup)
if (mqttClient) {
publishCmd(mqttClient, `promoDelay:${promoDelay.value}`)
}
})
onUnmounted(() => {
window.removeEventListener('beforeunload', cleanup)
cleanup()
})
</script>
<style scoped>
.status-container {
margin-bottom: 20px;
}
.status-frame {
border: 2px solid #d1d5db;
border-radius: 8px;
padding: 16px;
background: #f9fafb;
margin-bottom: 16px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.setup-frame {
grid-template-columns: 1fr;
gap: 20px;
}
.slider-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.slider-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.slider-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
}
.slider-value {
font-size: 18px;
font-weight: 600;
color: #111827;
}
.slider-input {
width: 100%;
cursor: pointer;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12px;
border-radius: 6px;
background: #e5e7eb;
color: #6b7280;
transition: background 0.3s, color 0.3s;
font-weight: 500;
}
.status-item.active {
background: #10b981;
color: #ffffff;
}
.status-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.8;
}
.status-value {
font-size: 20px;
font-weight: 600;
margin-top: 4px;
}
.control-buttons {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.btn-lock,
.btn-unlock {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.btn-lock {
background: #111827;
color: #ffffff;
}
.btn-lock:hover {
background: #1f2937;
}
.btn-unlock {
background: #e5e7eb;
color: #111827;
border: 1px solid #d1d5db;
}
.btn-unlock:hover {
background: #d1d5db;
}
</style>