├── LICENSE ├── README.md └── visca-proxy-rs232 └── visca-proxy-rs232.ino /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Utopian Tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # visca-ip-proxy-arduino 2 | Uses a WiFi enabled microcontroller to proxy VISCA commands from UDP to RS-232 and vice versa. 3 | 4 | The VISCA binary protocol is exactly the same over UDP as it is over wired connections. 5 | However there are exceptions to this rule. When using VISCA over IP, Sony cameras employ 6 | a header and footer surrounding each datagram. However, over a wired connection, they 7 | do not use such a header and footer, and only send the VISCA packet directly. 8 | 9 | This code works more like the PTZ Optics implementation. 10 | 11 | This code connects to WiFi and starts a UDP server. 12 | This code also connects to a VISCA camera over RS-232. 13 | On bootup, it will immediately send to the camera an ADDR_SET and POWER_ON command. 14 | 15 | From that point on, the code will simply proxy all data packets between RS-232 and UDP 16 | with very little error checking or validation. Whatever packet is received over UDP 17 | will be sent to the camera without modification, and whatever packet is received from 18 | the camera over RS-232 will be sent to the most recent UDP client without modification. 19 | 20 | WITH A FEW EXCEPTIONS: 21 | Cameras and controllers vary in their implementation of speed, and therefore this code 22 | allows the programmer to customize various speed curves for the PTZ operations. 23 | 24 | They are commented below. 25 | 26 | USAGE: 27 | Change the settings below, compile and upload! 28 | 29 | 30 | FINALLY: 31 | This code should work with minor modifications on any device that supports WiFi, RS-232, 32 | but it was designed for the M5Stack Atom series devices with the RS-232 attachment. 33 | 34 | As an added bonus, the Atom and Atom Matrix devices have RGB LEDs which allow us to 35 | use them as tally lights. This code employs that using the vMix tally protocol. If it 36 | successfully connects to vMix, it will by default make itself tally whatever input is 37 | live when it booted. You can change the tally by pressing the main button on the Atom 38 | or the Matrix. If you hold down the button, it will automatically set itself to whatever 39 | input is PREVIEW. 40 | 41 | NOTE: An RS-232 or RS-422 interface is definitely needed. The biggest concern is that 42 | Android devices by default use TTL for serial communications, and that protocol uses 43 | voltages that are incompatible with the RS standards. 44 | 45 | -------------------------------------------------------------------------------- /visca-proxy-rs232/visca-proxy-rs232.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "AsyncUDP.h" 4 | #include "M5Atom.h" 5 | 6 | /* 7 | * The VISCA binary protocol is exactly the same over UDP as it is over wired connections. 8 | * However there are exceptions to this rule. When using VISCA over IP, Sony cameras employ 9 | * a header and footer surrounding each datagram. However, over a wired connection, they 10 | * do not use such a header and footer, and only send the VISCA packet directly. 11 | * 12 | * This code works more like the PTZ Optics implementation. 13 | * 14 | * This code connects to WiFi and starts a UDP server. 15 | * This code also connects to a VISCA camera over RS-232. 16 | * On bootup, it will immediately send to the camera an ADDR_SET and POWER_ON command. 17 | * 18 | * From that point on, the code will simply proxy all data packets between RS-232 and UDP 19 | * with very little error checking or validation. Whatever packet is received over UDP 20 | * will be sent to the camera without modification, and whatever packet is received from 21 | * the camera over RS-232 will be sent to the most recent UDP client without modification. 22 | * 23 | * WITH A FEW EXCEPTIONS: 24 | * Cameras and controllers vary in their implementation of speed, and therefore this code 25 | * allows the programmer to customize various speed curves for the PTZ operations. 26 | * 27 | * They are commented below. 28 | * 29 | * USAGE: 30 | * Change the settings below, compile and upload! 31 | * 32 | * 33 | * FINALLY: 34 | * This code should work with minor modifications on any device that supports WiFi, RS-232, 35 | * but it was designed for the M5Stack Atom series devices with the RS-232 attachment. 36 | * 37 | * As an added bonus, the Atom and Atom Matrix devices have RGB LEDs which allow us to 38 | * use them as tally lights. This code employs that using the vMix tally protocol. If it 39 | * successfully connects to vMix, it will by default make itself tally whatever input is 40 | * live when it booted. You can change the tally by pressing the main button on the Atom 41 | * or the Matrix. If you hold down the button, it will automatically set itself to whatever 42 | * input is PREVIEW. 43 | * 44 | * NOTE: An RS-232 or RS-422 interface is definitely needed. The biggest concern is that 45 | * Android devices by default use TTL for serial communications, and that protocol uses 46 | * voltages that are incompatible with the RS standards. 47 | */ 48 | 49 | // uncomment this line if you are using an M5Stack Matrix 50 | // comment it if you are using an M5Stack Atom 51 | #define MATRIX 52 | 53 | 54 | // WIFI SETTINGS 55 | const char *SSID = "YOUR_SSID"; 56 | const char *PASSWORD = "your password"; 57 | 58 | // UDP SETTINGS 59 | const int udp_port = 52381; 60 | 61 | // RS232 Serial Settings 62 | const int txpin = 19; 63 | const int rxpin = 22; 64 | 65 | // set the baudrate to match that of the VISCA camera 66 | const int baudrate = 38400; 67 | 68 | // Use the following constants and functions to modify the speed of PTZ commands 69 | double ZOOMMULT = 0.3; // speed multiplier for the zoom functions 70 | double ZOOMEXP = 1.5; // exponential curve for the speed modification 71 | double PTZMULT = 0.3; // speed multiplier for the pan and tilt functions 72 | double PTZEXP = 1.0; // exponential curve for the speed modification 73 | 74 | // TALLY SETTINGS 75 | const IPAddress VMIX_IP = IPAddress(192, 168, 50, 12); 76 | const int VMIX_PORT = 8099; 77 | 78 | 79 | 80 | // STATE VARIABLES 81 | bool wifi_connected = false; 82 | bool ignore_button = false; 83 | 84 | IPAddress ip; 85 | WiFiClient tcp; 86 | AsyncUDP udp; 87 | int lastclientport = 0; 88 | IPAddress lastclientip; 89 | bool pwr_is_on = false; 90 | 91 | // memory buffers for VISCA commands 92 | size_t lastudp_len = 0; 93 | uint8_t lastudp_in[16]; 94 | size_t lastser_len = 0; 95 | uint8_t lastser_in[16]; 96 | 97 | // quick use VISCA commands 98 | const uint8_t pwr_on[] = {0x81, 0x01, 0x04, 0x00, 0x02, 0xff}; 99 | const uint8_t pwr_off[] = {0x81, 0x01, 0x04, 0x00, 0x03, 0xff}; 100 | const uint8_t addr_set[] = {0x88, 0x30, 0x01, 0xff}; // address set 101 | const uint8_t if_clear[] = {0x88, 0x01, 0x00, 0x01, 0xff}; // if clear 102 | 103 | // quick color constants 104 | // yes, on the Atom and Matrix devices 105 | // the red and green LEDs seem to be swapped 106 | const CRGB blue = CRGB(0x00, 0x00, 0xf0); 107 | const CRGB red = CRGB(0x00, 0xf0, 0x00); 108 | const CRGB green = CRGB(0xf0, 0x00, 0x00); 109 | const CRGB yellow = CRGB(0xf0, 0xf0, 0x00); 110 | const CRGB black = CRGB(0x00, 0x00, 0x00); 111 | CRGB ledcolor = yellow; 112 | 113 | // tally variables 114 | int total_inputs = 2; // vMix always loads with at least two inputs defined 115 | int tally_input = 1; // for mental consistency, we use input numbers based on 1 like vMix does 116 | int tally_status = 0; // 0: safe, 1: live, 2: preview 117 | const int tally_bufsize = 100; // vMix can have a lot of inputs 118 | uint8_t tb[tally_bufsize + 1]; // so there is always a null byte at the end... for printing 119 | 120 | 121 | // general helper function definitions 122 | double zoomcurve(int v) 123 | { 124 | return ZOOMMULT * pow(v, ZOOMEXP); 125 | } 126 | 127 | double ptzcurve(int v) 128 | { 129 | return PTZMULT * pow(v, PTZEXP); 130 | } 131 | 132 | void debug(char c) 133 | { 134 | Serial.print(c); 135 | } 136 | 137 | void debug(int n, int base) 138 | { 139 | Serial.print(n, base); 140 | Serial.print(' '); 141 | } 142 | 143 | void debug(uint8_t *buf, int len) 144 | { 145 | for (uint8_t i = 0; i < len; i++) 146 | { 147 | uint8_t elem = buf[i]; 148 | debug(elem, HEX); 149 | } 150 | } 151 | 152 | // turn on the whole screen if we have a MATRIX 153 | // otherwise, just light the single LED if we are an Atom 154 | // and remember the color just set 155 | void led(CRGB c) 156 | { 157 | #ifdef MATRIX 158 | for (int i = 0; i < 25; i++) 159 | { 160 | M5.dis.drawpix(i, c); 161 | // Serial.print(i); 162 | } 163 | M5.dis.setBrightness(8); 164 | #else 165 | M5.dis.drawpix(0, c); 166 | #endif 167 | ledcolor = c; 168 | } 169 | 170 | 171 | void send_bytes(uint8_t *b, int len) 172 | { 173 | for (int i = 0; i < len; i++) 174 | { 175 | uint8_t elem = b[i]; 176 | debug(elem); 177 | Serial1.write(elem); 178 | } 179 | } 180 | 181 | void send_visca(uint8_t *c, size_t len) 182 | { 183 | int i = 0; 184 | uint8_t elem; 185 | do 186 | { 187 | elem = c[i++]; 188 | Serial1.write(elem); 189 | } while (i < len && elem != 0xff); 190 | Serial.println(""); 191 | } 192 | 193 | // this function assumes you are sending 194 | // a valid visca command that endsend with a 0xff 195 | // This function will just keep sending bytes from 196 | // memory until it sees a 0xff, so be careful! 197 | void send_visca(const uint8_t *c) 198 | { 199 | int i = 0; 200 | uint8_t elem; 201 | do 202 | { 203 | elem = c[i++]; 204 | Serial1.write(elem); 205 | } while (elem != 0xff); 206 | Serial.println(""); 207 | } 208 | 209 | void visca_power(bool turnon) 210 | { 211 | if (turnon) 212 | { 213 | send_visca(addr_set); 214 | delay(500); 215 | send_visca(pwr_on); 216 | delay(2000); 217 | send_visca(if_clear); 218 | } 219 | else 220 | { 221 | send_visca(if_clear); 222 | delay(2000); 223 | send_visca(pwr_off); 224 | } 225 | pwr_is_on = turnon; 226 | } 227 | 228 | // for debugging purposes only 229 | // prints the current status to the debug serial port 230 | void status() 231 | { 232 | if (wifi_connected) 233 | { 234 | Serial.println("-- UDP LISTENING --"); 235 | Serial.print(WiFi.localIP()); 236 | Serial.print(":"); 237 | Serial.println(udp_port); 238 | Serial.print("tx: G"); 239 | Serial.print(txpin); 240 | Serial.print(" | rx: G"); 241 | Serial.println(rxpin); 242 | } 243 | else 244 | { 245 | Serial.println("CONNECTING ..."); 246 | Serial.println(SSID); 247 | Serial.println(PASSWORD); 248 | } 249 | } 250 | 251 | 252 | 253 | // FUNCTIONS TO RECEIVE VISCA OVER UDP ============= 254 | 255 | // this processes visca commands received over UDP 256 | // and decides if they need to be modified before 257 | // passing through to the camera. 258 | void handle_visca(uint8_t *buf, size_t len) 259 | { 260 | uint8_t modified[16]; 261 | size_t lastelement = 0; 262 | for (int i = 0; (i < len && i < 16); i++) 263 | { 264 | modified[i] = buf[i]; 265 | lastelement = i; 266 | } 267 | 268 | // is this a PTZ? 269 | if (modified[1] == 0x01 && modified[2] == 0x06 && modified[3] == 0x01) 270 | { 271 | Serial.println("PTZ CONTROL DETECTED... ADJUSTING SPEED"); 272 | modified[4] = (int)ptzcurve(modified[4]); 273 | modified[5] = (int)ptzcurve(modified[5]); 274 | } 275 | if (modified[1] == 0x01 && modified[2] == 0x04 && modified[3] == 0x07) 276 | { 277 | Serial.println("ZOOM CONTROL DETECTED, ADJUSTING SPEED"); 278 | int zoomspeed = modified[4] & 0b00001111; 279 | zoomspeed = (int)zoomcurve(zoomspeed); 280 | int zoomval = (modified[4] & 0b11110000) + zoomspeed; 281 | modified[4] = zoomval; 282 | } 283 | 284 | Serial1.write(modified, lastelement + 1); 285 | } 286 | 287 | void start_server() 288 | { 289 | Serial.print("Starting UDP server on port: "); 290 | Serial.println(udp_port); 291 | udp.close(); // will close only if needed 292 | if (udp.listen(udp_port)) 293 | { 294 | Serial.println("Server is Running!"); 295 | udp.onPacket([](AsyncUDPPacket packet) { 296 | CRGB oldc = ledcolor; 297 | led(yellow); 298 | 299 | // debug(packet); 300 | lastclientip = packet.remoteIP(); 301 | lastclientport = packet.remotePort(); 302 | 303 | Serial.print("Type of UDP datagram: "); 304 | Serial.print(packet.isBroadcast() ? "Broadcast" : packet.isMulticast() ? "Multicast" 305 | : "Unicast"); 306 | Serial.print(", Sender: "); 307 | Serial.print(lastclientip); 308 | Serial.print(":"); 309 | Serial.print(lastclientport); 310 | Serial.print(", Receiver: "); 311 | Serial.print(packet.localIP()); 312 | Serial.print(":"); 313 | Serial.print(packet.localPort()); 314 | Serial.print(", Message length: "); 315 | Serial.print(packet.length()); 316 | Serial.print(", Payload (hex):"); 317 | debug(packet.data(), packet.length()); 318 | Serial.println(); 319 | 320 | handle_visca(packet.data(), packet.length()); 321 | led(oldc); 322 | }); 323 | } 324 | else 325 | { 326 | Serial.println("Server failed to start"); 327 | } 328 | } 329 | 330 | 331 | // FUNCTIONS TO RECEIVE TALLY DATA OVER TCP 332 | void start_tally() 333 | { 334 | Serial.println("Starting Tally Listener"); 335 | tcp.connect(VMIX_IP, VMIX_PORT); 336 | tcp.write("SUBSCRIBE TALLY\r\n"); 337 | } 338 | int read_tally(int input) { return (tb[8 + input]) - 48; } 339 | int read_tally() { return read_tally(tally_input); } 340 | 341 | void update_tally() 342 | { 343 | tally_status = read_tally(); // ASCII NUMBERS 0-9 ARE ENCODED 48-57 (0x30-0x39) 344 | 345 | switch (tally_status) 346 | { 347 | case 0: 348 | led(black); 349 | break; 350 | case 1: 351 | led(red); 352 | break; 353 | case 2: 354 | led(green); 355 | break; 356 | } 357 | } 358 | 359 | // the TCP socket is read synchronously because I'm not that 360 | // good at Arduino programming yet. 361 | void check_tally() 362 | { 363 | int avail = tcp.available(); 364 | if (avail == 0) 365 | return; 366 | 367 | Serial.println("TCP data in buffer!"); 368 | // TALLY DATA ALWAYS LOOKS LIKE THIS: 369 | // TALLY OK 120001\r\n 370 | // (encoded as ASCII) 371 | // input x will be at buffer index 8 + x (because inputs are 1-based) 372 | // total inputs will be avail - 9 373 | int inputs = avail - 11; // the data will be terminated with \r\n 374 | if (inputs > 1 && total_inputs != inputs) 375 | { 376 | Serial.print("INPUT COUNT HAS CHANGED TO: "); 377 | Serial.println(inputs); 378 | total_inputs = inputs; 379 | } 380 | 381 | // vMix terminates messages with \r\n 382 | // readBytesUntil will discard the terminator char 383 | // but we also want to discard the \r before it 384 | size_t read = tcp.readBytesUntil('\n', tb, tally_bufsize); // never read more than the tally_bufsize 385 | if (read == 0) 386 | return; 387 | 388 | debug(tb, read); 389 | Serial.println(""); 390 | tb[read - 1] = 0; // convert the \r to 0 or at least make sure the final char has a null 391 | Serial.println((char *)tb); // since there's a null at the end, we can print the buffer like a string 392 | 393 | update_tally(); 394 | } 395 | 396 | 397 | 398 | // FUNCTIONS TO HANDLE WiFi EVENTS 399 | 400 | void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info) 401 | { 402 | led(green); 403 | ip = info.got_ip.ip_info.ip.addr; 404 | Serial.println("WiFi Connected!"); 405 | Serial.println(ip); 406 | wifi_connected = true; 407 | start_server(); // will stop any previous servers 408 | start_tally(); 409 | } 410 | 411 | void WiFiDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) 412 | { 413 | ip = IPAddress(0, 0, 0, 0); 414 | wifi_connected = false; 415 | } 416 | 417 | 418 | // FUNCTION TO READ DATA SENT FROM THE CAMERA 419 | // the RS-232 port is read synchronously because I'm not that 420 | // good at Arduino programming yet. 421 | // check if we have received data over the serial port 422 | void check_serial() 423 | { 424 | int available = Serial1.available(); 425 | while (available > 0) 426 | { 427 | led(yellow); 428 | Serial.println("Data available on Serial1"); 429 | int actual = Serial1.readBytesUntil(0xff, lastser_in, available); // does not include the terminator char 430 | if (actual < 16) 431 | { 432 | lastser_in[actual] = 0xff; 433 | actual++; 434 | } 435 | debug(lastser_in, actual); 436 | if (lastclientport > 0) 437 | udp.writeTo(lastser_in, actual, lastclientip, lastclientport); 438 | Serial.println(""); 439 | available = Serial1.available(); 440 | update_tally(); 441 | } 442 | } 443 | 444 | 445 | 446 | // put your setup code here, to run once: 447 | void setup() 448 | { 449 | M5.begin(true, true, true); 450 | Serial.begin(baudrate); 451 | 452 | led(yellow); 453 | 454 | Serial.println("started..."); 455 | Serial.println("connecting to WiFi..."); 456 | 457 | // connect to WiFi 458 | WiFi.setAutoConnect(true); 459 | WiFi.onEvent(WiFiGotIP, WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); 460 | WiFi.onEvent(WiFiDisconnected, WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); 461 | WiFi.begin(SSID, PASSWORD, 0, NULL, true); 462 | 463 | // start the visca serial connection 464 | Serial.println("connecting to camera..."); 465 | Serial1.begin(baudrate, SERIAL_8N1, rxpin, txpin, false, 200); 466 | 467 | visca_power(true); 468 | } 469 | 470 | void loop() 471 | { 472 | // HANDLE VARIOUS BUTTON EVENTS 473 | if (M5.Btn.wasPressed()) 474 | { 475 | tally_input = (tally_input % total_inputs) + 1; // mod before add yields 1-indexed values 476 | Serial.println("button!"); 477 | Serial.print("WATCHING INPUT: "); 478 | Serial.println(tally_input); 479 | update_tally(); 480 | } 481 | else if (M5.Btn.pressedFor(750) && !ignore_button) 482 | { 483 | ignore_button = true; 484 | Serial.println("long press button!"); 485 | // find the next active tally by cycling through all the inputs to see 486 | // if any of them are preview 487 | for (int i = 0; i < total_inputs; i++) 488 | { 489 | tally_input = (tally_input % total_inputs) + 1; // mod before add yields 1-indexed values 490 | if (read_tally() == 2) 491 | break; 492 | } 493 | Serial.print("WATCHING INPUT: "); 494 | Serial.println(tally_input); 495 | update_tally(); 496 | start_tally(); 497 | } 498 | else if (M5.Btn.wasReleased()) 499 | { 500 | ignore_button = false; 501 | } 502 | 503 | // blink if no wifi connection 504 | if (!wifi_connected) 505 | { 506 | led(black); 507 | delay(100); 508 | led(yellow); 509 | delay(100); 510 | return; 511 | } 512 | 513 | check_serial(); 514 | check_tally(); 515 | 516 | // don't overload the CPU! take a breather 517 | delay(100); 518 | Serial.print('.'); 519 | M5.update(); // reads the button again among other things. 520 | } 521 | --------------------------------------------------------------------------------