Initial demo for Promo-Operation

This commit is contained in:
Gustice authored and Jakob Gegeniger committed 2026-09-08 23:23:37 +02:00
1 parent 55b7909717
commit eb1c84cf05
25 files changed
+24615 -2

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
add_subdirectory(sword)
add_subdirectory(throneBridge)
+2
View File
@@ -0,0 +1,2 @@
create_interface()
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
https://github.com/yhirose/cpp-httplib
+80
View File
@@ -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()));
}
}
};
+13
View File
@@ -0,0 +1,13 @@
add_executable(SwordMain swordMain.cpp)
target_link_libraries(SwordMain
PkgConfig::LIBGPIOD
base
palAdapt
loRa
os
Mqtt
fmt::fmt
utils
Ilogging
)
+69
View File
@@ -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;
+207
View File
@@ -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 {};
}
}
};
+222
View File
@@ -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;
}
}
}
+169
View File
@@ -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 {};
}
};
+13
View File
@@ -0,0 +1,13 @@
add_executable(ThroneBridgeMain throneBridgeMain.cpp)
target_link_libraries(ThroneBridgeMain
PkgConfig::LIBGPIOD
base
palAdapt
loRa
os
Mqtt
fmt::fmt
utils
Ilogging
)
+92
View File
@@ -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;
+404
View File
@@ -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;
}
}
}
+321
View File
@@ -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 {};
}
}
};