diff --git a/src/components/VideoPresetsPanel.tsx b/src/components/VideoPresetsPanel.tsx index 683d783..55fde6e 100644 --- a/src/components/VideoPresetsPanel.tsx +++ b/src/components/VideoPresetsPanel.tsx @@ -26,10 +26,15 @@ const VideoPresetsPanel: React.FC = ({ }, }, { - name: "Microscope", + name: "Science", preset: { - num_sources: 1, - sources: [{ name: "Microscope", width: 100, height: 100, origin_x: 0, origin_y: 0 }], + num_sources: 4, + sources: [ + { name: "Microscope", width: 50, height: 50, origin_x: 0, origin_y: 0 }, + { name: "Strip", width: 50, height: 50, origin_x: 50, origin_y: 0 }, + { name: "Benedict", width: 50, height: 50, origin_x: 0, origin_y: 50 }, + { name: "HCL", width: 50, height: 50, origin_x: 50, origin_y: 50 }, + ], }, }, { diff --git a/src/components/panels/EspSensorPanel.tsx b/src/components/panels/EspSensorPanel.tsx new file mode 100644 index 0000000..0d8cc7e --- /dev/null +++ b/src/components/panels/EspSensorPanel.tsx @@ -0,0 +1,319 @@ +'use client'; + +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import ROSLIB from 'roslib'; +import { useROS } from '@/ros/ROSContext'; +import html2canvas from 'html2canvas'; +import { + ResponsiveContainer, + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Legend, +} from 'recharts'; + +interface EspSensorReadings { + methane: number; + co2: number; + polarimeter: number; + temperature: number; + moisture: number; +} + +type SensorKey = keyof EspSensorReadings; + +interface Point { + time: number; + methane: number; + co2: number; + polarimeter: number; + temperature: number; + moisture: number; +} + +const SENSOR_OPTIONS: { + key: SensorKey; + label: string; + color: string; + unit: string; +}[] = [ + { + key: 'methane', + label: 'Methane', + color: '#0070f3', + unit: '', + }, + { + key: 'co2', + label: 'CO₂', + color: '#28a745', + unit: '', + }, + { + key: 'polarimeter', + label: 'Polarimeter', + color: '#ff8800', + unit: '', + }, + { + key: 'temperature', + label: 'Temperature', + color: '#ff4d4d', + unit: '°C', + }, + { + key: 'moisture', + label: 'Moisture', + color: '#b84dff', + unit: '%', + }, +]; + +const EspSensorPanel: React.FC = () => { + const { ros } = useROS(); + + const [data, setData] = useState([]); + const [selectedSensor, setSelectedSensor] = useState('methane'); + const [windowSize, setWindowSize] = useState(30); + + const containerRef = useRef(null); + + const selectedOption = useMemo(() => { + return SENSOR_OPTIONS.find((option) => option.key === selectedSensor)!; + }, [selectedSensor]); + + useEffect(() => { + if (!ros) return; + + const sensorTopic = new ROSLIB.Topic({ + ros, + name: '/esp_sensor_readings', + messageType: 'interfaces/msg/EspSensorReadings', + }); + + const handleSensorReading = (msg: any) => { + const newPoint: Point = { + time: Date.now(), + methane: Number(msg.methane), + co2: Number(msg.co2), + polarimeter: Number(msg.polarimeter), + temperature: Number(msg.temperature), + moisture: Number(msg.moisture), + }; + + setData((prev) => { + const updated = [...prev, newPoint]; + return updated.length > windowSize ? updated.slice(-windowSize) : updated; + }); + }; + + sensorTopic.subscribe(handleSensorReading); + + return () => { + sensorTopic.unsubscribe(handleSensorReading); + }; + }, [ros, windowSize]); + + const latestValue = + data.length > 0 ? data[data.length - 1][selectedSensor] : null; + + const formatTime = (time: number) => + new Date(time).toLocaleTimeString([], { + minute: '2-digit', + second: '2-digit', + }); + + const formatValue = (value: number) => { + if (selectedSensor === 'temperature' || selectedSensor === 'moisture') { + return `${value.toFixed(1)}${selectedOption.unit}`; + } + + return `${value.toFixed(0)}${selectedOption.unit}`; + }; + + const downloadPNG = async () => { + if (!containerRef.current) return; + + const canvas = await html2canvas(containerRef.current, { + backgroundColor: '#181818', + }); + + const link = document.createElement('a'); + link.download = `esp-${selectedSensor}.png`; + link.href = canvas.toDataURL('image/png'); + link.click(); + }; + + return ( +
+
+
+

ESP Sensor Reading

+

{selectedOption.label}

+

+ {latestValue !== null ? formatValue(latestValue) : '--'} +

+
+ +
+ + + + + +
+
+ +
+ + + + + + + formatValue(Number(value))} + tick={{ fill: '#aaa', fontSize: 10 }} + axisLine={{ stroke: '#444' }} + tickLine={{ stroke: '#444' }} + width={55} + /> + + [ + formatValue(Number(value)), + selectedOption.label, + ]} + labelFormatter={(value) => formatTime(Number(value))} + contentStyle={{ + background: '#222', + border: '1px solid #444', + borderRadius: '8px', + color: '#fff', + }} + /> + + + + + + +
+ + +
+ ); +}; + +export default EspSensorPanel; \ No newline at end of file diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 5701851..1657fb5 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -20,7 +20,7 @@ import VideoControls from './VideoControls'; import MotorStatusPanel from './MotorStatusPanel'; import AntennaControlPanel from './AntennaControlPanel'; import ScienceControlPanel from './ScienceControlPanel'; -import { CO2Graph, MethaneGraph } from './ScienceGraphPanels'; +import EspSensorPanel from './EspSensorPanel'; import PDBRailsPanel from './PDBRails'; import ArmControlPanel from './ArmControlPanel'; @@ -36,8 +36,7 @@ type TileType = | 'MotorStatusPanel' | 'antennaControlPanel' | 'scienceControlPanel' - | 'co2Graph' - | 'methaneGraph' + | 'espSensorPanel' | 'pdbRails' | 'armControlPanel'; @@ -55,8 +54,7 @@ const TILE_DISPLAY_NAMES: Record = { MotorStatusPanel: 'Motor Status', antennaControlPanel: 'Antenna Control', scienceControlPanel: 'Science Motor Control', - co2Graph: 'CO2 Graph', - methaneGraph: 'Methane Graph', + espSensorPanel: 'ESP Sensor', pdbRails: 'PDB Rails', armControlPanel: 'Arm Control', }; @@ -73,8 +71,7 @@ const ALL_TILE_TYPES: TileType[] = [ 'MotorStatusPanel', 'antennaControlPanel', 'scienceControlPanel', - 'co2Graph', - 'methaneGraph', + 'espSensorPanel', 'pdbRails', 'armControlPanel', ]; @@ -399,16 +396,10 @@ const MosaicDashboard: React.FC = () => { ); - case 'co2Graph': + case 'espSensorPanel': return( - - - ) - case 'methaneGraph': - return( - - + ) case 'pdbRails': diff --git a/src/components/panels/PDBRails.tsx b/src/components/panels/PDBRails.tsx index ddf1b15..13233ee 100644 --- a/src/components/panels/PDBRails.tsx +++ b/src/components/panels/PDBRails.tsx @@ -25,7 +25,7 @@ const PDBRailsPanel: React.FC = () => { const pgTopic = new ROSLIB.Topic({ ros, - name: "/pdb_rails/pdb_pg", + name: "/pdb_rails_node/pdb_pg", messageType: "std_msgs/msg/UInt8", }); @@ -63,7 +63,7 @@ const PDBRailsPanel: React.FC = () => { const toggleTopic = new ROSLIB.Topic({ ros, - name: "/pdb_rails/pdb_toggle", + name: "/pdb_rails_node/pdb_toggle", messageType: "std_msgs/msg/UInt8", }); diff --git a/src/components/panels/ScienceControlPanel.tsx b/src/components/panels/ScienceControlPanel.tsx index 9635437..a8b34ca 100644 --- a/src/components/panels/ScienceControlPanel.tsx +++ b/src/components/panels/ScienceControlPanel.tsx @@ -12,6 +12,7 @@ type DCMotorConfig = { name: string; defaultTime: number; defaultDuty: number; + frequency: number; type: 'dc'; }; @@ -23,20 +24,29 @@ type ServoConfig = { maxPulseUs: number; periodUs: number; maxDegrees: number; + frequency: number; type: 'servo'; }; type MotorConfig = DCMotorConfig | ServoConfig; +type SendCommandFn = ( + motorID: number, + value: number, + duration?: number, + frequency?: number, + ramp?: number +) => void; + type DCMotorProps = { motor: DCMotorConfig; - sendCommand: (motorID: number, value: number, duration?: number) => void; + sendCommand: SendCommandFn; disabled: boolean; }; type ServoMotorProps = { motor: ServoConfig; - sendCommand: (motorID: number, value: number, duration?: number) => void; + sendCommand: SendCommandFn; disabled: boolean; }; @@ -44,31 +54,49 @@ function isDCMotor(motor: MotorConfig): motor is DCMotorConfig { return motor.type === 'dc'; } +const PWM_MAX = 1023; +const DEFAULT_PWM_FREQUENCY = 50; +const DEFAULT_RAMP = 0; + const motors: MotorConfig[] = [ - { id: 27, name: 'Strip', defaultTime: 2.0, defaultDuty: 50, type: 'dc' }, - { id: 26, name: 'Resin', defaultTime: 3.0, defaultDuty: 60, type: 'dc' }, - { id: 25, name: 'Polar', defaultTime: 1.5, defaultDuty: 40, type: 'dc' }, - { id: 33, name: 'Benit', defaultTime: 2.5, defaultDuty: 70, type: 'dc' }, - { id: 35, name: 'Stirrer', defaultTime: 2.0, defaultDuty: 55, type: 'dc' }, + { id: 23, name: 'Strip', defaultTime: 2.0, defaultDuty: 50, frequency: 2000, type: 'dc' }, + { id: 18, name: 'Resin Pump', defaultTime: 3.0, defaultDuty: 50, frequency: 2000, type: 'dc' }, + { id: 22, name: 'Polar', defaultTime: 1.5, defaultDuty: 50, frequency: 2000, type: 'dc' }, + { id: 21, name: 'Benedict', defaultTime: 2.5, defaultDuty: 50, frequency: 2000, type: 'dc' }, + { id: 19, name: 'Stirrer', defaultTime: 10.0, defaultDuty: 75, frequency: 2000, type: 'dc' }, + { id: 16, name: 'Heater', defaultTime: 10.0, defaultDuty: 75, frequency: 2000, type: 'dc' }, { - id: 32, + id: 13, name: 'Disk Servo', defaultPosition: 90, minPulseUs: 615, maxPulseUs: 2495, periodUs: 20000, maxDegrees: 195, + frequency: 50, type: 'servo', }, { - id: 25, + id: 27, name: 'Polar Servo', defaultPosition: 45, minPulseUs: 615, maxPulseUs: 2495, periodUs: 20000, maxDegrees: 195, + frequency: 50, + type: 'servo', + }, + { + id: 32, + name: 'Resin Servo', + defaultPosition: 45, + minPulseUs: 350, + maxPulseUs: 2500, + periodUs: 20000, + maxDegrees: 360, + frequency: 50, type: 'servo', } ]; @@ -79,39 +107,44 @@ const motors: MotorConfig[] = [ const ScienceControlPanel: React.FC = () => { const { ros } = useROS(); - const sendCommand = ( - motorID: number, - value: number, - duration?: number + const sendCommand: SendCommandFn = ( + motorID, + value, + duration, + frequency, + ramp = DEFAULT_RAMP ) => { if (!ros) return; - const safeDuration = Math.min(Math.max(duration ?? 0, 0), 31.5); - const safeDuty = Math.min(Math.max(value, 0), 100); - - const durationBits = Math.round(safeDuration / 0.5) & 0x3f; - const dutyBits = Math.round((safeDuty / 100) * 1023) & 0x3ff; + const safeDurationMs = Math.round( + Math.min(Math.max(duration ?? 0, 0), 65.535) * 10 + ); - const service = (durationBits << 10) | dutyBits; + const safeDutyPercent = Math.min(Math.max(value, 0), 100); + const dutyCycle = Math.round((safeDutyPercent / 100) * PWM_MAX); const topic = new ROSLIB.Topic({ ros, - name: '/science/device_control', - messageType: 'sensor_msgs/msg/NavSatStatus', + name: '/esp_pwm_command', + messageType: 'interfaces/msg/PwmCommand', }); topic.publish( new ROSLIB.Message({ - status: motorID, - service, + pin: motorID, + duty_cycle: dutyCycle, + duration: safeDurationMs, + frequency, + ramp, }) ); - console.log('[SCIENCE CMD]', { - status: motorID, - service, - durationBits, - dutyBits, + console.log('[SCIENCE PWM CMD]', { + pin: motorID, + duty_cycle: dutyCycle, + duration: safeDurationMs, + frequency, + ramp, }); }; @@ -123,14 +156,14 @@ const ScienceControlPanel: React.FC = () => { {motors.map((motor) => isDCMotor(motor) ? ( ) : ( = ({ }, [remaining]); const handleGo = () => { - const safeTime = clamp(time, 0, 999); + const safeTime = clamp(time, 0, 65.535); const safeDuty = clamp(duty, 0, 100); - sendCommand(motor.id, safeDuty, safeTime); + + sendCommand(motor.id, safeDuty, safeTime, motor.frequency); setStartTime(safeTime); setRemaining(safeTime); }; const handleStop = () => { - sendCommand(motor.id, 0, 0); + sendCommand(motor.id, 0, 0, motor.frequency); setRemaining(null); }; @@ -347,6 +381,7 @@ const DCMotor: React.FC = ({ type="number" step="0.1" min="0" + max="65.535" value={time} disabled={disabled} onChange={(e) => setTime(Number(e.target.value))} @@ -404,6 +439,9 @@ const DCMotor: React.FC = ({ ); }; +// -------------------- +// Servo Motor Component +// -------------------- const ServoMotor: React.FC = ({ motor, sendCommand, @@ -444,7 +482,7 @@ const ServoMotor: React.FC = ({ const dutyPercent = (pulseUs / motor.periodUs) * 100; - sendCommand(motor.id, dutyPercent, 0.5); + sendCommand(motor.id, dutyPercent, 0.5, motor.frequency, 0); setRemaining(0.5); }; diff --git a/src/components/panels/ScienceGraphPanels.tsx b/src/components/panels/ScienceGraphPanels.tsx deleted file mode 100644 index 5911bed..0000000 --- a/src/components/panels/ScienceGraphPanels.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// telemetryPanels.ts -import TelemetryGraph from "../TelemetryGraph"; - - -export const CO2Graph: React.FC = () => { - return ( - - ); -}; - -export const MethaneGraph: React.FC = () => { - return ( - - ); -}; \ No newline at end of file