mirror of
https://github.com/Mercantec-GHC/h5-projekt-mst.git
synced 2026-08-26 21:27:38 +02:00
add hat to footnotes
This commit is contained in:
parent
c2ab3067ea
commit
ec5b221558
@ -5,7 +5,7 @@
|
||||
|
||||
Løsningen består af 1) et spil implementeret som en Desktop-applikation, 2) en Skateboard-device implementeret som en embedded device men en ESP32-S3 og en MPU6050 kombineret accelerometer og gyroskop, 3) en backend-server som understøtter kommunikation over MQTT og over vores in-house TCP-protokol, og 4) en CI-opsætningen med pipelines for hvert kodeprojekt.
|
||||
|
||||
Koden samt yderligere materialer ligger i Github repo'et[1].
|
||||
Koden samt yderligere materialer ligger i Github repo'et[^1].
|
||||
|
||||
## Spillet
|
||||
|
||||
@ -21,13 +21,13 @@ For at køre spillet, installer Rust, SDL3 og SDL3_ttf, og kør `cargo run`.
|
||||
|
||||
Vi har valgt at skrive spillet i Rust. Dette har vi valgt, fordi vi alle har tidligere erfaring med at lave spil i Rust som Desktop-applikationer med SDL (SDL2). Vi har oplevet Rust som godt til Desktop-applikationer med kompleksitet og performance-krav. Diskuterede alternativer er C++ og Typescript. Siden vi ikke har lige så meget erfaring med C++ som gruppe blev dette valgt fra. Vi vurderede, at Typescript ikke passede godt til vores behov. Dele af vores applikation ligger tæt på operativsystemet i abstraktion, og vi har mindre erfaring med at udvikle med sådanne behov i Typescript end i Rust.
|
||||
|
||||
Spillet er en grafisk applikation, som skal kunne renderer til skærmen og reagere på input fra spilleren. Til at implementere denne funktionalitet har vi valgt at bruge library'et SDL3 (Simple Direct Medialayer)[2]. Specifikt benytter vi Rust-*crate*'en *sdl3*, som udsteder SDL3's API i et Rust-agtigt interface.[3] SDL3 er den relativt nye version af library'et, hvor SDL2 er stadig mere populært.
|
||||
Spillet er en grafisk applikation, som skal kunne renderer til skærmen og reagere på input fra spilleren. Til at implementere denne funktionalitet har vi valgt at bruge library'et SDL3 (Simple Direct Medialayer)[^2]. Specifikt benytter vi Rust-*crate*'en *sdl3*, som udsteder SDL3's API i et Rust-agtigt interface.[^3] SDL3 er den relativt nye version af library'et, hvor SDL2 er stadig mere populært.
|
||||
|
||||
Vi har valgt at bruge SDL3 af flere grunde. Den første grund er, at vi har arbejdet med SDL før. Dette gjorde det nemt for os, at lave en opsætning vi kunne bruge. Vi ville derved hurtigt finde ud af, om det passede til vores behov, vi skulle skifte til noget andet. Vi konkluderede, at det passede til vores behov.
|
||||
|
||||
Den anden grund er, at SDL's designprincipper passer godt ind i vores problemstilling. Vi vil gerne lave en cross-platform applikation samtidig med at implementere store dele af det grafiske selv. SDL tilbyder en letvægts platformagnostisk API, som gør det nemt at lave simpel og effektiv 2D rendering og nemt at opsamle og håndtere tastatur-input. Samtidig er der lille kompleksitet bygget ind i SDL. Istedet er designprincippet at udstede primitive API'er, så det er nemt for library'ets brugere at implementere det nødvendige funktionalitet.
|
||||
|
||||
Alternativer til SDL3 er først og fremmest SDL2. Vi valgte, at bruge den nye version, da vi vurderede, at versionen er moden nok, og at vi gerne ville lære forskellene mellem de 2. Vi har fundet meget få forskelle. Andre alternativer kunne være *bevy*[4]. Bevy er mere *batteries included* end SDL3. Oven i mere uddybet 2D-rendering tilbyder bevy mange andre features, som vi ikke behøver. Derudover dikterer bevy arkitekturen i koden, herunder tæt kobling med bevy's ECS-system.
|
||||
Alternativer til SDL3 er først og fremmest SDL2. Vi valgte, at bruge den nye version, da vi vurderede, at versionen er moden nok, og at vi gerne ville lære forskellene mellem de 2. Vi har fundet meget få forskelle. Andre alternativer kunne være *bevy*[^4]. Bevy er mere *batteries included* end SDL3. Oven i mere uddybet 2D-rendering tilbyder bevy mange andre features, som vi ikke behøver. Derudover dikterer bevy arkitekturen i koden, herunder tæt kobling med bevy's ECS-system.
|
||||
|
||||
Vi har 2 behov, som SDL3 skal udfylde. Det første er IO-håndtering. Dvs. oprettelse af et Desktop-vindue og håndtering af applikations-events. Det andet er rendering (rasterizering) af 2D-geometri. Dvs. en måde at tegne 2D-trekanter i farver på skærmen.
|
||||
|
||||
@ -45,7 +45,7 @@ Vi vil gerne lave 3D-rendering til vores spil. Vi har valgt at implementere 3D-r
|
||||
|
||||
3D-rendering, eller retter 3D-projektering er primært et matematisk problem. Vi har defineret nogle matematiske primitiver, og defineret diverse operationer, som er nødvendige for 3D-projektioner. Disse ligger i `src/engine/math.rs`. Dette inkluderer 2D-vektor `V2`, 3D-vektor `V3`, 2D- og 3D-trekanter `Triangle2` og `Triangle3` og 3x3 matrice `M3x3`. På de forskellige primitiver har vi defineret diverse matematiske operationer såsom sammenlægning og fratrækning af 3D-vektorer, gange med skalarværdi, længde af vektor, prik- og krydsprodukt, distance mellem 2 vektorer, osv. Nogle operationer er defineret som method-funktioner, eksempelvis `V3::cross`. Andre er implementeret med indbyggede Rust operatorer såsom `std::ops::Add` og `std::ops::Mul<f64>` for `V3`. Alle skalarværdier er repræsenteret med IEEE 754 double-floating point-tal, som i Rust staves `f64`, for 64-bit float.
|
||||
|
||||
3D-projektionen er implementeret med *Perspective Projection*[5] som funktioner på `V3` og `Triangle3` i methods ved navn `project_2d`. Følgende formel er anvendt:
|
||||
3D-projektionen er implementeret med *Perspective Projection*[^5] som funktioner på `V3` og `Triangle3` i methods ved navn `project_2d`. Følgende formel er anvendt:
|
||||
|
||||
<img src="./h5-mst-game-3d-math.jpg" width="50%" height="50%">
|
||||
<img src="./h5-mst-game-3d-illustration.jpg" width="50%" height="50%">
|
||||
@ -104,7 +104,7 @@ let mut indices_with_scores = self
|
||||
|
||||
p_scores.sort_by(|a, b| a.total_cmp(b));
|
||||
|
||||
let score = p_scores[0] * p_scores[1];
|
||||
let score = p_scores[^0] * p_scores[^1];
|
||||
(i, score)
|
||||
})
|
||||
.rev()
|
||||
@ -113,9 +113,9 @@ let mut indices_with_scores = self
|
||||
indices_with_scores.sort_by(|a, b| b.1.total_cmp(&a.1));
|
||||
```
|
||||
|
||||
Der der er værd at se, er at `p_scores`, som er hvert punkts score, udregnes ved at regne hvert punkts afstand til kameraet. Dernæst sorteret `p_scores`, så de tætteste punkter ligger i `[0]` og `[1]`, som derefter bruges til at regne den totale score. Til sidst sorteres `indices_with_scores`, så den længst væk liggende trekant ligger først.
|
||||
Der der er værd at se, er at `p_scores`, som er hvert punkts score, udregnes ved at regne hvert punkts afstand til kameraet. Dernæst sorteret `p_scores`, så de tætteste punkter ligger i `[^0]` og `[^1]`, som derefter bruges til at regne den totale score. Til sidst sorteres `indices_with_scores`, så den længst væk liggende trekant ligger først.
|
||||
|
||||
Denne algoritme til at beregne score er vi kommet frem til gennem eksperimentering. Om dette er den optimale algoritme, ved vi ikke. Et populært alternative til at benytte en algoritme på denne måde er Z-buffering[6]. Her beregnes afstanden for hvert enkelt pixel, og man opnår derved perfekt rendering af overlappende trekanter. Ulempen ved Z-buffering er, at det er beregningstungt. I realtidsapplikationer (såsom et spil) kan det derfor ikke svare sig, hvis man laver 3D-udregninerne på CPU'en. Det er ofte, at man foretager 3D-beregninerne på GPU'en istedet. Det gør vi ikke af flere årsager, så det behøver vi ikke at bekymre os om. Istedet er vores metode, at udregne en aggrigat-værdi for hver trekant. Dvs. istedet for en afstandsberegning for hvert pixel, laver vi 3 afstandsberegninger for hver trekant og en sortering af de 3 værdier.
|
||||
Denne algoritme til at beregne score er vi kommet frem til gennem eksperimentering. Om dette er den optimale algoritme, ved vi ikke. Et populært alternative til at benytte en algoritme på denne måde er Z-buffering[^6]. Her beregnes afstanden for hvert enkelt pixel, og man opnår derved perfekt rendering af overlappende trekanter. Ulempen ved Z-buffering er, at det er beregningstungt. I realtidsapplikationer (såsom et spil) kan det derfor ikke svare sig, hvis man laver 3D-udregninerne på CPU'en. Det er ofte, at man foretager 3D-beregninerne på GPU'en istedet. Det gør vi ikke af flere årsager, så det behøver vi ikke at bekymre os om. Istedet er vores metode, at udregne en aggrigat-værdi for hver trekant. Dvs. istedet for en afstandsberegning for hvert pixel, laver vi 3 afstandsberegninger for hver trekant og en sortering af de 3 værdier.
|
||||
|
||||
#### Filtering af trekanter
|
||||
|
||||
@ -153,7 +153,7 @@ Kommunikation med backend'en er enkapsuleret i `struct Server`-structet. Selvom
|
||||
|
||||
`Server`-struct'et har en metode `subscribe`, som kaldes med en callback-funktion. Denne metode registrer spillet i backend'en, sætter en datastream op, og kalder callback-funktionen for hvert sensor-measurement, der modtages fra backenden.
|
||||
|
||||
I spilkoden bliver disse events samlet i en event queue. Event queue'en er en FIFO-buffer implementeret med Rusts `VecDec`-kontainer. Siden event queue'en skal virke over en thread boundary, er det nødvendigt med synkronisering. Dette gøres med Rust's `Arc<Mutex<T>>` type-pattern[7].
|
||||
I spilkoden bliver disse events samlet i en event queue. Event queue'en er en FIFO-buffer implementeret med Rusts `VecDec`-kontainer. Siden event queue'en skal virke over en thread boundary, er det nødvendigt med synkronisering. Dette gøres med Rust's `Arc<Mutex<T>>` type-pattern[^7].
|
||||
|
||||
I `Game::update`-metoden tjekkes event queue'en for, om der er blevet tilføjet nye events. Hvis der er, så tømmes queue'en, og hver værdi bruges til at styre skateboardet.
|
||||
|
||||
@ -163,13 +163,13 @@ Skateboard'et er implementeret med en Arduino Nano ESP32, hvori der sidder en ES
|
||||
|
||||
Koden ligger i `skateboard/` i repo'et.
|
||||
|
||||
Vi har valgt at bruge en ESP32, specifikt en ESP32-S3[16], produceret af Espressif, på et Arduino Nano ESP32 development board[17]. Vi ønskede en chip med den funktionalitet, vi skulle bruge, hovedsageligt en MHz CPU, WiFi, en I2C bus og nem udvikling. ESP32 med Espressif's development framework tilbyder features i en relativt kost-effektiv pakke. Alternativer til en ESP32 kunne være Atmel AVR, ARM Cortex eller STM32 chips. Med tidligere erfaring med alle tre alternativer, var valget også taget for læringsmæssige formål.
|
||||
Vi har valgt at bruge en ESP32, specifikt en ESP32-S3[^16], produceret af Espressif, på et Arduino Nano ESP32 development board[^17]. Vi ønskede en chip med den funktionalitet, vi skulle bruge, hovedsageligt en MHz CPU, WiFi, en I2C bus og nem udvikling. ESP32 med Espressif's development framework tilbyder features i en relativt kost-effektiv pakke. Alternativer til en ESP32 kunne være Atmel AVR, ARM Cortex eller STM32 chips. Med tidligere erfaring med alle tre alternativer, var valget også taget for læringsmæssige formål.
|
||||
|
||||
Ikke at forveksle med de Israelske angrebsstyrker, ESP-IDF er Espressif's development framework (IoT Development Framework)[18]. ESP-IDF er et batteries included framework bestående af værktøjer, bootloaders, drivers og libraries til at udvikle firmwares til ESP32 chips. En ESP-IDF-applikationer er skrevet i C med CMake, med diverse libraries og CMake plugins.
|
||||
Ikke at forveksle med de Israelske angrebsstyrker, ESP-IDF er Espressif's development framework (IoT Development Framework)[^18]. ESP-IDF er et batteries included framework bestående af værktøjer, bootloaders, drivers og libraries til at udvikle firmwares til ESP32 chips. En ESP-IDF-applikationer er skrevet i C med CMake, med diverse libraries og CMake plugins.
|
||||
|
||||
### Skateboard-firmware
|
||||
|
||||
Til skateboardet har vi lavet et ESP-IDF projekt. Hertil hører der en `CMakeLists.txt`-fil, en `main`-mappe, en `main/CMakeLists.txt`-fil, en `Kconfig.projbuild`-fil og C- og H-filer i `main/`. Den første CMake-fil beskriver projektet og sætter aktiverer ESP-IDF's plugins. CMake-filen i `main/`-mappen beskriver, specifikt for main-komponentet, hvilke filer der skal kompileres og hvilke drivers (komponenter), som dette komponent afhænger af. `main/Kconfig.projbuild`-filen beskriver den konfiguration, som komponentet tilbyder. Konfigurationsmuligheder beskrevet her, vil dukke op i ESP-IDF's samlede konfigurationsmenu (`idf.py menuconfig`), og værdierne vil herefter være tilgængelige i C-koden. Mere om byggesystemet kan findes i manualen[19].
|
||||
Til skateboardet har vi lavet et ESP-IDF projekt. Hertil hører der en `CMakeLists.txt`-fil, en `main`-mappe, en `main/CMakeLists.txt`-fil, en `Kconfig.projbuild`-fil og C- og H-filer i `main/`. Den første CMake-fil beskriver projektet og sætter aktiverer ESP-IDF's plugins. CMake-filen i `main/`-mappen beskriver, specifikt for main-komponentet, hvilke filer der skal kompileres og hvilke drivers (komponenter), som dette komponent afhænger af. `main/Kconfig.projbuild`-filen beskriver den konfiguration, som komponentet tilbyder. Konfigurationsmuligheder beskrevet her, vil dukke op i ESP-IDF's samlede konfigurationsmenu (`idf.py menuconfig`), og værdierne vil herefter være tilgængelige i C-koden. Mere om byggesystemet kan findes i manualen[^19].
|
||||
|
||||
I skateboard-firmware'en har vi 4 hovedkomponenter (konceptuelt, ikke ESP-IDF-komponenter):
|
||||
- **Wifi interface:** Skateboardet skal forbinde til vores Linux-server på netværket, for at kunne kommunikere med Mosquitto message broker'en over MQTT. Dette er et interface over ESP-IDF's WiFi driver, specialiseret til vores formål.
|
||||
@ -190,7 +190,7 @@ WiFi interface'et bruger ESP-IDF's WiFi-driver. Derudover bruges forskellige fea
|
||||
|
||||
Konfigurationen af WiFi, dvs. SSID og password er konfigureret igennem Kconfig-menu'en. Dvs. man i configure time (før compile time) indtaster sit ønskede netværks-credentials. Dette er en midlertidig foranstaltning. I en videreudviklet version, bør man kunne konfigurere netværket i run time, eksempelvis via en mobilapp.
|
||||
|
||||
Et alternativ til at skulle konfigurere WiFi kunne være, at produktet salges sammen med et access point. I sådanne setup ville man også kunne bruge Bluetooth, som eksempelvis Wii Remote benytter[20]. Alternativt kunne man forbinde produktet direkte til computeren, hvor spillet køre. Vi valgte, at bruge WiFi i denne konfiguration, da vi vurderede, at det ville gøre udvikling simplest, og tillade at vi hurtigere kunne udvikle spillet.
|
||||
Et alternativ til at skulle konfigurere WiFi kunne være, at produktet salges sammen med et access point. I sådanne setup ville man også kunne bruge Bluetooth, som eksempelvis Wii Remote benytter[^20]. Alternativt kunne man forbinde produktet direkte til computeren, hvor spillet køre. Vi valgte, at bruge WiFi i denne konfiguration, da vi vurderede, at det ville gøre udvikling simplest, og tillade at vi hurtigere kunne udvikle spillet.
|
||||
|
||||
#### MQTT interface
|
||||
|
||||
@ -229,11 +229,11 @@ MPU6050'eren er forbundet til ESP32'eren via I2C. Modulet får også strøm fra
|
||||
<img src="./circuit-diagram.png" width="50%" height="50%">
|
||||
<img src="./h5-mst-breadboard-2.jpg" width="50%" height="50%">
|
||||
|
||||
General information om brug af MPU6050 kan findes i produktspecifikationen (databladet)[21]. MPU6050 kan køre på, og bliver forsynet med 3.3V. I2C-bussen skal have, og har, en clock rate på 400MHz. Opsætning af MPU'en gøres ved at skrive til I2C registers og læsning af målinger ved læsning af registers.
|
||||
General information om brug af MPU6050 kan findes i produktspecifikationen (databladet)[^21]. MPU6050 kan køre på, og bliver forsynet med 3.3V. I2C-bussen skal have, og har, en clock rate på 400MHz. Opsætning af MPU'en gøres ved at skrive til I2C registers og læsning af målinger ved læsning af registers.
|
||||
|
||||
Vi har valgt at lave vores egen driver til MPU6050. Vi eksperimenterede med 2 eksisterende drivers. Vi oplevede, at de havde outdatede dependencies og fejl i funktionaliteten. Ved at studere koden i de 2 drivers, vurderede vi, at det nemmeste ville være, at skrive vores egen ud fra manualen.
|
||||
|
||||
Driveren er defineret i `main/mpu6050.h` og `main/mpu6050.c`. Her defineres typen `Mpu6050` samt adskillige typer og funktioner, til konfigurering og benyttelse af sensor-modulet. MPU6050's register map (programmeringsmanual) beskriver hvordan man via I2C kan konfigure og benytte modulet[22].
|
||||
Driveren er defineret i `main/mpu6050.h` og `main/mpu6050.c`. Her defineres typen `Mpu6050` samt adskillige typer og funktioner, til konfigurering og benyttelse af sensor-modulet. MPU6050's register map (programmeringsmanual) beskriver hvordan man via I2C kan konfigure og benytte modulet[^22].
|
||||
|
||||
Disse er de mest væsentlige funktioner i driveren:
|
||||
```c
|
||||
@ -251,9 +251,9 @@ En anden forskel er, at de 2 interfaces har meget små interfaces, med målet om
|
||||
|
||||
> Though it may be strange to say that a driver is "flexible," we like this word because it emphasizes that the role of a device driver is providing *mechanism*, not *policy*.
|
||||
>
|
||||
> ... Most programming problems can indeed be split into two parts: "what capabilities are to be provided" (the mechanism) and "how those capabilities can be used" (the policy). If the two issues are addressed by different parts of the program, ... the software package is much easier to develop and to adapt to particular needs.[23]
|
||||
> ... Most programming problems can indeed be split into two parts: "what capabilities are to be provided" (the mechanism) and "how those capabilities can be used" (the policy). If the two issues are addressed by different parts of the program, ... the software package is much easier to develop and to adapt to particular needs.[^23]
|
||||
|
||||
For at bruge MPU6050'eren med driveren, bruger man de udstedte funktioner. `mpu6050_init()`-funktionen initialisere en MPU6050-device med en associeret struct-værdi, som indeholder diverse state, som driveren bruger internt. Før MPU-modulet kan bruges, skal det kalibreres. Dette gøres med `mpu6050_calibrate()`-funktionen[24].
|
||||
For at bruge MPU6050'eren med driveren, bruger man de udstedte funktioner. `mpu6050_init()`-funktionen initialisere en MPU6050-device med en associeret struct-værdi, som indeholder diverse state, som driveren bruger internt. Før MPU-modulet kan bruges, skal det kalibreres. Dette gøres med `mpu6050_calibrate()`-funktionen[^24].
|
||||
|
||||
Efter modulet er kalibreret, kan man aflæse værdierne med `mpu6050_get_rotation()` og `mpu6050_get_acceleration()`. Disse funktioner returnere sensorens aflæste værdier korrigeret for kalibreringen. `mpu6050_get_acceleration()` returnerer en 3D-vektor, hvor hver af værdierne repræsenterer accelerationen, der påvirker MPU'en på henholdsvis X-, Y- og Z-akserne. `mpu6050_get_rotation()` returnerer en 3D-vektor, hvor hver af værdierne repræsenterer vinkel*accelerationen* om hver af akserne.
|
||||
|
||||
@ -297,7 +297,7 @@ Her ses det, at libc's `atan()`-function regner i radianer, hvor MPU'en og vores
|
||||
|
||||
Den beregnede vinkel skal herefter sendes til serveren. Vores kommunikation med serveren, og derfor også med spillet, er begrænset af kommunikationslagene derimellem. Gennem eksperimentering har vi fundet ud af, at kommunikationen er bedst, når skateboardet sender 10 gange i sekundet. Vi har derfor et timer-setup, som afvikler publish-koden med 100 millisekunders mellemrum. Dette timer-setup bruger FreeRTOS's task scheduling timer (`vTaskDelay()`) til at pause task'en i 100 millisekunder.
|
||||
|
||||
Vi bruger både FreeRTOS task scheduling[25] og ESP-IDF high resolution timers[26] forskellige steder i koden. Forskellen på de to i denne sammenhæng er primært at ESP-IDF high resolution timers er, som navnet siger, meget præcise med en opløsning i mikrosekunder. FreeRTOS's task scheduling funktioner har derimod en opløsning i 10'ere af millisekunder. Fordelen ved FreeRTOS funktionerne er at man har flere muligheder, for hvordan timing mekanismen skal virke, da man har adgang til preemptive multitasking-faciliteterne.
|
||||
Vi bruger både FreeRTOS task scheduling[^25] og ESP-IDF high resolution timers[^26] forskellige steder i koden. Forskellen på de to i denne sammenhæng er primært at ESP-IDF high resolution timers er, som navnet siger, meget præcise med en opløsning i mikrosekunder. FreeRTOS's task scheduling funktioner har derimod en opløsning i 10'ere af millisekunder. Fordelen ved FreeRTOS funktionerne er at man har flere muligheder, for hvordan timing mekanismen skal virke, da man har adgang til preemptive multitasking-faciliteterne.
|
||||
|
||||
Publish-koden laver et JSON-objekt i en string med nuværende vinkelværdi. Koden til at laver JSON-string'et og publishing er følgende:
|
||||
```c
|
||||
@ -330,11 +330,11 @@ For at deploy backend'en kør `./deploy.sh`. Eventuelt byg og upload et opdatere
|
||||
|
||||
Vores backend-opsætning ligger på en server som kører Debian. Vi har lavet en opsætning med en bruger hver, dvs. en `mtk`-, `sfj`- og `tph`-bruger. Vi har sat SSH op på brugerne, så vi kan forbinde til hver vores bruger gennem SSH med public/private-nøgler. Password-authentificering er slået fra for SSH. På serveren har vi sat *sudo* op, så bruger kan køre sudo-kommandoer uden password.
|
||||
|
||||
Vi har installeret Docker på serveren og Docker Compose. Vores deployment fungerer ved, at filerne synkroniseres op på serveren og `sudo docker compose up -d` køres. Vi har valgt at beholde, at man skal have root access til Docker, dels fordi det ikke gør stor forskel, dels fordi så er host-miljøet agnostics for, hvilken bruger der kørte up-kommandoen, og dels fordi, der er security issues ved at give alle adgang til Docker-systemet[10].
|
||||
Vi har installeret Docker på serveren og Docker Compose. Vores deployment fungerer ved, at filerne synkroniseres op på serveren og `sudo docker compose up -d` køres. Vi har valgt at beholde, at man skal have root access til Docker, dels fordi det ikke gør stor forskel, dels fordi så er host-miljøet agnostics for, hvilken bruger der kørte up-kommandoen, og dels fordi, der er security issues ved at give alle adgang til Docker-systemet[^10].
|
||||
|
||||
### Mosquitto-instants
|
||||
|
||||
Mosquitto[8] er sat op med Docker Compose via det officielle Docker image[9]. Instansen er konfigureret med filen `deploy/mosquitto.conf`, og authorisering er konfigureret i users-filen i `deploy/mqtt_users`. For nuværende er der en enkelt bruger `test` med password'et `1234`. Mosquitto-instansen lytter på port `1883` både internt og eksternt, og så tillader den anonyme brugere. Dette betyder, at authorisering ikke er nødvendigt. I vores setup benytter vi dog stadig username/password authorisering.
|
||||
Mosquitto[^8] er sat op med Docker Compose via det officielle Docker image[^9]. Instansen er konfigureret med filen `deploy/mosquitto.conf`, og authorisering er konfigureret i users-filen i `deploy/mqtt_users`. For nuværende er der en enkelt bruger `test` med password'et `1234`. Mosquitto-instansen lytter på port `1883` både internt og eksternt, og så tillader den anonyme brugere. Dette betyder, at authorisering ikke er nødvendigt. I vores setup benytter vi dog stadig username/password authorisering.
|
||||
|
||||
Vi har valgt at bruge Mosquitto, da softwaren selv er relativ simpel. Efter at eksperimentere med RabbitMQ besluttede vi, at RabbitMQ var for advanceret til vores behov. Vi fandt ud af, at vi med meget lille energi kunne tilføje en Mosquitto-instans til vores Docker Compose-opsætning, som dækkede vores behov.
|
||||
|
||||
@ -370,7 +370,7 @@ $(build_dir)/$(test_dir)/test_%: $(obj_dir)/tests/%.o $(objects_without_main)
|
||||
$(LD) -o $@ $(CXXFLAGS) $^ $(LDFLAGS)
|
||||
```
|
||||
|
||||
Projektet er sat op til udvikling med Clang-værktøjerne, specific clangd-sprogserveren[11]. clangd er sat op med `compile_flags.txt`, som er en primitiv måde at fortælle clangd, hvordan den skal fortolke koden. Filen beskriver de flag, som specificeres til compiler'en, når koden kompileres (og et flag `-xc++`, som fortæller at `.h`-filer er C++ og ikke C). Derudover er der en `.clang-format`-fil, som dikterer hvordan clangd og clang-format skal formatere koden. Her har vi eksempelvis sat indent-bredde til 4 (spaces) og kolonnemaksimum til 80:
|
||||
Projektet er sat op til udvikling med Clang-værktøjerne, specific clangd-sprogserveren[^11]. clangd er sat op med `compile_flags.txt`, som er en primitiv måde at fortælle clangd, hvordan den skal fortolke koden. Filen beskriver de flag, som specificeres til compiler'en, når koden kompileres (og et flag `-xc++`, som fortæller at `.h`-filer er C++ og ikke C). Derudover er der en `.clang-format`-fil, som dikterer hvordan clangd og clang-format skal formatere koden. Her har vi eksempelvis sat indent-bredde til 4 (spaces) og kolonnemaksimum til 80:
|
||||
```yaml
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 80
|
||||
@ -391,7 +391,7 @@ client.publish("/my/topic", "message to publish");
|
||||
|
||||
#### TCP-server
|
||||
|
||||
Det andet komponent er en TCP-server. TCP-serveren understøtter vores inhouse protokol til at kommunikere data til spillet. TCP-serveren er enkapsuleret i `mst::server::Server`-klassen, defineret i `src/server.hpp` og `src/server.cpp`. Serveren bruger Linux's (POSIX's) indbyggede socket-API. Vi har valgt at bruge denne API, da vi har et lille behov for funktionalitet. Vi ønsker en simpel og barebones TCP-server, og derfor egner den relativt primitive socket TCP/IP-API sig godt. Derudover viste vi, at serveren kun skulle køre i et Linux-miljø. Før vi valgte socket-API'en og TCP-protokollen undersøgte vi libmicrohttp. Vi konkluderede, at en inhouse TCP protokol og socket-API'en ville være nemmest og simplest stil vores formål. Til implementeringen brugte vi *Beej's Guide to Network Programming* som reference[12].
|
||||
Det andet komponent er en TCP-server. TCP-serveren understøtter vores inhouse protokol til at kommunikere data til spillet. TCP-serveren er enkapsuleret i `mst::server::Server`-klassen, defineret i `src/server.hpp` og `src/server.cpp`. Serveren bruger Linux's (POSIX's) indbyggede socket-API. Vi har valgt at bruge denne API, da vi har et lille behov for funktionalitet. Vi ønsker en simpel og barebones TCP-server, og derfor egner den relativt primitive socket TCP/IP-API sig godt. Derudover viste vi, at serveren kun skulle køre i et Linux-miljø. Før vi valgte socket-API'en og TCP-protokollen undersøgte vi libmicrohttp. Vi konkluderede, at en inhouse TCP protokol og socket-API'en ville være nemmest og simplest stil vores formål. Til implementeringen brugte vi *Beej's Guide to Network Programming* som reference[^12].
|
||||
|
||||
|
||||
Serveren udstiller et interface som følgende:
|
||||
@ -409,7 +409,7 @@ Pt. er der et enkelt endpoint i TCP-protokellen: `Subscribe`. Et subscribe-kald
|
||||
|
||||
#### JSON-parser
|
||||
|
||||
I backend-applikationen er der en inhouse JSON-parser. Vi valgte, at bruge vores egen JSON-parser, da vi havde brug for den ekstra performance, vi kunne få ud af en custom implementering. JSON-parseren er enkapsuleret i `mst::json::Value` og `mst::json::parse()`, og defineret i `src/json.hpp` og `src/json.cpp`. JSON-parseren er originalt et C-projekt, som vi har ported til C++23. Med en hurtig tokenizer, fleksibel parser, vel-defineret interface og simpelt query-funktionalitet, forsøger JSON-parseren at være både hurtig og nem at bruge. Vores JSON-parser er ikke 100% standards complient[13], men den opfylder vores behov. Alternativer til en inhouse implementation kunne være nlohmann/json[14] eller simdjson[15]. Et eksempel (taget fra en unittest) er følgende:
|
||||
I backend-applikationen er der en inhouse JSON-parser. Vi valgte, at bruge vores egen JSON-parser, da vi havde brug for den ekstra performance, vi kunne få ud af en custom implementering. JSON-parseren er enkapsuleret i `mst::json::Value` og `mst::json::parse()`, og defineret i `src/json.hpp` og `src/json.cpp`. JSON-parseren er originalt et C-projekt, som vi har ported til C++23. Med en hurtig tokenizer, fleksibel parser, vel-defineret interface og simpelt query-funktionalitet, forsøger JSON-parseren at være både hurtig og nem at bruge. Vores JSON-parser er ikke 100% standards complient[^13], men den opfylder vores behov. Alternativer til en inhouse implementation kunne være nlohmann/json[^14] eller simdjson[^15]. Et eksempel (taget fra en unittest) er følgende:
|
||||
```c++
|
||||
auto object = *json::parse(R"( { "rotation": -0.0123 } )");
|
||||
|
||||
@ -421,7 +421,7 @@ ASSERT_EQ(f64_value, -0.0123);
|
||||
|
||||
## CI
|
||||
|
||||
Til hver af de 3 kodeprojekter er der en CI opsætning, som udfører diverse verificeringer når kode bliver *push*'et. Opsætningen er lavet med Github Action Workflows[27]. Til hver af de 3 projekter er der sat en pipeline op, som kloner koden, bygger koden og udfører andre verificeringer. Vi benytter et custom Docker images som byggemiljøer i CI-miljøet.
|
||||
Til hver af de 3 kodeprojekter er der en CI opsætning, som udfører diverse verificeringer når kode bliver *push*'et. Opsætningen er lavet med Github Action Workflows[^27]. Til hver af de 3 projekter er der sat en pipeline op, som kloner koden, bygger koden og udfører andre verificeringer. Vi benytter et custom Docker images som byggemiljøer i CI-miljøet.
|
||||
|
||||
Hver af de 3 pipelines er defineret i `.github/workflows/backend.yml`, `.github/workflows/game.yml` og `.github/workflows/skateboard.yml` henholdsvis. Disse pipelines er defineret som Github Actions. De bliver kørt, når kode *push*'es til hver af `backend/`-, `game/`- og `skateboard/`-mapperne. Dvs. backend'ens pipeline kører kun, når en commit ændre i filerne i `backend/`-mappen.
|
||||
|
||||
@ -439,44 +439,44 @@ Backend'ens pipeline bygger, kører unittests, ligesom game, men checker også f
|
||||
|
||||
Vi har valgt at implementere disse continuous integration pipelines primært for at styrke kodekvaliteten. CI pipelines sænker tiden det tager, at integrer og teste et system. De kan derfor bruges til at minimere tiden det tager, at få feedback på koden.
|
||||
|
||||
> Product teams can test ideas and iterate product designs faster with an optimized CI platform. Changes can be rapidly pushed and measured for success. Bugs or other issues can be quickly addressed and repaired.[28]
|
||||
> Product teams can test ideas and iterate product designs faster with an optimized CI platform. Changes can be rapidly pushed and measured for success. Bugs or other issues can be quickly addressed and repaired.[^28]
|
||||
|
||||
## Konklusion
|
||||
|
||||
For at opsummere: Vi har et Slope-agtigt spil med 3D-rendering som anvender diverse matematik og algoritmer til at rendere 3D. Vi har et skateboard, som består af et fysisk skateboard, hvorpå vi har installeret en ESP32-S3 microcontroller og MPU6050 gyroskop/accelerometer-modul til at måle og beregne skateboardets vinkel. Via en WiFi-forbindelse sender skateboardet data til backenden via MQTT. Backenden består af en Mosquitto message broker og en C++-serverapplikation. Serverapplikationen er selv en MQTT-klient, men udsteder også en server med en inhouse TCP-protokol. Spillet forbinder til backend'en, og modtager, gennem TCP-protokellen, sensordata, som spillet bruger som brugerinput.
|
||||
|
||||
[1]: https://github.com/Mercantec-GHC/h5-projekt-mst
|
||||
[2]: https://wiki.libsdl.org/SDL3/FrontPage
|
||||
[3]: https://docs.rs/sdl3/latest/sdl3/
|
||||
[4]: https://bevy.org/
|
||||
[5]: https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula
|
||||
[6]: https://en.wikipedia.org/wiki/Z-buffering
|
||||
[7]: https://doc.rust-lang.org/book/ch16-03-shared-state.html#atomic-reference-counting-with-arct
|
||||
[8]: https://mosquitto.org/
|
||||
[9]: https://hub.docker.com/_/eclipse-mosquitto/
|
||||
[10]: https://docs.docker.com/engine/security/#docker-daemon-attack-surface
|
||||
[11]: https://clangd.llvm.org/
|
||||
[12]: https://beej.us/guide/bgnet/
|
||||
[13]: https://www.json.org/json-en.html
|
||||
[14]: https://github.com/nlohmann/json
|
||||
[15]: https://github.com/simdjson/simdjson
|
||||
[16]: https://www.espressif.com/en/products/socs/esp32-s3/
|
||||
[17]: https://docs.arduino.cc/hardware/nano-esp32/
|
||||
[18]: https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32s3/index.html
|
||||
[19]: https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32s3/api-guides/build-system.html
|
||||
[20]: https://web.archive.org/web/20080212080618/http://wii.nintendo.com/controller.jsp
|
||||
[^1]: https://github.com/Mercantec-GHC/h5-projekt-mst
|
||||
[^2]: https://wiki.libsdl.org/SDL3/FrontPage
|
||||
[^3]: https://docs.rs/sdl3/latest/sdl3/
|
||||
[^4]: https://bevy.org/
|
||||
[^5]: https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula
|
||||
[^6]: https://en.wikipedia.org/wiki/Z-buffering
|
||||
[^7]: https://doc.rust-lang.org/book/ch16-03-shared-state.html#atomic-reference-counting-with-arct
|
||||
[^8]: https://mosquitto.org/
|
||||
[^9]: https://hub.docker.com/_/eclipse-mosquitto/
|
||||
[^10]: https://docs.docker.com/engine/security/#docker-daemon-attack-surface
|
||||
[^11]: https://clangd.llvm.org/
|
||||
[^12]: https://beej.us/guide/bgnet/
|
||||
[^13]: https://www.json.org/json-en.html
|
||||
[^14]: https://github.com/nlohmann/json
|
||||
[^15]: https://github.com/simdjson/simdjson
|
||||
[^16]: https://www.espressif.com/en/products/socs/esp32-s3/
|
||||
[^17]: https://docs.arduino.cc/hardware/nano-esp32/
|
||||
[^18]: https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32s3/index.html
|
||||
[^19]: https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32s3/api-guides/build-system.html
|
||||
[^20]: https://web.archive.org/web/20080212080618/http://wii.nintendo.com/controller.jsp
|
||||
|
||||
[21]: InvenSense Inc.: *MPU-6000 and MPU-6050 Product Specification*, Revision 3.4, 08/19/2013, https://product.tdk.com/system/files/dam/doc/product/sensor/mortion-inertial/imu/data_sheet/mpu-6000-datasheet1.pdf
|
||||
[^21]: InvenSense Inc.: *MPU-6000 and MPU-6050 Product Specification*, Revision 3.4, 08/19/2013, https://product.tdk.com/system/files/dam/doc/product/sensor/mortion-inertial/imu/data_sheet/mpu-6000-datasheet1.pdf
|
||||
|
||||
[22]: InvenSense Inc.: *MPU-6000 and MPU-6050 Register Map and Descriptions*, Revision 4.0, 3/09/2012, https://cdn.sparkfun.com/datasheets/Sensors/Accelerometers/RM-MPU-6000A.pdf
|
||||
[^22]: InvenSense Inc.: *MPU-6000 and MPU-6050 Register Map and Descriptions*, Revision 4.0, 3/09/2012, https://cdn.sparkfun.com/datasheets/Sensors/Accelerometers/RM-MPU-6000A.pdf
|
||||
|
||||
[23]: Alessandro Rubini, Jonathan Corbet: *Linux Device Drivers*, 2nd Edition, O'REILLY 2001
|
||||
[^23]: Alessandro Rubini, Jonathan Corbet: *Linux Device Drivers*, 2nd Edition, O'REILLY 2001
|
||||
|
||||
[24]: Denne funktion virkede ikke i en af de to eksisterende drivers vi eksperimenterede med. Uden kalibrering, kunne vi ikke bruge de udlæste værdier, men et kald til driverens kalibreringsfunktion producerede en fault-interrupt, som fik ESP32'eren til at genstarte sig selv. Fejlen lå i nogle af driverfunktionerne, som ikke virkede korrekt eller blev kaldt ukorrekt internt. Dette resulterede i en *division by zero*-fejl i kalibreringsfunktionen. Fun times!
|
||||
[^24]: Denne funktion virkede ikke i en af de to eksisterende drivers vi eksperimenterede med. Uden kalibrering, kunne vi ikke bruge de udlæste værdier, men et kald til driverens kalibreringsfunktion producerede en fault-interrupt, som fik ESP32'eren til at genstarte sig selv. Fejlen lå i nogle af driverfunktionerne, som ikke virkede korrekt eller blev kaldt ukorrekt internt. Dette resulterede i en *division by zero*-fejl i kalibreringsfunktionen. Fun times!
|
||||
|
||||
[25]: https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/system/freertos_idf.html
|
||||
[26]: https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/system/esp_timer.html
|
||||
[27]: https://docs.github.com/en/actions/concepts/workflows-and-actions/workflows
|
||||
[28]: https://www.atlassian.com/continuous-delivery/continuous-integration
|
||||
[^25]: https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/system/freertos_idf.html
|
||||
[^26]: https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/system/esp_timer.html
|
||||
[^27]: https://docs.github.com/en/actions/concepts/workflows-and-actions/workflows
|
||||
[^28]: https://www.atlassian.com/continuous-delivery/continuous-integration
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user