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" }) 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) => { baresip.connect().catch((error) => {
log.error("🐻 connection error:", error) log.error("🐻 connection error:", error)
phoneService.send({ type: "error", message: error.message }) phoneService.send({ type: "error", message: error.message })
@ -342,6 +347,16 @@ const answerCall = (ctx: PhoneContext) => {
ctx.baresip.accept() 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) => { const makeCall = async (ctx: PhoneContext) => {
log(`Dialing number: ${ctx.numberDialed}`) log(`Dialing number: ${ctx.numberDialed}`)
if (ctx.numberDialed === 1) { if (ctx.numberDialed === 1) {
@ -455,8 +470,9 @@ const phoneMachine = createMachine(
t("dial-stop", "outgoing"), t("dial-stop", "outgoing"),
t("digit_increment", "dialing", r(digitIncrement)), t("digit_increment", "dialing", r(digitIncrement)),
t("hang-up", "idle")), t("hang-up", "idle")),
outgoing: invoke(makeCall, outgoing: invoke(makeCall,
t("answered", "connected"), t("answered", "connected"),
t("dial-failed", "ready", a(playDialFailedMessage)),
t("hang-up", "idle", a(hangUp))), t("hang-up", "idle", a(hangUp))),
aborted: state( aborted: state(
t("hang-up", "idle")), t("hang-up", "idle")),

View File

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