Dans l’article précédent, nous avons pu faire fonctionner toute la partie hardware en pilotant des ventilateurs par la carte Arduino elle-même commandée par le PC. Maintenant il faut construire le programme qui va permettre de récupérer les informations du jeu Assetto Corsa, les traiter et commander les ventilateurs en conséquence.
La principale difficulté est de pouvoir récupérer les données du jeu de manière à pouvoir les utiliser. Plusieurs solutions existent sur Internet, mais celles-ci sont soit payantes soit pas adaptées et ce que je souhaite faire. Durant mes recherches je suis tombé sur ce dépôt Github de mdjarv. C’est un projet C# qui récupère les informations de la mémoire partagée de AssettoCorsa et de les mettre à disposition dans un objet C#. C’est la découverte de ce dépôt qui a déclenché tout le reste du projet.
Architecture du programme
Le PC hôte effectue donc plusieurs actions :
- Il attend d’être connecté à l’Arduino via le port série. Si celui-ci ne répond pas suivant le protocole, alors le PC considère que l’Arduino n’est pas connectée ;
- Si l’Arduino est connectée, alors il attend que le module assettocorsasharedmemory se connecte au jeu Assetto Corsa et récupère les données du jeu ;
- Si toutes ces conditions sont réunies, alors le programme suit le fonctionnement principal suivant :
- Il récupère de la mémoire partagée la vitesse du véhicule en km/h et la vitesse angulaire locale sur l’axe Y (explication à suivre)
- Il calcule une valeur de puissance pour chaque ventilateur allant de 0 à 255 en se basant sur des constantes définis comme par exemple la vitesse maximale, l’angle du ventilateur, ou encore une valeur de Gamma permettant d’ajuster le ressenti
La classe AssettoCorsaWindSimController représente le système complet.
Celle-ci contient une liste d’objets FanParameters qui représentent les constantes mentionnées précédemment pour chacun des ventilateurs, un objet ArduinoSerialCom permettant de communiquer avec l’Arduino (et donc de piloter les ventilateurs), et un objet AssettoCorsa représentant le jeu et les données extraites de sa mémoire partagée.
public AssettoCorsaWindSimController() {
ac = new AssettoCorsa();
ac.PhysicsInterval = 30;
ac.GraphicsInterval = 100; // Used for GameStatusUpdate
ac.PhysicsUpdated += ac_PhysicsInfoUpdated;
ac.GameStatusChanged += ac_GameStatusUpdated;
fansController = new ArduinoSerialCom();
fansControllerTimer = new Timer(3000);
fansControllerTimer.AutoReset = true;
fansControllerTimer.Elapsed += fansControllerTimer_Elapsed;
updateFansPower = false;
fansParams = new List<FanParameters>();
fansControllerTimer.Start();
}
Le timer fansControllerTimer
permet de détecter la présence ou non de l’Arduino et de s’y connecter. Une fois connectée, on récupère les différentes informations (pour l’instant simplement la version de firmware pour être sûr qu’il s’agit bien d’une carte communicante avec le bon protocole) et on démarre l’objet assettocorsasharedmemory
. Le timer reste actif tout le temps que le programme est actif, mais le contenu n’est exécuté que si la carte n’est pas connectée.
private void fansControllerTimer_Elapsed(object? sender, ElapsedEventArgs e ) {
if (!fansController.IsConnected) {
if (_debug_verbose>=1) Console.WriteLine("Trying to connect...");
foreach (string comport in fansController.ComportList) {
bool _connected;
lock(fansController) {
_connected = fansController.Connect(comport, 115200);
}
if (_connected) {
// Here, we know there will be 2 fans.
// TODO : plan to fetch infos from Arduino
// TODO : change FanParameters dynamically from the chosen car (even chosen combo car/track ?)
fansParams.Add(new FanParameters(30f));
fansParams.Add(new FanParameters(-30f));
fansController_activate();
ac.Start();
}
}
}
}
L’objet assettocorsamemoryshared
se connecte alors à la mémoire si elle est disponible, et mets à jour toutes les 30 millisecondes les valeurs (le contenu de la variable ac.PhysicsInterval
indique la période de raffraichissement, c’est-à-dire le temps qui s’écoulera entre deux appels à la fonction ac_PhysicsInfoUpdated
).
Cette fonction récupère la vitesse en km/h et une valeur appelée localangularvelocity
. Cette valeur est la variation de l’angle en radian, locale en terme de temps. On la convertit en degrés pour avoir une ordre de grandeur plus facilement compréhensible pour l’être humain (moi, en tout cas).
On affiche les différentes valeurs sur la console pour du debug (comme sur la vidéo ci-dessous) et on envoie les informations à la fonction de mise à jour de la puissance de souffle.
private void ac_PhysicsInfoUpdated(object? sender, PhysicsEventArgs e) {
float[] localangularvelocity = e.Physics.LocalAngularVelocity;
// Important data
float speedKmh = e.Physics.SpeedKmh;
float localangularvelocityY = localangularvelocity[1];
float localangularvelocityYDegrees = localangularvelocityY / MathF.PI * 180.0f;
if (_debug_verbose>=2 && gameStatus == AC_STATUS.AC_LIVE) Console.Write("{0, 6:F2}km/h, {1, 6:F1} ", speedKmh, localangularvelocityYDegrees);
fansController_Update(speedKmh, localangularvelocityY);
}
A la réception de ces données, on appelle les fonctions de calcul de la puissance pour chacun des ventilateurs, on affiche sur la console les valeurs obtenues puis on les envoie à la carte Arduino par l’intermédiaire de fansController.SetFanAPower
et fansController.SetFanBPower
.
private void fansController_Update(float speedKmh, float localAngularVelocityY) {
uint fanA_power = fansParams[0].CalculatePower(speedKmh, localAngularVelocityY);
uint fanB_power = fansParams[1].CalculatePower(speedKmh, localAngularVelocityY);
if (fansController.IsConnected && updateFansPower) {
String fanAString = "";
String fanBString = "";
if (fansParams[0].overload) fanAString += "+";
if (fansParams[0].underload) fanAString += "-";
if (fansParams[1].overload) fanBString += "+";
if (fansParams[1].underload) fanBString += "-";
if (_debug_verbose>=2) Console.WriteLine("| {0, -3} - {1, 3} | ({2,-2},{3,2})", fanA_power, fanB_power, fanAString, fanBString);
try {
lock(fansController) {
fansController.SetFanAPower(fanA_power);
}
}
catch (Exception ex) {
if (_debug_verbose>=0) Console.WriteLine("Could not send Power command for Fan A : "+ex.ToString());
}
try {
lock(fansController) {
fansController.SetFanBPower(fanB_power);
}
}
catch (Exception ex) {
if (_debug_verbose>=0) Console.WriteLine("Could not send Power command for Fan B : "+ex.ToString());
}
}
}
Le calcul de la puissance des ventilateurs se base sur plusieurs formules mathématiques différentes, dépendant du type choisi. Le premier type, « SPEED_ONLY », ne tient compte que de la vitesse du véhicule en km/h. Le deuxième type, « STRICT_VECTOR_PROJECTION », utilise la projection vectorielle pour tenir compte de la vitesse angulaire locale sur l’axe Y ainsi que de l’angle réel du ventilateur. Cependant, ce type de calcul n’a pas donné les résultats escomptés en termes de ressenti. C’est pourquoi le troisième type, « EXAGERATE_VECTOR_PROJECTION », a été créé pour explorer les effets sur le ressenti en utilisant une projection vectorielle exagérée. Dans ce type de calcul, le programme utilise un angle de projection de 60 degrés au lieu de l’angle réel du ventilateur.
La variable « gamma » sert à ajuster la réponse de la puissance des ventilateurs. Elle est utilisée pour effectuer une puissance exponentielle sur la valeur de puissance calculée, ce qui permet d’obtenir une réponse plus ou moins agressive. Par exemple, si la valeur de « gamma » est élevée (plus grand que 1), la réponse des ventilateurs sera plus douce, et si la valeur de « gamma » est faible (plus petit que 1), la réponse sera plus agressive.
public uint CalculatePower(float speedKmh, float angularVelocity) {
float powerValue = 0;
switch(powerCompFunc) {
case POWER_COMPUTATION.SPEED_ONLY:
default:
powerValue = ComputeSpeedOnly(speedKmh, angularVelocity);
break;
case POWER_COMPUTATION.STRICT_VECTOR_PROJECTION:
powerValue = ComputeVectorProj(speedKmh, angularVelocity, angle);
break;
case POWER_COMPUTATION.EXAGERATE_VECTOR_PROJECTION:
powerValue = ComputeVectorProj(speedKmh, angularVelocity, 60.0f);
break;
}
overload = powerValue > 1f;
underload = powerValue < 0f;
if (overload) powerValue = 1f;
if (underload) powerValue = 0f;
return Convert.ToUInt32(MathF.Pow(powerValue, gamma)*255f);
}
private float ComputeSpeedOnly(float speedKmh, float angularVelocity) {
return (float)(speedKmh/maxSpeed);
}
private float ComputeVectorProj(float speedKmh, float angularVelocity, float targetAngle) {
float baseValue = (float)(speedKmh/maxSpeed);
float angleRad = angle / 180.0f * MathF.PI;
float targetAngleRad = targetAngle / 180.0f * MathF.PI;
return baseValue * MathF.Cos(targetAngleRad*(angleRad - angularVelocity)/angleRad);
}
Démonstration
La vidéo ci-dessus fait la démonstration avec la McLaren MP4-12C GT3 sur le circuit du Red Bull Ring. La démonstration a été faite à la manette, mais des tests ont également été faits en condition « immersive » comme le montre la photo ci-dessous.
Les ventilateurs ont été configuré de la façon suivante :
– puissance max (255) correspond à une vitesse de 250 km/h
– angle défini à 30° et -30° (mais pas d’importance au vu du choix de la formule mathématique)
– gamma à 0.5 pour une réponse plus aggressive. C’est le choix qui donne le plus de dynamique au niveau de ressenti du souffle
– fonction EXAGERATE_VECTOR_PROJECTION pour exagérer le ressenti.
Pistes d’amélioration
Le système est à peu près fonctionnel comme il est. Il existe un bug qui maintient les ventilateurs en marche malgré la mise en pause du jeu qu’il va falloir que je corrige.
Une première piste d’amélioration visuellement évidente est la création d’un PCB dédié. Les branchements des ventilateurs et de l’alimentation 12V seraient plus simple. On pourrait imaginer la présence de header femelles et de header mâles pour pouvoir utiliser le système en shield Arduino Uno ou pour accueillir une Arduino Nano. La vision industrielle encore plus radicale serait de venir placer un microcontrôleur directement sur le PCB.
Une deuxième piste d’amélioration est le modèle et le type de ventilateur utilisé. En effet, les Noctua ont été conçu pour être silencieux et pour pouvoir faire circuler l’air pour refroidir au mieux des composants pour PC. Même à leur puissance max, ils ne produisent pas énormément de souffle, ou en tout cas trop peu pour moi. Il est peut-être possible d’utiliser les différents paramètres pour améliorer le ressenti, mais je crains que cela ne soit pas suffisant.
Enfin, des pistes d’améliorations en vrac :
- Réfléchir à une intégration mécanique
- Rendre le logiciel compatible avec d’autres jeux, de courses et autres
- Interface graphique permettant de customiser les constantes et les paramètres de manière la plus intuitive possible. L’idéal serait de donner la possibilité à des bétatesteurs de tester le système pour déterminer les meilleurs paramètres pour un ressenti optimal en fonction des ventilateurs utilisés.
- Mettre des I/O pour étendre le contrôle de plus de choses (le but ultime serait de faire de AssettoCorsaWindSim un logiciel gratuit et open-source pour interfacer un grand nombre d’éléments hardware avec les jeux de courses, avec la possibilité d’acheter du hardware customisable pour des accessoires simracing)
Ping : AssettoCorsaWindSim - Interface graphique - Paul Chanvin