Overview
Justina captures and transmits surgical telemetry data in real-time using WebSocket connections. Every movement of the surgical instrument, along with critical events like hemorrhages and tumor contacts, is streamed to the backend for analysis and storage.
Telemetry data is transmitted at ~60 FPS during active simulations, providing high-resolution movement tracking.
WebSocket Architecture
Connection Endpoint
The simulation establishes a persistent bidirectional connection to:
ws://[backend-url]/ws/simulation?token=[jwt-token]
WebSocket connections are authenticated using JWT tokens passed as query parameters: const token = getTokenFromCookies ();
websocketRef . current = new WebSocket (
` ${ WS_URL } /ws/simulation?token= ${ token } `
);
The backend HandshakeInterceptorImpl validates the token and extracts the surgeon ID before allowing the connection.
Backend Configuration
WebSocket handlers are registered in the Spring Boot configuration:
@ Configuration
@ EnableWebSocket
@ RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final SimulationWebSocketHandler simulationHandler ;
private final HandshakeInterceptorImpl handshakeInterceptor ;
@ Override
public void registerWebSocketHandlers ( WebSocketHandlerRegistry registry ) {
registry . addHandler (simulationHandler, "/ws/simulation" )
. addInterceptors (handshakeInterceptor)
. setAllowedOrigins ( "*" );
}
}
Telemetry Data Structure
Each telemetry message contains:
const telemetry = {
coordinates: [ x , y , z ], // 3D position of surgical instrument
event: evento , // Surgical event type
timestamp: Date . now () // Millisecond timestamp
};
websocketRef . current . send ( JSON . stringify ( telemetry ));
Backend DTO Validation
Incoming messages are validated using Jakarta Bean Validation:
public record TelemetryDTO (
@ NotNull ( message = "Las coordenadas son obligatorias" )
@ NotEmpty ( message = "Debe haber al menos una coordenada" )
@ Size ( min = 2 , max = 3 , message = "Coordinates debe tener 2 o 3 valores" )
double [] coordinates,
@ NotNull ( message = "El evento es obligatorio" )
SurgeryEvent event,
@ Positive ( message = "El timestamp debe ser positivo" )
long timestamp
) {}
Invalid telemetry messages cause the WebSocket connection to close with CloseStatus.BAD_DATA, ensuring data integrity.
Surgical Events
The system tracks five types of surgical events:
public enum SurgeryEvent {
NONE , // Regular movement without special event
TUMOR_TOUCH , // Instrument contacted tumor tissue
HEMORRHAGE , // Arterial or vascular damage detected
START , // Simulation began
FINISH // Simulation completed
}
Event Triggering
START Sent when user clicks “INICIAR CIRUGÍA” button
TUMOR_TOUCH Triggered by collision detection with tumor fragments
HEMORRHAGE Fired when scalpel intersects arterial mesh
FINISH Transmitted when user ends simulation or completes tumor removal
Event Transmission Example
function enviarEvento ( x : number , y : number , z : number , evento = "NONE" ) {
if ( ! websocketRef . current || websocketRef . current . readyState !== WebSocket . OPEN ) {
console . warn ( "WebSocket not open - event not sent:" , evento );
return ;
}
const telemetry = {
coordinates: [ x , y , z ],
event: evento ,
timestamp: Date . now ()
};
websocketRef . current . send ( JSON . stringify ( telemetry ));
}
Backend Message Processing
The SimulationWebSocketHandler processes incoming telemetry:
@ Override
public void handleTextMessage ( WebSocketSession session, TextMessage message) throws Exception {
// Parse JSON to DTO
TelemetryDTO dto = objectMapper . readValue ( message . getPayload (), TelemetryDTO . class );
// Validate constraints
Set < ConstraintViolation < TelemetryDTO >> violations = validator . validate (dto);
if ( ! violations . isEmpty ()) {
session . close ( CloseStatus . BAD_DATA );
return ;
}
// Map to domain model
Movement movement = new Movement (
dto . coordinates (),
dto . event (),
dto . timestamp ()
);
// Retrieve surgeon identity
UUID surgeonId = (UUID) session . getAttributes (). get ( "SURGEON_ID" );
if (surgeonId == null ) {
session . close ( CloseStatus . POLICY_VIOLATION );
return ;
}
// Get or create surgery session
SurgerySession surgery = activeSessions . computeIfAbsent ( session . getId (), k -> {
return new SurgerySession (surgeonId);
});
// Add movement to session
surgery . addMovement (movement);
// Persist on FINISH event
if ( movement . event () == SurgeryEvent . FINISH ) {
surgery . endSurgery ();
surgeryRepository . save (surgery);
activeSessions . remove ( session . getId ());
String response = String . format (
"{ \" status \" : \" SAVED \" , \" surgeryId \" : \" %s \" }" ,
surgery . getId ()
);
session . sendMessage ( new TextMessage (response));
AIWebSocketHandler . notificarNuevaCirugia ( surgery . getId ());
}
}
Movement Data Model
Each movement is stored as a domain record:
public record Movement (
double[] coordinates, // [x, y] or [x, y, z]
SurgeryEvent event,
long timestamp
) {}
Movements are aggregated into SurgerySession objects that represent complete surgical procedures.
Session Management
Active Sessions
The handler maintains a concurrent map of active surgeries:
private final Map < String , SurgerySession > activeSessions = new ConcurrentHashMap <>();
Session Lifecycle
Connection Established : WebSocket opens, surgeon authenticated
START Event : New SurgerySession created and stored
Movement Stream : Continuous telemetry added to session
FINISH Event : Session persisted to database and removed from memory
AI Notification : Analysis pipeline triggered
When a simulation completes, the server responds:
{
"status" : "SAVED" ,
"surgeryId" : "550e8400-e29b-41d4-a716-446655440000"
}
The frontend receives this message and initiates AI analysis polling:
websocketRef . current . onmessage = ( event ) => {
const message = JSON . parse ( event . data );
if ( message . status === "SAVED" ) {
const surgeryId = message . surgeryId ;
document . cookie = `lastSurgeryId= ${ surgeryId } ; path=/; max-age=86400` ;
consultarTrayectoria ( surgeryId ). then (() => {
iniciarPollingAnalisis ( surgeryId );
websocketRef . current ?. close ();
});
}
};
Connection States
WebSocket connections progress through states:
CONNECTING Initial handshake in progress
OPEN Ready to send/receive data
CLOSING Shutdown initiated
CLOSED Connection terminated
Error Handling
Frontend Error Management
websocketRef . current . onerror = ( error ) => {
console . error ( "WebSocket error:" , error );
};
websocketRef . current . onclose = () => {
console . log ( "WebSocket disconnected" );
};
Backend Validation Failures
Invalid Data : Connection closed with BAD_DATA status
Missing Authentication : Connection closed with POLICY_VIOLATION
Constraint Violations : DTO validation errors trigger immediate disconnect
Movement Events : ~60 messages/second during active use
Collision Events : Variable based on surgical precision
Control Events : 1 START + 1 FINISH per simulation
Typical 5-minute simulation:
~18,000 movement data points
~200 bytes per message
~3.6 MB total telemetry data
Security Features
JWT Authentication All connections require valid surgeon tokens
Origin Validation Configurable CORS policy for WebSocket endpoints
Session Isolation Each surgeon’s data stored separately by surgeon ID
Validation Layer Jakarta Bean Validation prevents malformed data injection
Next Steps
3D Simulation Learn about the Babylon.js simulation engine
AI Analysis See how telemetry data is analyzed by the AI pipeline