BLE Beacon Firmware (nRF52840)
Custom beacon firmware written in Embedded C using Nordic nRF5 SDK. Each beacon advertises a unique UUID payload every 100 ms in non-connectable mode to maximise battery life. Temperature and battery voltage are encoded in manufacturer-specific advertisement data bytes.
// BLE Advertisement payload structure
typedef struct {
uint16_t company_id; // 0x05A4 (assigned)
uint8_t asset_type; // 0x01=Tool, 0x02=Vehicle, 0x03=Person
uint16_t asset_id; // Unique asset identifier
int8_t tx_power; // Calibrated TX power at 1m (dBm)
uint8_t battery_pct; // 0–100%
int8_t temp_degc; // Ambient temperature
uint8_t motion_flag; // 1 = moving (ADXL accelerometer)
uint32_t seq_counter; // Monotonic counter for dedup
} __attribute__((packed)) beacon_adv_t;
// Advertising interval: 100 ms (non-connectable, non-scannable)
// TX Power: -4 dBm (balance range vs battery)
// Expected battery life @ CR2032: ~24 months
Gateway Firmware — RSSI Processing
Each Raspberry Pi 4 gateway runs a Python scanning daemon that receives HCI events from the nRF52840 USB dongle, applies RSSI windowed averaging (N=5), and feeds measurements into a per-tag Kalman filter to reduce multipath noise before publishing to MQTT.
class AssetTracker:
def __init__(self, asset_id):
self.kf = KalmanFilter1D(process_noise=0.1, obs_noise=2.5)
self.rssi_window = deque(maxlen=5)
self.last_seen = None
def update(self, raw_rssi, gateway_id):
self.rssi_window.append(raw_rssi)
avg_rssi = np.mean(self.rssi_window)
filtered = self.kf.update(avg_rssi)
distance_m = self._rssi_to_distance(filtered)
self._publish_mqtt(gateway_id, distance_m)
def _rssi_to_distance(self, rssi):
# Log-distance path loss model: d = 10^((TxPower - RSSI) / (10 * n))
# n=2.5 for factory floor (empirically calibrated)
return 10 ** ((-4 - rssi) / (10 * 2.5))