001package silicondust; 002/*- 003 * ########################################################################## 004 * TV H/W, EPGs, and Recording 005 * $Id: HDHRTuner.java 5999 2020-05-18 16:41:28Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/silicondust/trunk/src/main/java/silicondust/HDHRTuner.java $ 007 * %% 008 * Copyright (C) 2013 - 2020 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import java.io.IOException; 024import java.lang.reflect.Field; 025import java.lang.reflect.Modifier; 026import java.net.DatagramPacket; 027import java.net.DatagramSocket; 028import java.net.InetAddress; 029import java.net.InterfaceAddress; 030import java.net.NetworkInterface; 031import java.net.Socket; 032import java.net.SocketTimeoutException; 033import java.net.URI; 034import java.net.URISyntaxException; 035import java.nio.ByteBuffer; 036import java.util.ArrayList; 037import java.util.Collection; 038import java.util.Collections; 039import java.util.HashMap; 040import java.util.Iterator; 041import java.util.LinkedHashMap; 042import java.util.LinkedHashSet; 043import java.util.List; 044import java.util.Map; 045import java.util.Objects; 046import java.util.Random; 047import java.util.Set; 048import java.util.SortedMap; 049import java.util.SortedSet; 050import java.util.TreeMap; 051import java.util.TreeSet; 052import java.util.concurrent.ConcurrentHashMap; 053import java.util.stream.Collectors; 054import java.util.zip.CRC32; 055import org.apache.commons.lang3.StringUtils; 056import org.apache.commons.lang3.reflect.FieldUtils; 057 058import static java.lang.String.format; 059import static java.nio.ByteOrder.BIG_ENDIAN; 060import static java.nio.ByteOrder.LITTLE_ENDIAN; 061import static java.nio.charset.StandardCharsets.UTF_8; 062import static java.util.Arrays.asList; 063import static org.apache.commons.lang3.StringUtils.SPACE; 064 065/** 066 * {@link.uri http://www.silicondust.com/ SiliconDust} HDHomeRun tuner. 067 * 068 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 069 * @version $Revision: 5999 $ 070 */ 071public class HDHRTuner { 072 073 /** 074 * hdhomerun_pkt.h 075 */ 076 private static final int 077 DISCOVER_UDP_PORT = 65001, 078 CONTROL_TCP_PORT = 65001; 079 080 /** 081 * hdhomerun_pkt.h 082 */ 083 private static final int 084 MAX_PACKET_SIZE = 1460, 085 MAX_PAYLOAD_SIZE = 1452; 086 087 /** 088 * hdhomerun_pkt.h 089 */ 090 private static final short 091 TYPE_DISCOVER_REQ = 0x0002, 092 TYPE_DISCOVER_RPY = 0x0003, 093 TYPE_GETSET_REQ = 0x0004, 094 TYPE_GETSET_RPY = 0x0005, 095 TYPE_UPGRADE_REQ = 0x0006, 096 TYPE_UPGRADE_RPY = 0x0007; 097 098 /** 099 * hdhomerun_pkt.h 100 */ 101 private static final byte 102 TAG_DEVICE_TYPE = 0x01, 103 TAG_DEVICE_ID = 0x02, 104 TAG_GETSET_NAME = 0x03, 105 TAG_GETSET_VALUE = 0x04, 106 TAG_GETSET_LOCKKEY = 0x15, 107 TAG_ERROR_MESSAGE = 0x05, 108 TAG_TUNER_COUNT = 0x10; 109 110 /** 111 * hdhomerun_pkt.h 112 */ 113 private static final int 114 DEVICE_TYPE_WILDCARD = 0xFFFFFFFF, 115 DEVICE_TYPE_TUNER = 0x00000001; 116 117 /** 118 * hdhomerun_pkt.h 119 */ 120 private static final int DEVICE_ID_WILDCARD = 0xFFFFFFFF; 121 122 private static final String AT = "@"; 123 private static final String COLON = ":"; 124 private static final String EQUALS = "="; 125 private static final String LB = "["; 126 private static final String LF = "\n"; 127 private static final String RB = "]"; 128 private static final String SLASH = "/"; 129 130 private static final String AUTO = "auto"; 131 private static final String NONE = "none"; 132 133 /** 134 * Static method to determine the HDHomeRun tuners on the network. 135 * 136 * @return The {@link Map} of IDs and {@link HDHRTuner}s. 137 */ 138 public static Map<Integer,HDHRTuner> discover() { 139 HashMap<Integer,HDHRTuner> map = new HashMap<>(); 140 141 for (DiscoverReply reply : 142 new ArrayList<DiscoverReply>(thread().map().values())) { 143 if (! map.containsKey(reply.getID())) { 144 try { 145 map.put(reply.getID(), new HDHRTuner(reply)); 146 } catch (Exception exception) { 147 } 148 } 149 150 if (! map.containsKey(reply.getID())) { 151 thread().map().remove(reply.getInetAddress()); 152 } 153 } 154 155 return map; 156 } 157 158 private static DiscoveryThread thread = null; 159 160 private static DiscoveryThread thread() { 161 synchronized (HDHRTuner.class) { 162 if (thread == null || (! thread.isAlive())) { 163 thread = new DiscoveryThread(); 164 thread.start(); 165 166 try { 167 Thread.sleep(TIMEOUT / 2); 168 } catch (InterruptedException exception) { 169 } 170 } 171 } 172 173 return thread; 174 } 175 176 private final int id; 177 private final int count; 178 private final Socket socket; 179 private final String model; 180 private final FeatureMap featureMap; 181 private final transient Random random = new Random(); 182 private HDHRPrimeClient client = null; 183 private List<LineupEntry> lineup = null; 184 185 /** 186 * Sole public constructor. 187 * 188 * @param address The {@link InetAddress}. 189 */ 190 public HDHRTuner(InetAddress address) { this(ping(address)); } 191 192 private static DiscoverReply ping(InetAddress address) { 193 DiscoverReply reply = null; 194 195 try (DatagramSocket socket = new DatagramSocket()) { 196 socket.setBroadcast(true); 197 socket.setReuseAddress(true); 198 socket.setSoTimeout(1000); 199 200 new DiscoverRequest().send(socket, address, DISCOVER_UDP_PORT); 201 202 reply = new DiscoverReply(); 203 reply.receive(socket); 204 } catch (Exception exception) { 205 reply = null; 206 } 207 208 return reply; 209 } 210 211 private HDHRTuner(DiscoverReply reply) { 212 try { 213 id = reply.getID(); 214 count = reply.getCount(); 215 216 socket = new Socket(reply.getInetAddress(), CONTROL_TCP_PORT); 217 socket.setKeepAlive(true); 218 socket.setReuseAddress(true); 219 socket.setSoTimeout(2500); 220 221 model = get(0, "/sys/hwmodel"); 222 featureMap = new FeatureMap(); 223 } catch (Exception exception) { 224 throw new ExceptionInInitializerError(exception); 225 } 226 } 227 228 /** 229 * @return Tuner ID 230 */ 231 public int getID() { return id; } 232 233 /** 234 * @return Tuner Count 235 */ 236 public int getCount() { return count; } 237 238 /** 239 * @return Tuner {@link InetAddress} 240 */ 241 public InetAddress getInetAddress() { return socket.getInetAddress(); } 242 243 /** 244 * @return Tuner {@link URI} 245 */ 246 public URI getURI() { 247 URI uri = null; 248 249 try { 250 uri = 251 new URI("http", getInetAddress().getHostAddress(), "/", null); 252 } catch (URISyntaxException exception) { 253 } 254 255 return uri; 256 } 257 258 /** 259 * @return Tuner {@code /sys/hwmodel} 260 */ 261 public String getModel() { return (model != null) ? model : "unknown"; } 262 263 /** 264 * @return Tuner {@code /sys/features} 265 */ 266 public Map<String,Set<String>> getFeature() { return featureMap; } 267 268 /** 269 * @return {@link HDHRPrimeClient} 270 */ 271 public HDHRPrimeClient getClient() { 272 synchronized (this) { 273 if (client == null) { 274 client = 275 new HDHRPrimeClient(getInetAddress().getHostAddress()); 276 } 277 } 278 279 return client; 280 } 281 282 /** 283 * @return {@link HDHRPrimeClient#getLineup()} 284 */ 285 public List<LineupEntry> getLineup() { 286 synchronized (this) { 287 if (lineup == null || lineup.isEmpty()) { 288 try { 289 lineup = getClient().getLineup(); 290 } catch (Exception exception) { 291 lineup = Collections.emptyList(); 292 } 293 } 294 } 295 296 return lineup; 297 } 298 299 /** 300 * Method to obtain a lock on a tuner. 301 * 302 * @param tuner The tuner number. 303 * 304 * @return The lock key value. 305 * 306 * @throws IOException If the tuner cannot be locked. 307 */ 308 public int lock(int tuner) throws IOException { 309 int key = 0; 310 311 try { 312 while (key == 0) { 313 key = Math.abs(random.nextInt()) & 0xFFFFFFFF; 314 } 315 316 String name = format("/tuner%d/lockkey", tuner); 317 String value = format("%d", key); 318 GetSetReply reply = new GetSetReply(); 319 320 new GetSetRequest(0, name, value).send(socket); 321 reply.receive(socket); 322 323 if (reply.getErrorMessage() != null) { 324 throw new IOException(reply.getErrorMessage()); 325 } 326 327 Map<String,String> map = reply.map(); 328 329 if (NetworkInterface.getByInetAddress(InetAddress.getByName(map.get(name))) == null) { 330 throw new IOException("Cannot lock " + name); 331 } 332 } catch (IOException exception) { 333 throw exception; 334 } catch (Exception exception) { 335 throw new IOException(exception); 336 } 337 338 return key; 339 } 340 341 /** 342 * Method to unlock a tuner. 343 * 344 * @param tuner The tuner number. 345 * @param key The key value. 346 * 347 * @throws IOException If the tuner cannot be unlocked. 348 */ 349 public void unlock(int tuner, int key) throws IOException { 350 if (key != 0) { 351 set(key, format("/tuner%d/lockkey", tuner), format(NONE)); 352 } 353 } 354 355 /** 356 * Method to get a tuner lock owner. 357 * 358 * @param tuner The tuner number. 359 * @param key The key value. 360 * 361 * @return The {@link InetAddress} of the owner (or {@code null} if 362 * there is none). 363 * 364 * @throws IOException If the tuner locker cannot be determined. 365 */ 366 public InetAddress locker(int tuner, int key) throws IOException { 367 String string = get(key, format("/tuner%d/lockkey", tuner)); 368 369 return (! isNone(string)) ? InetAddress.getByName(string) : null; 370 } 371 372 private boolean isNone(String string) { 373 return StringUtils.isEmpty(string) || string.equals(NONE); 374 } 375 376 /** 377 * Method to get an option value from this {@link HDHRTuner}. 378 * 379 * @param key The tuner lockkey (if set). 380 * @param name The option name. 381 * 382 * @return The option value. 383 * 384 * @throws IOException If the option cannot be retrieved. 385 */ 386 public String get(int key, String name) throws IOException { 387 GetSetReply reply = new GetSetReply(); 388 389 new GetSetRequest(key, name, null).send(socket); 390 reply.receive(socket); 391 392 if (reply.getErrorMessage() != null) { 393 throw new IOException(name + ": " + reply.getErrorMessage()); 394 } 395 396 return reply.map().get(name); 397 } 398 399 /** 400 * Method to set an option value for this {@link HDHRTuner}. 401 * 402 * @param key The tuner lockkey (if set). 403 * @param name The option name. 404 * @param value The option value. 405 * @param pairs Optional name/value pairs. 406 * 407 * @throws IOException If the option(s) cannot be set. 408 */ 409 public void set(int key, 410 String name, String value, 411 String... pairs) throws IOException { 412 GetSetReply reply = new GetSetReply(); 413 414 new GetSetRequest(key, name, value, pairs).send(socket); 415 reply.receive(socket); 416 417 if (reply.getErrorMessage() != null) { 418 throw new IOException(name + ": " + reply.getErrorMessage()); 419 } 420 421 Map<String,String> map = reply.map(); 422 423 check(name, value, map); 424 425 for (int i = 0; i < pairs.length; i += 2) { 426 check(pairs[i], pairs[i + 1], map); 427 } 428 } 429 430 private void check(String name, String value, 431 Map<String,String> map) throws IOException { 432 if (! value.equals(map.get(name))) { 433 throw new IOException("Could not set " + name + " to " + value); 434 } 435 } 436 437 /** 438 * Method to get the configuration options supported by this 439 * {@link HDHRTuner}. 440 * 441 * @return The {@link SortedSet} of options. 442 * 443 * @throws IOException If the options cannot be retrieved. 444 */ 445 public SortedSet<String> options() throws IOException { 446 TreeSet<String> set = new TreeSet<>(); 447 String string = get(0, "help"); 448 449 for (String line : string.split(LF)) { 450 if (line.startsWith(SLASH)) { 451 String option = line.split(SPACE, 2)[0].trim(); 452 453 for (int i = 0, n = getCount(); i < n; i += 1) { 454 set.add(option.replaceAll("<n>", String.valueOf(i))); 455 } 456 } 457 } 458 459 return set; 460 } 461 462 /** 463 * Method to scan the first tuner available and return a {@link List} of 464 * {@link HDHRProgram}s found. 465 * 466 * @return The {@link List} of {@link HDHRProgram}s found. 467 * 468 * @throws IOException If the {@link HDHRProgram}s cannot be 469 * retrieved. 470 */ 471 public List<HDHRProgram> scan() throws IOException { 472 List<HDHRProgram> list = null; 473 IOException thrown = null; 474 475 for (int tuner = 0, n = getCount(); tuner < n; tuner += 1) { 476 int key = 0; 477 478 try { 479 key = lock(tuner); 480 list = scan(tuner, key); 481 thrown = null; 482 break; 483 } catch (IOException exception) { 484 thrown = exception; 485 continue; 486 } finally { 487 unlock(tuner, key); 488 } 489 } 490 491 if (thrown != null) { 492 throw thrown; 493 } 494 495 return list; 496 } 497 498 /** 499 * Method to scan the specified tuner and return a {@link List} of 500 * {@link HDHRProgram}s found. 501 * 502 * @param tuner The tuner to scan. 503 * @param key The key value. 504 * 505 * @return The {@link List} of {@link HDHRProgram}s found. 506 * 507 * @throws IOException If the {@link HDHRProgram}s cannot be 508 * retrieved. 509 */ 510 public List<HDHRProgram> scan(int tuner, int key) throws IOException { 511 if (! (0 <= tuner && tuner < getCount())) { 512 throw new IllegalArgumentException("tuner=" + tuner); 513 } 514 515 ArrayList<HDHRProgram> list = new ArrayList<>(); 516 517 for (int frequency : new TreeSet<Integer>(Channel.MAP.keySet())) { 518 StatusMap status = null; 519 520 try { 521 channel(tuner, key, frequency); 522 status = new StatusMap(tuner, key); 523 } catch (IOException exception) { 524 continue; 525 } 526 527 long deadline = System.currentTimeMillis() + 2500; 528 529 do { 530 status = new StatusMap(tuner, key); 531 532 if (status.hasLock() && status.getSS() > 45) { 533 break; 534 } else { 535 sleep(250); 536 } 537 } while (System.currentTimeMillis() < deadline); 538 539 if (status.hasLock()) { 540 deadline = System.currentTimeMillis() + 5000; 541 542 do { 543 status = new StatusMap(tuner, key); 544 545 if (status.getSEQ() >= 100) { 546 break; 547 } else { 548 sleep(250); 549 } 550 } while (System.currentTimeMillis() < deadline); 551 552 if (status.getSEQ() >= 100) { 553 sleep(5000); 554 555 StreaminfoMap streaminfo = 556 new StreaminfoMap(tuner, key, frequency); 557 558 if (streaminfo.getTSID() != null) { 559 if (! streaminfo.isEmpty()) { 560 list.addAll(streaminfo.values()); 561 } 562 } 563 } 564 } 565 } 566 567 target(tuner, key, null); 568 569 return list; 570 } 571 572 /** 573 * Method to get the {@code /tuner<n>/channelmap} value on the specified 574 * tuner, 575 * 576 * @param tuner The tuner to scan. 577 * @param key The key value. 578 579 * @return The value of the {@code /tuner<n>/channelmap}. 580 * 581 * @throws IOException If the channel map name cannot be 582 * retrieved. 583 */ 584 public String channelmap(int tuner, int key) throws IOException { 585 String string = get(key, format("/tuner%d/channelmap", tuner)); 586 587 return (! isNone(string)) ? string : null; 588 } 589 590 /** 591 * Method to specify the {@code /tuner<n>/channelmap} on the specified 592 * tuner, 593 * 594 * @param tuner The tuner to scan. 595 * @param key The key value. 596 * @param string The name of the {@code /tuner<n>/channelmap}. 597 * 598 * @throws IOException If the channel map name cannot be 599 * specified. 600 */ 601 public void channelmap(int tuner, int key, 602 String string) throws IOException { 603 set(key, 604 format("/tuner%d/channelmap", tuner), 605 (! isNone(string)) ? string : NONE); 606 } 607 608 /** 609 * Method to tune the specified tuner, 610 * 611 * @param tuner The tuner to scan. 612 * @param key The key value. 613 * @param channel The channel specification (number or 614 * frequency with optional modulation). 615 * @param program The program (subchannel) specification. 616 * 617 * @throws IOException If the tuner cannot be tuned. 618 * 619 * @see #channel(int,int,String) 620 * @see #program(int,int,int) 621 */ 622 public void channel(int tuner, int key, 623 String channel, int program) throws IOException { 624 channel(tuner, key, channel); 625 626 if (! isNone(channel)) { 627 program(tuner, key, program); 628 } 629 } 630 631 /** 632 * Method to tune the specified tuner, 633 * 634 * @param tuner The tuner to scan. 635 * @param key The key value. 636 * @param channel The channel specification (number or 637 * frequency). 638 * @param program The program (subchannel) specification. 639 * 640 * @throws IOException If the tuner cannot be tuned. 641 * 642 * @see #channel(int,int,int) 643 * @see #program(int,int,int) 644 */ 645 public void channel(int tuner, int key, 646 int channel, int program) throws IOException { 647 channel(tuner, key, channel); 648 program(tuner, key, program); 649 } 650 651 /** 652 * Method to tune the specified tuner, 653 * 654 * @param tuner The tuner to scan. 655 * @param key The key value. 656 * @param channel The channel specification (number or 657 * frequency with optional modulation). 658 * 659 * @throws IOException If the tuner cannot be tuned. 660 */ 661 public void channel(int tuner, int key, 662 String channel) throws IOException { 663 if (! isNone(channel)) { 664 if (channel.indexOf(COLON) < 0) { 665 channel = AUTO + COLON + channel; 666 } 667 } else { 668 channel = NONE; 669 } 670 671 set(key, format("/tuner%d/channel", tuner), channel); 672 } 673 674 /** 675 * Method to tune the specified tuner, 676 * 677 * @param tuner The tuner to scan. 678 * @param key The key value. 679 * @param channel The channel specification (number or 680 * frequency). 681 * 682 * @throws IOException If the tuner cannot be tuned. 683 */ 684 public void channel(int tuner, int key, int channel) throws IOException { 685 channel(tuner, key, format("%d", channel)); 686 } 687 688 /** 689 * Method to tune the specified tuner, 690 * 691 * @param tuner The tuner to scan. 692 * @param key The key value. 693 * @param program The program (subchannel) specification. 694 * 695 * @throws IOException If the tuner cannot be tuned. 696 */ 697 public void program(int tuner, int key, int program) throws IOException { 698 set(key, format("/tuner%d/program", tuner), format("%d", program)); 699 } 700 701 /** 702 * Method to set the tuner target, 703 * 704 * @param tuner The tuner to scan. 705 * @param key The key value. 706 * @param locator The target {@link URI}. 707 * 708 * @throws IOException If the tuner target cannot be set. 709 */ 710 public void target(int tuner, int key, URI locator) throws IOException { 711 set(key, 712 format("/tuner%d/target", tuner), 713 (locator != null) ? locator.toASCIIString() : NONE); 714 } 715 716 @Override 717 public String toString() { 718 StringBuilder buffer = 719 new StringBuilder(getModel()) 720 .append(COLON).append(Integer.toString(getID(), 16)) 721 .append(LB).append(Integer.toString(getCount())).append(RB); 722 InetAddress address = getInetAddress(); 723 724 if (address != null) { 725 buffer.append(AT).append(address.getHostAddress()); 726 } 727 728 return buffer.toString(); 729 } 730 731 private static void sleep(long milliseconds) { 732 try { 733 Thread.sleep(milliseconds); 734 } catch (InterruptedException exception) { 735 } 736 } 737 738 private static final int TIMEOUT = 10 * 1000; 739 740 private static class DiscoveryThread extends Thread { 741 private final DatagramSocket socket; 742 private final Set<InetAddress> addresses; 743 private final ConcurrentHashMap<InetAddress,DiscoverReply> map = 744 new ConcurrentHashMap<>(); 745 746 public DiscoveryThread() { 747 super(); 748 749 try { 750 socket = new DatagramSocket(); 751 socket.setBroadcast(true); 752 socket.setReuseAddress(true); 753 socket.setSoTimeout(TIMEOUT); 754 755 ArrayList<InterfaceAddress> list = new ArrayList<>(); 756 757 Collections.list(NetworkInterface.getNetworkInterfaces()) 758 .stream() 759 .forEach(t -> list.addAll(t.getInterfaceAddresses())); 760 761 addresses = 762 list.stream() 763 .filter(Objects::nonNull) 764 .map(t -> t.getBroadcast()) 765 .filter(Objects::nonNull) 766 .collect(Collectors.toSet()); 767 } catch (Exception exception) { 768 throw new ExceptionInInitializerError(exception); 769 } 770 771 setDaemon(true); 772 setName(getClass().getName()); 773 } 774 775 public Map<InetAddress,DiscoverReply> map() { return map; } 776 777 @Override 778 public void run() { 779 try { 780 List<Thread> list = asList(new Receiver(), new Sender()); 781 782 for (Thread thread : list) { 783 thread.start(); 784 } 785 786 for (Thread thread : list) { 787 try { 788 thread.join(); 789 } catch (InterruptedException exception) { 790 continue; 791 } 792 } 793 } finally { 794 if (socket != null) { 795 socket.close(); 796 } 797 } 798 } 799 800 private class Receiver extends Thread { 801 public Receiver() { 802 super(); 803 804 setName(getClass().getName()); 805 } 806 807 @Override 808 public void run() { 809 HashMap<InetAddress,DiscoverReply> map = new HashMap<>(); 810 811 for (;;) { 812 try { 813 DiscoverReply reply = new DiscoverReply(); 814 815 reply.receive(socket); 816 map.put(reply.getInetAddress(), reply); 817 818 map().putAll(map); 819 } catch (SocketTimeoutException exception) { 820 map().keySet().retainAll(map.keySet()); 821 map.clear(); 822 continue; 823 } catch (IOException exception) { 824 break; 825 } 826 } 827 } 828 } 829 830 private class Sender extends Thread { 831 public Sender() { 832 super(); 833 834 setName(getClass().getName()); 835 } 836 837 @Override 838 public void run() { 839 for (;;) { 840 DiscoverRequest request = new DiscoverRequest(); 841 842 for (InetAddress address : addresses) { 843 try { 844 request.send(socket, address, DISCOVER_UDP_PORT); 845 } catch (IOException exception) { 846 break; 847 } 848 } 849 850 try { 851 Thread.sleep(TIMEOUT); 852 } catch (InterruptedException exception) { 853 continue; 854 } 855 } 856 } 857 } 858 } 859 860 /** 861 * {@code /sys/features} {@link Map}. 862 */ 863 private class FeatureMap extends LinkedHashMap<String,Set<String>> { 864 private static final long serialVersionUID = -6053149128443314847L; 865 866 protected FeatureMap() throws IOException { 867 this(HDHRTuner.this.get(0, "/sys/features")); 868 } 869 870 private FeatureMap(String features) { 871 super(); 872 873 for (String line : features.split("[\\n]+")) { 874 line = line.trim(); 875 876 if (! StringUtils.isEmpty(line)) { 877 String[] entry = line.split(COLON, 2); 878 879 put(entry[0].trim(), new SetImpl(entry[1])); 880 } 881 } 882 } 883 884 private class SetImpl extends LinkedHashSet<String> { 885 private static final long serialVersionUID = 6764399687983160653L; 886 887 public SetImpl(String string) { 888 super(); 889 890 addAll(asList(string.trim().split("[\\p{Space}]+"))); 891 remove(StringUtils.EMPTY); 892 } 893 } 894 } 895 896 /** 897 * {@code /tuner<n>/status} {@link Map}. 898 */ 899 private class StatusMap extends LinkedHashMap<String,String> { 900 private static final long serialVersionUID = -4078700272266143306L; 901 902 private static final String LOCK = "lock"; 903 private static final String SEQ = "seq"; 904 private static final String SNQ = "snq"; 905 private static final String SS = "ss"; 906 907 protected StatusMap(int tuner, int key) throws IOException { 908 this(HDHRTuner.this.get(key, format("/tuner%d/status", tuner))); 909 } 910 911 private StatusMap(String status) { 912 super(); 913 914 for (String entry : status.split("[\\p{Space}]+")) { 915 String[] pair = entry.split("=", 2); 916 917 put(pair[0], pair[1]); 918 } 919 } 920 921 public String getModulation() { return get(LOCK); } 922 public int getSEQ() { return intValue(get(SEQ)); } 923 public int getSNQ() { return intValue(get(SNQ)); } 924 public int getSS() { return intValue(get(SS)); } 925 926 public boolean hasLock() { 927 return containsKey(LOCK) && (! get(LOCK).equals(NONE)); 928 } 929 930 private int intValue(String string) { 931 return (string != null) ? Integer.parseInt(string) : 0; 932 } 933 } 934 935 /** 936 * {@code /tuner<n>/streaminfo} {@link SortedMap}. 937 */ 938 private class StreaminfoMap extends TreeMap<Integer,HDHRProgram> { 939 private static final long serialVersionUID = 8619516460294488583L; 940 941 private final Integer tsid; 942 private final LinkedHashSet<String> flags = new LinkedHashSet<>(); 943 944 protected StreaminfoMap(int tuner, int key, 945 int frequency) throws IOException { 946 this(frequency, 947 new StatusMap(tuner, key).getModulation(), 948 HDHRTuner.this.get(key, 949 format("/tuner%d/streaminfo", tuner))); 950 } 951 952 private StreaminfoMap(int frequency, 953 String modulation, String streaminfo) { 954 super(); 955 956 ArrayList<String> list = new ArrayList<>(); 957 958 list.addAll(asList(streaminfo.split(LF))); 959 960 for (int i = 0, n = list.size(); i < n; i += 1) { 961 list.set(i, list.get(i).trim()); 962 } 963 964 while (list.remove(StringUtils.EMPTY)) { 965 } 966 967 Integer tsid = null; 968 Iterator<String> iterator = list.iterator(); 969 970 while (iterator.hasNext()) { 971 String line = iterator.next(); 972 973 if (line.startsWith("tsid=")) { 974 tsid = Integer.decode(line.split(EQUALS, 2)[1].trim()); 975 iterator.remove(); 976 } 977 } 978 979 this.tsid = tsid; 980 981 iterator = list.iterator(); 982 983 while (iterator.hasNext()) { 984 String line = iterator.next(); 985 986 if (HDHRProgram.PATTERN.matcher(line).matches()) { 987 HDHRProgram program = 988 new HDHRProgram(frequency, modulation, tsid, line); 989 990 put(program.getNumber(), program); 991 iterator.remove(); 992 } 993 } 994 995 flags.addAll(list); 996 } 997 998 public Integer getTSID() { return tsid; } 999 1000 public Set<String> getFlags() { return flags; } 1001 1002 @Override 1003 public String toString() { 1004 return ("tsid=" 1005 + ((tsid != null) 1006 ? ("0x" + Integer.toString(tsid, 16)) 1007 : NONE) 1008 + flags + super.toString()); 1009 } 1010 } 1011 1012 private static class Pkt { 1013 private static final SortedMap<Short,String> MAP = new TreeMap<>(); 1014 1015 static { 1016 try { 1017 List<Field> list = 1018 FieldUtils.getAllFieldsList(HDHRTuner.class) 1019 .stream() 1020 .filter(t -> Modifier.isStatic(t.getModifiers())) 1021 .filter(t -> Short.class.isAssignableFrom(t.getType())) 1022 .collect(Collectors.toList()); 1023 1024 for (Field field : list) { 1025 MAP.put(field.getShort(null), field.getName()); 1026 } 1027 } catch (Exception exception) { 1028 throw new ExceptionInInitializerError(exception); 1029 } 1030 } 1031 1032 private final ByteBuffer buffer; 1033 private final ByteBuffer payload; 1034 private transient String message = null; 1035 1036 public Pkt() { this((short) 0); } 1037 1038 public Pkt(short type) { 1039 this(ByteBuffer.allocate(MAX_PACKET_SIZE)); 1040 1041 buffer.rewind(); 1042 payload.rewind(); 1043 1044 setType(type); 1045 setPayloadLength(MAX_PAYLOAD_SIZE); 1046 } 1047 1048 private Pkt(ByteBuffer buffer) { 1049 this.buffer = buffer.order(BIG_ENDIAN); 1050 this.buffer.getShort(); 1051 this.buffer.getShort(); 1052 1053 this.payload = buffer.slice(); 1054 } 1055 1056 protected ByteBuffer buffer() { return buffer; } 1057 protected ByteBuffer payload() { return payload; } 1058 1059 public int getLength() { return getCRCOffset() + 4; } 1060 1061 public short getType() { return buffer.getShort(0); } 1062 protected void setType(short type) { buffer.putShort(0, type); } 1063 1064 protected void put(byte tag, byte value) { 1065 payload.put(tag); putLen(1); payload.put(value); 1066 } 1067 1068 protected void put(byte tag, short value) { 1069 payload.put(tag); putLen(2); payload.putShort(value); 1070 } 1071 1072 protected void put(byte tag, int value) { 1073 payload.put(tag); putLen(4); payload.putInt(value); 1074 } 1075 1076 protected void put(byte tag, String value) { 1077 payload.put(tag); 1078 1079 byte[] bytes = value.getBytes(UTF_8); 1080 1081 putLen(bytes.length + 1); 1082 payload.put(bytes); 1083 payload.put((byte) 0); 1084 } 1085 1086 private void putLen(int len) { 1087 if (len <= 127) { 1088 payload.put((byte) (len & 0xFF)); 1089 } else { 1090 payload.put((byte) ((len | 0x80) & 0xFF)); 1091 payload.put((byte) ((len >> 7) & 0xFF)); 1092 } 1093 } 1094 1095 protected String toString(ByteBuffer buffer) { 1096 return new String(buffer.array(), 1097 buffer.arrayOffset(), 1098 buffer.limit() - 1, 1099 UTF_8); 1100 } 1101 1102 public int getPayloadLength() { return buffer.getShort(2); } 1103 protected void setPayloadLength(int length) { 1104 buffer.putShort(2, (short) (length & 0xFFFF)); 1105 payload.limit(getPayloadLength()); 1106 } 1107 1108 public int getCRCOffset() { return 4 + getPayloadLength(); } 1109 1110 protected void seal(int length) { 1111 setPayloadLength(length); 1112 buffer.position(getCRCOffset()); 1113 1114 CRC32 crc = new CRC32(); 1115 1116 crc.update(buffer.array(), 0, buffer.position()); 1117 1118 buffer.order(LITTLE_ENDIAN); 1119 buffer.putInt((int) (crc.getValue() & 0xFFFFFFFF)); 1120 buffer.order(BIG_ENDIAN); 1121 buffer.flip(); 1122 } 1123 1124 protected void parse() { 1125 setPayloadLength(getPayloadLength()); 1126 1127 ByteBuffer payload = payload(); 1128 1129 while (payload.hasRemaining()) { 1130 byte tag = payload.get(); 1131 int len = payload.get(); 1132 1133 if ((len & 0x80) != 0) { 1134 len &= 0x7F; 1135 len |= ((int) payload.get()) << 7; 1136 } 1137 1138 ByteBuffer value = payload.slice(); 1139 1140 value.limit(len); 1141 1142 payload.position(payload.position() + len); 1143 1144 parse(tag, value); 1145 } 1146 } 1147 1148 protected void parse(byte tag, ByteBuffer value) { 1149 switch (tag) { 1150 case TAG_ERROR_MESSAGE: 1151 message = toString(value); 1152 break; 1153 1154 default: 1155 break; 1156 } 1157 } 1158 1159 public String getErrorMessage() { return message; } 1160 1161 public void receive(DatagramSocket socket) throws IOException { 1162 buffer().clear(); 1163 1164 DatagramPacket packet = 1165 new DatagramPacket(buffer().array(), getLength()); 1166 1167 socket.receive(packet); 1168 1169 receive(packet); 1170 } 1171 1172 protected void receive(DatagramPacket packet) { parse(); } 1173 1174 public void receive(Socket socket) throws IOException { 1175 buffer().clear(); 1176 1177 int length = 1178 socket.getInputStream() 1179 .read(buffer.array(), 0, buffer.limit()); 1180 1181 buffer.limit(length); 1182 1183 parse(); 1184 } 1185 1186 public void send(DatagramSocket socket, 1187 InetAddress address, int port) throws IOException { 1188 DatagramPacket packet = 1189 new DatagramPacket(buffer().array(), getLength()); 1190 1191 packet.setAddress(address); 1192 packet.setPort(port); 1193 1194 socket.send(packet); 1195 } 1196 1197 public void send(Socket socket) throws IOException { 1198 socket.getOutputStream().write(buffer().array(), 0, getLength()); 1199 } 1200 1201 @Override 1202 public String toString() { 1203 String type = MAP.get(getType()); 1204 1205 return (LB + ((type != null) ? type : String.valueOf(getType())) 1206 + COLON + getPayloadLength() + RB); 1207 } 1208 } 1209 1210 private static class DiscoverRequest extends Pkt { 1211 public DiscoverRequest() { 1212 super(TYPE_DISCOVER_REQ); 1213 1214 put(TAG_DEVICE_TYPE, DEVICE_TYPE_TUNER); 1215 put(TAG_DEVICE_ID, DEVICE_ID_WILDCARD); 1216 put(TAG_TUNER_COUNT, (byte) 0); 1217 1218 payload().limit(payload().position()); 1219 1220 seal(payload().limit()); 1221 } 1222 } 1223 1224 private static class DiscoverReply extends Pkt { 1225 private InetAddress address = null; 1226 private int id = -1; 1227 private int count = -1; 1228 1229 public DiscoverReply() { super(); } 1230 1231 public InetAddress getInetAddress() { return address; } 1232 public int getID() { return id; } 1233 public int getCount() { return count; } 1234 1235 @Override 1236 protected void receive(DatagramPacket packet) { 1237 super.receive(packet); 1238 1239 address = packet.getAddress(); 1240 } 1241 1242 @Override 1243 protected void parse() { 1244 super.parse(); 1245 1246 if (count == 0) { 1247 switch (id >> 20) { 1248 case 0x102: 1249 count = 1; 1250 break; 1251 1252 case 0x100: 1253 case 0x101: 1254 case 0x121: 1255 count = 2; 1256 break; 1257 1258 default: 1259 break; 1260 } 1261 } 1262 } 1263 1264 @Override 1265 protected void parse(byte tag, ByteBuffer value) { 1266 super.parse(tag, value); 1267 1268 switch (tag) { 1269 case TAG_DEVICE_ID: 1270 id = value.getInt(); 1271 break; 1272 1273 case TAG_TUNER_COUNT: 1274 count = value.get(); 1275 break; 1276 1277 default: 1278 break; 1279 } 1280 } 1281 } 1282 1283 private class GetSetRequest extends Pkt { 1284 public GetSetRequest(int key, 1285 String name, String value, String... pairs) { 1286 super(TYPE_GETSET_REQ); 1287 1288 put(TAG_GETSET_NAME, name); 1289 1290 if (value != null) { 1291 put(TAG_GETSET_VALUE, value); 1292 } 1293 1294 for (int i = 0; i < pairs.length; i += 2) { 1295 name = pairs[i]; 1296 value = pairs[i + 1]; 1297 1298 put(TAG_GETSET_NAME, name); 1299 1300 if (value != null) { 1301 put(TAG_GETSET_VALUE, value); 1302 } 1303 } 1304 1305 put(TAG_GETSET_LOCKKEY, key); 1306 1307 payload().limit(payload().position()); 1308 1309 seal(payload().limit()); 1310 } 1311 } 1312 1313 private class GetSetReply extends Pkt { 1314 private final TreeMap<String,String> map = new TreeMap<>(); 1315 private transient String name = null; 1316 1317 public GetSetReply() { super(); } 1318 1319 public Map<String,String> map() { return map; } 1320 1321 @Override 1322 protected void parse() { 1323 super.parse(); 1324 1325 if (name != null) { 1326 map.put(name, null); 1327 name = null; 1328 } 1329 } 1330 1331 @Override 1332 protected void parse(byte tag, ByteBuffer value) { 1333 super.parse(tag, value); 1334 1335 switch (tag) { 1336 case TAG_GETSET_NAME: 1337 if (name != null) { 1338 map.put(name, null); 1339 name = null; 1340 } 1341 1342 name = toString(value); 1343 break; 1344 1345 case TAG_GETSET_VALUE: 1346 map.put(name, toString(value)); 1347 name = null; 1348 break; 1349 1350 default: 1351 break; 1352 } 1353 } 1354 } 1355}