diff --git a/assets/allow.wav b/assets/allow.wav new file mode 100644 index 0000000..3864b52 Binary files /dev/null and b/assets/allow.wav differ diff --git a/assets/deny.wav b/assets/deny.wav new file mode 100644 index 0000000..100ae6b Binary files /dev/null and b/assets/deny.wav differ diff --git a/assets/promo.wav b/assets/promo.wav new file mode 100644 index 0000000..5d97d87 Binary files /dev/null and b/assets/promo.wav differ diff --git a/doc/commands.md b/doc/commands.md new file mode 100644 index 0000000..624c7e6 --- /dev/null +++ b/doc/commands.md @@ -0,0 +1,6 @@ +# Commands + +```sh +# set audio volume +alsamixer +``` \ No newline at end of file diff --git a/platform b/platform index 92c57ba..0dbf21f 160000 --- a/platform +++ b/platform @@ -1 +1 @@ -Subproject commit 92c57ba83f8ca7914c3ba6b7f15ec46be1993551 +Subproject commit 0dbf21f25cbb4822855e64f7654343c35961fdb6 diff --git a/src/interface.hpp b/src/interface.hpp new file mode 100644 index 0000000..81414f4 --- /dev/null +++ b/src/interface.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include + +static constexpr uint16_t UndefCmd = 0xFFFF; +enum ConMode_e { + ModeMqtt, + ModeLoRa +}; + +enum IncommingCommands_e : uint16_t { + Qurey, // Qurey info + SetMaintanance, // Set Maintanance mode + SetPromoMode, // Enable PromoMode + SetGamingMode, // Enable in Game mode +}; + +enum ServiceCommands_e { + Open, + Close, + Play1, + Play2, + Play3, +}; + +enum GameModeCommands_e { + GameNextTryLocked, // Lock sword on next try + GameNextTryRelease // Release sword on next try +}; + +enum OutgoingCommands_e { + Status, // Send Status + Response, // Confirm last command + Error, // Send error status + PullHappend // Message if User tried to pull the sword +}; + +enum GameModeResponses_e { + SwordPullHappend, // Message if User tried to pull the sword + SwordReleased, // Message that User pulled the sword + SwordReturned // 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 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 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; +}; \ No newline at end of file diff --git a/src/swordMain.cpp b/src/swordMain.cpp index cff7f69..a2cc8f6 100644 --- a/src/swordMain.cpp +++ b/src/swordMain.cpp @@ -1,148 +1,383 @@ #include "swordMain.hpp" +#include "interface.hpp" + using namespace pal; using namespace std::literals::string_view_literals; using namespace std::literals::chrono_literals; +using namespace std; // using namespace std::literals; -constexpr int OutPin = 23; +const string BrockerUrl = "mqtts://nerdyssey.de:8883"; +const string StationPath = "outdoor/king-arthur/sword/"; -enum IncommingCommands_e { - Qurey, // Qurey info - Disable, // Disable Station - Maintanance, // Set Maintanance mode - SetPromoMode, // Enable PromoMode - SetGamingMode, // Enable in Game mode - NextTryLock, // Lock sword on next try - NextTryRelease // Release sword on next try -}; -enum OutgoingCommands_e { - Version, // Send Version - Status, // Send Status - TryHappend, // Message if User tried to pull the sword - SwordReleased, // Message that User pulled the sword - SwordReturned // Message that User returned the sword -}; +constexpr uint8_t VersionArr[4] = {'T', 0, 5, 0}; +const string Version = "T0.5.0"; -/// @brief incomming / outgoing message codes -struct TxMsg { - TxMsg(std::uint32_t m) - : message(m) {}; - uint32_t message; -}; -// struct RsrsCtx { -// std::string name; -// Mutex& mtx; -// decltype(std::chrono::steady_clock::duration()) duration; -// }; +constexpr int SwordEntryPin = 5; +constexpr int SwordEndPin = 0; +constexpr int SwordPulledPin = 25; +constexpr int LinMotPin1 = 13; +constexpr int LinMotPin2 = 12; + +constexpr int GPIO_RST_PIN = 4; +constexpr int GPIO_DIO0_PIN = 27; +constexpr ConMode_e ConMode{ConMode_e::ModeMqtt}; + +const string soundPromo = "/home/gustice/workspace/kingArth/swordStation/assets/promo.wav"; +const string soundDeny = "/home/gustice/workspace/kingArth/swordStation/assets/deny.wav"; +const string soundAllow = "/home/gustice/workspace/kingArth/swordStation/assets/allow.wav"; + +void playWavFile(const string& filePath) { + if (filePath.empty()) { + fmt::println(stderr, "ERROR: Empty file path"); + return; + } + thread audioThread([filePath]() { + string cmd = "aplay \"" + filePath + "\" 2>/dev/null"; + int result = system(cmd.c_str()); + if (result != 0) { + fmt::println(stderr, "ERROR: Failed to play audio file: {}", filePath); + } + }); + audioThread.detach(); +} + +enum class OpMode { + Undefined, // Enable in Game mode + Maintanance = 1, // Set Maintanance mode + PromoMode, // Enable PromoMode + GamingMode, // Enable in Game mode +}; +OpMode Mode = OpMode::Maintanance; + +unique_ptr IoStat; /// @brief MQTT-Service provider -// void LoRaServiceTask(Thread::Context& ctx, Queue& queue) { -void MQttServiceTask(Thread::Context& ctx) { - printf("MQTT service: Starting\n"); - // uint32_t cnt{}; - while (!ctx.isCancelled()) { - Thread::sleep(2s); - // blinkSem.give(); - // auto e = std::make_unique(cnt++, "tick"); - // queue.enqueue(std::move(e)); - std::cout << "MQTT service: Running ...\n"; +void MQttServiceTask(Thread::Context& ctx, Queue& rxQueue, Queue& txQueue) { + string runTopic = StationPath + "run"; + string intTopic = StationPath + "int"; + string statTopic = StationPath + "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 commands = { + {"Qurey", IncommingCommands_e::Qurey}, + {"SetMaintanance", IncommingCommands_e::SetMaintanance}, + {"SetPromoMode", IncommingCommands_e::SetPromoMode}, + {"SetGamingMode", IncommingCommands_e::SetGamingMode}, + }; + static std::map serCommands = { + {"ServiceOpen", ServiceCommands_e::Open}, + {"ServiceClose", ServiceCommands_e::Close}, + {"ServicePlay1", ServiceCommands_e::Play1}, + {"ServicePlay2", ServiceCommands_e::Play2}, + {"ServicePlay3", ServiceCommands_e::Play3}, + }; + static std::map 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(commands.at(payload)); + return; + } + if (serCommands.contains(payload)) { + auto code = serCommands.at(payload); + rxQueue.emplace(code); + return; + } + if (gameCommands.contains(payload)) { + rxQueue.emplace(serCommands.at(payload)); + return; + } + + fmt::print("MQTT: unknown command {}:{}'\n", topic, payload); + }; + + try { + std::vector topicsToRegister{runTopic}; + MqttClient client("Sword", BrockerUrl, topicsToRegister); + 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& queue) { -void LoRaServiceTask(Thread::Context& ctx) { - printf("LoRa service: Starting\n"); - // uint32_t cnt{}; +void LoRaServiceTask(Thread::Context& ctx, Queue& rxQueue, Queue& txQueue) { + fmt::println("LoRa: Starting task:"); + + auto loraCallback = [&](std::vector 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(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()) { - Thread::sleep(2s); - // blinkSem.give(); - // auto e = std::make_unique(cnt++, "tick"); - // queue.enqueue(std::move(e)); - std::cout << "LoRa service: Running ...\n"; + auto tx = txQueue.dequeue(); + loRa.send({0}); + // printf ("Message: %s %lu\n", msg->message.c_str(), msg->cnt); } - // while (!ctx.isCancelled()) { - // auto msg = queue.dequeue(); // Wait with infinit timeout, guaranteed to succeed - // printf("Message: %s %lu\n", msg->message.c_str(), msg->cnt); - // } - // while (!ctx.isCancelled()) { - // blinkSem.take(); - // led.write(true); // execute blink event - // Thread::sleep(100ms); - // led.write(false); // execute blink event - // printf("Blink tick\n"); - // } -} - -// void RessourceAccessor(Thread::Context& tCtx, RsrsCtx& ctx) { -// using namespace std::chrono; -// fmt::println("starting Ressource Accessor {}", ctx.name); -// while (!tCtx.isCancelled()) { -// { -// StopWatch sw; -// ctx.mtx.claim(); -// auto e = duration_cast(sw.getEleapsed()); -// auto d = duration_cast(ctx.duration); -// fmt::println("took {}ms to quire, now access ressource for {}ms", -// (int)e.count(), (int)d.count()); -// Thread::sleep(ctx.duration); -// ctx.mtx.release(); -// } -// Thread::sleep(100ms); -// } -// } - -void itrHandler() { - TriggeredInput itr(26, "PItr"); - while (true) { - std::cout << "Await next interrupt\n"; - if (itr.await()) { - std::cout << "Interrupt Event\n"; - } + 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[]) { - printf("Sword Firmware\n"); - - OutputPort out(OutPin, "POut"); - InputPort in(20, "PIn"); - std::cout << "setting GPIO21 as Out and GPIO20 as input: "; + fmt::println("Sword Firmware"); + auto itrHandler = [&]() { + TriggeredInput itr(26, "PItr"); + while (true) { + std::cout << "Await next interrupt\n"; + if (itr.await()) { + std::cout << "Interrupt Event\n"; + } + } + }; std::thread interruptThread(itrHandler); - // Semaphore blinkSem; - auto mqttTask = Thread::createExplicit("MqttTask", MQttServiceTask); - auto loraTask = Thread::createExplicit("LoRaTask", LoRaServiceTask); - // static Queue txQueue; - // auto mqttTask = Thread::createExplicit("MqttTask", MQttServiceTask, std::ref(txQueue)); - // auto loraTask = Thread::createExplicit("LoRaTask", LoRaServiceTask, std::ref(txQueue)); + InputPort spPort(SwordPulledPin, "PInSPull"); + InputPort sbPort(SwordEntryPin, "PinSEntry"); + InputPort sePort(SwordEndPin, "PInSEnd"); + OutputPort mOut1(LinMotPin1, "M1:1"); + OutputPort mOut2(LinMotPin2, "M1:2"); + InputTransAware SwordEntry(sbPort); + InputTransAware SwordEnd(sePort); + InputTransAware SwordPulled(spPort); + LinMot SwordLatch(mOut1, mOut2); - // Mutex rsrMtx; - // RsrsCtx rsCtx1{ - // .name = "intense", - // .mtx = rsrMtx, - // .duration = 3000ms}; - // RsrsCtx rsCtx2{ - // .name = "light", - // .mtx = rsrMtx, - // .duration = 1000ms}; - // auto ressourceAccess1 = Thread::createExplicit("IntenseAccess", RessourceAccessor, std::ref(rsCtx1)); - // auto ressourceAccess2 = Thread::createExplicit("LightAccess", RessourceAccessor, std::ref(rsCtx2)); + IoStat = make_unique(SwordEntry, SwordEnd, SwordPulled, SwordLatch); - fmt::print("Running App\n"); + auto tick = [&]() { + SwordEntry.sample(); + SwordEnd.sample(); + SwordPulled.sample(); + }; + static Queue rxQueue; + static Queue txQueue; + static auto conTask = (ConMode == ModeMqtt) ? Thread::createExplicit("MqttTask", MQttServiceTask, std::ref(rxQueue), std::ref(txQueue)) + : Thread::createExplicit("LoRaTask", LoRaServiceTask, std::ref(rxQueue), std::ref(txQueue)); + + SwordLatch.moveOut(); + + const static map opCodes = { + {SetMaintanance, OpMode::Maintanance}, + {SetPromoMode, OpMode::PromoMode}, + {SetGamingMode, OpMode::GamingMode}, + }; + + fmt::println("Running App"); while (true) { - std::cout << "in=" << in.read() << "\n"; - out.write(0); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - std::cout << "in=" << in.read() << "\n"; - out.write(1); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - // std::this_thread::sleep_for(1s); + auto msg = rxQueue.dequeue(); + fmt::println("MAIN: Got message {}", msg->code); + + auto code = static_cast(msg->code); + if (opCodes.contains(code)) { + Mode == opCodes.at(code); + } else { + Mode = OpMode::Undefined; + } + + switch (Mode) { + case OpMode::Maintanance: { + fmt::println("Got Maintanance"); + auto e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + // txQueue.emplace(msg->code, 0); + + while (true) { + auto serMsg = rxQueue.tryDequeueFor(1000ms); + if (serMsg) { + auto code = static_cast(serMsg->code); + switch (code) { + case ServiceCommands_e::Open: { + IoStat->SwordLatch.moveIn(); + } break; + case ServiceCommands_e::Close: { + IoStat->SwordLatch.moveOut(); + } break; + case ServiceCommands_e::Play1: { + playWavFile(soundPromo); + } break; + case ServiceCommands_e::Play2: { + playWavFile(soundDeny); + } break; + case ServiceCommands_e::Play3: { + playWavFile(soundAllow); + } break; + } + } + + auto e = std::make_unique( + (uint8_t)Mode, + IoStat->SwordEnd.get(), + IoStat->SwordEntry.get(), + IoStat->SwordPulled.get(), + IoStat->SwordLatch.isLocked()); + txQueue.enqueue(std::move(e)); + } + e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + } break; + case OpMode::PromoMode: { + fmt::println("Got SetPromoMode"); + auto e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + + while (true) { + SwordPulled.sample(); + auto rx = txQueue.tryDequeueFor(10ms); + if (SwordPulled.isPosEdge()) { + // say: Play our tours + Thread::sleep(5s); + } + } + } break; + case OpMode::GamingMode: { + fmt::println("Got SetGamingMode"); + auto e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + + while (true) { + auto gameMsg = txQueue.dequeue(); + auto code = static_cast(msg->code); + switch (code) { + case GameModeCommands_e::GameNextTryLocked: { + auto e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + + fmt::println("Got NextTryLock"); + while (true) { + SwordPulled.sample(); + auto rx = txQueue.tryDequeueFor(20ms); + bool pulled = SwordPulled.read(); + if (SwordPulled.isPosEdge()) { + // say: You are not the one + Thread::sleep(5s); + } + } + } break; + case GameModeCommands_e::GameNextTryRelease: { + auto e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + SwordLatch.moveIn(); + + fmt::println("Got NextTryRelease"); + while (true) { + SwordEnd.sample(); + auto rx = txQueue.tryDequeueFor(20ms); + if (SwordEnd.isNegEdge()) { + break; + } + } + while (true) { + SwordEntry.sample(); + auto rx = txQueue.tryDequeueFor(20ms); + if (SwordEntry.isNegEdge()) { + break; + } + } + + // say: The prophecy is fulfilled + e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + + while (true) { + SwordEnd.sample(); + auto rx = txQueue.tryDequeueFor(20ms); + if (SwordEnd.isNegEdge()) { + break; + } + } + while (true) { + SwordEntry.sample(); + auto rx = txQueue.tryDequeueFor(20ms); + if (SwordEntry.isNegEdge()) { + break; + } + } + + e = std::make_unique(msg->code, 0); + txQueue.enqueue(std::move(e)); + + } break; + } + } + + } break; + + default: + break; + } } interruptThread.join(); return EXIT_SUCCESS; diff --git a/src/swordMain.hpp b/src/swordMain.hpp index 601d630..ee40c3f 100644 --- a/src/swordMain.hpp +++ b/src/swordMain.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -27,3 +28,57 @@ #include "spiPort.hpp" #include "textUtils.hpp" #include "thread.hpp" + +#include "palHw.hpp" + +class LinMot { + public: + LinMot(OutputPort& o1, OutputPort& o2) + : _out1(o1), + _out2(o2) { + } + + void moveOut() { + _out2.write(false); + _out1.write(true); + _locked = true; + } + void moveIn() { + _out2.write(true); + _out1.write(false); + _locked = false; + } + + bool isLocked() { + return _locked; + } + + private: + OutputPort& _out1; + OutputPort& _out2; + bool _locked; +}; + +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; +}; \ No newline at end of file