From 984073c1b3a264fc2d552afdff70a4de591751cf Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sun, 5 Dec 2021 08:46:41 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + include/README | 39 +++++++ lib/README | 46 ++++++++ platformio.ini | 22 ++++ requirements.txt | 3 + scene_is_currently_visible | 72 ++++++++++++ src/main.cpp | 228 +++++++++++++++++++++++++++++++++++++ test/README | 11 ++ 8 files changed, 422 insertions(+) create mode 100644 .gitignore create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 requirements.txt create mode 100755 scene_is_currently_visible create mode 100644 src/main.cpp create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03f4a3c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.pio diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..67622ee --- /dev/null +++ b/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:default] +board = nodemcuv2 +framework = arduino +monitor_speed = 115200 +platform = espressif8266 +upload_protocol = esptool +lib_deps = + bblanchon/ArduinoJson@^6.17.3 + fastled/FastLED@^3.4.0 + rweather/Crypto@^0.2.0 + agdl/Base64@^1.0.0 + links2004/WebSockets@^2.3.6 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d1df39c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +obs-websocket-py==0.5.3 +six==1.16.0 +websocket-client==1.2.1 diff --git a/scene_is_currently_visible b/scene_is_currently_visible new file mode 100755 index 0000000..35cf5c9 --- /dev/null +++ b/scene_is_currently_visible @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +from sys import argv +import logging +import time + +from obswebsocket import events, obsws + +logging.basicConfig( + format="%(asctime)s %(name)s [%(levelname)s] %(message)s", + level=logging.INFO, +) + +log = logging.getLogger(__name__) + +host = "localhost" +port = 4444 +password = "12345" + +my_source_name = argv[1] +my_source_is_program = False +my_source_is_preview = False + + +def on_event(message): + global my_source_is_program, my_source_is_preview + + my_source_in_current_event = False + + if isinstance(message, events.SwitchScenes): + visibility = "PROGRAM" + elif isinstance(message, events.PreviewSceneChanged): + visibility = "PREVIEW" + else: + return + + for source in message.datain["sources"]: + log.debug(f'visibility of scene {message.datain["scene-name"]} changed, {source["name"]} is now {visibility}') + if source["name"] == my_source_name: + my_source_in_current_event = True + + if my_source_in_current_event: + if visibility == "PROGRAM": + my_source_is_program = True + else: + my_source_is_preview = True + else: + if visibility == "PROGRAM": + my_source_is_program = False + else: + my_source_is_preview = False + + if my_source_is_program: + log.info(f"{my_source_name} is PROGRAM (and maybe preview)") + elif my_source_is_preview: + log.info(f"{my_source_name} is PREVIEW only") + else: + log.info(f"{my_source_name} is not visible") + + +ws = obsws(host, port, password) +ws.register(on_event) +ws.connect() + +try: + log.info("Waiting ...") + time.sleep(3600) + log.warn("Timeout!") +except KeyboardInterrupt: + pass + +ws.disconnect() diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c683155 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,228 @@ +#include +#include +#include +#include +#include +#include +#include + +// BEGIN CONFIGURATION + +#define LED_COUNT 19 +#define LED_BRIGHTNESS 100 +#define LED_PIN D4 + +#define WIFI_SSID "" +#define WIFI_PASS "" + +#define OBS_HOST "" +#define OBS_PORT 4444 +#define OBS_PASS "" + +#define OBS_SOURCE "" + +// END CONFIGURATION + +#define HASH_SIZE 32 + +CRGB leds[LED_COUNT]; +WebSocketsClient webSocket; +SHA256 sha256; + +bool is_currently_live = false; +bool is_currently_preview = false; + +void set_program() { + fill_solid(leds, LED_COUNT, CRGB::Red); + FastLED.show(); +} + +void set_preview() { + fill_solid(leds, LED_COUNT, CRGB::Green); + FastLED.show(); +} + +void set_idle() { + fill_solid(leds, LED_COUNT, CRGB::Black); + FastLED.show(); +} + +void set_error() { + fill_solid(leds, LED_COUNT, CRGB::Purple); + FastLED.show(); +} + +void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + Serial.println("[WS] disconnected"); + /* + We do NOT set the LEDs to off in here on purpose. Maybe + just the wifi connection was interrupted or something + like that. It is important we don't disable the LEDs, in + case the OBS instance is still running. + + We do, however, set the LEDs to the "error" state, to + indicate we don't currently know what OBS is showing. + */ + set_error(); + break; + + case WStype_CONNECTED: + Serial.printf("[WS] connected to %s\n", payload); + // Find out if we need authentication + webSocket.sendTXT("{\"request-type\":\"GetAuthRequired\",\"message-id\":\"1\"}"); + break; + + case WStype_TEXT: { + Serial.printf("[WS] %s", payload); + + StaticJsonDocument<5000> doc; + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + Serial.print("deserializeJson() failed: "); + Serial.println(error.c_str()); + break; + } + + if (doc.containsKey("authRequired")) { + Serial.println("[OBS] auth requested"); + + sha256.reset(); + sha256.update(OBS_PASS, strlen(OBS_PASS)); + const char* salt = doc["salt"]; + sha256.update(salt, strlen(salt)); + char value[HASH_SIZE]; + sha256.finalize(value, HASH_SIZE); + + Serial.print("[OBS] sha256 authentication hash is: "); + for (size_t i = 0; i < HASH_SIZE; i++) { + Serial.print(static_cast(value[i]), HEX); + } + Serial.println(); + + int encodedLength = Base64.encodedLength(HASH_SIZE); + char encodedPassSaltHash[encodedLength]; + Base64.encode(encodedPassSaltHash, value, HASH_SIZE); + + const char* challenge = doc["challenge"]; + sha256.reset(); + sha256.update(encodedPassSaltHash, encodedLength); + sha256.update(challenge, strlen(challenge)); + sha256.finalize(value, HASH_SIZE); + + Serial.print("[OBS] sha256 challenge hash is: "); + for (size_t i = 0; i < HASH_SIZE; i++) { + Serial.print(static_cast(value[i]), HEX); + } + Serial.println(); + + char encodedAuthString[encodedLength]; + Base64.encode(encodedAuthString, value, HASH_SIZE); + + String authRequest = String("{\"request-type\":\"Authenticate\",\"message-id\":\"2\",\"auth\":\"") + encodedAuthString + "\"}"; + webSocket.sendTXT(authRequest); + break; + + } else if (doc.containsKey("message-id") && (doc["message-id"]) == "2") { + if (strcmp(doc["status"], "ok") == 0) { + Serial.println("[OBS] authentication successful"); + webSocket.sendTXT("{\"request-type\":\"GetCurrentScene\",\"message-id\":\"3\"}"); + webSocket.sendTXT("{\"request-type\":\"GetPreviewScene\",\"message-id\":\"4\"}"); + } else { + Serial.println("Authenticated FAILED"); + set_error(); + } + break; + } else if (doc.containsKey("update-type")) { + bool my_source_in_current_event = false; + + for (uint8_t i = 0; i < doc["sources"].size(); i++) { + if (strcmp(doc["sources"][i]["name"], OBS_SOURCE) == 0) { + my_source_in_current_event = true; + } + } + + if (strcmp(doc["update-type"], "SwitchScenes") == 0 || strcmp(doc["update-type"], "GetCurrentScene") == 0) { + if (my_source_in_current_event) { + is_currently_live = true; + } else { + is_currently_live = false; + } + } else if (strcmp(doc["update-type"], "PreviewSceneChanged") == 0 || strcmp(doc["update-type"], "GetPreviewScene") == 0) { + if (my_source_in_current_event) { + is_currently_preview = true; + } else { + is_currently_preview = false; + } + } else if (strcmp(doc["update-type"], "Exiting") == 0) { + is_currently_preview = false; + is_currently_live = false; + } else if (strcmp(doc["update-type"], "StudioModeSwitched") == 0 && doc["new-state"] == false) { + is_currently_preview = false; + } + } + + if (is_currently_live) { + set_program(); + } else if (is_currently_preview) { + set_preview(); + } else { + set_idle(); + } + } + + case WStype_BIN: + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + case WStype_PING: + case WStype_PONG: + break; + } +} + +void setup() { + Serial.begin(115200); + + Serial.println("[Tally] starting up"); + + FastLED.addLeds(leds, LED_COUNT); + FastLED.setBrightness(LED_BRIGHTNESS); + + set_error(); + delay(500); + set_idle(); + delay(500); + set_error(); + delay(500); + set_idle(); + delay(200); + + Serial.printf("[Tally] connecting to wifi ssid: %s\n", WIFI_SSID); + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASS); + + while(WiFi.status() != WL_CONNECTED) { + delay(250); + Serial.print("."); + } + Serial.println(); + Serial.print("[Tally] connected to wifi, ip address"); + Serial.println(WiFi.localIP()); + + Serial.println("[Tally] connecting to OBS"); + webSocket.begin(OBS_HOST, OBS_PORT, "/"); + webSocket.onEvent(handleWebSocketEvent); + webSocket.setReconnectInterval(2000); + + Serial.println("[Tally] startup complete"); +} + +void loop() { + webSocket.loop(); +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html