diff --git a/vTally.ino b/vTally.ino new file mode 100644 index 0000000..c059e4d --- /dev/null +++ b/vTally.ino @@ -0,0 +1,1522 @@ +/* + vTally for vMix + Copyright 2021 CaliHC +*/ + +#include +#include +#include +#include +#include +#include +#include "ESPAsyncUDP.h" +#include "FS.h" + +// Constants +const float vers = 1.6; + +const int SsidMaxLength = 24; +const int PassMaxLength = 24; +const int HostNameMaxLength = 24; +const int TallyNumberMaxValue = 64; + +// LED setting +#define LED_PIN D2 +#define LED_NUM 1 +Adafruit_NeoPixel leds = Adafruit_NeoPixel(LED_NUM, LED_PIN, NEO_GRB + NEO_KHZ800); + +// Settings object +struct Settings +{ + char ssid[SsidMaxLength]; + char pass[PassMaxLength]; + char hostName[HostNameMaxLength]; + int tallyNumber; + int intensFull; + int intensDim; + int prgred; + int prggreen; + int prgblue; + int prvred; + int prvgreen; + int prvblue; + int offred; + int offgreen; + int offblue; + unsigned int viscabaud; + unsigned int viscaport; +}; + +// Default settings object +Settings defaultSettings = { + "SSID", + "PASSWORD", + "vmix_hostname", + 1, + 255, + 128, + 0, + 255, + 0, + 255, + 128, + 0, + 0, + 0, + 255, + 9600, + 52381 +}; + +Settings settings; + +// HTTP Server settings +ESP8266WebServer httpServer(80); +char deviceName[32]; +int status = WL_IDLE_STATUS; +bool apEnabled = false; +char apPass[64]; + +// vMix settings +int port = 8099; + +//// Tally info +char currentState = -1; +char oldState = -1; +const char tallyStateProgram = 1; +const char tallyStatePreview = 2; + +// The WiFi client +WiFiClient client; +const int timeout = 10; +const int delayTime = 10000; +int vmixcon = 0; + +// Time measure +const int interval = 5000; +unsigned long lastCheck = 0; + +// VISCAoIP 2 serial +SoftwareSerial viscaSerial; +int udpstate = 0; + +//// RS232 Serial Settings +const int txpin = D5; +const int rxpin = D6; + +//// Use the following constants and functions to modify the speed of PTZ commands +const double ZOOMMULT = 0.3; // speed multiplier for the zoom functions +const double ZOOMEXP = 1.5; // exponential curve for the speed modification +const double PTZMULT = 0.3; // speed multiplier for the pan and tilt functions +const double PTZEXP = 1.0; // exponential curve for the speed modification + +//// STATE VARIABLES +AsyncUDP udp; +int lastclientport = 0; +IPAddress lastclientip; +bool pwr_is_on = false; + +//// memory buffers for VISCA commands +size_t lastudp_len = 0; +uint8_t lastudp_in[16]; +size_t lastser_len = 0; +uint8_t lastser_in[16]; + +//// quick use VISCA commands +const uint8_t pwr_on[] = {0x81, 0x01, 0x04, 0x00, 0x02, 0xff}; +const uint8_t pwr_off[] = {0x81, 0x01, 0x04, 0x00, 0x03, 0xff}; +const uint8_t addr_set[] = {0x88, 0x30, 0x01, 0xff}; // address set +const uint8_t if_clear[] = {0x88, 0x01, 0x00, 0x01, 0xff}; // if clear +const uint8_t ifClear[] = {0x88, 0x01, 0x00, 0x01, 0xff}; // Checks to see if communication line is clear +const uint8_t irOff[] = {0x81, 0x01, 0x06, 0x09, 0x03, 0xff}; // Turn off IR control (required for speed control of Pan/Tilt on TelePresence cameras) +const uint8_t callLedOn[] = {0x81, 0x01, 0x33, 0x01, 0x01, 0xff}; +const uint8_t callLedOff[] = {0x81, 0x01, 0x33, 0x01, 0x00, 0xff}; +const uint8_t callLedBlink[] = {0x81, 0x01, 0x33, 0x01, 0x02, 0xff}; +/* + * Video formats values: + * Value HDMI SDI + * 0x00 1080p25 1080p25 + * 0x01 1080p30 1080p30 + * 0x02 1080p50 720p50 + * 0x03 1080p60 720p60 + * 0x04 720p25 720p25 + * 0x05 720p30 720p30 + * 0x06 720p50 720p50 + * 0x07 720p60 720p60 + */ +const uint8_t format = 0x01; +const uint8_t videoFormat[] = { 0x81, 0x01, 0x35, 0x00, format, 0x00, 0xff }; // 8x 01 35 0p 0q 0r ff p = reserved, q = video mode, r = Used in PrecisionHD 720p camera. + + + +// Load settings from EEPROM +void loadSettings() +{ + Serial.println(F("+--------------------+")); + Serial.println(F("| Loading settings |")); + Serial.println(F("+--------------------+")); + + long ptr = 0; + + for (int i = 0; i < SsidMaxLength; i++) + { + settings.ssid[i] = EEPROM.read(ptr); + ptr++; + } + + for (int i = 0; i < PassMaxLength; i++) + { + settings.pass[i] = EEPROM.read(ptr); + ptr++; + } + + for (int i = 0; i < HostNameMaxLength; i++) + { + settings.hostName[i] = EEPROM.read(ptr); + ptr++; + } + + settings.tallyNumber = EEPROM.read(ptr); + + ptr++; + settings.intensFull = EEPROM.read(ptr); + + ptr++; + settings.intensDim = EEPROM.read(ptr); + + ptr++; + settings.prgred = EEPROM.read(ptr); + ptr++; + settings.prggreen = EEPROM.read(ptr); + ptr++; + settings.prgblue = EEPROM.read(ptr); + + ptr++; + settings.prvred = EEPROM.read(ptr); + ptr++; + settings.prvgreen = EEPROM.read(ptr); + ptr++; + settings.prvblue = EEPROM.read(ptr); + + ptr++; + settings.offred = EEPROM.read(ptr); + ptr++; + settings.offgreen = EEPROM.read(ptr); + ptr++; + settings.offblue = EEPROM.read(ptr); + + ptr++; + byte low = EEPROM.read(ptr); + ptr++; + byte high = EEPROM.read(ptr); + settings.viscabaud = low + ((high << 8) & 0xFF00); + ptr++; + low = EEPROM.read(ptr); + ptr++; + high = EEPROM.read(ptr); + settings.viscaport = low + ((high << 8) & 0xFF00); + + + if (strlen(settings.ssid) == 0 || strlen(settings.pass) == 0 || strlen(settings.hostName) == 0 || settings.tallyNumber == 0 || settings.intensFull == 0 || settings.intensDim == 0 || settings.viscabaud == 0 || settings.viscaport == 0) + { + Serial.println(F("| No settings found")); + Serial.println(F("| Loading default settings")); + settings = defaultSettings; + saveSettings(); + restart(); + } + else + { + Serial.println(F("| Settings loaded")); + printSettings(); + Serial.println(F("+---------------------")); + } +} + +// Save settings to EEPROM +void saveSettings() +{ + Serial.println(F("+--------------------+")); + Serial.println(F("| Saving settings |")); + Serial.println(F("+--------------------+")); + + long ptr = 0; + + for (int i = 0; i < 512; i++) + { + EEPROM.write(i, 0); + } + + for (int i = 0; i < SsidMaxLength; i++) + { + EEPROM.write(ptr, settings.ssid[i]); + ptr++; + } + + for (int i = 0; i < PassMaxLength; i++) + { + EEPROM.write(ptr, settings.pass[i]); + ptr++; + } + + for (int i = 0; i < HostNameMaxLength; i++) + { + EEPROM.write(ptr, settings.hostName[i]); + ptr++; + } + + EEPROM.write(ptr, settings.tallyNumber); + + ptr++; + EEPROM.write(ptr, settings.intensFull); + + ptr++; + EEPROM.write(ptr, settings.intensDim); + + ptr++; + EEPROM.write(ptr, settings.prgred); + ptr++; + EEPROM.write(ptr, settings.prggreen); + ptr++; + EEPROM.write(ptr, settings.prgblue); + + ptr++; + EEPROM.write(ptr, settings.prvred); + ptr++; + EEPROM.write(ptr, settings.prvgreen); + ptr++; + EEPROM.write(ptr, settings.prvblue); + + ptr++; + EEPROM.write(ptr, settings.offred); + ptr++; + EEPROM.write(ptr, settings.offgreen); + ptr++; + EEPROM.write(ptr, settings.offblue); + + ptr++; + EEPROM.write(ptr, settings.viscabaud & 0xFF); + ptr++; + EEPROM.write(ptr, (settings.viscabaud >> 8) & 0xFF); + + ptr++; + EEPROM.write(ptr, settings.viscaport & 0xFF); + ptr++; + EEPROM.write(ptr, (settings.viscaport >> 8) & 0xFF); + + EEPROM.commit(); + + Serial.println(F("| Settings saved")); + printSettings(); + Serial.println(F("+---------------------")); +} + +// Print settings +void printSettings() +{ + Serial.print(F("| SSID : ")); + Serial.println(settings.ssid); + Serial.print(F("| SSID Password : ")); + Serial.println(settings.pass); + Serial.print(F("| vMix Hostname/IP: ")); + Serial.println(settings.hostName); + Serial.print(F("| Tally number : ")); + Serial.println(settings.tallyNumber); + Serial.print(F("| Intensity Full : ")); + Serial.println(settings.intensFull); + Serial.print(F("| Intensity Dim : ")); + Serial.println(settings.intensDim); + Serial.print(F("| Program-Color : ")); + Serial.print(settings.prgred); + Serial.print(F(",")); + Serial.print(settings.prggreen); + Serial.print(F(",")); + Serial.println(settings.prgblue); + Serial.print(F("| Preview-Color : ")); + Serial.print(settings.prvred); + Serial.print(F(",")); + Serial.print(settings.prvgreen); + Serial.print(F(",")); + Serial.println(settings.prvblue); + Serial.print(F("| Off-Color : ")); + Serial.print(settings.offred); + Serial.print(F(",")); + Serial.print(settings.offgreen); + Serial.print(F(",")); + Serial.println(settings.offblue); + Serial.print(F("| VISCA baud : ")); + Serial.println(settings.viscabaud); + Serial.print(F("| VISCA port : ")); + Serial.println(settings.viscaport); +} + +// Set led intensity from 0 to 255 +void ledSetIntensity(int intensity) +{ + leds.setBrightness(intensity); +} + +// Set LED's off +void ledSetOff() +{ + leds.setPixelColor(0, leds.Color(0,0,0)); + ledSetIntensity(0); + leds.show(); +} + +// Draw corner dots +void ledSetCornerDots() +{ + leds.setPixelColor(0, leds.Color(settings.offred,settings.offgreen,settings.offblue)); + ledSetIntensity(settings.intensDim); + leds.show(); +} + +// Draw L(ive) with LED's +void ledSetProgram() +{ + leds.setPixelColor(0, leds.Color(settings.prgred,settings.prggreen,settings.prgblue)); + ledSetIntensity(settings.intensFull); + leds.show(); +} + +// Draw P(review) with LED's +void ledSetPreview() +{ + leds.setPixelColor(0, leds.Color(settings.prvred,settings.prvgreen,settings.prvblue)); + ledSetIntensity(settings.intensFull); + leds.show(); +} + +// Draw C(onnecting) with LED's +void ledSetConnecting() +{ + leds.setPixelColor(0, leds.Color(0,255,255)); + ledSetIntensity(settings.intensDim); + leds.show(); +} + +// Draw S(ettings) with LED's +void ledSetSettings() +{ + leds.setPixelColor(0, leds.Color(255,0,255)); + ledSetIntensity(settings.intensDim); + leds.show(); +} + +// Set tally to off +void tallySetOff() +{ + Serial.println(F("| Tally off")); + + ledSetCornerDots(); + send_visca(callLedOff); +} + +// Set tally to program +void tallySetProgram() +{ + Serial.println(F("| Tally program")); + + ledSetProgram(); + send_visca(callLedOn); +} + +// Set tally to preview +void tallySetPreview() +{ + Serial.println(F("| Tally preview")); + + ledSetPreview(); + send_visca(callLedBlink); +} + +// Set tally to connecting +void tallySetConnecting() +{ + ledSetConnecting(); +} + +// Handle incoming data +void handleData(String data) +{ + // Check if server data is tally data + if (data.indexOf("TALLY OK") == 0) + { + vmixcon = 1; + char newState = data.charAt(settings.tallyNumber + 8); + + // Check if tally state has changed + if (currentState != newState) + { + currentState = newState; + + switch (currentState) + { + case '0': + tallySetOff(); + break; + case '1': + tallySetProgram(); + break; + case '2': + tallySetPreview(); + break; + default: + tallySetOff(); + } + } + } + else + { + vmixcon = 1; + Serial.print(F("| Response from vMix: ")); + Serial.println(data); + + //Serial.print(F("| FreeHeap: ")); + //Serial.println(ESP.getFreeHeap(),DEC); + } +} + +// Start access point +void apStart() +{ + ledSetSettings(); + Serial.println(F("(+--------------------+")); + Serial.println(F("(| AP Start |")); + Serial.println(F("(+--------------------+")); + Serial.print(F("| AP SSID : ")); + Serial.println(deviceName); + Serial.print(F("| AP password : ")); + Serial.println(apPass); + + WiFi.mode(WIFI_AP); + WiFi.hostname(deviceName); + WiFi.softAP(deviceName, apPass); + delay(100); + IPAddress myIP = WiFi.softAPIP(); + Serial.print(F("| IP address : ")); + Serial.println(myIP); + Serial.println(F("+---------------------")); + + + apEnabled = true; +} + +// Handle http server Tally request +void tallyPageHandler() +{ + String response_message = F(""); + + response_message += F(""); + response_message += F(""); + response_message += F("vTally by CaliHC - ") + String(deviceName) + F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + + response_message += F(""); + + response_message += F(""); + + response_message += F(""); + + + response_message += F(""); + + + response_message += F(""); + + response_message += F(""); + + response_message += F("
 
"); + switch (currentState) + { + case '0': + response_message += F("OFF"); //off + break; + case '1': + response_message += F("PROGRAM"); //prg + break; + case '2': + response_message += F("PREVIEW"); //prv + break; + case '3': + response_message += F("vMix Server not found!"); // no vMix Server + break; + case '4': + response_message += F("connected to vMix Server, waiting for data."); // no vMix Server + break; + default: + response_message += F("OFF"); //default off + } + response_message += F("
 
 
"); + + response_message += F(""); + + httpServer.sendHeader("Connection", "close"); + httpServer.send(200, "text/html", String(response_message)); + + //Serial.print(F("| FreeHeap: ")); + //Serial.println(ESP.getFreeHeap(),DEC); +} + +// Handle http server root request +void rootPageHandler() +{ + String response_message = F(""); + response_message += F(""); + response_message += F(""); + response_message += F("vTally by CaliHC - ") + String(deviceName) + F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + response_message += F(""); + + response_message += F(""); + + response_message += F("

vTally by

"); + + response_message += F("

vTally ID: ") + String(settings.tallyNumber) + F("

"); + + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F("

  Network/vMix/VISCA Settings

"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + //response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F("

  vMix Tally Settings

"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
"); + response_message += F("
 R 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
 G 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
 B 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
  
"); + response_message += F("
 Program 
"); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
"); + response_message += F("
 R 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
 G 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
 B 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
  
"); + response_message += F("
  Preview  
"); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
"); + response_message += F("
 R 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
 G 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
 B 
"); + response_message += F(""); + response_message += F("
"); + response_message += F("
  
"); + response_message += F("
      Off     
"); + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
 
"); + + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + response_message += F(""); + response_message += F("
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F("
"); + + response_message += F("
"); + response_message += F(" 
"); + + response_message += F("
"); + + response_message += F("
"); + response_message += F("

  Device Information

"); + response_message += F(""); + + char ip[13]; + sprintf(ip, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); + response_message += F(""); + + response_message += F(""); + + response_message += F(""); + if (apEnabled) + { + sprintf(ip, "%d.%d.%d.%d", WiFi.softAPIP()[0], WiFi.softAPIP()[1], WiFi.softAPIP()[2], WiFi.softAPIP()[3]); + response_message += F(""); + + response_message += F(""); + + response_message += F(""); + response_message += F(""); + + response_message += F(""); + + response_message += F(""); + response_message += F(""); + + response_message += F(""); + + response_message += F("
IP") + String(ip) + F("WiFi Signal Strength") + String(WiFi.RSSI()) + F(" dBm
MAC") + String(WiFi.macAddress()) + F("WiFi APActive (") + String(ip) + F(")"); + } + else + { + response_message += F("Inactive"); + } + response_message += F("
Device Name") + String(deviceName) + F("vMix StatusConnected"); + } + else + { + response_message += F("style='background-color:red;color:white;'>Disconnected"); + } + response_message += F("
  VISCA IP2SERIAL StatusRunning"); + } + else + { + response_message += F("style='border-radius: 0px 0px 10px 0px;background-color:red;color:white;'>Not Running"); + } + response_message += F("
"); + response_message += F("
"); + + + + response_message += F("
"); + response_message += F("

  vMix Tally

"); + + response_message += F("
"); + response_message += F(""); + + response_message += F("
"); + + + + response_message += F("
"); + + response_message += F("

vTally v") + String(vers) + F("     © 2021 by CaliHC

"); + + response_message += F(""); + response_message += F(""); + + httpServer.sendHeader("Connection", "close"); + httpServer.send(200, "text/html", String(response_message)); + + //Serial.print(F("| FreeHeap: ")); + //Serial.println(ESP.getFreeHeap(),DEC); +} + +// Settings POST handler +void handleSave() +{ + bool doRestart = false; + + httpServer.sendHeader("Location", String("/"), true); + httpServer.send(302, "text/plain", "Redirected to: /"); + + if (httpServer.hasArg("ssid")) + { + if (httpServer.arg("ssid").length() <= SsidMaxLength) + { + httpServer.arg("ssid").toCharArray(settings.ssid, SsidMaxLength); + doRestart = true; + } + } + + if (httpServer.hasArg("ssidpass")) + { + if (httpServer.arg("ssidpass").length() <= PassMaxLength) + { + httpServer.arg("ssidpass").toCharArray(settings.pass, PassMaxLength); + doRestart = true; + } + } + + if (httpServer.hasArg("hostname")) + { + if (httpServer.arg("hostname").length() <= HostNameMaxLength) + { + httpServer.arg("hostname").toCharArray(settings.hostName, HostNameMaxLength); + doRestart = true; + } + } + + if (httpServer.hasArg("inputnumber")) + { + if (httpServer.arg("inputnumber").toInt() > 0 and httpServer.arg("inputnumber").toInt() <= TallyNumberMaxValue) + { + settings.tallyNumber = httpServer.arg("inputnumber").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("intensFull")) + { + if (httpServer.arg("intensFull").toInt() >= 0 and httpServer.arg("intensFull").toInt() < 255) + { + settings.intensFull = httpServer.arg("intensFull").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("intensDim")) + { + if (httpServer.arg("intensDim").toInt() >= 0 and httpServer.arg("intensDim").toInt() < 255) + { + settings.intensDim = httpServer.arg("intensDim").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("prgred")) + { + if (httpServer.arg("prgred").toInt() >= 0 and httpServer.arg("prgred").toInt() < 255) + { + settings.prgred = httpServer.arg("prgred").toInt(); + doRestart = true; + } + } + if (httpServer.hasArg("prggreen")) + { + if (httpServer.arg("prggreen").toInt() >= 0 and httpServer.arg("prggreen").toInt() < 255) + { + settings.prggreen = httpServer.arg("prggreen").toInt(); + doRestart = true; + } + } + if (httpServer.hasArg("prgblue")) + { + if (httpServer.arg("prgblue").toInt() >= 0 and httpServer.arg("prgblue").toInt() < 255) + { + settings.prgblue = httpServer.arg("prgblue").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("prvred")) + { + if (httpServer.arg("prvred").toInt() >= 0 and httpServer.arg("prvred").toInt() < 255) + { + settings.prvred = httpServer.arg("prvred").toInt(); + doRestart = true; + } + } + if (httpServer.hasArg("prvgreen")) + { + if (httpServer.arg("prvgreen").toInt() >= 0 and httpServer.arg("prvgreen").toInt() < 255) + { + settings.prvgreen = httpServer.arg("prvgreen").toInt(); + doRestart = true; + } + } + if (httpServer.hasArg("prvblue")) + { + if (httpServer.arg("prvblue").toInt() >= 0 and httpServer.arg("prvblue").toInt() < 255) + { + settings.prvblue = httpServer.arg("prvblue").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("offred")) + { + if (httpServer.arg("offred").toInt() >= 0 and httpServer.arg("offred").toInt() < 255) + { + settings.offred = httpServer.arg("offred").toInt(); + doRestart = true; + } + } + if (httpServer.hasArg("offgreen")) + { + if (httpServer.arg("offgreen").toInt() >= 0 and httpServer.arg("offgreen").toInt() < 255) + { + settings.offgreen = httpServer.arg("offgreen").toInt(); + doRestart = true; + } + } + if (httpServer.hasArg("offblue")) + { + if (httpServer.arg("offblue").toInt() >= 0 and httpServer.arg("offblue").toInt() < 255) + { + settings.offblue = httpServer.arg("offblue").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("viscabaud")) + { + if (httpServer.arg("viscabaud").toInt() >= 2400 and httpServer.arg("viscabaud").toInt() <= 115200) + { + settings.viscabaud = httpServer.arg("viscabaud").toInt(); + doRestart = true; + } + } + + if (httpServer.hasArg("viscaport")) + { + if (httpServer.arg("viscaport").toInt() >= 1024 and httpServer.arg("viscaport").toInt() <= 65554) + { + settings.viscaport = httpServer.arg("viscaport").toInt(); + doRestart = true; + } + } + + if (doRestart == true) + { + restart(); + } +} + +// Connect to WiFi +void connectToWifi() +{ + Serial.println(F("+--------------------+")); + Serial.println(F("| Connecting to WiFi |")); + Serial.println(F("+--------------------+")); + Serial.print(F("| SSID : ")); + Serial.println(settings.ssid); + Serial.print(F("| Passphrase : ")); + Serial.println(settings.pass); + Serial.println(F("|")); + + int timeout = 15; + + WiFi.mode(WIFI_STA); + WiFi.hostname(deviceName); + WiFi.begin(settings.ssid, settings.pass); + + Serial.print(F("| Waiting for connection .")); + while (WiFi.status() != WL_CONNECTED and timeout > 0) + { + delay(1000); + timeout--; + Serial.print(F(".")); + } + + Serial.println(F("")); + + if (WiFi.status() == WL_CONNECTED) + { + Serial.println(F("| WiFi connected")); + Serial.println(F("|")); + Serial.print(F("| IP address : ")); + Serial.println(WiFi.localIP()); + Serial.print(F("| Device name : ")); + Serial.println(deviceName); + Serial.println(F("+---------------------")); + Serial.println(F("")); + } + else + { + if (WiFi.status() == WL_IDLE_STATUS) + Serial.println(F("| Idle")); + else if (WiFi.status() == WL_NO_SSID_AVAIL) + Serial.println(F("| No SSID Available")); + else if (WiFi.status() == WL_SCAN_COMPLETED) + Serial.println(F("| Scan Completed")); + else if (WiFi.status() == WL_CONNECT_FAILED) + Serial.println(F("| Connection Failed")); + else if (WiFi.status() == WL_CONNECTION_LOST) + Serial.println(F("| Connection Lost")); + else if (WiFi.status() == WL_DISCONNECTED) + Serial.println(F("| Disconnected")); + else + Serial.println(F("| Unknown Failure")); + + Serial.println(F("+---------------------")); + apStart(); + } +} + +// Connect to vMix instance +void connectTovMix() +{ + Serial.println(F("+--------------------+")); + Serial.println(F("| Connecting to vMix |")); + Serial.println(F("+--------------------+")); + Serial.print(F("| Connecting to ")); + Serial.println(settings.hostName); + + if (client.connect(settings.hostName, port)) + { + Serial.println(F("| Connected to vMix")); + + Serial.println(F("+---------------------")); + Serial.println(F("")); + + Serial.println(F("+--------------------+")); + Serial.println(F("| vMix Message Log |")); + Serial.println(F("+--------------------+")); + + tallySetOff(); + + // Subscribe to the tally events + client.println(F("SUBSCRIBE TALLY")); + } + else + { + vmixcon = 0; + currentState = '3'; + Serial.println(F("| vMix Server not found!")); + Serial.println(F("+---------------------")); + Serial.println(F("")); + } +} + +void restart() +{ + saveSettings(); + + Serial.println(F("")); + Serial.println(F("+--------------------+")); + Serial.println(F("| RESTART |")); + Serial.println(F("+--------------------+")); + Serial.println(F("")); + + ESP.restart(); + //start(); +} + +void banner() +{ + Serial.println(F("")); + Serial.println(F("")); + Serial.println(F("+--------------------+")); + Serial.print(F("| vTally v")); + Serial.print(String(vers)); + Serial.println(F(" |")); + Serial.println(F("| (c)2021 by CaliHC |")); + Serial.println(F("+--------------------+")); + Serial.println(F("")); +} + +void start() +{ + tallySetConnecting(); + + loadSettings(); + + Serial.println(F("")); + sprintf(deviceName, "vTally_%d", settings.tallyNumber); + sprintf(apPass, "%s%s", deviceName, "_pwd"); + + connectToWifi(); + + if (WiFi.status() == WL_CONNECTED) + { + viscaSerial.begin(settings.viscabaud, SWSERIAL_8N1, rxpin, txpin, false, 200); + start_visca(); + connectTovMix(); + } +} + +double zoomcurve(int v) +{ + return ZOOMMULT * pow(v, ZOOMEXP); +} + +double ptzcurve(int v) +{ + return PTZMULT * pow(v, PTZEXP); +} + +void debug(char c) +{ + Serial.print(c); +} + +void debug(int n, int base) +{ + Serial.print(n, base); + Serial.print(F("")); +} + +void debug(uint8_t *buf, int len) +{ + for (uint8_t i = 0; i < len; i++) + { + uint8_t elem = buf[i]; + debug(elem, HEX); + } +} + +void send_bytes(uint8_t *b, int len) +{ + for (int i = 0; i < len; i++) + { + uint8_t elem = b[i]; + viscaSerial.println(elem); + } +} + +void send_visca(uint8_t *c, size_t len) +{ + int i = 0; + uint8_t elem; + do + { + elem = c[i++]; + viscaSerial.print(elem); + } while (i < len && elem != 0xff); + Serial.println(F("| VISCA IP->SER")); +} + +void send_visca(const uint8_t *c) +{ + int i = 0; + uint8_t elem; + do + { + elem = c[i++]; + viscaSerial.print(elem); + } while (elem != 0xff); + Serial.println(F("| VISCA IP->SER")); +} + +void visca_power(bool turnon) +{ + if (turnon) + { + Serial.println(F("| VISCA Power On")); + send_visca(addr_set); + delay(500); + send_visca(pwr_on); + delay(2000); + send_visca(if_clear); + delay(500); + send_visca(irOff); + } + else + { + Serial.println(F("| VISCA Power Off")); + send_visca(if_clear); + delay(2000); + send_visca(pwr_off); + } + pwr_is_on = turnon; +} + +void handle_visca(uint8_t *buf, size_t len) +{ + uint8_t modified[16]; + size_t lastelement = 0; + for (int i = 0; (i < len && i < 16); i++) + { + modified[i] = buf[i]; + lastelement = i; + } + + // is this a PTZ? + if (modified[1] == 0x01 && modified[2] == 0x06 && modified[3] == 0x01) + { + Serial.println(F("| PTZ CONTROL DETECTED... ADJUSTING SPEED")); + modified[4] = (int)ptzcurve(modified[4]); + modified[5] = (int)ptzcurve(modified[5]); + } + if (modified[1] == 0x01 && modified[2] == 0x04 && modified[3] == 0x07) + { + Serial.println(F("| ZOOM CONTROL DETECTED, ADJUSTING SPEED")); + int zoomspeed = modified[4] & 0b00001111; + zoomspeed = (int)zoomcurve(zoomspeed); + int zoomval = (modified[4] & 0b11110000) + zoomspeed; + modified[4] = zoomval; + } + + Serial1.write(modified, lastelement + 1); +} + +void start_visca() +{ + Serial.println(F("+--------------------+")); + Serial.println(F("| VISCA server |")); + Serial.println(F("+--------------------+")); + Serial.print(F("| starting on UDP port ")); + Serial.println(settings.viscaport); + udp.close(); // will close only if needed + if (udp.listen(settings.viscaport)) + { + udpstate = 1; + Serial.println(F("| Server is Running!")); + udp.onPacket([](AsyncUDPPacket packet) { + // debug(packet); + lastclientip = packet.remoteIP(); + lastclientport = packet.remotePort(); + + Serial.print(F("| Type of UDP datagram: ")); + Serial.println(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" + : "Unicast"); + Serial.print(F("| Sender: ")); + Serial.print(lastclientip); + Serial.print(F(":")); + Serial.println(lastclientport); + Serial.print(F("| Receiver: ")); + Serial.print(packet.localIP()); + Serial.print(F(":")); + Serial.println(packet.localPort()); + Serial.print(F("| Message length: ")); + Serial.println(packet.length()); + Serial.print(F("| Payload (hex):")); + debug(packet.data(), packet.length()); + Serial.println(F("")); + + handle_visca(packet.data(), packet.length()); + }); + } + else + { + udpstate = 0; + Serial.println(F("| Server failed to start")); + } + Serial.println(F("+---------------------")); + Serial.println(F("")); +} + +void check_serial() +{ + int available = viscaSerial.available(); + while (available > 0) + { + Serial.println(F("| VISCA SER -> IP")); + int actual = viscaSerial.readBytesUntil(0xff, lastser_in, available); // does not include the terminator char + if (actual < 16) + { + lastser_in[actual] = 0xff; + actual++; + } + debug(lastser_in, actual); + if (lastclientport > 0) + udp.writeTo(lastser_in, actual, lastclientip, lastclientport); + + available = viscaSerial.available(); + } +} + + +void setup() +{ + Serial.begin(9600); + EEPROM.begin(512); + SPIFFS.begin(); + leds.begin(); + leds.setBrightness(50); + leds.show(); + + httpServer.on("/", HTTP_GET, rootPageHandler); + httpServer.on("/save", HTTP_POST, handleSave); + httpServer.on("/tally", HTTP_GET, tallyPageHandler); + httpServer.on("/zend", [](){ + //httpServer.sendHeader("Connection", "close"); + httpServer.sendHeader("Cache-Control", "no-cache"); + httpServer.sendHeader("Access-Control-Allow-Origin", "*"); + + if (currentState != oldState) { + httpServer.send(200, "text/event-stream", "event: message\ndata: refresh"+String(vmixcon)+"\nretry: 500\n\n"); + oldState = currentState; + } else { + httpServer.send(200, "text/event-stream", "event: message\ndata: norefresh\nretry: 500\n\n"); + } + + //Serial.print(F("| FreeHeap: ")); + //Serial.println(ESP.getFreeHeap(),DEC); + }); + httpServer.serveStatic("/", SPIFFS, "/", "max-age=315360000"); + httpServer.begin(); + + banner(); + + start(); +} + +void loop() +{ + httpServer.handleClient(); + + while (client.available()) + { + String data = client.readStringUntil('\r\n'); + handleData(data); + } + if (!client.connected() && !apEnabled && millis() > lastCheck + interval) + { + tallySetConnecting(); + + client.stop(); + + connectTovMix(); + lastCheck = millis(); + } + + check_serial(); +}