Upcoming Events

Weekly BMW races on Simracing.GP Other regular AC events on Simracing.GP Weekly GT3 Sprint Races on Simracing.GP Rookie friendly WTCR sereis Weekly rFactor 2 events

2DOF harness tensionner with Fly PTmover

Messages
69
Points
51
Hi!

My cockpit is static, I've build a pressure gSeat with bladders and RC servo.
I wanted to increase immersion by a harness tensionner.

I'd already tested a static harness tensionner on a 2 DOF plateform, and the feeling was great.
As my rig is static, I'll have to actuate the harness but the advantage is that I can make a 2 DOF tensionner.

principle:
2 DOF harness -> the strength on the right shoulder and left shoulder will be different

both will react along Surge (longitudinal acceleration)
and Sway (lateral acceleration) will tight one shoulder or the other (depending on left turn or right turn)
(maybe later add some heave information?)

harness : 5 points
the 5th will prevent the harness to move up, this will give actual tension (vs movement)!

prefer larger harness 3" (vs only 2")

actuators:
RC servos 35kg.cm (5V to 8,3V)
PSU @7,3V

arduino:
code for 4 servos as I want to combine the harness and the gSeat pressure bladders also RC servo driven.

software:
FlyPT mover https://www.xsimulator.net/community/faq/flypt-mover.29/category

1607507030563.png


1607507067271.png


Here is a video showing step by step how I drive the 2 DOF belt with Fly PTmover:

The strength from 35kg.cm servo is enough.
Power up the servo, sit down, thight the belt as you wish, and start the sim. If you tight the belt before powering up the servos, they are loose and they could be pushed out of their range...

The only drawback, in my opinion is the noise of the servos: they are whinning...

here some infos gathered in this FAQ https://www.xsimulator.net/community/faq/harness-tensioner-simulation.361/

► shopping list
"HV high torque servo motor Robot servo 35kg RDS3235 Metal gear Coreless motor digital servo arduino servo for Robotic DIY"
17€
I chose 270° range
You'll need a dedicated power supply (5V slower to 7,4V fastest)

Speed: 0.13sec/60 degree at(5v)
0.12sec/60 degree at(6v)
0.11sec/60 degree at(7.4v)

Torque: 29kg.cm.at(5v) -1.9A
32kg.cm.at(6v) -2.1A
35kg.cm.at(7.4v) -2.3A

or stronger but unecessary in my opinion
60kgcm 24€
https://www.aliexpress.com/item/4000055027119.html

speed is voltage related
torque is current related!

1/ choose speed AKA voltage
2/ check the spec which gives you the current at chosen voltage
3/ buy your PSU ;-)

if you choose 7,4V PSU, verify it'll be able to deliver up to 2.3A in order to give full torque (add a BIG margin ;-) )
https://fr.aliexpress.com/item/32823922664.html
11€ 7V 10A


here is my ball bearing support (12€ for 2 supports)
they are made of:
P000 https://www.aliexpress.com/item/32833812473.html
Zinc Alloy Diameter Bore Ball Bearing Pillow Block Mounted Support
they allow a big static disalignment as the bearing is mounted like a joint articulation :)

and 200mm Ø10 linear shaft Cylinder Chrome Plated Liner Rods
https://www.aliexpress.com/item/4001294745058.html

roller-jpg.434449


ball-bearing-harness-2-jpg.439581


ball-bearing-harness-1-jpg.439582


► budget:
2x 17€ for 35 kg.cm servo
11€ for 7V 10A PSU
(maybe consider a 12V 10A PSU + an Adjustable Power Module Constant Current 5A)
12€ for bearing rollers
15€ any arduino with a USB port (an original to support the community or a clone)

50€ for a used 3" width 5 points harness
total = 120€ all included



arduino code (for up to 4 servos)
C++:
// Multi Direct
// -> 4 servos
// <255><LeftBelt><127><127><RightBelt>
// Rig : Bit output -> 8 bits
// avec inversion
// PT Mover envoie de 0 à 255 par axe

/*Mover = output "Binary" et "10bits"
Arduino = Byte Data[2]
Data[0] = Serial.read();
Data[1] = Serial.read();
result = (Data[0] * 256 + Data[1]);

OU

Mover = output "Binary" et "8bits"
Arduino = Byte Data
Data = Serial.read(); on obtient directement le résultat*/

#include <Servo.h>  // local library "Servo.h" vs library partagée <Servo.h>

const byte nbServos = 4;

// create servo objects to control any servo
Servo myServo[nbServos];
const byte servoPin[nbServos] = {2, 3, 4, 5};  // pins digitales (pas forcément ~pwm)
const byte inversion[nbServos] = {1, 1, 0, 0 }; // paramètre à changer si un servo part le mauvais sens
int OldSerialValue[nbServos] = {0, 0, 0, 0};
int NewSerialValue[nbServos] = {0, 0, 0, 0};

// servo span:
int servoHomeDegres[nbServos] = { 0, 0, 0, 0}; //sera mis à jour avec la mesure de pression initiale
int servoMaxDegres[nbServos] = { 90, 90, 90, 90}; // cuisseG, cuisseD, côtéG, côtéD
int servoPositionTarget[nbServos] = {0, 0, 0, 0};

const byte deadZone = 0;

// =======================================
// Variables for info received from serial
// =======================================
int bufferPrevious = 0;      // To hold previous read fom serial command
int bufferCurrent = 0;       // To hold current read fom serial command
int bufferCount = 0;         // To hold current position in bufferCommand array
// byte bufferCommand[2*nbServos] = {0};  // (*2 if 10 bits) To hold received info from serial
int bufferCommand[4] = {0};  // To hold received info from serial

void setup()
{
  Serial.begin(115200); // opens serial port at specified baud rate

  // attach the Servos to the pins
  for (byte i = 0; i < nbServos; i++) {
    // pinMode(servoPin[i], OUTPUT); // done within the library
    myServo[i].attach(servoPin[i]);  // attaches the servo on servoPin pin
    }
  // move the servos to signal startup
  MoveAllServos255toDegres(125); // mi-course
  delay(1000);
  // send all servos to home
  MoveAllServos255toDegres(0);
}

void loop()
{
  // SerialValues contain the last order received (if there is no newer received, the last is kept)

  if (Serial.available())
  {
    bufferPrevious = bufferCurrent; // Store previous byte
    bufferCurrent = Serial.read(); // Get the new byte
    bufferCommand[bufferCount] = bufferCurrent; // Put the new byte in the array
    bufferCount++; // Change to next position in the array
    if (bufferCurrent == 255) bufferCount = 0; // one 255 is the start of the position info
    if (bufferCount == nbServos) //si 8 bits, nbServos // si 10 bits nbServos*2
      //Having reach buffer count, means we have the new positions and that we can update the aimed position
    {
      for (byte i = 0; i < nbServos; i++) {
        NewSerialValue[i] = bufferCommand[i];
        //NewSerialValue[i]= (bufferCommand[i*2] * 256) + bufferCommand[i*2+1]; // si 10 bits
      }
      bufferCount = 0;
    }
  }
  // Update orders sent to motor driver
  for (byte i = 0; i < nbServos; i++) {
    if (abs(OldSerialValue[i] - NewSerialValue[i]) > deadZone) {
      if (inversion[i] == 1)
      {
        envoiServoConsigne255toDegres(i, (255 - NewSerialValue[i]));
      }
      else
      {
        envoiServoConsigne255toDegres(i, NewSerialValue[i]);
      }
      OldSerialValue[i] = NewSerialValue[i];
    }
  }
}

void envoiServoConsigne255toDegres(byte servoID, int val )
{
  byte targetDegres;
  val = constrain(val, 0, 255); // constrain coupe au dessus et en dessous : écrêtage et pas mise à l'échelle (comme map)
  // sécurité pour éviter les cas où Simtools enverrait du négatif ou au-delà de 255
  targetDegres = map(val, 0, 255, servoHomeDegres[servoID], servoMaxDegres[servoID]);
  //  map(value, fromLow, fromHigh, toLow, toHigh)
  myServo[servoID].write(targetDegres);              // tell servo to go to position in variable : in steps of 1 degree
  // servo.write(angle)  -> angle: the value to write to the servo, from 0 to 180
}

void MoveAllServos255toDegres( int target)
{
  // send all servos to home
  for (byte i = 0; i < nbServos; i++) {
    envoiServoConsigne255toDegres(i, target);
  }
}

here is the setup:
setup 2DOF harness.png


and the file itself: replace .txt extension by .Mover extension
 

Attachments

  • 4Dof_Belt_255_multiDirect_v6bALPHA.txt
    94.9 KB · Views: 28
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
I can't seem to be sure of the root cause of the failure
My guess: with longer effective arms and larger strap movements,
your servos may stall with arms near right angles,
while shorter arms can use full rotation for harness tension,
applying maximum tension with arms nearly fully closed,
where servos have increased mechanical advantage:
less harness shortening per degree of servo rotation...
This could approach the difference between sin(90 degrees) and sin(0).
 

blekenbleu

Premium
Messages
1,024
Points
2,037
@Wotever's #experimental-belt-tensionner-instructions discord channel

An Arduino nano v3 drives two 280 Watt stepper motor drivers
with motion stop feedback based on a pair of Hall effect sensors.
A bunch of 3D-printed parts are required.

Instructions include this warning:
Warning ! This guide is to be used at your own risks. It involves main voltages, strong motors and chest/shoulders pressure. Even if lot of care has been taken to add security to the arduino firmware, anything could go wrong and I decline any responsibility if something goes wrong/gets broken.

This is a prototype and may not be issues free, feel free to improve and share any parts (code, 3d parts etc….)


Since his motors have about 10x the power of our hobby servos,
that warning does not IMO exaggerate.
I have downloaded his Custom serial device settings file
to see what can be learned for improving mine..
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
what can be learned
For @Wotever user interface:
  • It adds Dead zone (defaults to 0), Stepper speed (defaults to max), and Linearity
  • here are some guesses:
    • Dead zone may be to avoid inconsequential noise
    • Stepper speed may be for reducing crash violence
    • Linearity to account for changes in force vs changes in strap shortening, comparable to brake pedal load cells
  • messages are sent at 30Hz, rather than for changes
    • Arduino code times out after 30 seconds with no messages,
      • cutting stepper motor hold current.
      • this forfeits position control, requiring recalibration for resumed messages.
Within message code:
  • separate messages to set steps for each motor
    • IMO, combining these would reduce PC processing overhead
  • messages immediately return motor step 0 signals when !gameRunning
  • uses GameData.Acceleration*, instead of GameRawData.Physics.AccG0*
    • GameData.Acceleration* return [NULL] instead of 0 when !gameRunning (which is why my Javascript instead uses GameRawData.Physics.AccG0*)
  • relatively high resolution motor control values
    • properly spaced lower resolution tension steps (e.g. 50 or so) are imperceptible and reduce likelihood of motor noise from otherwise imperceptible changes.
  • motor control values are nonlinearity applied to linear sums of heave and sway
    • square root of sum of squares of heave and sway IMO better approximates "friction circle" of accelerations...
  • continuously updating stepper speed value (at 15Hz)
    • This would keep prevent Arduino code from timing out and losing stepper motor position.
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
Design considerations
Linearity to account for changes in force vs changes in strap shortening
3 actuator geometries:
  • reel - every degree of rotation moves the same amount of harness, assuming concentric circular reel cross-section
  • linear - as in Ricmotech G-Sense
  • swing-arm:
tension.png

Swing arm tensioners have inherent non-linearity.
Our hobby servos employ most of 180 degree range,
while e.g. SimXperience G-Belts probably use only about the upper 50%.
@Wotever's Custom serial driver Linearity slider suggests this geometry may not be ideal.

Other design considerations:
  • gearbox vs direct drive
    • direct drive tensioners want motors with torque comparable to that in direct drive steering and should be most responsive and quiet.
  • servo vs stepper motor
    • when a stepper motor is combined with an encoder, it becomes a servo motor with minor distinctions of lower current for the same holding torque and more cogging, depending on sophistication of drive voltage shaping and timings, compared to other motor types.
    • A stepper motor requires higher torque than any expected load in order to not lose track of position. This becomes a safety issue.
    • A servo motor returns position information independent of drive, allowing separate control of torque, which for hobby servos is determined by model selection and drive voltage.
Non-linear swing-arm action allows motors to deliver tension near 0 and 100 percent rotation greater than nominal value determined by effective swing-arm length. However, a question remains: over what range of swing-arm rotations are equal degree changes perceived as equal changes in tension? For e.g. human vision and hearing, perception of intensity increments are approximately logarithmic, with higher sensitivity for small changes at lower intensities.
  • What is the curve shape for just noticeable increases in belt tension?
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
What is the curve shape for just noticeable increases in belt tension?
Human vision can discern roughly 100 just noticeable differences in print densities and CRT display intensities, which made 8-bit digital intensity values, with appropriate non-linearities, suitable for rendering smooth tone gradients. On the other hand, for simulating acceleration forces by harness tensioning, slowly and smoothly incrementing tensions is not compelling. So long as e.g. felt differences between tension steps 4 and 12 are roughly equal to differences between steps 8 and 16, I see no value in rendering an undetected tension increment between any two adjacent steps.
A custom serial Javascript SimHub message was hacked to test for noticeable tension increments:
JavaScript:
// Repurpose gain sliders for just noticeable tension steps
// servo range: 2 to 90
if ($prop('Settings.max_test') || $prop('Settings.TestOffsets'))
  return;
var d = new Date();
var dg = Math.max($prop('Settings.decel_gain'),2);    // base tension
var yg = $prop('Settings.yaw_gain');                // tension increment
if (null == root["odg"]) {
  root["odg"] = dg;
  root["oyg"] = yg;
}
if (dg != root["odg"] || yg != root["oyg"] || null == root["Time"]) {
  root["Time"] = d.getTime();
  root["odg"] = dg;
  root["oyg"] = yg;
}
var s = root["Time"];
var t = d.getTime() - s;
// four tension difference pulses over 3.5 seconds
if (3500 < t)
  d = 2;            // timeout: release tension
else if (3000 < t)
  d = dg + yg;        // base + difference tensions
else if (2500 < t)
  d = dg;            // base tension
else if (2000 < t)
  d = dg + yg;
else if (1500 < t)
  d = dg;
else if (1000 < t)
  d = dg + yg;
else if (500 < t)
  d = dg;
else
  d = dg + yg;    // may be even or odd
var o = 1 + d;  // the other one
//return d.toString()+"\t"+o.toString();
return String.fromCharCode(d)+String.fromCharCode(o);
.. resulting in this "linearization" of perceived tension increments:
JustNoticeable.png

.. where servo values 2 to 90 map to percent rotation values 0 to 100 in the previous reply.
With maximum servo tension about so much as wanted without being distracting,
only 21 tension increments could be perceived under nearly ideal conditions,
including not being distracted by trying to drive at the same time.
Slightly coarsening steps to 16 allows encoding tensions to 4 bits,
leaving 3 of 7 ASCII bits for addressing 7 servos and one address reserved
for downloading a lookup table to convert those 4 tension bits back to servo values.

Applying swingarm geometry to servo values results in this plot
for equalizing perceived tension increments with belt movement:
BeltSteps.png
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
Slightly coarsening steps to 16 allows encoding tensions to 4 bits,
leaving 3 of 7 ASCII bits for addressing 7 servos and one address (0x70) reserved
for e.g. downloading a lookup table to convert those 4 tension bits back to servo values.
JavaScript:
// servo movement range and steps
// 20 experimental just noticeable tension increments (21 steps)
var jndx = [2,9,13,16,19,22,25,28,31,34,37,41,44,48,52,57,62,67,74,82,90];
var n = jndx.length - 1;
var base = jndx[0];
function tweak(value) {
  return value - base;
}
var jnd = jndx.map(tweak);
//return jnd.toString();

var step = [0x70];   // warn Arduino of LUT load
// Arduino adds these to LUT step values to drive left and right servo
step[1] = $prop('Settings.LeftOffset') + base;
step[2] = $prop('Settings.RightOffset') + base;
var m = ($prop('Settings.tmax') - base) / jnd[n];       // max tension rescale factor

step[17] = Math.round(m * jnd[n]);      // rescale last step
// Bresenham thru jnd increments to resample 14 steps
var l = 0;
for (i = 3; i < 17; i++) {
  l += n;
  var x = l / 15;                       // location in jnd[] corresponding to step[i]
  var k = Math.floor(x);                // linear interpolation: index into jnd[]
  var d = jnd[k + 1] - jnd[k];          // servo steps
  x -= k;                               // jnd[] fraction
  step[i] = Math.round(m * (jnd[k] + x * d));
}
step[18] = 127;                         // Arduino sketch sync code
//return step.toString();

step[19] = 0;            // left slackest value:  step[1]
step[20] = 0x10;     // right:  step[2]
if ($prop('Settings.max_test')) {
  step[19] += 15;       // most tense control indices
  step[20] += 15;
}

// send calibration table, then 127, then update servo positions
//return step.toString(); // Arduino would have to parse ASCII
return String.fromCharCode.apply(null,step);
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
Slightly coarsening steps to 16 allows encoding tensions to 4 bits,
leaving 3 of 7 ASCII bits for addressing 7 servos and one address reserved
Generic "blue pill" STM32F103C8T6 modules have seven 5-Volt tolerant PWM pins
suitable for e.g. directly driving hobby servos: 29-31, 42-46:
STM32F103C8T6-Blue-Pill-Pin-Layout.gif

These pins (channels) could be allocated:
  • 2 for harness tensioning
  • 2 for "wind speed' fan controls
  • 3 for G-seat (air bladders)
Implicit in my Arduino code and SimHub Javascript is a notion that a single ASCII character should suffice to set a value for any channel. Whether this can be satisfactorily implemented using only 4 of the 7 available ASCII character bits has yet to be confirmed. It appears not possible to generally use the most significant bit of bytes from SimHub to USB serial devices. This 7-bit constraint appears a Javascript limitation, but may be also a SimHub implementation limitation.

Regardless, a next step is evaluating whether/how well 4-bit precision suffices for harness tensioning.
 

blekenbleu

Premium
Messages
1,024
Points
2,037
Friction circle
The default cloud tune profile ensures that you won't clip (run out of available travel) during normal combinations of lateral and longitudinal G-Forces from trail braking, etc... If you turn the intensity up from there, you may encounter some clipping. Some folks prefer to just focus on heavy braking forces though, so in that case, leaving headroom for lateral forces is unneeded and you could crank up the intensity.
Based on @Wotever code:
var res = Math.min(Math.max(surgeComponent + swayComponent,0),1) ;
.. and the above quote, some harness tensioner code employs less than ideal math,
since most tires, if not overloaded, have similar coefficient of friction in any direction.
Supposing, for example, that coefficient of friction is 1.0, then a car with neither aerodynamic lift nor downforce can corner with 1 G or brake with 1 G but not both at the same time.
Maximum equal braking and cornering forces will be half the square root of 2 or about 0.707.
In this plot, sin(x) represents lateral G:
FrictionCircle.png

.. then the green plot represents available braking (or accelerating) G at the most loaded
(but not overloaded) tire. Using overly simplistic arithmetic sum would yield blue + curve,
resulting in unrealistically low tension on the "inner" shoulder harness strap. Turned around,
when telemetry reports 0.7 G deceleration and 0.7 G cornering, arithmetic sum
will tension one harness as if that tire could exert 1.4G cornering load, typically provoking clipping for IMO most interesting values of simultaneous cornering and braking forces.
 

blekenbleu

Premium
Messages
1,024
Points
2,037
sadly my motors burnt out
A RDS3235 servo failed while testing whether I could feel differences between 7-bit direct PWM and 4-bit LUT-to-PWM tension control granularity, and I mistakenly paused a SimHub session playback with tension applied. The servo that died had always run warmer and drawn more current than the survivor, perhaps from a short among motor windings. I'll order replacement RDS3235 servos, (to have a spare) as well as a pair of the stronger RDS5160 servos that @deandsfsdfewsad overloaded, which may be deployed for air pillow G-seat experiments.

I'll also reduce drive voltage from 8.1 to the recommended 7.4V..
eBay offers 3-year warranties @ $3 for RDS5160 servos...
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
whether I could feel differences between 7-bit direct PWM and 4-bit LUT-to-PWM tension control granularity
Here is an updated SimHub Custom serial profile for experimenting with just-noticeable-tension changes and granularity of control values. It still uses the same Arduino sketch for driving hobby servo tensioners.
Usage for this profile is described near the bottom of this page.
Since one harness tensioner servo failed, that Arduino sketch will not be updated for a month or so.
Preliminary results:
  • many tension changes are limited by servo slew rate, so effectively independent of granularity
  • slow, smooth gradual deceleration and lateral acceleration changes while racing are quite rare
    • IMO highly unlikely to notice 4-bit granularity while actually busy driving
    • It seems arguable whether I could consistently tell 4-bit from 7-bit granularity in a blind test while replaying the same session, back-to-back, and concentrating on tension changes.
    • "real" LUTs will have equal increments of just noticeable difference, while this test was between unequalized 7- and 4-bit granularity steps, and some should be more noticeable than others.
  • A servo died while testing granularity changes (not a blind test) and specifically looking for instances of recorded telemetry where differences might be felt.
 

blekenbleu

Premium
Messages
1,024
Points
2,037
Karl Gosling pre-announces Sim Racing Studio harness tensioner (Fall 2021)
Not really worth watching; mostly goes over SRS software features done IMO better in SimHub.
 
Messages
1,076
Points
1,952
ball-bearing-harness-2-jpg.439581


This is a very interesting, easy, unexpensive and functional idea. I suppose there is no alternative that doesn't implies drilling my bucket seat, is it?

I think I could use a couple of tubes made of polycarbonate or something like that, make a cut so that they can be opened and fitted on the base of the holes in the bucket. It would not be so efficient, but there would be less fiction and the sliding would be smoother than before.
 

blekenbleu

Premium
Messages
1,024
Points
2,037
alternative that doesn't implies drilling my bucket seat
Instead of drilling the seat, drill (aluminum, plastic or wood) flat stock,
which can be secured to the seat using double-sided tape AKA carpet tape.

Alternatively, make similar cutouts in e.g. squares of plywood, mount pulleys to squares and secure those to seat cutouts using paired flat metal hooks, e.g. cabinet finger pulls:

hook.jpg
pull.jpg
 
Last edited:
Messages
1,076
Points
1,952
Would you install the seatbelts crossed on the back of the seat (for a seat mover)? I mean that way when the seat moves left you would feel some tension on the right belt? Or isn't it worth?
 

blekenbleu

Premium
Messages
1,024
Points
2,037
what you can achieve
It finally occurred to me that, although an STM32 Blue Pill can simultaneously drive 7 PWM outputs,
wind fans are probably too far from the seat for reliably driving 25kHz PWM signals without additional electronics (although some available DMX512 cable should be good to at least 250kb/s);
a dedicated Stm32duino module for sim wind should be cheaper or at least easier.
Roughly $60 with 2 fans and a 12V power supply.
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
Would you install the seatbelts crossed on the back of the seat (for a seat mover)? I mean that way when the seat moves left you would feel some tension on the right belt? Or isn't it worth?
I don't know.
With a seat mover, I might try both ways, limiting strap movements to 4-5cm or perhaps trampoline springs?

In real life, hard right turns move the torso left, so more tension on the right harness,
but one's neck can push against the left harness, which motivates padding those straps.
If a seat mover tilts left for right turns, inertia has head initially moving right, which seems wrong...

Lacking a seat mover, tightening the right strap for right turns feels more correct,
having tried both ways.

Active tension by hobby servos, Arduino and SimHub is not very difficult...
 
Last edited:

blekenbleu

Premium
Messages
1,024
Points
2,037
a pair of the stronger RDS5160 servos
On incompletely dimensioned (but hopefully to scale) drawings @ AliExpress,
RDS5160 rotating bracket appears to have about 37mm effective radius,
compared with about 27mm for RDS3235,
so nearly equal strap tensions but 40% longer strap movements.
For 60 degrees at 7.4V, RDS3235 are rated 0.11 sec, while RDS5160 are 0.15 sec.
which works out to nearly identical strap movement speed, after factoring in bracket radii.

Nearly 75mm of strap shortening may be uncomfortably tight
as well as more liable to provoke servo stall and overheating.
 
Top