001/*----------------------------------------------------------------------------*/ 002/* Copyright (c) FIRST 2017-2018. All Rights Reserved. */ 003/* Open Source Software - may be modified and shared by FRC teams. The code */ 004/* must be accompanied by the FIRST BSD license file in the root directory of */ 005/* the project. */ 006/*----------------------------------------------------------------------------*/ 007 008package edu.wpi.first.networktables; 009 010import java.util.HashMap; 011import java.util.Map; 012import java.util.concurrent.ConcurrentHashMap; 013import java.util.concurrent.ConcurrentMap; 014import java.util.concurrent.TimeUnit; 015import java.util.concurrent.locks.Condition; 016import java.util.concurrent.locks.ReentrantLock; 017import java.util.function.Consumer; 018 019/** 020 * NetworkTables Instance. 021 * 022 * Instances are completely independent from each other. Table operations on 023 * one instance will not be visible to other instances unless the instances are 024 * connected via the network. The main limitation on instances is that you 025 * cannot have two servers on the same network port. The main utility of 026 * instances is for unit testing, but they can also enable one program to 027 * connect to two different NetworkTables networks. 028 * 029 * The global "default" instance (as returned by {@link #getDefault()}) is 030 * always available, and is intended for the common case when there is only 031 * a single NetworkTables instance being used in the program. 032 * 033 * Additional instances can be created with the {@link #create()} function. 034 * A reference must be kept to the NetworkTableInstance returned by this 035 * function to keep it from being garbage collected. 036 */ 037public final class NetworkTableInstance { 038 /** 039 * Client/server mode flag values (as returned by {@link #getNetworkMode()}). 040 * This is a bitmask. 041 */ 042 public static final int kNetModeNone = 0x00; 043 public static final int kNetModeServer = 0x01; 044 public static final int kNetModeClient = 0x02; 045 public static final int kNetModeStarting = 0x04; 046 public static final int kNetModeFailure = 0x08; 047 048 /** 049 * The default port that network tables operates on. 050 */ 051 public static final int kDefaultPort = 1735; 052 053 /** 054 * Construct from native handle. 055 * @param handle Native handle 056 */ 057 private NetworkTableInstance(int handle) { 058 m_owned = false; 059 m_handle = handle; 060 } 061 062 /** 063 * Destroys the instance (if created by {@link #create()}). 064 */ 065 public synchronized void free() { 066 if (m_owned && m_handle != 0) { 067 NetworkTablesJNI.destroyInstance(m_handle); 068 } 069 } 070 071 /** 072 * Determines if the native handle is valid. 073 * @return True if the native handle is valid, false otherwise. 074 */ 075 public boolean isValid() { 076 return m_handle != 0; 077 } 078 079 /* The default instance. */ 080 private static NetworkTableInstance s_defaultInstance; 081 082 /** 083 * Get global default instance. 084 * @return Global default instance 085 */ 086 public synchronized static NetworkTableInstance getDefault() { 087 if (s_defaultInstance == null) { 088 s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance()); 089 } 090 return s_defaultInstance; 091 } 092 093 /** 094 * Create an instance. 095 * Note: A reference to the returned instance must be retained to ensure the 096 * instance is not garbage collected. 097 * @return Newly created instance 098 */ 099 public static NetworkTableInstance create() { 100 NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance()); 101 inst.m_owned = true; 102 return inst; 103 } 104 105 /** 106 * Gets the native handle for the entry. 107 * @return Native handle 108 */ 109 public int getHandle() { 110 return m_handle; 111 } 112 113 /** 114 * Gets the entry for a key. 115 * @param name Key 116 * @return Network table entry. 117 */ 118 public NetworkTableEntry getEntry(String name) { 119 return new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name)); 120 } 121 122 /** 123 * Get entries starting with the given prefix. 124 * The results are optionally filtered by string prefix and entry type to 125 * only return a subset of all entries. 126 * 127 * @param prefix entry name required prefix; only entries whose name 128 * starts with this string are returned 129 * @param types bitmask of types; 0 is treated as a "don't care" 130 * @return Array of entries. 131 */ 132 public NetworkTableEntry[] getEntries(String prefix, int types) { 133 int[] handles = NetworkTablesJNI.getEntries(m_handle, prefix, types); 134 NetworkTableEntry[] entries = new NetworkTableEntry[handles.length]; 135 for (int i = 0; i < handles.length; i++) { 136 entries[i] = new NetworkTableEntry(this, handles[i]); 137 } 138 return entries; 139 } 140 141 /** 142 * Get information about entries starting with the given prefix. 143 * The results are optionally filtered by string prefix and entry type to 144 * only return a subset of all entries. 145 * 146 * @param prefix entry name required prefix; only entries whose name 147 * starts with this string are returned 148 * @param types bitmask of types; 0 is treated as a "don't care" 149 * @return Array of entry information. 150 */ 151 public EntryInfo[] getEntryInfo(String prefix, int types) { 152 return NetworkTablesJNI.getEntryInfo(this, m_handle, prefix, types); 153 } 154 155 /* Cache of created tables. */ 156 private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>(); 157 158 /** 159 * Gets the table with the specified key. 160 * 161 * @param key the key name 162 * @return The network table 163 */ 164 public NetworkTable getTable(String key) { 165 // prepend leading / if not present 166 String theKey; 167 if (key.isEmpty() || key.equals("/")) { 168 theKey = ""; 169 } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) { 170 theKey = key; 171 } else { 172 theKey = NetworkTable.PATH_SEPARATOR + key; 173 } 174 175 // cache created tables 176 NetworkTable table = m_tables.get(theKey); 177 if (table == null) { 178 table = new NetworkTable(this, theKey); 179 NetworkTable oldTable = m_tables.putIfAbsent(theKey, table); 180 if (oldTable != null) { 181 table = oldTable; 182 } 183 } 184 return table; 185 } 186 187 /** 188 * Deletes ALL keys in ALL subtables (except persistent values). 189 * Use with caution! 190 */ 191 public void deleteAllEntries() { 192 NetworkTablesJNI.deleteAllEntries(m_handle); 193 } 194 195 /* 196 * Callback Creation Functions 197 */ 198 199 private static class EntryConsumer<T> { 200 final NetworkTableEntry entry; 201 final Consumer<T> consumer; 202 203 EntryConsumer(NetworkTableEntry entry, Consumer<T> consumer) { 204 this.entry = entry; 205 this.consumer = consumer; 206 } 207 } 208 209 private final ReentrantLock m_entryListenerLock = new ReentrantLock(); 210 private final Map<Integer, EntryConsumer<EntryNotification>> m_entryListeners = new HashMap<>(); 211 private Thread m_entryListenerThread; 212 private int m_entryListenerPoller; 213 private boolean m_entryListenerWaitQueue; 214 private final Condition m_entryListenerWaitQueueCond = m_entryListenerLock.newCondition(); 215 216 private void startEntryListenerThread() { 217 m_entryListenerThread = new Thread(() -> { 218 boolean wasInterrupted = false; 219 while (!Thread.interrupted()) { 220 EntryNotification[] events; 221 try { 222 events = NetworkTablesJNI.pollEntryListener(this, m_entryListenerPoller); 223 } catch (InterruptedException ex) { 224 m_entryListenerLock.lock(); 225 try { 226 if (m_entryListenerWaitQueue) { 227 m_entryListenerWaitQueue = false; 228 m_entryListenerWaitQueueCond.signalAll(); 229 continue; 230 } 231 } finally { 232 m_entryListenerLock.unlock(); 233 } 234 Thread.currentThread().interrupt(); 235 wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid 236 break; 237 } 238 for (EntryNotification event : events) { 239 EntryConsumer<EntryNotification> listener; 240 m_entryListenerLock.lock(); 241 try { 242 listener = m_entryListeners.get(event.listener); 243 } finally { 244 m_entryListenerLock.unlock(); 245 } 246 if (listener != null) { 247 event.entryObject = listener.entry; 248 try { 249 listener.consumer.accept(event); 250 } catch (Throwable throwable) { 251 System.err.println("Unhandled exception during entry listener callback: " + throwable.toString()); 252 throwable.printStackTrace(); 253 } 254 } 255 } 256 } 257 m_entryListenerLock.lock(); 258 try { 259 if (!wasInterrupted) { 260 NetworkTablesJNI.destroyEntryListenerPoller(m_entryListenerPoller); 261 } 262 m_entryListenerPoller = 0; 263 } finally { 264 m_entryListenerLock.unlock(); 265 } 266 }, "NTEntryListener"); 267 m_entryListenerThread.setDaemon(true); 268 m_entryListenerThread.start(); 269 } 270 271 /** 272 * Add a listener for all entries starting with a certain prefix. 273 * 274 * @param prefix UTF-8 string prefix 275 * @param listener listener to add 276 * @param flags {@link EntryListenerFlags} bitmask 277 * @return Listener handle 278 */ 279 public int addEntryListener(String prefix, Consumer<EntryNotification> listener, int flags) { 280 m_entryListenerLock.lock(); 281 try { 282 if (m_entryListenerPoller == 0) { 283 m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle); 284 startEntryListenerThread(); 285 } 286 int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, prefix, flags); 287 m_entryListeners.put(handle, new EntryConsumer<>(null, listener)); 288 return handle; 289 } finally { 290 m_entryListenerLock.unlock(); 291 } 292 } 293 294 /** 295 * Add a listener for a particular entry. 296 * 297 * @param entry the entry 298 * @param listener listener to add 299 * @param flags {@link EntryListenerFlags} bitmask 300 * @return Listener handle 301 */ 302 public int addEntryListener(NetworkTableEntry entry, Consumer<EntryNotification> listener, int flags) { 303 if (!equals(entry.getInstance())) { 304 throw new IllegalArgumentException("entry does not belong to this instance"); 305 } 306 m_entryListenerLock.lock(); 307 try { 308 if (m_entryListenerPoller == 0) { 309 m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle); 310 startEntryListenerThread(); 311 } 312 int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, entry.getHandle(), flags); 313 m_entryListeners.put(handle, new EntryConsumer<>(entry, listener)); 314 return handle; 315 } finally { 316 m_entryListenerLock.unlock(); 317 } 318 } 319 320 /** 321 * Remove an entry listener. 322 * @param listener Listener handle to remove 323 */ 324 public void removeEntryListener(int listener) { 325 NetworkTablesJNI.removeEntryListener(listener); 326 } 327 328 /** 329 * Wait for the entry listener queue to be empty. This is primarily useful 330 * for deterministic testing. This blocks until either the entry listener 331 * queue is empty (e.g. there are no more events that need to be passed along 332 * to callbacks or poll queues) or the timeout expires. 333 * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, 334 * or a negative value to block indefinitely 335 * @return False if timed out, otherwise true. 336 */ 337 public boolean waitForEntryListenerQueue(double timeout) { 338 if (!NetworkTablesJNI.waitForEntryListenerQueue(m_handle, timeout)) { 339 return false; 340 } 341 m_entryListenerLock.lock(); 342 try { 343 if (m_entryListenerPoller != 0) { 344 m_entryListenerWaitQueue = true; 345 NetworkTablesJNI.cancelPollEntryListener(m_entryListenerPoller); 346 while (m_entryListenerWaitQueue) { 347 try { 348 if (timeout < 0) { 349 m_entryListenerWaitQueueCond.await(); 350 } else { 351 return m_entryListenerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS); 352 } 353 } catch (InterruptedException ex) { 354 Thread.currentThread().interrupt(); 355 return true; 356 } 357 } 358 } 359 } finally { 360 m_entryListenerLock.unlock(); 361 } 362 return true; 363 } 364 365 private final ReentrantLock m_connectionListenerLock = new ReentrantLock(); 366 private final Map<Integer, Consumer<ConnectionNotification>> m_connectionListeners = new HashMap<>(); 367 private Thread m_connectionListenerThread; 368 private int m_connectionListenerPoller; 369 private boolean m_connectionListenerWaitQueue; 370 private final Condition m_connectionListenerWaitQueueCond = m_connectionListenerLock.newCondition(); 371 372 private void startConnectionListenerThread() { 373 m_connectionListenerThread = new Thread(() -> { 374 boolean wasInterrupted = false; 375 while (!Thread.interrupted()) { 376 ConnectionNotification[] events; 377 try { 378 events = NetworkTablesJNI.pollConnectionListener(this, m_connectionListenerPoller); 379 } catch (InterruptedException ex) { 380 m_connectionListenerLock.lock(); 381 try { 382 if (m_connectionListenerWaitQueue) { 383 m_connectionListenerWaitQueue = false; 384 m_connectionListenerWaitQueueCond.signalAll(); 385 continue; 386 } 387 } finally { 388 m_connectionListenerLock.unlock(); 389 } 390 Thread.currentThread().interrupt(); 391 wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid 392 break; 393 } 394 for (ConnectionNotification event : events) { 395 Consumer<ConnectionNotification> listener; 396 m_connectionListenerLock.lock(); 397 try { 398 listener = m_connectionListeners.get(event.listener); 399 } finally { 400 m_connectionListenerLock.unlock(); 401 } 402 if (listener != null) { 403 try { 404 listener.accept(event); 405 } catch (Throwable throwable) { 406 System.err.println("Unhandled exception during connection listener callback: " + throwable.toString()); 407 throwable.printStackTrace(); 408 } 409 } 410 } 411 } 412 m_connectionListenerLock.lock(); 413 try { 414 if (!wasInterrupted) { 415 NetworkTablesJNI.destroyConnectionListenerPoller(m_connectionListenerPoller); 416 } 417 m_connectionListenerPoller = 0; 418 } finally { 419 m_connectionListenerLock.unlock(); 420 } 421 }, "NTConnectionListener"); 422 m_connectionListenerThread.setDaemon(true); 423 m_connectionListenerThread.start(); 424 } 425 426 /** 427 * Add a connection listener. 428 * 429 * @param listener Listener to add 430 * @param immediateNotify Notify listener of all existing connections 431 * @return Listener handle 432 */ 433 public int addConnectionListener(Consumer<ConnectionNotification> listener, boolean immediateNotify) { 434 m_connectionListenerLock.lock(); 435 try { 436 if (m_connectionListenerPoller == 0) { 437 m_connectionListenerPoller = NetworkTablesJNI.createConnectionListenerPoller(m_handle); 438 startConnectionListenerThread(); 439 } 440 int handle = NetworkTablesJNI.addPolledConnectionListener(m_connectionListenerPoller, immediateNotify); 441 m_connectionListeners.put(handle, listener); 442 return handle; 443 } finally { 444 m_connectionListenerLock.unlock(); 445 } 446 } 447 448 /** 449 * Remove a connection listener. 450 * @param listener Listener handle to remove 451 */ 452 public void removeConnectionListener(int listener) { 453 m_connectionListenerLock.lock(); 454 try { 455 m_connectionListeners.remove(listener); 456 } finally { 457 m_connectionListenerLock.unlock(); 458 } 459 NetworkTablesJNI.removeConnectionListener(listener); 460 } 461 462 /** 463 * Wait for the connection listener queue to be empty. This is primarily useful 464 * for deterministic testing. This blocks until either the connection listener 465 * queue is empty (e.g. there are no more events that need to be passed along 466 * to callbacks or poll queues) or the timeout expires. 467 * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, 468 * or a negative value to block indefinitely 469 * @return False if timed out, otherwise true. 470 */ 471 public boolean waitForConnectionListenerQueue(double timeout) { 472 if (!NetworkTablesJNI.waitForConnectionListenerQueue(m_handle, timeout)) { 473 return false; 474 } 475 m_connectionListenerLock.lock(); 476 try { 477 if (m_connectionListenerPoller != 0) { 478 m_connectionListenerWaitQueue = true; 479 NetworkTablesJNI.cancelPollConnectionListener(m_connectionListenerPoller); 480 while (m_connectionListenerWaitQueue) { 481 try { 482 if (timeout < 0) { 483 m_connectionListenerWaitQueueCond.await(); 484 } else { 485 return m_connectionListenerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS); 486 } 487 } catch (InterruptedException ex) { 488 Thread.currentThread().interrupt(); 489 return true; 490 } 491 } 492 } 493 } finally { 494 m_connectionListenerLock.unlock(); 495 } 496 return true; 497 } 498 499 /* 500 * Remote Procedure Call Functions 501 */ 502 503 private final ReentrantLock m_rpcCallLock = new ReentrantLock(); 504 private final Map<Integer, EntryConsumer<RpcAnswer>> m_rpcCalls = new HashMap<>(); 505 private Thread m_rpcCallThread; 506 private int m_rpcCallPoller; 507 private boolean m_rpcCallWaitQueue; 508 private final Condition m_rpcCallWaitQueueCond = m_rpcCallLock.newCondition(); 509 510 private void startRpcCallThread() { 511 m_rpcCallThread = new Thread(() -> { 512 boolean wasInterrupted = false; 513 while (!Thread.interrupted()) { 514 RpcAnswer[] events; 515 try { 516 events = NetworkTablesJNI.pollRpc(this, m_rpcCallPoller); 517 } catch (InterruptedException ex) { 518 m_rpcCallLock.lock(); 519 try { 520 if (m_rpcCallWaitQueue) { 521 m_rpcCallWaitQueue = false; 522 m_rpcCallWaitQueueCond.signalAll(); 523 continue; 524 } 525 } finally { 526 m_rpcCallLock.unlock(); 527 } 528 Thread.currentThread().interrupt(); 529 wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid 530 break; 531 } 532 for (RpcAnswer event : events) { 533 EntryConsumer<RpcAnswer> listener; 534 m_rpcCallLock.lock(); 535 try { 536 listener = m_rpcCalls.get(event.entry); 537 } finally { 538 m_rpcCallLock.unlock(); 539 } 540 if (listener != null) { 541 event.entryObject = listener.entry; 542 try { 543 listener.consumer.accept(event); 544 } catch (Throwable throwable) { 545 System.err.println("Unhandled exception during RPC callback: " + throwable.toString()); 546 throwable.printStackTrace(); 547 } 548 } 549 } 550 } 551 m_rpcCallLock.lock(); 552 try { 553 if (!wasInterrupted) { 554 NetworkTablesJNI.destroyRpcCallPoller(m_rpcCallPoller); 555 } 556 m_rpcCallPoller = 0; 557 } finally { 558 m_rpcCallLock.unlock(); 559 } 560 }, "NTRpcCall"); 561 m_rpcCallThread.setDaemon(true); 562 m_rpcCallThread.start(); 563 } 564 565 private static final byte[] rev0def = new byte[] {0}; 566 567 /** 568 * Create a callback-based RPC entry point. Only valid to use on the server. 569 * The callback function will be called when the RPC is called. 570 * This function creates RPC version 0 definitions (raw data in and out). 571 * @param entry the entry 572 * @param callback callback function 573 */ 574 public void createRpc(NetworkTableEntry entry, Consumer<RpcAnswer> callback) { 575 m_rpcCallLock.lock(); 576 try { 577 if (m_rpcCallPoller == 0) { 578 m_rpcCallPoller = NetworkTablesJNI.createRpcCallPoller(m_handle); 579 startRpcCallThread(); 580 } 581 NetworkTablesJNI.createPolledRpc(entry.getHandle(), rev0def, m_rpcCallPoller); 582 m_rpcCalls.put(entry.getHandle(), new EntryConsumer<>(entry, callback)); 583 } finally { 584 m_rpcCallLock.unlock(); 585 } 586 } 587 588 /** 589 * Wait for the incoming RPC call queue to be empty. This is primarily useful 590 * for deterministic testing. This blocks until either the RPC call 591 * queue is empty (e.g. there are no more events that need to be passed along 592 * to callbacks or poll queues) or the timeout expires. 593 * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, 594 * or a negative value to block indefinitely 595 * @return False if timed out, otherwise true. 596 */ 597 public boolean waitForRpcCallQueue(double timeout) { 598 if (!NetworkTablesJNI.waitForRpcCallQueue(m_handle, timeout)) { 599 return false; 600 } 601 m_rpcCallLock.lock(); 602 try { 603 if (m_rpcCallPoller != 0) { 604 m_rpcCallWaitQueue = true; 605 NetworkTablesJNI.cancelPollRpc(m_rpcCallPoller); 606 while (m_rpcCallWaitQueue) { 607 try { 608 if (timeout < 0) { 609 m_rpcCallWaitQueueCond.await(); 610 } else { 611 return m_rpcCallWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS); 612 } 613 } catch (InterruptedException ex) { 614 Thread.currentThread().interrupt(); 615 return true; 616 } 617 } 618 } 619 } finally { 620 m_rpcCallLock.unlock(); 621 } 622 return true; 623 } 624 625 /* 626 * Client/Server Functions 627 */ 628 629 /** 630 * Set the network identity of this node. 631 * This is the name used during the initial connection handshake, and is 632 * visible through ConnectionInfo on the remote node. 633 * @param name identity to advertise 634 */ 635 public void setNetworkIdentity(String name) { 636 NetworkTablesJNI.setNetworkIdentity(m_handle, name); 637 } 638 639 /** 640 * Get the current network mode. 641 * @return Bitmask of NetworkMode. 642 */ 643 public int getNetworkMode() { 644 return NetworkTablesJNI.getNetworkMode(m_handle); 645 } 646 647 /** 648 * Starts a server using the networktables.ini as the persistent file, 649 * using the default listening address and port. 650 */ 651 public void startServer() { 652 startServer("networktables.ini"); 653 } 654 655 /** 656 * Starts a server using the specified persistent filename, using the default 657 * listening address and port. 658 * 659 * @param persistFilename the name of the persist file to use 660 */ 661 public void startServer(String persistFilename) { 662 startServer(persistFilename, ""); 663 } 664 665 /** 666 * Starts a server using the specified filename and listening address, 667 * using the default port. 668 * 669 * @param persistFilename the name of the persist file to use 670 * @param listenAddress the address to listen on, or empty to listen on any 671 * address 672 */ 673 public void startServer(String persistFilename, String listenAddress) { 674 startServer(persistFilename, listenAddress, kDefaultPort); 675 } 676 677 /** 678 * Starts a server using the specified filename, listening address, and port. 679 * 680 * @param persistFilename the name of the persist file to use 681 * @param listenAddress the address to listen on, or empty to listen on any 682 * address 683 * @param port port to communicate over 684 */ 685 public void startServer(String persistFilename, String listenAddress, int port) { 686 NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port); 687 } 688 689 /** 690 * Stops the server if it is running. 691 */ 692 public void stopServer() { 693 NetworkTablesJNI.stopServer(m_handle); 694 } 695 696 /** 697 * Starts a client. Use SetServer to set the server name and port. 698 */ 699 public void startClient() { 700 NetworkTablesJNI.startClient(m_handle); 701 } 702 703 /** 704 * Starts a client using the specified server and the default port 705 * 706 * @param serverName server name 707 */ 708 public void startClient(String serverName) { 709 startClient(serverName, kDefaultPort); 710 } 711 712 /** 713 * Starts a client using the specified server and port 714 * 715 * @param serverName server name 716 * @param port port to communicate over 717 */ 718 public void startClient(String serverName, int port) { 719 NetworkTablesJNI.startClient(m_handle, serverName, port); 720 } 721 722 /** 723 * Starts a client using the specified servers and default port. The 724 * client will attempt to connect to each server in round robin fashion. 725 * 726 * @param serverNames array of server names 727 */ 728 public void startClient(String[] serverNames) { 729 startClient(serverNames, kDefaultPort); 730 } 731 732 /** 733 * Starts a client using the specified servers and port number. The 734 * client will attempt to connect to each server in round robin fashion. 735 * 736 * @param serverNames array of server names 737 * @param port port to communicate over 738 */ 739 public void startClient(String[] serverNames, int port) { 740 int[] ports = new int[serverNames.length]; 741 for (int i = 0; i < serverNames.length; i++) { 742 ports[i] = port; 743 } 744 startClient(serverNames, ports); 745 } 746 747 /** 748 * Starts a client using the specified (server, port) combinations. The 749 * client will attempt to connect to each server in round robin fashion. 750 * 751 * @param serverNames array of server names 752 * @param ports array of port numbers 753 */ 754 public void startClient(String[] serverNames, int[] ports) { 755 NetworkTablesJNI.startClient(m_handle, serverNames, ports); 756 } 757 758 /** 759 * Starts a client using commonly known robot addresses for the specified 760 * team using the default port number. 761 * 762 * @param team team number 763 */ 764 public void startClientTeam(int team) { 765 startClientTeam(team, kDefaultPort); 766 } 767 768 /** 769 * Starts a client using commonly known robot addresses for the specified 770 * team. 771 * 772 * @param team team number 773 * @param port port to communicate over 774 */ 775 public void startClientTeam(int team, int port) { 776 NetworkTablesJNI.startClientTeam(m_handle, team, port); 777 } 778 779 /** 780 * Stops the client if it is running. 781 */ 782 public void stopClient() { 783 NetworkTablesJNI.stopClient(m_handle); 784 } 785 786 /** 787 * Sets server address and port for client (without restarting client). 788 * Changes the port to the default port. 789 * 790 * @param serverName server name 791 */ 792 public void setServer(String serverName) { 793 setServer(serverName, kDefaultPort); 794 } 795 796 /** 797 * Sets server address and port for client (without restarting client). 798 * 799 * @param serverName server name 800 * @param port port to communicate over 801 */ 802 public void setServer(String serverName, int port) { 803 NetworkTablesJNI.setServer(m_handle, serverName, port); 804 } 805 806 /** 807 * Sets server addresses and port for client (without restarting client). 808 * Changes the port to the default port. The client will attempt to connect 809 * to each server in round robin fashion. 810 * 811 * @param serverNames array of server names 812 */ 813 public void setServer(String[] serverNames) { 814 setServer(serverNames, kDefaultPort); 815 } 816 817 /** 818 * Sets server addresses and port for client (without restarting client). 819 * The client will attempt to connect to each server in round robin fashion. 820 * 821 * @param serverNames array of server names 822 * @param port port to communicate over 823 */ 824 public void setServer(String[] serverNames, int port) { 825 int[] ports = new int[serverNames.length]; 826 for (int i = 0; i < serverNames.length; i++) { 827 ports[i] = port; 828 } 829 setServer(serverNames, ports); 830 } 831 832 /** 833 * Sets server addresses and ports for client (without restarting client). 834 * The client will attempt to connect to each server in round robin fashion. 835 * 836 * @param serverNames array of server names 837 * @param ports array of port numbers 838 */ 839 public void setServer(String[] serverNames, int[] ports) { 840 NetworkTablesJNI.setServer(m_handle, serverNames, ports); 841 } 842 843 /** 844 * Sets server addresses and port for client (without restarting client). 845 * Changes the port to the default port. The client will attempt to connect 846 * to each server in round robin fashion. 847 * 848 * @param team team number 849 */ 850 public void setServerTeam(int team) { 851 setServerTeam(team, kDefaultPort); 852 } 853 854 /** 855 * Sets server addresses and port for client (without restarting client). 856 * Connects using commonly known robot addresses for the specified team. 857 * 858 * @param team team number 859 * @param port port to communicate over 860 */ 861 public void setServerTeam(int team, int port) { 862 NetworkTablesJNI.setServerTeam(m_handle, team, port); 863 } 864 865 /** 866 * Starts requesting server address from Driver Station. 867 * This connects to the Driver Station running on localhost to obtain the 868 * server IP address, and connects with the default port. 869 */ 870 public void startDSClient() { 871 startDSClient(kDefaultPort); 872 } 873 874 /** 875 * Starts requesting server address from Driver Station. 876 * This connects to the Driver Station running on localhost to obtain the 877 * server IP address. 878 * 879 * @param port server port to use in combination with IP from DS 880 */ 881 public void startDSClient(int port) { 882 NetworkTablesJNI.startDSClient(m_handle, port); 883 } 884 885 /** 886 * Stops requesting server address from Driver Station. 887 */ 888 public void stopDSClient() { 889 NetworkTablesJNI.stopDSClient(m_handle); 890 } 891 892 /** 893 * Set the periodic update rate. 894 * Sets how frequently updates are sent to other nodes over the network. 895 * 896 * @param interval update interval in seconds (range 0.01 to 1.0) 897 */ 898 public void setUpdateRate(double interval) { 899 NetworkTablesJNI.setUpdateRate(m_handle, interval); 900 } 901 902 /** 903 * Flushes all updated values immediately to the network. 904 * Note: This is rate-limited to protect the network from flooding. 905 * This is primarily useful for synchronizing network updates with 906 * user code. 907 */ 908 public void flush() { 909 NetworkTablesJNI.flush(m_handle); 910 } 911 912 /** 913 * Gets information on the currently established network connections. 914 * If operating as a client, this will return either zero or one values. 915 * @return array of connection information 916 */ 917 public ConnectionInfo[] getConnections() { 918 return NetworkTablesJNI.getConnections(m_handle); 919 } 920 921 /** 922 * Return whether or not the instance is connected to another node. 923 * @return True if connected. 924 */ 925 public boolean isConnected() { 926 return NetworkTablesJNI.isConnected(m_handle); 927 } 928 929 /** 930 * Saves persistent keys to a file. The server does this automatically. 931 * 932 * @param filename file name 933 * @throws PersistentException if error saving file 934 */ 935 public void savePersistent(String filename) throws PersistentException { 936 NetworkTablesJNI.savePersistent(m_handle, filename); 937 } 938 939 /** 940 * Loads persistent keys from a file. The server does this automatically. 941 * 942 * @param filename file name 943 * @return List of warnings (errors result in an exception instead) 944 * @throws PersistentException if error reading file 945 */ 946 public String[] loadPersistent(String filename) throws PersistentException { 947 return NetworkTablesJNI.loadPersistent(m_handle, filename); 948 } 949 950 /** 951 * Save table values to a file. The file format used is identical to 952 * that used for SavePersistent. 953 * @param filename filename 954 * @param prefix save only keys starting with this prefix 955 * @throws PersistentException if error saving file 956 */ 957 public void saveEntries(String filename, String prefix) throws PersistentException { 958 NetworkTablesJNI.saveEntries(m_handle, filename, prefix); 959 } 960 961 /** 962 * Load table values from a file. The file format used is identical to 963 * that used for SavePersistent / LoadPersistent. 964 * @param filename filename 965 * @param prefix load only keys starting with this prefix 966 * @return List of warnings (errors result in an exception instead) 967 * @throws PersistentException if error saving file 968 */ 969 public String[] loadEntries(String filename, String prefix) throws PersistentException { 970 return NetworkTablesJNI.loadEntries(m_handle, filename, prefix); 971 } 972 973 private final ReentrantLock m_loggerLock = new ReentrantLock(); 974 private final Map<Integer, Consumer<LogMessage>> m_loggers = new HashMap<>(); 975 private Thread m_loggerThread; 976 private int m_loggerPoller; 977 private boolean m_loggerWaitQueue; 978 private final Condition m_loggerWaitQueueCond = m_loggerLock.newCondition(); 979 980 private void startLogThread() { 981 m_loggerThread = new Thread(() -> { 982 boolean wasInterrupted = false; 983 while (!Thread.interrupted()) { 984 LogMessage[] events; 985 try { 986 events = NetworkTablesJNI.pollLogger(this, m_loggerPoller); 987 } catch (InterruptedException ex) { 988 Thread.currentThread().interrupt(); 989 wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid 990 break; 991 } 992 for (LogMessage event : events) { 993 Consumer<LogMessage> logger; 994 m_loggerLock.lock(); 995 try { 996 logger = m_loggers.get(event.logger); 997 } finally { 998 m_loggerLock.unlock(); 999 } 1000 if (logger != null) { 1001 try { 1002 logger.accept(event); 1003 } catch (Throwable throwable) { 1004 System.err.println("Unhandled exception during logger callback: " + throwable.toString()); 1005 throwable.printStackTrace(); 1006 } 1007 } 1008 } 1009 } 1010 m_loggerLock.lock(); 1011 try { 1012 if (!wasInterrupted) { 1013 NetworkTablesJNI.destroyLoggerPoller(m_loggerPoller); 1014 } 1015 m_rpcCallPoller = 0; 1016 } finally { 1017 m_loggerLock.unlock(); 1018 } 1019 }, "NTLogger"); 1020 m_loggerThread.setDaemon(true); 1021 m_loggerThread.start(); 1022 } 1023 1024 /** 1025 * Add logger callback function. By default, log messages are sent to stderr; 1026 * this function sends log messages with the specified levels to the provided 1027 * callback function instead. The callback function will only be called for 1028 * log messages with level greater than or equal to minLevel and less than or 1029 * equal to maxLevel; messages outside this range will be silently ignored. 1030 * 1031 * @param func log callback function 1032 * @param minLevel minimum log level 1033 * @param maxLevel maximum log level 1034 * @return Logger handle 1035 */ 1036 public int addLogger(Consumer<LogMessage> func, int minLevel, int maxLevel) { 1037 m_loggerLock.lock(); 1038 try { 1039 if (m_loggerPoller == 0) { 1040 m_loggerPoller = NetworkTablesJNI.createLoggerPoller(m_handle); 1041 startLogThread(); 1042 } 1043 int handle = NetworkTablesJNI.addPolledLogger(m_loggerPoller, minLevel, maxLevel); 1044 m_loggers.put(handle, func); 1045 return handle; 1046 } finally { 1047 m_loggerLock.unlock(); 1048 } 1049 } 1050 1051 /** 1052 * Remove a logger. 1053 * @param logger Logger handle to remove 1054 */ 1055 public void removeLogger(int logger) { 1056 m_loggerLock.lock(); 1057 try { 1058 m_loggers.remove(logger); 1059 } finally { 1060 m_loggerLock.unlock(); 1061 } 1062 NetworkTablesJNI.removeLogger(logger); 1063 } 1064 1065 /** 1066 * Wait for the incoming log event queue to be empty. This is primarily useful 1067 * for deterministic testing. This blocks until either the log event 1068 * queue is empty (e.g. there are no more events that need to be passed along 1069 * to callbacks or poll queues) or the timeout expires. 1070 * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, 1071 * or a negative value to block indefinitely 1072 * @return False if timed out, otherwise true. 1073 */ 1074 public boolean waitForLoggerQueue(double timeout) { 1075 if (!NetworkTablesJNI.waitForLoggerQueue(m_handle, timeout)) { 1076 return false; 1077 } 1078 m_loggerLock.lock(); 1079 try { 1080 if (m_loggerPoller != 0) { 1081 m_loggerWaitQueue = true; 1082 NetworkTablesJNI.cancelPollLogger(m_loggerPoller); 1083 while (m_loggerWaitQueue) { 1084 try { 1085 if (timeout < 0) { 1086 m_loggerWaitQueueCond.await(); 1087 } else { 1088 return m_loggerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS); 1089 } 1090 } catch (InterruptedException ex) { 1091 Thread.currentThread().interrupt(); 1092 return true; 1093 } 1094 } 1095 } 1096 } finally { 1097 m_loggerLock.unlock(); 1098 } 1099 return true; 1100 } 1101 1102 @Override 1103 public boolean equals(Object o) { 1104 if (o == this) { 1105 return true; 1106 } 1107 if (!(o instanceof NetworkTableInstance)) { 1108 return false; 1109 } 1110 NetworkTableInstance other = (NetworkTableInstance) o; 1111 return m_handle == other.m_handle; 1112 } 1113 1114 @Override 1115 public int hashCode() { 1116 return m_handle; 1117 } 1118 1119 private boolean m_owned; 1120 private int m_handle; 1121}