// mqttService.js import mqtt from 'mqtt'; const mqttUrl = `${import.meta.env.VITE_MQTT_SERVER ?? 'ws://localhost:1884'}`; const topics = [ 'PIU_COD/AIR_DRYER/OVERVIEW', 'PIU_COD/AIR_DRYER/AIR_DRYER_A', 'PIU_COD/AIR_DRYER/AIR_DRYER_B', 'PIU_COD/AIR_DRYER/AIR_DRYER_C', 'PIU_COD/COMPRESSOR/OVERVIEW', 'PIU_COD/COMPRESSOR/COMPRESSOR_A', 'PIU_COD/COMPRESSOR/COMPRESSOR_B', 'PIU_COD/COMPRESSOR/COMPRESSOR_C', 'PIU_COD/ERROR_CODE/SIM', ]; const options = { keepalive: 30, clientId: 'react_mqtt_' + Math.random().toString(16).substr(2, 8), protocolId: 'MQTT', protocolVersion: 4, clean: true, reconnectPeriod: 1000, connectTimeout: 30 * 1000, username: `${import.meta.env.VITE_MQTT_USERNAME ?? ''}`, // jika ada password: `${import.meta.env.VITE_MQTT_PASSWORD ?? ''}`, // jika ada }; const client = mqtt.connect(mqttUrl, options); // Track connection status let isConnected = false; client.on('connect', () => { console.log('MQTT Connected'); isConnected = true; // Subscribe default topic client.subscribe(topics, (err) => { if (err) console.error('Subscribe error:', err); else console.log(`Subscribed to topics: ${topics.join(', ')}`); }); }); client.on('error', (err) => { console.error('Connection error: ', err); client.end(); }); client.on('close', () => { console.log('MQTT Disconnected'); isConnected = false; }); /** * Publish message to MQTT * @param {string} topic * @param {string} message */ const publishMessage = (topic, message) => { if (client && isConnected && message.trim() !== '') { client.publish(topic, message); } else { console.warn('MQTT not connected or message empty'); } }; /** * Listen to incoming messages * @param {function} callback - Function(topic, message) */ const listenMessage = (callback) => { client.on('message', (topic, message) => { callback(topic, message.toString()); }); }; const colorIds = ['c_1023', 'c_2023', 'c_2023']; const StatusColor = (el, num) => { switch (num) { case 1: el.style.fill = 'orange'; break; case 2: el.style.fill = 'green'; break; default: el.style.fill = 'rgb(216,216,216)'; } }; const dryerIds = ['c_4018', 'c_4021', 'c_5018', 'c_5021', 'c_6018', 'c_6021']; const handleBoolean = (svg, el, value) => { if (!dryerIds.includes(el.id)) { el.style.display = value ? '' : 'none'; return; } const i = dryerIds.indexOf(el.id); const id1 = dryerIds[i - (i % 2)]; const id2 = dryerIds[i - (i % 2) + 1]; const el1 = svg.getElementById(id1); const el2 = svg.getElementById(id2); if (!el1 || !el2) return; el1.style.fill = value ? 'rgb(216,216,216)' : 'yellow'; el2.style.fill = value ? 'yellow' : 'rgb(216,216,216)'; }; const handleNumber = (el, value) => { const num = Number(value); if (colorIds.includes(el.id)) { StatusColor(el, num); } else { el.textContent = num.toFixed(2); } }; const handleOther = (el, value) => { el.textContent = value; }; const setValSvg = (listenTopic, svg) => { client.on('message', (topic, message) => { if (topic === listenTopic) { const objChanel = JSON.parse(message); Object.entries(objChanel).forEach(([key, value]) => { const el = svg.getElementById(key); if (!el) return; if (typeof value === 'boolean') { handleBoolean(svg, el, value); } else if (!isNaN(value)) { handleNumber(el, value); } else { handleOther(el, value); } }); const target = svg.querySelector('text[x="225.25"][y="537.752"]'); if (target) target.remove(); } }); }; // === NOTIFICATION LISTENER === const notifListeners = []; const onNotifUpdate = (callback) => { notifListeners.push(callback); }; client.on('message', (topic, message) => { if (topic === import.meta.env.VITE_MQTT_TOPIC_COD) { try { const payload = JSON.parse(message.toString()); notifListeners.forEach((cb) => cb(payload)); } catch (err) { console.error('Invalid notif payload', err); } } }); export { publishMessage, listenMessage, setValSvg, onNotifUpdate };