| asterisk | ||
| recordings | ||
| scripts | ||
| sms-api-data | ||
| sms_api | ||
| sounds | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile.ast23 | ||
| Dockerfile.sms_api | ||
| README.md | ||
SIM7600 (LTE/VoLTE) Asterisk Gateway
Minimal, reproducible setup for a SIM7600G-H on Unraid using Asterisk 23 + chan_quectel.
This configuration matches the working hardware setup we validated.
What’s included
Dockerfile.ast23buildschan_quectelwith SIM7600 CLCC-noise patchdocker-compose.ymlruns a single Asterisk container (asterisk-sim7600)docker-compose.ymlalso runs an SMS HTTP API (sim7600-sms-api)asterisk/configs for SIP + dialplan + modemrecordings/bind-mount for call recordingssounds/bind-mount for custom audiosms-api-data/bind-mount for persisted chat/message history
Requirements
- SIM7600 in serial-audio mode (PID
9011) /dev/ttyUSB0..4present/dev/serial/by-id/*present
If your by-id paths differ, update asterisk/quectel.conf.
Quick start
cp .env.example .env
docker compose up -d --build
Validate device:
docker exec asterisk-sim7600 asterisk -rx "module show like quectel"
docker exec asterisk-sim7600 asterisk -rx "quectel show devices"
Validate the SMS API:
curl http://localhost:${SMS_API_PORT:-8080}/healthz
Known‑good audio settings (SIM7600)
Working combo used:
audio=...-if06-port0data=...-if04-port0slin16=yes- AT commands before playback/record:
AT+CHFA=0AT+CMICGAIN=3AT+COUTGAIN=8
These are wired into the dialplan for playback + recording.
Dialplan shortcuts
Record a call to your phone
docker exec asterisk-sim7600 asterisk -rx \
"originate Quectel/quectel0/1XXXXXXXXXX extension s@record-call"
Recording saved to:
./recordings/recording-YYYYMMDD-HHMMSS.wav
Play custom audio to a call
- Put an 8kHz mono PCM WAV into
./sounds/(example:custom.wav). - Call with:
docker exec asterisk-sim7600 asterisk -rx \
"originate Quectel/quectel0/1XXXXXXXXXX extension s@play-audio"
By default it plays hello-world. To play your custom file, set:
AUDIO_FILE=custom/custom
(or edit the dialplan to hardcode your file).
To convert a WAV to 8k mono:
ffmpeg -i input.wav -ac 1 -ar 8000 -acodec pcm_s16le custom.wav
Dialplan examples (current)
These live in asterisk/extensions_custom.conf.
play-audio
[play-audio]
exten => s,1,Answer()
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CHFA=0")
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CMICGAIN=3")
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+COUTGAIN=8")
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CPCMREG=1")
same => n,Set(AUDIO_FILE=${IF($["${AUDIO_FILE}"=""]?hello-world:${AUDIO_FILE})})
same => n,Wait(1)
same => n,Playback(${AUDIO_FILE})
same => n,Wait(1)
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CPCMREG=0")
same => n,Hangup()
record-call
[record-call]
exten => s,1,Answer()
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CHFA=0")
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CMICGAIN=3")
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+COUTGAIN=8")
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CPCMREG=1")
same => n,Set(RECFILE=/var/spool/asterisk/monitor/recording-${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}.wav)
same => n,MixMonitor(${RECFILE})
same => n,Wait(600)
same => n,System(/usr/sbin/asterisk -rx "quectel cmd quectel0 AT+CPCMREG=0")
same => n,Hangup()
SIP endpoint (direct config)
Edit asterisk/pjsip_custom.conf (default user pyclient / pyclientpass), then:
docker exec asterisk-sim7600 asterisk -rx "pjsip reload"
Use the [from-pjsip] rules in asterisk/extensions_custom.conf (PJSIP listens on UDP 5160).
SMS HTTP API
The repo now includes a FastAPI service that wraps outbound quectel sms and stores chats/messages in SQLite.
It is intentionally SMS-only for now:
- one sender line (
SMS_DEVICE, defaultquectel0) - one recipient per chat
- plain text only
- outbound messages are queued through Asterisk AMI
- inbound messages are pushed into the same store from the
[quectel-incoming]dialplan - realtime delivery is available through webhooks, server-sent events, and WebSockets
Auth
Set these in .env before exposing the API:
SMS_API_BEARER_TOKEN=replace-me
SMS_API_INTERNAL_TOKEN=replace-me-too
If you change SMS_API_INTERNAL_TOKEN, update SMS_API_INTERNAL_TOKEN in asterisk/extensions_custom.conf to match before reloading the dialplan.
Browser clients can also use access_token=<SMS_API_BEARER_TOKEN> on the SSE and WebSocket endpoints when they cannot set an Authorization header directly.
Endpoints
GET /healthzGET /v3/phone_numbersGET /v3/chatsPOST /v3/chatsGET /v3/chats/{chatId}GET /v3/chats/{chatId}/messagesPOST /v3/chats/{chatId}/messagesGET /v3/webhooksPOST /v3/webhooksDELETE /v3/webhooks/{webhookId}GET /v3/events/streamGET /v3/events/ws(WebSocket)
Interactive docs:
open http://localhost:${SMS_API_PORT:-8080}/docs
Example: list phone numbers
curl http://localhost:${SMS_API_PORT:-8080}/v3/phone_numbers \
-H "Authorization: Bearer ${SMS_API_BEARER_TOKEN}"
Example: create a chat and send the first SMS
curl http://localhost:${SMS_API_PORT:-8080}/v3/chats \
-H "Authorization: Bearer ${SMS_API_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"from": "+17203454122",
"to": ["+17208828227"],
"message": {
"parts": [
{ "type": "text", "value": "hello from the SIM7600 gateway" }
],
"idempotency_key": "demo-chat-1"
}
}'
Example: send a follow-up SMS
curl http://localhost:${SMS_API_PORT:-8080}/v3/chats/<chat-id>/messages \
-H "Authorization: Bearer ${SMS_API_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"message": {
"parts": [
{ "type": "text", "value": "follow-up message" }
],
"idempotency_key": "demo-chat-2"
}
}'
Example: subscribe a webhook for live message events
Supported event types are:
message.inboundmessage.outbound
If event_types is omitted, the subscription receives both.
curl http://localhost:${SMS_API_PORT:-8080}/v3/webhooks \
-H "Authorization: Bearer ${SMS_API_BEARER_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://example.com/hooks/sms",
"event_types": ["message.inbound"],
"secret": "replace-me"
}'
Webhook deliveries are POSTed as JSON and include:
X-SMS-API-Event-IdX-SMS-API-Event-TypeX-SMS-API-Signature-256: sha256=<hex>when a secret is configured
Example: stream events over SSE
If events is omitted, the stream includes both supported message event types.
curl -N "http://localhost:${SMS_API_PORT:-8080}/v3/events/stream?access_token=${SMS_API_BEARER_TOKEN}&events=message.inbound"
SSE payloads use standard event framing:
id: <event-id>
event: message.inbound
data: {"id":"<event-id>","type":"message.inbound","occurred_at":"...","chat":{...},"message":{...}}
Example: stream events over WebSocket
python3 - <<'PY'
import asyncio
import json
import websockets
async def main():
uri = "ws://localhost:8080/v3/events/ws?access_token=change-me&events=message.inbound"
async with websockets.connect(uri) as websocket:
while True:
print(json.loads(await websocket.recv()))
asyncio.run(main())
PY
WebSocket clients receive the same JSON event envelope as webhooks/SSE, plus periodic system.keepalive messages when the stream is idle.
Notes
- The API normalizes 10-digit US numbers to
+1XXXXXXXXXX. - Outbound send currently supports single-line text only.
GET /v3/chatsandGET /v3/chats/{chatId}/messagesuse simple offset cursors.- Message history persists in
./sms-api-data/sms_api.db. - Webhooks, SSE, and WebSocket streams fire for both inbound and outbound stored messages unless filtered with
event_types/events.
PJSIP + Python control (dial + play + record)
- Put an 8kHz mono PCM WAV into
./sounds/(exampleintro.wav). - Reload PJSIP + dialplan after edits:
docker exec asterisk-sim7600 asterisk -rx "pjsip reload"
docker exec asterisk-sim7600 asterisk -rx "dialplan reload"
- Run the Python script from your machine:
python3 scripts/pjsip_call.py --server <unraid-ip> --number 17208828227 --audio-file custom/intro
Recording saved to:
./recordings/pjsip-<number>-YYYYMMDD-HHMMSS.wav
Notes:
--audio-fileis optional; default ishello-world.- The script uses PJSIP Python bindings (
pjsua2). Install/build PJSIP with Python support before running the script.
Notes
Dockerfile.ast23patcheschan_quectelto ignore SIM7600 “VOICE CALL: BEGIN/END” noise in CLCC parsing.- If audio is silent, verify the
if06audio port and the AT gain commands above.