663 lines
13 KiB
Markdown
663 lines
13 KiB
Markdown
# LilyGo T-Embed Setup and Communication Protocol
|
|
|
|
## Overview
|
|
|
|
The LilyGo T-Embed is an ESP32-based device with an integrated CC1101 sub-GHz transceiver. This document covers hardware setup, firmware options, and the communication protocol between Android Termux and the T-Embed.
|
|
|
|
## Hardware Specifications
|
|
|
|
### LilyGo T-Embed CC1101
|
|
- **MCU**: ESP32-S3 (Dual-core 240MHz)
|
|
- **Display**: 1.9" ST7789 TFT (170x320 pixels)
|
|
- **RF Chip**: CC1101 Sub-GHz transceiver
|
|
- **Frequency Range**:
|
|
- 300-348 MHz
|
|
- 387-464 MHz
|
|
- 779-928 MHz
|
|
- **Storage**: MicroSD card slot
|
|
- **Connectivity**: USB-C (serial + power)
|
|
- **Battery**: Built-in LiPo charger
|
|
|
|
### CC1101 Transceiver Capabilities
|
|
- **Modulation**: ASK/OOK, FSK, GFSK, MSK
|
|
- **Data Rate**: 0.6 - 500 kbps
|
|
- **RX Sensitivity**: -112 dBm @ 1.2 kbps
|
|
- **TX Power**: Configurable up to +12 dBm
|
|
- **FIFO Buffer**: 64 bytes RX/TX
|
|
|
|
## Firmware Options
|
|
|
|
### 1. Bruce Firmware (Recommended)
|
|
**Repository**: https://github.com/pr3y/Bruce
|
|
|
|
Bruce is a multi-tool firmware with excellent Sub-GHz support designed for T-Embed.
|
|
|
|
**Features**:
|
|
- Read/replay .sub files (Flipper compatible)
|
|
- Spectrum analyzer
|
|
- Frequency scanner
|
|
- Signal recorder
|
|
- SD card support for signature storage
|
|
|
|
**Installation**:
|
|
```bash
|
|
# Download from releases
|
|
wget https://github.com/pr3y/Bruce/releases/latest/download/Bruce_T-Embed.bin
|
|
|
|
# Flash using esptool (in Termux)
|
|
pip install esptool
|
|
esptool.py --chip esp32s3 --port /dev/ttyUSB0 write_flash 0x0 Bruce_T-Embed.bin
|
|
```
|
|
|
|
### 2. ESP32-Marauder
|
|
**Repository**: https://github.com/justcallmekoko/ESP32Marauder
|
|
|
|
Originally for WiFi, has been adapted for Sub-GHz.
|
|
|
|
### 3. Custom Firmware (Planned)
|
|
GigLez-specific firmware optimized for continuous scanning and GPS coordination.
|
|
|
|
## Communication Protocol
|
|
|
|
### Serial Connection
|
|
|
|
The T-Embed communicates via USB serial at 115200 baud.
|
|
|
|
**Termux Serial Setup**:
|
|
```bash
|
|
# Install required packages
|
|
pkg install libusb python
|
|
|
|
# Python serial library
|
|
pip install pyserial
|
|
|
|
# Find device
|
|
ls /dev/ttyUSB*
|
|
# Usually /dev/ttyUSB0 or /dev/ttyACM0
|
|
```
|
|
|
|
### Command Protocol
|
|
|
|
GigLez uses a JSON-based command protocol over serial.
|
|
|
|
#### Command Structure
|
|
|
|
All commands are JSON objects terminated by `\n`:
|
|
|
|
```json
|
|
{"cmd": "COMMAND_NAME", "params": {...}, "id": 12345}
|
|
```
|
|
|
|
- `cmd`: Command name (uppercase)
|
|
- `params`: Command-specific parameters
|
|
- `id`: Optional request ID for response matching
|
|
|
|
#### Response Structure
|
|
|
|
```json
|
|
{"status": "ok|error", "data": {...}, "id": 12345, "timestamp": 1704998400}
|
|
```
|
|
|
|
- `status`: "ok" or "error"
|
|
- `data`: Response data (command-specific)
|
|
- `id`: Request ID (if provided)
|
|
- `timestamp`: Unix timestamp from device
|
|
|
|
### Command Reference
|
|
|
|
#### 1. SCAN
|
|
|
|
Start scanning for signals on specified frequencies.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "SCAN",
|
|
"params": {
|
|
"frequencies": [433920000, 315000000, 868000000],
|
|
"duration": 60,
|
|
"modulation": "OOK",
|
|
"rssi_threshold": -90
|
|
},
|
|
"id": 1
|
|
}
|
|
```
|
|
|
|
**Parameters**:
|
|
- `frequencies`: Array of frequencies in Hz
|
|
- `duration`: Scan duration in seconds (0 = continuous)
|
|
- `modulation`: "OOK", "FSK", or "AUTO"
|
|
- `rssi_threshold`: Minimum signal strength in dBm
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"scan_id": "scan_20250111_1234",
|
|
"started_at": 1704998400
|
|
},
|
|
"id": 1
|
|
}
|
|
```
|
|
|
|
#### 2. CAPTURE
|
|
|
|
Capture a detected signal.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "CAPTURE",
|
|
"params": {
|
|
"frequency": 433920000,
|
|
"duration": 5,
|
|
"format": "RAW"
|
|
},
|
|
"id": 2
|
|
}
|
|
```
|
|
|
|
**Parameters**:
|
|
- `frequency`: Frequency in Hz
|
|
- `duration`: Capture duration in seconds
|
|
- `format`: "RAW", "DECODED", or "BOTH"
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"capture_id": "cap_20250111_1235",
|
|
"frequency": 433920000,
|
|
"rssi": -75,
|
|
"protocol": "Princeton",
|
|
"raw_data": [2980, -240, 520, -980, 520, -980, ...],
|
|
"decoded": {
|
|
"bit": 24,
|
|
"key": "00 00 00 00 00 95 D5 D4",
|
|
"te": 400
|
|
}
|
|
},
|
|
"id": 2
|
|
}
|
|
```
|
|
|
|
#### 3. STOP_SCAN
|
|
|
|
Stop an active scan.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "STOP_SCAN",
|
|
"params": {
|
|
"scan_id": "scan_20250111_1234"
|
|
},
|
|
"id": 3
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"total_signals": 127,
|
|
"unique_protocols": 8
|
|
},
|
|
"id": 3
|
|
}
|
|
```
|
|
|
|
#### 4. REPLAY
|
|
|
|
Replay a stored .sub file.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "REPLAY",
|
|
"params": {
|
|
"file": "/sd/captures/garage_door.sub",
|
|
"repeat": 3,
|
|
"delay_ms": 100
|
|
},
|
|
"id": 4
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"transmitted": 3,
|
|
"frequency": 433920000
|
|
},
|
|
"id": 4
|
|
}
|
|
```
|
|
|
|
#### 5. STATUS
|
|
|
|
Get device status.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "STATUS",
|
|
"id": 5
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"firmware": "GigLez-T-Embed v0.1.0",
|
|
"uptime": 3600,
|
|
"battery_voltage": 3.85,
|
|
"battery_percent": 78,
|
|
"sd_card": {
|
|
"present": true,
|
|
"free_mb": 15234,
|
|
"total_mb": 31456
|
|
},
|
|
"current_scan": "scan_20250111_1234",
|
|
"cc1101": {
|
|
"status": "idle",
|
|
"frequency": 433920000,
|
|
"rssi": -95
|
|
}
|
|
},
|
|
"id": 5
|
|
}
|
|
```
|
|
|
|
#### 6. CONFIG
|
|
|
|
Configure device settings.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "CONFIG",
|
|
"params": {
|
|
"auto_save": true,
|
|
"save_path": "/sd/giglez/",
|
|
"led_brightness": 50,
|
|
"spectrum_display": false
|
|
},
|
|
"id": 6
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"config_updated": true
|
|
},
|
|
"id": 6
|
|
}
|
|
```
|
|
|
|
#### 7. LIST_FILES
|
|
|
|
List captured .sub files on SD card.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "LIST_FILES",
|
|
"params": {
|
|
"path": "/sd/giglez/",
|
|
"pattern": "*.sub"
|
|
},
|
|
"id": 7
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"files": [
|
|
{
|
|
"name": "capture_001.sub",
|
|
"size": 245,
|
|
"created": 1704998400
|
|
},
|
|
{
|
|
"name": "capture_002.sub",
|
|
"size": 312,
|
|
"created": 1704998500
|
|
}
|
|
],
|
|
"total_count": 2,
|
|
"total_bytes": 557
|
|
},
|
|
"id": 7
|
|
}
|
|
```
|
|
|
|
#### 8. GET_FILE
|
|
|
|
Retrieve a file from SD card.
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cmd": "GET_FILE",
|
|
"params": {
|
|
"path": "/sd/giglez/capture_001.sub"
|
|
},
|
|
"id": 8
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"filename": "capture_001.sub",
|
|
"size": 245,
|
|
"content": "Filetype: Flipper SubGhz Key File\nVersion: 1\n..."
|
|
},
|
|
"id": 8
|
|
}
|
|
```
|
|
|
|
### Event Notifications
|
|
|
|
The T-Embed can send unsolicited event notifications:
|
|
|
|
#### SIGNAL_DETECTED
|
|
|
|
```json
|
|
{
|
|
"event": "SIGNAL_DETECTED",
|
|
"data": {
|
|
"frequency": 433920000,
|
|
"rssi": -72,
|
|
"timestamp": 1704998400,
|
|
"protocol": "Unknown",
|
|
"duration_ms": 45
|
|
}
|
|
}
|
|
```
|
|
|
|
#### SCAN_COMPLETE
|
|
|
|
```json
|
|
{
|
|
"event": "SCAN_COMPLETE",
|
|
"data": {
|
|
"scan_id": "scan_20250111_1234",
|
|
"total_signals": 127,
|
|
"duration": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
#### ERROR
|
|
|
|
```json
|
|
{
|
|
"event": "ERROR",
|
|
"data": {
|
|
"code": "SD_CARD_FULL",
|
|
"message": "SD card is full, cannot save capture"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Python Communication Library
|
|
|
|
### Basic Usage
|
|
|
|
```python
|
|
import serial
|
|
import json
|
|
import time
|
|
|
|
class TEmbedController:
|
|
def __init__(self, port='/dev/ttyUSB0', baudrate=115200):
|
|
self.serial = serial.Serial(port, baudrate, timeout=1)
|
|
self.request_id = 0
|
|
time.sleep(2) # Wait for device reset
|
|
|
|
def send_command(self, cmd, params=None):
|
|
"""Send command and return response"""
|
|
self.request_id += 1
|
|
message = {
|
|
'cmd': cmd,
|
|
'params': params or {},
|
|
'id': self.request_id
|
|
}
|
|
|
|
# Send
|
|
self.serial.write(json.dumps(message).encode() + b'\n')
|
|
|
|
# Wait for response
|
|
line = self.serial.readline().decode().strip()
|
|
if line:
|
|
return json.loads(line)
|
|
return None
|
|
|
|
def start_scan(self, frequencies, duration=60):
|
|
"""Start scanning on specified frequencies"""
|
|
return self.send_command('SCAN', {
|
|
'frequencies': frequencies,
|
|
'duration': duration,
|
|
'modulation': 'AUTO',
|
|
'rssi_threshold': -90
|
|
})
|
|
|
|
def capture_signal(self, frequency, duration=5):
|
|
"""Capture signal at frequency"""
|
|
return self.send_command('CAPTURE', {
|
|
'frequency': frequency,
|
|
'duration': duration,
|
|
'format': 'BOTH'
|
|
})
|
|
|
|
def get_status(self):
|
|
"""Get device status"""
|
|
return self.send_command('STATUS')
|
|
|
|
def listen_events(self, callback):
|
|
"""Listen for event notifications"""
|
|
while True:
|
|
line = self.serial.readline().decode().strip()
|
|
if line:
|
|
try:
|
|
data = json.loads(line)
|
|
if 'event' in data:
|
|
callback(data)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Example usage
|
|
device = TEmbedController('/dev/ttyUSB0')
|
|
|
|
# Check status
|
|
status = device.get_status()
|
|
print(f"Battery: {status['data']['battery_percent']}%")
|
|
|
|
# Start scan
|
|
scan = device.start_scan([433920000, 315000000, 868000000], duration=60)
|
|
print(f"Scan started: {scan['data']['scan_id']}")
|
|
|
|
# Listen for signals
|
|
def on_event(event):
|
|
if event['event'] == 'SIGNAL_DETECTED':
|
|
print(f"Signal detected: {event['data']['frequency']} Hz, RSSI: {event['data']['rssi']} dBm")
|
|
|
|
device.listen_events(on_event)
|
|
```
|
|
|
|
### Async Implementation
|
|
|
|
For better performance in production:
|
|
|
|
```python
|
|
import asyncio
|
|
import aioserial
|
|
|
|
class AsyncTEmbedController:
|
|
def __init__(self, port='/dev/ttyUSB0', baudrate=115200):
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.request_id = 0
|
|
self.pending_requests = {}
|
|
|
|
async def connect(self):
|
|
self.serial = aioserial.AioSerial(port=self.port, baudrate=self.baudrate)
|
|
await asyncio.sleep(2)
|
|
|
|
async def send_command(self, cmd, params=None):
|
|
self.request_id += 1
|
|
message = {
|
|
'cmd': cmd,
|
|
'params': params or {},
|
|
'id': self.request_id
|
|
}
|
|
|
|
# Send
|
|
await self.serial.write_async(json.dumps(message).encode() + b'\n')
|
|
|
|
# Create future for response
|
|
future = asyncio.Future()
|
|
self.pending_requests[self.request_id] = future
|
|
|
|
return await future
|
|
|
|
async def listen(self, event_callback):
|
|
"""Background task to listen for responses and events"""
|
|
while True:
|
|
line = await self.serial.readline_async()
|
|
if line:
|
|
try:
|
|
data = json.loads(line.decode().strip())
|
|
|
|
# Handle response
|
|
if 'id' in data and data['id'] in self.pending_requests:
|
|
self.pending_requests[data['id']].set_result(data)
|
|
del self.pending_requests[data['id']]
|
|
|
|
# Handle event
|
|
elif 'event' in data:
|
|
await event_callback(data)
|
|
|
|
except json.JSONDecodeError:
|
|
pass
|
|
```
|
|
|
|
## GPS Coordination
|
|
|
|
### Timestamp Synchronization
|
|
|
|
The T-Embed doesn't have GPS, so timestamps come from Android:
|
|
|
|
```python
|
|
import time
|
|
|
|
# Send timestamp to device
|
|
device.send_command('CONFIG', {
|
|
'system_time': int(time.time())
|
|
})
|
|
|
|
# Device will use this to calibrate its internal clock
|
|
```
|
|
|
|
### Capture with GPS
|
|
|
|
When a signal is captured, immediately tag it with GPS:
|
|
|
|
```python
|
|
import android
|
|
from tembed import TEmbedController
|
|
|
|
# Initialize Android SL4A for GPS
|
|
droid = android.Android()
|
|
droid.startLocating()
|
|
|
|
# T-Embed controller
|
|
device = TEmbedController()
|
|
|
|
# Event handler
|
|
async def on_signal(event):
|
|
if event['event'] == 'SIGNAL_DETECTED':
|
|
# Get current GPS location
|
|
location = droid.getLastKnownLocation().result
|
|
gps = location.get('gps') or location.get('network')
|
|
|
|
if gps:
|
|
# Store in database with GPS
|
|
store_capture({
|
|
'frequency': event['data']['frequency'],
|
|
'rssi': event['data']['rssi'],
|
|
'latitude': gps['latitude'],
|
|
'longitude': gps['longitude'],
|
|
'accuracy': gps.get('accuracy', 0),
|
|
'timestamp': event['data']['timestamp']
|
|
})
|
|
|
|
# Start listening
|
|
await device.listen(on_signal)
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Device Not Detected
|
|
|
|
```bash
|
|
# Check USB devices
|
|
lsusb
|
|
|
|
# Check serial ports
|
|
ls -l /dev/ttyUSB* /dev/ttyACM*
|
|
|
|
# Grant permissions (Termux)
|
|
termux-usb -l # List devices
|
|
termux-usb -r /dev/bus/usb/XXX/YYY # Request permission
|
|
```
|
|
|
|
### Serial Communication Errors
|
|
|
|
```python
|
|
# Add error handling
|
|
try:
|
|
response = device.send_command('STATUS')
|
|
except serial.SerialException as e:
|
|
print(f"Serial error: {e}")
|
|
# Attempt reconnection
|
|
device.serial.close()
|
|
device.serial.open()
|
|
```
|
|
|
|
### Firmware Update
|
|
|
|
```bash
|
|
# Backup SD card first
|
|
adb pull /sdcard/giglez /path/to/backup
|
|
|
|
# Flash new firmware
|
|
esptool.py --chip esp32s3 --port /dev/ttyUSB0 erase_flash
|
|
esptool.py --chip esp32s3 --port /dev/ttyUSB0 write_flash 0x0 firmware.bin
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
1. Flash Bruce firmware to T-Embed
|
|
2. Test serial communication from Termux
|
|
3. Implement capture coordination with GPS
|
|
4. Build async event handling system
|
|
5. Develop custom firmware optimized for GigLez
|