Initial commit: BLE relay controller for Atom VoiceS3R
This commit is contained in:
commit
7c9dde172a
15 changed files with 764 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# PlatformIO
|
||||
.pio/
|
||||
.pioenvs/
|
||||
.piolibdeps/
|
||||
.clang_complete
|
||||
.gcc-flags.json
|
||||
>Sconscript
|
||||
>SConscript
|
||||
>*.scons
|
||||
|
||||
# Android/Gradle
|
||||
.gradle/
|
||||
build/
|
||||
local.properties
|
||||
*.iml
|
||||
.idea/
|
||||
*.apk
|
||||
*.aab
|
||||
*.ap_
|
||||
*.dex
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
165
README.md
Normal file
165
README.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Sylvia Touch Relay
|
||||
|
||||
BLE-gestuurde relais-switch voor de M5Stack Atom VoiceS3R (ESP32-S3) met een bijbehorende Android-app. Het relais kan worden bediend via een capacitieve touch-sensor (open-drain) óf via Bluetooth Low Energy vanaf een Android-telefoon.
|
||||
|
||||
## Hardware
|
||||
|
||||
| Component | Details |
|
||||
|---|---|
|
||||
| Board | M5Stack Atom VoiceS3R (SKU: C126-ECHO) |
|
||||
| SoC | ESP32-S3-PICO-1-N8R8 (8MB Flash, 8MB PSRAM) |
|
||||
| Touch sensor | Open-drain sensor op **G38** (3.3V, interne pullup) |
|
||||
| Relais / LED | Op **G1** (3.3V GPIO output) |
|
||||
| Status LED | Op **G46** (knippert 3x bij opstarten) |
|
||||
|
||||
### Pinout (Atom VoiceS3R onderste header)
|
||||
|
||||
```
|
||||
G38 G39 3.3V GND 5V G5 G6 G7 G8
|
||||
```
|
||||
|
||||
- **G38** — Touch sensor input (active LOW, interne pullup ingeschakeld)
|
||||
- **G1** — Relais output (HIGH = aan, LOW = uit)
|
||||
- **G46** — Status LED (3x knipperen bij opstarten, daarna uit)
|
||||
|
||||
### Aansluiting
|
||||
|
||||
```
|
||||
Sensor GND → GND
|
||||
Sensor OUT → G38
|
||||
Sensor VCC → 3.3V
|
||||
|
||||
Relais + → G1
|
||||
Relais - → GND
|
||||
```
|
||||
|
||||
> **Let op:** De ESP32-S3 interne pullup is ~45kΩ. Bij lange draden of elektrische ruis een externe 4.7kΩ–10kΩ pullup naar 3.3V plaatsen.
|
||||
|
||||
## Firmware
|
||||
|
||||
De firmware is geschreven in C++ met behulp van PlatformIO en de Arduino-framework. BLE wordt verzorgd door de ESP32 BLE library.
|
||||
|
||||
### Logica
|
||||
|
||||
Het relais gaat aan als **één van beide** bronnen actief is:
|
||||
|
||||
| Bron | Voorwaarde | Resultaat |
|
||||
|---|---|---|
|
||||
| Touch sensor | G38 = LOW (aanraken) | Relais aan |
|
||||
| BLE command | App stuurt `0x01` | Relais aan |
|
||||
| Geen van beide | G38 = HIGH + geen BLE command | Relais uit |
|
||||
|
||||
De touch sensor en BLE werken onafhankelijk. Als de sensor losgelaten wordt én de BLE-knop niet ingedrukt is, gaat het relais uit.
|
||||
|
||||
### BLE GATT
|
||||
|
||||
| Eigenschap | UUID |
|
||||
|---|---|
|
||||
| Service | `4fafc201-1fb5-459e-8fcc-c5c9c331914b` |
|
||||
| Characteristic | `beb5483e-36e1-4688-b7f5-ea07361f26d8` |
|
||||
|
||||
De characteristic ondersteunt `WRITE` en `NOTIFY`. Schrijf `0x01` om het relais aan te zetten, `0x00` om uit te zetten.
|
||||
|
||||
### Serial monitor
|
||||
|
||||
Baudrate: **115200** (USB CDC). De firmware stuurt heartbeat-statussen elke 5 seconden en logt statuswijzigingen:
|
||||
|
||||
```
|
||||
=== Atom VoiceS3R BLE Relay ===
|
||||
LED blink done, starting BLE...
|
||||
BLE: advertising started as 'AtomS3R-Relay'
|
||||
Setup done.
|
||||
[heartbeat] relay=OFF touch=no ble=disconnected
|
||||
BLE: device connected
|
||||
Relay ON (touch=YES, ble=connected)
|
||||
Relay OFF (touch=no, ble=connected)
|
||||
```
|
||||
|
||||
### Uploaden
|
||||
|
||||
> **Download-mode:** Houd de reset-knop ~2 seconden ingedrukt tot de groene LED aangaat, laat dan los. Het board is nu in download-mode.
|
||||
|
||||
```bash
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
Na het uploaden treedt er een harde reset op. Als het board in download-mode blijft, USB eruit/erin of kort op reset tikken om in run-mode te komen.
|
||||
|
||||
## Android App
|
||||
|
||||
### Features
|
||||
|
||||
- **Auto-connect:** De app scant automatisch bij opstarten naar het BLE-apparaat "AtomS3R-Relay"
|
||||
- **Auto-reconnect:** Bij wegvallen van de verbinding wordt na 2 seconden opnieuw gescant
|
||||
- **Hold-to-activate:** Houd de knop ingedrukt voor relais aan, loslaten voor relais uit (zelfde gedrag als de sensor)
|
||||
- **Permissies:** Vraagt BLUETOOTH_SCAN, BLUETOOTH_CONNECT en ACCESS_FINE_LOCATION aan (Android 12+)
|
||||
|
||||
### Bouwen
|
||||
|
||||
Vereisten:
|
||||
- Android SDK (API 34+)
|
||||
- Java 21
|
||||
- Gradle 8.5
|
||||
|
||||
```bash
|
||||
cd android
|
||||
JAVA_HOME=/pad/naar/jdk21 ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
De APK staat na build in `android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||
|
||||
### Installeren
|
||||
|
||||
```bash
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
Of via draadloze foutopsporing:
|
||||
|
||||
```bash
|
||||
adb connect <ip>:<poort>
|
||||
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
### UUID's
|
||||
|
||||
De app gebruikt dezelfde UUID's als de firmware:
|
||||
|
||||
```kotlin
|
||||
private val SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
|
||||
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
|
||||
```
|
||||
|
||||
## Projectstructuur
|
||||
|
||||
```
|
||||
sylvia-touch-relay/
|
||||
├── platformio.ini # PlatformIO configuratie (ESP32-S3)
|
||||
├── src/
|
||||
│ └── main.cpp # Firmware (BLE + sensor + relais)
|
||||
├── android/
|
||||
│ ├── build.gradle # Root Gradle config
|
||||
│ ├── settings.gradle
|
||||
│ ├── gradle.properties
|
||||
│ ├── gradlew # Gradle wrapper
|
||||
│ ├── gradle/wrapper/
|
||||
│ │ └── gradle-wrapper.properties
|
||||
│ └── app/
|
||||
│ ├── build.gradle # App module config
|
||||
│ └── src/main/
|
||||
│ ├── AndroidManifest.xml
|
||||
│ ├── java/com/eddy/atomrelay/
|
||||
│ │ └── MainActivity.kt
|
||||
│ └── res/
|
||||
│ ├── layout/activity_main.xml
|
||||
│ └── values/strings.xml
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Technische notities
|
||||
|
||||
- De Atom VoiceS3R gebruikt de ES8311 audio codec op G45/G0/G48/G4/G3/G17/G11, IR TX op G47, en de knop op G41. Deze pinnen zijn niet beschikbaar voor eigen gebruik.
|
||||
- G1 en G2 zitten op de HY2.0-4P Grove poort. G5-G8, G38, G39 zitten op de onderste expantie-header.
|
||||
- De firmware gebruikt geen M5Unified library — alleen Arduino core + ESP32 BLE. Dit maakt de serial output betrouwbaarder via USB CDC.
|
||||
- `ARDUINO_USB_CDC_ON_BOOT=1` is ingeschakeld voor virtuele serial via USB.
|
||||
37
android/app/build.gradle
Normal file
37
android/app/build.gradle
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.eddy.atomrelay'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.eddy.atomrelay"
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
}
|
||||
28
android/app/src/main/AndroidManifest.xml
Normal file
28
android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="Atom Relay"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.AppCompat.Light">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
232
android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt
Normal file
232
android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package com.eddy.atomrelay
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import java.util.UUID
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AtomRelay"
|
||||
private val SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
|
||||
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
|
||||
private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||
private const val DEVICE_NAME = "AtomS3R-Relay"
|
||||
}
|
||||
|
||||
private var bluetoothGatt: BluetoothGatt? = null
|
||||
private var relayCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var isScanning = false
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private lateinit var statusText: TextView
|
||||
private lateinit var relayButton: Button
|
||||
private lateinit var scanButton: Button
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
val device = result.device
|
||||
val name = device?.name ?: ""
|
||||
Log.d(TAG, "Found device: $name (${device?.address})")
|
||||
if (name == DEVICE_NAME) {
|
||||
stopScan()
|
||||
connectToDevice(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val gattCallback = object : BluetoothGattCallback() {
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
Log.d(TAG, "GATT connected, discovering services...")
|
||||
gatt.discoverServices()
|
||||
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
||||
Log.d(TAG, "GATT disconnected")
|
||||
runOnUiThread {
|
||||
statusText.text = "Reconnecting..."
|
||||
relayButton.isEnabled = false
|
||||
}
|
||||
gatt.close()
|
||||
bluetoothGatt = null
|
||||
relayCharacteristic = null
|
||||
// Auto-reconnect after 2 seconds
|
||||
handler.postDelayed({ startScan() }, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||
val service = gatt.getService(SERVICE_UUID)
|
||||
if (service != null) {
|
||||
relayCharacteristic = service.getCharacteristic(CHARACTERISTIC_UUID)
|
||||
if (relayCharacteristic != null) {
|
||||
// Enable notifications
|
||||
gatt.setCharacteristicNotification(relayCharacteristic, true)
|
||||
val descriptor = relayCharacteristic!!.getDescriptor(CCCD_UUID)
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
gatt.writeDescriptor(descriptor)
|
||||
Log.d(TAG, "Services discovered, relay characteristic found")
|
||||
runOnUiThread {
|
||||
statusText.text = "Connected"
|
||||
relayButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Service not found")
|
||||
runOnUiThread { statusText.text = "Service not found" }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicWrite(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
status: Int
|
||||
) {
|
||||
Log.d(TAG, "Write status: $status")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
statusText = findViewById(R.id.statusText)
|
||||
relayButton = findViewById(R.id.relayButton)
|
||||
scanButton = findViewById(R.id.scanButton)
|
||||
|
||||
scanButton.setOnClickListener { startScan() }
|
||||
setupRelayButton()
|
||||
|
||||
// Auto-connect on app start
|
||||
handler.postDelayed({ startScan() }, 500)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == 1 && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
||||
startScan()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRelayButton() {
|
||||
relayButton.setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
sendRelayCommand(true)
|
||||
relayButton.text = "ON"
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
sendRelayCommand(false)
|
||||
relayButton.text = "HOLD"
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startScan() {
|
||||
if (isScanning) return
|
||||
if (!hasPermissions()) {
|
||||
requestPermissions()
|
||||
return
|
||||
}
|
||||
|
||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||
if (adapter == null || !adapter.isEnabled) {
|
||||
statusText.text = "Bluetooth not enabled"
|
||||
return
|
||||
}
|
||||
|
||||
isScanning = true
|
||||
statusText.text = "Scanning..."
|
||||
scanButton.isEnabled = false
|
||||
Log.d(TAG, "Starting BLE scan for $DEVICE_NAME")
|
||||
|
||||
adapter.bluetoothLeScanner.startScan(scanCallback)
|
||||
|
||||
// Stop scan after 10 seconds
|
||||
handler.postDelayed({
|
||||
if (isScanning) stopScan()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun stopScan() {
|
||||
if (!isScanning) return
|
||||
isScanning = false
|
||||
scanButton.isEnabled = true
|
||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
|
||||
if (statusText.text == "Scanning...") {
|
||||
statusText.text = "Device not found"
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun connectToDevice(device: BluetoothDevice) {
|
||||
runOnUiThread { statusText.text = "Connecting..." }
|
||||
Log.d(TAG, "Connecting to ${device.address}")
|
||||
bluetoothGatt = device.connectGatt(this, false, gattCallback)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun sendRelayCommand(on: Boolean) {
|
||||
val char = relayCharacteristic ?: return
|
||||
val gatt = bluetoothGatt ?: return
|
||||
char.value = byteArrayOf(if (on) 0x01 else 0x00)
|
||||
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
||||
gatt.writeCharacteristic(char)
|
||||
Log.d(TAG, "Sent relay ${if (on) "ON" else "OFF"}")
|
||||
}
|
||||
|
||||
private fun getRequiredPermissions(): Array<String> {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
arrayOf(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
} else {
|
||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasPermissions(): Boolean {
|
||||
return getRequiredPermissions().all {
|
||||
ActivityCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
ActivityCompat.requestPermissions(this, getRequiredPermissions(), 1)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopScan()
|
||||
bluetoothGatt?.close()
|
||||
}
|
||||
}
|
||||
32
android/app/src/main/res/layout/activity_main.xml
Normal file
32
android/app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?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:gravity="center"
|
||||
android:padding="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Disconnected"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginBottom="48dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/relayButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:text="HOLD"
|
||||
android:textSize="24sp"
|
||||
android:enabled="false"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/scanButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Scan & Connect"
|
||||
android:layout_marginTop="48dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
4
android/app/src/main/res/values/strings.xml
Normal file
4
android/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Atom Relay</string>
|
||||
</resources>
|
||||
4
android/build.gradle
Normal file
4
android/build.gradle
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
plugins {
|
||||
id 'com.android.application' version '8.2.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
|
||||
}
|
||||
4
android/gradle.properties
Normal file
4
android/gradle.properties
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
69
android/gradlew
vendored
Executable file
69
android/gradlew
vendored
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Gradle start up script
|
||||
#
|
||||
|
||||
DIRNAME=$(dirname "$0")
|
||||
APP_HOME=$DIRNAME
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in
|
||||
'' | soft) :;;
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$DEFAULT_JVM_OPTS \
|
||||
$JAVA_OPTS \
|
||||
$GRADLE_OPTS \
|
||||
"-Dorg.gradle.appname=${0##*/}" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
16
android/settings.gradle
Normal file
16
android/settings.gradle
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "AtomRelay"
|
||||
include ':app'
|
||||
15
platformio.ini
Normal file
15
platformio.ini
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[env:m5stack-atoms3r]
|
||||
platform = espressif32@6.7.0
|
||||
board = esp32-s3-devkitc-1
|
||||
framework = arduino
|
||||
board_build.arduino.memory_type = qio_opi
|
||||
build_flags =
|
||||
-DESP32S3
|
||||
-DBOARD_HAS_PSRAM
|
||||
-mfix-esp32-psram-cache-issue
|
||||
-DCORE_DEBUG_LEVEL=5
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DARDUINO_USB_MODE=1
|
||||
lib_deps =
|
||||
upload_port = /dev/ttyACM0
|
||||
upload_speed = 115200
|
||||
122
src/main.cpp
Normal file
122
src/main.cpp
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#include <Arduino.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <BLE2902.h>
|
||||
|
||||
#define SENSOR_PIN 38
|
||||
#define RELAY_PIN 1
|
||||
#define LED_PIN 46
|
||||
|
||||
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
||||
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
|
||||
|
||||
BLECharacteristic *pCharacteristic;
|
||||
bool deviceConnected = false;
|
||||
bool bleCommand = false;
|
||||
bool relayState = false;
|
||||
|
||||
class MyServerCallbacks : public BLEServerCallbacks {
|
||||
void onConnect(BLEServer *pServer) override {
|
||||
deviceConnected = true;
|
||||
Serial.println("BLE: device connected");
|
||||
}
|
||||
void onDisconnect(BLEServer *pServer) override {
|
||||
deviceConnected = false;
|
||||
bleCommand = false;
|
||||
Serial.println("BLE: device disconnected");
|
||||
BLEDevice::startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks {
|
||||
void onWrite(BLECharacteristic *pChar) override {
|
||||
std::string value = pChar->getValue();
|
||||
if (value.length() > 0) {
|
||||
uint8_t cmd = (uint8_t)value[0];
|
||||
if (cmd == 0x01) {
|
||||
bleCommand = true;
|
||||
Serial.println("BLE: relay ON");
|
||||
} else {
|
||||
bleCommand = false;
|
||||
Serial.println("BLE: relay OFF");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void setup() {
|
||||
delay(2000);
|
||||
Serial.begin(115200);
|
||||
delay(500);
|
||||
Serial.println("=== Atom VoiceS3R BLE Relay ===");
|
||||
|
||||
pinMode(SENSOR_PIN, INPUT_PULLUP);
|
||||
pinMode(RELAY_PIN, OUTPUT);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
|
||||
// Visual confirmation: blink LED 3x before BLE init
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
delay(200);
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
delay(200);
|
||||
}
|
||||
Serial.println("LED blink done, starting BLE...");
|
||||
|
||||
BLEDevice::init("AtomS3R-Relay");
|
||||
BLEDevice::setMTU(23);
|
||||
BLEServer *pServer = BLEDevice::createServer();
|
||||
pServer->setCallbacks(new MyServerCallbacks());
|
||||
|
||||
BLEService *pService = pServer->createService(SERVICE_UUID);
|
||||
pCharacteristic = pService->createCharacteristic(
|
||||
CHARACTERISTIC_UUID,
|
||||
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
|
||||
);
|
||||
pCharacteristic->addDescriptor(new BLE2902());
|
||||
pCharacteristic->setCallbacks(new MyCharacteristicCallbacks());
|
||||
pCharacteristic->setValue("0");
|
||||
pService->start();
|
||||
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID(SERVICE_UUID);
|
||||
pAdvertising->setScanResponse(true);
|
||||
pAdvertising->setMinPreferred(0x06);
|
||||
pAdvertising->setMinPreferred(0x12);
|
||||
BLEDevice::startAdvertising();
|
||||
|
||||
Serial.println("BLE: advertising started as 'AtomS3R-Relay'");
|
||||
Serial.println("Setup done.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int sensorState = digitalRead(SENSOR_PIN);
|
||||
|
||||
bool touchActive = (sensorState == LOW);
|
||||
bool bleActive = (deviceConnected && bleCommand);
|
||||
|
||||
relayState = touchActive || bleActive;
|
||||
|
||||
digitalWrite(RELAY_PIN, relayState ? HIGH : LOW);
|
||||
|
||||
static bool lastRelay = false;
|
||||
if (relayState != lastRelay) {
|
||||
Serial.printf("Relay %s (touch=%s, ble=%s)\n",
|
||||
relayState ? "ON" : "OFF",
|
||||
sensorState == LOW ? "YES" : "no",
|
||||
deviceConnected ? "connected" : "disconnected");
|
||||
lastRelay = relayState;
|
||||
}
|
||||
|
||||
static uint32_t heartbeat = 0;
|
||||
if (millis() - heartbeat > 5000) {
|
||||
Serial.printf("[heartbeat] relay=%s touch=%s ble=%s\n",
|
||||
relayState ? "ON" : "OFF",
|
||||
sensorState == LOW ? "YES" : "no",
|
||||
deviceConnected ? "connected" : "disconnected");
|
||||
heartbeat = millis();
|
||||
}
|
||||
|
||||
delay(50);
|
||||
}
|
||||
Loading…
Reference in a new issue