bluez project – Building a Real Bluetooth Advertisement App on Linux

 

🛍️ Café Bluebite — Part 2
bluez project – Building a Real Bluetooth Advertisement App on Linux
Device Scanning, BD_ADDR Parsing, SDP Capability Check & OBEX Advertisement Push (Steps 2–10)
Step 2–3
File Setup
Step 4–5
Scan & Compare
Step 7
SDP / OPUSH
Steps 8–9
obexftp Push

SEO Keywords:

hcitool inq parse output bash grep compare files bash script sdptool OPUSH check obexftp Bluetooth push RFCOMM channel SDP extract BD_ADDR list comparison new Bluetooth device detection BlueZ bash script tutorial OBEX Object Push shell script cut tail Linux command

Quick Recap of Part 1

In Part 1 we defined the requirements, drew the flowchart, understood SDP service records, and wrote Step 1 which initializes the Bluetooth adapter with the right name and with all inbound scans disabled. Our adapter is now active but invisible — it can reach out but cannot be reached.

In this part we write Steps 2 through 10. These steps form the core loop of the application: creating tracking files, scanning for nearby devices, detecting new arrivals, checking if they can accept a push, and sending the advertisement. Each step is explained in detail with the actual code.

Step 2 — Create the Device Tracking File

Why We Need a “Previous Devices” File

The application runs in an infinite loop. Every 5 seconds it scans for nearby devices and gets a fresh list. The problem is: how do we know if a device on the new list is really new (just walked in) or has been there the whole time (already got the ad)?

We solve this with two files:

previous_bt_devices.txt

Stores the list of devices seen in the last scan iteration. Starts empty. Gets updated at the end of every loop.

new_bt_devices.txt

Stores the list of devices seen in the current scan iteration. Rebuilt every loop by hcitool inq.

Any address that appears in new_bt_devices.txt but NOT in previous_bt_devices.txt is a brand new device — someone who just walked into range. That device gets the advertisement pushed to it.

To create the initial empty file we use a very simple Bash trick:

# The > operator with nothing on the left creates an empty file
# (or truncates an existing one to zero bytes)
> previous_bt_devices.txt

# Equivalent to:
# touch previous_bt_devices.txt  — but touch does not truncate
# echo -n "" > previous_bt_devices.txt — same effect

This one-liner is clean and idiomatic in Bash. No need for touch or echo. On first run the file is empty, which means the comparison in Step 5 will treat every discovered device as “new.”

Step 3 — Initialize the Advertisement Message

Writing the Deal-of-the-Day Text File

The advertisement is a plain text file (advt.txt) that will be pushed to every compatible phone. Using plain text is ideal for OBEX Object Push because all phones can open a .txt file. No special app or format is required.

# Create the advertisement message file
# The first echo uses > to create/overwrite the file
echo "Welcome to the shopping mall !!" > advt.txt

# Subsequent echoes use >> to APPEND (not overwrite) additional lines
echo "Visit Cafe Bluebite for exciting deals" >> advt.txt
echo "Get 15% off if you show this message" >> advt.txt

Bash Redirect Operators — > vs >>
> (Single Greater-Than)
Redirects output to a file.
OVERWRITES any existing content.
If file doesn’t exist: creates it.echo "line 1" > file.txt
→ file.txt contains only “line 1”

>> (Double Greater-Than)
Redirects output to a file.
APPENDS to existing content.
If file doesn’t exist: creates it.echo "line 2" >> file.txt
→ file.txt now has “line 1” + “line 2”

After Step 3 runs, advt.txt contains exactly three lines. This file will remain unchanged throughout the program’s execution — we just keep pushing the same file to every new device we find.

Step 4 — Bluetooth Inquiry Scan and BD_ADDR Extraction

Running hcitool inq and Parsing the Output

This is where the real Bluetooth work begins. We use hcitool inq to discover every Bluetooth device within range (approximately 10 metres). The raw output of hcitool inq looks like this:

Inquiring ...
  68:ED:43:25:0E:99   clock offset: 0x298e   class: 0x7a020c
  00:17:83:DC:72:E9   clock offset: 0x7596   class: 0x1a0114

We only need the BD_ADDR (the hardware address like 68:ED:43:25:0E:99) from each line. The clock offset and class information are not needed for our purpose. So we run two post-processing commands to strip the unwanted data.

Step 4 — Data Pipeline: Raw inq Output → Clean BD_ADDR List
hcitool inq → temp_bt_devices.txt (RAW)
Inquiring …
68:ED:43:25:0E:99   clock offset: 0x298e   class: 0x7a020c
00:17:83:DC:72:E9   clock offset: 0x7596   class: 0x1a0114
↓ tail -n +2 (skip first line “Inquiring …”) ↓
  68:ED:43:25:0E:99   clock offset: 0x298e   class: 0x7a020c
00:17:83:DC:72:E9   clock offset: 0x7596   class: 0x1a0114
↓ cut -c2-18 (keep only columns 2 to 18 = BD_ADDR) ↓
new_bt_devices.txt (CLEAN)
68:ED:43:25:0E:99
00:17:83:DC:72:E9

Here is the actual code for Step 4:

# Step 4: Scan for devices and extract BD_ADDRs

# Run hcitool inquiry, redirect ALL output to temp file
hcitool inq > temp_bt_devices.txt

# Now process the temp file to extract only BD_ADDRs:
#
#  tail -n +2    : Skip the first line ("Inquiring ...")
#                  "-n +2" means "start from line number 2"
#
#  cut -c2-18   : Keep only characters 2 through 18 from each line
#                  Each line starts with 2 spaces, then the 17-char
#                  BD_ADDR (XX:XX:XX:XX:XX:XX = 17 chars)
#                  So columns 2-18 give us exactly the BD_ADDR

tail -n +2 temp_bt_devices.txt | cut -c2-18 > new_bt_devices.txt

# At this point new_bt_devices.txt contains one BD_ADDR per line:
# 68:ED:43:25:0E:99
# 00:17:83:DC:72:E9

# Count how many devices were found (useful for logging)
num_devices=$(cat new_bt_devices.txt | wc -l)
echo "Found $num_devices devices in this iteration"

Why cut -c2-18 and not cut -c1-17? Look at the raw output carefully — there are two leading spaces before each BD_ADDR. Column 1 is a space, column 2 starts the address. A BD_ADDR is 17 characters long (6 pairs of hex digits separated by 5 colons: XX:XX:XX:XX:XX:XX). So columns 2 to 18 (inclusive) give exactly 17 characters = one BD_ADDR.

Step 5 — Detecting New Devices by Comparing the Two Lists

Using grep to Find Addresses That Weren’t There Before

Now we have two files: new_bt_devices.txt (current scan) and previous_bt_devices.txt (last scan). We need to find every address in the new file that is NOT present in the previous file — those are the newly arrived devices.

We use a for loop that iterates over every address in the new file, and for each one, uses grep to check if it exists in the previous file.

Step 5 — How grep Detects New vs Known Devices
new_bt_devices.txt
68:ED:43:25:0E:99
00:17:83:DC:72:E9
11:22:33:44:55:66 ← NEW
for each
address
grep $addr
previous_bt_
devices.txt
Exit status:
0 = found
1 = not found
previous_bt_devices.txt
68:ED:43:25:0E:99
00:17:83:DC:72:E9
68:ED… → FOUND
$? = 0 → SKIP
11:22… → NOT FOUND
$? = 1 → PROCESS ✅
# Step 5: Compare device lists and find new arrivals

# Loop over every BD_ADDR in the current scan file
for line in $(cat new_bt_devices.txt)
do
    # Check if this address was already seen in the previous scan.
    # We redirect grep's output to /dev/null because we don't
    # care about the matching text — only the exit status ($?).
    # $? is 0 if grep found a match, 1 if it didn't.
    grep $line previous_bt_devices.txt > /dev/null

    # If $? equals 1 it means grep did NOT find the address
    # in the previous list — this is a new device!
    if [ "$?" -eq "1" ]
    then
        echo "New Device found: $line"
        # Steps 7, 8, and 9 will be inserted here
    fi
done

Why redirect grep to /dev/null? The grep command does two things: it prints the matching lines AND it sets the exit status variable $?. We only care about the exit status (did the address appear or not?). By redirecting the printed output to /dev/null (Linux’s “trash bin” for unwanted output), we suppress the screen output without losing the exit status that the if check depends on.

Step 7 — The check_sdp Function

Checking if the Device Can Receive an OBEX Push

Now we have a new device’s BD_ADDR. But before pushing anything, we need to verify two things:

  1. Does this device support the OBEX Object Push (OPUSH) service? If not, it cannot receive our file at all.
  2. If it does, which RFCOMM channel number should we connect on? This varies per device.

We package this logic into a reusable bash function called check_sdp. Functions in bash are defined once at the top of the script and called by name later. They can accept arguments (like a BD_ADDR) via $1, $2, etc., and can return exit codes via the return statement.

check_sdp Function — Internal Logic Flow
check_sdp $BD_ADDR
sdptool search --bdaddr $1 OPUSH > temp_sdp.txt
Query the remote device’s SDP server for OPUSH service
grep "OBEX Object Push" temp_sdp.txt
Is the string “OBEX Object Push” in the output? ◇

YES ($retval=0) NO ($retval=1) → return 1 (FAIL)
↓ YES
grep "Channel: " temp_sdp.txt > temp_sdp1.txt
Find the line containing the RFCOMM channel number
channel_num=$(cut -d: -f2 temp_sdp1.txt)
Extract the number after the colon in “Channel: 9”
return 0 (SUCCESS) — $channel_num is set
#################################################################
# function check_sdp
# Input:  $1 = BD_ADDR of the target device
# Output: Returns 0 if the device has OPUSH. Returns 1 otherwise.
#         Also sets the global variable $channel_num if OPUSH found.
# Note:   The channel_num variable is used by push_advt()
#################################################################
function check_sdp ()
{
    # Create/empty the temporary files used for SDP output parsing
    > temp_sdp.txt
    > temp_sdp1.txt

    # Run an SDP search for the OPUSH service on the remote device.
    # $1 is the BD_ADDR passed as the first argument to this function.
    # sdptool will connect to the device, query its SDP server,
    # and print results for any OPUSH service records found.
    sdptool search --bdaddr $1 OPUSH > temp_sdp.txt

    # Check if the text "OBEX Object Push" appears in the output.
    # If the device has the service, sdptool will have printed
    # "Service Name: OBEX Object Push" somewhere in the file.
    grep "OBEX Object Push" temp_sdp.txt > /dev/null
    retval=$?   # Save exit status: 0=found, 1=not found

    if [ $retval -eq 0 ]
    then
        # OPUSH service was found!
        # Now extract the RFCOMM channel number from the output.
        # The channel line looks like:  "      Channel: 9"
        # We filter for lines containing "Channel: " and save to temp_sdp1.txt
        grep "Channel: " temp_sdp.txt > temp_sdp1.txt
        retval=$?

        if [ $retval -eq 0 ]
        then
            # cut -d: -f2  means:
            #   -d:   use colon as the delimiter (field separator)
            #   -f2   take the second field (the part AFTER the colon)
            # So "Channel: 9" → field 2 → " 9"
            # The backtick syntax $(...) runs a command and captures output
            channel_num=`cut -d: -f2 temp_sdp1.txt`

            echo "OBEX Object Push found on Channel Number: $channel_num"
        fi
    fi

    # Return the same status as our grep result:
    # 0 = OPUSH found (success), 1 = not found (failure)
    return $retval
}

How cut -d: -f2 Extracts the Channel Number
Input Line
      Channel: 9
Split by “:” delimiter
Field 1: ” Channel”
Field 2: ” 9″ ← -f2 keeps this
$channel_num
 9
(leading space is ok,
obexftp handles it)

Steps 8 & 9 — Checking the Result and Pushing the Ad

Step 8: Reading check_sdp’s Return Value

After calling check_sdp, we check its return value. A Bash function’s return value is accessible in the special variable $? immediately after the function call. The convention we follow is: 0 = success, 1 = failure — the same convention used by Linux programs everywhere.

# Step 8: Call check_sdp and handle the result
echo "Checking Services on device: $line ..."
check_sdp $line   # $line holds the current device's BD_ADDR

# $? now holds check_sdp's return value
# 0 = OPUSH was found, we can push
# 1 = OPUSH not found, skip this device
if [ "$?" -eq "0" ]
then
    echo "Device supports OPUSH. Pushing advertisement..."
    push_advt $line   # Call Step 9's function
else
    echo "Device does not support OPUSH. Skipping."
fi

Important: You must capture $? immediately after the function call. If you do any other command in between, $? will be overwritten with that command’s exit status. This is a very common bug in shell scripts — always check $? right after the command you care about.

Step 9: The push_advt Function — Using obexftp to Send the File

This function handles the actual file transfer. It uses the obexftp command-line tool which implements the OBEX protocol and handles the full connection, session, and file transfer transparently.

#################################################################
# function push_advt
# Input:  $1 = BD_ADDR of the destination device
# Uses:   $channel_num (set by check_sdp earlier)
# Output: None (obexftp handles success/failure internally)
# Note:   advt.txt must exist in the current directory
#################################################################
function push_advt ()
{
    # Print what we are about to do (useful for debugging)
    echo "Executing: obexftp --bluetooth $1 -B $channel_num -p advt.txt"

    # Run obexftp to push the advertisement file
    # --bluetooth $1    : target device's BD_ADDR
    # -B $channel_num   : RFCOMM channel number to connect on
    #                     (extracted from SDP earlier)
    # -p advt.txt       : -p means "put" (push) this file
    obexftp --bluetooth $1 -B $channel_num -p advt.txt
}

What obexftp Does Internally When You Call It
obexftp –bluetooth 68:ED:43:25:0E:99 -B 9 -p advt.txt
1 Establishes ACL connection to the phone on the default L2CAP PSM
2 Opens RFCOMM connection on channel 9 (from -B flag)
3 Sends OBEX CONNECT request to the phone’s OPUSH server
4 Sends OBEX PUT request with advt.txt as the object body
5 Phone receives the file and shows a popup: “Received advt.txt from Cafe Bluebite — Open?”
6 OBEX DISCONNECT — connection cleaned up, obexftp exits

Step 10 requires no extra code — it is automatically handled by the for loop from Step 5 which already iterates over every new device. Once we finish with one device (steps 7, 8, 9) the loop moves on to the next BD_ADDR in the file. When the loop exhausts all addresses, execution falls through to Step 11.

Steps 2–10 Combined — The Loop Body in Context

How All the Steps Fit Inside the Main Loop

Here is how the code from Steps 2 through 10 fits together as one cohesive unit. Read this top-to-bottom and it should make complete sense:

# === BEFORE THE LOOP (Steps 2 and 3) ===

# Step 2: Create empty tracking file
> previous_bt_devices.txt

# Step 3: Write the advertisement
echo "Welcome to the shopping mall !!" > advt.txt
echo "Visit Cafe Bluebite for exciting deals" >> advt.txt
echo "Get 15% off if you show this message" >> advt.txt

# === INSIDE THE INFINITE LOOP (Steps 4–11 repeat forever) ===

# Step 4: Discover devices and clean up the output
hcitool inq > temp_bt_devices.txt
tail -n +2 temp_bt_devices.txt | cut -c2-18 > new_bt_devices.txt

num_devices=$(cat new_bt_devices.txt | wc -l)
echo "Found $num_devices devices in this iteration"

# Step 5: Loop over every found device
for line in $(cat new_bt_devices.txt)
do
    # Check if this address existed in the previous scan
    grep $line previous_bt_devices.txt > /dev/null

    # Step 6: Is it a new device? ($? eq 1 means NOT found = new)
    if [ "$?" -eq "1" ]
    then
        echo "New Device found: $line"

        # Step 7: SDP check — does it support OPUSH?
        echo "Checking Services..."
        check_sdp $line

        # Step 8: Was OPUSH found?
        if [ "$?" -eq "0" ]
        then
            # Step 9: Push the advertisement
            echo "Device supports OPUSH. Pushing advertisement..."
            push_advt $line
        else
            echo "Device does not support OPUSH. Skipping."
        fi

    fi
    # Step 10: Loop continues automatically to next device
done

# Step 11: Save current list and pause (covered in Part 3)

Common Student Questions Answered

Q: What if obexftp fails to connect — does the script crash?

No. obexftp will print an error message but exits with a non-zero status. Our script does not check obexftp’s exit status (it could, using another $? check). The for loop just continues to the next device. In a production app you would add retry logic.

Q: What if the same phone appears in two consecutive scans?

At Step 11 (next part) we copy temp_bt_devices.txt to previous_bt_devices.txt. So the next iteration’s grep check will find that address in the previous file and the if block will NOT execute. The phone only gets one push.

Q: What does “channel_num” look like — does the space cause problems?

The cut command extracts 9 (with a leading space). When you pass -B $channel_num to obexftp, the shell expands it to -B 9. obexftp handles this fine because it treats 9 as a number and ignores surrounding whitespace. You can also trim it with: channel_num=$(echo $channel_num | tr -d ' ')

Q: Why is there no Step 6 code? The flowchart shows Step 6.

The flowchart’s Step 6 decision (“New device found?”) is implemented by the if [ "$?" -eq "1" ] check inside the for loop. The flowchart uses Steps 6 and 10 to represent loop decisions — in code these map to the if statement and the natural iteration of the for loop respectively.

Part 2 Complete!

You now understand every step of the scanning, comparison, SDP check, and OBEX push logic. In Part 3 we cover Step 11, assemble the complete Bluebite.sh script, and review the disclaimer and summary.

Part 3: Complete Script & Summary → ← Back to Part 1

Leave a Reply

Your email address will not be published. Required fields are marked *