001package jmri.jmrix.openlcb; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import jmri.DccLocoAddress; 005import jmri.LocoAddress; 006import jmri.SpeedStepMode; 007import jmri.jmrix.AbstractThrottle; 008import jmri.SystemConnectionMemo; 009 010import org.openlcb.NodeID; 011import org.openlcb.OlcbInterface; 012import org.openlcb.implementations.VersionedValueListener; 013import org.openlcb.implementations.throttle.RemoteTrainNode; 014import org.openlcb.implementations.throttle.TractionThrottle; 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018import java.util.ArrayList; 019import java.util.List; 020 021import static org.openlcb.messages.TractionControlRequestMessage.MPH; 022 023/** 024 * An implementation of DccThrottle for OpenLCB. 025 * 026 * @author Bob Jacobsen Copyright (C) 2012 027 */ 028public class OlcbThrottle extends AbstractThrottle { 029 030 /** 031 * Constructor 032 * @param address Dcc loco address 033 * @param memo system connection memo 034 */ 035 public OlcbThrottle(DccLocoAddress address, SystemConnectionMemo memo) { 036 super(memo); 037 OlcbInterface iface = memo.get(OlcbInterface.class); 038 039 // cache settings. It would be better to read the 040 // actual state, but I don't know how to do this 041 synchronized(this) { 042 this.speedSetting = 0; 043 speedStepMode = SpeedStepMode.NMRA_DCC_128; 044 } 045 // Functions default to false 046 this.isForward = true; 047 048 this.address = address; 049 050 // create OpenLCB library object that does the magic & activate 051 if (iface.getNodeStore() == null) { 052 log.error("Failed to access Mimic Node Store"); 053 } 054 if (iface.getDatagramService() == null) { 055 log.error("Failed to access Datagram Service"); 056 } 057 ot = new TractionThrottle(iface); 058 NodeID nid; 059 if (address instanceof OpenLcbLocoAddress) { 060 nid = ((OpenLcbLocoAddress) address).getNode(); 061 } else { 062 nid = guessDCCNodeID(this.address.isLongAddress(), this.address.getNumber()); 063 } 064 ot.start(new RemoteTrainNode(nid, iface)); 065 066 speedListener = new VersionedValueListener<Float>(ot.getSpeed()) { 067 @Override 068 public void update(Float speedAndDir) { 069 updateSpeedAndDirFromNetwork(speedAndDir); 070 } 071 }; 072 for (int i = 0; i <= 28; i++) { 073 int finalI = i; 074 fnListeners.add(new VersionedValueListener<Boolean>(ot.getFunction(finalI)) { 075 @Override 076 public void update(Boolean state) { 077 updateFunction(finalI, state); 078 } 079 }); 080 } 081 } 082 083 public static NodeID guessDCCNodeID(boolean isLong, int dccAddress) { 084 // Here we make a guess at the OpenLCB Node ID that represents the given DCC address. 085 // This should be replaced by a lookup protocol, but we don't have code for that yet. 086 // 0x060100000000 is reserved by the OpenLCB Unique Identifiers Standard for DCC 087 // locomotives. Within that range we guess using a simple encoding of short address 088 // being as-is, long address being OR-ed with 0xC000. This is close to the DCC 089 // protocol's bit layout (e.g. CV17/CV18, CV1). 090 if (isLong) { 091 return new NodeID(new byte[]{6, 1, 0, 0, (byte) (((dccAddress >> 8) & 0xFF) | 0xC0), 092 (byte) (dccAddress & 0xFF)}); 093 } else { 094 return new NodeID(new byte[]{6, 1, 0, 0, 0, (byte) (dccAddress & 0xFF)}); 095 } 096 } 097 098 final TractionThrottle ot; 099 100 final DccLocoAddress address; 101 VersionedValueListener<Float> speedListener; 102 List<VersionedValueListener<Boolean>> fnListeners = new ArrayList<>(); 103 104 /** 105 * {@inheritDoc} 106 */ 107 @Override 108 public LocoAddress getLocoAddress() { 109 return address; 110 } 111 112 /** 113 * {@inheritDoc} 114 */ 115 @Override 116 public String toString() { 117 return getLocoAddress().toString(); 118 } 119 120 /** 121 * Set the speed and direction 122 * <p> 123 * This intentionally skips the emergency stop value of 1. 124 * 125 * @param speed Number from 0 to 1; less than zero is emergency stop 126 */ 127 @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY") // OK to compare floating point, notify on any change 128 @Override 129 public synchronized void setSpeedSetting(float speed) { 130 float oldSpeed = this.speedSetting; 131 if (speed > 1.0) { 132 log.warn("Speed was set too high: {}", speed); 133 } 134 this.speedSetting = speed; 135 136 // send to OpenLCB 137 if (speed >= 0.0) { 138 speedListener.setFromOwner(getSpeedAndDir()); 139 } else { 140 speedListener.setFromOwner(Float.NaN); 141 } 142 log.debug("Speed set update old {} new {} int", oldSpeed, speedSetting); 143 144 // notify 145 firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting); 146 record(speed); 147 } 148 149 /** 150 * Called when the speed and direction value is updated from a network feedback. This is 151 * typically originating from another throttle, possibly controlling another consist member. 152 * @param speedAndDir speed and direction in meters per second, negative for reverse; -0.0 is 153 * different than +0.0 154 */ 155 private void updateSpeedAndDirFromNetwork(Float speedAndDir) { 156 float newSpeed; 157 float direction = Math.copySign(1.0f, speedAndDir); 158 if (speedAndDir.isNaN()) { 159 // e-stop 160 newSpeed = -1.0f; 161 direction = isForward ? 1.0f : -1.0f; 162 } else { 163 newSpeed = speedAndDir / (126 * (float) MPH); 164 if (direction < 0) { 165 newSpeed = -newSpeed; 166 } 167 } 168 float oldSpeed; 169 boolean oldDir; 170 synchronized(this) { 171 oldSpeed = speedSetting; 172 oldDir = isForward; 173 speedSetting = newSpeed; 174 isForward = direction > 0; 175 log.debug("Speed listener update old {} new {}", oldSpeed, speedSetting); 176 firePropertyChange(SPEEDSETTING, oldSpeed, speedSetting); 177 if (oldDir != isForward) { 178 firePropertyChange(ISFORWARD, oldDir, isForward); 179 } 180 } 181 } 182 183 /** 184 * {@inheritDoc} 185 */ 186 @Override 187 public void setIsForward(boolean forward) { 188 boolean old = isForward; 189 isForward = forward; 190 synchronized(this) { 191 speedListener.setFromOwner(getSpeedAndDir()); 192 } 193 firePropertyChange(ISFORWARD, old, isForward); 194 } 195 196 /** 197 * @return the speed and direction as an OpenLCB value. 198 */ 199 private float getSpeedAndDir() { 200 float sp = speedSetting * 126 * (float)MPH; 201 if (speedSetting < 0) { 202 // e-stop is encoded as negative speed setting. 203 sp = 0; 204 } 205 return Math.copySign(sp, isForward ? 1.0f : -1.0f); 206 } 207 208 /** 209 * {@inheritDoc} 210 */ 211 @Override 212 public void setFunction(int functionNum, boolean newState) { 213 updateFunction(functionNum, newState); 214 // send to OpenLCB 215 if (functionNum >= 0 && functionNum < fnListeners.size()) { 216 fnListeners.get(functionNum).setFromOwner(newState); 217 } 218 } 219 220 /** 221 * {@inheritDoc} 222 */ 223 @Override 224 public void throttleDispose() { 225 log.debug("throttleDispose() called for address {}", address); 226 speedListener.release(); 227 for (VersionedValueListener<Boolean> l: fnListeners) { 228 l.release(); 229 } 230 ot.release(); 231 finishRecord(); 232 } 233 234 // initialize logging 235 private final static Logger log = LoggerFactory.getLogger(OlcbThrottle.class); 236 237}