001package jmri.jmrix.loconet;
002
003import java.util.concurrent.DelayQueue;
004import java.util.concurrent.Delayed;
005import java.util.concurrent.TimeUnit;
006import javax.annotation.Nonnull;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * Delay LocoNet messages that need to be throttled.
012 * <p>
013 * A LocoNetThrottledTransmitter object sits in front of a LocoNetInterface
014 * (e.g. TrafficHandler) and meters out specific LocoNet messages.
015 *
016 * <p>
017 * The internal Memo class is used to hold the pending message and the time it's
018 * to be sent. Time computations are in units of milliseconds, as that's all the
019 * accuracy that's needed here.
020 *
021 * @author Bob Jacobsen Copyright (C) 2009
022 */
023public class LocoNetThrottledTransmitter implements LocoNetInterface {
024
025    public LocoNetThrottledTransmitter(@Nonnull LocoNetInterface controller, boolean mTurnoutExtraSpace) {
026        this.controller = controller;
027        this.memo = controller.getSystemConnectionMemo();
028        this.mTurnoutExtraSpace = mTurnoutExtraSpace;
029
030        // calculation is needed time to send on DCC:
031        // msec*nBitsInPacket*packetRepeat/bitRate*safetyFactor
032        minInterval = 1000 * (18 + 3 * 10) * 3 / 16000 * 2;
033
034        if (mTurnoutExtraSpace) {
035            minInterval = minInterval * 4;
036        }
037
038        attachServiceThread();
039    }
040
041    /**
042     * Reference to the system connection memo.
043     */
044    LocoNetSystemConnectionMemo memo = null;
045
046    /**
047     * Set the system connection memo associated with this traffic controller.
048     *
049     * @param m associated systemConnectionMemo object
050     */
051    @Override
052    public void setSystemConnectionMemo(LocoNetSystemConnectionMemo m) {
053        log.debug("LnTrafficController set memo to {}", m.getUserName());
054        memo = m;
055    }
056
057    /**
058     * Get the system connection memo associated with this traffic controller.
059     *
060     * @return the associated systemConnectionMemo object
061     */
062    @Override
063    public LocoNetSystemConnectionMemo getSystemConnectionMemo() {
064        log.debug("getSystemConnectionMemo {} called in LnTC", memo.getUserName());
065        return memo;
066    }
067
068    boolean mTurnoutExtraSpace;
069
070    /**
071     * Request that server thread cease operation, no more messages can be sent.
072     * Note that this returns before the thread is known to be done if it still
073     * has work pending.  If you need to be sure it's done, check and wait on
074     * !running.
075     */
076    public void dispose() {
077        disposed = true;
078
079        // put a shutdown request on the queue after any existing
080        Memo m = new Memo(null, nowMSec(), TimeUnit.MILLISECONDS) {
081            @Override
082            boolean requestsShutDown() {
083                return true;
084            }
085        };
086        queue.add(m);
087    }
088
089    volatile boolean disposed = false;
090    volatile boolean running = false;
091
092    // interface being shadowed
093    LocoNetInterface controller;
094
095    // Forward methods to underlying interface
096    @Override
097    public void addLocoNetListener(int mask, LocoNetListener listener) {
098        controller.addLocoNetListener(mask, listener);
099    }
100
101    @Override
102    public void removeLocoNetListener(int mask, LocoNetListener listener) {
103        controller.removeLocoNetListener(mask, listener);
104    }
105
106    @Override
107    public boolean status() {
108        return controller.status();
109    }
110
111    /**
112     * Accept a message to be sent after suitable delay.
113     */
114    @Override
115    public void sendLocoNetMessage(LocoNetMessage msg) {
116        if (disposed) {
117            log.error("Message sent after queue disposed");
118            return;
119        }
120
121        long sendTime = calcSendTimeMSec();
122
123        Memo m = new Memo(msg, sendTime, TimeUnit.MILLISECONDS);
124        queue.add(m);
125
126    }
127
128    // minimum time in msec between messages
129    long minInterval;
130
131    long lastSendTimeMSec = 0;
132
133    long calcSendTimeMSec() {
134        // next time is at least now or minInterval after latest so far
135        lastSendTimeMSec = Math.max(nowMSec(), minInterval + lastSendTimeMSec);
136        return lastSendTimeMSec;
137    }
138
139    DelayQueue<Memo> queue = new DelayQueue<Memo>();
140
141    /**
142     * Constant for the name of the Service Thread.
143     * Requires the connection UserName prepending.
144     */
145    public static final String SERVICE_THREAD_NAME = " LocoNetThrottledTransmitter";
146
147    private void attachServiceThread() {
148        theServiceThread = new ServiceThread();
149        theServiceThread.setPriority(Thread.NORM_PRIORITY);
150        theServiceThread.setName( memo.getUserName() + SERVICE_THREAD_NAME);
151        theServiceThread.setDaemon(true);
152        theServiceThread.start();
153    }
154
155    ServiceThread theServiceThread;
156
157    class ServiceThread extends Thread {
158
159        @Override
160        public void run() {
161            running = true;
162            while (true) {
163                try {
164                    Memo m = queue.take();
165
166                    // check for request to shutdown
167                    if (m.requestsShutDown()) {
168                        log.debug("item requests shutdown");
169                        break;
170                    }
171
172                    // normal request
173                    if (log.isDebugEnabled()) {
174                        log.debug("forwarding message: {}", m.getMessage());
175                    }
176                    controller.sendLocoNetMessage(m.getMessage());
177                    // and go round again
178                } catch (InterruptedException e) {
179                    // request to terminate
180                    this.interrupt();
181                    break;
182                }
183            }
184            running = false;
185        }
186    }
187
188    // a separate method to ease testing by stopping clock
189    static long nowMSec() {
190        return System.currentTimeMillis();
191    }
192
193    static class Memo implements Delayed {
194
195        public Memo(LocoNetMessage msg, long endTime, TimeUnit unit) {
196            this.msg = msg;
197            this.endTimeMsec = unit.toMillis(endTime);
198        }
199
200        LocoNetMessage getMessage() {
201            return msg;
202        }
203
204        boolean requestsShutDown() {
205            return false;
206        }
207
208        long endTimeMsec;
209        LocoNetMessage msg;
210
211        @Override
212        public long getDelay(TimeUnit unit) {
213            long delay = endTimeMsec - nowMSec();
214            return unit.convert(delay, TimeUnit.MILLISECONDS);
215        }
216
217        @Override
218        public int compareTo(Delayed d) {
219            // -1 means this is less than m
220            long delta;
221            if (d instanceof Memo) {
222                delta = this.endTimeMsec - ((Memo)d).endTimeMsec;
223            } else {
224                delta = this.getDelay(TimeUnit.MILLISECONDS)
225                        - d.getDelay(TimeUnit.MILLISECONDS);
226            }
227            if (delta > 0) {
228                return 1;
229            } else if (delta < 0) {
230                return -1;
231            } else {
232                return 0;
233            }
234        }
235
236        // ensure consistent with compareTo
237        @Override
238        public boolean equals(Object o) {
239            if (o == null) {
240                return false;
241            }
242            if (o instanceof Delayed) {
243                return (compareTo((Delayed) o) == 0);
244            } else {
245                return false;
246            }
247        }
248
249        @Override
250        public int hashCode() {
251            return (int) (this.getDelay(TimeUnit.MILLISECONDS) & 0xFFFFFF);
252        }
253    }
254
255    private final static Logger log = LoggerFactory.getLogger(LocoNetThrottledTransmitter.class);
256
257}