Turn a Linux laptop into a Bluetooth call deck for an Android phone.
CallScoot makes the laptop behave like a local call console:
- Android phone sends call audio to the laptop over Bluetooth HFP/HSP
- laptop microphone audio goes back to the phone
- laptop speakers play the remote caller
- optional echo cancellation is enabled by default
- optional ADB helpers can dial / answer / hang up Android calls
- optional SIP telephony mode can register to a SIP server and place calls directly
- optional call policies can auto-answer / auto-reject incoming calls
- optional real-time voice agent mode can run inside the call
- local HTTP API makes it easy for another app to use CallScoot as a call runtime
- runs as a headless user service on an always-on Linux box
This project is Linux-first and tested against:
- Debian 13 / BlueZ 5.82
- PipeWire 1.4.x
- WirePlumber 0.5.x
- Android phone over Bluetooth HFP/HSP
See also:
docs/WHAT-THIS-REPO-DOES-TODAY.mddocs/CALL-AUTOMATION.mddocs/AI-AGENT-INTEGRATION.mddocs/AI-VOICE-ROUTING.mddocs/AI-AGENT-RUNBOOK.mddocs/API.mddocs/SIP.mddocs/DEPLOYMENT.mdexamples/lead_campaign_app.pyexamples/minimal_client_app.py
As shipped today, CallScoot is primarily a Linux-side Bluetooth call audio bridge with an optional SIP telephony backend.
It is useful when all of these are true:
- your Android phone is paired to the laptop over Bluetooth
- Android is allowed to use the laptop for call audio
- Linux exposes the phone as an HFP/HSP Bluetooth audio device
- you want the laptop microphone + laptop speakers to be the active call endpoint
When those conditions are met, CallScoot does this automatically:
- watches PipeWire / BlueZ for a live Bluetooth HFP/HSP source + sink pair
- switches the Bluetooth card to a headset / hands-free profile when possible
- creates an audio graph so that:
- phone call audio plays on the laptop speakers
- laptop mic audio goes back to the phone
- optionally inserts echo cancellation in the middle
- removes the bridge again when the Bluetooth call route disappears
In short:
This repo turns a Linux laptop into a local Bluetooth speakerphone / headset bridge for Android calls.
This repository intentionally does not do these things yet:
- no Android companion app
- no contact sync
- no voice command parser like "call Ahmet"
- no custom Bluetooth telephony stack
- no raw phone-call audio capture over ADB
- no GUI
Optional ADB commands are included only as convenience helpers for dialing / answering / hanging up. The actual call audio path is still Bluetooth HFP/HSP.
The goal is simple:
Keep an old laptop on at home, pair it with your Android phone, and use the laptop's mic + speakers as the call endpoint.
No cloud service. No vendor lock-in. No proprietary phone desktop suite.
CallScoot does not try to capture protected phone-call audio over ADB. Instead it uses the standard path that car kits and headsets use:
Android call audio <-> Bluetooth HFP/HSP <-> Linux laptop
Then on Linux it builds this PipeWire/PulseAudio graph:
phone BT source -> echo-cancel sink -> laptop speakers
laptop mic/AEC -> loopback -> phone BT sink
So the phone keeps the actual cellular/VoIP call, while the laptop becomes the live microphone/speaker endpoint.
- Auto-detects Bluetooth HFP/HSP device pairs and stream-based call routes
- Auto-switches BlueZ cards to an HFP profile when possible
- Echo cancellation via
module-echo-cancel - Two loopbacks via
module-loopback - Headless-friendly WirePlumber config (
seat-monitoring = disabled) - Systemd user service for always-on usage
- Optional ADB call helpers, active-device auto-selection, and auto-answer support
- Optional SIP backend using
pjsua2with configurable server / username / password / transport - Incoming-call policy engine: allowlist / blocklist / business hours
- Per-call session logs under
~/.local/state/callscoot/calls/ - ElevenAgents real-time voice agent mode
- Local HTTP API for outbound call orchestration and event streaming
- Zero-dependency Python client helper for external apps (
src/callscoot_client.py) - Optional
callscoot-agentpipeline with OpenAI / Ollama / whisper-cli / espeak / mock providers
| Command | What it does |
|---|---|
callscoot pair |
Opens a temporary Bluetooth pairing window on the laptop |
callscoot devices |
Lists paired Bluetooth devices |
callscoot trust MAC |
Marks a paired device as trusted in BlueZ |
callscoot connect MAC |
Tries to connect the paired Bluetooth device |
callscoot configure ... |
Saves the target device / local audio / latency / ADB or SIP preferences |
callscoot up |
Builds the audio bridge immediately if a phone HFP/HSP route exists |
callscoot down |
Removes the bridge modules |
callscoot daemon |
Runs the background watcher that auto-builds the bridge |
callscoot status |
Prints config, selected devices, BlueZ devices, PipeWire route info, service state |
callscoot logs -f |
Follows daemon logs |
callscoot calls |
Lists recent call sessions |
callscoot call-show ID |
Shows one call session including transcript if present |
callscoot dial NUMBER |
Starts a call through the selected telephony backend (adb or sip) |
callscoot answer |
Answers through the selected telephony backend |
callscoot hangup |
Hangs up through the selected telephony backend |
callscoot-agent bootstrap-audio |
Creates the AI virtual sinks and points CallScoot at them |
callscoot-agent run |
Runs the AI call agent |
callscoot-api |
Runs the local HTTP API for external apps |
bin/callscoot launcher
src/callscoot.py main CLI + daemon
src/callscoot_agent.py AI call agent launcher
src/callscoot_api.py local HTTP API for external apps
src/callscoot_client.py zero-dependency Python client helper
src/sip_backend.py optional SIP telephony backend
src/agent_orchestrator.py ElevenAgents runtime
src/audio_bridge.py PulseAudio capture/playback bridge
src/agent_events.py structured per-call event log writer
src/agent_memory.py local SQLite memory/profile store
src/agent_control.py pending call requests + command queue
scripts/install-system.sh root/system package setup
scripts/install-user.sh user install + systemd services
systemd/callscoot-daemon.service
systemd/callscoot-agent.service
systemd/callscoot-api.service
config/10-callscoot-bluetooth.conf
CallScoot is meant to run as a headless runtime on the Linux machine that is paired to the Android phone.
Your own app should sit on top of the local API.
client app
-> CallScoot local API
-> CallScoot runtime services
-> ElevenAgents
-> Android phone over Bluetooth HFP/HSP
Recommended default:
- run CallScoot and your client app on the same Linux machine
- let the client app call
http://127.0.0.1:8788 - keep UI / business logic in the client app
- keep Bluetooth / telephony / session lifecycle in CallScoot
See docs/DEPLOYMENT.md for the full deployment model.
sudo ./scripts/install-system.sh "$USER"This installs:
- BlueZ
- PipeWire
- WirePlumber
- Bluetooth SPA plugins
- PulseAudio compatibility tools
- ADB
- jq
- espeak-ng
It also enables bluetooth.service and loginctl enable-linger for the target user.
./scripts/install-user.shThis installs:
~/.local/bin/callscoot~/.local/bin/callscoot-agent~/.local/bin/callscoot-api~/.local/lib/callscoot/*.py~/.config/systemd/user/callscoot-daemon.service~/.config/systemd/user/callscoot-agent.service~/.config/systemd/user/callscoot-api.service~/.config/wireplumber/wireplumber.conf.d/10-callscoot-bluetooth.conf
And it restarts PipeWire/WirePlumber and enables:
callscoot-daemon.servicecallscoot-agent.servicecallscoot-api.service
Open a pairing window on the laptop:
callscoot pairThen on the phone:
- Open Bluetooth settings
- Pair with the laptop
- Trust / allow call audio on Android if asked
After pairing:
callscoot devicesIf you want to pin CallScoot to one specific phone:
callscoot configure --device AA:BB:CC:DD:EE:FFYou can also trust and reconnect manually:
callscoot trust AA:BB:CC:DD:EE:FF
callscoot connect AA:BB:CC:DD:EE:FFThe daemon is the normal mode:
systemctl --user status callscoot-daemon.service
callscoot logs -fCallScoot can be used as a local call runtime by another application.
Start / inspect the API service:
systemctl --user status callscoot-api.service
curl http://127.0.0.1:8788/v1/healthQueue an outbound call with injected call context:
curl -X POST http://127.0.0.1:8788/v1/outbound-calls \
-H 'Content-Type: application/json' \
-d '{
"number": "+905551112233",
"dynamic_variables": {
"campaign_name": "lead_qualification",
"contact_name": "Efe"
},
"metadata": {
"lead_id": "lead-123"
}
}'Stream structured events from the active session:
curl -N 'http://127.0.0.1:8788/v1/events/stream?session_id=current'See docs/API.md for the full external-app integration flow.
If you are writing a Python client app, start from:
src/callscoot_client.pyexamples/minimal_client_app.pyexamples/lead_campaign_app.py
When the Android phone exposes an HFP/HSP audio route, CallScoot automatically builds the bridge.
callscoot up
callscoot downIf you want to target a specific phone manually:
callscoot up --device AA:BB:CC:DD:EE:FFShow current config:
callscoot configureExamples:
callscoot configure --device AA:BB:CC:DD:EE:FF
callscoot configure --echo-cancel on
callscoot configure --latency 40
callscoot configure --sink alsa_output.pci-0000_00_1f.3.analog-stereo
callscoot configure --source alsa_input.pci-0000_00_1f.3.analog-stereo
callscoot configure --adb-serial 192.168.1.50:5555
callscoot configure --auto-answer on
callscoot configure --auto-answer-delay 2
callscoot configure --max-call-duration 600
callscoot configure --auto-select-device on
callscoot configure --policy-mode allowlist --allow-caller +905551112233 --unknown-callers deny --auto-reject on
callscoot configure --business-hours 09:00-18:00 --business-days mon,tue,wed,thu,friClear pinned values:
callscoot configure --clear-device
callscoot configure --clear-adb-serialcallscoot configure \
--telephony-backend sip \
--sip-server sip.example.com \
--sip-username 1001 \
--sip-password supersecret \
--sip-port 5060 \
--sip-transport udp \
--sip-audio-mode directOptional SIP audio device overrides (direct mode):
callscoot configure --sip-playback-device pipewire
callscoot configure --sip-capture-device pipewireEnable SIP + AI agent routing:
callscoot configure --sip-audio-mode agent
systemctl --user restart callscoot-api.service callscoot-agent.serviceRemove SIP settings and switch back to Android/ADB mode:
callscoot configure --clear-sipSee docs/SIP.md for details.
If your Android device is already reachable with ADB:
callscoot dial +905551112233
callscoot answer
callscoot hangup
callscoot configure --auto-answer onYou can also delay auto-answer slightly so Bluetooth audio has time to settle:
callscoot configure --auto-answer-delay 2If you want long calls to be cut off automatically, set a maximum call duration in seconds:
callscoot configure --max-call-duration 600Use 0 to disable the limit.
And you can apply call policies:
callscoot configure --policy-mode blocklist --block-caller '*4443322' --auto-reject onThese are convenience helpers only. The actual audio path for calls should still be Bluetooth HFP/HSP.
For an always-on desk-phone style setup:
callscoot configure \
--auto-select-device on \
--auto-answer on \
--auto-answer-delay 2 \
--max-call-duration 600 \
--policy-mode allow_all \
--log-calls on
systemctl --user restart callscoot-daemon.serviceMore advanced examples are in docs/CALL-AUTOMATION.md.
Create the AI virtual sinks:
callscoot-agent bootstrap-audio
systemctl --user restart callscoot-daemon.serviceConfigure a provider stack, for example:
callscoot-agent configure \
--stt-provider mock \
--llm-provider mock \
--tts-provider mockSmoke-test the pipeline:
callscoot-agent reply "Merhaba"Run the continuous agent loop:
callscoot-agent runFor provider details, see docs/AI-AGENT-RUNBOOK.md.
callscoot statusIt prints:
- current config
- active bridge module IDs
- default sink/source
- selected Bluetooth / ADB devices
- active call policy snapshot
- detected Bluetooth call-audio routes (
bluez_pairs) - BlueZ cards and active profiles
- paired/connected Bluetooth devices
- ADB devices
- current ADB call info
- active call session
- systemd user service status
Useful logs:
callscoot logs -f
journalctl --user -u callscoot-daemon.service -fUseful low-level checks:
pactl list short cards
pactl list short sinks
pactl list short sources
bluetoothctl devices Paired
bluetoothctl devices ConnectedThe intended split is:
- CallScoot = runtime / telephony substrate / local API
- your app = UI / CRM logic / campaign logic / reporting
If you are building a Python app, you can vendor or import:
src/callscoot_client.py
Minimal example:
from callscoot_client import CallScootClient
client = CallScootClient(base_url="http://127.0.0.1:8788")
client.health()
client.queue_outbound_call(
"+905551112233",
dynamic_variables={"campaign_name": "survey"},
metadata={"lead_id": "42"},
)
session_id = client.wait_for_session_start()
session = client.wait_for_session_end(session_id)
print(session["meta"].get("summary"))For the full integration shape, see:
- On Android, make sure the Bluetooth device is allowed for call audio
- During a call, Android may require selecting the Bluetooth device as the active audio route once
- Echo cancellation is enabled by default, but physical speaker volume still matters
- Headphones will always sound cleaner than laptop speakers, but speakerphone mode works
- If more than one Bluetooth audio device is around, set
--device/target_device
- Audio stays local between your laptop and your phone
- No cloud relay
- No remote SaaS dependency
- ADB helpers are optional and local-only
MIT