Panneau de commande de bricolage de vaisseau spatial bricolage


Bonjour chers lecteurs!

Une idée m'est venue ici, mais pas pour assembler un panneau de commande pour un vaisseau spatial. Vers USB. Avec prise en charge du pilote natif. HID personnalisé. Pour coller et tout fonctionne, sans danses ni tambourins. En conséquence, nous avons obtenu une sorte de «gamepad» monstrueux pour les simulateurs spatiaux. En général, jugez par vous-même.

Au début, je n'avais aucune idée de ce qui allait se passer à la fin. Je voulais deux joysticks principaux, comme sur Soyuz-MS, quelques commutateurs, boutons et plusieurs écrans.

Ayant estimé la surface de travail de ma table, j'ai choisi les dimensions de la console en largeur et profondeur 500 * 300 mm. Et après avoir fouillé dans les entrepôts de construction et les magasins à la recherche de matériaux de construction, il a choisi une hauteur de 125 mm. En conséquence, j'ai acquis une feuille de contreplaqué de 4 mm, des lattes de 20 * 12 mm et une planche de 120 * 20 mm.

Dans le cad, un croquis de la télécommande a été rapidement esquissé. Et je l'ai fait dans un arbre pendant très longtemps. Trois mois. le week-end. Et pas parce qu'il travaillait si imposant comme scie, mais par manque de temps. Le panneau était mastic, poncé et peint avec de la peinture émaillée, de couleur similaire à de vrais panneaux de vaisseaux spatiaux ou d'avions.



Mais pour l'instant, laissez le travail de peinture de côté et je parlerai de rembourrage électronique.

Des pièces radio ont été achetées sur Ali. En tant que joysticks, je les ai trouvés. En général, la situation avec de tels joysticks est une couture complète. Les solutions industrielles sont trop chères, mais bon marché, se présentent comme des jouets et donc mauvaises. Ils sont de très bonne qualité, mais ils ne sauront pas combien de temps ils seront.


Le reste de la petite chose n'a pas causé de problèmes. Le contrôleur a sélectionné STM32. En tant qu'ADC pour joysticks, ADS1118 16 bits. Une alimentation 12 V a également été achetée. En fait, cette tension est due au fait que j'ai obtenu une jauge de carburant du «shah», que je voulais également attacher ici.


Sur la photo, l'alimentation, stabilisateurs pour 5 et 3,3 V, STM32, MCP23017, ADS1118

Contrôleur 100 broches STM32F407VET6, connecté à celui-ci:

2 sélecteurs à 4 positions
1 résistance variable
2 commutateurs d'essieu
4 axes principaux
2 axes auxiliaires
2 essieux de contrôle
4 interrupteurs à clé, 2 boutons chacun
20 boutons avec LED
4 interrupteurs principaux avec LED
2 boutons champignon avec LED
2 boutons de minuterie
3 interrupteurs avec LED
13 interrupteurs
2 ADS1118 (ADC)
4 MAX7219 (écrans LED à 8 chiffres)
2 TM1637 (heures d'affichage)
1 PCF8574 (extenseur d'E / S, branché sur l'écran de synthèse de caractères)


La structure résultante

Ce sera un peu trop pour des centaines de jambes du MK, j'ai décidé, et j'ai ajouté ici des extenseurs d'E / S: quatre pièces de MCP23017, pour 16 entrées ou sorties chacune. À l'avenir, je dirai que le délai d'interrogation des entrées de l'extenseur s'est avéré être d'environ 0,13 ms par puce, à une vitesse de bus I2C de 400 kHz. Autrement dit, il avec une marge couvre le temps d'interrogation USB minimum de 1 ms.

Afin de ne pas piloter le bus I2C avec des requêtes inutiles, le MCP23017 possède des sorties d'interruption qui sont réglées lorsque l'état des entrées change. Je les ai également appliqués dans mon projet. En fin de compte, en raison du vacillement des contacts, ces interruptions étaient inutiles.

L'ADS1118 ADC ne suit pas quelque peu la vitesse USB, ses performances déclarées sont au maximum de 820 échantillons par seconde, ce qui correspond à 1,2 ms, tandis qu'il dispose de plusieurs entrées déjà connectées à l'ADC via le multiplexeur. J'ai utilisé 2 entrées sur une puce, donc le temps de mise à jour des valeurs est de 2,4 ms. Mauvais, mais que pouvez-vous faire? Malheureusement, il n'y a pas d'autre ADC rapide 16 bits sur Ali.


À l'intérieur, cela ressemble à ceci, mais après avoir installé les fils, c'est bien pire

Le programme CPU est écrit dans le style d'un programme PLC. Aucune demande de blocage. Le noyau n'attend pas la périphérie, n'a pas eu le temps et l'enfer, au prochain cycle il interrogera. Il n'y a pas non plus de RTOS dans le projet, je l'ai essayé, j'ai rencontré un temps d'attente de tâche minimum de 1 ms - il s'avère lentement si nous devons envoyer des données via USB avec une fréquence de 1 ms. En conséquence, j'ai réalisé que j'utiliserais le système d'exploitation sans osDelay (), puis pourquoi RTOS? Tout comme dans un automate, placer les instructions du programme une par une dans une boucle infinie suffit.

Bien sûr, utilisé les bibliothèques CubeMX et HAL. Au fait, je suis récemment passé à HAL et je me suis interrogé sur la commodité. Je ne sais pas pourquoi ce n’est toujours pas très populaire, l’essentiel est de le comprendre au début, puis ça se passera très simplement. C'est comme si vous programmiez Arduino.

L'appareil que nous aurons USB HID personnalisé. HID est une souris, un clavier, une manette de jeu, un joystick, et plus encore. Et il y a une coutume. Tout cela ne nécessite pas de pilotes du système d'exploitation. Plus précisément, ils sont déjà écrits par le développeur. Un appareil personnalisé est bon en ce que nous combinons nous-mêmes les capacités de tous les appareils ci-dessus à notre discrétion.

En général, le truc USB est très compliqué, il a un manuel de près de mille pages et vous ne pouvez pas le prendre en un clin d’œil. Qui ne veut pas lire les manuels lourds, il y a un super article USB dans un NutShell, google. Elle a également une traduction. Je vais essayer d'expliquer certains points "sur les doigts".

Transfert de données par paquets USB avec un tas de niveaux et d'abstractions. L'appareil est avec nous - il ne peut pas demander de données, l'hôte initie l'intégralité du transfert. L'hôte écrit et demande des données aux soi-disant points de terminaison, physiquement ce sont des tampons dans la mémoire MK. Pour que l'hôte comprenne par quels points de terminaison il est possible d'écrire, quels points de terminaison à lire et quelles données il peut interpréter comme des boutons et des axes de notre appareil et, en général, quel type d'appareil nous avons ici, au début de la connexion, il demande des descripteurs d'appareil. Il y a beaucoup de ces descripteurs et il est difficile de les composer et vous pouvez comme vous le souhaitez, et aussi faire des erreurs n'importe où. Physiquement, il s'agit d'un tableau d'octets.

En fait, CubeMX générera un code d'initialisation HID personnalisé mieux que nous.





Veuillez faire attention à la dernière image sous le numéro 3. C'est la taille du descripteur en octets, qui détermine quels axes et boutons se trouvent sur notre appareil. Ce descripteur est généré dans l' outil de description HID . Il existe plusieurs exemples d'autoformation. En général, voici mon descripteur. Il n'y a pas encore de données pour les affichages, pour faciliter la compréhension, mais tous les boutons et axes des joysticks sont présents. Il doit être placé dans le fichier usbd_custom_hid_if.c. Par défaut, cette poignée rend le cube vide.

Descripteur HID (taille 104 octets)
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END = { /* USER CODE BEGIN 0 */ 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x09, 0x04, // USAGE (Joystick) 0xa1, 0x01, // COLLECTION (Application) 0x05, 0x02, // USAGE_PAGE (Simulation Controls) 0x09, 0xbb, // USAGE (Throttle) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x27, 0xff, 0xff, 0x00, 0x00, // LOGICAL_MAXIMUM (65535) 0x75, 0x10, // REPORT_SIZE (16) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x01, // USAGE (Pointer) 0xa1, 0x00, // COLLECTION (Physical) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x95, 0x02, // REPORT_COUNT (2) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x32, // USAGE (Z) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x33, // USAGE (Rx) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x34, // USAGE (Ry) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x35, // USAGE (Rz) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x36, // USAGE (Slider) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x39, // USAGE (Hat switch) 0x15, 0x01, // LOGICAL_MINIMUM (1) 0x25, 0x08, // LOGICAL_MAXIMUM (8) 0x35, 0x00, // PHYSICAL_MINIMUM (0) 0x46, 0x0e, 0x01, // PHYSICAL_MAXIMUM (270) 0x65, 0x14, // UNIT (Eng Rot:Angular Pos) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x40, // USAGE_MAXIMUM (Button 64) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x40, // REPORT_COUNT (64) 0x55, 0x00, // UNIT_EXPONENT (0) 0x65, 0x00, // UNIT (None) 0x81, 0x02, // INPUT (Data,Var,Abs) /* USER CODE END 0 */ 0xC0 /* END_COLLECTION */ }; 


En fait, il peut être composé comme vous le souhaitez, vous définissez d'abord la PAGE UTILISATION et l'UTILISATION requise, par exemple, l'axe USAGE (Throttle), puis après le mot INPUT (Data, Var, Abs), le système supposera que nous avons l'axe "Gas". La dimension de l'axe variable et son nombre sont définis par les paramètres LOGICAL_MAXIMUM, MINIMUM, REPORT_SIZE, REPORT_COUNT, qui doivent être avant INPUT.

Plus de détails sur ces paramètres, ainsi que ce que (données, Var, Abs) peuvent être trouvés dans la définition de classe de périphérique pour les périphériques d'interface humaine (HID) v1.11 .

Ce qui suit est un exemple d'initialisation de l'axe des gaz à partir de mon descripteur. Dans cet exemple, Throttle a une plage de valeurs de 0 à 65535, ce qui correspond à une variable uint16_t.

  0x05, 0x02, // USAGE_PAGE (Simulation Controls) 0x09, 0xbb, // USAGE (Throttle) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x27, 0xff, 0xff, 0x00, 0x00, // LOGICAL_MAXIMUM (65535) 0x75, 0x10, // REPORT_SIZE (16) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 

Et oui, pourtant, disons que vous ne pouvez pas écrire LOGICAL_MAXIMUM, MINIMUM, REPORT_SIZE, REPORT_COUNT à chaque fois, l'hôte déterminera cette valeur par le paramètre précédent. Ceci est illustré par les axes qui se succèdent, sans préciser la taille et le nombre:

  0x09, 0x32, // USAGE (Z) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x33, // USAGE (Rx) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x34, // USAGE (Ry) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x35, // USAGE (Rz) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x36, // USAGE (Slider) 

La structure suivante correspond à tout ce descripteur, qui est plus haut sous le spoiler. En fait, il n'est plus obligatoire, il est juste plus pratique d'enregistrer sur la base de pointeurs.

 #pragma pack(push, 1) typedef struct _myReportStruct { uint16_t Throttle; uint16_t X; uint16_t Y; uint16_t Z; uint16_t Rx; uint16_t Ry; uint16_t Rz; uint16_t Slider; uint8_t Hat; // 0 - none, 1 - up, 2 - up-right, 3 - right, 4 - down-right... uint32_t Buttons1; // 32 buttons of 1 bit each uint32_t Buttons2; // 32 buttons of 1 bit each }myReportStruct; #pragma pack(pop) volatile myReportStruct Desk; 

Cette structure peut être envoyée à l'hôte par la fonction

 USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t *) &Desk, sizeof(Desk)); 

Le premier paramètre est une poignée USB, il est déjà créé dans notre cube. Vous devrez peut-être inclure le fichier nécessaire avec l'inclusion où ce descripteur est initialisé pour la première fois et écrire extern USBD_HandleTypeDef hUsbDeviceFS; afin que vous puissiez travailler avec lui. Le deuxième paramètre est un pointeur sur notre structure et le troisième est la taille de la structure en octets.

Après avoir rempli et fait clignoter le contrôleur, vous remarquerez que quelque chose USB se déplace lentement. Les données de notre panel ne sont pas mises à jour rapidement. Pour être rapide, dans les fichiers usbd_customhid.h, vous devez remplacer #define CUSTOM_HID_EPIN_SIZE par la valeur maximale 0x40, #define CUSTOM_HID_EPOUT_SIZE également défini 0x40. Dans le fichier usbd_customhid.c, recherchez des commentaires dans le descripteur de noeud final "/ * bInterval: Polling Interval (20 ms) * /" et modifiez l'octet de descripteur sur 0x01 pour chaque noeud final, deux fois seulement. Ce qui correspondra à un échange de données de 1 ms.


Cela devrait être quelque chose comme ça. Appareil standard sans installer de pilote

En général, la fonction de gestion est un peu mal comprise. C'est assez facile à faire et tous les boutons et axes fonctionnent déjà. Reste à faire fonctionner les écrans. Je l'ai fait, environ six mois, et depuis six mois, le panneau ramasse de la poussière dans une longue boîte. Pas de temps. J'ai donc décidé de présenter l'article sous cette forme, sinon il risque même de ne pas sortir.

Avec les écrans, tout est le même qu'avec les axes. Pour eux, nous devons compléter notre descripteur d'appareil HID, simplement indiquer qu'il s'agit d'affichages et au lieu d'accepter les données d'entrée, l'hôte enverra les données de sortie.

La poignée de l'appareil HID a considérablement augmenté. Ici, j'ai déjà appliqué les paramètres Report ID afin de ne pas obstruer le tampon d'émission / réception et les points d'extrémité avec des données complètes et de distinguer le type de télégramme que nous avons reçu. L'ID de rapport est un octet uint8_t avec la valeur qui vient au début du télégramme. La valeur que nous avons définie dans le descripteur d'appareil HID.

CUSTOM_HID_ReportDesc_FS
 //AXIS 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x04, // USAGE (Joystick) 0xa1, 0x01, // COLLECTION (Application)28 0x05, 0x02, // USAGE_PAGE (Simulation Controls) 0x09, 0xbb, // USAGE (Throttle) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x27, 0xff, 0xff, 0x00, 0x00, // LOGICAL_MAXIMUM (65535) 0x75, 0x10, // REPORT_SIZE (16) 0x95, 0x01, // REPORT_COUNT (1) 0x85, 0x01, // REPORT_ID (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x01, // USAGE (Pointer) 0xa1, 0x00, // COLLECTION (Physical) 0x09, 0x30, // USAGE (X) 0x09, 0x31, // USAGE (Y) 0x95, 0x02, // REPORT_COUNT (2) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x32, // USAGE (Z) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x33, // USAGE (Rx) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x34, // USAGE (Ry) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x35, // USAGE (Rz) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x09, 0x36, // USAGE (Slider) 0x81, 0x02, // INPUT (Data,Var,Abs) //HAT 0x09, 0x39, // USAGE (Hat switch) 0x15, 0x01, // LOGICAL_MINIMUM (1) 0x25, 0x08, // LOGICAL_MAXIMUM (8) 0x35, 0x00, // PHYSICAL_MINIMUM (0) 0x46, 0x0e, 0x01, // PHYSICAL_MAXIMUM (270) 0x65, 0x14, // UNIT (Eng Rot:Angular Pos) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) //Buttons 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (Button 1) 0x29, 0x40, // USAGE_MAXIMUM (Button 64) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x40, // REPORT_COUNT (64) 0x55, 0x00, // UNIT_EXPONENT (0) 0x65, 0x00, // UNIT (None) 0x81, 0x02, // INPUT (Data,Var,Abs) //LEDs 0x85, 0x02, // REPORT_ID (2) 0x05, 0x08, // USAGE_PAGE (LEDs) 0x09, 0x4B, // USAGE (Generic Indicator) 0x95, 0x40, // REPORT_COUNT (16) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0xc0, // END_COLLECTION //LCD Displays 0x05, 0x14, // USAGE_PAGE (Alphnumeric Display) 0x09, 0x01, // USAGE (Alphanumeric Display) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0xa1, 0x02, // COLLECTION (Logical) 0x09, 0x32, // USAGE (Cursor Position Report) 0xa1, 0x02, // COLLECTION (Logical) 0x85, 0x04, // REPORT_ID (4) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x01, // REPORT_COUNT (1) 0x25, 0x13, // LOGICAL_MAXIMUM (19) 0x09, 0x34, // USAGE (Column) 0xb1, 0x22, // FEATURE (Data,Var,Abs,NPrf) 0x25, 0x03, // LOGICAL_MAXIMUM (3) 0x09, 0x33, // USAGE (Row) 0x91, 0x22, // OUTPUT (Data,Var,Abs,NPrf) 0xc0, // END_COLLECTION 0x09, 0x2b, // USAGE (Character Report) 0xa1, 0x02, // COLLECTION (Logical) 0x85, 0x05, // REPORT_ID (5) 0x95, 0x14, // REPORT_COUNT (20) 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) 0x09, 0x2c, // USAGE (Display Data) 0x92, 0x02, 0x01, // OUTPUT (Data,Var,Abs,Buf) 0xc0, // END_COLLECTION 0x09, 0x24, // USAGE (Display Control Report) 0x85, 0x06, // REPORT_ID (6) 0x95, 0x01, // REPORT_COUNT (1) 0x91, 0x22, // OUTPUT (Data,Var,Abs,NPrf) 0xc0, // END_COLLECTION //LED Displays 0x05, 0x14, // USAGE_PAGE (Alphnumeric Display) 0x09, 0x01, // USAGE (Alphanumeric Display) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0xa1, 0x02, // COLLECTION (Logical) 0x09, 0x2b, // USAGE (Character Report) 0xa1, 0x02, // COLLECTION (Logical) 0x85, 0x07, // REPORT_ID (7) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x28, // REPORT_COUNT (40) 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) 0x09, 0x2c, // USAGE (Display Data) 0x92, 0x02, 0x01, // OUTPUT (Data,Var,Abs,Buf) 0xc0, // END_COLLECTION //Other DATA 0x06, 0x00, 0xff, // USAGE_PAGE (Generic Desktop) 0x09, 0x01, // USAGE (Vendor Usage 1) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x08, // REPORT_ID (8) 0x09, 0x01, // USAGE (Vendor Usage 1) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x27, 0xff, 0xff, 0x00, 0x00, // LOGICAL_MAXIMUM (65535) 0x75, 0x10, // REPORT_SIZE (16) 0x95, 0x0A, // REPORT_COUNT (10) 0x91, 0x82, // OUTPUT (Data,Var,Abs,Vol) 

La sortie est traitée dans la fonction statique int8_t CUSTOM_HID_OutEvent_FS (uint8_t event_idx, uint8_t state) , qui, par défaut, se trouve dans usbd_custom_hid_if.c.

statique int8_t CUSTOM_HID_OutEvent_FS ()
 static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state) { /* USER CODE BEGIN 6 */ uint8_t dataReceiveArray[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE]; USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef*)hUsbDeviceFS.pClassData; for (uint8_t i = 0; i < USBD_CUSTOMHID_OUTREPORT_BUF_SIZE; i++) { dataReceiveArray[i] = hhid->Report_buf[i]; } if (dataReceiveArray[0] == 2) //report ID 2 leds { //  Report id == 2,   -     dataReceiveArray[1 + N], ,  LED } if (dataReceiveArray[0] == 4) //report ID 4 cursor position { //  Report id == 4,   -,     LCD } if (dataReceiveArray[0] == 5) //report ID 5 display data { //  Report id == 5,   -,     USB  LCD } //   ,   ID     return (USBD_OK); /* USER CODE END 6 */ } 


Il ne reste plus qu'à écrire un programme sur un PC qui envoie les rapports nécessaires pour piloter les écrans. Cependant, pour vérifier le code MK, un excellent programme de ST: USB HID Demonstrator convient. Il vous permet d'envoyer des rapports depuis un PC avec n'importe quel contenu.


Test d'affichage LED

À ce stade, j'ai terminé jusqu'à présent. Et on ne sait pas si je vais recommencer.

Il se joue dans des simulateurs plus intéressants qu'avec un clavier. Mais pas tellement qu'il y eut un effet wow direct. Le clavier, ça ressemble aussi à un panneau de contrôle. Mais contrôler les essieux du joystick est, au minimum, inhabituel. Sentez-vous comme un astronaute. Certes, une combinaison spatiale est nécessaire pour une immersion complète.

J'espère que vous étiez intéressé. Des fautes de frappe, des inexactitudes et des délires sont présents. Ceux qui veulent approfondir le code peuvent le voir ici .

Cordialement

Source: https://habr.com/ru/post/fr434854/


All Articles