KMBox NET Tester

Outil console en C# (.NET 8) qui pilote un boîtier KMBox NET en UDP : poignée de main, mouvements et clics de test, mesure de la latence, de la gigue et des pertes de paquets.

Présentation

Le KMBox NET est un boîtier matériel qui se comporte comme une souris et un clavier, et reçoit ses commandes par le réseau. Quand le boîtier ne réagit pas, la cause peut être le câble, le port, l'identifiant de l'appareil, la carte réseau ou le pare-feu. J'ai écrit KMBox NET Tester pour trancher vite : une application console en C# sur .NET 8 qui parle le protocole UDP du boîtier, envoie des mouvements et des clics de test, et mesure le temps de réponse de la liaison. Le README la présente comme utile pour les configurations dites DMA, et son avertissement évoque un mauvais usage dans les jeux en ligne. L'outil ne vise aucun jeu en particulier et ne lit aucune mémoire. Le code C# tient en quatre fichiers.

Un paquet de 72 octets écrit champ par champ

J'ai reproduit dans KMBoxClient la structure de paquet officielle et les codes de commande de l'en-tête C++ kmboxNet.h. Chaque paquet commence par 16 octets d'en-tête : quatre entiers non signés de 32 bits en little-endian, qui portent l'identifiant de l'appareil (saisi en hexadécimal), une valeur aléatoire, un index incrémenté à chaque envoi et le code de la commande. Les commandes souris ajoutent 56 octets de charge utile, soit 72 octets au total. Le premier entier de cette charge porte l'état du bouton, les deux suivants le déplacement relatif en x et en y.

J'écris chaque champ avec BinaryPrimitives sur des Span plutôt que de marshaller une structure : WriteUInt32LittleEndian pour l'en-tête, WriteInt32LittleEndian pour la charge, où x et y sont signés. Les décalages restent visibles et l'ordre des octets ne dépend pas de la machine. La classe déclare six commandes, mais le client n'en envoie que trois : la connexion, le déplacement et le clic gauche.

Une boucle d'écoute pour chronométrer l'aller-retour

Le client ouvre un UdpClient sur un port choisi par le système. Si l'utilisateur donne l'adresse IP de sa carte Ethernet, le socket se lie à cette interface, ce qui compte sur un PC à plusieurs cartes réseau. Dès sa construction, il lance en tâche de fond ReceiveLoop, qui écoute en continu avec ReceiveAsync.

Avant la poignée de main, IsReachableAsync envoie un ping ICMP avec un délai de 500 ms, pour séparer un problème de câblage d'un problème de protocole. Pour la mesure, MeasureLatencyAsync crée un TaskCompletionSource<bool>, démarre un Stopwatch, envoie un paquet de connexion réduit à l'en-tête et met la réponse en concurrence avec Task.Delay(1000) grâce à Task.WhenAny. Le premier datagramme reçu par la boucle complète la tâche et arrête la mesure. Sinon, la méthode renvoie -1, compté comme une perte.

Au lancement, l'outil demande l'adresse, le port et l'identifiant du boîtier ainsi que l'IP locale, vérifie que le boîtier répond avant d'envoyer la poignée de main, puis ouvre le menu de test.

Dix échantillons et quatre paliers

La méthode Program.RunDiagnostics enchaîne dix mesures, avec une pause de 100 ms après chacune. Elle en tire la moyenne, le minimum, le maximum, la gigue et le taux de perte. La moyenne est classée en quatre paliers : sous 1 ms, sous 3 ms, sous 7 ms et au-delà. La capture diagnostics.png du dépôt montre une moyenne de 2,69 ms sans perte.

Dix échantillons d'aller-retour, puis la synthèse : 2,69 ms de moyenne, aucune perte, et une gigue de 0,54 ms qui n'est que l'écart entre le maximum et le minimum.

Le menu propose aussi un déplacement unique de (100, 100), un clic gauche (appui, puis relâchement 50 ms plus tard) et un test de cercle. L'adresse, le port et l'identifiant sont enregistrés dans config.json avec System.Text.Json et proposés par défaut au lancement suivant.

Un cercle qui ne dérive pas

Le paquet de déplacement ne porte que des déplacements relatifs entiers. En arrondissant l'écart entre deux points théoriques successifs, l'erreur s'accumulerait à chaque pas et le cercle glisserait. Tester.RunCircleTest garde donc la position cumulée réellement envoyée et calcule chaque déplacement à partir d'elle : l'écart avec la trajectoire idéale ne dépasse pas une demi-unité par axe.

angle += stepSize;
double targetX = radius * Math.Cos(angle);
double targetY = radius * Math.Sin(angle);
 
int deltaX = (int)Math.Round(targetX - lastX);
int deltaY = (int)Math.Round(targetY - lastY);
 
if (deltaX != 0 || deltaY != 0)
{
    client.SendMove(deltaX, deltaY);
    lastX += deltaX; lastY += deltaY;
}

Le menu le lance avec un rayon de 200, 100 pas par tour et 10 ms de pause par pas, jusqu'à l'appui d'une touche.

Ce que je corrigerais aujourd'hui

La boucle de réception libère la mesure sur n'importe quel datagramme, sans lire son contenu ni vérifier qu'il répond au paquet envoyé. Une réponse tardive à un échantillon perdu peut clore trop tôt la mesure suivante. J'apparierais chaque réponse à sa requête, par exemple avec l'index de l'en-tête si le boîtier le renvoie.

Calculée comme maximum moins minimum, la gigue ne dépend que des deux valeurs extrêmes. Un écart-type serait plus représentatif. Le test de cercle, cadencé par Thread.Sleep dont la précision dépend du timer de Windows, ne mesure pas le rythme d'envoi obtenu : il montre la fluidité à l'œil.

La configuration est enregistrée avant le test de connexion, alors que le README parle de la dernière configuration valide. Le programme s'abonne à l'événement OnLog, que le client ne déclenche jamais. La solution kmbox tester.slnx ne référence pas le projet C#, seulement un projet C++ « Hello World » généré par Visual Studio, dont les fichiers de compilation sont versionnés. Enfin, il n'y a aucun test automatisé, alors que la construction des paquets s'y prête bien.