11using MarvinsAIRARefactored . Classes ;
2+ using MarvinsAIRARefactored . Controls ;
23using MarvinsAIRARefactored . Windows ;
34
45namespace MarvinsAIRARefactored . Components ;
56
67public class SeatBeltTensioner
78{
8- // Telemetry → SBT update rate: 60fps source / 6 = 10 updates per second
9- private const int UpdateInterval = 6 ;
9+ // Telemetry → SBT update rate: 60fps source / 3 = 20 updates per second
10+ private const int UpdateInterval = 3 ;
1011
1112 public bool IsConnected { get ; private set ; } = false ;
1213
1314 private readonly UsbSerialPortHelper _usbSerialPortHelper = new ( "MAIRA SBT" ) ;
1415
16+ private readonly SeatBeltTensionerGraph _surgeGraph = new ( ) ;
17+ private readonly SeatBeltTensionerGraph _swayGraph = new ( ) ;
18+ private readonly SeatBeltTensionerGraph _heaveGraph = new ( ) ;
19+
20+ private readonly SeatBeltTensionerGraph _leftShoulderGraph = new ( ) ;
21+ private readonly SeatBeltTensionerGraph _rightShoulderGraph = new ( ) ;
22+
1523 private int _updateCounter = UpdateInterval + 2 ;
1624 private int _lastSentLeftTenths = - 1 ;
1725 private int _lastSentRightTenths = - 1 ;
1826
27+ private float _longAccelSum = 0f ;
28+ private float _latAccelSum = 0f ;
29+ private float _vertAccelSum = 0f ;
30+ private float _pitchSum = 0f ;
31+ private float _rollSum = 0f ;
32+ private int _accelSampleCount = 0 ;
33+
1934 public SeatBeltTensioner ( )
2035 {
2136 var app = App . Instance ! ;
@@ -35,6 +50,15 @@ public void Initialize()
3550
3651 _usbSerialPortHelper . Initialize ( ) ;
3752
53+ var sbtPage = MainWindow . _seatBeltTensionerPage ;
54+
55+ _surgeGraph . Initialize ( sbtPage . SurgeGraph_Image , 0.3f , 0f , 0.2f , 1f , 0.08f , 0.58f ) ;
56+ _swayGraph . Initialize ( sbtPage . SwayGraph_Image , 0.1f , 0.3f , 0f , 0.5f , 1f , 0f ) ;
57+ _heaveGraph . Initialize ( sbtPage . HeaveGraph_Image , 0.1f , 0.2f , 0.3f , 0.3f , 0.7f , 1f ) ;
58+
59+ _leftShoulderGraph . Initialize ( sbtPage . LeftShoulderGraph_Image , 0f , 0f , 0.2f , 0f , 0f , 1f ) ;
60+ _rightShoulderGraph . Initialize ( sbtPage . RightShoulderGraph_Image , 0.2f , 0f , 0f , 1f , 0f , 0f ) ;
61+
3862 if ( ! _usbSerialPortHelper . DeviceFound )
3963 {
4064 app . Logger . WriteLine ( "[SeatBeltTensioner] Device not found - disabling SeatBeltTensionerEnabled" ) ;
@@ -160,7 +184,28 @@ private void Update( App app )
160184 {
161185 var settings = DataContext . DataContext . Instance . Settings ;
162186
163- if ( ! settings . SeatBeltTensionerEnabled || ! IsConnected || ! app . Simulator . IsOnTrack )
187+ if ( _accelSampleCount == 0 )
188+ {
189+ return ;
190+ }
191+
192+ var longAccelAvg = _longAccelSum / _accelSampleCount ;
193+ var latAccelAvg = _latAccelSum / _accelSampleCount ;
194+ var vertAccelAvg = _vertAccelSum / _accelSampleCount ;
195+
196+ var pitch = _pitchSum / _accelSampleCount ;
197+ var roll = _rollSum / _accelSampleCount ;
198+
199+ _longAccelSum = 0f ;
200+ _latAccelSum = 0f ;
201+ _vertAccelSum = 0f ;
202+
203+ _pitchSum = 0f ;
204+ _rollSum = 0f ;
205+
206+ _accelSampleCount = 0 ;
207+
208+ if ( ! settings . SeatBeltTensionerEnabled || ! IsConnected )
164209 {
165210 return ;
166211 }
@@ -176,14 +221,57 @@ private void Update( App app )
176221 // Calculate normalized neutral position
177222 var neutralPositionNormalized = ( neutralTenths - 900 ) / ( float ) rangeTenths ;
178223
224+ // Compute gravity components in car body space using averaged pitch and roll.
225+ // iRacing includes gravity in the acceleration telemetry (specific force), so we subtract
226+ // the gravitational contribution to get the true inertial acceleration.
227+ // Pitch: positive = nose up. Roll: positive = left side up.
228+ var cosPitch = MathF . Cos ( pitch ) ;
229+ var sinPitch = MathF . Sin ( pitch ) ;
230+ var cosRoll = MathF . Cos ( roll ) ;
231+ var sinRoll = MathF . Sin ( roll ) ;
232+
233+ // Gravity component along each body axis (what iRacing adds to raw acceleration)
234+ var gravVert = MathZ . OneG * cosPitch * cosRoll ;
235+
236+ float longAccel ;
237+ float latAccel ;
238+ float vertAccel ;
239+
240+ if ( settings . SeatBeltTensionerSubtractFullGravity )
241+ {
242+ var gravLong = MathZ . OneG * - sinPitch ;
243+ var gravLat = MathZ . OneG * cosPitch * sinRoll ;
244+
245+ longAccel = longAccelAvg - gravLong ;
246+ latAccel = latAccelAvg - gravLat ;
247+ vertAccel = vertAccelAvg - gravVert ;
248+ }
249+ else
250+ {
251+ longAccel = longAccelAvg ;
252+ latAccel = latAccelAvg ;
253+ vertAccel = vertAccelAvg - gravVert ;
254+ }
255+
179256 // Surge normalized [-1..1]:
180- var surgeNormalized = Math . Clamp ( - app . Simulator . LongAccel / MathZ . OneG / settings . SeatBeltTensionerSurgeMaxG , - 1f , 1f ) ;
257+ var surgeNormalized = Math . Clamp ( - longAccel / MathZ . OneG / settings . SeatBeltTensionerSurgeMaxG , - 1f , 1f ) ;
181258
182259 // Sway normalized [-1..1]: positive biases right belt tighter, left belt looser
183- var swayNormalized = Math . Clamp ( app . Simulator . LatAccel / MathZ . OneG / settings . SeatBeltTensionerSwayMaxG , - 1f , 1f ) ;
260+ var swayNormalized = Math . Clamp ( latAccel / MathZ . OneG / settings . SeatBeltTensionerSwayMaxG , - 1f , 1f ) ;
184261
185262 // Heave normalized [-1..1]: bumps and crests both tighten both belts
186- var heaveNormalized = Math . Clamp ( app . Simulator . VertAccel / MathZ . OneG / settings . SeatBeltTensionerHeaveMaxG , - 1f , 1f ) ;
263+ var heaveNormalized = Math . Clamp ( - vertAccel / MathZ . OneG / settings . SeatBeltTensionerHeaveMaxG , - 1f , 1f ) ;
264+
265+ if ( MairaAppMenuPopup . CurrentAppPage == MainWindow . AppPage . SeatBeltTensioner )
266+ {
267+ _surgeGraph . Advance ( surgeNormalized ) ;
268+ _swayGraph . Advance ( swayNormalized ) ;
269+ _heaveGraph . Advance ( heaveNormalized ) ;
270+
271+ _surgeGraph . WritePixels ( ) ;
272+ _swayGraph . WritePixels ( ) ;
273+ _heaveGraph . WritePixels ( ) ;
274+ }
187275
188276 // Combine into per-arm normalized signal and offset by neutral position
189277 var leftCombinedNormalized = surgeNormalized + heaveNormalized - swayNormalized + neutralPositionNormalized ;
@@ -197,6 +285,24 @@ private void Update( App app )
197285 var leftTargetPositionTenths = Math . Clamp ( ( int ) MathF . Round ( limitedLeftNormalized * rangeTenths + 900 ) , minimumTenths , maximumTenths ) ;
198286 var rightTargetPositionTenths = Math . Clamp ( ( int ) MathF . Round ( limitedRightNormalized * rangeTenths + 900 ) , minimumTenths , maximumTenths ) ;
199287
288+ if ( MairaAppMenuPopup . CurrentAppPage == MainWindow . AppPage . SeatBeltTensioner )
289+ {
290+ // Remap tenths to [-1..1]: -1=minimum, 0=neutral, +1=maximum (piecewise linear)
291+ var leftShoulderNormalized = leftTargetPositionTenths <= neutralTenths
292+ ? ( float ) ( leftTargetPositionTenths - neutralTenths ) / ( neutralTenths - minimumTenths )
293+ : ( float ) ( leftTargetPositionTenths - neutralTenths ) / ( maximumTenths - neutralTenths ) ;
294+
295+ var rightShoulderNormalized = rightTargetPositionTenths <= neutralTenths
296+ ? ( float ) ( rightTargetPositionTenths - neutralTenths ) / ( neutralTenths - minimumTenths )
297+ : ( float ) ( rightTargetPositionTenths - neutralTenths ) / ( maximumTenths - neutralTenths ) ;
298+
299+ _leftShoulderGraph . Advance ( leftShoulderNormalized ) ;
300+ _rightShoulderGraph . Advance ( rightShoulderNormalized ) ;
301+
302+ _leftShoulderGraph . WritePixels ( ) ;
303+ _rightShoulderGraph . WritePixels ( ) ;
304+ }
305+
200306 // Send the new positions to the SBT if they have changed since the last update
201307 SendSetPosition ( leftTargetPositionTenths , rightTargetPositionTenths ) ;
202308 }
@@ -208,6 +314,18 @@ private void OnPortClosed( object? sender, EventArgs e )
208314
209315 public void Tick ( App app )
210316 {
317+ if ( app . Simulator . IsOnTrack )
318+ {
319+ _longAccelSum += app . Simulator . LongAccel ;
320+ _latAccelSum += app . Simulator . LatAccel ;
321+ _vertAccelSum += app . Simulator . VertAccel ;
322+
323+ _pitchSum += app . Simulator . Pitch ;
324+ _rollSum += app . Simulator . Roll ;
325+
326+ _accelSampleCount ++ ;
327+ }
328+
211329 _updateCounter -- ;
212330
213331 if ( _updateCounter <= 0 )
0 commit comments