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) {