Bạn muốn thêm tính năng theo dõi vị trí/sự hiện diện vào dự án? Thất vọng vì giải pháp ít ỏi (hoặc không có)?
Đừng lo, bạn không đơn độc!
Bài viết hướng dẫn triển khai ứng dụng theo dõi và thông báo cơ bản. Sử dụng Particle Argon và Tile Mate.
Kết thúc: bạn biết được Tile có hiện diện hay không. Dùng Pushover gửi push notification đến thiết bị bạn chọn.
Bắt đầu nào!
Lưu ý: Bài viết dài. Có thể tải PDF để lưu và đọc sau.
Điều Tra Ban Đầu
Ý tưởng dùng Tile không rõ ràng ngay lập đầu. Thử dùng điện thoại thì hợp lý hơn. Nhưng không khả thi: cần nghiên cứu thêm và viết app Bluetooth iOS.
→ Loại bỏ điện thoại.
Câu hỏi: "Thiết bị nào luôn quảng bá (advertise)?"
Điều đó dẫn đến thiết bị theo dõi như Tile.
Sau khi nhận Tile, test thông thường. Đầu tiên: app Tile.
Kết nối và dùng được. Thậm chí cho phát nhạc vui tai. 😊
Tiếp theo: app quét Bluetooth. Lướt kết quả – Có Tile!
Chờ vài giờ kiểm tra lại. Muốn chắc Tile không ngủ sau thời gian. Kết quả: Tile luôn quảng bá, khoảng mỗi 8 giây.
Kết luận: dễ dàng dùng cho phát hiện sự hiện diện (presence detection).
Bước tiếp: làm sao chạy được trên Argon.
Quảng Bá (Advertising)
Như đã thấy, Tile quảng bá ~8 giây/lần. Dễ dàng quét bằng bất kỳ thiết bị nào: Argon, Xenon, Boron.
Gợi ý dùng Argon. Lý do: Bluetooth và Mesh dùng chung radio. Khi quét Tile, Xenon kết nối Mesh thường bỏ lỡ gói quảng bá → false positive (giả dương) và bực bội.
Cùng lý do: đảm bảo Argon KHÔNG kết nối mesh network. Gỡ bằng CLI: cắm thiết bị vào máy tính, chạy:
particle mesh remove <device name/ID>
Thay <device name/ID> bằng tên/ID thiết bị của bạn.
Quay lại phần chính.
Quảng bá Bluetooth có nhiều mục đích. Thường đánh dấu bắt đầu giai đoạn ghép cặp (pairing). Thiết bị quảng bá cho biết mình sẵn sàng.
Ngoài ra, thiết bị quảng bá chỉ ra service (dịch vụ) nào mà nó có. Ta dùng để lọc thiết bị không phù hợp.
Ví dụ service UUID của Tile: 0xfeed.
Khi quét, kiểm tra xem thiết bị có service UUID 0xfeed không.
Trước khi đi sâu Bluetooth, setup app debug bằng Logger.
Logging (Ghi Nhật Ký)
Dùng Logger để hiển thị log từ app qua particle serial monitor.
Tính năng hay: message hierarchy (cấp độ tin nhắn). Cho phép tắt thông báo không cần thiết.
Ví dụ: message debug. Có thể xóa/bình luận, hoặc tăng LOG_LEVEL để bỏ qua.
Các level trong logging.h (Particle device-os repo):
typedef enum LogLevel {
LOG_LEVEL_ALL = 1,
LOG_LEVEL_TRACE = 1,
LOG_LEVEL_INFO = 30,
LOG_LEVEL_WARN = 40,
LOG_LEVEL_ERROR = 50,
LOG_LEVEL_PANIC = 60,
LOG_LEVEL_NONE = 70,
// Compatibility levels
DEFAULT_LEVEL = 0,
ALL_LEVEL = LOG_LEVEL_ALL,
TRACE_LEVEL = LOG_LEVEL_TRACE,
LOG_LEVEL = LOG_LEVEL_TRACE, // Deprecated
DEBUG_LEVEL = LOG_LEVEL_TRACE, // Deprecated
INFO_LEVEL = LOG_LEVEL_INFO,
WARN_LEVEL = LOG_LEVEL_WARN,
ERROR_LEVEL = LOG_LEVEL_ERROR,
PANIC_LEVEL = LOG_LEVEL_PANIC,
NO_LOG_LEVEL = LOG_LEVEL_NONE
} LogLevel;
Cách dùng: gọi Log.trace, Log.info, Log.warn, Log.error.
Log.trace("This is a TRACE message.");
Set LOG_LEVEL_INFO → chỉ thấy Log.info, Log.warn, Log.error. LOG_LEVEL_WARN → chỉ Log.warn, Log.error.
Setup: set default level LOG_LEVEL_ERROR, app-specific LOG_LEVEL = LOG_LEVEL_TRACE:
SerialLogHandler logHandler(115200, LOG_LEVEL_ERROR, {
{ "app", LOG_LEVEL_TRACE }, // enable all app messages
});
Tránh spam log DeviceOS, vẫn nhận toàn bộ message app.
Muốn set toàn bộ device một level: SerialLogHandler logHandler(LOG_LEVEL_INFO);.
Logger rất tiện khi dùng Particle DeviceOS. Tiếp tục phần chính!
Thiết Lập (Setting It Up)
Đầu tiên: đảm bảo DeviceOS version ≥ 1.3 (có Bluetooth). Xem hướng dẫn tại đây.
Quét Tile trong hàm loop() theo khoảng thời gian. Dùng timer millis():
// Scan for devices
if( (millis() > lastSeen + TILE_RE_CHECK_MS) ){
BLE.scan(scanResultCallback, NULL);
}
Định nghĩa lastSeen đầu file:
system_tick_t lastSeen = 0;
Dùng để theo dõi lần cuối Tile được "thấy" (Argon nhận gói quảng bá).
TILE_RE_CHECK_MS định nghĩa:
#define TILE_RE_CHECK_MS 7500
Kiểm tra tối thiểu 7.5 giây/lần.
Dùng BLE.scan tìm Tile. Khi gọi, bắt đầu quét. Khi tìm thấy thiết bị, scanResultCallback kích hoạt.
Định nghĩa scanResultCallback đầu file:
void scanResultCallback(const BleScanResult *scanResult, void *context) {
}
Chứa BleScanResult: địa chỉ, RSSI, tên thiết bị (nếu có), service info. Hữu ích khi tìm Tile!
Lưu ý: BLE.scan không trả về cho đến khi quét xong. Timeout mặc định 5 giây. Đổi bằng BLE.setScanTimeout(). Đơn vị 10ms. 500ms → giá trị 50.
Cho app này, khuyến nghị 8s (8000ms): BLE.setScanTimeout(800);. Thiết bị quét đủ lâu để Tile quảng bá, giảm bỏ lỡ gói tin.
Xử Lý Kết Quả Quét
Định nghĩa bên trong scanResultCallback.
Lấy service info từ advertising data. Cách tốt: scanResult->advertisingData.serviceUUID. Truyền mảng UUID để copy.
BleUuid uuids[4];
int uuidsAvail = scanResult->advertisingData.serviceUUID(uuids,sizeof(uuids)/sizeof(BleUuid));
uuids được populate, lặp qua. uuidsAvail = số UUID có sẵn.
Tìm UUID đặc biệt. Định nghĩa đầu file:
#define TILE_UUID 0xfeed
UUID thường dài hơn. UUID ngắn như này nghĩa là reserved hoặc trong Bluetooth spec. Kiểm tra cùng cách 32/128-bit.
In thông tin thiết bị cho diagnostic: RSSI và MAC address:
BleAddress addr = scanResult->address;
Log.trace("MAC: %02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
Log.trace("RSSI: %dBm", scanResult->rssi);
Lặp qua UUID có sẵn, tìm Tile:
for(int i = 0; i < uuidsAvail; i++){
if( uuids[i].shorted() == TILE_UUID ) {
Log.trace("UUID: %x", uuids[i].shorted());
// Stop scanning
BLE.stopScanning();
return;
}
}
So sánh "shorted" version của UUID với TILE_UUID. Integer đơn giản, không cần so sánh bộ nhớ phức tạp.
Dùng Log.trace in diagnostic. In ra shorted() của UUID.
Test!
Nạp firmware vào Argon. Mở terminal chạy particle serial monitor xem debug. Ví dụ output:
0000005825 [app] TRACE: MAC: 65:C7:B3:AF:73:5C
0000005827 [app] TRACE: RSSI: -37Bm
...
0000006299 [app] TRACE: UUID: feed
Chú ý: message có TRACE và [app] → trace message từ app code. Tiện!
Code dễ bị spam log nếu môi trường nhiều thiết bị Bluetooth quảng bá. Khi Tile bật, cuối cùng sẽ thấy UUID: feed → Argon tìm thấy Tile!
Tiếp theo: dùng nút Mode onboard để "lập trình" địa chỉ Tile vào bộ nhớ. Lọc bỏ thiết bị không quan tâm.
Thêm Thiết Khi Nhấn Nút (Add Device On Button Push)
Cách monitor nút Mode: theo tài liệu dùng System.on.
System.on(button_click, eventHandler);
Tham số 1: tên system event (button_click). Tham số 2: event handler function (eventHandler).
Tạo eventHandler:
void eventHandler(system_event_t event, int duration, void* ) {
}
Quan trọng: không dùng Log trong eventHandler. Test dễ: toggle LED D7.
Khởi tạo LED trong setup():
pinMode(D7,OUTPUT);
Trong eventHandler:
if( event == button_click ) {
if( digitalRead(D7) ) {
digitalWrite(D7,LOW);
} else {
digitalWrite(D7,HIGH);
}
}
Ghi D7 (LED xanh onboard). Dùng digitalRead đọc trạng thái LED: HIGH hoặc LOW.
Nạp firmware, có điều khiển LED xanh đẹp!
Phần sau: dùng nút Mode đưa thiết bị vào "learning mode" (chế độ học). Cho phép one-touch setup với Tile mục tiêu.
Lưu Địa Chỉ Vào EEPROM
Lưu địa chỉ Tile vào EEPROM (bộ nhớ không bay hơi). Khởi động lại/mất điện vẫn nhận diện Tile.
Câu hỏi: làm sao lưu địa chỉ lần đầu?
Theo dõi nhấn nút → đưa thiết bị vào "learning mode". Thiết bị quét Tile, lưu địa chỉ nếu tìm thấy.
Thêm điều kiện trong if( uuids[i].shorted() == TILE_UUID ):
// If we're in learning mode. Save to EEPROM
if( isLearningModeOn() ) {
searchAddress = scanResult->address;
EEPROM.put(TILE_EEPROM_ADDRESS, searchAddress);
setLearningModeOff();
}
Dùng trạng thái D7 biết "learning mode". Đọc D7 bằng digitalRead(D7). Tạo function rõ ràng:
bool isLearningModeOn() {
return (digitalRead(D7) == HIGH);
}
Thay digitalWrite(D7,LOW/HIGH) bằng function tương tự:
void setLearningModeOn() { digitalWrite(D7,HIGH); }
void setLearningModeOff() { digitalWrite(D7,LOW); }
Gán biến global searchAddress = scan result. Khai báo đầu file:
BleAddress searchAddress;
Lưu non-volatile memory bằng EEPROM.put. TILE_EEPROM_ADDRESS = 0xa. Định nghĩa đầu file:
#define TILE_EEPROM_ADDRESS 0xa
Cuối cùng tắt LED và "learning mode" bằng setLearningModeOff().
Mỗi lần tìm thấy thiết bị, cập nhật lastSeen bằng millis(). Theo dõi lastRSSI (độ mạnh tín hiệu gần đúng) qua scanResult->rssi.
Tổng thể thay đổi:
...
if( uuids[i].shorted() == TILE_UUID ) {
Log.trace("UUID: %x", uuids[i].shorted());
if( isLearningModeOn() ) {
searchAddress = scanResult->address;
EEPROM.put(TILE_EEPROM_ADDRESS, searchAddress);
setLearningModeOff();
}
// Save info
lastSeen = millis();
lastRSSI = scanResult->rssi;
BLE.stopScanning();
return;
}
Trước function trên, lọc thiết bị không khớp searchAddress. Thêm trước if( uuids[i].shorted() == TILE_UUID ):
// If device address doesn't match or we're not in "learning mode"
if( !(searchAddress == scanResult->address) && !isLearningModeOn() ) {
return;
}
Bỏ qua thiết bị không khớp. Chỉ xử lý nếu khớp địa chỉ HOẶC đang learning mode.
Để load searchAddress khi khởi động, đọc từ flash. Thêm vào setup():
EEPROM.get(TILE_EEPROM_ADDRESS, searchAddress);
Kiểm tra địa chỉ hợp lệ. Không hợp lệ nếu tất cả byte = 0xFF:
if( searchAddress == BleAddress("ff:ff:ff:ff:ff:ff") ) {
Log.warn("Place this board into learning mode");
Log.warn("and keep your Tile near by.");
}
Giờ có thể "dạy" Argon địa chỉ Tile. Test!
Test.
Compile chạy app, log không còn output? Phải "dạy" địa chỉ Tile. Nhấn nút Mode → LED xanh sáng.
Khi Tile được tìm thấy, LED tắt, thấy output giống trước:
0000006296 [app] TRACE: MAC: D7:E7:FE:0C:A5:C0
0000006298 [app] TRACE: RSSI: -60Bm
0000006299 [app] TRACE: UUID: feed
Thiết bị đã ghi nhớ!
Kiểm tra sau reset: nhấn nút reset, xem output như trên. Nếu hiện → vẫn tốt!
Cập Nhật Lên Cloud
Tạo function checkTileStateChanged kiểm tra thay đổi trạng thái Tile theo interval.
bool checkTileStateChanged( TilePresenceType *presence ) {
}
Mục đích: so sánh lastSeen với "timeout" duration. Timeout = TILE_NOT_HERE_MS = 30000 (30 giây) định nghĩa đầu chương trình.
2 trường hợp khác: lastSeen == 0 (chưa tìm thấy Tile sau khởi động), và lastSeen > 0 (đã thấy).
Trong checkTileStateChanged gom hết:
// Check to see if it's here.
if( millis() > lastSeen+TILE_NOT_HERE_MS ) {
} else if ( lastSeen == 0 ) {
} else {
}
return false;
Function chỉ return true khi trạng thái thay đổi. Dùng con trỏ TilePresenceType.
TilePresenceType enum các trạng thái. Đặt đầu file:
typedef enum { PresenceUnknown, Here, NotHere } TilePresenceType;
Biến global truyền vào function. Đặt đầu file:
TilePresenceType present = PresenceUnknown;
So sánh từng giai đoạn: đáp ứng tiêu chí? Trạng thái khác lần trước? Nếu có, return true. Cập nhật presence giá trị mới. Ví dụ: *presence = NotHere;.
Function hoàn chỉnh:
bool checkTileStateChanged( TilePresenceType *presence ) {
if( millis() > lastSeen+TILE_NOT_HERE_MS ) {
if( *presence != NotHere ) {
*presence = NotHere;
Log.trace("not here!");
return true;
}
} else if ( lastSeen == 0 ) {
if( *presence != PresenceUnknown ) {
*presence = PresenceUnknown;
Log.trace("unknown!");
return true;
}
} else {
if( *presence != Here ) {
*presence = Here;
Log.trace("here!");
return true;
}
}
return false;
}
Dùng function trong main loop dưới timer Ble.scan(). Gửi JSON payload: Bluetooth Address, lastSeen, lastRSSI, message.
if( checkTileStateChanged(&present) ) {
}
Dùng mảng char lấy address dạng string. Chain toString() với toCharArray.
char address[18];
searchAddress.toString().toCharArray(address,sizeof(address));
Ví dụ payload string:
status = String::format("{\"address\":\"%s\",\"lastSeen\":%d,\"lastRSSI\":%i,\"status\":\"%s\"}", address, lastSeen, lastRSSI, messages[present]);
status là String khai báo đầu file.
messages là mảng string const static map với TilePresenceType:
const char * messages[] { "unknown", "here", "not here" };
PresenceUnknown → "unknown", Here → "here", v.v. Cách gán string cho enum rẻ và dễ.
Publish và process ngay:
Particle.publish("status", status, PRIVATE, WITH_ACK);
Particle.process();
Function tổng thể trong loop:
if( checkTileStateChanged(&present) ) {
char address[18];
searchAddress.toString().toCharArray(address,sizeof(address));
status = String::format("{\"address\":\"%s\",\"lastSeen\":%d,\"lastRSSI\":%i,\"status\":\"%s\"}", address, lastSeen, lastRSSI, messages[present]);
Particle.publish("status", status, PRIVATE, WITH_ACK);
Particle.process();
}
Test!
Test!
Test Publish event không cần rời Particle Workbench. Mở terminal mới: View → Terminal. Chạy:
particle subscribe --device <device_name> <event_name>
Thay <device_name> tên/ID thiết bị. <event_name> = status.
Test: tháo pin Tile, chờ "not here". Gắn pin lại, chờ "here".
Ví dụ output:
> particle subscribe --device hamster_turkey status
Subscribing to "status" from hamster_turkey's stream
Listening to: /v1/devices/hamster_turkey/events/status
{"name":"status","data":"{\"address\":\"C0:A5:0C:FE:E7:D7\",\"lastSeen\":40154002,\"lastRSSI\":-82,\"status\":\"not here\"}","ttl":60,"published_at":"2019-09-07T02:29:42.232Z","coreid":"e00fce68d36c42ef433428eb"}
{"name":"status","data":"{\"address\":\"C0:A5:0C:FE:E7:D7\",\"lastSeen\":40193547,\"lastRSSI\":-83,\"status\":\"here\"}","ttl":60,"published_at":"2019-09-07T02:29:50.352Z","coreid":"e00fce68d36c42ef433428eb"}
Cấu Hình Webhook
Phần cuối: setup push notification bằng webhook. Dùng Pushover API dễ dàng gửi push notification đến thiết bị bạn chọn.
Pushover API cực dễ dùng. App của họ như "dao đa năng" khi không muốn code app gửi push.
Đầu tiên: lấy user key. Đăng nhập Pushover. (Cần tạo tài khoản nếu chưa có).
Nếu đăng nhập mà không thấy trang này, click logo Pushover để về trang chủ.
Tạo application: click Apps & Plugins trên cùng.
Click Create a New Application → lấy API Token cho Particle Webhook.
Đặt tên, mô tả (tùy chọn). Tích hộp rồi Create Application.
Trang sau: copy và lưu API Token/Key – cần cho bước sau.
Setup Webhook: vào console.particle.io tạo integration mới.
- Event Name: status
- URL: https://api.pushover.net/1/messages.json
- Lọc theo thiết bị cụ thể: chọn trong Device dropdown.
Advanced Settings:
Tạo fields: token, user, title, message. Set token = API Token vừa lấy. User = User Key.
title: tiêu đề tin nhắn. Đặt gì phù hợp.
message: The Tile is currently {{{status}}}. RSSI: {{{lastRSSI}}}.
Dùng mustache template. Cho phép dùng data từ payload publish và format lại. Kết quả: The Tile is currently here. RSSI: -77.
Sẽ nói thêm về template trong guide khác. Đón đọc!
Test
Integration xong, test như trước: tháo pin, chờ "not here". Gắn pin, chờ "here".
Giao diện iPhone:
Đã test nhiều lần! 😊
Nếu đến đây mọi thứ hoạt động, tuyệt vời. Bạn đã có Tile tracker cho nhà, văn phòng...
Code Hoàn Chỉnh
Tìm code hoàn chỉnh? Tôi cũng vậy! Hosted trên Github tại đây.
Kết Luận
Kỹ thuật và công nghệ bài viết áp dụng được nhiều cách. Tóm tắt điểm chính:
- Dùng Bluetooth Central quét và nhận diện Tile off-the-shelf
- Lưu info Tile vào EEPROM → lấy lại khi khởi động
- Dùng
Particle.publishđẩy cập nhật lên cloud - Dùng Particle Integration Webhook tạo push notification khi trạng thái thay đổi
Mở rộng, hack, làm cho nó thành của bạn. Đừng quên chia sẻ! Tôi muốn nghe từ bạn: hello@jaredwolff.com
Thích bài viết? Click share link dưới và lan tỏa. :)
Bài cross-post từ blog của tôi. Xem bản gốc tại đây.
Muốn học thêm? Tôi viết guide khai thác tối đa Particle Platform. Tìm hiểu tại đây.