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}