Handle dial failures with error tone and return to dial tone

When a dial command times out (e.g., due to SIP registration failure):
- Play an error tone (fast busy signal)
- Return to ready state (dial tone resumes)

This provides feedback to the user when calls can't be placed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Corey Johnson 2026-01-21 14:55:32 -08:00
parent 2428afd3db
commit bed1fa0eb8
2 changed files with 32 additions and 5 deletions

View File

@ -119,6 +119,11 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
phoneService.send({ type: "remote-hang-up" })
})
baresip.dialFailed.on(({ reason }) => {
log.error("🐻 dial failed:", reason)
phoneService.send({ type: "dial-failed" } as any)
})
baresip.connect().catch((error) => {
log.error("🐻 connection error:", error)
phoneService.send({ type: "error", message: error.message })
@ -342,6 +347,16 @@ const answerCall = (ctx: PhoneContext) => {
ctx.baresip.accept()
}
const playDialFailedMessage = async () => {
log.error("📞 Call failed - playing error tone")
// Play fast busy signal (reorder tone)
const errorTone = await player.playTone([480, 620], 500)
await sleep(500)
errorTone.stop()
await player.playTone([480, 620], 500)
await sleep(500)
}
const makeCall = async (ctx: PhoneContext) => {
log(`Dialing number: ${ctx.numberDialed}`)
if (ctx.numberDialed === 1) {
@ -455,8 +470,9 @@ const phoneMachine = createMachine(
t("dial-stop", "outgoing"),
t("digit_increment", "dialing", r(digitIncrement)),
t("hang-up", "idle")),
outgoing: invoke(makeCall,
outgoing: invoke(makeCall,
t("answered", "connected"),
t("dial-failed", "ready", a(playDialFailedMessage)),
t("hang-up", "idle", a(hangUp))),
aborted: state(
t("hang-up", "idle")),

View File

@ -8,6 +8,7 @@ export class Baresip {
callEstablished = new Emitter<{ contact: string }>()
callReceived = new Emitter<{ contact: string }>()
hungUp = new Emitter()
dialFailed = new Emitter<{ reason: string }>()
error = new Emitter<{ message: string; statusCode?: string; reason?: string }>()
registrationSuccess = new Emitter()
@ -39,8 +40,11 @@ export class Baresip {
executeCommand("a")
}
dial(phoneNumber: string) {
executeCommand(`d${phoneNumber}`)
async dial(phoneNumber: string) {
const success = await executeCommand(`d${phoneNumber}`)
if (!success) {
this.dialFailed.emit({ reason: "Command timed out - registration may have failed" })
}
}
hangUp() {
@ -52,6 +56,7 @@ export class Baresip {
this.callReceived.removeAllListeners()
this.hungUp.removeAllListeners()
this.registrationSuccess.removeAllListeners()
this.dialFailed.removeAllListeners()
this.error.removeAllListeners()
}
@ -113,15 +118,21 @@ export class Baresip {
}
}
const executeCommand = async (command: string) => {
const executeCommand = async (command: string): Promise<boolean> => {
try {
const url = new URL(`/?${command}`, "http://127.0.0.1:8000")
const response = await Bun.fetch(url)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000)
const response = await fetch(url, { signal: controller.signal })
clearTimeout(timeout)
if (!response.ok) {
throw new Error(`Error executing command: ${response.statusText}`)
}
return true
} catch (error) {
log.error("Failed to execute command:", error)
return false
}
}