initial commit
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Bluetooth permissions for Android 10 (API 29) -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- Bluetooth permissions for Android 12+ (API 31+) -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
||||
android:usesPermissionFlags="neverForLocation" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<!-- Internet for voice recognition -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.StageControl"
|
||||
android:screenOrientation="portrait">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:keepScreenOn="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".DeviceListActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.stage.control;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class DeepSeekHelper {
|
||||
private static final String TAG = "DeepSeekHelper";
|
||||
// Replace with your actual DeepSeek API Key
|
||||
private static final String API_KEY = "sk-09319d4513b34ef78be61b8863f1c6a5";
|
||||
private static final String API_URL = "https://api.deepseek.com/chat/completions";
|
||||
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
private final Gson gson = new Gson();
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
public interface DeepSeekCallback {
|
||||
void onResponse(String command);
|
||||
void onError(Throwable t);
|
||||
}
|
||||
|
||||
public void processVoiceCommand(String voiceText, DeepSeekCallback callback) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
// Constructing the JSON body according to the official DeepSeek format
|
||||
JsonObject jsonRequest = new JsonObject();
|
||||
jsonRequest.addProperty("model", "deepseek-chat");
|
||||
|
||||
JsonArray messages = new JsonArray();
|
||||
|
||||
JsonObject systemMessage = new JsonObject();
|
||||
systemMessage.addProperty("role", "system");
|
||||
systemMessage.addProperty("content", "You are a stage control mapper. Map the user's input to ONE of these characters ONLY: '1', '2', '3', 'S', 'H'. " +
|
||||
"'1': Action 1, '2': Action 2, '3': Action 3, 'S': Stop, 'H': Home. Return ONLY the character. If no match, return 'UNKNOWN'.");
|
||||
messages.add(systemMessage);
|
||||
|
||||
JsonObject userMessage = new JsonObject();
|
||||
userMessage.addProperty("role", "user");
|
||||
userMessage.addProperty("content", voiceText);
|
||||
messages.add(userMessage);
|
||||
|
||||
jsonRequest.add("messages", messages);
|
||||
jsonRequest.addProperty("stream", false);
|
||||
|
||||
// Using official format for RequestBody and Headers
|
||||
MediaType mediaType = MediaType.parse("application/json");
|
||||
RequestBody body = RequestBody.create(jsonRequest.toString(), mediaType);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(API_URL)
|
||||
.post(body)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("Authorization", "Bearer " + API_KEY)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
callback.onError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "No error body";
|
||||
Log.e(TAG, "DeepSeek Error " + response.code() + ": " + errorBody);
|
||||
callback.onError(new IOException("Error " + response.code() + ": " + errorBody));
|
||||
return;
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
|
||||
|
||||
// Parsing according to official response format
|
||||
String content = jsonResponse.getAsJsonArray("choices")
|
||||
.get(0).getAsJsonObject()
|
||||
.getAsJsonObject("message")
|
||||
.get("content").getAsString().trim().toUpperCase();
|
||||
|
||||
Log.d(TAG, "DeepSeek Raw: " + content);
|
||||
|
||||
String command = "UNKNOWN";
|
||||
if (content.contains("1")) command = "1";
|
||||
else if (content.contains("2")) command = "2";
|
||||
else if (content.contains("3")) command = "3";
|
||||
else if (content.contains("S")) command = "S";
|
||||
else if (content.contains("H")) command = "H";
|
||||
|
||||
callback.onResponse(command);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
callback.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.stage.control;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class DeviceListActivity extends AppCompatActivity {
|
||||
|
||||
private ListView listView;
|
||||
private List<BluetoothDevice> deviceList = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_device_list);
|
||||
|
||||
listView = findViewById(R.id.lv_devices);
|
||||
loadPairedDevices();
|
||||
}
|
||||
|
||||
private void loadPairedDevices() {
|
||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (adapter == null) {
|
||||
Toast.makeText(this, "블루투스를 사용할 수 없습니다.", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, "블루투스 권한이 필요합니다.", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Set<BluetoothDevice> pairedDevices = adapter.getBondedDevices();
|
||||
List<String> displayNames = new ArrayList<>();
|
||||
deviceList.clear();
|
||||
|
||||
if (pairedDevices == null || pairedDevices.isEmpty()) {
|
||||
Toast.makeText(this, "페어링된 장치가 없습니다.\n설정에서 HC-05를 먼저 페어링하세요.", Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
for (BluetoothDevice device : pairedDevices) {
|
||||
deviceList.add(device);
|
||||
String name = device.getName() != null ? device.getName() : "Unknown";
|
||||
displayNames.add(name + "\n" + device.getAddress());
|
||||
}
|
||||
|
||||
ArrayAdapter<String> listAdapter = new ArrayAdapter<>(
|
||||
this,
|
||||
android.R.layout.simple_list_item_1,
|
||||
displayNames
|
||||
) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, android.view.ViewGroup parent) {
|
||||
View v = super.getView(position, convertView, parent);
|
||||
TextView tv = (TextView) v;
|
||||
tv.setTextSize(20f);
|
||||
tv.setPadding(32, 32, 32, 32);
|
||||
tv.setTextColor(getResources().getColor(R.color.text_primary, null));
|
||||
tv.setBackgroundColor(getResources().getColor(R.color.bg_card, null));
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
listView.setAdapter(listAdapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
BluetoothDevice selected = deviceList.get(position);
|
||||
Intent result = new Intent();
|
||||
result.putExtra("device_address", selected.getAddress());
|
||||
result.putExtra("device_name", selected.getName() != null ? selected.getName() : selected.getAddress());
|
||||
setResult(Activity.RESULT_OK, result);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.stage.control;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.ai.client.generativeai.GenerativeModel;
|
||||
import com.google.ai.client.generativeai.java.GenerativeModelFutures;
|
||||
import com.google.ai.client.generativeai.type.Content;
|
||||
import com.google.ai.client.generativeai.type.GenerateContentResponse;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class GeminiHelper {
|
||||
private static final String TAG = "GeminiHelper";
|
||||
// Ensure this API key is valid from Google AI Studio
|
||||
private static final String API_KEY = "AIzaSyCDiEk5wdKM2qZb988k0DAs7E-kXkeE2Xw";
|
||||
|
||||
private final GenerativeModelFutures model;
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
public interface GeminiResponseCallback {
|
||||
void onResponse(String command);
|
||||
void onError(Throwable t);
|
||||
}
|
||||
|
||||
public GeminiHelper() {
|
||||
// Using gemini-1.5-flash for stability and free tier
|
||||
GenerativeModel gm = new GenerativeModel("gemini-1.5-flash", API_KEY);
|
||||
model = GenerativeModelFutures.from(gm);
|
||||
}
|
||||
|
||||
public void processVoiceCommand(String voiceText, GeminiResponseCallback callback) {
|
||||
// Updated prompt to include Action 3 and clearer mapping
|
||||
String prompt = "Map this voice input to exactly one character: '1', '2', '3', 'S', or 'H'.\n" +
|
||||
"- '1' for Action 1, First, One\n" +
|
||||
"- '2' for Action 2, Second, Two\n" +
|
||||
"- '3' for Action 3, Third, Three\n" +
|
||||
"- 'S' for Stop, Halt, Pause\n" +
|
||||
"- 'H' for Home, Reset, Return\n" +
|
||||
"If no match, reply 'UNKNOWN'.\n" +
|
||||
"Input: \"" + voiceText + "\"\n" +
|
||||
"Reply ONLY with the character.";
|
||||
|
||||
Content content = new Content.Builder()
|
||||
.addText(prompt)
|
||||
.build();
|
||||
|
||||
try {
|
||||
ListenableFuture<GenerateContentResponse> response = model.generateContent(content);
|
||||
|
||||
Futures.addCallback(response, new FutureCallback<GenerateContentResponse>() {
|
||||
@Override
|
||||
public void onSuccess(GenerateContentResponse result) {
|
||||
try {
|
||||
String text = result.getText();
|
||||
if (text != null) {
|
||||
text = text.trim().toUpperCase();
|
||||
String command = "UNKNOWN";
|
||||
if (text.contains("1")) command = "1";
|
||||
else if (text.contains("2")) command = "2";
|
||||
else if (text.contains("3")) command = "3";
|
||||
else if (text.contains("S")) command = "S";
|
||||
else if (text.contains("H")) command = "H";
|
||||
callback.onResponse(command);
|
||||
} else {
|
||||
callback.onResponse("UNKNOWN");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
Log.e(TAG, "Gemini Failure: " + t.toString());
|
||||
callback.onError(t);
|
||||
}
|
||||
}, executor);
|
||||
} catch (Exception e) {
|
||||
callback.onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.stage.control;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.speech.RecognizerIntent;
|
||||
import android.util.Log;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "StageControl";
|
||||
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
private static final int REQUEST_PERMISSIONS = 100;
|
||||
|
||||
private TextView tvStatus, tvVoiceResult, tvSentCommand;
|
||||
private Button btnConnect, btnVoice;
|
||||
|
||||
private BluetoothAdapter bluetoothAdapter;
|
||||
private BluetoothSocket bluetoothSocket;
|
||||
private OutputStream outputStream;
|
||||
private String connectedDeviceName = "";
|
||||
|
||||
private GeminiHelper geminiHelper;
|
||||
|
||||
private enum BtState { NOT_CONNECTED, CONNECTING, CONNECTED }
|
||||
private BtState currentState = BtState.NOT_CONNECTED;
|
||||
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
private ActivityResultLauncher<Intent> devicePickerLauncher, voiceLauncher, enableBtLauncher;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
tvStatus = findViewById(R.id.tv_status);
|
||||
tvVoiceResult = findViewById(R.id.tv_voice_result);
|
||||
tvSentCommand = findViewById(R.id.tv_sent_command);
|
||||
btnConnect = findViewById(R.id.btn_connect);
|
||||
btnVoice = findViewById(R.id.btn_voice);
|
||||
|
||||
btnConnect.setOnClickListener(v -> onConnectClicked());
|
||||
btnVoice.setOnClickListener(v -> onVoiceClicked());
|
||||
|
||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bluetoothAdapter == null) {
|
||||
Toast.makeText(this, "Bluetooth not supported", Toast.LENGTH_LONG).show();
|
||||
btnConnect.setEnabled(false);
|
||||
}
|
||||
|
||||
geminiHelper = new GeminiHelper();
|
||||
|
||||
registerLaunchers();
|
||||
checkPermissions();
|
||||
updateUI(BtState.NOT_CONNECTED);
|
||||
}
|
||||
|
||||
private void registerLaunchers() {
|
||||
devicePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
connectToDevice(result.getData().getStringExtra("device_address"), result.getData().getStringExtra("device_name"));
|
||||
}
|
||||
});
|
||||
|
||||
voiceLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
ArrayList<String> results = result.getData().getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
|
||||
if (results != null && !results.isEmpty()) processVoiceWithAI(results.get(0));
|
||||
} else {
|
||||
tvVoiceResult.setText("Voice recognition failed.");
|
||||
}
|
||||
});
|
||||
|
||||
enableBtLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() != Activity.RESULT_OK) Toast.makeText(this, "Please turn on Bluetooth", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
private void processVoiceWithAI(String voiceText) {
|
||||
tvVoiceResult.setText("Analyzing (Gemini): " + voiceText);
|
||||
geminiHelper.processVoiceCommand(voiceText, new GeminiHelper.GeminiResponseCallback() {
|
||||
@Override
|
||||
public void onResponse(String command) {
|
||||
mainHandler.post(() -> {
|
||||
// tvVoiceResult.setText(voiceText + " -> " + command);
|
||||
tvVoiceResult.setText(voiceText); // 인식한 텍스트 그대로 출력
|
||||
if (!command.equals("UNKNOWN")) sendBluetoothCommand(command);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
mainHandler.post(() -> {
|
||||
String fullError = t.getMessage();
|
||||
// tvVoiceResult.setText("Gemini Error!\n" + fullError);
|
||||
tvVoiceResult.setText(voiceText); // 에러 시에도 인식한 텍스트 그대로 출력
|
||||
Log.e(TAG, "Gemini Error: " + fullError);
|
||||
|
||||
String fallback = mapManual(voiceText);
|
||||
if (fallback != null) {
|
||||
tvVoiceResult.append("\nFallback: " + fallback);
|
||||
sendBluetoothCommand(fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String mapManual(String t) {
|
||||
String s = t.toLowerCase();
|
||||
if (s.contains("push")) return "1";
|
||||
if (s.contains("spin") || s.contains("spend")) return "2";
|
||||
if (s.contains("penalty")) return "3";
|
||||
return null;
|
||||
}
|
||||
|
||||
private void onConnectClicked() {
|
||||
if (currentState == BtState.CONNECTED) { disconnect(); return; }
|
||||
if (bluetoothAdapter == null) return;
|
||||
if (!bluetoothAdapter.isEnabled()) { enableBtLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); return; }
|
||||
devicePickerLauncher.launch(new Intent(this, DeviceListActivity.class));
|
||||
}
|
||||
|
||||
private void onVoiceClicked() {
|
||||
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
|
||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
|
||||
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "en-US");
|
||||
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak command");
|
||||
try {
|
||||
voiceLauncher.launch(intent);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "Speech recognition unavailable", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void connectToDevice(String addr, String name) {
|
||||
updateUI(BtState.CONNECTING);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
||||
mainHandler.post(() -> {
|
||||
Toast.makeText(this, "No Bluetooth Permission", Toast.LENGTH_SHORT).show();
|
||||
updateUI(BtState.NOT_CONNECTED);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
BluetoothDevice dev = bluetoothAdapter.getRemoteDevice(addr);
|
||||
bluetoothSocket = dev.createRfcommSocketToServiceRecord(SPP_UUID);
|
||||
bluetoothAdapter.cancelDiscovery();
|
||||
bluetoothSocket.connect();
|
||||
outputStream = bluetoothSocket.getOutputStream();
|
||||
mainHandler.post(() -> { connectedDeviceName = name; updateUI(BtState.CONNECTED); });
|
||||
} catch (Exception e) {
|
||||
mainHandler.post(() -> {
|
||||
updateUI(BtState.NOT_CONNECTED);
|
||||
Toast.makeText(this, "Connection failed: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void sendBluetoothCommand(String cmd) {
|
||||
if (outputStream == null) return;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
outputStream.write(cmd.getBytes());
|
||||
outputStream.flush();
|
||||
mainHandler.post(() -> tvSentCommand.setText(cmd));
|
||||
} catch (Exception e) {
|
||||
mainHandler.post(() -> {
|
||||
Toast.makeText(this, "Send failed", Toast.LENGTH_SHORT).show();
|
||||
updateUI(BtState.NOT_CONNECTED);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
try { if (outputStream != null) outputStream.close(); if (bluetoothSocket != null) bluetoothSocket.close(); } catch (Exception e) {}
|
||||
updateUI(BtState.NOT_CONNECTED);
|
||||
}
|
||||
|
||||
private void updateUI(BtState s) {
|
||||
currentState = s;
|
||||
switch (s) {
|
||||
case NOT_CONNECTED:
|
||||
tvStatus.setText("⚫ Not Connected");
|
||||
tvStatus.setTextColor(getColor(R.color.status_disconnected));
|
||||
btnConnect.setText("Connect Bluetooth");
|
||||
btnVoice.setEnabled(false);
|
||||
btnVoice.setAlpha(0.4f);
|
||||
break;
|
||||
case CONNECTING:
|
||||
tvStatus.setText("🟡 Connecting...");
|
||||
tvStatus.setTextColor(getColor(R.color.status_connecting));
|
||||
btnConnect.setText("Connecting...");
|
||||
btnConnect.setEnabled(false);
|
||||
break;
|
||||
case CONNECTED:
|
||||
tvStatus.setText("🟢 Connected: " + connectedDeviceName);
|
||||
tvStatus.setTextColor(getColor(R.color.status_connected));
|
||||
btnConnect.setText("Disconnect");
|
||||
btnConnect.setEnabled(true);
|
||||
btnVoice.setEnabled(true);
|
||||
btnVoice.setAlpha(1.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPermissions() {
|
||||
List<String> needed = new ArrayList<>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.BLUETOOTH_CONNECT);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.BLUETOOTH_SCAN);
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.RECORD_AUDIO);
|
||||
if (!needed.isEmpty()) ActivityCompat.requestPermissions(this, needed.toArray(new String[0]), REQUEST_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/bg_card"/>
|
||||
<corners android:radius="12dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M31,63.92V20.33h19.51v43.59h-19.51z M51.44,63.92V20.33h19.51v43.59h-19.51z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/bg_dark">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Select Paired Device"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:padding="24dp"
|
||||
android:background="@color/bg_card"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Please select your HC-05 device"
|
||||
android:textColor="@color/text_label"
|
||||
android:textSize="16sp"
|
||||
android:padding="16dp"/>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/lv_devices"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="1dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_dark"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<!-- ─── BLUETOOTH STATUS ─── -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/card_bg"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:letterSpacing="0.1"
|
||||
android:text="📡 Bluetooth Status"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/text_label"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="⚫ Not Connected"
|
||||
android:textColor="@color/status_disconnected"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ─── VOICE RESULT ─── -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_weight="1.5"
|
||||
android:background="@drawable/card_bg"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:letterSpacing="0.1"
|
||||
android:text="🎙 Recognized commands"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/text_label"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_voice_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="left"
|
||||
android:text="—"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ─── SENT COMMAND ─── -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/card_bg"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:letterSpacing="0.1"
|
||||
android:text="📤 Commands sent"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/text_label"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_sent_command"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="—"
|
||||
android:textColor="@color/accent_green"
|
||||
android:textSize="60sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ─── CONNECT BUTTON ─── -->
|
||||
<Button
|
||||
android:id="@+id/btn_connect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="@color/btn_connect"
|
||||
android:stateListAnimator="@null"
|
||||
android:text="Bluetooth connect"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- ─── VOICE COMMAND BUTTON ─── -->
|
||||
<Button
|
||||
android:id="@+id/btn_voice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="96dp"
|
||||
android:alpha="0.4"
|
||||
android:backgroundTint="@color/btn_voice"
|
||||
android:enabled="false"
|
||||
android:stateListAnimator="@null"
|
||||
android:text="🎤 Voice commands"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0D0D0D</color>
|
||||
<!-- Backgrounds -->
|
||||
<color name="bg_dark">#0D0D0D</color>
|
||||
<color name="bg_card">#1A1A2E</color>
|
||||
|
||||
<!-- Text -->
|
||||
<color name="text_primary">#FFFFFF</color>
|
||||
<color name="text_label">#888888</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
|
||||
<!-- Status colors -->
|
||||
<color name="status_connected">#00E676</color>
|
||||
<color name="status_connecting">#FFD600</color>
|
||||
<color name="status_disconnected">#FF5252</color>
|
||||
|
||||
<!-- Button colors -->
|
||||
<color name="btn_connect">#1565C0</color>
|
||||
<color name="btn_disconnect">#B71C1C</color>
|
||||
<color name="btn_voice">#6A1B9A</color>
|
||||
|
||||
<!-- Accent -->
|
||||
<color name="accent_green">#00E676</color>
|
||||
|
||||
<!-- Misc -->
|
||||
<color name="divider">#333333</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Stage Control</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.StageControl" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/btn_connect</item>
|
||||
<item name="colorPrimaryVariant">@color/bg_card</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="android:windowBackground">@color/bg_dark</item>
|
||||
<item name="android:statusBarColor">@color/bg_card</item>
|
||||
<item name="android:navigationBarColor">@color/bg_dark</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user