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    private void attachServiceThread() {
142        theServiceThread = new ServiceThread();
143        theServiceThread.setPriority(Thread.NORM_PRIORITY);
144        theServiceThread.setName("LocoNetThrottledTransmitter"); // NOI18N
145        theServiceThread.setDaemon(true);
146        theServiceThread.start();
147    }
148
149    ServiceThread theServiceThread;
150
151    class ServiceThread extends Thread {
152
153        @Override
154        public void run() {
155            running = true;
156            while (true) {
157                try {
158                    Memo m = queue.take();
159
160                    // check for request to shutdown
161                    if (m.requestsShutDown()) {
162                        log.debug("item requests shutdown");
163                        break;
164                    }
165
166                    // normal request
167                    if (log.isDebugEnabled()) {
168                        log.debug("forwarding message: {}", m.getMessage());
169                    }
170                    controller.sendLocoNetMessage(m.getMessage());
171                    // and go round again
172                } catch (InterruptedException e) {
173                    // request to terminate
174                    this.interrupt();
175                    break;
176                }
177            }
178            running = false;
179        }
180    }
181
182    // a separate method to ease testing by stopping clock
183    static long nowMSec() {
184        return System.currentTimeMillis();
185    }
186
187    static class Memo implements Delayed {
188
189        public Memo(LocoNetMessage msg, long endTime, TimeUnit unit) {
190            this.msg = msg;
191            this.endTimeMsec = unit.toMillis(endTime);
192        }
193
194        LocoNetMessage getMessage() {
195            return msg;
196        }
197
198        boolean requestsShutDown() {
199            return false;
200        }
201
202        long endTimeMsec;
203        LocoNetMessage msg;
204
205        @Override
206        public long getDelay(TimeUnit unit) {
207            long delay = endTimeMsec - nowMSec();
208            return unit.convert(delay, TimeUnit.MILLISECONDS);
209        }
210
211        @Override
212        public int compareTo(Delayed d) {
213            // -1 means this is less than m
214            long delta;
215            if (d instanceof Memo) {
216                delta = this.endTimeMsec - ((Memo)d).endTimeMsec;
217            } else {
218                delta = this.getDelay(TimeUnit.MILLISECONDS)
219                        - d.getDelay(TimeUnit.MILLISECONDS);
220            }
221            if (delta > 0) {
222                return 1;
223            } else if (delta < 0) {
224                return -1;
225            } else {
226                return 0;
227            }
228        }
229
230        // ensure consistent with compareTo
231        @Override
232        public boolean equals(Object o) {
233            if (o == null) {
234                return false;
235            }
236            if (o instanceof Delayed) {
237                return (compareTo((Delayed) o) == 0);
238            } else {
239                return false;
240            }
241        }
242
243        @Override
244        public int hashCode() {
245            return (int) (this.getDelay(TimeUnit.MILLISECONDS) & 0xFFFFFF);
246        }
247    }
248
249    private final static Logger log = LoggerFactory.getLogger(LocoNetThrottledTransmitter.class);
250
251}