001package jmri.jmrix.openlcb; 002 003import java.util.ArrayList; 004import java.util.List; 005import javax.annotation.CheckForNull; 006import javax.annotation.Nonnull; 007 008import jmri.ProgListener; 009import jmri.ProgrammerException; 010import jmri.ProgrammingMode; 011import org.openlcb.Connection; 012import org.openlcb.EventID; 013import org.openlcb.IdentifyProducersMessage; 014import org.openlcb.MessageDecoder; 015import org.openlcb.NodeID; 016import org.openlcb.OlcbInterface; 017import org.openlcb.ProducerIdentifiedMessage; 018import org.openlcb.VerifyNodeIDNumberGlobalMessage; 019import org.openlcb.implementations.MemoryConfigurationService; 020 021/** 022 * Provide access to the hardware DCC decoder programming capability. 023 * <p> 024 * Programmers come in multiple types: 025 * <ul> 026 * <li>Global, previously "Service Mode" or on a programming track 027 * <li>Addressed, previously "Ops Mode" also known as "programming on the main" 028 * </ul> 029 * Different equipment may also require different programmers: 030 * <ul> 031 * <li>DCC CV programming, on service mode track or on the main 032 * <li>CBUS Node Variable programmers 033 * <li>LocoNet System Variable programmers 034 * <li>LocoNet Op Switch programmers 035 * <li>etc 036 * </ul> 037 * Depending on which type you have, only certain modes can be set. Valid modes 038 * are specified by the class static constants. 039 * <p> 040 * You get a Programmer object from a {@link jmri.AddressedProgrammer}, which in turn 041 * can be located from the {@link jmri.InstanceManager}. 042 * <p> 043 * Starting in JMRI 3.5.5, the CV addresses are Strings for generality. The 044 * methods that use ints for CV addresses will later be deprecated. 045 * <hr> 046 * This file is part of JMRI. 047 * <p> 048 * JMRI is free software; you can redistribute it and/or modify it under the 049 * terms of version 2 of the GNU General Public License as published by the Free 050 * Software Foundation. See the "COPYING" file for a copy of this license. 051 * <p> 052 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 053 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 054 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 055 * 056 * @see jmri.AddressedProgrammer 057 * @author Bob Jacobsen Copyright (C) 2015 058 * @since 4.1.1 059 */ 060public class OlcbProgrammer extends jmri.jmrix.AbstractProgrammer implements jmri.AddressedProgrammer { 061 062 /// Memory space number used for DCC CVs. 063 public static final int SPACE_DCC_CV = 0xF8; 064 065 /// Programming tracks export this event as a producer. 066 public static final EventID IS_PROGRAMMINGTRACK_EVENT = new EventID("09.00.99.FE.FF.FF.00.02"); 067 068 /// No locomotive is detected on the programming track. 069 public static final int ERROR_NO_LOCO = 0x2031; 070 /// The verify after a write operation got no ack. 071 public static final int ERROR_FAILED_VERIFY = 0x2032; 072 /// A POM read never received a reply from the locomotive. 073 public static final int ERROR_NO_RAILCOM = 0x2033; 074 /// A POM read returned only garbage railcom data (e.g. nacks). 075 public static final int ERROR_INVALID_RESPONSE = 0x2034; 076 /// Short circuit condition was detected on the programming track. 077 public static final int ERROR_PGM_SHORT = 0x2035; 078 079 /// Unimplemented command. 080 public static final int ERROR_UNIMPLEMENTED_CMD = 0x1042; 081 082 /// Invalid arguments were given to the command. 083 public static final int ERROR_INVALID_ARGUMENTS = 0x1080; 084 085 /// The program track is disabled. 086 public static final int ERROR_PGM_DISABLED = 0x1021; 087 088 /// Interface to which this programmer is bound to. 089 private final OlcbInterface iface; 090 /// Target OpenLCB node to send requests to. This is set to the train node when we are an addressed programmer. 091 /// It may be null if we are a global programmer and we have not looked up the programming track node ID yet. 092 @CheckForNull 093 NodeID nid; 094 /// Stores the DCC address value (for addressed programmer only). 095 private int dccAddress; 096 /// Stores the dcc address type (for addressed programmer only). 097 private boolean dccIsLong; 098 099 /// Listens for producer identified messages denoting a programming track. 100 private ProgTrackListener listener; 101 102 /** 103 * Creates a programmer for a given OpenLCB node. 104 * 105 * @param system system connection memo 106 * @param nid the target node to use for DCC CV programming. This can be a train node or a program track node. 107 */ 108 public OlcbProgrammer(OlcbInterface system, @CheckForNull NodeID nid) { 109 this.iface = system; 110 this.nid = nid; 111 if (nid != null) { 112 system.getOutputConnection().registerStartNotification(new Connection.ConnectionListener() { 113 @Override 114 public void connectionActive(Connection connection) { 115 // Sends an addressed verify node ID message to ensure that the remote node exists and we have an alias. 116 getInterface().getOutputConnection().put( 117 new VerifyNodeIDNumberGlobalMessage(getInterface().getNodeId(),nid), null); 118 } 119 }); 120 } else { 121 startProgTrackLookup(); 122 } 123 } 124 125 /** 126 * Creates an addressed programmer for a train node given by a DCC address. 127 * 128 * @param system system connection memo 129 * @param isLong dcc address type 130 * @param address dcc address number 131 */ 132 public OlcbProgrammer(OlcbInterface system, boolean isLong, int address) { 133 this(system, OlcbThrottle.guessDCCNodeID(isLong, address)); 134 this.dccIsLong = isLong; 135 this.dccAddress = address; 136 } 137 138 /** 139 * {@inheritDoc} 140 */ 141 @Override 142 @Nonnull 143 public List<ProgrammingMode> getSupportedModes() { 144 List<ProgrammingMode> retval = new ArrayList<>(); 145 retval.add(ProgrammingMode.DIRECTBYTEMODE); 146 retval.add(ProgrammingMode.OPSBYTEMODE); 147 return retval; 148 } 149 150 /** 151 * {@inheritDoc} 152 */ 153 @Override 154 protected void timeout() { 155 } 156 157 /** 158 * {@inheritDoc} 159 */ 160 @Override 161 public void writeCV(String CV, int val, ProgListener p) throws ProgrammerException { 162 checkProgramTrack(); 163 getInterface().getMemoryConfigurationService().requestWrite(nid, SPACE_DCC_CV, getCvAddress(CV), new byte[]{(byte) val}, new MemoryConfigurationService.McsWriteHandler() { 164 @Override 165 public void handleSuccess() { 166 notifyProgListenerEnd(p, val, ProgListener.OK); 167 } 168 169 @Override 170 public void handleFailure(int i) { 171 if (i == ERROR_NO_RAILCOM && (dccAddress > 0 || dccIsLong)) { 172 // We swallow the NO_RAILCOM error for writes, because POM writes should return OK to JMRI when 173 // RailCom is unavailable. JMRI can not distinguish whether the decoder is not present or that there 174 // is no RailCom support. For a correct operation of the UI, POM writes have to return OK. 175 notifyProgListenerEnd(p, val, ProgListener.OK); 176 return; 177 } 178 notifyProgListenerEnd(p, 0, olcbErrorToProgStatus(i)); 179 } 180 }); 181 } 182 183 /** 184 * {@inheritDoc} 185 */ 186 @Override 187 public void readCV(String CV, ProgListener p) throws ProgrammerException { 188 checkProgramTrack(); 189 getInterface().getMemoryConfigurationService().requestRead(nid, SPACE_DCC_CV, getCvAddress(CV), 1, new MemoryConfigurationService.McsReadHandler() { 190 @Override 191 public void handleReadData(NodeID nodeID, int i, long l, byte[] bytes) { 192 if (bytes.length < 1) { 193 handleFailure(0x1000); 194 return; 195 } 196 if (p != null) { 197 notifyProgListenerEnd(p, bytes[0] & 0xff, ProgListener.OK); 198 } 199 } 200 201 @Override 202 public void handleFailure(int i) { 203 log.debug("CV {} read - memory config error 0x{}", CV, Integer.toHexString(i)); 204 notifyProgListenerEnd(p, 0, olcbErrorToProgStatus(i)); 205 } 206 }); 207 } 208 209 /** 210 * {@inheritDoc} 211 */ 212 @Override 213 public void confirmCV(String CV, int val, ProgListener p) throws ProgrammerException { 214 checkProgramTrack(); 215 } 216 217 /** 218 * {@inheritDoc} 219 */ 220 @Override 221 public boolean getLongAddress() { 222 return dccIsLong; 223 } 224 225 /** 226 * {@inheritDoc} 227 */ 228 @Override 229 public int getAddressNumber() { 230 return dccAddress; 231 } 232 233 /** 234 * {@inheritDoc} 235 */ 236 @Override 237 public String getAddress() { 238 if (dccAddress > 0 || dccIsLong) { 239 return Integer.toString(dccAddress) + (dccIsLong ? "L" : "S"); 240 } 241 if (nid != null) 242 return nid.toString(); 243 return "null"; 244 } 245 246 private OlcbInterface getInterface() { 247 return iface; 248 } 249 250 /** 251 * Translates a (string) CV number into an address to read on the 252 * 253 * @param cvName CV address as provided to the various interface functions. 254 * @return memory space address to perform the read/write to. 255 */ 256 private long getCvAddress(String cvName) { 257 int cvNum = Integer.parseInt(cvName); 258 return cvNum - 1; 259 } 260 261 class ProgTrackListener extends MessageDecoder { 262 @Override 263 public void handleProducerIdentified(ProducerIdentifiedMessage msg, Connection sender) { 264 if (!msg.getEventID().equals(IS_PROGRAMMINGTRACK_EVENT)) { 265 return; 266 } 267 if (msg.getSourceNodeID() == null) { 268 log.error("Found programming track with null source node."); 269 return; 270 } 271 if (msg.getSourceNodeID().getContents()[0] == 0) { 272 log.error("Found programming track with invalid source node: {}", msg.getSourceNodeID()); 273 return; 274 } 275 if (foundProgrammingTrack(msg.getSourceNodeID())) { 276 iface.unRegisterMessageListener(this); 277 isRegistered = false; 278 } 279 } 280 281 boolean isRegistered = false; 282 } 283 284 private void startProgTrackLookup() { 285 if (listener == null) { 286 listener = new ProgTrackListener(); 287 } 288 if (!listener.isRegistered) { 289 iface.registerMessageListener(listener); 290 listener.isRegistered = true; 291 } 292 iface.getOutputConnection().registerStartNotification(new Connection.ConnectionListener() { 293 @Override 294 public void connectionActive(Connection connection) { 295 iface.getOutputConnection().put(new IdentifyProducersMessage( 296 iface.getNodeId(), IS_PROGRAMMINGTRACK_EVENT), null); 297 } 298 }); 299 } 300 301 /** 302 * Notifies that a programming track device was found. 303 * @param nodeID Node ID of the programming track node. 304 * @return true if no further programming tracks need to be looked for. 305 */ 306 private boolean foundProgrammingTrack(@Nonnull NodeID nodeID) { 307 if (nid == null) { 308 nid = nodeID; 309 log.info("Found programming track {}.", nodeID); 310 } 311 return true; 312 } 313 314 private void checkProgramTrack() throws ProgrammerException { 315 if (nid == null) { 316 throw new ProgrammerException("No programming track found."); 317 } 318 } 319 320 /** 321 * Translates an OpenLCB error code (16-bit integer) to a ProgListener error code. 322 * 323 * @param olcbError openlcb error code (16-bit usigned integer) 324 * @return prog listener error code. 325 */ 326 private static int olcbErrorToProgStatus(int olcbError) { 327 switch (olcbError) { 328 case ERROR_NO_LOCO: 329 return ProgListener.NoLocoDetected; 330 case ERROR_FAILED_VERIFY: 331 return ProgListener.ConfirmFailed; 332 case ERROR_NO_RAILCOM: 333 /// @todo how do we represent that the target loco does not support railcom? 334 return ProgListener.NoAck; 335 case ERROR_INVALID_RESPONSE: 336 return ProgListener.CommError; 337 case ERROR_PGM_SHORT: 338 return ProgListener.ProgrammingShort; 339 case ERROR_UNIMPLEMENTED_CMD: 340 return ProgListener.NotImplemented; 341 case ERROR_INVALID_ARGUMENTS: 342 return ProgListener.SequenceError; 343 case ERROR_PGM_DISABLED: 344 /// @todo this is not a very accurate representation of a configuration error. 345 return ProgListener.ProgrammerBusy; 346 default: 347 break; 348 } 349 if ((olcbError & 0x2000) != 0) { 350 // Unknown temporary error 351 return ProgListener.SequenceError; 352 } 353 if ((olcbError & 0x1000) != 0) { 354 // Unknown permanent error 355 return ProgListener.NotImplemented; 356 } 357 if (olcbError != 0) { 358 return ProgListener.UnknownError; 359 } else { 360 return ProgListener.OK; 361 } 362 } 363 364 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OlcbProgrammer.class); 365}