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