state of the mqtt

This commit is contained in:
cpu
2025-05-12 23:58:46 +02:00
parent 413e7ce4cf
commit 634ef1e511
5 changed files with 116 additions and 58 deletions

View File

@@ -178,16 +178,19 @@ onMounted(() => {
// Example: Sync with OS theme only if no theme saved (requires store modification) // Example: Sync with OS theme only if no theme saved (requires store modification)
// syncWithOsThemeIfNeeded(); // syncWithOsThemeIfNeeded();
// *** Auto-connect MQTT if URL is stored and not already connected/connecting *** // Auto-connect MQTT only if URL is stored AND user desires connection
const storedBrokerUrl = store.getters.mqttBrokerUrl; const storedBrokerUrl = store.getters.mqttBrokerUrl;
const connectDesired = store.getters.mqttConnectDesired; // Get the flag
if (storedBrokerUrl && if (storedBrokerUrl &&
connectDesired && // Check the flag
MqttService.connectionStatus.value !== 'connected' && MqttService.connectionStatus.value !== 'connected' &&
MqttService.connectionStatus.value !== 'connecting') { MqttService.connectionStatus.value !== 'connecting') {
console.log("App.vue: Auto-connecting to stored MQTT broker:", storedBrokerUrl); console.log("App.vue: Auto-connecting to stored MQTT broker (user desired):", storedBrokerUrl);
MqttService.connect(storedBrokerUrl); MqttService.connect(storedBrokerUrl);
} else if (storedBrokerUrl && !connectDesired) {
console.log("App.vue: MQTT Broker URL is stored, but user previously disconnected. Not auto-connecting.");
} }
// *** End Auto-connect MQTT ***
}).catch(error => { }).catch(error => {
console.error("App.vue: Error during store.dispatch('loadState'):", error); console.error("App.vue: Error during store.dispatch('loadState'):", error);
}); });

View File

@@ -114,15 +114,33 @@ const connect = async (brokerUrl = 'ws://localhost:9001') => {
} }
}; };
const disconnect = () => { /* ... existing disconnect logic ... */ const disconnect = () => {
if (client.value && typeof client.value.end === 'function') { if (client.value) {
console.log('MQTT: Disconnecting...'); console.log('MQTT: Disconnecting/Stopping connection attempt...');
connectionStatus.value = 'disconnected'; // Set status immediately to give user feedback, 'close' event will confirm
client.value.end(true, () => { // but if it was 'connecting', it might not emit 'close' if it never truly connected.
console.log('MQTT: Disconnected callback triggered.'); const wasConnecting = connectionStatus.value === 'connecting';
client.value.end(true, () => { // true forces close and stops reconnect attempts
console.log('MQTT: client.end() callback executed.');
// The 'close' event listener on the client should handle final cleanup
// like setting client.value = null and connectionStatus.value = 'disconnected'.
// If it was just 'connecting' and never connected, 'close' might not fire reliably.
if (wasConnecting && connectionStatus.value !== 'disconnected') {
connectionStatus.value = 'disconnected';
client.value = null; // Ensure cleanup if 'close' doesn't fire from 'connecting' state
}
}); });
// If it was 'connecting', we might want to immediately reflect disconnected state
// as 'end(true)' stops further attempts.
if (wasConnecting) {
connectionStatus.value = 'disconnected';
// Note: client.value will be fully nulled on the 'close' event or after end() callback.
}
} else { } else {
connectionStatus.value = 'disconnected'; console.log('MQTT: No active client to disconnect.');
connectionStatus.value = 'disconnected'; // Ensure status is correct
} }
}; };

View File

@@ -36,6 +36,7 @@ const initialState = {
globalMqttStopPause: null, // New globalMqttStopPause: null, // New
globalMqttRunAll: null, // New globalMqttRunAll: null, // New
mqttBrokerUrl: 'ws://localhost:9001', // Default, user can change mqttBrokerUrl: 'ws://localhost:9001', // Default, user can change
mqttConnectDesired: false,
currentPlayerIndex: 0, currentPlayerIndex: 0,
gameMode: 'normal', gameMode: 'normal',
isMuted: false, isMuted: false,
@@ -80,6 +81,7 @@ export default createStore({
globalMqttStopPause: persistedState.globalMqttStopPause || initialState.globalMqttStopPause, globalMqttStopPause: persistedState.globalMqttStopPause || initialState.globalMqttStopPause,
globalMqttRunAll: persistedState.globalMqttRunAll || initialState.globalMqttRunAll, globalMqttRunAll: persistedState.globalMqttRunAll || initialState.globalMqttRunAll,
mqttBrokerUrl: persistedState.mqttBrokerUrl || initialState.mqttBrokerUrl, mqttBrokerUrl: persistedState.mqttBrokerUrl || initialState.mqttBrokerUrl,
mqttConnectDesired: persistedState.hasOwnProperty('mqttConnectDesired') ? persistedState.mqttConnectDesired : initialState.mqttConnectDesired,
gameRunning: false, // Always start non-running gameRunning: false, // Always start non-running
currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0, currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0,
}; };
@@ -113,6 +115,10 @@ export default createStore({
}, },
SET_MQTT_BROKER_URL(state, url) { SET_MQTT_BROKER_URL(state, url) {
state.mqttBrokerUrl = url; state.mqttBrokerUrl = url;
state.mqttConnectDesired = true;
},
SET_MQTT_CONNECT_DESIRED(state, desired) {
state.mqttConnectDesired = desired;
}, },
SET_GLOBAL_MQTT_STOP_PAUSE(state, char) { SET_GLOBAL_MQTT_STOP_PAUSE(state, char) {
state.globalMqttStopPause = char; state.globalMqttStopPause = char;
@@ -251,9 +257,10 @@ export default createStore({
})), })),
globalHotkeyStopPause: state.globalHotkeyStopPause, globalHotkeyStopPause: state.globalHotkeyStopPause,
globalHotkeyRunAll: state.globalHotkeyRunAll, globalHotkeyRunAll: state.globalHotkeyRunAll,
globalMqttStopPause: state.globalMqttStopPause, // Save globalMqttStopPause: state.globalMqttStopPause,
globalMqttRunAll: state.globalMqttRunAll, // Save globalMqttRunAll: state.globalMqttRunAll,
mqttBrokerUrl: state.mqttBrokerUrl, // Save mqttBrokerUrl: state.mqttBrokerUrl,
mqttConnectDesired: state.mqttConnectDesired,
currentPlayerIndex: state.currentPlayerIndex, currentPlayerIndex: state.currentPlayerIndex,
gameMode: state.gameMode, gameMode: state.gameMode,
isMuted: state.isMuted, isMuted: state.isMuted,
@@ -268,6 +275,10 @@ export default createStore({
commit('SET_MQTT_BROKER_URL', url); commit('SET_MQTT_BROKER_URL', url);
dispatch('saveState'); dispatch('saveState');
}, },
setMqttConnectDesired({ commit, dispatch }, desired) {
commit('SET_MQTT_CONNECT_DESIRED', desired);
dispatch('saveState');
},
setGlobalMqttStopPause({ commit, dispatch }, char) { setGlobalMqttStopPause({ commit, dispatch }, char) {
commit('SET_GLOBAL_MQTT_STOP_PAUSE', char); commit('SET_GLOBAL_MQTT_STOP_PAUSE', char);
dispatch('saveState'); dispatch('saveState');
@@ -333,6 +344,7 @@ export default createStore({
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause); commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause);
commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll); commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll);
commit('SET_MQTT_BROKER_URL', freshInitialState.mqttBrokerUrl); // Reset commit('SET_MQTT_BROKER_URL', freshInitialState.mqttBrokerUrl); // Reset
commit('SET_MQTT_CONNECT_DESIRED', freshInitialState.mqttConnectDesired); // Reset
commit('SET_GLOBAL_MQTT_STOP_PAUSE', freshInitialState.globalMqttStopPause); // Reset commit('SET_GLOBAL_MQTT_STOP_PAUSE', freshInitialState.globalMqttStopPause); // Reset
commit('SET_GLOBAL_MQTT_RUN_ALL', freshInitialState.globalMqttRunAll); // Reset commit('SET_GLOBAL_MQTT_RUN_ALL', freshInitialState.globalMqttRunAll); // Reset
@@ -486,6 +498,7 @@ export default createStore({
globalHotkeyStopPause: state => state.globalHotkeyStopPause, globalHotkeyStopPause: state => state.globalHotkeyStopPause,
globalHotkeyRunAll: state => state.globalHotkeyRunAll, globalHotkeyRunAll: state => state.globalHotkeyRunAll,
mqttBrokerUrl: state => state.mqttBrokerUrl, mqttBrokerUrl: state => state.mqttBrokerUrl,
mqttConnectDesired: state => state.mqttConnectDesired,
globalMqttStopPause: state => state.globalMqttStopPause, globalMqttStopPause: state => state.globalMqttStopPause,
globalMqttRunAll: state => state.globalMqttRunAll, globalMqttRunAll: state => state.globalMqttRunAll,
totalPlayers: state => state.players.length, totalPlayers: state => state.players.length,

View File

@@ -4,8 +4,9 @@
<h1 class="text-4xl font-bold text-blue-600 dark:text-blue-400">Nexus Timer Setup</h1> <h1 class="text-4xl font-bold text-blue-600 dark:text-blue-400">Nexus Timer Setup</h1>
</header> </header>
<!-- Player Management --> <!-- Player Management (no changes) -->
<section class="w-full max-w-3xl bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md mb-6"> <section class="w-full max-w-3xl bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md mb-6">
<!-- ... Player list ... -->
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold">Players ({{ players.length }})</h2> <h2 class="text-2xl font-semibold">Players ({{ players.length }})</h2>
<button @click="openAddPlayerModal" class="btn btn-primary" :disabled="players.length >= 99"> <button @click="openAddPlayerModal" class="btn btn-primary" :disabled="players.length >= 99">
@@ -52,22 +53,40 @@
<div class="mb-6"> <div class="mb-6">
<label for="mqttBrokerUrl" class="block text-sm font-medium text-gray-700 dark:text-gray-300">MQTT Broker URL</label> <label for="mqttBrokerUrl" class="block text-sm font-medium text-gray-700 dark:text-gray-300">MQTT Broker URL</label>
<div class="mt-1 flex rounded-md shadow-sm"> <div class="mt-1 flex rounded-md shadow-sm">
<input type="text" id="mqttBrokerUrl" v-model="localMqttBrokerUrl" placeholder="ws://localhost:9001" class="input-base flex-1 rounded-none rounded-l-md" :disabled="mqttConnectionStatus === 'connected' || mqttConnectionStatus === 'connecting'"> <input
<button @click="toggleMqttConnection" type="text"
:class="['btn inline-flex items-center px-3 rounded-l-none rounded-r-md border border-l-0', id="mqttBrokerUrl"
mqttConnectionStatus === 'connected' ? 'bg-green-500 hover:bg-green-600 border-green-600' : v-model="localMqttBrokerUrl"
mqttConnectionStatus === 'connecting' ? 'bg-yellow-500 border-yellow-600 cursor-not-allowed' : placeholder="ws://broker.example.com:9001"
mqttConnectionStatus === 'error' ? 'bg-red-500 hover:bg-red-600 border-red-600' : class="input-base flex-1 rounded-none rounded-l-md"
'btn-secondary border-gray-300 dark:border-gray-500']" :disabled="mqttConnectionStatus === 'connected' || mqttConnectionStatus === 'connecting'"
:disabled="mqttConnectionStatus === 'connecting'"> >
{{ mqttConnectionStatus === 'connected' ? 'Disconnect' : (mqttConnectionStatus === 'connecting' ? 'Connecting...' : 'Connect') }} <button
@click="toggleMqttConnection"
:class="['btn inline-flex items-center px-4 rounded-l-none rounded-r-md border border-l-0 text-white',
mqttConnectionStatus === 'connected' ? 'bg-red-500 hover:bg-red-600 border-red-600' :
mqttConnectionStatus === 'connecting' ? 'bg-yellow-500 hover:bg-yellow-600 border-yellow-600 !text-black' : // Stop button is yellow
'btn-primary border-blue-500']"
>
{{
mqttConnectionStatus === 'connected' ? 'Disconnect' :
(mqttConnectionStatus === 'connecting' ? 'Stop' : 'Connect')
}}
</button> </button>
</div> </div>
<p v-if="mqttError" class="text-xs text-red-500 mt-1">{{ mqttError }}</p> <p v-if="mqttError" class="text-xs text-red-500 mt-1">{{ mqttError }}</p>
<p v-else-if="mqttConnectionStatus === 'connected'" class="text-xs text-green-500 mt-1">Connected to {{ store.getters.mqttBrokerUrl }}. Subscribed to '{{ MqttService.MQTT_TOPIC_GAME }}'.</p> <p v-else-if="mqttConnectionStatus === 'connected'" class="text-xs text-green-500 mt-1">
<p v-else-if="mqttConnectionStatus === 'disconnected' && !mqttError" class="text-xs text-gray-500 mt-1">Not connected to MQTT broker.</p> Connected to {{ store.getters.mqttBrokerUrl }}. Subscribed to '{{ MqttService.MQTT_TOPIC_GAME }}'.
</p>
<p v-else-if="mqttConnectionStatus === 'connecting'" class="text-xs text-yellow-600 mt-1">
Connecting to {{ localMqttBrokerUrl }}... (Click "Stop" to cancel)
</p>
<p v-else-if="mqttConnectionStatus === 'disconnected'" class="text-xs text-gray-500 mt-1">
Not connected.
</p>
</div> </div>
<!-- Global Triggers (no changes to this section) -->
<div> <div>
<p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Global "Stop/Pause All" Trigger:</p> <p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Global "Stop/Pause All" Trigger:</p>
<div class="flex items-center justify-between space-x-4 mb-3"> <div class="flex items-center justify-between space-x-4 mb-3">
@@ -87,7 +106,6 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Global "Run All Timers" Trigger:</p> <p class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Global "Run All Timers" Trigger:</p>
<div class="flex items-center justify-between space-x-4 mb-6"> <div class="flex items-center justify-between space-x-4 mb-6">
@@ -141,6 +159,12 @@
</div> </div>
</template> </template>
<style scoped>
.trigger-clear-btn {
@apply text-xs text-blue-500 hover:underline ml-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-600;
}
</style>
<script setup> <script setup>
import { ref, computed, watch, onUnmounted } from 'vue'; import { ref, computed, watch, onUnmounted } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
@@ -255,9 +279,8 @@ const handleGlobalHotkeyCaptured = (key) => {
isCapturingGlobalHotkey.value = false; isCapturingGlobalHotkey.value = false;
const actionType = currentGlobalActionType.value; const actionType = currentGlobalActionType.value;
if (!actionType || key.length !== 1) { currentGlobalActionType.value = null; return; } if (!actionType || key.length !== 1) { currentGlobalActionType.value = null; return; }
let conflictMessage = ''; let conflictMessage = '';
if (store.state.players.some(p => p.hotkey === key)) { // Only check player hotkeys if (store.state.players.some(p => p.hotkey === key)) {
conflictMessage = `Hotkey "${key.toUpperCase()}" is already used by a player.`; conflictMessage = `Hotkey "${key.toUpperCase()}" is already used by a player.`;
} }
else if (actionType === 'stopPause' && store.state.globalHotkeyRunAll === key) { else if (actionType === 'stopPause' && store.state.globalHotkeyRunAll === key) {
@@ -265,9 +288,7 @@ const handleGlobalHotkeyCaptured = (key) => {
} else if (actionType === 'runAll' && store.state.globalHotkeyStopPause === key) { } else if (actionType === 'runAll' && store.state.globalHotkeyStopPause === key) {
conflictMessage = `Hotkey "${key.toUpperCase()}" is already the Global Stop/Pause All Hotkey.`; conflictMessage = `Hotkey "${key.toUpperCase()}" is already the Global Stop/Pause All Hotkey.`;
} }
if (conflictMessage) { alert(conflictMessage); currentGlobalActionType.value = null; return; } if (conflictMessage) { alert(conflictMessage); currentGlobalActionType.value = null; return; }
if (actionType === 'stopPause') store.dispatch('setGlobalHotkeyStopPause', key); if (actionType === 'stopPause') store.dispatch('setGlobalHotkeyStopPause', key);
else if (actionType === 'runAll') store.dispatch('setGlobalHotkeyRunAll', key); else if (actionType === 'runAll') store.dispatch('setGlobalHotkeyRunAll', key);
currentGlobalActionType.value = null; currentGlobalActionType.value = null;
@@ -296,9 +317,8 @@ const handleGlobalMqttCharCapturedDirect = (charKey) => {
isCapturingGlobalMqttChar.value = false; isCapturingGlobalMqttChar.value = false;
const actionType = currentGlobalActionType.value; const actionType = currentGlobalActionType.value;
if (!actionType || charKey.length !== 1) { currentGlobalActionType.value = null; return; } if (!actionType || charKey.length !== 1) { currentGlobalActionType.value = null; return; }
let conflictMessage = ''; let conflictMessage = '';
if (store.state.players.some(p => p.mqttChar === charKey)) { // Only check player MQTT chars if (store.state.players.some(p => p.mqttChar === charKey)) {
conflictMessage = `MQTT Char "${charKey.toUpperCase()}" is already used by a player.`; conflictMessage = `MQTT Char "${charKey.toUpperCase()}" is already used by a player.`;
} }
else if (actionType === 'stopPause' && store.state.globalMqttRunAll === charKey) { else if (actionType === 'stopPause' && store.state.globalMqttRunAll === charKey) {
@@ -306,26 +326,38 @@ const handleGlobalMqttCharCapturedDirect = (charKey) => {
} else if (actionType === 'runAll' && store.state.globalMqttStopPause === charKey) { } else if (actionType === 'runAll' && store.state.globalMqttStopPause === charKey) {
conflictMessage = `MQTT Char "${charKey.toUpperCase()}" is already the Global Stop/Pause All MQTT Char.`; conflictMessage = `MQTT Char "${charKey.toUpperCase()}" is already the Global Stop/Pause All MQTT Char.`;
} }
if (conflictMessage) { alert(conflictMessage); currentGlobalActionType.value = null; return; } if (conflictMessage) { alert(conflictMessage); currentGlobalActionType.value = null; return; }
if (actionType === 'stopPause') store.dispatch('setGlobalMqttStopPause', charKey); if (actionType === 'stopPause') store.dispatch('setGlobalMqttStopPause', charKey);
else if (actionType === 'runAll') store.dispatch('setGlobalMqttRunAll', charKey); else if (actionType === 'runAll') store.dispatch('setGlobalMqttRunAll', charKey);
currentGlobalActionType.value = null; currentGlobalActionType.value = null;
}; };
const toggleMqttConnection = async () => { const toggleMqttConnection = async () => {
if (MqttService.connectionStatus.value === 'connected') { if (MqttService.connectionStatus.value === 'connected' || MqttService.connectionStatus.value === 'connecting') {
MqttService.disconnect(); MqttService.disconnect();
} else { // User explicitly disconnected or stopped connecting
await store.dispatch('setMqttConnectDesired', false);
} else { // Status is 'disconnected' or 'error'
if (!localMqttBrokerUrl.value.trim()) { if (!localMqttBrokerUrl.value.trim()) {
alert("Please enter MQTT Broker URL."); alert("Please enter MQTT Broker URL (e.g., ws://host:port).");
return; return;
} }
try {
const url = new URL(localMqttBrokerUrl.value);
if(!url.protocol.startsWith('ws')) {
throw new Error("URL must start with ws:// or wss://");
}
} catch (e) {
alert(`Invalid MQTT Broker URL: ${e.message}. Please use format ws://host:port or wss://host:port.`);
return;
}
// Saving URL also sets mqttConnectDesired to true in the store
await store.dispatch('setMqttBrokerUrl', localMqttBrokerUrl.value); await store.dispatch('setMqttBrokerUrl', localMqttBrokerUrl.value);
// await store.dispatch('setMqttConnectDesired', true); // This is now handled by setMqttBrokerUrl action
MqttService.connect(localMqttBrokerUrl.value); MqttService.connect(localMqttBrokerUrl.value);
} }
}; };
onUnmounted(() => { onUnmounted(() => {
if (MqttService.isCapturingMqttChar.value) { if (MqttService.isCapturingMqttChar.value) {
MqttService.stopMqttCharCapture(); MqttService.stopMqttCharCapture();

View File

@@ -6,29 +6,21 @@ import { resolve } from 'path';
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const appVersion = packageJson.version; const appVersion = packageJson.version;
// Get current date (this will be the server's local time where the build runs)
const now = new Date(); const now = new Date();
const dateTimeFormat = new Intl.DateTimeFormat('sk-SK', {
// Options for date formatting, targeting CET/CEST
const dateTimeFormatOptionsCEST = {
year: 'numeric', month: '2-digit', day: '2-digit', year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false hour12: false, // Use 24-hour format
}); timeZone: 'Europe/Bratislava' // Or 'Europe/Prague', 'Europe/Berlin', etc. (any major CET/CEST city)
// This will automatically handle Daylight Saving Time (CEST vs CET)
};
const parts = dateTimeFormat.formatToParts(now); // Generate build time string using a specific time zone that observes CET/CEST
let day = '', month = '', year = '', hour = '', minute = '', second = ''; const appBuildTime = now.toLocaleString('sk-SK', dateTimeFormatOptionsCEST) + " CEST/CET"; // Add timezone indicator for clarity
parts.forEach(part => {
switch (part.type) {
case 'day': day = part.value; break;
case 'month': month = part.value; break;
case 'year': year = part.value; break;
case 'hour': hour = part.value; break;
case 'minute': minute = part.value; break;
case 'second': second = part.value; break;
}
});
// Assemble the date part without unwanted spaces
const appBuildTime = `${day}.${month}.${year} ${hour}:${minute}:${second}`;
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],