001package jmri.jmrix.loconet.bluetooth;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.io.OutputStream;
008import java.util.Vector;
009
010import javax.annotation.Nonnull;
011import javax.bluetooth.BluetoothStateException;
012import javax.bluetooth.DeviceClass;
013import javax.bluetooth.DiscoveryAgent;
014import javax.bluetooth.DiscoveryListener;
015import javax.bluetooth.LocalDevice;
016import javax.bluetooth.RemoteDevice;
017import javax.bluetooth.ServiceRecord;
018import javax.bluetooth.UUID;
019import javax.microedition.io.Connection;
020import javax.microedition.io.Connector;
021import javax.microedition.io.StreamConnection;
022import jmri.jmrix.ConnectionStatus;
023import jmri.jmrix.loconet.LnPacketizer;
024import jmri.jmrix.loconet.LnPortController;
025import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029/**
030 * Provide access to LocoNet via a LocoNet Bluetooth adapter.
031 */
032public class LocoNetBluetoothAdapter extends LnPortController {
033
034    public LocoNetBluetoothAdapter() {
035        this(new LocoNetSystemConnectionMemo());
036    }
037
038    public LocoNetBluetoothAdapter(LocoNetSystemConnectionMemo adapterMemo) {
039        super(adapterMemo);
040        option1Name = "CommandStation"; // NOI18N
041        option2Name = "TurnoutHandle"; // NOI18N
042        options.put(option1Name, new Option(Bundle.getMessage("CommandStationTypeLabel"), commandStationNames, false));
043        options.put(option2Name, new Option(Bundle.getMessage("TurnoutHandling"),
044                new String[]{Bundle.getMessage("HandleNormal"), Bundle.getMessage("HandleSpread"), Bundle.getMessage("HandleOneOnly"), Bundle.getMessage("HandleBoth")})); // I18N
045    }
046
047    @Override
048    public Vector<String> getPortNames() {
049        return LocoNetBluetoothAdapter.discoverPortNames();
050    }
051
052    @Override
053    public String openPort(String portName, String appName) {
054        int[] responseCode = new int[]{-1};
055        Exception[] exception = new Exception[]{null};
056        try {
057            // Find the RemoteDevice with this name.
058            RemoteDevice[] devices = LocalDevice.getLocalDevice().getDiscoveryAgent().retrieveDevices(DiscoveryAgent.PREKNOWN);
059            if (devices != null) {
060                for (RemoteDevice device : devices) {
061                    if (device.getFriendlyName(false).equals(portName)) {
062                        Object[] waitObj = new Object[0];
063                        // Start a search for a serialport service (UUID 0x1101)
064                        LocalDevice.getLocalDevice().getDiscoveryAgent().searchServices(new int[]{0x0100}, new UUID[]{new UUID(0x1101)}, device, new DiscoveryListener() {
065                            @Override
066                            public void servicesDiscovered(int transID, ServiceRecord[] servRecord) {
067                                synchronized (waitObj) {
068                                    for (ServiceRecord service : servRecord) {
069                                        // Service found, get url for connection.
070                                        String url = service.getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
071                                        if (url == null) {
072                                            continue;
073                                        }
074                                        try {
075                                            // Open connection.
076                                            Connection conn = Connector.open(url, Connector.READ_WRITE);
077                                            if (conn instanceof StreamConnection) { // The connection should be a StreamConnection, otherwise it's a one way communication.
078                                                StreamConnection stream = (StreamConnection) conn;
079                                                in = stream.openInputStream();
080                                                out = stream.openOutputStream();
081                                                opened = true;
082                                                // Port is open, let openPort continue.
083                                                //waitObj.notify();
084                                            } else {
085                                                throw new IOException("Could not establish a two-way communication");
086                                            }
087                                        } catch (IOException IOe) {
088                                            exception[0] = IOe;
089                                        }
090                                    }
091                                    if (!opened) {
092                                        exception[0] = new IOException("No service found to connect to");
093                                    }
094                                }
095                            }
096
097                            @Override
098                            public void serviceSearchCompleted(int transID, int respCode) {
099                                synchronized (waitObj) {
100                                    // Search for services complete, if the port was not opened, save the response code for error analysis.
101                                    responseCode[0] = respCode;
102                                    // Search completer, let openPort continue.
103                                    waitObj.notify();
104                                }
105                            }
106
107                            @Override
108                            public void inquiryCompleted(int discType) {
109                            }
110
111                            @Override
112                            public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
113                            }
114                        });
115                        synchronized (waitObj) {
116                            // Wait until either the port is open on the search has returned a response code.
117                            while (!opened && responseCode[0] == -1) {
118                                try {
119                                    // Wait for search to complete.
120                                    waitObj.wait();
121                                } catch (InterruptedException ex) {
122                                    log.error("Thread unexpectedly interrupted", ex);
123                                }
124                            }
125                        }
126                        break;
127                    }
128                }
129            }
130        } catch (BluetoothStateException BSe) {
131            log.error("Exception when using bluetooth");
132            return BSe.getLocalizedMessage();
133        } catch (IOException IOe) {
134            log.error("Unknown IOException when establishing connection to {}", portName);
135            return IOe.getLocalizedMessage();
136        }
137
138        if (!opened) {
139            ConnectionStatus.instance().setConnectionState(null, portName, ConnectionStatus.CONNECTION_DOWN);
140            if (exception[0] != null) {
141                log.error("Exception when connecting to {}", portName);
142                return exception[0].getLocalizedMessage();
143            }
144            switch (responseCode[0]) {
145                case DiscoveryListener.SERVICE_SEARCH_COMPLETED:
146                    log.error("Bluetooth connection {} not opened, unknown error", portName);
147                    return "Unknown error: failed to connect to " + portName;
148                case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE:
149                    log.error("Bluetooth device {} could not be reached", portName);
150                    return "Could not find " + portName;
151                case DiscoveryListener.SERVICE_SEARCH_ERROR:
152                    log.error("Error when searching for {}", portName);
153                    return "Error when searching for " + portName;
154                case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS:
155                    log.error("No serial service found on {}", portName);
156                    return "Invalid bluetooth device: " + portName;
157                case DiscoveryListener.SERVICE_SEARCH_TERMINATED:
158                    log.error("Service search on {} ended prematurely", portName);
159                    return "Search for " + portName + " ended unexpectedly";
160                default:
161                    log.warn("Unhandled response code: {}", responseCode[0]);
162                    break;
163            }
164            log.error("Unknown error when connecting to {}", portName);
165            return "Unknown error when connecting to " + portName;
166        }
167
168        return null; // normal operation
169    }
170
171    /**
172     * Set up all of the other objects to operate.
173     */
174    @Override
175    public void configure() {
176        setCommandStationType(getOptionState(option1Name));
177        setTurnoutHandling(getOptionState(option2Name));
178        // connect to a packetizing traffic controller
179        LnPacketizer packets = new LnPacketizer(this.getSystemConnectionMemo());
180        packets.connectPort(this);
181
182        // create memo
183        this.getSystemConnectionMemo().setLnTrafficController(packets);
184        // do the common manager config
185
186        this.getSystemConnectionMemo().configureCommandStation(commandStationType,
187                mTurnoutNoRetry, mTurnoutExtraSpace, mTranspondingAvailable, mInterrogateAtStart, mLoconetProtocolAutoDetect);
188        this.getSystemConnectionMemo().configureManagers();
189
190        // start operation
191        packets.startThreads();
192    }
193
194    // base class methods for the LnPortController interface
195    @Override
196    public DataInputStream getInputStream() {
197        if (!opened) {
198            log.error("getInputStream called before load(), stream not available");
199            return null;
200        }
201        return new DataInputStream(in);
202    }
203
204    @Override
205    public DataOutputStream getOutputStream() {
206        if (!opened) {
207            log.error("getOutputStream called before load(), stream not available");
208        }
209        return new DataOutputStream(out);
210    }
211
212    @Override
213    public boolean status() {
214        return opened;
215    }
216
217    // private control members
218    private boolean opened = false;
219    private InputStream in = null;
220    private OutputStream out = null;
221
222    /**
223     * {@inheritDoc}
224     */
225    @Override
226    public String[] validBaudRates() {
227        return new String[]{};
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    @Override
234    public int[] validBaudNumbers() {
235        return new int[]{};
236    }
237
238    @Nonnull
239    protected static Vector<String> discoverPortNames() {
240        Vector<String> portNameVector = new Vector<>();
241        try {
242            RemoteDevice[] devices = LocalDevice.getLocalDevice().getDiscoveryAgent().retrieveDevices(DiscoveryAgent.PREKNOWN);
243            if (devices != null) {
244                for (RemoteDevice device : devices) {
245                    portNameVector.add(device.getFriendlyName(false));
246                }
247            }
248        } catch (IOException ex) {
249            log.error("Unable to use bluetooth device", ex);
250        }
251        return portNameVector;
252    }
253
254    private final static Logger log = LoggerFactory.getLogger(LocoNetBluetoothAdapter.class);
255
256}