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    /**
272     * Can this command station handle the &lt;#&gt; request to get supported number of cabs(locos)?
273     * @return true if yes or false if no
274     */
275    public boolean isMaxNumSlotsMsgSupported() {
276        boolean ret = false;
277        try {
278            ret = (jmri.Version.compareCanonicalVersions(version, "3.0.0") >= 0);
279        } catch (IllegalArgumentException ignore) {
280        }
281        return ret;  
282    }
283
284    // A few utility functions
285    /**
286     * Get the Lower byte of a locomotive address from the decimal locomotive
287     * address.
288     * @param address loco address.
289     * @return loco address byte lo.
290     */
291    public static int getDCCAddressLow(int address) {
292        /* For addresses below 128, we just return the address, otherwise,
293         we need to return the upper byte of the address after we add the
294         offset 0xC000. The first address used for addresses over 127 is 0xC080*/
295        if (address < 128) {
296            return (address);
297        } else {
298            int temp = address + 0xC000;
299            temp = temp & 0x00FF;
300            return temp;
301        }
302    }
303
304    /**
305     * Get the Upper byte of a locomotive address from the decimal locomotive
306     * address.
307     * @param address loco address.
308     * @return high byte of address.
309     */
310    public static int getDCCAddressHigh(int address) {
311        /* this isn't actually the high byte, For addresses below 128, we
312         just return 0, otherwise, we need to return the upper byte of the
313         address after we add the offset 0xC000 The first address used for
314         addresses over 127 is 0xC080*/
315        if (address < 128) {
316            return (0x00);
317        } else {
318            int temp = address + 0xC000;
319            temp = temp & 0xFF00;
320            temp = temp / 256;
321            return temp;
322        }
323    }
324
325    /* To implement the CommandStation Interface, we have to define the
326     sendPacket function */
327    /**
328     * Send a specific packet to the rails.
329     *
330     * @param packet  Byte array representing the packet, including the
331     *                error-correction byte. Must not be null.
332     * @param repeats Number of times to repeat the transmission.
333     */
334    @Override
335    public boolean sendPacket(@Nonnull byte [] packet, int repeats) {
336
337        if (_tc == null) {
338            log.error("Send Packet Called without setting traffic controller");
339            return false;
340        }
341
342        int reg = 0;  // register 0, so this doesn't repeat
343        //  DCC++ BaseStation code appends its own error-correction byte.
344        // So we have to omit the JMRI-generated one.
345        DCCppMessage msg = DCCppMessage.makeWriteDCCPacketMainMsg(reg, packet.length - 1, packet);
346        assert msg != null;
347        log.debug("sendPacket:'{}'", msg);
348
349        for (int i = 0; i < repeats; i++) {
350            _tc.sendDCCppMessage(msg, null);
351        }
352        return true;
353    }
354
355    /*
356     * For the command station interface, we need to set the traffic 
357     * controller.
358     */
359    public void setTrafficController(DCCppTrafficController tc) {
360        _tc = tc;
361    }
362
363    private DCCppTrafficController _tc = null;
364
365    public void setSystemConnectionMemo(DCCppSystemConnectionMemo memo) {
366        adaptermemo = memo;
367    }
368
369    public DCCppSystemConnectionMemo getSystemConnectionMemo() {
370        return adaptermemo;
371    }
372
373    private DCCppSystemConnectionMemo adaptermemo;
374
375    private void creatermgr() {
376        if (rmgr == null) {
377            rmgr = new DCCppRegisterManager(maxNumSlots);
378        }
379    }
380
381    @Override
382    public String getUserName() {
383        if (adaptermemo == null) {
384            return "DCC++";
385        }
386        return adaptermemo.getUserName();
387    }
388
389    @Override
390    @Nonnull
391    public String getSystemPrefix() {
392        if (adaptermemo == null) {
393            return "D";
394        }
395        return adaptermemo.getSystemPrefix();
396    }
397
398    public int requestNewRegister(int addr) {
399        creatermgr();
400        return (rmgr.requestRegister(addr));
401    }
402
403    public void releaseRegister(int addr) {
404        creatermgr();
405        rmgr.releaseRegister(addr);
406    }
407
408    // Return DCCppConstants.NO_REGISTER_FREE if address is not in list
409    public int getRegisterNum(int addr) {
410        creatermgr();
411        return (rmgr.getRegisterNum(addr));
412    }
413
414    // Return DCCppConstants.REGISTER_UNALLOCATED if register is unused.
415    public int getRegisterAddress(int num) {
416        creatermgr();
417        return (rmgr.getRegisterAddress(num));
418    }
419
420    // entries will be received in order, but the whole list may be sent again
421    public void setTrackMode(int i, String mode) {        
422        if (this.trackModes.size() > i) {
423            this.trackModes.set(i, mode); //update it
424        } else {
425            this.trackModes.add(mode);  // add it
426        }
427    }
428    public List<String> getTrackModes() {
429        return trackModes;
430    }
431    public String getTrackMode(int i) {
432        if (this.trackModes.size() > i) {
433            return trackModes.get(i);
434        } else {
435            return "";  //don't crash downstream
436        }
437    }
438
439    /*
440     * We need to register for logging
441     */
442    private final static Logger log = LoggerFactory.getLogger(DCCppCommandStation.class);
443
444}