001package jmri.jmrix.tams;
002
003import java.util.Hashtable;
004import javax.annotation.Nonnull;
005import jmri.JmriException;
006import jmri.Sensor;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * Implement sensor manager for Tams systems. The Manager handles all the state\
012 * changes Requires v1.4.7 or higher of TAMS software to work correctly
013 * <p>
014 * System names are "TSnnn:yy", where T is the user configurable system prefix,
015 * nnn is the Tams Object Number for a given S88 Bus Module and
016 * yy is the port on that module.
017 *
018 * @author Kevin Dickerson Copyright (C) 2009
019 * @author Jan Boen and Sergiu Costan
020 *
021 * Rework Poll for status using binary commands send xEvtSen (78 CB)h this
022 * returns multiple bytes first byte address of the S88 sensor, second and third
023 * bytes = values of that sensor this repeats for each sensor with changes the
024 * last byte contains 00h this means all reports have been received
025 *
026 * xEvtSen reports sensor changes
027 */
028public class TamsSensorManager extends jmri.managers.AbstractSensorManager implements TamsListener {
029
030    public int maxSE; //Will hold the highest value of board number x 2 and we use this value to determine to tell the Tams MC how many S88 half-modules to poll
031
032    public TamsSensorManager(TamsSystemConnectionMemo memo) {
033        super(memo);
034        init();
035    }
036
037    private void init() {
038        TamsTrafficController tc = getMemo().getTrafficController();
039        //Connect to the TrafficManager
040        tc.addTamsListener(this);
041        TamsMessage tm = TamsMessage.setXSR();//auto reset after reading S88
042        tc.sendTamsMessage(tm, this);
043        log.debug("Sending TamsMessage = {} , isBinary = {} and replyType = {}",
044                tm.toString(), tm.isBinary(), tm.getReplyType());
045        //Add polling for sensor state changes
046        tm = TamsMessage.getXEvtSen(); //reports only sensors with changed states
047        //tc.sendTamsMessage(tm, this);
048        tc.addPollMessage(tm, this);
049        log.debug("TamsMessage added to poll queue = {} {} and replyType = {}",
050                jmri.util.StringUtil.appendTwoHexFromInt(tm.getElement(0) & 0xFF, ""),
051                jmri.util.StringUtil.appendTwoHexFromInt(tm.getElement(1) & 0xFF, ""),
052                tm.getReplyType());
053    }
054
055    //The hash table simply holds the object number against the TamsSensor ref.
056    private final Hashtable<Integer, Hashtable<Integer, TamsSensor>> _ttams = new Hashtable<>(); // stores known Tams Obj
057
058    /**
059     * {@inheritDoc}
060     */
061    @Override
062    @Nonnull
063    public TamsSystemConnectionMemo getMemo() {
064        return (TamsSystemConnectionMemo) memo;
065    }
066
067    /**
068     * {@inheritDoc}
069     * <p>
070     * System name is normalized to ensure uniqueness.
071     * @throws IllegalArgumentException when SystemName can't be converted
072     */
073    @Override
074    @Nonnull
075    protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException {
076        TamsTrafficController tc = getMemo().getTrafficController();
077        TamsSensor s = new TamsSensor(systemName, userName);
078        log.debug("Creating new TamsSensor: {}", systemName);
079        if (systemName.contains(":")) {
080            int board;
081            int channel;
082
083            String curAddress = systemName.substring(getSystemPrefix().length() + 1);
084            int seperator = curAddress.indexOf(':');
085            try {
086                board = Integer.parseInt(curAddress.substring(0, seperator));
087                log.debug("Creating new TamsSensor with board: {}", board);
088                if (!_ttams.containsKey(board)) {
089                    _ttams.put(board, new Hashtable<>());
090                    //log.debug("_ttams: {}", _ttams.toString());
091                }
092            } catch (NumberFormatException ex) {
093                throw new IllegalArgumentException("Unable to convert " +  // NOI18N
094                        systemName.substring(getSystemPrefix().length() + 1) +
095                        " into the Module and port format of nn:xx"); // NOI18N
096            }
097            Hashtable<Integer, TamsSensor> sensorList = _ttams.get(board);
098            try {
099                channel = Integer.parseInt(curAddress.substring(seperator + 1));
100                if (!sensorList.containsKey(channel)) {
101                    sensorList.put(channel, s);
102                }
103            } catch (NumberFormatException ex) {
104                throw new IllegalArgumentException("Unable to convert " +  // NOI18N
105                        systemName.substring(getSystemPrefix().length() + 1) +
106                        " into the Module and port format of nn:xx"); // NOI18N
107            }
108            if ((board * 2) > maxSE) {//Check if newly defined board number is higher than what we know
109                maxSE = board * 2;//adjust xSE and inform Tams MC
110                log.debug("Changed xSE to {}", maxSE);
111                TamsMessage tm = new TamsMessage("xSE " + Integer.toString(maxSE));
112                tm.setBinary(false);
113                tm.setReplyType('S');
114                tc.sendTamsMessage(tm, this);
115            }
116        }
117        //Probably sending the status check 16 times but should work...
118        //Get initial status of sensors
119        TamsMessage tm = TamsMessage.setXSensOff(); //force report from sensors with at least 1 port set
120        tc.sendTamsMessage(tm, this);
121        tm = TamsMessage.getXEvtSen(); //reports only sensors with changed states
122        tc.sendTamsMessage(tm, this);
123        log.debug("Returning this sensor: {}", s.toString());
124        return s;
125    }
126
127    @Override
128    @Nonnull
129    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
130        if (!curAddress.contains(":")) {
131            throw new JmriException("Hardware Address passed should be past in the form 'Module:port', was " + curAddress);
132        }
133
134        //Address format passed is in the form of board:channel or T:turnout address
135        int seperator = curAddress.indexOf(':');
136        try {
137            board = Integer.parseInt(curAddress.substring(0, seperator));
138        } catch (NumberFormatException ex) {
139            throw new JmriException("First part of "+curAddress+" in front of : should be a number");
140        }
141        try {
142            port = Integer.parseInt(curAddress.substring(seperator + 1));
143        } catch (NumberFormatException ex) {
144            throw new JmriException("Port Address, Second part of "+curAddress+" after : should be a number");
145        }
146
147        if (port == 0 || port > 16) {
148            throw new JmriException("Port number in "+curAddress+" must be between 1 and 16");
149        }
150        StringBuilder sb = new StringBuilder();
151        sb.append(getSystemPrefix());
152        sb.append("S");
153        sb.append(board);
154        sb.append(":");
155        //Little work around to pad single digit address out.
156        padPortNumber(port, sb);
157        return sb.toString();
158    }
159
160    /**
161     * Validates to contain at least 1 number . . .
162     * <p>
163     * TODO: add custom TamsSensor validation.
164     * {@inheritDoc}
165     */
166    @Override
167    @Nonnull
168    public String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale) throws jmri.NamedBean.BadSystemNameException {
169        return validateTrimmedMin1NumberSystemNameFormat(name,locale);
170    }
171
172    int board = 0;
173    int port = 0;
174
175    void padPortNumber(int portNo, StringBuilder sb) {
176        if (portNo < 10) {
177            sb.append("0");
178        }
179        sb.append(portNo);
180    }
181
182    /**
183     * Determine if it is possible to add a range of sensors in numerical order
184     * eg 1 to 16, primarily used to enable/disable the add range box in the add
185     * sensor panel.
186     *
187     * @return true
188     */
189    @Override
190    public boolean allowMultipleAdditions(@Nonnull String systemName) {
191        return true;
192    }
193
194    // to listen for status changes from Tams system
195    @Override
196    public void reply(TamsReply r) {
197        //log.debug("ReplyType = " + tm.getReplyType() + ", Binary? = " +  tm.isBinary()+ ", OneByteReply = " + tm.getReplyOneByte());
198        if (getMemo().getTrafficController().replyType == 'S') {//Only handle Sensor events
199            log.debug("*** Tams Sensor Reply ***");
200            if ( getMemo().getTrafficController().replyBinary ) {
201                log.debug("Reply to binary command = {}", r );
202                if ((r.getNumDataElements() > 1) && (r.getElement(0) > 0x00)) {
203                    // Here we break up a long sensor related TamsReply into individual S88 module status'
204                    int numberOfReplies = r.getNumDataElements() / 3;
205                    //log.debug("Incoming Reply = ");
206                    //for (int i = 0; i < r.getNumDataElements(); i++) {
207                        //log.debug("Byte " + i + " = " + jmri.util.StringUtil.appendTwoHexFromInt(r.getElement(i) & 0xFF, ""));
208                    //}
209                    //log.debug("length of reply = " + r.getNumDataElements() + " & number of replies = " + numberOfReplies);
210                    for (int i = 0; i < numberOfReplies; i++) {
211                        //create a new TamsReply and pass it to the decoder
212                        TamsReply tr = new TamsReply();
213                        tr.setBinary(true);
214                        tr.setElement(0, r.getElement(3 * i));
215                        tr.setElement(1, r.getElement(3 * i + 1));
216                        tr.setElement(2, r.getElement(3 * i + 2));
217                        log.debug("Going to pass this to the decoder = {} {} {}",
218                                tr.getElement(0), tr.getElement(1), tr.getElement(2));
219                        //The decodeSensorState will do the actual decoding of each individual S88 port
220                        decodeSensorState(tr);
221                    }
222                }
223            }
224        }
225    }
226
227    @Override
228    public void message(TamsMessage m) {
229        // messages are ignored
230    }
231
232    private void decodeSensorState(TamsReply r) {
233        //reply to XEvtSen consists of 3 bytes per S88 module
234        //byte 1 = S88 board number 1 to 52 in binary format
235        //byte 2 = bits 1 to 8
236        //byte 3 = bits 9 to 16
237        String sensorprefix = getSystemPrefix() + "S" + r.getElement(0) + ":";
238        log.debug("Decoding sensor: {}", sensorprefix);
239        log.debug("Lower Byte: {}", r.getElement(1));
240        log.debug("Upper Byte: {}", r.getElement(2));
241        Hashtable<Integer, TamsSensor> sensorList = _ttams.get(board);
242        int i = (r.getElement(1) & 0xff) << 8;//first 8 ports in second element of the reply
243        log.debug("i after loading first byte= {}", Integer.toString(i,2));
244        i = i + (r.getElement(2) & 0xff);//first 8 ports in third element of the reply
245        log.debug("i after loading second byte= {}", Integer.toString(i,2));
246        int mask = 0b1000000000000000;
247        for (int j = 1; j <= 16; j++) {
248            int result = i & mask;
249            //log.debug("mask= {}", Integer.toString(mask, 2));
250            //log.debug("result= {}", Integer.toString(result, 2));
251            if (sensorList != null) {
252                TamsSensor ms = sensorList.get(j);
253                log.debug("ms: {}", ms);
254                if (ms == null) {
255                    log.debug("ms = NULL!");
256                    StringBuilder sb = new StringBuilder();
257                    sb.append(sensorprefix);
258                    //Little work around to pad single digit address out.
259                    padPortNumber(j, sb);
260                    try {
261                        ms = (TamsSensor) provideSensor(sb.toString());
262                    } catch (Exception e){
263                        log.warn("Could not provide Sensor {}: {}",sb.toString(),e.getLocalizedMessage());
264                    }
265                }
266                if (ms != null) {
267                    log.debug("ms = exists and is not null");
268                    if (result == 0) {
269                        ms.setOwnState(Sensor.INACTIVE);
270                        log.debug("{}{} INACTIVE", sensorprefix, j);
271                    } else {
272                        log.debug("{}{} ACTIVE", sensorprefix, j);
273                        ms.setOwnState(Sensor.ACTIVE);
274                    }
275                }
276            }
277            mask = mask / 2;
278        }
279        log.debug("sensor decoding is done");
280    }
281
282    @Override
283    public void dispose() {
284        getMemo().getTrafficController().removePollMessage(TamsMessage.getXEvtSen(), this);
285        getMemo().getTrafficController().removeTamsListener(this);
286        super.dispose();
287    }
288
289    private final static Logger log = LoggerFactory.getLogger(TamsSensorManager.class);
290}