/* Copyright 2012 Kasper Skårhøj, SKAARHOJ, kasperskaarhoj@gmail.com This file is part of the ATEM library for Arduino The ATEM library is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The ATEM library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with the ATEM library. If not, see http://www.gnu.org/licenses/. */ #if defined(ARDUINO) && ARDUINO >= 100 #include "Arduino.h" #else #include "WProgram.h" #endif #include "ATEM.h" /** * Constructor, setting up IP address for the switcher (and local port to send packets from) */ ATEM::ATEM(IPAddress ip, uint16_t localPort){ // Set up Udp communication object: EthernetUDP Udp; _Udp = Udp; _switcherIP = ip; // Set switcher IP address _localPort = localPort; // Set local port (just a random number I picked) _serialOutput = false; } /** * Initiating connection handshake to the ATEM switcher */ void ATEM::connect() { _localPacketIdCounter = 1; // Init localPacketIDCounter to 1; _hasInitialized = false; _Udp.begin(_localPort); // Send connectString to ATEM: // TODO: Describe packet contents according to rev.eng. API byte connectHello[] = { 0x10, 0x14, 0x53, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; _Udp.beginPacket(_switcherIP, 9910); _Udp.write(connectHello,20); _Udp.endPacket(); // Waiting for the ATEM to answer back with a packet 20 bytes long. // According to packet analysis with WireShark, this feedback from ATEM // comes within a few microseconds! uint16_t packetSize = 0; while(packetSize!=20) { packetSize = _Udp.parsePacket(); } // Read the response packet. We will only subtract the session ID // According to packet analysis with WireShark, this feedback from ATEM // comes a few microseconds after our connect invitation above. Two packets immediately follow each other. // After approx. 200 milliseconds a third packet is sent from ATEM - a sort of re-sent because it gets impatient. // And it seems that THIS third packet is the one we actually read and respond to. In other words, I believe that // the ethernet interface on Arduino actually misses the first two for some reason! while(!_Udp.available()){} // Waiting.... TODO: Implement some way to exit if there is no answer! _Udp.read(_packetBuffer,20); _sessionID = _packetBuffer[15]; // Send connectAnswerString to ATEM: _Udp.beginPacket(_switcherIP, 9910); // TODO: Describe packet contents according to rev.eng. API byte connectHelloAnswerString[] = { 0x80, 0x0c, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00 }; _Udp.write(connectHelloAnswerString,12); _Udp.endPacket(); } /** * Keeps connection to the switcher alive - basically, this means answering back to ping packages. * Therefore: Call this in the Arduino loop() function and make sure it gets call at least 2 times a second * Other recommendations might come up in the future. */ void ATEM::runLoop() { // WARNING: // It can cause severe timing problems using "slow" functions such as Serial.print*() // in the runloop, in particular during "boot" where the ATEM delivers some 10-20 kbytes of system status info which // must exit the RX-buffer quite fast. Therefore, using Serial.print for debugging in this // critical phase will in it self affect program execution! // Limit of the RX buffer of the Ethernet interface is another general issue. // When ATEM sends the initial system status packets (10-20 kbytes), they are sent with a few microseconds in between // The RX buffer of the Ethernet interface on Arduino simply does not have the kapacity to take more than 2k at a time. // This means, that we only receive the first packet, the others seems to be discarded. Luckily most information we like to // know about is in the first packet (and some in the second, the rest is probably thumbnails for the media player). // It may be possible to bump up this buffer to 4 or 8 k by simply re-configuring the amount of allowed sockets on the interface. // For some more information from a guy seemingly having a similar issue, look here: // http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1282170842 // If there's data available, read a packet uint16_t packetSize = _Udp.parsePacket(); if (_Udp.available() && packetSize !=0) { // Read packet header of 12 bytes: _Udp.read(_packetBuffer, 12); // Read out packet length (first word), remote packet ID number and "command": uint16_t packetLength = word(_packetBuffer[0] & B00000111, _packetBuffer[1]); _lastRemotePacketID = word(_packetBuffer[10],_packetBuffer[11]); uint8_t command = _packetBuffer[0] & B11111000; boolean command_ACK = command & B00001000 ? true : false; // If true, ATEM expects an acknowledgement answer back! // The five bits in "command" (from LSB to MSB): // 1 = ACK, "Please respond to this packet" (using the _lastRemotePacketID). Exception: The initial 10-20 kbytes of Switcher status // 2 = ?. Set during initialization? (first hand-shake packets contains that) // 3 = "This is a retransmission". You will see this bit set if the ATEM switcher did not get a timely response to a packet. // 4 = ? ("hello packet" according to "ratte", forum at atemuser.com) // 5 = "This is a response on your request". So set this when answering... if (packetSize==packetLength) { // Just to make sure these are equal, they should be! // If a packet is 12 bytes long it indicates that all the initial information // has been delivered from the ATEM and we can begin to answer back on every request // Currently we don't know any other way to decide if an answer should be sent back... if(!_hasInitialized && packetSize == 12) { _hasInitialized = true; if (_serialOutput) Serial.println("_hasInitialized=TRUE"); } if (packetLength > 12) { _parsePacket(packetLength); } // If we are initialized, lets answer back no matter what: // TODO: "_hasInitialized && " should be inserted back before "command_ACK" but // with Arduino 1.0 UDP library it has proven MORE likely that the initial // connection is made if we ALWAYS answer the switcher back. // Apparently the initial "chaos" of keeping up with the incoming data confuses // the UDP library so that we might never get initialized - and thus never get connected // So... for now this is how we do it: if (command_ACK) { if (_serialOutput) { Serial.print("ACK, rpID: "); Serial.println(_lastRemotePacketID, DEC); } _sendAnswerPacket(_lastRemotePacketID); } } else { if (_serialOutput) { Serial.print("ERROR: Packet size mismatch: "); Serial.print(packetSize, DEC); Serial.print(" != "); Serial.println(packetLength, DEC); } // Flushing the buffer: // TODO: Other way? _Udp.flush() ?? while(_Udp.available()) { _Udp.read(_packetBuffer, 96); } } } } /** * If a package longer than a normal acknowledgement is received from the ATEM Switcher we must read through the contents. * Usually such a package contains updated state information about the mixer * Selected information is extracted in this function and transferred to internal variables in this library. */ void ATEM::_parsePacket(uint16_t packetLength) { uint8_t idx; // General reusable index usable for keyers, mediaplayer etc below. // If packet is more than an ACK packet (= if its longer than 12 bytes header), lets parse it: uint16_t indexPointer = 12; while (indexPointer < packetLength) { // Read the length of segment (first word): _Udp.read(_packetBuffer, 2); uint16_t cmdLength = word(0, _packetBuffer[1]); // If length of segment fits into buffer, lets read it, otherwise throw an error: if (cmdLength>2 && cmdLength<=96) { // Read the rest of the segment: _Udp.read(_packetBuffer, cmdLength-2); // Get the "command string", basically this is the 4 char variable name in the ATEM memory holding the various state values of the system: char cmdStr[] = { _packetBuffer[-2+4], _packetBuffer[-2+5], _packetBuffer[-2+6], _packetBuffer[-2+7], '\0' }; // Extract the specific state information we like to know about: if(strcmp(cmdStr, "PrgI") == 0) { // Program Bus status _ATEM_PrgI = _packetBuffer[-2+8+1]; if (_serialOutput) Serial.print("Program Bus: "); if (_serialOutput) Serial.println(_ATEM_PrgI, DEC); } else if(strcmp(cmdStr, "PrvI") == 0) { // Preview Bus status _ATEM_PrvI = _packetBuffer[-2+8+1]; if (_serialOutput) Serial.print("Preview Bus: "); if (_serialOutput) Serial.println(_packetBuffer[-2+8+1], DEC); } else if(strcmp(cmdStr, "TlIn") == 0) { // Tally status for inputs 1-8 // Inputs 1-8, bit 0 = Prg tally, bit 1 = Prv tally. Both can be set simultaneously. _ATEM_TlIn[0] = _packetBuffer[-2+8+2]; _ATEM_TlIn[1] = _packetBuffer[-2+8+3]; _ATEM_TlIn[2] = _packetBuffer[-2+8+4]; _ATEM_TlIn[3] = _packetBuffer[-2+8+5]; _ATEM_TlIn[4] = _packetBuffer[-2+8+6]; _ATEM_TlIn[5] = _packetBuffer[-2+8+7]; _ATEM_TlIn[6] = _packetBuffer[-2+8+8]; _ATEM_TlIn[7] = _packetBuffer[-2+8+9]; if (_serialOutput) Serial.println("Tally updated: "); } else if(strcmp(cmdStr, "Time") == 0) { // Time. What is this anyway? } else if(strcmp(cmdStr, "TrPr") == 0) { // Transition Preview _ATEM_TrPr = _packetBuffer[-2+8+1] > 0 ? true : false; if (_serialOutput) Serial.print("Transition Preview: "); if (_serialOutput) Serial.println(_ATEM_TrPr, BIN); } else if(strcmp(cmdStr, "TrPs") == 0) { // Transition Position _ATEM_TrPs_frameCount = _packetBuffer[-2+8+2]; // Frames count down _ATEM_TrPs_position = _packetBuffer[-2+8+4]*256 + _packetBuffer[-2+8+5]; // Position 0-1000 } else if(strcmp(cmdStr, "TrSS") == 0) { // Transition Style and Keyer on next transition _ATEM_TrSS_KeyersOnNextTransition = _packetBuffer[-2+8+2] & B11111; // Bit 0: Background; Bit 1-4: Key 1-4 if (_serialOutput) Serial.print("Keyers on Next Transition: "); if (_serialOutput) Serial.println(_ATEM_TrSS_KeyersOnNextTransition, BIN); _ATEM_TrSS_TransitionStyle = _packetBuffer[-2+8+1]; if (_serialOutput) Serial.print("Transition Style: "); // 0=MIX, 1=DIP, 2=WIPE, 3=DVE, 4=STING if (_serialOutput) Serial.println(_ATEM_TrSS_TransitionStyle, DEC); } else if(strcmp(cmdStr, "FtbS") == 0) { // Fade To Black State _ATEM_FtbS_frameCount = _packetBuffer[-2+8+2]; // Frames count down } else if(strcmp(cmdStr, "DskS") == 0) { // Downstream Keyer state. Also contains information about the frame count in case of "Auto" idx = _packetBuffer[-2+8+0]; if (idx >=0 && idx <=1) { _ATEM_DskOn[idx] = _packetBuffer[-2+8+1] > 0 ? true : false; if (_serialOutput) Serial.print("Dsk Keyer "); if (_serialOutput) Serial.print(idx+1); if (_serialOutput) Serial.print(": "); if (_serialOutput) Serial.println(_ATEM_DskOn[idx], BIN); } } else if(strcmp(cmdStr, "DskP") == 0) { // Downstream Keyer Tie idx = _packetBuffer[-2+8+0]; if (idx >=0 && idx <=1) { _ATEM_DskTie[idx] = _packetBuffer[-2+8+1] > 0 ? true : false; if (_serialOutput) Serial.print("Dsk Keyer"); if (_serialOutput) Serial.print(idx+1); if (_serialOutput) Serial.print(" Tie: "); if (_serialOutput) Serial.println(_ATEM_DskTie[idx], BIN); } } else if(strcmp(cmdStr, "KeOn") == 0) { // Upstead Keyer on idx = _packetBuffer[-2+8+1]; if (idx >=0 && idx <=3) { _ATEM_KeOn[idx] = _packetBuffer[-2+8+2] > 0 ? true : false; if (_serialOutput) Serial.print("Upstream Keyer "); if (_serialOutput) Serial.print(idx+1); if (_serialOutput) Serial.print(": "); if (_serialOutput) Serial.println(_ATEM_KeOn[idx], BIN); } } else if(strcmp(cmdStr, "ColV") == 0) { // Color Generator Change // Todo: Relatively easy: 8 bytes, first is the color generator, the last 6 is hsl words } else if(strcmp(cmdStr, "MPCE") == 0) { // Media Player Clip Enable idx = _packetBuffer[-2+8+0]; if (idx >=0 && idx <=1) { _ATEM_MPType[idx] = _packetBuffer[-2+8+1]; _ATEM_MPStill[idx] = _packetBuffer[-2+8+2]; _ATEM_MPClip[idx] = _packetBuffer[-2+8+3]; } } else if(strcmp(cmdStr, "AuxS") == 0) { // Aux Output Source uint8_t auxInput = _packetBuffer[-2+8+0]; if (auxInput >=0 && auxInput <=2) { _ATEM_AuxS[auxInput] = _packetBuffer[-2+8+1]; if (_serialOutput) Serial.print("Aux "); if (_serialOutput) Serial.print(auxInput+1); if (_serialOutput) Serial.print(" Output: "); if (_serialOutput) Serial.println(_ATEM_AuxS[auxInput], DEC); } } else if (_hasInitialized){ // All the rest... if (_serialOutput) { Serial.print("???? Unknown token: "); Serial.print(cmdStr); Serial.print(" : "); } for(uint8_t a=(-2+8);a0 ? true : false; } boolean ATEM::getPreviewTally(uint8_t inputNumber) { // TODO: Validate that input number exists on current model! 1-8 at the moment. return (_ATEM_TlIn[inputNumber-1] & 2) >0 ? true : false; } /******************************** * * ATEM Switcher Change methods * Asks the switcher to changes something * ********************************/ void ATEM::changeProgramInput(uint8_t inputNumber) { // TODO: Validate that input number exists on current model! // On ATEM 1M/E: Black (0), 1 (1), 2 (2), 3 (3), 4 (4), 5 (5), 6 (6), 7 (7), 8 (8), Bars (9), Color1 (10), Color 2 (11), Media 1 (12), Media 2 (14) uint8_t commandBytes[4] = {0, inputNumber, 0, 0}; _sendCommandPacket("CPgI", commandBytes, 4); } void ATEM::changePreviewInput(uint8_t inputNumber) { // TODO: Validate that input number exists on current model! uint8_t commandBytes[4] = {0, inputNumber, 0, 0}; _sendCommandPacket("CPvI", commandBytes, 4); } void ATEM::doCut() { uint8_t commandBytes[4] = {0, 0xef, 0xbf, 0x5f}; // I don't know what that actually means... _sendCommandPacket("DCut", commandBytes, 4); } void ATEM::doAuto() { uint8_t commandBytes[4] = {0, 0x32, 0x16, 0x02}; // I don't know what that actually means... _sendCommandPacket("DAut", commandBytes, 4); } void ATEM::fadeToBlackActivate() { uint8_t commandBytes[4] = {0x00, 0x02, 0x58, 0x99}; _sendCommandPacket("FtbA", commandBytes, 4); // Reflected back from ATEM in "FtbS" } void ATEM::changeTransitionPosition(word value) { if (value>0 && value<=1000) { uint8_t commandBytes[4] = {0, 0xe4, value/256, value%256}; _sendCommandPacket("CTPs", commandBytes, 4); // Change Transition Position (CTPs) } } void ATEM::changeTransitionPositionDone() { // When the last value of the transition is sent (1000), send this one too (we are done, change tally lights and preview bus!) uint8_t commandBytes[4] = {0, 0xf6, 0, 0}; // Done _sendCommandPacket("CTPs", commandBytes, 4); // Change Transition Position (CTPs) } void ATEM::changeTransitionPreview(bool state) { uint8_t commandBytes[4] = {0x00, state ? 0x01 : 0x00, 0x00, 0x00}; _sendCommandPacket("CTPr", commandBytes, 4); // Reflected back from ATEM in "TrPr" } void ATEM::changeTransitionType(uint8_t type) { if (type>=0 && type<=4) { // 0=MIX, 1=DIP, 2=WIPE, 3=DVE, 4=STING uint8_t commandBytes[4] = {0x01, 0x00, type, 0x02}; _sendCommandPacket("CTTp", commandBytes, 4); // Reflected back from ATEM in "TrSS" } } void ATEM::changeUpstreamKeyOn(uint8_t keyer, bool state) { if (keyer>=1 && keyer<=4) { // Todo: Should match available keyers depending on model? uint8_t commandBytes[4] = {0x00, keyer-1, state ? 0x01 : 0x00, 0x90}; _sendCommandPacket("CKOn", commandBytes, 4); // Reflected back from ATEM in "KeOn" } } void ATEM::changeUpstreamKeyNextTransition(uint8_t keyer, bool state) { // Not supporting "Background" if (keyer>=1 && keyer<=4) { // Todo: Should match available keyers depending on model? uint8_t stateValue = _ATEM_TrSS_KeyersOnNextTransition; if (state) { stateValue = stateValue | (B10 << (keyer-1)); } else { stateValue = stateValue & (~(B10 << (keyer-1))); } // TODO: Requires internal storage of state here so we can preserve all other states when changing the one we want to change. uint8_t commandBytes[4] = {0x02, 0x00, 0x6a, stateValue & B11111}; _sendCommandPacket("CTTp", commandBytes, 4); // Reflected back from ATEM in "TrSS" } } void ATEM::changeDownstreamKeyOn(uint8_t keyer, bool state) { if (keyer>=1 && keyer<=2) { // Todo: Should match available keyers depending on model? uint8_t commandBytes[4] = {keyer-1, state ? 0x01 : 0x00, 0xff, 0xff}; _sendCommandPacket("CDsL", commandBytes, 4); // Reflected back from ATEM in "DskP" and "DskS" } } void ATEM::changeDownstreamKeyTie(uint8_t keyer, bool state) { if (keyer>=1 && keyer<=2) { // Todo: Should match available keyers depending on model? uint8_t commandBytes[4] = {keyer-1, state ? 0x01 : 0x00, 0xff, 0xff}; _sendCommandPacket("CDsT", commandBytes, 4); } } void ATEM::doAutoDownstreamKeyer(uint8_t keyer) { if (keyer>=1 && keyer<=2) { // Todo: Should match available keyers depending on model? uint8_t commandBytes[4] = {keyer-1, 0x32, 0x16, 0x02}; // I don't know what that actually means... _sendCommandPacket("DDsA", commandBytes, 4); } } void ATEM::changeAuxState(uint8_t auxOutput, uint8_t inputNumber) { // TODO: Validate that input number exists on current model! // On ATEM 1M/E: Black (0), 1 (1), 2 (2), 3 (3), 4 (4), 5 (5), 6 (6), 7 (7), 8 (8), Bars (9), Color1 (10), Color 2 (11), Media 1 (12), Media 1 Key (13), Media 2 (14), Media 2 Key (15), Program (16), Preview (17), Clean1 (18), Clean 2 (19) if (auxOutput>=1 && auxOutput<=3) { // Todo: Should match available aux outputs uint8_t commandBytes[4] = {auxOutput-1, inputNumber, 0, 0}; _sendCommandPacket("CAuS", commandBytes, 4); } } void ATEM::settingsMemorySave() { uint8_t commandBytes[4] = {0, 0, 0, 0}; _sendCommandPacket("SRsv", commandBytes, 4); } void ATEM::settingsMemoryClear() { uint8_t commandBytes[4] = {0, 0, 0, 0}; _sendCommandPacket("SRcl", commandBytes, 4); } void ATEM::changeColorValue(uint8_t colorGenerator, uint16_t hue, uint16_t saturation, uint16_t lightness) { if (colorGenerator>=1 && colorGenerator<=2 && hue>=0 && hue<=3600 && saturation >=0 && saturation <=1000 && lightness >=0 && lightness <= 1000 ) { // Todo: Should match available aux outputs uint8_t commandBytes[8] = {0x07, colorGenerator-1, highByte(hue), lowByte(hue), highByte(saturation), lowByte(saturation), highByte(lightness), lowByte(lightness) }; _sendCommandPacket("CClV", commandBytes, 8); } } void ATEM::mediaPlayerSelectSource(uint8_t mediaPlayer, boolean movieclip, uint8_t sourceIndex) { if (mediaPlayer>=1 && mediaPlayer<=2) { // TODO: Adjust to particular ATEM model... (here 1M/E) uint8_t commandBytes[12]; memset(commandBytes, 0, 12); commandBytes[1] = mediaPlayer-1; if (movieclip) { commandBytes[0] = 4; if (sourceIndex>=1 && sourceIndex<=2) { commandBytes[4] = sourceIndex-1; } } else { commandBytes[0] = 2; if (sourceIndex>=1 && sourceIndex<=32) { commandBytes[3] = sourceIndex-1; } } commandBytes[9] = 0x10; _sendCommandPacket("MPSS", commandBytes, 12); // For some reason you have to send this command immediate after (or in fact it could be in the same packet) // If not done, the clip will not change if there is a shift from stills to clips or vice versa. uint8_t commandBytes2[8] = {0x01, mediaPlayer-1, movieclip?2:1, 0xbf, movieclip?0x96:0xd5, 0xb6, 0x04, 0}; _sendCommandPacket("MPSS", commandBytes2, 8); } }