001package jmri.jmrix.anyma;
002
003import static java.lang.System.arraycopy;
004
005import java.util.Arrays;
006import java.util.concurrent.Executors;
007import java.util.concurrent.ScheduledExecutorService;
008import java.util.concurrent.TimeUnit;
009import javax.usb.UsbConst;
010import jmri.util.MathUtil;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * Traffic controller for Anyma DMX.
016 *
017 * @author George Warner Copyright (c) 2017-2018
018 * @since 4.9.6
019 */
020public class AnymaDMX_TrafficController {
021
022    private byte[] old_data = new byte[512];
023    private byte[] new_data = new byte[512];
024    private ScheduledExecutorService execService = null;
025    private AnymaDMX_UsbPortAdapter controller = null;
026
027    /**
028     * Create a new AnymaTrafficController instance.
029     */
030    public AnymaDMX_TrafficController() {
031       // this forces first pass to transmit everything
032        Arrays.fill(old_data, (byte) -1);
033
034        execService = Executors.newScheduledThreadPool(5);
035        execService.scheduleAtFixedRate(() -> {
036            // if the new_data has changed...
037            if (!Arrays.equals(old_data, new_data)) {
038                // find indexes to first/last bytes that are different
039                int from = old_data.length;
040                int to = 0;
041                for (int i = 0; i < old_data.length; i++) {
042                    if (old_data[i] != new_data[i]) {
043                        from = Math.min(from, i);
044                        to = i;
045                    }
046                }
047                if (from <= to) {
048                    int len = to - from + 1;
049                    byte[] buf = new byte[len];
050                    System.arraycopy(new_data, from, buf, 0, len);
051                    if (sendChannelRangeValues(from, to, buf)) {
052                        arraycopy(new_data, from, old_data, from, len);
053                    }
054                }
055            }
056        }, 0, 100L, TimeUnit.MILLISECONDS); // 10 times per second
057    }
058
059    /**
060     * Make connection to existing PortController (adapter) object.
061     *
062     * @param p the AnymaDMX_UsbPortAdapter we're connecting to
063     */
064    public void connectPort(AnymaDMX_UsbPortAdapter p) {
065        if (controller != null) {
066            log.warn("connectPort called when already connected");
067        } else {
068            log.debug("connectPort invoked");
069        }
070        controller = p;
071    }
072
073    /**
074     * set a channel's value
075     *
076     * @param channel the channel (1 - 512 inclusive)
077     * @param value   the value
078     */
079    public void setChannelValue(int channel, byte value) {
080        if ((1 <= channel) && (channel <= 512)) {
081            new_data[channel - 1] = value;
082        }
083    }
084
085    /**
086     * set the values for a range of channels
087     *
088     * @param from the beginning index (inclusive)
089     * @param to   the ending index (inclusive)
090     * @param buf  the data to send
091     * note: the from/to indexes are 1-512 (inclusive)
092     */
093    public void setChannelRangeValues(int from, int to, byte buf[]) {
094        if ((1 <= from) && (from <= 512) && (1 <= to) && (to <= 512)) {
095            int len = to - from + 1;
096            if (len == buf.length) {
097                arraycopy(new_data, from - 1, buf, 0, len);
098            } else {
099                log.error("range does not match buffer size");
100            }
101        } else {
102            log.error("channel(s) out of range (1-512): {from: {}, to: {}}.",
103                    from, to);
104        }
105    }
106
107    /**
108     * send the values for a range of channels (to the controller)
109     *
110     * @param from the beginning index (inclusive)
111     * @param to   the ending index (inclusive)
112     * @param buf  the data to send
113     * @return true if successful
114     * note: the from/to indexes are 0-511 (inclusive)
115     */
116    private boolean sendChannelRangeValues(int from, int to, byte buf[]) {
117        boolean result = false; // assume failure (pessimist!)
118        if (controller != null) {
119            from = MathUtil.pin(from, 0, 511);
120            to = MathUtil.pin(to, from, 511);
121            int len = to - from + 1;
122            byte requestType = UsbConst.REQUESTTYPE_TYPE_VENDOR
123                    | UsbConst.REQUESTTYPE_RECIPIENT_DEVICE
124                    | UsbConst.ENDPOINT_DIRECTION_OUT;
125            byte request = 0x02;    // anyma dmx cmd_SetChannelRange
126            result = controller.sendControlTransfer(
127                    requestType, request, len, from, buf);
128        }
129        return result;
130    }
131
132    /**
133     * Clean up threads and local storage.
134     */
135    public void dispose(){
136       // modified from the javadoc for ExecutorService 
137       execService.shutdown(); // Disable new tasks from being submitted
138       try {
139          // Wait a while for existing tasks to terminate
140          if (!execService.awaitTermination(60, TimeUnit.SECONDS)) {
141             execService.shutdownNow(); // Cancel currently executing tasks
142             // Wait a while for tasks to respond to being cancelled
143             if (!execService.awaitTermination(60, TimeUnit.SECONDS))
144                 log.error("Pool did not terminate");
145          }
146        } catch (InterruptedException ie) {
147            // (Re-)Cancel if current thread also interrupted
148            execService.shutdownNow();
149            // Preserve interrupt status
150            Thread.currentThread().interrupt();
151        }
152    }
153
154    private final static Logger log
155            = LoggerFactory.getLogger(AnymaDMX_TrafficController.class);
156
157}