various fixes, improvements, documentation

This commit is contained in:
Franzi 2021-12-12 14:22:30 +01:00
parent 300ca56d3b
commit 3818e5b6c3
Signed by: kunsi
GPG Key ID: 12E3D2136B818350
4 changed files with 179 additions and 55 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.pio
include/config.h

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# OBS-WebSocket Tally Light
Quick Facts:
* runs on ESP8266 and similar
* supports Authentication (but doesn't require it)
* uses industry-standard colours
* automatically reconnects if disconnected
* provides debug output over serial
* works with and without studio mode
## Dependencies
* `platformio` set up on your machine
* an instance of OBS running the websockets plugin
* some wifi network over which the OBS instance is reachable
## Setup
1. copy `include/config.example,h` to `include/config.h`
2. edit `include/config.h` to match your wifi and obs settings
3. connect your ESP8266 board using USB
4. run `pio run -t upload` to install the tally light onto your board
If flashing succeeds, your LED strip should blink twice, then switch to
tally light mode.
The serial console is configured to use _115200_ baud.
## Colours
### Red
The configured source is currently live (visible in program).
### Green
Only available in studio mode.
The configured source is currenly in preview.
### Off
The configured source is currenly not visible.
### Purple
Authentication failed, either by a missing or wrong password. The light
will automatically restart after 10 seconds.

53
include/config.example.h Normal file
View File

@ -0,0 +1,53 @@
/*
OBS-Websocket-Tally
Please note all pin numbers use the ESP8266 pin numbers. Take a look
at your breakout boards documentation to find out which label on the
board corresponds to which ESP8266 pin.
*/
// How many LEDs do you have connected?
#define LED_COUNT 10
// Choose anything between 0 (off) and 255 (supernova-bright)
#define LED_BRIGHTNESS 100
// Which pin is your LED strip connected to. The default is labeled D6
// on NodeMCU, D0 on Wemos D1 mini.
#define LED_PIN 12
// Configure your wifi credentials in here
#define WIFI_SSID "my wifi name"
#define WIFI_PASS "really secure password"
// The IP address of the machine running OBS Studio
#define OBS_HOST "172.19.138.143"
// The port on which the websockets plugin listens. Default is 4444
#define OBS_PORT 4444
/*
If your websockets plugin requires authentication, set it here.
The Tally light will automatically restart if the password is wrong
(or unset but needed), but silently ignores if you set a password
without needing it.
*/
//#define OBS_PASS ""
/*
The name of the source as set in OBS. The tally light will perform
a exact match on that value, so please make sure you enter your
source name exactly as spelled inside OBS.
*/
#define OBS_SOURCE "ATEM"
/*
If defined, you'll get a status LED if the tally light is connected
to the configured OBS instance. The light will be on if the light
is able to receive information from OBS. If there's no connection
or authentication has failed. the light will be off.
The default uses the onboard LED on the NodeMCU. On Wemos D1 mini
this pin is labeled D6.
*/
#define STATUS_LED 16

View File

@ -1,20 +1,11 @@
#define LED_COUNT 10
#define LED_BRIGHTNESS 100
#define LED_PIN 12
#include <config.h>
#define WIFI_SSID ""
#define WIFI_PASS ""
#define OBS_HOST ""
#define OBS_PORT 4444
//#define OBS_PASS ""
#define OBS_SOURCE "ATEM"
static_assert(sizeof(WIFI_SSID) > 1, "WIFI_SSID is empty");
static_assert(sizeof(OBS_HOST) > 1, "OBS_HOST is empty");
static_assert(sizeof(OBS_SOURCE) > 1, "OBS_SOURCE is empty");
#define FASTLED_ESP8266_RAW_PIN_ORDER
// END CONFIGURATION
#include <Arduino.h>
#include <Hash.h>
#include <ESP8266WiFi.h>
@ -27,6 +18,8 @@ CRGB leds[LED_COUNT];
WebSocketsClient webSocket;
#ifdef OBS_PASS
static_assert(sizeof(OBS_PASS) > 1, "OBS_PASS must be non-empty if defined");
#include <SHA256.h>
#define HASH_SIZE 32
@ -35,6 +28,7 @@ SHA256 sha256;
bool is_currently_live = false;
bool is_currently_preview = false;
bool is_currently_connected = false;
void set_program() {
Serial.println("[Tally] PROGRAM");
@ -73,23 +67,19 @@ void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
We can't set the LEDs to "error", because if someone
quits OBS, we will get disconnected.
*/
is_currently_connected = false;
break;
case WStype_CONNECTED:
Serial.printf("[WS] connected to %s\n", payload);
#ifdef OBS_PASS
// Find out if we need authentication
webSocket.sendTXT("{\"request-type\":\"GetAuthRequired\",\"message-id\":\"1\"}");
#else
webSocket.sendTXT("{\"request-type\":\"GetCurrentScene\",\"message-id\":\"3\"}");
webSocket.sendTXT("{\"request-type\":\"GetPreviewScene\",\"message-id\":\"4\"}");
#endif
break;
case WStype_TEXT: {
Serial.printf("[WS] %s\n", payload);
StaticJsonDocument<5000> doc;
StaticJsonDocument<10000> doc;
DeserializationError error = deserializeJson(doc, payload);
if (error) {
@ -98,59 +88,77 @@ void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
break;
}
#ifdef OBS_PASS
if (doc.containsKey("authRequired")) {
Serial.println("[OBS] auth requested");
if (doc["authRequired"]) {
#ifdef OBS_PASS
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);
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<unsigned int>(value[i]), HEX);
Serial.print("[OBS] sha256 authentication hash is: ");
for (size_t i = 0; i < HASH_SIZE; i++) {
Serial.print(static_cast<unsigned int>(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<unsigned int>(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);
#else
Serial.println("[OBS] auth requested, but not configured!");
set_error();
delay(10000);
ESP.restart();
#endif
} else {
#ifdef OBS_PASS
Serial.println("[OBS] auth configured, but not needed");
#endif
webSocket.sendTXT("{\"request-type\":\"GetCurrentScene\",\"message-id\":\"3\"}");
webSocket.sendTXT("{\"request-type\":\"GetPreviewScene\",\"message-id\":\"4\"}");
is_currently_connected = true;
}
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<unsigned int>(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;
#ifdef OBS_PASS
} 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\"}");
is_currently_connected = true;
} else {
Serial.println("[OBS] authentication FAILED");
set_error();
delay(10000);
ESP.restart();
}
break;
}
#endif
if (doc.containsKey("sources")) {
} else if (doc.containsKey("sources")) {
bool my_source_in_current_event = false;
for (uint8_t i = 0; i < doc["sources"].size(); i++) {
@ -180,6 +188,7 @@ void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
Serial.println("[OBS] quit");
is_currently_preview = false;
is_currently_live = false;
is_currently_connected = false;
} else if (strcmp(doc["update-type"], "StudioModeSwitched") == 0 && !doc["new-state"]) {
Serial.println("[OBS] studio mode disabled");
is_currently_preview = false;
@ -210,6 +219,14 @@ void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
case WStype_FRAGMENT_FIN:
break;
}
#ifdef STATUS_LED
if (is_currently_connected) {
digitalWrite(STATUS_LED, LOW);
} else {
digitalWrite(STATUS_LED, HIGH);
}
#endif
}
void setup() {
@ -220,6 +237,10 @@ void setup() {
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, LED_COUNT);
FastLED.setBrightness(LED_BRIGHTNESS);
#ifdef STATUS_LED
pinMode(STATUS_LED, OUTPUT);
#endif
set_error();
delay(100);
set_idle();