Merge branch 'platformio' into 'main'

Platformio

See merge request msc/buenzliai!1
This commit is contained in:
Marcel Schuerch 2024-06-05 10:40:48 +02:00
commit 16b14972b4
57 changed files with 3565 additions and 6 deletions

49
.gitattributes vendored Normal file
View file

@ -0,0 +1,49 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
# ------------------------
# LFS
#-------------------------
# libraries and packages
*.a filter=lfs -crlf
*.apk filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs -crlf
*.dylib filter=lfs diff=lfs merge=lfs -text
*.exe filter=lfs -crlf
*.rar filter=lfs diff=lfs merge=lfs -text
*.unitypackage filter=lfs -crlf
*.zip filter=lfs -crlf
# models and playables
*.blend filter=lfs -crlf
*.wav filter=lfs diff=lfs merge=lfs -text
# images
*.cubemap filter=lfs -crlf
*.exr filter=lfs -crlf
*.fbx filter=lfs -crlf
*.hdr filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs -crlf
*.gif filter=lfs -crlf
*.mp3 filter=lfs -crlf
*.wav filter=lfs -crlf
*.mp4 filter=lfs -crlf
*.png filter=lfs -crlf
*.psd filter=lfs -crlf
*.tga filter=lfs -crlf
*.tif filter=lfs -crlf

8
.gitignore vendored
View file

@ -1,6 +1,2 @@
# Ignore Vim backup files
*~
# Ignore Vim undo files
*.un~
.pio
.idea

BIN
assets/0001WaitingForGameStart.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0002AfterMainstation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0003HackingStationMuhvella.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0004PickupStationMuhvella.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0005AfterStationMuhvella.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0006HackingStationMagie.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0007PickupStationMagie.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0008DONTUSEDOESNOTWORK.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0009HackingBicola.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0010PickupBicola.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0011AfterBicola.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0012HackingTobione.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0013PickupTobione.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0014AfterTobione.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0015End.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0016AfterStationMagie.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0017WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0018WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0019WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0020WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0021WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0022WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0023WrongStation.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0024PutMeBack.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0025PutMeBack.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0026PutMeBack.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0027PutMeBack.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0028PutMeBack.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/0029Intro.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

30
copyFiles.py Normal file
View file

@ -0,0 +1,30 @@
import os
import shutil
def copy_files_to_root(src_folder, dest_root):
# Check if source folder exists
if not os.path.exists(src_folder):
print(f"Source folder '{src_folder}' does not exist.")
return
# Create destination root folder if it does not exist
if not os.path.exists(dest_root):
os.makedirs(dest_root)
# Copy files
for item in os.listdir(src_folder):
src_path = os.path.join(src_folder, item)
dest_path = os.path.join(dest_root, item)
if os.path.isfile(src_path):
shutil.copy2(src_path, dest_path)
print(f"Copied '{src_path}' to '{dest_path}'")
else:
print(f"Skipped '{src_path}' as it is not a file")
# Define source folder and destination root
src_folder = 'C:/Users/Marcel/CLionProjects/BuenzliIO/assets'
dest_root = 'D:/'
# Execute the copy function
copy_files_to_root(src_folder, dest_root)

39
include/README Normal file
View file

@ -0,0 +1,39 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View file

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

18
platformio.ini Normal file
View file

@ -0,0 +1,18 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
adafruit/Adafruit BusIO@^1.16.1
dfrobot/DFRobotDFPlayerMini@^1.0.6
fmalpartida/LiquidCrystal@^1.5.0

1820
src/Adafruit_PN532.cpp Normal file

File diff suppressed because it is too large Load diff

221
src/Adafruit_PN532.h Normal file
View file

@ -0,0 +1,221 @@
/**************************************************************************/
/*!
@file Adafruit_PN532.h
v2.0 - Refactored to add I2C support from Adafruit_NFCShield_I2C library.
v1.1 - Added full command list
- Added 'verbose' mode flag to constructor to toggle debug output
- Changed readPassiveTargetID() to return variable length values
*/
/**************************************************************************/
#ifndef ADAFRUIT_PN532_H
#define ADAFRUIT_PN532_H
#include "Arduino.h"
#include <Adafruit_I2CDevice.h>
#include <Adafruit_SPIDevice.h>
#define PN532_PREAMBLE (0x00) ///< Command sequence start, byte 1/3
#define PN532_STARTCODE1 (0x00) ///< Command sequence start, byte 2/3
#define PN532_STARTCODE2 (0xFF) ///< Command sequence start, byte 3/3
#define PN532_POSTAMBLE (0x00) ///< EOD
#define PN532_HOSTTOPN532 (0xD4) ///< Host-to-PN532
#define PN532_PN532TOHOST (0xD5) ///< PN532-to-host
// PN532 Commands
#define PN532_COMMAND_DIAGNOSE (0x00) ///< Diagnose
#define PN532_COMMAND_GETFIRMWAREVERSION (0x02) ///< Get firmware version
#define PN532_COMMAND_GETGENERALSTATUS (0x04) ///< Get general status
#define PN532_COMMAND_READREGISTER (0x06) ///< Read register
#define PN532_COMMAND_WRITEREGISTER (0x08) ///< Write register
#define PN532_COMMAND_READGPIO (0x0C) ///< Read GPIO
#define PN532_COMMAND_WRITEGPIO (0x0E) ///< Write GPIO
#define PN532_COMMAND_SETSERIALBAUDRATE (0x10) ///< Set serial baud rate
#define PN532_COMMAND_SETPARAMETERS (0x12) ///< Set parameters
#define PN532_COMMAND_SAMCONFIGURATION (0x14) ///< SAM configuration
#define PN532_COMMAND_POWERDOWN (0x16) ///< Power down
#define PN532_COMMAND_RFCONFIGURATION (0x32) ///< RF config
#define PN532_COMMAND_RFREGULATIONTEST (0x58) ///< RF regulation test
#define PN532_COMMAND_INJUMPFORDEP (0x56) ///< Jump for DEP
#define PN532_COMMAND_INJUMPFORPSL (0x46) ///< Jump for PSL
#define PN532_COMMAND_INLISTPASSIVETARGET (0x4A) ///< List passive target
#define PN532_COMMAND_INATR (0x50) ///< ATR
#define PN532_COMMAND_INPSL (0x4E) ///< PSL
#define PN532_COMMAND_INDATAEXCHANGE (0x40) ///< Data exchange
#define PN532_COMMAND_INCOMMUNICATETHRU (0x42) ///< Communicate through
#define PN532_COMMAND_INDESELECT (0x44) ///< Deselect
#define PN532_COMMAND_INRELEASE (0x52) ///< Release
#define PN532_COMMAND_INSELECT (0x54) ///< Select
#define PN532_COMMAND_INAUTOPOLL (0x60) ///< Auto poll
#define PN532_COMMAND_TGINITASTARGET (0x8C) ///< Init as target
#define PN532_COMMAND_TGSETGENERALBYTES (0x92) ///< Set general bytes
#define PN532_COMMAND_TGGETDATA (0x86) ///< Get data
#define PN532_COMMAND_TGSETDATA (0x8E) ///< Set data
#define PN532_COMMAND_TGSETMETADATA (0x94) ///< Set metadata
#define PN532_COMMAND_TGGETINITIATORCOMMAND (0x88) ///< Get initiator command
#define PN532_COMMAND_TGRESPONSETOINITIATOR (0x90) ///< Response to initiator
#define PN532_COMMAND_TGGETTARGETSTATUS (0x8A) ///< Get target status
#define PN532_RESPONSE_INDATAEXCHANGE (0x41) ///< Data exchange
#define PN532_RESPONSE_INLISTPASSIVETARGET (0x4B) ///< List passive target
#define PN532_WAKEUP (0x55) ///< Wake
#define PN532_SPI_STATREAD (0x02) ///< Stat read
#define PN532_SPI_DATAWRITE (0x01) ///< Data write
#define PN532_SPI_DATAREAD (0x03) ///< Data read
#define PN532_SPI_READY (0x01) ///< Ready
#define PN532_I2C_ADDRESS (0x48 >> 1) ///< Default I2C address
#define PN532_I2C_READBIT (0x01) ///< Read bit
#define PN532_I2C_BUSY (0x00) ///< Busy
#define PN532_I2C_READY (0x01) ///< Ready
#define PN532_I2C_READYTIMEOUT (20) ///< Ready timeout
#define PN532_MIFARE_ISO14443A (0x00) ///< MiFare
// Mifare Commands
#define MIFARE_CMD_AUTH_A (0x60) ///< Auth A
#define MIFARE_CMD_AUTH_B (0x61) ///< Auth B
#define MIFARE_CMD_READ (0x30) ///< Read
#define MIFARE_CMD_WRITE (0xA0) ///< Write
#define MIFARE_CMD_TRANSFER (0xB0) ///< Transfer
#define MIFARE_CMD_DECREMENT (0xC0) ///< Decrement
#define MIFARE_CMD_INCREMENT (0xC1) ///< Increment
#define MIFARE_CMD_STORE (0xC2) ///< Store
#define MIFARE_ULTRALIGHT_CMD_WRITE (0xA2) ///< Write (MiFare Ultralight)
// Prefixes for NDEF Records (to identify record type)
#define NDEF_URIPREFIX_NONE (0x00) ///< No prefix
#define NDEF_URIPREFIX_HTTP_WWWDOT (0x01) ///< HTTP www. prefix
#define NDEF_URIPREFIX_HTTPS_WWWDOT (0x02) ///< HTTPS www. prefix
#define NDEF_URIPREFIX_HTTP (0x03) ///< HTTP prefix
#define NDEF_URIPREFIX_HTTPS (0x04) ///< HTTPS prefix
#define NDEF_URIPREFIX_TEL (0x05) ///< Tel prefix
#define NDEF_URIPREFIX_MAILTO (0x06) ///< Mailto prefix
#define NDEF_URIPREFIX_FTP_ANONAT (0x07) ///< FTP
#define NDEF_URIPREFIX_FTP_FTPDOT (0x08) ///< FTP dot
#define NDEF_URIPREFIX_FTPS (0x09) ///< FTPS
#define NDEF_URIPREFIX_SFTP (0x0A) ///< SFTP
#define NDEF_URIPREFIX_SMB (0x0B) ///< SMB
#define NDEF_URIPREFIX_NFS (0x0C) ///< NFS
#define NDEF_URIPREFIX_FTP (0x0D) ///< FTP
#define NDEF_URIPREFIX_DAV (0x0E) ///< DAV
#define NDEF_URIPREFIX_NEWS (0x0F) ///< NEWS
#define NDEF_URIPREFIX_TELNET (0x10) ///< Telnet prefix
#define NDEF_URIPREFIX_IMAP (0x11) ///< IMAP prefix
#define NDEF_URIPREFIX_RTSP (0x12) ///< RTSP
#define NDEF_URIPREFIX_URN (0x13) ///< URN
#define NDEF_URIPREFIX_POP (0x14) ///< POP
#define NDEF_URIPREFIX_SIP (0x15) ///< SIP
#define NDEF_URIPREFIX_SIPS (0x16) ///< SIPS
#define NDEF_URIPREFIX_TFTP (0x17) ///< TFPT
#define NDEF_URIPREFIX_BTSPP (0x18) ///< BTSPP
#define NDEF_URIPREFIX_BTL2CAP (0x19) ///< BTL2CAP
#define NDEF_URIPREFIX_BTGOEP (0x1A) ///< BTGOEP
#define NDEF_URIPREFIX_TCPOBEX (0x1B) ///< TCPOBEX
#define NDEF_URIPREFIX_IRDAOBEX (0x1C) ///< IRDAOBEX
#define NDEF_URIPREFIX_FILE (0x1D) ///< File
#define NDEF_URIPREFIX_URN_EPC_ID (0x1E) ///< URN EPC ID
#define NDEF_URIPREFIX_URN_EPC_TAG (0x1F) ///< URN EPC tag
#define NDEF_URIPREFIX_URN_EPC_PAT (0x20) ///< URN EPC pat
#define NDEF_URIPREFIX_URN_EPC_RAW (0x21) ///< URN EPC raw
#define NDEF_URIPREFIX_URN_EPC (0x22) ///< URN EPC
#define NDEF_URIPREFIX_URN_NFC (0x23) ///< URN NFC
#define PN532_GPIO_VALIDATIONBIT (0x80) ///< GPIO validation bit
#define PN532_GPIO_P30 (0) ///< GPIO 30
#define PN532_GPIO_P31 (1) ///< GPIO 31
#define PN532_GPIO_P32 (2) ///< GPIO 32
#define PN532_GPIO_P33 (3) ///< GPIO 33
#define PN532_GPIO_P34 (4) ///< GPIO 34
#define PN532_GPIO_P35 (5) ///< GPIO 35
/**
* @brief Class for working with Adafruit PN532 NFC/RFID breakout boards.
*/
class Adafruit_PN532 {
public:
Adafruit_PN532(uint8_t clk, uint8_t miso, uint8_t mosi,
uint8_t ss); // Software SPI
Adafruit_PN532(uint8_t ss, SPIClass *theSPI = &SPI); // Hardware SPI
Adafruit_PN532(uint8_t irq, uint8_t reset,
TwoWire *theWire = &Wire); // Hardware I2C
Adafruit_PN532(uint8_t reset, HardwareSerial *theSer); // Hardware UART
bool begin(void);
void reset(void);
void wakeup(void);
// Generic PN532 functions
bool SAMConfig(void);
uint32_t getFirmwareVersion(void);
bool sendCommandCheckAck(uint8_t *cmd, uint8_t cmdlen,
uint16_t timeout = 100);
bool writeGPIO(uint8_t pinstate);
uint8_t readGPIO(void);
bool setPassiveActivationRetries(uint8_t maxRetries);
// ISO14443A functions
bool readPassiveTargetID(
uint8_t cardbaudrate, uint8_t *uid, uint8_t *uidLength,
uint16_t timeout = 0); // timeout 0 means no timeout - will block forever.
bool startPassiveTargetIDDetection(uint8_t cardbaudrate);
bool readDetectedPassiveTargetID(uint8_t *uid, uint8_t *uidLength);
bool inDataExchange(uint8_t *send, uint8_t sendLength, uint8_t *response,
uint8_t *responseLength);
bool inListPassiveTarget();
uint8_t AsTarget();
uint8_t getDataTarget(uint8_t *cmd, uint8_t *cmdlen);
uint8_t setDataTarget(uint8_t *cmd, uint8_t cmdlen);
// Mifare Classic functions
bool mifareclassic_IsFirstBlock(uint32_t uiBlock);
bool mifareclassic_IsTrailerBlock(uint32_t uiBlock);
uint8_t mifareclassic_AuthenticateBlock(uint8_t *uid, uint8_t uidLen,
uint32_t blockNumber,
uint8_t keyNumber, uint8_t *keyData);
uint8_t mifareclassic_ReadDataBlock(uint8_t blockNumber, uint8_t *data);
uint8_t mifareclassic_WriteDataBlock(uint8_t blockNumber, uint8_t *data);
uint8_t mifareclassic_FormatNDEF(void);
uint8_t mifareclassic_WriteNDEFURI(uint8_t sectorNumber,
uint8_t uriIdentifier, const char *url);
// Mifare Ultralight functions
uint8_t mifareultralight_ReadPage(uint8_t page, uint8_t *buffer);
uint8_t mifareultralight_WritePage(uint8_t page, uint8_t *data);
// NTAG2xx functions
uint8_t ntag2xx_ReadPage(uint8_t page, uint8_t *buffer);
uint8_t ntag2xx_WritePage(uint8_t page, uint8_t *data);
uint8_t ntag2xx_WriteNDEFURI(uint8_t uriIdentifier, char *url,
uint8_t dataLen);
// Help functions to display formatted text
static void PrintHex(const byte *data, const uint32_t numBytes);
static void PrintHexChar(const byte *pbtData, const uint32_t numBytes);
private:
int8_t _irq = -1, _reset = -1, _cs = -1;
int8_t _uid[7]; // ISO14443A uid
int8_t _uidLen; // uid len
int8_t _key[6]; // Mifare Classic key
int8_t _inListedTag; // Tg number of inlisted tag.
// Low level communication functions that handle both SPI and I2C.
void readdata(uint8_t *buff, uint8_t n);
void writecommand(uint8_t *cmd, uint8_t cmdlen);
bool isready();
bool waitready(uint16_t timeout);
bool readack();
Adafruit_SPIDevice *spi_dev = NULL;
Adafruit_I2CDevice *i2c_dev = NULL;
HardwareSerial *ser_dev = NULL;
};
#endif

73
src/audioState.h Normal file
View file

@ -0,0 +1,73 @@
#pragma once
#include <Arduino.h>
#include <DFRobotDFPlayerMini.h>
#include <LiquidCrystal.h>
inline void printMp3Detail(const uint8_t type, const int value)
{
Serial.println(); // Add a newline before printing the details
switch (type)
{
case TimeOut:
Serial.println("Time Out!");
break;
case WrongStack:
Serial.println("Stack Wrong!");
break;
case DFPlayerCardInserted:
Serial.println("Card Inserted!");
break;
case DFPlayerCardRemoved:
Serial.println("Card Removed!");
break;
case DFPlayerCardOnline:
Serial.println("Card Online!");
break;
case DFPlayerUSBInserted:
Serial.println("USB Inserted!");
break;
case DFPlayerUSBRemoved:
Serial.println("USB Removed!");
break;
case DFPlayerPlayFinished:
Serial.print("Num:");
Serial.print(value);
Serial.println(" Finished!");
break;
case DFPlayerError:
Serial.print("DFPlayerError: ");
switch (value)
{
case Busy:
Serial.println("Card not found");
break;
case Sleeping:
Serial.println("Sleeping");
break;
case SerialWrongStack:
Serial.println("Wrong Stack");
break;
case CheckSumNotMatch:
Serial.println("Checksum Error");
break;
case FileIndexOut:
Serial.println("File Index OOB");
break;
case FileMismatch:
Serial.println("File Mismatch");
break;
case Advertise:
Serial.println("In Advertise");
break;
default:
Serial.println("Unknown Error");
break;
}
break;
default:
Serial.println("Unknown Type");
break;
}
delay(100);
}

22
src/context.cpp Normal file
View file

@ -0,0 +1,22 @@
#include "Context.h"
Context::Context(Station* stations, const int stationCount, const unsigned long delta, DFRobotDFPlayerMini* dfPlayer)
{
this->stations = stations;
this->stationCount = stationCount;
this->delta = delta;
this->dfPlayer = dfPlayer;
}
int Context::getStationIndex(const Station* station) const
{
for (int i = 0; i < stationCount; ++i)
{
if (&stations[i] == station)
{
return i;
}
}
return -1;
}

16
src/context.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <DFRobotDFPlayerMini.h>
#include "station.h"
class Context {
public:
Context(Station* stations, int stationCount, unsigned long delta, DFRobotDFPlayerMini* dfPlayer);
Station* stations;
int stationCount;
unsigned long delta;
DFRobotDFPlayerMini* dfPlayer;
int getStationIndex(const Station* station) const;
bool isPlaying() const;
};

186
src/main.cpp Normal file
View file

@ -0,0 +1,186 @@
#include <LiquidCrystal.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_PN532.h>
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include "station.h"
#include "statemachine.h"
#include "state.h"
#include "states.h"
#include "timer.h"
#include "audioState.h"
#include "sequences.h"
#define PN532_CS 10
// Using this library to set a timeout on readPassiveTagID
// The library in use is slightly modified to fix this issue: https://github.com/adafruit/Adafruit-PN532/issues/117
Adafruit_PN532 nfc(PN532_CS);
LiquidCrystal lcd(9, 8, 5, 4, 3, 2);
SoftwareSerial softSerial(6, 7); // RX, TX
DFRobotDFPlayerMini dfPlayer;
Station stations[5] = {
Station(1680767519, "Main"), Station(102951984, "Muhvella"), Station(3346823711, "Magie"),
Station(2174777887, "Bicola"), Station(3569318175, "Tobione")
};
Station* currentStation;
int currentStationIndex = -1;
Timer stationDetectionTimer(500, true);
String previousDisplayContent;
StateMachine stateMachine(new Startup());
unsigned long lastLoopTime = 0;
unsigned long currentLoopTime = 0;
unsigned long deltaTime = 0;
void setupNFC()
{
nfc.begin();
uint32_t versiondata = nfc.getFirmwareVersion();
if (!versiondata)
{
while (1); // halt
}
// configure board to read RFID tags and cards
nfc.SAMConfig();
}
Station* detectStation()
{
uint8_t uid[7];
uint8_t uidLength;
constexpr uint16_t timeout = 100;
const bool success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, timeout);
if (!success)
{
return nullptr;
}
uint32_t id = 0;
for (uint8_t i = 0; i < uidLength; i++)
{
id <<= 8;
id |= uid[i];
}
Station* found = 0;
for (unsigned int i = 0; i < sizeof(stations) / sizeof(stations[0]); i++)
{
Station* currentStation = &stations[i];
if (currentStation->check(id))
{
found = currentStation;
}
}
return found;
}
void checkStations()
{
Station* station = detectStation();
if (station != nullptr)
{
if (currentStation == nullptr)
{
currentStation = station;
stateMachine.putDown(Context(stations, sizeof(stations) / sizeof(stations[0]), deltaTime, &dfPlayer), currentStation);
}
}
else
{
if (currentStation != nullptr)
{
currentStation = nullptr;
stateMachine.pickedUp(Context(stations, sizeof(stations) / sizeof(stations[0]), deltaTime, &dfPlayer));
}
}
}
void printString(const String& str)
{
int currentColumn = 0;
int currentRow = 0;
lcd.clear();
for (unsigned int i = 0; i < str.length(); i++)
{
lcd.setCursor(currentColumn, currentRow);
lcd.print(str[i]);
currentColumn++;
if (currentColumn >= 16)
{
currentColumn = 0;
currentRow++;
if (currentRow >= 2)
{
break;
}
}
}
}
void loop(void)
{
currentLoopTime = millis();
deltaTime = currentLoopTime - lastLoopTime;
bool doCheck = stationDetectionTimer.update(deltaTime);
if (doCheck)
{
checkStations();
}
stateMachine.update(Context(stations, sizeof(stations) / sizeof(stations[0]), deltaTime, &dfPlayer));
String newContent = stateMachine.updateDisplay(Context(stations, sizeof(stations) / sizeof(stations[0]), deltaTime, &dfPlayer));
if (previousDisplayContent != newContent)
{
lcd.clear();
printString(newContent);
previousDisplayContent = newContent;
}
lastLoopTime = currentLoopTime;
delay(10);
}
void setup(void)
{
randomSeed(analogRead(0));
Serial.begin(9600);
// Serial.println(ON_MAIN_STATION.getEntryAtIndex(0)->isAudio);
setupNFC();
lcd.begin(16, 2);
stationDetectionTimer.start();
softSerial.begin(9600);
lcd.print("Initializing MP3");
lcd.clear();
if (!dfPlayer.begin(softSerial, true, true))
{
lcd.print("Failed to init");
lcd.setCursor(0, 1);
lcd.print("DFPlayer!");
while (true)
{
delay(100);
// printMp3Detail(&lcd, dfPlayer.readType(), dfPlayer.read());
}
}
dfPlayer.volume(20);
}

44
src/sequence.cpp Normal file
View file

@ -0,0 +1,44 @@
#include <Arduino.h>
#include "sequence.h"
SequenceEntry::SequenceEntry(const bool is_audio, const int duration, const int audio_id):
isAudio(is_audio),
duration(duration),
audioId(audio_id)
{
}
SequenceEntry* Sequence::getEntryAtIndex(const int index) const
{
if (index >= this->length)
{
// illegal argument
return nullptr;
}
return this->entries[index];
}
int Sequence::getLength() const
{
return this->length;
}
int Sequence::getRepeatIndex() const
{
return repeatIndex;
}
Sequence::Sequence(const int repeatIndex): entries{}, length(0), repeatIndex(repeatIndex)
{
}
void Sequence::addEntry(SequenceEntry entry)
{
if (this->length == 20)
{
// max amount of entries
return;
}
entries[length] = &entry;
length += 1;
}

25
src/sequence.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <Arduino.h>
class SequenceEntry {
public:
SequenceEntry(bool is_audio, int duration, int audio_id);
bool isAudio;
int duration;
int audioId;
};
class Sequence {
private:
SequenceEntry* entries[20];
int length;
int repeatIndex;
public:
explicit Sequence(int repeatIndex);
SequenceEntry* getEntryAtIndex(int index) const;
int getLength() const;
int getRepeatIndex() const;
void addEntry(SequenceEntry entry);
};

4
src/sequences.cpp Normal file
View file

@ -0,0 +1,4 @@
#include "sequences.h"
#include "sequence.h"
#include "sounds.h"

3
src/sequences.h Normal file
View file

@ -0,0 +1,3 @@
#pragma once
#include "sequences.h"
#include "sequence.h"

30
src/sounds.h Normal file
View file

@ -0,0 +1,30 @@
#include <Arduino.h>
#define NONE 0
#define WAITING_FOR_GAME_START 1
#define AFTER_MAIN_STATION 2
#define HACKING_STATION_MUHVELLA 3
#define PICKUP_STATION_MUHVELLA 4
#define AFTER_MUHVELLA 5
#define HACKING_STATION_MAGIE 6
#define PICKUP_STATION_MAGIE 7
#define AFTER_MAGIE 16 // weird hack because file 8 does not seem to work...
#define HACKING_STATION_BICOLA 9
#define PICKUP_STATION_BICOLA 10
#define AFTER_BICOLA 11
#define HACKING_STATION_TOBIONE 12
#define PICKUP_STATION_TOBIONE 13
#define AFTER_TOBIONE 14
#define END 15
#define WRONG_STATION_START 17
#define WRONG_STATION_END 23
#define PUT_ME_BACK_START 24
#define PUT_ME_BACK_END 28
#define START_INSTRUCTIONS 29

26
src/state.h Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <audioState.h>
#include <timer.h>
#include "station.h"
#include "context.h"
#include "sequence.h"
class Station;
class State
{
public:
virtual ~State() = default;
virtual State* pickedUp(Context context) { return this; }
virtual State* putDown(Context context, Station* newStation) { return this; }
virtual State* update(Context context) { return this; }
virtual void activated(Context context)
{
}
virtual String updateDisplay(const Context& context) { return ""; }
};

46
src/statemachine.cpp Normal file
View file

@ -0,0 +1,46 @@
#include "statemachine.h"
#include "station.h"
#include "context.h"
StateMachine::StateMachine(State* initialState)
: currentState(initialState)
{
}
StateMachine::~StateMachine()
{
delete currentState;
}
void StateMachine::updateState(State* newState, const Context& context)
{
if (newState != currentState)
{
delete currentState;
currentState = newState;
currentState->activated(context);
}
}
void StateMachine::pickedUp(const Context& context)
{
State* newState = currentState->pickedUp(context);
updateState(newState, context);
}
void StateMachine::putDown(const Context& context, Station* newStation)
{
State* newState = currentState->putDown(context, newStation);
updateState(newState, context);
}
void StateMachine::update(const Context& context)
{
State* newState = currentState->update(context);
updateState(newState, context);
}
String StateMachine::updateDisplay(const Context& context) const
{
return currentState->updateDisplay(context);
}

21
src/statemachine.h Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include <LiquidCrystal.h>
#include "state.h"
#include "station.h"
#include "context.h"
class StateMachine {
private:
State* currentState;
void updateState(State* newState, const Context& context);
public:
StateMachine(State* initialState);
~StateMachine();
void pickedUp(const Context& context);
void putDown(const Context& context, Station* newStation);
void update(const Context& context);
String updateDisplay(const Context& context) const;
};

476
src/states.cpp Normal file
View file

@ -0,0 +1,476 @@
#include "states.h"
#include "state.h"
#include "station.h"
#include "sounds.h"
#include "text.h"
Startup::Startup() = default;
State* Startup::putDown(const Context context, Station* newStation)
{
const int newStationIndex = context.getStationIndex(newStation);
if (newStationIndex == 0)
{
return new WaitingForGameStart();
}
return this;
}
State* Startup::update(const Context context)
{
return this;
}
String Startup::updateDisplay(const Context& context)
{
return "Legg mi uf die ersti Station!";
}
void Startup::activated(Context context)
{
}
//------------------------------------
WaitingForGameStart::WaitingForGameStart() = default;
State* WaitingForGameStart::pickedUp(const Context context)
{
return new StartInstructions(context.stations + 1);
}
State* WaitingForGameStart::update(const Context context)
{
return this;
}
String WaitingForGameStart::updateDisplay(const Context& context)
{
return WAITING_FOR_START_TEXT;
}
void WaitingForGameStart::activated(const Context context)
{
context.dfPlayer->loop(WAITING_FOR_GAME_START);
}
//------------------------------------
StartInstructions::StartInstructions(Station* targetStation): timer(Timer(0, false))
{
this->targetStation = targetStation;
}
State* StartInstructions::putDown(const Context context, Station* newStation)
{
if (newStation == this->targetStation)
{
return new Hacking(targetStation);
}
if (newStation == context.stations)
{
return new Restarting();
}
return new IncorrectStation(targetStation, context.stations);
}
State* StartInstructions::update(const Context context)
{
bool done = timer.update(context.delta);
if (done)
{
return new OnTheMove(targetStation, context.stations);
}
return this;
}
String StartInstructions::updateDisplay(const Context& context)
{
return INTRO_TEXT;
}
void StartInstructions::activated(const Context context)
{
context.dfPlayer->play(START_INSTRUCTIONS);
timer = Timer(39000, false); // todo set correct time
timer.start();
}
//------------------------------------
OnTheMove::OnTheMove(Station* targetStation, Station* previousStation)
{
this->targetStation = targetStation;
this->previousStation = previousStation;
}
State* OnTheMove::putDown(const Context context, Station* newStation)
{
if (newStation == this->targetStation)
{
if (targetStation == context.stations) // is home base?
{
return new End();
}
return new Hacking(this->targetStation);
}
if (newStation == context.stations)
{
return new Restarting();
}
return new IncorrectStation(targetStation, previousStation);
}
State* OnTheMove::update(const Context context)
{
return this;
}
String OnTheMove::updateDisplay(const Context& context)
{
const int index = context.getStationIndex(this->targetStation);
if (index == 0)
{
return TOBIONE_AFTER_TEXT;
}
else if (index == 1)
{
return MAIN_AFTER_PICKUP_TEXT;
}
else if (index == 2)
{
return MUHVELLA_AFTER_TEXT;
}
else if (index == 3)
{
return MAGIE_AFTER_TEXT;
}
else if (index == 4)
{
return BICOLA_AFTER_TEXT;
}
return "UNDEFINED";
}
void OnTheMove::activated(const Context context)
{
const int index = context.getStationIndex(this->targetStation);
if (index == 0)
{
context.dfPlayer->loop(AFTER_TOBIONE);
}
else if (index == 1)
{
context.dfPlayer->loop(AFTER_MAIN_STATION);
}
else if (index == 2)
{
context.dfPlayer->loop(AFTER_MUHVELLA);
}
else if (index == 3)
{
context.dfPlayer->loop(AFTER_MAGIE);
}
else if (index == 4)
{
context.dfPlayer->loop(AFTER_BICOLA);
}
}
//------------------------------------
IncorrectStation::IncorrectStation(Station* targetStation, Station* previousStation)
{
this->targetStation = targetStation;
this->previousStation = previousStation;
}
State* IncorrectStation::pickedUp(const Context context)
{
return new OnTheMove(this->targetStation, previousStation);
}
State* IncorrectStation::update(const Context context)
{
return this;
}
String IncorrectStation::updateDisplay(const Context& context)
{
return WRONG_STATION_TEXT;
}
void IncorrectStation::activated(const Context context)
{
context.dfPlayer->play(random(WRONG_STATION_START, WRONG_STATION_END + 1));
}
//------------------------------------
Hacking::Hacking(Station* currentStation): timer(5000, false)
{
this->currentStation = currentStation;
this->timer.start();
}
State* Hacking::pickedUp(const Context context)
{
return new Complain(currentStation);
}
State* Hacking::update(const Context context)
{
const bool done = timer.update(context.delta);
if (done)
{
const int currentStationIndex = context.getStationIndex(currentStation);
const int nextStationIndex = (currentStationIndex + 1) % context.stationCount;
Station* nextStation = context.stations + nextStationIndex;
return new WaitingForPickup(currentStation, nextStation);
}
return this;
}
String Hacking::updateDisplay(const Context& context)
{
auto currentStationIndex = context.getStationIndex(this->currentStation);
if (currentStationIndex == 1)
{
return MUHVELLA_HACKING_TEXT;
}
if (currentStationIndex == 2)
{
return MAGIE_HACKING_TEXT;
}
if (currentStationIndex == 3)
{
return BICOLA_HACKING_TEXT;
}
if (currentStationIndex == 4)
{
return TOBIONE_HACKING_TEXT;
}
return "UNDEFINED";
}
void Hacking::activated(const Context context)
{
// todo change times
auto currentStationIndex = context.getStationIndex(this->currentStation);
if (currentStationIndex == 1)
{
timer = Timer(25000, false);
context.dfPlayer->play(HACKING_STATION_MUHVELLA);
}
if (currentStationIndex == 2)
{
timer = Timer(29000, false);
context.dfPlayer->play(HACKING_STATION_MAGIE);
}
if (currentStationIndex == 3)
{
timer = Timer(30000, false);
context.dfPlayer->play(HACKING_STATION_BICOLA);
}
if (currentStationIndex == 4)
{
timer = Timer(37000, false);
context.dfPlayer->play(HACKING_STATION_TOBIONE);
}
timer.start();
}
//------------------------------------
Complain::Complain(Station* currentStation)
{
this->currentStation = currentStation;
}
State* Complain::putDown(const Context context, Station* newStation)
{
if (this->currentStation == newStation) // was put back on correct station
{
if (currentStation == context.stations) // if wants to be put back on main station
{
return new End();
}
return new Hacking(this->currentStation);
}
if (newStation == context.stations) // if was incorrectly put back on main station
{
return new Restarting();
}
return this; // was not put back on correct station (just keeps complaining)
}
State* Complain::update(Context context)
{
return this;
}
String Complain::updateDisplay(const Context& context)
{
return COMPLAIN_TEXT;
}
void Complain::activated(const Context context)
{
context.dfPlayer->play(random(PUT_ME_BACK_START, PUT_ME_BACK_END + 1));
}
//------------------------------------
WaitingForPickup::WaitingForPickup(Station* currentStation, Station* targetStation)
{
this->currentStation = currentStation;
this->targetStation = targetStation;
}
State* WaitingForPickup::pickedUp(Context context)
{
return new OnTheMove(targetStation, currentStation);
}
State* WaitingForPickup::update(Context context)
{
return this;
}
String WaitingForPickup::updateDisplay(const Context& context)
{
auto currentStationIndex = context.getStationIndex(this->currentStation);
if (currentStationIndex == 1)
{
return MUHVELLA_PICKUP_TEXT;
}
if (currentStationIndex == 2)
{
return MAGIE_PICKUP_TEXT;
}
if (currentStationIndex == 3)
{
return BICOLA_PICKUP_TEXT;
}
if (currentStationIndex == 4)
{
return TOBIONE_PICKUP_TEXT;
}
}
void WaitingForPickup::activated(Context context)
{
auto currentStationIndex = context.getStationIndex(this->currentStation);
if (currentStationIndex == 1)
{
context.dfPlayer->loop(PICKUP_STATION_MUHVELLA);
}
if (currentStationIndex == 2)
{
context.dfPlayer->loop(PICKUP_STATION_MAGIE);
}
if (currentStationIndex == 3)
{
context.dfPlayer->loop(PICKUP_STATION_BICOLA);
}
if (currentStationIndex == 4)
{
context.dfPlayer->loop(PICKUP_STATION_TOBIONE);
}
}
//------------------------------------
End::End(): timer(Timer(0, false))
{
}
State* End::pickedUp(const Context context)
{
return new Complain(context.stations);
}
State* End::update(const Context context)
{
const bool done = timer.update(context.delta);
if (done)
{
return new Restarting();
}
return this;
}
String End::updateDisplay(const Context& context)
{
return END_TEXT;
}
void End::activated(const Context context)
{
context.dfPlayer->play(END);
timer = Timer(45000, false);
timer.start();
}
//------------------------------------
Restarting::Restarting(): timer(Timer(10000, false))
{
}
State* Restarting::pickedUp(const Context context)
{
return new StartInstructions(context.stations + 1); // starting despite not beeing in waiting for game start
}
State* Restarting::update(const Context context)
{
const bool done = timer.update(context.delta);
if (done)
{
return new WaitingForGameStart();
}
return this;
}
String Restarting::updateDisplay(const Context& context)
{
String animLine = "";
const String full = ".-`-._.-`-._. :)";
const float percent = static_cast<float>(timer.getCurrentTime()) / static_cast<float>(timer.getDuration());
for (int i = 0; i < full.length() * percent; i++)
{
animLine = animLine + full[i];
}
return "RESTARTING " + animLine;
}
void Restarting::activated(const Context context)
{
timer.start();
context.dfPlayer->stop();
}

136
src/states.h Normal file
View file

@ -0,0 +1,136 @@
#pragma once
#include <timer.h>
#include "context.h"
#include "state.h"
#include "states.h"
#include "station.h"
class Startup final : public State
{
public:
Startup();
State* putDown(Context context, Station* newStation) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class WaitingForGameStart final : public State
{
public:
WaitingForGameStart();
State* pickedUp(Context context) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class StartInstructions final : public State
{
private:
Station* targetStation;
Timer timer;
public:
explicit StartInstructions(Station* targetStation);
State* putDown(Context context, Station* newStation) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class OnTheMove final : public State
{
private:
Station* targetStation;
Station* previousStation;
public:
explicit OnTheMove(Station* targetStation, Station* previousStation);
State* putDown(Context context, Station* newStation) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class IncorrectStation final : public State
{
private:
Station* targetStation;
Station* previousStation;
public:
explicit IncorrectStation(Station* targetStation, Station* previousStation);
State* pickedUp(Context context) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class Hacking final : public State
{
private:
Timer timer;
Station* currentStation;
public:
explicit Hacking(Station* currentStation);
State* pickedUp(Context context) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class Complain final : public State
{
private:
Station* currentStation;
public:
explicit Complain(Station* currentStation);
State* putDown(Context context, Station* newStation) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class WaitingForPickup final : public State
{
private:
Station* currentStation;
Station* targetStation;
public:
WaitingForPickup(Station* currentStation, Station* targetStation);
State* pickedUp(Context context) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class End final : public State
{
protected:
Timer timer;
public:
End();
State* pickedUp(Context context) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};
class Restarting final : public State
{
protected:
Timer timer;
public:
Restarting();
State* pickedUp(Context context) override;
State* update(Context context) override;
String updateDisplay(const Context& context) override;
void activated(Context context) override;
};

17
src/station.cpp Normal file
View file

@ -0,0 +1,17 @@
#include <Arduino.h>
#include "station.h"
Station::Station(uint32_t nfcTagId, String name) {
{
this->nfcTagId = nfcTagId;
this->name = name;
}
}
bool Station::check(uint32_t nfcTagId) {
return nfcTagId == this->nfcTagId;
}
String Station::getName() {
return this->name;
}

12
src/station.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
class Station {
private:
uint32_t nfcTagId;
String name;
public:
Station(uint32_t nfcTagId, String name);
bool check(uint32_t nfcTagId);
String getName();
};

30
src/text.h Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#define WAITING_FOR_START_TEXT "Lupf mich uf"
#define INTRO_TEXT "Danke :D <3"
#define MAIN_AFTER_PICKUP_TEXT "Lueg chli ume"
#define MUHVELLA_HACKING_TEXT "Eifach ligge lahBitte warte"
#define MUHVELLA_PICKUP_TEXT "Los! Witer =O"
#define MUHVELLA_AFTER_TEXT "Ufd Suechi! Gids Suppe? :P"
#define MAGIE_HACKING_TEXT "Wart rasch ^^ "
#define MAGIE_PICKUP_TEXT "Und witer gahts "
#define MAGIE_AFTER_TEXT "Yoga und Chrueter? :/"
#define BICOLA_HACKING_TEXT "Mues chli sueche"
#define BICOLA_PICKUP_TEXT "Und witer!"
#define BICOLA_AFTER_TEXT "Fast gschafft :,)"
#define TOBIONE_HACKING_TEXT "So nah und doch so fern -.-"
#define TOBIONE_PICKUP_TEXT "Endspurt!"
#define TOBIONE_AFTER_TEXT "goi mer wieder hei"
#define WRONG_STATION_TEXT "falschi Station"
#define COMPLAIN_TEXT "Wieder zrug stelle"
#define END_TEXT "DU HESCH ES GSCHAFFT!!!"

56
src/timer.cpp Normal file
View file

@ -0,0 +1,56 @@
#include "timer.h"
#include <HardwareSerial.h>
Timer::Timer(const unsigned long duration, const bool autoRestart): currentTime(0), running(false)
{
this->autoRestart = autoRestart;
this->duration = duration;
this->endTime = duration;
}
void Timer::start()
{
this->running = true;
}
bool Timer::update(const unsigned long delta)
{
// Serial.print("Timer ");
if (!running)
{
// Serial.print(" not running...");
return false;
}
bool timedOut = false;
this->currentTime += delta;
// Serial.print(" target : " + String(this->currentTime) + "/" + String(this->endTime));
if (currentTime > this->endTime)
{
timedOut = true;
}
if (timedOut && this->autoRestart)
{
this->endTime += this->duration;
}
if (timedOut && !this->autoRestart)
{
running = false;
}
return timedOut;
}
bool Timer::isRunning() const
{
return this->running;
}
unsigned long Timer::getDuration() const
{
return this->duration;
}
unsigned long Timer::getCurrentTime() const
{
return this->currentTime;
}

19
src/timer.h Normal file
View file

@ -0,0 +1,19 @@
#pragma once
class Timer {
private:
unsigned long duration;
bool autoRestart;
unsigned long currentTime;
unsigned long endTime;
bool running;
public:
Timer(unsigned long duration, bool autoRestart);
void start();
bool update(unsigned long delta);
bool isRunning() const;
unsigned long getDuration() const;
unsigned long getCurrentTime() const;
};

11
test/README Normal file
View file

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html