A full-featured Rust simulator for Open Protocol tightening controllers with a modern web interface - test your integrations without physical tightening hardware.
Integrating tightening tools into Manufacturing Execution Systems requires extensive testing. Physical controllers are expensive, often unavailable during development, and difficult to use for simulating edge cases, error conditions, and high-volume scenarios.
This simulator implements the Open Protocol specification with both a TCP server for integrations and a modern web dashboard for monitoring and control. Use it to:
I built this while working on MES integrations for manufacturing assembly lines. Every time we needed to test new integration code or troubleshoot issues, we'd either wait for hardware availability or risk disrupting production systems. This simulator eliminates that bottleneck.
Important Note: This simulator implements the specific MIDs and features I needed for my integration work. It covers the most common use cases (tightening results, batch management, parameter sets, multi-spindle) but is not a complete Open Protocol implementation. For example:
This focused approach made it practical to build and maintain. The architecture is designed to be extensible, so additional features can be added as needed. Contributions welcome!
It's written in Rust for performance and type safety, with a SvelteKit frontend for a modern developer experience. The manufacturing industry needs better tooling.
Real-time operations overview with device status, latest tightening results, connection health metrics, and live performance indicators.
Configure single tightening operations with PSET selection and manual parameter override options.
Automated tightening cycles and multi-spindle configuration for testing complex assembly scenarios.
Network failure simulation for resilience testing, with configurable packet loss, latency, and corruption rates.
Full CRUD interface for managing tightening parameter sets with visual torque/angle range displays.
Intuitive range sliders and real-time validation for torque and angle configuration.
Real-time event stream with filtering, search, and multiple view modes for debugging and monitoring.
git clone https://github.com/Jarrekstar/open-protocol-device-simulator
cd open-protocol-device-simulator
# Build backend
cargo build --release
# Install frontend dependencies
cd frontend
npm install
cd ..
Option 1: Backend + Frontend (Recommended)
# Terminal 1: Start backend
cargo run --release
# Terminal 2: Start frontend dev server
cd frontend
npm run dev
Then open http://localhost:5173 in your browser to access the web dashboard.
Option 2: Backend Only
cargo run --release
This starts:
0.0.0.0:8080 (Open Protocol)0.0.0.0:8081 (REST & WebSocket)Via Web Interface:
Via Command Line:
# Terminal 1: Start simulator
cargo run
# Terminal 2: Connect a client and subscribe to results
echo '00200060001 001' | nc localhost 8080
# Terminal 3: Trigger a tightening
curl -X POST http://localhost:8081/simulate/tightening \
-H "Content-Type: application/json" \
-d '{"torque": 12.5, "angle": 40.0, "ok": true}'
You should see a MID 0061 tightening result message in Terminal 2.
Modern, responsive web interface built with SvelteKit:
Dashboard Page - Mission Control interface:
Control Panel - Advanced simulation controls:
PSET Management - Full parameter set CRUD:
Events Page - Real-time event monitoring:
Implements the most commonly used MIDs from the Open Protocol specification:
Core Communication:
Parameter Sets:
Tightening Results:
Vehicle ID:
Tool Control:
Multi-Spindle Mode:
Web Interface:
Backend Features:
Built with modern Rust backend and SvelteKit frontend:
┌──────────────────────────────────────────────────────────────────┐
│ TCP Layer (Port 8080) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client 1 │ │ Client 2 │ │ Client 3 │ (Open Protocol) │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └─────────────┴─────────────┘ │
│ │ │
│ ┌───────────▼──────────────┐ │
│ │ Handler Registry │ │
│ │ (MID Router) │ │
│ └───────────┬──────────────┘ │
│ │ │
│ ┌───────────▼──────────────┐ │
│ │ Observable Device State │ ◄──────┐ │
│ │ + Event Broadcasting │ │ │
│ │ + Failure Injection │ │ │
│ └───────────┬──────────────┘ │ │
│ │ │ │
│ ┌───────────▼──────────────┐ │ │
│ │ Event Broadcaster │ │ │
│ │ (tokio broadcast) │ │ │
│ └──────────────────────────┘ │ │
└─────────────────────────────────────────────│────────────────────┘
│
┌───────────────────────┴─────────┐
│ │
┌─────────────────────▼──────────────┐ ┌───────────────▼───────────────┐
│ HTTP REST API (Port 8081) │ │ WebSocket (Port 8081) │
│ - GET /state │ │ - /ws/events │
│ - POST /simulate/tightening │ │ • Real-time event stream │
│ - POST /auto-tightening/* │ │ • Ping/pong latency │
│ - POST /config/multi-spindle │ │ • Connection health │
│ - GET/POST /config/failure │ └───────────────┬───────────────┘
│ - CRUD /psets │ │
│ - POST /psets/{id}/select │ │
└────────────────┬───────────────────┘ │
│ │
└────────────────┬──────────────────────┘
│
┌────────────────▼────────────────┐
│ SQLite Database │
│ - PSETs (simulator.db) │
│ - Torque/angle ranges │
└─────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────┐
│ SvelteKit Frontend (Dev: Port 5173) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Dashboard │ │ Control Panel│ │ PSETs │ │
│ │ (/) │ │ (/control) │ │ (/psets) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ │
│ │ Events │ Technologies: │
│ │ (/events) │ • Svelte 5 + TypeScript │
│ └──────────────┘ • Tailwind CSS + Skeleton UI │
│ • Real-time WebSocket connection │
│ • Responsive dark/light themes │
└───────────────────────────────────────────────────────────────────┘
Backend (Rust):
src/
├── main.rs # TCP server & event multiplexing
├── batch_manager.rs # Batch logic (counter, completion)
├── device_fsm.rs # Device operational state machine
├── session.rs # Connection session FSM (TypeState)
├── subscriptions.rs # Per-client subscription tracking
├── state.rs # Observable device state
├── events.rs # Event definitions (pub/sub)
├── multi_spindle.rs # Multi-spindle coordinator
├── http_server.rs # HTTP + WebSocket server (Axum)
├── pset_manager.rs # PSET CRUD with SQLite
├── handler/
│ ├── mod.rs # Handler registry
│ ├── communication_*.rs # MID 0001-0005
│ ├── pset_*.rs # MID 0014-0019
│ ├── tool_*.rs # MID 0042-0043
│ ├── vehicle_id*.rs # MID 0050-0053
│ ├── tightening_*.rs # MID 0060-0063
│ ├── multi_spindle_*.rs # MID 0090-0102
│ └── keep_alive.rs # MID 9999
├── protocol/
│ ├── parser.rs # Message parsing
│ ├── serializer.rs # Response serialization
│ └── field.rs # Field encoding
└── codec/
└── null_delimited_codec.rs # Framing (0x00 delimiter)
Frontend (SvelteKit):
frontend/src/
├── routes/
│ ├── +page.svelte # Dashboard (/)
│ ├── +layout.svelte # Root layout with nav
│ ├── control/+page.svelte # Control panel
│ ├── psets/+page.svelte # PSET management
│ └── events/+page.svelte # Event viewer
├── lib/
│ ├── components/
│ │ ├── ui/ # 22 reusable components
│ │ ├── control/ # Control-specific components
│ │ ├── psets/ # PSET-specific components
│ │ ├── events/ # Event-specific components
│ │ └── layout/ # Navigation, header, footer
│ ├── stores/
│ │ ├── device.ts # Device state store
│ │ └── websocket.ts # WebSocket connection manager
│ ├── api/
│ │ └── client.ts # HTTP API client
│ ├── types/
│ │ └── index.ts # TypeScript type definitions
│ ├── utils/
│ │ └── logger.ts # Logging utilities
│ └── config/
│ └── constants.ts # App constants
└── app.css # Global styles + Tailwind
curl http://localhost:8081/state
Response:
{
"cell_id": 1,
"channel_id": 1,
"controller_name": "OpenProtocolSimulator",
"current_pset_id": 1,
"batch_manager": {
"counter": 0,
"target_size": 1,
"completed": false
},
"device_fsm_state": "Idle",
"tool_enabled": true,
"vehicle_id": null,
"multi_spindle_config": null
}
curl -X POST http://localhost:8081/simulate/tightening \
-H "Content-Type: application/json" \
-d '{
"torque": 12.5,
"angle": 40.0,
"ok": true
}'
All fields are optional (defaults: torque=12.5, angle=40.0, ok=true).
curl -X POST http://localhost:8081/auto-tightening/start \
-H "Content-Type: application/json" \
-d '{
"interval_ms": 2000,
"duration_ms": 1500,
"failure_rate": 0.15
}'
Parameters:
interval_ms: Time between cycles (default: 3000)duration_ms: Duration of each tightening (default: 1500)failure_rate: Probability of NOK result, 0.0-1.0 (default: 0.1)Auto-tightening runs continuously through multiple batches until stopped or tool disabled.
List all PSETs:
curl http://localhost:8081/psets
Get PSET by ID:
curl http://localhost:8081/psets/1
Create PSET:
curl -X POST http://localhost:8081/psets \
-H "Content-Type: application/json" \
-d '{
"name": "Custom PSET",
"torque_min": 10.0,
"torque_max": 20.0,
"angle_min": 30.0,
"angle_max": 60.0,
"description": "Custom parameter set for testing"
}'
Update PSET:
curl -X PUT http://localhost:8081/psets/6 \
-H "Content-Type: application/json" \
-d '{
"name": "Updated PSET",
"torque_min": 12.0,
"torque_max": 18.0
}'
Delete PSET:
curl -X DELETE http://localhost:8081/psets/6
Select Active PSET:
curl -X POST http://localhost:8081/psets/2/select
curl -X POST http://localhost:8081/config/multi-spindle \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"spindle_count": 4,
"sync_tightening_id": 12345
}'
Parameters:
enabled: Enable/disable multi-spindle modespindle_count: Number of spindles (2-16)sync_tightening_id: Synchronization ID for coordinated tighteningGet current failure config:
curl http://localhost:8081/config/failure
Configure failure scenarios:
curl -X POST http://localhost:8081/config/failure \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"packet_loss_rate": 0.05,
"latency_ms": 100,
"corrupt_rate": 0.02,
"disconnect_rate": 0.01
}'
Parameters:
enabled: Enable/disable failure injectionpacket_loss_rate: Probability of dropping responses (0.0-1.0)latency_ms: Additional latency to add to responsescorrupt_rate: Probability of corrupting message data (0.0-1.0)disconnect_rate: Probability of disconnecting client (0.0-1.0)JavaScript/TypeScript:
const ws = new WebSocket('ws://localhost:8081/ws/events');
ws.onopen = () => {
console.log('Connected to simulator');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'InitialState') {
console.log('Device state:', data.state);
} else if (data.type === 'TighteningCompleted') {
console.log('Tightening result:', data.result);
} else if (data.type === 'ToolStateChanged') {
console.log('Tool enabled:', data.enabled);
}
// Send ping for latency measurement
ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
Event Types:
InitialState - Sent immediately on connection with full device stateTighteningCompleted - Sent after each tightening operationToolStateChanged - Sent when tool is enabled/disabledAutoTighteningProgress - Sent during auto-tightening with progressPsetChanged - Sent when active PSET changesVehicleIdChanged - Sent when VIN is updatedMultiSpindleResultCompleted - Sent after multi-spindle operationMultiSpindleStatusCompleted - Sent with multi-spindle status updateBatchCompleted - Sent when batch is completed# Terminal 1: Start simulator
cargo run
# Terminal 2: Configure batch size (4 bolts)
echo '0025001900100030004' | nc localhost 8080
# Terminal 3: Connect client and subscribe to MID 0061
echo '00200060001 001' | nc localhost 8080
# Terminal 4: Trigger 4 tightenings
for i in {1..4}; do
curl -X POST http://localhost:8081/simulate/tightening -d '{"ok": true}'
sleep 1
done
Expected: Client receives 4 MID 0061 messages with batch_counter 1, 2, 3, 4 (last one with batch complete).
# Bolt 1: OK
curl -X POST http://localhost:8081/simulate/tightening -d '{"ok": true}'
# Bolt 2: NOK (counter stays at 1)
curl -X POST http://localhost:8081/simulate/tightening -d '{"ok": false}'
# Integrator shows operator the NOK and enables retry button
# Operator presses retry → Integrator sends MID 43
# Bolt 2 retry: OK (counter advances to 2)
curl -X POST http://localhost:8081/simulate/tightening -d '{"ok": true}'
# Continue with remaining bolts...
# Terminal 1: Start simulator
cargo run
# Terminal 2: Client subscribes
echo '00200060001 001' | nc localhost 8080
# Terminal 3: Start continuous auto-tightening
curl -X POST http://localhost:8081/auto-tightening/start \
-d '{
"interval_ms": 1000,
"duration_ms": 500,
"failure_rate": 0.10
}'
# Auto-tightening completes default batch (size=1), then waits...
# Terminal 4: Integrator sends Batch 1 (engine bolts)
echo '0025001900100030006' | nc localhost 8080 # 6 bolts
# Auto-tightening resumes, completes 6 bolts, waits...
# Terminal 4: Integrator sends Batch 2 (oil pan bolts)
echo '0025001900100030008' | nc localhost 8080 # 8 bolts
# Completes 8 more bolts, waits...
# Terminal 5: Stop when done
curl -X POST http://localhost:8081/auto-tightening/stop
const net = require('net');
const client = net.createConnection({ port: 8080, host: 'localhost' }, () => {
console.log('Connected to simulator');
// Send MID 0001 (communication start)
client.write('00200001001 001\0');
});
client.on('data', (data) => {
console.log('Received:', data.toString());
// Handle MID 0002 (start acknowledge), then subscribe to results
if (data.toString().includes('0002')) {
client.write('00200060001 001\0'); // Subscribe to MID 0061
}
// Handle MID 0061 (tightening result)
if (data.toString().includes('0061')) {
console.log('Tightening result received!');
}
});
client.on('end', () => {
console.log('Disconnected');
});
[Length:4][MID:4][Revision:3][NoAck:1][StationID:2][SpindleID:2][Spare:1][Data:N][NULL:1]
Example MID 0001:
00200001001 001\0
^^^^ ^
| |
Length (20 bytes) Null terminator
^^^^
MID (0001)
23 parameters in strict order:
Batch Counter Logic:
Batch Reset Behavior:
Tool Lock Control:
Subscriptions:
Backend:
Frontend:
Database:
Running Tests:
# Run all tests
cargo test
# Run specific test
cargo test test_batch_with_nok
# Run with output
cargo test -- --nocapture
Test Coverage: 56 tests covering:
Building for Production:
cargo build --release
# Binary location
./target/release/open-protocol-device-simulator
Running in Development:
# With auto-reload (requires cargo-watch)
cargo install cargo-watch
cargo watch -x run
# With debug logging
RUST_LOG=debug cargo run
Development Server:
cd frontend
npm run dev
# Opens on http://localhost:5173
Type Checking:
cd frontend
# Check types
npm run check
# Watch mode
npm run check:watch
Building for Production:
cd frontend
npm run build
# Preview production build
npm run preview
Project Structure:
src/routes/ (file-based routing)src/lib/components/src/lib/stores/src/lib/api/src/lib/types/Key Technologies:
$state, $derived, $effectBackend Architecture:
Frontend Architecture:
Issue: Client can't connect to port 8080
Solution: Check if simulator is running and firewall allows connections
Issue: Client subscribed but doesn't receive tightening results
Solution:
Issue: Counter stays at same value after tightening
Solution: This is expected behavior on NOK tightenings. Counter only increments on OK.
Issue: Client receives corrupted MID 0061 data
Solution:
RUST_LOG=debug cargo runThe simulator is built with Tokio async runtime for efficient concurrent operation:
Performance characteristics depend on hardware and workload. For production deployments, benchmark with your specific use case.
Scope Note: This simulator was built to cover the simplest use case needed to verify MES integrations during real-world development work. It implements the core functionality required for most integration scenarios but is not a complete Open Protocol implementation.
Protocol Limitations:
Not Yet Implemented:
What IS Implemented: The simulator handles the 80% use case for integration testing:
These features cover most integration scenarios. Additional MIDs and features can be added as needed - the architecture is designed to be extensible. See docs/ directory for design documents.
Implemented Features:
Additional documentation is available in the project:
Design Documents:
DESIGN_SYSTEM.md - Complete UI/UX design system and component library referencedocs/multi-spindle-brd-tdd.md - Design document for multi-spindle mode (now implemented)docs/multi-device-simulation.md - Discussion about simulating multiple physical devices (shelved feature)Code Documentation:
cargo doc --openfrontend/src/lib/types/Contributions are welcome! Areas for enhancement:
High Priority:
Medium Priority:
Documentation:
Please open an issue to discuss major changes before submitting a PR.
Open Protocol is an industry-standard communication protocol for tightening controllers, led by Atlas Copco but implemented by many manufacturers including Desoutter, Stanley, Cleco, and others. This simulator is not affiliated with or endorsed by Atlas Copco or any other manufacturer - it's an independent implementation of the publicly available specification.
Licensed under either of:
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
For issues, questions, or contributions:
Open Protocol:
Backend Technologies:
Frontend Technologies:
Built by someone who got tired of waiting for hardware to test MES integrations. Now with a modern web interface. 🔧✨