various fixes, improvements, documentation
This commit is contained in:
parent
300ca56d3b
commit
3818e5b6c3
4 changed files with 179 additions and 55 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
.pio
|
.pio
|
||||||
|
include/config.h
|
||||||
|
|
49
README.md
Normal file
49
README.md
Normal 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
53
include/config.example.h
Normal 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
|
131
src/main.cpp
131
src/main.cpp
|
@ -1,20 +1,11 @@
|
||||||
#define LED_COUNT 10
|
#include <config.h>
|
||||||
#define LED_BRIGHTNESS 100
|
|
||||||
#define LED_PIN 12
|
|
||||||
|
|
||||||
#define WIFI_SSID ""
|
static_assert(sizeof(WIFI_SSID) > 1, "WIFI_SSID is empty");
|
||||||
#define WIFI_PASS ""
|
static_assert(sizeof(OBS_HOST) > 1, "OBS_HOST is empty");
|
||||||
|
static_assert(sizeof(OBS_SOURCE) > 1, "OBS_SOURCE is empty");
|
||||||
#define OBS_HOST ""
|
|
||||||
#define OBS_PORT 4444
|
|
||||||
//#define OBS_PASS ""
|
|
||||||
|
|
||||||
#define OBS_SOURCE "ATEM"
|
|
||||||
|
|
||||||
#define FASTLED_ESP8266_RAW_PIN_ORDER
|
#define FASTLED_ESP8266_RAW_PIN_ORDER
|
||||||
|
|
||||||
// END CONFIGURATION
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Hash.h>
|
#include <Hash.h>
|
||||||
#include <ESP8266WiFi.h>
|
#include <ESP8266WiFi.h>
|
||||||
|
@ -27,6 +18,8 @@ CRGB leds[LED_COUNT];
|
||||||
WebSocketsClient webSocket;
|
WebSocketsClient webSocket;
|
||||||
|
|
||||||
#ifdef OBS_PASS
|
#ifdef OBS_PASS
|
||||||
|
static_assert(sizeof(OBS_PASS) > 1, "OBS_PASS must be non-empty if defined");
|
||||||
|
|
||||||
#include <SHA256.h>
|
#include <SHA256.h>
|
||||||
#define HASH_SIZE 32
|
#define HASH_SIZE 32
|
||||||
|
|
||||||
|
@ -35,6 +28,7 @@ SHA256 sha256;
|
||||||
|
|
||||||
bool is_currently_live = false;
|
bool is_currently_live = false;
|
||||||
bool is_currently_preview = false;
|
bool is_currently_preview = false;
|
||||||
|
bool is_currently_connected = false;
|
||||||
|
|
||||||
void set_program() {
|
void set_program() {
|
||||||
Serial.println("[Tally] 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
|
We can't set the LEDs to "error", because if someone
|
||||||
quits OBS, we will get disconnected.
|
quits OBS, we will get disconnected.
|
||||||
*/
|
*/
|
||||||
|
is_currently_connected = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WStype_CONNECTED:
|
case WStype_CONNECTED:
|
||||||
Serial.printf("[WS] connected to %s\n", payload);
|
Serial.printf("[WS] connected to %s\n", payload);
|
||||||
#ifdef OBS_PASS
|
|
||||||
// Find out if we need authentication
|
// Find out if we need authentication
|
||||||
webSocket.sendTXT("{\"request-type\":\"GetAuthRequired\",\"message-id\":\"1\"}");
|
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;
|
break;
|
||||||
|
|
||||||
case WStype_TEXT: {
|
case WStype_TEXT: {
|
||||||
Serial.printf("[WS] %s\n", payload);
|
Serial.printf("[WS] %s\n", payload);
|
||||||
|
|
||||||
StaticJsonDocument<5000> doc;
|
StaticJsonDocument<10000> doc;
|
||||||
DeserializationError error = deserializeJson(doc, payload);
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -98,59 +88,77 @@ void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef OBS_PASS
|
|
||||||
if (doc.containsKey("authRequired")) {
|
if (doc.containsKey("authRequired")) {
|
||||||
Serial.println("[OBS] auth requested");
|
if (doc["authRequired"]) {
|
||||||
|
#ifdef OBS_PASS
|
||||||
|
Serial.println("[OBS] auth requested");
|
||||||
|
|
||||||
sha256.reset();
|
sha256.reset();
|
||||||
sha256.update(OBS_PASS, strlen(OBS_PASS));
|
sha256.update(OBS_PASS, strlen(OBS_PASS));
|
||||||
const char* salt = doc["salt"];
|
const char* salt = doc["salt"];
|
||||||
sha256.update(salt, strlen(salt));
|
sha256.update(salt, strlen(salt));
|
||||||
char value[HASH_SIZE];
|
char value[HASH_SIZE];
|
||||||
sha256.finalize(value, HASH_SIZE);
|
sha256.finalize(value, HASH_SIZE);
|
||||||
|
|
||||||
Serial.print("[OBS] sha256 authentication hash is: ");
|
Serial.print("[OBS] sha256 authentication hash is: ");
|
||||||
for (size_t i = 0; i < HASH_SIZE; i++) {
|
for (size_t i = 0; i < HASH_SIZE; i++) {
|
||||||
Serial.print(static_cast<unsigned int>(value[i]), HEX);
|
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;
|
break;
|
||||||
|
|
||||||
|
#ifdef OBS_PASS
|
||||||
} else if (doc.containsKey("message-id") && doc["message-id"] == "2") {
|
} else if (doc.containsKey("message-id") && doc["message-id"] == "2") {
|
||||||
if (strcmp(doc["status"], "ok") == 0) {
|
if (strcmp(doc["status"], "ok") == 0) {
|
||||||
Serial.println("[OBS] authentication successful");
|
Serial.println("[OBS] authentication successful");
|
||||||
webSocket.sendTXT("{\"request-type\":\"GetCurrentScene\",\"message-id\":\"3\"}");
|
webSocket.sendTXT("{\"request-type\":\"GetCurrentScene\",\"message-id\":\"3\"}");
|
||||||
webSocket.sendTXT("{\"request-type\":\"GetPreviewScene\",\"message-id\":\"4\"}");
|
webSocket.sendTXT("{\"request-type\":\"GetPreviewScene\",\"message-id\":\"4\"}");
|
||||||
|
is_currently_connected = true;
|
||||||
} else {
|
} else {
|
||||||
Serial.println("[OBS] authentication FAILED");
|
Serial.println("[OBS] authentication FAILED");
|
||||||
set_error();
|
set_error();
|
||||||
|
delay(10000);
|
||||||
|
ESP.restart();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
if (doc.containsKey("sources")) {
|
} else if (doc.containsKey("sources")) {
|
||||||
bool my_source_in_current_event = false;
|
bool my_source_in_current_event = false;
|
||||||
|
|
||||||
for (uint8_t i = 0; i < doc["sources"].size(); i++) {
|
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");
|
Serial.println("[OBS] quit");
|
||||||
is_currently_preview = false;
|
is_currently_preview = false;
|
||||||
is_currently_live = false;
|
is_currently_live = false;
|
||||||
|
is_currently_connected = false;
|
||||||
} else if (strcmp(doc["update-type"], "StudioModeSwitched") == 0 && !doc["new-state"]) {
|
} else if (strcmp(doc["update-type"], "StudioModeSwitched") == 0 && !doc["new-state"]) {
|
||||||
Serial.println("[OBS] studio mode disabled");
|
Serial.println("[OBS] studio mode disabled");
|
||||||
is_currently_preview = false;
|
is_currently_preview = false;
|
||||||
|
@ -210,6 +219,14 @@ void handleWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
|
||||||
case WStype_FRAGMENT_FIN:
|
case WStype_FRAGMENT_FIN:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef STATUS_LED
|
||||||
|
if (is_currently_connected) {
|
||||||
|
digitalWrite(STATUS_LED, LOW);
|
||||||
|
} else {
|
||||||
|
digitalWrite(STATUS_LED, HIGH);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
|
@ -220,6 +237,10 @@ void setup() {
|
||||||
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, LED_COUNT);
|
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, LED_COUNT);
|
||||||
FastLED.setBrightness(LED_BRIGHTNESS);
|
FastLED.setBrightness(LED_BRIGHTNESS);
|
||||||
|
|
||||||
|
#ifdef STATUS_LED
|
||||||
|
pinMode(STATUS_LED, OUTPUT);
|
||||||
|
#endif
|
||||||
|
|
||||||
set_error();
|
set_error();
|
||||||
delay(100);
|
delay(100);
|
||||||
set_idle();
|
set_idle();
|
||||||
|
|
Loading…
Reference in a new issue