diff --git a/App.tsx b/App.tsx index 241c97d..4b90fce 100644 --- a/App.tsx +++ b/App.tsx @@ -9,7 +9,8 @@ import { LadderEditor } from './components/LadderEditor'; import { SystemSetupDialog } from './components/SystemSetupDialog'; import { SimObject, ObjectType, AxisObject, CylinderObject, SwitchObject, LedObject, - IOLogicRule, LogicCondition, LogicAction, ProjectData, AxisData, LogicTriggerType, LogicActionType + IOLogicRule, LogicCondition, LogicAction, ProjectData, AxisData, LogicTriggerType, LogicActionType, + LogicMathOp, MEMORY_SIZE, ADDR_AXIS_BASE, ADDR_AXIS_STRIDE, OFF_AXIS_CURRENT_POS, OFF_AXIS_TARGET_POS } from './types'; import { EditableObject } from './components/SceneObjects'; import { Layout as LayoutIcon, Cpu, Play, Pause, Square, Download, Upload, Magnet, Settings } from 'lucide-react'; @@ -36,7 +37,8 @@ const SimulationLoop = ({ setInputs: (inputs: boolean[]) => void, setOutputs: (outputs: boolean[]) => void, axes: AxisData[], - setAxes: React.Dispatch> + setAxes: React.Dispatch>, + memoryView: React.MutableRefObject }) => { useFrame((state, delta) => { @@ -64,6 +66,25 @@ const SimulationLoop = ({ const effectiveInputs = sensorInputs.map((bit, i) => bit || manualInputs[i]); setInputs(effectiveInputs); + // --- 1. Memory IO Scan (Axis Global State -> Memory) --- + // Update Memory Mapped Axis Areas (Read Current Pos from App State) + // In a real PLC, this happens at the start of scan. + axes.forEach(axis => { + const base = ADDR_AXIS_BASE + (axis.id * ADDR_AXIS_STRIDE); + // 0: Status (Uint16) - reserved + memoryView.current.setUint16(base, 0, true); + // 2: Current Pos (Float32) + memoryView.current.setFloat32(base + OFF_AXIS_CURRENT_POS, axis.value, true); + // 10: Speed (Float32) + memoryView.current.setFloat32(base + 10, axis.speed, true); + + // Sync Target to memory if it wasn't written by logic, or initialize it + // (Optional: In strict PLC, memory drives axis. If we want UI slider to work, UI must write to memory OR axis. Logic takes precedence) + // Here we write the *current* target to memory so logic sees it, unless logic overwrites it. + // Check if we need to sync? For now, let's write it so 'readback' works. + // memoryView.current.setFloat32(base + OFF_AXIS_TARGET_POS, axis.target, true); + }); + // --- 2. PLC Logic Execution --- const logicOutputs = new Array(32).fill(false); @@ -79,6 +100,7 @@ const SimulationLoop = ({ const inputState = effectiveInputs[rule.inputPort]; conditionMet = inputState; // Default IS_ON behavior for now } else if (rule.triggerType === LogicTriggerType.AXIS_COMPARE) { + // Legacy Axis Compare const axis = axes[rule.triggerAxisIndex]; if (axis) { switch (rule.triggerCompareOp) { @@ -87,6 +109,18 @@ const SimulationLoop = ({ case LogicCondition.EQUAL: conditionMet = Math.abs(axis.value - rule.triggerValue) < 0.1; break; } } + } else if (rule.triggerType === LogicTriggerType.MEM_COMPARE) { + // Memory Compare + // Read 4 bytes at address as Float32 (Standardize on Float for M-registers for now) + const addr = rule.triggerAddress || 0; + if (addr >= 0 && addr < MEMORY_SIZE - 4) { + const val = memoryView.current.getFloat32(addr, true); + switch (rule.triggerCompareOp) { + case LogicCondition.GREATER: conditionMet = val > rule.triggerValue; break; + case LogicCondition.LESS: conditionMet = val < rule.triggerValue; break; + case LogicCondition.EQUAL: conditionMet = Math.abs(val - rule.triggerValue) < 0.001; break; + } + } } // 2. Execute Action if Trigger Met @@ -96,7 +130,28 @@ const SimulationLoop = ({ logicOutputs[rule.outputPort] = true; } } else if (rule.actionType === LogicActionType.AXIS_MOVE) { + // Legacy Move: Directly set Axis Target axisUpdates.set(rule.targetAxisIndex, rule.targetAxisValue); + + // Also update Memory Map for consistency + const base = ADDR_AXIS_BASE + (rule.targetAxisIndex * ADDR_AXIS_STRIDE); + if (base < MEMORY_SIZE) memoryView.current.setFloat32(base + OFF_AXIS_TARGET_POS, rule.targetAxisValue, true); + + } else if (rule.actionType === LogicActionType.MEM_OPERATION) { + // Memory Math + const addr = rule.memAddress || 0; + if (addr >= 0 && addr < MEMORY_SIZE - 4) { + let currentVal = memoryView.current.getFloat32(addr, true); + let newVal = currentVal; + + switch (rule.memOperation) { + case LogicMathOp.SET: newVal = rule.memOperandValue; break; + case LogicMathOp.ADD: newVal = currentVal + rule.memOperandValue; break; + case LogicMathOp.SUB: newVal = currentVal - rule.memOperandValue; break; + } + + memoryView.current.setFloat32(addr, newVal, true); + } } } }); @@ -105,7 +160,27 @@ const SimulationLoop = ({ const effectiveOutputs = logicOutputs.map((bit, i) => bit || manualOutputs[i]); setOutputs(effectiveOutputs); - // Apply Axis Logic Updates (Only if changed to prevent thrashing) + // --- 3. Memory Output Scan (Memory -> Axis Command) --- + // Read Target Position from Memory for each axis and apply to State + axes.forEach(axis => { + const base = ADDR_AXIS_BASE + (axis.id * ADDR_AXIS_STRIDE); + const memTarget = memoryView.current.getFloat32(base + OFF_AXIS_TARGET_POS, true); + + // Simple change detection (epsilon) or just overwrite if non-zero? + // Issue: If UI Slider sets axis.target, and memory has 0, memory overwrites UI. + // User Requirement: "write to memory... motor moves". + // So Memory is the source of truth for Target. + // But initially memory is 0. So Axis goes to 0? + // Workaround: If memory target is exactly 0 and axis is not 0, maybe don't force? + // Better: Initialize Memory with Axis Target on startup. + // For now, let's assume Logic drives Memory. + + if (memTarget !== axis.target && Math.abs(memTarget - axis.target) > 0.001) { + axisUpdates.set(axis.id, memTarget); + } + }); + + // Apply Axis Logic Updates if (axisUpdates.size > 0) { setAxes(prev => prev.map(a => { if (axisUpdates.has(a.id)) { @@ -179,6 +254,10 @@ const SimulationLoop = ({ export default function App() { const [activeView, setActiveView] = useState<'layout' | 'logic'>('layout'); + // --- Memory System (Ref-based, high frequency) --- + const memoryBuffer = useRef(new ArrayBuffer(MEMORY_SIZE)); // 10000 bytes + const memoryView = useRef(new DataView(memoryBuffer.current)); + const [objects, setObjects] = useState([]); const [logicRules, setLogicRules] = useState([]); @@ -493,6 +572,7 @@ export default function App() { setOutputs={setOutputs} axes={axes} setAxes={setAxes} + memoryView={memoryView} /> diff --git a/components/LadderEditor.tsx b/components/LadderEditor.tsx index 6d848ad..648267b 100644 --- a/components/LadderEditor.tsx +++ b/components/LadderEditor.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Trash2, PlusCircle, Power, Zap, Gauge, Move } from 'lucide-react'; -import { IOLogicRule, LogicCondition, LogicAction, LogicTriggerType, LogicActionType, AxisData } from '../types'; +import { Trash2, PlusCircle, Power, Zap, Gauge, Move, Calculator, Cpu } from 'lucide-react'; +import { IOLogicRule, LogicCondition, LogicAction, LogicTriggerType, LogicActionType, AxisData, LogicMathOp, ADDR_AXIS_BASE, OFF_AXIS_TARGET_POS, ADDR_AXIS_STRIDE, MEMORY_SIZE } from '../types'; interface LadderEditorProps { logicRules: IOLogicRule[]; @@ -44,11 +44,15 @@ export const LadderEditor: React.FC = ({ triggerAxisIndex: 0, triggerCompareOp: LogicCondition.GREATER, triggerValue: 0, + triggerAddress: 0, actionType: LogicActionType.OUTPUT_COIL, outputPort: 0, action: LogicAction.SET_ON, targetAxisIndex: 0, - targetAxisValue: 0 + targetAxisValue: 0, + memAddress: 0, + memOperation: LogicMathOp.SET, + memOperandValue: 0 })} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded-full text-xs font-bold transition-all transform active:scale-95" > @@ -67,8 +71,11 @@ export const LadderEditor: React.FC = ({ {logicRules.map((rule, index) => { const isInputTrigger = rule.triggerType === LogicTriggerType.INPUT_BIT; const isAxisTrigger = rule.triggerType === LogicTriggerType.AXIS_COMPARE; + const isMemTrigger = rule.triggerType === LogicTriggerType.MEM_COMPARE; + const isOutputAction = rule.actionType === LogicActionType.OUTPUT_COIL; const isAxisAction = rule.actionType === LogicActionType.AXIS_MOVE; + const isMemAction = rule.actionType === LogicActionType.MEM_OPERATION; const inputActive = isInputTrigger && currentInputs[rule.inputPort]; const outputActive = isOutputAction && currentOutputs[rule.outputPort]; @@ -103,6 +110,12 @@ export const LadderEditor: React.FC = ({ > Axis Logic +
@@ -119,7 +132,7 @@ export const LadderEditor: React.FC = ({
- ) : ( + ) : isAxisTrigger ? (
onUpdateRule(rule.id, { triggerAddress: parseInt(e.target.value) || 0 })} + /> +
+ +
+ VAL + onUpdateRule(rule.id, { triggerValue: parseFloat(e.target.value) })} + /> +
+ )} @@ -166,6 +210,12 @@ export const LadderEditor: React.FC = ({ > Move Axis +
@@ -182,13 +232,17 @@ export const LadderEditor: React.FC = ({
- ) : ( + ) : isAxisAction ? (
MOVE @@ -201,6 +255,37 @@ export const LadderEditor: React.FC = ({ onChange={(e) => onUpdateRule(rule.id, { targetAxisValue: parseFloat(e.target.value) })} />
+ ) : ( + // MEMORY ACTION +
+
+ ADDR + onUpdateRule(rule.id, { memAddress: parseInt(e.target.value) || 0 })} + /> +
+ +
+ VAL + onUpdateRule(rule.id, { memOperandValue: parseFloat(e.target.value) })} + /> +
+
)} diff --git a/types.ts b/types.ts index 75a03c3..70f322a 100644 --- a/types.ts +++ b/types.ts @@ -67,17 +67,34 @@ export interface LedObject extends BaseObject { outputPort: number; // Reads this Output bit to turn on } +// Memory Map Constants +export const MEMORY_SIZE = 10000; +export const ADDR_USER_START = 0; +export const ADDR_USER_END = 7999; +export const ADDR_AXIS_BASE = 8000; +export const ADDR_AXIS_STRIDE = 100; + +// Axis Memory Layout (Offsets from Axis Base) +export const OFF_AXIS_STATUS = 0; // Uint16 +export const OFF_AXIS_CURRENT_POS = 2; // Float32 +export const OFF_AXIS_TARGET_POS = 6; // Float32 +export const OFF_AXIS_SPEED = 10; // Float32 +export const OFF_AXIS_ACCEL = 14; // Float32 +export const OFF_AXIS_DECEL = 18; // Float32 + export type SimObject = AxisObject | CylinderObject | SwitchObject | LedObject; // Logic Types export enum LogicTriggerType { INPUT_BIT = 'INPUT_BIT', - AXIS_COMPARE = 'AXIS_COMPARE', + AXIS_COMPARE = 'AXIS_COMPARE', // Deprecated in favor of MEM_COMPARE, but kept for compatibility + MEM_COMPARE = 'MEM_COMPARE', } export enum LogicActionType { OUTPUT_COIL = 'OUTPUT_COIL', - AXIS_MOVE = 'AXIS_MOVE', + AXIS_MOVE = 'AXIS_MOVE', // Deprecated in favor of Memory Write + MEM_OPERATION = 'MEM_OPERATION', } export enum LogicCondition { @@ -90,12 +107,18 @@ export enum LogicCondition { LESS_EQUAL = '<=', } +export enum LogicMathOp { + SET = '=', + ADD = '+', + SUB = '-', +} + export enum LogicAction { SET_ON = 'ON', SET_OFF = 'OFF', TOGGLE = 'TOGGLE', - MOVE_ABS = 'MOVE_ABS', - MOVE_REL = 'MOVE_REL', + MOVE_ABS = 'MOVE_ABS', // Legacy + MOVE_REL = 'MOVE_REL', // Legacy } export interface IOLogicRule { @@ -105,16 +128,27 @@ export interface IOLogicRule { // Trigger triggerType: LogicTriggerType; inputPort: number; // For INPUT_BIT - triggerAxisIndex: number; // For AXIS_COMPARE - triggerCompareOp: LogicCondition; // For AXIS_COMPARE (>, <, ==) - triggerValue: number; // For AXIS_COMPARE + + // Axis/Memory Trigger + triggerAxisIndex: number; // For AXIS_COMPARE (Legacy) + triggerCompareOp: LogicCondition; // >, <, == + triggerValue: number; // For AXIS_COMPARE (Legacy) or MEM_COMPARE Constant + + triggerAddress: number; // For MEM_COMPARE (Address to check) // Action actionType: LogicActionType; outputPort: number; // For OUTPUT_COIL - action: LogicAction; // For OUTPUT_COIL (ON/OFF) - targetAxisIndex: number; // For AXIS_MOVE - targetAxisValue: number; // For AXIS_MOVE + action: LogicAction; // For OUTPUT_COIL + + // Axis Action (Legacy) + targetAxisIndex: number; + targetAxisValue: number; + + // Memory Action + memAddress: number; // Address to write to + memOperation: LogicMathOp; // =, +, - + memOperandValue: number; // Value to add/sub/set } // Full Project Export Type @@ -125,4 +159,6 @@ export interface ProjectData { inputNames: string[]; outputNames: string[]; axes: AxisData[]; + // Memory snapshot could be large, maybe store only non-zero or user logic? + // For now, we won't persist full RAM, only config. }