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-2025
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            log.debug(" using programmer node {}, isBoosterOn = {}", progNode, isBoosterOn);
165            if (isBoosterOn) {
166                startLongTimer();
167                tc.sendBiDiBMessage(message, progNode);
168            }
169            else {
170                // if the booster of OFF, return immediately without waiting for the timeout.
171                log.warn("BiDiB Booster is switched off!");
172                progState = NOTPROGRAMMING;
173                notifyProgListenerEnd(_val, jmri.ProgListener.NoAck);
174            }
175        }
176        else {
177            log.warn("no prog node available!");
178            progState = NOTPROGRAMMING;
179            notifyProgListenerEnd(_val, jmri.ProgListener.NotImplemented);
180        }
181    }
182
183    private jmri.ProgListener _usingProgrammer = null;
184
185    // internal method to remember who's using the programmer
186    protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException {
187        // test for only one!
188        if (_usingProgrammer != null && _usingProgrammer != p) {
189            if (log.isInfoEnabled()) {
190                log.info("programmer already in use by {}", _usingProgrammer);
191            }
192            throw new jmri.ProgrammerException("programmer in use");
193        } else {
194            _usingProgrammer = p;
195        }
196    }
197    
198    
199    private void createProgrammerListener() {
200        // create BiDiB message listener
201        MessageListener messageListener = new DefaultMessageListener() {
202            //TODO implement retries somewhow...
203            @Override
204            public void csProgState(
205                byte[] address, int messageNum, CommandStationProgState commandStationProgState, int remainingTime, int cvNumber, int cvData) {
206                if (progState == NOTPROGRAMMING) {
207                    // we get the complete set of replies now, so ignore these
208                    if (log.isDebugEnabled()) {
209                        log.debug("reply in NOTPROGRAMMING state");
210                    }
211                } else if (progState == COMMANDSENT) {
212                    log.debug("node addr: {}, msg node addr: {}", progNode.getAddr(), address);
213                    if (NodeUtils.isAddressEqual(progNode.getAddr(), address)  &&  _cv == cvNumber) {
214                        log.info("GLOBAL PROGRAMMER CS_PROG_STATE was signalled, node addr: {}, state: {}, CV: {}, value: {}, remaining time: {}",
215                                address, commandStationProgState.getType(), cvNumber, cvData, remainingTime);
216                        if ( (commandStationProgState.getType() & 0x80) != 0) { //bit 7 = 1 means operation has finished
217                            stopTimer();
218                            progState = NOTPROGRAMMING;
219                            if ( (commandStationProgState.getType() & 0x40) == 0) {//bit 6 = 0 means OK
220                                log.debug(" prog ok");
221                                if (_progRead) {
222                                    // read was in progress - get return value
223                                    _val = cvData;
224                                }
225                                // if this was a read, we retrieved the value above.  If its a
226                                // write, we're to return the original write value
227                                notifyProgListenerEnd(_val, jmri.ProgListener.OK);
228                            }
229                            else { //not ok - return error
230                                if (commandStationProgState == CommandStationProgState.PROG_NO_LOCO ) {
231                                    log.debug(" error: no loco detected");
232                                    notifyProgListenerEnd(_val, jmri.ProgListener.NoLocoDetected);
233                                }
234                                else if (commandStationProgState == CommandStationProgState.PROG_STOPPED) {
235                                    log.debug(" error: user aborted");
236                                    notifyProgListenerEnd(_val, jmri.ProgListener.UserAborted);
237                                }
238                                else if (commandStationProgState == CommandStationProgState.PROG_NO_ANSWER) {
239                                    log.debug(" error: no answer");
240                                    // hack for BiDiB simulator - it does not report CV8 (manufacturer) and CV7 (decoder version)
241                                    // JMRI identify needs them, so we use return CV8=238 (NMRA Reserved) and CV7=42 (you know...)
242                                    if ( _progRead  &&  (cvNumber == 8 || cvNumber == 7)) {
243                                        //if (cvNumber == 8) _val = 238;
244                                        //if (cvNumber == 7) _val = 42;
245                                        if (cvNumber == 8) _val = 145;
246                                        if (cvNumber == 7) _val = 26;
247                                        notifyProgListenerEnd(_val, jmri.ProgListener.OK);
248                                    }
249                                    else {
250                                        _val = 0;
251                                        log.warn(" error: no answer, CV probably not implemented");
252                                        notifyProgListenerEnd(_val, jmri.ProgListener.NoAck);
253                                        //notifyProgListenerEnd(_val, jmri.ProgListener.NotImplemented);
254                                        //notifyProgListenerEnd(_val, jmri.ProgListener.OK);
255                                    }
256                                }
257                                else if (commandStationProgState == CommandStationProgState.PROG_SHORT) {
258                                    log.warn(" error: programming short");
259                                    notifyProgListenerEnd(_val, jmri.ProgListener.ProgrammingShort);
260                                }
261                                else if (commandStationProgState == CommandStationProgState.PROG_VERIFY_FAILED) {
262                                    log.warn(" error: verify failed");
263                                    notifyProgListenerEnd(_val, jmri.ProgListener.ConfirmFailed);
264                                }
265                                else {
266                                    log.warn(" error: unknown error");
267                                    notifyProgListenerEnd(_val, jmri.ProgListener.UnknownError);
268                                }
269                            }
270                        }
271                        else {
272                            log.debug(" not finished...");
273                            // not finished - ignore so far...
274                        }
275                    }
276                }
277            }
278            @Override
279            public void boosterState(byte[] address, int messageNum, BoosterState state, BoosterControl control) {
280                Node node = tc.getNodeByAddr(address);
281                log.info("BOOSTER STATE was signalled: {}, control: {}", state.getType(), control.getType());
282                if (node != null  &&  node == progNode) {
283                    isBoosterOn = ((state.getType() & 0x80) == 0x80);
284                }
285            }
286        };
287        tc.addMessageListener(messageListener);        
288    }
289
290    /** 
291     * {@inheritDoc}
292     *
293     * Internal routine to handle a timeout
294     */
295    @Override
296    protected synchronized void timeout() {
297        if (progState != NOTPROGRAMMING) {
298            // we're programming, time to stop
299            if (log.isDebugEnabled()) {
300                log.debug("timeout!");
301            }
302            // perhaps no loco present? Fail back to end of programming
303            progState = NOTPROGRAMMING;
304            cleanup();
305            notifyProgListenerEnd(_val, jmri.ProgListener.FailedTimeout);
306            
307            tc.checkProgMode(false, progNode); //be sure PROG mode is switched off
308            tc.setCurrentGlobalProgrammerNode(null); //invalidate, so the progNode must be evaluated again the next time
309        }
310    }
311
312    // Internal method to cleanup in case of a timeout. Separate routine
313    // so it can be changed in subclasses.
314    void cleanup() {
315    }
316
317    // internal method to notify of the final result
318    protected void notifyProgListenerEnd(int value, int status) {
319        if (log.isDebugEnabled()) {
320            log.debug("notifyProgListenerEnd value {} status {}", value, status);
321        }
322        // the programmingOpReply handler might send an immediate reply, so
323        // clear the current listener _first_
324        jmri.ProgListener temp = _usingProgrammer;
325        _usingProgrammer = null;
326        notifyProgListenerEnd(temp, value, status);
327    }
328
329    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BiDiBProgrammer.class);
330
331}