diff --git a/src/components/panels/ScienceControlPanel.tsx b/src/components/panels/ScienceControlPanel.tsx index 0f6d449..66b52d5 100644 --- a/src/components/panels/ScienceControlPanel.tsx +++ b/src/components/panels/ScienceControlPanel.tsx @@ -14,13 +14,11 @@ interface RunPolarimeterResponse { file_path: string; } -type DCMotorConfig = { +type MotorConfig = { id: number; name: string; defaultTime: number; defaultDuty: number; - frequency: number; - type: 'dc'; }; type ServoConfig = { @@ -30,74 +28,78 @@ type ServoConfig = { minPulseUs: number; maxPulseUs: number; maxDegrees: number; - frequency: number; - type: 'servo'; }; -type MotorConfig = DCMotorConfig | ServoConfig; - -type SendCommandFn = ( +type SendMotorFn = ( motorID: number, - type: number, value: number, duration?: number, - frequency?: number, ramp?: number ) => void; -type DCMotorProps = { - motor: DCMotorConfig; - sendCommand: SendCommandFn; +type SendServoFn = ( + motorID: number, + value: number, +) => void; + +type MotorProps = { + motor: MotorConfig; + sendCommand: SendMotorFn; disabled: boolean; }; type ServoMotorProps = { motor: ServoConfig; - sendCommand: SendCommandFn; + sendCommand: SendServoFn; disabled: boolean; }; -const TYPE_DC = 0; -const TYPE_SERVO = 1; - -function isDCMotor(motor: MotorConfig): motor is DCMotorConfig { - return motor.type === 'dc'; +interface MotorStatus { + temperature: number; + bus_voltage: number; + output_percent: number; + output_voltage: number; + output_current: number; + position: number; + velocity: number; + fwd_limit: boolean; + rev_limit: boolean; + active_errors: number; } -const PWM_MAX = 1023; -const DEFAULT_PWM_FREQUENCY = 50; const DEFAULT_RAMP = 0; const motors: MotorConfig[] = [ - { id: 18, name: 'Strip', defaultTime: 3.0, defaultDuty: 50, frequency: 2000, type: 'dc' }, - { id: 23, name: 'Resin Pump', defaultTime: 2.5, 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: 2, name: 'Strip', defaultTime: 10.0, defaultDuty: 100 }, + { id: 0, name: 'Resin Pump', defaultTime: 2.5, defaultDuty: 100 }, + { id: 5, name: 'Polar', defaultTime: 1.5, defaultDuty: 100 }, + { id: 4, name: 'Benedict', defaultTime: 8, defaultDuty: 100 }, + { id: 1, name: 'Stirrer', defaultTime: 10.0, defaultDuty: 50 }, + { id: 6, name: 'Heater', defaultTime: 20.0, defaultDuty: 75 }, +] + +const servos: ServoConfig[] = [ { - id: 13, + id: 2, name: 'Disk Servo', defaultPosition: 90, minPulseUs: 615, maxPulseUs: 2495, maxDegrees: 195, - frequency: 50, - type: 'servo', }, { - id: 32, + id: 0, name: 'Resin Servo', defaultPosition: 45, minPulseUs: 350, maxPulseUs: 2500, maxDegrees: 360, - frequency: 50, - type: 'servo', } ]; +const CM_PER_TICK = 0.15 / 13 / 4096; // 1.5 mm pitch thread, 1:13 gear ratio, 12-bit encoder +const DRILL_HEIGHT = 30; + // -------------------- // Panel // -------------------- @@ -106,49 +108,58 @@ const ScienceControlPanel: React.FC = () => { const [title, setTitle] = useState(""); const [polarStatus, setPolarStatus] = useState(""); + const [drill, setDrill] = useState(); + const [elevator, setElevator] = useState(); + const [zero, setZero] = useState(0); // TODO: Service for resetting talon encoder estimate - const sendCommand: SendCommandFn = ( + const sendMotor: SendMotorFn = ( motorID, - type, value, duration, - frequency, ramp = DEFAULT_RAMP ) => { if (!ros) return; - const safeDurationMs = Math.round( - Math.min(Math.max(duration ?? 0, 0), 65.535) * 10 + const safeDuration = Math.round( + Math.min(Math.max(duration ?? 0, 0), 6553.5) * 10 ); - const safeDutyPercent = Math.min(Math.max(value, 0), 100); - const dutyCycle = Math.round((safeDutyPercent / 100) * PWM_MAX); + const dutyCycle = Math.min(Math.max(value, 0), 100); const topic = new ROSLIB.Topic({ ros, - name: '/esp_pwm_command', - messageType: 'interfaces/msg/PwmCommand', + name: '/science/motor', + messageType: 'interfaces/msg/ScienceMotor', }); topic.publish( new ROSLIB.Message({ pin: motorID, - type: type, duty_cycle: dutyCycle, - duration: safeDurationMs, - frequency, + duration: safeDuration, ramp, }) ); + }; + + const sendServo: SendServoFn = ( + motorID, + value, + ) => { + if (!ros) return; - console.log('[SCIENCE PWM CMD]', { - pin: motorID, - type: type, - duty_cycle: dutyCycle, - duration: safeDurationMs, - frequency, - ramp, + const topic = new ROSLIB.Topic({ + ros, + name: '/science/servo', + messageType: 'interfaces/msg/ScienceServo', }); + + topic.publish( + new ROSLIB.Message({ + pin: motorID, + us: value, + }) + ); }; const handlePolar = () => { @@ -158,7 +169,7 @@ const ScienceControlPanel: React.FC = () => { const polarSrv = new ROSLIB.Service({ ros, - name: "/run_polarimeter", + name: "/science/run_polarimeter", serviceType: "interfaces/srv/RunPolarimeter", }); @@ -170,22 +181,75 @@ const ScienceControlPanel: React.FC = () => { ); }; + useEffect(() => { + if (!ros) return; + + const drillTopic = new ROSLIB.Topic({ + ros, + name: '/drill/status', + messageType: 'ros_phoenix/msg/MotorStatus', + }); + + const handleDrillReading = (msg: any) => { + setDrill(msg as MotorStatus); + }; + + drillTopic.subscribe(handleDrillReading); + + return () => { + drillTopic.unsubscribe(handleDrillReading); + }; + }, [ros]); + + useEffect(() => { + if (!ros) return; + + const elevatorTopic = new ROSLIB.Topic({ + ros, + name: '/elevator/status', + messageType: 'ros_phoenix/msg/MotorStatus', + }); + + const handleElevatorReading = (msg: any) => { + setElevator(msg as MotorStatus); + }; + + elevatorTopic.subscribe(handleElevatorReading); + + return () => { + elevatorTopic.unsubscribe(handleElevatorReading); + }; + }, [ros]); + return (
+
+
+ + Drill Height: {(DRILL_HEIGHT - (((elevator?.position ?? 0) - zero) * CM_PER_TICK)).toFixed(2)} cm above ground + Drill Current: {drill?.output_current?.toFixed(2) ?? '--'} A + Drill Percent: {((drill?.output_percent ?? 0) * 100).toFixed(0)}% +
+
+ Elevator Current: {elevator?.output_current?.toFixed(2) ?? '--'} A + Elevator Percent: {((elevator?.output_percent ?? 0) * 100).toFixed(0)}% +
+
{motors.map((motor) => - isDCMotor(motor) ? ( - ) : ( + ) + } + {servos.map((servo) => ( ) @@ -350,6 +414,25 @@ const ScienceControlPanel: React.FC = () => { background: #00ffcc; transition: width 0.1s linear; } + + .drill-info { + border: 1px solid #3a3a3a; + border-radius: 10px; + padding: .75rem; + margin-bottom: 1rem; + box-shadow: 0 3px 10px rgba(0,0,0,.4); + background: linear-gradient(145deg,#2b2b2b,#202020); + color: #eaeaea; + } + + .right { + vertical-align: -moz-middle-with-baseline; + vertical-align: -webkit-baseline-middle; + } + + .drill-reading { + font-weight: 600; + } `}
); @@ -358,7 +441,7 @@ const ScienceControlPanel: React.FC = () => { // -------------------- // DC Motor Component // -------------------- -const DCMotor: React.FC = ({ +const DCMotor: React.FC = ({ motor, sendCommand, disabled, @@ -392,17 +475,17 @@ const DCMotor: React.FC = ({ }, [remaining]); const handleGo = () => { - const safeTime = clamp(time, 0, 65.535); + const safeTime = clamp(time, 0, 6553.5); const safeDuty = clamp(duty, 0, 100); - sendCommand(motor.id, TYPE_DC, safeDuty, safeTime, motor.frequency); + sendCommand(motor.id, safeDuty, safeTime); setStartTime(safeTime); setRemaining(safeTime); }; const handleStop = () => { - sendCommand(motor.id, TYPE_DC, 0, 0, motor.frequency); + sendCommand(motor.id, 0, 0); setRemaining(null); }; @@ -421,7 +504,7 @@ const DCMotor: React.FC = ({ type="number" step="0.1" min="0" - max="65.535" + max="6553.5" value={time} disabled={disabled} onChange={(e) => setTime(Number(e.target.value))} @@ -495,12 +578,11 @@ const ServoMotor: React.FC = ({ const handleGo = () => { const safePos = clamp(position, 0, motor.maxDegrees); - const pulseUs = + const pulseUs = Math.round( motor.minPulseUs + - (safePos / motor.maxDegrees) * (motor.maxPulseUs - motor.minPulseUs); + (safePos / motor.maxDegrees) * (motor.maxPulseUs - motor.minPulseUs)); - const dutyPercent = (pulseUs * motor.frequency / 1000000) * 100; - sendCommand(motor.id, TYPE_SERVO, dutyPercent, 0, motor.frequency); + sendCommand(motor.id, pulseUs); }; return ( diff --git a/src/components/panels/ScienceSensorPanel.tsx b/src/components/panels/ScienceSensorPanel.tsx index b3b25c5..1825fa4 100644 --- a/src/components/panels/ScienceSensorPanel.tsx +++ b/src/components/panels/ScienceSensorPanel.tsx @@ -15,23 +15,23 @@ import { Legend, } from 'recharts'; -interface ScienceSensorReadings { - methane: number; - co2: number; - polarimeter: number; - temperature: number; - moisture: number; +interface ADCPoint { + time: number; + adc1: number; + adc2: number; + adc3: number; } -type SensorKey = keyof ScienceSensorReadings; - -interface Point { +interface CO2Point { time: number; - methane: number; co2: number; - polarimeter: number; +} + +type SensorKey = 'adc1' | 'adc2' | 'adc3' | 'co2'; + +type Temp = { temperature: number; - moisture: number; + humidity: number; } const SENSOR_OPTIONS: { @@ -41,8 +41,8 @@ const SENSOR_OPTIONS: { unit: string; }[] = [ { - key: 'methane', - label: 'Methane', + key: 'adc1', + label: 'ADC 1', color: '#0070f3', unit: '', }, @@ -50,33 +50,29 @@ const SENSOR_OPTIONS: { key: 'co2', label: 'CO₂', color: '#28a745', - unit: '', + unit: 'ppm', }, { - key: 'polarimeter', - label: 'Polarimeter', + key: 'adc2', + label: 'ADC 2', color: '#ff8800', unit: '', }, { - key: 'temperature', - label: 'Temperature', + key: 'adc3', + label: 'ADC 3', color: '#ff4d4d', - unit: '°C', - }, - { - key: 'moisture', - label: 'Moisture', - color: '#b84dff', - unit: '%', + unit: '', }, ]; const ScienceSensorPanel: React.FC = () => { const { ros } = useROS(); - const [data, setData] = useState([]); - const [selectedSensor, setSelectedSensor] = useState('methane'); + const [adc, setAdc] = useState([]); + const [co2, setCo2] = useState([]); + const [temp, setTemp] = useState(); + const [selectedSensor, setSelectedSensor] = useState('adc1'); const [windowSize, setWindowSize] = useState(30); const containerRef = useRef(null); @@ -88,37 +84,84 @@ const ScienceSensorPanel: React.FC = () => { useEffect(() => { if (!ros) return; - const sensorTopic = new ROSLIB.Topic({ + const adcTopic = new ROSLIB.Topic({ ros, - name: '/science_sensor_readings', - messageType: 'interfaces/msg/ScienceSensorReadings', + name: '/science/adc', + messageType: 'interfaces/msg/ScienceADC', }); const handleSensorReading = (msg: any) => { - const newPoint: Point = { + const newPoint: ADCPoint = { time: Date.now(), - methane: Number(msg.methane), - co2: Number(msg.co2), - polarimeter: Number(msg.polarimeter), - temperature: Number(msg.temperature), - moisture: Number(msg.moisture), + adc1: msg.adc1, + adc2: msg.adc2, + adc3: msg.adc3, }; - setData((prev) => { + setAdc((prev) => { const updated = [...prev, newPoint]; return updated.length > windowSize ? updated.slice(-windowSize) : updated; }); }; - sensorTopic.subscribe(handleSensorReading); + adcTopic.subscribe(handleSensorReading); return () => { - sensorTopic.unsubscribe(handleSensorReading); + adcTopic.unsubscribe(handleSensorReading); }; }, [ros, windowSize]); - const latestValue = - data.length > 0 ? data[data.length - 1][selectedSensor] : null; + useEffect(() => { + if (!ros) return; + + const co2Topic = new ROSLIB.Topic({ + ros, + name: '/science/co2', + messageType: 'std_msgs/msg/UInt16', + }); + + const handleSensorReading = (msg: any) => { + const newPoint: CO2Point = { + time: Date.now(), + co2: msg.data, + }; + + setCo2((prev) => { + const updated = [...prev, newPoint]; + return updated.length > windowSize ? updated.slice(-windowSize) : updated; + }); + }; + + co2Topic.subscribe(handleSensorReading); + + return () => { + co2Topic.unsubscribe(handleSensorReading); + }; + }, [ros, windowSize]); + + useEffect(() => { + if (!ros) return; + + const tempTopic = new ROSLIB.Topic({ + ros, + name: '/science/temp', + messageType: 'interfaces/msg/DHT22', + }); + + const handleTempReading = (msg: any) => { + setTemp(msg); + }; + + tempTopic.subscribe(handleTempReading); + + return () => { + tempTopic.unsubscribe(handleTempReading); + }; + }, [ros]); + + const latestValue = selectedSensor === 'co2' ? + (co2.length > 0 ? co2[co2.length - 1][selectedSensor] : null) + : (adc.length > 0 ? adc[adc.length - 1][selectedSensor] : null); const formatTime = (time: number) => new Date(time).toLocaleTimeString([], { @@ -127,10 +170,6 @@ const ScienceSensorPanel: React.FC = () => { }); const formatValue = (value: number) => { - if (selectedSensor === 'temperature' || selectedSensor === 'moisture') { - return `${value.toFixed(1)}${selectedOption.unit}`; - } - return `${value.toFixed(0)}${selectedOption.unit}`; }; @@ -152,9 +191,9 @@ const ScienceSensorPanel: React.FC = () => {

Science Sensor Reading

-

{selectedOption.label}

+

Temperature: {temp?.temperature?.toFixed(1)}°C Humidity: {temp?.humidity?.toFixed(1)}%

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

@@ -187,7 +226,7 @@ const ScienceSensorPanel: React.FC = () => {