From e2bd29e2c244deb87722ea2b47eb8912474d2af5 Mon Sep 17 00:00:00 2001 From: legop3 Date: Wed, 29 Apr 2026 13:12:04 -0400 Subject: [PATCH] splite --- .../discordBotService/batteryEmbeds.js | 135 ++++++++++++++++++ .../discordBotService/commands/status.js | 77 +--------- .../integrations/busEvents.js | 122 +--------------- 3 files changed, 146 insertions(+), 188 deletions(-) create mode 100644 server/src/services/discordBotService/batteryEmbeds.js diff --git a/server/src/services/discordBotService/batteryEmbeds.js b/server/src/services/discordBotService/batteryEmbeds.js new file mode 100644 index 00000000..88a5152f --- /dev/null +++ b/server/src/services/discordBotService/batteryEmbeds.js @@ -0,0 +1,135 @@ +// Discord Battery Embed Helpers +// Purpose: Centralizes rover battery/status snapshot formatting and embed rendering for Discord surfaces. +// Scope: Shared presentation logic for status commands and admin alert battery event posts. +const { EmbedBuilder } = require('discord.js'); + +function formatVoltage(voltageMv) { + if (voltageMv == null) return 'n/a'; + return `${(voltageMv / 1000).toFixed(2)}V`; +} + +function formatCurrent(currentMa) { + if (currentMa == null) return 'n/a'; + return `${currentMa}mA`; +} + +function formatChargeState(batteryState) { + if (!batteryState) return 'n/a'; + const charge = batteryState.charge; + const capacity = batteryState.capacity; + const percent = batteryState.percentDisplay; + const chargeText = charge != null && capacity != null ? `${charge}/${capacity}mAh` : 'n/a'; + const percentText = percent != null ? `${percent}%` : 'n/a'; + return `${chargeText} (${percentText})`; +} + +function formatDockEmoji(docked) { return docked ? '🏠' : '🧭'; } +function formatChargeEmoji(charging) { return charging ? '⚡' : '🔌'; } +function formatLockEmoji(locked) { return locked ? '🔒' : '🔓'; } +function formatBatteryEmoji(batteryState) { + if (batteryState?.urgentActive) return '🛑'; + if (batteryState?.warnActive) return 'âš ī¸'; + return '🔋'; +} +function formatOiEmoji(oiMode) { + if (oiMode === 'full') return 'đŸ•šī¸'; + if (oiMode === 'safe') return '🧰'; + if (oiMode === 'passive') return 'đŸŸĸ'; + if (oiMode === 'off') return 'âšī¸'; + return '❔'; +} + +function isCharging(sensors) { + const label = sensors?.chargingState?.label?.toLowerCase(); + const chargingByLabel = label === 'waiting' || label === 'full charging' || label === 'trickle charging'; + const code = sensors?.chargingState?.code; + const chargingByCode = code === 2 || code === 3 || code === 4; + return chargingByLabel || chargingByCode; +} + +function buildRoverStatusSnapshot(record) { + if (!record) return null; + const sensors = record.lastSensor?.decoded || record.lastSensor?.sensors || null; + return { + id: record.id, + name: record.meta?.name || record.id, + locked: record.locked, + lockReason: record.lockReason, + docked: Boolean(sensors?.chargingSources?.homeBase), + charging: isCharging(sensors), + chargingLabel: sensors?.chargingState?.label || 'unknown', + voltageMv: sensors?.voltageMv ?? null, + currentMa: sensors?.currentMa ?? null, + batteryState: record.batteryState, + oiMode: sensors?.oiMode?.label || 'unknown', + }; +} + +function buildBatteryStatusEmbed({ color = 0x2196f3, records = [], includeOi = true }) { + const embed = new EmbedBuilder().setTitle('Rover Battery Status').setColor(color).setTimestamp(new Date()); + const snapshots = records.map((entry) => buildRoverStatusSnapshot(entry)).filter(Boolean); + if (!snapshots.length) { + embed.setDescription('No rovers online.'); + return embed; + } + + snapshots.forEach((snapshot) => { + const lockLabel = snapshot.locked ? `locked${snapshot.lockReason ? ` (${snapshot.lockReason})` : ''}` : 'unlocked'; + const dockLabel = snapshot.docked ? 'docked' : 'undocked'; + const chargingLabel = snapshot.charging ? `charging (${snapshot.chargingLabel})` : 'not charging'; + const header = [ + formatBatteryEmoji(snapshot.batteryState), + formatDockEmoji(snapshot.docked), + formatChargeEmoji(snapshot.charging), + formatLockEmoji(snapshot.locked), + ].join(' '); + + const lines = [ + `Dock: ${dockLabel}`, + `Charging: ${chargingLabel}`, + `Battery: ${formatChargeState(snapshot.batteryState)}`, + `Voltage: ${formatVoltage(snapshot.voltageMv)}`, + `Current: ${formatCurrent(snapshot.currentMa)}`, + ]; + if (includeOi) { + lines.push(`OI: ${snapshot.oiMode} ${formatOiEmoji(snapshot.oiMode)}`); + } + lines.push(`Lock: ${lockLabel}`); + + embed.addFields({ + name: `${header} ${snapshot.name}`, + value: lines.join('\n'), + inline: true, + }); + }); + + return embed; +} + +function buildBatteryCaption(type, record) { + const snapshot = buildRoverStatusSnapshot(record); + const base = snapshot?.name || 'unknown'; + const percent = snapshot?.batteryState?.percentDisplay; + const percentLabel = percent != null ? `${percent}%` : 'n/a'; + const dockLabel = snapshot?.docked ? 'docked' : 'undocked'; + const chargingLabel = snapshot?.charging ? 'charging' : 'not charging'; + const detail = `${dockLabel}, ${chargingLabel}, ${formatVoltage(snapshot?.voltageMv ?? null)}, ${formatCurrent(snapshot?.currentMa ?? null)}, ${formatChargeState(snapshot?.batteryState ?? null)}`; + + switch (type) { + case 'battery.warn': return `Battery warn: ${base} at ${percentLabel}. ${detail}`; + case 'battery.urgent': return `Battery urgent: ${base} at ${percentLabel}. ${detail}`; + case 'battery.docked': return `Docked: ${base}. ${detail}`; + case 'battery.undocked': return `Undocked: ${base}. ${detail}`; + case 'battery.charging.start': return `Charging started: ${base}. ${detail}`; + case 'battery.charging.stop': return `Charging stopped: ${base}. ${detail}`; + case 'battery.locked': return `Locked for charging: ${base}. ${detail}`; + case 'battery.unlocked': return `Unlocked after charging: ${base}. ${detail}`; + default: return `Battery update: ${base}. ${detail}`; + } +} + +module.exports = { + buildRoverStatusSnapshot, + buildBatteryStatusEmbed, + buildBatteryCaption, +}; diff --git a/server/src/services/discordBotService/commands/status.js b/server/src/services/discordBotService/commands/status.js index 217b5a8c..93815479 100644 --- a/server/src/services/discordBotService/commands/status.js +++ b/server/src/services/discordBotService/commands/status.js @@ -2,31 +2,9 @@ // Purpose: Handles rover status display command with battery and lock details. // Scope: Builds and sends rover status embed for one rover or all visible rovers. const { EmbedBuilder } = require('discord.js'); +const { buildBatteryStatusEmbed } = require('../batteryEmbeds'); function createStatusCommand({ rovers, roverManager }) { - function formatVoltage(voltageMv) { return voltageMv == null ? 'n/a' : `${(voltageMv / 1000).toFixed(2)}V`; } - function formatCurrent(currentMa) { return currentMa == null ? 'n/a' : `${currentMa}mA`; } - function formatDockEmoji(docked) { return docked ? '🏠' : '🧭'; } - function formatChargeEmoji(charging) { return charging ? '⚡' : '🔌'; } - function formatLockEmoji(locked) { return locked ? '🔒' : '🔓'; } - function formatBatteryEmoji(batteryState) { - if (batteryState?.urgentActive) return '🛑'; - if (batteryState?.warnActive) return 'âš ī¸'; - return '🔋'; - } - function formatOiEmoji(oiMode) { - if (oiMode === 'full') return 'đŸ•šī¸'; - if (oiMode === 'safe') return '🧰'; - if (oiMode === 'passive') return 'đŸŸĸ'; - if (oiMode === 'off') return 'âšī¸'; - return '❔'; - } - function formatChargeState(batteryState) { - if (!batteryState) return 'n/a'; - const chargeText = batteryState.charge != null && batteryState.capacity != null ? `${batteryState.charge}/${batteryState.capacity}mAh` : 'n/a'; - const percentText = batteryState.percentDisplay != null ? `${batteryState.percentDisplay}%` : 'n/a'; - return `${chargeText} (${percentText})`; - } function findRoverRecord(id) { if (!id) return null; for (const record of rovers.values()) { @@ -34,54 +12,6 @@ function createStatusCommand({ rovers, roverManager }) { } return null; } - function buildSnapshot(record) { - if (!record) return null; - const sensors = record.lastSensor?.decoded || record.lastSensor?.sensors || null; - return { - name: record.meta?.name || record.id, - locked: record.locked, - lockReason: record.lockReason, - docked: Boolean(sensors?.chargingSources?.homeBase), - charging: Boolean([2,3,4].includes(sensors?.chargingState?.code)), - chargingLabel: sensors?.chargingState?.label || 'unknown', - voltageMv: sensors?.voltageMv ?? null, - currentMa: sensors?.currentMa ?? null, - batteryState: record.batteryState, - oiMode: sensors?.oiMode?.label || 'unknown', - }; - } - function buildEmbed(records) { - const embed = new EmbedBuilder().setTitle('Rover Battery Status').setColor(0x2196f3).setTimestamp(new Date()); - const snapshots = records.map(buildSnapshot).filter(Boolean); - if (!snapshots.length) { - embed.setDescription('No rovers online.'); - return embed; - } - snapshots.forEach((s) => { - const lockLabel = s.locked ? `locked${s.lockReason ? ` (${s.lockReason})` : ''}` : 'unlocked'; - const header = [ - formatBatteryEmoji(s.batteryState), - formatDockEmoji(s.docked), - formatChargeEmoji(s.charging), - formatLockEmoji(s.locked), - ].join(' '); - embed.addFields({ - name: `${header} ${s.name}`, - value: [ - `Dock: ${s.docked ? 'docked' : 'undocked'}`, - `Charging: ${s.charging ? `charging (${s.chargingLabel})` : 'not charging'}`, - `Battery: ${formatChargeState(s.batteryState)}`, - `Voltage: ${formatVoltage(s.voltageMv)}`, - `Current: ${formatCurrent(s.currentMa)}`, - `OI: ${s.oiMode} ${formatOiEmoji(s.oiMode)}`, - `Lock: ${lockLabel}`, - ].join('\n'), - inline: true, - }); - }); - return embed; - } - return async function handleStatusCommand(message, roverId) { const single = roverId ? findRoverRecord(roverId) : null; if (roverId && !single) { @@ -90,7 +20,10 @@ function createStatusCommand({ rovers, roverManager }) { return; } const records = roverId ? [single] : Array.from(rovers.values()).filter((entry) => roverManager.canReplayRoverId(entry?.id)); - await message.reply({ embeds: [buildEmbed(records)], allowedMentions: { parse: [], repliedUser: false } }); + await message.reply({ + embeds: [buildBatteryStatusEmbed({ color: 0x2196f3, records, includeOi: true })], + allowedMentions: { parse: [], repliedUser: false }, + }); }; } diff --git a/server/src/services/discordBotService/integrations/busEvents.js b/server/src/services/discordBotService/integrations/busEvents.js index 3be06942..08a924f8 100644 --- a/server/src/services/discordBotService/integrations/busEvents.js +++ b/server/src/services/discordBotService/integrations/busEvents.js @@ -2,6 +2,7 @@ // Purpose: Handles event-bus announcements to Discord channels. // Scope: Processes supported event types and posts formatted messages/embeds. const { EmbedBuilder, AttachmentBuilder } = require('discord.js'); +const { buildBatteryStatusEmbed, buildBatteryCaption } = require('../batteryEmbeds'); function createBusEventHandler(deps) { const { logger, discordConfig, MODES, roverManager, rovers, schedulePresenceRotation, formatDuration, sendToChannel } = deps; @@ -17,117 +18,6 @@ function createBusEventHandler(deps) { return embed; } - function formatVoltage(voltageMv) { - if (voltageMv == null) return 'n/a'; - return `${(voltageMv / 1000).toFixed(2)}V`; - } - - function formatCurrent(currentMa) { - if (currentMa == null) return 'n/a'; - return `${currentMa}mA`; - } - - function formatChargeState(batteryState) { - if (!batteryState) return 'n/a'; - const charge = batteryState.charge; - const capacity = batteryState.capacity; - const percent = batteryState.percentDisplay; - const chargeText = charge != null && capacity != null ? `${charge}/${capacity}mAh` : 'n/a'; - const percentText = percent != null ? `${percent}%` : 'n/a'; - return `${chargeText} (${percentText})`; - } - - function formatDockEmoji(docked) { return docked ? '🏠' : '🧭'; } - function formatChargeEmoji(charging) { return charging ? '⚡' : '🔌'; } - function formatLockEmoji(locked) { return locked ? '🔒' : '🔓'; } - function formatBatteryEmoji(batteryState) { - if (batteryState?.urgentActive) return '🛑'; - if (batteryState?.warnActive) return 'âš ī¸'; - return '🔋'; - } - - function isCharging(sensors) { - const label = sensors?.chargingState?.label?.toLowerCase(); - const chargingByLabel = label === 'waiting' || label === 'full charging' || label === 'trickle charging'; - const code = sensors?.chargingState?.code; - const chargingByCode = code === 2 || code === 3 || code === 4; - return chargingByLabel || chargingByCode; - } - - function buildRoverStatusSnapshot(record) { - if (!record) return null; - const sensors = record.lastSensor?.decoded || record.lastSensor?.sensors || null; - return { - id: record.id, - name: record.meta?.name || record.id, - locked: record.locked, - lockReason: record.lockReason, - docked: Boolean(sensors?.chargingSources?.homeBase), - charging: isCharging(sensors), - chargingLabel: sensors?.chargingState?.label || 'unknown', - voltageMv: sensors?.voltageMv ?? null, - currentMa: sensors?.currentMa ?? null, - batteryState: record.batteryState, - }; - } - - function buildBatteryStatusEmbed(color, records = null) { - const embed = buildEmbed({ title: 'Rover Battery Status', color: color || 0x2196f3 }); - const sourceRecords = records || Array.from(rovers.values()); - const snapshots = sourceRecords.map((entry) => buildRoverStatusSnapshot(entry)).filter(Boolean); - if (!snapshots.length) { - embed.setDescription('No rovers online.'); - return embed; - } - snapshots.forEach((snapshot) => { - const lockLabel = snapshot.locked ? `locked${snapshot.lockReason ? ` (${snapshot.lockReason})` : ''}` : 'unlocked'; - const dockLabel = snapshot.docked ? 'docked' : 'undocked'; - const chargingLabel = snapshot.charging ? `charging (${snapshot.chargingLabel})` : 'not charging'; - const header = [ - formatBatteryEmoji(snapshot.batteryState), - formatDockEmoji(snapshot.docked), - formatChargeEmoji(snapshot.charging), - formatLockEmoji(snapshot.locked), - ].join(' '); - embed.addFields({ - name: `${header} ${snapshot.name}`, - value: [ - `Dock: ${dockLabel}`, - `Charging: ${chargingLabel}`, - `Battery: ${formatChargeState(snapshot.batteryState)}`, - `Voltage: ${formatVoltage(snapshot.voltageMv)}`, - `Current: ${formatCurrent(snapshot.currentMa)}`, - `Lock: ${lockLabel}`, - ].join('\n'), - inline: true, - }); - }); - return embed; - } - - function buildBatteryCaption(type, payload) { - const roverId = payload?.roverId || 'unknown'; - const record = rovers.get(roverId) || null; - const snapshot = buildRoverStatusSnapshot(record); - const base = snapshot?.name || roverId; - const percent = snapshot?.batteryState?.percentDisplay; - const percentLabel = percent != null ? `${percent}%` : 'n/a'; - const dockLabel = snapshot?.docked ? 'docked' : 'undocked'; - const chargingLabel = snapshot?.charging ? 'charging' : 'not charging'; - const detail = `${dockLabel}, ${chargingLabel}, ${formatVoltage(snapshot?.voltageMv ?? null)}, ${formatCurrent(snapshot?.currentMa ?? null)}, ${formatChargeState(snapshot?.batteryState ?? null)}`; - - switch (type) { - case 'battery.warn': return `Battery warn: ${base} at ${percentLabel}. ${detail}`; - case 'battery.urgent': return `Battery urgent: ${base} at ${percentLabel}. ${detail}`; - case 'battery.docked': return `Docked: ${base}. ${detail}`; - case 'battery.undocked': return `Undocked: ${base}. ${detail}`; - case 'battery.charging.start': return `Charging started: ${base}. ${detail}`; - case 'battery.charging.stop': return `Charging stopped: ${base}. ${detail}`; - case 'battery.locked': return `Locked for charging: ${base}. ${detail}`; - case 'battery.unlocked': return `Unlocked after charging: ${base}. ${detail}`; - default: return `Battery update: ${base}. ${detail}`; - } - } async function announce({ channelId, content, pingRoleId, color, title, description, embeds, files }) { if (!channelId) return; @@ -165,22 +55,22 @@ function createBusEventHandler(deps) { announce({ channelId: channels.adminAlerts, color: 0xf0b651, title: 'Dock Guard Triggered', description: `${payload?.roverId} (${payload?.reasonText || 'undocked'}) for ${formatDuration(payload?.idleMs)}.` }); break; case 'battery.warn': - announce({ channelId: channels.adminAlerts, pingRoleId: roles.adminPing || null, color: 0xf0b651, content: buildBatteryCaption(type, payload), embeds: [buildBatteryStatusEmbed(0xf0b651, Array.from(rovers.values()))] }); + announce({ channelId: channels.adminAlerts, pingRoleId: roles.adminPing || null, color: 0xf0b651, content: buildBatteryCaption(type, rovers.get(payload?.roverId || 'unknown')), embeds: [buildBatteryStatusEmbed({ color: 0xf0b651, records: Array.from(rovers.values()), includeOi: false })] }); break; case 'battery.urgent': - announce({ channelId: channels.adminAlerts, pingRoleId: roles.adminPing || null, color: 0xe53935, content: buildBatteryCaption(type, payload), embeds: [buildBatteryStatusEmbed(0xe53935, Array.from(rovers.values()))] }); + announce({ channelId: channels.adminAlerts, pingRoleId: roles.adminPing || null, color: 0xe53935, content: buildBatteryCaption(type, rovers.get(payload?.roverId || 'unknown')), embeds: [buildBatteryStatusEmbed({ color: 0xe53935, records: Array.from(rovers.values()), includeOi: false })] }); break; case 'battery.docked': case 'battery.undocked': case 'battery.charging.start': - announce({ channelId: channels.adminAlerts, color: 0x2196f3, content: buildBatteryCaption(type, payload), embeds: [buildBatteryStatusEmbed(0x2196f3, Array.from(rovers.values()))] }); + announce({ channelId: channels.adminAlerts, color: 0x2196f3, content: buildBatteryCaption(type, rovers.get(payload?.roverId || 'unknown')), embeds: [buildBatteryStatusEmbed({ color: 0x2196f3, records: Array.from(rovers.values()), includeOi: false })] }); break; case 'battery.charging.stop': case 'battery.locked': - announce({ channelId: channels.adminAlerts, color: 0xf0b651, content: buildBatteryCaption(type, payload), embeds: [buildBatteryStatusEmbed(0xf0b651, Array.from(rovers.values()))] }); + announce({ channelId: channels.adminAlerts, color: 0xf0b651, content: buildBatteryCaption(type, rovers.get(payload?.roverId || 'unknown')), embeds: [buildBatteryStatusEmbed({ color: 0xf0b651, records: Array.from(rovers.values()), includeOi: false })] }); break; case 'battery.unlocked': - announce({ channelId: channels.adminAlerts, color: 0x4caf50, content: buildBatteryCaption(type, payload), embeds: [buildBatteryStatusEmbed(0x4caf50, Array.from(rovers.values()))] }); + announce({ channelId: channels.adminAlerts, color: 0x4caf50, content: buildBatteryCaption(type, rovers.get(payload?.roverId || 'unknown')), embeds: [buildBatteryStatusEmbed({ color: 0x4caf50, records: Array.from(rovers.values()), includeOi: false })] }); break; case 'humanAlert.buttonPressed': { const imageBase64 = payload?.imageBase64 ? String(payload.imageBase64) : '';