001package jmri.jmrix.bidib;
002
003import java.util.ArrayList;
004import java.util.List;
005import javax.annotation.Nonnull;
006import jmri.Programmer;
007
008import jmri.ProgrammingMode;
009import jmri.jmrix.AbstractProgrammer;
010import org.bidib.jbidibc.core.DefaultMessageListener;
011import org.bidib.jbidibc.core.MessageListener;
012import org.bidib.jbidibc.messages.enums.CommandStationProgState;
013import org.bidib.jbidibc.messages.Node;
014import org.bidib.jbidibc.messages.enums.BoosterControl;
015import org.bidib.jbidibc.messages.enums.BoosterState;
016import org.bidib.jbidibc.messages.enums.CommandStationPt;
017import org.bidib.jbidibc.messages.message.BidibCommandMessage;
018import org.bidib.jbidibc.messages.message.CommandStationProgMessage;
019import org.bidib.jbidibc.messages.utils.NodeUtils;
020
021/**
022 * Convert the jmri.Programmer interface into BiDiB.
023 * <P>
024 * This has two states: NOTPROGRAMMING, and COMMANDSENT. The transitions to and
025 * from programming mode are now handled in the TrafficController code.
026 *
027 * @author Bob Jacobsen Copyright (C) 2001, 2016
028 * @author Eckart Meyer Copyright (C) 2019-2023
029 */
030public class BiDiBProgrammer extends AbstractProgrammer {
031
032    protected BiDiBTrafficController tc;
033    protected Node progNode; //the BiDiB progNode to sent the MSG_CS_PROG message to
034    private boolean isBoosterOn = false;
035
036//    @SuppressWarnings("OverridableMethodCallInConstructor")
037    public BiDiBProgrammer(BiDiBTrafficController tc) {
038        this.tc = tc;
039        super.SHORT_TIMEOUT = 4000;
040        progNode = tc.getCurrentGlobalProgrammerNode();
041        log.debug("global programmer node: {}", progNode);
042
043        if (getSupportedModes().size() > 0) {
044            setMode(getSupportedModes().get(0));
045        }
046        
047        createProgrammerListener();
048    }
049
050    /** 
051     * {@inheritDoc}
052     *
053     * BiDiB programming modes available depend on settings
054     */
055    @Override
056    @Nonnull
057    public List<ProgrammingMode> getSupportedModes() {
058        List<ProgrammingMode> ret = new ArrayList<>();
059        if (tc == null) {
060            log.warn("getSupportedModes called with null tc", new Exception("traceback"));
061        }
062        java.util.Objects.requireNonNull(tc, "TrafficController reference needed");
063
064        ret.add(ProgrammingMode.DIRECTBYTEMODE);
065        //ret.add(ProgrammingMode.DIRECTBITMODE); //TODO! BiDiB should be able to do this!
066        return ret;
067    }
068    
069    // getCanRead/getCanWrite: BiDiB protocol allows CVs from 1...1024 - this is the default implementation
070
071    /** 
072     * {@inheritDoc}
073     * 
074     * The default implementation does not check for cv &gt; 1024 - not neccessary? We do it here anywhere
075     */
076    @Override
077    public boolean getCanWrite(String cv) {
078        if (!getCanWrite()) {
079            return false; // check basic implementation first
080        }
081        return Integer.parseInt(cv) <= 1024;
082    }
083
084    /** 
085     * {@inheritDoc}
086     */
087    @Nonnull
088    @Override
089    public Programmer.WriteConfirmMode getWriteConfirmMode(String addr) {
090        return WriteConfirmMode.DecoderReply;
091    }
092
093    // members for handling the programmer interface
094    int progState = 0;
095    static final int NOTPROGRAMMING = 0;// is notProgramming
096    static final int COMMANDSENT = 2;  // read/write command sent, waiting reply
097    static final int COMMANDSENT_2 = 4; // ops programming mode, send msg twice
098    boolean _progRead = false;
099    int _val; // remember the value being read/written for confirmative reply
100    int _cv; // remember the cv being read/written
101
102    /** 
103     * {@inheritDoc}
104     */
105    @Override
106    public synchronized void writeCV(String CVname, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
107        final int CV = Integer.parseInt(CVname);
108        log.info("write mode: {}, CV={}, val={}", getMode().getStandardName(), CV, val);
109        if (log.isDebugEnabled()) {
110            log.debug("writeCV {} listens {}", CV, p);
111        }
112        useProgrammer(p);
113        if (!getCanWrite(CVname)) {
114            throw new jmri.ProgrammerException("CV number not supported");
115        }
116        if (progNode == null) {
117            throw new jmri.ProgrammerException("No Global Programmer node found!");
118        }
119        _progRead = false;
120        // set state
121        progState = COMMANDSENT;
122        _val = val;
123        _cv = CV;
124
125//TODO bit mode ??
126        sendBiDiBMessage(new CommandStationProgMessage(CommandStationPt.BIDIB_CS_PROG_WR_BYTE, _cv, _val));
127    }
128
129    /** 
130     * {@inheritDoc}
131     */
132    @Override
133    public void confirmCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
134        readCV(CV, p);
135    }
136
137    /** 
138     * {@inheritDoc}
139     */
140    @Override
141    public synchronized void readCV(String CVname, jmri.ProgListener p) throws jmri.ProgrammerException {
142        final int CV = Integer.parseInt(CVname);
143        log.info("read mode: {}, CV={}", getMode().getStandardName(), CV);
144        if (log.isDebugEnabled()) {
145            log.debug("readCV {} listens {}", CV, p);
146        }
147        useProgrammer(p);
148        if (!getCanRead(CVname)) {
149            throw new jmri.ProgrammerException("CV number not supported");
150        }
151        _progRead = true;
152
153        // set commandPending state
154        progState = COMMANDSENT;
155        _cv = CV;
156
157//TODO bit mode ??
158        sendBiDiBMessage(new CommandStationProgMessage(CommandStationPt.BIDIB_CS_PROG_RD_BYTE, _cv, 0));
159    }
160    
161    private void sendBiDiBMessage(BidibCommandMessage message) {
162        progNode = tc.getCurrentGlobalProgrammerNode(); //the global programmer progNode may have changed TODO: make the progNode user selectable!
163        if (progNode != null) {
164            if (isBoosterOn) {
165                startLongTimer();
166                tc.sendBiDiBMessage(message, progNode);
167            }
168            else {
169                // if the booster of OFF, return immediately without waiting for the timeout.
170                progState = NOTPROGRAMMING;
171                notifyProgListenerEnd(_val, jmri.ProgListener.NoAck);
172            }
173        }
174        else {
175            progState = NOTPROGRAMMING;
176            notifyProgListenerEnd(_val, jmri.ProgListener.NotImplemented);
177        }
178    }
179
180    private jmri.ProgListener _usingProgrammer = null;
181
182    // internal method to remember who's using the programmer
183    protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException {
184        // test for only one!
185        if (_usingProgrammer != null && _usingProgrammer != p) {
186            if (log.isInfoEnabled()) {
187                log.info("programmer already in use by {}", _usingProgrammer);
188            }
189            throw new jmri.ProgrammerException("programmer in use");
190        } else {
191            _usingProgrammer = p;
192        }
193    }
194    
195    
196    private void createProgrammerListener() {
197        // create BiDiB message listener
198        MessageListener messageListener = new DefaultMessageListener() {
199            //TODO implement retries somewhow...
200            @Override
201            public void csProgState(
202                byte[] address, int messageNum, CommandStationProgState commandStationProgState, int remainingTime, int cvNumber, int cvData) {
203                if (progState == NOTPROGRAMMING) {
204                    // we get the complete set of replies now, so ignore these
205                    if (log.isDebugEnabled()) {
206                        log.debug("reply in NOTPROGRAMMING state");
207                    }
208                } else if (progState == COMMANDSENT) {
209                    log.debug("node addr: {}, msg node addr: {}", progNode.getAddr(), address);
210                    if (NodeUtils.isAddressEqual(progNode.getAddr(), address)  &&  _cv == cvNumber) {
211                        log.info("GLOBAL PROGRAMMER CS_PROG_STATE was signalled, node addr: {}, state: {}, CV: {}, value: {}, remaining time: {}",
212                                address, commandStationProgState.getType(), cvNumber, cvData, remainingTime);
213                        if ( (commandStationProgState.getType() & 0x80) != 0) { //bit 7 = 1 means operation has finished
214                            stopTimer();
215                            progState = NOTPROGRAMMING;
216                            if ( (commandStationProgState.getType() & 0x40) == 0) {//bit 6 = 0 means OK
217                                log.debug(" prog ok");
218                                if (_progRead) {
219                                    // read was in progress - get return value
220                                    _val = cvData;
221                                }
222                                // if this was a read, we retrieved the value above.  If its a
223                                // write, we're to return the original write value
224                                notifyProgListenerEnd(_val, jmri.ProgListener.OK);
225                            }
226                            else { //not ok - return error
227                                if (commandStationProgState == CommandStationProgState.PROG_NO_LOCO ) {
228                                    log.debug(" error: no loco detected");
229                                    notifyProgListenerEnd(_val, jmri.ProgListener.NoLocoDetected);
230                                }
231                                else if (commandStationProgState == CommandStationProgState.PROG_STOPPED) {
232                                    log.debug(" error: user aborted");
233                                    notifyProgListenerEnd(_val, jmri.ProgListener.UserAborted);
234                                }
235                                else if (commandStationProgState == CommandStationProgState.PROG_NO_ANSWER) {
236                                    log.debug(" error: no answer");
237                                    // hack for BiDiB simulator - it does not report CV8 (manufacturer) and CV7 (decoder version)
238                                    // JMRI identify needs them, so we use return CV8=238 (NMRA Reserved) and CV7=42 (you know...)
239                                    if ( _progRead  &&  (cvNumber == 8 || cvNumber == 7)) {
240                                        //if (cvNumber == 8) _val = 238;
241                                        //if (cvNumber == 7) _val = 42;
242                                        if (cvNumber == 8) _val = 145;
243                                        if (cvNumber == 7) _val = 26;
244                                        notifyProgListenerEnd(_val, jmri.ProgListener.OK);
245                                    }
246                                    else {
247                                        _val = 0;
248                                        log.warn(" error: no answer, CV probably not implemented");
249                                        notifyProgListenerEnd(_val, jmri.ProgListener.NoAck);
250                                        //notifyProgListenerEnd(_val, jmri.ProgListener.NotImplemented);
251                                        //notifyProgListenerEnd(_val, jmri.ProgListener.OK);
252                                    }
253                                }
254                                else if (commandStationProgState == CommandStationProgState.PROG_SHORT) {
255                                    log.warn(" error: programming short");
256                                    notifyProgListenerEnd(_val, jmri.ProgListener.ProgrammingShort);
257                                }
258                                else if (commandStationProgState == CommandStationProgState.PROG_VERIFY_FAILED) {
259                                    log.warn(" error: verify failed");
260                                    notifyProgListenerEnd(_val, jmri.ProgListener.ConfirmFailed);
261                                }
262                                else {
263                                    log.warn(" error: unknown error");
264                                    notifyProgListenerEnd(_val, jmri.ProgListener.UnknownError);
265                                }
266                            }
267                        }
268                        else {
269                            log.debug(" not finished...");
270                            // not finished - ignore so far...
271                        }
272                    }
273                }
274            }
275            @Override
276            public void boosterState(byte[] address, int messageNum, BoosterState state, BoosterControl control) {
277                Node node = tc.getNodeByAddr(address);
278                log.info("BOOSTER STATE was signalled: {}, control: {}", state.getType(), control.getType());
279                if (node != null  &&  node == progNode) {
280                    isBoosterOn = ((state.getType() & 0x80) == 0x80);
281                }
282            }
283        };
284        tc.addMessageListener(messageListener);        
285    }
286
287    /** 
288     * {@inheritDoc}
289     *
290     * Internal routine to handle a timeout
291     */
292    @Override
293    protected synchronized void timeout() {
294        if (progState != NOTPROGRAMMING) {
295            // we're programming, time to stop
296            if (log.isDebugEnabled()) {
297                log.debug("timeout!");
298            }
299            // perhaps no loco present? Fail back to end of programming
300            progState = NOTPROGRAMMING;
301            cleanup();
302            notifyProgListenerEnd(_val, jmri.ProgListener.FailedTimeout);
303            
304            tc.checkProgMode(false, progNode); //be sure PROG mode is switched off
305            tc.setCurrentGlobalProgrammerNode(null); //invalidate, so the progNode must be evaluated again the next time
306        }
307    }
308
309    // Internal method to cleanup in case of a timeout. Separate routine
310    // so it can be changed in subclasses.
311    void cleanup() {
312    }
313
314    // internal method to notify of the final result
315    protected void notifyProgListenerEnd(int value, int status) {
316        if (log.isDebugEnabled()) {
317            log.debug("notifyProgListenerEnd value {} status {}", value, status);
318        }
319        // the programmingOpReply handler might send an immediate reply, so
320        // clear the current listener _first_
321        jmri.ProgListener temp = _usingProgrammer;
322        _usingProgrammer = null;
323        notifyProgListenerEnd(temp, value, status);
324    }
325
326    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BiDiBProgrammer.class);
327
328}