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 Home from '../views/Home.vue'
import Maintenance from '../views/Maintenance.vue'
import Setup from '../views/Setup.vue'
import Players from '../views/Players.vue'
import Start from '../views/Start.vue'
import Text from '../views/Text.vue'
@@ -28,6 +29,11 @@ const routes: any[] = [
name: 'TextDirect',
component: Text
},
{
path: '/setup',
name: 'Setup',
component: Setup
},
{
path: '/maintenance',
name: 'Maintenance',
+51 -19
View File
@@ -30,7 +30,7 @@ export function getMqttClient(): any {
export function publishStartHit(): void {
const tempClient = getClient()
const mqttTopic = mqttTopicRoot + 'run'
const mqttTopic = mqttTopicRoot + 'set'
tempClient.on('connect', () => {
tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => {
@@ -71,28 +71,28 @@ export function subscribeToIntTopic(callback: (response: IntTopicResponse) => vo
}
interface IntTopicResponse {
version: string
mode: number
sE: boolean
sS: boolean
sP: boolean
lE: boolean
snd: boolean
vol: number
delay: number
}
export function parseIntMessage(message: string): IntTopicResponse {
const result: IntTopicResponse = {
version: '',
mode: 0,
sE: false, // EndPoint
sS: false, // StartPoint
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+)/)
if (modeMatch) {
result.mode = parseInt(modeMatch[1], 10)
@@ -113,19 +113,33 @@ export function parseIntMessage(message: string): IntTopicResponse {
if (lEMatch) {
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
}
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 {
const client = getClient()
const mqttTopic = mqttTopicRoot + 'stat'
const statMqttTopic = mqttTopicRoot + 'stat'
const verMqttTopic = mqttTopicRoot + 'ver'
client.on('connect', () => {
publicMaintananceCmd(client, "SetMaintanance")
client.subscribe(mqttTopic, { qos: 1 }, (error) => {
publishCmd(client, "SetMaintanance")
client.subscribe(statMqttTopic, { qos: 1 }, (error) => {
if (error) {
console.error('MQTT subscribe failed:', error)
}
@@ -137,14 +151,22 @@ export function enterMaintananceMode(
})
client.on('message', (topic: string, message: Buffer) => {
if (topic === mqttTopic) {
console.log(`MQTT: Got ${topic}: ${message}`)
if (topic === statMqttTopic) {
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 sS = status.sS ? 1 : 0
const sP = status.sP ? 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 {
publicMaintananceCmd(client, "Exit")
publishCmd(client, "Exit")
const mqttTopic = mqttTopicRoot + 'stat'
client.unsubscribe(mqttTopic, (error) => {
if (error) {
@@ -162,10 +184,20 @@ export function exitMaintananceMode(client: mqtt.MqttClient): void {
client.end()
}
export function publicMaintananceCmd(client: mqtt.MqttClient, message : string): void {
const mqttTopic = mqttTopicRoot + 'run'
export function publishCmd(client: mqtt.MqttClient, message: string): void {
const mqttTopic = mqttTopicRoot + 'cfg'
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) {
console.error('MQTT publish failed:', error)
}
+45 -5
View File
@@ -7,19 +7,34 @@
<div class="info-section">
<div class="info-box">
<h3>Version</h3>
<p id="version-number">1.0.0</p>
<p id="version-number">0.8.0</p>
</div>
<div class="info-box">
<h3>Service Hotline</h3>
<p id="service-hotline">+1 800-123-4567</p>
<p id="service-hotline">+49 176 64036128</p>
</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="/start" class="nav-button">Start</router-link>
<router-link to="/text" class="nav-button">Text</router-link>
<router-link to="/maintenance" class="nav-button">Maintenance</router-link>
</nav>
</div>
</div>
</div>
</section>
</main>
@@ -27,3 +42,28 @@
<script setup lang="ts">
</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>
<div class="status-container">
<h3>Inputs</h3>
<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 }">
<span class="status-label">StartSens:</span>
<span class="status-value">{{ startSensStatus }}</span>
</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 }">
<span class="status-label">PullSens:</span>
<span class="status-value">{{ pullSensStatus }}</span>
@@ -29,14 +30,61 @@
<div class="control-buttons">
<button @click="handleLock" class="btn-lock">Lock</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(2)" class="btn-unlock">Play sound 2</button>
<button @click="handlePlayX(3)" class="btn-unlock">Play sound 3</button>
</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>
<h2>Game Views</h2>
@@ -55,61 +103,97 @@
</template>
<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 { 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 statTopicClient: mqtt.MqttClient | null = null
let mqttClient: mqtt.MqttClient | null = null
function isActive(path: string): boolean {
return route.path === path
}
function cleanup(): void {
if (statTopicClient) {
exitMaintananceMode(statTopicClient)
if (mqttClient) {
exitMaintananceMode(mqttClient)
}
}
function handleLock(): void {
if (statTopicClient) {
publicMaintananceCmd(statTopicClient, "ServiceClose")
if (mqttClient) {
publishCmd(mqttClient, "closeLock")
}
}
function handleUnlock(): void {
if (statTopicClient) {
publicMaintananceCmd(statTopicClient, "ServiceOpen")
if (mqttClient) {
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 {
if (statTopicClient) {
if (mqttClient) {
if (sound === 1) {
publicMaintananceCmd(statTopicClient, "ServicePlay1")
publishCmd(mqttClient, "playSound:Promo")
} else if (sound === 2) {
publicMaintananceCmd(statTopicClient, "ServicePlay2")
publishCmd(mqttClient, "playSound:Deny")
} else if (sound === 3) {
publicMaintananceCmd(statTopicClient, "ServicePlay3")
publishCmd(mqttClient, "playSound:Chosen")
}
}
}
onMounted(() => {
statTopicClient = enterMaintananceMode((endSens, startSens, pullSens, lock) => {
endSensStatus.value = endSens
startSensStatus.value = startSens
pullSensStatus.value = pullSens
lockStatus.value = lock
})
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) => {
version.value = ver
}
)
window.addEventListener('beforeunload', cleanup)
})
@@ -135,6 +219,41 @@ onUnmounted(() => {
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;
+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>