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