Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AngelAmoSanchez/TFG-RaspberryPi-BLE/llms.txt

Use this file to discover all available pages before exploring further.

The system classifies every detected device into one of three proximity zones based on its Received Signal Strength Indicator (RSSI). Getting the zone thresholds right for your specific environment is the single most effective way to improve counting accuracy. This guide explains what the zones mean, how to measure real-world RSSI values, and how to push updated thresholds to the system.

What the zones mean

Signal strength is measured in dBm — a negative number where values closer to zero indicate stronger (nearer) signals. The three zones map to approximate physical distances:
ZoneRSSI rangeApproximate distance
NEAR≥ −60 dBm0 – 2 m
MEDIUM≥ −75 dBm and < −60 dBm2 – 5 m
FAR< −75 dBm> 5 m
These distances are defined in src/scanner/detection.py:
class Zone(Enum):
    """Zonas de proximidad según la señal RSSI"""

    NEAR = "near"   # RSSI >= -60 dBm
    MEDIUM = "medium"  # -75 <= RSSI < -60 dBm
    FAR = "far"     # RSSI < -75 dBm

    def get_description(self) -> str:
        descriptions = {
            Zone.NEAR: "Zona Cercana (0-2m)",
            Zone.MEDIUM: "Zona Media (2-5m)",
            Zone.FAR: "Zona Lejana (>5m)",
        }
        return descriptions[self]
Classification happens in DetectionProcessor.classify_zone:
def classify_zone(self, rssi: int) -> Zone:
    if rssi >= self.near_threshold:
        return Zone.NEAR
    elif rssi >= self.medium_threshold:
        return Zone.MEDIUM
    else:
        return Zone.FAR

Default thresholds

The defaults are set in src/config.py and in the backend’s src/config.py:
# raspberry-pi/src/config.py
near_threshold: int = -60   # RSSI para zona cercana
medium_threshold: int = -75  # RSSI para zona media
# backend-cloud/src/config.py
devices_per_person: float = float(os.getenv("DEVICES_PER_PERSON", "1.5"))
near_threshold: int = int(os.getenv("NEAR_THRESHOLD", "-60"))
medium_threshold: int = int(os.getenv("MEDIUM_THRESHOLD", "-75"))
These defaults work reasonably well in open spaces. In practice, you almost always need to adjust them.

How environment affects RSSI

RSSI is not a reliable proxy for distance — it varies significantly based on:
  • Obstacles — walls, shelving, and people between the device and the Pi attenuate the signal. A device that reads −55 dBm in an open corridor might read −75 dBm through a wall at the same physical distance.
  • Interference — Wi-Fi (2.4 GHz), microwave ovens, and other BLE devices all compete for the same spectrum.
  • Device type — transmit power varies by manufacturer. A smartwatch typically broadcasts at lower power than a phone, producing weaker RSSI at the same distance.
  • Antenna orientation — holding a phone in a pocket or bag changes the effective signal level by 5–15 dBm.
Always calibrate thresholds in the actual deployment environment, not in a lab or open space. A single calibration session with a few representative devices is enough to set workable thresholds.

Using calibrate_ble.py to measure RSSI

The calibrate_ble.py script scans for 20 seconds, records RSSI history for every visible device, and then prints summary statistics and suggested thresholds.
1

Copy the script to the Pi and install Bleak

The script requires Bleak. If you have already run install.sh, Bleak is already installed in the virtual environment.
source venv/bin/activate
2

Run the calibration scan

Stand at the distance you want to define as the boundary between two zones, then run:
python3 calibrate_ble.py
While the scan runs, walk toward and away from the Pi with a phone, smartwatch, or other BLE device. The script prints live RSSI readings:
[AA:BB:CC...] Pixel 8                    | RSSI:  -54 dBm | CERCA (1-2m)
[AA:BB:CC...] Pixel 8                    | RSSI:  -71 dBm | MEDIO (2-4m)
3

Read the summary output

After 20 seconds the script prints per-device statistics and suggested thresholds:
UMBRALES RECOMENDADOS PARA config.py

Basado en 142 mediciones:

@dataclass
class ZoneConfig:
    near_threshold: int = -58    # Top 30% señales (cercanas)
    medium_threshold: int = -74  # Top 70% señales (medias)
    # far = resto (peor que -74)
The suggested thresholds are percentile-based: the top 30 % of observed signal strengths define the NEAR boundary, the top 70 % define the MEDIUM boundary.
4

Note the values for your environment

Write down the recommended near_threshold and medium_threshold values. You will use them in the next section.

Updating thresholds via the dashboard

The ThresholdSettings component in the frontend provides a form where you can enter new threshold values and save them. Changes take effect immediately and trigger retroactive reclassification of all stored detections.

Updating thresholds via the API

You can also update thresholds directly with a PUT request to the settings endpoint:
curl -X PUT https://your-backend/api/v1/settings/thresholds \
  -H "Content-Type: application/json" \
  -d '{"near_threshold": -58, "medium_threshold": -74}'
The request body schema (from backend-cloud/src/api/routes/settings.py):
{
  "near_threshold": -58,
  "medium_threshold": -74
}
Both fields are required. The API validates that near_threshold > medium_threshold; if that condition is not met it returns HTTP 400. A successful response includes reclassification statistics:
{
  "message": "Umbrales actualizados correctamente",
  "thresholds": {
    "near_threshold": -58,
    "medium_threshold": -74
  },
  "recalculated": {
    "total_detections": 4821,
    "updated_to_near": 1203,
    "updated_to_medium": 2104,
    "updated_to_far": 1514
  }
}
To reset to defaults (−60 / −75), send:
curl -X POST https://your-backend/api/v1/settings/thresholds/reset
curl https://your-backend/api/v1/settings/thresholds
Response:
{
  "near_threshold": -60,
  "medium_threshold": -75,
  "description": {
    "near": "RSSI ≥ -60 dBm",
    "medium": "-75 dBm ≤ RSSI < -60 dBm",
    "far": "RSSI < -75 dBm"
  }
}

Retroactive reclassification

When you update thresholds, the backend immediately reclassifies every historical detection record in the database using the new boundaries. The recalculated block in the API response tells you how many records moved to each zone.
Reclassification rewrites the zone column for every row in the detections table. On large datasets this can take several seconds. The operation runs inside a database transaction — if it fails, the thresholds are not updated either.
This means historical charts and counts in the dashboard automatically reflect the new thresholds after you save, without any data loss.

The DEVICES_PER_PERSON ratio

The backend estimates the number of people from the number of detected devices using a simple divisor:
estimated_people = detected_devices / DEVICES_PER_PERSON
The default value is 1.5, set in both backend-cloud/src/config.py and in fly.toml:
devices_per_person: float = float(os.getenv("DEVICES_PER_PERSON", "1.5"))
The ratio exists because most people carry more than one BLE-emitting device (phone, smartwatch, earbuds), and because MAC randomization can cause a single device to appear multiple times. A ratio of 1.5 means the system assumes each person is represented by an average of 1.5 detected device entries per scan cycle. When to adjust it:
Increase DEVICES_PER_PERSON. If your venue has many people who carry multiple active BLE devices (smartwatches, earphones) or if MAC randomization is frequent, try values between 2.0 and 3.0.
Decrease DEVICES_PER_PERSON. In venues where many visitors do not carry phones (children’s events, manufacturing floors) or where Bluetooth is commonly disabled, try values closer to 1.0.
Do a manual count in a controlled session: let N known people walk past the Pi, record the raw device count from the API, then calculate ratio = device_count / N. Run this a few times and average the results.
To update the ratio, set the DEVICES_PER_PERSON environment variable on the backend and restart the service.

Build docs developers (and LLMs) love