diff --git a/arduino/arduino.ino b/arduino/arduino.ino index 8a78cf5..9ca3ae0 100644 --- a/arduino/arduino.ino +++ b/arduino/arduino.ino @@ -17,6 +17,13 @@ #define OP_PULSE 17 #define OP_TOGGLE 18 #define OP_ANALOG_RANGE 19 +#define OP_ANALOG_COMPARE_GT 20 +#define OP_ANALOG_COMPARE_GE 21 +#define OP_ANALOG_COMPARE_LT 22 +#define OP_ANALOG_COMPARE_LE 23 +#define OP_ANALOG_COMPARE_EQ 24 +#define OP_ANALOG_COMPARE_NE 25 +#define OP_SHIFT_REGISTER 26 const int MAX_INSTRUCTIONS = 300; const int MAX_VARIABLES = 60; @@ -399,6 +406,100 @@ void executeInstructions() { variables[outputVar] = (value >= min && value <= max) ? 1 : 0; break; } + case OP_ANALOG_COMPARE_GT: { + byte aVar = instructions[pc++]; + byte bVar = instructions[pc++]; + byte outputVar = instructions[pc++]; + variables[outputVar] = (variables[aVar] > variables[bVar]) ? 1 : 0; + break; + } + case OP_ANALOG_COMPARE_GE: { + byte aVar = instructions[pc++]; + byte bVar = instructions[pc++]; + byte outputVar = instructions[pc++]; + variables[outputVar] = (variables[aVar] >= variables[bVar]) ? 1 : 0; + break; + } + case OP_ANALOG_COMPARE_LT: { + byte aVar = instructions[pc++]; + byte bVar = instructions[pc++]; + byte outputVar = instructions[pc++]; + variables[outputVar] = (variables[aVar] < variables[bVar]) ? 1 : 0; + break; + } + case OP_ANALOG_COMPARE_LE: { + byte aVar = instructions[pc++]; + byte bVar = instructions[pc++]; + byte outputVar = instructions[pc++]; + variables[outputVar] = (variables[aVar] <= variables[bVar]) ? 1 : 0; + break; + } + case OP_ANALOG_COMPARE_EQ: { + byte aVar = instructions[pc++]; + byte bVar = instructions[pc++]; + byte outputVar = instructions[pc++]; + // Add a small tolerance for analog value comparisons + variables[outputVar] = (abs(variables[aVar] - variables[bVar]) < 5) ? 1 : 0; + break; + } + case OP_ANALOG_COMPARE_NE: { + byte aVar = instructions[pc++]; + byte bVar = instructions[pc++]; + byte outputVar = instructions[pc++]; + // Add a small tolerance for analog value comparisons + variables[outputVar] = (abs(variables[aVar] - variables[bVar]) >= 5) ? 1 : 0; + break; + } + case OP_SHIFT_REGISTER: { + byte dataVar = instructions[pc++]; + byte clockVar = instructions[pc++]; + byte resetVar = instructions[pc++]; + byte numOutputs = instructions[pc++]; + byte initialState = instructions[pc++]; + byte baseOutputVar = instructions[pc++]; + + static bool shiftRegisterInitialized[MAX_VARIABLES] = {false}; + static uint8_t shiftRegisterState[MAX_VARIABLES] = {0}; + static bool prevClockState[MAX_VARIABLES] = {false}; + + // Initialize on first run + if (!shiftRegisterInitialized[baseOutputVar]) { + // Set only the initial output bit high, others low + shiftRegisterState[baseOutputVar] = (1 << initialState); + shiftRegisterInitialized[baseOutputVar] = true; + } + + // Reset logic - only if reset is connected (not 255) and reset is HIGH + if (resetVar != 255 && variables[resetVar] == HIGH) { + // Reset to initial state (only the initial output bit high) + shiftRegisterState[baseOutputVar] = (1 << initialState); + } + + // Clock rising edge detection + bool currentClockState = variables[clockVar]; + if (currentClockState && !prevClockState[baseOutputVar]) { + // On rising edge of clock, shift the register + if (variables[dataVar]) { + // Shift left and set LSB to 1 + shiftRegisterState[baseOutputVar] = (shiftRegisterState[baseOutputVar] << 1) | 0x01; + } else { + // Shift left and set LSB to 0 + shiftRegisterState[baseOutputVar] = (shiftRegisterState[baseOutputVar] << 1) & 0xFE; + } + + // Handle wrap-around for the number of outputs + if (shiftRegisterState[baseOutputVar] >= (1 << numOutputs)) { + shiftRegisterState[baseOutputVar] = 1; // Reset to first output + } + } + prevClockState[baseOutputVar] = currentClockState; + + // Set output variables (one for each output) + for (byte i = 0; i < numOutputs; i++) { + variables[baseOutputVar + i] = (shiftRegisterState[baseOutputVar] >> i) & 0x01; + } + break; + } } } diff --git a/logic-editor/src/App.test.tsx b/logic-editor/src/App.test.tsx index 5dcae2d..7b86263 100644 --- a/logic-editor/src/App.test.tsx +++ b/logic-editor/src/App.test.tsx @@ -727,3 +727,114 @@ test('larger project without OR. connecting two buttons to same input', () => { expect(bytecodeToString(generateBytecode(test as any))).toEqual(expectedBytecode); }); + +test('analogComparerNode test', () => { + const test = { + "nodes": [ + { + "id": "dndnode_0", + "type": "analogComparerNode", + "position": { + "x": 355.08583015996766, + "y": 267.65848888782415 + }, + "data": { + "label": "analogComparerNode", + "inputs": 2, + "selectedBoard": "arduino_nano" + }, + "width": 160, + "height": 103 + }, + { + "id": "dndnode_1", + "type": "inputNode", + "position": { + "x": 177.975220378801, + "y": 253.12988417921287 + }, + "data": { + "label": "inputNode", + "inputs": 2, + "selectedBoard": "arduino_nano", + "pin": "14" + }, + "width": 97, + "height": 59, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 177.975220378801, + "y": 253.12988417921287 + } + }, + { + "id": "dndnode_2", + "type": "inputNode", + "position": { + "x": 173.82419046205496, + "y": 319.5463628471503 + }, + "data": { + "label": "inputNode", + "inputs": 2, + "selectedBoard": "arduino_nano", + "pin": "15" + }, + "width": 97, + "height": 59, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 173.82419046205496, + "y": 319.5463628471503 + } + }, + { + "id": "dndnode_3", + "type": "outputNode", + "position": { + "x": 569.5557091918491, + "y": 304.3259198190813 + }, + "data": { + "label": "outputNode", + "inputs": 2, + "selectedBoard": "arduino_nano", + "pin": "13" + }, + "width": 97, + "height": 59, + "selected": false, + "dragging": false + } + ], + "edges": [ + { + "source": "dndnode_1", + "sourceHandle": "out", + "target": "dndnode_0", + "targetHandle": "a", + "id": "reactflow__edge-dndnode_1out-dndnode_0a" + }, + { + "source": "dndnode_2", + "sourceHandle": "out", + "target": "dndnode_0", + "targetHandle": "b", + "id": "reactflow__edge-dndnode_2out-dndnode_0b" + }, + { + "source": "dndnode_0", + "sourceHandle": "out", + "target": "dndnode_3", + "targetHandle": "in", + "id": "reactflow__edge-dndnode_0out-dndnode_3in" + } + ], + "board": "arduino_nano" +}; + const expectedBytecode = "1,14,1,15,2,13,5,14,0,5,15,1,20,0,1,2,4,13,2"; + + expect(bytecodeToString(generateBytecode(test as any))).toEqual(expectedBytecode); +}); diff --git a/logic-editor/src/App.tsx b/logic-editor/src/App.tsx index c85d606..0181ff8 100644 --- a/logic-editor/src/App.tsx +++ b/logic-editor/src/App.tsx @@ -47,10 +47,15 @@ const blockTypes = [ { type: 'andNode', label: 'AND', color: 'bg-blue-500' }, // { type: 'orNode', label: 'OR', color: 'bg-purple-500' }, { type: 'notNode', label: 'NOT', color: 'bg-yellow-500' }, + { type: 'nandNode', label: 'NAND', color: 'bg-blue-400' }, + { type: 'norNode', label: 'NOR', color: 'bg-purple-400' }, + { type: 'xorNode', label: 'XOR', color: 'bg-yellow-400' }, { type: 'latchNode', label: 'LATCH', color: 'bg-orange-500' }, { type: 'pulseNode', label: 'PULSE (beta)', color: 'bg-cyan-500' }, { type: 'toggleNode', label: 'TOGGLE', color: 'bg-pink-500' }, { type: 'analogRangeNode', label: 'ANALOG RANGE', color: 'bg-teal-500' }, + { type: 'analogComparerNode', label: 'ANALOG COMPARER', color: 'bg-indigo-500' }, + { type: 'shiftRegisterNode', label: 'SHIFT REGISTER', color: 'bg-purple-600' }, ]; // === Node Definitions === @@ -162,6 +167,211 @@ function PulseNode({ data, id }: any) { ); } +function NandNode({ data, id }: any) { + const { inputs = 2 } = data; + const handleSpacing = 15; + const baseHeight = 50; + const dynamicHeight = baseHeight + (inputs - 3) * handleSpacing; + + return ( +
+
NAND
+ {Array.from({ length: inputs }).map((_, idx) => ( + + ))} + +
+ Inputs: + data.onChangeInputs(id, parseInt(e.target.value))} + className={`${inputClasses} w-8 ml-2`} + /> +
+
+ ); +} + +function NorNode({ data, id }: any) { + const { inputs = 2 } = data; + const handleSpacing = 15; + const baseHeight = 50; + const dynamicHeight = baseHeight + (inputs - 3) * handleSpacing; + + return ( +
+
NOR
+ {Array.from({ length: inputs }).map((_, idx) => ( + + ))} + +
+ Inputs: + data.onChangeInputs(id, parseInt(e.target.value))} + className={`${inputClasses} w-8 ml-2`} + /> +
+
+ ); +} + +function XorNode({ data, id }: any) { + const { inputs = 2 } = data; + const handleSpacing = 15; + const baseHeight = 50; + const dynamicHeight = baseHeight + (inputs - 3) * handleSpacing; + + return ( +
+
XOR
+ {Array.from({ length: inputs }).map((_, idx) => ( + + ))} + +
+ Inputs: + data.onChangeInputs(id, parseInt(e.target.value))} + className={`${inputClasses} w-8 ml-2`} + /> +
+
+ ); +} + +function AnalogComparerNode({ data, id }: any) { + return ( +
+
ANALOG COMPARER
+ + +
A
+
B
+ + + + +
+ ); +} + +function ShiftRegisterNode({ data, id }: any) { + const outputs = data.outputs || 4; + const handleSpacing = 15; + const baseHeight = 80; + const dynamicHeight = baseHeight + (outputs - 4) * handleSpacing; + + return ( +
+
SHIFT REGISTER
+ + {/* Input handles */} + + + + +
data
+
clock
+
reset
+ + {/* Configuration */} +
+
+ Outputs: + data.onChangeOutputs(id, parseInt(e.target.value))} + className={`${inputClasses} ml-1 w-10`} + /> +
+
+ Initial: + +
+
+ + {/* Output handles */} + {Array.from({ length: outputs }).map((_, idx) => ( + + ))} +
+ ); +} + function InputNode({ data, id }: any) { const pins = [ ...((boards as any)[data.selectedBoard]?.digital || []), @@ -343,12 +553,41 @@ export default function App() { andNode: AndNode, orNode: OrNode, notNode: NotNode, + nandNode: NandNode, + norNode: NorNode, + xorNode: XorNode, latchNode: LatchNode, pulseNode: PulseNode, toggleNode: ToggleNode, analogRangeNode: AnalogRangeNode, + analogComparerNode: AnalogComparerNode, + shiftRegisterNode: ShiftRegisterNode, }), []); + const handleComparisonTypeChange = useCallback((nodeId: string, comparisonType: string) => { + setNodes((nds) => + nds.map((n) => + n.id === nodeId ? { ...n, data: { ...n.data, comparisonType } } : n + ) + ); + }, [setNodes]); + + const handleOutputsChange = useCallback((nodeId: string, outputs: number) => { + setNodes((nds) => + nds.map((n) => + n.id === nodeId ? { ...n, data: { ...n.data, outputs } } : n + ) + ); + }, [setNodes]); + + const handleInitialOutputChange = useCallback((nodeId: string, initialOutput: number) => { + setNodes((nds) => + nds.map((n) => + n.id === nodeId ? { ...n, data: { ...n.data, initialOutput } } : n + ) + ); + }, [setNodes]); + const handleMinChange = useCallback((nodeId: string, min: number) => { setNodes((nds) => nds.map((n) => @@ -525,7 +764,7 @@ export default function App() { }; const getArduinoInoFile = async () => { - const githubUrl = 'https://raw.githubusercontent.com/MerzSebastian/OpenPLC/refs/heads/main/arduino/arduino.ino'; + const githubUrl = 'https://raw.githubusercontent.com/MerzSebastian/OpenPLC/refs/heads/feature/add-nodes/arduino/arduino.ino'; const response = await fetch(githubUrl); return await response.text(); } @@ -731,12 +970,15 @@ export default function App() { ...n.data, selectedBoard, onChangePin: handlePinChange, - onChangeInputs: n.type === 'andNode' || n.type === 'orNode' ? handleInputsChange : undefined, + onChangeInputs: n.type === 'andNode' || n.type === 'orNode' || n.type === 'nandNode' || n.type === 'norNode' || n.type === 'xorNode' ? handleInputsChange : undefined, onChangeInitialState: n.type === 'latchNode' || n.type === 'toggleNode' ? handleInitialStateChange : undefined, onChangePulseLength: n.type === 'pulseNode' ? handlePulseLengthChange : undefined, onChangeInterval: n.type === 'pulseNode' ? handleIntervalChange : undefined, onChangeMin: n.type === 'analogRangeNode' ? handleMinChange : undefined, onChangeMax: n.type === 'analogRangeNode' ? handleMaxChange : undefined, + onChangeComparisonType: n.type === 'analogComparerNode' ? handleComparisonTypeChange : undefined, + onChangeOutputs: n.type === 'shiftRegisterNode' ? handleOutputsChange : undefined, + onChangeInitialOutput: n.type === 'shiftRegisterNode' ? handleInitialOutputChange : undefined, } }))} edges={edges} diff --git a/logic-editor/src/bytecode-gen.ts b/logic-editor/src/bytecode-gen.ts index 045319e..730af62 100644 --- a/logic-editor/src/bytecode-gen.ts +++ b/logic-editor/src/bytecode-gen.ts @@ -15,6 +15,13 @@ export const OP_LATCH = 16; export const OP_PULSE = 17; export const OP_TOGGLE = 18; export const OP_ANALOG_RANGE = 19; +export const OP_ANALOG_COMPARE_GT = 20; +export const OP_ANALOG_COMPARE_GE = 21; +export const OP_ANALOG_COMPARE_LT = 22; +export const OP_ANALOG_COMPARE_LE = 23; +export const OP_ANALOG_COMPARE_EQ = 24; +export const OP_ANALOG_COMPARE_NE = 25; +export const OP_SHIFT_REGISTER = 26; export const OP_DELAY = 30; // Types for the logic configuration @@ -32,6 +39,9 @@ interface NodeData { initialState?: number; pulseLength?: number; interval?: number; + comparisonType?: string; + outputs?: number; + initialOutput?: number; } interface Node { @@ -188,7 +198,7 @@ export function generateBytecode(config: LogicConfig): number[] { visited.add(nodeId); const currentNode = nodeDict[nodeId]; - if (currentNode.type === 'analogRangeNode') return true; + if (currentNode.type === 'analogRangeNode' || currentNode.type === 'analogComparerNode') return true; for (const neighbor of graph[nodeId]) { if (checkConnectedToAnalog(neighbor, visited)) return true; @@ -349,6 +359,148 @@ export function generateBytecode(config: LogicConfig): number[] { } break; } + case 'nandNode': { + const inputVars: number[] = []; + for (const edge of edges) { + if (edge.target === nodeId) { + const sourceNodeId = edge.source; + if (varIndexMap[sourceNodeId] !== undefined) { + inputVars.push(varIndexMap[sourceNodeId]); + } + } + } + + if (inputVars.length >= 2 && varIndexMap[nodeId] !== undefined) { + instructions.push(OP_NAND); + instructions.push(inputVars.length); + for (const inputVar of inputVars) { + instructions.push(inputVar); + } + instructions.push(varIndexMap[nodeId]); + } + break; + } + case 'norNode': { + const inputVars: number[] = []; + for (const edge of edges) { + if (edge.target === nodeId) { + const sourceNodeId = edge.source; + if (varIndexMap[sourceNodeId] !== undefined) { + inputVars.push(varIndexMap[sourceNodeId]); + } + } + } + + if (inputVars.length >= 2 && varIndexMap[nodeId] !== undefined) { + instructions.push(OP_NOR); + instructions.push(inputVars.length); + for (const inputVar of inputVars) { + instructions.push(inputVar); + } + instructions.push(varIndexMap[nodeId]); + } + break; + } + case 'xorNode': { + const inputVars: number[] = []; + for (const edge of edges) { + if (edge.target === nodeId) { + const sourceNodeId = edge.source; + if (varIndexMap[sourceNodeId] !== undefined) { + inputVars.push(varIndexMap[sourceNodeId]); + } + } + } + + if (inputVars.length >= 2 && varIndexMap[nodeId] !== undefined) { + instructions.push(OP_XOR); + instructions.push(inputVars.length); + for (const inputVar of inputVars) { + instructions.push(inputVar); + } + instructions.push(varIndexMap[nodeId]); + } + break; + } + case 'analogComparerNode': { + // Find inputs A and B + let aVar = -1; + let bVar = -1; + + for (const edge of edges) { + if (edge.target === nodeId) { + const sourceNodeId = edge.source; + if (varIndexMap[sourceNodeId] !== undefined) { + if (edge.targetHandle === 'a') { + aVar = varIndexMap[sourceNodeId]; + } else if (edge.targetHandle === 'b') { + bVar = varIndexMap[sourceNodeId]; + } + } + } + } + + if (aVar >= 0 && bVar >= 0 && varIndexMap[nodeId] !== undefined) { + const comparisonType = node.data.comparisonType || '>'; + let opcode; + + switch (comparisonType) { + case '>': opcode = OP_ANALOG_COMPARE_GT; break; + case '>=': opcode = OP_ANALOG_COMPARE_GE; break; + case '<': opcode = OP_ANALOG_COMPARE_LT; break; + case '<=': opcode = OP_ANALOG_COMPARE_LE; break; + case '==': opcode = OP_ANALOG_COMPARE_EQ; break; + case '!=': opcode = OP_ANALOG_COMPARE_NE; break; + default: opcode = OP_ANALOG_COMPARE_GT; + } + + instructions.push(opcode); + instructions.push(aVar); + instructions.push(bVar); + instructions.push(varIndexMap[nodeId]); + } + break; + } + case 'shiftRegisterNode': { + // Find data, clock, and reset inputs + let dataVar = -1; + let clockVar = -1; + let resetVar = -1; + + for (const edge of edges) { + if (edge.target === nodeId) { + const sourceNodeId = edge.source; + if (varIndexMap[sourceNodeId] !== undefined) { + if (edge.targetHandle === 'data') { + dataVar = varIndexMap[sourceNodeId]; + } else if (edge.targetHandle === 'clock') { + clockVar = varIndexMap[sourceNodeId]; + } else if (edge.targetHandle === 'reset') { + resetVar = varIndexMap[sourceNodeId]; + } + } + } + } + + // If reset is not connected, use a special value (255) to indicate no reset + if (resetVar === -1) { + resetVar = 255; // Special value meaning "no reset connected" + } + + if (dataVar >= 0 && clockVar >= 0 && varIndexMap[nodeId] !== undefined) { + const outputs = node.data.outputs || 4; + const initialState = node.data.initialState || 0; + + instructions.push(OP_SHIFT_REGISTER); + instructions.push(dataVar); + instructions.push(clockVar); + instructions.push(resetVar); // This can be 255 now + instructions.push(outputs); + instructions.push(initialState); + instructions.push(varIndexMap[nodeId]); + } + break; + } case 'orNode': { const inputVars: number[] = []; for (const edge of edges) {