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}