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(
140                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
141            if (exception[0] != null) {
142                log.error("Exception when connecting to {}", portName);
143                return exception[0].getLocalizedMessage();
144            }
145            switch (responseCode[0]) {
146                case DiscoveryListener.SERVICE_SEARCH_COMPLETED:
147                    log.error("Bluetooth connection {} not opened, unknown error", portName);
148                    return "Unknown error: failed to connect to " + portName;
149                case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE:
150                    log.error("Bluetooth device {} could not be reached", portName);
151                    return "Could not find " + portName;
152                case DiscoveryListener.SERVICE_SEARCH_ERROR:
153                    log.error("Error when searching for {}", portName);
154                    return "Error when searching for " + portName;
155                case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS:
156                    log.error("No serial service found on {}", portName);
157                    return "Invalid bluetooth device: " + portName;
158                case DiscoveryListener.SERVICE_SEARCH_TERMINATED:
159                    log.error("Service search on {} ended prematurely", portName);
160                    return "Search for " + portName + " ended unexpectedly";
161                default:
162                    log.warn("Unhandled response code: {}", responseCode[0]);
163                    break;
164            }
165            log.error("Unknown error when connecting to {}", portName);
166            return "Unknown error when connecting to " + portName;
167        }
168
169        return null; // normal operation
170    }
171
172    /**
173     * Set up all of the other objects to operate.
174     */
175    @Override
176    public void configure() {
177        setCommandStationType(getOptionState(option1Name));
178        setTurnoutHandling(getOptionState(option2Name));
179        // connect to a packetizing traffic controller
180        LnPacketizer packets = new LnPacketizer(this.getSystemConnectionMemo());
181        packets.connectPort(this);
182
183        // create memo
184        this.getSystemConnectionMemo().setLnTrafficController(packets);
185        // do the common manager config
186
187        this.getSystemConnectionMemo().configureCommandStation(commandStationType,
188                mTurnoutNoRetry, mTurnoutExtraSpace, mTranspondingAvailable, mInterrogateAtStart, mLoconetProtocolAutoDetect);
189        this.getSystemConnectionMemo().configureManagers();
190
191        // start operation
192        packets.startThreads();
193    }
194
195    // base class methods for the LnPortController interface
196    @Override
197    public DataInputStream getInputStream() {
198        if (!opened) {
199            log.error("getInputStream called before load(), stream not available");
200            return null;
201        }
202        return new DataInputStream(in);
203    }
204
205    @Override
206    public DataOutputStream getOutputStream() {
207        if (!opened) {
208            log.error("getOutputStream called before load(), stream not available");
209        }
210        return new DataOutputStream(out);
211    }
212
213    @Override
214    public boolean status() {
215        return opened;
216    }
217
218    // private control members
219    private boolean opened = false;
220    private InputStream in = null;
221    private OutputStream out = null;
222
223    /**
224     * {@inheritDoc}
225     */
226    @Override
227    public String[] validBaudRates() {
228        return new String[]{};
229    }
230
231    /**
232     * {@inheritDoc}
233     */
234    @Override
235    public int[] validBaudNumbers() {
236        return new int[]{};
237    }
238
239    @Nonnull
240    protected static Vector<String> discoverPortNames() {
241        Vector<String> portNameVector = new Vector<>();
242        try {
243            RemoteDevice[] devices = LocalDevice.getLocalDevice().getDiscoveryAgent().retrieveDevices(DiscoveryAgent.PREKNOWN);
244            if (devices != null) {
245                for (RemoteDevice device : devices) {
246                    portNameVector.add(device.getFriendlyName(false));
247                }
248            }
249        } catch (IOException ex) {
250            log.error("Unable to use bluetooth device", ex);
251        }
252        return portNameVector;
253    }
254
255    private static final Logger log = LoggerFactory.getLogger(LocoNetBluetoothAdapter.class);
256
257}