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}