Add Memory Viewer and Axis Memory Map visualization
This commit is contained in:
35
App.tsx
35
App.tsx
@@ -13,7 +13,8 @@ import {
|
|||||||
LogicMathOp, MEMORY_SIZE, ADDR_AXIS_BASE, ADDR_AXIS_STRIDE, OFF_AXIS_CURRENT_POS, OFF_AXIS_TARGET_POS
|
LogicMathOp, MEMORY_SIZE, ADDR_AXIS_BASE, ADDR_AXIS_STRIDE, OFF_AXIS_CURRENT_POS, OFF_AXIS_TARGET_POS
|
||||||
} from './types';
|
} from './types';
|
||||||
import { EditableObject } from './components/SceneObjects';
|
import { EditableObject } from './components/SceneObjects';
|
||||||
import { Layout as LayoutIcon, Cpu, Play, Pause, Square, Download, Upload, Magnet, Settings } from 'lucide-react';
|
import { MemoryViewer } from './components/MemoryViewer';
|
||||||
|
import { Layout as LayoutIcon, Cpu, Play, Pause, Square, Download, Upload, Magnet, Settings, HardDrive } from 'lucide-react';
|
||||||
|
|
||||||
// --- Simulation Manager (PLC Scan Cycle) ---
|
// --- Simulation Manager (PLC Scan Cycle) ---
|
||||||
const SimulationLoop = ({
|
const SimulationLoop = ({
|
||||||
@@ -26,7 +27,8 @@ const SimulationLoop = ({
|
|||||||
setInputs,
|
setInputs,
|
||||||
setOutputs,
|
setOutputs,
|
||||||
axes,
|
axes,
|
||||||
setAxes
|
setAxes,
|
||||||
|
memoryView
|
||||||
}: {
|
}: {
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
objects: SimObject[],
|
objects: SimObject[],
|
||||||
@@ -253,7 +255,7 @@ const SimulationLoop = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [activeView, setActiveView] = useState<'layout' | 'logic'>('layout');
|
const [activeView, setActiveView] = useState<'layout' | 'logic' | 'memory'>('layout');
|
||||||
// --- Memory System (Ref-based, high frequency) ---
|
// --- Memory System (Ref-based, high frequency) ---
|
||||||
const memoryBuffer = useRef(new ArrayBuffer(MEMORY_SIZE)); // 10000 bytes
|
const memoryBuffer = useRef(new ArrayBuffer(MEMORY_SIZE)); // 10000 bytes
|
||||||
const memoryView = useRef(new DataView(memoryBuffer.current));
|
const memoryView = useRef(new DataView(memoryBuffer.current));
|
||||||
@@ -412,22 +414,30 @@ export default function App() {
|
|||||||
<div className="h-16 bg-gray-900 border-b border-gray-800 flex items-center px-6 justify-between shrink-0 z-20 shadow-lg">
|
<div className="h-16 bg-gray-900 border-b border-gray-800 flex items-center px-6 justify-between shrink-0 z-20 shadow-lg">
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center font-bold text-lg shadow-[0_0_15px_rgba(37,99,235,0.4)]">M</div>
|
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center font-bold text-lg shadow-[0_0_15px_rgba(37,99,235,0.4)]">
|
||||||
<span className="font-bold tracking-tight text-lg">MotionSim</span>
|
<span className="text-xl font-black bg-gradient-to-r from-blue-500 to-purple-600 text-transparent bg-clip-text">SIMP</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] bg-gray-800 text-gray-400 px-1.5 py-0.5 rounded ml-2 font-mono">v0.9.2</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex bg-gray-950 p-1 rounded-xl border border-gray-800 shadow-inner">
|
<div className="flex bg-gray-900 rounded-lg p-1 border border-gray-800">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView('layout')}
|
onClick={() => setActiveView('layout')}
|
||||||
className={`flex items-center gap-2 px-6 py-2 rounded-lg text-sm font-bold transition-all ${activeView === 'layout' ? 'bg-blue-600 text-white shadow-md' : 'text-gray-500 hover:text-gray-300'}`}
|
className={`flex items-center gap-2 px-4 py-1.5 rounded-md text-xs font-bold transition-all ${activeView === 'layout' ? 'bg-gray-800 text-white shadow shadow-black/50' : 'text-gray-500 hover:text-gray-300'}`}
|
||||||
>
|
>
|
||||||
<LayoutIcon size={32} /> Layout
|
<LayoutIcon size={14} /> Layout
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView('logic')}
|
onClick={() => setActiveView('logic')}
|
||||||
className={`flex items-center gap-2 px-6 py-2 rounded-lg text-sm font-bold transition-all ${activeView === 'logic' ? 'bg-blue-600 text-white shadow-md' : 'text-gray-500 hover:text-gray-300'}`}
|
className={`flex items-center gap-2 px-4 py-1.5 rounded-md text-xs font-bold transition-all ${activeView === 'logic' ? 'bg-gray-800 text-white shadow shadow-black/50' : 'text-gray-500 hover:text-gray-300'}`}
|
||||||
>
|
>
|
||||||
<Cpu size={32} /> Logic
|
<Cpu size={14} /> Logic
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('memory')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-1.5 rounded-md text-xs font-bold transition-all ${activeView === 'memory' ? 'bg-gray-800 text-white shadow shadow-black/50' : 'text-gray-500 hover:text-gray-300'}`}
|
||||||
|
>
|
||||||
|
<HardDrive size={14} /> Memory
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -605,6 +615,11 @@ export default function App() {
|
|||||||
axes={axes}
|
axes={axes}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Memory View */}
|
||||||
|
{activeView === 'memory' && (
|
||||||
|
<MemoryViewer memoryView={memoryView} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SystemSetupDialog
|
<SystemSetupDialog
|
||||||
|
|||||||
203
components/MemoryViewer.tsx
Normal file
203
components/MemoryViewer.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Settings, RefreshCw, Hexagon, FileText, Binary, Hash } from 'lucide-react';
|
||||||
|
import { MEMORY_SIZE } from '../types';
|
||||||
|
|
||||||
|
interface MemoryViewerProps {
|
||||||
|
memoryView: React.MutableRefObject<DataView>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewMode = 'float' | 'int16' | 'int32' | 'byte' | 'hex' | 'ascii';
|
||||||
|
|
||||||
|
export const MemoryViewer: React.FC<MemoryViewerProps> = ({ memoryView }) => {
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('float');
|
||||||
|
const [startAddr, setStartAddr] = useState(0);
|
||||||
|
const [rows, setRows] = useState(20);
|
||||||
|
const [, setTick] = useState(0); // For forcing re-render
|
||||||
|
|
||||||
|
// Auto-refresh memory view
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTick(t => t + 1);
|
||||||
|
}, 100); // 10Hz refresh
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStride = () => {
|
||||||
|
switch (viewMode) {
|
||||||
|
case 'float': return 4;
|
||||||
|
case 'int32': return 4;
|
||||||
|
case 'int16': return 2;
|
||||||
|
default: return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCols = () => {
|
||||||
|
switch (viewMode) {
|
||||||
|
case 'float':
|
||||||
|
case 'int32': return 4; // 4 items per row
|
||||||
|
case 'int16': return 8; // 8 items per row
|
||||||
|
default: return 16; // 16 bytes per row
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stride = getStride();
|
||||||
|
const cols = getCols();
|
||||||
|
const rowSize = stride * cols;
|
||||||
|
const totalRows = Math.ceil(MEMORY_SIZE / rowSize);
|
||||||
|
|
||||||
|
// Pagination / Scroll Helper
|
||||||
|
const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const top = e.currentTarget.scrollTop;
|
||||||
|
const rowHeight = 24; // approx
|
||||||
|
const startRow = Math.floor(top / rowHeight);
|
||||||
|
// Optimization: Just render what's needed?
|
||||||
|
// For now, let's keep it simple: render from startAddr relative to scroll if virtual,
|
||||||
|
// or just use a fixed window if we don't assume full scroll height.
|
||||||
|
// Let's implement full scroll height logic:
|
||||||
|
// Actually simplicity first: User can jump addresses.
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCell = (addr: number) => {
|
||||||
|
if (addr >= MEMORY_SIZE) return <span className="text-gray-800">-</span>;
|
||||||
|
try {
|
||||||
|
const view = memoryView.current;
|
||||||
|
let val: string | number = '';
|
||||||
|
let color = 'text-gray-400';
|
||||||
|
|
||||||
|
switch (viewMode) {
|
||||||
|
case 'float':
|
||||||
|
val = view.getFloat32(addr, true).toFixed(4);
|
||||||
|
if (parseFloat(val) !== 0) color = 'text-blue-400 font-bold';
|
||||||
|
break;
|
||||||
|
case 'int32':
|
||||||
|
val = view.getInt32(addr, true);
|
||||||
|
if (val !== 0) color = 'text-green-400 font-bold';
|
||||||
|
break;
|
||||||
|
case 'int16':
|
||||||
|
val = view.getInt16(addr, true);
|
||||||
|
if (val !== 0) color = 'text-green-400 font-bold';
|
||||||
|
break;
|
||||||
|
case 'byte':
|
||||||
|
val = view.getUint8(addr);
|
||||||
|
if (val !== 0) color = 'text-yellow-400 font-bold';
|
||||||
|
break;
|
||||||
|
case 'hex':
|
||||||
|
val = view.getUint8(addr).toString(16).toUpperCase().padStart(2, '0');
|
||||||
|
if (val !== '00') color = 'text-purple-400 font-bold';
|
||||||
|
break;
|
||||||
|
case 'ascii':
|
||||||
|
const charCode = view.getUint8(addr);
|
||||||
|
// Printable ASCII 32-126
|
||||||
|
val = (charCode >= 32 && charCode <= 126) ? String.fromCharCode(charCode) : '.';
|
||||||
|
if (val !== '.') color = 'text-orange-400 font-bold';
|
||||||
|
else color = 'text-gray-700';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return <span className={`font-mono text-xs ${color}`}>{val}</span>;
|
||||||
|
} catch (e) {
|
||||||
|
return <span className="text-red-900">ERR</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate rows
|
||||||
|
const visibleRows = [];
|
||||||
|
// For scrolling, let's just make a really tall div and stick the viewport?
|
||||||
|
// Or just a simple Pager for now to ensure performance?
|
||||||
|
// "Layout | Logic | Memory" - full screen.
|
||||||
|
// Let's do a virtualized-like list but fixed to logical startAddr for manual navigation?
|
||||||
|
// Or just render all 10000 bytes?
|
||||||
|
// 10000 / 16 = 625 rows. Not too bad for React.
|
||||||
|
// Let's render all rows (clamped to sensible limit if needed, but 600 rows is fine if simple)
|
||||||
|
|
||||||
|
// Actually, let's render *only* visible based on scrollTop if we can, or just render all for simplicity if performant.
|
||||||
|
// 600 rows of 16 cols = 9600 elements. Might be heavy on 10Hz re-render.
|
||||||
|
// Let's render a "Window" window around `startAddr`.
|
||||||
|
|
||||||
|
const displayRows = 50; // Show 50 rows at a time (enough for screen)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 bg-gray-950 flex flex-col overflow-hidden text-gray-300">
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="h-12 border-b border-gray-800 flex items-center px-4 justify-between shrink-0 bg-gray-900">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 font-bold text-xs">
|
||||||
|
<Settings size={14} /> <span>Data View:</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex bg-gray-950 rounded p-1">
|
||||||
|
<button onClick={() => setViewMode('hex')} className={`px-3 py-1 text-xs rounded ${viewMode === 'hex' ? 'bg-purple-600 text-white' : 'text-gray-500 hover:text-white'}`}>HEX</button>
|
||||||
|
<button onClick={() => setViewMode('byte')} className={`px-3 py-1 text-xs rounded ${viewMode === 'byte' ? 'bg-yellow-600 text-white' : 'text-gray-500 hover:text-white'}`}>Byte</button>
|
||||||
|
<button onClick={() => setViewMode('int16')} className={`px-3 py-1 text-xs rounded ${viewMode === 'int16' ? 'bg-green-600 text-white' : 'text-gray-500 hover:text-white'}`}>Int16</button>
|
||||||
|
<button onClick={() => setViewMode('int32')} className={`px-3 py-1 text-xs rounded ${viewMode === 'int32' ? 'bg-green-700 text-white' : 'text-gray-500 hover:text-white'}`}>Int32</button>
|
||||||
|
<button onClick={() => setViewMode('float')} className={`px-3 py-1 text-xs rounded ${viewMode === 'float' ? 'bg-blue-600 text-white' : 'text-gray-500 hover:text-white'}`}>Float</button>
|
||||||
|
<button onClick={() => setViewMode('ascii')} className={`px-3 py-1 text-xs rounded ${viewMode === 'ascii' ? 'bg-orange-600 text-white' : 'text-gray-500 hover:text-white'}`}>ASCII</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono text-gray-500">Go To Address:</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="bg-gray-800 border-none rounded px-2 py-1 text-xs font-mono w-24 text-white"
|
||||||
|
value={startAddr}
|
||||||
|
onChange={e => setStartAddr(Math.max(0, parseInt(e.target.value) || 0))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid Content */}
|
||||||
|
<div className="flex-1 overflow-auto p-4 custom-scrollbar">
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-left text-xs font-mono text-gray-600 border-b border-gray-800 pb-2 w-24">Address</th>
|
||||||
|
{/* Headers +0, +1 ... */}
|
||||||
|
{Array.from({ length: cols }).map((_, i) => (
|
||||||
|
<th key={i} className="text-center text-xs font-mono text-gray-600 border-b border-gray-800 pb-2">
|
||||||
|
+{(i * stride).toString(16).toUpperCase().padStart(2, '0')}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="text-left text-xs font-mono text-gray-600 border-b border-gray-800 pb-2 pl-4">ASCII / Interpretation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from({ length: 100 }).map((_, rIndex) => { // Render 100 rows starting from startAddr
|
||||||
|
const rowAddr = startAddr + (rIndex * rowSize);
|
||||||
|
if (rowAddr >= MEMORY_SIZE) return null;
|
||||||
|
|
||||||
|
const isReserved = rowAddr >= 8000;
|
||||||
|
return (
|
||||||
|
<tr key={rowAddr} className={`${isReserved ? 'bg-blue-900/10 hover:bg-blue-900/20' : 'hover:bg-gray-900/50'}`}>
|
||||||
|
<td className="py-1 text-xs font-mono text-gray-500 border-r border-gray-800/50">
|
||||||
|
M{rowAddr.toString().padStart(4, '0')}
|
||||||
|
</td>
|
||||||
|
{Array.from({ length: cols }).map((_, cIndex) => {
|
||||||
|
const cellAddr = rowAddr + (cIndex * stride);
|
||||||
|
return (
|
||||||
|
<td key={cIndex} className="text-center border-b border-gray-800/10">
|
||||||
|
{renderCell(cellAddr)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Optional ASCII side-view for the whole row */}
|
||||||
|
<td className="pl-4 text-xs font-mono text-gray-600 truncate border-b border-gray-800/10">
|
||||||
|
{Array.from({ length: rowSize }).map((_, b) => {
|
||||||
|
const bAddr = rowAddr + b;
|
||||||
|
if (bAddr >= MEMORY_SIZE) return '';
|
||||||
|
const c = memoryView.current.getUint8(bAddr);
|
||||||
|
return (c >= 32 && c <= 126) ? String.fromCharCode(c) : '.';
|
||||||
|
}).join('')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Empty row to indicate reserved space start if not visible? No, pure highlighting is enough */}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="h-12 w-full flex justify-center items-center text-gray-600 text-xs">
|
||||||
|
<button onClick={() => setStartAddr(Math.min(startAddr + 100 * rowSize, MEMORY_SIZE))} className="hover:text-white">Load More...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
import { AxisData } from '../types';
|
import { AxisData, ADDR_AXIS_BASE, ADDR_AXIS_STRIDE, OFF_AXIS_CURRENT_POS, OFF_AXIS_TARGET_POS, OFF_AXIS_SPEED } from '../types';
|
||||||
|
|
||||||
interface SystemSetupDialogProps {
|
interface SystemSetupDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -160,6 +161,20 @@ export const SystemSetupDialog: React.FC<SystemSetupDialogProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Memory Map Info */}
|
||||||
|
<div className="w-48 bg-gray-900 rounded p-3 text-[10px] font-mono text-gray-500 border border-gray-800">
|
||||||
|
<div className="flex justify-between border-b border-gray-800 pb-1 mb-1">
|
||||||
|
<span className="font-bold text-gray-400">MEMORY MAP</span>
|
||||||
|
<span className="text-blue-500">Base: {ADDR_AXIS_BASE + (axis.id * ADDR_AXIS_STRIDE)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-2 gap-y-1">
|
||||||
|
<span>Status:</span> <span className="text-right text-gray-300">+{0}</span>
|
||||||
|
<span>Cur Pos:</span> <span className="text-right text-gray-300">+{OFF_AXIS_CURRENT_POS}</span>
|
||||||
|
<span>Cmd Pos:</span> <span className="text-right text-purple-400">+{OFF_AXIS_TARGET_POS}</span>
|
||||||
|
<span>Speed:</span> <span className="text-right text-gray-300">+{OFF_AXIS_SPEED}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-32 flex flex-col gap-1">
|
<div className="w-32 flex flex-col gap-1">
|
||||||
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-wider">Type</label>
|
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-wider">Type</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
Reference in New Issue
Block a user