From 6f0dea895a693c2f039e4d66d99adb5420c3e6d4 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:25:13 +0100 Subject: [PATCH 01/25] Added macro atlas screen for OpenShock --- generate_macro_atlas.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/generate_macro_atlas.py b/generate_macro_atlas.py index c64af7b..1236e35 100644 --- a/generate_macro_atlas.py +++ b/generate_macro_atlas.py @@ -107,7 +107,7 @@ ] TILE_LABELS_P2 = [ - ["DBG", "TWTCH", "INTRP", "-----", "DM"], + ["DBG", "TWTCH", "INTRP", "SHOCK", "DM"], ["-----", "-----", "-----", "-----", "-----"], ["-----", "-----", "-----", "-----", "-----"], ] @@ -806,6 +806,38 @@ def layout_dm_compose(buf): buf.put_status_bar() +def layout_openshock(buf): + """OpenShock control screen: frame + labels + Execute button.""" + buf.put_frame("OPENSHOCK") + + # Row 1 (ty=0): Shocker Selection + buf.put_text(3, 1, " < ", inverted=True) # tx=0 + buf.put_text(34, 1, " > ", inverted=True) # tx=4 + + # Row 2: Separator + buf.put_hline(2) + + # Row 3: Status + buf.put_text(4, 3, "ACTIVE MODE:") + + # Row 4 (ty=1): Intensity & Duration on same line + # tx=0: Int -, tx=2: Int +, tx=3: Dur -, tx=4: Dur + + buf.put_text(2, 4, " - ", inverted=True) # tx=0 + buf.put_text(16, 4, " + ", inverted=True) # tx=2 + buf.put_text(24, 4, " - ", inverted=True) # tx=3 + buf.put_text(34, 4, " + ", inverted=True) # tx=4 + + # Row 5 : Separator + buf.put_hline(5) + + # Row 6 (ty=2): Mode & Execute + # tx=0-1: MODE button (11 chars), tx=3-4: EXECUTE button (11 chars) + buf.put_text(2, 6, " [ MODE ] ", inverted=True) # tx=0-1 + buf.put_text(25, 6, " [ EXECUTE ] ", inverted=True) # tx=3-4 + + buf.put_status_bar() + + def layout_dm_pair(buf): """DM Pair CHOOSE mode: frame + DIAL/SCAN buttons + hint text.""" buf.put_frame("DM PAIR") @@ -909,6 +941,7 @@ def layout_dm_pair_joined(buf): 45: ("PAIR OK", layout_dm_pair_complete), 46: ("PAIR FAIL", layout_dm_pair_failed), 47: ("PAIR JOIN", layout_dm_pair_joined), + 48: ("OPENSHOCK", layout_openshock), } From a9a7570b0426b5b02eaedcf544af12f55f6fad83 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:25:36 +0100 Subject: [PATCH 02/25] Regenerated macro atlas --- WilliamsTube_MacroAtlas.png | Bin 73266 -> 75949 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/WilliamsTube_MacroAtlas.png b/WilliamsTube_MacroAtlas.png index 8d7b1999898ad8d3f395cd2f74abb724822a5327..9923ec8f49c7c8b9fe22fda8f38896d66eadc2a5 100644 GIT binary patch literal 75949 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

!8=(Gghm%VnP-U>4sIjFTi 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 Ret-Eq`}cV7e&7*&;qO Date: Wed, 8 Apr 2026 16:34:35 +0100 Subject: [PATCH 03/25] Add OpenShock API client and screen --- yip_os/src/net/OpenShockClient.cpp | 223 +++++++++++++++++++++++++ yip_os/src/net/OpenShockClient.hpp | 56 +++++++ yip_os/src/screens/OpenShockScreen.cpp | 217 ++++++++++++++++++++++++ yip_os/src/screens/OpenShockScreen.hpp | 42 +++++ 4 files changed, 538 insertions(+) create mode 100644 yip_os/src/net/OpenShockClient.cpp create mode 100644 yip_os/src/net/OpenShockClient.hpp create mode 100644 yip_os/src/screens/OpenShockScreen.cpp create mode 100644 yip_os/src/screens/OpenShockScreen.hpp diff --git a/yip_os/src/net/OpenShockClient.cpp b/yip_os/src/net/OpenShockClient.cpp new file mode 100644 index 0000000..5783aee --- /dev/null +++ b/yip_os/src/net/OpenShockClient.cpp @@ -0,0 +1,223 @@ +/** + * OpenShockClient.cpp + * V1.0.0 + * + * Adds OpenShock API integration to YipOS for remote control of OpenShock + * devices. + * + * By otter_oasis + */ + +#include "OpenShockClient.hpp" +#include "core/Logger.hpp" +#include +#include + +namespace YipOS { + +using json = nlohmann::json; + +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, + void *userp) { + auto *str = static_cast(userp); + str->append(static_cast(contents), size * nmemb); + return size * nmemb; +} + +OpenShockClient::OpenShockClient() { + curl_global_init(CURL_GLOBAL_ALL); + curl_ = curl_easy_init(); +} + +OpenShockClient::~OpenShockClient() { + if (curl_) + curl_easy_cleanup(curl_); + curl_global_cleanup(); +} + +bool OpenShockClient::FetchShockers() { + if (!HasToken()) { + shockers_.clear(); + return true; + } + + shockers_.clear(); + std::string response; + + // Fetch owned shockers + if (PerformGet(std::string(API_BASE) + "/1/shockers/own", response)) { + ParseShockers(response, true); + } + + // Fetch shared shockers + response.clear(); + if (PerformGet(std::string(API_BASE) + "/1/shockers/shared", response)) { + ParseShockers(response, false); + } + + Logger::Debug("OpenShockClient: Fetched " + std::to_string(shockers_.size()) + + " shockers"); + return true; +} + +bool OpenShockClient::ParseShockers(const std::string &json_str, + bool is_owned) { + try { + auto j = json::parse(json_str); + if (!j.contains("data") || !j["data"].is_array()) + return false; + + for (auto &hub : j["data"]) { + std::string hub_name = + hub.contains("name") ? hub["name"].get() : "Hub"; + if (hub.contains("shockers") && hub["shockers"].is_array()) { + for (auto &item : hub["shockers"]) { + Shocker s; + s.id = item["id"].get(); + std::string shocker_name = item.contains("name") + ? item["name"].get() + : "Shocker"; + s.name = "[" + hub_name + "] " + shocker_name; + s.is_owned = is_owned; + shockers_.push_back(s); + } + } + } + return true; + } catch (const std::exception &e) { + Logger::Warning("OpenShockClient: JSON parse error: " + + std::string(e.what())); + return false; + } +} + +bool OpenShockClient::SendControl(const std::string &shocker_id, + const std::string &type, float intensity, + int duration_ms) { + if (!HasToken()) { + return false; + } + + // Map string type to OpenShock.Common.Models.ControlType for API + // 0=Stop, 1=Shock, 2=Vibrate, 3=Sound + int type_int = 2; // Default to Vibe for safety + if (type.find("SHOCK") != std::string::npos) + type_int = 1; + else if (type.find("VIBE") != std::string::npos) + type_int = 2; + else if (type.find("SOUND") != std::string::npos) + type_int = 3; + + json payload = { + {"shocks", json::array({{{"id", shocker_id}, + {"type", type_int}, + {"intensity", static_cast(intensity)}, + {"duration", duration_ms}, + {"exclusive", true}}})}}; + + Logger::Info("OpenShock: Sending " + type + " (" + + std::to_string(static_cast(intensity)) + "%, " + + std::to_string(duration_ms) + "ms) to " + shocker_id); + + return PerformPost(std::string(API_BASE) + "/2/shockers/control", + payload.dump()); +} + +void OpenShockClient::SetToken(const std::string &token) { + if (token_ != token) { + token_ = token; + token_valid_ = false; // Reset validity when token changes + } +} + +bool OpenShockClient::PerformGet(const std::string &url, + std::string &response) { + if (!curl_) + return false; + + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl_, CURLOPT_USERAGENT, USER_AGENT); + + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, ("OpenShockToken: " + token_).c_str()); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl_); + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + Logger::Warning("OpenShockClient: GET failed: " + + std::string(curl_easy_strerror(res))); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code >= 200 && http_code < 300) { + token_valid_ = true; + } else { + if (http_code == 401) + token_valid_ = false; + Logger::Warning("OpenShockClient: GET " + url + " failed (HTTP " + + std::to_string(http_code) + ")"); + if (!response.empty()) { + Logger::Warning("OpenShockClient: Response: " + response); + } + } + + return http_code == 200; +} + +bool OpenShockClient::PerformPost(const std::string &url, + const std::string &payload) { + if (!curl_) + return false; + + std::string response; + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, payload.c_str()); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl_, CURLOPT_USERAGENT, USER_AGENT); + + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, ("OpenShockToken: " + token_).c_str()); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl_); + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + Logger::Warning("OpenShockClient: POST failed: " + + std::string(curl_easy_strerror(res))); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code >= 200 && http_code < 300) { + token_valid_ = true; + } else { + if (http_code == 401) + token_valid_ = false; + Logger::Warning("OpenShockClient: POST " + url + " failed (HTTP " + + std::to_string(http_code) + ")"); + if (!response.empty()) { + Logger::Warning("OpenShockClient: Response: " + response); + } + return false; + } + + return true; +} + +} // namespace YipOS diff --git a/yip_os/src/net/OpenShockClient.hpp b/yip_os/src/net/OpenShockClient.hpp new file mode 100644 index 0000000..c07a178 --- /dev/null +++ b/yip_os/src/net/OpenShockClient.hpp @@ -0,0 +1,56 @@ +/** + * OpenShockClient.cpp + * V1.0.0 + * + * Adds OpenShock API integration to YipOS for remote control of OpenShock + * devices By otter_oasis + */ + +#pragma once + +#include +#include + +typedef void CURL; + +namespace YipOS { + +struct Shocker { + std::string id; + std::string name; + bool is_owned = false; +}; + +class OpenShockClient { +public: + OpenShockClient(); + ~OpenShockClient(); + + void SetToken(const std::string &token); + bool HasToken() const { return !token_.empty(); } + bool IsTokenValid() const { return token_valid_; } + + // Fetch list of shockers (owned and shared) + bool FetchShockers(); + const std::vector &GetShockers() const { return shockers_; } + + // Control shockers + // type: "Shock", "Vibrate", "Sound", "Stop" + bool SendControl(const std::string &shocker_id, const std::string &type, + float intensity, int duration_ms); + +private: + bool ParseShockers(const std::string &json, bool is_owned); + bool PerformPost(const std::string &url, const std::string &payload); + bool PerformGet(const std::string &url, std::string &response); + + CURL *curl_ = nullptr; + std::string token_; + bool token_valid_ = false; + std::vector shockers_; + + static constexpr const char *API_BASE = "https://api.openshock.app"; + static constexpr const char *USER_AGENT = "YipOS/1.0"; +}; + +} // namespace YipOS diff --git a/yip_os/src/screens/OpenShockScreen.cpp b/yip_os/src/screens/OpenShockScreen.cpp new file mode 100644 index 0000000..d402a02 --- /dev/null +++ b/yip_os/src/screens/OpenShockScreen.cpp @@ -0,0 +1,217 @@ +/** + * OpenShockClient.cpp + * V1.0.0 + * + * Adds OpenShock API integration to YipOS for remote control of OpenShock + * devices. + * + * By otter_oasis + */ + +#include "OpenShockScreen.hpp" +#include "app/PDAController.hpp" +#include "app/PDADisplay.hpp" +#include "core/Config.hpp" +#include "core/Glyphs.hpp" +#include "core/Logger.hpp" +#include "core/TimeUtil.hpp" +#include "net/OpenShockClient.hpp" +#include +#include + +namespace YipOS { + +using namespace Glyphs; + +OpenShockScreen::OpenShockScreen(PDAController &pda) : Screen(pda) { + name = "SHOCK"; + macro_index = 48; // OPENSHOCK macro +} + +void OpenShockScreen::Render() { + // The OPENSHOCK title and layout is baked into the macro texture + RenderContent(); + RenderStatusBar(); +} + +void OpenShockScreen::RenderContent() { + RenderShockerSelection(); + RenderModeSelection(); + + // Draw feedback if flashing, otherwise restore the default label area + if (show_success_flash_) { + display_.WriteText(25, 6, " [ SENT! ] ", true); + } else { + display_.WriteText(25, 6, " [ EXECUTE ] ", true); + } + + // Intensity Value + char buf[16]; + std::snprintf(buf, sizeof(buf), "%3.0f%%", intensity_); + display_.WriteText(8, 4, buf); + + // Duration Value + std::snprintf(buf, sizeof(buf), "%1.1fs", duration_ms_ / 1000.0f); + display_.WriteText(28, 4, buf, false); +} + +void OpenShockScreen::RenderDynamic() { + if (show_success_flash_ && MonotonicNow() > flash_end_time_) { + show_success_flash_ = false; + } + + RenderContent(); + RenderClock(); + RenderCursor(); +} + +void OpenShockScreen::RenderShockerSelection() { + auto *client = pda_.GetOpenShockClient(); + if (!client) + return; + + if (!client->HasToken()) { + display_.WriteText(8, 1, "SETUP IN APP"); + return; + } + + const auto &items = client->GetShockers(); + std::string label = "NO DEVICES OR BAD TOKEN"; + if (!items.empty()) { + if (selected_shocker_idx_ >= static_cast(items.size())) { + selected_shocker_idx_ = 0; + } + label = items[selected_shocker_idx_].name; + } + + display_.WriteGlyph(1, 1, G_TRACKER); + + // Name centered between arrows (cols 8 to 31), padded to 24 chars to clear + // old text + std::string padded = label.substr(0, 24); + if (padded.length() < 24) + padded.append(24 - padded.length(), ' '); + display_.WriteText(8, 1, padded); +} + +void OpenShockScreen::RenderModeSelection() { + auto *client = pda_.GetOpenShockClient(); + + // Status indicator at Column 0 (to the left of ACTIVE MODE label) on Row 3 + std::string status = "- "; + if (client) { + if (client->HasToken()) { + status = client->IsTokenValid() ? " " : "! "; + } + } + display_.WriteText(2, 1, status); + + std::string val = + (!client || !client->HasToken() || client->GetShockers().empty()) + ? "UNAVAILABLE" + : MODES[mode_idx_]; + + // Add hazard markers if in SHOCK mode + if (mode_idx_ == 0 && client && client->HasToken()) { + val = "!!" + std::string(MODES[0]) + "!!"; + } + + // Pad with spaces to clear old text (e.g. switching from !!SHOCK!! to VIBE) + if (val.length() < 11) { + val += std::string(11 - val.length(), ' '); + } + + // Positioned at Col 17 to sit directly after the new macro label position + // with a space on Row 3 + display_.WriteText(17, 3, val); +} + +bool OpenShockScreen::OnInput(const std::string &key) { + if (key == "TL") { + pda_.PopScreen(); + return true; + } + + if (key.size() == 2 && key[0] >= '1' && key[0] <= '5' && key[1] >= '1' && + key[1] <= '3') { + int tx = key[0] - '1'; + int ty = key[1] - '1'; + + auto *client = pda_.GetOpenShockClient(); + auto &config = pda_.GetConfig(); + bool changed = false; + + if (ty == 0) { // Top Row: Devices (Unchanged) + if (tx == 0) { // Previous + if (selected_shocker_idx_ > 0) { + selected_shocker_idx_--; + changed = true; + } + } else if (tx == 4) { // Next + if (client && selected_shocker_idx_ < + static_cast(client->GetShockers().size()) - 1) { + selected_shocker_idx_++; + changed = true; + } + } + } else if (ty == 1) { // Middle Row: Intensity (Left) & Duration (Right) + float i_step = 2.5f; + int d_step = 1000; + try { + i_step = std::stof(config.GetState("openshock.intensity_step", "2.5")); + } catch (...) { + } + try { + d_step = std::stoi(config.GetState("openshock.duration_step", "1000")); + } catch (...) { + } + + if (tx == 0) { // Int Down + intensity_ = std::max(0.0f, intensity_ - i_step); + changed = true; + } else if (tx == 2) { // Int Up + intensity_ = std::min(100.0f, intensity_ + i_step); + changed = true; + } else if (tx == 3) { // Dur Down + duration_ms_ = std::max(100, duration_ms_ - d_step); + changed = true; + } else if (tx == 4) { // Dur Up + duration_ms_ = std::min(30000, duration_ms_ + d_step); + changed = true; + } + } else if (ty == 2) { // Bottom Row: Mode Cycle (Left) & Execute (Right) + if (tx <= 1) { // Mode (tx 0-1) + mode_idx_ = (mode_idx_ + 1) % 3; + changed = true; + } else if (tx >= 3) { // EXECUTE (tx 3-4) + if (client && !client->GetShockers().empty()) { + const auto &s = client->GetShockers()[selected_shocker_idx_]; + client->SendControl(s.id, MODES[mode_idx_], intensity_, duration_ms_); + show_success_flash_ = true; + flash_end_time_ = MonotonicNow() + 2.0; + changed = true; + } + } + } + + if (changed) { + display_.BeginBuffered(); + RenderContent(); + } + return true; + } + + return false; +} + +void OpenShockScreen::Update() { + static double last_fetch = 0; + if (MonotonicNow() - last_fetch > 60.0) { + if (auto *client = pda_.GetOpenShockClient()) { + client->FetchShockers(); + } + last_fetch = MonotonicNow(); + } +} + +} // namespace YipOS diff --git a/yip_os/src/screens/OpenShockScreen.hpp b/yip_os/src/screens/OpenShockScreen.hpp new file mode 100644 index 0000000..a72a971 --- /dev/null +++ b/yip_os/src/screens/OpenShockScreen.hpp @@ -0,0 +1,42 @@ +/** + * OpenShockClient.cpp + * V1.0.0 + * + * Adds OpenShock API integration to YipOS for remote control of OpenShock + * devices. + * + * By otter_oasis + */ + +#pragma once + +#include "Screen.hpp" + +namespace YipOS { + +class OpenShockScreen : public Screen { +public: + OpenShockScreen(PDAController &pda); + + void Render() override; + void RenderContent() override; + void RenderDynamic() override; + bool OnInput(const std::string &key) override; + void Update() override; + +private: + void RenderModeSelection(); + void RenderShockerSelection(); + + int selected_shocker_idx_ = 0; + int mode_idx_ = 1; // 0=Shock, 1=Vibrate, 2=Sound + float intensity_ = 25.0f; + int duration_ms_ = 1000; + + bool show_success_flash_ = false; + double flash_end_time_ = 0; + + static constexpr const char *MODES[] = {"SHOCK", "VIBE ", "SOUND"}; +}; + +} // namespace YipOS From 14981668b39d7cb0402ac1cda1d45ee8d8bbf1ab Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:35:40 +0100 Subject: [PATCH 04/25] Register OpenShock screen to yip-boi menu --- yip_os/src/screens/Screen.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yip_os/src/screens/Screen.cpp b/yip_os/src/screens/Screen.cpp index f1265f3..3b195cf 100644 --- a/yip_os/src/screens/Screen.cpp +++ b/yip_os/src/screens/Screen.cpp @@ -39,6 +39,7 @@ #include "DMPairScreen.hpp" #include "DMComposeScreen.hpp" #include "DMMessageScreen.hpp" +#include "OpenShockScreen.hpp" #include "app/PDAController.hpp" #include "app/PDADisplay.hpp" #include "core/Glyphs.hpp" @@ -181,6 +182,7 @@ std::unique_ptr CreateScreen(const std::string& name, PDAController& pda {"DM_PAIR", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, {"DM_COMPOSE", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, {"DM_MSG", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, + {"SHOCK", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, }; auto it = registry.find(name); From 5206aff2e0d8ebf111d8f48237ffdd3b32563ef5 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:36:19 +0100 Subject: [PATCH 05/25] Register and initialize OpenShock client --- yip_os/src/app/PDAController.cpp | 9 +++++++++ yip_os/src/app/PDAController.hpp | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/yip_os/src/app/PDAController.cpp b/yip_os/src/app/PDAController.cpp index 2c12d9e..87654fa 100644 --- a/yip_os/src/app/PDAController.cpp +++ b/yip_os/src/app/PDAController.cpp @@ -12,6 +12,7 @@ #include "net/TwitchClient.hpp" #include "media/MediaController.hpp" #include "platform/SystemStats.hpp" +#include "net/OpenShockClient.hpp" #include "core/Glyphs.hpp" #include "core/Config.hpp" #include "core/Logger.hpp" @@ -97,6 +98,14 @@ PDAController::PDAController(PDADisplay& display, NetTracker& net_tracker, Confi } } + // Initialize OpenShock client + openshock_client_ = std::make_unique(); + std::string os_token = config_.GetState("openshock.token"); + if (!os_token.empty()) { + openshock_client_->SetToken(os_token); + } + openshock_client_->FetchShockers(); + // Push home screen as root auto home = std::make_unique(*this); screen_stack_.push_back(std::move(home)); diff --git a/yip_os/src/app/PDAController.hpp b/yip_os/src/app/PDAController.hpp index d1eb1dc..92b739d 100644 --- a/yip_os/src/app/PDAController.hpp +++ b/yip_os/src/app/PDAController.hpp @@ -34,6 +34,7 @@ class StockClient; class TwitchClient; struct TwitchMessage; class TranslationWorker; +class OpenShockClient; class PDAController { public: @@ -141,6 +142,9 @@ class PDAController { void RefreshChatCache(); void MarkChatSeen(); + // OpenShock integration + OpenShockClient* GetOpenShockClient() { return openshock_client_.get(); } + // Hard lock (full LOCK screen from home tile) void SetLocked(bool locked); bool IsLocked() const { return locked_; } @@ -231,6 +235,7 @@ class PDAController { std::unique_ptr stock_client_; std::unique_ptr twitch_client_; const TwitchMessage* selected_twitch_ = nullptr; + std::unique_ptr openshock_client_; std::string assets_path_; std::unique_ptr dm_notify_sound_; bool prev_has_unseen_dm_ = false; From 873dbe16da3de6975bb0c8caf2335c4917bce752 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:36:39 +0100 Subject: [PATCH 06/25] Add SHOCK glyph --- yip_os/src/core/Glyphs.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yip_os/src/core/Glyphs.hpp b/yip_os/src/core/Glyphs.hpp index 3b15d09..8c0396f 100644 --- a/yip_os/src/core/Glyphs.hpp +++ b/yip_os/src/core/Glyphs.hpp @@ -32,7 +32,7 @@ constexpr TileLabel TILE_LABELS[HOME_PAGES][TILE_ROWS][TILE_COLS] = { {{"VRCX"}, {"HEART"}, {"BFI"}, {"STONK"}, {"CHAT"}}, {{"CC"}, {"AVTR"}, {"TEXT"}, {"MEDIA"}, {"LOCK"}}}, // Page 1 - {{{"DBG"}, {"TWTCH"}, {"INTRP"}, {"-----"}, {"DM"}}, + {{{"DBG"}, {"TWTCH"}, {"INTRP"}, {"SHOCK"}, {"DM"}}, {{"-----"}, {"-----"}, {"-----"}, {"-----"}, {"-----"}}, {{"-----"}, {"-----"}, {"-----"}, {"-----"}, {"-----"}}}, }; From 093148e692357923e5c45c52cc2b741b49cebfc0 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:38:08 +0100 Subject: [PATCH 07/25] Add YipOS OpenShock settings tab --- yip_os/src/ui/UIManager_OpenShock.cpp | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 yip_os/src/ui/UIManager_OpenShock.cpp diff --git a/yip_os/src/ui/UIManager_OpenShock.cpp b/yip_os/src/ui/UIManager_OpenShock.cpp new file mode 100644 index 0000000..14aeaa5 --- /dev/null +++ b/yip_os/src/ui/UIManager_OpenShock.cpp @@ -0,0 +1,141 @@ +/** + * OpenShockClient.cpp + * V1.0.0 + * + * Adds OpenShock API integration to YipOS for remote control of OpenShock + * devices. + * + * By otter_oasis + */ + +#include "UIManager.hpp" +#include "app/PDAController.hpp" +#include "core/Config.hpp" +#include "core/Logger.hpp" +#include "net/OpenShockClient.hpp" + +#include +#include +#include + +namespace YipOS { + +void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { + ImGui::Text("OpenShock Integration"); + ImGui::TextDisabled( + "Drive your OpenShock devices directly from the Yip-Boi."); + + ImGui::Separator(); + + auto *client = pda.GetOpenShockClient(); + if (!client) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), + "OpenShock client not initialized."); + return; + } + + // Initialize buffer from config state + if (!openshock_token_initialized_) { + std::string token = config.GetState("openshock.token"); + std::snprintf(openshock_token_buf_.data(), openshock_token_buf_.size(), + "%s", token.c_str()); + + std::string i_step = config.GetState("openshock.intensity_step", "2.5"); + try { + openshock_intensity_step_ = std::stof(i_step); + } catch (...) { + } + + std::string d_step = config.GetState("openshock.duration_step", "1000"); + try { + openshock_duration_step_ = std::stoi(d_step); + } catch (...) { + } + + openshock_token_initialized_ = true; + } + + // --- Status --- + if (!client->HasToken()) { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), + "Status: Offline (no token)"); + } else if (client->IsTokenValid()) { + ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Status: Connected"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), + "Status: Authentication Failed"); + } + + ImGui::Spacing(); + + // --- Token --- + ImGui::Text("API Token"); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##os_token", openshock_token_buf_.data(), + openshock_token_buf_.size(), ImGuiInputTextFlags_Password); + ImGui::TextDisabled( + "Generate a token at https://next.openshock.app/settings/api-tokens"); + + ImGui::Spacing(); + + // --- Increments --- + ImGui::Text("Control Increments (Yip-Boi Screen)"); + ImGui::SetNextItemWidth(150); + ImGui::InputFloat("Intensity Step (%)", &openshock_intensity_step_, 0.5f, + 5.0f, "%.1f"); + ImGui::SetNextItemWidth(150); + ImGui::InputInt("Duration Step (ms)", &openshock_duration_step_, 100, 1000); + + ImGui::Spacing(); + + // --- Actions --- + if (ImGui::Button("Apply & Save Settings")) { + std::string token(openshock_token_buf_.data()); + // Robust trim (all whitespace) + token.erase(token.begin(), + std::find_if(token.begin(), token.end(), [](unsigned char ch) { + return !std::isspace(ch); + })); + token.erase(std::find_if(token.rbegin(), token.rend(), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + token.end()); + + config.SetState("openshock.token", token); + config.SetState("openshock.intensity_step", + std::to_string(openshock_intensity_step_)); + config.SetState("openshock.duration_step", + std::to_string(openshock_duration_step_)); + + client->SetToken(token); + client->FetchShockers(); + + if (!config_path_.empty()) + config.SaveToFile(config_path_); + Logger::Info("OpenShock settings updated."); + } + + ImGui::Separator(); + ImGui::Text("Tools"); + + if (ImGui::Button("Refresh Shocker List")) { + client->FetchShockers(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Test Vibration")) { + const auto &shockers = client->GetShockers(); + if (!shockers.empty()) { + // Vibrate first device for 1s at 20% + client->SendControl(shockers[0].id, "Vibrate", 20.0f, 1000); + Logger::Info("OpenShock: Sent test vibration to " + shockers[0].name); + } else { + Logger::Warning("OpenShock: No shockers available to test."); + } + } + ImGui::TextDisabled( + "Sends a 1.0s vibrate at 20%% intensity to the first available device."); +} + +} // namespace YipOS From e5a364c1db5a7d1bf7db191e9dc0b9790a69accc Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:38:52 +0100 Subject: [PATCH 08/25] Register new OpenShock tab to UI --- yip_os/src/ui/UIManager.cpp | 9 +++++---- yip_os/src/ui/UIManager.hpp | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/yip_os/src/ui/UIManager.cpp b/yip_os/src/ui/UIManager.cpp index e7818a8..a0853bc 100644 --- a/yip_os/src/ui/UIManager.cpp +++ b/yip_os/src/ui/UIManager.cpp @@ -197,9 +197,9 @@ void UIManager::Render(PDAController& pda, Config& config, OSCManager& osc) { { static const char* tab_labels[] = { "Status", "OSC", "Display", "VRCX", "CC", "INTRP", "Avatar", - "Text", "IMG", "Stocks", "Twitch", "DM", "NVRAM", "Log" + "Text", "IMG", "Stocks", "Twitch", "DM", "OpenShock", "NVRAM", "Log" }; - static constexpr int TAB_COUNT = 14; + static constexpr int TAB_COUNT = 15; static constexpr int ROW1_COUNT = 7; ImGuiStyle& style = ImGui::GetStyle(); @@ -243,8 +243,9 @@ void UIManager::Render(PDAController& pda, Config& config, OSCManager& osc) { case 9: RenderStocksTab(pda, config); break; case 10: RenderTwitchTab(pda, config); break; case 11: RenderDMTab(pda, config); break; - case 12: RenderNVRAMTab(pda, config); break; - case 13: RenderLogTab(); break; + case 12: RenderOpenShockTab(pda, config); break; + case 13: RenderNVRAMTab(pda, config); break; + case 14: RenderLogTab(); break; } } diff --git a/yip_os/src/ui/UIManager.hpp b/yip_os/src/ui/UIManager.hpp index a2e4fb6..58a340b 100644 --- a/yip_os/src/ui/UIManager.hpp +++ b/yip_os/src/ui/UIManager.hpp @@ -57,6 +57,7 @@ class UIManager { void RenderTwitchTab(PDAController& pda, Config& config); void RenderIMGTab(PDAController& pda, Config& config); void RenderDMTab(PDAController& pda, Config& config); + void RenderOpenShockTab(PDAController& pda, Config& config); void RenderNVRAMTab(PDAController& pda, Config& config); void RenderLogTab(); @@ -111,6 +112,12 @@ class UIManager { std::array dm_join_code_buf_ = {}; std::unordered_map> dm_compose_bufs_; + // OpenShock tab state + std::array openshock_token_buf_ = {}; + bool openshock_token_initialized_ = false; + float openshock_intensity_step_ = 2.5f; + int openshock_duration_step_ = 100; + // OSC Query server (optional, for status display) OSCQueryServer* osc_query_ = nullptr; From ecd72a93a187b22d4ec893765da1b3c2ab026011 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:39:15 +0100 Subject: [PATCH 09/25] Add OpenShock files to build --- yip_os/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yip_os/CMakeLists.txt b/yip_os/CMakeLists.txt index bb847d2..5a54aaf 100644 --- a/yip_os/CMakeLists.txt +++ b/yip_os/CMakeLists.txt @@ -148,7 +148,9 @@ set(YIPOS_SOURCES src/screens/DMPairScreen.cpp src/screens/DMComposeScreen.cpp src/screens/DMMessageScreen.cpp + src/screens/OpenShockScreen.cpp src/net/DMClient.cpp + src/net/OpenShockClient.cpp src/img/QRGen.cpp src/translate/TranslationWorker.cpp src/img/VQEncoder.cpp @@ -164,6 +166,7 @@ set(YIPOS_SOURCES src/ui/UIManager_Data.cpp src/ui/UIManager_Config.cpp src/ui/UIManager_DM.cpp + src/ui/UIManager_OpenShock.cpp ) # Platform-specific sources From 4726b1054dd8aad2e6d3f68b3de8995283f71975 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 16:41:57 +0100 Subject: [PATCH 10/25] Update filename --- yip_os/src/net/OpenShockClient.hpp | 6 ++++-- yip_os/src/screens/OpenShockScreen.cpp | 2 +- yip_os/src/screens/OpenShockScreen.hpp | 2 +- yip_os/src/ui/UIManager_OpenShock.cpp | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/yip_os/src/net/OpenShockClient.hpp b/yip_os/src/net/OpenShockClient.hpp index c07a178..fdf7ca1 100644 --- a/yip_os/src/net/OpenShockClient.hpp +++ b/yip_os/src/net/OpenShockClient.hpp @@ -1,9 +1,11 @@ /** - * OpenShockClient.cpp + * OpenShockClient.hpp * V1.0.0 * * Adds OpenShock API integration to YipOS for remote control of OpenShock - * devices By otter_oasis + * devices. + * + * By otter_oasis */ #pragma once diff --git a/yip_os/src/screens/OpenShockScreen.cpp b/yip_os/src/screens/OpenShockScreen.cpp index d402a02..1bd8298 100644 --- a/yip_os/src/screens/OpenShockScreen.cpp +++ b/yip_os/src/screens/OpenShockScreen.cpp @@ -1,5 +1,5 @@ /** - * OpenShockClient.cpp + * OpenShockScreen.cpp * V1.0.0 * * Adds OpenShock API integration to YipOS for remote control of OpenShock diff --git a/yip_os/src/screens/OpenShockScreen.hpp b/yip_os/src/screens/OpenShockScreen.hpp index a72a971..4743d99 100644 --- a/yip_os/src/screens/OpenShockScreen.hpp +++ b/yip_os/src/screens/OpenShockScreen.hpp @@ -1,5 +1,5 @@ /** - * OpenShockClient.cpp + * OpenShockScreen.cpp * V1.0.0 * * Adds OpenShock API integration to YipOS for remote control of OpenShock diff --git a/yip_os/src/ui/UIManager_OpenShock.cpp b/yip_os/src/ui/UIManager_OpenShock.cpp index 14aeaa5..61bbe6f 100644 --- a/yip_os/src/ui/UIManager_OpenShock.cpp +++ b/yip_os/src/ui/UIManager_OpenShock.cpp @@ -1,5 +1,5 @@ /** - * OpenShockClient.cpp + * UIManager_OpenShock.cpp * V1.0.0 * * Adds OpenShock API integration to YipOS for remote control of OpenShock From 2eada908bfb2aad2124dab440928808265da60f5 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 17:14:51 +0100 Subject: [PATCH 11/25] Added OpenShock enable toggle and warning message --- yip_os/src/app/PDAController.cpp | 5 +++-- yip_os/src/ui/UIManager.hpp | 1 + yip_os/src/ui/UIManager_OpenShock.cpp | 29 ++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/yip_os/src/app/PDAController.cpp b/yip_os/src/app/PDAController.cpp index 87654fa..28dfe66 100644 --- a/yip_os/src/app/PDAController.cpp +++ b/yip_os/src/app/PDAController.cpp @@ -101,10 +101,11 @@ PDAController::PDAController(PDADisplay& display, NetTracker& net_tracker, Confi // Initialize OpenShock client openshock_client_ = std::make_unique(); std::string os_token = config_.GetState("openshock.token"); - if (!os_token.empty()) { + std::string os_enabled = config_.GetState("openshock.enabled", "0"); + if (os_enabled != "0" && !os_token.empty()) { openshock_client_->SetToken(os_token); + openshock_client_->FetchShockers(); } - openshock_client_->FetchShockers(); // Push home screen as root auto home = std::make_unique(*this); diff --git a/yip_os/src/ui/UIManager.hpp b/yip_os/src/ui/UIManager.hpp index 58a340b..9153f82 100644 --- a/yip_os/src/ui/UIManager.hpp +++ b/yip_os/src/ui/UIManager.hpp @@ -113,6 +113,7 @@ class UIManager { std::unordered_map> dm_compose_bufs_; // OpenShock tab state + bool openshock_enabled_ = false; std::array openshock_token_buf_ = {}; bool openshock_token_initialized_ = false; float openshock_intensity_step_ = 2.5f; diff --git a/yip_os/src/ui/UIManager_OpenShock.cpp b/yip_os/src/ui/UIManager_OpenShock.cpp index 61bbe6f..dec4b6d 100644 --- a/yip_os/src/ui/UIManager_OpenShock.cpp +++ b/yip_os/src/ui/UIManager_OpenShock.cpp @@ -24,8 +24,18 @@ void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { ImGui::Text("OpenShock Integration"); ImGui::TextDisabled( "Drive your OpenShock devices directly from the Yip-Boi."); + ImGui::TextDisabled("Module by @otter_oasis."); + + ImGui::Spacing(); + + ImGui::TextDisabled("Warning: Using shocking devices is at your own risk."); + ImGui::TextDisabled("Use responsibly and follow all safety guidelines."); + ImGui::TextDisabled( + "Remember: If other people can interact with your yip-boi, they can " + "control your shocks."); ImGui::Separator(); + ImGui::Spacing(); auto *client = pda.GetOpenShockClient(); if (!client) { @@ -36,6 +46,9 @@ void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { // Initialize buffer from config state if (!openshock_token_initialized_) { + std::string enabled = config.GetState("openshock.enabled", "0"); + openshock_enabled_ = (enabled != "0"); + std::string token = config.GetState("openshock.token"); std::snprintf(openshock_token_buf_.data(), openshock_token_buf_.size(), "%s", token.c_str()); @@ -55,8 +68,13 @@ void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { openshock_token_initialized_ = true; } + ImGui::Checkbox("Enable OpenShock Integration", &openshock_enabled_); + ImGui::Spacing(); + // --- Status --- - if (!client->HasToken()) { + if (!openshock_enabled_) { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Status: Disabled"); + } else if (!client->HasToken()) { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Status: Offline (no token)"); } else if (client->IsTokenValid()) { @@ -101,14 +119,19 @@ void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { .base(), token.end()); + config.SetState("openshock.enabled", openshock_enabled_ ? "1" : "0"); config.SetState("openshock.token", token); config.SetState("openshock.intensity_step", std::to_string(openshock_intensity_step_)); config.SetState("openshock.duration_step", std::to_string(openshock_duration_step_)); - client->SetToken(token); - client->FetchShockers(); + if (openshock_enabled_) { + client->SetToken(token); + client->FetchShockers(); + } else { + client->SetToken(""); + } if (!config_path_.empty()) config.SaveToFile(config_path_); From 633d591ff0cf5aff3a5301e8281f23d019a7b251 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Wed, 8 Apr 2026 17:19:10 +0100 Subject: [PATCH 12/25] Show intensity and duration as zeroed out when unavailable --- yip_os/src/screens/OpenShockScreen.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/yip_os/src/screens/OpenShockScreen.cpp b/yip_os/src/screens/OpenShockScreen.cpp index 1bd8298..a890198 100644 --- a/yip_os/src/screens/OpenShockScreen.cpp +++ b/yip_os/src/screens/OpenShockScreen.cpp @@ -45,13 +45,24 @@ void OpenShockScreen::RenderContent() { display_.WriteText(25, 6, " [ EXECUTE ] ", true); } + auto *client = pda_.GetOpenShockClient(); + bool available = client && client->HasToken() && !client->GetShockers().empty(); + // Intensity Value char buf[16]; - std::snprintf(buf, sizeof(buf), "%3.0f%%", intensity_); + if (available) { + std::snprintf(buf, sizeof(buf), "%3.0f%%", intensity_); + } else { + std::snprintf(buf, sizeof(buf), " - "); + } display_.WriteText(8, 4, buf); // Duration Value - std::snprintf(buf, sizeof(buf), "%1.1fs", duration_ms_ / 1000.0f); + if (available) { + std::snprintf(buf, sizeof(buf), "%1.1fs", duration_ms_ / 1000.0f); + } else { + std::snprintf(buf, sizeof(buf), " - "); + } display_.WriteText(28, 4, buf, false); } From 59e293917606dd80b93b3ad6ce30cce64fca1940 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:10:41 +0100 Subject: [PATCH 13/25] Replace OpenShock tab with new general Shock tab in UI --- yip_os/src/ui/UIManager_OpenShock.cpp | 164 ------------------- yip_os/src/ui/UIManager_Shock.cpp | 221 ++++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 164 deletions(-) delete mode 100644 yip_os/src/ui/UIManager_OpenShock.cpp create mode 100644 yip_os/src/ui/UIManager_Shock.cpp diff --git a/yip_os/src/ui/UIManager_OpenShock.cpp b/yip_os/src/ui/UIManager_OpenShock.cpp deleted file mode 100644 index dec4b6d..0000000 --- a/yip_os/src/ui/UIManager_OpenShock.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/** - * UIManager_OpenShock.cpp - * V1.0.0 - * - * Adds OpenShock API integration to YipOS for remote control of OpenShock - * devices. - * - * By otter_oasis - */ - -#include "UIManager.hpp" -#include "app/PDAController.hpp" -#include "core/Config.hpp" -#include "core/Logger.hpp" -#include "net/OpenShockClient.hpp" - -#include -#include -#include - -namespace YipOS { - -void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { - ImGui::Text("OpenShock Integration"); - ImGui::TextDisabled( - "Drive your OpenShock devices directly from the Yip-Boi."); - ImGui::TextDisabled("Module by @otter_oasis."); - - ImGui::Spacing(); - - ImGui::TextDisabled("Warning: Using shocking devices is at your own risk."); - ImGui::TextDisabled("Use responsibly and follow all safety guidelines."); - ImGui::TextDisabled( - "Remember: If other people can interact with your yip-boi, they can " - "control your shocks."); - - ImGui::Separator(); - ImGui::Spacing(); - - auto *client = pda.GetOpenShockClient(); - if (!client) { - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), - "OpenShock client not initialized."); - return; - } - - // Initialize buffer from config state - if (!openshock_token_initialized_) { - std::string enabled = config.GetState("openshock.enabled", "0"); - openshock_enabled_ = (enabled != "0"); - - std::string token = config.GetState("openshock.token"); - std::snprintf(openshock_token_buf_.data(), openshock_token_buf_.size(), - "%s", token.c_str()); - - std::string i_step = config.GetState("openshock.intensity_step", "2.5"); - try { - openshock_intensity_step_ = std::stof(i_step); - } catch (...) { - } - - std::string d_step = config.GetState("openshock.duration_step", "1000"); - try { - openshock_duration_step_ = std::stoi(d_step); - } catch (...) { - } - - openshock_token_initialized_ = true; - } - - ImGui::Checkbox("Enable OpenShock Integration", &openshock_enabled_); - ImGui::Spacing(); - - // --- Status --- - if (!openshock_enabled_) { - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Status: Disabled"); - } else if (!client->HasToken()) { - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), - "Status: Offline (no token)"); - } else if (client->IsTokenValid()) { - ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Status: Connected"); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), - "Status: Authentication Failed"); - } - - ImGui::Spacing(); - - // --- Token --- - ImGui::Text("API Token"); - ImGui::SetNextItemWidth(-1); - ImGui::InputText("##os_token", openshock_token_buf_.data(), - openshock_token_buf_.size(), ImGuiInputTextFlags_Password); - ImGui::TextDisabled( - "Generate a token at https://next.openshock.app/settings/api-tokens"); - - ImGui::Spacing(); - - // --- Increments --- - ImGui::Text("Control Increments (Yip-Boi Screen)"); - ImGui::SetNextItemWidth(150); - ImGui::InputFloat("Intensity Step (%)", &openshock_intensity_step_, 0.5f, - 5.0f, "%.1f"); - ImGui::SetNextItemWidth(150); - ImGui::InputInt("Duration Step (ms)", &openshock_duration_step_, 100, 1000); - - ImGui::Spacing(); - - // --- Actions --- - if (ImGui::Button("Apply & Save Settings")) { - std::string token(openshock_token_buf_.data()); - // Robust trim (all whitespace) - token.erase(token.begin(), - std::find_if(token.begin(), token.end(), [](unsigned char ch) { - return !std::isspace(ch); - })); - token.erase(std::find_if(token.rbegin(), token.rend(), - [](unsigned char ch) { return !std::isspace(ch); }) - .base(), - token.end()); - - config.SetState("openshock.enabled", openshock_enabled_ ? "1" : "0"); - config.SetState("openshock.token", token); - config.SetState("openshock.intensity_step", - std::to_string(openshock_intensity_step_)); - config.SetState("openshock.duration_step", - std::to_string(openshock_duration_step_)); - - if (openshock_enabled_) { - client->SetToken(token); - client->FetchShockers(); - } else { - client->SetToken(""); - } - - if (!config_path_.empty()) - config.SaveToFile(config_path_); - Logger::Info("OpenShock settings updated."); - } - - ImGui::Separator(); - ImGui::Text("Tools"); - - if (ImGui::Button("Refresh Shocker List")) { - client->FetchShockers(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Test Vibration")) { - const auto &shockers = client->GetShockers(); - if (!shockers.empty()) { - // Vibrate first device for 1s at 20% - client->SendControl(shockers[0].id, "Vibrate", 20.0f, 1000); - Logger::Info("OpenShock: Sent test vibration to " + shockers[0].name); - } else { - Logger::Warning("OpenShock: No shockers available to test."); - } - } - ImGui::TextDisabled( - "Sends a 1.0s vibrate at 20%% intensity to the first available device."); -} - -} // namespace YipOS diff --git a/yip_os/src/ui/UIManager_Shock.cpp b/yip_os/src/ui/UIManager_Shock.cpp new file mode 100644 index 0000000..18ceec9 --- /dev/null +++ b/yip_os/src/ui/UIManager_Shock.cpp @@ -0,0 +1,221 @@ +/** + * UIManager_Shock.cpp + * V1.0.0 + * + * Shock device configuration screen for YipOS. + * + * By otter_oasis + */ + +#include "UIManager.hpp" +#include "app/PDAController.hpp" +#include "core/Config.hpp" +#include "core/Logger.hpp" +#include "net/OpenShockClient.hpp" +#include "net/PiShockClient.hpp" +#include "net/ShockManager.hpp" + +#include +#include +#include + +namespace YipOS { + +void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { + ImGui::Text("Shocker Integration"); + ImGui::TextDisabled( + "Drive your PiShock & OpenShock devices directly from the Yip-Boi."); + ImGui::Spacing(); + + ImGui::TextDisabled("Warning: Using shocking devices is at your own risk."); + ImGui::TextDisabled("Use responsibly and follow all safety guidelines."); + ImGui::TextDisabled( + "Remember: If other people can interact with your yip-boi, they can " + "control your shocks."); + + ImGui::Separator(); + ImGui::Spacing(); + + auto *manager = pda.GetShockManager(); + if (!manager) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), + "ShockManager not initialized."); + return; + } + + // --- Initialize Config Buffers --- + if (!openshock_token_initialized_ || !pishock_initialized_) { + // OpenShock + std::string os_enabled = config.GetState("openshock.enabled", "0"); + openshock_enabled_ = (os_enabled != "0"); + + std::string token = config.GetState("openshock.token"); + std::snprintf(openshock_token_buf_.data(), openshock_token_buf_.size(), + "%s", token.c_str()); + + // PiShock + std::string ps_enabled = config.GetState("pishock.enabled", "0"); + pishock_enabled_ = (ps_enabled != "0"); + + std::string ps_user = config.GetState("pishock.username"); + std::snprintf(pishock_username_buf_.data(), pishock_username_buf_.size(), + "%s", ps_user.c_str()); + + std::string ps_api = config.GetState("pishock.apikey"); + std::snprintf(pishock_apikey_buf_.data(), pishock_apikey_buf_.size(), "%s", + ps_api.c_str()); + + std::string i_step = config.GetState("openshock.intensity_step", "2.5"); + try { + openshock_intensity_step_ = std::stof(i_step); + } catch (...) { + } + std::string d_step = config.GetState("openshock.duration_step", "1000"); + try { + openshock_duration_step_ = std::stoi(d_step); + } catch (...) { + } + + openshock_token_initialized_ = true; + pishock_initialized_ = true; + } + + // --- PiShock Config --- + ImGui::Text("PiShock Configuration"); + ImGui::Checkbox("Enable PiShock", &pishock_enabled_); + + if (!pishock_enabled_) { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Status: Disabled"); + } else { + auto *ps = manager->GetPiShockClient(); + if (ps && ps->HasConfig()) { + if (ps->IsTokenValid()) { + ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Status: Verified"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), + "Status: Auth Pending/Failed"); + } + } else { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), + "Status: Unconfigured"); + } + } + + ImGui::SetNextItemWidth(150); + ImGui::InputText("Username", pishock_username_buf_.data(), + pishock_username_buf_.size()); + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##ps_token", pishock_apikey_buf_.data(), + pishock_apikey_buf_.size(), ImGuiInputTextFlags_Password); + ImGui::TextDisabled("API Key generated at https://login.pishock.com/account"); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- OpenShock Config --- + ImGui::Text("OpenShock Configuration"); + ImGui::Checkbox("Enable OpenShock", &openshock_enabled_); + + if (!openshock_enabled_) { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Status: Disabled"); + } else { + auto *os = manager->GetOpenShockClient(); + if (os && os->HasConfig()) { + if (os->IsTokenValid()) { + ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.4f, 1.0f), "Status: Verified"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), + "Status: Auth Pending/Failed"); + } + } else { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), + "Status: Unconfigured"); + } + } + + ImGui::SetNextItemWidth(-1); + ImGui::InputText("##os_token", openshock_token_buf_.data(), + openshock_token_buf_.size(), ImGuiInputTextFlags_Password); + ImGui::TextDisabled( + "API Token generated at https://next.openshock.app/settings/api-tokens"); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- Global Wrist Screen Controls --- + ImGui::Text("Wrist Screen Controls"); + ImGui::TextDisabled( + "Step sizes used by wrist buttons for all shock devices."); + ImGui::SetNextItemWidth(150); + ImGui::InputFloat("Intensity Step (%)", &openshock_intensity_step_, 0.5f, + 5.0f, "%.1f"); + ImGui::SetNextItemWidth(150); + ImGui::InputInt("Duration Step (ms)", &openshock_duration_step_, 100, 1000); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // --- Actions --- + if (ImGui::Button("Apply & Save Settings")) { + // OpenShock string trim + std::string os_token(openshock_token_buf_.data()); + os_token.erase( + std::remove_if(os_token.begin(), os_token.end(), + [](unsigned char ch) { return std::isspace(ch); }), + os_token.end()); + + std::string ps_api(pishock_apikey_buf_.data()); + ps_api.erase( + std::remove_if(ps_api.begin(), ps_api.end(), + [](unsigned char ch) { return std::isspace(ch); }), + ps_api.end()); + + std::string ps_user(pishock_username_buf_.data()); + ps_user.erase( + std::remove_if(ps_user.begin(), ps_user.end(), + [](unsigned char ch) { return std::isspace(ch); }), + ps_user.end()); + + config.SetState("openshock.enabled", openshock_enabled_ ? "1" : "0"); + config.SetState("openshock.token", os_token); + config.SetState("openshock.intensity_step", + std::to_string(openshock_intensity_step_)); + config.SetState("openshock.duration_step", + std::to_string(openshock_duration_step_)); + + config.SetState("pishock.enabled", pishock_enabled_ ? "1" : "0"); + config.SetState("pishock.username", ps_user); + config.SetState("pishock.apikey", ps_api); + + manager->InitFromConfig(config); + + if (!config_path_.empty()) + config.SaveToFile(config_path_); + Logger::Info("Shock manager settings updated."); + } + + ImGui::Separator(); + ImGui::Text("Tools"); + + if (ImGui::Button("Refresh Shocker List")) { + manager->FetchShockers(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Test Vibration")) { + const auto &shockers = manager->GetShockers(); + if (!shockers.empty()) { + manager->SendControl(shockers[0].id, shockers[0].backend, "Vibrate", + 20.0f, 1000); + Logger::Info("ShockManager: Sent test vibration to " + shockers[0].name); + } else { + Logger::Warning("ShockManager: No shockers available to test."); + } + } +} + +} // namespace YipOS From 60718c069f38e47087674be433d72cd92a9cad50 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:11:39 +0100 Subject: [PATCH 14/25] Add new ShockManager and client interface for multiple backends (openshock and pishock) --- yip_os/src/net/IShockClient.hpp | 45 +++++++++++ yip_os/src/net/ShockManager.cpp | 130 ++++++++++++++++++++++++++++++++ yip_os/src/net/ShockManager.hpp | 51 +++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 yip_os/src/net/IShockClient.hpp create mode 100644 yip_os/src/net/ShockManager.cpp create mode 100644 yip_os/src/net/ShockManager.hpp diff --git a/yip_os/src/net/IShockClient.hpp b/yip_os/src/net/IShockClient.hpp new file mode 100644 index 0000000..08c5b85 --- /dev/null +++ b/yip_os/src/net/IShockClient.hpp @@ -0,0 +1,45 @@ +/** + * IShockClient.hpp + * V1.0.0 + * + * Interface for Shock API integration to YipOS for remote control of + * OpenShock and PiShock devices. + * + * By otter_oasis + */ + +#pragma once + +#include +#include + +namespace YipOS { + +struct Shocker { + std::string id; + std::string name; + bool is_owned = false; + std::string backend; // "openshock" or "pishock" +}; + +class IShockClient { +public: + virtual ~IShockClient() = default; + + virtual void SetEnabled(bool enabled) = 0; + virtual bool HasConfig() const = 0; + virtual bool IsTokenValid() const = 0; + virtual bool IsEnabled() const = 0; + + virtual bool FetchShockers() = 0; + virtual const std::vector &GetShockers() const = 0; + + virtual bool SendControl(const std::string &shocker_id, + const std::string &type, float intensity, + int duration_ms) = 0; + + virtual int GetMinDurationMs() const = 0; + virtual int GetMaxDurationMs() const = 0; +}; + +} // namespace YipOS diff --git a/yip_os/src/net/ShockManager.cpp b/yip_os/src/net/ShockManager.cpp new file mode 100644 index 0000000..1897e8e --- /dev/null +++ b/yip_os/src/net/ShockManager.cpp @@ -0,0 +1,130 @@ +/** + * ShockManager.cpp + * V1.0.0 + * + * Manages multiple shock device APIs (OpenShock, PiShock) for YipOS. + * + * By otter_oasis + */ + +#include "ShockManager.hpp" +#include "OpenShockClient.hpp" +#include "PiShockClient.hpp" +#include "core/Config.hpp" +#include "core/Logger.hpp" + +namespace YipOS { + +ShockManager::ShockManager() { + openshock_ = std::make_unique(); + pishock_ = std::make_unique(); +} + +ShockManager::~ShockManager() = default; + +void ShockManager::InitFromConfig(Config &config) { + Logger::Info("ShockManager: Initialising from config"); + + // OpenShock + std::string os_enabled = config.GetState("openshock.enabled", "0"); + openshock_->SetEnabled(os_enabled != "0"); + openshock_->SetToken(config.GetState("openshock.token", "")); + Logger::Info(std::string("ShockManager: OpenShock ") + + (os_enabled != "0" ? "enabled" : "disabled")); + + // PiShock + std::string ps_enabled = config.GetState("pishock.enabled", "0"); + pishock_->SetEnabled(ps_enabled != "0"); + pishock_->SetCredentials(config.GetState("pishock.username", ""), + config.GetState("pishock.apikey", "")); + Logger::Info(std::string("ShockManager: PiShock ") + + (ps_enabled != "0" ? "enabled" : "disabled")); + + FetchShockers(); +} + +void ShockManager::FetchShockers() { + shockers_.clear(); + Logger::Info("ShockManager: Fetching shockers from all enabled backends"); + + if (openshock_->IsEnabled()) { + openshock_->FetchShockers(); + const auto &os_list = openshock_->GetShockers(); + shockers_.insert(shockers_.end(), os_list.begin(), os_list.end()); + Logger::Info("ShockManager: OpenShock returned " + + std::to_string(os_list.size()) + " shocker(s)"); + } else { + Logger::Debug("ShockManager: OpenShock skipped (disabled)"); + } + + if (pishock_->IsEnabled()) { + pishock_->FetchShockers(); + const auto &ps_list = pishock_->GetShockers(); + shockers_.insert(shockers_.end(), ps_list.begin(), ps_list.end()); + Logger::Info("ShockManager: PiShock returned " + + std::to_string(ps_list.size()) + " shocker(s)"); + } else { + Logger::Debug("ShockManager: PiShock skipped (disabled)"); + } + + Logger::Info("ShockManager: Total shockers available: " + + std::to_string(shockers_.size())); +} + +bool ShockManager::SendControl(const std::string &shocker_id, + const std::string &backend, + const std::string &type, float intensity, + int duration_ms) { + Logger::Info("ShockManager: Routing " + type + " → backend='" + backend + + "' id='" + shocker_id + "'"); + if (backend == "openshock" && openshock_->IsEnabled()) { + bool ok = openshock_->SendControl(shocker_id, type, intensity, duration_ms); + if (!ok) + Logger::Warning("ShockManager: OpenShock command failed"); + return ok; + } else if (backend == "pishock" && pishock_->IsEnabled()) { + bool ok = pishock_->SendControl(shocker_id, type, intensity, duration_ms); + if (!ok) + Logger::Warning("ShockManager: PiShock command failed"); + return ok; + } + Logger::Warning("ShockManager: No enabled backend matched '" + backend + + "' — command dropped"); + return false; +} + +int ShockManager::GetMinDurationMs(const std::string &backend) const { + if (backend == "openshock") + return openshock_->GetMinDurationMs(); + if (backend == "pishock") + return pishock_->GetMinDurationMs(); + return 100; // default +} + +int ShockManager::GetMaxDurationMs(const std::string &backend) const { + if (backend == "openshock") + return openshock_->GetMaxDurationMs(); + if (backend == "pishock") + return pishock_->GetMaxDurationMs(); + return 15000; // default +} + +bool ShockManager::HasAnyConfig() const { + return openshock_->HasConfig() || pishock_->HasConfig(); +} + +bool ShockManager::IsHealthy() const { + // If a service IS configured but its token/auth is NOT valid, manager is + // unhealthy. + if (openshock_->IsEnabled() && openshock_->HasConfig() && + !openshock_->IsTokenValid()) { + return false; + } + if (pishock_->IsEnabled() && pishock_->HasConfig() && + !pishock_->IsTokenValid()) { + return false; + } + return true; +} + +} // namespace YipOS diff --git a/yip_os/src/net/ShockManager.hpp b/yip_os/src/net/ShockManager.hpp new file mode 100644 index 0000000..b9b88ef --- /dev/null +++ b/yip_os/src/net/ShockManager.hpp @@ -0,0 +1,51 @@ +/** + * ShockManager.hpp + * V1.0.0 + * + * Manages multiple shock device APIs (OpenShock, PiShock) for YipOS. + * + * By otter_oasis + */ + +#pragma once + +#include "IShockClient.hpp" +#include +#include +#include + +namespace YipOS { + +class Config; +class OpenShockClient; +class PiShockClient; + +class ShockManager { +public: + ShockManager(); + ~ShockManager(); + + void InitFromConfig(Config &config); + + void FetchShockers(); + const std::vector &GetShockers() const { return shockers_; } + + bool SendControl(const std::string &shocker_id, const std::string &backend, + const std::string &type, float intensity, int duration_ms); + + int GetMinDurationMs(const std::string &backend) const; + int GetMaxDurationMs(const std::string &backend) const; + + bool IsHealthy() const; + bool HasAnyConfig() const; + + OpenShockClient *GetOpenShockClient() const { return openshock_.get(); } + PiShockClient *GetPiShockClient() const { return pishock_.get(); } + +private: + std::unique_ptr openshock_; + std::unique_ptr pishock_; + std::vector shockers_; +}; + +} // namespace YipOS From 4800d9512cb2150dbae002c6573d7f0d03e78779 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:12:05 +0100 Subject: [PATCH 15/25] Update atlas frame name from OpenShock to Shock --- WilliamsTube_MacroAtlas.png | Bin 75949 -> 75892 bytes generate_macro_atlas.py | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WilliamsTube_MacroAtlas.png b/WilliamsTube_MacroAtlas.png index 9923ec8f49c7c8b9fe22fda8f38896d66eadc2a5..38e5298664c15ac7592d5bab0b05a89de8497054 100644 GIT binary patch delta 6750 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 K%Z~Z~>;C}dK+-S( delta 7020 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 lHDTA+-G8!vd%G!LlRbIvQ<7#+CA1DW`;* Date: Thu, 9 Apr 2026 15:13:12 +0100 Subject: [PATCH 16/25] Generalize shock screen --- .../{OpenShockScreen.cpp => ShockScreen.cpp} | 104 ++++++++++-------- .../{OpenShockScreen.hpp => ShockScreen.hpp} | 9 +- 2 files changed, 64 insertions(+), 49 deletions(-) rename yip_os/src/screens/{OpenShockScreen.cpp => ShockScreen.cpp} (61%) rename yip_os/src/screens/{OpenShockScreen.hpp => ShockScreen.hpp} (77%) diff --git a/yip_os/src/screens/OpenShockScreen.cpp b/yip_os/src/screens/ShockScreen.cpp similarity index 61% rename from yip_os/src/screens/OpenShockScreen.cpp rename to yip_os/src/screens/ShockScreen.cpp index a890198..f7fa6c6 100644 --- a/yip_os/src/screens/OpenShockScreen.cpp +++ b/yip_os/src/screens/ShockScreen.cpp @@ -1,21 +1,19 @@ /** - * OpenShockScreen.cpp + * ShockScreen.cpp * V1.0.0 * - * Adds OpenShock API integration to YipOS for remote control of OpenShock - * devices. + * Screen for controlling multiple shock devices via the ShockManager interface. * * By otter_oasis */ -#include "OpenShockScreen.hpp" +#include "ShockScreen.hpp" #include "app/PDAController.hpp" #include "app/PDADisplay.hpp" #include "core/Config.hpp" #include "core/Glyphs.hpp" -#include "core/Logger.hpp" #include "core/TimeUtil.hpp" -#include "net/OpenShockClient.hpp" +#include "net/ShockManager.hpp" #include #include @@ -23,18 +21,18 @@ namespace YipOS { using namespace Glyphs; -OpenShockScreen::OpenShockScreen(PDAController &pda) : Screen(pda) { +ShockScreen::ShockScreen(PDAController &pda) : Screen(pda) { name = "SHOCK"; macro_index = 48; // OPENSHOCK macro } -void OpenShockScreen::Render() { +void ShockScreen::Render() { // The OPENSHOCK title and layout is baked into the macro texture RenderContent(); RenderStatusBar(); } -void OpenShockScreen::RenderContent() { +void ShockScreen::RenderContent() { RenderShockerSelection(); RenderModeSelection(); @@ -45,8 +43,9 @@ void OpenShockScreen::RenderContent() { display_.WriteText(25, 6, " [ EXECUTE ] ", true); } - auto *client = pda_.GetOpenShockClient(); - bool available = client && client->HasToken() && !client->GetShockers().empty(); + auto *manager = pda_.GetShockManager(); + bool available = + manager && manager->HasAnyConfig() && !manager->GetShockers().empty(); // Intensity Value char buf[16]; @@ -66,7 +65,7 @@ void OpenShockScreen::RenderContent() { display_.WriteText(28, 4, buf, false); } -void OpenShockScreen::RenderDynamic() { +void ShockScreen::RenderDynamic() { if (show_success_flash_ && MonotonicNow() > flash_end_time_) { show_success_flash_ = false; } @@ -76,18 +75,18 @@ void OpenShockScreen::RenderDynamic() { RenderCursor(); } -void OpenShockScreen::RenderShockerSelection() { - auto *client = pda_.GetOpenShockClient(); - if (!client) +void ShockScreen::RenderShockerSelection() { + auto *manager = pda_.GetShockManager(); + if (!manager) return; - if (!client->HasToken()) { + if (!manager->HasAnyConfig()) { display_.WriteText(8, 1, "SETUP IN APP"); return; } - const auto &items = client->GetShockers(); - std::string label = "NO DEVICES OR BAD TOKEN"; + const auto &items = manager->GetShockers(); + std::string label = "NO DEVICES OR BAD AUTH"; if (!items.empty()) { if (selected_shocker_idx_ >= static_cast(items.size())) { selected_shocker_idx_ = 0; @@ -105,25 +104,23 @@ void OpenShockScreen::RenderShockerSelection() { display_.WriteText(8, 1, padded); } -void OpenShockScreen::RenderModeSelection() { - auto *client = pda_.GetOpenShockClient(); +void ShockScreen::RenderModeSelection() { + auto *manager = pda_.GetShockManager(); // Status indicator at Column 0 (to the left of ACTIVE MODE label) on Row 3 std::string status = "- "; - if (client) { - if (client->HasToken()) { - status = client->IsTokenValid() ? " " : "! "; - } + if (manager && manager->HasAnyConfig()) { + status = manager->IsHealthy() ? " " : "! "; } display_.WriteText(2, 1, status); std::string val = - (!client || !client->HasToken() || client->GetShockers().empty()) + (!manager || !manager->HasAnyConfig() || manager->GetShockers().empty()) ? "UNAVAILABLE" : MODES[mode_idx_]; // Add hazard markers if in SHOCK mode - if (mode_idx_ == 0 && client && client->HasToken()) { + if (mode_idx_ == 0 && manager && manager->HasAnyConfig()) { val = "!!" + std::string(MODES[0]) + "!!"; } @@ -137,7 +134,7 @@ void OpenShockScreen::RenderModeSelection() { display_.WriteText(17, 3, val); } -bool OpenShockScreen::OnInput(const std::string &key) { +bool ShockScreen::OnInput(const std::string &key) { if (key == "TL") { pda_.PopScreen(); return true; @@ -148,23 +145,33 @@ bool OpenShockScreen::OnInput(const std::string &key) { int tx = key[0] - '1'; int ty = key[1] - '1'; - auto *client = pda_.GetOpenShockClient(); + auto *manager = pda_.GetShockManager(); auto &config = pda_.GetConfig(); bool changed = false; - if (ty == 0) { // Top Row: Devices (Unchanged) + if (ty == 0) { // Top Row: Devices (Unchanged) + bool device_changed = false; if (tx == 0) { // Previous if (selected_shocker_idx_ > 0) { selected_shocker_idx_--; changed = true; + device_changed = true; } } else if (tx == 4) { // Next - if (client && selected_shocker_idx_ < - static_cast(client->GetShockers().size()) - 1) { + if (manager && + selected_shocker_idx_ < + static_cast(manager->GetShockers().size()) - 1) { selected_shocker_idx_++; changed = true; + device_changed = true; } } + if (device_changed && manager && !manager->GetShockers().empty()) { + const auto &s = manager->GetShockers()[selected_shocker_idx_]; + int min_d = manager->GetMinDurationMs(s.backend); + int max_d = manager->GetMaxDurationMs(s.backend); + duration_ms_ = std::clamp(duration_ms_, min_d, max_d); + } } else if (ty == 1) { // Middle Row: Intensity (Left) & Duration (Right) float i_step = 2.5f; int d_step = 1000; @@ -183,11 +190,19 @@ bool OpenShockScreen::OnInput(const std::string &key) { } else if (tx == 2) { // Int Up intensity_ = std::min(100.0f, intensity_ + i_step); changed = true; - } else if (tx == 3) { // Dur Down - duration_ms_ = std::max(100, duration_ms_ - d_step); - changed = true; - } else if (tx == 4) { // Dur Up - duration_ms_ = std::min(30000, duration_ms_ + d_step); + } else if (tx == 3 || tx == 4) { // Duration Down/Up + int min_d = 100; + int max_d = 30000; + if (manager && !manager->GetShockers().empty()) { + const auto &s = manager->GetShockers()[selected_shocker_idx_]; + min_d = manager->GetMinDurationMs(s.backend); + max_d = manager->GetMaxDurationMs(s.backend); + } + if (tx == 3) { + duration_ms_ = std::max(min_d, duration_ms_ - d_step); + } else { + duration_ms_ = std::min(max_d, duration_ms_ + d_step); + } changed = true; } } else if (ty == 2) { // Bottom Row: Mode Cycle (Left) & Execute (Right) @@ -195,9 +210,10 @@ bool OpenShockScreen::OnInput(const std::string &key) { mode_idx_ = (mode_idx_ + 1) % 3; changed = true; } else if (tx >= 3) { // EXECUTE (tx 3-4) - if (client && !client->GetShockers().empty()) { - const auto &s = client->GetShockers()[selected_shocker_idx_]; - client->SendControl(s.id, MODES[mode_idx_], intensity_, duration_ms_); + if (manager && !manager->GetShockers().empty()) { + const auto &s = manager->GetShockers()[selected_shocker_idx_]; + manager->SendControl(s.id, s.backend, MODES[mode_idx_], intensity_, + duration_ms_); show_success_flash_ = true; flash_end_time_ = MonotonicNow() + 2.0; changed = true; @@ -215,12 +231,12 @@ bool OpenShockScreen::OnInput(const std::string &key) { return false; } -void OpenShockScreen::Update() { +void ShockScreen::Update() { static double last_fetch = 0; - if (MonotonicNow() - last_fetch > 60.0) { - if (auto *client = pda_.GetOpenShockClient()) { - client->FetchShockers(); - } + if (MonotonicNow() - last_fetch > 30.0) { + auto *manager = pda_.GetShockManager(); + if (manager) + manager->FetchShockers(); last_fetch = MonotonicNow(); } } diff --git a/yip_os/src/screens/OpenShockScreen.hpp b/yip_os/src/screens/ShockScreen.hpp similarity index 77% rename from yip_os/src/screens/OpenShockScreen.hpp rename to yip_os/src/screens/ShockScreen.hpp index 4743d99..05c3676 100644 --- a/yip_os/src/screens/OpenShockScreen.hpp +++ b/yip_os/src/screens/ShockScreen.hpp @@ -1,9 +1,8 @@ /** - * OpenShockScreen.cpp + * ShockScreen.cpp * V1.0.0 * - * Adds OpenShock API integration to YipOS for remote control of OpenShock - * devices. + * Screen for controlling multiple shock devices via the ShockManager interface. * * By otter_oasis */ @@ -14,9 +13,9 @@ namespace YipOS { -class OpenShockScreen : public Screen { +class ShockScreen : public Screen { public: - OpenShockScreen(PDAController &pda); + ShockScreen(PDAController &pda); void Render() override; void RenderContent() override; From 92c745409115b40f0261c224f69cf34a0eb224d1 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:13:44 +0100 Subject: [PATCH 17/25] Modify OpenShockClient to use new ShockManager interface --- yip_os/src/net/OpenShockClient.cpp | 15 ++++++++++++--- yip_os/src/net/OpenShockClient.hpp | 27 ++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/yip_os/src/net/OpenShockClient.cpp b/yip_os/src/net/OpenShockClient.cpp index 5783aee..cabce8d 100644 --- a/yip_os/src/net/OpenShockClient.cpp +++ b/yip_os/src/net/OpenShockClient.cpp @@ -36,21 +36,26 @@ OpenShockClient::~OpenShockClient() { } bool OpenShockClient::FetchShockers() { - if (!HasToken()) { + if (!enabled_ || !HasConfig()) { + Logger::Debug("OpenShockClient: Skipping fetch (disabled or no token)"); shockers_.clear(); return true; } + Logger::Info("OpenShockClient: Fetching shockers"); + Logger::Info("OpenShockClient: Authenticating with Token"); shockers_.clear(); std::string response; // Fetch owned shockers + Logger::Info("OpenShockClient: Requesting own devices"); if (PerformGet(std::string(API_BASE) + "/1/shockers/own", response)) { ParseShockers(response, true); } // Fetch shared shockers response.clear(); + Logger::Info("OpenShockClient: Requesting shared devices"); if (PerformGet(std::string(API_BASE) + "/1/shockers/shared", response)) { ParseShockers(response, false); } @@ -79,6 +84,7 @@ bool OpenShockClient::ParseShockers(const std::string &json_str, : "Shocker"; s.name = "[" + hub_name + "] " + shocker_name; s.is_owned = is_owned; + s.backend = "openshock"; shockers_.push_back(s); } } @@ -94,7 +100,7 @@ bool OpenShockClient::ParseShockers(const std::string &json_str, bool OpenShockClient::SendControl(const std::string &shocker_id, const std::string &type, float intensity, int duration_ms) { - if (!HasToken()) { + if (!HasConfig()) { return false; } @@ -126,7 +132,8 @@ bool OpenShockClient::SendControl(const std::string &shocker_id, void OpenShockClient::SetToken(const std::string &token) { if (token_ != token) { token_ = token; - token_valid_ = false; // Reset validity when token changes + token_valid_ = false; + Logger::Info("OpenShockClient: Token updated"); } } @@ -206,6 +213,8 @@ bool OpenShockClient::PerformPost(const std::string &url, if (http_code >= 200 && http_code < 300) { token_valid_ = true; + Logger::Info("OpenShockClient: Command sent successfully (HTTP " + + std::to_string(http_code) + ")"); } else { if (http_code == 401) token_valid_ = false; diff --git a/yip_os/src/net/OpenShockClient.hpp b/yip_os/src/net/OpenShockClient.hpp index fdf7ca1..39fa621 100644 --- a/yip_os/src/net/OpenShockClient.hpp +++ b/yip_os/src/net/OpenShockClient.hpp @@ -10,6 +10,7 @@ #pragma once +#include "IShockClient.hpp" #include #include @@ -17,29 +18,28 @@ typedef void CURL; namespace YipOS { -struct Shocker { - std::string id; - std::string name; - bool is_owned = false; -}; - -class OpenShockClient { +class OpenShockClient : public IShockClient { public: OpenShockClient(); - ~OpenShockClient(); + ~OpenShockClient() override; void SetToken(const std::string &token); - bool HasToken() const { return !token_.empty(); } - bool IsTokenValid() const { return token_valid_; } + void SetEnabled(bool enabled) override { enabled_ = enabled; } + bool IsEnabled() const override { return enabled_; } + bool HasConfig() const override { return !token_.empty(); } + bool IsTokenValid() const override { return token_valid_; } // Fetch list of shockers (owned and shared) - bool FetchShockers(); - const std::vector &GetShockers() const { return shockers_; } + bool FetchShockers() override; + const std::vector &GetShockers() const override { return shockers_; } // Control shockers // type: "Shock", "Vibrate", "Sound", "Stop" bool SendControl(const std::string &shocker_id, const std::string &type, - float intensity, int duration_ms); + float intensity, int duration_ms) override; + + int GetMinDurationMs() const override { return 100; } + int GetMaxDurationMs() const override { return 30000; } private: bool ParseShockers(const std::string &json, bool is_owned); @@ -49,6 +49,7 @@ class OpenShockClient { CURL *curl_ = nullptr; std::string token_; bool token_valid_ = false; + bool enabled_ = true; std::vector shockers_; static constexpr const char *API_BASE = "https://api.openshock.app"; From de4a68098daaf7fad16fdec049eaa9733dc3f205 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:21:24 +0100 Subject: [PATCH 18/25] Update Screens to point to new unified shock screen --- yip_os/src/screens/Screen.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yip_os/src/screens/Screen.cpp b/yip_os/src/screens/Screen.cpp index 3b195cf..2fb5f98 100644 --- a/yip_os/src/screens/Screen.cpp +++ b/yip_os/src/screens/Screen.cpp @@ -39,7 +39,7 @@ #include "DMPairScreen.hpp" #include "DMComposeScreen.hpp" #include "DMMessageScreen.hpp" -#include "OpenShockScreen.hpp" +#include "ShockScreen.hpp" #include "app/PDAController.hpp" #include "app/PDADisplay.hpp" #include "core/Glyphs.hpp" @@ -182,7 +182,7 @@ std::unique_ptr CreateScreen(const std::string& name, PDAController& pda {"DM_PAIR", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, {"DM_COMPOSE", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, {"DM_MSG", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, - {"SHOCK", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, + {"SHOCK", [](PDAController& p) -> std::unique_ptr { return std::make_unique(p); }}, }; auto it = registry.find(name); From d879d5af47796ea90c2ef8c911f67d2e260d34b6 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:25:29 +0100 Subject: [PATCH 19/25] Add new PiShockClient --- yip_os/src/net/PiShockClient.cpp | 383 +++++++++++++++++++++++++++++++ yip_os/src/net/PiShockClient.hpp | 61 +++++ 2 files changed, 444 insertions(+) create mode 100644 yip_os/src/net/PiShockClient.cpp create mode 100644 yip_os/src/net/PiShockClient.hpp diff --git a/yip_os/src/net/PiShockClient.cpp b/yip_os/src/net/PiShockClient.cpp new file mode 100644 index 0000000..7334d7e --- /dev/null +++ b/yip_os/src/net/PiShockClient.cpp @@ -0,0 +1,383 @@ +/** + * PiShockClient.cpp + * V1.0.0 + * + * Adds PiShock API integration to YipOS for remote control of PiShock + * devices. + */ + +#include "PiShockClient.hpp" +#include "core/Logger.hpp" +#include +#include +#include + +namespace YipOS { + +using json = nlohmann::json; + +static size_t WriteCallback(void *contents, size_t size, size_t nmemb, + void *userp) { + auto *str = static_cast(userp); + str->append(static_cast(contents), size * nmemb); + return size * nmemb; +} + +PiShockClient::PiShockClient() { + curl_global_init(CURL_GLOBAL_ALL); + curl_ = curl_easy_init(); +} + +PiShockClient::~PiShockClient() { + if (curl_) + curl_easy_cleanup(curl_); + curl_global_cleanup(); +} + +void PiShockClient::SetCredentials(const std::string &username, + const std::string &apikey) { + if (username_ != username || apikey_ != apikey) { + username_ = username; + apikey_ = apikey; + token_valid_ = false; + } +} + +std::string PiShockClient::URLEncode(const std::string &value) { + if (!curl_) + return value; + + char *output = + curl_easy_escape(curl_, value.c_str(), static_cast(value.length())); + if (output) { + std::string result(output); + curl_free(output); + return result; + } + return value; +} + +bool PiShockClient::FetchShockers() { + if (!enabled_ || !HasConfig()) { + Logger::Debug("PiShockClient: Skipping fetch (disabled or no credentials)"); + shockers_.clear(); + return true; + } + + Logger::Info("PiShockClient: Fetching shockers"); + + shockers_.clear(); + std::string response; + + // 1. Resolve Username to Numeric UserId + // Following doc: + // https://auth.pishock.com/Auth/GetUserIfAPIKeyValid?apikey={apikey}&username={username} + std::string encoded_user = URLEncode(username_); + std::string encoded_key = URLEncode(apikey_); + + std::string auth_url = + "https://auth.pishock.com/Auth/GetUserIfAPIKeyValid?apikey=" + + encoded_key + "&username=" + encoded_user; + + std::string user_id = ""; + if (PerformGet(auth_url, response)) { + Logger::Debug("PiShockClient: Raw Auth Response: " + response); + + try { + auto j = json::parse(response); + + // Check for both "UserId" and "UserID" + if (j.contains("UserId") && !j["UserId"].is_null()) { + user_id = j["UserId"].is_number() + ? std::to_string(j["UserId"].get()) + : j["UserId"].get(); + } else if (j.contains("UserID") && !j["UserID"].is_null()) { + user_id = j["UserID"].is_number() + ? std::to_string(j["UserID"].get()) + : j["UserID"].get(); + } + } catch (const std::exception &e) { + Logger::Warning("PiShockClient: JSON parse error: " + + std::string(e.what())); + } + } + + // If the ID is "0" or empty, the API Key/Username combo is likely wrong + if (user_id.empty() || user_id == "0") { + Logger::Error( + "PiShockClient: Auth failed. Check logs for Raw Auth Response."); + token_valid_ = false; + return false; + } + + Logger::Info("PiShockClient: Authenticating with UserID: " + user_id); + bool any_success = false; + + // Step 2: Fetch owned shockers + response.clear(); + Logger::Info("PiShockClient: Requesting own devices"); + std::string own_url = + "https://ps.pishock.com/PiShock/GetUserDevices?UserId=" + user_id + + "&Token=" + encoded_key + "&api=true"; + if (PerformGet(own_url, response)) { + if (ParseUserDevices(response)) + any_success = true; + } + + // Step 3: Fetch share codes + response.clear(); + Logger::Info("PiShockClient: Requesting shared devices"); + std::string codes_url = + "https://ps.pishock.com/PiShock/GetShareCodesByOwner?UserId=" + user_id + + "&Token=" + encoded_key + "&api=true"; + + if (PerformGet(codes_url, response)) { + Logger::Debug("PiShockClient: Raw Share Codes Response: " + response); + + std::vector codes; + try { + auto j = json::parse(response); + + // If it's an object like {"Name": [12345]} + if (j.is_object()) { + for (auto &element : j.items()) { + if (element.value().is_array()) { + for (const auto &code : element.value()) { + if (code.is_number()) { + codes.push_back(std::to_string(code.get())); + } else if (code.is_string()) { + codes.push_back(code.get()); + } + } + } + } + } + // Fallback for standard array format + else if (j.is_array()) { + for (const auto &item : j) { + if (item.is_string()) + codes.push_back(item.get()); + else if (item.is_object() && item.contains("Code")) { + codes.push_back(item["Code"].is_string() + ? item["Code"].get() + : std::to_string(item["Code"].get())); + } + } + } + } catch (const std::exception &e) { + Logger::Warning("PiShockClient: Error parsing share codes: " + + std::string(e.what())); + } + + if (!codes.empty()) { + // Step 4: Resolve share codes to device info + std::string resolve_url = + "https://ps.pishock.com/PiShock/GetShockersByShareIds?UserId=" + + user_id + "&Token=" + encoded_key + "&api=true"; + for (const auto &code : codes) { + resolve_url += "&shareIds=" + URLEncode(code); + } + + response.clear(); + if (PerformGet(resolve_url, response)) { + Logger::Debug("PiShockClient: Shared Devices Response: " + response); + if (ParseSharedDevices(response)) + any_success = true; + } + } + } + + token_valid_ = any_success; + Logger::Debug("PiShockClient: Fetched " + std::to_string(shockers_.size()) + + " shockers"); + return any_success; +} + +bool PiShockClient::ParseUserDevices(const std::string &json_str) { + try { + auto j = json::parse(json_str); + if (!j.is_array()) + return false; + + for (const auto &hub : j) { + std::string hub_name = + hub.contains("Name") ? hub["Name"].get() : "Hub"; + if (hub.contains("Shockers") && hub["Shockers"].is_array()) { + for (const auto &item : hub["Shockers"]) { + Shocker s; + s.id = item.contains("Code") ? item["Code"].get() : ""; + s.name = "[" + hub_name + "] " + + (item.contains("Name") ? item["Name"].get() + : "Shocker"); + s.is_owned = true; + s.backend = "pishock"; + if (!s.id.empty()) + shockers_.push_back(s); + } + } + } + return true; + } catch (const std::exception &e) { + Logger::Warning("PiShockClient: JSON parse error (user devices): " + + std::string(e.what())); + return false; + } +} + +bool PiShockClient::ParseSharedDevices(const std::string &json_str) { + try { + auto j = json::parse(json_str); + + if (!j.is_object()) + return false; + + bool found_any = false; + + // Iterate through the keys + for (auto &user_entry : j.items()) { + if (user_entry.value().is_array()) { + for (const auto &item : user_entry.value()) { + Shocker s; + + if (item.contains("shareCode")) { + s.id = item["shareCode"].get(); + } else if (item.contains("shareId")) { + s.id = item["shareId"].is_number() + ? std::to_string(item["shareId"].get()) + : item["shareId"].get(); + } + + s.name = item.contains("shockerName") + ? item["shockerName"].get() + : "Shared Shocker"; + s.is_owned = false; + s.backend = "pishock"; + + if (!s.id.empty()) { + shockers_.push_back(s); + found_any = true; + } + } + } + } + return found_any; + } catch (const std::exception &e) { + Logger::Warning("PiShockClient: JSON parse error (shared devices): " + + std::string(e.what())); + return false; + } +} + +bool PiShockClient::SendControl(const std::string &shocker_id, + const std::string &type, float intensity, + int duration_ms) { + if (!HasConfig()) + return false; + + // 0=Shock, 1=Vibrate, 2=Sound + int type_int = 1; + if (type.find("SHOCK") != std::string::npos) + type_int = 0; + else if (type.find("VIBE") != std::string::npos) + type_int = 1; + else if (type.find("SOUND") != std::string::npos) + type_int = 2; + + // PiShock requires duration in seconds (1-15 range) + int duration_s = std::clamp(static_cast(duration_ms / 1000), 1, 15); + int intensity_int = std::clamp(static_cast(intensity), 1, 100); + + json payload = {{"Username", username_}, // apioperate uses Username string + {"Apikey", apikey_}, {"Code", shocker_id}, + {"Name", "YipOS"}, {"Op", type_int}, + {"Duration", duration_s}, {"Intensity", intensity_int}}; + + Logger::Info("PiShock: Sending " + type + " (" + + std::to_string(intensity_int) + "%, " + + std::to_string(duration_s) + "s) to " + shocker_id); + + return PerformPost("https://do.pishock.com/api/apioperate/", payload.dump()); +} + +bool PiShockClient::PerformGet(const std::string &url, std::string &response) { + if (!curl_) + return false; + + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl_, CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L); + + CURLcode res = curl_easy_perform(curl_); + + if (res != CURLE_OK) { + Logger::Warning("PiShockClient: GET failed: " + + std::string(curl_easy_strerror(res))); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code < 200 || http_code >= 300) { + Logger::Warning("PiShockClient: GET " + url + " failed (HTTP " + + std::to_string(http_code) + ")"); + return false; + } + + return true; +} + +bool PiShockClient::PerformPost(const std::string &url, + const std::string &payload) { + if (!curl_) + return false; + + std::string response; + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, payload.c_str()); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl_, CURLOPT_USERAGENT, USER_AGENT); + + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl_); + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + Logger::Warning("PiShockClient: POST failed: " + + std::string(curl_easy_strerror(res))); + return false; + } + + // Handle PiShock-specific "Not Authorized" within HTTP 200 response + if (response.find("Not Authorized") != std::string::npos) { + token_valid_ = false; + Logger::Warning("PiShockClient: Auth failed (Not Authorized)"); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code >= 200 && http_code < 300) { + token_valid_ = true; + Logger::Info("PiShockClient: Command sent successfully (HTTP " + + std::to_string(http_code) + ")"); + return true; + } + + Logger::Warning("PiShockClient: POST failed (HTTP " + + std::to_string(http_code) + ")"); + return false; +} + +} // namespace YipOS \ No newline at end of file diff --git a/yip_os/src/net/PiShockClient.hpp b/yip_os/src/net/PiShockClient.hpp new file mode 100644 index 0000000..bd20545 --- /dev/null +++ b/yip_os/src/net/PiShockClient.hpp @@ -0,0 +1,61 @@ +/** + * PiShockClient.hpp + * V1.0.0 + * + * Adds PiShock API integration to YipOS for remote control of PiShock + * devices. + * + * By otter_oasis + */ + +#pragma once + +#include "IShockClient.hpp" +#include +#include + +typedef void CURL; + +namespace YipOS { + +class PiShockClient : public IShockClient { +public: + PiShockClient(); + ~PiShockClient() override; + + void SetCredentials(const std::string &username, const std::string &apikey); + void SetEnabled(bool enabled) override { enabled_ = enabled; } + bool IsEnabled() const override { return enabled_; } + bool HasConfig() const override { + return !username_.empty() && !apikey_.empty(); + } + bool IsTokenValid() const override { return token_valid_; } + + bool FetchShockers() override; + const std::vector &GetShockers() const override { return shockers_; } + + bool SendControl(const std::string &shocker_id, const std::string &type, + float intensity, int duration_ms) override; + + int GetMinDurationMs() const override { return 1000; } + int GetMaxDurationMs() const override { return 15000; } + +private: + bool PerformPost(const std::string &url, const std::string &payload); + bool PerformGet(const std::string &url, std::string &response); + bool ParseUserDevices(const std::string &json_str); + bool ParseSharedDevices(const std::string &json_str); + + std::string URLEncode(const std::string &value); + + CURL *curl_ = nullptr; + std::string username_; + std::string apikey_; + bool token_valid_ = false; + bool enabled_ = true; + std::vector shockers_; + + static constexpr const char *USER_AGENT = "YipOS/1.0"; +}; + +} // namespace YipOS From 3bb2aed02c6f2ffd698526115451b86bcb880cd2 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:33:31 +0100 Subject: [PATCH 20/25] Initialize new ShockManager --- yip_os/src/app/PDAController.cpp | 15 +++++---------- yip_os/src/app/PDAController.hpp | 10 +++++----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/yip_os/src/app/PDAController.cpp b/yip_os/src/app/PDAController.cpp index 28dfe66..922daf9 100644 --- a/yip_os/src/app/PDAController.cpp +++ b/yip_os/src/app/PDAController.cpp @@ -12,7 +12,7 @@ #include "net/TwitchClient.hpp" #include "media/MediaController.hpp" #include "platform/SystemStats.hpp" -#include "net/OpenShockClient.hpp" +#include "net/ShockManager.hpp" #include "core/Glyphs.hpp" #include "core/Config.hpp" #include "core/Logger.hpp" @@ -98,14 +98,9 @@ PDAController::PDAController(PDADisplay& display, NetTracker& net_tracker, Confi } } - // Initialize OpenShock client - openshock_client_ = std::make_unique(); - std::string os_token = config_.GetState("openshock.token"); - std::string os_enabled = config_.GetState("openshock.enabled", "0"); - if (os_enabled != "0" && !os_token.empty()) { - openshock_client_->SetToken(os_token); - openshock_client_->FetchShockers(); - } + // Initialize shock manager + shock_manager_ = std::make_unique(); + shock_manager_->InitFromConfig(config_); // Push home screen as root auto home = std::make_unique(*this); @@ -829,4 +824,4 @@ void PDAController::RefreshStockCache() { stock_client_->FetchAll(window); } -} // namespace YipOS +} // namespace YipOS \ No newline at end of file diff --git a/yip_os/src/app/PDAController.hpp b/yip_os/src/app/PDAController.hpp index 92b739d..35b93f6 100644 --- a/yip_os/src/app/PDAController.hpp +++ b/yip_os/src/app/PDAController.hpp @@ -34,7 +34,7 @@ class StockClient; class TwitchClient; struct TwitchMessage; class TranslationWorker; -class OpenShockClient; +class ShockManager; class PDAController { public: @@ -142,8 +142,8 @@ class PDAController { void RefreshChatCache(); void MarkChatSeen(); - // OpenShock integration - OpenShockClient* GetOpenShockClient() { return openshock_client_.get(); } + // OpenShock & PiShock integration + ShockManager* GetShockManager() { return shock_manager_.get(); } // Hard lock (full LOCK screen from home tile) void SetLocked(bool locked); @@ -235,7 +235,7 @@ class PDAController { std::unique_ptr stock_client_; std::unique_ptr twitch_client_; const TwitchMessage* selected_twitch_ = nullptr; - std::unique_ptr openshock_client_; + std::unique_ptr shock_manager_; std::string assets_path_; std::unique_ptr dm_notify_sound_; bool prev_has_unseen_dm_ = false; @@ -297,4 +297,4 @@ class PDAController { std::array, SPVR_DEVICE_COUNT> spvr_status_{}; }; -} // namespace YipOS +} // namespace YipOS \ No newline at end of file From 66fd2d4f56ed8e5d83e951bb5aac98c469340067 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:35:53 +0100 Subject: [PATCH 21/25] Change UIManager entries to hold Shock tab states --- yip_os/src/ui/UIManager.cpp | 2 +- yip_os/src/ui/UIManager.hpp | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/yip_os/src/ui/UIManager.cpp b/yip_os/src/ui/UIManager.cpp index a0853bc..8797e9b 100644 --- a/yip_os/src/ui/UIManager.cpp +++ b/yip_os/src/ui/UIManager.cpp @@ -197,7 +197,7 @@ void UIManager::Render(PDAController& pda, Config& config, OSCManager& osc) { { static const char* tab_labels[] = { "Status", "OSC", "Display", "VRCX", "CC", "INTRP", "Avatar", - "Text", "IMG", "Stocks", "Twitch", "DM", "OpenShock", "NVRAM", "Log" + "Text", "IMG", "Stocks", "Twitch", "DM", "Shock", "NVRAM", "Log" }; static constexpr int TAB_COUNT = 15; static constexpr int ROW1_COUNT = 7; diff --git a/yip_os/src/ui/UIManager.hpp b/yip_os/src/ui/UIManager.hpp index 9153f82..9e02bb1 100644 --- a/yip_os/src/ui/UIManager.hpp +++ b/yip_os/src/ui/UIManager.hpp @@ -112,12 +112,16 @@ class UIManager { std::array dm_join_code_buf_ = {}; std::unordered_map> dm_compose_bufs_; - // OpenShock tab state + // Shock tab state (OpenShock & PiShock) bool openshock_enabled_ = false; std::array openshock_token_buf_ = {}; bool openshock_token_initialized_ = false; float openshock_intensity_step_ = 2.5f; int openshock_duration_step_ = 100; + bool pishock_enabled_ = false; + std::array pishock_username_buf_ = {}; + std::array pishock_apikey_buf_ = {}; + bool pishock_initialized_ = false; // OSC Query server (optional, for status display) OSCQueryServer* osc_query_ = nullptr; @@ -136,4 +140,4 @@ class UIManager { int initial_height_ = 480; }; -} // namespace YipOS +} // namespace YipOS \ No newline at end of file From 815488433f71bec2b94fd01f6dd1328a570b9237 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:36:20 +0100 Subject: [PATCH 22/25] Add new Shock files and PiShockClient to build --- yip_os/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yip_os/CMakeLists.txt b/yip_os/CMakeLists.txt index 5a54aaf..2e4fd66 100644 --- a/yip_os/CMakeLists.txt +++ b/yip_os/CMakeLists.txt @@ -148,9 +148,11 @@ set(YIPOS_SOURCES src/screens/DMPairScreen.cpp src/screens/DMComposeScreen.cpp src/screens/DMMessageScreen.cpp - src/screens/OpenShockScreen.cpp + src/screens/ShockScreen.cpp src/net/DMClient.cpp src/net/OpenShockClient.cpp + src/net/PiShockClient.cpp + src/net/ShockManager.cpp src/img/QRGen.cpp src/translate/TranslationWorker.cpp src/img/VQEncoder.cpp @@ -166,7 +168,7 @@ set(YIPOS_SOURCES src/ui/UIManager_Data.cpp src/ui/UIManager_Config.cpp src/ui/UIManager_DM.cpp - src/ui/UIManager_OpenShock.cpp + src/ui/UIManager_Shock.cpp ) # Platform-specific sources From 7d892434912132c7e8ff49c9f9c6f3199008c1a3 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 15:50:47 +0100 Subject: [PATCH 23/25] Generalize Shock tab name function --- yip_os/src/ui/UIManager.cpp | 2 +- yip_os/src/ui/UIManager.hpp | 2 +- yip_os/src/ui/UIManager_Shock.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/yip_os/src/ui/UIManager.cpp b/yip_os/src/ui/UIManager.cpp index 8797e9b..c1fa272 100644 --- a/yip_os/src/ui/UIManager.cpp +++ b/yip_os/src/ui/UIManager.cpp @@ -243,7 +243,7 @@ void UIManager::Render(PDAController& pda, Config& config, OSCManager& osc) { case 9: RenderStocksTab(pda, config); break; case 10: RenderTwitchTab(pda, config); break; case 11: RenderDMTab(pda, config); break; - case 12: RenderOpenShockTab(pda, config); break; + case 12: RenderShockTab(pda, config); break; case 13: RenderNVRAMTab(pda, config); break; case 14: RenderLogTab(); break; } diff --git a/yip_os/src/ui/UIManager.hpp b/yip_os/src/ui/UIManager.hpp index 9e02bb1..57b9d2d 100644 --- a/yip_os/src/ui/UIManager.hpp +++ b/yip_os/src/ui/UIManager.hpp @@ -57,7 +57,7 @@ class UIManager { void RenderTwitchTab(PDAController& pda, Config& config); void RenderIMGTab(PDAController& pda, Config& config); void RenderDMTab(PDAController& pda, Config& config); - void RenderOpenShockTab(PDAController& pda, Config& config); + void RenderShockTab(PDAController& pda, Config& config); void RenderNVRAMTab(PDAController& pda, Config& config); void RenderLogTab(); diff --git a/yip_os/src/ui/UIManager_Shock.cpp b/yip_os/src/ui/UIManager_Shock.cpp index 18ceec9..be6f46c 100644 --- a/yip_os/src/ui/UIManager_Shock.cpp +++ b/yip_os/src/ui/UIManager_Shock.cpp @@ -21,7 +21,7 @@ namespace YipOS { -void UIManager::RenderOpenShockTab(PDAController &pda, Config &config) { +void UIManager::RenderShockTab(PDAController &pda, Config &config) { ImGui::Text("Shocker Integration"); ImGui::TextDisabled( "Drive your PiShock & OpenShock devices directly from the Yip-Boi."); From 52cb1113cc5f5a01c703e1761433b140ddc58b91 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Thu, 9 Apr 2026 18:36:59 +0100 Subject: [PATCH 24/25] Rename variables to match new name, update description of module and add footer --- yip_os/src/net/OpenShockClient.cpp | 1 - yip_os/src/screens/ShockScreen.cpp | 4 ++-- yip_os/src/ui/UIManager_Shock.cpp | 18 ++++++++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/yip_os/src/net/OpenShockClient.cpp b/yip_os/src/net/OpenShockClient.cpp index cabce8d..650793a 100644 --- a/yip_os/src/net/OpenShockClient.cpp +++ b/yip_os/src/net/OpenShockClient.cpp @@ -133,7 +133,6 @@ void OpenShockClient::SetToken(const std::string &token) { if (token_ != token) { token_ = token; token_valid_ = false; - Logger::Info("OpenShockClient: Token updated"); } } diff --git a/yip_os/src/screens/ShockScreen.cpp b/yip_os/src/screens/ShockScreen.cpp index f7fa6c6..11c4845 100644 --- a/yip_os/src/screens/ShockScreen.cpp +++ b/yip_os/src/screens/ShockScreen.cpp @@ -176,11 +176,11 @@ bool ShockScreen::OnInput(const std::string &key) { float i_step = 2.5f; int d_step = 1000; try { - i_step = std::stof(config.GetState("openshock.intensity_step", "2.5")); + i_step = std::stof(config.GetState("shock.intensity_step", "2.5")); } catch (...) { } try { - d_step = std::stoi(config.GetState("openshock.duration_step", "1000")); + d_step = std::stoi(config.GetState("shock.duration_step", "1000")); } catch (...) { } diff --git a/yip_os/src/ui/UIManager_Shock.cpp b/yip_os/src/ui/UIManager_Shock.cpp index be6f46c..ca9000c 100644 --- a/yip_os/src/ui/UIManager_Shock.cpp +++ b/yip_os/src/ui/UIManager_Shock.cpp @@ -24,13 +24,13 @@ namespace YipOS { void UIManager::RenderShockTab(PDAController &pda, Config &config) { ImGui::Text("Shocker Integration"); ImGui::TextDisabled( - "Drive your PiShock & OpenShock devices directly from the Yip-Boi."); + "Drive your PiShock & OpenShock devices directly from the PDA."); ImGui::Spacing(); ImGui::TextDisabled("Warning: Using shocking devices is at your own risk."); ImGui::TextDisabled("Use responsibly and follow all safety guidelines."); ImGui::TextDisabled( - "Remember: If other people can interact with your yip-boi, they can " + "Remember: If other people can interact with your PDA, they can " "control your shocks."); ImGui::Separator(); @@ -65,12 +65,12 @@ void UIManager::RenderShockTab(PDAController &pda, Config &config) { std::snprintf(pishock_apikey_buf_.data(), pishock_apikey_buf_.size(), "%s", ps_api.c_str()); - std::string i_step = config.GetState("openshock.intensity_step", "2.5"); + std::string i_step = config.GetState("shock.intensity_step", "2.5"); try { openshock_intensity_step_ = std::stof(i_step); } catch (...) { } - std::string d_step = config.GetState("openshock.duration_step", "1000"); + std::string d_step = config.GetState("shock.duration_step", "1000"); try { openshock_duration_step_ = std::stoi(d_step); } catch (...) { @@ -181,9 +181,9 @@ void UIManager::RenderShockTab(PDAController &pda, Config &config) { config.SetState("openshock.enabled", openshock_enabled_ ? "1" : "0"); config.SetState("openshock.token", os_token); - config.SetState("openshock.intensity_step", + config.SetState("shock.intensity_step", std::to_string(openshock_intensity_step_)); - config.SetState("openshock.duration_step", + config.SetState("shock.duration_step", std::to_string(openshock_duration_step_)); config.SetState("pishock.enabled", pishock_enabled_ ? "1" : "0"); @@ -216,6 +216,12 @@ void UIManager::RenderShockTab(PDAController &pda, Config &config) { Logger::Warning("ShockManager: No shockers available to test."); } } + + // --- Footer --- + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextDisabled("Module by @otter_oasis"); } } // namespace YipOS From 2758c5fdcd567aa08a4e34e0578b30ca1c491720 Mon Sep 17 00:00:00 2001 From: Steven Wheeler Date: Fri, 10 Apr 2026 12:11:58 +0100 Subject: [PATCH 25/25] Tidy up variables and rename from openshock to shock --- yip_os/src/ui/UIManager.hpp | 6 ++++-- yip_os/src/ui/UIManager_Shock.cpp | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/yip_os/src/ui/UIManager.hpp b/yip_os/src/ui/UIManager.hpp index 57b9d2d..f9373f5 100644 --- a/yip_os/src/ui/UIManager.hpp +++ b/yip_os/src/ui/UIManager.hpp @@ -116,13 +116,15 @@ class UIManager { bool openshock_enabled_ = false; std::array openshock_token_buf_ = {}; bool openshock_token_initialized_ = false; - float openshock_intensity_step_ = 2.5f; - int openshock_duration_step_ = 100; + bool pishock_enabled_ = false; std::array pishock_username_buf_ = {}; std::array pishock_apikey_buf_ = {}; bool pishock_initialized_ = false; + float shock_intensity_step_ = 2.5f; + int shock_duration_step_ = 100; + // OSC Query server (optional, for status display) OSCQueryServer* osc_query_ = nullptr; diff --git a/yip_os/src/ui/UIManager_Shock.cpp b/yip_os/src/ui/UIManager_Shock.cpp index ca9000c..424622c 100644 --- a/yip_os/src/ui/UIManager_Shock.cpp +++ b/yip_os/src/ui/UIManager_Shock.cpp @@ -67,12 +67,12 @@ void UIManager::RenderShockTab(PDAController &pda, Config &config) { std::string i_step = config.GetState("shock.intensity_step", "2.5"); try { - openshock_intensity_step_ = std::stof(i_step); + shock_intensity_step_ = std::stof(i_step); } catch (...) { } std::string d_step = config.GetState("shock.duration_step", "1000"); try { - openshock_duration_step_ = std::stoi(d_step); + shock_duration_step_ = std::stoi(d_step); } catch (...) { } @@ -149,10 +149,10 @@ void UIManager::RenderShockTab(PDAController &pda, Config &config) { ImGui::TextDisabled( "Step sizes used by wrist buttons for all shock devices."); ImGui::SetNextItemWidth(150); - ImGui::InputFloat("Intensity Step (%)", &openshock_intensity_step_, 0.5f, + ImGui::InputFloat("Intensity Step (%)", &shock_intensity_step_, 0.5f, 5.0f, "%.1f"); ImGui::SetNextItemWidth(150); - ImGui::InputInt("Duration Step (ms)", &openshock_duration_step_, 100, 1000); + ImGui::InputInt("Duration Step (ms)", &shock_duration_step_, 100, 1000); ImGui::Spacing(); ImGui::Separator(); @@ -182,9 +182,9 @@ void UIManager::RenderShockTab(PDAController &pda, Config &config) { config.SetState("openshock.enabled", openshock_enabled_ ? "1" : "0"); config.SetState("openshock.token", os_token); config.SetState("shock.intensity_step", - std::to_string(openshock_intensity_step_)); + std::to_string(shock_intensity_step_)); config.SetState("shock.duration_step", - std::to_string(openshock_duration_step_)); + std::to_string(shock_duration_step_)); config.SetState("pishock.enabled", pishock_enabled_ ? "1" : "0"); config.SetState("pishock.username", ps_user);