001package jmri.jmrix.dccpp;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import javax.annotation.Nonnull;
007
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011/**
012 * Defines the standard/common routines used in multiple classes related to the
013 * DCC++ Command Station, on a DCC++ network.
014 *
015 * @author Bob Jacobsen Copyright (C) 2001
016 * @author Portions by Paul Bender Copyright (C) 2003
017 * @author Mark Underwood Copyright (C) 2015
018 * @author Harald Barth Copyright (C) 2019
019 *
020 * Based on LenzCommandStation by Bob Jacobsen and Paul Bender
021 */
022public class DCCppCommandStation implements jmri.CommandStation {
023
024    /* The First group of routines is for obtaining the Software and
025     hardware version of the Command station */
026    /**
027     * We need to add a few data members for saving the version information we
028     * get from the layout.
029     *
030     */
031    @Nonnull private String stationType = "Unknown";
032    @Nonnull private String build       = "Unknown";
033    @Nonnull private String version     = "0.0.0";
034    private DCCppRegisterManager rmgr = null;
035    private int maxNumSlots = DCCppConstants.MAX_MAIN_REGISTERS; //default to register size
036    private List<String> trackModes = new ArrayList<String>();   //Modes for tracks A-H (as returned from EX-CS) 
037
038    public DCCppCommandStation() {
039        super();
040    }
041
042    public DCCppCommandStation(DCCppSystemConnectionMemo memo) {
043        super();
044        adaptermemo = memo;
045    }
046
047    public void setStationType(String s) {
048        if (!stationType.equals(s)) {
049            log.info("Station Type set to '{}'", s);
050            stationType = s;            
051        }
052    }
053    
054    /**
055     * Get the Station Type of the connected Command Station
056     * it is populated by response from the CS, initially "Unknown" 
057     * @return StationType
058     */
059    @Nonnull
060    public String getStationType() {
061        return stationType;
062    }
063
064    public void setBuild(String s) {
065        if (!build.equals(s)) {
066            log.info("Build set to '{}'", s);
067            build = s;            
068        }
069    }
070
071    /**
072     * Get the Build of the connected Command Station
073     * it is populated by response from the CS, initially "Unknown" 
074     * @return Build
075     */
076    @Nonnull
077    public String getBuild() {
078        return build;
079    }
080
081    public void setVersion(String s) {
082        if (!version.equals(s)) {
083            if (jmri.Version.isCanonicalVersion(s)) {
084                log.info("Version set to '{}'", s);
085                version = s;
086            } else {
087                log.warn("'{}' is not a canonical version, version not changed", s);
088            }
089        }
090    }
091
092    /**
093     * Get the canonical version of the connected Command Station
094     * it is populated by response from the CS, so initially '0.0.0' 
095     * @return Version
096     */
097    @Nonnull
098    public String getVersion() {
099        return version;
100    }
101
102    /**
103     * Parse the DCC++ CS status response to pull out the base station version
104     * and software version.
105     * @param l status response to query.
106     */
107    protected void setCommandStationInfo(DCCppReply l) {
108 // V1.0 Syntax
109 //String syntax = "iDCC\\+\\+\\s+BASE\\s+STATION\\s+v([a-zA-Z0-9_.]+):\\s+BUILD\\s+((\\d+\\s\\w+\\s\\d+)\\s+(\\d+:\\d+:\\d+))";
110 // V1.1 Syntax
111 //String syntax = "iDCC\\+\\+BASE STATION FOR ARDUINO \\b(\\w+)\\b \\/ (ARDUINO|POLOLU\\sMC33926) MOTOR SHIELD: BUILD ((\\d+\\s\\w+\\s\\d+)\\s+(\\d+:\\d+:\\d+))";
112 // V1.0/V1.1 Simplified
113 //String syntax = "iDCC\\+\\+(.*): BUILD (.*)";
114        // V1.2.1 Syntax
115        // String syntax = "iDCC++ BASE STATION FOR ARDUINO \\b(\\w+)\\b \\/ (ARDUINO|POLOLU\\sMC33926) MOTOR SHIELD: ((\\d+\\s\\w+\\s\\d+)\\s+(\\d+:\\d+:\\d+))";
116        // Changes from v1.1: space between "DCC++" and "BASE", and "BUILD" is removed.
117        // V1.0/V1.1/V1.2 Simplified
118        // String syntax = "iDCC\\+\\+\\s?(.*):\\s?(?:BUILD)? (.*)";
119
120        setStationType(l.getStationType());
121        setBuild(l.getBuildString());
122        setVersion(l.getVersion());
123    }
124
125    protected void setCommandStationMaxNumSlots(DCCppReply l) {
126        int newNumSlots = l.getValueInt(1);
127        setCommandStationMaxNumSlots(newNumSlots);
128    }
129    protected void setCommandStationMaxNumSlots(int newNumSlots) {
130        if (newNumSlots < maxNumSlots) {
131            log.warn("Command Station maxNumSlots cannot be reduced from {} to {}", maxNumSlots, newNumSlots);
132            return;
133        }
134        if (newNumSlots != maxNumSlots) {
135            log.info("changing maxNumSlots from {} to {}", maxNumSlots, newNumSlots);
136            maxNumSlots = newNumSlots;
137        }
138    }
139    protected int getCommandStationMaxNumSlots() {
140        return maxNumSlots;
141    }
142
143    /**
144     * Provide the version string returned during the initial check.
145     * @return version string.
146     */
147    public String getVersionString() {
148        return(stationType + ": BUILD " + build);
149    }
150
151    /**
152     * Remember whether or not in service mode.
153     */
154    boolean mInServiceMode = false;
155
156    /**
157     * DCC++ command station does provide Ops Mode.
158     * @return always true.
159     */
160    public boolean isOpsModePossible() {
161        return true;
162    }
163
164    /**
165     * Does this command station require JMRI to send periodic function refresh packets?
166     * @return true if required, false if not
167     */
168    public boolean isFunctionRefreshRequired() {
169        boolean ret = true;
170        try {
171            //command stations starting with 3 handle their own function refresh
172            ret = (jmri.Version.compareCanonicalVersions(version, "3.0.0") < 0);
173        } catch (IllegalArgumentException ignore) {
174        }
175        return ret;  
176    }
177
178    /**
179     * Does this command station support the latest Current commands &lt;JG&gt;, &lt;JI&gt;, etc.?
180     * @return true if supported, false if not
181     */
182    public boolean isCurrentListSupported() {
183        boolean ret = false;
184        try {
185            ret = (jmri.Version.compareCanonicalVersions(version, "4.2.20") >= 0);
186        } catch (IllegalArgumentException ignore) {
187        }
188        return ret;  
189    }
190
191    /**
192     * Can this command station handle the Read with a starting value ('V'erify)
193     * @return true if yes or false if no
194     */
195    public boolean isReadStartValSupported() {
196        boolean ret = false;
197        try {
198            //command stations starting with 3 can handle reads with startVals
199            ret = (jmri.Version.compareCanonicalVersions(version, "3.0.0") >= 0);
200        } catch (IllegalArgumentException ignore) {
201        }
202        return ret;  
203    }
204 
205    /**
206     * Can this command station handle the Servo and Vpin Turnout creation message formats?
207     * @return true if yes or false if no
208     */
209    public boolean isServoTurnoutCreationSupported() {
210        boolean ret = false;
211        try {
212            // SERVO and VPIN turnout commands added at 3.2.0
213            ret = (jmri.Version.compareCanonicalVersions(version, "3.2.0") >= 0);
214        } catch (IllegalArgumentException ignore) {
215        }
216        return ret;  
217    }
218
219    /**
220     * Does this command station require the new "J" commands for turnout definitions?
221     * @return true if yes or false if no
222     */
223    public boolean isTurnoutIDsMessageRequired() {
224        boolean ret = false;
225        try {
226            ret = (jmri.Version.compareCanonicalVersions(version, "5.0.0") >= 0);
227        } catch (IllegalArgumentException ignore) {
228        }
229        return ret;  
230    }
231
232    /**
233     * Does this command station need the throttle register to be sent?
234     * @return true if yes or false if no
235     */
236    public boolean isThrottleRegisterRequired() {
237        boolean ret = true;
238        try {
239            ret = (jmri.Version.compareCanonicalVersions(version, "4.0.0") < 0);
240        } catch (IllegalArgumentException ignore) {
241        }
242        return ret;  
243    }
244
245    /**
246     * Can this command station handle the newer (V4) function message format?
247     * @return true if yes or false if no
248     */
249    public boolean isFunctionV4Supported() {
250        boolean ret = false;
251        try {
252            ret = (jmri.Version.compareCanonicalVersions(version, "4.0.0") >= 0);
253        } catch (IllegalArgumentException ignore) {
254        }
255        return ret;  
256    }
257
258    /**
259     * Can this command station handle the newer (V4) program message formats?
260     * @return true if yes or false if no
261     */
262    public boolean isProgramV4Supported() {
263        boolean ret = false;
264        try {
265            ret = (jmri.Version.compareCanonicalVersions(version, "4.0.1") >= 0);
266        } catch (IllegalArgumentException ignore) {
267        }
268        return ret;  
269    }
270
271    // A few utility functions
272    /**
273     * Get the Lower byte of a locomotive address from the decimal locomotive
274     * address.
275     * @param address loco address.
276     * @return loco address byte lo.
277     */
278    public static int getDCCAddressLow(int address) {
279        /* For addresses below 128, we just return the address, otherwise,
280         we need to return the upper byte of the address after we add the
281         offset 0xC000. The first address used for addresses over 127 is 0xC080*/
282        if (address < 128) {
283            return (address);
284        } else {
285            int temp = address + 0xC000;
286            temp = temp & 0x00FF;
287            return temp;
288        }
289    }
290
291    /**
292     * Get the Upper byte of a locomotive address from the decimal locomotive
293     * address.
294     * @param address loco address.
295     * @return high byte of address.
296     */
297    public static int getDCCAddressHigh(int address) {
298        /* this isn't actually the high byte, For addresses below 128, we
299         just return 0, otherwise, we need to return the upper byte of the
300         address after we add the offset 0xC000 The first address used for
301         addresses over 127 is 0xC080*/
302        if (address < 128) {
303            return (0x00);
304        } else {
305            int temp = address + 0xC000;
306            temp = temp & 0xFF00;
307            temp = temp / 256;
308            return temp;
309        }
310    }
311
312    /* To implement the CommandStation Interface, we have to define the
313     sendPacket function */
314    /**
315     * Send a specific packet to the rails.
316     *
317     * @param packet  Byte array representing the packet, including the
318     *                error-correction byte. Must not be null.
319     * @param repeats Number of times to repeat the transmission.
320     */
321    @Override
322    public boolean sendPacket(@Nonnull byte [] packet, int repeats) {
323
324        if (_tc == null) {
325            log.error("Send Packet Called without setting traffic controller");
326            return false;
327        }
328
329        int reg = 0;  // register 0, so this doesn't repeat
330        //  DCC++ BaseStation code appends its own error-correction byte.
331        // So we have to omit the JMRI-generated one.
332        DCCppMessage msg = DCCppMessage.makeWriteDCCPacketMainMsg(reg, packet.length - 1, packet);
333        assert msg != null;
334        log.debug("sendPacket:'{}'", msg);
335
336        for (int i = 0; i < repeats; i++) {
337            _tc.sendDCCppMessage(msg, null);
338        }
339        return true;
340    }
341
342    /*
343     * For the command station interface, we need to set the traffic 
344     * controller.
345     */
346    public void setTrafficController(DCCppTrafficController tc) {
347        _tc = tc;
348    }
349
350    private DCCppTrafficController _tc = null;
351
352    public void setSystemConnectionMemo(DCCppSystemConnectionMemo memo) {
353        adaptermemo = memo;
354    }
355
356    public DCCppSystemConnectionMemo getSystemConnectionMemo() {
357        return adaptermemo;
358    }
359
360    private DCCppSystemConnectionMemo adaptermemo;
361
362    private void creatermgr() {
363        if (rmgr == null) {
364            rmgr = new DCCppRegisterManager(maxNumSlots);
365        }
366    }
367
368    @Override
369    public String getUserName() {
370        if (adaptermemo == null) {
371            return "DCC++";
372        }
373        return adaptermemo.getUserName();
374    }
375
376    @Override
377    @Nonnull
378    public String getSystemPrefix() {
379        if (adaptermemo == null) {
380            return "D";
381        }
382        return adaptermemo.getSystemPrefix();
383    }
384
385    public int requestNewRegister(int addr) {
386        creatermgr();
387        return (rmgr.requestRegister(addr));
388    }
389
390    public void releaseRegister(int addr) {
391        creatermgr();
392        rmgr.releaseRegister(addr);
393    }
394
395    // Return DCCppConstants.NO_REGISTER_FREE if address is not in list
396    public int getRegisterNum(int addr) {
397        creatermgr();
398        return (rmgr.getRegisterNum(addr));
399    }
400
401    // Return DCCppConstants.REGISTER_UNALLOCATED if register is unused.
402    public int getRegisterAddress(int num) {
403        creatermgr();
404        return (rmgr.getRegisterAddress(num));
405    }
406
407    // entries will be received in order, but the whole list may be sent again
408    public void setTrackMode(int i, String mode) {        
409        if (this.trackModes.size() > i) {
410            this.trackModes.set(i, mode); //update it
411        } else {
412            this.trackModes.add(mode);  // add it
413        }
414    }
415    public List<String> getTrackModes() {
416        return trackModes;
417    }
418    public String getTrackMode(int i) {
419        if (this.trackModes.size() > i) {
420            return trackModes.get(i);
421        } else {
422            return "";  //don't crash downstream
423        }
424    }
425
426    /*
427     * We need to register for logging
428     */
429    private final static Logger log = LoggerFactory.getLogger(DCCppCommandStation.class);
430
431}