Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- mipmap/ic_launcher created with Android Studio -> New -> Image Asset using @color/colorPrimaryDark as background color -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.kai_morich.simple_bluetooth_le_terminal;

import android.os.Bundle;
import android.os.StrictMode;

import androidx.fragment.app.FragmentManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
Expand All @@ -10,6 +12,8 @@ public class MainActivity extends AppCompatActivity implements FragmentManager.O
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package de.kai_morich.simple_bluetooth_le_terminal;

import android.util.Log;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public abstract class TcpMultiServer implements Runnable {
protected int serverPort = 8080;
protected ServerSocket serverSocket = null;
protected boolean isStopped = false;
protected Thread runningThread= null;
protected String name;
protected boolean global;
private static final String TAG = "TcpMultiServer";
public abstract void newclient(Socket clientSocket);

public TcpMultiServer(int port, String name) {
this(port, name, false);
}

public TcpMultiServer(int port, String name, boolean global) {
this.serverPort = port;
this.name = name;
this.global = global;
}

public void run() {
synchronized(this) {
this.runningThread = Thread.currentThread();
}
try {
if (global)
this.serverSocket = new ServerSocket(this.serverPort);
else /* listen only on 127.0.0.1 */
this.serverSocket = new ServerSocket(this.serverPort, 0,
InetAddress.getByName(null));

} catch (IOException e) {
throw new RuntimeException("Cannot open port " + this.serverPort, e);
}
Log.d(TAG, name + " started @Port: " + this.serverPort);
while(! isStopped()){
Socket clientSocket = null;
try {
clientSocket = this.serverSocket.accept();
} catch (IOException e) {
if(isStopped()) {
Log.d(TAG,"Stopped.");
return;
}
throw new RuntimeException(
"Error accepting client connection", e);
}
this.newclient(clientSocket);
}
Log.d(TAG, name + " Stopped.");
while(! isStopped()){
Socket clientSocket = null;
try {
clientSocket = this.serverSocket.accept();
} catch (IOException e) {
if(isStopped()) {
Log.d(TAG,"Stopped.");
return;
}
throw new RuntimeException(
"Error accepting client connection", e);
}
this.newclient(clientSocket);
}
Log.d(TAG,name + " Stopped.");
}

private synchronized boolean isStopped() {
return this.isStopped;
}

public synchronized void stop(){
this.isStopped = true;
try {
this.serverSocket.close();
} catch (IOException e) {
throw new RuntimeException("Error closing server", e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,42 @@
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import static android.content.res.Configuration.KEYBOARD_12KEY;

public class TerminalFragment extends Fragment implements ServiceConnection, SerialListener {

private enum Connected { False, Pending, True }
private enum Connected {False, Pending, True}

private String deviceAddress;
private SerialService service;
Expand Down Expand Up @@ -67,20 +78,21 @@ public void onDestroy() {
@Override
public void onStart() {
super.onStart();
if(service != null)
if (service != null)
service.attach(this);
else
getActivity().startService(new Intent(getActivity(), SerialService.class)); // prevents service destroy on unbind from recreated activity caused by orientation change
}

@Override
public void onStop() {
if(service != null && !getActivity().isChangingConfigurations())
if (service != null && !getActivity().isChangingConfigurations())
service.detach();
super.onStop();
}

@SuppressWarnings("deprecation") // onAttach(context) was added with API 23. onAttach(activity) works for all API versions
@SuppressWarnings("deprecation")
// onAttach(context) was added with API 23. onAttach(activity) works for all API versions
@Override
public void onAttach(@NonNull Activity activity) {
super.onAttach(activity);
Expand All @@ -89,14 +101,17 @@ public void onAttach(@NonNull Activity activity) {

@Override
public void onDetach() {
try { getActivity().unbindService(this); } catch(Exception ignored) {}
try {
getActivity().unbindService(this);
} catch (Exception ignored) {
}
super.onDetach();
}

@Override
public void onResume() {
super.onResume();
if(initialStart && service != null) {
if (initialStart && service != null) {
initialStart = false;
getActivity().runOnUiThread(this::connect);
}
Expand All @@ -106,7 +121,7 @@ public void onResume() {
public void onServiceConnected(ComponentName name, IBinder binder) {
service = ((SerialService.SerialBinder) binder).getService();
service.attach(this);
if(initialStart && isResumed()) {
if (initialStart && isResumed()) {
initialStart = false;
getActivity().runOnUiThread(this::connect);
}
Expand Down Expand Up @@ -144,6 +159,8 @@ public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
menu.findItem(R.id.hex).setChecked(hexEnabled);
}

protected TcpMultiServer tcpserver;

@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
Expand All @@ -169,6 +186,55 @@ public boolean onOptionsItemSelected(MenuItem item) {
sendText.setHint(hexEnabled ? "HEX mode" : "");
item.setChecked(hexEnabled);
return true;
} else if (id == R.id.tcpserver) {
if (item.isChecked()) {
status("stop server");
if (tcpserver != null)
tcpserver.stop();
if (clientSocket != null) {
try {
clientSocket.close();
} catch (Exception e) {
}
}
item.setChecked(false);
return true;
}
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setTitle("Select Port number");
final EditText input = new EditText(getActivity());
input.setInputType(InputType.TYPE_CLASS_NUMBER);
input.setRawInputType(KEYBOARD_12KEY);
input.setText("3000");
alert.setView(input);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
//Put actions for OK button here
Log.d("port", "cliecked on" + input.getText());
int port;
try {
port = Integer.parseInt(input.getText().toString());
} catch (NumberFormatException nfe) {
port = 3000;
}
status("tcpserver started on port: " + port + "\n");
tcpserver = new TcpMultiServer(port, "ble-server", true) {
@Override
public void newclient(Socket clientSocket) {
new Thread(new BLEserver(clientSocket)).start();
}
};
new Thread(tcpserver).start();
item.setChecked(true);
}
});
alert.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
item.setChecked(false);
}
});
alert.show();
return true;
} else {
return super.onOptionsItemSelected(item);
}
Expand Down Expand Up @@ -196,14 +262,14 @@ private void disconnect() {
}

private void send(String str) {
if(connected != Connected.True) {
if (connected != Connected.True) {
Toast.makeText(getActivity(), "not connected", Toast.LENGTH_SHORT).show();
return;
}
try {
String msg;
byte[] data;
if(hexEnabled) {
if (hexEnabled) {
StringBuilder sb = new StringBuilder();
TextUtil.toHexString(sb, TextUtil.fromHexString(str));
TextUtil.toHexString(sb, newline.getBytes());
Expand All @@ -213,7 +279,7 @@ private void send(String str) {
msg = str;
data = (str + newline).getBytes();
}
SpannableStringBuilder spn = new SpannableStringBuilder(msg+'\n');
SpannableStringBuilder spn = new SpannableStringBuilder(msg + '\n');
spn.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorSendText)), 0, spn.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
receiveText.append(spn);
service.write(data);
Expand All @@ -223,11 +289,17 @@ private void send(String str) {
}

private void receive(byte[] data) {
if(hexEnabled) {
if (tcpoutput != null) {
try {
tcpoutput.write(data);
} catch (IOException e) {
e.printStackTrace();
}
} else if (hexEnabled) {
receiveText.append(TextUtil.toHexString(data) + '\n');
} else {
String msg = new String(data);
if(newline.equals(TextUtil.newline_crlf) && msg.length() > 0) {
if (newline.equals(TextUtil.newline_crlf) && msg.length() > 0) {
// don't show CR as ^M if directly before LF
msg = msg.replace(TextUtil.newline_crlf, TextUtil.newline_lf);
// special handling if CR and LF come in separate fragments
Expand All @@ -243,7 +315,7 @@ private void receive(byte[] data) {
}

private void status(String str) {
SpannableStringBuilder spn = new SpannableStringBuilder(str+'\n');
SpannableStringBuilder spn = new SpannableStringBuilder(str + '\n');
spn.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorStatusText)), 0, spn.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
receiveText.append(spn);
}
Expand Down Expand Up @@ -274,4 +346,43 @@ public void onSerialIoError(Exception e) {
disconnect();
}

protected Socket clientSocket = null;

protected OutputStream tcpoutput = null;

private class BLEserver implements Runnable {
protected InputStream input;

public BLEserver(Socket clientSocketin) {
status("tcpserver: client connected");
try {
this.input = clientSocketin.getInputStream();
tcpoutput = clientSocketin.getOutputStream();
clientSocket = clientSocketin;
} catch (IOException e) {
e.printStackTrace();
}
}

public void run() {
try {
byte inputdata[] = new byte[160];
while (42 == 42) {
int len;

if ((len = input.read(inputdata)) < 0) {
tcpoutput = null;
break;
}
byte[] result = new byte[len];
System.arraycopy(inputdata, 0, result, 0, len);
service.write(result);
}
} catch (IOException e) {
tcpoutput = null;
e.printStackTrace();
}
status("tcpserver: client disconnected");
}
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/menu/menu_terminal.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@
android:title="HEX Mode"
android:checkable="true"
app:showAsAction="never" />
<item
android:id="@+id/tcpserver"
android:checkable="true"
android:checked="false"
android:title="Server"
app:showAsAction="never" />
</menu>