Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.

Interface control document

1. Identification

ItemValue
DocumentICD-CMSMARTEYE-001 rev 1
ProductSmartEye module (SmartEye Module in the module registry)
Product versions coveredf44f1ac
PeerSmartEye eye-tracking software, sending its UDP output packet type 4
HostHEAT Capture, HEAT.Capture.ModuleSDK
Settings sectionHEAT.SmartEyeModule
Data modelSimulationFrame carrying one Entity with GazeComponent, PupillometryDataComponent, SpatialComponent and BiometricSensorComponent
Date2026-09-02
OwnerVRAI Simulation

2. Scope

This document defines the UDP datagram envelope the module accepts, the packet-type and data-identifier values it decodes, the byte order and per-identifier wire sizes it enforces, and the mapping from those identifiers to the fields and readings published to HEAT Capture.

The interface is receive-only. The module opens a listening socket and never sends anything to SmartEye: no subscription request, no handshake, no acknowledgement, no configuration. SmartEye must be configured independently to send its output to the workstation and port the module is listening on.

3. Interface inventory

IDNameDirectionTransportPeer
IF-CMSMARTEYE-01SmartEye output packet streamSmartEye to moduleUDP datagrams to a bound local port, default 5003SmartEye software
IF-CMSMARTEYE-02Frame publicationModule to hostModule SDK publication pathHEAT Capture

4. IF-CMSMARTEYE-01 SmartEye output packet stream

4.1 Transport and addressing

ItemValue
SocketUdpClient bound to IPAddress.Any on the configured port
PortUdpPort setting, default 5003
Local addressAll interfaces. No interface selection setting exists.
Sender addressNot checked. The remote endpoint is read into a discarded variable and never validated or filtered.
MulticastNo multicast group is joined, so only unicast and broadcast traffic addressed to the bound port is received.
Socket receive bufferSet to 65507 bytes, one maximum-size datagram
Maximum datagram65507 bytes. Larger datagrams are not representable in UDP.
Read modelBlocking Receive on a dedicated background thread named SmartEye-UDP

Because the socket receive buffer holds roughly one maximum datagram, a burst that arrives while the receive thread is parsing or publishing can be discarded by the operating system before the module sees it. There is no counter or log for that loss.

4.2 Connection lifecycle

There is no connection. UDP is connectionless and the module is a passive listener, so there is no handshake, no keepalive, no disconnect detection and no reconnect logic.

StartCapture logs SmartEye Module starting capture on UDP port {Port}, constructs the receiver with the port value read at that moment, subscribes to its packet event, and starts the thread, which logs SmartEye UDP receiver listening on port {Port}. StartCapture is bounded by the SDK StartupTimeout of 5 seconds; the module does not override it. If the port is already in use the socket constructor throws inside StartCapture, the SDK logs SmartEye Module StartCapture failed. and the module does not start.

EndCapture logs “SmartEye Module ending capture”, unsubscribes the packet event, stops the receiver and disposes it. Stopping clears the running flag and closes the socket, which unblocks the pending Receive; the thread then logs “SmartEye UDP receiver stopped”. EndCapture does not wait for the thread to exit, and it does not clear the retained snapshot. See 5.6.

Silence is indistinguishable from absence. If SmartEye is not running, is not configured to send, or is sending to a different port, the module reports nothing: the listening log line appears exactly as it would with a healthy sender. See Troubleshooting.

4.3 Authentication

None. There is no credential, token, shared secret or source-address check. Any process that can send a UDP datagram to the bound port, on any interface of the workstation, can inject a packet that the module will accept as a measurement.

4.4 Packet envelope

All integers and all floating-point values are big-endian. The module decodes them with BinaryPrimitives.ReadUInt16BigEndian, ReadUInt32BigEndian and ReadDoubleBigEndian, so no host byte-order dependency exists.

A datagram is a 4-byte header followed by a sequence of length-prefixed entries:

Datagram +0 packetType u16 big-endian, must equal 4 +2 length u16 big-endian, payload byte count following the header +4 payload length bytes Payload: zero or more entries, packed with no padding +0 dataId u16 big-endian, see 4.5 +2 dataLength u16 big-endian, byte count of this entry's data +4 data dataLength bytes

Envelope acceptance rules, in the order the parser applies them:

  1. A datagram shorter than 4 bytes is rejected.
  2. A packetType other than 4 is rejected. SmartEye’s other packet types are not decoded.
  3. A datagram shorter than 4 + length is rejected.
  4. Only the first length bytes after the header are treated as payload. Any bytes beyond that are ignored.
  5. Entries are read until fewer than 4 bytes remain in the payload.

Rejection means the whole datagram is discarded and no event is raised. Nothing is logged for a discarded datagram at any log level.

Data element wire sizes are fixed: 1 byte for a boolean, 4 for an unsigned integer, 8 for a double-precision float, 24 for a three-component vector of doubles, 32 for a four-component vector of doubles. Vector components are packed in X, Y, Z order, and the quaternion in X, Y, Z, W order.

4.5 Data identifiers decoded

Each entry is matched on dataId together with an exact dataLength. Both must match for the value to be taken. “Destination” states where the value ends up: a published field or reading name, or the point at which it is dropped.

Frame information:

Data idSizeTypeDestination
0x00014u32Retained as FrameNumber, never published
0x00038f64Retained as TimeStamp, never published. The SmartEye timestamp is not used as the frame time; see 6.
0x00058f64Frame rate. Decoded, then dropped when the packet is copied to the retained snapshot.

Head pose:

Data idSizeTypeDestination
0x001024vec3 f64SpatialComponent.pose.position
0x001224vec3 f64Rodrigues rotation vector, converted to SpatialComponent.pose.orientation only when 0x001d is absent
0x001d32vec4 f64Quaternion, SpatialComponent.pose.orientation
0x001324vec3 f64Nose direction. Retained, never published.
0x001424vec3 f64Up direction. Retained, never published.
0x001524vec3 f64Left-ear direction. Decoded, then dropped at the snapshot copy.
0x00168f64Head heading. Retained, never published.
0x00178f64Head pitch. Retained, never published.
0x00188f64Head roll. Retained, never published.

Gaze, raw and filtered. Where two identifiers feed one published field, the filtered value is used when present and the raw value only when the filtered one is absent in that packet:

Data idSizeTypeDestination
0x001a24vec3 f64GazeComponent.gaze_origin, fallback for 0x0500
0x050024vec3 f64GazeComponent.gaze_origin, preferred
0x001b24vec3 f64GazeComponent.left_gaze_origin, fallback for 0x0501
0x050124vec3 f64GazeComponent.left_gaze_origin, preferred
0x001c24vec3 f64GazeComponent.right_gaze_origin, fallback for 0x0502
0x050224vec3 f64GazeComponent.right_gaze_origin, preferred
0x002024vec3 f64GazeComponent.combined_eye_position
0x002324vec3 f64GazeComponent.left_eye_position
0x002624vec3 f64GazeComponent.right_eye_position
0x002124vec3 f64GazeComponent.gaze_direction, fallback for 0x0030
0x003024vec3 f64GazeComponent.gaze_direction, preferred
0x00228f64GazeComponent.gaze_direction_quality, fallback for 0x0031
0x00318f64GazeComponent.gaze_direction_quality, preferred
0x002424vec3 f64GazeComponent.left_gaze_direction, fallback for 0x0032
0x003224vec3 f64GazeComponent.left_gaze_direction, preferred
0x00258f64GazeComponent.left_gaze_direction_quality, fallback for 0x0033
0x00338f64GazeComponent.left_gaze_direction_quality, preferred
0x002724vec3 f64GazeComponent.right_gaze_direction, fallback for 0x0034
0x003424vec3 f64GazeComponent.right_gaze_direction, preferred
0x00288f64GazeComponent.right_gaze_direction_quality, fallback for 0x0035
0x00358f64GazeComponent.right_gaze_direction_quality, preferred

Filtered and raw are selected per field, not per packet. A single published frame can carry a filtered combined direction and a raw left direction if that is what the packet contained. The values are never averaged or blended.

Eyelid and pupil:

Data idSizeTypeDestination
0x00508f64PupillometryDataComponent.eyelid_openness
0x00518f64PupillometryDataComponent.eyelid_openness_quality
0x00528f64PupillometryDataComponent.left_eyelid_openness
0x00538f64PupillometryDataComponent.left_eyelid_openness_quality
0x00548f64PupillometryDataComponent.right_eyelid_openness
0x00558f64PupillometryDataComponent.right_eyelid_openness_quality
0x00608f64pupil_diameter, fallback for 0x0066
0x00668f64pupil_diameter, preferred
0x00618f64pupil_diameter_quality, fallback for 0x0067
0x00678f64pupil_diameter_quality, preferred
0x00628f64left_eye_pupil_diameter, fallback for 0x0068
0x00688f64left_eye_pupil_diameter, preferred
0x00638f64left_eye_pupil_diameter_quality, fallback for 0x0069
0x00698f64left_eye_pupil_diameter_quality, preferred
0x00648f64right_eye_pupil_diameter, fallback for 0x006a
0x006a8f64right_eye_pupil_diameter, preferred
0x00658f64right_eye_pupil_diameter_quality, fallback for 0x006b
0x006b8f64right_eye_pupil_diameter_quality, preferred

Eyelid openness has no filtered identifier in this parser, so 0x0050 to 0x0055 are used as sent.

Analysis, status and occlusion:

Data idSizeTypeDestination
0x003f1boolGazeComponent.blink
0x003d1boolSaccade flag. Retained, never published.
0x003e1boolFixation flag. Retained, never published.
0x00c04u32Tracking-state code. Retained, never published.
0x06001boolGazeComponent.has_glasses
0x06018f64Glasses-detection quality. Retained, never published.
0x06021boolFace-mask flag. Retained, never published.
0x06041boolGazeComponent.left_eye_occluded
0x06058f64Left-eye occlusion quality. Retained, never published.
0x06061boolGazeComponent.right_eye_occluded
0x06078f64Right-eye occlusion quality. Retained, never published.

Occlusion is published as a bare boolean while its quality score is discarded, so a consumer cannot tell a confident occlusion from a low-confidence guess.

Drowsiness, emotion and activity, all published as named readings on BiometricSensorComponent:

Data idSizeTypeReading name
0x03f54u32Drowsiness9Level
0x03f68f64Drowsiness9LevelQ
0x03f74u32Drowsiness9LevelStatus
0x03fa8f64Anger
0x03fb8f64AngerQ
0x03fc8f64Disgust
0x03fd8f64DisgustQ
0x03fe8f64Happiness
0x03ff8f64HappinessQ
0x05038f64Neutral
0x05048f64NeutralQ
0x05058f64Sadness
0x05068f64SadnessQ
0x05078f64Surprise
0x05088f64SurpriseQ
0x05098f64Valence
0x050a8f64ValenceQ
0x050b8f64Mood
0x050c8f64MoodQ
0x050d4u32DominantEmotion
0x050e8f64DominantEmotionQ
0x03f01boolSpeaking, published as 1.0 or 0.0
0x03f18f64SpeakingQ

Profile identity, decoded and then dropped when the packet is copied to the retained snapshot:

Data idSizeTypeDestination
0x03f24u32Profile id. Dropped at the snapshot copy.
0x03f38f64Profile id quality. Dropped at the snapshot copy.

Identifiers named in the module’s identifier table but not decoded by the parser at this revision, so they are skipped as if unknown: eyeglasses status 0x00c1, per-eye blink closing and opening mid-time, amplitude and speed 0x00e0 to 0x00eb, left and right eyelid state 0x0390 and 0x0391, keypoints found 0x0510, camera-drift heading, pitch and roll 0x0512 to 0x0514.

4.6 Sample validity handling

There is no sample-validity concept beyond structural checks. Concretely:

  • Presence is per identifier, per packet. Any identifier SmartEye omits leaves the corresponding value unset for that packet.
  • An entry whose dataLength does not exactly equal the expected size for its identifier is skipped. It is treated identically to an unknown identifier: no value is taken, nothing is logged.
  • Quality values are carried through as numbers. The module never thresholds, gates or rejects a measurement based on its quality value, on the tracking-state code 0x00c0, or on the occlusion flags. A gaze direction with a quality of zero is published exactly like one with a quality of one.
  • Value ranges are not validated. A decoded double is cast to float for publication with no range or finiteness check, so a non-finite or out-of-range value from the sender is published as such.
  • A packet is accepted whole or not at all at the envelope level, but partially at the entry level: see the truncated-entry row in 4.7.

4.7 Error behaviour

ConditionBehaviour
Datagram shorter than 4 bytesDatagram discarded, no event raised, nothing logged
packetType not equal to 4Datagram discarded, no event raised, nothing logged
Datagram shorter than 4 + lengthDatagram discarded, no event raised, nothing logged
Unknown dataIdEntry skipped, parsing continues with the next entry, nothing logged
Known dataId with an unexpected dataLengthEntry skipped, parsing continues with the next entry, nothing logged
Entry dataLength exceeding the remaining payloadEntry loop stops. The packet is still delivered with whatever was decoded before the truncation. Nothing is logged.
Fewer than 4 bytes left in the payloadEntry loop ends, remaining bytes ignored
Type-4 datagram containing no decodable entryAn all-unset packet is delivered and replaces the retained snapshot. Nothing is logged. This is not distinguished from a real measurement in which SmartEye happened to send nothing.
Socket closed during shutdownThe SocketException is swallowed while the running flag is clear, the loop exits, “SmartEye UDP receiver stopped” is logged
Any other exception in the receive loopLogged as a warning “SmartEye UDP receive error”, the loop continues
Port already bound at capture startThe socket constructor throws inside StartCapture, the SDK logs “SmartEye Module StartCapture failed.” and the module does not start
Exception thrown while building or publishing a frame on the fixed-rate pathThe SDK logs “Error in module” and the capture loop continues
Exception thrown while building or publishing a frame on the native-rate pathCaught by the receive loop, logged as “SmartEye UDP receive error”, the loop continues

No condition in this table produces a retry, a counter, or a state change visible to HEAT Capture. A malformed or hostile stream degrades silently.

4.8 Timing and cadence

The module imposes no rate on the sender. The publication cadence depends on SendAtNativeRate:

SendAtNativeRateCadencePath
true, the defaultOne frame per accepted type-4 datagram, at whatever rate SmartEye sendsFrame is built and published on the SmartEye-UDP receive thread. HandleCapture returns null every cycle, so the SDK interval publishes nothing.
falseOne frame per SDK Interval, 1000 ms by defaultHandleCapture builds a frame from the retained snapshot. Datagrams arriving between cycles update the snapshot and are otherwise discarded.

On the native-rate path the frame build and the publish run inside the receive loop, between two Receive calls, so publication cost is back-pressure on datagram reception rather than on the capture loop.

Neither path uses the SmartEye frame number 0x0001 or timestamp 0x0003 for ordering or de-duplication. On the native-rate path a duplicate or out-of-order datagram is published as a new frame.

5. IF-CMSMARTEYE-02 Frame publication and data model reference

5.1 Entity

ItemValue
Entity nameSmartEye_Data
Entity idThe hash code of a freshly generated GUID, masked to the low 31 bits. Computed once per module instance, so it is arbitrary and not stable across restarts of HEAT Capture. It is not derived from the entity name and carries nothing SmartEye reported.
ComponentsGazeComponent, PupillometryDataComponent, SpatialComponent, BiometricSensorComponent, all four attached on every frame
Capture timeUTC Unix milliseconds, taken when the frame is built
Entities per frameExactly one

All four components are attached unconditionally, whether or not the snapshot contained anything for them. An empty SpatialComponent or a BiometricSensorComponent with no readings is still present in the frame.

5.2 Head pose

SpatialComponent.pose is emitted only when head position 0x0010 was present. Orientation is resolved in this order:

  1. The quaternion 0x001d, used directly.
  2. Otherwise the Rodrigues vector 0x0012, converted to a unit quaternion by the module. A vector whose magnitude is below 1e-10 yields the identity quaternion.
  3. Otherwise the identity quaternion (0, 0, 0, 1).

Case 3 means a frame can carry an authoritative-looking neutral head orientation that reflects only the absence of rotation data. There is no published flag distinguishing it from a measured neutral pose.

5.3 Fields never populated

GazeComponent.IPD, GazeComponent.gaze_point and GazeComponent.gaze_screen_point are never set by this module: no SmartEye identifier is mapped to them. PupillometryDataComponent.other_pupil_data is never populated; the drowsiness, emotion and activity values go to BiometricSensorComponent instead. SpatialComponent fields other than pose are never set.

No EntityEvent is ever published. Saccade 0x003d and fixation 0x003e are decoded and retained but are not turned into events or published fields.

5.4 Absence semantics per field type

Whether a consumer can distinguish “SmartEye did not send this” from “SmartEye sent zero” depends on the proto field type, not on anything the module does:

Published field groupAbsence is distinguishable
Float3 gaze origins, directions and eye positions, and SpatialComponent.poseYes. The message is left unset.
optional quality floats, left_eye_occluded, right_eye_occluded, has_glassesYes. The field is left unset.
blinkNo. It is a plain bool and reads false when 0x003f was absent.
eyelid_openness, left_eyelid_openness, right_eyelid_openness, pupil_diameter, left_eye_pupil_diameter, right_eye_pupil_diameterNo. They are plain floats and read 0 when the identifier was absent.
BiometricSensorComponent readingsYes. A reading is added only when its identifier was present, so an absent value has no entry.

5.5 Publication before a current packet

The retained snapshot is created empty when the module instance is constructed, is not cleared by StartCapture, and is not cleared by EndCapture. On the fixed-rate path a frame is built from it on every cycle regardless. This is audit finding SE-01, and it is a property of this revision, not of the SmartEye protocol.

Two consequences follow. First, before any packet arrives, the seed snapshot has every field unset and the module publishes this:

Published itemSeed value
All Float3 gaze origins, directions and eye positionsUnset
All quality valuesUnset
left_eye_occluded, right_eye_occluded, has_glassesUnset
blinkfalse
eyelid_openness, left_eyelid_openness, right_eyelid_openness0
pupil_diameter, left_eye_pupil_diameter, right_eye_pupil_diameter0
SpatialComponent.poseUnset, because head position is unset
BiometricSensorComponentPresent with zero readings

A consumer therefore cannot distinguish “no SmartEye data yet” from a real measurement of fully closed eyelids and zero pupil diameter with no blink.

Second, and more consequential, the snapshot outlives a capture session. Because the module instance is initialised once and reused across starts and stops, the last packet of one session remains in the snapshot and is published as the first frames of the next session, timestamped with the new session’s capture time. An earlier subject’s gaze, pupil, head-pose, drowsiness and emotion values can be attributed to a new subject.

Do not treat an early fixed-rate frame as evidence of a live tracker. Correlate with a changing value, or run the module on its default native-rate path, where a frame is only built from an immediately received packet. Native rate does not fix SE-01; it avoids the code path on which SE-01 manifests.

6. Units and conventions

ConceptConvention
Positions and originsAs reported by SmartEye, in SmartEye’s own world coordinate system and units. The module applies no transform, scaling or axis remapping.
DirectionsAs reported by SmartEye. Not renormalised.
Head orientationUnit quaternion in X, Y, Z, W order. Either SmartEye’s own quaternion, or a module conversion of its Rodrigues vector, in which magnitude is the rotation angle in radians and direction is the rotation axis.
Pupil diameter and eyelid opennessAs reported by SmartEye. No unit conversion.
Quality valuesAs reported by SmartEye. The proto comments describe quality as a 0..1 measure, but the module neither checks nor rescales the value.
Enumerated codesDominantEmotion, Drowsiness9Level, Drowsiness9LevelStatus and the tracking-state code are numeric codes published unmapped. The module does not decode which number means what.
BooleansAny non-zero byte is true.
Numeric precisionSmartEye sends 64-bit doubles. Every value is cast to 32-bit float for publication, so precision is reduced.
Time baseHost wall clock, UTC Unix milliseconds, applied when the frame is built and again by the SDK when the packet is wrapped. The SmartEye timestamp 0x0003 and frame number 0x0001 are decoded but never published, so frames cannot be aligned to SmartEye’s own clock.

The authority for coordinate-system handedness, origin, axis directions and units is SmartEye’s own documentation. The module does not define them and does not assert them.

7. Configuration affecting the interface

SettingDefaultInterface effect
UdpPort5003The local port the socket binds to. Read once at capture start, so a change takes effect only on the next capture session.
SendAtNativeRatetrueSelects the publication path in 4.8. Read on every packet and on every capture cycle, so a change takes effect immediately, mid-session.
EnabledtrueWhether the module runs at all
Interval1000 msPublication cadence on the fixed-rate path only. Has no effect while SendAtNativeRate is true.
OverrideNodeId, NodeIdfalse, 0Node attribution of the published packet

There is no setting for the listening interface, for a sender allow-list, for which identifier groups to publish, or for a quality threshold. Emotion and drowsiness readings cannot be turned off independently of gaze.

See Manage capture modules for the generic settings and the Module SDK ICD for the lifecycle contract.

8. Compatibility and versioning

There is no version negotiation and no version field in the envelope. Compatibility rests on three assumptions: that the sender uses packet type 4, that identifier numbers keep their meaning, and that each identifier keeps its wire size.

A SmartEye release that changes an identifier’s wire size causes that field to be dropped silently, because the size guard fails and the entry is treated as unknown. A release that reassigns an identifier number to a different quantity causes that quantity to be published under the old field or reading name, with no diagnostic. A release that adds identifiers is tolerated: unknown identifiers are skipped and the rest of the packet still parses.

The identifier subset listed as undecoded at the end of 4.5 is a property of this revision. Enabling those outputs in SmartEye does not make them appear in HEAT Capture.

9. Security considerations

The listening socket is unauthenticated and accepts datagrams from any source address on any interface of the workstation, so any host that can route a UDP packet to the bound port can inject a measurement, and any local process can do the same. Nothing in the module distinguishes an injected packet from a genuine one. Restrict the port at the host firewall to the address of the machine running SmartEye.

The stream itself is unencrypted. Any device on the path can read it, and the module cannot detect tampering.

The payload is personal data. Gaze direction and origin, eye position, pupil diameter, eyelid openness, blink, head pose, glasses and face-mask detection, drowsiness level, facial-emotion estimates and a speaking flag are all measurements of an identifiable person’s body and inferred internal state. The profile identifier SmartEye can send is decoded, though this revision discards it before publication. Gaze and pupil data support inferences beyond the stated purpose of a session, including attention, cognitive load and, in combination with the emotion readings, affective state. Treat captured SmartEye sessions accordingly: restrict who can read them, apply the deployment’s retention limits, and obtain the consent the deployment’s jurisdiction requires before recording. There is no setting that reduces the published set, so consent must cover everything in 4.5.

10. Conformance

  1. Configure SmartEye to send its output to the workstation’s address on the configured port. Start a capture session and confirm the log records “SmartEye Module starting capture on UDP port 5003” and “SmartEye UDP receiver listening on port 5003”.
  2. Confirm one entity named SmartEye_Data appears in the captured data, carrying all four components.
  3. With a subject tracked, confirm gaze_direction and pupil_diameter change across successive frames.
  4. Confirm the filtered-over-raw preference. Disable SmartEye’s filtered gaze output and confirm gaze_direction still populates from the raw identifier 0x0021.
  5. Confirm the byte order end to end by checking that a published pupil diameter is a plausible millimetre-scale number rather than a value of extreme magnitude, which is what a byte-order mismatch would produce.
  6. Verify packet-type filtering. Confirm that enabling another SmartEye packet type produces no additional data and no log entry.
  7. Verify SE-01 on the fixed-rate path. Set SendAtNativeRate to false, start a session with SmartEye stopped, and confirm the frames carry the seed values in 5.5, in particular pupil_diameter of 0 and blink of false with no pose.
  8. Verify the cross-session case. With SendAtNativeRate set to false, run a session with a tracked subject, stop it, stop SmartEye, then start a second session and confirm whether the first frames repeat the previous subject’s last measurement. Until SE-01 is fixed, discard the frames preceding the first packet of each session.
  9. Stop SmartEye mid-session and confirm the module logs nothing and publishes no error. On the native-rate path, confirm frame publication stops.
  10. Confirm the entity id is not relied on across restarts: restart HEAT Capture and confirm the id for SmartEye_Data differs while the name does not.

11. Revision history

RevDateProduct versionChange
12026-09-02f44f1acInitial issue.
Opens your browser's print dialog. Choose "Save as PDF" as the destination.