Compare commits
80 Commits
d7f004ee72
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a589e104c | |||
| 07127fb074 | |||
| 17cc33332d | |||
| 5406c9917f | |||
| 37d1bd6db1 | |||
| 8c15260166 | |||
| 6b36435759 | |||
| 675a99930a | |||
| f012ce9fe0 | |||
| 2a4d007805 | |||
| 93b7c5fe9e | |||
| 1ce021c76f | |||
| 480baa97fa | |||
| 8d97a5d33f | |||
| f5b1849ada | |||
| 0113edb816 | |||
| a1b047f4bb | |||
| d51ff27e26 | |||
| ac477e290e | |||
| 4906dc31eb | |||
| be48e8ada7 | |||
| 918092cd9f | |||
| b1f5578be9 | |||
| 9c1d19af67 | |||
| 6376185622 | |||
| 3febb6411e | |||
| 0c3a8bfa39 | |||
| 8305adf917 | |||
| fae79ad8b0 | |||
| 794d8e36c9 | |||
| 0785d9a755 | |||
| 11ad746f04 | |||
| 149a142c22 | |||
| e84efc2e8c | |||
| 76ac36a59b | |||
| 5e7a817e03 | |||
| 1b8d3e17b8 | |||
| 93a7da7855 | |||
| fb4578ac51 | |||
| 63f8f2aaac | |||
| 55b5421671 | |||
| bd0a8cce8d | |||
| 832a60d044 | |||
| a041d5a49c | |||
| 38396738a6 | |||
| 65688b7b99 | |||
| 395d577b78 | |||
| c13b6d73c9 | |||
| 6b1bbca992 | |||
| 9d5dad0e8d | |||
| a6bedb6b79 | |||
| e460aac7a1 | |||
| 5113c0a850 | |||
| 4e6780af6d | |||
| 9f25c0540a | |||
| f464fcbf1b | |||
| ce4d0d1a44 | |||
| 978f93ec3d | |||
| c0911aa9c2 | |||
| 667600c14e | |||
| 2cb0a33b8e | |||
| 492f47a669 | |||
| fe450cd39e | |||
| 8bd284be13 | |||
| 5a8ed22e6a | |||
| bf8cee7918 | |||
| ba7d0ca0a0 | |||
| ec588242bd | |||
| bc48ed2228 | |||
| 19caf794ef | |||
| 1e3b193b8c | |||
| db63ca64b2 | |||
| a7d39f26e0 | |||
| 0581402acc | |||
| 5db4aa3efe | |||
| ae5b4791f1 | |||
| cf672fbf3e | |||
| af7ca36deb | |||
| 924fa66c3e | |||
| 06af540be3 |
@@ -11,25 +11,35 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
uses: https://github.com/actions/checkout@v3
|
||||
|
||||
- name: Build and Deploy
|
||||
- name: Build Docs
|
||||
run: |
|
||||
# 1. venv erstellen
|
||||
# 1. Virtuelle Umgebung erstellen
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install --upgrade pip
|
||||
|
||||
# 2. Pfad zur requirements.txt anpassen
|
||||
# 2. Abhängigkeiten installieren (Pfad zu deiner requirements.txt)
|
||||
.venv/bin/pip install -r doc/requirements.txt
|
||||
|
||||
# 3. MkDocs sagen, wo die Konfigurationsdatei liegt (-f Parameter)
|
||||
# 3. MkDocs Build (erzeugt den Ordner doc/site)
|
||||
.venv/bin/python -m mkdocs build -f doc/mkdocs.yml
|
||||
|
||||
- name: Deploy to Gitea Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 4. Pfad zum generierten HTML-Ordner anpassen
|
||||
# MkDocs erstellt 'site' standardmäßig dort, wo die yml liegt
|
||||
publish_dir: ./doc/site
|
||||
publish_branch: pages
|
||||
- name: Deploy via rsync
|
||||
shell: bash
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
SSH_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
run: |
|
||||
# 1. Den SSH-Key aus dem Secret in eine temporäre Datei schreiben
|
||||
echo "$SSH_KEY" > deploy_key
|
||||
chmod 600 deploy_key
|
||||
|
||||
# 2. Rsync ausführen
|
||||
# -e konfiguriert SSH so, dass der Key genutzt wird und die Host-Prüfung entfällt
|
||||
rsync -avzr --delete \
|
||||
-e "ssh -i deploy_key -o StrictHostKeyChecking=no" \
|
||||
./doc/site/ deploy@$SSH_HOST:/var/www/pages/lasertag
|
||||
|
||||
# 3. Sicherheit: Key-Datei nach dem Transfer sofort löschen
|
||||
rm deploy_key
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@
|
||||
**/.vscode
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
doc/site
|
||||
|
||||
|
||||
153
LICENSE
Normal file
153
LICENSE
Normal file
@@ -0,0 +1,153 @@
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
|
||||
|
||||
Section 1 – Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
|
||||
k. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
|
||||
|
||||
l. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
|
||||
m. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
|
||||
n. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
|
||||
|
||||
Section 2 – Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
|
||||
B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section 6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
|
||||
|
||||
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply.
|
||||
|
||||
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.
|
||||
|
||||
Section 3 – License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified form), You must:
|
||||
|
||||
A. retain the following if it is supplied by the Licensor with the Licensed Material:
|
||||
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
ii. a copyright notice;
|
||||
iii. a notice that refers to this Public License;
|
||||
iv. a notice that refers to the disclaimer of warranties;
|
||||
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
|
||||
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
|
||||
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
|
||||
|
||||
Section 4 – Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only;
|
||||
|
||||
b. if You include all or a substantial portion of the database contents in an Adapted Material, then the Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License;
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
|
||||
Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
|
||||
|
||||
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
|
||||
Section 6 – Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
|
||||
|
||||
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
|
||||
|
||||
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
|
||||
|
||||
Section 7 – Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
|
||||
|
||||
Section 8 – Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
|
||||
|
||||
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
23
README.md
23
README.md
@@ -0,0 +1,23 @@
|
||||
# Lasertag (nRF + Thread)
|
||||
|
||||
DIY-Lasertag-System auf Basis von nRF52840, Thread und BLE. Enthält Firmware (Zephyr), Hardware-Designs und die begleitende Dokumentation.
|
||||
|
||||
## Dokumentation
|
||||
- Live: [home.iten.pro/lasertag](https://home.iten.pro/lasertag)
|
||||
- Quellen: doc/ (MkDocs mit Material-Theme)
|
||||
- Schnelleinstieg: Konzept, Gameplay, Planung unter doc/docs/
|
||||
|
||||
## Repo-Überblick
|
||||
- firmware/ – Zephyr-Anwendungen (Leader, Weapon) und gemeinsame Libraries
|
||||
- hardware/ – KiCad-Designs und Skizzen
|
||||
- doc/ – MkDocs-Projekt für die Doku (build/serve über mkdocs)
|
||||
- software/ – Provisioning-Frontend und Tools
|
||||
|
||||
## Lokale Doku bauen
|
||||
```bash
|
||||
cd doc
|
||||
mkdocs serve # oder: mkdocs build
|
||||
```
|
||||
|
||||
## Lizenz
|
||||
CC BY-NC-SA 4.0 – siehe [LICENSE](LICENSE) für den vollständigen Text.
|
||||
|
||||
124
doc/docs/concepts/gameplay.md
Normal file
124
doc/docs/concepts/gameplay.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Gameplay & Spielmodi (für Spieltester)
|
||||
|
||||
Dieses Dokument ist bewusst leicht verständlich. Ziel: Euch zeigen, was wir vorhaben, und euer Feedback einsammeln. Welche Ideen habt ihr? Was macht am meisten Spaß?
|
||||
|
||||
## Kurz erklärt
|
||||
|
||||
- Wir spielen draußen mit Westen (zeigen Treffer) und Waffen (schießen mit Infrarot-Licht, ungefährlich für Augen).
|
||||
- Jede Weste gehört zu einem Team (z.B. Rot, Blau, Grün). Treffer werden von der Weste gezählt.
|
||||
- Ein Spiel dauert meist 5–15 Minuten – danach gibt es Punkte/Statistiken.
|
||||
|
||||
## Rollen
|
||||
|
||||
- **Leader-Box**: Startet/stoppt das Spiel, zeigt den Countdown, sammelt Punkte.
|
||||
- **Weste**: Zeigt Treffer, Leben und Teamfarbe.
|
||||
- **Waffe**: Schießt IR-Licht. Manche Waffen fühlen sich unterschiedlich an (z.B. Sniper = weiter, Shotgun = breiter Streukegel).
|
||||
- **Medic**: Kann heilen (kurze Distanz, breiter Strahl). Hat begrenzte Heilungen oder eine Pause zwischen Heilungen.
|
||||
- **Medipack**: Kleines Gerät, das per Taste eine Heilung auslöst (kurze Distanz, breite Streuung), mit Limit oder Cooldown. Kann Sounds abspielen (z.B. „nääääääään“ bei Cooldown, „Sorry, I am empty“ wenn leer).
|
||||
|
||||
## Spielmodi (Ideen)
|
||||
|
||||
- **Team Battle (Standard)**: Zwei Teams, wer mehr Treffer/Eliminierungen hat, gewinnt.
|
||||
- **Base Domination**: Stationen auf dem Feld. Wer länger eine Station hält, sammelt Punkte.
|
||||
- **King of the Hill**: Ein Spot in der Mitte – wer ihn hält, sammelt Punkte.
|
||||
- **Zombie**: Wenige starten als Zombie. Wird ein Mensch getroffen, wird er zum Zombie. Menschen gewinnen, wenn sie bis zum Ende überleben; Zombies, wenn alle infiziert sind.
|
||||
- **Medic Rescue**: Nur Medics können Teamkameraden wiederbeleben. Heilen geht nur aus nächster Nähe.
|
||||
- **Capture the Flag (leicht)**: Eine „Flagge“ (Box) muss ins eigene Lager gebracht werden. Treffer lassen dich die Flagge fallen.
|
||||
- **Stealth Runde**: Wenig Licht, niedrige Lebenspunkte, kürzere Spielzeit – mehr Spannung.
|
||||
|
||||
## Zonen-Ideen
|
||||
|
||||
- **Heal-Zone**: In der eigenen Basis heilt man langsam.
|
||||
- **Kill-/Radiation-Zone**: Warnung, dann langsam Schaden, wenn man zu lange drin bleibt.
|
||||
- **Respawn-Zone**: Tote Spieler müssen in ihre Home-Zone oder eine eroberte Base zurück, um neu zu starten.
|
||||
|
||||
## Power-Ups & Items
|
||||
Power-Ups können auf mehrere Arten ausgelöst werden – alle machen das Spiel strategischer und spannender!
|
||||
|
||||
### Aktive Power-Up-Stationen (mit Deckel)
|
||||
|
||||
- **Hardware**: Box mit Deckel (Sensor), Status-LED, IR-Sender
|
||||
- **So funktioniert's:**
|
||||
1. Spieler öffnet den Deckel → LED blinkt grün
|
||||
2. Spieler schießt drauf (mit Waffe oder Medipack)
|
||||
3. Box dekodiert den Schuss (Shooter-ID aus IR) und sendet das Power-Up per **Thread Unicast** an genau diesen Spieler (CoAP `game/powerup`).
|
||||
4. **Weste** des Schützen bekommt das Power-Up und schickt das Kommando an ihre **Waffe**: „20s kein Schaden" oder „20s doppel Damage". IR-Fallback nur, falls kein Mesh verfügbar.
|
||||
5. Box hat Cooldown (z.B. 30s), dann kann sie wieder aktiviert werden
|
||||
- **Beispiel-Power-Ups**: 🛡️ Shield (1 Treffer blocken), ⚡ Damage-Boost (+50%), 🩹 Health-Pack, 🔫 Ammo-Reload (für Medics)
|
||||
- **Teamplay:** Eine Person öffnet, die andere schießt drauf → beide profitieren
|
||||
|
||||
### Passive Power-Up-Boxen (Buzzer)
|
||||
|
||||
- **Hardware**: Kleine Box mit Buzzer-Button, schwacher IR-Sender, Cooldown-LED
|
||||
- **So funktioniert's:**
|
||||
1. Spieler drückt Buzzer → schwaches IR-Signal wird gesendet
|
||||
2. Weste in unmittelbarer Nähe empfängt es (z.B. nur 2m Reichweite)
|
||||
3. Power-Up gilt 15-30s, dann lädt es wieder auf
|
||||
- **Vorteil**: Keine großen Boxen nötig, schneller zu nutzen, mehr Platz für Power-Ups auf dem Feld
|
||||
|
||||
### Zonen-Effekte (von Basen/Joinern)
|
||||
|
||||
- **Hardware**: Leader-Box sendet periodisch ein Netzwerk-Signal (Thread)
|
||||
- **So funktioniert's:**
|
||||
1. Base von Team Rot sendet: „Wer von Team Rot in 15m Reichweite? Ihr bekommt +50% Damage für 30s"
|
||||
2. Weste prüft: „Bin ich von Team Rot? Bin ich nah bei der Base (RSSI-Check)? → Ja, aktiviere +50% Damage"
|
||||
3. Weste sendet zu ihrer Waffe: „Nächste 30s: 1.5x Damage"
|
||||
- **Beispiele:**
|
||||
- Heal-Zone: In der eigenen Basis langsam heilen
|
||||
- Danger-Zone: In gegnerischer Base nehmt ihr 1s Schaden
|
||||
- Team-Präsenz-Effekt: Wenn **alle** Spieler eines Teams in Reichweite der eigenen Base sind → alle erhalten +100% Angriff für 10s
|
||||
|
||||
### Strategische/zentrale Power-Ups (vom Spielleiter)
|
||||
Der Spielleiter kann jederzeit per Tastendruck ein Team-weites Power-Up auslösen:
|
||||
|
||||
- **Underdog-Bonus**: Wenn ein Team unterliegt → 20s Unsterblichkeit (kein Schaden) für das schwächere Team
|
||||
- **Base-Schuss-Trigger**: Wenn Spieler A die gegnerische Base beschießt → Team A erhält 30s Shield
|
||||
- **Turbo-Round**: Alle Spieler beide Teams: 60s lang 2x Damage und 2x Speed
|
||||
- **Spannungs-Booster**: Letzten 30s vor Spielende: Alle Spieler können nicht sterben (Shield-Dauerbuff)
|
||||
|
||||
### Wie Power-Ups wirken
|
||||
|
||||
| Power-Up | Effekt auf Weste | Effekt auf Waffe | Dauer |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 🛡️ Shield | 1 Treffer wird abgeblockt | — | 20–40s |
|
||||
| ⚡ Damage-Boost | Weniger Schaden empfangen (Attacken weniger effektiv) | Doppelter Schaden beim Schießen | 20–60s |
|
||||
| 🩹 Heal-Pack | +50 Health sofort | — | Sofort |
|
||||
| 🏃 Speed | Schneller reagieren | Schneller schießen (kürzerer Cooldown) | 15–30s |
|
||||
| 💀 Weakness | Mehr Schaden empfangen | Weniger Schaden beim Schießen | 10–20s |
|
||||
| 🔫 Ammo | Medipack wird aufgeladen | — | 1 Magazin |
|
||||
|
||||
**Wichtig**: Power-Ups können so konfiguriert werden, dass sie:
|
||||
|
||||
- Bei Spieler-Death verfallen (Shield ist weg nach Tod)
|
||||
- **Oder** beim Respawn erhalten bleiben (wichtig bei strategischen Boosts)
|
||||
|
||||
### Regeln
|
||||
|
||||
- Nur **eine** Box pro Power-Up-Typ pro Spieler gleichzeitig
|
||||
- Wenn zwei Power-Ups aktiv sind (z.B. Shield UND Damage-Boost) → beide wirken
|
||||
- Fallen in Dead-State weg (Power-Up hat keine Wirkung, wenn Spieler schon tot ist)
|
||||
|
||||
## Match-Ablauf (typisch)
|
||||
|
||||
1) Alle wählen Team/Waffe/Rolle (optional Medic).
|
||||
2) Leader startet den Countdown (laut & Licht).
|
||||
3) Spiel läuft 5–15 Minuten.
|
||||
4) Am Ende sammelt die Leader-Box die Treffer-Logs.
|
||||
5) Punkte/Statistik ansehen: Kills, Tode, Heals, wer die Base wie lange hielt.
|
||||
|
||||
## Safety & Fair Play
|
||||
|
||||
- Kein Rennen auf rutschigem Boden; keine Schläge/Checks.
|
||||
- Treffer zählen nur über IR (kein „Nachrufen“).
|
||||
- Headshots sind erlaubt, aber vorsichtig zielen (IR ist ungefährlich, trotzdem fair bleiben).
|
||||
- Respekt: Keine Beleidigungen, kein Schummeln (z.B. Sensor verdecken).
|
||||
|
||||
## Was sagt ihr?
|
||||
|
||||
- Welche Modi gefallen euch am besten?
|
||||
- Wollt ihr mehr Spannung (wenig HP) oder längere Matches (viel HP)?
|
||||
- Mehr Action (Shotgun/Spread) oder mehr Taktik (Sniper/Shield/Medic)?
|
||||
- Findet ihr Heal-/Kill-Zonen spannend oder nervig?
|
||||
- Braucht es mehr Sounds/Lichter, oder ist es so schon genug?
|
||||
|
||||
Schreibt eure Ideen/Anmerkungen dazu – wir bauen das ein!
|
||||
317
doc/docs/concepts/hardware.md
Normal file
317
doc/docs/concepts/hardware.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Hardware-Konzept
|
||||
|
||||
Dieses Dokument beschreibt die physischen Komponenten des Lasertag-Systems. Alle Knoten basieren auf dem Nordic nRF52840 SoC.
|
||||
|
||||
**Ziele:** robuste Outdoor-Tauglichkeit, klare Rollen-Trennung (Waffe/Weste/Leader), einfache Wartung (Steckverbinder, modulare Boards) und reproduzierbare Reichweite über saubere Stromversorgung.
|
||||
|
||||
## 1. Geräteübersicht
|
||||
|
||||
### 1.1 Waffe (Weapon Unit)
|
||||
|
||||
Die Waffe ist das primäre Interaktionsgerät. Sie muss robust und reaktionsschnell sein.
|
||||
|
||||
* **Controller:** nRF52840 (Dongle oder Modul).
|
||||
* **IR-Sender:** High-Power IR-LED (940nm oder 850nm) mit Optik/Linse und Treiberstufe (Reichweite > 50m).
|
||||
* **Feedback:** Muzzle Flash (helle LED), Solenoid (6V Open Frame, Rückstoss), Audio (Schussgeräusche, "Leer"-Klicken).
|
||||
* **Eingabe:** Abzug (Trigger), Nachladen (Taster), optionaler Schalter für Feuermodus.
|
||||
* **Stromversorgung:** 2S LiPo (7.4V) mit Buck auf 5V, nRF52840 mit interner 3.3V-Regelung.
|
||||
|
||||
### 1.2 Weste (Player Hub)
|
||||
|
||||
Die Weste ist die zentrale Recheneinheit des Spielers und trägt die Sensorik.
|
||||
|
||||
* **Controller:** nRF52840 DK oder Custom Board.
|
||||
* **Sensorik (IR-Empfänger):** Verteilte Sensoren für 360° Abdeckung (Kopf, Brust/Rücken, Schultern).
|
||||
* **Beleuchtung:** Adressierbare RGB-LEDs (WS2812B) an den Sensor-Positionen für Teamfarbe/Treffer.
|
||||
* **Audio:** Leistungsstarker Lautsprecher für Sprachausgabe.
|
||||
* **Verbindung:** Zentrale Box am Rücken mit Steckverbindern zu den Sensorgruppen.
|
||||
|
||||
### 1.3 Leader Box (Game Controller)
|
||||
|
||||
Die Leader Box dient zur Spielsteuerung und als Infrastruktur-Knoten.
|
||||
|
||||
* **Controller:** nRF52840.
|
||||
* **Modi (Hardware-Schalter):** 2 DIP-Schalter zur Wahl von Leader / Repeater / Base.
|
||||
* **Ausstattung:** IR-Empfänger, RGB-LEDs, Bluetooth-Gateway zur Smartphone-App.
|
||||
* **Stromversorgung:** Großer Akku für lange Laufzeit.
|
||||
|
||||
### 1.4 Power-Up-Stationen (Deckel-Boxen)
|
||||
|
||||
Physische Gegenstände, die Spieler aktivieren, um Power-Ups auszulösen.
|
||||
|
||||
* **Controller:** nRF52840 Modul oder DK.
|
||||
* **Sensoren:** Magnetschalter oder Druckschalter (unter Deckel) zur Erkennung von "Deckel offen".
|
||||
* **Aktor:** IR-LED-Treiber (ähnlich zur Waffe) – sendet Power-Up-Codes.
|
||||
* **Feedback:** RGB-LED (Status: Idle grün, aktiv blinkend, Cooldown rot).
|
||||
* **Kommunikation:** Optional Thread (CoAP) für Logging an Leader ("Station XYZ wurde von Spieler 3 aktiviert").
|
||||
* **Stromversorgung:** 2S LiPo oder USB-betrieben (wenn stationär).
|
||||
* **Cooldown:** Hardware oder Software gesteuert (z.B. 30s nach Aktivierung).
|
||||
|
||||
### 1.5 Buzzer-Power-Ups
|
||||
|
||||
Kompakte passive Power-Up-Geräte mit Druckschalter.
|
||||
|
||||
* **Controller:** nRF52840 Modul.
|
||||
* **Eingabe:** Einfacher Druckschalter (Buzzer-Button).
|
||||
* **Aktor:** Schwacher IR-LED-Sender (kurze Reichweite ~2-3m, breiter Strahl) für Power-Up-Code.
|
||||
* **Feedback:** Cooldown-LED (Rot = kann nicht drücken, Grün = aktiv).
|
||||
* **Kommunikation:** Optional Thread (nur für Admin-Logging, nicht kritisch für Gameplay).
|
||||
* **Stromversorgung:** 2S LiPo mit Schutzschaltung.
|
||||
* **Cooldown:** Intern gesteuert (z.B. 15s zwischen Aktivierungen).
|
||||
|
||||
## 2. Energieversorgung & Verkabelung
|
||||
|
||||
### 2.1 Akkusystem
|
||||
|
||||
**Standard:** 2S LiPo (7.4 V nominal, 8.4 V voll geladen, 6.0 V Entladungsschutz).
|
||||
**Alternative:** 1S nur für Tests oder Low-Power-Prototypen – ungeeignet für hohe IR-LED-Ströme (siehe Abschnitt 4.1).
|
||||
|
||||
**Schutz & Laden:**
|
||||
|
||||
- **Zellenschutz-IC:** HY2120-CB + FS8205A (Dual-FET) – schützt vor Über-/Unterspannung, Überstrom, Kurzschluss.
|
||||
- **Lade-IC:** IP2326 (2S Balancing, USB-C) – ermöglicht einfaches Laden ohne externe Balancer.
|
||||
- **Fuel Gauge:** Spannungsteiler-basierte ADC-Messung (R1=100k, R2=47k) → Software-Mapping auf Ladestand (6.0 V = 0 %, 8.4 V = 100 %).
|
||||
|
||||
!!! info "Warum 2S?"
|
||||
2S-Systeme bieten ausreichend Headroom für IR-LED-Konstantstromquellen (>4.8 V nötig bei 3A) und stabile Versorgung auch bei hoher Last. 1S-Zellen brechen unter 1A+ schnell auf 3.4–3.6 V ein.
|
||||
|
||||
### 2.2 Spannungsebenen & Wandler
|
||||
|
||||
**Primär-Rail (Batterie, 6.0–8.4 V):**
|
||||
Direkt gespeist: IR-LED-Treiber, Muzzle-Flash-LED, Solenoid 6V (Open Frame, taktiles Feedback Rückstoss).
|
||||
|
||||
**Sekundär-Rail (5.0 V, ~1.5 A):**
|
||||
Buck-Converter (z.B. MP2315, TPS62130) für Audio-IC (MAX98357A), adressierbare LEDs (WS2812B) und nachgeschalteten LDO. Hohe Schaltfrequenz gewünscht (geringe Induktivität, kompakte Bauform).
|
||||
|
||||
**Tertiär-Rail (3.3 V, ~30 mA):**
|
||||
LDO (z.B. MCP1826, AMS1117-3.3) aus 5V Buck-Ausgang für nRF52840 und QSPI Flash. Geringer Dropout (5V → 3.3V = 1.7V) reduziert Wärmeentwicklung; Low-Noise-Design minimiert Störungen auf der RF-Schaltung.
|
||||
|
||||
**IR-Empfänger (5V mit Level-Shift):**
|
||||
TSOP48xx-Module laufen an 5V (besserer SNR, robuster gegen Sonnenlicht). Output-Signal wird per **AO4300A N-Channel MOSFET** (Open-Drain) auf 3.3V gewandelt → invertierendes Level-Shifting. Software kompensiert Invertierung mit `GPIO_ACTIVE_LOW`.
|
||||
|
||||
**Verkabelung Weste:**
|
||||
Sternförmige Abgänge zu Sensor-Modulen (Kopf/Brust/Schultern); jeweils 5V + GND + WS2812-Data; verriegelnde Stecker (JST-XH o.ä.), Polyfuse pro Ast, Verpolschutz (Diode/FET).
|
||||
|
||||
### 2.3 Leistungsbilanz (Weste)
|
||||
|
||||
Die Weste hat durch LEDs und Audio den höchsten Verbrauch; hier die Peak-Abschätzung:
|
||||
|
||||
**5V-Rail (Buck-Converter):**
|
||||
|
||||
| Komponente | Zustand | Strom | Leistung |
|
||||
| :--- | :--- | ---: | ---: |
|
||||
| Audio (MAX98357A) | Volllast (3W @ 90% η) | ~670 mA | 3.35 W |
|
||||
| WS2812B LEDs (5×) | 100 % Weiß | 300 mA | 1.50 W |
|
||||
| IR-Empfänger (5×) | Dauerbetrieb @ 5V | ~50 mA | 0.25 W |
|
||||
| LDO 3.3V (Durchleitung) | 30 mA @ 3.3V | ~30 mA | 0.15 W |
|
||||
| **Gesamt 5V (Peak)** | | **~1.05 A** | **5.25 W** |
|
||||
|
||||
**3.3V-Rail (LDO aus 5V Buck):**
|
||||
|
||||
| Komponente | Zustand | Strom | Leistung |
|
||||
| :--- | :--- | ---: | ---: |
|
||||
| nRF52840 | BLE+Thread aktiv | ~15 mA | 0.05 W |
|
||||
| QSPI Flash | Read/Write Burst | ~15 mA | 0.05 W |
|
||||
| **Gesamt 3.3V (Peak)** | | **~30 mA** | **0.10 W** |
|
||||
|
||||
**Auslegung:**
|
||||
- **Buck-Regler:** 1.5 A Nennstrom (30 % Reserve), hohe Schaltfrequenz (>1 MHz) für kompakte Drossel/Kondensatoren.
|
||||
- **LDO:** 100 mA Nennstrom ausreichend; Verlustleistung bei 30 mA: $P_{loss} = (5V - 3.3V) \cdot 30mA = 51 mW$ (unkritisch). Low-Noise-Design (< 50 µVrms) für sauberen RF-Betrieb.
|
||||
|
||||
### 2.4 Blockschaltbild Energieversorgung
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
BAT["2S LiPo<br/>6.0–8.4V"] <--> PROT["Zellenschutz<br/>HY2120+FS8205A"]
|
||||
CHG["USB-C Lader<br/>IP2326"] --> PROT
|
||||
|
||||
PROT --> BUCK["Buck 5.0V<br/>MP2315/TPS62130<br/>1.5A"]
|
||||
PROT --> IR_DRV["IR-LED<br/>Konstantstromquelle"]
|
||||
PROT --> MUZZLE["Muzzle Flash<br/>LED-Treiber"]
|
||||
PROT --> SOL["Solenoid 6V<br/>Open Frame"]
|
||||
|
||||
BUCK --> AUDIO["Audio Amp<br/>MAX98357A"]
|
||||
BUCK --> LED["WS2812B LEDs<br/>mit Level Shift"]
|
||||
BUCK --> IR_RX["IR-Empfänger<br/>TSOP48xx (5V)"]
|
||||
BUCK --> LDO["LDO 3.3V<br/>MCP1826/AMS1117<br/>100mA"]
|
||||
|
||||
LDO --> NRF["nRF52840<br/>3.3V"]
|
||||
LDO --> FLASH["QSPI Flash<br/>3.3V"]
|
||||
|
||||
IR_RX -->|"AO4300A<br/>MOSFET Shifter"| NRF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Stückliste (Übersicht)
|
||||
|
||||
Diese Tabelle gibt einen Überblick über die groben Komponenten pro Einheit. Detaillierte Part Numbers und Bezugsquellen folgen in separaten Docs.
|
||||
|
||||
| Komponente | Waffe | Weste | Leader | Menge | Anmerkung |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| nRF52840 (SoC/Modul) | ✓ | ✓ | ✓ | 1/Gerät | Zephyr SDK Support |
|
||||
| IR-LED (High-Power) | ✓ | | | | 940nm, > 50m Reichweite |
|
||||
| IR-Empfänger (38kHz) | | ✓ | ✓ | 5–10 | Verteilt auf Kopf/Torso/Schulter |
|
||||
| RGB-LED (WS2812B) | | ✓ | ✓ | 1–3 | Teamfarbe + Status |
|
||||
| Solenoid (6V Open Frame) | ✓ | | | | Taktiles Feedback Rückstoss |
|
||||
| Lautsprecher | ✓ | ✓ | | | Schussgeräusche + Sprachausgabe |
|
||||
| 2S LiPo Akku | ✓ | ✓ | ✓ | 1 | 7.4V, ggf. unterschiedliche Kapazität |
|
||||
| Lade-IC (IP2326) | ✓ | ✓ | ✓ | 1 | 2S Balancing |
|
||||
| Zellenschutz (FS8205A) | ✓ | ✓ | ✓ | 1 | Verpolschutz + OV/UV |
|
||||
| Spannungsteiler-ADC | ✓ | ✓ | ✓ | 1 | Fuel Gauge (R1=100k, R2=47k) |
|
||||
| Taster (Trigger/Reload) | ✓ | | | | Auch optional Dip-Switch für Leader |
|
||||
| USB-C / Pogo-Pad | ✓ | ✓ | ✓ | 1 | Laden + Debug-Konsole |
|
||||
| Steckverbinder (JST-XH) | ✓ | ✓ | | | Modular aufgebaut |
|
||||
|
||||
## 4. Schaltungskomponenten (Detail)
|
||||
|
||||
### 4.1 IR-LED-Treiber (Konstantstromquelle)
|
||||
|
||||
#### Funktionsprinzip
|
||||
|
||||
Hybride PNP/NPN-Topologie für präzisen, modulierten IR-Puls (38 kHz). Die Stromquelle stellt sicher, dass bei wechselnder Batteriespannung der LED-Strom konstant bleibt (→ reproduzierbare Reichweite).
|
||||
|
||||

|
||||
|
||||
#### Stromeinstellung
|
||||
|
||||
Der Sollstrom wird über $R_{set}$ definiert:
|
||||
|
||||
$$R_{set} = \frac{0,65\,\text{V}}{I_{\text{LED}}}$$
|
||||
|
||||
**Beispiele:**
|
||||
|
||||
| $I_{\text{LED}}$ | $R_{set}$ | Einsatz |
|
||||
| :--- | :--- | :--- |
|
||||
| 0,5 A | 1,30 Ω | Standard/Nahkampf |
|
||||
| 1,0 A | 0,65 Ω | Hohe Reichweite (SFH 4550) |
|
||||
| 2,0 A | 0,33 Ω | Pulsbetrieb (extreme Leistung) |
|
||||
| 3,0 A | 0,22 Ω | Scharfschütze (Oslon Black) |
|
||||
|
||||
**Thermik:** Bei 38-kHz-Modulation (Duty-Cycle ~30 %) ist $P_{\text{avg}} = R_{set} \cdot I^2_{\text{LED}} \cdot DC$ → deutlich unter Peak. $R_{set}$ muss aber Spitzenstrom verkraften → impulsfeste Typen (Metallschicht, Drahtwiderstand).
|
||||
|
||||
#### Headroom & Akkuwahl
|
||||
|
||||
Minimalspannung für stabile Regelung:
|
||||
|
||||
$$V_{\text{CC,min}} = V_{f(\text{LED})} + 0,65\,\text{V} + 1,0\,\text{V}_{\text{Headroom}}$$
|
||||
|
||||
| $I_{\text{LED}}$ | $V_f$ (typ.) | $V_{\text{CC,min}}$ | Akku |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 0,5 A | 2,0 V | 3,65 V | 1S (nur voll geladen) |
|
||||
| 1,0 A | 2,4 V | 4,05 V | 2S empfohlen |
|
||||
| 2,0 A | 2,8 V | 4,45 V | 2S erforderlich |
|
||||
| 3,0 A | 3,2 V | 4,85 V | 2S erforderlich |
|
||||
|
||||
!!! warning "1S ungeeignet für >1A"
|
||||
1S-Akkus brechen unter Last auf 3,4–3,6 V ein → Regelung versagt, Reichweite bricht ein. 2S liefert auch bei Teilentladung (7,0 V) genug Headroom.
|
||||
|
||||
### 4.2 Adressierbare LEDs (WS2812B)
|
||||
|
||||
**Anforderung:** 5V-Versorgung, aber Daten-Pegel kompatibel mit nRF52840 (3.3V Logic).
|
||||
|
||||
**Level-Shift:** SN74AHCT1G125 (3.3V → 5V, Single-Gate); schnell genug für WS2812-Timing (800 kHz).
|
||||
**Serienwiderstand:** ~330 Ω nach dem Shifter → dämpft Reflexionen auf der Data-Leitung, verhindert Überschwinger.
|
||||
|
||||
**Layout:** Data-Leitung kurz halten; bei mehreren LEDs in Serie: Bypass-Kondensator (100 nF + 10 µF) pro 3–5 LEDs.
|
||||
|
||||
### 4.3 Audio-Verstärker (MAX98357A)
|
||||
|
||||
**Ziel:** Klare Schuss- und Sprach-Ausgabe mit minimalem Aufwand und geringer CPU-Last.
|
||||
|
||||
**Architektur:** I2S Class-D Verstärker – DAC + Endstufe integriert, filterlose Topologie (wenige Bauteile).
|
||||
|
||||
* **Schnittstelle:** I2S (digital); Audio-Stream per EasyDMA vom nRF52840 → CPU bleibt frei für Game Logic.
|
||||
* **Leistung:** ~3.2 W @ 4Ω – laut genug für Outdoor-Einsatz.
|
||||
* **Effizienz:** ~90 % → Akku-schonend; geringer Ruhestrom im Idle.
|
||||
* **Layout:** Kurze, symmetrische Leitungen zu Speaker-Terminals; separate Ground-Plane; Entkopplung (10 µF + 100 nF) nahe VDD-Pin.
|
||||
|
||||
### 4.4 Flash-Speicher (QSPI)
|
||||
|
||||
**Aufgabe:** Audio-Files (Schuss-FX, Ansagen) und Spiel-Logs (optional Treffer-Historie).
|
||||
|
||||
* **Technik:** QSPI-NOR-Flash (z.B. W25Q128JV, GD25Q16C); 1.8 V oder 3.3 V; XIP-fähig (Execute-in-Place für Code möglich).
|
||||
* **Kapazität:** 8–16 MB; reicht für ~3 min @ 22 kHz oder ~1.5 min @ 44 kHz (16 bit mono). Empfehlung: 22 kHz – höhere Sample-Rate bringt bei Outdoor-Speaker kaum Mehrwert.
|
||||
* **Interface:** QSPI (4-Bit parallel); nRF52840 unterstützt DMA-basierten Zugriff → schnelle Reads ohne CPU-Last.
|
||||
* **Layout:** Flash nahe am MCU (< 5 cm Leitungslänge); Differenzen in Trace-Längen < 1 mm; saubere Ground-Plane; JEDEC-ID beim Boot prüfen.
|
||||
|
||||
### 4.5 Akku-Überwachung (Fuel Gauge)
|
||||
|
||||
**Prinzip:** Spannungsteiler + ADC für 2S-Akkus (0–8.4 V) → Software-basierte Ladezustandsschätzung (kein dediziertes Fuel-Gauge-IC nötig).
|
||||
|
||||
**Schaltungskomponenten:**
|
||||
|
||||
| Bauteil | Wert | Funktion |
|
||||
| :--- | :--- | :--- |
|
||||
| $R_1$ | 100 kΩ | Spannungsteiler – oberer Zweig |
|
||||
| $R_2$ | 47 kΩ | Spannungsteiler – unterer Zweig (→ ADC) |
|
||||
| $C_1$ | 100 nF | Tiefpass-Glättung am ADC-Eingang |
|
||||
|
||||
**Softwarelogik:**
|
||||
|
||||
1. **ADC-Konvertierung:** 12-bit ADC liest $V_{\text{div}}$ (max. 3.3 V bei VRef = 3.3 V).
|
||||
2. **Rückrechnung:** $V_{\text{bat}} = V_{\text{adc}} \cdot \frac{R_1 + R_2}{R_2} = V_{\text{adc}} \cdot 3.13$
|
||||
3. **Mapping:** Lookup-Table oder linear interpoliert:
|
||||
- 8.4 V → 100 % (voll geladen)
|
||||
- 7.4 V → ~50 % (nominal)
|
||||
- 6.0 V → 0 % (Schutzschaltung aktiv)
|
||||
|
||||
**Kalibrierung:** Einmalig bei Produktion: Spannung an bekanntem Referenzpunkt messen, Offset/Gain in NVS speichern.
|
||||
|
||||
---
|
||||
|
||||
## 5. Bauteil-Übersicht & Empfehlungen
|
||||
|
||||
Konsolidierte Liste der Schlüsselkomponenten mit konkreten Part-Vorschlägen. Detaillierte BOMs folgen in separaten Docs (Waffe/Weste/Leader).
|
||||
|
||||
| Kategorie | Bauteil/Funktion | Vorschlag | Alternativen | Anmerkung |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **MCU** | Mikrocontroller | nRF52840 | — | Zephyr-Support, BLE+Thread |
|
||||
| **Energie** | 2S-Akku | Li-Po 7.4V, 1000–2000 mAh | — | Kapazität je nach Gerät |
|
||||
| | Zellenschutz | HY2120-CB + FS8205A | DW01A + 8205A | OV/UV/OC-Protection |
|
||||
| | Lade-IC | IP2326 (2S Balancing) | TP4056 (nur 1S) | USB-C, Balancing integriert |
|
||||
| | Buck 5V | MP2315, TPS62130 | — | 1.5 A, >1 MHz Schaltfrequenz |
|
||||
| | LDO 3.3V | MCP1826, AMS1117-3.3 | XC6206P332MR | Low-Noise für RF, < 0.5V Dropout |
|
||||
| **IR** | IR-LED | SFH 4550, Oslon Black | TSAL6400 | 940 nm, >50 m Reichweite |
|
||||
| | IR-Empfänger | TSOP4838, TSOP38438 | VS1838B | 38 kHz Demodulator, 5V Supply |
|
||||
| | LED-Treiber | PNP/NPN diskret | IRL530 (Logic-FET) | Konstantstrom, PWM-fähig |
|
||||
| | Level-Shifter IR | AO4300A (N-Ch MOSFET) | BSS138, 2N7002 | 5V → 3.3V, invertierend |
|
||||
| **LED** | Adressierbare | WS2812B (5050) | SK6812, APA102 | 5V, ~60 mA/LED @ weiß |
|
||||
| | Level-Shift | SN74AHCT1G125 | 74HCT245 (8-Kanal) | 3.3V → 5V, single-gate |
|
||||
| **Audio** | Class-D Amp | MAX98357A | PAM8302, TPA2005D1 | I2S, 3.2W @ 4Ω |
|
||||
| | Speaker | 4Ω, 3–5W | 8Ω (lower SPL) | Outdoor-tauglich |
|
||||
| **Speicher** | QSPI Flash | W25Q128JV (16 MB) | GD25Q16C (2 MB) | NOR-Flash, 3.3V |
|
||||
| **Feedback** | Solenoid | 6V Open Frame | — | Rückstoss direkt ab Batterie |
|
||||
| | Muzzle LED | Weiß/Gelb, 1W+ | Cree XP-E2 | Sichtbar bei Tag |
|
||||
| **Passiv** | $R_{\text{set}}$ (IR) | 0.22–1.3 Ω, 3W | Metallschicht, Draht | Impulsfest |
|
||||
| | Spannungsteiler | 100k + 47k, 1% | 0.1% für Präzision | Fuel Gauge |
|
||||
| **Mechanik** | Stecker | JST-XH (2.54mm) | Molex PicoBlade | Verriegelnd, 3–5 Pole |
|
||||
| | Taster | Omron B3F, Alps SKQG | Cherry MX (größer) | Trigger, Reload |
|
||||
|
||||
**Hinweise:**
|
||||
|
||||
- **IR-LED:** Oslon Black für extreme Reichweite (3A-Betrieb), SFH 4550 für Standard (1–2A).
|
||||
- **Audio:** MAX98357A ist quasi-Standard; Alternativen (PAM8302) haben höheren THD, aber OK für SFX.
|
||||
- **Flash:** 16 MB erlauben ~6 min Audio @ 22 kHz – gut für zukünftige Erweiterungen (z.B. mehrsprachige Ansagen).
|
||||
- **Stecker:** JST-XH ist weit verbreitet und günstig; Molex PicoBlade kompakter, aber teurer.
|
||||
|
||||
### 5.1 IR-LEDs
|
||||
|
||||
| Typ | Leistung |Bemerkungen |
|
||||
|-----|----------|------------|
|
||||
| **SFH 4725S** | 3W | Standardmodell für 940nm<br>**Vorteile:** Sehr bewährt, gute Effizienz |
|
||||
| **SFH 4726S** | 3W | Ähnlich wie die 4725S, aber oft mit einer leicht anderen internen Linsencharakteristik (breiterer Abstrahlwinkel ohne externe Optik). |
|
||||
| **SFH 4727AS** | 5W | Das 940-nm-Gegenstück zu deiner 4715AS. <br>**Vorteil:** Für deine 3-A-Pulse im Outdoor-Modus die stabilste Wahl. Sie verträgt die hohen Pulsströme thermisch am besten.|
|
||||
|**SFH 4725AS**| 3W | Eine neuere "A"-Revision mit verbesserter Wärmeableitung.|
|
||||
|
||||
Es wird empfohlen, entsprechende "STAR"-Aluplatinen zu verwenden, um die Wärmeableitung zu garantieren.
|
||||
|
||||
### IR-Empfänger
|
||||
|
||||
| Typ | Bemerkungen |
|
||||
|-----|------------|
|
||||
| **TSOP34456 / TSOP38456** | Der Standard für 56 kHz.<br>**Charakteristik**: Besitzt eine sehr agressive **AGC (Automatic Gain Controll)**<br>**Problem:** Bei extrem starken Signalen im Nahbereich kann die AGC "zumachen" und die Hüllkurve verzerren. |
|
||||
| **TSSP4056 / TSSP77056** | **Vorteil:** Er hat eine **feste Verstärkung (Fixed Gain)**. Er regelt also nicht ab, wenn das Signal stark wird.<br>**Nutzen:** Das Signal bleibt viel konstanter als bei bei einem TSOP.|
|
||||
|
||||
|
||||
*Stand: 04.01.2026*
|
||||
121
doc/docs/concepts/mobile_app.md
Normal file
121
doc/docs/concepts/mobile_app.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Mobile App
|
||||
|
||||
Um das Spiel zu steuern, wird eine mobile App entwickelt. Sie soll anfangs für Android verfügbar sein. Wenn die Hürden nicht zu groß sind, wird auch eine iOS-Version in Betracht gezogen. Dazu wird die Entwicklung in Flutter stattfinden.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Der grundsätzliche Ablauf sieht so aus:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start([App-Start])
|
||||
choose_mode{Modus wählen?}
|
||||
choose_leader[Weg A: Leader (Spiel)]
|
||||
maintenance[Weg B: Ausrüstung warten]
|
||||
check_state{Leader-Status?}
|
||||
|
||||
lobby[2a. Lobby-Phase]
|
||||
game[2b. Spiel-Phase]
|
||||
evaluation[2c. Auswertungs-Phase]
|
||||
|
||||
start-->choose_mode
|
||||
choose_mode-->|Leader / Spiel|choose_leader
|
||||
choose_mode-->|Wartung|maintenance
|
||||
maintenance-->|Zurück|choose_mode
|
||||
|
||||
choose_leader-->check_state
|
||||
check_state-->|Kein Spiel läuft|lobby
|
||||
check_state-->|Spiel läuft|game
|
||||
check_state-->|Spiel abgeschlossen|evaluation
|
||||
|
||||
lobby-->|Spiel starten|game
|
||||
game-->|Spiel abbrechen|lobby
|
||||
game-->|Spielende|evaluation
|
||||
evaluation-->|Neues Spiel|lobby
|
||||
|
||||
classDef phase fill:#eef4ff,stroke:#6c8be0,stroke-width:1px,color:#0d1b2a
|
||||
class lobby,game,evaluation phase
|
||||
```
|
||||
|
||||
### 1. Leader bestimmen
|
||||
- Beim Start der App wird geschaut, ob der zuletzt gewählte Game-Leader über BLE erreichbar ist.
|
||||
- Ist der nicht erreichbar, wird nach allen verfügbaren Leader-Knoten gescannt und zur Auswahl angeboten.
|
||||
|
||||
### Startbildschirm: Zwei-Wege-Strategie
|
||||
- **Weg A (Spiel-Modus):** Leader wählen, in die Lobby-Phase wechseln, Parameter verteilen und ein Spiel starten.
|
||||
- **Weg B (Wartungs-Modus):** Waffe oder Weste direkt auswählen, um Standalone-Einstellungen vorzunehmen (Name ändern, Firmware prüfen, Batteriestand checken), ohne den Leader hochzufahren.
|
||||
|
||||
**UI-Vorschlag Start-Screen**
|
||||
- **Oben: Spiel-Leiter finden (Primär):** Fokus auf Leader-Knoten, großes Funkturm-Icon, Einstieg in den Game-Flow (Lobby -> Spiel -> Auswertung).
|
||||
- **Unten: Meine Ausrüstung (Sekundär):** Liste aller anderen gefundenen BLE-Geräte (Westen, Waffen), dezentere Darstellung, Ziel: Schnellkonfiguration einzelner Hardware-Komponenten.
|
||||
|
||||
### 2a. Lobby-Phase
|
||||
|
||||
In der Lobby-Phase wird das Spiel vorbereitet: Spielmodus wählen, Teams/Spieler zuordnen und Parameter konfigurieren.
|
||||
|
||||
#### Thread-Netzwerk & Provisionierung
|
||||
- Thread-Parameter werden am Leader konfiguriert und über das Thread-Netzwerk an alle verbundenen Geräte verteilt
|
||||
- Im Hintergrund läuft permanent ein BLE-Scan nach neuen Geräten
|
||||
- Neu gefundene Geräte werden automatisch mit den Thread-Parametern provisioniert und dem Netzwerk hinzugefügt
|
||||
|
||||
#### Geräte-Discovery & Monitoring
|
||||
- Die App triggert den Leader in regelmäßigen Abständen, eine "Who is there"-Multicast-Abfrage im Thread-Netzwerk auszuführen
|
||||
- Zurückgemeldete Geräte werden in einer Liste erfasst
|
||||
- Beim ersten Erscheinen eines Geräts werden dessen Parameter abgerufen: Name, Typ, spezifische Eigenschaften
|
||||
- Geräte, die sich über einen bestimmten Zeitraum nicht mehr melden, werden ausgegraut
|
||||
- **Auto-Synchronisation:** Meldet ein Gerät Spieler- oder Team-Zuordnungen (z.B. "Spieler 3, Team 2"), die in der App noch nicht existieren, werden diese automatisch angelegt (z.B. "Spieler 3" und "Team 2"), um Konfigurationskonflikte zu vermeiden
|
||||
|
||||
#### Benutzeroberfläche
|
||||
|
||||
**Geräteliste:**
|
||||
|
||||
- Zeigt alle verfügbaren Geräte an
|
||||
- Antippen eines Geräts blendet zusätzliche Icons ein:
|
||||
- **Verbindungsstatus:** Zeigt an, ob das Gerät kürzlich gesehen wurde
|
||||
- **Zuordnungsstatus:** Ob das Gerät einem Spieler/Team zugeordnet ist
|
||||
- **Identifikation:** Lässt das Gerät blinken (3x) oder LED atmen zur physischen Identifikation
|
||||
- **Entfernen:** Nur aktiv bei ausgegrauten Geräten
|
||||
- **Einstellungen:** Geräteeinstellungen bearbeiten
|
||||
|
||||
**Teamliste:**
|
||||
|
||||
- Zeigt alle Teams an
|
||||
- Antippen erweitert die Ansicht mit den zugewiesenen Spielern
|
||||
|
||||
**Spielerliste:**
|
||||
|
||||
- Zeigt alle Spieler mit Teamzuordnungsstatus an
|
||||
- Spieler können angelegt, gelöscht und umbenannt werden
|
||||
|
||||
#### Spielkonfiguration
|
||||
- Spielkonfigurationen können gespeichert und später neu geladen werden
|
||||
- Alle Spielparameter (Dauer, Respawns, etc.) sind hier einstellbar
|
||||
|
||||
#### Spielstart-Bedingungen
|
||||
Das Spiel kann nur gestartet werden, wenn folgende Bedingungen erfüllt sind:
|
||||
|
||||
- Alle Westen und Waffen sind zugeordnet
|
||||
- Jede Weste ist genau einem Spieler zugeordnet (und umgekehrt)
|
||||
- Der Spielmodus ist gesetzt und konfiguriert
|
||||
- Alle Basen, Power-Ups etc. sind konfiguriert
|
||||
- Kein ausgegrautes Gerät ist vorhanden
|
||||
- Je nach Spielmodus: Alle Spieler sind einem Team zugeordnet
|
||||
|
||||
Sind nicht alle Bedingungen erfüllt, werden die Gründe angezeigt.
|
||||
|
||||
#### Übergang zur Spiel-Phase
|
||||
- Beim "Spiel starten" wird eine zufällige Spiel-ID generiert und an den Leader übermittelt
|
||||
- Die aktuelle Spielkonfiguration wird gespeichert
|
||||
- Dadurch kann das Spiel auch nach App-Neustart ausgewertet werden
|
||||
|
||||
### 2b. Spiel-Phase
|
||||
- Laufendes Spiel: Status/Timer anzeigen, optional Live-Events (Treffer, Bases) und Admin-Aktionen wie "Spiel abbrechen".
|
||||
- Spielende erfolgt je nach Modus (Timer, Score, Objective) und wechselt in die Auswertungs-Phase.
|
||||
- Sollte die App während der Spielphase gestartet werden, wird geprüft, ob die Spiel-ID auf dem gewählten Leader-Knoten mit der gespeicherten ID zusammenpasst. Wenn ja, schaltet sich die App auf das Spiel auf. Wenn nein, meldet die App einen Fehler, dass eine Verbindung aufgrund eines laufenden Spieles nicht möglich ist.
|
||||
Je nach Spielmodus ist es der App möglich, in das Spielgeschehen einzugreifen (zum Beispiel Power-Ups senden etc.).
|
||||
### 2c. Auswertungs-Phase
|
||||
- Geräte auslesen, Scores sammeln, Rangliste erzeugen und anzeigen.
|
||||
- Optionen: "Neues Spiel" führt zurück in die Lobby; Export/Share der Ergebnisse möglich.
|
||||
- Sollte die App während der Auswertungsphase gestartet werden, wird geprüft, ob die Spiel-ID auf dem gewählten Leader-Knoten mit der gespeicherten ID zusammenpasst. Wenn ja, schaltet sich die App auf das Spiel auf. Wenn nein, meldet die App einen Fehler, dass die Spiel-ID unbekannt ist und somit eine Auswertung nicht möglich ist.
|
||||
|
||||
Eine Auswertung erfolgt erst, wenn alle Geräte, die zur Auswertung erforderlich sind, abgerufen werden konnten. Ist das innerhalb einer bestimmten Zeit nicht möglich, wird dem Benutzer angezeigt, welche fehlen. Er kann dann dafür sorgen, dass diese in Reichweite kommen und die Auswertung abschliessen.
|
||||
432
doc/docs/concepts/software.md
Normal file
432
doc/docs/concepts/software.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Software-Konzept & Spielablauf
|
||||
|
||||
Dieses Dokument beschreibt die Software-Architektur, die Rollenverteilung und die Kommunikationsabläufe des Lasertag-Systems.
|
||||
|
||||
## Überblick
|
||||
|
||||
* Architektur: Leader als BLE/Thread-Brücke, Westen als Spiel-Authority für Health/Regeln, Waffen als Sensor/Aktor.
|
||||
* Funk: BLE für Provisionierung/App, Thread (CoAP/UDP) für Spielverkehr, IR für Treffer-Übertragung.
|
||||
* Prinzip: Dezentraler Trefferentscheid (auf der Weste) mit optionalem Live-Ticker zum Leader; Leader hält Spielstatus und sammelt Logs.
|
||||
|
||||
## 1. System-Rollen & Hardware-Typen
|
||||
|
||||
Das System basiert auf nRF52840-Chips, die über OpenThread (802.15.4) kommunizieren. Am wichtigsten dabei ist die Kommunikation von der Weste zur Waffe, da die Waffe beim Death des Spielers sofort deaktiviert werden muss. Die OpenThread Message Priority soll verwendet werden, um die Weste-Waffe-Kommunikation zusätzlich zu optimieren. Multicasts: Low, Weste -> Waffe enable und disable: High, Rest normal.
|
||||
|
||||
### A. Leader Box (Game Controller)
|
||||
* **Funktion:** Zentrale Spielsteuerung, Zeitgeber, Gateway zur Smartphone-App.
|
||||
* **Modi (wählbar via DIP-Schalter):**
|
||||
* `00` **Leader:** Spielleiter, BLE-Gateway, sammelt Punkte.
|
||||
* `01` **Repeater:** Router im Mesh zur Reichweitenverlängerung (z.B. am Baum).
|
||||
* `11` **Base:** Interaktives Ziel (z.B. für "Domination"-Modus).
|
||||
* **Hardware:** IR-Empfänger, RGB-LEDs, BLE aktiv.
|
||||
|
||||
### B. Weste (Player Hub)
|
||||
* **Funktion:** Zentrale Einheit des Spielers. Verwaltet Lebenspunkte, empfängt Treffer, steuert Audio.
|
||||
* **Sensoren:** Kopf (3x: Stirn, Links, Rechts), Brust/Rücken, Seiten (Schultern/Arme).
|
||||
* **Kommunikation:** Hält die Verbindung zur Waffe (Pairing) und zum Leader.
|
||||
|
||||
### C. Waffe
|
||||
* **Funktion:** Aussenden der IR-Signale, haptisches Feedback, Muzzle-Flash.
|
||||
* **Typen:** Pistole, Sniper, Shotgun (unterschiedliche IR-Leistung/Fokussierung).
|
||||
* **Logik:** Sendet "Schuss"-Events, empfängt "Sperren"-Befehle von der Weste (wenn tot).
|
||||
|
||||
---
|
||||
|
||||
## 2. Provisionierung & Setup (Lobby-Phase)
|
||||
|
||||
Bevor das Spiel startet, müssen Geräte dem Netzwerk beitreten und Spielern zugeordnet werden.
|
||||
|
||||
### Schritt 1: Netzwerk-Beitritt (Provisioning)
|
||||
* **Szenario:** Neue Hardware wird zum ersten Mal verwendet.
|
||||
* **Ablauf:**
|
||||
1. Spielleiter verbindet App via BLE mit **Leader Box**.
|
||||
2. Leader Box öffnet das Thread-Netzwerk (Commissioning).
|
||||
3. Neue Geräte (Waffe/Weste) werden in den Pairing-Modus versetzt (z.B. Tastenkombination).
|
||||
4. Geräte erhalten Netzwerk-Credentials und treten dem Mesh bei.
|
||||
|
||||
### Schritt 2: Spieler-Konfiguration (Assignment)
|
||||
* **Ziel:** Zuordnung von Hardware zu einer logischen `PlayerID` und einem `Team`.
|
||||
* **Identifikation:** Jedes nRF52-Board hat eine eindeutige **EUI-64** (MAC).
|
||||
* **Ablauf:**
|
||||
1. App scannt **QR-Code** oder **NFC-Tag** an der Weste/Waffe (Payload: EUI-64 Adresse).
|
||||
2. App sendet Konfiguration an Leader Box: `EUI-64 -> {PlayerID, Team, Name}`.
|
||||
3. Leader Box löst EUI-64 in aktuelle IPv6-Adresse auf (Neighbor Table / Discovery).
|
||||
4. Leader sendet Konfigurations-Paket (CoAP Unicast) an das Gerät:
|
||||
* **Weste:** Erhält PlayerID, TeamID, MaxHealth, evtl. Rolle.
|
||||
* **Waffe:** Erhält Damage-Wert, Nachladezeit, Magazingröße, Waffentyp.
|
||||
5. Geräte speichern ID/Team im RAM (optional NVS für Persistenz bei Neustart).
|
||||
|
||||
### Provisioning – Sequenzdiagramm
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User as Spielleiter
|
||||
participant App
|
||||
participant Leader
|
||||
participant Weste
|
||||
participant Waffe
|
||||
|
||||
User->>App: Verbindung via BLE
|
||||
App->>Leader: Thread-Credentials (BLE)
|
||||
Leader->>Leader: Öffne Mesh für Provisioning
|
||||
|
||||
User->>Weste: Pairing-Modus aktiviert
|
||||
User->>Waffe: Pairing-Modus aktiviert
|
||||
|
||||
Weste->>Leader: Beitritt ins Thread-Netz
|
||||
Waffe->>Leader: Beitritt ins Thread-Netz
|
||||
|
||||
User->>App: QR-Code Weste scannen
|
||||
App->>Leader: PlayerID 1, Team=Rot, Name="Alice"
|
||||
Leader->>Weste: Konfig (CoAP PUT /game/conf)
|
||||
Weste->>Weste: Speichere PlayerID=1, Team=Rot
|
||||
|
||||
User->>App: QR-Code Waffe scannen
|
||||
App->>Leader: PlayerID 1, Waffentyp=Pistol
|
||||
Leader->>Waffe: Konfig (CoAP PUT /game/wconf)
|
||||
Waffe->>Waffe: Speichere Damage=10, ReloadTime=500ms
|
||||
|
||||
App->>Leader: "Spieler bereit?"
|
||||
Leader->>App: Ping alle Knoten → OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Spielablauf (Game Loop)
|
||||
|
||||
### Phase A: Vorbereitung
|
||||
* **Leader:** Sendet Multicast `GAME_STATE_LOBBY`.
|
||||
* **Geräte:** Spielen Idle-Animation ab, warten auf Start.
|
||||
* **Check:** Leader kann "Ping" an alle senden, um Anwesenheit zu prüfen.
|
||||
|
||||
### Phase B: Countdown
|
||||
* **Leader:** Sendet Multicast `GAME_START_COUNTDOWN` (Payload: 10 sek).
|
||||
* **Westen:** Zählen laut herunter: "10, 9, 8...".
|
||||
|
||||
### Phase C: Spiel läuft (Running)
|
||||
* **Status:** `GAME_STATE_RUNNING`.
|
||||
* **Aktion:** Waffen sind entsperrt. Sensoren sind scharf.
|
||||
* **Treffer-Logik (Dezentral):**
|
||||
1. Waffe A schießt (sendet IR-Frame mit `Type=Hit`, `ShooterID`, `Damage`, `CRC8`).
|
||||
2. Weste B empfängt IR-Signal über TSOP4838, validiert CRC.
|
||||
3. Weste B berechnet Schaden (unter Berücksichtigung von Trefferzone-Multiplikator).
|
||||
4. Weste B zieht Lebenspunkte ab.
|
||||
5. **Feedback:** Weste B leuchtet/vibriert/spielt Sound ("Ugh!").
|
||||
6. **Speicherung:** Weste B speichert den Treffer im internen Flash-Log (`Timestamp, ShooterID, Zone, Damage`).
|
||||
7. *(Optional)* Weste B sendet UDP-Paket an Leader für Live-Scoreboard (Best Effort).
|
||||
|
||||
!!! info "Warum kein MilesTag2?"
|
||||
MilesTag2 wurde als Basis erwogen, ist aber mit ~40 ms Frame-Zeit und starren 8-Bit-IDs zu langsam und unflexibel. Unser Custom-Protokoll bietet:
|
||||
|
||||
- **Kürzere Frames:** ~36 ms vs. ~40 ms (weniger anfällig für Zittern/Bewegung)
|
||||
- **Flexible Type-Codes:** Hit/Heal/PowerUp/Admin in einem Format
|
||||
- **CRC8-Prüfung:** >99.5% Fehlerrate-Erkennung bei Sonnenlicht
|
||||
- **Variable Daten:** 13-Bit-Payload anpassbar pro Type
|
||||
|
||||
Details siehe [IR-Protokoll-Spezifikation](../specifications/ir_protocol.md).
|
||||
* **Heilquellen:** Medic/Medipack-IR (breit gestreut, kurze Reichweite, negativer Damage) werden als Heilung interpretiert.
|
||||
* **Zonen-Effekte:** Bases/Joiner senden `game/zone` (Link-Local, Hop=1); Weste prüft RSSI-Schwelle und addiert HP-Deltas (friend/foe) nach optionalem Warn-Countdown.
|
||||
|
||||
### Phase D: Spieler eliminiert
|
||||
* **Bedingung:** Lebenspunkte <= 0.
|
||||
* **Weste B:**
|
||||
* Spielt "Dead"-Sound.
|
||||
* Leuchtet dauerhaft in Teamfarbe (oder aus).
|
||||
* Sendet CoAP Unicast an **eigene Waffe**: `CMD_DISABLE`.
|
||||
* **Respawn (falls aktiv):**
|
||||
* Nach Zeitablauf (z.B. 30s) sendet Weste an Waffe: `CMD_ENABLE`.
|
||||
* Lebenspunkte werden zurückgesetzt.
|
||||
|
||||
### Phase E: Spielende & Auswertung
|
||||
* **Leader:** Sendet Multicast `GAME_STATE_FINISHED`.
|
||||
* **Ablauf:**
|
||||
1. Alle Spieler kommen zusammen.
|
||||
2. Spielleiter drückt in App "Daten abrufen".
|
||||
3. Leader Box fragt nacheinander (Unicast) alle bekannten Westen ab: `GET /game/log`.
|
||||
4. Westen übertragen ihre Treffer-Historie.
|
||||
5. App berechnet Highscores, MVP, Trefferquoten.
|
||||
|
||||
### Game Loop – Zustandsautomat (Weste)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Idle
|
||||
|
||||
Idle --> Lobby: GAME_STATE_LOBBY empfangen
|
||||
Lobby --> Countdown: GAME_START_COUNTDOWN empfangen
|
||||
Countdown --> Running: Countdown = 0
|
||||
|
||||
Running --> Running: IR-Hit empfangen → Damage abzug, Log
|
||||
Running --> Dead: Health <= 0
|
||||
|
||||
Dead --> Dead: Waffe CMD_DISABLE senden
|
||||
Dead --> Running: Respawn aktiviert (CMD_ENABLE)
|
||||
|
||||
Running --> Finished: GAME_STATE_FINISHED
|
||||
Dead --> Finished: GAME_STATE_FINISHED
|
||||
|
||||
Finished --> [*]
|
||||
|
||||
note right of Running
|
||||
- Health-Tracking
|
||||
- Treffer-Log
|
||||
- LED/Audio-Feedback
|
||||
end note
|
||||
|
||||
note right of Dead
|
||||
- Waffe gesperrt
|
||||
- LED dauerhaft an
|
||||
- Optional Respawn-Timer
|
||||
end note
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Spielmodi
|
||||
|
||||
Die Logik für die Modi wird primär auf den Westen implementiert (Regelwerk), gesteuert durch Flags vom Leader.
|
||||
|
||||
### Team Deathmatch
|
||||
* Klassisch Rot gegen Blau.
|
||||
* Friendly Fire konfigurierbar (an/aus).
|
||||
* Siegbedingung: Meiste Kills oder wenigste Tode nach Zeitablauf.
|
||||
|
||||
### Last Man Standing (Free-for-all)
|
||||
* Jeder gegen Jeden.
|
||||
* Keine Teams (oder jeder hat eigene Team-ID).
|
||||
* Kein Respawn.
|
||||
|
||||
### Zombie (Infected)
|
||||
* **Start:** 1 Spieler ist "Zombie" (Team Grün), Rest "Mensch" (Team Rot).
|
||||
* **Regel:**
|
||||
* Zombie hat unendlich Leben (oder sehr viel).
|
||||
* Mensch hat 1 Leben.
|
||||
* Wird Mensch getroffen -> Wechselt Team zu Zombie (Weste leuchtet grün, Waffe sendet ab jetzt Zombie-ID).
|
||||
* Wird Zombie getroffen -> "Stunned" (Waffe 5s gesperrt).
|
||||
* **Ziel:** Überleben bis Zeitablauf.
|
||||
|
||||
### Base Domination
|
||||
* **Hardware:** Leader-Boxen im Modus `11` (Base) verteilt im Gelände.
|
||||
* **Ablauf:**
|
||||
* Spieler schießt auf Base-Box.
|
||||
* Base-Box wechselt Farbe zu Teamfarbe des Schützen.
|
||||
* Base-Box zählt Zeit für das haltende Team.
|
||||
* Am Ende fragt Leader alle Base-Boxen ab: "Wie lange warst du Rot? Wie lange Blau?".
|
||||
|
||||
### Medic & Heal Packs
|
||||
* **Rollen:**
|
||||
* **Medic-Spieler:** Rolle `Medic` in der Spieler-Konfiguration; deren Waffe feuert ein "Heal-IR" mit breiter Streuung und sehr kurzer Reichweite.
|
||||
* **Medipacks (Objekte):** Aktiv durch Tastendruck; senden erst nach Button-Press ein Heal-IR mit breiter Streuung.
|
||||
* **Wirkung:**
|
||||
* Heal-IR ist als eigene Damage-Class kodiert (negativer Schaden → Heilung).
|
||||
* Reichweite absichtlich klein (< 2–3 m) und stark gestreut, damit Heilen ein Positionierungs-Feature bleibt.
|
||||
* Treffer-Logik auf der Weste interpretiert diese Pakete als Heilung (+HP, begrenzt durch `health_max`).
|
||||
* **Balancing:**
|
||||
* Heal pro Tick konfigurierbar; zusätzlich wählbar: max. Anzahl Heilungen, Mindest-Pause zwischen Heilungen oder beides.
|
||||
* Friendly-only oder auch Self-Heal konfigurierbar.
|
||||
* Hardware: Kann identisch zur Waffe sein (Trigger/Taster, Audio). Beispiele: "nääääääään" bei Cooldown, "Sorry, I am empty" wenn Magazin leer ist.
|
||||
|
||||
### Zonen-Effekte (Bases/Joiner)
|
||||
* **Joiner/Base-Knoten** können zusätzlich periodisch CoAP-Pakete auf Ressource `zone` senden (Link-Local, Hop-Limit=1).
|
||||
* **Anwendungsfälle:**
|
||||
* **Heal-Zonen:** In Team-Homebase oder dominierten Bases → Team-Mitglieder werden geheilt, Gegner ggf. geschwächt.
|
||||
* **Kill-/Radiation-Zonen:** Schadenszonen mit Warn-Countdown; Gegner müssen den Bereich verlassen.
|
||||
* **Respawn-Zonen:** Spielmodi, in denen tote Spieler nur in der eigenen Home-Zone oder einer dominierten Base respawnen dürfen; die Zone validiert Präsenz (RSSI) und erlaubt dann Respawn.
|
||||
* **Paketfelder (Beispiel):** `team: 2, friend: +20, foe: -10, rssi: -70, warn: 5`
|
||||
* `team`: Besitzer der Zone (z.B. 2 = Blau)
|
||||
* `friend`: HP-Delta für eigenes Team (+20 Heal)
|
||||
* `foe`: HP-Delta für andere Teams (-10 Schaden)
|
||||
* `rssi`: Mindest-RSSI (dBm) für Wirksamkeit (z.B. -70 → nur nahe dran)
|
||||
* `warn`: Anzahl Aussendungen als Warnung, bevor der Effekt scharf wird
|
||||
* **Instant-Death Beispiel:** `team: 0, friend: -128, foe: -128, warn: 0, rssi: -60` → Jeder Empfänger mit RSSI besser als -60 dBm fällt sofort auf 0 HP.
|
||||
|
||||
---
|
||||
|
||||
## 5. Technische Spezifikation (API & Datenstrukturen)
|
||||
|
||||
Die Kommunikation erfolgt über CoAP (UDP). Alle Payloads sind binär (`__packed` C-Structs, Little Endian für nRF52, Network Byte Order für Interop optional).
|
||||
|
||||
### 5.1 CoAP Endpunkte
|
||||
|
||||
| Ressource | Methode | Typ | Beschreibung | Payload |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `game/state` | PUT | Multicast | Globaler Spielstatus (Start/Stop/Zeit) | `struct game_state_packet` |
|
||||
| `game/conf` | PUT | Unicast | Konfiguration für einen Spieler | `struct player_config_packet` |
|
||||
| `game/wconf` | PUT | Unicast | Konfiguration für eine Waffe | `struct weapon_config_packet` |
|
||||
| `game/hit` | POST | Unicast | Treffer-Meldung (Live-Ticker) | `struct hit_report_packet` |
|
||||
| `game/log` | GET | Unicast | Abruf der gespeicherten Trefferdaten | `struct flash_log_entry[]` |
|
||||
| `game/zone` | PUT | Multicast (Link-Local, Hop=1) | Zonen-Effekte (Heal/Kill) aus Bases/Joinern | `struct zone_effect_packet` |
|
||||
|
||||
| `game/powerup` | PUT | Unicast | Power-Up-Event von Weste zu Waffe | `struct powerup_event_packet` |
|
||||
| `game/pup_log` | GET | Unicast | Abruf aller Power-Up-Aktivierungen | `struct powerup_log_entry[]` |
|
||||
|
||||
### 5.2 Datenstrukturen
|
||||
|
||||
#### Spielstatus (Multicast)
|
||||
```c
|
||||
struct game_state_packet {
|
||||
uint8_t state; // 0=Idle, 1=Lobby, 2=Running, 3=Paused, 4=Finished
|
||||
uint8_t game_mode; // 0=TeamDeathmatch, 1=Zombie, 2=Base
|
||||
uint16_t game_id; // Rolling Counter zur Deduplizierung
|
||||
uint16_t remaining_sec; // Restzeit in Sekunden
|
||||
uint8_t flags; // Bitmaske (z.B. FriendlyFire)
|
||||
} __packed;
|
||||
```
|
||||
|
||||
#### Spieler-Konfiguration (Provisioning)
|
||||
```c
|
||||
struct player_config_packet {
|
||||
uint16_t player_id; // Logische ID (1-65535)
|
||||
uint8_t team_id; // 0=Rot, 1=Blau, 2=Grün (Zombie), ...
|
||||
uint8_t role; // 0=Soldier, 1=Medic, 2=Sniper
|
||||
uint8_t damage_out; // Basis-Schaden der Waffe
|
||||
uint8_t health_max; // Maximale Lebenspunkte
|
||||
char name[16]; // Anzeigename (null-terminated)
|
||||
} __packed;
|
||||
```
|
||||
|
||||
#### Waffen-Konfiguration
|
||||
```c
|
||||
struct weapon_config_packet {
|
||||
uint8_t base_damage; // Schaden pro Schuss
|
||||
uint16_t reload_time_ms;// Zeit für Nachladen
|
||||
uint8_t magazine_size; // Schuss pro Magazin
|
||||
} __packed;
|
||||
```
|
||||
|
||||
#### Treffer-Bericht (Live & Log)
|
||||
```c
|
||||
struct hit_report_packet {
|
||||
uint32_t timestamp; // ms seit Spielstart
|
||||
uint16_t shooter_id; // ID des Schützen (aus IR)
|
||||
uint16_t victim_id; // Eigene ID
|
||||
uint8_t damage; // Erlittener Schaden
|
||||
uint8_t hit_location; // 0=Unbekannt, 1=Kopf, 2=Brust, 3=Rücken
|
||||
} __packed;
|
||||
|
||||
// Zonen-Effekte (link-local, Hop-Limit=1)
|
||||
struct zone_effect_packet {
|
||||
uint8_t team_id; // Besitzer der Zone (0=neutral)
|
||||
int8_t friend_delta; // HP-Delta für eigenes Team (z.B. +20 Heal)
|
||||
int8_t foe_delta; // HP-Delta für andere Teams (z.B. -10 Schaden)
|
||||
int8_t rssi_thresh_dbm; // Mindest-RSSI für Wirksamkeit (z.B. -70 dBm)
|
||||
uint8_t warn_count; // Anzahl Warn-Pakete vor scharfem Effekt
|
||||
} __packed;
|
||||
|
||||
// Power-Up Event (von Weste zu Waffe nach IR-Empfang)
|
||||
struct powerup_event_packet {
|
||||
uint16_t player_id; // Betroffener Spieler
|
||||
uint8_t powerup_type; // 0=SHIELD, 1=DAMAGE_BOOST, 2=HEAL, 3=SPEED, etc.
|
||||
uint16_t duration_sec; // Dauer des Power-Ups in Sekunden
|
||||
uint8_t persist_on_death; // 0=verfällt bei Death, 1=bleibt auch nach Respawn
|
||||
uint16_t damage_multiplier;// Für Damage-Boost: z.B. 150 = 1.5x
|
||||
int8_t health_delta; // Für Heal-Pack: z.B. +50
|
||||
} __packed;
|
||||
|
||||
// Power-Up Log Entry (für Auswertung nach Spielende)
|
||||
struct powerup_log_entry {
|
||||
uint32_t timestamp; // ms seit Spielstart
|
||||
uint16_t player_id; // Spieler, der Powerup aktiviert hat
|
||||
uint8_t powerup_type; // Type des PU
|
||||
uint8_t source; // 0=IR-Station, 1=Buzzer-Box, 2=Leader-Kommando, 3=Zone
|
||||
} __packed;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. IR-Protokoll (Physical Layer)
|
||||
|
||||
Das IR-Signal nutzt eine 38kHz Trägerfrequenz (NEC-ähnlich).
|
||||
|
||||
### 6.1 Standard-Shooting-Payload (32-bit)
|
||||
|
||||
Payloads von Waffen und Medics (Protokoll-ID `0xAA`):
|
||||
|
||||
* Bits 24-31: `Protokoll-ID` (`0xAA` für Standard-Spiel-IRs)
|
||||
* Bits 8-23: `Shooter ID` (16-bit, eindeutig für jeden Spieler)
|
||||
* Bits 4-7: `Team-Code` (4-bit, 0=Rot, 1=Blau, 2=Grün, 3=Neutral)
|
||||
* Bits 0-3: `Damage Class` (4-bit, 0-15, definiert Basis-Schaden; 0xFF = Healing)
|
||||
|
||||
**Beispiel:** ShooterID=42, Team=Rot, Damage=10 → `0xAA002A0A`
|
||||
|
||||
### 6.2 Power-Up-Payload (32-bit)
|
||||
|
||||
Payloads von Power-Up-Stationen und Buzzer-Boxen (Protokoll-ID `0xBB`):
|
||||
|
||||
* Bits 24-31: `Protokoll-ID` (`0xBB` für Power-Up-Signale)
|
||||
* Bits 16-23: `Power-Up-Type` (0=SHIELD, 1=DAMAGE_BOOST, 2=HEAL_PACK, 3=SPEED, 4=WEAKNESS, 5=AMMO, etc.)
|
||||
* Bits 0-15: `Power-Up-Flags` (optional: Duration-Encoding, Persist-Flag, etc.)
|
||||
|
||||
**Beispiel (Shield):** `0xBB000000` (Duration wird in CoAP-Paket übertragen)
|
||||
|
||||
---
|
||||
|
||||
## 7. Power-Ups & Items (erweitert)
|
||||
|
||||
### Hardw are-Seite
|
||||
* **Power-Up-Stationen (Deckel-Boxen):** nRF52840 + Magnetschalter + IR-Sender + Status-LED; Cooldown intern gesteuert (z.B. 30s).
|
||||
* **Buzzer-Power-Ups:** nRF52840 + Druckschalter + schwacher IR-Sender (2-3m) + LED; optional Thread-Logging.
|
||||
* **Beide Typen können zentral über Leader konfiguriert werden.**
|
||||
|
||||
### Software-Seite
|
||||
* **IR-Dekodierung:** Weste prüft Protokoll-ID (`0xAA` vs. `0xBB`) und ruft Handler auf.
|
||||
* **Weste:** Speichert aktive Power-Ups lokal (Typ, Duration, Multiplier); sendet via `game/powerup` Unicast zu Waffe; prüft `persist_on_death` bei Respawn.
|
||||
* **Waffe:** Empfängt `game/powerup`, appliziert Damage-Multiplier auf nächste Schüsse.
|
||||
* **Power-Up-Typen:**
|
||||
- SHIELD: Blockiert 1 Treffer
|
||||
- DAMAGE_BOOST: Multipliziert ausgehenden Schaden (z.B. 1.5x)
|
||||
- HEAL_PACK: Instant +HP
|
||||
- SPEED: Schneller schießen (Cooldown-Reducer)
|
||||
- WEAKNESS: Negativ, mehr Schaden empfangen
|
||||
- AMMO: Medipack-Magazine aufladen
|
||||
* **Leader-Kommandos:** Kann zentrale Power-Ups auslösen (Underdog-Bonus, Base-Schuss-Trigger, Turbo-Round) → Multicast/Unicast.
|
||||
* **Logging:** Alle Power-Up-Aktivierungen in Flash-Log speichern, abrufbar via `game/pup_log` nach Spielende.
|
||||
|
||||
## 8. Weste – Zustandsautomat (detailliert)
|
||||
|
||||
### Übergangstabelle
|
||||
|
||||
| Von | Nach | Auslöser | Aktion | Bedingung |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| Idle | Lobby | CoAP `GAME_STATE_LOBBY` | LED idle-Animation, warten auf Start | Multicast vom Leader |
|
||||
| Lobby | Countdown | CoAP `GAME_START_COUNTDOWN` | Audio-Countdown 10→1 sec, Countdown-Timer init | Payload: 10 sek |
|
||||
| Countdown | Running | Countdown = 0 | Health reset, Treffer-Sensor aktivieren, Waffe unlock | Timer lokal abgelaufen |
|
||||
| Running | Dead | Health <= 0 | LED rot/aus, Dead-Sound, CoAP CMD_DISABLE an Waffe | Nach IR-Hit-Verarbeitung |
|
||||
| Dead | Running | Respawn-Timer = 0 | Health reset, CoAP CMD_ENABLE an Waffe, Sensor on | Optional; Config-abhängig |
|
||||
| Running | Finished | CoAP `GAME_STATE_FINISHED` | Alle Daten sichern, Logs speichern, LED Idle | Multicast vom Leader |
|
||||
| Dead | Finished | CoAP `GAME_STATE_FINISHED` | Logs speichern (optional), LED Idle | Multicast vom Leader |
|
||||
|
||||
### Treffer-Verarbeitung im Running-State (Flowchart)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["IR-Signal empfangen"] --> B{"ShooterID prüfen"}
|
||||
B -->|Friendly Fire off + eigenes Team| C["Signal verwerfen"]
|
||||
B -->|Valid| D["Hit Location extrahieren"]
|
||||
D --> E["Zone-Multiplikator anwenden"]
|
||||
E --> F["Effektiven Schaden berechnen"]
|
||||
F --> G["Health -= Damage"]
|
||||
G --> H{"Health > 0?"}
|
||||
H -->|Ja| I["LED blinken + Sound"]
|
||||
I --> J["Hit ins Flash-Log schreiben"]
|
||||
J --> K["Optional: CoAP Hit-Report an Leader"]
|
||||
H -->|Nein| L["Health = 0"]
|
||||
L --> M["LED rot, Dead-Sound"]
|
||||
M --> N["CoAP CMD_DISABLE an Waffe"]
|
||||
N --> O["Zustand → Dead"]
|
||||
```
|
||||
|
||||
### Übergangsbeschreibungen
|
||||
|
||||
- **Idle → Lobby:** App oder Spielleiter triggert über BLE. Leader sendet `GAME_STATE_LOBBY` Multicast. Weste wechselt in Warte-Modus.
|
||||
- **Lobby → Countdown:** Leader sendet `GAME_START_COUNTDOWN` mit Sekunden-Payload (z.B. 10). Weste zählt laut herunter.
|
||||
- **Countdown → Running:** Timer lokal = 0 → Weste aktiviert Sensoren, Health voll, Waffe freigegeben.
|
||||
- **Running → Dead:** Health <= 0 nach Treffer-Verarbeitung → Waffe gesperrt, LED rot/aus, Respawn-Timer optional starten.
|
||||
- **Dead → Running:** Optional nur wenn Respawn aktiv. Leader schickt evtl. Kommando oder Timer läuft ab.
|
||||
- **{Running|Dead} → Finished:** Leader sendet `GAME_STATE_FINISHED` → Weste speichert finale Logs, bereit für Auswertung.
|
||||
|
||||
## 9. Offene Punkte & Annahmen
|
||||
|
||||
* Sicherheitsmodell: Keine Auth auf IR, minimale Auth/ACL auf BLE/Thread? (noch zu klären).
|
||||
* Anti-Cheat: Debounce IR, Rate-Limit pro Waffe, Rolling Codes möglich.
|
||||
* Telemetrie: Sampling-Intervall für Live-Ticker und Limit pro Sekunde definieren.
|
||||
@@ -1,3 +1,20 @@
|
||||
# Eriks Lasertag
|
||||
|
||||
Das hier beschreibt das ganze Lasertag-Gedöns.
|
||||
Kurzer Überblick über das DIY-Lasertag-System auf Basis von nRF52840, Thread und BLE.
|
||||
|
||||
**Was dich hier erwartet**
|
||||
|
||||
- Architektur & Spielablauf: siehe Konzept Software.
|
||||
- Hardware-Ideen: Waffe, Weste, Leader, Stromversorgung.
|
||||
- Roadmap & To-Dos: grobe Phasen und Module.
|
||||
|
||||
**Schnelleinstieg**
|
||||
|
||||
- Konzept Software: Rolle Leader/Weste/Waffe, Game Loop, CoAP-API.
|
||||
- Konzept Hardware: Aufbau der Einheiten, LED-Treiber, Akku-Setup.
|
||||
- Gameplay & Modi: [concepts/gameplay.md](concepts/gameplay.md) – Kurz erklärt, Rollen, Power-Ups und Spielmodi.
|
||||
- Planung: Zephyr-Workspace-Struktur und Roadmap.
|
||||
|
||||
**Lizenz**
|
||||
|
||||
- Inhalte stehen unter CC BY-NC-SA 4.0. Details unter [license.md](license.md).
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# Hardware-Konzept
|
||||
|
||||
## 1. Systemübersicht
|
||||
|
||||
Das Lasertag-System besteht aus einer hierarchischen Architektur, bei der ein Leader-Element mehrere Westeneinheiten mit zugeordneten Waffensystemen steuert.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Leader((Leader))
|
||||
|
||||
Leader --> Weste1[Weste 1]
|
||||
Leader --> Weste2[Weste 2]
|
||||
|
||||
Weste1 --> WaffeA(Waffe A)
|
||||
Weste1 --> WaffeB(Waffe B)
|
||||
|
||||
Weste2 --> WaffeC(Waffe C)
|
||||
|
||||
%% Styling (Optional)
|
||||
%% style Leader fill:#f96,stroke:#333,stroke-width:2px
|
||||
%% style Weste1 fill:#bbf,stroke:#333
|
||||
%% style Weste2 fill:#bbf,stroke:#333 -->
|
||||
```
|
||||
## 2. Schaltungskomponenten
|
||||
|
||||
### 2.1 LED-Treiber
|
||||
|
||||
#### Grundkonzept
|
||||
|
||||
Der LED-Treiber realisiert eine präzise Konstantstromquelle als Hybridschaltung aus PNP- und NPN-Transistoren. Diese Architektur ermöglicht eine stabile und effiziente Ansteuerung von Infrarot-Leuchtdioden mit definierten Stromwerten.
|
||||
|
||||

|
||||
|
||||
#### Stromeinstelling
|
||||
|
||||
Der Zielstrom wird über den Messwiderstand $R_{set}$ eingestellt. Die Berechnung folgt der Formel:
|
||||
|
||||
$$R_{set} = \frac{0,65V}{I_{LED}}$$
|
||||
|
||||
Die folgenden Tabelle zeigt typische Stromwerte, die erforderlichen Widerstände und die entsprechenden Anwendungsfälle:
|
||||
|
||||
| Stromstärke ($I_{LED}$) | Widerstand ($R_{set}$) | Ausgangsleistung ($P_{min}$) | Einsatzbereich |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 0,5 A | 1,30 $\Omega$ | 0,5 W | Standard / Nahkampf |
|
||||
| 1,0 A | 0,65 $\Omega$ | 1,0 W | Hohe Reichweite (SFH 4550) |
|
||||
| 2,0 A | 0,33 $\Omega$ | 2,0 W | Extrem hohe Leistung (Pulsbetrieb) |
|
||||
| 3,0 A | 0,22 $\Omega$ | 3,0 W | Scharfschützen-Modus (Oslon Black) |
|
||||
|
||||
#### Thermische Betrachtung
|
||||
|
||||
Im Lasertag-Betrieb werden die Infrarot-Signale hochfrequent moduliert (beispielsweise mit 38 kHz). Dies führt zu einem signifikant geringeren mittleren Wärmeeintrag in den Messwiderstand als eine kontinuierliche Strombelastung suggeriert:
|
||||
|
||||
$$P_{avg} = (R_{set} \cdot I_{LED}^2) \cdot \text{Duty Cycle}$$
|
||||
|
||||
!!! info "Bedeutung der Widerstandsspezifikation"
|
||||
Obwohl die Duty-Cycle-Modulation die durchschnittliche Verlustleistung reduziert, müssen Widerstände für $R_{set}$ für die auftretenden Stromspitzen ausgelegt sein. Wir empfehlen impulsfeste Typen (Metallschicht- oder Drahtwiderständen), um die Stromspitzen bis zu 3 A ohne Materialermüdung zu verkraften.
|
||||
|
||||
#### Spannungsversorgung und Headroom-Anforderungen
|
||||
|
||||
Die Konstantstromquelle benötigt eine Mindestverspannung zwischen Versorgung und Ausgang, um präzise die Sollstromstärke zu halten. Diese sogenannte Headroom-Spannung errechnet sich aus:
|
||||
|
||||
$$V_{CC} > V_{f(\text{LED})} + 0,65V + 1,0V_{\text{Headroom}}$$
|
||||
|
||||
**Kritischer Aspekt bei Lithium-Ionen-Akkus:** Die Akkuspannung sinkt während der Entladung kontinuierlich. Unterschreitet $V_{CC}$ den erforderlichen Schwellwert, bricht die Regelung zusammen und der LED-Strom kann die Sollvorgabe nicht mehr erreichen. Dies führt zu einer drastischen Reduktion der Reichweite des Senders.
|
||||
|
||||
Die Minimalspannung für stabilen Betrieb wird bestimmt durch:
|
||||
|
||||
$$V_{CC,\text{min}} = V_{f(\text{LED})} + V_{R_{set}} + V_{\text{Headroom}}$$
|
||||
|
||||
Die folgende Tabelle zeigt die erforderlichen Minimalspannungen für verschiedene Stromvorgaben und die Eignung unterschiedlicher Akkusysteme:
|
||||
|
||||
| Stromstärke ($I_{LED}$) | Typ. LED-Spannung ($V_{f}$) | Erforderliche Spannung ($V_{CC,\text{min}}$) | Akku-Empfehlung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 0,5 A | ~2,0 V | 3,65 V | 1S (nur bei voller Ladung) |
|
||||
| 1,0 A | ~2,4 V | 4,05 V | 2S empfohlen |
|
||||
| 2,0 A | ~2,8 V | 4,45 V | 2S erforderlich |
|
||||
| 3,0 A | ~3,2 V | 4,85 V | 2S erforderlich |
|
||||
|
||||
!!! warning "1S-System: Einschränkungen unter Last"
|
||||
Ein einzelner 1S Li-Po Akku sinkt unter hohen Stromlasten schnell auf 3,4 V bis 3,6 V ab. Für konstante Reichweite bei Strömen ab 1 A ist ein 2S-System daher technisch überlegen.
|
||||
|
||||
#### 2S-Akkusystem: Komplexität und Lösungsansätze
|
||||
|
||||
Ein 2S-Akkusystem bietet zwar Spannungsstabilität, erfordert jedoch anspruchsvollere Schutz- und Überwachungsfunktionen:
|
||||
|
||||
- **Laden:** Moderne 2S-Ladechips mit integriertem Balancing (beispielsweise der IP2326) ermöglichen vereinfachte Ladevorgänge.
|
||||
- **Zellenschutz:** Ein Zellenschutz-IC wie der HY2120-CB in Kombination mit einem Dual-Channel MOSFET (beispielsweise FS8205A) verhindert Über- und Unterspannungszustände.
|
||||
- **Fuel Gauge:** Spezielle Fuel-Gauge-ICs für 2S-Systeme sind selten oder komplex. Als praktische Alternative wird eine Spannungsteiler-ADC-Messung zur Ladezustandsabschätzung eingesetzt. Der Spannungsteiler muss durch ein Schaltgattersystem steuerbar sein.
|
||||
|
||||
### 2.2 Akku-Überwachung (Fuel Gauge)
|
||||
|
||||
#### Spannungsmessung bei 2S-Akkus
|
||||
|
||||
Für 2S-Akkusysteme mit Spannungen bis 8,4 V wird die Akkuspannung über einen Spannungsteiler auf den ADC-Eingangspegel reduziert. Dieser Messwert dient zur Ladezustandsabschätzung und Fehlerdiagnose.
|
||||
|
||||
#### Schaltungskomponenten
|
||||
|
||||
| Komponente | Wert | Funktion |
|
||||
| :--- | :--- | :--- |
|
||||
| $R_1$ | 100 k$\Omega$ | Spannungsteiler – oberer Zweig |
|
||||
| $R_2$ | 47 k$\Omega$ | Spannungsteiler – unterer Zweig |
|
||||
| $C_1$ | 100 nF | Glättungskondensator am ADC-Eingang |
|
||||
|
||||
#### Softwarelogik
|
||||
|
||||
Die Ladezustandsbestimmung erfolgt in drei Schritten:
|
||||
|
||||
1. **ADC-Konvertierung:** Der Rohwert des ADC-Eingangs wird eingelesen.
|
||||
|
||||
2. **Spannungsrückrechnung:** Die Realspannung wird aus dem ADC-Wert berechnet:
|
||||
$V_{\text{bat}} = V_{\text{adc}} \cdot \frac{R_1 + R_2}{R_2}$
|
||||
|
||||
3. **Ladezustand-Mapping:** Die Batteriespannung wird auf einen prozentualen Ladezustand abgebildet:
|
||||
|
||||
- **6,0 V** → 0 % (Entladungsschutz aktiv)
|
||||
- **8,4 V** → 100 % (vollständig geladen)
|
||||
|
||||
Für höhere Genauigkeit können mehrere Messpunkte verwendet und linear interpoliert werden.
|
||||
9
doc/docs/license.md
Normal file
9
doc/docs/license.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Lizenz
|
||||
|
||||
Dieses Projekt steht unter der Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
|
||||
|
||||
- Kurzfassung: [creativecommons.org/licenses/by-nc-sa/4.0/](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
||||
- Volltext: [creativecommons.org/licenses/by-nc-sa/4.0/legalcode](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode)
|
||||
- Repo-Lizenzdatei: [gitea.iten.pro/edi/lasertag/src/branch/main/LICENSE](https://gitea.iten.pro/edi/lasertag/src/branch/main/LICENSE)
|
||||
|
||||
Nutzung zu kommerziellen Zwecken ist nicht gestattet. Bearbeitungen muessen gleichartig geteilt werden und eine Namensnennung erfordern.
|
||||
@@ -29,25 +29,52 @@ lasertag/
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1: Foundation (Struktur & Shell)
|
||||
- [ ] Verzeichnisstruktur und `CMakeLists.txt` Verknüpfungen erstellen.
|
||||
- [ ] Basis-Firmware mit **Zephyr Shell** zur Konfiguration.
|
||||
- [ ] Implementierung von NVS (Non-Volatile Storage) zum Speichern von Gerätenamen und Team-IDs.
|
||||
### Entwicklungs-Roadmap (Schritt-für-Schritt)
|
||||
|
||||
### Phase 2: Connectivity (BLE & Thread)
|
||||
- [ ] **BLE-Provisioning:** Übertragung von Thread-Credentials via Bluetooth.
|
||||
- [ ] **Thread-Setup:** Aufbau eines stabilen Mesh-Netzwerks zwischen Leader und Westen.
|
||||
- [ ] **Web-Interface:** Verbindung der Chrome Web Bluetooth App mit dem Leader.
|
||||
Diese Roadmap führt vom nRF52840DK bis zum fertigen Produkt.
|
||||
|
||||
### Phase 3: Core Game Logic (CoAP Brücke)
|
||||
- [ ] **Multicast-Broadcast:** "Spiel Start"-Kommando von Web-App -> Leader -> Thread-Mesh.
|
||||
- [ ] **Unicast-Feedback:** Treffermeldung von Weste -> Leader -> Web-App.
|
||||
- [ ] **Reliability:** Implementierung von CoAP Confirmable Messages für kritische Befehle (z. B. Waffe deaktivieren).
|
||||
#### Phase 1: Die "Tisch"-Basis (PoC)
|
||||
**Ziel:** Stabile Thread-Kommunikation und IR-Signalerzeugung verifizieren.
|
||||
|
||||
### Phase 4: Erweitert (Bases & NFC)
|
||||
- [ ] **Rollen-Switch:** Leader-Hardware erkennt via GPIO (Schalter), ob sie als Base agiert.
|
||||
- [ ] **Capture the Base:** Logik für Teambesitz und Zeitmessung.
|
||||
- [ ] **NFC-Provisioning:** (Optional) Schnelles Zuweisen von IDs via Smartphone-NFC.
|
||||
- [ ] Zephyr Setup: Installation des nRF Connect SDK (NCS) und VS Code.
|
||||
- [ ] Custom Board Definition: Board-File anlegen, das die Pins des nRF52840DK auf die geplanten Funktionen mappt (PWM für IR, GPIO für Buttons).
|
||||
- [ ] Thread Mesh: Minimalen OpenThread-Stack aufsetzen. Ein DK als Leader (FTD), einer als Child. UDP/CoAP-Ping bei Knopfdruck.
|
||||
- [ ] IR-Engine: Custom IR-Protokoll (pulse-distance, 38 kHz) mit nrfx_pwm + PPI implementieren. Signal mit Oszilloskop/Logic Analyzer verifizieren. Sicherstellen, dass Funk die IR-Engine nicht stört. **Hinweis:** MilesTag2 wurde verworfen zugunsten eines eigenen, kürzeren Protokolls mit CRC8 (siehe [Spezifikationen](specifications/ir_protocol.md)).
|
||||
|
||||
#### Phase 2: Der "Prototyp" (Integration)
|
||||
**Ziel:** Einbindung von Audio und Solenoid.
|
||||
|
||||
- [ ] Audio: MAX98357A am I2S-Interface. WAV-Player, der Samples aus internem Flash abspielt.
|
||||
- [ ] Solenoid-Treiber: MOSFET-Schaltung auf Breadboard. PWM-Logik für "Kick" (100% für 30ms) und "Hold" (30%). Thermik des Solenoids testen.
|
||||
- [ ] Haptik-Sync: Audio ("Bang!") und Solenoid-Kick in der Software synchronisieren.
|
||||
|
||||
#### Phase 3: Die "Optik & Sensorik" (Physik)
|
||||
**Ziel:** Reichweitentest.
|
||||
|
||||
- [ ] Linsen-Test: Oslon Black LED auf Star-Platine + Carclo/LEDiL-Linse justieren (Abstand exakt einhalten).
|
||||
- [ ] Sensor-Array: TSOP-Sensoren für die Weste verdrahten. Outdoor-Test bei Sonne. Software-Filterung anpassen, um Reflexions-Fehler zu minimieren.
|
||||
|
||||
#### Phase 4: Die "App & Logik" (System)
|
||||
**Ziel:** Spielsteuerung.
|
||||
|
||||
- [ ] BLE Gateway: Leader-Box sendet Thread-Statusdaten via BLE an Smartphone (Web Bluetooth API oder nRF Toolbox App).
|
||||
- [ ] Web App: Einfache HTML/JS-Seite, die via Web Bluetooth API mit dem Leader spricht, um ein Spiel zu starten ("Start Game" als Thread Broadcast).
|
||||
|
||||
#### Phase 5: Das "Custom PCB" (Hardware)
|
||||
**Ziel:** Miniaturisierung.
|
||||
|
||||
- [ ] Schaltplan: Schaltungen in KiCad übertragen. Trennung von Analog-GND (Sensoren) und Power-GND (Solenoid) beachten.
|
||||
- [ ] Layout: Antennenplatzierung/Impedanz anpassen; Testpunkte für SWD vorsehen (Debug).
|
||||
|
||||
### Grobe Zeitachsen (Richtwerte)
|
||||
|
||||
| Phase | Ziel | Dauer (Richtwert) | Abhängigkeiten |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 1 | Thread + IR PoC | 1–2 Wochen | HW: nRF52840DK, Logic-Analyzer |
|
||||
| 2 | Audio + Solenoid | 2 Wochen | Phase 1 abgeschlossen |
|
||||
| 3 | Optik & Sensorik | 1–2 Wochen | Phase 2 abgeschlossen, Outdoor-Tests |
|
||||
| 4 | App & Steuerung | 1–2 Wochen | Phase 2 abgeschlossen, BLE-Gateway vorhanden |
|
||||
| 5 | Custom PCB | 3–4 Wochen | Phasen 1–3 verifiziert, Schaltplan stabil |
|
||||
|
||||
## Technologie-Stack
|
||||
- **Hardware:** nRF52840 (DK & Custom PCBs)
|
||||
|
||||
188
doc/docs/specifications/ir_protocol.md
Normal file
188
doc/docs/specifications/ir_protocol.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# IR-Kommunikationsprotokoll
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Infrarot-Kommunikationsprotokoll basiert auf Pulse-Distance-Codierung mit 38 kHz Träger und ist ähnlich Sony SIRC, aber optimiert für die Anforderungen des Lasertag-Systems. Das Protokoll bietet robuste Übertragung mit CRC-Fehlerprüfung und kurzen Frame-Zeiten (~36 ms).
|
||||
|
||||
## Physikalische Schicht
|
||||
|
||||
| Parameter | Wert | Anmerkung |
|
||||
|-----------|------|----------|
|
||||
| Trägerfrequenz | 38 kHz | Standard für TSOP48xx Empfänger |
|
||||
| Tastgrad (Duty Cycle) | 50 % | Konfigurierbar (25–75 %) |
|
||||
| Modulation | PWM mit Pulse-Distance-Codierung | Hardware-basiert via nRF52 PWM-Peripheral |
|
||||
| Empfänger | TSOP4838 (kompatibel) | Active-Low Ausgang, 38 kHz Bandpass |
|
||||
|
||||
## Timing-Spezifikation
|
||||
|
||||
| Symbol | Dauer | Toleranz | Beschreibung |
|
||||
|--------|-------|----------|-------------|
|
||||
| **Start Burst** | 4 × Basistakt (Standard 2400 µs) | ±200 µs | Träger AN, Frame-Synchronisations-Impuls |
|
||||
| **Gap** | 1 × Basistakt (Standard 600 µs) | ±100 µs | Träger AUS nach Start (optional, konfigurierbar) |
|
||||
| **Mark** | 1 × Basistakt (Standard 600 µs) | ±100 µs | Träger AN (konstant für alle Bits) |
|
||||
| **Space 0** | 1 × Basistakt (Standard 600 µs) | ±100 µs | Träger AUS für logisch 0 |
|
||||
| **Space 1** | 2 × Basistakt (Standard 1200 µs) | ±150 µs | Träger AUS für logisch 1 |
|
||||
|
||||
**Basistakt:** `CONFIG_IR_PROTO_BASE_US` (Standard 600 µs). Alle Zeiten ergeben sich daraus per Multiplikatoren (`IR_PROTO_*_MULT`).
|
||||
|
||||
### Bit-Codierung
|
||||
|
||||
```
|
||||
Bit 0: [Mark 600µs] + [Space 600µs] = 1.2 ms
|
||||
Bit 1: [Mark 600µs] + [Space 1.2ms] = 1.8 ms
|
||||
```
|
||||
|
||||
### Beispiel-Wellenform (3 Bits: `101`)
|
||||
|
||||
**Träger-Timing für Bits 1-0-1:**
|
||||
|
||||
| Segment | Dauer | State | Bit-Wert |
|
||||
|---------|-------|-------|---------|
|
||||
| Mark | 600 µs | AN | |
|
||||
| Space 1 | 1200 µs | AUS | **1** (1.8 ms total) |
|
||||
| Mark | 600 µs | AN | |
|
||||
| Space 0 | 600 µs | AUS | **0** (1.2 ms total) |
|
||||
| Mark | 600 µs | AN | |
|
||||
| Space 1 | 1200 µs | AUS | **1** (1.8 ms total) |
|
||||
|
||||
## Frame-Format
|
||||
|
||||
Alle Frames bestehen aus 24 Bits, übertragen MSB-first:
|
||||
|
||||
title Frame
|
||||
| Feld | Start Burst + Gap | Type | Data | CRC8 |
|
||||
|------|-------------------|------|------|------|
|
||||
| **Dauer** | (Start: 4× Basis) + (Gap: 1× Basis) | 3 Bits | 13 Bits | 8 Bits |
|
||||
| **Funktion** | Synchronisation | Frame-Typ | Payload | Fehlerprüfung |
|
||||
| **Summe** | – | – | – | **24 Bits** |
|
||||
|
||||
**Gesamte Frame-Zeit:** ~39 ms bei Standardwerten (Start 2400 µs + Gap 600 µs + 24 × 1.5 ms Bit-Durchschnitt). Mit angepasstem Basistakt/Multi skaliert alles linear.
|
||||
|
||||
### Type-Feld (3 Bits)
|
||||
|
||||
| Wert | Typ | Beschreibung |
|
||||
|------|-----|-------------|
|
||||
| `000` | Hit | Standard-Schuss |
|
||||
| `001` | Heal | Medic-Heilung oder Health Pack |
|
||||
| `010` | PowerUp | Station Power-Up Grant |
|
||||
| `011` | Admin | System-Steuerbefehle |
|
||||
| `100`–`111` | Reserviert | Zukünftige Nutzung |
|
||||
|
||||
### Data-Feld (13 Bits) – Type-abhängig
|
||||
|
||||
#### Hit-Frame (`000`)
|
||||
|
||||
| Bit-Range | Feld | Wertbereich | Beschreibung |
|
||||
|-----------|------|-------------|-------------|
|
||||
| 0–7 | Shooter ID | 0–255 | ID des Schützen (256 mögliche Spieler) |
|
||||
| 8–12 | Damage | 0–31 | Schadenpunkte (0 = kein Schaden, 31 = Maximum) |
|
||||
|
||||
#### Heal-Frame (`001`)
|
||||
|
||||
| Bit-Range | Feld | Wertbereich | Beschreibung |
|
||||
|-----------|------|-------------|-------------|
|
||||
| 0–7 | Healer ID | 0–255 | ID des Heilers (Medic oder Station) |
|
||||
| 8–12 | Amount | 0–31 | Heilpunkte wiederhergestellt |
|
||||
|
||||
#### PowerUp-Frame (`010`)
|
||||
|
||||
| Bit-Range | Feld | Wertbereich | Beschreibung |
|
||||
|-----------|------|-------------|-------------|
|
||||
| 0–7 | Station ID | 0–255 | Stations-ID, die das Power-Up gewährt |
|
||||
| 8–12 | PowerUp | 0–31 | Power-Up-Typ-Identifier |
|
||||
|
||||
#### Admin-Frame (`011`)
|
||||
|
||||
| Bit-Range | Feld | Wertbereich | Beschreibung |
|
||||
|-----------|------|-------------|-------------|
|
||||
| 0–12 | Command Data | 0–8191 | Implementierungsdefinierte Steuerbefehle |
|
||||
|
||||
### CRC-Feld (8 Bits)
|
||||
|
||||
- **Algorithmus:** CRC-8-CCITT
|
||||
- **Polynom:** 0x07 (x⁸ + x² + x + 1)
|
||||
- **Initialwert:** 0x00
|
||||
- **Eingabe:** Type (3 Bits) + Data (13 Bits) = 16 Bits
|
||||
- **Zweck:** Fehlererkennung bei Bitfehlern durch Umgebungslicht oder Interferenzen
|
||||
|
||||
**Erwartete Fehlererkennungsrate:** >99.5 % für Einfach- oder Doppelbitfehler
|
||||
|
||||
## Beispiel-Frame
|
||||
|
||||
**Hit von Spieler 42 mit 10 Schaden:**
|
||||
|
||||
```
|
||||
Type: 000 (Hit)
|
||||
Data: 00101010 01010 (ShooterID=42, Damage=10)
|
||||
CRC8: [berechnet aus obigen Daten]
|
||||
|
||||
Kompletter Frame (24 Bits):
|
||||
000 00101010 01010 CCCCCCCC
|
||||
│ │ │ └─ CRC8
|
||||
│ │ └─ Damage (10)
|
||||
│ └─ Shooter ID (42)
|
||||
└─ Type (Hit)
|
||||
```
|
||||
|
||||
**Übertragungsabfolge:**
|
||||
|
||||
1. Start Burst: 2400 µs Träger AN
|
||||
2. Bit 0 (Type): 600 µs Mark + 600 µs Space
|
||||
3. Bit 1 (Type): 600 µs Mark + 600 µs Space
|
||||
4. Bit 2 (Type): 600 µs Mark + 600 µs Space
|
||||
5. ... (21 weitere Bits)
|
||||
6. Ende: Träger AUS
|
||||
|
||||
## Empfänger-Implementierung
|
||||
|
||||
### Hardware-Anforderungen
|
||||
|
||||
- TSOP4838 verbunden mit GPIO mit Interrupt-Fähigkeit
|
||||
- Steigende/fallende Flanken-Erkennung
|
||||
- Timer zur Messung der Space-Dauern
|
||||
|
||||
### Software-State-Machine
|
||||
|
||||
1. **IDLE:** Auf Start Burst warten (2000–2800 µs)
|
||||
2. **SYNC:** Start Burst erkannt, Vorbereitung zur Bit-Empfang
|
||||
3. **DATA:** Space nach jedem Mark messen, 24 Bits dekodieren
|
||||
4. **VALIDATE:** CRC prüfen, Frame bei Gültigkeit verarbeiten
|
||||
|
||||
### Timing-Toleranzen
|
||||
|
||||
- Breite Toleranzbereiche (±17–33 %) kompensieren Interrupt-Jitter und Träger-Drift
|
||||
- Fehlgeschlagener CRC zeigt beschädigten Frame an → stille Verwerfung
|
||||
- Empfänger resynchronisiert automatisch beim nächsten Start Burst
|
||||
|
||||
## Konfigurierbare Parameter
|
||||
|
||||
Die Protokoll-Timing kann via Kconfig für verschiedene Umgebungen angepasst werden:
|
||||
|
||||
- `CONFIG_IR_SEND_CARRIER_HZ`: Trägerfrequenz (30–45 kHz)
|
||||
- `CONFIG_IR_SEND_DUTY_CYCLE_PERCENT`: PWM Tastgrad (25–75 %)
|
||||
- `CONFIG_IR_PROTO_BASE_US`: Basistakt (300–1000 µs)
|
||||
- `CONFIG_IR_PROTO_START_MULT`: Startburst-Faktor (2–8)
|
||||
- `CONFIG_IR_PROTO_GAP_MULT`: Gap-Faktor (0–4; 0 = kein Gap)
|
||||
- `CONFIG_IR_PROTO_MARK_MULT`: Mark-Faktor (1–2)
|
||||
- `CONFIG_IR_PROTO_SPACE0_MULT`: Space0-Faktor (1–3)
|
||||
- `CONFIG_IR_PROTO_SPACE1_MULT`: Space1-Faktor (1–4)
|
||||
|
||||
Die Standardwerte folgen Sony SIRC Timing-Konventionen für bewährte Zuverlässigkeit.
|
||||
|
||||
## Leistungscharakteristiken
|
||||
|
||||
| Metrik | Wert |
|
||||
|--------|------|
|
||||
| Frame-Zeit | ~39 ms |
|
||||
| Datenrate | ~410 bit/s |
|
||||
| Max. Spieler-IDs | 256 |
|
||||
| Reichweite (Außen) | ~50–100 m (abhängig von Sender-Leistung und Umgebungslicht) |
|
||||
| Fehler-Erkennung | >99.5 % via CRC-8 |
|
||||
| Störfestigkeit | Hoch (Hardware-Bandpass 38 kHz) |
|
||||
|
||||
---
|
||||
|
||||
## Bluetooth LE Protokoll
|
||||
|
||||
*Zu dokumentieren: BLE-Charakteristiken für Spielstatus-Synchronisierung, Team-Zuordnung, etc.*
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
site_name: Eriks Laser Tag
|
||||
site_url: https://gitea.iten.pro/edi/lasertag
|
||||
site_url: https://home.iten.pro/lasertag/
|
||||
repo_url: https://gitea.iten.pro/edi/lasertag
|
||||
|
||||
nav:
|
||||
- Übersicht: index.md
|
||||
- Konzept:
|
||||
- Software: konzept_hardware.md
|
||||
- Hardware: concepts/hardware.md
|
||||
- Software: concepts/software.md
|
||||
- Gameplay & Modi: concepts/gameplay.md
|
||||
- Mobile App: concepts/mobile_app.md
|
||||
- Spezifikationen:
|
||||
- IR-Protokoll: specifications/ir_protocol.md
|
||||
- Planung: planung.md
|
||||
- Lizenz: license.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
@@ -14,6 +21,17 @@ theme:
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- print-site:
|
||||
add_to_navigation: true
|
||||
print_page_title: 'PDF / Druckversion'
|
||||
add_print_site_banner: false
|
||||
add_cover_page: true
|
||||
cover_page_template: ""
|
||||
path_to_pdf: "lasertag-dokumentation.pdf"
|
||||
enabled: true
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
pymdown-extensions
|
||||
mkdocs-print-site-plugin
|
||||
|
||||
1
firmware/apps/_samples/audio/.gitignore
vendored
Normal file
1
firmware/apps/_samples/audio/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/audio/CMakeLists.txt
Normal file
10
firmware/apps/_samples/audio/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
|
||||
project(_mcumgr)
|
||||
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
44
firmware/apps/_samples/audio/nrf52840dk_nrf52840.overlay
Normal file
44
firmware/apps/_samples/audio/nrf52840dk_nrf52840.overlay
Normal file
@@ -0,0 +1,44 @@
|
||||
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
|
||||
|
||||
// You can also use the buttons in the sidebar to perform actions on nodes.
|
||||
// Actions currently available include:
|
||||
|
||||
// * Enabling / disabling the node
|
||||
// * Adding the bus to a bus
|
||||
// * Removing the node
|
||||
// * Connecting ADC channels
|
||||
|
||||
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
|
||||
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
nordic,pm-ext-flash = &mx25r64;
|
||||
};
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
i2s0_default: i2s0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
|
||||
};
|
||||
};
|
||||
|
||||
i2s0_sleep: i2s0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&i2s0 {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&i2s0_default>;
|
||||
pinctrl-1 = <&i2s0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
};
|
||||
4
firmware/apps/_samples/audio/pm_static.yml
Normal file
4
firmware/apps/_samples/audio/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
30
firmware/apps/_samples/audio/prj.conf
Normal file
30
firmware/apps/_samples/audio/prj.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell configuration
|
||||
CONFIG_SHELL=y
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
|
||||
# # MCU Manager
|
||||
# CONFIG_NET_BUF=y
|
||||
# CONFIG_ZCBOR=y
|
||||
# CONFIG_MCUMGR=y
|
||||
# CONFIG_BASE64=y
|
||||
# CONFIG_CRC=y
|
||||
# CONFIG_MCUMGR_TRANSPORT_SHELL=y
|
||||
|
||||
# # MCUMGR groups
|
||||
# CONFIG_MCUMGR_GRP_OS=y
|
||||
# CONFIG_MCUMGR_GRP_OS_ECHO=y
|
||||
# CONFIG_MCUMGR_GRP_FS=y
|
||||
# CONFIG_MCUMGR_GRP_FS_CHECKSUM_HASH=y
|
||||
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_LASERTAG_UTILS=y
|
||||
CONFIG_FS_MGMT=y
|
||||
CONFIG_FS_MGMT_LOG_LEVEL_DBG=n
|
||||
CONFIG_AUDIO=y
|
||||
CONFIG_AUDIO_LOG_LEVEL_DBG=y
|
||||
68
firmware/apps/_samples/audio/src/main.c
Normal file
68
firmware/apps/_samples/audio/src/main.c
Normal file
@@ -0,0 +1,68 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/logging/log_ctrl.h>
|
||||
#include <fs_mgmt.h>
|
||||
#include <audio.h>
|
||||
#include <hal/nrf_i2s.h>
|
||||
#include <lasertag_utils.h>
|
||||
|
||||
LOG_MODULE_REGISTER(MMS, LOG_LEVEL_INF);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("Starting Audio Sample Application...");
|
||||
LOG_INF("Sleeping for one second to allow thread analyzer to initialize and log thread states before we start the test.");
|
||||
k_sleep(K_MSEC(1000));
|
||||
|
||||
int err;
|
||||
LOG_INF("Audio test snippet");
|
||||
err = fs_mgmt_init();
|
||||
if (err)
|
||||
{
|
||||
LOG_ERR("Failed to initialize fs_mgmt: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = audio_init();
|
||||
if (err)
|
||||
{
|
||||
LOG_ERR("Failed to initialize audio: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
LOG_INF("Triggering NULL file playback to test error handling...");
|
||||
audio_play_file(NULL);
|
||||
while(log_process());
|
||||
LOG_INF("Triggering NULL sound to test error handling...");
|
||||
audio_play_sound(NULL);
|
||||
while(log_process());
|
||||
LOG_INF("Triggering nonexistent sound to test error handling...");
|
||||
audio_play_sound("nonexistent_file");
|
||||
k_sleep(K_MSEC(100));
|
||||
while(log_process());
|
||||
LOG_INF("Triggering very long file name to test error handling...");
|
||||
audio_play_sound("very_long_file_name_that_exceeds_the_maximum_length_allowed_by_the_system_to_test_error_handling");
|
||||
while(log_process());
|
||||
|
||||
LOG_INF("Triggering first sound...");
|
||||
audio_play_sound("s1");
|
||||
k_sleep(K_MSEC(100));
|
||||
audio_stop();
|
||||
LOG_INF("Triggering second sound after abort...");
|
||||
audio_play_sound("s1");
|
||||
|
||||
k_sleep(K_MSEC(100));
|
||||
// Directly stop the I2S peripheral to simulate an abrupt stop that
|
||||
// might occur with a DMA failure or similar issue. This will cause the
|
||||
// next playback attempt to hit the slab timeout and trigger the I2S
|
||||
// reset logic in the audio thread.
|
||||
LOG_INF("Simulating failure by stopping I2S directly...");
|
||||
NRF_I2S0->TASKS_STOP = 1;
|
||||
NRF_I2S0->ENABLE = 0;
|
||||
LOG_INF("Triggering third sound after failure simulation...");
|
||||
audio_play_sound("s1");
|
||||
LOG_INF(FORMAT_GREEN_BOLD("If you made it to this point, the test completed successfully and everything should work fine!"));
|
||||
LOG_INF(FORMAT_BRIGHT_BOLD("More output might follow due to the async nature of the audio playback."));
|
||||
|
||||
return 0;
|
||||
}
|
||||
2
firmware/apps/_samples/audio/tool/requirements.txt
Normal file
2
firmware/apps/_samples/audio/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/audio/tool/tool.py
Normal file
120
firmware/apps/_samples/audio/tool/tool.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import serial
|
||||
import base64
|
||||
import cbor2
|
||||
import struct
|
||||
import time
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Icons (NerdFont / Emoji)
|
||||
ICON_DIR = "📁"
|
||||
ICON_FILE = "📄"
|
||||
|
||||
class nRF_FS_Client:
|
||||
def __init__(self, port, baud):
|
||||
try:
|
||||
self.ser = serial.Serial(port, baud, timeout=0.2)
|
||||
self.seq = 0
|
||||
self.ser.reset_input_buffer()
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: Could not open {port} ({e})")
|
||||
sys.exit(1)
|
||||
|
||||
def crc16(self, data):
|
||||
crc = 0x0000
|
||||
for byte in data:
|
||||
crc ^= (byte << 8)
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc = crc << 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
|
||||
def build_packet(self, group, cmd, payload):
|
||||
self.seq = (self.seq + 1) % 256
|
||||
cbor_payload = cbor2.dumps(payload)
|
||||
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
|
||||
full_body = header + cbor_payload
|
||||
checksum = self.crc16(full_body)
|
||||
full_msg = full_body + struct.pack(">H", checksum)
|
||||
return struct.pack(">H", len(full_msg)) + full_msg
|
||||
|
||||
def request(self, group, cmd, payload):
|
||||
packet = self.build_packet(group, cmd, payload)
|
||||
b64_data = base64.b64encode(packet).decode()
|
||||
self.ser.write(f"\x06\t{b64_data}\n".encode())
|
||||
|
||||
full_response_b64 = ""
|
||||
expected_len = -1
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < 3.0:
|
||||
line = self.ser.readline().strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
|
||||
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
|
||||
|
||||
if is_smp or is_cont_special:
|
||||
full_response_b64 += line[2:].decode()
|
||||
try:
|
||||
raw_data = base64.b64decode(full_response_b64)
|
||||
if expected_len == -1 and len(raw_data) >= 2:
|
||||
expected_len = struct.unpack(">H", raw_data[:2])[0]
|
||||
|
||||
if expected_len != -1 and len(raw_data) >= expected_len + 2:
|
||||
if raw_data[8] == self.seq:
|
||||
return cbor2.loads(raw_data[10:-2])
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def list_recursive(self, path="/", prefix=""):
|
||||
res = self.request(64, 0, {"path": path})
|
||||
if res is None or 'files' not in res:
|
||||
return
|
||||
|
||||
# Sorting: directories first, then names
|
||||
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
|
||||
count = len(entries)
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == count - 1)
|
||||
name = entry['n']
|
||||
is_dir = entry.get('t', 'f').startswith('d')
|
||||
|
||||
# Line style selection
|
||||
# connector = "└── " if is_last else "├── "
|
||||
connector = "└─ " if is_last else "├─ "
|
||||
|
||||
|
||||
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
|
||||
|
||||
if is_dir:
|
||||
# Extend prefix for the next level
|
||||
extension = " " if is_last else "│ "
|
||||
sub_path = f"{path}/{name}".replace("//", "/")
|
||||
self.list_recursive(sub_path, prefix + extension)
|
||||
|
||||
def close(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
|
||||
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
|
||||
args = parser.parse_args()
|
||||
|
||||
client = nRF_FS_Client(args.port, 115200)
|
||||
print(f"--- Directory tree on nRF ({args.port}) ---")
|
||||
try:
|
||||
# Initial call
|
||||
client.list_recursive("/")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
firmware/apps/_samples/ir_recv_adc/.gitignore
vendored
Normal file
1
firmware/apps/_samples/ir_recv_adc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/ir_recv_adc/CMakeLists.txt
Normal file
10
firmware/apps/_samples/ir_recv_adc/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
|
||||
project(_mcumgr)
|
||||
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
@@ -0,0 +1,51 @@
|
||||
#include <zephyr/dt-bindings/adc/adc.h>
|
||||
#include <zephyr/dt-bindings/adc/nrf-adc.h>
|
||||
|
||||
/ {
|
||||
zephyr,user {
|
||||
/* Mappt die logischen Kanäle 0-3 auf die physischen ADC-Knoten */
|
||||
io-channels = <&adc 0>, <&adc 1>, <&adc 2>, <&adc 3>;
|
||||
};
|
||||
};
|
||||
|
||||
&adc {
|
||||
status = "okay";
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
channel@0 {
|
||||
reg = <0>;
|
||||
zephyr,gain = "ADC_GAIN_1_4";
|
||||
zephyr,reference = "ADC_REF_VDD_1_4";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
|
||||
zephyr,input-positive = <NRF_SAADC_AIN0>; /* Pin P0.02 */
|
||||
zephyr,resolution = <12>;
|
||||
};
|
||||
|
||||
channel@1 {
|
||||
reg = <1>;
|
||||
zephyr,gain = "ADC_GAIN_1_4";
|
||||
zephyr,reference = "ADC_REF_VDD_1_4";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
|
||||
zephyr,input-positive = <NRF_SAADC_AIN1>; /* Pin P0.03 */
|
||||
zephyr,resolution = <12>;
|
||||
};
|
||||
|
||||
channel@2 {
|
||||
reg = <2>;
|
||||
zephyr,gain = "ADC_GAIN_1_4";
|
||||
zephyr,reference = "ADC_REF_VDD_1_4";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
|
||||
zephyr,input-positive = <NRF_SAADC_AIN2>; /* Pin P0.04 */
|
||||
zephyr,resolution = <12>;
|
||||
};
|
||||
|
||||
channel@3 {
|
||||
reg = <3>;
|
||||
zephyr,gain = "ADC_GAIN_1_4";
|
||||
zephyr,reference = "ADC_REF_VDD_1_4";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
|
||||
zephyr,input-positive = <NRF_SAADC_AIN3>; /* Pin P0.05 */
|
||||
zephyr,resolution = <12>;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
|
||||
|
||||
// You can also use the buttons in the sidebar to perform actions on nodes.
|
||||
// Actions currently available include:
|
||||
|
||||
// * Enabling / disabling the node
|
||||
// * Adding the bus to a bus
|
||||
// * Removing the node
|
||||
// * Connecting ADC channels
|
||||
|
||||
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
|
||||
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
nordic,pm-ext-flash = &mx25r64;
|
||||
};
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
i2s0_default: i2s0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
|
||||
};
|
||||
};
|
||||
|
||||
i2s0_sleep: i2s0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&i2s0 {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&i2s0_default>;
|
||||
pinctrl-1 = <&i2s0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
};
|
||||
4
firmware/apps/_samples/ir_recv_adc/pm_static.yml
Normal file
4
firmware/apps/_samples/ir_recv_adc/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
25
firmware/apps/_samples/ir_recv_adc/prj.conf
Normal file
25
firmware/apps/_samples/ir_recv_adc/prj.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART-Grundlagen
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell-Konfiguration
|
||||
CONFIG_SHELL=y
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
|
||||
# Lasertag-spezifische Konfiguration
|
||||
CONFIG_LASERTAG_UTILS=y
|
||||
CONFIG_IR_RECV=y
|
||||
CONFIG_IR_RECV_LOG_LEVEL_DBG=y
|
||||
# UART basics
|
||||
|
||||
# Thread Analyzer aktivieren
|
||||
CONFIG_THREAD_ANALYZER=y
|
||||
# Shell configuration
|
||||
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5
|
||||
|
||||
# CPU-Laufzeit-Statistiken aktivieren
|
||||
CONFIG_THREAD_RUNTIME_STATS=y
|
||||
# Lasertag-specific configuration
|
||||
|
||||
15
firmware/apps/_samples/ir_recv_adc/src/main.c
Normal file
15
firmware/apps/_samples/ir_recv_adc/src/main.c
Normal file
@@ -0,0 +1,15 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <ir_recv.h>
|
||||
|
||||
LOG_MODULE_REGISTER(ir_recv_adc, LOG_LEVEL_INF);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("Starting IR receive ADC application...");
|
||||
lasertag_utils_init();
|
||||
ir_recv_init();
|
||||
|
||||
return 0;
|
||||
}
|
||||
2
firmware/apps/_samples/ir_recv_adc/tool/requirements.txt
Normal file
2
firmware/apps/_samples/ir_recv_adc/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/ir_recv_adc/tool/tool.py
Normal file
120
firmware/apps/_samples/ir_recv_adc/tool/tool.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import serial
|
||||
import base64
|
||||
import cbor2
|
||||
import struct
|
||||
import time
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Icons (NerdFont / Emoji)
|
||||
ICON_DIR = "📁"
|
||||
ICON_FILE = "📄"
|
||||
|
||||
class nRF_FS_Client:
|
||||
def __init__(self, port, baud):
|
||||
try:
|
||||
self.ser = serial.Serial(port, baud, timeout=0.2)
|
||||
self.seq = 0
|
||||
self.ser.reset_input_buffer()
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: Could not open {port} ({e})")
|
||||
sys.exit(1)
|
||||
|
||||
def crc16(self, data):
|
||||
crc = 0x0000
|
||||
for byte in data:
|
||||
crc ^= (byte << 8)
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc = crc << 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
|
||||
def build_packet(self, group, cmd, payload):
|
||||
self.seq = (self.seq + 1) % 256
|
||||
cbor_payload = cbor2.dumps(payload)
|
||||
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
|
||||
full_body = header + cbor_payload
|
||||
checksum = self.crc16(full_body)
|
||||
full_msg = full_body + struct.pack(">H", checksum)
|
||||
return struct.pack(">H", len(full_msg)) + full_msg
|
||||
|
||||
def request(self, group, cmd, payload):
|
||||
packet = self.build_packet(group, cmd, payload)
|
||||
b64_data = base64.b64encode(packet).decode()
|
||||
self.ser.write(f"\x06\t{b64_data}\n".encode())
|
||||
|
||||
full_response_b64 = ""
|
||||
expected_len = -1
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < 3.0:
|
||||
line = self.ser.readline().strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
|
||||
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
|
||||
|
||||
if is_smp or is_cont_special:
|
||||
full_response_b64 += line[2:].decode()
|
||||
try:
|
||||
raw_data = base64.b64decode(full_response_b64)
|
||||
if expected_len == -1 and len(raw_data) >= 2:
|
||||
expected_len = struct.unpack(">H", raw_data[:2])[0]
|
||||
|
||||
if expected_len != -1 and len(raw_data) >= expected_len + 2:
|
||||
if raw_data[8] == self.seq:
|
||||
return cbor2.loads(raw_data[10:-2])
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def list_recursive(self, path="/", prefix=""):
|
||||
res = self.request(64, 0, {"path": path})
|
||||
if res is None or 'files' not in res:
|
||||
return
|
||||
|
||||
# Sorting: directories first, then names
|
||||
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
|
||||
count = len(entries)
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == count - 1)
|
||||
name = entry['n']
|
||||
is_dir = entry.get('t', 'f').startswith('d')
|
||||
|
||||
# Line style selection
|
||||
# connector = "└── " if is_last else "├── "
|
||||
connector = "└─ " if is_last else "├─ "
|
||||
|
||||
|
||||
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
|
||||
|
||||
if is_dir:
|
||||
# Extend prefix for the next level
|
||||
extension = " " if is_last else "│ "
|
||||
sub_path = f"{path}/{name}".replace("//", "/")
|
||||
self.list_recursive(sub_path, prefix + extension)
|
||||
|
||||
def close(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
|
||||
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
|
||||
args = parser.parse_args()
|
||||
|
||||
client = nRF_FS_Client(args.port, 115200)
|
||||
print(f"--- Directory tree on nRF ({args.port}) ---")
|
||||
try:
|
||||
# Initial call
|
||||
client.list_recursive("/")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
firmware/apps/_samples/ir_recv_sim/.gitignore
vendored
Normal file
1
firmware/apps/_samples/ir_recv_sim/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/ir_recv_sim/CMakeLists.txt
Normal file
10
firmware/apps/_samples/ir_recv_sim/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
|
||||
project(_mcumgr)
|
||||
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
@@ -0,0 +1,44 @@
|
||||
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
|
||||
|
||||
// You can also use the buttons in the sidebar to perform actions on nodes.
|
||||
// Actions currently available include:
|
||||
|
||||
// * Enabling / disabling the node
|
||||
// * Adding the bus to a bus
|
||||
// * Removing the node
|
||||
// * Connecting ADC channels
|
||||
|
||||
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
|
||||
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
nordic,pm-ext-flash = &mx25r64;
|
||||
};
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
i2s0_default: i2s0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
|
||||
};
|
||||
};
|
||||
|
||||
i2s0_sleep: i2s0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&i2s0 {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&i2s0_default>;
|
||||
pinctrl-1 = <&i2s0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
};
|
||||
4
firmware/apps/_samples/ir_recv_sim/pm_static.yml
Normal file
4
firmware/apps/_samples/ir_recv_sim/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
26
firmware/apps/_samples/ir_recv_sim/prj.conf
Normal file
26
firmware/apps/_samples/ir_recv_sim/prj.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell configuration
|
||||
CONFIG_SHELL=y
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
CONFIG_CPLUSPLUS=y
|
||||
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_LASERTAG_UTILS=y
|
||||
CONFIG_IR_RECV=y
|
||||
CONFIG_IR_RECV_LOG_LEVEL_INF=y
|
||||
CONFIG_IR_RECV_SIMULATOR=y
|
||||
|
||||
# Enable Thread analyzer
|
||||
CONFIG_THREAD_ANALYZER=y
|
||||
CONFIG_THREAD_ANALYZER_AUTO=y
|
||||
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5
|
||||
|
||||
# Enable CPU runtime statistics
|
||||
CONFIG_THREAD_RUNTIME_STATS=y
|
||||
CONFIG_THREAD_RUNTIME_STATS_USE_TIMING_FUNCTIONS=y
|
||||
|
||||
31
firmware/apps/_samples/ir_recv_sim/src/main.c
Normal file
31
firmware/apps/_samples/ir_recv_sim/src/main.c
Normal file
@@ -0,0 +1,31 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <ir_recv.h>
|
||||
|
||||
LOG_MODULE_REGISTER(ir_recv_sim, LOG_LEVEL_INF);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("Starting IR receive simulator application...");
|
||||
lasertag_utils_init();
|
||||
ir_recv_init();
|
||||
|
||||
ir_packet_t test_packet = {0};
|
||||
|
||||
/* Test 1: Perfektes Signal */
|
||||
LOG_INF("Sending perfect packet...");
|
||||
test_packet.data.fields.type = 1;
|
||||
test_packet.data.fields.value = 15;
|
||||
|
||||
uint8_t id = 0;
|
||||
|
||||
for(;;)
|
||||
{
|
||||
test_packet.data.fields.id = id++;
|
||||
ir_recv_sim_send_packet(&test_packet, NULL);
|
||||
k_sleep(K_MSEC(300));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
2
firmware/apps/_samples/ir_recv_sim/tool/requirements.txt
Normal file
2
firmware/apps/_samples/ir_recv_sim/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/ir_recv_sim/tool/tool.py
Normal file
120
firmware/apps/_samples/ir_recv_sim/tool/tool.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import serial
|
||||
import base64
|
||||
import cbor2
|
||||
import struct
|
||||
import time
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Icons (NerdFont / Emoji)
|
||||
ICON_DIR = "📁"
|
||||
ICON_FILE = "📄"
|
||||
|
||||
class nRF_FS_Client:
|
||||
def __init__(self, port, baud):
|
||||
try:
|
||||
self.ser = serial.Serial(port, baud, timeout=0.2)
|
||||
self.seq = 0
|
||||
self.ser.reset_input_buffer()
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: Could not open {port} ({e})")
|
||||
sys.exit(1)
|
||||
|
||||
def crc16(self, data):
|
||||
crc = 0x0000
|
||||
for byte in data:
|
||||
crc ^= (byte << 8)
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc = crc << 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
|
||||
def build_packet(self, group, cmd, payload):
|
||||
self.seq = (self.seq + 1) % 256
|
||||
cbor_payload = cbor2.dumps(payload)
|
||||
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
|
||||
full_body = header + cbor_payload
|
||||
checksum = self.crc16(full_body)
|
||||
full_msg = full_body + struct.pack(">H", checksum)
|
||||
return struct.pack(">H", len(full_msg)) + full_msg
|
||||
|
||||
def request(self, group, cmd, payload):
|
||||
packet = self.build_packet(group, cmd, payload)
|
||||
b64_data = base64.b64encode(packet).decode()
|
||||
self.ser.write(f"\x06\t{b64_data}\n".encode())
|
||||
|
||||
full_response_b64 = ""
|
||||
expected_len = -1
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < 3.0:
|
||||
line = self.ser.readline().strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
|
||||
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
|
||||
|
||||
if is_smp or is_cont_special:
|
||||
full_response_b64 += line[2:].decode()
|
||||
try:
|
||||
raw_data = base64.b64decode(full_response_b64)
|
||||
if expected_len == -1 and len(raw_data) >= 2:
|
||||
expected_len = struct.unpack(">H", raw_data[:2])[0]
|
||||
|
||||
if expected_len != -1 and len(raw_data) >= expected_len + 2:
|
||||
if raw_data[8] == self.seq:
|
||||
return cbor2.loads(raw_data[10:-2])
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def list_recursive(self, path="/", prefix=""):
|
||||
res = self.request(64, 0, {"path": path})
|
||||
if res is None or 'files' not in res:
|
||||
return
|
||||
|
||||
# Sorting: directories first, then names
|
||||
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
|
||||
count = len(entries)
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == count - 1)
|
||||
name = entry['n']
|
||||
is_dir = entry.get('t', 'f').startswith('d')
|
||||
|
||||
# Line style selection
|
||||
# connector = "└── " if is_last else "├── "
|
||||
connector = "└─ " if is_last else "├─ "
|
||||
|
||||
|
||||
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
|
||||
|
||||
if is_dir:
|
||||
# Extend prefix for the next level
|
||||
extension = " " if is_last else "│ "
|
||||
sub_path = f"{path}/{name}".replace("//", "/")
|
||||
self.list_recursive(sub_path, prefix + extension)
|
||||
|
||||
def close(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
|
||||
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
|
||||
args = parser.parse_args()
|
||||
|
||||
client = nRF_FS_Client(args.port, 115200)
|
||||
print(f"--- Directory tree on nRF ({args.port}) ---")
|
||||
try:
|
||||
# Initial call
|
||||
client.list_recursive("/")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
10
firmware/apps/_samples/ir_send/CMakeLists.txt
Normal file
10
firmware/apps/_samples/ir_send/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(ir_send)
|
||||
|
||||
# Define application source files
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
13
firmware/apps/_samples/ir_send/Kconfig
Normal file
13
firmware/apps/_samples/ir_send/Kconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
config IR_SEND_SAMPLE_BURST_US
|
||||
int "IR test burst length (microseconds)"
|
||||
default 1000
|
||||
range 50 100000
|
||||
help
|
||||
Duration of the carrier burst for each test pulse in the sample app.
|
||||
|
||||
config IR_SEND_SAMPLE_PERIOD_MS
|
||||
int "IR test burst period (milliseconds)"
|
||||
default 1000
|
||||
range 10 60000
|
||||
help
|
||||
Interval between consecutive test bursts in the sample app.
|
||||
@@ -0,0 +1 @@
|
||||
../../../../boards/nrf52840dk/nrf52840dk_nrf52840.overlay
|
||||
13
firmware/apps/_samples/ir_send/prj.conf
Normal file
13
firmware/apps/_samples/ir_send/prj.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
# Logging
|
||||
CONFIG_LOG=y
|
||||
|
||||
# IR Send Library
|
||||
CONFIG_IR_SEND=y
|
||||
|
||||
# PWM driver for IR carrier
|
||||
CONFIG_PWM=y
|
||||
CONFIG_PWM_NRFX=y
|
||||
|
||||
# Sample test configuration
|
||||
CONFIG_IR_SEND_SAMPLE_BURST_US=1000
|
||||
CONFIG_IR_SEND_SAMPLE_PERIOD_MS=1000
|
||||
37
firmware/apps/_samples/ir_send/src/main.c
Normal file
37
firmware/apps/_samples/ir_send/src/main.c
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* ir_send sample app - IR transmission test
|
||||
*/
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "ir_send.h"
|
||||
|
||||
LOG_MODULE_REGISTER(ir_send);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("=== IR Send Sample ===");
|
||||
LOG_INF("Board: %s", CONFIG_BOARD);
|
||||
|
||||
int ret = ir_send_init();
|
||||
if (ret != 0) {
|
||||
LOG_ERR("Failed to initialize IR send: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ir_send_set_frequency(CONFIG_IR_SEND_CARRIER_HZ);
|
||||
LOG_INF("Ready to test IR transmission (burst %u us every %u ms @ %u Hz)",
|
||||
CONFIG_IR_SEND_SAMPLE_BURST_US,
|
||||
CONFIG_IR_SEND_SAMPLE_PERIOD_MS,
|
||||
CONFIG_IR_SEND_CARRIER_HZ);
|
||||
|
||||
while (true) {
|
||||
ret = ir_send_pulse(CONFIG_IR_SEND_SAMPLE_BURST_US);
|
||||
if (ret != 0) {
|
||||
LOG_ERR("ir_send_pulse failed: %d", ret);
|
||||
}
|
||||
k_msleep(CONFIG_IR_SEND_SAMPLE_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
1
firmware/apps/_samples/mcumgr
Submodule
1
firmware/apps/_samples/mcumgr
Submodule
Submodule firmware/apps/_samples/mcumgr added at b9c1c03f6e
1
firmware/apps/_samples/thread/.gitignore
vendored
Normal file
1
firmware/apps/_samples/thread/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/thread/CMakeLists.txt
Normal file
10
firmware/apps/_samples/thread/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
|
||||
project(_mcumgr)
|
||||
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
44
firmware/apps/_samples/thread/nrf52840dk_nrf52840.overlay
Normal file
44
firmware/apps/_samples/thread/nrf52840dk_nrf52840.overlay
Normal file
@@ -0,0 +1,44 @@
|
||||
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
|
||||
|
||||
// You can also use the buttons in the sidebar to perform actions on nodes.
|
||||
// Actions currently available include:
|
||||
|
||||
// * Enabling / disabling the node
|
||||
// * Adding the bus to a bus
|
||||
// * Removing the node
|
||||
// * Connecting ADC channels
|
||||
|
||||
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
|
||||
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
nordic,pm-ext-flash = &mx25r64;
|
||||
};
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
i2s0_default: i2s0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
|
||||
};
|
||||
};
|
||||
|
||||
i2s0_sleep: i2s0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&i2s0 {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&i2s0_default>;
|
||||
pinctrl-1 = <&i2s0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
};
|
||||
4
firmware/apps/_samples/thread/pm_static.yml
Normal file
4
firmware/apps/_samples/thread/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
22
firmware/apps/_samples/thread/prj.conf
Normal file
22
firmware/apps/_samples/thread/prj.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell configuration
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
CONFIG_FILE_SYSTEM_SHELL=y
|
||||
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_BLE_MGMT=y
|
||||
CONFIG_GAME_MGMT=y
|
||||
CONFIG_GAME_MGMT_SHELL=y
|
||||
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_LASERTAG_ROLE_LEADER=y
|
||||
CONFIG_THREAD_MGMT=y
|
||||
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_THREAD_MGMT_SHELL=y
|
||||
CONFIG_FS_MGMT=y
|
||||
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_AUDIO_LOG_LEVEL_DBG=y
|
||||
44
firmware/apps/_samples/thread/src/main.c
Normal file
44
firmware/apps/_samples/thread/src/main.c
Normal file
@@ -0,0 +1,44 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <thread_mgmt.h>
|
||||
#include <game_mgmt.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <fs_mgmt.h>
|
||||
#include <audio.h>
|
||||
|
||||
LOG_MODULE_REGISTER(OT_SAMPLE, LOG_LEVEL_INF);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("Starting Thread Management test application...");
|
||||
lasertag_utils_init();
|
||||
int rc = thread_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Thread management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Thread management initialized successfully.");
|
||||
|
||||
rc = fs_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("File system management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("File system management initialized successfully.");
|
||||
|
||||
rc = audio_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Audio initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Audio initialized successfully.");
|
||||
|
||||
rc = game_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Game management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Game management initialized successfully.");
|
||||
|
||||
return 0;
|
||||
}
|
||||
2
firmware/apps/_samples/thread/tool/requirements.txt
Normal file
2
firmware/apps/_samples/thread/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/thread/tool/tool.py
Normal file
120
firmware/apps/_samples/thread/tool/tool.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import serial
|
||||
import base64
|
||||
import cbor2
|
||||
import struct
|
||||
import time
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Icons (NerdFont / Emoji)
|
||||
ICON_DIR = "📁"
|
||||
ICON_FILE = "📄"
|
||||
|
||||
class nRF_FS_Client:
|
||||
def __init__(self, port, baud):
|
||||
try:
|
||||
self.ser = serial.Serial(port, baud, timeout=0.2)
|
||||
self.seq = 0
|
||||
self.ser.reset_input_buffer()
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: Could not open {port} ({e})")
|
||||
sys.exit(1)
|
||||
|
||||
def crc16(self, data):
|
||||
crc = 0x0000
|
||||
for byte in data:
|
||||
crc ^= (byte << 8)
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc = crc << 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
|
||||
def build_packet(self, group, cmd, payload):
|
||||
self.seq = (self.seq + 1) % 256
|
||||
cbor_payload = cbor2.dumps(payload)
|
||||
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
|
||||
full_body = header + cbor_payload
|
||||
checksum = self.crc16(full_body)
|
||||
full_msg = full_body + struct.pack(">H", checksum)
|
||||
return struct.pack(">H", len(full_msg)) + full_msg
|
||||
|
||||
def request(self, group, cmd, payload):
|
||||
packet = self.build_packet(group, cmd, payload)
|
||||
b64_data = base64.b64encode(packet).decode()
|
||||
self.ser.write(f"\x06\t{b64_data}\n".encode())
|
||||
|
||||
full_response_b64 = ""
|
||||
expected_len = -1
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < 3.0:
|
||||
line = self.ser.readline().strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
|
||||
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
|
||||
|
||||
if is_smp or is_cont_special:
|
||||
full_response_b64 += line[2:].decode()
|
||||
try:
|
||||
raw_data = base64.b64decode(full_response_b64)
|
||||
if expected_len == -1 and len(raw_data) >= 2:
|
||||
expected_len = struct.unpack(">H", raw_data[:2])[0]
|
||||
|
||||
if expected_len != -1 and len(raw_data) >= expected_len + 2:
|
||||
if raw_data[8] == self.seq:
|
||||
return cbor2.loads(raw_data[10:-2])
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def list_recursive(self, path="/", prefix=""):
|
||||
res = self.request(64, 0, {"path": path})
|
||||
if res is None or 'files' not in res:
|
||||
return
|
||||
|
||||
# Sorting: directories first, then names
|
||||
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
|
||||
count = len(entries)
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == count - 1)
|
||||
name = entry['n']
|
||||
is_dir = entry.get('t', 'f').startswith('d')
|
||||
|
||||
# Line style selection
|
||||
# connector = "└── " if is_last else "├── "
|
||||
connector = "└─ " if is_last else "├─ "
|
||||
|
||||
|
||||
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
|
||||
|
||||
if is_dir:
|
||||
# Extend prefix for the next level
|
||||
extension = " " if is_last else "│ "
|
||||
sub_path = f"{path}/{name}".replace("//", "/")
|
||||
self.list_recursive(sub_path, prefix + extension)
|
||||
|
||||
def close(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
|
||||
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
|
||||
args = parser.parse_args()
|
||||
|
||||
client = nRF_FS_Client(args.port, 115200)
|
||||
print(f"--- Directory tree on nRF ({args.port}) ---")
|
||||
try:
|
||||
# Initial call
|
||||
client.list_recursive("/")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
firmware/apps/_samples/utils/.gitignore
vendored
Normal file
1
firmware/apps/_samples/utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/utils/CMakeLists.txt
Normal file
10
firmware/apps/_samples/utils/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
|
||||
project(_mcumgr)
|
||||
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
44
firmware/apps/_samples/utils/nrf52840dk_nrf52840.overlay
Normal file
44
firmware/apps/_samples/utils/nrf52840dk_nrf52840.overlay
Normal file
@@ -0,0 +1,44 @@
|
||||
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
|
||||
|
||||
// You can also use the buttons in the sidebar to perform actions on nodes.
|
||||
// Actions currently available include:
|
||||
|
||||
// * Enabling / disabling the node
|
||||
// * Adding the bus to a bus
|
||||
// * Removing the node
|
||||
// * Connecting ADC channels
|
||||
|
||||
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
|
||||
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
nordic,pm-ext-flash = &mx25r64;
|
||||
};
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
i2s0_default: i2s0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
|
||||
};
|
||||
};
|
||||
|
||||
i2s0_sleep: i2s0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
|
||||
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
|
||||
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&i2s0 {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&i2s0_default>;
|
||||
pinctrl-1 = <&i2s0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
};
|
||||
4
firmware/apps/_samples/utils/pm_static.yml
Normal file
4
firmware/apps/_samples/utils/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
14
firmware/apps/_samples/utils/prj.conf
Normal file
14
firmware/apps/_samples/utils/prj.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell configuration
|
||||
CONFIG_SHELL=y
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_LASERTAG_UTILS=y
|
||||
CONFIG_LASERTAG_UTILS_LOG_LEVEL_DBG=y
|
||||
|
||||
24
firmware/apps/_samples/utils/src/main.c
Normal file
24
firmware/apps/_samples/utils/src/main.c
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <lasertag_utils.h>
|
||||
|
||||
LOG_MODULE_REGISTER(MMS, LOG_LEVEL_INF);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("Starting Utils test application...");
|
||||
lasertag_utils_init();
|
||||
|
||||
uint8_t data[] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe};
|
||||
uint8_t crc = lastertag_crc8(data, sizeof(data));
|
||||
LOG_INF("CRC8: 0x%02X", crc);
|
||||
if (crc != 0xbe) {
|
||||
LOG_ERR("CRC8 check failed!!");
|
||||
} else {
|
||||
LOG_INF("CRC8 check passed.");
|
||||
}
|
||||
LOG_INF(FORMAT_BLUE_BOLD("This should be in blue and bold if ANSI color codes are supported in the terminal."));
|
||||
LOG_INF("Here, only a part should be " FORMAT_RED("red") " and the rest normal.");
|
||||
LOG_INF(FORMAT_RED_BOLD("this ") FORMAT_GREEN_BOLD("is ") FORMAT_BLUE_BOLD("colorful") FORMAT_YELLOW_BOLD(" as") FORMAT_BRIGHT_BOLD(" fuck") "!");
|
||||
return 0;
|
||||
}
|
||||
2
firmware/apps/_samples/utils/tool/requirements.txt
Normal file
2
firmware/apps/_samples/utils/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/utils/tool/tool.py
Normal file
120
firmware/apps/_samples/utils/tool/tool.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import serial
|
||||
import base64
|
||||
import cbor2
|
||||
import struct
|
||||
import time
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Icons (NerdFont / Emoji)
|
||||
ICON_DIR = "📁"
|
||||
ICON_FILE = "📄"
|
||||
|
||||
class nRF_FS_Client:
|
||||
def __init__(self, port, baud):
|
||||
try:
|
||||
self.ser = serial.Serial(port, baud, timeout=0.2)
|
||||
self.seq = 0
|
||||
self.ser.reset_input_buffer()
|
||||
except serial.SerialException as e:
|
||||
print(f"Error: Could not open {port} ({e})")
|
||||
sys.exit(1)
|
||||
|
||||
def crc16(self, data):
|
||||
crc = 0x0000
|
||||
for byte in data:
|
||||
crc ^= (byte << 8)
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc = crc << 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
|
||||
def build_packet(self, group, cmd, payload):
|
||||
self.seq = (self.seq + 1) % 256
|
||||
cbor_payload = cbor2.dumps(payload)
|
||||
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
|
||||
full_body = header + cbor_payload
|
||||
checksum = self.crc16(full_body)
|
||||
full_msg = full_body + struct.pack(">H", checksum)
|
||||
return struct.pack(">H", len(full_msg)) + full_msg
|
||||
|
||||
def request(self, group, cmd, payload):
|
||||
packet = self.build_packet(group, cmd, payload)
|
||||
b64_data = base64.b64encode(packet).decode()
|
||||
self.ser.write(f"\x06\t{b64_data}\n".encode())
|
||||
|
||||
full_response_b64 = ""
|
||||
expected_len = -1
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < 3.0:
|
||||
line = self.ser.readline().strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
|
||||
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
|
||||
|
||||
if is_smp or is_cont_special:
|
||||
full_response_b64 += line[2:].decode()
|
||||
try:
|
||||
raw_data = base64.b64decode(full_response_b64)
|
||||
if expected_len == -1 and len(raw_data) >= 2:
|
||||
expected_len = struct.unpack(">H", raw_data[:2])[0]
|
||||
|
||||
if expected_len != -1 and len(raw_data) >= expected_len + 2:
|
||||
if raw_data[8] == self.seq:
|
||||
return cbor2.loads(raw_data[10:-2])
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def list_recursive(self, path="/", prefix=""):
|
||||
res = self.request(64, 0, {"path": path})
|
||||
if res is None or 'files' not in res:
|
||||
return
|
||||
|
||||
# Sorting: directories first, then names
|
||||
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
|
||||
count = len(entries)
|
||||
|
||||
for i, entry in enumerate(entries):
|
||||
is_last = (i == count - 1)
|
||||
name = entry['n']
|
||||
is_dir = entry.get('t', 'f').startswith('d')
|
||||
|
||||
# Line style selection
|
||||
# connector = "└── " if is_last else "├── "
|
||||
connector = "└─ " if is_last else "├─ "
|
||||
|
||||
|
||||
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
|
||||
|
||||
if is_dir:
|
||||
# Extend prefix for the next level
|
||||
extension = " " if is_last else "│ "
|
||||
sub_path = f"{path}/{name}".replace("//", "/")
|
||||
self.list_recursive(sub_path, prefix + extension)
|
||||
|
||||
def close(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
|
||||
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
|
||||
args = parser.parse_args()
|
||||
|
||||
client = nRF_FS_Client(args.port, 115200)
|
||||
print(f"--- Directory tree on nRF ({args.port}) ---")
|
||||
try:
|
||||
# Initial call
|
||||
client.list_recursive("/")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
firmware/apps/_samples/watchdog
Submodule
1
firmware/apps/_samples/watchdog
Submodule
Submodule firmware/apps/_samples/watchdog added at 10ca5017c5
@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.20)
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../libs)
|
||||
|
||||
# Set board root to find custom board overlays in firmware/boards
|
||||
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(lasertag_leader)
|
||||
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
# Console and Logging
|
||||
CONFIG_LOG=y
|
||||
|
||||
# Shell and Built-in Commands
|
||||
CONFIG_SHELL=y
|
||||
CONFIG_KERNEL_SHELL=y
|
||||
CONFIG_DEVICE_SHELL=y
|
||||
CONFIG_REBOOT=y
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# --- STACK SIZE UPDATES (Fixes the Hard Fault) ---
|
||||
CONFIG_MAIN_STACK_SIZE=4096
|
||||
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
|
||||
CONFIG_BT_RX_STACK_SIZE=2048
|
||||
# Shell configuration
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
CONFIG_FILE_SYSTEM_SHELL=y
|
||||
|
||||
# Storage and Settings (NVS)
|
||||
CONFIG_FLASH=y
|
||||
CONFIG_FLASH_MAP=y
|
||||
CONFIG_NVS=y
|
||||
CONFIG_SETTINGS=y
|
||||
# Stack protection
|
||||
CONFIG_HW_STACK_PROTECTION=y
|
||||
CONFIG_STACK_SENTINEL=y
|
||||
|
||||
# Network and OpenThread
|
||||
CONFIG_NETWORKING=y
|
||||
CONFIG_NET_L2_OPENTHREAD=y
|
||||
CONFIG_OPENTHREAD=y
|
||||
CONFIG_OPENTHREAD_FTD=y
|
||||
CONFIG_OPENTHREAD_SHELL=y
|
||||
|
||||
# --- CoAP & UDP Features ---
|
||||
CONFIG_OPENTHREAD_COAP=y
|
||||
CONFIG_OPENTHREAD_MANUAL_START=y
|
||||
|
||||
# Bluetooth
|
||||
CONFIG_BT=y
|
||||
CONFIG_BT_PERIPHERAL=y
|
||||
CONFIG_BT_DEVICE_NAME="Lasertag-Device"
|
||||
CONFIG_BT_DEVICE_NAME_DYNAMIC=y
|
||||
|
||||
# Enable Lasertag Shared Modules
|
||||
CONFIG_LASERTAG_UTILS=y
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_BLE_MGMT=y
|
||||
CONFIG_GAME_MGMT=y
|
||||
CONFIG_GAME_MGMT_SHELL=y
|
||||
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_THREAD_MGMT=y
|
||||
CONFIG_BLE_MGMT=y
|
||||
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_THREAD_MGMT_SHELL=y
|
||||
CONFIG_FS_MGMT=y
|
||||
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_AUDIO_LOG_LEVEL_DBG=y
|
||||
|
||||
CONFIG_LASERTAG_ROLE_LEADER=y
|
||||
|
||||
CONFIG_ENTROPY_GENERATOR=y
|
||||
@@ -1,38 +1,56 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <thread_mgmt.h>
|
||||
#include <ble_mgmt.h>
|
||||
#include <game_mgmt.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <fs_mgmt.h>
|
||||
#include <audio.h>
|
||||
#include <zephyr/random/random.h>
|
||||
|
||||
LOG_MODULE_REGISTER(leader_app, CONFIG_LOG_DEFAULT_LEVEL);
|
||||
LOG_MODULE_REGISTER(OT_SAMPLE, LOG_LEVEL_INF);
|
||||
|
||||
uint64_t generate_64bit_random(void) {
|
||||
uint64_t rnd_val;
|
||||
|
||||
/* Füllt den Speicherbereich der Variable mit Zufallsbytes */
|
||||
sys_csrand_get(&rnd_val, sizeof(rnd_val));
|
||||
|
||||
return rnd_val;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* Initialize shared project logic and NVS */
|
||||
LOG_INF("Starting Thread Management test application...");
|
||||
lasertag_utils_init();
|
||||
|
||||
/* Initialize and start BLE management for provisioning */
|
||||
int rc = ble_mgmt_init();
|
||||
if (rc) {
|
||||
LOG_ERR("BLE initialization failed (err %d)", rc);
|
||||
return rc;
|
||||
} else {
|
||||
LOG_INF("BLE Management initialized successfully.");
|
||||
}
|
||||
|
||||
/* Initialize and start OpenThread stack */
|
||||
rc = thread_mgmt_init();
|
||||
if (rc) {
|
||||
LOG_ERR("Thread initialization failed (err %d)", rc);
|
||||
} else {
|
||||
LOG_INF("Leader Application successfully started with Thread Mesh.");
|
||||
int rc = thread_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Thread management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Thread management initialized successfully.");
|
||||
|
||||
while (1) {
|
||||
/* Main loop - handle high-level game logic here */
|
||||
k_sleep(K_MSEC(1000));
|
||||
rc = fs_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("File system management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("File system management initialized successfully.");
|
||||
|
||||
rc = audio_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Audio initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Audio initialized successfully.");
|
||||
|
||||
rc = game_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Game management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF(FORMAT_BRIGHT("Game management initialized successfully. Switching to LOBBY state..."));
|
||||
game_mgmt_set_game_id(generate_64bit_random()); /* Set a dummy game ID for testing */
|
||||
game_mgmt_set_state(SYS_STATE_LOBBY);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
13
firmware/apps/vest/CMakeLists.txt
Normal file
13
firmware/apps/vest/CMakeLists.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
# Tell Zephyr to look into our libs folder for extra modules
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../libs)
|
||||
|
||||
# Set board root to find custom board overlays in firmware/boards
|
||||
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(lasertag_vest)
|
||||
|
||||
# Define application source files
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
5
firmware/apps/vest/VERSION
Normal file
5
firmware/apps/vest/VERSION
Normal file
@@ -0,0 +1,5 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 0
|
||||
PATCHLEVEL = 0
|
||||
VERSION_TWEAK = 0
|
||||
EXTRAVERSION =
|
||||
1
firmware/apps/vest/pm_static.yml
Symbolic link
1
firmware/apps/vest/pm_static.yml
Symbolic link
@@ -0,0 +1 @@
|
||||
../leader/pm_static.yml
|
||||
28
firmware/apps/vest/prj.conf
Normal file
28
firmware/apps/vest/prj.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell configuration
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
CONFIG_FILE_SYSTEM_SHELL=y
|
||||
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_AUDIO=y
|
||||
CONFIG_AUDIO_LOG_LEVEL_DBG=y
|
||||
|
||||
CONFIG_BLE_MGMT=y
|
||||
|
||||
CONFIG_GAME_MGMT=y
|
||||
CONFIG_GAME_MGMT_SHELL=y
|
||||
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
|
||||
|
||||
CONFIG_THREAD_MGMT=y
|
||||
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_THREAD_MGMT_SHELL=y
|
||||
|
||||
CONFIG_FS_MGMT=y
|
||||
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
|
||||
|
||||
CONFIG_LASERTAG_ROLE_VEST=y
|
||||
44
firmware/apps/vest/src/main.c
Normal file
44
firmware/apps/vest/src/main.c
Normal file
@@ -0,0 +1,44 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <thread_mgmt.h>
|
||||
#include <game_mgmt.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <fs_mgmt.h>
|
||||
#include <audio.h>
|
||||
|
||||
LOG_MODULE_REGISTER(OT_SAMPLE, LOG_LEVEL_INF);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("Starting Thread Management test application...");
|
||||
lasertag_utils_init();
|
||||
int rc = thread_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Thread management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Thread management initialized successfully.");
|
||||
|
||||
rc = fs_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("File system management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("File system management initialized successfully.");
|
||||
|
||||
rc = audio_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Audio initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF("Audio initialized successfully.");
|
||||
|
||||
rc = game_mgmt_init();
|
||||
if (rc < 0) {
|
||||
LOG_ERR("Game management initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
LOG_INF(FORMAT_BRIGHT("Game management initialized successfully. Switching to LOBBY state..."));
|
||||
game_mgmt_set_state(SYS_STATE_LOBBY);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
# Zephyr mitteilen, dass unsere Libs Teil des Projekts sind
|
||||
# Tell Zephyr that our libs are part of the project
|
||||
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../libs)
|
||||
|
||||
# Set board root to find custom board overlays in firmware/boards
|
||||
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(lasertag_weapon)
|
||||
|
||||
|
||||
110
firmware/apps/weapon/main.c
Normal file
110
firmware/apps/weapon/main.c
Normal file
@@ -0,0 +1,110 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <dk_buttons_and_leds.h>
|
||||
#include <openthread/thread.h>
|
||||
#include <openthread/coap.h>
|
||||
|
||||
/* Our new library */
|
||||
#include "game_logic.h"
|
||||
|
||||
LOG_MODULE_REGISTER(weapon_app, LOG_LEVEL_INF);
|
||||
|
||||
/* Game context */
|
||||
static struct game_ctx game;
|
||||
|
||||
/* Forward Declarations */
|
||||
static void on_button_changed(uint32_t button_state, uint32_t has_changed);
|
||||
|
||||
/* --- Game Logic Callbacks --- */
|
||||
|
||||
static void on_game_state_change(enum game_state new_state)
|
||||
{
|
||||
LOG_INF("APP: Game state changed -> %d", new_state);
|
||||
|
||||
switch (new_state) {
|
||||
case GAME_STATE_RUNNING:
|
||||
dk_set_led_on(DK_LED1); // LED on when game is running
|
||||
break;
|
||||
case GAME_STATE_FINISHED:
|
||||
dk_set_led_off(DK_LED1);
|
||||
// Blink or similar
|
||||
break;
|
||||
default:
|
||||
dk_set_led_off(DK_LED1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_hit_received(uint16_t shooter_id)
|
||||
{
|
||||
LOG_WARN("APP: OUCH! Hit by player %d. Health: %d", shooter_id, game.health);
|
||||
|
||||
// Visuelles Feedback: LED 2 blinkt kurz
|
||||
dk_set_led_on(DK_LED2);
|
||||
k_msleep(200);
|
||||
dk_set_led_off(DK_LED2);
|
||||
|
||||
// TODO: Send CoAP message to leader later!
|
||||
// send_hit_report_to_leader(...);
|
||||
}
|
||||
|
||||
static void on_shot_fired(void)
|
||||
{
|
||||
LOG_INF("APP: BANG! Shot fired.");
|
||||
// TODO: Send IR protocol here (NEC/RC5)
|
||||
}
|
||||
|
||||
/* --- Hardware Callbacks --- */
|
||||
|
||||
static void on_button_changed(uint32_t button_state, uint32_t has_changed)
|
||||
{
|
||||
// Button 1: Shoot
|
||||
if ((has_changed & DK_BTN1_MSK) && (button_state & DK_BTN1_MSK)) {
|
||||
if (game.current_state == GAME_STATE_RUNNING) {
|
||||
on_shot_fired();
|
||||
} else {
|
||||
LOG_INF("Shot blocked - game not running.");
|
||||
}
|
||||
}
|
||||
|
||||
// Button 2: Simulate hit (Self-Hit Test)
|
||||
if ((has_changed & DK_BTN2_MSK) && (button_state & DK_BTN2_MSK)) {
|
||||
LOG_INF("Simulating hit by player 99...");
|
||||
struct game_hit_packet hit_packet;
|
||||
|
||||
// Pretend the IR sensor detected player 99
|
||||
if (game_logic_register_hit(99, &hit_packet)) {
|
||||
// If hit was valid (game running, we're still alive), we now have a packet
|
||||
// that we could send via Thread.
|
||||
LOG_INF("Hit registered! Damage: %d", hit_packet.damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Main --- */
|
||||
|
||||
void main(void)
|
||||
{
|
||||
LOG_INF("Lasertag Weapon Start");
|
||||
|
||||
int err = dk_buttons_init(on_button_changed);
|
||||
if (err) {
|
||||
LOG_ERR("Buttons could not be initialized (err %d)", err);
|
||||
}
|
||||
|
||||
// Game Logic Setup
|
||||
game.on_state_change = on_game_state_change;
|
||||
game.on_hit_received = on_hit_received;
|
||||
|
||||
// Initialize as player with ID from Kconfig (or NVS later)
|
||||
game_logic_init(&game, CONFIG_LASERTAG_PLAYER_ID_DEFAULT);
|
||||
|
||||
// For testing, we manually set the status to RUNNING,
|
||||
// until we receive the start signal from the leader via Thread.
|
||||
struct game_state_packet fake_start = {.state = GAME_STATE_RUNNING};
|
||||
game_logic_handle_state_update(&fake_start);
|
||||
|
||||
while (1) {
|
||||
k_sleep(K_FOREVER);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,23 @@
|
||||
CONFIG_LASERTAG_UTILS=y
|
||||
CONFIG_LOG=y
|
||||
|
||||
# UART basics
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||
|
||||
# Shell configuration
|
||||
CONFIG_SHELL_BACKEND_SERIAL=y
|
||||
CONFIG_FILE_SYSTEM_SHELL=y
|
||||
|
||||
# Lasertag-specific configuration
|
||||
CONFIG_BLE_MGMT=y
|
||||
CONFIG_GAME_MGMT=y
|
||||
CONFIG_GAME_MGMT_SHELL=y
|
||||
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_THREAD_MGMT=y
|
||||
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_THREAD_MGMT_SHELL=y
|
||||
CONFIG_FS_MGMT=y
|
||||
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
|
||||
CONFIG_AUDIO_LOG_LEVEL_DBG=y
|
||||
|
||||
CONFIG_LASERTAG_ROLE_VEST=y
|
||||
56
firmware/boards/nrf52840dk/nrf52840dk_nrf52840.overlay
Normal file
56
firmware/boards/nrf52840dk/nrf52840dk_nrf52840.overlay
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Device Tree Overlay für nRF52840 DK
|
||||
* Definiert GPIO-Pins für Trigger, LEDs und IR-Transmission (PWM3 @ P0.16)
|
||||
*/
|
||||
|
||||
/ {
|
||||
aliases {
|
||||
trigger-btn = &button0;
|
||||
ir-output = &ir_tx0;
|
||||
led-status = &led0;
|
||||
led-power = &led1;
|
||||
};
|
||||
|
||||
buttons {
|
||||
compatible = "gpio-keys";
|
||||
button0: button_0 {
|
||||
gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
|
||||
label = "Trigger Button";
|
||||
};
|
||||
};
|
||||
|
||||
leds {
|
||||
compatible = "gpio-leds";
|
||||
led0: led_0 {
|
||||
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
|
||||
label = "Status LED";
|
||||
};
|
||||
led1: led_1 {
|
||||
gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>;
|
||||
label = "Power LED";
|
||||
};
|
||||
};
|
||||
|
||||
ir_pwm: ir_pwm {
|
||||
compatible = "pwm-leds";
|
||||
ir_tx0: ir_tx_0 {
|
||||
pwms = <&pwm3 0 PWM_NSEC(26316) PWM_POLARITY_NORMAL>;
|
||||
label = "IR TX PWM";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&pwm3 {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&pwm3_default>;
|
||||
pinctrl-names = "default";
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
pwm3_default: pwm3_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(PWM_OUT0, 0, 16)>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Add library subdirectories
|
||||
# Build ble_mgmt and thread_mgmt first since lasertag_utils depends on them
|
||||
add_subdirectory(ble_mgmt)
|
||||
add_subdirectory(thread_mgmt)
|
||||
add_subdirectory(lasertag_utils)
|
||||
add_subdirectory(lasertag_utils)
|
||||
add_subdirectory(ir)
|
||||
add_subdirectory(game_mgmt)
|
||||
add_subdirectory(fs_mgmt)
|
||||
add_subdirectory(audio)
|
||||
@@ -1,4 +1,26 @@
|
||||
# Main entry point for custom project Kconfigs
|
||||
choice LASERTAG_DEVICE_ROLE
|
||||
prompt "Lasertag Device Role"
|
||||
default LASERTAG_ROLE_VEST
|
||||
help
|
||||
Select the role of the lasertag device. This can be used to conditionally compile code specific to vests, guns, or other device types.
|
||||
config LASERTAG_ROLE_VEST
|
||||
bool "Vest"
|
||||
help
|
||||
A standard role for the vest device, which may have responsibilities such as receiving hit notifications, managing player health, etc.
|
||||
config LASERTAG_ROLE_WEAPON
|
||||
bool "Weapon"
|
||||
help
|
||||
A special role for the weapon device, which may have additional responsibilities such as sending hit notifications, managing ammo count, etc.
|
||||
config LASERTAG_ROLE_LEADER
|
||||
bool "Game Leader"
|
||||
help
|
||||
A special role for the game leader device, which may have additional responsibilities such as starting/stopping games, managing player lists, etc.
|
||||
endchoice
|
||||
|
||||
rsource "lasertag_utils/Kconfig"
|
||||
rsource "thread_mgmt/Kconfig"
|
||||
rsource "ble_mgmt/Kconfig"
|
||||
rsource "ble_mgmt/Kconfig"
|
||||
rsource "ir/Kconfig"
|
||||
rsource "game_mgmt/Kconfig"
|
||||
rsource "fs_mgmt/Kconfig"
|
||||
rsource "audio/Kconfig"
|
||||
5
firmware/libs/audio/CMakeLists.txt
Normal file
5
firmware/libs/audio/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
if(CONFIG_AUDIO)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/audio.c)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
76
firmware/libs/audio/Kconfig
Normal file
76
firmware/libs/audio/Kconfig
Normal file
@@ -0,0 +1,76 @@
|
||||
menuconfig AUDIO
|
||||
bool "Audio Support"
|
||||
select FS_MGMT
|
||||
select I2S
|
||||
select I2S_NRFX if DT_HAS_NORDIC_NRF_I2S_ENABLED
|
||||
help
|
||||
Library for initializing and managing the audio subsystem.
|
||||
|
||||
if AUDIO
|
||||
config AUDIO_DEFAULT_VOLUME
|
||||
int "Default Audio Volume (0..255)"
|
||||
default 128
|
||||
range 0 255
|
||||
help
|
||||
Set the default audio volume level. 0 is silent, 255 is maximum volume. Default is 128 (50% volume).
|
||||
|
||||
config AUDIO_SAMPLE_RATE
|
||||
int "Audio Sample Rate (Hz)"
|
||||
default 16000
|
||||
range 8000 48000
|
||||
help
|
||||
Set the audio sample rate in Hz. Common values are 8000, 16000, 44100, and 48000 Hz. Default is 16000 Hz.
|
||||
|
||||
config AUDIO_WORD_WIDTH
|
||||
int "Audio Bit Depth"
|
||||
default 16
|
||||
range 8 32
|
||||
help
|
||||
Set the audio bit depth. Common values are 8, 16, 24, and 32 bits. Default is 16 bits.
|
||||
|
||||
config AUDIO_BLOCK_COUNT
|
||||
int "Audio Block Count"
|
||||
default 4
|
||||
range 1 16
|
||||
help
|
||||
Set the number of audio blocks for buffering. More blocks can help with smoother audio but use more memory. Default is 4 blocks.
|
||||
|
||||
config AUDIO_BLOCK_SIZE
|
||||
int "Audio Block Size (bytes)"
|
||||
default 1024
|
||||
range 256 8192
|
||||
help
|
||||
Set the size of each audio block in bytes. Larger blocks can reduce CPU overhead but increase latency. Default is 1024 bytes.
|
||||
|
||||
config AUDIO_THREAD_PRIORITY
|
||||
int "Audio Thread Priority"
|
||||
default 5
|
||||
range 0 255
|
||||
help
|
||||
Set the thread priority for audio processing. Lower numbers indicate higher priority. Default is 5
|
||||
|
||||
config AUDIO_STACK_SIZE
|
||||
int "Audio Thread Stack Size (bytes)"
|
||||
default 1200
|
||||
range 256 8192
|
||||
help
|
||||
Set the stack size for the audio processing thread in bytes. Default is 2048 bytes.
|
||||
|
||||
config AUDIO_SAMPLE_FOLDER
|
||||
string "Audio Sample Folder"
|
||||
default "a"
|
||||
help
|
||||
Set the folder path where audio sample files are stored. No leading or trailing slashes. Default is "a".
|
||||
|
||||
config AUDIO_MAX_PATH_LEN
|
||||
int "Maximum Audio File Path Length"
|
||||
default 32
|
||||
range 8 128
|
||||
help
|
||||
Set the maximum length for audio file paths. Default is 16 characters.
|
||||
|
||||
# Logging configuration for the Audio module
|
||||
module = AUDIO
|
||||
module-str = audio
|
||||
source "subsys/logging/Kconfig.template.log_config"
|
||||
endif # AUDIO
|
||||
28
firmware/libs/audio/include/audio.h
Normal file
28
firmware/libs/audio/include/audio.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @file audio.h
|
||||
* @brief Public API for the audio subsystem.
|
||||
* This header defines the public interface for the audio subsystem, which
|
||||
* provides functionality to play audio files and manage audio playback. It
|
||||
* abstracts away the details of the underlying I2S peripheral and file
|
||||
* system, allowing other parts of the application to easily trigger audio
|
||||
* playback by specifying file paths or sound names.
|
||||
*
|
||||
* FLASH MEMORY USAGE:
|
||||
* LOG LEVEL DEBUG: ~1.8 KB
|
||||
* LOG LEVEL INFO: ~1.7 KB
|
||||
* LOG LEVEL WARNING: ~1.2 KB
|
||||
* LOG LEVEL ERROR: ~1.1 KB
|
||||
*
|
||||
* RAM USAGE (without stack and audio buffers):
|
||||
* Any LOG LEVEL: ~0.47 KB
|
||||
*/
|
||||
#ifndef AUDIO_H
|
||||
#define AUDIO_H
|
||||
|
||||
int audio_init(void);
|
||||
int audio_play_sound(const char* file);
|
||||
int audio_play_file(const char* file);
|
||||
void audio_stop(void);
|
||||
|
||||
#endif // AUDIO_H
|
||||
|
||||
271
firmware/libs/audio/src/audio.c
Normal file
271
firmware/libs/audio/src/audio.c
Normal file
@@ -0,0 +1,271 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/drivers/i2s.h>
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <audio.h>
|
||||
#include <string.h>
|
||||
|
||||
LOG_MODULE_REGISTER(audio, CONFIG_AUDIO_LOG_LEVEL);
|
||||
|
||||
#define SAMPLES_PER_BLOCK (CONFIG_AUDIO_BLOCK_SIZE / (CONFIG_AUDIO_WORD_WIDTH / 8) / 2) // Divide by 2 for stereo
|
||||
#define BLOCK_DURATION_MS ((SAMPLES_PER_BLOCK * 1000U) / CONFIG_AUDIO_SAMPLE_RATE) // Duration of audio in each block in milliseconds
|
||||
#define MAX_WAIT_TIME_MS (3 * BLOCK_DURATION_MS) // Maximum time to wait for the I2S peripheral to request the next block before we consider it stalled and reset it
|
||||
|
||||
/* Get the I2S device from the devicetree */
|
||||
#define I2S_NODE DT_NODELABEL(i2s0)
|
||||
static const struct device *i2s_dev = DEVICE_DT_GET(I2S_NODE);
|
||||
|
||||
/* Memory Slab for I2S DMA */
|
||||
K_MEM_SLAB_DEFINE(audio_slab, CONFIG_AUDIO_BLOCK_SIZE, CONFIG_AUDIO_BLOCK_COUNT, 4);
|
||||
|
||||
/* Globals */
|
||||
static volatile bool abort_playback = false;
|
||||
static volatile uint8_t audio_volume = CONFIG_AUDIO_DEFAULT_VOLUME;
|
||||
|
||||
static void wait_for_i2s_drain(void)
|
||||
{
|
||||
const uint32_t frames_per_block = CONFIG_AUDIO_BLOCK_SIZE / 4;
|
||||
const uint32_t block_ms = (frames_per_block * 1000U) / CONFIG_AUDIO_SAMPLE_RATE;
|
||||
const uint32_t max_wait_ms = (block_ms + 1U) * CONFIG_AUDIO_BLOCK_COUNT + 5U;
|
||||
int64_t deadline = k_uptime_get() + max_wait_ms;
|
||||
|
||||
while (k_mem_slab_num_free_get(&audio_slab) < CONFIG_AUDIO_BLOCK_COUNT)
|
||||
{
|
||||
if (k_uptime_get() >= deadline)
|
||||
{
|
||||
LOG_WRN("Timeout waiting for I2S drain");
|
||||
break;
|
||||
}
|
||||
k_sleep(K_MSEC(1));
|
||||
}
|
||||
}
|
||||
|
||||
/* Message Queue: transfers the file path to the thread */
|
||||
K_MSGQ_DEFINE(audio_msgq, CONFIG_AUDIO_MAX_PATH_LEN, 10, 4);
|
||||
|
||||
/* Audio thread function */
|
||||
void audio_thread_fn(void *p1, void *p2, void *p3)
|
||||
{
|
||||
ARG_UNUSED(p1);
|
||||
ARG_UNUSED(p2);
|
||||
ARG_UNUSED(p3);
|
||||
|
||||
char file_path[CONFIG_AUDIO_MAX_PATH_LEN];
|
||||
struct fs_file_t file;
|
||||
fs_file_t_init(&file);
|
||||
|
||||
LOG_DBG("Audio thread started, priority %d", k_thread_priority_get(k_current_get()));
|
||||
while (1)
|
||||
{
|
||||
bool trigger_started = false;
|
||||
if (k_msgq_get(&audio_msgq, &file_path, K_FOREVER) == 0)
|
||||
{
|
||||
abort_playback = false;
|
||||
|
||||
if (fs_open(&file, file_path, FS_O_READ) < 0)
|
||||
{
|
||||
LOG_ERR("thread: could not open %s", file_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_DBG("thread: preparing %s...", file_path);
|
||||
|
||||
uint32_t queued_blocks = 0;
|
||||
|
||||
while (!abort_playback)
|
||||
{
|
||||
void *mem_block;
|
||||
if (k_mem_slab_alloc(&audio_slab, &mem_block, K_MSEC(MAX_WAIT_TIME_MS)) < 0)
|
||||
{
|
||||
LOG_ERR("audio: slab timeout (I2S stall? DMA failure?) - skipping sound and resetting I2S...");
|
||||
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
|
||||
audio_init();
|
||||
break;
|
||||
}
|
||||
if (abort_playback)
|
||||
{
|
||||
LOG_DBG("thread: playback aborted while waiting for memory block.");
|
||||
k_mem_slab_free(&audio_slab, mem_block);
|
||||
break;
|
||||
}
|
||||
|
||||
int16_t *data_ptr = (int16_t *)mem_block;
|
||||
const uint32_t max_mono_samples = CONFIG_AUDIO_BLOCK_SIZE / 4;
|
||||
|
||||
ssize_t bytes_read = fs_read(&file, data_ptr, max_mono_samples * sizeof(int16_t));
|
||||
|
||||
if (bytes_read <= 0)
|
||||
{
|
||||
k_mem_slab_free(&audio_slab, mem_block);
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t samples_read = bytes_read / sizeof(int16_t);
|
||||
|
||||
// Padding with zeros if we read less than a full block of mono samples
|
||||
if (samples_read < max_mono_samples)
|
||||
{
|
||||
memset(&data_ptr[samples_read], 0, (max_mono_samples - samples_read) * sizeof(int16_t));
|
||||
}
|
||||
|
||||
uint32_t *stereo_dst = (uint32_t *)mem_block;
|
||||
for (int32_t i = max_mono_samples - 1; i >= 0; i--)
|
||||
{
|
||||
int32_t scaled = (int32_t)data_ptr[i] * audio_volume;
|
||||
int16_t sample = (int16_t)(scaled >> 8);
|
||||
|
||||
stereo_dst[i] = ((uint16_t)sample << 16) | (uint16_t)sample;
|
||||
}
|
||||
|
||||
if (i2s_write(i2s_dev, mem_block, CONFIG_AUDIO_BLOCK_SIZE) < 0)
|
||||
{
|
||||
k_mem_slab_free(&audio_slab, mem_block);
|
||||
break;
|
||||
}
|
||||
|
||||
queued_blocks++;
|
||||
|
||||
// We start playback only when 2 blocks are in the DMA queue to avoid underruns
|
||||
if (!trigger_started && queued_blocks >= 2)
|
||||
{
|
||||
if (i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START) == 0)
|
||||
{
|
||||
trigger_started = true;
|
||||
LOG_DBG("thread: playback started.");
|
||||
}
|
||||
}
|
||||
|
||||
if (samples_read < max_mono_samples)
|
||||
{
|
||||
// Short sample: start with a single queued block so DRAIN can play it.
|
||||
if (!trigger_started)
|
||||
{
|
||||
if (i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START) == 0)
|
||||
{
|
||||
trigger_started = true;
|
||||
LOG_DBG("thread: playback started (short sample).");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (abort_playback)
|
||||
{
|
||||
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
|
||||
trigger_started = false;
|
||||
LOG_DBG("thread: playback aborted.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (k_msgq_num_used_get(&audio_msgq) > 0)
|
||||
{
|
||||
LOG_DBG("thread: play request pending, not draining I2S to minimize latency...");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DBG("thread: sample finished, waiting for I2S to drain...");
|
||||
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
|
||||
trigger_started = false;
|
||||
wait_for_i2s_drain();
|
||||
LOG_DBG("thread: playback finished.");
|
||||
}
|
||||
}
|
||||
fs_close(&file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
K_THREAD_DEFINE(audio_thread, CONFIG_AUDIO_STACK_SIZE, audio_thread_fn, NULL, NULL, NULL, CONFIG_AUDIO_THREAD_PRIORITY, 0, 0);
|
||||
|
||||
int audio_init(void)
|
||||
{
|
||||
LOG_DBG("Initializing audio subsystem...");
|
||||
if (!device_is_ready(i2s_dev))
|
||||
{
|
||||
LOG_ERR("I2S device not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
/* Initial configuration of the I2S peripheral */
|
||||
struct i2s_config config = {
|
||||
.word_size = CONFIG_AUDIO_WORD_WIDTH,
|
||||
.channels = 2,
|
||||
.format = I2S_FMT_DATA_FORMAT_I2S,
|
||||
.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER,
|
||||
.frame_clk_freq = CONFIG_AUDIO_SAMPLE_RATE,
|
||||
.mem_slab = &audio_slab,
|
||||
.block_size = CONFIG_AUDIO_BLOCK_SIZE,
|
||||
.timeout = SYS_FOREVER_MS,
|
||||
};
|
||||
|
||||
int ret = i2s_configure(i2s_dev, I2S_DIR_TX, &config);
|
||||
if (ret < 0)
|
||||
{
|
||||
LOG_ERR("Failed to configure I2S: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
LOG_DBG("Audio subsystem initialized successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_play_file(const char *file)
|
||||
{
|
||||
if (file == NULL)
|
||||
{
|
||||
LOG_ERR("audio_play_file: file path is NULL");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
size_t len = strnlen(file, CONFIG_AUDIO_MAX_PATH_LEN);
|
||||
if (len >= CONFIG_AUDIO_MAX_PATH_LEN)
|
||||
{
|
||||
LOG_ERR("audio_play_file: file path too long: %s", file);
|
||||
return -ENAMETOOLONG;
|
||||
}
|
||||
|
||||
char path_item[CONFIG_AUDIO_MAX_PATH_LEN];
|
||||
memcpy(path_item, file, len + 1);
|
||||
|
||||
if (k_msgq_put(&audio_msgq, path_item, K_NO_WAIT) < 0)
|
||||
{
|
||||
LOG_ERR("audio_play_file: message queue full");
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
LOG_DBG("Queued file for playback: %s", file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_play_sound(const char *file)
|
||||
{
|
||||
if (file == NULL)
|
||||
{
|
||||
LOG_ERR("audio_play_sound: file name is NULL");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
size_t len = strnlen(file, CONFIG_AUDIO_MAX_PATH_LEN) + strlen(CONFIG_FS_MGMT_MOUNT_POINT) + strlen(CONFIG_AUDIO_SAMPLE_FOLDER) + 2;
|
||||
if (len >= CONFIG_AUDIO_MAX_PATH_LEN)
|
||||
{
|
||||
LOG_ERR("audio_play_sound: file path too long: %s/%s/%s", CONFIG_FS_MGMT_MOUNT_POINT, CONFIG_AUDIO_SAMPLE_FOLDER, file);
|
||||
return -ENAMETOOLONG;
|
||||
}
|
||||
char path[CONFIG_AUDIO_MAX_PATH_LEN];
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s/%s",
|
||||
CONFIG_FS_MGMT_MOUNT_POINT,
|
||||
CONFIG_AUDIO_SAMPLE_FOLDER,
|
||||
file);
|
||||
|
||||
return audio_play_file(path);
|
||||
}
|
||||
|
||||
void audio_stop(void)
|
||||
{
|
||||
abort_playback = true;
|
||||
k_msgq_purge(&audio_msgq);
|
||||
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
|
||||
LOG_DBG("Playback stop requested, message queue purged");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
if(CONFIG_BLE_MGMT)
|
||||
zephyr_library()
|
||||
zephyr_sources(src/ble_mgmt.c)
|
||||
zephyr_library_sources(src/ble_mgmt.c)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
@@ -1,11 +1,30 @@
|
||||
menuconfig BLE_MGMT
|
||||
bool "BLE Management"
|
||||
depends on BT
|
||||
select BT
|
||||
select BT_PERIPHERAL
|
||||
select BT_DEVICE_NAME_DYNAMIC
|
||||
help
|
||||
Library for BLE provisioning of the lasertag device.
|
||||
|
||||
if BLE_MGMT
|
||||
config BLE_MGMT_LOG_LEVEL
|
||||
int "BLE Management Log Level"
|
||||
default 3
|
||||
module = BLE_MGMT
|
||||
module-str = ble_mgmt
|
||||
source "subsys/logging/Kconfig.template.log_config"
|
||||
|
||||
config BLE_MGMT_CAN_BE_GAME_LEADER
|
||||
bool "Can be game leader"
|
||||
default n
|
||||
help
|
||||
Allow this device to take the game leader role in the lasertag game.
|
||||
|
||||
config BT_DEVICE_NAME
|
||||
default "Lasertag Device"
|
||||
config BT_L2CAP_TX_MTU
|
||||
default 252
|
||||
config BT_BUF_ACL_RX_SIZE
|
||||
default 251
|
||||
config BT_BUF_ACL_TX_SIZE
|
||||
default 251
|
||||
config BT_ATT_PREPARE_COUNT
|
||||
default 5
|
||||
endif
|
||||
@@ -3,14 +3,39 @@
|
||||
|
||||
/**
|
||||
* @file ble_mgmt.h
|
||||
* @brief Bluetooth Low Energy management for provisioning.
|
||||
* @brief Bluetooth Low Energy management for provisioning and game communication.
|
||||
* This module handles Bluetooth initialization, advertising, and stopping advertising.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Device types for LaserTag devices.
|
||||
*/
|
||||
#define LT_TYPE_LEADER 0x01
|
||||
#define LT_TYPE_WEAPON 0x02
|
||||
#define LT_TYPE_VEST 0x03
|
||||
#define LT_TYPE_BEACON 0x04
|
||||
|
||||
/**
|
||||
* @brief Device configuration payload structure for BLE management.
|
||||
*/
|
||||
typedef struct __packed {
|
||||
uint8_t system_state; /* Offset 0 */
|
||||
uint64_t game_id; /* Offset 1 */
|
||||
uint16_t pan_id; /* Offset 9 */
|
||||
uint8_t channel; /* Offset 11 */
|
||||
uint8_t ext_pan_id[8]; /* Offset 12 */
|
||||
uint8_t network_key[16]; /* Offset 20 */
|
||||
char network_name[17]; /* Offset 36 */
|
||||
char node_name[33]; /* Offset 53 */
|
||||
} device_config_payload_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize Bluetooth and prepare services.
|
||||
*
|
||||
* @param device_type The type of the device (e.g., leader, weapon, vest, beacon).
|
||||
* @return 0 on success.
|
||||
*/
|
||||
int ble_mgmt_init(void);
|
||||
int ble_mgmt_init(uint8_t device_type);
|
||||
|
||||
/**
|
||||
* @brief Start Bluetooth advertising so the web app can find the device.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* BLE Management Module (ble_mgmt.c)
|
||||
* * Structural Fix: Offloading heavy NVS and Thread operations to a workqueue
|
||||
* to prevent stack overflows in the BT RX thread.
|
||||
*/
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
@@ -9,190 +15,249 @@
|
||||
#include <lasertag_utils.h>
|
||||
#include <thread_mgmt.h>
|
||||
#include <ble_mgmt.h>
|
||||
#include <game_mgmt.h>
|
||||
#include <string.h>
|
||||
|
||||
LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
|
||||
|
||||
/**
|
||||
* Basis UUID: 03afe2cf-6c64-4a22-9289-c3ae820cbcxx
|
||||
*/
|
||||
#define LT_UUID_BASE_VAL \
|
||||
BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc00)
|
||||
/* UUID Definitions */
|
||||
#define BT_UUID_LT_PROV_SERVICE BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1000))
|
||||
#define BT_UUID_LT_PROV_NAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1001))
|
||||
#define BT_UUID_LT_PROV_TYPE_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1008))
|
||||
#define BT_UUID_LT_PROV_CONFIG_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c100c))
|
||||
|
||||
#define BT_UUID_LT_SERVICE BT_UUID_DECLARE_128(LT_UUID_BASE_VAL)
|
||||
#define BT_UUID_LT_NAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc01))
|
||||
#define BT_UUID_LT_PANID_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc02))
|
||||
#define BT_UUID_LT_CHAN_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc03))
|
||||
#define BT_UUID_LT_EXTPAN_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc04))
|
||||
#define BT_UUID_LT_NETKEY_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc05))
|
||||
#define BT_UUID_LT_NETNAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc06))
|
||||
#define BT_UUID_LT_NODES_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc07))
|
||||
/* Global state and Workqueue structures */
|
||||
static uint8_t device_role = 0;
|
||||
static uint8_t adv_enabled = 0;
|
||||
static struct k_work_delayable adv_restart_work;
|
||||
|
||||
/* --- GATT Callbacks --- */
|
||||
/* Buffers for asynchronous config application */
|
||||
static device_config_payload_t pending_config;
|
||||
static struct k_work config_apply_work;
|
||||
|
||||
static ssize_t read_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
void *buf, uint16_t len, uint16_t offset)
|
||||
/* ============================================================================
|
||||
Workqueue Handlers
|
||||
============================================================================ */
|
||||
static void config_apply_work_handler(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
LOG_DBG("conf rcv, name: " FORMAT_BOLD("%s") ", state: " FORMAT_BOLD("%d") ", game-id: " FORMAT_BOLD("0x%llx") ", net name: " FORMAT_BOLD("%s") ", channel: " FORMAT_BOLD("%u") ", pan: " FORMAT_BOLD("0x%04X"),
|
||||
pending_config.node_name,
|
||||
pending_config.system_state,
|
||||
pending_config.game_id,
|
||||
pending_config.network_name,
|
||||
pending_config.channel,
|
||||
pending_config.pan_id);
|
||||
LOG_HEXDUMP_DBG(pending_config.ext_pan_id, 8, "ext pan id");
|
||||
LOG_HEXDUMP_DBG(pending_config.network_key, 16, "network key");
|
||||
|
||||
if (pending_config.system_state != SYS_STATE_NO_CHANGE) {
|
||||
game_mgmt_set_state((sys_state_t)pending_config.system_state);
|
||||
}
|
||||
if (pending_config.game_id != 0) {
|
||||
game_mgmt_set_game_id(pending_config.game_id);
|
||||
}
|
||||
|
||||
if (pending_config.node_name[0] != '\0') {
|
||||
lasertag_set_device_name(pending_config.node_name, strlen(pending_config.node_name));
|
||||
bt_set_name(lasertag_get_device_name());
|
||||
}
|
||||
|
||||
if (pending_config.channel != 0) {
|
||||
thread_mgmt_restart_thread_stack(&pending_config, false);
|
||||
}
|
||||
}
|
||||
|
||||
static void adv_restart_work_handler(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
LOG_DBG("Restarting BLE advertising via System Workqueue...");
|
||||
if (adv_enabled == 0) {
|
||||
int err = ble_mgmt_adv_start();
|
||||
if (err) {
|
||||
LOG_ERR("Fehler beim Neustart des Advertisings (err %d)", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ============================================================================
|
||||
GATT Handlers
|
||||
============================================================================ */
|
||||
|
||||
static ssize_t read_leader_config(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
void *buf, uint16_t len, uint16_t offset)
|
||||
{
|
||||
device_config_payload_t payload;
|
||||
memset(&payload, 0, sizeof(payload));
|
||||
|
||||
payload.system_state = (uint8_t)game_mgmt_get_state();
|
||||
payload.game_id = game_mgmt_get_game_id();
|
||||
payload.pan_id = thread_mgmt_get_pan_id();
|
||||
payload.channel = thread_mgmt_get_channel();
|
||||
thread_mgmt_get_ext_pan_id(payload.ext_pan_id);
|
||||
thread_mgmt_get_network_key(payload.network_key);
|
||||
thread_mgmt_get_network_name(payload.network_name, sizeof(payload.network_name));
|
||||
strncpy(payload.node_name, lasertag_get_device_name(), 32);
|
||||
|
||||
LOG_DBG("conf snd, name: " FORMAT_BOLD("%s") ", state: " FORMAT_BOLD("%d") ", game-id: "
|
||||
FORMAT_BOLD("0x%llx") ", net name: " FORMAT_BOLD("%s") ", channel: " FORMAT_BOLD("%u") ", pan: " FORMAT_BOLD("0x%04X"),
|
||||
payload.node_name,
|
||||
payload.system_state,
|
||||
payload.game_id,
|
||||
payload.network_name,
|
||||
payload.channel,
|
||||
payload.pan_id);
|
||||
LOG_HEXDUMP_DBG(payload.ext_pan_id, 8, "ext pan id");
|
||||
LOG_HEXDUMP_DBG(payload.network_key, 16, "network key");
|
||||
|
||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, &payload, sizeof(payload));
|
||||
}
|
||||
|
||||
static ssize_t write_leader_config(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
|
||||
{
|
||||
if (len != sizeof(device_config_payload_t)) {
|
||||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
}
|
||||
|
||||
/* Copy data to buffer and delegate to system workqueue */
|
||||
memcpy(&pending_config, buf, sizeof(pending_config));
|
||||
k_work_submit(&config_apply_work);
|
||||
|
||||
LOG_DBG("Config write received, delegated to workqueue.");
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Simple value handlers for name and type */
|
||||
static ssize_t read_simple_val(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
void *buf, uint16_t len, uint16_t offset)
|
||||
{
|
||||
const char *val_ptr = NULL;
|
||||
size_t val_len = 0;
|
||||
|
||||
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_NAME_CHAR) == 0) {
|
||||
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_TYPE_CHAR) == 0) {
|
||||
val_ptr = (char *)&device_role;
|
||||
val_len = sizeof(device_role);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NAME_CHAR) == 0) {
|
||||
val_ptr = lasertag_get_device_name();
|
||||
val_len = strlen(val_ptr);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PANID_CHAR) == 0) {
|
||||
static uint16_t pan_id;
|
||||
pan_id = lasertag_get_thread_pan_id();
|
||||
val_ptr = (char *)&pan_id;
|
||||
val_len = sizeof(pan_id);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_CHAN_CHAR) == 0) {
|
||||
static uint8_t chan;
|
||||
chan = lasertag_get_thread_channel();
|
||||
val_ptr = (char *)&chan;
|
||||
val_len = sizeof(chan);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_EXTPAN_CHAR) == 0) {
|
||||
val_ptr = (char *)lasertag_get_thread_ext_pan_id();
|
||||
val_len = 8;
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_NETKEY_CHAR) == 0) {
|
||||
val_ptr = (char *)lasertag_get_thread_network_key();
|
||||
val_len = 16;
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_NETNAME_CHAR) == 0) {
|
||||
val_ptr = lasertag_get_thread_network_name();
|
||||
val_len = strlen(val_ptr);
|
||||
}
|
||||
|
||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, val_ptr, val_len);
|
||||
}
|
||||
|
||||
static ssize_t write_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
|
||||
static ssize_t write_name(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
|
||||
{
|
||||
int rc = 0;
|
||||
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_NAME_CHAR) == 0) {
|
||||
rc = lasertag_set_device_name(buf, len);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PANID_CHAR) == 0) {
|
||||
if (len != 2) return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
rc = lasertag_set_thread_pan_id(*(uint16_t*)buf);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_CHAN_CHAR) == 0) {
|
||||
if (len != 1) return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
rc = lasertag_set_thread_channel(*(uint8_t*)buf);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_EXTPAN_CHAR) == 0) {
|
||||
if (len != 8) return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
rc = lasertag_set_thread_ext_pan_id(buf);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_NETKEY_CHAR) == 0) {
|
||||
if (len != 16) return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
rc = lasertag_set_thread_network_key(buf);
|
||||
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_NETNAME_CHAR) == 0) {
|
||||
rc = lasertag_set_thread_network_name(buf, len);
|
||||
}
|
||||
|
||||
if (rc) return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
|
||||
return len;
|
||||
int rc = lasertag_set_device_name(buf, len);
|
||||
if (rc == 0) bt_set_name(lasertag_get_device_name());
|
||||
return rc ? BT_GATT_ERR(BT_ATT_ERR_UNLIKELY) : len;
|
||||
}
|
||||
|
||||
static ssize_t read_discovered_nodes(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
void *buf, uint16_t len, uint16_t offset)
|
||||
{
|
||||
const char *list = thread_mgmt_get_discovered_list();
|
||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, list, strlen(list));
|
||||
}
|
||||
|
||||
static ssize_t write_discover_cmd(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
|
||||
{
|
||||
/* Wenn irgendwas geschrieben wird, triggere Discovery im Thread Mesh */
|
||||
thread_mgmt_discover_nodes();
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Service Definition */
|
||||
/* ============================================================================
|
||||
Service Definition
|
||||
============================================================================ */
|
||||
BT_GATT_SERVICE_DEFINE(provisioning_svc,
|
||||
BT_GATT_PRIMARY_SERVICE(BT_UUID_LT_SERVICE),
|
||||
|
||||
/* Gerätename */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_NAME_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_lasertag_val, write_lasertag_val, NULL),
|
||||
|
||||
/* Thread PAN ID */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PANID_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_lasertag_val, write_lasertag_val, NULL),
|
||||
|
||||
/* Thread Kanal */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_CHAN_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_lasertag_val, write_lasertag_val, NULL),
|
||||
|
||||
/* Extended PAN ID */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_EXTPAN_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_lasertag_val, write_lasertag_val, NULL),
|
||||
|
||||
/* Netzwerk Key */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_NETKEY_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_lasertag_val, write_lasertag_val, NULL),
|
||||
|
||||
/* Thread Netzwerk Name */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_NETNAME_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_lasertag_val, write_lasertag_val, NULL),
|
||||
|
||||
/* Knoten-Liste / Discovery Trigger */
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_NODES_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_discovered_nodes, write_discover_cmd, NULL),
|
||||
BT_GATT_PRIMARY_SERVICE(BT_UUID_LT_PROV_SERVICE),
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_NAME_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_simple_val, write_name, NULL),
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_TYPE_CHAR,
|
||||
BT_GATT_CHRC_READ, BT_GATT_PERM_READ,
|
||||
read_simple_val, NULL, NULL),
|
||||
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_CONFIG_CHAR,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
|
||||
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
|
||||
read_leader_config, write_leader_config, NULL),
|
||||
);
|
||||
|
||||
/* ============================================================================
|
||||
Advertising & Management
|
||||
============================================================================ */
|
||||
static uint8_t mfg_data[] = { 0xff, 0xff, 0x00 };
|
||||
static const struct bt_data ad[] = {
|
||||
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
|
||||
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
|
||||
0x00, 0xbc, 0x0c, 0x82, 0xae, 0xc3, 0x89, 0x92,
|
||||
0x22, 0x4a, 0x64, 0x6c, 0xcf, 0xe2, 0xaf, 0x03),
|
||||
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
|
||||
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
|
||||
0x00, 0x10, 0x0c, 0x82, 0xae, 0xc3, 0x89, 0x92,
|
||||
0x22, 0x4a, 0x64, 0x6c, 0xcf, 0xe2, 0xaf, 0x03),
|
||||
BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)),
|
||||
};
|
||||
|
||||
int ble_mgmt_init(void)
|
||||
int ble_mgmt_init(uint8_t device_type)
|
||||
{
|
||||
device_role = device_type;
|
||||
/* Initialize work structures */
|
||||
k_work_init_delayable(&adv_restart_work, adv_restart_work_handler);
|
||||
k_work_init(&config_apply_work, config_apply_work_handler);
|
||||
|
||||
int err = bt_enable(NULL);
|
||||
if (err) return err;
|
||||
LOG_INF("Bluetooth initialisiert");
|
||||
LOG_DBG("Bluetooth initialized successfully.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ble_mgmt_adv_start(void)
|
||||
{
|
||||
mfg_data[2] = device_role;
|
||||
const char *name = lasertag_get_device_name();
|
||||
bt_set_name(name);
|
||||
|
||||
struct bt_data dynamic_sd[] = {
|
||||
BT_DATA(BT_DATA_NAME_COMPLETE, name, strlen(name)),
|
||||
};
|
||||
|
||||
struct bt_le_adv_param adv_param = {
|
||||
.id = BT_ID_DEFAULT,
|
||||
struct bt_data sd[] = { BT_DATA(BT_DATA_NAME_COMPLETE, name, strlen(name)) };
|
||||
struct bt_le_adv_param param = {
|
||||
.options = (BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_SCANNABLE),
|
||||
.interval_min = BT_GAP_ADV_FAST_INT_MIN_2,
|
||||
.interval_max = BT_GAP_ADV_FAST_INT_MAX_2,
|
||||
};
|
||||
int err = bt_le_adv_start(¶m, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
|
||||
if (!err) adv_enabled = 1;
|
||||
return err;
|
||||
}
|
||||
|
||||
int err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), dynamic_sd, ARRAY_SIZE(dynamic_sd));
|
||||
if (!err) {
|
||||
LOG_INF("Advertising gestartet als: %s", name);
|
||||
/**
|
||||
* Stop BLE advertising.
|
||||
*
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int ble_mgmt_adv_stop(void)
|
||||
{
|
||||
int err = bt_le_adv_stop();
|
||||
if (!err)
|
||||
{
|
||||
LOG_DBG("Advertising stopped");
|
||||
adv_enabled = 0;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
int ble_mgmt_adv_stop(void)
|
||||
/* ============================================================================
|
||||
BLE Connection Event Handlers
|
||||
============================================================================ */
|
||||
|
||||
/**
|
||||
* Callback for when a device connects.
|
||||
* Logs the connection and updates advertising state.
|
||||
*/
|
||||
static void connected(struct bt_conn *conn, uint8_t err)
|
||||
{
|
||||
int err = bt_le_adv_stop();
|
||||
if (!err) {
|
||||
LOG_INF("Advertising gestoppt");
|
||||
if (err) {
|
||||
LOG_ERR("Verbindung fehlgeschlagen (err %u)", err);
|
||||
} else {
|
||||
LOG_DBG("Host verbunden");
|
||||
adv_enabled = 0;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when a device disconnects.
|
||||
* Logs the disconnection and schedules advertising restart.
|
||||
*/
|
||||
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
||||
{
|
||||
LOG_DBG("Verbindung getrennt (Grund %u)", reason);
|
||||
k_work_reschedule(&adv_restart_work, K_MSEC(100));
|
||||
}
|
||||
|
||||
/* Connection callbacks structure */
|
||||
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.connected = connected,
|
||||
.disconnected = disconnected,
|
||||
};
|
||||
17
firmware/libs/fs_mgmt/CMakeLists.txt
Normal file
17
firmware/libs/fs_mgmt/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
if(CONFIG_FS_MGMT)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/fs_mgmt.c)
|
||||
zephyr_include_directories(include)
|
||||
|
||||
if(CONFIG_FILE_SYSTEM_LITTLEFS)
|
||||
if(DEFINED ZEPHYR_LITTLEFS_MODULE_DIR)
|
||||
zephyr_include_directories(${ZEPHYR_LITTLEFS_MODULE_DIR})
|
||||
elseif(DEFINED WEST_TOPDIR)
|
||||
zephyr_include_directories(${WEST_TOPDIR}/modules/fs/littlefs)
|
||||
endif()
|
||||
|
||||
if(DEFINED ZEPHYR_BASE)
|
||||
zephyr_include_directories(${ZEPHYR_BASE}/modules/littlefs)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
30
firmware/libs/fs_mgmt/Kconfig
Normal file
30
firmware/libs/fs_mgmt/Kconfig
Normal file
@@ -0,0 +1,30 @@
|
||||
menuconfig FS_MGMT
|
||||
bool "File System Management"
|
||||
select FLASH
|
||||
select FLASH_MAP
|
||||
select FILE_SYSTEM
|
||||
select FILE_SYSTEM_LITTLEFS
|
||||
select FILE_SYSTEM_MKFS
|
||||
help
|
||||
Library for initializing and managing the file system.
|
||||
|
||||
if FS_MGMT
|
||||
config FS_MGMT_MOUNT_POINT
|
||||
string "Littlefs Mount Point"
|
||||
default "/lfs"
|
||||
help
|
||||
Set the mount point for the Littlefs file system. Default is "/lfs".
|
||||
|
||||
config FS_MGMT_MCUMGR_HANDLER
|
||||
bool "Enable Custom MCUMGR FS Handlers"
|
||||
default y
|
||||
depends on MCUMGR_GRP_FS
|
||||
help
|
||||
Enables the custom MCUMGR group (ID 64) for listing (ls)
|
||||
and removing (rm) files via SMP.
|
||||
|
||||
# Logging configuration for the File System Management module
|
||||
module = FS_MGMT
|
||||
module-str = fs_mgmt
|
||||
source "subsys/logging/Kconfig.template.log_config"
|
||||
endif # FS_MGMT
|
||||
7
firmware/libs/fs_mgmt/include/fs_mgmt.h
Normal file
7
firmware/libs/fs_mgmt/include/fs_mgmt.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef FS_MGMT_H
|
||||
#define FS_MGMT_H
|
||||
|
||||
int fs_mgmt_init(void);
|
||||
|
||||
#endif
|
||||
|
||||
327
firmware/libs/fs_mgmt/src/fs_mgmt.c
Normal file
327
firmware/libs/fs_mgmt/src/fs_mgmt.c
Normal file
@@ -0,0 +1,327 @@
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zephyr/fs/littlefs.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <fs_mgmt.h>
|
||||
|
||||
LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL);
|
||||
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
|
||||
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
|
||||
|
||||
static struct fs_mount_t fs_storage_mnt = {
|
||||
.type = FS_LITTLEFS,
|
||||
.fs_data = &fs_storage_data,
|
||||
.storage_dev = (void *)STORAGE_PARTITION_ID,
|
||||
.mnt_point = CONFIG_FS_MGMT_MOUNT_POINT,
|
||||
};
|
||||
|
||||
#ifdef CONFIG_FS_MGMT_MCUMGR_HANDLER
|
||||
#include <zephyr/mgmt/mcumgr/mgmt/mgmt.h>
|
||||
#include <zephyr/mgmt/mcumgr/smp/smp.h>
|
||||
#include <zephyr/fs/fs.h>
|
||||
#include <zcbor_decode.h>
|
||||
#include <zcbor_encode.h>
|
||||
#include <mgmt/mcumgr/util/zcbor_bulk.h>
|
||||
|
||||
#define CUSTOM_GROUP_ID 64
|
||||
#define CMD_LS 0
|
||||
#define CMD_RM 1
|
||||
|
||||
#define FS_MGMT_MAX_PATH_LEN 128
|
||||
|
||||
static int fs_mgmt_count_entries(const char *abs_path, bool recursive, int *count)
|
||||
{
|
||||
struct fs_dir_t dirp;
|
||||
struct fs_dirent entry;
|
||||
|
||||
fs_dir_t_init(&dirp);
|
||||
if (fs_opendir(&dirp, abs_path) != 0)
|
||||
{
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
|
||||
{
|
||||
(*count)++;
|
||||
|
||||
if (recursive && entry.type == FS_DIR_ENTRY_DIR)
|
||||
{
|
||||
char child_path[FS_MGMT_MAX_PATH_LEN];
|
||||
int len = snprintk(child_path, sizeof(child_path), "%s/%s", abs_path, entry.name);
|
||||
if (len <= 0 || len >= sizeof(child_path))
|
||||
{
|
||||
fs_closedir(&dirp);
|
||||
return -ENAMETOOLONG;
|
||||
}
|
||||
|
||||
int rc = fs_mgmt_count_entries(child_path, true, count);
|
||||
if (rc != 0)
|
||||
{
|
||||
fs_closedir(&dirp);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs_closedir(&dirp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool fs_mgmt_encode_entries(zcbor_state_t *zse, const char *abs_path, const char *rel_prefix, bool recursive)
|
||||
{
|
||||
struct fs_dir_t dirp;
|
||||
struct fs_dirent entry;
|
||||
|
||||
fs_dir_t_init(&dirp);
|
||||
if (fs_opendir(&dirp, abs_path) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
|
||||
while (ok && fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
|
||||
{
|
||||
const char *type_char = (entry.type == FS_DIR_ENTRY_DIR) ? "d" : "f";
|
||||
char rel_name[FS_MGMT_MAX_PATH_LEN];
|
||||
|
||||
if (rel_prefix[0] == '\0')
|
||||
{
|
||||
int len = snprintk(rel_name, sizeof(rel_name), "%s", entry.name);
|
||||
if (len <= 0 || len >= sizeof(rel_name))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int len = snprintk(rel_name, sizeof(rel_name), "%s/%s", rel_prefix, entry.name);
|
||||
if (len <= 0 || len >= sizeof(rel_name))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ok = ok && zcbor_map_start_encode(zse, 2) &&
|
||||
zcbor_tstr_put_lit(zse, "n") &&
|
||||
zcbor_tstr_encode(zse, &(struct zcbor_string){.value = (const uint8_t *)rel_name, .len = strlen(rel_name)}) &&
|
||||
zcbor_tstr_put_lit(zse, "t") &&
|
||||
zcbor_tstr_encode(zse, &(struct zcbor_string){.value = (const uint8_t *)type_char, .len = 1}) &&
|
||||
zcbor_map_end_encode(zse, 2);
|
||||
|
||||
if (ok && recursive && entry.type == FS_DIR_ENTRY_DIR)
|
||||
{
|
||||
char child_abs_path[FS_MGMT_MAX_PATH_LEN];
|
||||
int len = snprintk(child_abs_path, sizeof(child_abs_path), "%s/%s", abs_path, entry.name);
|
||||
if (len <= 0 || len >= sizeof(child_abs_path))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
ok = fs_mgmt_encode_entries(zse, child_abs_path, rel_name, true);
|
||||
}
|
||||
}
|
||||
|
||||
fs_closedir(&dirp);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int custom_ls_handler(struct smp_streamer *ctxt)
|
||||
{
|
||||
int file_count = 0;
|
||||
char path[FS_MGMT_MAX_PATH_LEN] = "";
|
||||
|
||||
zcbor_state_t *zsd = ctxt->reader->zs;
|
||||
zcbor_state_t *zse = ctxt->writer->zs;
|
||||
|
||||
/* --- DECODING --- */
|
||||
struct zcbor_string res_path = {0};
|
||||
bool path_found = false;
|
||||
|
||||
if (zcbor_map_start_decode(zsd))
|
||||
{
|
||||
while (zcbor_tstr_decode(zsd, &res_path))
|
||||
{
|
||||
if (res_path.len == 4 && memcmp(res_path.value, "path", 4) == 0)
|
||||
{
|
||||
struct zcbor_string path_val;
|
||||
if (zcbor_tstr_decode(zsd, &path_val))
|
||||
{
|
||||
int len = MIN(path_val.len, sizeof(path) - 1);
|
||||
memcpy(path, path_val.value, len);
|
||||
path[len] = '\0';
|
||||
path_found = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
zcbor_any_skip(zsd, NULL);
|
||||
}
|
||||
}
|
||||
zcbor_map_end_decode(zsd);
|
||||
}
|
||||
|
||||
// If no path is provided, default to root
|
||||
if (!path_found || strlen(path) == 0)
|
||||
{
|
||||
strcpy(path, "/");
|
||||
}
|
||||
|
||||
/* --- PROCESSING & ENCODING --- */
|
||||
int rc = fs_mgmt_count_entries(path, false, &file_count);
|
||||
if (rc != 0)
|
||||
{
|
||||
return (rc == -ENOENT) ? MGMT_ERR_ENOENT : MGMT_ERR_EUNKNOWN;
|
||||
}
|
||||
|
||||
bool ok = zcbor_tstr_put_lit(zse, "files") && zcbor_list_start_encode(zse, file_count);
|
||||
ok = ok && fs_mgmt_encode_entries(zse, path, "", false);
|
||||
ok = ok && zcbor_list_end_encode(zse, file_count);
|
||||
|
||||
return ok ? 0 : MGMT_ERR_ENOMEM;
|
||||
}
|
||||
|
||||
static int custom_rm_handler(struct smp_streamer *ctxt)
|
||||
{
|
||||
char path[64] = {0};
|
||||
zcbor_state_t *zsd = ctxt->reader->zs;
|
||||
zcbor_state_t *zse = ctxt->writer->zs;
|
||||
bool path_found = false;
|
||||
struct zcbor_string key;
|
||||
|
||||
/* --- DECODING --- */
|
||||
if (!zcbor_map_start_decode(zsd))
|
||||
{
|
||||
return MGMT_ERR_EINVAL;
|
||||
}
|
||||
|
||||
while (zcbor_tstr_decode(zsd, &key))
|
||||
{
|
||||
if (key.len == 4 && memcmp(key.value, "path", 4) == 0)
|
||||
{
|
||||
struct zcbor_string val;
|
||||
if (zcbor_tstr_decode(zsd, &val))
|
||||
{
|
||||
size_t len = MIN(val.len, sizeof(path) - 1);
|
||||
memcpy(path, val.value, len);
|
||||
path[len] = '\0';
|
||||
path_found = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
zcbor_any_skip(zsd, NULL);
|
||||
}
|
||||
}
|
||||
zcbor_map_end_decode(zsd);
|
||||
|
||||
if (!path_found)
|
||||
{
|
||||
return MGMT_ERR_EINVAL;
|
||||
}
|
||||
|
||||
/* --- PROCESSING --- */
|
||||
int rc = fs_unlink(path);
|
||||
|
||||
/* --- ENCODING RESPONSE --- */
|
||||
// Return the filesystem result code (0 = success)
|
||||
bool ok = zcbor_tstr_put_lit(zse, "rc") && zcbor_int32_put(zse, rc);
|
||||
|
||||
if (rc != 0)
|
||||
{
|
||||
LOG_WRN("Failed to remove %s: %d", path, rc);
|
||||
// Optional: map to more specific mgmt errors
|
||||
return (rc == -ENOENT) ? MGMT_ERR_ENOENT : MGMT_ERR_EUNKNOWN;
|
||||
}
|
||||
|
||||
return ok ? 0 : MGMT_ERR_ENOMEM;
|
||||
}
|
||||
|
||||
static const struct mgmt_handler custom_handlers[] = {
|
||||
[CMD_LS] = {
|
||||
.mh_read = custom_ls_handler,
|
||||
.mh_write = NULL},
|
||||
[CMD_RM] = {
|
||||
.mh_read = NULL,
|
||||
.mh_write = custom_rm_handler // Use WRITE for delete operations
|
||||
},
|
||||
};
|
||||
|
||||
static struct mgmt_group custom_group = {
|
||||
.mg_handlers = custom_handlers,
|
||||
.mg_handlers_count = ARRAY_SIZE(custom_handlers),
|
||||
.mg_group_id = CUSTOM_GROUP_ID,
|
||||
};
|
||||
|
||||
#endif /* CONFIG_FS_MGMT_MCUMGR_HANDLER */
|
||||
|
||||
int fs_mgmt_init(void)
|
||||
{
|
||||
int rc;
|
||||
LOG_DBG("Initializing filesystem management module");
|
||||
rc = fs_mount(&fs_storage_mnt);
|
||||
if (rc)
|
||||
{
|
||||
LOG_ERR("Filesystem mount failed (err %d)", rc);
|
||||
return rc;
|
||||
}
|
||||
else
|
||||
{
|
||||
struct fs_statvfs stat;
|
||||
uint64_t total;
|
||||
uint64_t free;
|
||||
|
||||
LOG_DBG("Filesystem mounted successfully at %s", fs_storage_mnt.mnt_point);
|
||||
|
||||
rc = fs_statvfs(fs_storage_mnt.mnt_point, &stat);
|
||||
if (rc == 0)
|
||||
{
|
||||
total = (uint64_t)stat.f_blocks * (uint64_t)stat.f_frsize;
|
||||
free = (uint64_t)stat.f_bfree * (uint64_t)stat.f_frsize;
|
||||
uint64_t total_kb = total / 1024;
|
||||
uint64_t free_kb = free / 1024;
|
||||
uint64_t used_kb = total_kb - free_kb;
|
||||
uint64_t used_pct_x10 = 0;
|
||||
LOG_DBG("Filesystem total size/used/free size: %4llu/%4llu/%4llu KB",
|
||||
(unsigned long long)total_kb,
|
||||
(unsigned long long)used_kb,
|
||||
(unsigned long long)free_kb);
|
||||
if (total_kb > 0)
|
||||
{
|
||||
used_pct_x10 = (used_kb * 1000) / total_kb;
|
||||
}
|
||||
if (used_pct_x10 >= 900)
|
||||
{
|
||||
LOG_ERR("Filesystem used: %llu.%llu%%",
|
||||
(unsigned long long)(used_pct_x10 / 10),
|
||||
(unsigned long long)(used_pct_x10 % 10));
|
||||
}
|
||||
else if (used_pct_x10 >= 750)
|
||||
{
|
||||
LOG_WRN("Filesystem used: %llu.%llu%%",
|
||||
(unsigned long long)(used_pct_x10 / 10),
|
||||
(unsigned long long)(used_pct_x10 % 10));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DBG("Filesystem used: %llu.%llu%%",
|
||||
(unsigned long long)(used_pct_x10 / 10),
|
||||
(unsigned long long)(used_pct_x10 % 10));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_WRN("Filesystem statvfs failed (err %d)", rc);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CONFIG_FS_MGMT_MCUMGR_HANDLER
|
||||
mgmt_register_group(&custom_group);
|
||||
LOG_DBG("Custom MCUMGR group registered with ID %d", CUSTOM_GROUP_ID);
|
||||
#endif /* CONFIG_FS_MGMT_MCUMGR_HANDLER */
|
||||
|
||||
return 0;
|
||||
}
|
||||
16
firmware/libs/game_mgmt/CMakeLists.txt
Normal file
16
firmware/libs/game_mgmt/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
if(CONFIG_GAME_MGMT)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(
|
||||
src/game_mgmt.c
|
||||
src/game_mgmt_coap.c
|
||||
src/game_mgmt_timing.c
|
||||
)
|
||||
if(CONFIG_LASERTAG_ROLE_LEADER)
|
||||
zephyr_library_sources(src/game_mgmt_thread.c)
|
||||
endif()
|
||||
if(CONFIG_LASERTAG_ROLE_LEADER)
|
||||
zephyr_library_sources(src/game_mgmt_device_list.c)
|
||||
endif()
|
||||
zephyr_include_directories(include)
|
||||
zephyr_library_include_directories(include_lib_only)
|
||||
endif()
|
||||
41
firmware/libs/game_mgmt/Kconfig
Normal file
41
firmware/libs/game_mgmt/Kconfig
Normal file
@@ -0,0 +1,41 @@
|
||||
menuconfig GAME_MGMT
|
||||
bool "Game Management"
|
||||
# select BT
|
||||
select OPENTHREAD
|
||||
select OPENTHREAD_COAP
|
||||
select LASERTAG_UTILS
|
||||
select AUDIO
|
||||
select ENTROPY_GENERATOR
|
||||
help
|
||||
Library for managing game states and logic in the lasertag device.
|
||||
|
||||
if GAME_MGMT
|
||||
config GAME_MGMT_SHELL
|
||||
bool "Enable shell commands for Game Management"
|
||||
select SHELL
|
||||
default n
|
||||
|
||||
config GAME_MGMT_BEACON_INTERVAL_S
|
||||
int "Game Management beacon interval (s)"
|
||||
default 5
|
||||
range 1 60
|
||||
help
|
||||
Interval in milliseconds for sending leader beacons.
|
||||
config GAME_MGMT_BEACON_THREAD_PRIORITY
|
||||
int "Game Management beacon thread priority"
|
||||
default 10
|
||||
range 0 10
|
||||
help
|
||||
Thread priority for the Game Management beaconing thread (leader device).
|
||||
config GAME_MGMT_BEACON_THREAD_STACK_SIZE
|
||||
int "Game Management beacon thread stack size"
|
||||
default 1024
|
||||
range 256 4096
|
||||
help
|
||||
Stack size for the Game Management beaconing thread (leader device).
|
||||
|
||||
# Logging configuration for the Game Management module
|
||||
module = GAME_MGMT
|
||||
module-str = game_mgmt
|
||||
source "subsys/logging/Kconfig.template.log_config"
|
||||
endif
|
||||
200
firmware/libs/game_mgmt/include/game_mgmt.h
Normal file
200
firmware/libs/game_mgmt/include/game_mgmt.h
Normal file
@@ -0,0 +1,200 @@
|
||||
#ifndef GAME_MGMT_H
|
||||
#define GAME_MGMT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <openthread/thread.h>
|
||||
#include <lasertag_utils.h>
|
||||
|
||||
/**
|
||||
* @brief System states for the Lasertag devices.
|
||||
* These states define the behavior of the device in the network.
|
||||
*/
|
||||
typedef enum {
|
||||
SYS_STATE_NO_CHANGE = 0x00, /* Placeholder for no state change */
|
||||
SYS_STATE_IDLE = 0x01, /* Device is on but inactive */
|
||||
SYS_STATE_LOBBY = 0x02, /* Discovery phase: Leader sends beacon, Nodes reply */
|
||||
SYS_STATE_STARTING = 0x03, /* Countdown phase before match start */
|
||||
SYS_STATE_RUNNING = 0x04, /* Match is active: IR and local logic enabled */
|
||||
SYS_STATE_POST_GAME = 0x05 /* Match ended: Data collection and review */
|
||||
} sys_state_t;
|
||||
|
||||
/**
|
||||
* @brief Game control command structure for THREAD communication.
|
||||
* This structure is used to send control commands (start/end game, set ID)
|
||||
* over the Thread network. It is designed to be flexible for future
|
||||
* expansion.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
GAME_CTRL_CMD_START_GAME = 0x00, /* Command to start a game, includes start time and duration */
|
||||
GAME_CTRL_CMD_REQUEST_ABORT_START_NO_TIME_SYNC = 0x01, /* Request to abort a pending game start when sender has no time sync */
|
||||
GAME_CTRL_CMD_ABORT_START = 0x0F, /* Command to abort a pending game start */
|
||||
GAME_CTRL_CMD_END_GAME = 0x20, /* Command to end the current game */
|
||||
GAME_CTRL_CMD_SET_ID = 0x30, /* Command to set the game ID */
|
||||
// Future commands can be added here
|
||||
} game_ctrl_command_t;
|
||||
|
||||
/**
|
||||
* @brief Payload structure for game control messages sent over Thread.
|
||||
* The union allows for different data formats based on the command type.
|
||||
*/
|
||||
typedef struct __packed
|
||||
{
|
||||
game_ctrl_command_t command;
|
||||
union
|
||||
{
|
||||
struct
|
||||
{
|
||||
uint32_t start_time; // lower 32 bits of OpenThread network time in microseconds
|
||||
uint32_t duration; // in seconds, maximum 49 days (2^32 seconds), should be sufficient for any match ;)
|
||||
} start_game;
|
||||
struct
|
||||
{
|
||||
uint64_t game_id; // Unique identifier for the game, can be used for stats tracking or match history
|
||||
} game_id;
|
||||
uint8_t raw[16]; // For future expansion or custom commands
|
||||
} data;
|
||||
} game_control_payload_t;
|
||||
|
||||
/**
|
||||
* @brief Leader beacon payload sent periodically during lobby/discovery.
|
||||
*/
|
||||
typedef struct __packed
|
||||
{
|
||||
uint64_t game_id;
|
||||
} game_leader_beacon_payload_t;
|
||||
|
||||
/**
|
||||
* @brief Player presence payload sent as unicast response to a leader beacon.
|
||||
*/
|
||||
typedef struct __packed
|
||||
{
|
||||
lasertag_device_type_t device_type;
|
||||
uint8_t player_id;
|
||||
uint8_t team_id;
|
||||
} game_player_presence_payload_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t eui64[8];
|
||||
lasertag_device_type_t device_type;
|
||||
uint8_t player_id;
|
||||
uint8_t team_id;
|
||||
uint8_t missed;
|
||||
} game_device_info_t;
|
||||
|
||||
/**
|
||||
* @brief Callback for state changes.
|
||||
* Allows apps/modules to react when the system transitions (e.g., UI updates).
|
||||
*/
|
||||
typedef void (*game_mgmt_state_cb_t)(sys_state_t new_state);
|
||||
|
||||
/**
|
||||
* @brief Initialize the game management module.
|
||||
* Sets the initial state and prepares timers/workqueues.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
int game_mgmt_init(void);
|
||||
|
||||
/**
|
||||
* @brief Send a game control payload as Thread multicast CoAP message.
|
||||
* Uses realm-local all-nodes multicast address `ff03::1`.
|
||||
* @param payload Payload to send.
|
||||
* @return 0 on success, negative errno-style value on failure.
|
||||
*/
|
||||
int game_mgmt_send_control_multicast(const game_control_payload_t *payload);
|
||||
|
||||
/**
|
||||
* @brief Send a game control payload as Thread unicast CoAP message.
|
||||
* @param peer_addr_str IPv6 destination address string.
|
||||
* @param payload Payload to send.
|
||||
* @return 0 on success, negative errno-style value on failure.
|
||||
*/
|
||||
int game_mgmt_send_control_unicast(const otIp6Address *peer_addr, const game_control_payload_t *payload);
|
||||
|
||||
/**
|
||||
* @brief Generic multicast sender for Thread CoAP messages.
|
||||
* Uses realm-local all-nodes multicast address `ff03::1`.
|
||||
* @param uri_path CoAP URI path (without leading slash), e.g. `g`.
|
||||
* @param payload Raw payload buffer.
|
||||
* @param payload_len Payload size in bytes.
|
||||
* @return 0 on success, negative errno-style value on failure.
|
||||
*/
|
||||
int game_mgmt_send_multicast(const char *uri_path, const void *payload, size_t payload_len);
|
||||
|
||||
/**
|
||||
* @brief Generic unicast sender for Thread CoAP messages.
|
||||
* @param peer_addr_str IPv6 destination address string.
|
||||
* @param uri_path CoAP URI path (without leading slash), e.g. `g`.
|
||||
* @param payload Raw payload buffer.
|
||||
* @param payload_len Payload size in bytes.
|
||||
* @return 0 on success, negative errno-style value on failure.
|
||||
*/
|
||||
int game_mgmt_send_unicast(const char *peer_addr_str,
|
||||
const char *uri_path,
|
||||
const void *payload,
|
||||
size_t payload_len);
|
||||
|
||||
/**
|
||||
* @brief Send leader beacon via multicast.
|
||||
* @param payload Beacon payload.
|
||||
* @return 0 on success, negative errno-style value on failure.
|
||||
*/
|
||||
int game_mgmt_send_leader_beacon_multicast(const game_leader_beacon_payload_t *payload);
|
||||
|
||||
/**
|
||||
* @brief Send player presence as unicast response to leader.
|
||||
* @param leader_addr_str IPv6 address string of the leader.
|
||||
* @param payload Player presence payload.
|
||||
* @return 0 on success, negative errno-style value on failure.
|
||||
*/
|
||||
int game_mgmt_send_player_presence_unicast(const char *leader_addr_str,
|
||||
const game_player_presence_payload_t *payload);
|
||||
|
||||
/**
|
||||
* @brief Set the system state and trigger corresponding actions.
|
||||
* Leader: Starts/stops beacons. Nodes: Starts/stops heartbeats.
|
||||
* @param state The target state to transition to.
|
||||
*/
|
||||
void game_mgmt_set_state(sys_state_t state);
|
||||
|
||||
/**
|
||||
* @brief Returns the current system state.
|
||||
* @return Current sys_state_t value.
|
||||
*/
|
||||
sys_state_t game_mgmt_get_state(void);
|
||||
|
||||
/**
|
||||
* @brief Set the current game ID.
|
||||
* @param id The game ID to set.
|
||||
*/
|
||||
void game_mgmt_set_game_id(uint64_t id);
|
||||
|
||||
/**
|
||||
* @brief Get the current game ID.
|
||||
* @return The current game ID.
|
||||
*/
|
||||
uint64_t game_mgmt_get_game_id(void);
|
||||
|
||||
/**
|
||||
* @brief Registers a callback for state changes.
|
||||
* @param cb Function to be called on transition.
|
||||
*/
|
||||
void game_mgmt_register_state_cb(game_mgmt_state_cb_t cb);
|
||||
|
||||
/**
|
||||
* @brief Set the unicast address of the current leader.
|
||||
* This is used for sending unicast messages (e.g., presence responses).
|
||||
* @param addr Pointer to the leader's IPv6 address, or NULL to clear.
|
||||
*/
|
||||
void game_mgmt_set_leader_unicast_addr(const otIp6Address *addr);
|
||||
|
||||
/**
|
||||
* @brief Get the unicast address of the current leader.
|
||||
* @param addr Pointer to an otIp6Address struct to be filled with the leader's address.
|
||||
* @return true if a valid leader address is set, false if not.
|
||||
*/
|
||||
bool game_mgmt_get_leader_unicast_addr(otIp6Address *addr);
|
||||
|
||||
#endif /* GAME_MGMT_H */
|
||||
171
firmware/libs/game_mgmt/src/game_mgmt.c
Normal file
171
firmware/libs/game_mgmt/src/game_mgmt.c
Normal file
@@ -0,0 +1,171 @@
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <thread_mgmt.h>
|
||||
#include <game_mgmt.h>
|
||||
#include <lasertag_utils.h>
|
||||
#include <game_mgmt_coap.h>
|
||||
#include <game_mgmt_timing.h>
|
||||
#include <game_mgmt_device_list.h>
|
||||
|
||||
LOG_MODULE_REGISTER(game_mgmt, CONFIG_GAME_MGMT_LOG_LEVEL);
|
||||
|
||||
static sys_state_t g_current_state = SYS_STATE_IDLE;
|
||||
static uint64_t g_current_game_id = 0;
|
||||
static otIp6Address g_leader_unicast_addr;
|
||||
|
||||
int game_mgmt_init(void)
|
||||
{
|
||||
int err = game_mgmt_coap_init();
|
||||
if (err)
|
||||
{
|
||||
LOG_ERR("Failed to initialize CoAP service: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_LASERTAG_ROLE_LEADER)
|
||||
static const char* device_type = "Leader";
|
||||
game_mgmt_device_list_init();
|
||||
#elif IS_ENABLED(CONFIG_LASERTAG_ROLE_WEAPON)
|
||||
static const char* device_type = "Weapon";
|
||||
#elif IS_ENABLED(CONFIG_LASERTAG_ROLE_VEST)
|
||||
static const char* device_type = "Vest";
|
||||
#else
|
||||
static const char* device_type = "Unknown";
|
||||
#endif
|
||||
|
||||
LOG_INF("Game management initialized. Device type: " FORMAT_BRIGHT_GREEN_BOLD("%s"), device_type);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void game_mgmt_set_leader_unicast_addr(const otIp6Address *addr)
|
||||
{
|
||||
if (addr)
|
||||
{
|
||||
g_leader_unicast_addr = *addr;
|
||||
}
|
||||
else
|
||||
{
|
||||
memset(&g_leader_unicast_addr, 0, sizeof(g_leader_unicast_addr));
|
||||
}
|
||||
}
|
||||
|
||||
bool game_mgmt_get_leader_unicast_addr(otIp6Address *addr)
|
||||
{
|
||||
if (addr && !otIp6IsAddressUnspecified(&g_leader_unicast_addr))
|
||||
{
|
||||
*addr = g_leader_unicast_addr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void game_mgmt_set_state(sys_state_t state)
|
||||
{
|
||||
if (g_current_state == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("State change: %d -> %d", g_current_state, state);
|
||||
g_current_state = state;
|
||||
}
|
||||
|
||||
sys_state_t game_mgmt_get_state(void)
|
||||
{
|
||||
return g_current_state;
|
||||
}
|
||||
|
||||
void game_mgmt_set_game_id(uint64_t id)
|
||||
{
|
||||
if (g_current_game_id == id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_current_game_id = id;
|
||||
LOG_DBG("Game ID updated: 0x%llx", id);
|
||||
}
|
||||
|
||||
uint64_t game_mgmt_get_game_id(void)
|
||||
{
|
||||
return g_current_game_id;
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_GAME_MGMT_SHELL)
|
||||
#include <zephyr/shell/shell.h>
|
||||
|
||||
static int cmd_game_start(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
char *endptr = NULL;
|
||||
uint32_t delay_s = (uint32_t)strtoul(argv[1], &endptr, 10);
|
||||
if ((endptr == argv[1]) || (*endptr != '\0'))
|
||||
{
|
||||
shell_error(sh, "Invalid delay_s: %s", argv[1]);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
uint32_t duration_s = 600;
|
||||
if (argc > 2)
|
||||
{
|
||||
endptr = NULL;
|
||||
duration_s = (uint32_t)strtoul(argv[2], &endptr, 10);
|
||||
if ((endptr == argv[2]) || (*endptr != '\0'))
|
||||
{
|
||||
shell_error(sh, "Invalid duration_s: %s", argv[2]);
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
game_control_payload_t payload = {.command = GAME_CTRL_CMD_START_GAME};
|
||||
|
||||
uint64_t now = thread_mgmt_get_network_time();
|
||||
if (now == 0)
|
||||
{
|
||||
shell_error(sh, "Network time not synchronized, cannot start game.");
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
payload.data.start_game.start_time = (uint32_t)(now + (delay_s * 1000000ULL));
|
||||
payload.data.start_game.duration = duration_s;
|
||||
|
||||
int err = game_mgmt_send_control_multicast(&payload);
|
||||
if (err)
|
||||
{
|
||||
shell_error(sh, "game_mgmt_send_control_multicast failed: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
shell_print(sh, "Start broadcast sent for T+%u s (duration: %u s).", delay_s, duration_s);
|
||||
game_mgmt_schedule_start(game_mgmt_expand_t32_us(payload.data.start_game.start_time, now), duration_s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_game_abort(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
ARG_UNUSED(argc);
|
||||
ARG_UNUSED(argv);
|
||||
|
||||
game_control_payload_t payload = {
|
||||
.command = GAME_CTRL_CMD_ABORT_START,
|
||||
};
|
||||
|
||||
int err = game_mgmt_send_control_multicast(&payload);
|
||||
if (err)
|
||||
{
|
||||
shell_error(sh, "Failed to send abort broadcast: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
game_mgmt_cancel_scheduled_start("manual shell abort");
|
||||
shell_print(sh, "Abort broadcast sent.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
SHELL_STATIC_SUBCMD_SET_CREATE(game_sub,
|
||||
SHELL_CMD_ARG(start, NULL, "<delay_s> [duration_s]", cmd_game_start, 2, 1),
|
||||
SHELL_CMD_ARG(abort, NULL, "", cmd_game_abort, 1, 0),
|
||||
SHELL_SUBCMD_SET_END);
|
||||
|
||||
SHELL_CMD_REGISTER(game, &game_sub, "Game Management", NULL);
|
||||
#endif
|
||||
9
firmware/libs/ir/CMakeLists.txt
Normal file
9
firmware/libs/ir/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
if(CONFIG_IR_SEND)
|
||||
add_subdirectory(send)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
|
||||
if(CONFIG_IR_RECV)
|
||||
add_subdirectory(recv)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
2
firmware/libs/ir/Kconfig
Normal file
2
firmware/libs/ir/Kconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
rsource "recv/Kconfig"
|
||||
rsource "send/Kconfig"
|
||||
19
firmware/libs/ir/include/ir.h
Normal file
19
firmware/libs/ir/include/ir.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef IR_H
|
||||
#define IR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// structure representing a decoded IR packet
|
||||
typedef struct {
|
||||
union {
|
||||
uint8_t bytes[3];
|
||||
struct {
|
||||
uint32_t type : 3;
|
||||
uint32_t id : 8; /* Ersetzt shooter_id / healer_id */
|
||||
uint32_t value : 5; /* Ersetzt damage / amount */
|
||||
uint32_t crc : 8;
|
||||
} fields;
|
||||
} data;
|
||||
} __attribute__((packed)) ir_packet_t;
|
||||
|
||||
#endif /* IR_H */
|
||||
6
firmware/libs/ir/recv/CMakeLists.txt
Normal file
6
firmware/libs/ir/recv/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
if(CONFIG_IR_RECV)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/ir_recv.c)
|
||||
zephyr_include_directories(include)
|
||||
zephyr_library_link_libraries_ifdef(CONFIG_NRFX nrfx)
|
||||
endif()
|
||||
89
firmware/libs/ir/recv/Kconfig
Normal file
89
firmware/libs/ir/recv/Kconfig
Normal file
@@ -0,0 +1,89 @@
|
||||
menuconfig IR_RECV
|
||||
bool "IR Receiver"
|
||||
help
|
||||
Enable support for receiving IR signals using the ADC and DMA.
|
||||
|
||||
if IR_RECV
|
||||
config IR_RECV_HW_DEPENDENCIES
|
||||
bool
|
||||
default y if !IR_RECV_SIMULATOR
|
||||
select ADC
|
||||
select NRFX_GPPI
|
||||
select NRFX_TIMER
|
||||
select NRFX_TIMER1
|
||||
select NRFX_PPI
|
||||
select NRFX_SAADC
|
||||
|
||||
config IR_RECV_SIMULATOR
|
||||
bool "Enable IR receiver simulator"
|
||||
select ENTROPY_GENERATOR
|
||||
help
|
||||
Replaces real ADC/Hardware input with a software-based sample generator
|
||||
for protocol testing.
|
||||
|
||||
config IR_RECV_BUFFER_COUNT
|
||||
int "Number of DMA buffers"
|
||||
default 8
|
||||
range 2 16
|
||||
help
|
||||
Number of buffers in the circular chain to handle CPU jitter.
|
||||
|
||||
config IR_RECV_SAMPLES_PER_BUFFER
|
||||
int "Samples per channel per buffer"
|
||||
default 32
|
||||
help
|
||||
Number of samples for each of the 4 channels (3x IR, 1x Vbat) per buffer.
|
||||
|
||||
config IR_RECV_INVERT_SIGNAL
|
||||
bool "Invert IR input signal"
|
||||
default n
|
||||
help
|
||||
Invert logic: High-level means IR carrier detected.
|
||||
|
||||
config IR_RECV_THREAD_PRIO_SIM
|
||||
int "Simulator thread priority"
|
||||
default 4
|
||||
range 0 20
|
||||
help
|
||||
Thread priority for the IR receive simulator.
|
||||
|
||||
config IR_RECV_THREAD_STACK_SIM
|
||||
int "Simulator thread stack size"
|
||||
default 1024
|
||||
range 256 8192
|
||||
help
|
||||
Stack size in bytes for the IR receive simulator thread.
|
||||
|
||||
config IR_RECV_THREAD_PRIO_ADC
|
||||
int "ADC sampling thread priority"
|
||||
default 2
|
||||
range 0 20
|
||||
help
|
||||
Thread priority for hardware ADC sampling.
|
||||
|
||||
config IR_RECV_THREAD_STACK_ADC
|
||||
int "ADC sampling thread stack size"
|
||||
default 1024
|
||||
range 256 8192
|
||||
help
|
||||
Stack size in bytes for the hardware ADC sampling thread.
|
||||
|
||||
config IR_RECV_THREAD_PRIO_PROCESS
|
||||
int "IR processing thread priority"
|
||||
default 3
|
||||
range 0 20
|
||||
help
|
||||
Thread priority for IR buffer processing.
|
||||
|
||||
config IR_RECV_THREAD_STACK_PROCESS
|
||||
int "IR processing thread stack size"
|
||||
default 2048
|
||||
range 256 8192
|
||||
help
|
||||
Stack size in bytes for the IR processing thread.
|
||||
|
||||
# Logging configuration for the IR Receiver module
|
||||
module = IR_RECV
|
||||
module-str = ir_recv
|
||||
source "subsys/logging/Kconfig.template.log_config"
|
||||
endif
|
||||
29
firmware/libs/ir/recv/include/ir_recv.h
Normal file
29
firmware/libs/ir/recv/include/ir_recv.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifndef IR_RECV_H
|
||||
#define IR_RECV_H
|
||||
|
||||
#include <ir.h>
|
||||
|
||||
/**
|
||||
* @brief Initialize IR receive pipeline (stub).
|
||||
*
|
||||
* Intended to configure GPIO/interrupts/ppi for future implementation.
|
||||
* @return 0 on success, negative errno otherwise.
|
||||
*/
|
||||
int ir_recv_init(void);
|
||||
|
||||
#ifdef CONFIG_IR_RECV_SIMULATOR
|
||||
|
||||
/* Configuration for injecting signal errors */
|
||||
typedef struct {
|
||||
uint8_t noise_flips_per_8; /* Noise: number of inverted samples per block of 8 (0, 1, 2) */
|
||||
uint8_t jitter_mark; /* Jitter for mark pulse: +/- samples (e.g. 2) */
|
||||
uint8_t jitter_space_0; /* Jitter for space0: +/- samples (e.g. 4) */
|
||||
uint8_t jitter_space_1; /* Jitter for space1: +/- samples (e.g. 2) */
|
||||
} ir_sim_error_t;
|
||||
|
||||
/* err can be NULL to send a perfect signal */
|
||||
void ir_recv_sim_send_packet(ir_packet_t *packet, ir_sim_error_t *err);
|
||||
|
||||
#endif
|
||||
|
||||
#endif /* IR_RECV_H */
|
||||
483
firmware/libs/ir/recv/src/ir_recv.c
Normal file
483
firmware/libs/ir/recv/src/ir_recv.c
Normal file
@@ -0,0 +1,483 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <ir_recv.h>
|
||||
#include <ir.h>
|
||||
#include <lasertag_utils.h>
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include "ir_recv.h"
|
||||
#include "lasertag_utils.h"
|
||||
|
||||
LOG_MODULE_REGISTER(ir_recv, CONFIG_IR_RECV_LOG_LEVEL);
|
||||
|
||||
#define ADC_CHANNELS 4
|
||||
#define SAMPLES_PER_BUFFER CONFIG_IR_RECV_SAMPLES_PER_BUFFER
|
||||
#define BUFFER_COUNT CONFIG_IR_RECV_BUFFER_COUNT
|
||||
|
||||
static int16_t adc_buffers[BUFFER_COUNT][SAMPLES_PER_BUFFER * ADC_CHANNELS];
|
||||
static uint8_t write_idx = 0, read_idx = 0;
|
||||
static struct k_sem adc_sem;
|
||||
|
||||
/* --- Enhanced Simulator --- */
|
||||
#ifdef CONFIG_IR_RECV_SIMULATOR
|
||||
#include <zephyr/random/random.h>
|
||||
|
||||
K_SEM_DEFINE(sim_start_sem, 0, 1);
|
||||
|
||||
#define SIM_MAX_SAMPLES 1000
|
||||
static bool sim_buffer[SIM_MAX_SAMPLES];
|
||||
static uint32_t sim_total_samples = 0;
|
||||
static bool sim_trigger = false;
|
||||
static uint32_t sim_sample_pos = 0;
|
||||
|
||||
/* Hilfsfunktion für den Jitter: Liefert einen Zufallswert zwischen -max und +max */
|
||||
static int get_jitter(uint8_t max_jitter)
|
||||
{
|
||||
if (max_jitter == 0)
|
||||
return 0;
|
||||
return (int)(sys_rand32_get() % (max_jitter * 2 + 1)) - max_jitter;
|
||||
}
|
||||
|
||||
void ir_recv_sim_send_packet(ir_packet_t *packet, ir_sim_error_t *err)
|
||||
{
|
||||
/* Blockieren, bis vorheriges Paket abgearbeitet ist */
|
||||
while (sim_trigger)
|
||||
{
|
||||
k_msleep(5);
|
||||
}
|
||||
|
||||
ir_packet_t pkt;
|
||||
memcpy(&pkt, packet, sizeof(ir_packet_t));
|
||||
pkt.data.fields.crc = lastertag_crc8(pkt.data.bytes, 2);
|
||||
|
||||
ir_sim_error_t default_err = {0};
|
||||
if (err == NULL)
|
||||
{
|
||||
err = &default_err;
|
||||
}
|
||||
|
||||
sim_total_samples = 0;
|
||||
|
||||
/* 1. Header Mark */
|
||||
int m_len = 32 + get_jitter(err->jitter_mark);
|
||||
for (int i = 0; i < m_len && sim_total_samples < SIM_MAX_SAMPLES; i++)
|
||||
{
|
||||
sim_buffer[sim_total_samples++] = 1;
|
||||
}
|
||||
|
||||
/* 2. Header Gap */
|
||||
int g_len = 8 + get_jitter(err->jitter_space_0);
|
||||
for (int i = 0; i < g_len && sim_total_samples < SIM_MAX_SAMPLES; i++)
|
||||
{
|
||||
sim_buffer[sim_total_samples++] = 0;
|
||||
}
|
||||
|
||||
/* 3. Payload Bits (24 Bit) */
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
bool bit = (pkt.data.bytes[i / 8] >> (i % 8)) & 1;
|
||||
|
||||
/* Space Phase */
|
||||
int s_base = bit ? 16 : 8;
|
||||
int s_jit = bit ? get_jitter(err->jitter_space_1) : get_jitter(err->jitter_space_0);
|
||||
int s_len = s_base + s_jit;
|
||||
for (int j = 0; j < s_len && sim_total_samples < SIM_MAX_SAMPLES; j++)
|
||||
{
|
||||
sim_buffer[sim_total_samples++] = 0;
|
||||
}
|
||||
|
||||
/* Mark Phase */
|
||||
int bm_len = 8 + get_jitter(err->jitter_mark);
|
||||
for (int j = 0; j < bm_len && sim_total_samples < SIM_MAX_SAMPLES; j++)
|
||||
{
|
||||
sim_buffer[sim_total_samples++] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 4. Rauschen injizieren (Bit Flips) */
|
||||
if (err->noise_flips_per_8 > 0)
|
||||
{
|
||||
for (uint32_t i = 0; i < sim_total_samples; i += 8)
|
||||
{
|
||||
for (int f = 0; f < err->noise_flips_per_8; f++)
|
||||
{
|
||||
uint32_t flip_idx = i + (sys_rand32_get() % 8);
|
||||
if (flip_idx < sim_total_samples)
|
||||
{
|
||||
sim_buffer[flip_idx] = !sim_buffer[flip_idx]; /* Bit kippen */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sim_sample_pos = 0;
|
||||
sim_trigger = true;
|
||||
|
||||
sim_sample_pos = 0;
|
||||
sim_trigger = true;
|
||||
k_sem_give(&sim_start_sem);
|
||||
|
||||
LOG_DBG("Simulator: Queued (Type: %u, CRC: 0x%02X), Total Samples: %u",
|
||||
pkt.data.fields.type, pkt.data.fields.crc, sim_total_samples);
|
||||
}
|
||||
|
||||
void ir_recv_sim_thread(void *p1, void *p2, void *p3)
|
||||
{
|
||||
while (1)
|
||||
{
|
||||
if (!sim_trigger)
|
||||
{
|
||||
k_sem_take(&sim_start_sem, K_FOREVER);
|
||||
}
|
||||
|
||||
k_usleep(75 * SAMPLES_PER_BUFFER);
|
||||
if (!sim_trigger)
|
||||
continue;
|
||||
|
||||
int16_t *buf = adc_buffers[write_idx];
|
||||
for (int i = 0; i < SAMPLES_PER_BUFFER; i++)
|
||||
{
|
||||
bool level = 0;
|
||||
if (sim_sample_pos < sim_total_samples)
|
||||
{
|
||||
level = sim_buffer[sim_sample_pos];
|
||||
}
|
||||
sim_sample_pos++; /* Unbedingtes Inkrement */
|
||||
|
||||
bool active = IS_ENABLED(CONFIG_IR_RECV_INVERT_SIGNAL) ? !level : level;
|
||||
buf[i * ADC_CHANNELS] = active ? 0x0FFF : 0x0000;
|
||||
buf[i * ADC_CHANNELS + 3] = 2400; // VBat Dummy
|
||||
}
|
||||
write_idx = (write_idx + 1) % BUFFER_COUNT;
|
||||
k_sem_give(&adc_sem);
|
||||
|
||||
/* Nach dem Puffer noch eine kurze Pause einhalten, um das Paket abzuschließen */
|
||||
if (sim_sample_pos >= sim_total_samples + 50)
|
||||
{
|
||||
sim_trigger = false;
|
||||
LOG_DBG("Simulation sequence finished.");
|
||||
}
|
||||
}
|
||||
}
|
||||
K_THREAD_DEFINE(sim_tid, CONFIG_IR_RECV_THREAD_STACK_SIM, ir_recv_sim_thread, NULL, NULL, NULL,
|
||||
CONFIG_IR_RECV_THREAD_PRIO_SIM, 0, 0);
|
||||
#endif // CONFIG_IR_RECV_SIMULATOR
|
||||
|
||||
#ifndef CONFIG_IR_RECV_SIMULATOR
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#include <nrfx_timer.h>
|
||||
#include <helpers/nrfx_gppi.h>
|
||||
#include <hal/nrf_saadc.h>
|
||||
|
||||
/* ADC-Kanäle dynamisch aus dem DeviceTree (zephyr,user) laden */
|
||||
static const struct adc_dt_spec adc_channels[] = {
|
||||
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 0),
|
||||
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 1),
|
||||
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 2),
|
||||
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 3)
|
||||
};
|
||||
|
||||
static int hw_adc_setup(void)
|
||||
{
|
||||
int err;
|
||||
for (int i = 0; i < ARRAY_SIZE(adc_channels); i++) {
|
||||
if (err) {
|
||||
LOG_ERR("ADC controller for channel %d not ready", i);
|
||||
return err;
|
||||
}
|
||||
err = adc_channel_setup_dt(&adc_channels[i]);
|
||||
if (err) {
|
||||
LOG_ERR("Could not setup channel %d (err: %d)", i, err);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
LOG_DBG("Hardware ADC configured via DeviceTree (EasyDMA Mode).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Wir reservieren uns Timer 1 für den ADC-Takt */
|
||||
static nrfx_timer_t adc_timer = NRFX_TIMER_INSTANCE(1);
|
||||
|
||||
void hw_adc_thread(void *p1, void *p2, void *p3)
|
||||
{
|
||||
/* 1. Hardware Timer auf 1 MHz einstellen */
|
||||
nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG(NRF_TIMER_FREQ_1MHz);
|
||||
timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32;
|
||||
nrfx_timer_init(&adc_timer, &timer_cfg, NULL);
|
||||
|
||||
/* Compare-Event bei exakt 75 µs auslösen und Timer danach automatisch nullen */
|
||||
nrfx_timer_extended_compare(&adc_timer, NRF_TIMER_CC_CHANNEL0, 75,
|
||||
NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
|
||||
|
||||
/* 2. PPI: Verbinde das Timer-Event direkt in Hardware mit dem ADC-Sample-Trigger */
|
||||
nrfx_gppi_handle_t ppi_handle;
|
||||
int err = nrfx_gppi_conn_alloc(
|
||||
nrfx_timer_event_address_get(&adc_timer, NRF_TIMER_EVENT_COMPARE0),
|
||||
nrf_saadc_task_address_get(NRF_SAADC, NRF_SAADC_TASK_SAMPLE),
|
||||
&ppi_handle);
|
||||
if (err != 0) {
|
||||
LOG_ERR("GPPI connection alloc failed: %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Alles einschalten */
|
||||
nrfx_gppi_conn_enable(ppi_handle);
|
||||
nrfx_timer_enable(&adc_timer);
|
||||
|
||||
/* 3. Zephyr ADC-Sequenz (Ohne Software-Intervall!) */
|
||||
struct adc_sequence_options options = {
|
||||
.extra_samplings = SAMPLES_PER_BUFFER - 1,
|
||||
.interval_us = 0,
|
||||
};
|
||||
|
||||
struct adc_sequence sequence = {
|
||||
.options = &options,
|
||||
.channels = BIT(adc_channels[0].channel_id) |
|
||||
BIT(adc_channels[1].channel_id) |
|
||||
BIT(adc_channels[2].channel_id) |
|
||||
BIT(adc_channels[3].channel_id),
|
||||
.buffer_size = SAMPLES_PER_BUFFER * ADC_CHANNELS * sizeof(int16_t),
|
||||
.resolution = adc_channels[0].resolution,
|
||||
.oversampling = 0,
|
||||
.calibrate = false,
|
||||
};
|
||||
|
||||
while (1) {
|
||||
sequence.buffer = adc_buffers[write_idx];
|
||||
|
||||
/* Zephyr setzt den DMA-Speicher auf und wartet auf das Ende der 32 Samples.
|
||||
* Den Startschuss für jedes einzelne Sample feuert ab sofort im Hintergrund
|
||||
* das PPI-Modul exakt alle 75µs ab. Die CPU schläft hier tief und fest! */
|
||||
int err = adc_read(adc_channels[0].dev, &sequence);
|
||||
|
||||
if (err < 0) {
|
||||
LOG_ERR("ADC read error: %d", err);
|
||||
k_msleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
write_idx = (write_idx + 1) % BUFFER_COUNT;
|
||||
k_sem_give(&adc_sem);
|
||||
}
|
||||
}
|
||||
K_THREAD_DEFINE(hw_adc_tid, CONFIG_IR_RECV_THREAD_STACK_ADC, hw_adc_thread, NULL, NULL, NULL,
|
||||
CONFIG_IR_RECV_THREAD_PRIO_ADC, 0, 0);
|
||||
|
||||
#endif // !CONFIG_IR_RECV_SIMULATOR
|
||||
|
||||
typedef enum
|
||||
{
|
||||
IR_STATE_IDLE,
|
||||
IR_STATE_HEADER_SYNC,
|
||||
IR_STATE_WAIT_SPACE,
|
||||
IR_STATE_FIND_MARK,
|
||||
IR_STATE_SYNC_MARK,
|
||||
IR_STATE_VALIDATE
|
||||
} ir_state_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ir_state_t state;
|
||||
uint32_t sample_window;
|
||||
uint32_t bit_acc;
|
||||
uint16_t timer;
|
||||
uint8_t bit_count;
|
||||
uint8_t max_energy;
|
||||
uint16_t timer_at_max;
|
||||
uint8_t sync_window;
|
||||
} ir_ctx_t;
|
||||
|
||||
static ir_ctx_t channels[3];
|
||||
|
||||
static void process_ir_sample(ir_ctx_t *ctx, int16_t raw)
|
||||
{
|
||||
bool active = (raw > 2048);
|
||||
if (IS_ENABLED(CONFIG_IR_RECV_INVERT_SIGNAL))
|
||||
{
|
||||
active = !active;
|
||||
}
|
||||
|
||||
/* 32-Bit Shift-Register aktualisieren */
|
||||
ctx->sample_window = (ctx->sample_window << 1) | (active ? 1 : 0);
|
||||
|
||||
/* Energie über 32 Samples (Header) und 8 Samples (Bits) berechnen */
|
||||
uint8_t energy_32 = __builtin_popcount(ctx->sample_window);
|
||||
uint8_t energy_8 = __builtin_popcount(ctx->sample_window & 0xFF);
|
||||
|
||||
switch (ctx->state)
|
||||
{
|
||||
case IR_STATE_IDLE:
|
||||
if (energy_32 >= 24)
|
||||
{
|
||||
ctx->state = IR_STATE_HEADER_SYNC;
|
||||
ctx->timer = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case IR_STATE_HEADER_SYNC:
|
||||
ctx->timer++;
|
||||
/* Entspannt: 3 von 8 Samples dürfen verrauscht sein, es gilt trotzdem als Gap */
|
||||
if (energy_8 <= 3)
|
||||
{
|
||||
ctx->state = IR_STATE_FIND_MARK;
|
||||
ctx->timer = 0;
|
||||
ctx->bit_count = 0;
|
||||
ctx->bit_acc = 0;
|
||||
}
|
||||
else if (ctx->timer > 50)
|
||||
{
|
||||
ctx->state = IR_STATE_IDLE; /* Timeout */
|
||||
}
|
||||
break;
|
||||
|
||||
case IR_STATE_FIND_MARK:
|
||||
ctx->timer++;
|
||||
/* Trigger bei 4 bleibt. Ab 4 Einsen fangen wir an, das Maximum zu suchen */
|
||||
if (energy_8 >= 4)
|
||||
{
|
||||
ctx->state = IR_STATE_SYNC_MARK;
|
||||
ctx->max_energy = energy_8;
|
||||
ctx->timer_at_max = ctx->timer;
|
||||
ctx->sync_window = 0;
|
||||
}
|
||||
else if (ctx->timer > 40)
|
||||
{
|
||||
ctx->state = IR_STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case IR_STATE_SYNC_MARK:
|
||||
ctx->timer++;
|
||||
ctx->sync_window++;
|
||||
|
||||
if (energy_8 > ctx->max_energy)
|
||||
{
|
||||
ctx->max_energy = energy_8;
|
||||
ctx->timer_at_max = ctx->timer;
|
||||
}
|
||||
|
||||
if (ctx->sync_window >= 10)
|
||||
{
|
||||
/* Entspannt: Wenn der Peak mindestens 5 Einsen hat, ist es ein validierter Puls */
|
||||
if (ctx->max_energy >= 5)
|
||||
{
|
||||
bool bit = (ctx->timer_at_max >= 20);
|
||||
|
||||
ctx->bit_acc = (ctx->bit_acc >> 1) | (bit ? (1 << 23) : 0);
|
||||
|
||||
LOG_DBG("Bit %u: %d (T_Max:%u, E_Max:%u)",
|
||||
ctx->bit_count, bit, ctx->timer_at_max, ctx->max_energy);
|
||||
|
||||
if (++ctx->bit_count >= 24)
|
||||
{
|
||||
ctx->state = IR_STATE_VALIDATE;
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx->state = IR_STATE_WAIT_SPACE;
|
||||
|
||||
/* BITWEISER RESYNC: Ja, das funktioniert exakt wie gewünscht.
|
||||
* Der Timer wird so weit zurückgeschraubt, als ob er GENAU am
|
||||
* Peak (timer_at_max) auf 0 gesetzt worden wäre. */
|
||||
ctx->timer = ctx->timer - ctx->timer_at_max;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx->state = IR_STATE_IDLE;
|
||||
LOG_DBG("Mark sync failed. Max Energy: %u", ctx->max_energy);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case IR_STATE_WAIT_SPACE:
|
||||
ctx->timer++;
|
||||
|
||||
/* Entspannt: Auch hier lassen wir bis zu 3 Stör-Samples im Space zu */
|
||||
if (energy_8 <= 3)
|
||||
{
|
||||
ctx->state = IR_STATE_FIND_MARK;
|
||||
}
|
||||
else if (ctx->timer > 30)
|
||||
{
|
||||
ctx->state = IR_STATE_IDLE;
|
||||
LOG_DBG("Wait space timeout.");
|
||||
}
|
||||
break;
|
||||
|
||||
case IR_STATE_VALIDATE:
|
||||
{
|
||||
ir_packet_t p;
|
||||
p.data.bytes[0] = ctx->bit_acc & 0xFF;
|
||||
p.data.bytes[1] = (ctx->bit_acc >> 8) & 0xFF;
|
||||
p.data.bytes[2] = (ctx->bit_acc >> 16) & 0xFF;
|
||||
|
||||
if (lastertag_crc8(p.data.bytes, 2) == p.data.fields.crc)
|
||||
{
|
||||
LOG_DBG(FORMAT_BLUE_BOLD("VALID: Type %u, ID %u, Val %u"),
|
||||
p.data.fields.type,
|
||||
p.data.fields.id,
|
||||
p.data.fields.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_WRN("CRC Error! Acc: 0x%06X", ctx->bit_acc);
|
||||
}
|
||||
ctx->state = IR_STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Main processing thread for incoming ADC buffers.
|
||||
*/
|
||||
void ir_recv_thread(void *arg1, void *arg2, void *arg3)
|
||||
{
|
||||
while (1)
|
||||
{
|
||||
k_sem_take(&adc_sem, K_FOREVER);
|
||||
while (read_idx != write_idx)
|
||||
{
|
||||
int16_t *buf = adc_buffers[read_idx];
|
||||
for (int i = 0; i < SAMPLES_PER_BUFFER; i++)
|
||||
{
|
||||
/* Now this call matches the static function name above */
|
||||
process_ir_sample(&channels[0], buf[i * ADC_CHANNELS + 0]);
|
||||
process_ir_sample(&channels[1], buf[i * ADC_CHANNELS + 1]);
|
||||
process_ir_sample(&channels[2], buf[i * ADC_CHANNELS + 2]);
|
||||
}
|
||||
read_idx = (read_idx + 1) % BUFFER_COUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
K_THREAD_DEFINE(ir_recv_tid, CONFIG_IR_RECV_THREAD_STACK_PROCESS, ir_recv_thread, NULL, NULL, NULL,
|
||||
CONFIG_IR_RECV_THREAD_PRIO_PROCESS, 0, 0);
|
||||
|
||||
/**
|
||||
* @brief Initialization of the IR receiver module.
|
||||
*/
|
||||
int ir_recv_init(void)
|
||||
{
|
||||
k_sem_init(&adc_sem, 0, BUFFER_COUNT);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
channels[i].state = IR_STATE_IDLE;
|
||||
}
|
||||
|
||||
#ifndef CONFIG_IR_RECV_SIMULATOR
|
||||
int err = hw_adc_setup();
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_DBG("IR Receiver initialized. Mode: %s",
|
||||
IS_ENABLED(CONFIG_IR_RECV_SIMULATOR) ? "Simulator" : "Hardware");
|
||||
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user