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)
// 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 connectDesired = store.getters.mqttConnectDesired; // Get the flag
if (storedBrokerUrl &&
connectDesired && // Check the flag
MqttService.connectionStatus.value !== 'connected' &&
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);
} 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 => {
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 ... */
if (client.value && typeof client.value.end === 'function') {
console.log('MQTT: Disconnecting...');
const disconnect = () => {
if (client.value) {
console.log('MQTT: Disconnecting/Stopping connection attempt...');
// Set status immediately to give user feedback, 'close' event will confirm
// but if it was 'connecting', it might not emit 'close' if it never truly connected.
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.end(true, () => {
console.log('MQTT: Disconnected callback triggered.');
client.value = null; // Ensure cleanup if 'close' doesn't fire from 'connecting' state
}
});
} else {
// 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 {
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
globalMqttRunAll: null, // New
mqttBrokerUrl: 'ws://localhost:9001', // Default, user can change
mqttConnectDesired: false,
currentPlayerIndex: 0,
gameMode: 'normal',
isMuted: false,
@@ -80,6 +81,7 @@ export default createStore({
globalMqttStopPause: persistedState.globalMqttStopPause || initialState.globalMqttStopPause,
globalMqttRunAll: persistedState.globalMqttRunAll || initialState.globalMqttRunAll,
mqttBrokerUrl: persistedState.mqttBrokerUrl || initialState.mqttBrokerUrl,
mqttConnectDesired: persistedState.hasOwnProperty('mqttConnectDesired') ? persistedState.mqttConnectDesired : initialState.mqttConnectDesired,
gameRunning: false, // Always start non-running
currentPlayerIndex: persistedState.currentPlayerIndex !== undefined && persistedState.currentPlayerIndex < playersToUse.length ? persistedState.currentPlayerIndex : 0,
};
@@ -113,6 +115,10 @@ export default createStore({
},
SET_MQTT_BROKER_URL(state, url) {
state.mqttBrokerUrl = url;
state.mqttConnectDesired = true;
},
SET_MQTT_CONNECT_DESIRED(state, desired) {
state.mqttConnectDesired = desired;
},
SET_GLOBAL_MQTT_STOP_PAUSE(state, char) {
state.globalMqttStopPause = char;
@@ -251,9 +257,10 @@ export default createStore({
})),
globalHotkeyStopPause: state.globalHotkeyStopPause,
globalHotkeyRunAll: state.globalHotkeyRunAll,
globalMqttStopPause: state.globalMqttStopPause, // Save
globalMqttRunAll: state.globalMqttRunAll, // Save
mqttBrokerUrl: state.mqttBrokerUrl, // Save
globalMqttStopPause: state.globalMqttStopPause,
globalMqttRunAll: state.globalMqttRunAll,
mqttBrokerUrl: state.mqttBrokerUrl,
mqttConnectDesired: state.mqttConnectDesired,
currentPlayerIndex: state.currentPlayerIndex,
gameMode: state.gameMode,
isMuted: state.isMuted,
@@ -268,6 +275,10 @@ export default createStore({
commit('SET_MQTT_BROKER_URL', url);
dispatch('saveState');
},
setMqttConnectDesired({ commit, dispatch }, desired) {
commit('SET_MQTT_CONNECT_DESIRED', desired);
dispatch('saveState');
},
setGlobalMqttStopPause({ commit, dispatch }, char) {
commit('SET_GLOBAL_MQTT_STOP_PAUSE', char);
dispatch('saveState');
@@ -333,6 +344,7 @@ export default createStore({
commit('SET_GLOBAL_HOTKEY_STOP_PAUSE', freshInitialState.globalHotkeyStopPause);
commit('SET_GLOBAL_HOTKEY_RUN_ALL', freshInitialState.globalHotkeyRunAll);
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_RUN_ALL', freshInitialState.globalMqttRunAll); // Reset
@@ -486,6 +498,7 @@ export default createStore({
globalHotkeyStopPause: state => state.globalHotkeyStopPause,
globalHotkeyRunAll: state => state.globalHotkeyRunAll,
mqttBrokerUrl: state => state.mqttBrokerUrl,
mqttConnectDesired: state => state.mqttConnectDesired,
globalMqttStopPause: state => state.globalMqttStopPause,
globalMqttRunAll: state => state.globalMqttRunAll,
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>
</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">
<!-- ... Player list ... -->
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold">Players ({{ players.length }})</h2>
<button @click="openAddPlayerModal" class="btn btn-primary" :disabled="players.length >= 99">
@@ -52,22 +53,40 @@
<div class="mb-6">
<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">
<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'">
<button @click="toggleMqttConnection"
:class="['btn inline-flex items-center px-3 rounded-l-none rounded-r-md border border-l-0',
mqttConnectionStatus === 'connected' ? 'bg-green-500 hover:bg-green-600 border-green-600' :
mqttConnectionStatus === 'connecting' ? 'bg-yellow-500 border-yellow-600 cursor-not-allowed' :
mqttConnectionStatus === 'error' ? 'bg-red-500 hover:bg-red-600 border-red-600' :
'btn-secondary border-gray-300 dark:border-gray-500']"
:disabled="mqttConnectionStatus === 'connecting'">
{{ mqttConnectionStatus === 'connected' ? 'Disconnect' : (mqttConnectionStatus === 'connecting' ? 'Connecting...' : 'Connect') }}
<input
type="text"
id="mqttBrokerUrl"
v-model="localMqttBrokerUrl"
placeholder="ws://broker.example.com:9001"
class="input-base flex-1 rounded-none rounded-l-md"
:disabled="mqttConnectionStatus === 'connected' || mqttConnectionStatus === 'connecting'"
>
<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>
</div>
<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 === 'disconnected' && !mqttError" class="text-xs text-gray-500 mt-1">Not connected to MQTT broker.</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 === '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>
<!-- Global Triggers (no changes to this section) -->
<div>
<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">
@@ -87,7 +106,6 @@
</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>
<div class="flex items-center justify-between space-x-4 mb-6">
@@ -141,6 +159,12 @@
</div>
</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>
import { ref, computed, watch, onUnmounted } from 'vue';
import { useStore } from 'vuex';
@@ -255,9 +279,8 @@ const handleGlobalHotkeyCaptured = (key) => {
isCapturingGlobalHotkey.value = false;
const actionType = currentGlobalActionType.value;
if (!actionType || key.length !== 1) { currentGlobalActionType.value = null; return; }
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.`;
}
else if (actionType === 'stopPause' && store.state.globalHotkeyRunAll === key) {
@@ -265,9 +288,7 @@ const handleGlobalHotkeyCaptured = (key) => {
} else if (actionType === 'runAll' && store.state.globalHotkeyStopPause === key) {
conflictMessage = `Hotkey "${key.toUpperCase()}" is already the Global Stop/Pause All Hotkey.`;
}
if (conflictMessage) { alert(conflictMessage); currentGlobalActionType.value = null; return; }
if (actionType === 'stopPause') store.dispatch('setGlobalHotkeyStopPause', key);
else if (actionType === 'runAll') store.dispatch('setGlobalHotkeyRunAll', key);
currentGlobalActionType.value = null;
@@ -296,9 +317,8 @@ const handleGlobalMqttCharCapturedDirect = (charKey) => {
isCapturingGlobalMqttChar.value = false;
const actionType = currentGlobalActionType.value;
if (!actionType || charKey.length !== 1) { currentGlobalActionType.value = null; return; }
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.`;
}
else if (actionType === 'stopPause' && store.state.globalMqttRunAll === charKey) {
@@ -306,26 +326,38 @@ const handleGlobalMqttCharCapturedDirect = (charKey) => {
} else if (actionType === 'runAll' && store.state.globalMqttStopPause === charKey) {
conflictMessage = `MQTT Char "${charKey.toUpperCase()}" is already the Global Stop/Pause All MQTT Char.`;
}
if (conflictMessage) { alert(conflictMessage); currentGlobalActionType.value = null; return; }
if (actionType === 'stopPause') store.dispatch('setGlobalMqttStopPause', charKey);
else if (actionType === 'runAll') store.dispatch('setGlobalMqttRunAll', charKey);
currentGlobalActionType.value = null;
};
const toggleMqttConnection = async () => {
if (MqttService.connectionStatus.value === 'connected') {
if (MqttService.connectionStatus.value === 'connected' || MqttService.connectionStatus.value === 'connecting') {
MqttService.disconnect();
} else {
// User explicitly disconnected or stopped connecting
await store.dispatch('setMqttConnectDesired', false);
} else { // Status is 'disconnected' or 'error'
if (!localMqttBrokerUrl.value.trim()) {
alert("Please enter MQTT Broker URL.");
alert("Please enter MQTT Broker URL (e.g., ws://host:port).");
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('setMqttConnectDesired', true); // This is now handled by setMqttBrokerUrl action
MqttService.connect(localMqttBrokerUrl.value);
}
};
onUnmounted(() => {
if (MqttService.isCapturingMqttChar.value) {
MqttService.stopMqttCharCapture();

View File

@@ -6,29 +6,21 @@ import { resolve } from 'path';
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const appVersion = packageJson.version;
// Get current date (this will be the server's local time where the build runs)
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',
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);
let day = '', month = '', year = '', hour = '', minute = '', second = '';
// Generate build time string using a specific time zone that observes CET/CEST
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({
plugins: [vue()],