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