Initial demo for Promo-Operation
This commit is contained in:
1 parent
55b7909717
commit
eb1c84cf05
25 files changed
+24615
-2
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
.cache/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "platform"]
|
||||||
|
path = platform
|
||||||
|
url = git@github.com:Gustice/RPiLnxPlatform.git
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "(gdb) Launch",
|
||||||
|
"preLaunchTask": "CMake: build",
|
||||||
|
"type": "cppdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/build/src/sword/SwordMain",
|
||||||
|
"args": [
|
||||||
|
//"test",
|
||||||
|
"promo",
|
||||||
|
"${workspaceFolder}/assets",
|
||||||
|
"-b", "'mqtts://nerdyssey.de:8883'",
|
||||||
|
"-t", "'staging/outdoor/king-arthur/sword/'",
|
||||||
|
"-p", "'awesomeexodusoutdooradventures'",
|
||||||
|
"-u", "'exodususer'",
|
||||||
|
"-l", "'log.nerdyssey.de'",
|
||||||
|
"-k", "'uYpJC6WyZAbFK2MUgwNH'"
|
||||||
|
],
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"cwd": "${fileDirname}",
|
||||||
|
"environment": [],
|
||||||
|
"externalConsole": false,
|
||||||
|
"MIMode": "gdb",
|
||||||
|
"setupCommands": [
|
||||||
|
{
|
||||||
|
"description": "Enable pretty-printing for gdb",
|
||||||
|
"text": "-enable-pretty-printing",
|
||||||
|
"ignoreFailures": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Set Disassembly Flavor to Intel",
|
||||||
|
"text": "-gdb-set disassembly-flavor intel",
|
||||||
|
"ignoreFailures": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cmake",
|
||||||
|
"label": "CMake: build",
|
||||||
|
"command": "build",
|
||||||
|
"targets": [
|
||||||
|
"SwordMain"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"detail": "CMake template build task"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(SwordProp C CXX)
|
||||||
|
|
||||||
|
include(platform/tools/project.cmake)
|
||||||
|
ProjectSetup()
|
||||||
|
|
||||||
|
PlatformRequirements()
|
||||||
|
|
||||||
|
add_subdirectory(platform/extern)
|
||||||
|
|
||||||
|
add_subdirectory(platform/src)
|
||||||
|
addSubDirs_ofRoot("${CMAKE_CURRENT_LIST_DIR}/platform/modules")
|
||||||
|
core_addSubDirs_inAbsPath("${CMAKE_CURRENT_LIST_DIR}/platform/palCore/modules")
|
||||||
|
|
||||||
|
addSubDirs_ofRoot("${CMAKE_CURRENT_LIST_DIR}/src/modules")
|
||||||
|
add_subdirectory(src)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
; sudo nano /etc/systemd/system/SwordMain.service
|
||||||
|
[Unit]
|
||||||
|
Description=Service for sword stationg
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=pi
|
||||||
|
WorkingDirectory=/home/gustice/myapp
|
||||||
|
ExecStart=/home/pi/myapp/start.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
|
||||||
|
; BrockerUrl "mqtts://nerdyssey.de:8883";
|
||||||
|
; Topic "outdoor/king-arthur/sword/";
|
||||||
|
; MqttUser "exodususer";
|
||||||
|
; MqttPassword "awesomeexodusoutdooradventures";
|
||||||
|
; AsserDir "/home/gustice/workspace/kingArth/swordStation/assets";
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
aplay -l
|
||||||
|
sudo nano /etc/asound.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
defaults.pcm.card 2
|
||||||
|
defaults.ctl.card 2
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Start GUI
|
||||||
|
alsamixer
|
||||||
|
# CLI
|
||||||
|
amixer sset Master 50%
|
||||||
|
amixer sset Master 10%+
|
||||||
|
amixer sset Master 10%-
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
apt install -y libpaho-mqttpp-dev
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Autostart applications
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo nano /etc/systemd/system/SwordMain.service
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=My Raspberry Pi Application
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=gustice
|
||||||
|
WorkingDirectory=/home/pi/SwordMain
|
||||||
|
ExecStart=/home/pi/SwordMain/start.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
chmod +x /home/pi/SwordMain/start.sh
|
||||||
|
# Reload systemd
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
# Enable the service
|
||||||
|
sudo systemctl enable SwordMain.service
|
||||||
|
# start immediately
|
||||||
|
sudo systemctl start SwordMain.service
|
||||||
|
# check status
|
||||||
|
sudo systemctl status SwordMain.service
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
promo $(pwd)/assets -b "mqtts://nerdyssey.de:8883" -t "outdoor/king-arthur/sword/" -p "awesomeexodusoutdooradventures" -u "exodususer"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
build:
|
||||||
|
cmake -B build -S .
|
||||||
|
cmake --build build --target SwordMain
|
||||||
|
|
||||||
|
run:
|
||||||
|
build/src/SwordMain promo ./assets -b "mqtts://nerdyssey.de:8883" -t "outdoor/king-arthur/sword/" -p "awesomeexodusoutdooradventures" -u "exodususer" -l "log.nerdyssey.de" -k "uYpJC6WyZAbFK2MUgwNH"
|
||||||
|
|
||||||
|
test:
|
||||||
|
build/src/SwordMain promo ./assets -b "mqtts://nerdyssey.de:8883" -t "staging/outdoor/king-arthur/sword/" -p "awesomeexodusoutdooradventures" -u "exodususer" -l "log.nerdyssey.de" -k "uYpJC6WyZAbFK2MUgwNH"
|
||||||
|
|
||||||
|
# deploy:
|
||||||
|
# cp build/src/SwordMain .
|
||||||
|
# tar -cfz swordMain.tar.gz assets SwordMain
|
||||||
|
# curl --user your_username:your_access_token --upload-file ./swordMain.tar.gz git.nerdyssey.de/exodus/burg-stett-king-arth-sword/0.5.0/swordMain.tar.gz
|
||||||
|
|
||||||
|
# download:
|
||||||
|
# curl -O https://gitea.example.com/api/packages/my_organization/generic/my-app/1.0.0/my-app-linux-amd64.tar.gz
|
||||||
Submodule
+1
Submodule platform added at 0723a1dddf.
@@ -1,4 +1,25 @@
|
|||||||
# Sword station
|
# King Arthur IoT-Devices
|
||||||
|
|
||||||
Sword station for Kingh-Arthur game
|
```
|
||||||
|
🌐 Web-Services
|
||||||
|
|
|
||||||
|
Sword-Station
|
||||||
|
ThroneBridge ------ ThroneStation
|
||||||
|
|
||||||
|
Device 1 Device 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sword station
|
||||||
|
|
||||||
|
Sword station for Kingh-Arthur game.
|
||||||
|
Features Sword for designating the **Chosen One**
|
||||||
|
|
||||||
|
## Throne-Bridge
|
||||||
|
|
||||||
|
Protocol-Bridge station for Kingh-Arthur game
|
||||||
|
Service to connect the Throne via MQTT
|
||||||
|
|
||||||
|
## Throne-Station
|
||||||
|
|
||||||
|
Throne station
|
||||||
|
Features Throne for final coronation
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
add_subdirectory(sword)
|
||||||
|
add_subdirectory(throneBridge)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
create_interface()
|
||||||
|
|
||||||
Vendored
+22813
File diff suppressed because it is too large.
Load diff
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/yhirose/cpp-httplib
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../extern/httplib.h"
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <fmt/chrono.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
class SeqLogger {
|
||||||
|
public:
|
||||||
|
SeqLogger(std::string logUrl, std::string apiKey, std::string name)
|
||||||
|
: cli(logUrl),
|
||||||
|
ApiKey(apiKey),
|
||||||
|
AppName(name) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendBootUp(std::string version, std::string mode) {
|
||||||
|
std::string timestamp = createTimestamp();
|
||||||
|
nlohmann::json json_payload = {
|
||||||
|
{"Events", nlohmann::json::array({
|
||||||
|
{
|
||||||
|
{"Level", "Information"},
|
||||||
|
{"Timestamp", timestamp},
|
||||||
|
{"MessageTemplate", "[{App}] Firing up in {Mode}! Version {Ver}"},
|
||||||
|
{"Properties", {
|
||||||
|
{"App", AppName},
|
||||||
|
{"Ver", version},
|
||||||
|
{"Mode", mode}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
};
|
||||||
|
sendPayload(json_payload.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendEvent(std::string event, std::string message) {
|
||||||
|
std::string timestamp = createTimestamp();
|
||||||
|
nlohmann::json json_payload = {
|
||||||
|
{"Events", nlohmann::json::array({
|
||||||
|
{
|
||||||
|
{"Level", "Information"},
|
||||||
|
{"Timestamp", timestamp},
|
||||||
|
{"MessageTemplate", "[{App}]: Got {Event}; " + message},
|
||||||
|
{"Properties", {
|
||||||
|
{"App", AppName},
|
||||||
|
{"Event", Version}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
};
|
||||||
|
sendPayload(json_payload.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
httplib::Client cli;
|
||||||
|
const std::string ApiKey;
|
||||||
|
const std::string AppName;
|
||||||
|
const std::string Version;
|
||||||
|
|
||||||
|
static std::string createTimestamp() {
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
return fmt::format("{:%Y-%m-%dT%H:%M:%SZ}", now);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendPayload(std::string payload) {
|
||||||
|
httplib::Headers headers = {
|
||||||
|
{"X-Seq-ApiKey", ApiKey}};
|
||||||
|
auto res = cli.Post("/api/events/raw", headers, payload, "application/json");
|
||||||
|
if (res) {
|
||||||
|
if (res->status == 201) {
|
||||||
|
fmt::println("Success! Log accepted by Seq.");
|
||||||
|
} else {
|
||||||
|
fmt::println("Seq rejected the log.");
|
||||||
|
fmt::println(" HTTP Status: {}", res->status);
|
||||||
|
fmt::println(" Server Response: {}", res->body);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt::println(stderr, "Failed to connect to Seq server. Error: {}", httplib::to_string(res.error()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
add_executable(SwordMain swordMain.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(SwordMain
|
||||||
|
PkgConfig::LIBGPIOD
|
||||||
|
base
|
||||||
|
palAdapt
|
||||||
|
loRa
|
||||||
|
os
|
||||||
|
Mqtt
|
||||||
|
fmt::fmt
|
||||||
|
utils
|
||||||
|
Ilogging
|
||||||
|
)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static constexpr uint16_t UndefCmd = 0xFFFF;
|
||||||
|
|
||||||
|
enum ServiceCommands_e {
|
||||||
|
SetConfigVolume = 0x11, // Set Volume
|
||||||
|
SetConfigPromoDelay, // Set Delay between Promo
|
||||||
|
SetLockOpen,
|
||||||
|
SetLockClose,
|
||||||
|
SetDisableSound,
|
||||||
|
SetEnableSound,
|
||||||
|
PlaySoundPromo,
|
||||||
|
PlaySoundDeny,
|
||||||
|
PlaySoundChosen,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum GameModeCommands_e {
|
||||||
|
GameStarted = 0x21, // One Game has started (mute Promo mode)
|
||||||
|
GameFinished, // The Game is finished (resume to Promo mode)
|
||||||
|
GameNextTryLocked, // Lock sword on next try
|
||||||
|
GameNextTryRelease, // Release sword on next try
|
||||||
|
};
|
||||||
|
|
||||||
|
enum OutgoingCommands_e {
|
||||||
|
Status = 0x00, // Send Status
|
||||||
|
Response = 0x01, // Confirm last command
|
||||||
|
Error = 0x02, // Send error status
|
||||||
|
PullHappend = 0x03 // Message if User tried to pull the sword
|
||||||
|
};
|
||||||
|
|
||||||
|
enum GameModeResponses_e {
|
||||||
|
SwordPullHappend = 0x11, // Message if User tried to pull the sword
|
||||||
|
SwordReleased = 0x12, // Message that User pulled the sword
|
||||||
|
SwordReturned = 0x13 // Message that User returned the sword
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StatusFrame {
|
||||||
|
uint8_t cmd{OutgoingCommands_e::Status};
|
||||||
|
uint8_t version[4];
|
||||||
|
uint8_t mode;
|
||||||
|
uint8_t healt;
|
||||||
|
uint8_t sensors;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MqttQe {
|
||||||
|
MqttQe(std::string topic, std::string payload)
|
||||||
|
: Topic(topic),
|
||||||
|
Payload(payload) {}
|
||||||
|
std::string Topic;
|
||||||
|
std::string Payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class OpMode {
|
||||||
|
Undefined, // Enable in Game mode
|
||||||
|
Maintanance = 1, // Set Maintanance mode
|
||||||
|
PromoMode, // Enable PromoMode
|
||||||
|
GamingMode, // Enable in Game mode
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int SwordEntryPin = 16; // B1 Pulled-Out-Sensor
|
||||||
|
constexpr int SwordEndPin = 6; // B2 Put-In-Sensor
|
||||||
|
constexpr int SwordPulledPin = 13; // B3 Pull-Sensor
|
||||||
|
constexpr int sparePin = 12;
|
||||||
|
|
||||||
|
constexpr int LinMotPin1 = 5;
|
||||||
|
constexpr int LinMotPin2 = 0;
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "swordMain.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
class MqttChannel {
|
||||||
|
public:
|
||||||
|
MqttChannel(string version, SwordConfig cfg, DeviceIoState& ioStat, SeqLogger& log)
|
||||||
|
: Config(cfg),
|
||||||
|
IoStat(ioStat),
|
||||||
|
Logger(log),
|
||||||
|
runTopic(cfg.getStationPath() + "run"),
|
||||||
|
cfgTopic(cfg.getStationPath() + "cfg"),
|
||||||
|
verTopic(cfg.getStationPath() + "ver"),
|
||||||
|
eventTopic(cfg.getStationPath() + "event"),
|
||||||
|
statTopic(cfg.getStationPath() + "stat"),
|
||||||
|
resetTopic(cfg.getStationPath() + "reset"),
|
||||||
|
|
||||||
|
gameCommands({
|
||||||
|
{"gameStarted", GameModeCommands_e::GameStarted},
|
||||||
|
{"gameFinished", GameModeCommands_e::GameFinished},
|
||||||
|
{"stayLocked", GameModeCommands_e::GameNextTryLocked},
|
||||||
|
{"releaseLock", GameModeCommands_e::GameNextTryRelease},
|
||||||
|
}),
|
||||||
|
serviceCommands({
|
||||||
|
{"openLock", ServiceCommands_e::SetLockOpen},
|
||||||
|
{"closeLock", ServiceCommands_e::SetLockClose},
|
||||||
|
{"mute", ServiceCommands_e::SetDisableSound},
|
||||||
|
{"unmute", ServiceCommands_e::SetEnableSound},
|
||||||
|
{"playSound:Promo", ServiceCommands_e::PlaySoundPromo},
|
||||||
|
{"playSound:Deny", ServiceCommands_e::PlaySoundDeny},
|
||||||
|
{"playSound:Chosen", ServiceCommands_e::PlaySoundChosen},
|
||||||
|
}),
|
||||||
|
setCommands({
|
||||||
|
{"volume", ServiceCommands_e::SetConfigVolume},
|
||||||
|
{"promoDelay", ServiceCommands_e::SetConfigPromoDelay},
|
||||||
|
}),
|
||||||
|
subTopics{{runTopic, cfgTopic, resetTopic}},
|
||||||
|
client("Sword",
|
||||||
|
cfg.getBrockerUrl(),
|
||||||
|
subTopics,
|
||||||
|
{cfg.getMqttUser(), cfg.getMqttPassword()}) {
|
||||||
|
fmt::println("MQTT: Starting task:");
|
||||||
|
mqttCall = [this](string topic, string payload) {
|
||||||
|
if (topic == resetTopic ||
|
||||||
|
topic == cfgTopic) {
|
||||||
|
auto mq = std::make_unique<MqttQe>(topic, payload);
|
||||||
|
MasterQueue.enqueue(std::move(mq));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mq = std::make_unique<MqttQe>(topic, payload);
|
||||||
|
MqttQueue.enqueue(std::move(mq));
|
||||||
|
};
|
||||||
|
|
||||||
|
fmt::println("MQTT: service: successfully connected ...");
|
||||||
|
client.registerCallback(mqttCall);
|
||||||
|
|
||||||
|
client.send(verTopic, fmt::format("{}", version));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MqttQe> tryDeque() {
|
||||||
|
auto serMsg = MqttQueue.dequeue();
|
||||||
|
return serMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MqttQe> tryDequeueFor(ITime::Duration timeout) {
|
||||||
|
auto serMsg = MqttQueue.tryDequeueFor(timeout);
|
||||||
|
return serMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushInput() {
|
||||||
|
Thread::sleep(3s);
|
||||||
|
int cnt{};
|
||||||
|
while (true) {
|
||||||
|
if (!MqttQueue.tryDequeueFor(10ms)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
if (!MasterQueue.tryDequeueFor(10ms)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
fmt::println("Input queue flushed, {} Messages discarted", cnt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void runMasterService(Thread::Context& ctx, MqttChannel& instance, function<void()> resetCall) {
|
||||||
|
while (true) {
|
||||||
|
auto serMsg = instance.MasterQueue.dequeue();
|
||||||
|
if (!serMsg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serMsg->Topic == instance.resetTopic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto payload = serMsg->Payload;
|
||||||
|
if (payload == "mute") {
|
||||||
|
instance.IoStat.Mute = true;
|
||||||
|
}
|
||||||
|
if (payload == "unmute") {
|
||||||
|
instance.IoStat.Mute = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.Logger.sendEvent("Ctrl", payload);
|
||||||
|
if (auto pos = payload.find(":"); pos != std::string::npos) {
|
||||||
|
auto val = payload.substr(pos + 1);
|
||||||
|
if (auto iVal = tryParseInt(val); iVal.has_value()) {
|
||||||
|
auto val = iVal.value();
|
||||||
|
if (payload.starts_with("volume:")) {
|
||||||
|
if (val >= 20 && val <= 100) {
|
||||||
|
instance.IoStat.Volume = val;
|
||||||
|
MediaPlayer::setVolume(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.starts_with("promoDelay:")) {
|
||||||
|
if (val >= 30 && val <= 300) {
|
||||||
|
instance.IoStat.PromoDelay = std::chrono::seconds(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.serviceCommands.contains(payload)) {
|
||||||
|
auto code = instance.serviceCommands.at(payload);
|
||||||
|
switch (code) {
|
||||||
|
case ServiceCommands_e::SetLockOpen: {
|
||||||
|
instance.IoStat.SwordLatch.moveIn();
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::SetLockClose: {
|
||||||
|
instance.IoStat.SwordLatch.moveOut();
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::PlaySoundPromo: {
|
||||||
|
MediaPlayer::playWavFile(instance.Config.getSoundPromo());
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::PlaySoundDeny: {
|
||||||
|
MediaPlayer::playWavFile(instance.Config.getSoundDeny());
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::PlaySoundChosen: {
|
||||||
|
MediaPlayer::playWavFile(instance.Config.getSoundAllow());
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendStatusMessage(OpMode mode) {
|
||||||
|
try {
|
||||||
|
auto& s = IoStat;
|
||||||
|
client.send(statTopic, fmt::format("M:{}, snd:{}, sS:{},sE:{},sP:{},lE:{},v:{},pD:{}",
|
||||||
|
(int)mode,
|
||||||
|
!s.Mute,
|
||||||
|
s.SwordEntry.get(),
|
||||||
|
s.SwordEnd.get(),
|
||||||
|
s.SwordPulled.get(),
|
||||||
|
s.SwordLatch.isLocked(),
|
||||||
|
s.Volume,
|
||||||
|
s.PromoDelay));
|
||||||
|
} catch (const exception& exc) {
|
||||||
|
fmt::println(stderr, "\nERROR: Unable to connect to Setup bridge: {}", exc.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void sendEvent(std::string event) {
|
||||||
|
try {
|
||||||
|
auto& s = IoStat;
|
||||||
|
client.send(eventTopic, fmt::format("{}", event));
|
||||||
|
} catch (const exception& exc) {
|
||||||
|
fmt::println(stderr, "\nERROR: Unable to connect to Setup bridge: {}", exc.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<string, GameModeCommands_e>& getGameCommands() { return gameCommands; }
|
||||||
|
const std::map<string, ServiceCommands_e>& getSerCommands() { return serviceCommands; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
SwordConfig Config;
|
||||||
|
Queue<MqttQe> MasterQueue;
|
||||||
|
Queue<MqttQe> MqttQueue;
|
||||||
|
DeviceIoState& IoStat;
|
||||||
|
SeqLogger& Logger;
|
||||||
|
const string runTopic;
|
||||||
|
const string cfgTopic;
|
||||||
|
const string verTopic;
|
||||||
|
const string eventTopic;
|
||||||
|
const string statTopic;
|
||||||
|
const string resetTopic;
|
||||||
|
|
||||||
|
const std::map<string, GameModeCommands_e> gameCommands;
|
||||||
|
const std::map<string, ServiceCommands_e> serviceCommands;
|
||||||
|
const std::map<string, ServiceCommands_e> setCommands;
|
||||||
|
vector<string> subTopics;
|
||||||
|
MqttClient client;
|
||||||
|
std::function<void(string, string)> mqttCall;
|
||||||
|
|
||||||
|
static std::optional<int> tryParseInt(std::string val) {
|
||||||
|
try {
|
||||||
|
int iVal = std::stoi(val);
|
||||||
|
return {iVal};
|
||||||
|
} catch (...) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
#include "swordMain.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "mqttChannel.hpp"
|
||||||
|
|
||||||
|
using namespace pal;
|
||||||
|
using namespace std::literals::string_view_literals;
|
||||||
|
using namespace std::literals::chrono_literals;
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
constexpr uint8_t VersionArr[4] = {'V', 0, 8, 0};
|
||||||
|
const string Version = "V 0.8.0";
|
||||||
|
// const string Version = fmt::format("{} {}.{}.{}", VersionArr[0], VersionArr[1], VersionArr[2], VersionArr[3]);
|
||||||
|
|
||||||
|
SwordConfig SwordCfg{};
|
||||||
|
unique_ptr<DeviceIoState> IoStat;
|
||||||
|
OpMode Mode = OpMode::Maintanance;
|
||||||
|
|
||||||
|
void processPromo(MqttChannel& mqtt, SeqLogger& log);
|
||||||
|
void processProduction(MqttChannel& mqtt, SeqLogger& log);
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
CLI::App app{"Sword Firmware"};
|
||||||
|
string modeArg = "test";
|
||||||
|
string assetsBasePath = "/home/user/assets/sword";
|
||||||
|
string brocker = "mqtts://nerdyssey.de:8883";
|
||||||
|
string topic = "staging/outdoor/king-arthur/sword/";
|
||||||
|
string user = "exodususer";
|
||||||
|
string password = "awesomeexodusoutdooradventures";
|
||||||
|
string logUrl = "log.nerdyssey.de";
|
||||||
|
string logApiKey = "uYpJC6WyZAbFK2MUgwNH";
|
||||||
|
app.add_option("startmode", modeArg, "Startup mode")->required();
|
||||||
|
app.add_option("basepath", assetsBasePath, "Base bath for assets")->required();
|
||||||
|
app.add_option("-b,--brocker", brocker, "MQTT Topic URL");
|
||||||
|
app.add_option("-t,--topic", topic, "MQTT Topic");
|
||||||
|
app.add_option("-p,--passwd", password, "MQTT Brocker password");
|
||||||
|
app.add_option("-u,--user", user, "MQTT Brocker user");
|
||||||
|
app.add_option("-l,--log", logUrl, "Logging Service");
|
||||||
|
app.add_option("-k,--key", logApiKey, "Logging API Key");
|
||||||
|
|
||||||
|
CLI11_PARSE(app, argc, argv);
|
||||||
|
|
||||||
|
SeqLogger log(logUrl, logApiKey, "Sword");
|
||||||
|
SwordCfg.setVar(assetsBasePath, brocker, topic, user, password);
|
||||||
|
|
||||||
|
fmt::println("{}", app.get_description());
|
||||||
|
if (modeArg == "promo") {
|
||||||
|
fmt::println("starting with promo");
|
||||||
|
Mode = OpMode::PromoMode;
|
||||||
|
} else if (modeArg == "game") {
|
||||||
|
fmt::println("starting with promo");
|
||||||
|
Mode = OpMode::GamingMode;
|
||||||
|
} else {
|
||||||
|
fmt::println("unknown {}", modeArg);
|
||||||
|
}
|
||||||
|
constexpr array<const char*, 4> modes = {"Undefined", "Maintanance", "PromoMode", "Production"};
|
||||||
|
log.sendBootUp(Version, modes[static_cast<int>(Mode)]);
|
||||||
|
|
||||||
|
auto itrHandler = [&]() {
|
||||||
|
TriggeredInput itr(26, "PItr");
|
||||||
|
while (true) {
|
||||||
|
cout << "Await next interrupt\n";
|
||||||
|
if (itr.await()) {
|
||||||
|
cout << "Interrupt Event\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread interruptThread(itrHandler);
|
||||||
|
|
||||||
|
InputPort sbPort(SwordEntryPin, "PinSEntry");
|
||||||
|
InputPort sePort(SwordEndPin, "PInSEnd");
|
||||||
|
InputPort spPort(SwordPulledPin, "PInSPull");
|
||||||
|
InputPort sparePort(sparePin, "spare");
|
||||||
|
OutputPort mOut1(LinMotPin1, "M1:1");
|
||||||
|
OutputPort mOut2(LinMotPin2, "M1:2");
|
||||||
|
InputTransAware SwordEntry(sbPort);
|
||||||
|
InputTransAware SwordEnd(sePort);
|
||||||
|
InputTransAware SwordPulled(spPort);
|
||||||
|
LinMot SwordLatch(mOut1, mOut2);
|
||||||
|
|
||||||
|
IoStat = make_unique<DeviceIoState>(SwordEntry, SwordEnd, SwordPulled, SwordLatch);
|
||||||
|
|
||||||
|
MqttChannel mqtt(Version, SwordCfg, *IoStat, log);
|
||||||
|
auto resetCall = [&]() {
|
||||||
|
log.sendEvent("Reset", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
mqtt.flushInput();
|
||||||
|
static auto conTask = Thread::createExplicit("MqttTask", MqttChannel::runMasterService, ref(mqtt), resetCall);
|
||||||
|
|
||||||
|
SwordLatch.moveOut();
|
||||||
|
fmt::println("Running App");
|
||||||
|
while (true) {
|
||||||
|
switch (Mode) {
|
||||||
|
case OpMode::Maintanance: {
|
||||||
|
fmt::println("Running Maintanance");
|
||||||
|
// txQueue.emplace<ResponseTxMsg>(msg->code, 0);
|
||||||
|
Thread::sleep(500ms);
|
||||||
|
IoStat->tick();
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
// auto e = make_unique<ResponseTxMsg>(msg->code, 0);
|
||||||
|
// txQueue.enqueue(move(e));
|
||||||
|
} break;
|
||||||
|
case OpMode::PromoMode: {
|
||||||
|
fmt::println("Running PromoMode");
|
||||||
|
processPromo(mqtt, log);
|
||||||
|
} break;
|
||||||
|
case OpMode::GamingMode: {
|
||||||
|
fmt::println("Running GamingMode");
|
||||||
|
processProduction(mqtt, log);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interruptThread.join();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void processPromo(MqttChannel& mqtt, SeqLogger& log) {
|
||||||
|
auto tn = chrono::steady_clock::now();
|
||||||
|
auto& serCmds = mqtt.getSerCommands();
|
||||||
|
while (true) {
|
||||||
|
Thread::sleep(10ms);
|
||||||
|
IoStat->tick();
|
||||||
|
if (IoStat->SwordPulled.isPosEdge()) {
|
||||||
|
if (!IoStat->Mute) {
|
||||||
|
log.sendEvent("Pull", "Start Audio");
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundPromo());
|
||||||
|
fmt::println("Waiting for {}", IoStat->PromoDelay);
|
||||||
|
Thread::sleep(IoStat->PromoDelay);
|
||||||
|
fmt::println("Waiting done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chrono::steady_clock::now() > (tn + 1s)) {
|
||||||
|
tn = chrono::steady_clock::now();
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processProduction(MqttChannel& mqtt, SeqLogger& log) {
|
||||||
|
auto& serCmds = mqtt.getSerCommands();
|
||||||
|
auto& gameCmds = mqtt.getGameCommands();
|
||||||
|
while (true) {
|
||||||
|
auto msg = mqtt.tryDeque();
|
||||||
|
if (!msg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto code = gameCmds.at(msg->Payload);
|
||||||
|
switch (code) {
|
||||||
|
case GameModeCommands_e::GameStarted: {
|
||||||
|
fmt::println("Got GameStarted");
|
||||||
|
log.sendEvent("Req", "GameStarted");
|
||||||
|
}
|
||||||
|
case GameModeCommands_e::GameFinished: {
|
||||||
|
fmt::println("Got GameFinished");
|
||||||
|
log.sendEvent("Req", "GameFinished");
|
||||||
|
}
|
||||||
|
|
||||||
|
case GameModeCommands_e::GameNextTryLocked: {
|
||||||
|
fmt::println("Got NextTryLock");
|
||||||
|
log.sendEvent("Req", "StayLockded");
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordPulled.sample();
|
||||||
|
if (IoStat->SwordPulled.isPosEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
// say: You are not the one
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundDeny());
|
||||||
|
Thread::sleep(15s);
|
||||||
|
log.sendEvent("Req", "Pulled");
|
||||||
|
mqtt.sendEvent("pull");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case GameModeCommands_e::GameNextTryRelease: {
|
||||||
|
fmt::println("NextTryRelease");
|
||||||
|
log.sendEvent("Req", "UnlockSword");
|
||||||
|
IoStat->SwordLatch.moveIn();
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEnd.sample();
|
||||||
|
if (IoStat->SwordEnd.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEntry.sample();
|
||||||
|
if (IoStat->SwordEntry.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mqtt.sendEvent("pulled-out");
|
||||||
|
log.sendEvent("Req", "Pulled Out");
|
||||||
|
// say: You ARE the one
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundAllow());
|
||||||
|
Thread::sleep(15s);
|
||||||
|
mqtt.sendEvent("ceremonyDone");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEnd.sample();
|
||||||
|
if (IoStat->SwordEnd.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEntry.sample();
|
||||||
|
if (IoStat->SwordEntry.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.sendEvent("Req", "Put Back");
|
||||||
|
mqtt.sendEvent("put-in");
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/poll.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <CLI/CLI.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "MqttClient.hpp"
|
||||||
|
#include "coreExcept.hpp"
|
||||||
|
#include "gpioPort.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include "queue.hpp"
|
||||||
|
#include "textUtils.hpp"
|
||||||
|
#include "thread.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
class LinMot {
|
||||||
|
public:
|
||||||
|
LinMot(OutputPort& o1, OutputPort& o2)
|
||||||
|
: _out1(o1),
|
||||||
|
_out2(o2) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveOut() { set(true); }
|
||||||
|
void moveIn() { set(false); }
|
||||||
|
bool isLocked() { return _locked; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
OutputPort& _out1;
|
||||||
|
OutputPort& _out2;
|
||||||
|
bool _locked;
|
||||||
|
void set(bool val) {
|
||||||
|
_out1.write(val);
|
||||||
|
_out2.write(!val);
|
||||||
|
_locked = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeviceIoState {
|
||||||
|
public:
|
||||||
|
DeviceIoState(pal::InputTransAware& entry,
|
||||||
|
pal::InputTransAware& end,
|
||||||
|
pal::InputTransAware& pulled,
|
||||||
|
LinMot& latch)
|
||||||
|
: SwordEntry(entry),
|
||||||
|
SwordEnd(end),
|
||||||
|
SwordPulled(pulled),
|
||||||
|
SwordLatch(latch) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick() {
|
||||||
|
SwordEntry.sample();
|
||||||
|
SwordEnd.sample();
|
||||||
|
SwordPulled.sample();
|
||||||
|
}
|
||||||
|
|
||||||
|
pal::InputTransAware& SwordEntry;
|
||||||
|
pal::InputTransAware& SwordEnd;
|
||||||
|
pal::InputTransAware& SwordPulled;
|
||||||
|
LinMot& SwordLatch;
|
||||||
|
|
||||||
|
std::chrono::seconds PromoDelay{150};
|
||||||
|
int Volume{50};
|
||||||
|
bool Mute{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class SwordConfig {
|
||||||
|
public:
|
||||||
|
std::string getSoundPromo() { return soundPromo; }
|
||||||
|
std::string getSoundDeny() { return soundDeny; }
|
||||||
|
std::string getSoundAllow() { return soundAllow; }
|
||||||
|
std::string getBrockerUrl() { return brockerUrl; }
|
||||||
|
std::string getMqttUser() { return mqttUser; }
|
||||||
|
std::string getMqttPassword() { return mqttPassword; }
|
||||||
|
std::string getStationPath() { return stationPath; }
|
||||||
|
|
||||||
|
void setVar(std::string base,
|
||||||
|
std::string brocker, std::string topic,
|
||||||
|
std::string user, std::string passwd) {
|
||||||
|
if (wasSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
soundPromo = base + soundPromo;
|
||||||
|
soundDeny = base + soundDeny;
|
||||||
|
soundAllow = base + soundAllow;
|
||||||
|
|
||||||
|
brockerUrl = brocker;
|
||||||
|
stationPath = topic;
|
||||||
|
mqttUser = user;
|
||||||
|
mqttPassword = passwd;
|
||||||
|
wasSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool wasSet{};
|
||||||
|
std::string assetBase{};
|
||||||
|
std::string soundPromo{"/promo.wav"};
|
||||||
|
std::string soundPromoShort{"/promoShort.wav"};
|
||||||
|
std::string soundDeny{"/deny.wav"};
|
||||||
|
std::string soundAllow{"/allow.wav"};
|
||||||
|
|
||||||
|
std::string brockerUrl;
|
||||||
|
std::string mqttUser;
|
||||||
|
std::string mqttPassword;
|
||||||
|
std::string stationPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MediaPlayer {
|
||||||
|
public:
|
||||||
|
static void playWavFile(const string& filePath) {
|
||||||
|
if (filePath.empty()) {
|
||||||
|
fmt::println(stderr, "ERROR: Empty file path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread audioThread([filePath]() {
|
||||||
|
auto cmd = fmt::format("aplay '{}' 2>/dev/null", filePath);
|
||||||
|
auto result = system(cmd.c_str());
|
||||||
|
if (result != 0) {
|
||||||
|
fmt::println(stderr, "ERROR: Failed to play audio file: '{}'", filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
audioThread.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setVolume(int volume) {
|
||||||
|
if (volume < 0) {
|
||||||
|
volume = 0;
|
||||||
|
}
|
||||||
|
if (volume > 100) {
|
||||||
|
volume = 100;
|
||||||
|
}
|
||||||
|
static const std::string control = findVolumeControl();
|
||||||
|
if (control.empty()) {
|
||||||
|
fmt::println(stderr, "ERROR: No ALSA volume control found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto cmd = fmt::format("amixer -q sset '{}' {}% 2>/dev/null", control, volume);
|
||||||
|
auto result = system(cmd.c_str());
|
||||||
|
if (result != 0) {
|
||||||
|
fmt::println(stderr, "ERROR: Failed to set volume to {}%", volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string findVolumeControl() {
|
||||||
|
for (const auto& name : {"Speaker", "PCM", "Master", "Headphones", "Headphone"}) {
|
||||||
|
auto cmd = fmt::format("amixer sget '{}' 2>/dev/null", name);
|
||||||
|
if (system(cmd.c_str()) == 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
add_executable(ThroneBridgeMain throneBridgeMain.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(ThroneBridgeMain
|
||||||
|
PkgConfig::LIBGPIOD
|
||||||
|
base
|
||||||
|
palAdapt
|
||||||
|
loRa
|
||||||
|
os
|
||||||
|
Mqtt
|
||||||
|
fmt::fmt
|
||||||
|
utils
|
||||||
|
Ilogging
|
||||||
|
)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
static constexpr uint16_t UndefCmd = 0xFFFF;
|
||||||
|
|
||||||
|
enum IncommingCommands_e : uint16_t {
|
||||||
|
Qurey = 0x00, // Qurey info
|
||||||
|
SetMaintanance = 0x01, // Set Maintanance mode
|
||||||
|
SetPromoMode = 0x02, // Enable PromoMode
|
||||||
|
SetGamingMode = 0x03, // Enable in Game mode
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ServiceCommands_e {
|
||||||
|
SetConfigVolume = 0x11, // Set Volume
|
||||||
|
SetConfigPromoDelay, // Set Delay between Promo
|
||||||
|
PlaySoundPromo,
|
||||||
|
PlaySoundCoronation,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum GameModeCommands_e {
|
||||||
|
GameStarted = 0x21, // One Game has started (mute Promo mode)
|
||||||
|
GameFinished, // The Game is finished (resume to Promo mode)
|
||||||
|
GameOpenCoinSlot, // Lock sword on next try
|
||||||
|
GameCloseCoinSlot, // Release sword on next try
|
||||||
|
};
|
||||||
|
|
||||||
|
enum OutgoingCommands_e {
|
||||||
|
Status = 0x00, // Send Status
|
||||||
|
Response = 0x01, // Confirm last command
|
||||||
|
Error = 0x02, // Send error status
|
||||||
|
PullHappend = 0x03 // Message if User tried to pull the sword
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StatusFrame {
|
||||||
|
uint8_t cmd{OutgoingCommands_e::Status};
|
||||||
|
uint8_t version[4];
|
||||||
|
uint8_t mode;
|
||||||
|
uint8_t healt;
|
||||||
|
uint8_t sensors;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResponseFrame {
|
||||||
|
uint8_t cmd{OutgoingCommands_e::Response};
|
||||||
|
uint8_t resCmd;
|
||||||
|
uint8_t status;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RxMsg {
|
||||||
|
RxMsg(uint16_t m)
|
||||||
|
: code(m) {}
|
||||||
|
uint16_t code;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TxMsg {
|
||||||
|
TxMsg(uint32_t m)
|
||||||
|
: cmd(m) {}
|
||||||
|
uint8_t cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MqttQe {
|
||||||
|
MqttQe(std::string topic, std::string payload)
|
||||||
|
: Topic(topic),
|
||||||
|
Payload(payload) {}
|
||||||
|
std::string Topic;
|
||||||
|
std::string Payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResponseTxMsg : public TxMsg {
|
||||||
|
ResponseTxMsg(uint16_t cmd, uint8_t stat)
|
||||||
|
: TxMsg(OutgoingCommands_e::Response) {};
|
||||||
|
uint16_t resCmd;
|
||||||
|
uint8_t status;
|
||||||
|
};
|
||||||
|
struct StatusTxMsg : public TxMsg {
|
||||||
|
StatusTxMsg(uint8_t mode, uint8_t swEnd, uint8_t swBeg, uint8_t swPull, uint8_t out)
|
||||||
|
: TxMsg(OutgoingCommands_e::Status),
|
||||||
|
_mode(mode),
|
||||||
|
_inSwEnd(swEnd),
|
||||||
|
_inSwBeg(swBeg),
|
||||||
|
_inSwPull(swPull),
|
||||||
|
_out(out) {
|
||||||
|
}
|
||||||
|
uint8_t _mode;
|
||||||
|
bool _inSwEnd;
|
||||||
|
bool _inSwBeg;
|
||||||
|
bool _inSwPull;
|
||||||
|
bool _out;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int GPIO_RST_PIN = 1;
|
||||||
|
constexpr int GPIO_DIO0_PIN = 27;
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
#include "throneBridgeMain.hpp"
|
||||||
|
|
||||||
|
using namespace pal;
|
||||||
|
using namespace std::literals::string_view_literals;
|
||||||
|
using namespace std::literals::chrono_literals;
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
constexpr uint8_t VersionArr[4] = {'V', 0, 8, 0};
|
||||||
|
const string Version = "V 0.8.0";
|
||||||
|
// const string Version = fmt::format("{} {}.{}.{}", VersionArr[0], VersionArr[1], VersionArr[2], VersionArr[3]);
|
||||||
|
|
||||||
|
SwordConfig SwordCfg{};
|
||||||
|
unique_ptr<DeviceIoState> IoStat;
|
||||||
|
OpMode Mode = OpMode::Maintanance;
|
||||||
|
|
||||||
|
void processMaintanence(MqttChannel& mqtt);
|
||||||
|
void processPromo(MqttChannel& mqtt, SeqLogger& log);
|
||||||
|
void processProduction(MqttChannel& mqtt, SeqLogger& log);
|
||||||
|
|
||||||
|
/// @brief MQTT-Service provider
|
||||||
|
void MQttServiceTask(Thread::Context& ctx, Queue<RxMsg>& rxQueue, Queue<TxMsg>& txQueue) {
|
||||||
|
string runTopic = SwordCfg.getStationPath() + "run";
|
||||||
|
string intTopic = SwordCfg.getStationPath() + "int";
|
||||||
|
string statTopic = SwordCfg.getStationPath() + "stat";
|
||||||
|
|
||||||
|
fmt::println("MQTT: Starting task:");
|
||||||
|
fmt::println(" Topic: {}", runTopic);
|
||||||
|
|
||||||
|
auto mqttCallback = [&](string topic, string payload) {
|
||||||
|
fmt::print("MQTT: on sendTrigger {}:{}'\n", topic, payload);
|
||||||
|
|
||||||
|
static std::map<string, IncommingCommands_e> commands = {
|
||||||
|
{"Qurey", IncommingCommands_e::Qurey},
|
||||||
|
{"SetMaintanance", IncommingCommands_e::SetMaintanance},
|
||||||
|
{"SetPromoMode", IncommingCommands_e::SetPromoMode},
|
||||||
|
{"SetGamingMode", IncommingCommands_e::SetGamingMode},
|
||||||
|
};
|
||||||
|
static std::map<string, ServiceCommands_e> serCommands = {
|
||||||
|
{"ServiceOpen", ServiceCommands_e::Open},
|
||||||
|
{"ServiceClose", ServiceCommands_e::Close},
|
||||||
|
{"ServicePlay1", ServiceCommands_e::Play1},
|
||||||
|
{"ServicePlay2", ServiceCommands_e::Play2},
|
||||||
|
{"ServicePlay3", ServiceCommands_e::Play3},
|
||||||
|
};
|
||||||
|
static std::map<string, GameModeCommands_e> gameCommands = {
|
||||||
|
{"GameStayLocked", GameModeCommands_e::GameNextTryLocked},
|
||||||
|
{"GameReleaseLock", GameModeCommands_e::GameNextTryRelease},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (topic != runTopic) {
|
||||||
|
fmt::print("MQTT: ignore invalid topic {}'\n", topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commands.contains(payload)) {
|
||||||
|
rxQueue.emplace<RxMsg>(commands.at(payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (serCommands.contains(payload)) {
|
||||||
|
auto code = serCommands.at(payload);
|
||||||
|
rxQueue.emplace<RxMsg>(code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gameCommands.contains(payload)) {
|
||||||
|
rxQueue.emplace<RxMsg>(serCommands.at(payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt::print("MQTT: unknown command {}:{}'\n", topic, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::vector<string> topicsToRegister{runTopic};
|
||||||
|
MqttClient client("Sword", SwordCfg.getBrockerUrl(), topicsToRegister, {SwordCfg.getMqttUser(), SwordCfg.getMqttPassword()});
|
||||||
|
fmt::println("MQTT: service: successfully connected ...");
|
||||||
|
client.registerCallback(mqttCallback);
|
||||||
|
|
||||||
|
client.send(statTopic, fmt::format("{} Mode={}, sE:0,sS:0,sP:0,lE:0",
|
||||||
|
Version, (int)Mode));
|
||||||
|
|
||||||
|
int cnt = 0;
|
||||||
|
fmt::println("MQTT: Running ...");
|
||||||
|
while (!ctx.isCancelled()) {
|
||||||
|
auto tx = txQueue.dequeue();
|
||||||
|
|
||||||
|
switch (tx->cmd) {
|
||||||
|
case OutgoingCommands_e::Status: {
|
||||||
|
auto& s = *IoStat;
|
||||||
|
client.send(statTopic, fmt::format("{} Mode={}, sE:{},sS:{},sP:{},lE:{}",
|
||||||
|
Version, (int)Mode,
|
||||||
|
s.SwordEnd.isActive(),
|
||||||
|
s.SwordEntry.isActive(),
|
||||||
|
s.SwordPulled.isActive(),
|
||||||
|
s.SwordLatch.isLocked()));
|
||||||
|
} break;
|
||||||
|
case OutgoingCommands_e::Response:
|
||||||
|
client.send(intTopic, "some response");
|
||||||
|
break;
|
||||||
|
case OutgoingCommands_e::Error:
|
||||||
|
client.send(intTopic, "some error");
|
||||||
|
break;
|
||||||
|
case OutgoingCommands_e::PullHappend:
|
||||||
|
client.send(intTopic, "pulled");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
client.send(intTopic, "sent");
|
||||||
|
}
|
||||||
|
} catch (const exception& exc) {
|
||||||
|
fmt::println(stderr, "\nERROR: Unable to connect to Setup bridge: {}", exc.what());
|
||||||
|
ctx.markFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief LoRa-Service provider
|
||||||
|
void LoRaServiceTask(Thread::Context& ctx, Queue<RxMsg>& rxQueue, Queue<TxMsg>& txQueue) {
|
||||||
|
fmt::println("LoRa: Starting task:");
|
||||||
|
|
||||||
|
auto loraCallback = [&](std::vector<uint8_t> data) {
|
||||||
|
auto hex = bytesToHex(data);
|
||||||
|
auto str = bytesToPrintable(data);
|
||||||
|
fmt::print("LoRa: Received Byts: {}\n 0x{}\n {}\n", data.size(), hex, str);
|
||||||
|
|
||||||
|
auto e = std::make_unique<RxMsg>(0);
|
||||||
|
rxQueue.enqueue(std::move(e));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto spi = SpiPort({.mode = SpiPort::Mode0,
|
||||||
|
.cs = SpiPort::Cs0,
|
||||||
|
.frequ = 1'000'000});
|
||||||
|
|
||||||
|
LoRa loRa({
|
||||||
|
.frequency = 868'000'000,
|
||||||
|
.port = spi,
|
||||||
|
.DioPin = GPIO_DIO0_PIN,
|
||||||
|
.resetPin = GPIO_RST_PIN,
|
||||||
|
},
|
||||||
|
loraCallback);
|
||||||
|
|
||||||
|
fmt::println("LoRa: Running ...");
|
||||||
|
while (!ctx.isCancelled()) {
|
||||||
|
auto tx = txQueue.dequeue();
|
||||||
|
loRa.send({0});
|
||||||
|
// printf ("Message: %s %lu\n", msg->message.c_str(), msg->cnt);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
} catch (const exception& exc) {
|
||||||
|
fmt::println(stderr, "\nERROR: Unable to connect to Setup bridge: {}", exc.what());
|
||||||
|
ctx.markFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
CLI::App app{"Sword Firmware"};
|
||||||
|
string modeArg = "test";
|
||||||
|
string assetsBasePath = "/home/user/assets";
|
||||||
|
string brocker = "mqtts://nerdyssey.de:8883";
|
||||||
|
string topic = "staging/outdoor/king-arthur/sword/";
|
||||||
|
string user = "exodususer";
|
||||||
|
string password = "awesomeexodusoutdooradventures";
|
||||||
|
string logUrl = "log.nerdyssey.de";
|
||||||
|
string logApiKey = "uYpJC6WyZAbFK2MUgwNH";
|
||||||
|
app.add_option("startmode", modeArg, "Startup mode")->required();
|
||||||
|
app.add_option("basepath", assetsBasePath, "Base bath for assets")->required();
|
||||||
|
app.add_option("-b,--brocker", brocker, "MQTT Topic URL");
|
||||||
|
app.add_option("-t,--topic", topic, "MQTT Topic");
|
||||||
|
app.add_option("-p,--passwd", password, "MQTT Brocker password");
|
||||||
|
app.add_option("-u,--user", user, "MQTT Brocker user");
|
||||||
|
app.add_option("-l,--log", logUrl, "Logging Service");
|
||||||
|
app.add_option("-k,--key", logApiKey, "Logging API Key");
|
||||||
|
|
||||||
|
CLI11_PARSE(app, argc, argv);
|
||||||
|
|
||||||
|
SeqLogger log(logUrl, logApiKey, "Sword");
|
||||||
|
SwordCfg.setVar(assetsBasePath, brocker, topic, user, password);
|
||||||
|
|
||||||
|
fmt::println("{}", app.get_description());
|
||||||
|
if (modeArg == "promo") {
|
||||||
|
fmt::println("starting with promo");
|
||||||
|
Mode = OpMode::PromoMode;
|
||||||
|
} else if (modeArg == "game") {
|
||||||
|
fmt::println("starting with promo");
|
||||||
|
Mode = OpMode::GamingMode;
|
||||||
|
} else {
|
||||||
|
fmt::println("unknown {}", modeArg);
|
||||||
|
}
|
||||||
|
constexpr array<const char*, 4> modes = {"Undefined", "Maintanance", "PromoMode", "Production"};
|
||||||
|
log.sendBootUp(Version, modes[static_cast<int>(Mode)]);
|
||||||
|
|
||||||
|
auto itrHandler = [&]() {
|
||||||
|
TriggeredInput itr(26, "PItr");
|
||||||
|
while (true) {
|
||||||
|
cout << "Await next interrupt\n";
|
||||||
|
if (itr.await()) {
|
||||||
|
cout << "Interrupt Event\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread interruptThread(itrHandler);
|
||||||
|
|
||||||
|
InputPort sbPort(SwordEntryPin, "PinSEntry");
|
||||||
|
InputPort sePort(SwordEndPin, "PInSEnd");
|
||||||
|
InputPort spPort(SwordPulledPin, "PInSPull");
|
||||||
|
InputPort sparePort(sparePin, "spare");
|
||||||
|
OutputPort mOut1(LinMotPin1, "M1:1");
|
||||||
|
OutputPort mOut2(LinMotPin2, "M1:2");
|
||||||
|
InputTransAware SwordEntry(sbPort);
|
||||||
|
InputTransAware SwordEnd(sePort);
|
||||||
|
InputTransAware SwordPulled(spPort);
|
||||||
|
LinMot SwordLatch(mOut1, mOut2);
|
||||||
|
|
||||||
|
IoStat = make_unique<DeviceIoState>(SwordEntry, SwordEnd, SwordPulled, SwordLatch);
|
||||||
|
|
||||||
|
static Queue<RxMsg> rxQueue;
|
||||||
|
static Queue<TxMsg> txQueue;
|
||||||
|
MqttChannel mqtt(Version, SwordCfg, *IoStat, log);
|
||||||
|
auto resetCall = [&]() {
|
||||||
|
log.sendEvent("Reset", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
mqtt.flushInput();
|
||||||
|
static auto conTask = Thread::createExplicit("MqttTask", MqttChannel::runMasterService, ref(mqtt), resetCall);
|
||||||
|
|
||||||
|
SwordLatch.moveOut();
|
||||||
|
fmt::println("Running App");
|
||||||
|
while (true) {
|
||||||
|
switch (Mode) {
|
||||||
|
case OpMode::Maintanance: {
|
||||||
|
fmt::println("Running Maintanance");
|
||||||
|
// txQueue.emplace<ResponseTxMsg>(msg->code, 0);
|
||||||
|
processMaintanence(mqtt);
|
||||||
|
// auto e = make_unique<ResponseTxMsg>(msg->code, 0);
|
||||||
|
// txQueue.enqueue(move(e));
|
||||||
|
} break;
|
||||||
|
case OpMode::PromoMode: {
|
||||||
|
fmt::println("Running PromoMode");
|
||||||
|
processPromo(mqtt, log);
|
||||||
|
} break;
|
||||||
|
case OpMode::GamingMode: {
|
||||||
|
fmt::println("Running GamingMode");
|
||||||
|
processProduction(mqtt, log);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interruptThread.join();
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void processMaintanenceCommand(enum ServiceCommands_e code) {
|
||||||
|
switch (code) {
|
||||||
|
case ServiceCommands_e::SetLockOpen: {
|
||||||
|
IoStat->SwordLatch.moveIn();
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::SetLockClose: {
|
||||||
|
IoStat->SwordLatch.moveOut();
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::PlaySoundPromo: {
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundPromo());
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::PlaySoundDeny: {
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundDeny());
|
||||||
|
} break;
|
||||||
|
case ServiceCommands_e::PlaySoundChosen: {
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundAllow());
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processMaintanence(MqttChannel& mqtt) {
|
||||||
|
auto& serCmds = mqtt.getSerCommands();
|
||||||
|
while (true) {
|
||||||
|
auto msg = mqtt.tryDequeueFor(500ms);
|
||||||
|
if (msg) {
|
||||||
|
if (serCmds.contains(msg->Payload)) {
|
||||||
|
auto code = serCmds.at(msg->Payload);
|
||||||
|
processMaintanenceCommand(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IoStat->tick();
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processPromo(MqttChannel& mqtt, SeqLogger& log) {
|
||||||
|
auto tn = chrono::steady_clock::now();
|
||||||
|
auto& serCmds = mqtt.getSerCommands();
|
||||||
|
while (true) {
|
||||||
|
auto msg = mqtt.tryDequeueFor(10ms);
|
||||||
|
if (msg) {
|
||||||
|
if (serCmds.contains(msg->Payload)) {
|
||||||
|
auto code = serCmds.at(msg->Payload);
|
||||||
|
processMaintanenceCommand(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IoStat->tick();
|
||||||
|
if (IoStat->SwordPulled.isPosEdge()) {
|
||||||
|
log.sendEvent("Pull", "Start Audio");
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundPromo());
|
||||||
|
fmt::println("Waiting for {}", IoStat->PromoDelay);
|
||||||
|
Thread::sleep(IoStat->PromoDelay);
|
||||||
|
fmt::println("Waiting done");
|
||||||
|
}
|
||||||
|
if (chrono::steady_clock::now() > (tn + 1s)) {
|
||||||
|
tn = chrono::steady_clock::now();
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void processProduction(MqttChannel& mqtt, SeqLogger& log) {
|
||||||
|
auto& serCmds = mqtt.getSerCommands();
|
||||||
|
auto& gameCmds = mqtt.getGameCommands();
|
||||||
|
while (true) {
|
||||||
|
auto msg = mqtt.tryDeque();
|
||||||
|
if (!msg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serCmds.contains(msg->Payload)) {
|
||||||
|
auto code = serCmds.at(msg->Payload);
|
||||||
|
processMaintanenceCommand(code);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto code = gameCmds.at(msg->Payload);
|
||||||
|
switch (code) {
|
||||||
|
case GameModeCommands_e::GameStarted: {
|
||||||
|
fmt::println("Got GameStarted");
|
||||||
|
log.sendEvent("Req", "GameStarted");
|
||||||
|
}
|
||||||
|
case GameModeCommands_e::GameFinished: {
|
||||||
|
fmt::println("Got GameFinished");
|
||||||
|
log.sendEvent("Req", "GameFinished");
|
||||||
|
}
|
||||||
|
|
||||||
|
case GameModeCommands_e::GameNextTryLocked: {
|
||||||
|
fmt::println("Got NextTryLock");
|
||||||
|
log.sendEvent("Req", "StayLockded");
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordPulled.sample();
|
||||||
|
if (IoStat->SwordPulled.isPosEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
// say: You are not the one
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundDeny());
|
||||||
|
Thread::sleep(15s);
|
||||||
|
log.sendEvent("Req", "Pulled");
|
||||||
|
mqtt.sendEvent("pull");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case GameModeCommands_e::GameNextTryRelease: {
|
||||||
|
fmt::println("NextTryRelease");
|
||||||
|
log.sendEvent("Req", "UnlockSword");
|
||||||
|
IoStat->SwordLatch.moveIn();
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEnd.sample();
|
||||||
|
if (IoStat->SwordEnd.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEntry.sample();
|
||||||
|
if (IoStat->SwordEntry.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mqtt.sendEvent("pulled-out");
|
||||||
|
log.sendEvent("Req", "Pulled Out");
|
||||||
|
// say: You ARE the one
|
||||||
|
MediaPlayer::playWavFile(SwordCfg.getSoundAllow());
|
||||||
|
Thread::sleep(15s);
|
||||||
|
mqtt.sendEvent("ceremonyDone");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEnd.sample();
|
||||||
|
if (IoStat->SwordEnd.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
IoStat->SwordEntry.sample();
|
||||||
|
if (IoStat->SwordEntry.isNegEdge()) {
|
||||||
|
mqtt.sendStatusMessage(Mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.sendEvent("Req", "Put Back");
|
||||||
|
mqtt.sendEvent("put-in");
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/poll.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <CLI/CLI.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <functional>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "MqttClient.hpp"
|
||||||
|
#include "coreExcept.hpp"
|
||||||
|
#include "gpioPort.hpp"
|
||||||
|
#include "loRa.hpp"
|
||||||
|
#include "logging.hpp"
|
||||||
|
#include "mutex.hpp"
|
||||||
|
#include "palHw.hpp"
|
||||||
|
#include "queue.hpp"
|
||||||
|
#include "semaphore.hpp"
|
||||||
|
#include "spiPort.hpp"
|
||||||
|
#include "textUtils.hpp"
|
||||||
|
#include "thread.hpp"
|
||||||
|
|
||||||
|
// internal interfaces
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
class DeviceIoState {
|
||||||
|
public:
|
||||||
|
DeviceIoState(int& count,
|
||||||
|
bool& protect)
|
||||||
|
: CoinAccepted(count),
|
||||||
|
CoinDenied(count),
|
||||||
|
CoinProtector(protect) {
|
||||||
|
}
|
||||||
|
|
||||||
|
int& CoinAccepted;
|
||||||
|
int& CoinDenied;
|
||||||
|
bool& CoinProtector;
|
||||||
|
|
||||||
|
std::chrono::seconds PromoDelay{60};
|
||||||
|
int Volume{100};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThroneConfig {
|
||||||
|
public:
|
||||||
|
std::string getSoundPromo() { return soundPromo; }
|
||||||
|
std::string getSoundCoronation() { return soundCoronation; }
|
||||||
|
std::string getBrockerUrl() { return brockerUrl; }
|
||||||
|
std::string getMqttUser() { return mqttUser; }
|
||||||
|
std::string getMqttPassword() { return mqttPassword; }
|
||||||
|
std::string getStationPath() { return stationPath; }
|
||||||
|
|
||||||
|
void setVar(std::string base,
|
||||||
|
std::string brocker, std::string topic,
|
||||||
|
std::string user, std::string passwd) {
|
||||||
|
if (wasSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
soundPromo = base + soundPromo;
|
||||||
|
soundCoronation = base + soundCoronation;
|
||||||
|
|
||||||
|
brockerUrl = brocker;
|
||||||
|
stationPath = topic;
|
||||||
|
mqttUser = user;
|
||||||
|
mqttPassword = passwd;
|
||||||
|
wasSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool wasSet{};
|
||||||
|
std::string soundPromo{"/promo.wav"};
|
||||||
|
std::string soundCoronation{"/coronation.wav"};
|
||||||
|
|
||||||
|
std::string brockerUrl;
|
||||||
|
std::string mqttUser;
|
||||||
|
std::string mqttPassword;
|
||||||
|
std::string stationPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class OpMode {
|
||||||
|
Undefined, // Enable in Game mode
|
||||||
|
Maintanance = 1, // Set Maintanance mode
|
||||||
|
PromoMode, // Enable PromoMode
|
||||||
|
GamingMode, // Enable in Game mode
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int CoinProtectPin = 16;
|
||||||
|
constexpr int CoinCntPin = 6;
|
||||||
|
constexpr int GPIO_RST_PIN = 1;
|
||||||
|
constexpr int GPIO_DIO0_PIN = 27;
|
||||||
|
|
||||||
|
class MediaPlayer {
|
||||||
|
public:
|
||||||
|
static void playWavFile(const string& filePath) {
|
||||||
|
if (filePath.empty()) {
|
||||||
|
fmt::println(stderr, "ERROR: Empty file path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread audioThread([filePath]() {
|
||||||
|
auto cmd = fmt::format("aplay '{}' 2>/dev/null", filePath);
|
||||||
|
auto result = system(cmd.c_str());
|
||||||
|
if (result != 0) {
|
||||||
|
fmt::println(stderr, "ERROR: Failed to play audio file: '{}'", filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
audioThread.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setVolume(int volume) {
|
||||||
|
if (volume < 0) {
|
||||||
|
volume = 0;
|
||||||
|
}
|
||||||
|
if (volume > 100) {
|
||||||
|
volume = 100;
|
||||||
|
}
|
||||||
|
static const std::string control = findVolumeControl();
|
||||||
|
if (control.empty()) {
|
||||||
|
fmt::println(stderr, "ERROR: No ALSA volume control found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto cmd = fmt::format("amixer -q sset '{}' {}% 2>/dev/null", control, volume);
|
||||||
|
auto result = system(cmd.c_str());
|
||||||
|
if (result != 0) {
|
||||||
|
fmt::println(stderr, "ERROR: Failed to set volume to {}%", volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string findVolumeControl() {
|
||||||
|
for (const auto& name : {"Speaker", "PCM", "Master", "Headphones", "Headphone"}) {
|
||||||
|
auto cmd = fmt::format("amixer sget '{}' 2>/dev/null", name);
|
||||||
|
if (system(cmd.c_str()) == 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MqttChannel {
|
||||||
|
public:
|
||||||
|
MqttChannel(string version, SwordConfig cfg, DeviceIoState& ioStat, SeqLogger& log)
|
||||||
|
: Config(cfg),
|
||||||
|
IoStat(ioStat),
|
||||||
|
Logger(log),
|
||||||
|
setTopic(cfg.getStationPath() + "set"),
|
||||||
|
verTopic(cfg.getStationPath() + "ver"),
|
||||||
|
eventTopic(cfg.getStationPath() + "event"),
|
||||||
|
statTopic(cfg.getStationPath() + "stat"),
|
||||||
|
resetTopic(cfg.getStationPath() + "reset"),
|
||||||
|
|
||||||
|
gameCommands({
|
||||||
|
{"gameStarted", GameModeCommands_e::GameStarted},
|
||||||
|
{"gameFinished", GameModeCommands_e::GameFinished},
|
||||||
|
{"openCoinSlot", GameModeCommands_e::GameOpenCoinSlot},
|
||||||
|
{"closeCoinSlot", GameModeCommands_e::GameCloseCoinSlot},
|
||||||
|
}),
|
||||||
|
serviceCommands({
|
||||||
|
{"playSound:Promo", ServiceCommands_e::PlaySoundPromo},
|
||||||
|
{"playSound:Ceremony", ServiceCommands_e::PlaySoundCoronation},
|
||||||
|
}),
|
||||||
|
setCommands({
|
||||||
|
{"volume", ServiceCommands_e::SetConfigVolume},
|
||||||
|
{"promoDelay", ServiceCommands_e::SetConfigPromoDelay},
|
||||||
|
}),
|
||||||
|
subTopics{{setTopic, resetTopic}},
|
||||||
|
client("ThroneB",
|
||||||
|
cfg.getBrockerUrl(),
|
||||||
|
subTopics,
|
||||||
|
{cfg.getMqttUser(), cfg.getMqttPassword()}) {
|
||||||
|
fmt::println("MQTT: Starting task:");
|
||||||
|
fmt::println(" Topic: {}", setTopic);
|
||||||
|
|
||||||
|
mqttCall = [this](string topic, string payload) {
|
||||||
|
if (topic == resetTopic ||
|
||||||
|
payload.starts_with("volume") ||
|
||||||
|
payload.starts_with("promoDelay")) {
|
||||||
|
auto mq = std::make_unique<MqttQe>(topic, payload);
|
||||||
|
MasterQueue.enqueue(std::move(mq));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mq = std::make_unique<MqttQe>(topic, payload);
|
||||||
|
MqttQueue.enqueue(std::move(mq));
|
||||||
|
};
|
||||||
|
|
||||||
|
fmt::println("MQTT: service: successfully connected ...");
|
||||||
|
client.registerCallback(mqttCall);
|
||||||
|
|
||||||
|
client.send(verTopic, fmt::format("{}", version));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MqttQe> tryDeque() {
|
||||||
|
auto serMsg = MqttQueue.dequeue();
|
||||||
|
return serMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MqttQe> tryDequeueFor(ITime::Duration timeout) {
|
||||||
|
auto serMsg = MqttQueue.tryDequeueFor(timeout);
|
||||||
|
return serMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushInput() {
|
||||||
|
Thread::sleep(3s);
|
||||||
|
int cnt{};
|
||||||
|
while (true) {
|
||||||
|
if (!MqttQueue.tryDequeueFor(10ms)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
if (!MasterQueue.tryDequeueFor(10ms)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
fmt::println("Input queue flushed, {} Messages discarted", cnt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void runMasterService(Thread::Context& ctx, MqttChannel& instance, function<void()> resetCall) {
|
||||||
|
while (true) {
|
||||||
|
auto serMsg = instance.MasterQueue.dequeue();
|
||||||
|
if (!serMsg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serMsg->Topic == instance.resetTopic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto payload = serMsg->Payload;
|
||||||
|
instance.Logger.sendEvent("Ctrl", payload);
|
||||||
|
if (auto pos = payload.find(":"); pos != std::string::npos) {
|
||||||
|
auto val = payload.substr(pos + 1);
|
||||||
|
if (auto iVal = tryParseInt(val); iVal.has_value()) {
|
||||||
|
auto val = iVal.value();
|
||||||
|
if (payload.starts_with("volume:")) {
|
||||||
|
if (val >= 10 && val <= 100) {
|
||||||
|
instance.IoStat.Volume = val;
|
||||||
|
MediaPlayer::setVolume(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.starts_with("promoDelay:")) {
|
||||||
|
if (val >= 30 && val <= 300) {
|
||||||
|
instance.IoStat.PromoDelay = std::chrono::seconds(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendStatusMessage(OpMode mode) {
|
||||||
|
try {
|
||||||
|
auto& s = IoStat;
|
||||||
|
client.send(statTopic, fmt::format("M:{}, cA:{},cD:{},cE:{},v:{},pD:{}",
|
||||||
|
(int)mode,
|
||||||
|
s.CoinAccepted,
|
||||||
|
s.CoinDenied,
|
||||||
|
s.CoinProtector,
|
||||||
|
s.Volume,
|
||||||
|
s.PromoDelay));
|
||||||
|
} catch (const exception& exc) {
|
||||||
|
fmt::println(stderr, "\nERROR: Unable to connect to Setup bridge: {}", exc.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void sendEvent(std::string event) {
|
||||||
|
try {
|
||||||
|
auto& s = IoStat;
|
||||||
|
client.send(eventTopic, fmt::format("{}", event));
|
||||||
|
} catch (const exception& exc) {
|
||||||
|
fmt::println(stderr, "\nERROR: Unable to connect to Setup bridge: {}", exc.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<string, GameModeCommands_e>& getGameCommands() { return gameCommands; }
|
||||||
|
const std::map<string, ServiceCommands_e>& getSerCommands() { return serviceCommands; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
SwordConfig Config;
|
||||||
|
Queue<MqttQe> MasterQueue;
|
||||||
|
Queue<MqttQe> MqttQueue;
|
||||||
|
DeviceIoState& IoStat;
|
||||||
|
SeqLogger& Logger;
|
||||||
|
const string setTopic;
|
||||||
|
const string verTopic;
|
||||||
|
const string eventTopic;
|
||||||
|
const string statTopic;
|
||||||
|
const string resetTopic;
|
||||||
|
|
||||||
|
const std::map<string, GameModeCommands_e> gameCommands;
|
||||||
|
const std::map<string, ServiceCommands_e> serviceCommands;
|
||||||
|
const std::map<string, ServiceCommands_e> setCommands;
|
||||||
|
vector<string> subTopics;
|
||||||
|
MqttClient client;
|
||||||
|
std::function<void(string, string)> mqttCall;
|
||||||
|
|
||||||
|
static std::optional<int> tryParseInt(std::string val) {
|
||||||
|
try {
|
||||||
|
int iVal = std::stoi(val);
|
||||||
|
return {iVal};
|
||||||
|
} catch (...) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in new issue
Block a user