+ +
+
- - 00:00 - - - - - - - - - - - - - - - - - + +
+ + +
+ + +
+ 00:00AM +
+ + +
CO - ppm -
NO2 - ppm -
Temperature - ℃ -
Humidity - % -
+ + + + + + + + + + + + + + + + + +
CO + -- + +
NO2 + -- + +
Temperature + -- + +
Humidity + -- + +
-
+ + +
diff --git a/js/circle-helper.js b/js/circle-helper.js index 1acc6e1..8f96ec4 100644 --- a/js/circle-helper.js +++ b/js/circle-helper.js @@ -1,30 +1,287 @@ -/*global tau */ -/*jslint unparam: true */ -(function (tau) { - // This logic works only on circular device. - if (tau.support.shape.circle) { - /** - * pagebeforeshow event handler - * Do preparatory works and adds event listeners - */ - document.addEventListener("pagebeforeshow", function (event) { - /** - * page - Active page element - * list - NodeList object for lists in the page - */ - var page, list; - - page = event.target; - if ( - page.id !== "page-snaplistview" && - page.id !== "page-swipelist" && - page.id !== "page-marquee-list" - ) { - list = page.querySelector(".ui-listview"); - if (list) { - tau.widget.ArcListview(list); - } - } - }); - } -})(tau); +/** + * Circle Helper - Galaxy Watch 3 Edition + * Enhanced circular UI support with modern JavaScript + */ + +(() => { + "use strict"; + + /** + * Initialize circular UI enhancements for Galaxy Watch 3. + */ + const initializeCircularUI = () => { + try { + // Check if running on circular device + if (typeof tau === "undefined") { + console.warn("TAU framework not available"); + return; + } + + if (!tau.support || !tau.support.shape || !tau.support.shape.circle) { + console.log("Non-circular device detected, skipping circular enhancements"); + return; + } + + console.log("Initializing circular UI enhancements for Galaxy Watch 3"); + + // Enhanced page lifecycle management + document.addEventListener("pagebeforeshow", handlePageBeforeShow); + document.addEventListener("pageshow", handlePageShow); + document.addEventListener("pagehide", handlePageHide); + + // Initialize circular-specific features + initializeCircularTable(); + initializeRotaryScrolling(); + initializeCircularAnimations(); + + console.log("Circular UI enhancements initialized successfully"); + } catch (error) { + console.error("Error initializing circular UI:", error); + } + }; + + /** + * Handle page before show event with circular optimizations. + */ + const handlePageBeforeShow = (event) => { + try { + const page = event.target; + console.log(`Page before show: ${page.id}`); + + // Initialize any list views with circular support + const listViews = page.querySelectorAll(".ui-listview"); + listViews.forEach(list => { + try { + tau.widget.ArcListview(list); + } catch (error) { + console.warn("Could not initialize ArcListview:", error); + } + }); + + // Apply circular layout optimizations + applyCircularLayout(page); + + } catch (error) { + console.error("Error in pagebeforeshow handler:", error); + } + }; + + /** + * Handle page show event. + */ + const handlePageShow = (event) => { + try { + const page = event.target; + console.log(`Page shown: ${page.id}`); + + // Refresh circular animations + refreshCircularAnimations(page); + + } catch (error) { + console.error("Error in pageshow handler:", error); + } + }; + + /** + * Handle page hide event. + */ + const handlePageHide = (event) => { + try { + const page = event.target; + console.log(`Page hidden: ${page.id}`); + + // Clean up any circular-specific resources + cleanupCircularResources(page); + + } catch (error) { + console.error("Error in pagehide handler:", error); + } + }; + + /** + * Initialize circular table layout for sensor data. + */ + const initializeCircularTable = () => { + try { + const table = document.querySelector("table"); + if (table) { + table.classList.add("circular-optimized"); + + // Add circular-specific styling + table.style.borderRadius = "15px"; + table.style.overflow = "hidden"; + + // Optimize row spacing for circular display + const rows = table.querySelectorAll("tr"); + rows.forEach((row, index) => { + row.style.animationDelay = `${index * 0.1}s`; + row.classList.add("fade-in-circular"); + }); + } + } catch (error) { + console.error("Error initializing circular table:", error); + } + }; + + /** + * Initialize rotary scrolling for circular navigation. + */ + const initializeRotaryScrolling = () => { + try { + // Enable smooth scrolling with rotary input + const scrollableElements = document.querySelectorAll(".ui-content, table"); + + scrollableElements.forEach(element => { + element.addEventListener("scroll", (event) => { + // Add smooth scrolling feedback + element.style.transform = `scale(${0.98 + (element.scrollTop / 1000) * 0.02})`; + }); + }); + + } catch (error) { + console.error("Error initializing rotary scrolling:", error); + } + }; + + /** + * Initialize circular-specific animations. + */ + const initializeCircularAnimations = () => { + try { + // Add CSS for circular animations if not already present + if (!document.getElementById("circular-animations")) { + const style = document.createElement("style"); + style.id = "circular-animations"; + style.textContent = ` + .fade-in-circular { + animation: fadeInCircular 0.8s ease-out forwards; + opacity: 0; + } + + @keyframes fadeInCircular { + 0% { + opacity: 0; + transform: scale(0.8) rotate(-5deg); + } + 100% { + opacity: 1; + transform: scale(1) rotate(0deg); + } + } + + .circular-optimized { + border-radius: 50% / 20%; + transition: all 0.3s ease; + } + + .circular-pulse { + animation: circularPulse 2s infinite; + } + + @keyframes circularPulse { + 0%, 100% { + border-radius: 15px; + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.1); + } + 50% { + border-radius: 20px; + box-shadow: 0 0 0 10px rgba(255, 255, 255, 0); + } + } + `; + document.head.appendChild(style); + } + } catch (error) { + console.error("Error initializing circular animations:", error); + } + }; + + /** + * Apply circular layout optimizations to a page. + */ + const applyCircularLayout = (page) => { + try { + // Optimize content positioning for circular display + const content = page.querySelector(".ui-content"); + if (content) { + content.style.padding = "20px"; + content.style.borderRadius = "50%"; + content.style.background = "radial-gradient(circle, rgba(0,0,0,0.1) 0%, transparent 70%)"; + } + + // Optimize clock display for circular layout + const clock = page.querySelector("#device-clock"); + if (clock) { + clock.classList.add("circular-pulse"); + } + + // Optimize table for circular display + const table = page.querySelector("table"); + if (table) { + table.classList.add("circular-optimized"); + } + + } catch (error) { + console.error("Error applying circular layout:", error); + } + }; + + /** + * Refresh circular animations on page. + */ + const refreshCircularAnimations = (page) => { + try { + const animatedElements = page.querySelectorAll(".fade-in-circular"); + animatedElements.forEach((element, index) => { + element.style.animation = "none"; + setTimeout(() => { + element.style.animation = `fadeInCircular 0.8s ease-out ${index * 0.1}s forwards`; + }, 50); + }); + } catch (error) { + console.error("Error refreshing circular animations:", error); + } + }; + + /** + * Clean up circular-specific resources. + */ + const cleanupCircularResources = (page) => { + try { + // Remove any temporary circular enhancements + const temporaryElements = page.querySelectorAll(".temp-circular"); + temporaryElements.forEach(element => element.remove()); + } catch (error) { + console.error("Error cleaning up circular resources:", error); + } + }; + + /** + * Public API for circular UI management. + */ + const CircularUIManager = { + initialize: initializeCircularUI, + applyLayout: applyCircularLayout, + refreshAnimations: refreshCircularAnimations, + isCircularDevice: () => { + return typeof tau !== "undefined" && + tau.support && + tau.support.shape && + tau.support.shape.circle; + } + }; + + // Initialize when TAU is ready + if (typeof tau !== "undefined") { + initializeCircularUI(); + } else { + // Wait for TAU to load + document.addEventListener("DOMContentLoaded", () => { + setTimeout(initializeCircularUI, 100); + }); + } + + // Expose to global scope + window.CircularUIManager = CircularUIManager; + +})(); diff --git a/js/connect.js b/js/connect.js index e15d217..4b9fbe2 100644 --- a/js/connect.js +++ b/js/connect.js @@ -1,136 +1,402 @@ /** - * Pyrrha Tizen Web API code. + * Pyrrha Tizen Web API code - Samsung Accessory Protocol Integration * - * Utilities for connecting with the mobile app. + * Modern utilities for connecting with the mobile app via Samsung Accessory Protocol. + * Updated for Galaxy Watch 3 with improved error handling and connection management. */ -var SAAgent = null; -var SASocket = null; +// Connection state management +let SAAgent = null; +let SASocket = null; +let connectionState = "disconnected"; // 'disconnected', 'connecting', 'connected' +let reconnectAttempts = 0; +const MAX_RECONNECT_ATTEMPTS = 3; +const RECONNECT_DELAY = 5000; // 5 seconds -/* ----------- UI entry points ----------- */ +/* ----------- Modern UI entry points ----------- */ /** - * Establish a connection + * Establish connection with enhanced error handling and state management. */ -function connect() { - if (SASocket) { - createHTML("Already connected!"); - document.getElementById("connect").innerText = "Disconnect"; - return false; - } - try { - webapis.sa.requestSAAgent(onsuccess, function (err) { - console.log("err [" + err.name + "] msg[" + err.message + "]"); - }); - } catch (err) { - console.log("exception [" + err.name + "] msg[" + err.message + "]"); - } -} +const connect = async () => { + try { + if (SASocket && connectionState === "connected") { + createHTML("Already connected to mobile app!"); + updateConnectButton("Disconnect"); + return false; + } + + if (connectionState === "connecting") { + createHTML("Connection in progress..."); + return false; + } + + connectionState = "connecting"; + updateConnectButton("Connecting..."); + createHTML("Connecting to mobile app..."); + + // Check if Samsung Accessory Service is available + if (typeof webapis === "undefined" || !webapis.sa) { + throw new Error("Samsung Accessory Service not available"); + } + + await new Promise((resolve, reject) => { + webapis.sa.requestSAAgent( + (agents) => onsuccess(agents, resolve, reject), + (error) => { + console.error("SA Agent request failed:", error); + reject(new Error(`Failed to request SA Agent: ${error.name} - ${error.message}`)); + } + ); + }); + + } catch (error) { + console.error("Connection error:", error); + connectionState = "disconnected"; + updateConnectButton("Connect"); + createHTML(`Connection failed: ${error.message}`); + + // Auto-retry logic + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + reconnectAttempts++; + createHTML(`Retrying connection (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`); + setTimeout(connect, RECONNECT_DELAY); + } + } +}; /** - * Fetch data. + * Request sensor data from mobile app. */ -function fetch() { - try { - SASocket.sendData(CHANNELID, "Hello Accessory!"); - } catch (err) { - console.log("exception [" + err.name + "] msg[" + err.message + "]"); - } -} +const requestSensorData = async () => { + try { + if (!SASocket || connectionState !== "connected") { + throw new Error("Not connected to mobile app"); + } + + const request = { + type: "sensor_request", + timestamp: Date.now(), + requestId: Math.random().toString(36).substr(2, 9) + }; + + SASocket.sendData(CHANNELID, JSON.stringify(request)); + console.log("Sensor data requested:", request.requestId); + + } catch (error) { + console.error("Error requesting sensor data:", error); + createHTML(`Data request failed: ${error.message}`); + } +}; /** - * Disconnect. + * Enhanced disconnect with proper cleanup. */ -function disconnect() { - try { - if (SASocket != null) { - SASocket.close(); - SASocket = null; - createHTML("closeConnection"); - document.getElementById("connect").innerText = "Connect"; +const disconnect = async () => { + try { + console.log("Disconnecting from mobile app..."); + + if (SASocket) { + SASocket.close(); + SASocket = null; + } + + if (SAAgent) { + // Clean up agent if needed + SAAgent = null; + } + + connectionState = "disconnected"; + reconnectAttempts = 0; + updateConnectButton("Connect"); + createHTML("Disconnected from mobile app"); + + // Update sensor display connection status + if (window.PyrrhaWatch) { + window.PyrrhaWatch.updateConnectionStatus(false); + } + + } catch (error) { + console.error("Error during disconnect:", error); + createHTML(`Disconnect error: ${error.message}`); } - } catch (err) { - console.log("exception [" + err.name + "] msg[" + err.message + "]"); - } -} +}; /** - * Create popup content to show messages in the UI. + * Modern UI feedback with enhanced accessibility. */ -function createHTML(logString) { - var content = document.getElementById("toast-content"); - content.textContent = logString; - tau.openPopup("#toast"); -} +const createHTML = (message, type = "info") => { + try { + const content = document.getElementById("toast-content"); + if (content) { + content.textContent = message; + content.setAttribute("role", "alert"); + content.setAttribute("aria-live", "polite"); -/* ----------- Bluetooth callbacks ----------- */ + // Add visual feedback based on message type + content.className = `toast-${type}`; + + if (typeof tau !== "undefined") { + tau.openPopup("#toast"); + } + + console.log(`UI Message [${type}]: ${message}`); + } + } catch (error) { + console.error("Error displaying message:", error); + } +}; + +/** + * Update connect button state. + */ +const updateConnectButton = (text) => { + try { + const connectButton = document.getElementById("connect"); + if (connectButton) { + connectButton.textContent = text; + connectButton.disabled = (text === "Connecting..."); + } + } catch (error) { + console.error("Error updating connect button:", error); + } +}; + +/* ----------- Enhanced Samsung Accessory Protocol callbacks ----------- */ /** - * Callback for connecting to provider. + * Modern callback for connecting to mobile app provider. */ -var agentCallback = { - onconnect: function (socket) { - SASocket = socket; - createHTML("PyrrhaWatch Connection established with RemotePeer"); - SASocket.setSocketStatusListener(function (reason) { - console.log("Service connection lost, Reason : [" + reason + "]"); - disconnect(); - }); - SASocket.setDataReceiveListener(onreceive); - }, - onerror: onerror, +const agentCallback = { + onconnect: (socket) => { + try { + SASocket = socket; + connectionState = "connected"; + reconnectAttempts = 0; + + updateConnectButton("Disconnect"); + createHTML("Connected to Pyrrha mobile app!", "success"); + + // Set up connection monitoring + SASocket.setSocketStatusListener((reason) => { + console.warn(`Connection lost, reason: ${reason}`); + handleConnectionLoss(reason); + }); + + // Set up data reception with enhanced parsing + SASocket.setDataReceiveListener(onDataReceive); + + // Update global connection status + if (window.PyrrhaWatch) { + window.PyrrhaWatch.updateConnectionStatus(true); + } + + // Request initial sensor data + setTimeout(requestSensorData, 1000); + + } catch (error) { + console.error("Error in onconnect callback:", error); + onerror(error); + } + }, + onerror: onerror }; /** - * Callback for finding provider. + * Enhanced callback for finding mobile app provider. */ -var peerAgentFindCallback = { - onpeeragentfound: function (peerAgent) { +const peerAgentFindCallback = { + onpeeragentfound: (peerAgent) => { + try { + console.log(`Found peer agent: ${peerAgent.appName}`); + + if (peerAgent.appName === ProviderAppName) { + SAAgent.setServiceConnectionListener(agentCallback); + SAAgent.requestServiceConnection(peerAgent); + createHTML(`Connecting to ${ProviderAppName}...`, "info"); + } else { + console.warn(`Unexpected app found: ${peerAgent.appName}`); + createHTML(`Found ${peerAgent.appName}, looking for ${ProviderAppName}`, "warning"); + } + } catch (error) { + console.error("Error in peer agent found callback:", error); + onerror(error); + } + }, + onerror: onerror +}; + +/* ----------- Enhanced Samsung Accessory Protocol event handlers ----------- */ + +/** + * Handle successful SA Agent acquisition. + */ +const onsuccess = (agents, resolve, reject) => { try { - if (peerAgent.appName == ProviderAppName) { - SAAgent.setServiceConnectionListener(agentCallback); - SAAgent.requestServiceConnection(peerAgent); - } else { - createHTML("Not expected app!! : " + peerAgent.appName); - } - } catch (err) { - console.log("exception [" + err.name + "] msg[" + err.message + "]"); + console.log(`Found ${agents.length} SA agents`); + + if (agents.length > 0) { + SAAgent = agents[0]; + SAAgent.setPeerAgentFindListener(peerAgentFindCallback); + SAAgent.findPeerAgents(); + + createHTML("Searching for mobile app...", "info"); + resolve(agents); + } else { + const error = new Error("No Samsung Accessory agents found"); + console.error(error.message); + reject(error); + } + } catch (error) { + console.error("Error in onsuccess handler:", error); + reject(error); + } +}; + +/** + * Enhanced error handler with recovery logic. + */ +const onerror = (error) => { + console.error("Samsung Accessory Protocol error:", error); + + connectionState = "disconnected"; + updateConnectButton("Connect"); + + const message = error.message || error.name || "Unknown connection error"; + createHTML(`Connection error: ${message}`, "error"); + + // Reset connection state + if (SASocket) { + SASocket = null; + } + if (SAAgent) { + SAAgent = null; + } + + // Update global connection status + if (window.PyrrhaWatch) { + window.PyrrhaWatch.updateConnectionStatus(false); + } +}; + +/** + * Handle connection loss with reconnection logic. + */ +const handleConnectionLoss = (reason) => { + console.warn(`Connection lost: ${reason}`); + + connectionState = "disconnected"; + updateConnectButton("Connect"); + createHTML(`Connection lost: ${reason}`, "warning"); + + // Clean up + SASocket = null; + + // Update global connection status + if (window.PyrrhaWatch) { + window.PyrrhaWatch.updateConnectionStatus(false); + } + + // Auto-reconnect after delay + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + setTimeout(() => { + createHTML("Attempting to reconnect...", "info"); + connect(); + }, RECONNECT_DELAY); } - }, - onerror: onerror, }; -/* ----------- Bluetooth event handlers ----------- */ +/** + * Enhanced message handler with JSON parsing and sensor data processing. + */ +const onDataReceive = (channelId, data) => { + try { + console.log(`Received data on channel ${channelId}:`, data); + + // Try to parse JSON data from mobile app + let parsedData; + try { + parsedData = JSON.parse(data); + } catch (parseError) { + // Handle plain text messages + createHTML(data, "info"); + return; + } + + // Handle different types of messages + switch (parsedData.type) { + case "sensor_data": + handleSensorData(parsedData); + break; + case "alert": + handleAlert(parsedData); + break; + case "status": + handleStatusUpdate(parsedData); + break; + default: + console.log("Unknown message type:", parsedData.type); + createHTML(`Received: ${parsedData.message || data}`, "info"); + } + + } catch (error) { + console.error("Error processing received data:", error); + createHTML(`Data processing error: ${error.message}`, "error"); + } +}; /** - * Handle success. + * Handle sensor data from mobile app. */ -function onsuccess(agents) { - try { - if (agents.length > 0) { - SAAgent = agents[0]; - SAAgent.setPeerAgentFindListener(peerAgentFindCallback); - SAAgent.findPeerAgents(); - document.getElementById("connect").innerText = "Disconnect"; - } else { - createHTML("Not found SAAgent!!"); +const handleSensorData = (data) => { + try { + console.log("Processing sensor data:", data); + + // Update sensor displays if we have the data + if (data.sensors) { + // TODO: Update actual sensor displays with real data + // This will replace the simulated data in pyrrha.js + createHTML("Sensor data updated", "success"); + } + } catch (error) { + console.error("Error handling sensor data:", error); } - } catch (err) { - console.log("exception [" + err.name + "] msg[" + err.message + "]"); - } -} +}; /** - * Log errors. + * Handle alert messages from mobile app. */ -function onerror(err) { - console.log("err [" + err + "]"); - document.getElementById("connect").innerText = "Connect"; -} +const handleAlert = (data) => { + try { + console.log("Processing alert:", data); + + if (window.PyrrhaWatch && window.PyrrhaWatch.sendNotification) { + window.PyrrhaWatch.sendNotification(data.message, data.severity || "warning"); + } else { + createHTML(`Alert: ${data.message}`, "error"); + } + } catch (error) { + console.error("Error handling alert:", error); + } +}; /** - * Message handler. + * Handle status updates from mobile app. */ -function onreceive(channelId, data) { - createHTML(data); -} +const handleStatusUpdate = (data) => { + try { + console.log("Processing status update:", data); + createHTML(`Status: ${data.message}`, "info"); + } catch (error) { + console.error("Error handling status update:", error); + } +}; + +// Expose functions for global access +window.PyrrhaMobileConnection = { + connect, + disconnect, + requestSensorData, + connectionState: () => connectionState, + isConnected: () => connectionState === "connected" +}; diff --git a/js/constants.js b/js/constants.js index 78e8765..15964f9 100644 --- a/js/constants.js +++ b/js/constants.js @@ -1,14 +1,109 @@ -// Should be consistent with -// https://github.com/Pyrrha-Platform/Pyrrha-Dashboard/blob/main/pyrrha-dashboard/src/utils/Constants.js -const TMP_RED = 32; -const HUM_RED = 80; -const CO_RED = 420; -const NO2_RED = 8; - -// UI settings -const useToast = true; -const notifyTmpHum = false; - -// Bluetooth settings -const CHANNELID = 104; -const ProviderAppName = "PyrrhaMobileProvider"; +/** + * Pyrrha Watch App Constants - Galaxy Watch 3 Edition + * + * Should be consistent with: + * https://github.com/Pyrrha-Platform/Pyrrha-Dashboard/blob/main/pyrrha-dashboard/src/utils/Constants.js + */ + +// Sensor threshold constants (matching Dashboard values) +const SENSOR_THRESHOLDS = { + // Temperature threshold in Celsius + TMP_RED: 32, + // Humidity threshold in percentage + HUM_RED: 80, + // Carbon Monoxide threshold in ppm + CO_RED: 420, + // Nitrogen Dioxide threshold in ppm + NO2_RED: 8 +}; + +// Legacy constants for backward compatibility +const TMP_RED = SENSOR_THRESHOLDS.TMP_RED; +const HUM_RED = SENSOR_THRESHOLDS.HUM_RED; +const CO_RED = SENSOR_THRESHOLDS.CO_RED; +const NO2_RED = SENSOR_THRESHOLDS.NO2_RED; + +// UI Configuration for Galaxy Watch 3 +const UI_CONFIG = { + // Use toast notifications (true) or Tizen notifications (false) + useToast: true, + // Enable temperature and humidity notifications + notifyTmpHum: false, + // Update intervals in milliseconds + sensorUpdateInterval: 3000, + clockUpdateInterval: 1000, + // Notification settings + notificationCooldown: 30000, // 30 seconds between same type notifications + maxReconnectAttempts: 3, + reconnectDelay: 5000 // 5 seconds +}; + +// Legacy UI settings for backward compatibility +const useToast = UI_CONFIG.useToast; +const notifyTmpHum = UI_CONFIG.notifyTmpHum; + +// Samsung Accessory Protocol Configuration +const ACCESSORY_CONFIG = { + // Communication channel ID + channelId: 104, + // Expected mobile app provider name + providerAppName: "PyrrhaMobileProvider", + // Protocol version + protocolVersion: "2.0", + // Service profile + serviceProfile: "/org/pyrrha-platform/readings" +}; + +// Legacy Bluetooth settings for backward compatibility +const CHANNELID = ACCESSORY_CONFIG.channelId; +const ProviderAppName = ACCESSORY_CONFIG.providerAppName; + +// Message Types for Samsung Accessory Protocol +const MESSAGE_TYPES = { + SENSOR_REQUEST: "sensor_request", + SENSOR_DATA: "sensor_data", + ALERT: "alert", + STATUS: "status", + HEARTBEAT: "heartbeat" +}; + +// Watch-specific Configuration +const WATCH_CONFIG = { + // Galaxy Watch 3 specific features + supportsRotatingBezel: true, + supportsCircularUI: true, + supportsAdvancedHaptics: true, + // Display settings + screenShape: "circle", + screenSize: "normal", + // Power management + backgroundSupport: true, + // Sensor data retention + maxDataPoints: 100, + dataRetentionHours: 24 +}; + +// App Information +const APP_INFO = { + name: "Pyrrha", + version: "2.0.0", + targetPlatform: "Galaxy Watch 3", + tizenVersion: "5.5", + buildDate: new Date().toISOString().split("T")[0] +}; + +// Export configuration for use in other modules +if (typeof module !== "undefined" && module.exports) { + module.exports = { + SENSOR_THRESHOLDS, + UI_CONFIG, + ACCESSORY_CONFIG, + MESSAGE_TYPES, + WATCH_CONFIG, + APP_INFO, + // Legacy exports + TMP_RED, HUM_RED, CO_RED, NO2_RED, + useToast, notifyTmpHum, + CHANNELID, ProviderAppName + }; +} diff --git a/js/control.js b/js/control.js index 6325f3a..e3e36c3 100644 --- a/js/control.js +++ b/js/control.js @@ -1,52 +1,374 @@ /** - * Manage the screens. + * Pyrrha Watch App Control System - Galaxy Watch 3 Edition + * + * Enhanced screen management, hardware key handling, and rotating bezel support. */ -(function () { - window.addEventListener("tizenhwkey", function (ev) { - if (ev.keyName === "back") { - var page = document.getElementsByClassName("ui-page-active")[0], - pageid = page ? page.id : ""; - - if (pageid === "main") { - try { - tizen.application.getCurrentApplication().exit(); - } catch (ignore) {} - } else { - window.history.back(); - } + +(() => { + "use strict"; + + let isAppActive = true; + let rotaryFocusIndex = 0; + const focusableElements = []; + + /** + * Enhanced hardware key management for Galaxy Watch 3. + */ + const initializeHardwareKeys = () => { + try { + window.addEventListener("tizenhwkey", (event) => { + console.log(`Hardware key pressed: ${event.keyName}`); + + switch (event.keyName) { + case "back": + handleBackKey(event); + break; + case "menu": + handleMenuKey(event); + break; + default: + console.log(`Unhandled key: ${event.keyName}`); + } + }); + + console.log("Hardware key listeners initialized"); + } catch (error) { + console.error("Error initializing hardware keys:", error); + } + }; + + /** + * Handle back key press with proper navigation. + */ + const handleBackKey = (event) => { + try { + const activePage = document.getElementsByClassName("ui-page-active")[0]; + const pageId = activePage ? activePage.id : ""; + + // Close any open popups first + if (typeof tau !== "undefined" && tau.isPopupActive && tau.isPopupActive()) { + tau.closePopup(); + return; + } + + // If on main page, exit app + if (pageId === "details" || pageId === "main" || !pageId) { + try { + if (typeof tizen !== "undefined" && tizen.application) { + tizen.application.getCurrentApplication().exit(); + } + } catch (exitError) { + console.warn("Could not exit application:", exitError); + } + } else { + window.history.back(); + } + } catch (error) { + console.error("Error handling back key:", error); + } + }; + + /** + * Handle menu key press for quick actions. + */ + const handleMenuKey = (event) => { + try { + // Toggle connection or show quick menu + if (window.PyrrhaMobileConnection) { + if (window.PyrrhaMobileConnection.isConnected()) { + window.PyrrhaMobileConnection.requestSensorData(); + } else { + window.PyrrhaMobileConnection.connect(); + } + } + } catch (error) { + console.error("Error handling menu key:", error); + } + }; + + /** + * Enhanced power management for Galaxy Watch 3. + */ + const initializePowerManagement = () => { + try { + if (typeof tizen === "undefined" || !tizen.power) { + console.warn("Tizen power API not available"); + return; + } + + // Screen state change listener + tizen.power.setScreenStateChangeListener((prevState, currState) => { + console.log(`Screen state changed: ${prevState} → ${currState}`); + + switch (currState) { + case "SCREEN_NORMAL": + handleScreenWake(prevState); + break; + case "SCREEN_DIM": + handleScreenDim(); + break; + case "SCREEN_OFF": + handleScreenOff(); + break; + } + }); + + // Screen brightness change listener + tizen.power.setScreenBrightnessChangeListener((brightness) => { + console.log(`Screen brightness changed: ${brightness}`); + adjustUIForBrightness(brightness); + }); + + console.log("Power management initialized"); + } catch (error) { + console.error("Error initializing power management:", error); + } + }; + + /** + * Handle screen wake with app refresh. + */ + const handleScreenWake = (prevState) => { + try { + isAppActive = true; + + // Refresh sensor data when screen wakes up + if (window.PyrrhaWatch && window.PyrrhaWatch.setSensorValues) { + setTimeout(() => { + window.PyrrhaWatch.setSensorValues(); + }, 500); + } + + // Request fresh data from mobile app if connected + if (window.PyrrhaMobileConnection && window.PyrrhaMobileConnection.isConnected()) { + setTimeout(() => { + window.PyrrhaMobileConnection.requestSensorData(); + }, 1000); + } + + console.log("App refreshed on screen wake"); + } catch (error) { + console.error("Error handling screen wake:", error); + } + }; + + /** + * Handle screen dimming. + */ + const handleScreenDim = () => { + isAppActive = false; + console.log("Screen dimmed, reducing activity"); + }; + + /** + * Handle screen off. + */ + const handleScreenOff = () => { + isAppActive = false; + console.log("Screen off, app backgrounded"); + }; + + /** + * Adjust UI based on screen brightness. + */ + const adjustUIForBrightness = (brightness) => { + try { + const root = document.documentElement; + + if (brightness < 0.3) { + // Very dim - enhance contrast + root.style.setProperty("--primary-text", "#ffffff"); + root.style.setProperty("--secondary-text", "#eeeeee"); + } else if (brightness > 0.8) { + // Very bright - reduce harsh contrasts + root.style.setProperty("--primary-text", "#f0f0f0"); + root.style.setProperty("--secondary-text", "#cccccc"); + } else { + // Normal brightness - default values + root.style.setProperty("--primary-text", "#ffffff"); + root.style.setProperty("--secondary-text", "#cccccc"); + } + } catch (error) { + console.error("Error adjusting UI for brightness:", error); + } + }; + + /** + * Initialize rotating bezel support for Galaxy Watch 3. + */ + const initializeRotaryNavigation = () => { + try { + if (typeof tau === "undefined") { + console.warn("TAU framework not available for rotary navigation"); + return; + } + + // Add rotary event listener + document.addEventListener("rotarydetent", (event) => { + handleRotaryInput(event.detail.direction); + }); + + // Initialize focusable elements + updateFocusableElements(); + + console.log("Rotary navigation initialized"); + } catch (error) { + console.error("Error initializing rotary navigation:", error); + } + }; + + /** + * Handle rotating bezel input. + */ + const handleRotaryInput = (direction) => { + try { + if (focusableElements.length === 0) { + updateFocusableElements(); + return; + } + + // Remove current focus + if (focusableElements[rotaryFocusIndex]) { + focusableElements[rotaryFocusIndex].classList.remove("rotary-focus"); + } + + // Update focus index + if (direction === "CW") { + rotaryFocusIndex = (rotaryFocusIndex + 1) % focusableElements.length; + } else { + rotaryFocusIndex = (rotaryFocusIndex - 1 + focusableElements.length) % focusableElements.length; + } + + // Apply new focus + if (focusableElements[rotaryFocusIndex]) { + focusableElements[rotaryFocusIndex].classList.add("rotary-focus"); + focusableElements[rotaryFocusIndex].focus(); + } + + console.log(`Rotary focus: ${rotaryFocusIndex}/${focusableElements.length}`); + } catch (error) { + console.error("Error handling rotary input:", error); + } + }; + + /** + * Update list of focusable elements. + */ + const updateFocusableElements = () => { + try { + focusableElements.length = 0; + + const selectors = [ + "#device-clock", + "table tr", + "#connect", + "#fetch" + ]; + + selectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(element => { + if (element.offsetParent !== null) { // visible elements only + focusableElements.push(element); + } + }); + }); + + console.log(`Found ${focusableElements.length} focusable elements`); + } catch (error) { + console.error("Error updating focusable elements:", error); + } + }; + + /** + * Enhanced toast notification system. + */ + const initializeToastSystem = () => { + try { + const toastPopup = document.getElementById("toast"); + + if (toastPopup) { + toastPopup.addEventListener("popupshow", (event) => { + // Auto-close toast after 4 seconds + setTimeout(() => { + if (typeof tau !== "undefined") { + tau.closePopup("#toast"); + } + }, 4000); + }); + + // Handle toast tap to dismiss + toastPopup.addEventListener("click", () => { + if (typeof tau !== "undefined") { + tau.closePopup("#toast"); + } + }); + } + + console.log("Toast system initialized"); + } catch (error) { + console.error("Error initializing toast system:", error); + } + }; + + /** + * Update connection status indicator. + */ + const updateConnectionIndicator = (connected, connecting = false) => { + try { + const indicator = document.getElementById("connection-status"); + if (indicator) { + indicator.classList.remove("connected", "connecting"); + + if (connecting) { + indicator.classList.add("connecting"); + indicator.setAttribute("aria-label", "Connecting to mobile app"); + } else if (connected) { + indicator.classList.add("connected"); + indicator.setAttribute("aria-label", "Connected to mobile app"); + } else { + indicator.setAttribute("aria-label", "Disconnected from mobile app"); + } + } + } catch (error) { + console.error("Error updating connection indicator:", error); + } + }; + + /** + * Initialize all control systems. + */ + const initialize = () => { + try { + console.log("Initializing Pyrrha Watch App controls..."); + + initializeHardwareKeys(); + initializePowerManagement(); + initializeRotaryNavigation(); + initializeToastSystem(); + + // Set up periodic UI updates + setInterval(updateFocusableElements, 10000); // Update every 10 seconds + + console.log("All control systems initialized successfully"); + } catch (error) { + console.error("Error during control system initialization:", error); + } + }; + + // Initialize when DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initialize); + } else { + initialize(); } - }); -})(); -/** - * Keep the app from going to sleep. - */ -(function () { - try { - tizen.power.setScreenStateChangeListener(function (prevState, currState) { - if (currState === "SCREEN_NORMAL" && prevState === "SCREEN_OFF") { - // On screen wake - var app = tizen.application.getCurrentApplication(); - tizen.application.launch(app.appInfo.id, function () { - // You can do something here when your app has launched - }); - } - }); - } catch (e) {} -})(); + // Expose control functions globally + window.PyrrhaWatchControl = { + updateConnectionIndicator, + isAppActive: () => isAppActive, + refreshFocusableElements: updateFocusableElements, + handleRotaryInput + }; -/** - * Handle toast alerts. - */ -(function (tau) { - var toastPopup = document.getElementById("toast"); - toastPopup.addEventListener( - "popupshow", - function (ev) { - setTimeout(function () { - tau.closePopup(); - }, 3000); - }, - false - ); -})(window.tau); +})(); diff --git a/js/low-battery-check.js b/js/low-battery-check.js index 52e63f5..d8e0fe9 100644 --- a/js/low-battery-check.js +++ b/js/low-battery-check.js @@ -1,60 +1,430 @@ -(function () { - var systeminfo = { - systeminfo: null, - - lowThreshold: 0.04, - - listenBatteryLowState: function () { - var self = this; - - try { - this.systeminfo.addPropertyValueChangeListener( - "BATTERY", - function change(battery) { - if (!battery.isCharging) { - try { - tizen.application.getCurrentApplication().exit(); - } catch (ignore) {} - } - }, - { - lowThreshold: self.lowThreshold, - }, - function onError(error) { - console.warn("An error occurred " + error.message); +/** + * Battery Management System - Galaxy Watch 3 Edition + * Enhanced power monitoring with modern JavaScript and better user experience + */ + +(() => { + "use strict"; + + /** + * Battery monitoring configuration for Galaxy Watch 3 + */ + const BATTERY_CONFIG = { + // Critical battery level (4% = 0.04) + criticalThreshold: 0.04, + // Low battery warning level (15% = 0.15) + lowThreshold: 0.15, + // Very low battery level (8% = 0.08) + veryLowThreshold: 0.08, + // Check interval in milliseconds + checkInterval: 30000, // 30 seconds + // Grace period before exit (in milliseconds) + exitGracePeriod: 10000 // 10 seconds + }; + + let batteryMonitor = null; + let lastBatteryLevel = 1.0; + let lowBatteryWarningShown = false; + let exitTimer = null; + + /** + * Enhanced battery monitoring system + */ + class BatteryMonitor { + constructor() { + this.systemInfo = null; + this.isInitialized = false; + this.listeners = []; + } + + /** + * Initialize battery monitoring with enhanced features + */ + async initialize() { + try { + if (typeof tizen !== "object" || !tizen.systeminfo) { + console.warn("Tizen system info not available - battery monitoring disabled"); + return false; + } + + this.systemInfo = tizen.systeminfo; + this.isInitialized = true; + + // Initial battery check + await this.checkCurrentBatteryState(); + + // Set up continuous monitoring + this.setupBatteryListener(); + this.startPeriodicCheck(); + + console.log("Battery monitoring initialized successfully"); + return true; + + } catch (error) { + console.error("Error initializing battery monitor:", error); + return false; + } + } + + /** + * Check current battery state with detailed logging + */ + async checkCurrentBatteryState() { + return new Promise((resolve, reject) => { + try { + this.systemInfo.getPropertyValue( + "BATTERY", + (battery) => { + this.processBatteryUpdate(battery); + resolve(battery); + }, + (error) => { + console.error("Error getting battery state:", error); + reject(error); + } + ); + } catch (error) { + console.error("Exception checking battery state:", error); + reject(error); + } + }); + } + + /** + * Setup battery change listener with enhanced monitoring + */ + setupBatteryListener() { + try { + // Listen for critical battery level + const criticalListener = this.systemInfo.addPropertyValueChangeListener( + "BATTERY", + (battery) => this.handleCriticalBattery(battery), + { lowThreshold: BATTERY_CONFIG.criticalThreshold }, + (error) => console.error("Critical battery listener error:", error) + ); + + // Listen for low battery level + const lowListener = this.systemInfo.addPropertyValueChangeListener( + "BATTERY", + (battery) => this.handleLowBattery(battery), + { lowThreshold: BATTERY_CONFIG.lowThreshold }, + (error) => console.error("Low battery listener error:", error) + ); + + // Listen for general battery changes + const generalListener = this.systemInfo.addPropertyValueChangeListener( + "BATTERY", + (battery) => this.processBatteryUpdate(battery), + {}, + (error) => console.error("Battery listener error:", error) + ); + + this.listeners.push(criticalListener, lowListener, generalListener); + console.log("Battery listeners setup successfully"); + + } catch (error) { + console.error("Error setting up battery listeners:", error); + } + } + + /** + * Start periodic battery check + */ + startPeriodicCheck() { + setInterval(() => { + if (this.isInitialized) { + this.checkCurrentBatteryState().catch(error => { + console.warn("Periodic battery check failed:", error); + }); + } + }, BATTERY_CONFIG.checkInterval); + } + + /** + * Process battery update with comprehensive handling + */ + processBatteryUpdate(battery) { + try { + const level = Math.round(battery.level * 100); + const isCharging = battery.isCharging; + const hasChanged = Math.abs(battery.level - lastBatteryLevel) > 0.01; + + if (hasChanged) { + console.log(`Battery: ${level}% (${isCharging ? "charging" : "discharging"})`); + lastBatteryLevel = battery.level; + } + + // Update UI battery indicator if available + this.updateBatteryUI(battery); + + // Handle different battery states + if (battery.level <= BATTERY_CONFIG.criticalThreshold && !isCharging) { + this.handleCriticalBattery(battery); + } else if (battery.level <= BATTERY_CONFIG.veryLowThreshold && !isCharging) { + this.handleVeryLowBattery(battery); + } else if (battery.level <= BATTERY_CONFIG.lowThreshold && !isCharging) { + this.handleLowBattery(battery); + } else if (isCharging && lowBatteryWarningShown) { + this.handleChargingStarted(battery); + } + + } catch (error) { + console.error("Error processing battery update:", error); + } + } + + /** + * Handle critical battery level with graceful shutdown + */ + handleCriticalBattery(battery) { + try { + const level = Math.round(battery.level * 100); + console.warn(`Critical battery level: ${level}%`); + + // Show critical battery notification + this.showBatteryNotification( + `Critical battery: ${level}%\nApp will close in 10 seconds`, + "critical" + ); + + // Graceful shutdown with delay + if (exitTimer) { + clearTimeout(exitTimer); + } + + exitTimer = setTimeout(() => { + try { + console.log("Exiting app due to critical battery level"); + if (tizen && tizen.application) { + tizen.application.getCurrentApplication().exit(); + } + } catch (error) { + console.error("Error exiting application:", error); + } + }, BATTERY_CONFIG.exitGracePeriod); + + } catch (error) { + console.error("Error handling critical battery:", error); + } + } + + /** + * Handle very low battery level + */ + handleVeryLowBattery(battery) { + try { + const level = Math.round(battery.level * 100); + console.warn(`Very low battery: ${level}%`); + + this.showBatteryNotification( + `Very low battery: ${level}%\nPlease charge soon`, + "warning" + ); + + // Reduce app activity to conserve power + this.enablePowerSaveMode(); + + } catch (error) { + console.error("Error handling very low battery:", error); + } + } + + /** + * Handle low battery level with user notification + */ + handleLowBattery(battery) { + try { + const level = Math.round(battery.level * 100); + + if (!lowBatteryWarningShown) { + console.warn(`Low battery warning: ${level}%`); + + this.showBatteryNotification( + `Low battery: ${level}%\nConsider charging`, + "info" + ); + + lowBatteryWarningShown = true; + } + + } catch (error) { + console.error("Error handling low battery:", error); + } + } + + /** + * Handle charging started + */ + handleChargingStarted(battery) { + try { + console.log("Charging started, clearing low battery warnings"); + + lowBatteryWarningShown = false; + + if (exitTimer) { + clearTimeout(exitTimer); + exitTimer = null; + } + + // Show charging notification + this.showBatteryNotification("Charging started", "success"); + + // Disable power save mode + this.disablePowerSaveMode(); + + } catch (error) { + console.error("Error handling charging started:", error); + } + } + + /** + * Show battery notification to user + */ + showBatteryNotification(message, type = "info") { + try { + // Use app's notification system if available + if (window.PyrrhaWatch && window.PyrrhaWatch.sendNotification) { + window.PyrrhaWatch.sendNotification(message, type); + } else if (typeof createHTML === "function") { + createHTML(message, type); + } else { + console.log(`Battery notification: ${message}`); + } + } catch (error) { + console.error("Error showing battery notification:", error); + } + } + + /** + * Enable power save mode + */ + enablePowerSaveMode() { + try { + console.log("Enabling power save mode"); + + // Reduce update frequency + if (window.PyrrhaWatch) { + // Could implement reduced update intervals here + } + + // Dim display elements + document.body.classList.add("power-save-mode"); + + } catch (error) { + console.error("Error enabling power save mode:", error); + } + } + + /** + * Disable power save mode + */ + disablePowerSaveMode() { + try { + console.log("Disabling power save mode"); + + // Restore normal operation + document.body.classList.remove("power-save-mode"); + + } catch (error) { + console.error("Error disabling power save mode:", error); + } + } + + /** + * Update battery UI indicator + */ + updateBatteryUI(battery) { + try { + // Add battery level to connection status area if desired + const statusElement = document.getElementById("connection-status"); + if (statusElement) { + const level = Math.round(battery.level * 100); + statusElement.setAttribute("title", + `Battery: ${level}% (${battery.isCharging ? "charging" : "discharging"})`); + } + } catch (error) { + console.error("Error updating battery UI:", error); + } + } + + /** + * Clean up battery monitoring + */ + cleanup() { + try { + if (exitTimer) { + clearTimeout(exitTimer); + exitTimer = null; + } + + // Remove listeners if possible + this.listeners.forEach(listener => { + try { + // Tizen doesn't provide a direct way to remove listeners + // but they'll be cleaned up when the app exits + } catch (error) { + console.warn("Error removing battery listener:", error); + } + }); + + this.isInitialized = false; + console.log("Battery monitor cleaned up"); + + } catch (error) { + console.error("Error during battery monitor cleanup:", error); + } + } + } + + /** + * Initialize battery monitoring system + */ + const initializeBatteryMonitoring = async () => { + try { + batteryMonitor = new BatteryMonitor(); + const success = await batteryMonitor.initialize(); + + if (success) { + console.log("Advanced battery monitoring active"); + + // Add power save mode CSS + const style = document.createElement("style"); + style.textContent = ` + .power-save-mode { + filter: brightness(0.7); + transition: filter 0.5s ease; + } + .power-save-mode * { + animation-duration: 2s !important; } - ); - } catch (ignore) {} - }, - - checkBatteryLowState: function () { - var self = this; - - try { - this.systeminfo.getPropertyValue( - "BATTERY", - function (battery) { - if (battery.level < self.lowThreshold && !battery.isCharging) { - try { - tizen.application.getCurrentApplication().exit(); - } catch (ignore) {} - } - }, - null - ); - } catch (ignore) {} - }, - - init: function () { - if (typeof tizen === "object" && typeof tizen.systeminfo === "object") { - this.systeminfo = tizen.systeminfo; - this.checkBatteryLowState(); - this.listenBatteryLowState(); - } else { - console.warn("tizen.systeminfo is not available."); - } - }, - }; - - systeminfo.init(); + `; + document.head.appendChild(style); + } + + } catch (error) { + console.error("Failed to initialize battery monitoring:", error); + } + }; + + // Initialize when DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializeBatteryMonitoring); + } else { + initializeBatteryMonitoring(); + } + + // Clean up on page unload + window.addEventListener("beforeunload", () => { + if (batteryMonitor) { + batteryMonitor.cleanup(); + } + }); + + // Expose battery monitor for external access + window.PyrrhaWatchBattery = { + getMonitor: () => batteryMonitor, + isLowBattery: () => lastBatteryLevel <= BATTERY_CONFIG.lowThreshold, + getCurrentLevel: () => Math.round(lastBatteryLevel * 100) + }; + })(); diff --git a/js/pyrrha.js b/js/pyrrha.js index 6dce818..91dc6aa 100644 --- a/js/pyrrha.js +++ b/js/pyrrha.js @@ -2,126 +2,230 @@ * Pyrrha Tizen Web API code. * * For providing haptic alerts when thresholds are breached and regular readings. + * Updated for Galaxy Watch 3 with modern JavaScript (ES6+) and improved error handling. */ /** - * Display the clock. + * Display the clock with modern JavaScript. */ -(function () { - "use strict"; - - function setTime() { - var date = new Date(), - hours = date.getHours(), - minutes = - date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(), - ampm = hours >= 12 ? "PM" : "AM", - time = ""; - - hours = hours < 10 ? "0" + hours : hours; - time = hours + ":" + minutes + ampm; - - document.getElementById("device-clock").innerText = time; - - window.setTimeout(setTime, 1000); - } - setTime(); +(() => { + "use strict"; + + const setTime = () => { + try { + const date = new Date(); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const ampm = date.getHours() >= 12 ? "PM" : "AM"; + const time = `${hours}:${minutes}${ampm}`; + + const clockElement = document.getElementById("device-clock"); + if (clockElement) { + clockElement.textContent = time; + } + + setTimeout(setTime, 1000); + } catch (error) { + console.error("Error updating clock:", error); + setTimeout(setTime, 5000); // Retry after 5 seconds on error + } + }; + + // Initialize clock when DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setTime); + } else { + setTime(); + } })(); /** - * Receive readings and provide notifications as needed. + * Sensor readings and notification system with modern JavaScript. */ -(function () { - "use strict"; - - function sendNotification(message) { - console.log("sendNotification [" + message + "]"); - - if (useToast) { - createHTML(message); - } else { - var notificationGroupDict = { - content: message, - images: { - iconPath: "img/pyrrha-watch-icon.png", - }, - actions: { - vibration: true, - }, - }; - - var notification = new tizen.UserNotification( - "SIMPLE", - "Limit alert", - notificationGroupDict - ); - - tizen.notification.post(notification); - } - } - - function setSensorValues() { - console.log("setSensorValues"); - - // Get a reference to the UI widgets - var displayCo = document.getElementById("display-co"); - var displayNo2 = document.getElementById("display-no2"); - var displayTmp = document.getElementById("display-tmp"); - var displayHum = document.getElementById("display-hum"); - - // To simulate for local testing, set up some random values to display and log. - var readingCo = (Math.random() * 150).toFixed(1); - var readingNo2 = (Math.random() * 10).toFixed(1); - var readingTmp = (Math.random() * 50).toFixed(1); - var readingHum = (Math.random() * 100).toFixed(1); - console.log(readingCo); - console.log(readingNo2); - console.log(readingTmp); - console.log(readingHum); - - // Set the values - displayCo.innerText = readingCo; - displayNo2.innerText = readingNo2; - displayTmp.innerText = readingTmp; - displayHum.innerText = readingHum; - - // TODO: Get real-time values from Bluetooth - /* - - */ - - // Send notifications if readings are too high - if (parseInt(displayCo.innerText) >= CO_RED) { - displayCo.className = "color-red"; - sendNotification("CO high"); +(() => { + "use strict"; + + // State management + let isConnectedToMobile = false; + const lastNotificationTime = {}; + const NOTIFICATION_COOLDOWN = 30000; // 30 seconds between same type notifications + + /** + * Send notification with haptic feedback and rate limiting. + */ + const sendNotification = async (message, type = "warning") => { + try { + console.log(`sendNotification [${message}] type [${type}]`); + + // Rate limiting to prevent notification spam + const now = Date.now(); + if (lastNotificationTime[type] && (now - lastNotificationTime[type] < NOTIFICATION_COOLDOWN)) { + console.log(`Notification ${type} rate limited`); + return; + } + lastNotificationTime[type] = now; + + // Enhanced haptic feedback for Galaxy Watch 3 + if (typeof tizen !== "undefined" && tizen.feedback) { + tizen.feedback.play("VIBRATION_WARNING"); + } + + if (useToast) { + createHTML(message); + } else { + const notificationDict = { + content: message, + images: { + iconPath: "pyrrha-watch-icon.png" + }, + actions: { + vibration: true + }, + vibration: true + }; + + const notification = new tizen.UserNotification( + "SIMPLE", + "Pyrrha Safety Alert", + notificationDict + ); + + tizen.notification.post(notification); + } + } catch (error) { + console.error("Error sending notification:", error); + } + }; + + /** + * Update sensor display with proper error handling and validation. + */ + const updateSensorDisplay = (element, value, threshold, sensor, unit = "") => { + try { + if (!element) { + console.warn(`Element not found for sensor ${sensor}`); + return false; + } + + const numericValue = parseFloat(value); + if (isNaN(numericValue)) { + console.warn(`Invalid value for ${sensor}: ${value}`); + element.textContent = "--"; + element.className = "color-yellow"; + return false; + } + + element.textContent = numericValue.toFixed(1); + + if (numericValue >= threshold) { + element.className = "color-red reading"; + sendNotification(`${sensor} level high: ${numericValue.toFixed(1)}${unit}`, sensor); + return true; // Alert condition + } else { + element.className = "color-green reading"; + return false; // Normal condition + } + } catch (error) { + console.error(`Error updating ${sensor} display:`, error); + return false; + } + }; + + /** + * Generate realistic test data based on time patterns. + */ + const generateTestData = () => { + const hour = new Date().getHours(); + const minute = new Date().getMinutes(); + + // Simulate more realistic patterns - higher readings during "emergency" periods + const isEmergencyTime = (hour >= 14 && hour <= 16) || (minute >= 45 && minute <= 59); + const baseMultiplier = isEmergencyTime ? 2.5 : 1.0; + + return { + co: (Math.random() * 100 * baseMultiplier + 50).toFixed(1), + no2: (Math.random() * 5 * baseMultiplier + 1).toFixed(1), + temp: (Math.random() * 20 + 15 + (baseMultiplier - 1) * 10).toFixed(1), + humidity: (Math.random() * 40 + 30 + (baseMultiplier - 1) * 20).toFixed(1) + }; + }; + + /** + * Main sensor value update function with modern async patterns. + */ + const setSensorValues = async () => { + try { + console.log("setSensorValues - updating sensor readings"); + + // Get DOM elements with error checking + const elements = { + co: document.getElementById("display-co"), + no2: document.getElementById("display-no2"), + temp: document.getElementById("display-tmp"), + humidity: document.getElementById("display-hum") + }; + + // Verify all elements exist + const missingElements = Object.entries(elements) + .filter(([key, element]) => !element) + .map(([key]) => key); + + if (missingElements.length > 0) { + console.warn("Missing DOM elements:", missingElements); + } + + // Get sensor data (simulated for now, will be replaced with real data) + const readings = isConnectedToMobile ? + await getRealSensorData() : + generateTestData(); + + // Update displays and check thresholds + const alerts = { + co: updateSensorDisplay(elements.co, readings.co, CO_RED, "CO", " ppm"), + no2: updateSensorDisplay(elements.no2, readings.no2, NO2_RED, "NO₂", " ppm"), + temp: notifyTmpHum ? updateSensorDisplay(elements.temp, readings.temp, TMP_RED, "Temperature", "°C") : false, + humidity: notifyTmpHum ? updateSensorDisplay(elements.humidity, readings.humidity, HUM_RED, "Humidity", "%") : false + }; + + // Log alert status + const activeAlerts = Object.entries(alerts).filter(([key, isAlert]) => isAlert); + if (activeAlerts.length > 0) { + console.warn("Active alerts:", activeAlerts.map(([key]) => key)); + } + + // Schedule next update + setTimeout(setSensorValues, 3000); + + } catch (error) { + console.error("Error in setSensorValues:", error); + // Retry with exponential backoff on error + setTimeout(setSensorValues, 5000); + } + }; + + /** + * Placeholder for real sensor data from mobile app. + */ + const getRealSensorData = async () => { + // TODO: Implement actual Bluetooth data reception + // This will be connected to the Samsung Accessory Protocol + return generateTestData(); + }; + + // Initialize sensor monitoring + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setSensorValues); } else { - displayCo.className = "color-green"; + setSensorValues(); } - if (parseInt(displayNo2.innerText) >= NO2_RED) { - displayNo2.className = "color-red"; - sendNotification("NO2 high"); - } else { - displayNo2.className = "color-green"; - } - - if (parseInt(displayTmp.innerText) >= TMP_RED) { - displayTmp.className = "color-red"; - if (notifyTmpHum) sendNotification("Temp high"); - } else { - displayTmp.className = "color-green"; - } - - if (parseInt(displayHum.innerText) >= HUM_RED) { - displayHum.className = "color-red"; - if (notifyTmpHum) sendNotification("Hum high"); - } else { - displayHum.className = "color-green"; - } - - // Refresh values every second - window.setTimeout(setSensorValues, 3000); - } - - setSensorValues(); + // Expose functions for external use + window.PyrrhaWatch = { + setSensorValues, + sendNotification, + updateConnectionStatus: (connected) => { + isConnectedToMobile = connected; + console.log(`Mobile connection status: ${connected ? "Connected" : "Disconnected"}`); + } + }; })(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f60e0e5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "pyrrha-watch-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pyrrha-watch-app", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "prettier": "^3.6.2" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..85d702f --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "pyrrha-watch-app", + "version": "1.0.0", + "description": "[![License](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![Slack](https://img.shields.io/static/v1?label=Slack&message=%23prometeo-pyrrha&color=blue)](https://callforcode.org/slack)", + "main": "index.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Pyrrha-Platform/Pyrrha-Watch-App.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/Pyrrha-Platform/Pyrrha-Watch-App/issues" + }, + "homepage": "https://github.com/Pyrrha-Platform/Pyrrha-Watch-App#readme", + "devDependencies": { + "prettier": "^3.6.2" + } +}