001package jmri.jmrix.ieee802154.xbee;
002
003import com.digi.xbee.api.RemoteXBeeDevice;
004import com.digi.xbee.api.exceptions.InterfaceNotOpenException;
005import com.digi.xbee.api.exceptions.TimeoutException;
006import com.digi.xbee.api.exceptions.XBeeException;
007import com.digi.xbee.api.io.IOLine;
008import com.digi.xbee.api.io.IOMode;
009import com.digi.xbee.api.io.IOSample;
010import com.digi.xbee.api.listeners.IIOSampleReceiveListener;
011
012import java.util.Locale;
013import javax.annotation.Nonnull;
014import jmri.JmriException;
015import jmri.NamedBean;
016import jmri.Sensor;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * Manage the XBee specific Sensor implementation.
022 * <p>
023 * System names are formatted as one of:
024 * <ul>
025 *   <li>"ZSnnn", where nnn is the sensor number without padding</li>
026 *   <li>"ZSstring:pin", where string is a node address and pin is the io pin used.</li>
027 * </ul>
028 * Z is the user-configurable system prefix.
029 *
030 * @author Paul Bender Copyright (C) 2003-2016
031 */
032public class XBeeSensorManager extends jmri.managers.AbstractSensorManager implements IIOSampleReceiveListener{
033
034    // ctor has to register for XBee events
035    public XBeeSensorManager(XBeeConnectionMemo memo) {
036        super(memo);
037        tc = (XBeeTrafficController) memo.getTrafficController();
038        tc.getXBee().addIOSampleListener(this);
039    }
040
041    /**
042     * {@inheritDoc}
043     */
044    @Override
045    @Nonnull
046    public XBeeConnectionMemo getMemo() {
047        return (XBeeConnectionMemo) memo;
048    }
049
050    protected XBeeTrafficController tc = null;
051
052    // to free resources when no longer used
053    @Override
054    public void dispose() {
055        tc.getXBee().removeIOSampleListener(this);
056        super.dispose();
057    }
058
059    // XBee specific methods
060
061    /**
062     * {@inheritDoc}
063     * <p>
064     * System name is normalized to ensure uniqueness.
065     * @throws IllegalArgumentException when SystemName can't be converted
066     */
067    @Override
068    @Nonnull
069    protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException {
070        XBeeNode curNode;
071        String name = addressFromSystemName(systemName);
072        if ((curNode = (XBeeNode) tc.getNodeFromName(name)) == null) {
073            if ((curNode = (XBeeNode) tc.getNodeFromAddress(name)) == null) {
074                try {
075                    curNode = (XBeeNode) tc.getNodeFromAddress(Integer.parseInt(name));
076                } catch (java.lang.NumberFormatException nfe) {
077                    // we couldn't find the node
078                    throw new IllegalArgumentException("Unable to convert " +  // NOI18N
079                            systemName + " to XBee sensor address"); // NOI18N
080                }
081            }
082        }
083        int pin = pinFromSystemName(systemName);
084        if (curNode != null && !curNode.getPinAssigned(pin)) {
085            log.debug("Adding sensor to pin {}", pin);
086            curNode.setPinBean(pin, new XBeeSensor(systemName, userName, tc));
087            return (XBeeSensor) curNode.getPinBean(pin);
088        } else {
089            log.debug("Failed to create sensor {}", systemName);
090            throw new IllegalArgumentException("Can't assign pin for " +  // NOI18N
091                    systemName +
092                    " XBee sensor"); // NOI18N
093        }
094    }
095
096    /**
097     * {@inheritDoc}
098     */
099    @Override
100    @Nonnull
101    public String validateSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) {
102        super.validateSystemNameFormat(name, locale);
103        int pin = pinFromSystemName(name);
104        if (pin < 0 || pin > 7) {
105            throw new NamedBean.BadSystemNameException(
106                    Bundle.getMessage(Locale.ENGLISH, "SystemNameInvalidPin", name),
107                    Bundle.getMessage(locale, "SystemNameInvalidPin", name));
108        }
109        return name;
110    }
111    
112    /**
113     * {@inheritDoc}
114     */
115    @Override
116    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
117        if (tc.getNodeFromName(addressFromSystemName(systemName)) == null
118                && tc.getNodeFromAddress(addressFromSystemName(systemName)) == null) {
119            try {
120                if (tc.getNodeFromAddress(Integer.parseInt(addressFromSystemName(systemName))) == null) {
121                    return NameValidity.INVALID;
122                } else {
123                    return (pinFromSystemName(systemName) >= 0
124                            && pinFromSystemName(systemName) <= 7) ? NameValidity.VALID : NameValidity.INVALID;
125                }
126            } catch (java.lang.NumberFormatException nfe) {
127                // if there was a number format exception, we couldn't find the node.
128                log.debug("Unable to convert {} into the XBee node and pin format of nn:xx", systemName);
129                return NameValidity.INVALID;
130            }
131
132        } else {
133            return (pinFromSystemName(systemName) >= 0
134                    && pinFromSystemName(systemName) <= 7) ? NameValidity.VALID : NameValidity.INVALID;
135        }
136    }
137
138    // IIOSampleReceiveListener methods
139
140    @Override
141    public synchronized void ioSampleReceived(RemoteXBeeDevice remoteDevice,IOSample ioSample) {
142        if (log.isDebugEnabled()) {
143            log.debug("received io sample {} from {}",ioSample,remoteDevice);
144        }
145
146        XBeeNode node = (XBeeNode) tc.getNodeFromXBeeDevice(remoteDevice);
147        for (int i = 0; i <= 8; i++) {
148            if (!node.getPinAssigned(i)
149                && ioSample.hasDigitalValue(IOLine.getDIO(i))) {
150                   // get pin direction
151                   IOMode mode = IOMode.DISABLED;  // assume disabled as default.
152                   try  {
153                       mode = remoteDevice.getIOConfiguration(IOLine.getDIO(i));
154                   } catch (TimeoutException toe) {
155                      log.debug("Timeout retrieving IO line mode for {} on {}",IOLine.getDIO(i),remoteDevice);
156                      // is this a hidden terminal?  This was triggered by an 
157                      // IO Sample, so we know we can hear the other node, but
158                      // it may not hear us.  In this case, assume we are 
159                      // working with an input pin.
160                      mode = IOMode.DIGITAL_IN;
161                   } catch (InterfaceNotOpenException ino) {
162                      log.error("Interface Not Open retrieving IO line mode for {} on {}",IOLine.getDIO(i),remoteDevice);
163                   } catch (XBeeException xbe) {
164                      log.error("Error retrieving IO line mode for {} on {}",IOLine.getDIO(i),remoteDevice);
165                   }
166               
167                   if(mode == IOMode.DIGITAL_IN ) {
168                        // thisis an input, check to see if it exists as a sensor.
169                        node = (XBeeNode) tc.getNodeFromXBeeDevice(remoteDevice);
170
171                        // Sensor name is prefix followed by NI/address
172                        // followed by the bit number.
173                        String sName = getSystemNamePrefix()
174                                + node.getPreferedName() + ":" + i;
175                        XBeeSensor s = (XBeeSensor) getSensor(sName);
176                        if (s == null) {
177                            // the sensor doesn't exist, so provide a new one.
178                            try {
179                               provideSensor(sName);
180                               if (log.isDebugEnabled()) {
181                                   log.debug("DIO {} enabled as sensor",sName);
182                               }
183                            } catch(java.lang.IllegalArgumentException iae){
184                               // if provideSensor fails, it will throw an IllegalArgumentException, so catch that,log it if debugging is enabled, and then re-throw it.
185                               if (log.isDebugEnabled()) {
186                                   log.debug("Attempt to enable DIO {} as sensor failed",sName);
187                               }
188                               throw iae;
189                            }
190                        }
191                    }
192               }
193            }
194        }
195
196    // for now, set this to false. multiple additions currently works
197    // partially, but not for all possible cases.
198    @Override
199    public boolean allowMultipleAdditions(@Nonnull String systemName) {
200        return false;
201    }
202
203    @Override
204    @Nonnull
205    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
206        String encoderAddress = addressFromSystemName(prefix + typeLetter() + curAddress);
207        int input = pinFromSystemName(prefix + typeLetter() + curAddress);
208
209        if (encoderAddress.equals("")) {
210            throw new JmriException("I unable to determine hardware address");
211        }
212        return prefix + typeLetter() + encoderAddress + ":" + input;
213    }
214
215    private String addressFromSystemName(String systemName) {
216        String encoderAddress;
217
218        if (systemName.contains(":")) {
219            //Address format passed is in the form of encoderAddress:input or S:light address
220            int seperator = systemName.indexOf(":");
221            encoderAddress = systemName.substring(getSystemPrefix().length() + 1, seperator);
222        } else {
223            encoderAddress = systemName.substring(getSystemPrefix().length() + 1, systemName.length() - 1);
224        }
225        if (log.isDebugEnabled()) {
226            log.debug("Converted {} to hardware address {}", systemName, encoderAddress);
227        }
228        return encoderAddress;
229    }
230
231    private int pinFromSystemName(String systemName) {
232        int input = 0;
233        int iName = 0;
234
235        if (systemName.contains(":")) {
236            //Address format passed is in the form of encoderAddress:input or L:light address
237            int seperator = systemName.indexOf(":");
238            try {
239                input = Integer.parseInt(systemName.substring(seperator + 1));
240            } catch (NumberFormatException ex) {
241                log.debug("Unable to convert {} into the cab and input format of nn:xx", systemName);
242                return -1;
243            }
244        } else {
245            try {
246                iName = Integer.parseInt(systemName.substring(getSystemPrefix().length() + 1));
247                input = iName % 10;
248            } catch (NumberFormatException ex) {
249                log.debug("Unable to convert {} Hardware Address to a number", systemName);
250                return -1;
251            }
252        }
253        if (log.isDebugEnabled()) {
254            log.debug("Converted {} to pin number{}", systemName, input);
255        }
256        return input;
257    }
258
259    @Override
260    public void deregister(@Nonnull jmri.Sensor s) {
261        super.deregister(s);
262        // remove the specified sensor from the associated XBee pin.
263        String systemName = s.getSystemName();
264        String name = addressFromSystemName(systemName);
265        int pin = pinFromSystemName(systemName);
266        XBeeNode curNode;
267        if ((curNode = (XBeeNode) tc.getNodeFromName(name)) == null) {
268            if ((curNode = (XBeeNode) tc.getNodeFromAddress(name)) == null) {
269                try {
270                    curNode = (XBeeNode) tc.getNodeFromAddress(Integer.parseInt(name));
271                } catch (java.lang.NumberFormatException nfe) {
272                    // if there was a number format exception, we couldn't
273                    // find the node.
274                    curNode = null;
275                }
276            }
277        }
278        if (curNode != null) {
279            if (curNode.removePinBean(pin, s)) {
280                log.debug("Removing sensor from pin {}", pin);
281            } else {
282                log.debug("Failed to removing sensor from pin {}", pin);
283            }
284        }
285    }
286
287    /**
288     * Do the sensor objects provided by this manager support configuring
289     * an internal pullup or pull down resistor?
290     * <p>
291     * For Raspberry Pi systems, it is possible to set the pullup or
292     * pulldown resistor, so return true.
293     *
294     * @return true if pull up/pull down configuration is supported.
295     */
296    @Override
297    public boolean isPullResistanceConfigurable(){
298       return true;
299    }
300
301    /**
302     * {@inheritDoc}
303     */
304    @Override
305    public String getEntryToolTip() {
306        return Bundle.getMessage("AddEntryToolTip");
307    }
308
309    private final static Logger log = LoggerFactory.getLogger(XBeeSensorManager.class);
310
311}