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 005// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY 006 007package edu.wpi.first.networktables; 008 009import edu.wpi.first.util.WPIUtilJNI; 010import edu.wpi.first.util.concurrent.Event; 011import edu.wpi.first.util.datalog.DataLog; 012import edu.wpi.first.util.protobuf.Protobuf; 013import edu.wpi.first.util.struct.Struct; 014import java.nio.charset.StandardCharsets; 015import java.util.EnumSet; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Map; 019import java.util.OptionalLong; 020import java.util.Set; 021import java.util.concurrent.ConcurrentHashMap; 022import java.util.concurrent.ConcurrentMap; 023import java.util.concurrent.TimeUnit; 024import java.util.concurrent.locks.Condition; 025import java.util.concurrent.locks.ReentrantLock; 026import java.util.function.Consumer; 027import us.hebi.quickbuf.ProtoMessage; 028 029/** 030 * NetworkTables Instance. 031 * 032 * <p>Instances are completely independent from each other. Table operations on one instance will 033 * not be visible to other instances unless the instances are connected via the network. The main 034 * limitation on instances is that you cannot have two servers on the same network port. The main 035 * utility of instances is for unit testing, but they can also enable one program to connect to two 036 * different NetworkTables networks. 037 * 038 * <p>The global "default" instance (as returned by {@link #getDefault()}) is always available, and 039 * is intended for the common case when there is only a single NetworkTables instance being used in 040 * the program. 041 * 042 * <p>Additional instances can be created with the {@link #create()} function. A reference must be 043 * kept to the NetworkTableInstance returned by this function to keep it from being garbage 044 * collected. 045 */ 046public final class NetworkTableInstance implements AutoCloseable { 047 /** Client/server mode flag values (as returned by {@link #getNetworkMode()}). */ 048 public enum NetworkMode { 049 /** Running in server mode. */ 050 kServer(0x01), 051 052 /** Running in NT3 client mode. */ 053 kClient3(0x02), 054 055 /** Running in NT4 client mode. */ 056 kClient4(0x04), 057 058 /** Currently starting up (either client or server). */ 059 kStarting(0x08), 060 061 /** Running in local-only mode. */ 062 kLocal(0x10); 063 064 private final int value; 065 066 NetworkMode(int value) { 067 this.value = value; 068 } 069 070 /** 071 * Returns the network mode value. 072 * 073 * @return The network mode value. 074 */ 075 public int getValue() { 076 return value; 077 } 078 } 079 080 /** The default port that network tables operates on for NT3. */ 081 public static final int kDefaultPort3 = 1735; 082 083 /** The default port that network tables operates on for NT4. */ 084 public static final int kDefaultPort4 = 5810; 085 086 /** 087 * Construct from native handle. 088 * 089 * @param handle Native handle 090 */ 091 private NetworkTableInstance(int handle) { 092 m_owned = false; 093 m_handle = handle; 094 } 095 096 /** Destroys the instance (if created by {@link #create()}). */ 097 @Override 098 public synchronized void close() { 099 if (m_owned && m_handle != 0) { 100 m_listeners.close(); 101 m_schemas.forEach((k, v) -> v.close()); 102 NetworkTablesJNI.destroyInstance(m_handle); 103 m_handle = 0; 104 } 105 } 106 107 /** 108 * Determines if the native handle is valid. 109 * 110 * @return True if the native handle is valid, false otherwise. 111 */ 112 public boolean isValid() { 113 return m_handle != 0; 114 } 115 116 /* The default instance. */ 117 private static NetworkTableInstance s_defaultInstance; 118 119 /** 120 * Get global default instance. 121 * 122 * @return Global default instance 123 */ 124 public static synchronized NetworkTableInstance getDefault() { 125 if (s_defaultInstance == null) { 126 s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance()); 127 } 128 return s_defaultInstance; 129 } 130 131 /** 132 * Create an instance. Note: A reference to the returned instance must be retained to ensure the 133 * instance is not garbage collected. 134 * 135 * @return Newly created instance 136 */ 137 public static NetworkTableInstance create() { 138 NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance()); 139 inst.m_owned = true; 140 return inst; 141 } 142 143 /** 144 * Gets the native handle for the instance. 145 * 146 * @return Native handle 147 */ 148 public int getHandle() { 149 return m_handle; 150 } 151 152 /** 153 * Get (generic) topic. 154 * 155 * @param name topic name 156 * @return Topic 157 */ 158 public Topic getTopic(String name) { 159 Topic topic = m_topics.get(name); 160 if (topic == null) { 161 int handle = NetworkTablesJNI.getTopic(m_handle, name); 162 topic = new Topic(this, handle); 163 Topic oldTopic = m_topics.putIfAbsent(name, topic); 164 if (oldTopic != null) { 165 topic = oldTopic; 166 } 167 // also cache by handle 168 m_topicsByHandle.putIfAbsent(handle, topic); 169 } 170 return topic; 171 } 172 173 /** 174 * Get boolean topic. 175 * 176 * @param name topic name 177 * @return BooleanTopic 178 */ 179 public BooleanTopic getBooleanTopic(String name) { 180 Topic topic = m_topics.get(name); 181 if (topic instanceof BooleanTopic t) { 182 return t; 183 } 184 185 int handle; 186 if (topic == null) { 187 handle = NetworkTablesJNI.getTopic(m_handle, name); 188 } else { 189 handle = topic.getHandle(); 190 } 191 192 BooleanTopic wrapTopic = new BooleanTopic(this, handle); 193 m_topics.put(name, wrapTopic); 194 195 // also cache by handle 196 m_topicsByHandle.put(handle, wrapTopic); 197 198 return wrapTopic; 199 } 200 201 /** 202 * Get long topic. 203 * 204 * @param name topic name 205 * @return IntegerTopic 206 */ 207 public IntegerTopic getIntegerTopic(String name) { 208 Topic topic = m_topics.get(name); 209 if (topic instanceof IntegerTopic t) { 210 return t; 211 } 212 213 int handle; 214 if (topic == null) { 215 handle = NetworkTablesJNI.getTopic(m_handle, name); 216 } else { 217 handle = topic.getHandle(); 218 } 219 220 IntegerTopic wrapTopic = new IntegerTopic(this, handle); 221 m_topics.put(name, wrapTopic); 222 223 // also cache by handle 224 m_topicsByHandle.put(handle, wrapTopic); 225 226 return wrapTopic; 227 } 228 229 /** 230 * Get float topic. 231 * 232 * @param name topic name 233 * @return FloatTopic 234 */ 235 public FloatTopic getFloatTopic(String name) { 236 Topic topic = m_topics.get(name); 237 if (topic instanceof FloatTopic t) { 238 return t; 239 } 240 241 int handle; 242 if (topic == null) { 243 handle = NetworkTablesJNI.getTopic(m_handle, name); 244 } else { 245 handle = topic.getHandle(); 246 } 247 248 FloatTopic wrapTopic = new FloatTopic(this, handle); 249 m_topics.put(name, wrapTopic); 250 251 // also cache by handle 252 m_topicsByHandle.put(handle, wrapTopic); 253 254 return wrapTopic; 255 } 256 257 /** 258 * Get double topic. 259 * 260 * @param name topic name 261 * @return DoubleTopic 262 */ 263 public DoubleTopic getDoubleTopic(String name) { 264 Topic topic = m_topics.get(name); 265 if (topic instanceof DoubleTopic t) { 266 return t; 267 } 268 269 int handle; 270 if (topic == null) { 271 handle = NetworkTablesJNI.getTopic(m_handle, name); 272 } else { 273 handle = topic.getHandle(); 274 } 275 276 DoubleTopic wrapTopic = new DoubleTopic(this, handle); 277 m_topics.put(name, wrapTopic); 278 279 // also cache by handle 280 m_topicsByHandle.put(handle, wrapTopic); 281 282 return wrapTopic; 283 } 284 285 /** 286 * Get String topic. 287 * 288 * @param name topic name 289 * @return StringTopic 290 */ 291 public StringTopic getStringTopic(String name) { 292 Topic topic = m_topics.get(name); 293 if (topic instanceof StringTopic t) { 294 return t; 295 } 296 297 int handle; 298 if (topic == null) { 299 handle = NetworkTablesJNI.getTopic(m_handle, name); 300 } else { 301 handle = topic.getHandle(); 302 } 303 304 StringTopic wrapTopic = new StringTopic(this, handle); 305 m_topics.put(name, wrapTopic); 306 307 // also cache by handle 308 m_topicsByHandle.put(handle, wrapTopic); 309 310 return wrapTopic; 311 } 312 313 /** 314 * Get byte[] topic. 315 * 316 * @param name topic name 317 * @return RawTopic 318 */ 319 public RawTopic getRawTopic(String name) { 320 Topic topic = m_topics.get(name); 321 if (topic instanceof RawTopic t) { 322 return t; 323 } 324 325 int handle; 326 if (topic == null) { 327 handle = NetworkTablesJNI.getTopic(m_handle, name); 328 } else { 329 handle = topic.getHandle(); 330 } 331 332 RawTopic wrapTopic = new RawTopic(this, handle); 333 m_topics.put(name, wrapTopic); 334 335 // also cache by handle 336 m_topicsByHandle.put(handle, wrapTopic); 337 338 return wrapTopic; 339 } 340 341 /** 342 * Get boolean[] topic. 343 * 344 * @param name topic name 345 * @return BooleanArrayTopic 346 */ 347 public BooleanArrayTopic getBooleanArrayTopic(String name) { 348 Topic topic = m_topics.get(name); 349 if (topic instanceof BooleanArrayTopic t) { 350 return t; 351 } 352 353 int handle; 354 if (topic == null) { 355 handle = NetworkTablesJNI.getTopic(m_handle, name); 356 } else { 357 handle = topic.getHandle(); 358 } 359 360 BooleanArrayTopic wrapTopic = new BooleanArrayTopic(this, handle); 361 m_topics.put(name, wrapTopic); 362 363 // also cache by handle 364 m_topicsByHandle.put(handle, wrapTopic); 365 366 return wrapTopic; 367 } 368 369 /** 370 * Get long[] topic. 371 * 372 * @param name topic name 373 * @return IntegerArrayTopic 374 */ 375 public IntegerArrayTopic getIntegerArrayTopic(String name) { 376 Topic topic = m_topics.get(name); 377 if (topic instanceof IntegerArrayTopic t) { 378 return t; 379 } 380 381 int handle; 382 if (topic == null) { 383 handle = NetworkTablesJNI.getTopic(m_handle, name); 384 } else { 385 handle = topic.getHandle(); 386 } 387 388 IntegerArrayTopic wrapTopic = new IntegerArrayTopic(this, handle); 389 m_topics.put(name, wrapTopic); 390 391 // also cache by handle 392 m_topicsByHandle.put(handle, wrapTopic); 393 394 return wrapTopic; 395 } 396 397 /** 398 * Get float[] topic. 399 * 400 * @param name topic name 401 * @return FloatArrayTopic 402 */ 403 public FloatArrayTopic getFloatArrayTopic(String name) { 404 Topic topic = m_topics.get(name); 405 if (topic instanceof FloatArrayTopic t) { 406 return t; 407 } 408 409 int handle; 410 if (topic == null) { 411 handle = NetworkTablesJNI.getTopic(m_handle, name); 412 } else { 413 handle = topic.getHandle(); 414 } 415 416 FloatArrayTopic wrapTopic = new FloatArrayTopic(this, handle); 417 m_topics.put(name, wrapTopic); 418 419 // also cache by handle 420 m_topicsByHandle.put(handle, wrapTopic); 421 422 return wrapTopic; 423 } 424 425 /** 426 * Get double[] topic. 427 * 428 * @param name topic name 429 * @return DoubleArrayTopic 430 */ 431 public DoubleArrayTopic getDoubleArrayTopic(String name) { 432 Topic topic = m_topics.get(name); 433 if (topic instanceof DoubleArrayTopic t) { 434 return t; 435 } 436 437 int handle; 438 if (topic == null) { 439 handle = NetworkTablesJNI.getTopic(m_handle, name); 440 } else { 441 handle = topic.getHandle(); 442 } 443 444 DoubleArrayTopic wrapTopic = new DoubleArrayTopic(this, handle); 445 m_topics.put(name, wrapTopic); 446 447 // also cache by handle 448 m_topicsByHandle.put(handle, wrapTopic); 449 450 return wrapTopic; 451 } 452 453 /** 454 * Get String[] topic. 455 * 456 * @param name topic name 457 * @return StringArrayTopic 458 */ 459 public StringArrayTopic getStringArrayTopic(String name) { 460 Topic topic = m_topics.get(name); 461 if (topic instanceof StringArrayTopic t) { 462 return t; 463 } 464 465 int handle; 466 if (topic == null) { 467 handle = NetworkTablesJNI.getTopic(m_handle, name); 468 } else { 469 handle = topic.getHandle(); 470 } 471 472 StringArrayTopic wrapTopic = new StringArrayTopic(this, handle); 473 m_topics.put(name, wrapTopic); 474 475 // also cache by handle 476 m_topicsByHandle.put(handle, wrapTopic); 477 478 return wrapTopic; 479 } 480 481 482 /** 483 * Get protobuf-encoded value topic. 484 * 485 * @param <T> value class (inferred from proto) 486 * @param <MessageType> protobuf message type (inferred from proto) 487 * @param name topic name 488 * @param proto protobuf serialization implementation 489 * @return ProtobufTopic 490 */ 491 public <T, MessageType extends ProtoMessage<?>> 492 ProtobufTopic<T> getProtobufTopic(String name, Protobuf<T, MessageType> proto) { 493 Topic topic = m_topics.get(name); 494 if (topic instanceof ProtobufTopic<?> t && t.getProto().equals(proto)) { 495 @SuppressWarnings("unchecked") 496 ProtobufTopic<T> wrapTopic = (ProtobufTopic<T>) topic; 497 return wrapTopic; 498 } 499 500 int handle; 501 if (topic == null) { 502 handle = NetworkTablesJNI.getTopic(m_handle, name); 503 } else { 504 handle = topic.getHandle(); 505 } 506 507 ProtobufTopic<T> wrapTopic = ProtobufTopic.wrap(this, handle, proto); 508 m_topics.put(name, wrapTopic); 509 510 // also cache by handle 511 m_topicsByHandle.put(handle, wrapTopic); 512 513 return wrapTopic; 514 } 515 516 /** 517 * Get struct-encoded value topic. 518 * 519 * @param <T> value class (inferred from struct) 520 * @param name topic name 521 * @param struct struct serialization implementation 522 * @return StructTopic 523 */ 524 public <T> 525 StructTopic<T> getStructTopic(String name, Struct<T> struct) { 526 Topic topic = m_topics.get(name); 527 if (topic instanceof StructTopic<?> t && t.getStruct().equals(struct)) { 528 @SuppressWarnings("unchecked") 529 StructTopic<T> wrapTopic = (StructTopic<T>) topic; 530 return wrapTopic; 531 } 532 533 int handle; 534 if (topic == null) { 535 handle = NetworkTablesJNI.getTopic(m_handle, name); 536 } else { 537 handle = topic.getHandle(); 538 } 539 540 StructTopic<T> wrapTopic = StructTopic.wrap(this, handle, struct); 541 m_topics.put(name, wrapTopic); 542 543 // also cache by handle 544 m_topicsByHandle.put(handle, wrapTopic); 545 546 return wrapTopic; 547 } 548 549 /** 550 * Get struct-encoded value array topic. 551 * 552 * @param <T> value class (inferred from struct) 553 * @param name topic name 554 * @param struct struct serialization implementation 555 * @return StructArrayTopic 556 */ 557 public <T> 558 StructArrayTopic<T> getStructArrayTopic(String name, Struct<T> struct) { 559 Topic topic = m_topics.get(name); 560 if (topic instanceof StructArrayTopic<?> t && t.getStruct().equals(struct)) { 561 @SuppressWarnings("unchecked") 562 StructArrayTopic<T> wrapTopic = (StructArrayTopic<T>) topic; 563 return wrapTopic; 564 } 565 566 int handle; 567 if (topic == null) { 568 handle = NetworkTablesJNI.getTopic(m_handle, name); 569 } else { 570 handle = topic.getHandle(); 571 } 572 573 StructArrayTopic<T> wrapTopic = StructArrayTopic.wrap(this, handle, struct); 574 m_topics.put(name, wrapTopic); 575 576 // also cache by handle 577 m_topicsByHandle.put(handle, wrapTopic); 578 579 return wrapTopic; 580 } 581 582 private Topic[] topicHandlesToTopics(int[] handles) { 583 Topic[] topics = new Topic[handles.length]; 584 for (int i = 0; i < handles.length; i++) { 585 topics[i] = getCachedTopic(handles[i]); 586 } 587 return topics; 588 } 589 590 /** 591 * Get all published topics. 592 * 593 * @return Array of topics. 594 */ 595 public Topic[] getTopics() { 596 return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, "", 0)); 597 } 598 599 /** 600 * Get published topics starting with the given prefix. The results are optionally filtered by 601 * string prefix to only return a subset of all topics. 602 * 603 * @param prefix topic name required prefix; only topics whose name starts with this string are 604 * returned 605 * @return Array of topic information. 606 */ 607 public Topic[] getTopics(String prefix) { 608 return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, 0)); 609 } 610 611 /** 612 * Get published topics starting with the given prefix. The results are optionally filtered by 613 * string prefix and data type to only return a subset of all topics. 614 * 615 * @param prefix topic name required prefix; only topics whose name starts with this string are 616 * returned 617 * @param types bitmask of data types; 0 is treated as a "don't care" 618 * @return Array of topic information. 619 */ 620 public Topic[] getTopics(String prefix, int types) { 621 return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, types)); 622 } 623 624 /** 625 * Get published topics starting with the given prefix. The results are optionally filtered by 626 * string prefix and data type to only return a subset of all topics. 627 * 628 * @param prefix topic name required prefix; only topics whose name starts with this string are 629 * returned 630 * @param types array of data type strings 631 * @return Array of topic information. 632 */ 633 public Topic[] getTopics(String prefix, String[] types) { 634 return topicHandlesToTopics(NetworkTablesJNI.getTopicsStr(m_handle, prefix, types)); 635 } 636 637 /** 638 * Get information about all topics. 639 * 640 * @return Array of topic information. 641 */ 642 public TopicInfo[] getTopicInfo() { 643 return NetworkTablesJNI.getTopicInfos(this, m_handle, "", 0); 644 } 645 646 /** 647 * Get information about topics starting with the given prefix. The results are optionally 648 * filtered by string prefix to only return a subset of all topics. 649 * 650 * @param prefix topic name required prefix; only topics whose name starts with this string are 651 * returned 652 * @return Array of topic information. 653 */ 654 public TopicInfo[] getTopicInfo(String prefix) { 655 return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, 0); 656 } 657 658 /** 659 * Get information about topics starting with the given prefix. The results are optionally 660 * filtered by string prefix and data type to only return a subset of all topics. 661 * 662 * @param prefix topic name required prefix; only topics whose name starts with this string are 663 * returned 664 * @param types bitmask of data types; 0 is treated as a "don't care" 665 * @return Array of topic information. 666 */ 667 public TopicInfo[] getTopicInfo(String prefix, int types) { 668 return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, types); 669 } 670 671 /** 672 * Get information about topics starting with the given prefix. The results are optionally 673 * filtered by string prefix and data type to only return a subset of all topics. 674 * 675 * @param prefix topic name required prefix; only topics whose name starts with this string are 676 * returned 677 * @param types array of data type strings 678 * @return Array of topic information. 679 */ 680 public TopicInfo[] getTopicInfo(String prefix, String[] types) { 681 return NetworkTablesJNI.getTopicInfosStr(this, m_handle, prefix, types); 682 } 683 684 /* Cache of created entries. */ 685 private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>(); 686 687 /** 688 * Gets the entry for a key. 689 * 690 * @param name Key 691 * @return Network table entry. 692 */ 693 public NetworkTableEntry getEntry(String name) { 694 NetworkTableEntry entry = m_entries.get(name); 695 if (entry == null) { 696 entry = new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name)); 697 NetworkTableEntry oldEntry = m_entries.putIfAbsent(name, entry); 698 if (oldEntry != null) { 699 entry = oldEntry; 700 } 701 } 702 return entry; 703 } 704 705 /* Cache of created topics. */ 706 private final ConcurrentMap<String, Topic> m_topics = new ConcurrentHashMap<>(); 707 private final ConcurrentMap<Integer, Topic> m_topicsByHandle = new ConcurrentHashMap<>(); 708 709 Topic getCachedTopic(String name) { 710 Topic topic = m_topics.get(name); 711 if (topic == null) { 712 int handle = NetworkTablesJNI.getTopic(m_handle, name); 713 topic = new Topic(this, handle); 714 Topic oldTopic = m_topics.putIfAbsent(name, topic); 715 if (oldTopic != null) { 716 topic = oldTopic; 717 } 718 // also cache by handle 719 m_topicsByHandle.putIfAbsent(handle, topic); 720 } 721 return topic; 722 } 723 724 Topic getCachedTopic(int handle) { 725 Topic topic = m_topicsByHandle.get(handle); 726 if (topic == null) { 727 topic = new Topic(this, handle); 728 Topic oldTopic = m_topicsByHandle.putIfAbsent(handle, topic); 729 if (oldTopic != null) { 730 topic = oldTopic; 731 } 732 } 733 return topic; 734 } 735 736 /* Cache of created tables. */ 737 private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>(); 738 739 /** 740 * Gets the table with the specified key. 741 * 742 * @param key the key name 743 * @return The network table 744 */ 745 public NetworkTable getTable(String key) { 746 // prepend leading / if not present 747 String theKey; 748 if (key.isEmpty() || "/".equals(key)) { 749 theKey = ""; 750 } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) { 751 theKey = key; 752 } else { 753 theKey = NetworkTable.PATH_SEPARATOR + key; 754 } 755 756 // cache created tables 757 NetworkTable table = m_tables.get(theKey); 758 if (table == null) { 759 table = new NetworkTable(this, theKey); 760 NetworkTable oldTable = m_tables.putIfAbsent(theKey, table); 761 if (oldTable != null) { 762 table = oldTable; 763 } 764 } 765 return table; 766 } 767 768 /* 769 * Callback Creation Functions 770 */ 771 772 private static class ListenerStorage implements AutoCloseable { 773 private final ReentrantLock m_lock = new ReentrantLock(); 774 private final Map<Integer, Consumer<NetworkTableEvent>> m_listeners = new HashMap<>(); 775 776 @SuppressWarnings("PMD.SingularField") 777 private Thread m_thread; 778 779 private int m_poller; 780 private boolean m_waitQueue; 781 private final Event m_waitQueueEvent = new Event(); 782 private final Condition m_waitQueueCond = m_lock.newCondition(); 783 private final NetworkTableInstance m_inst; 784 785 ListenerStorage(NetworkTableInstance inst) { 786 m_inst = inst; 787 } 788 789 int add( 790 String[] prefixes, 791 EnumSet<NetworkTableEvent.Kind> eventKinds, 792 Consumer<NetworkTableEvent> listener) { 793 m_lock.lock(); 794 try { 795 if (m_poller == 0) { 796 m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle()); 797 startThread(); 798 } 799 int h = NetworkTablesJNI.addListener(m_poller, prefixes, eventKinds); 800 m_listeners.put(h, listener); 801 return h; 802 } finally { 803 m_lock.unlock(); 804 } 805 } 806 807 int add( 808 int handle, 809 EnumSet<NetworkTableEvent.Kind> eventKinds, 810 Consumer<NetworkTableEvent> listener) { 811 m_lock.lock(); 812 try { 813 if (m_poller == 0) { 814 m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle()); 815 startThread(); 816 } 817 int h = NetworkTablesJNI.addListener(m_poller, handle, eventKinds); 818 m_listeners.put(h, listener); 819 return h; 820 } finally { 821 m_lock.unlock(); 822 } 823 } 824 825 int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> listener) { 826 m_lock.lock(); 827 try { 828 if (m_poller == 0) { 829 m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle()); 830 startThread(); 831 } 832 int h = NetworkTablesJNI.addLogger(m_poller, minLevel, maxLevel); 833 m_listeners.put(h, listener); 834 return h; 835 } finally { 836 m_lock.unlock(); 837 } 838 } 839 840 void remove(int listener) { 841 m_lock.lock(); 842 try { 843 m_listeners.remove(listener); 844 } finally { 845 m_lock.unlock(); 846 } 847 NetworkTablesJNI.removeListener(listener); 848 } 849 850 @Override 851 public void close() { 852 if (m_poller != 0) { 853 NetworkTablesJNI.destroyListenerPoller(m_poller); 854 } 855 m_poller = 0; 856 } 857 858 private void startThread() { 859 m_thread = 860 new Thread( 861 () -> { 862 boolean wasInterrupted = false; 863 int[] handles = new int[] { m_poller, m_waitQueueEvent.getHandle() }; 864 while (!Thread.interrupted()) { 865 try { 866 WPIUtilJNI.waitForObjects(handles); 867 } catch (InterruptedException ex) { 868 m_lock.lock(); 869 try { 870 if (m_waitQueue) { 871 m_waitQueue = false; 872 m_waitQueueCond.signalAll(); 873 } 874 } finally { 875 m_lock.unlock(); 876 } 877 Thread.currentThread().interrupt(); 878 // don't try to destroy poller, as its handle is likely no longer valid 879 wasInterrupted = true; 880 break; 881 } 882 for (NetworkTableEvent event : 883 NetworkTablesJNI.readListenerQueue(m_inst, m_poller)) { 884 Consumer<NetworkTableEvent> listener; 885 m_lock.lock(); 886 try { 887 listener = m_listeners.get(event.listener); 888 } finally { 889 m_lock.unlock(); 890 } 891 if (listener != null) { 892 try { 893 listener.accept(event); 894 } catch (Throwable throwable) { 895 System.err.println( 896 "Unhandled exception during listener callback: " 897 + throwable.toString()); 898 throwable.printStackTrace(); 899 } 900 } 901 } 902 m_lock.lock(); 903 try { 904 if (m_waitQueue) { 905 m_waitQueue = false; 906 m_waitQueueCond.signalAll(); 907 } 908 } finally { 909 m_lock.unlock(); 910 } 911 } 912 m_lock.lock(); 913 try { 914 if (!wasInterrupted) { 915 NetworkTablesJNI.destroyListenerPoller(m_poller); 916 } 917 m_poller = 0; 918 } finally { 919 m_lock.unlock(); 920 } 921 }, 922 "NTListener"); 923 m_thread.setDaemon(true); 924 m_thread.start(); 925 } 926 927 boolean waitForQueue(double timeout) { 928 m_lock.lock(); 929 try { 930 if (m_poller != 0) { 931 m_waitQueue = true; 932 m_waitQueueEvent.set(); 933 while (m_waitQueue) { 934 try { 935 if (timeout < 0) { 936 m_waitQueueCond.await(); 937 } else { 938 return m_waitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS); 939 } 940 } catch (InterruptedException ex) { 941 Thread.currentThread().interrupt(); 942 return true; 943 } 944 } 945 } 946 } finally { 947 m_lock.unlock(); 948 } 949 return true; 950 } 951 } 952 953 private final ListenerStorage m_listeners = new ListenerStorage(this); 954 955 /** 956 * Remove a connection listener. 957 * 958 * @param listener Listener handle to remove 959 */ 960 public void removeListener(int listener) { 961 m_listeners.remove(listener); 962 } 963 964 /** 965 * Wait for the listener queue to be empty. This is primarily useful for deterministic 966 * testing. This blocks until either the listener queue is empty (e.g. there are no 967 * more events that need to be passed along to callbacks or poll queues) or the timeout expires. 968 * 969 * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to 970 * block indefinitely 971 * @return False if timed out, otherwise true. 972 */ 973 public boolean waitForListenerQueue(double timeout) { 974 return m_listeners.waitForQueue(timeout); 975 } 976 977 /** 978 * Add a connection listener. The callback function is called asynchronously on a separate 979 * thread, so it's important to use synchronization or atomics when accessing any shared state 980 * from the callback function. 981 * 982 * @param immediateNotify Notify listener of all existing connections 983 * @param listener Listener to add 984 * @return Listener handle 985 */ 986 public int addConnectionListener( 987 boolean immediateNotify, Consumer<NetworkTableEvent> listener) { 988 EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kConnection); 989 if (immediateNotify) { 990 eventKinds.add(NetworkTableEvent.Kind.kImmediate); 991 } 992 return m_listeners.add(m_handle, eventKinds, listener); 993 } 994 995 /** 996 * Add a time synchronization listener. The callback function is called asynchronously on a 997 * separate thread, so it's important to use synchronization or atomics when accessing any shared 998 * state from the callback function. 999 * 1000 * @param immediateNotify Notify listener of current time synchronization value 1001 * @param listener Listener to add 1002 * @return Listener handle 1003 */ 1004 public int addTimeSyncListener( 1005 boolean immediateNotify, Consumer<NetworkTableEvent> listener) { 1006 EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync); 1007 if (immediateNotify) { 1008 eventKinds.add(NetworkTableEvent.Kind.kImmediate); 1009 } 1010 return m_listeners.add(m_handle, eventKinds, listener); 1011 } 1012 1013 /** 1014 * Add a listener for changes on a particular topic. The callback function is called 1015 * asynchronously on a separate thread, so it's important to use synchronization or atomics when 1016 * accessing any shared state from the callback function. 1017 * 1018 * <p>This creates a corresponding internal subscriber with the lifetime of the 1019 * listener. 1020 * 1021 * @param topic Topic 1022 * @param eventKinds set of event kinds to listen to 1023 * @param listener Listener function 1024 * @return Listener handle 1025 */ 1026 public int addListener( 1027 Topic topic, 1028 EnumSet<NetworkTableEvent.Kind> eventKinds, 1029 Consumer<NetworkTableEvent> listener) { 1030 if (topic.getInstance().getHandle() != m_handle) { 1031 throw new IllegalArgumentException("topic is not from this instance"); 1032 } 1033 return m_listeners.add(topic.getHandle(), eventKinds, listener); 1034 } 1035 1036 /** 1037 * Add a listener for changes on a subscriber. The callback function is called 1038 * asynchronously on a separate thread, so it's important to use synchronization or atomics when 1039 * accessing any shared state from the callback function. This does NOT keep the subscriber 1040 * active. 1041 * 1042 * @param subscriber Subscriber 1043 * @param eventKinds set of event kinds to listen to 1044 * @param listener Listener function 1045 * @return Listener handle 1046 */ 1047 public int addListener( 1048 Subscriber subscriber, 1049 EnumSet<NetworkTableEvent.Kind> eventKinds, 1050 Consumer<NetworkTableEvent> listener) { 1051 if (subscriber.getTopic().getInstance().getHandle() != m_handle) { 1052 throw new IllegalArgumentException("subscriber is not from this instance"); 1053 } 1054 return m_listeners.add(subscriber.getHandle(), eventKinds, listener); 1055 } 1056 1057 /** 1058 * Add a listener for changes on a subscriber. The callback function is called 1059 * asynchronously on a separate thread, so it's important to use synchronization or atomics when 1060 * accessing any shared state from the callback function. This does NOT keep the subscriber 1061 * active. 1062 * 1063 * @param subscriber Subscriber 1064 * @param eventKinds set of event kinds to listen to 1065 * @param listener Listener function 1066 * @return Listener handle 1067 */ 1068 public int addListener( 1069 MultiSubscriber subscriber, 1070 EnumSet<NetworkTableEvent.Kind> eventKinds, 1071 Consumer<NetworkTableEvent> listener) { 1072 if (subscriber.getInstance().getHandle() != m_handle) { 1073 throw new IllegalArgumentException("subscriber is not from this instance"); 1074 } 1075 return m_listeners.add(subscriber.getHandle(), eventKinds, listener); 1076 } 1077 1078 /** 1079 * Add a listener for changes on an entry. The callback function is called 1080 * asynchronously on a separate thread, so it's important to use synchronization or atomics when 1081 * accessing any shared state from the callback function. 1082 * 1083 * @param entry Entry 1084 * @param eventKinds set of event kinds to listen to 1085 * @param listener Listener function 1086 * @return Listener handle 1087 */ 1088 public int addListener( 1089 NetworkTableEntry entry, 1090 EnumSet<NetworkTableEvent.Kind> eventKinds, 1091 Consumer<NetworkTableEvent> listener) { 1092 if (entry.getTopic().getInstance().getHandle() != m_handle) { 1093 throw new IllegalArgumentException("entry is not from this instance"); 1094 } 1095 return m_listeners.add(entry.getHandle(), eventKinds, listener); 1096 } 1097 1098 /** 1099 * Add a listener for changes to topics with names that start with any of the given 1100 * prefixes. The callback function is called asynchronously on a separate thread, so it's 1101 * important to use synchronization or atomics when accessing any shared state from the callback 1102 * function. 1103 * 1104 * <p>This creates a corresponding internal subscriber with the lifetime of the 1105 * listener. 1106 * 1107 * @param prefixes Topic name string prefixes 1108 * @param eventKinds set of event kinds to listen to 1109 * @param listener Listener function 1110 * @return Listener handle 1111 */ 1112 public int addListener( 1113 String[] prefixes, 1114 EnumSet<NetworkTableEvent.Kind> eventKinds, 1115 Consumer<NetworkTableEvent> listener) { 1116 return m_listeners.add(prefixes, eventKinds, listener); 1117 } 1118 1119 /* 1120 * Client/Server Functions 1121 */ 1122 1123 /** 1124 * Get the current network mode. 1125 * 1126 * @return Enum set of NetworkMode. 1127 */ 1128 public EnumSet<NetworkMode> getNetworkMode() { 1129 int flags = NetworkTablesJNI.getNetworkMode(m_handle); 1130 EnumSet<NetworkMode> rv = EnumSet.noneOf(NetworkMode.class); 1131 for (NetworkMode mode : NetworkMode.values()) { 1132 if ((flags & mode.getValue()) != 0) { 1133 rv.add(mode); 1134 } 1135 } 1136 return rv; 1137 } 1138 1139 /** 1140 * Starts local-only operation. Prevents calls to startServer or startClient from taking effect. 1141 * Has no effect if startServer or startClient has already been called. 1142 */ 1143 public void startLocal() { 1144 NetworkTablesJNI.startLocal(m_handle); 1145 } 1146 1147 /** 1148 * Stops local-only operation. startServer or startClient can be called after this call to start 1149 * a server or client. 1150 */ 1151 public void stopLocal() { 1152 NetworkTablesJNI.stopLocal(m_handle); 1153 } 1154 1155 /** 1156 * Starts a server using the networktables.json as the persistent file, using the default 1157 * listening address and port. 1158 */ 1159 public void startServer() { 1160 startServer("networktables.json"); 1161 } 1162 1163 /** 1164 * Starts a server using the specified persistent filename, using the default listening address 1165 * and port. 1166 * 1167 * @param persistFilename the name of the persist file to use 1168 */ 1169 public void startServer(String persistFilename) { 1170 startServer(persistFilename, ""); 1171 } 1172 1173 /** 1174 * Starts a server using the specified filename and listening address, using the default port. 1175 * 1176 * @param persistFilename the name of the persist file to use 1177 * @param listenAddress the address to listen on, or empty to listen on any address 1178 */ 1179 public void startServer(String persistFilename, String listenAddress) { 1180 startServer(persistFilename, listenAddress, kDefaultPort3, kDefaultPort4); 1181 } 1182 1183 /** 1184 * Starts a server using the specified filename, listening address, and port. 1185 * 1186 * @param persistFilename the name of the persist file to use 1187 * @param listenAddress the address to listen on, or empty to listen on any address 1188 * @param port3 port to communicate over (NT3) 1189 */ 1190 public void startServer(String persistFilename, String listenAddress, int port3) { 1191 startServer(persistFilename, listenAddress, port3, kDefaultPort4); 1192 } 1193 1194 /** 1195 * Starts a server using the specified filename, listening address, and port. 1196 * 1197 * @param persistFilename the name of the persist file to use 1198 * @param listenAddress the address to listen on, or empty to listen on any address 1199 * @param port3 port to communicate over (NT3) 1200 * @param port4 port to communicate over (NT4) 1201 */ 1202 public void startServer(String persistFilename, String listenAddress, int port3, int port4) { 1203 NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port3, port4); 1204 } 1205 1206 /** Stops the server if it is running. */ 1207 public void stopServer() { 1208 NetworkTablesJNI.stopServer(m_handle); 1209 } 1210 1211 /** 1212 * Starts a NT3 client. Use SetServer or SetServerTeam to set the server name and port. 1213 * 1214 * @param identity network identity to advertise (cannot be empty string) 1215 */ 1216 public void startClient3(String identity) { 1217 NetworkTablesJNI.startClient3(m_handle, identity); 1218 } 1219 1220 /** 1221 * Starts a NT4 client. Use SetServer or SetServerTeam to set the server name and port. 1222 * 1223 * @param identity network identity to advertise (cannot be empty string) 1224 */ 1225 public void startClient4(String identity) { 1226 NetworkTablesJNI.startClient4(m_handle, identity); 1227 } 1228 1229 /** Stops the client if it is running. */ 1230 public void stopClient() { 1231 NetworkTablesJNI.stopClient(m_handle); 1232 } 1233 1234 /** 1235 * Sets server address and port for client (without restarting client). Changes the port to the 1236 * default port. 1237 * 1238 * @param serverName server name 1239 */ 1240 public void setServer(String serverName) { 1241 setServer(serverName, 0); 1242 } 1243 1244 /** 1245 * Sets server address and port for client (without restarting client). 1246 * 1247 * @param serverName server name 1248 * @param port port to communicate over (0=default) 1249 */ 1250 public void setServer(String serverName, int port) { 1251 NetworkTablesJNI.setServer(m_handle, serverName, port); 1252 } 1253 1254 /** 1255 * Sets server addresses and port for client (without restarting client). Changes the port to the 1256 * default port. The client will attempt to connect to each server in round robin fashion. 1257 * 1258 * @param serverNames array of server names 1259 */ 1260 public void setServer(String[] serverNames) { 1261 setServer(serverNames, 0); 1262 } 1263 1264 /** 1265 * Sets server addresses and port for client (without restarting client). The client will attempt 1266 * to connect to each server in round robin fashion. 1267 * 1268 * @param serverNames array of server names 1269 * @param port port to communicate over (0=default) 1270 */ 1271 public void setServer(String[] serverNames, int port) { 1272 int[] ports = new int[serverNames.length]; 1273 for (int i = 0; i < serverNames.length; i++) { 1274 ports[i] = port; 1275 } 1276 setServer(serverNames, ports); 1277 } 1278 1279 /** 1280 * Sets server addresses and ports for client (without restarting client). The client will 1281 * attempt to connect to each server in round robin fashion. 1282 * 1283 * @param serverNames array of server names 1284 * @param ports array of port numbers (0=default) 1285 */ 1286 public void setServer(String[] serverNames, int[] ports) { 1287 NetworkTablesJNI.setServer(m_handle, serverNames, ports); 1288 } 1289 1290 /** 1291 * Sets server addresses and port for client (without restarting client). Changes the port to the 1292 * default port. The client will attempt to connect to each server in round robin fashion. 1293 * 1294 * @param team team number 1295 */ 1296 public void setServerTeam(int team) { 1297 setServerTeam(team, 0); 1298 } 1299 1300 /** 1301 * Sets server addresses and port for client (without restarting client). Connects using commonly 1302 * known robot addresses for the specified team. 1303 * 1304 * @param team team number 1305 * @param port port to communicate over (0=default) 1306 */ 1307 public void setServerTeam(int team, int port) { 1308 NetworkTablesJNI.setServerTeam(m_handle, team, port); 1309 } 1310 1311 /** 1312 * Disconnects the client if it's running and connected. This will automatically start 1313 * reconnection attempts to the current server list. 1314 */ 1315 public void disconnect() { 1316 NetworkTablesJNI.disconnect(m_handle); 1317 } 1318 1319 /** 1320 * Starts requesting server address from Driver Station. This connects to the Driver Station 1321 * running on localhost to obtain the server IP address, and connects with the default port. 1322 */ 1323 public void startDSClient() { 1324 startDSClient(0); 1325 } 1326 1327 /** 1328 * Starts requesting server address from Driver Station. This connects to the Driver Station 1329 * running on localhost to obtain the server IP address. 1330 * 1331 * @param port server port to use in combination with IP from DS (0=default) 1332 */ 1333 public void startDSClient(int port) { 1334 NetworkTablesJNI.startDSClient(m_handle, port); 1335 } 1336 1337 /** Stops requesting server address from Driver Station. */ 1338 public void stopDSClient() { 1339 NetworkTablesJNI.stopDSClient(m_handle); 1340 } 1341 1342 /** 1343 * Flushes all updated values immediately to the local client/server. This does not flush to the 1344 * network. 1345 */ 1346 public void flushLocal() { 1347 NetworkTablesJNI.flushLocal(m_handle); 1348 } 1349 1350 /** 1351 * Flushes all updated values immediately to the network. Note: This is rate-limited to protect 1352 * the network from flooding. This is primarily useful for synchronizing network updates with 1353 * user code. 1354 */ 1355 public void flush() { 1356 NetworkTablesJNI.flush(m_handle); 1357 } 1358 1359 /** 1360 * Gets information on the currently established network connections. If operating as a client, 1361 * this will return either zero or one values. 1362 * 1363 * @return array of connection information 1364 */ 1365 public ConnectionInfo[] getConnections() { 1366 return NetworkTablesJNI.getConnections(m_handle); 1367 } 1368 1369 /** 1370 * Return whether or not the instance is connected to another node. 1371 * 1372 * @return True if connected. 1373 */ 1374 public boolean isConnected() { 1375 return NetworkTablesJNI.isConnected(m_handle); 1376 } 1377 1378 /** 1379 * Get the time offset between server time and local time. Add this value to local time to get 1380 * the estimated equivalent server time. In server mode, this always returns 0. In client mode, 1381 * this returns the time offset only if the client and server are connected and have exchanged 1382 * synchronization messages. Note the time offset may change over time as it is periodically 1383 * updated; to receive updates as events, add a listener to the "time sync" event. 1384 * 1385 * @return Time offset in microseconds (optional) 1386 */ 1387 public OptionalLong getServerTimeOffset() { 1388 return NetworkTablesJNI.getServerTimeOffset(m_handle); 1389 } 1390 1391 /** 1392 * Starts logging entry changes to a DataLog. 1393 * 1394 * @param log data log object; lifetime must extend until StopEntryDataLog is called or the 1395 * instance is destroyed 1396 * @param prefix only store entries with names that start with this prefix; the prefix is not 1397 * included in the data log entry name 1398 * @param logPrefix prefix to add to data log entry names 1399 * @return Data logger handle 1400 */ 1401 public int startEntryDataLog(DataLog log, String prefix, String logPrefix) { 1402 return NetworkTablesJNI.startEntryDataLog(m_handle, log, prefix, logPrefix); 1403 } 1404 1405 /** 1406 * Stops logging entry changes to a DataLog. 1407 * 1408 * @param logger data logger handle 1409 */ 1410 public static void stopEntryDataLog(int logger) { 1411 NetworkTablesJNI.stopEntryDataLog(logger); 1412 } 1413 1414 /** 1415 * Starts logging connection changes to a DataLog. 1416 * 1417 * @param log data log object; lifetime must extend until StopConnectionDataLog is called or the 1418 * instance is destroyed 1419 * @param name data log entry name 1420 * @return Data logger handle 1421 */ 1422 public int startConnectionDataLog(DataLog log, String name) { 1423 return NetworkTablesJNI.startConnectionDataLog(m_handle, log, name); 1424 } 1425 1426 /** 1427 * Stops logging connection changes to a DataLog. 1428 * 1429 * @param logger data logger handle 1430 */ 1431 public static void stopConnectionDataLog(int logger) { 1432 NetworkTablesJNI.stopConnectionDataLog(logger); 1433 } 1434 1435 /** 1436 * Add logger callback function. By default, log messages are sent to stderr; this function sends 1437 * log messages with the specified levels to the provided callback function instead. The callback 1438 * function will only be called for log messages with level greater than or equal to minLevel and 1439 * less than or equal to maxLevel; messages outside this range will be silently ignored. 1440 * 1441 * @param minLevel minimum log level 1442 * @param maxLevel maximum log level 1443 * @param func callback function 1444 * @return Listener handle 1445 */ 1446 public int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> func) { 1447 return m_listeners.addLogger(minLevel, maxLevel, func); 1448 } 1449 1450 /** 1451 * Returns whether there is a data schema already registered with the given name that this 1452 * instance has published. This does NOT perform a check as to whether the schema has already 1453 * been published by another node on the network. 1454 * 1455 * @param name Name (the string passed as the data type for topics using this schema) 1456 * @return True if schema already registered 1457 */ 1458 public boolean hasSchema(String name) { 1459 return m_schemas.containsKey("/.schema/" + name); 1460 } 1461 1462 /** 1463 * Registers a data schema. Data schemas provide information for how a certain data type string 1464 * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g. 1465 * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas 1466 * are published just like normal topics, with the name being generated from the provided name: 1467 * "/.schema/name". Duplicate calls to this function with the same name are silently ignored. 1468 * 1469 * @param name Name (the string passed as the data type for topics using this schema) 1470 * @param type Type of schema (e.g. "protobuf", "struct", etc) 1471 * @param schema Schema data 1472 */ 1473 public void addSchema(String name, String type, byte[] schema) { 1474 m_schemas.computeIfAbsent("/.schema/" + name, k -> { 1475 RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}"); 1476 pub.setDefault(schema); 1477 return pub; 1478 }); 1479 } 1480 1481 /** 1482 * Registers a data schema. Data schemas provide information for how a certain data type string 1483 * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g. 1484 * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas 1485 * are published just like normal topics, with the name being generated from the provided name: 1486 * "/.schema/name". Duplicate calls to this function with the same name are silently ignored. 1487 * 1488 * @param name Name (the string passed as the data type for topics using this schema) 1489 * @param type Type of schema (e.g. "protobuf", "struct", etc) 1490 * @param schema Schema data 1491 */ 1492 public void addSchema(String name, String type, String schema) { 1493 m_schemas.computeIfAbsent("/.schema/" + name, k -> { 1494 RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}"); 1495 pub.setDefault(StandardCharsets.UTF_8.encode(schema)); 1496 return pub; 1497 }); 1498 } 1499 1500 /** 1501 * Registers a protobuf schema. Duplicate calls to this function with the same name are silently 1502 * ignored. 1503 * 1504 * @param proto protobuf serialization object 1505 */ 1506 public void addSchema(Protobuf<?, ?> proto) { 1507 proto.forEachDescriptor( 1508 this::hasSchema, 1509 (typeString, schema) -> addSchema(typeString, "proto:FileDescriptorProto", schema)); 1510 } 1511 1512 /** 1513 * Registers a struct schema. Duplicate calls to this function with the same name are silently 1514 * ignored. 1515 * 1516 * @param struct struct serialization object 1517 */ 1518 public void addSchema(Struct<?> struct) { 1519 addSchemaImpl(struct, new HashSet<>()); 1520 } 1521 1522 @Override 1523 public boolean equals(Object other) { 1524 return other == this || other instanceof NetworkTableInstance inst && m_handle == inst.m_handle; 1525 } 1526 1527 @Override 1528 public int hashCode() { 1529 return m_handle; 1530 } 1531 1532 private void addSchemaImpl(Struct<?> struct, Set<String> seen) { 1533 String typeString = struct.getTypeString(); 1534 if (hasSchema(typeString)) { 1535 return; 1536 } 1537 if (!seen.add(typeString)) { 1538 throw new UnsupportedOperationException(typeString + ": circular reference with " + seen); 1539 } 1540 addSchema(typeString, "structschema", struct.getSchema()); 1541 for (Struct<?> inner : struct.getNested()) { 1542 addSchemaImpl(inner, seen); 1543 } 1544 seen.remove(typeString); 1545 } 1546 1547 private boolean m_owned; 1548 private int m_handle; 1549 private final ConcurrentMap<String, RawPublisher> m_schemas = new ConcurrentHashMap<>(); 1550}