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}