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}