13 KiB
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:
# 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:
# 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:
{"cmd": "COMMAND_NAME", "params": {...}, "id": 12345}
cmd: Command name (uppercase)params: Command-specific parametersid: Optional request ID for response matching
Response Structure
{"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:
{
"cmd": "SCAN",
"params": {
"frequencies": [433920000, 315000000, 868000000],
"duration": 60,
"modulation": "OOK",
"rssi_threshold": -90
},
"id": 1
}
Parameters:
frequencies: Array of frequencies in Hzduration: Scan duration in seconds (0 = continuous)modulation: "OOK", "FSK", or "AUTO"rssi_threshold: Minimum signal strength in dBm
Response:
{
"status": "ok",
"data": {
"scan_id": "scan_20250111_1234",
"started_at": 1704998400
},
"id": 1
}
2. CAPTURE
Capture a detected signal.
Request:
{
"cmd": "CAPTURE",
"params": {
"frequency": 433920000,
"duration": 5,
"format": "RAW"
},
"id": 2
}
Parameters:
frequency: Frequency in Hzduration: Capture duration in secondsformat: "RAW", "DECODED", or "BOTH"
Response:
{
"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:
{
"cmd": "STOP_SCAN",
"params": {
"scan_id": "scan_20250111_1234"
},
"id": 3
}
Response:
{
"status": "ok",
"data": {
"total_signals": 127,
"unique_protocols": 8
},
"id": 3
}
4. REPLAY
Replay a stored .sub file.
Request:
{
"cmd": "REPLAY",
"params": {
"file": "/sd/captures/garage_door.sub",
"repeat": 3,
"delay_ms": 100
},
"id": 4
}
Response:
{
"status": "ok",
"data": {
"transmitted": 3,
"frequency": 433920000
},
"id": 4
}
5. STATUS
Get device status.
Request:
{
"cmd": "STATUS",
"id": 5
}
Response:
{
"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:
{
"cmd": "CONFIG",
"params": {
"auto_save": true,
"save_path": "/sd/giglez/",
"led_brightness": 50,
"spectrum_display": false
},
"id": 6
}
Response:
{
"status": "ok",
"data": {
"config_updated": true
},
"id": 6
}
7. LIST_FILES
List captured .sub files on SD card.
Request:
{
"cmd": "LIST_FILES",
"params": {
"path": "/sd/giglez/",
"pattern": "*.sub"
},
"id": 7
}
Response:
{
"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:
{
"cmd": "GET_FILE",
"params": {
"path": "/sd/giglez/capture_001.sub"
},
"id": 8
}
Response:
{
"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
{
"event": "SIGNAL_DETECTED",
"data": {
"frequency": 433920000,
"rssi": -72,
"timestamp": 1704998400,
"protocol": "Unknown",
"duration_ms": 45
}
}
SCAN_COMPLETE
{
"event": "SCAN_COMPLETE",
"data": {
"scan_id": "scan_20250111_1234",
"total_signals": 127,
"duration": 60
}
}
ERROR
{
"event": "ERROR",
"data": {
"code": "SD_CARD_FULL",
"message": "SD card is full, cannot save capture"
}
}
Python Communication Library
Basic Usage
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:
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:
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:
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
# 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
# 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
# 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
- Flash Bruce firmware to T-Embed
- Test serial communication from Termux
- Implement capture coordination with GPS
- Build async event handling system
- Develop custom firmware optimized for GigLez