step implementing maintanance logic
This commit is contained in:
+1
-2
@@ -5,5 +5,4 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
+140
-5
@@ -4,23 +4,30 @@ const mqttBrokerHost = '91.99.29.8'
|
||||
const mqttBrokerPort = 9001
|
||||
const mqttBrokerPath = '/mqtt'
|
||||
const mqttUseTls = false
|
||||
const mqttTopic = 'king-arthur/page/startup'
|
||||
const mqttTopicRoot = 'outdoor/king-arthur/sword/'
|
||||
const mqttMessage = 'start-hit'
|
||||
|
||||
let client: any = null
|
||||
|
||||
function getClient(): mqtt.MqttClient {
|
||||
const protocol = mqttUseTls ? 'wss' : 'ws'
|
||||
const brokerUrl = `${protocol}://${mqttBrokerHost}:${mqttBrokerPort}${mqttBrokerPath}`
|
||||
const tempClient = mqtt.connect(brokerUrl)
|
||||
return tempClient
|
||||
}
|
||||
|
||||
export function getMqttClient(): any {
|
||||
return client
|
||||
}
|
||||
|
||||
export function publishStartHit(): void {
|
||||
const protocol = mqttUseTls ? 'wss' : 'ws'
|
||||
const brokerUrl = `${protocol}://${mqttBrokerHost}:${mqttBrokerPort}${mqttBrokerPath}`
|
||||
const tempClient = mqtt.connect(brokerUrl)
|
||||
const tempClient = getClient()
|
||||
|
||||
const mqttTopic = mqttTopicRoot + 'run'
|
||||
|
||||
tempClient.on('connect', () => {
|
||||
tempClient.publish(mqttTopic, mqttMessage, { qos: 1 }, () => {
|
||||
tempClient.end()
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,3 +36,131 @@ export function publishStartHit(): void {
|
||||
tempClient.end(true)
|
||||
})
|
||||
}
|
||||
|
||||
export function subscribeToIntTopic(callback: (response: IntTopicResponse) => void): mqtt.MqttClient {
|
||||
const tempClient = getClient()
|
||||
const mqttTopic = mqttTopicRoot + 'int'
|
||||
|
||||
tempClient.on('connect', () => {
|
||||
tempClient.subscribe(mqttTopic, { qos: 1 }, (error) => {
|
||||
if (error) {
|
||||
console.error('MQTT subscribe failed:', error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
tempClient.on('message', (topic: string, message: Buffer) => {
|
||||
if (topic === mqttTopic) {
|
||||
const parsedMessage = parseIntMessage(message.toString())
|
||||
callback(parsedMessage)
|
||||
}
|
||||
})
|
||||
|
||||
tempClient.on('error', (error: any) => {
|
||||
console.error('MQTT connection error:', error)
|
||||
})
|
||||
|
||||
return tempClient
|
||||
}
|
||||
|
||||
interface IntTopicResponse {
|
||||
version: string
|
||||
mode: number
|
||||
sE: boolean
|
||||
sS: boolean
|
||||
sP: boolean
|
||||
lE: boolean
|
||||
}
|
||||
|
||||
export function parseIntMessage(message: string): IntTopicResponse {
|
||||
const result: IntTopicResponse = {
|
||||
version: '',
|
||||
mode: 0,
|
||||
sE: false, // EndPoint
|
||||
sS: false, // StartPoint
|
||||
sP: false, // PullSensor
|
||||
lE: false // LatchEnable
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
const sEMatch = message.match(/sE:(true|false)/)
|
||||
if (sEMatch) {
|
||||
result.sE = sEMatch[1] === 'true'
|
||||
}
|
||||
const sSMatch = message.match(/sS:(true|false)/)
|
||||
if (sSMatch) {
|
||||
result.sS = sSMatch[1] === 'true'
|
||||
}
|
||||
const sPMatch = message.match(/sP:(true|false)/)
|
||||
if (sPMatch) {
|
||||
result.sP = sPMatch[1] === 'true'
|
||||
}
|
||||
const lEMatch = message.match(/lE:(true|false)/)
|
||||
if (lEMatch) {
|
||||
result.lE = lEMatch[1] === 'true'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function enterMaintananceMode(
|
||||
callback: (sE: number, sS: number, sP: number, lE: number) => void)
|
||||
: mqtt.MqttClient {
|
||||
const client = getClient()
|
||||
|
||||
const mqttTopic = mqttTopicRoot + 'stat'
|
||||
|
||||
client.on('connect', () => {
|
||||
publicMaintananceCmd(client, "SetMaintanance")
|
||||
client.subscribe(mqttTopic, { qos: 1 }, (error) => {
|
||||
if (error) {
|
||||
console.error('MQTT subscribe failed:', error)
|
||||
}
|
||||
})
|
||||
})
|
||||
client.on('error', (error: any) => {
|
||||
console.error('MQTT operation failed:', error)
|
||||
client.end(true)
|
||||
})
|
||||
|
||||
client.on('message', (topic: string, message: Buffer) => {
|
||||
if (topic === mqttTopic) {
|
||||
const messageStr = message.toString()
|
||||
const status = parseIntMessage(message.toString())
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
export function exitMaintananceMode(client: mqtt.MqttClient): void {
|
||||
publicMaintananceCmd(client, "Exit")
|
||||
const mqttTopic = mqttTopicRoot + 'stat'
|
||||
client.unsubscribe(mqttTopic, (error) => {
|
||||
if (error) {
|
||||
console.error('MQTT unsubscribe failed:', error)
|
||||
}
|
||||
})
|
||||
client.end()
|
||||
}
|
||||
|
||||
export function publicMaintananceCmd(client: mqtt.MqttClient, message : string): void {
|
||||
const mqttTopic = mqttTopicRoot + 'run'
|
||||
console.log(`MQTT: Publish ${mqttTopic}: ${message}`)
|
||||
client.publish(mqttTopic, message, { qos: 1 }, (error) => {
|
||||
if (error) {
|
||||
console.error('MQTT publish failed:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
<main class="app">
|
||||
<nav class="nav">
|
||||
<router-link to="/" class="nav-link">Home</router-link>
|
||||
<router-link to="/players" class="nav-link" :class="{ active: route.path === '/players' }">Page 1: Players</router-link>
|
||||
<router-link to="/start" class="nav-link" :class="{ active: route.path === '/start' }">Page 2: Start</router-link>
|
||||
<router-link to="/text" class="nav-link" :class="{ active: route.path === '/text' }">Page 3: Text</router-link>
|
||||
<router-link to="/players" class="nav-link" :class="{ active: route.path === '/players' }">
|
||||
Page 1: Players</router-link>
|
||||
<router-link to="/start" class="nav-link" :class="{ active: route.path === '/start' }">
|
||||
Page 2: Start</router-link>
|
||||
<router-link to="/text" class="nav-link" :class="{ active: route.path === '/text' }">
|
||||
Page 3: Text</router-link>
|
||||
</nav>
|
||||
<section class="page-content">
|
||||
<Players v-if="route.path === '/players'" />
|
||||
|
||||
+184
-4
@@ -1,10 +1,52 @@
|
||||
<template>
|
||||
<main class="app">
|
||||
<h1>Simple Player Web App</h1>
|
||||
<h1>Maintanance View</h1>
|
||||
|
||||
<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: pullSensStatus === 1 }">
|
||||
<span class="status-label">PullSens:</span>
|
||||
<span class="status-value">{{ pullSensStatus }}</span>
|
||||
</div>
|
||||
<div class="status-item" :class="{ active: lockStatus === 1 }">
|
||||
<span class="status-label">Lock:</span>
|
||||
<span class="status-value">{{ lockStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Outputs</h3>
|
||||
<div class="status-frame">
|
||||
<div class="control-buttons">
|
||||
<button @click="handleLock" class="btn-lock">Lock</button>
|
||||
<button @click="handleUnlock" class="btn-unlock">Unlock</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(3)" class="btn-unlock">Play sound 3</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Status</h3>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Game Views</h2>
|
||||
<nav class="nav">
|
||||
<router-link to="/maintenance/players" class="nav-link" :class="{ active: isActive('/maintenance/players') }">Page 1: Players</router-link>
|
||||
<router-link to="/maintenance/start" class="nav-link" :class="{ active: isActive('/maintenance/start') }">Page 2: Start</router-link>
|
||||
<router-link to="/maintenance/text" class="nav-link" :class="{ active: isActive('/maintenance/text') }">Page 3: Text</router-link>
|
||||
<router-link to="/maintenance/players" class="nav-link" :class="{ active: isActive('/maintenance/players') }">Page
|
||||
1: Players</router-link>
|
||||
<router-link to="/maintenance/start" class="nav-link" :class="{ active: isActive('/maintenance/start') }">Page 2:
|
||||
Start</router-link>
|
||||
<router-link to="/maintenance/text" class="nav-link" :class="{ active: isActive('/maintenance/text') }">Page 3:
|
||||
Text</router-link>
|
||||
</nav>
|
||||
<section class="page-content">
|
||||
<router-view></router-view>
|
||||
@@ -13,11 +55,149 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { enterMaintananceMode, exitMaintananceMode, publicMaintananceCmd } from '../services/mqtt'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type mqtt from 'mqtt'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const endSensStatus = ref<number>(0)
|
||||
const startSensStatus = ref<number>(0)
|
||||
const pullSensStatus = ref<number>(0)
|
||||
const lockStatus = ref<number>(0)
|
||||
|
||||
let statTopicClient: mqtt.MqttClient | null = null
|
||||
|
||||
function isActive(path: string): boolean {
|
||||
return route.path === path
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
if (statTopicClient) {
|
||||
exitMaintananceMode(statTopicClient)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLock(): void {
|
||||
if (statTopicClient) {
|
||||
publicMaintananceCmd(statTopicClient, "ServiceClose")
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnlock(): void {
|
||||
if (statTopicClient) {
|
||||
publicMaintananceCmd(statTopicClient, "ServiceOpen")
|
||||
}
|
||||
}
|
||||
|
||||
function handlePlayX(sound: number): void {
|
||||
if (statTopicClient) {
|
||||
if (sound === 1) {
|
||||
publicMaintananceCmd(statTopicClient, "ServicePlay1")
|
||||
} else if (sound === 2) {
|
||||
publicMaintananceCmd(statTopicClient, "ServicePlay2")
|
||||
} else if (sound === 3) {
|
||||
publicMaintananceCmd(statTopicClient, "ServicePlay3")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
statTopicClient = enterMaintananceMode((endSens, startSens, pullSens, lock) => {
|
||||
endSensStatus.value = endSens
|
||||
startSensStatus.value = startSens
|
||||
pullSensStatus.value = pullSens
|
||||
lockStatus.value = lock
|
||||
})
|
||||
window.addEventListener('beforeunload', cleanup)
|
||||
})
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
<div>
|
||||
<h2>Add players</h2>
|
||||
<div class="row">
|
||||
<input
|
||||
v-model="playerInput"
|
||||
type="text"
|
||||
placeholder="Enter player name"
|
||||
@keydown.enter="addPlayer"
|
||||
/>
|
||||
<input v-model="playerInput" type="text" placeholder="Enter player name" @keydown.enter="addPlayer" />
|
||||
<button @click="addPlayer" type="button">Add</button>
|
||||
</div>
|
||||
<ul class="player-list">
|
||||
@@ -15,7 +10,8 @@
|
||||
<span class="player-name">{{ player }}</span>
|
||||
<button class="delete-btn" @click="deletePlayer(index)" title="Delete player">
|
||||
<svg class="trash-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-9l-1 1H5v2h14V4z" fill="currentColor"/>
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-9l-1 1H5v2h14V4z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user