<!-- The following code snippet demonstrates how to declare Bluetooth-related permissions
in your app if it targets Android 12 or higher: -->
<!-- Needed only if your app looks for Bluetooth devices.
If your app doesn't use Bluetooth scan results to derive physical
location information, you can strongly assert that your app
doesn't derive physical location. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- Needed only if your app communicates with already-paired Bluetooth devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<string name="menu_first">1. Grant Permission
vv_tvOut1 vv_tvOut2
protected void onCreate(Bundle savedInstanceState) {
...
// Need grant permission once per install
cpf_checkBTPermissions();
...
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_first: cpf_requestBTPermissions();
...
private void cpf_checkBTPermissions() {
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) {
binding.vvTvOut1.setText("BLUETOOTH_SCAN already granted.\n");
}
else {
binding.vvTvOut1.setText("BLUETOOTH_SCAN NOT granted.\n");
}
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) {
binding.vvTvOut2.setText("BLUETOOTH_CONNECT NOT granted.\n");
}
else {
binding.vvTvOut2.setText("BLUETOOTH_CONNECT already granted.\n");
}
}
// https://www.geeksforgeeks.org/android-how-to-request-permissions-in-android-application/
private void cpf_requestBTPermissions() {
// We can give any value but unique for each permission.
final int BLUETOOTH_SCAN_CODE = 100;
final int BLUETOOTH_CONNECT_CODE = 101;
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.BLUETOOTH_SCAN},
BLUETOOTH_SCAN_CODE);
}
else {
Toast.makeText(MainActivity.this,
"BLUETOOTH_SCAN already granted", Toast.LENGTH_SHORT) .show();
}
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.BLUETOOTH_CONNECT},
BLUETOOTH_CONNECT_CODE);
}
else {
Toast.makeText(MainActivity.this,
"BLUETOOTH_CONNECT already granted", Toast.LENGTH_SHORT) .show();
}
}
<string name="menu_second">2. Search BT List <string name="menu_third">3. Connect to EV3
...
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
// BT Variables
private final String CV_ROBOTNAME = "EV3A";
private BluetoothAdapter cv_btInterface = null;
private Set<BluetoothDevice> cv_pairedDevices = null;
private BluetoothDevice cv_btDevice = null;
private BluetoothSocket cv_btSocket = null;
...
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_first: cpf_requestBTPermissions();
return true;
case R.id.menu_second: cv_btDevice = cpf_locateInPairedBTList(CV_ROBOTNAME);
return true;
case R.id.menu_third: cpf_connectToEV3(cv_btDevice);
return true;
default:
return super.onOptionsItemSelected(menuItem);
}
}
// Modify from chap14, pp390 findRobot()
private BluetoothDevice cpf_locateInPairedBTList(String name) {
BluetoothDevice lv_bd = null;
try {
cv_btInterface = BluetoothAdapter.getDefaultAdapter();
cv_pairedDevices = cv_btInterface.getBondedDevices();
Iterator<BluetoothDevice> lv_it = cv_pairedDevices.iterator();
while (lv_it.hasNext()) {
lv_bd = lv_it.next();
if (lv_bd.getName().equalsIgnoreCase(name)) {
binding.vvTvOut1.setText(name + " is in paired list");
return lv_bd;
}
}
binding.vvTvOut1.setText(name + " is NOT in paired list");
}
catch (Exception e) {
binding.vvTvOut1.setText("Failed in findRobot() " + e.getMessage());
}
return null;
}
// Modify frmo chap14, pp391 connectToRobot()
private void cpf_connectToEV3(BluetoothDevice bd) {
try {
cv_btSocket = bd.createRfcommSocketToServiceRecord
(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
cv_btSocket.connect();
binding.vvTvOut2.setText("Connect to " + bd.getName() + " at " + bd.getAddress());
}
catch (Exception e) {
binding.vvTvOut2.setText("Error interacting with remote device [" +
e.getMessage() + "]");
}
}
}
...
import java.io.InputStream;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity {
//// HERE
// Data stream to/from NXT bluetooth
private InputStream cv_is = null;
private OutputStream cv_os = null;
...
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
...
case R.id.menu_fourth: cpf_EV3MoveMotor();
return true;
case R.id.menu_fifth: cpf_EV3PlayTone();
return true;
case R.id.menu_sixth: cpf_disconnFromEV3(cv_btDevice);
return true;
// Modify frmo chap14, pp391 connectToRobot()
private void cpf_connectToEV3(BluetoothDevice bd) {
try {
cv_btSocket = bd.createRfcommSocketToServiceRecord
(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
cv_btSocket.connect();
//// HERE
cv_is = cv_btSocket.getInputStream();
cv_os = cv_btSocket.getOutputStream();
...
private void cpf_disconnFromEV3(BluetoothDevice bd) {
try {
cv_btSocket.close();
cv_is.close();
cv_os.close();
binding.vvTvOut2.setText(bd.getName() + " is disconnect " );
} catch (Exception e) {
binding.vvTvOut2.setText("Error in disconnect -> " + e.getMessage());
}
}
// Communication Developer Kit Page 27
// 4.2.2 Start motor B & C forward at power 50 for 3 rotation and braking at destination
private void cpf_EV3MoveMotor() {
try {
byte[] buffer = new byte[20]; // 0x12 command length
buffer[0] = (byte) (20-2);
buffer[1] = 0;
buffer[2] = 34;
buffer[3] = 12;
buffer[4] = (byte) 0x80;
buffer[5] = 0;
buffer[6] = 0;
buffer[7] = (byte) 0xae;
buffer[8] = 0;
buffer[9] = (byte) 0x06;
buffer[10] = (byte) 0x81;
buffer[11] = (byte) 0x32;
buffer[12] = 0;
buffer[13] = (byte) 0x82;
buffer[14] = (byte) 0x84;
buffer[15] = (byte) 0x03;
buffer[16] = (byte) 0x82;
buffer[17] = (byte) 0xB4;
buffer[18] = (byte) 0x00;
buffer[19] = 1;
cv_os.write(buffer);
cv_os.flush();
}
catch (Exception e) {
binding.vvTvOut1.setText("Error in MoveForward(" + e.getMessage() + ")");
}
}
// 4.2.5 Play a 1Kz tone at level 2 for 1 sec.
private void cpf_EV3PlayTone() {
...
}
a) final bytes
1200xxxx800000AE000681320082840382B40001
bbbbmmmmtthhhhcccccccccccccccccccccccccc
b) command
opOUTPUT_STEP_SPEED Opcode
LC0(LAYER_0) Layer 0 – encoded as single byte constant
LC0(MOTOR_A + MOTOR_B) Motor B & C (motor list) encoded as single byte constant
LC1(SPEED_50) Speed 50% encoded as one constant byte to follow
LC0(0) No STEP1 i.e. full speed from beginning – encoded as single byte constant
LC2(900) STEP2 for 2.5 rotation (900 degrees) – encodes as two bytes to follow.
LC2(180) STEP3 for 0.5 rotation (180 degrees) for better precision at destination – encoded as two bytes to follow.
LC0(BRAKE) Brake (1) – encoded as single byte constant.
c) command value
0xae opOUTPUT_STEP_SPEED
0x00 LC0(LAYER_0)
0x06 LC0(MOTOR_A + MOTOR_B) motor DCBA in binary (0110)
0x32 LC1(SPEED_50) (32)_{16}=(50)_{10}
0x00 LC0(0)
0x0384 LC2(900) (0x384)_{16}=(900)_{10}
0x00b4 LC2(180) (0xb4)_{16}=(180)_{10}
0x01 LC0(BRAKE)
d) command value format (13 bytes)
0xae opOUTPUT_STEP_SPEED
0x00 LC0(LAYER_0)
0x06 LC0(MOTOR_A + MOTOR_B) motor DCBA in binary (0110)
* 0x8132 LC1(SPEED_50) (32)_{16}=(50)_{10} 81->%
0x00 LC0(0)
** 0x828403 LC2(900) (0x384)_{16}=(900)_{10} 82->val, little Endian
** 0x82B400 LC2(180) (0xb4)_{16}=(180)_{10}
0x01 LC0(BRAKE)
e) header (16 bytes)
0x80 direct command, no reply
0x0000 Reservation (allocation) of global and local variables
0xae opOUTPUT_STEP_SPEED
0x00 LC0(LAYER_0)
0x06 LC0(MOTOR_A + MOTOR_B) motor DCBA in binary (0110)
* 0x8132 LC1(SPEED_50) (32)_{16}=(50)_{10} 81->%
0x00 LC0(0)
** 0x828403 LC2(900) (0x384)_{16}=(900)_{10} 82->val, little Endian
** 0x82B400 LC2(180) (0xb4)_{16}=(180)_{10}
0x01 LC0(BRAKE)
e) header (18 bytes)
0xxxxx Message counter, Little Endian
0x80 direct command, no reply
0x0000 Reservation (allocation) of global and local variables
0xae opOUTPUT_STEP_SPEED
0x00 LC0(LAYER_0)
0x06 LC0(MOTOR_A + MOTOR_B) motor DCBA in binary (0110)
* 0x8132 LC1(SPEED_50) (32)_{16}=(50)_{10} 81->%
0x00 LC0(0)
** 0x828403 LC2(900) (0x384)_{16}=(900)_{10} 82->val, little Endian
** 0x82B400 LC2(180) (0xb4)_{16}=(180)_{10}
0x01 LC0(BRAKE)
f) complte command to send
0x1200 command size (18), hex (0x0012), Little Endian
0xxxxx Message counter, Little Endian
0x80 direct command, no reply
0x0000 Reservation (allocation) of global and local variables
0xae opOUTPUT_STEP_SPEED
0x00 LC0(LAYER_0)
0x06 LC0(MOTOR_A + MOTOR_B) motor DCBA in binary (0110)
* 0x8132 LC1(SPEED_50) (32)_{16}=(50)_{10} 81->%
0x00 LC0(0)
** 0x828403 LC2(900) (0x384)_{16}=(900)_{10} 82->val, Little Endian
** 0x82B400 LC2(180) (0xb4)_{16}=(180)_{10}
0x01 LC0(BRAKE)
1200xxxx800000AE000681320082840382B40001
0F00xxxx8000009401810282E80382E803 0x94 opSOUND Opcode sound related 0x01 LC0(TONE) Command (TONE) encoded as single byte constant 0x02 LC1(2) Sound-level 2 encoded as one constant byte to follow 0x03e8 LC2(1000) Frequency 1000 Hz. encoded as two constant bytes to follow 0x03e8 LC2(1000) Duration 1000 mS. encoded as two constant bytes to follow 0x0F00 0xxxxx 0x80 0x0000 0x94 opSOUND 0x01 LC0(TONE) 0x8102 LC1(2) 0x82E803 LC2(1000) 0x82E803 LC2(1000)
public class MyRecyclerViewData {
private String mv_model;
private String mv_os;
private int mv_price;
public MyRecyclerViewData(){
}
public MyRecyclerViewData(String model, String os, int price){
mv_model = model;
mv_os = os;
mv_price = price;
}
public String getModel(){
return this.mv_model;
}
public String getOS(){
return this.mv_os;
}
public int getPrice(){
return this.mv_price;
}
public void setModel(String model){
mv_model = model;
}
public void setOS(String os){
mv_os = os;
}
public void setPrice(int price){
mv_price = price;
}
}
...
ArrayList<MyRecyclerViewData> lv_data;
...
String[] lv_Model = {"iPad","Xoom","Playbook","TouchPad","Surface"};
String[] lv_OS = {"iOS","Android","BlackBerry","WebOS","Windows"};
int[] lv_Price = {499,799,499,499,449};
lv_data = new ArrayList<>();
for (int i=0; i < lv_Model.length; i++ ) {
lv_data.add(new MyRecyclerViewData(lv_Model[i], lv_OS[i], lv_Price[i]));
}
lv_adapter = new MyRecyclerViewAdapter(lv_data);
...
lv_data.add(1, new MyRecyclerViewData("New Model", "New OS", 123));
...
lv_data.remove(2);
...
private final ArrayList&MyRecyclerViewData> mv_data;
...
public MyRecyclerViewAdapter(ArrayList<MyRecyclerViewData> data) {
mv_data = data;
}
...
public void onBindViewHolder(MyRecyclerViewAdapter.MyViewHolder holder, int position) {
holder.cv_tvModel.setText(mv_data.get(position).getModel());
holder.cv_tvOS.setText(mv_data.get(position).getOS());
holder.cv_tvPrice.setText("$"+mv_data.get(position).getPrice());
...
return mv_data.size();
...
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView. ##########
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_marginVertical="16dp"
android:layout_marginHorizontal="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="4dp">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Medium Text"
android:id="@+id/vv_tvModel"
android:layout_marginLeft="25dp"
android:layout_marginStart="25dp"
android:layout_alignTop="@+id/vv_tvPrice"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Large Text"
android:id="@+id/vv_tvPrice"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:background="@android:color/holo_green_light"
android:layout_margin="10dp"
android:padding="15dp"
android:textColor="@android:color/background_light" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="Small Text"
android:id="@+id/vv_tvOS"
android:layout_alignBottom="@+id/vv_tvPrice"
android:layout_alignLeft="@+id/vv_tvModel"
android:layout_alignStart="@+id/vv_tvModel" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/vv_tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rc_strTitle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.022" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout ############
android:id="@+id/vv_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/vv_rvList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/vv_tvTitle" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/vv_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
SwipeRefreshLayout cv_refresh;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
cv_refresh = (SwipeRefreshLayout) findViewById(R.id.vv_refresh);
cv_refresh.setOnRefreshListener(this);
cv_refresh.setColorSchemeColors(Color.GREEN);
}
@Override
public void onRefresh() {
cv_refresh.setRefreshing(true);
for (int i=0; i < lv_data.size(); i++) {
lv_data.get(i).setPrice((int) Math.ceil(Math.random()*500+1000));
}
lv_adapter.notifyDataSetChanged();
// This line is important as it explicitly refreshes only once
// If "true" it implicitly refreshes forever
cv_refresh.setRefreshing(false);
}
}