Initial commit: Phase 1 & Phase 2 infrastructure complete
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user