wip
This commit is contained in:
parent
a0cc0c85cf
commit
843f3680e1
664
src/phone.ts
664
src/phone.ts
|
|
@ -5,6 +5,9 @@ import { sleep } from "bun"
|
||||||
import { processStderr, processStdout } from "./utils/stdio"
|
import { processStderr, processStdout } from "./utils/stdio"
|
||||||
import Buzz from "./buzz"
|
import Buzz from "./buzz"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
import { GPIO } from "./pins"
|
||||||
|
import { Agent } from "./agent"
|
||||||
|
import { searchWeb } from "./agent/tools"
|
||||||
|
|
||||||
type CancelableTask = () => void
|
type CancelableTask = () => void
|
||||||
|
|
||||||
|
|
@ -19,327 +22,424 @@ type PhoneContext = {
|
||||||
cancelAgent?: CancelableTask
|
cancelAgent?: CancelableTask
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pins = {
|
const gpio = new GPIO({ resetOnClose: true })
|
||||||
ringer: 17,
|
using ringer = gpio.output(17)
|
||||||
hook: 27,
|
using inputs = gpio.inputGroup({
|
||||||
rotaryInUse: 22,
|
hook: { pin: 27, debounce: 50 },
|
||||||
rotaryNumber: 23,
|
rotaryInUse: { pin: 22, debounce: 50 },
|
||||||
}
|
rotaryNumber: { pin: 23, debounce: 10 },
|
||||||
|
})
|
||||||
|
|
||||||
export const startPhone = async () => {
|
export const startPhone = async (agentId: string, apiKey: string) => {
|
||||||
await Buzz.setVolume(0.4)
|
await Buzz.setVolume(0.4)
|
||||||
|
log.info(`📞 Hook ${inputs.pins.hook.value}`)
|
||||||
|
await handleInputEvents()
|
||||||
|
}
|
||||||
|
|
||||||
const baresipConfig = join(import.meta.dir, "..", "baresip")
|
const handleInputEvents = async () => {
|
||||||
const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
|
let digit = 0
|
||||||
|
for await (const event of inputs.events()) {
|
||||||
baresip.registrationSuccess.connect(async () => {
|
switch (event.pin) {
|
||||||
log.info("🐻 server connected")
|
case "hook":
|
||||||
const result = await gpio.get(pins.hook)
|
const type = event.value == 0 ? "hang_up" : "pick_up"
|
||||||
if (result.state === "low") {
|
log.info(`📞 Hook ${event.value} sending ${type}`)
|
||||||
phoneService.send({ type: "initialized" })
|
if (type === "hang_up") {
|
||||||
|
ringer.value = 1
|
||||||
} else {
|
} else {
|
||||||
phoneService.send({ type: "pick_up" })
|
ringer.value = 0
|
||||||
}
|
}
|
||||||
})
|
break
|
||||||
|
|
||||||
baresip.callReceived.connect(({ contact }) => {
|
case "rotaryInUse":
|
||||||
log.info(`🐻 incoming call from ${contact}`)
|
if (event.value === 0) {
|
||||||
phoneService.send({ type: "incoming_call", from: contact })
|
digit = 0
|
||||||
})
|
} else {
|
||||||
|
log.info(`📞 Dialed digit: ${digit}`)
|
||||||
baresip.callEstablished.connect(({ contact }) => {
|
|
||||||
log.info(`🐻 call established with ${contact}`)
|
|
||||||
phoneService.send({ type: "answered" })
|
|
||||||
})
|
|
||||||
|
|
||||||
baresip.hungUp.connect(() => {
|
|
||||||
log.info("🐻 call hung up")
|
|
||||||
phoneService.send({ type: "remote_hang_up" })
|
|
||||||
})
|
|
||||||
|
|
||||||
baresip.connect().catch((error) => {
|
|
||||||
log.error("🐻 connection error:", error)
|
|
||||||
phoneService.send({ type: "error", message: error.message })
|
|
||||||
})
|
|
||||||
|
|
||||||
baresip.error.connect(async ({ message }) => {
|
|
||||||
log.error("🐻 error:", message)
|
|
||||||
phoneService.send({ type: "error", message })
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
await ring(500)
|
|
||||||
await sleep(250)
|
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "rotaryNumber":
|
||||||
|
if (event.value === 1) {
|
||||||
|
digit += 1
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.error(`📞 Unknown pin event: ${event.pin}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = process.env.ELEVEN_API_KEY
|
||||||
|
const agentId = process.env.ELEVEN_AGENT_ID
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error("❌ Error: ELEVEN_API_KEY environment variable is required")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
}
|
||||||
|
|
||||||
let agentProcess: Bun.Subprocess
|
if (!agentId) {
|
||||||
|
console.error(
|
||||||
const initializeAgent = () => {
|
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required"
|
||||||
agentProcess = Bun.spawn({
|
|
||||||
cwd: "/home/corey/code/tone/packages/py-agent",
|
|
||||||
cmd: ["/home/corey/.local/bin/uv", "run", "main.py"],
|
|
||||||
stdin: "pipe",
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
env: { ...process.env, PYTHONUNBUFFERED: "1" },
|
|
||||||
})
|
|
||||||
|
|
||||||
log.info("☎️ Started agent process", agentProcess.pid)
|
|
||||||
|
|
||||||
processStdout(agentProcess, (line) => {
|
|
||||||
log.info(`🐍 ${line}`)
|
|
||||||
if (line === "Starting agent session") {
|
|
||||||
log.info(`💎 HEY! THE AGENT STARTED`)
|
|
||||||
} else if (line.startsWith("Conversation ended.")) {
|
|
||||||
phoneService.send({ type: "remote_hang_up" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
processStderr(agentProcess)
|
|
||||||
|
|
||||||
agentProcess.exited.then((code) => {
|
|
||||||
log.error(`💥 Agent process exited with code ${code}`)
|
|
||||||
phoneService.send({ type: "remote_hang_up" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeAgent()
|
|
||||||
|
|
||||||
const startAgent = () => {
|
|
||||||
log.info("☎️ Starting agent conversation")
|
|
||||||
|
|
||||||
if (agentProcess?.stdin) {
|
|
||||||
agentProcess.stdin.write("start\n")
|
|
||||||
} else {
|
|
||||||
log.error("☎️ No agent process stdin available")
|
|
||||||
phoneService.send({ type: "remote_hang_up" })
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
log.info("☎️ Stopping agent conversation")
|
|
||||||
if (agentProcess?.stdin) {
|
|
||||||
agentProcess.stdin.write("stop\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = (initial?: Partial<PhoneContext>): PhoneContext => ({
|
|
||||||
numberDialed: 0,
|
|
||||||
baresip,
|
|
||||||
startAgent,
|
|
||||||
...initial,
|
|
||||||
})
|
|
||||||
|
|
||||||
const phoneMachine = createMachine(
|
|
||||||
"initializing",
|
|
||||||
// prettier-ignore
|
|
||||||
{
|
|
||||||
initializing: state(
|
|
||||||
transition("initialized", "idle"),
|
|
||||||
transition("pick_up", "ready", reduce(playDialTone)),
|
|
||||||
transition("error", "fault", reduce(handleError))),
|
|
||||||
idle: state(
|
|
||||||
transition("incoming_call", "incoming", reduce(incomingCall)),
|
|
||||||
transition("pick_up", "ready", reduce(playDialTone))),
|
|
||||||
incoming: state(
|
|
||||||
transition("remote_hang_up", "idle", reduce(stopRinger)),
|
|
||||||
transition("pick_up", "connected", reduce(callAnswered))),
|
|
||||||
connected: state(
|
|
||||||
transition("remote_hang_up", "ready", reduce(playDialTone)),
|
|
||||||
transition("hang_up", "idle", reduce(stopCall))),
|
|
||||||
ready: state(
|
|
||||||
transition("dial_start", "dialing", reduce(dialStart)),
|
|
||||||
transition("dial_timeout", "aborted", reduce(stopDialTone)),
|
|
||||||
transition("hang_up", "idle", reduce(stopDialTone))),
|
|
||||||
dialing: state(
|
|
||||||
transition("dial_stop", "outgoing", reduce(makeCall), guard((ctx) => !callAgentGuard(ctx))),
|
|
||||||
transition("dial_stop", "connectedToAgent", reduce(makeAgentCall), guard((ctx) => callAgentGuard(ctx))),
|
|
||||||
transition("digit_increment", "dialing", reduce(digitIncrement)),
|
|
||||||
transition("hang_up", "idle", reduce(stopDialTone))),
|
|
||||||
outgoing: state(
|
|
||||||
transition("start_agent", "connectedToAgent"),
|
|
||||||
transition("answered", "connected"),
|
|
||||||
transition("hang_up", "idle", reduce(stopCall))),
|
|
||||||
connectedToAgent: state(
|
|
||||||
transition("remote_hang_up", "ready", reduce(stopAgent)),
|
|
||||||
transition("hang_up", "idle", reduce(stopAgent))),
|
|
||||||
aborted: state(
|
|
||||||
transition("hang_up", "idle")),
|
|
||||||
fault: state(),
|
|
||||||
},
|
|
||||||
context
|
|
||||||
)
|
)
|
||||||
|
console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai")
|
||||||
const phoneService = interpret(phoneMachine, () => {})
|
process.exit(1)
|
||||||
|
|
||||||
d._onEnter = function (machine, to, state, prevState, event) {
|
|
||||||
log.info(`📱 ${machine.current} -> ${to} (${JSON.stringify(event)})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
gpio.monitor(pins.hook, { bias: "pull-up" }, (event) => {
|
|
||||||
const type = event.edge === "falling" ? "hang_up" : "pick_up"
|
|
||||||
log.info(`📞 Hook ${event.edge} sending ${type}`)
|
|
||||||
phoneService.send({ type })
|
|
||||||
})
|
|
||||||
|
|
||||||
gpio.monitor(pins.rotaryInUse, { bias: "pull-up", throttleMs: 90 }, (event) => {
|
|
||||||
const type = event.edge === "falling" ? "dial_start" : "dial_stop"
|
|
||||||
log.debug(`📞 Rotary in-use ${event.edge} sending ${type}`)
|
|
||||||
phoneService.send({ type })
|
|
||||||
})
|
|
||||||
|
|
||||||
gpio.monitor(pins.rotaryNumber, { bias: "pull-up", throttleMs: 90 }, (event) => {
|
|
||||||
if (event.edge !== "rising") return
|
|
||||||
phoneService.send({ type: "digit_increment" })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Graceful shutdown handling
|
|
||||||
const cleanup = () => {
|
|
||||||
log.info("🛑 Shutting down, stopping agent process")
|
|
||||||
if (agentProcess?.stdin) {
|
|
||||||
agentProcess.stdin.write("quit\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("SIGINT", cleanup)
|
|
||||||
process.on("SIGTERM", cleanup)
|
|
||||||
process.on("exit", cleanup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const incomingCallRing = (): CancelableTask => {
|
await startPhone(agentId, apiKey)
|
||||||
let abortController = new AbortController()
|
|
||||||
|
|
||||||
const playRingtone = async () => {
|
// log.info("📞 GPIO inputs initialized")
|
||||||
while (!abortController.signal.aborted) {
|
|
||||||
await ring(2000, abortController.signal)
|
|
||||||
await sleep(4000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playRingtone().catch((error) => log.error("Ringer error:", error))
|
|
||||||
|
|
||||||
return () => abortController.abort()
|
// // const baresipConfig = join(import.meta.dir, "..", "baresip")
|
||||||
}
|
// // const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
|
||||||
|
|
||||||
const handleError = (ctx: PhoneContext, event: { type: "error"; message?: string }) => {
|
// // baresip.registrationSuccess.connect(async () => {
|
||||||
ctx.lastError = event.message
|
// // log.info("🐻 server connected")
|
||||||
log.error(`Phone error: ${event.message}`)
|
// // const result = await gpio.get(pins.hook)
|
||||||
return ctx
|
// // if (result.state === "low") {
|
||||||
}
|
// // phoneService.send({ type: "initialized" })
|
||||||
|
// // } else {
|
||||||
|
// // phoneService.send({ type: "pick_up" })
|
||||||
|
// // }
|
||||||
|
// // })
|
||||||
|
|
||||||
const incomingCall = (ctx: PhoneContext, event: { type: "incoming_call"; from?: string }) => {
|
// // baresip.callReceived.connect(({ contact }) => {
|
||||||
ctx.peer = event.from
|
// // log.info(`🐻 incoming call from ${contact}`)
|
||||||
ctx.cancelRinger = incomingCallRing()
|
// // phoneService.send({ type: "incoming_call", from: contact })
|
||||||
log.info(`Incoming call from ${event.from}`)
|
// // })
|
||||||
|
|
||||||
return ctx
|
// // baresip.callEstablished.connect(({ contact }) => {
|
||||||
}
|
// // log.info(`🐻 call established with ${contact}`)
|
||||||
|
// // phoneService.send({ type: "answered" })
|
||||||
|
// // })
|
||||||
|
|
||||||
const stopRinger = (ctx: PhoneContext) => {
|
// // baresip.hungUp.connect(() => {
|
||||||
ctx.cancelRinger?.()
|
// // log.info("🐻 call hung up")
|
||||||
ctx.cancelRinger = undefined
|
// // phoneService.send({ type: "remote_hang_up" })
|
||||||
return ctx
|
// // })
|
||||||
}
|
|
||||||
|
|
||||||
const playDialTone = (ctx: PhoneContext) => {
|
// // baresip.connect().catch((error) => {
|
||||||
const tone = new ToneGenerator()
|
// // log.error("🐻 connection error:", error)
|
||||||
|
// // phoneService.send({ type: "error", message: error.message })
|
||||||
|
// // })
|
||||||
|
|
||||||
tone.loopTone([350, 440])
|
// // baresip.error.connect(async ({ message }) => {
|
||||||
|
// // log.error("🐻 error:", message)
|
||||||
|
// // phoneService.send({ type: "error", message })
|
||||||
|
// // for (let i = 0; i < 4; i++) {
|
||||||
|
// // await ring(500)
|
||||||
|
// // await sleep(250)
|
||||||
|
// // }
|
||||||
|
// // process.exit(1)
|
||||||
|
// // })
|
||||||
|
|
||||||
ctx.cancelDialTone = () => {
|
// const agent = new Agent({
|
||||||
tone.stop()
|
// agentId,
|
||||||
}
|
// apiKey,
|
||||||
|
// tools: {
|
||||||
|
// search_web: (args: { query: string }) => searchWeb(args.query),
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
return ctx
|
// handleAgentEvents(agent)
|
||||||
}
|
|
||||||
|
|
||||||
const playOutgoingTone = () => {
|
// const startAgent = () => {
|
||||||
const tone = new ToneGenerator()
|
// log.info("☎️ Starting agent conversation")
|
||||||
let canceled = false
|
|
||||||
|
|
||||||
const play = async () => {
|
// if (agentProcess?.stdin) {
|
||||||
while (!canceled) {
|
// agentProcess.stdin.write("start\n")
|
||||||
await tone.playTone([440, 480], 2000)
|
// } else {
|
||||||
await sleep(4000)
|
// log.error("☎️ No agent process stdin available")
|
||||||
}
|
// phoneService.send({ type: "remote_hang_up" })
|
||||||
}
|
// }
|
||||||
|
|
||||||
play().catch((error) => log.error("Outgoing tone error:", error))
|
// return () => {
|
||||||
|
// log.info("☎️ Stopping agent conversation")
|
||||||
|
// if (agentProcess?.stdin) {
|
||||||
|
// agentProcess.stdin.write("stop\n")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
return () => {
|
// const context = (initial?: Partial<PhoneContext>): PhoneContext => ({
|
||||||
tone.stop()
|
// numberDialed: 0,
|
||||||
canceled = true
|
// baresip,
|
||||||
}
|
// startAgent,
|
||||||
}
|
// ...initial,
|
||||||
|
// })
|
||||||
|
|
||||||
const dialStart = (ctx: PhoneContext) => {
|
// const phoneMachine = createMachine(
|
||||||
ctx.numberDialed = 0
|
// "initializing",
|
||||||
ctx = stopDialTone(ctx)
|
// // prettier-ignore
|
||||||
|
// {
|
||||||
|
// initializing: state(
|
||||||
|
// transition("initialized", "idle"),
|
||||||
|
// transition("pick_up", "ready", reduce(playDialTone)),
|
||||||
|
// transition("error", "fault", reduce(handleError))),
|
||||||
|
// idle: state(
|
||||||
|
// transition("incoming_call", "incoming", reduce(incomingCall)),
|
||||||
|
// transition("pick_up", "ready", reduce(playDialTone))),
|
||||||
|
// incoming: state(
|
||||||
|
// transition("remote_hang_up", "idle", reduce(stopRinger)),
|
||||||
|
// transition("pick_up", "connected", reduce(callAnswered))),
|
||||||
|
// connected: state(
|
||||||
|
// transition("remote_hang_up", "ready", reduce(playDialTone)),
|
||||||
|
// transition("hang_up", "idle", reduce(stopCall))),
|
||||||
|
// ready: state(
|
||||||
|
// transition("dial_start", "dialing", reduce(dialStart)),
|
||||||
|
// transition("dial_timeout", "aborted", reduce(stopDialTone)),
|
||||||
|
// transition("hang_up", "idle", reduce(stopDialTone))),
|
||||||
|
// dialing: state(
|
||||||
|
// transition("dial_stop", "outgoing", reduce(makeCall), guard((ctx) => !callAgentGuard(ctx))),
|
||||||
|
// transition("dial_stop", "connectedToAgent", reduce(makeAgentCall), guard((ctx) => callAgentGuard(ctx))),
|
||||||
|
// transition("digit_increment", "dialing", reduce(digitIncrement)),
|
||||||
|
// transition("hang_up", "idle", reduce(stopDialTone))),
|
||||||
|
// outgoing: state(
|
||||||
|
// transition("start_agent", "connectedToAgent"),
|
||||||
|
// transition("answered", "connected"),
|
||||||
|
// transition("hang_up", "idle", reduce(stopCall))),
|
||||||
|
// connectedToAgent: state(
|
||||||
|
// transition("remote_hang_up", "ready", reduce(stopAgent)),
|
||||||
|
// transition("hang_up", "idle", reduce(stopAgent))),
|
||||||
|
// aborted: state(
|
||||||
|
// transition("hang_up", "idle")),
|
||||||
|
// fault: state(),
|
||||||
|
// },
|
||||||
|
// context
|
||||||
|
// )
|
||||||
|
|
||||||
return ctx
|
// const phoneService = interpret(phoneMachine, () => {})
|
||||||
}
|
|
||||||
|
|
||||||
const makeCall = (ctx: PhoneContext) => {
|
// d._onEnter = function (machine, to, state, prevState, event) {
|
||||||
log.info(`Dialing number: ${ctx.numberDialed}`)
|
// log.info(`📱 ${machine.current} -> ${to} (${JSON.stringify(event)})`)
|
||||||
if (ctx.numberDialed === 1) {
|
// }
|
||||||
ctx.baresip.dial("+13476229543")
|
|
||||||
} else if (ctx.numberDialed === 2) {
|
|
||||||
ctx.baresip.dial("+18109643563")
|
|
||||||
} else {
|
|
||||||
const playTone = async () => {
|
|
||||||
const tone = new ToneGenerator()
|
|
||||||
await tone.playTone([900], 200)
|
|
||||||
await tone.playTone([1350], 200)
|
|
||||||
await tone.playTone([1750], 200)
|
|
||||||
}
|
|
||||||
playTone().catch((error) => log.error("Error playing tone:", error))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx
|
// gpio.monitor(pins.hook, { bias: "pull-up" }, (event) => {
|
||||||
}
|
// const type = event.edge === "falling" ? "hang_up" : "pick_up"
|
||||||
|
// log.info(`📞 Hook ${event.edge} sending ${type}`)
|
||||||
|
// phoneService.send({ type })
|
||||||
|
// })
|
||||||
|
|
||||||
const makeAgentCall = (ctx: PhoneContext) => {
|
// gpio.monitor(pins.rotaryInUse, { bias: "pull-up", throttleMs: 90 }, (event) => {
|
||||||
log.info(`Calling agent`)
|
// const type = event.edge === "falling" ? "dial_start" : "dial_stop"
|
||||||
ctx.cancelAgent = ctx.startAgent()
|
// log.debug(`📞 Rotary in-use ${event.edge} sending ${type}`)
|
||||||
|
// phoneService.send({ type })
|
||||||
|
// })
|
||||||
|
|
||||||
return ctx
|
// gpio.monitor(pins.rotaryNumber, { bias: "pull-up", throttleMs: 90 }, (event) => {
|
||||||
}
|
// if (event.edge !== "rising") return
|
||||||
|
// phoneService.send({ type: "digit_increment" })
|
||||||
|
// })
|
||||||
|
|
||||||
const callAgentGuard = (ctx: PhoneContext) => {
|
// // Graceful shutdown handling
|
||||||
return ctx.numberDialed === 10
|
// const cleanup = () => {
|
||||||
}
|
// log.info("🛑 Shutting down, stopping agent process")
|
||||||
|
// if (agentProcess?.stdin) {
|
||||||
|
// agentProcess.stdin.write("quit\n")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const callAnswered = (ctx: PhoneContext) => {
|
// process.on("SIGINT", cleanup)
|
||||||
ctx.baresip.accept()
|
// process.on("SIGTERM", cleanup)
|
||||||
|
// process.on("exit", cleanup)
|
||||||
|
// }
|
||||||
|
|
||||||
ctx.cancelDialTone?.()
|
// const handleAgentEvents = (agent: Agent) => {
|
||||||
ctx.cancelDialTone = undefined
|
// agent.events.connect(async (event) => {
|
||||||
|
// switch (event.type) {
|
||||||
|
// case "connected":
|
||||||
|
// console.log("✅ Connected to AI agent\n")
|
||||||
|
// break
|
||||||
|
|
||||||
ctx.cancelRinger?.()
|
// case "user_transcript":
|
||||||
ctx.cancelRinger = undefined
|
// console.log(`👤 You: ${event.transcript}`)
|
||||||
|
// break
|
||||||
|
|
||||||
return ctx
|
// case "agent_response":
|
||||||
}
|
// console.log(`🤖 Agent: ${event.response}`)
|
||||||
|
// break
|
||||||
|
|
||||||
const stopCall = (ctx: PhoneContext) => {
|
// case "audio":
|
||||||
ctx.baresip.hangUp()
|
// await waitingIndicator.stop()
|
||||||
return ctx
|
// const audioBuffer = Buffer.from(event.audioBase64, "base64")
|
||||||
}
|
// streamPlayback.write(audioBuffer)
|
||||||
|
// break
|
||||||
|
|
||||||
const stopAgent = (ctx: PhoneContext) => {
|
// case "interruption":
|
||||||
log.info("🛑 Stopping agent")
|
// console.log("🛑 User interrupted")
|
||||||
ctx.cancelAgent?.()
|
// streamPlayback?.stop()
|
||||||
ctx.cancelAgent = undefined
|
// streamPlayback = player.playStream() // Reset playback stream
|
||||||
return ctx
|
// break
|
||||||
}
|
|
||||||
|
|
||||||
const stopDialTone = (ctx: PhoneContext) => {
|
// case "tool_call":
|
||||||
ctx.cancelDialTone?.()
|
// waitingIndicator.start(streamPlayback)
|
||||||
ctx.cancelDialTone = undefined
|
// console.log(`🔧 Tool call: ${event.name}(${JSON.stringify(event.args)})`)
|
||||||
|
// break
|
||||||
|
|
||||||
return ctx
|
// case "tool_result":
|
||||||
}
|
// console.log(`✅ Tool result: ${JSON.stringify(event.result)}`)
|
||||||
|
// break
|
||||||
|
|
||||||
const digitIncrement = (ctx: PhoneContext) => {
|
// case "tool_error":
|
||||||
ctx.numberDialed += 1
|
// console.error(`❌ Tool error: ${event.error}`)
|
||||||
return ctx
|
// break
|
||||||
}
|
|
||||||
|
// case "disconnected":
|
||||||
|
// console.log("\n👋 Conversation ended, returning to dialtone\n")
|
||||||
|
// streamPlayback?.stop()
|
||||||
|
// state = "WAITING_FOR_VOICE"
|
||||||
|
// phoneService.send({ type: "remote_hang_up" })
|
||||||
|
// break
|
||||||
|
|
||||||
|
// case "error":
|
||||||
|
// console.error("Agent error:", event.error)
|
||||||
|
// break
|
||||||
|
|
||||||
|
// case "ping":
|
||||||
|
// break
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// console.log(`😵💫 ${event.type}`)
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const incomingCallRing = (): CancelableTask => {
|
||||||
|
// let abortController = new AbortController()
|
||||||
|
|
||||||
|
// const playRingtone = async () => {
|
||||||
|
// while (!abortController.signal.aborted) {
|
||||||
|
// await ring(2000, abortController.signal)
|
||||||
|
// await sleep(4000)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// playRingtone().catch((error) => log.error("Ringer error:", error))
|
||||||
|
|
||||||
|
// return () => abortController.abort()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const handleError = (ctx: PhoneContext, event: { type: "error"; message?: string }) => {
|
||||||
|
// ctx.lastError = event.message
|
||||||
|
// log.error(`Phone error: ${event.message}`)
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const incomingCall = (ctx: PhoneContext, event: { type: "incoming_call"; from?: string }) => {
|
||||||
|
// ctx.peer = event.from
|
||||||
|
// ctx.cancelRinger = incomingCallRing()
|
||||||
|
// log.info(`Incoming call from ${event.from}`)
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const stopRinger = (ctx: PhoneContext) => {
|
||||||
|
// ctx.cancelRinger?.()
|
||||||
|
// ctx.cancelRinger = undefined
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const playDialTone = (ctx: PhoneContext) => {
|
||||||
|
// const tone = new ToneGenerator()
|
||||||
|
|
||||||
|
// tone.loopTone([350, 440])
|
||||||
|
|
||||||
|
// ctx.cancelDialTone = () => {
|
||||||
|
// tone.stop()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const playOutgoingTone = () => {
|
||||||
|
// const tone = new ToneGenerator()
|
||||||
|
// let canceled = false
|
||||||
|
|
||||||
|
// const play = async () => {
|
||||||
|
// while (!canceled) {
|
||||||
|
// await tone.playTone([440, 480], 2000)
|
||||||
|
// await sleep(4000)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// play().catch((error) => log.error("Outgoing tone error:", error))
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// tone.stop()
|
||||||
|
// canceled = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const dialStart = (ctx: PhoneContext) => {
|
||||||
|
// ctx.numberDialed = 0
|
||||||
|
// ctx = stopDialTone(ctx)
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const makeCall = (ctx: PhoneContext) => {
|
||||||
|
// log.info(`Dialing number: ${ctx.numberDialed}`)
|
||||||
|
// if (ctx.numberDialed === 1) {
|
||||||
|
// ctx.baresip.dial("+13476229543")
|
||||||
|
// } else if (ctx.numberDialed === 2) {
|
||||||
|
// ctx.baresip.dial("+18109643563")
|
||||||
|
// } else {
|
||||||
|
// const playTone = async () => {
|
||||||
|
// const tone = new ToneGenerator()
|
||||||
|
// await tone.playTone([900], 200)
|
||||||
|
// await tone.playTone([1350], 200)
|
||||||
|
// await tone.playTone([1750], 200)
|
||||||
|
// }
|
||||||
|
// playTone().catch((error) => log.error("Error playing tone:", error))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const makeAgentCall = (ctx: PhoneContext) => {
|
||||||
|
// log.info(`Calling agent`)
|
||||||
|
// ctx.cancelAgent = ctx.startAgent()
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const callAgentGuard = (ctx: PhoneContext) => {
|
||||||
|
// return ctx.numberDialed === 10
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const callAnswered = (ctx: PhoneContext) => {
|
||||||
|
// ctx.baresip.accept()
|
||||||
|
|
||||||
|
// ctx.cancelDialTone?.()
|
||||||
|
// ctx.cancelDialTone = undefined
|
||||||
|
|
||||||
|
// ctx.cancelRinger?.()
|
||||||
|
// ctx.cancelRinger = undefined
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const stopCall = (ctx: PhoneContext) => {
|
||||||
|
// ctx.baresip.hangUp()
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const stopAgent = (ctx: PhoneContext) => {
|
||||||
|
// log.info("🛑 Stopping agent")
|
||||||
|
// ctx.cancelAgent?.()
|
||||||
|
// ctx.cancelAgent = undefined
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const stopDialTone = (ctx: PhoneContext) => {
|
||||||
|
// ctx.cancelDialTone?.()
|
||||||
|
// ctx.cancelDialTone = undefined
|
||||||
|
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const digitIncrement = (ctx: PhoneContext) => {
|
||||||
|
// ctx.numberDialed += 1
|
||||||
|
// return ctx
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,17 @@ export class InputGroup<T extends string = string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get pins(): Record<T, { readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }> {
|
get pins(): Record<
|
||||||
const result = {} as Record<T, { readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }>
|
T,
|
||||||
|
{ readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }
|
||||||
|
> {
|
||||||
|
const result = {} as Record<
|
||||||
|
T,
|
||||||
|
{
|
||||||
|
readonly value: 0 | 1
|
||||||
|
waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
for (const [name, config] of this.#pinMap) {
|
for (const [name, config] of this.#pinMap) {
|
||||||
const offset = config.offset
|
const offset = config.offset
|
||||||
|
|
@ -43,9 +52,10 @@ export class InputGroup<T extends string = string> {
|
||||||
if (ret === -1) throw new Error("Failed to get pin value")
|
if (ret === -1) throw new Error("Failed to get pin value")
|
||||||
return ret as 0 | 1
|
return ret as 0 | 1
|
||||||
},
|
},
|
||||||
waitForValue: (targetValue: 0 | 1, timeout?: number) => this.#waitForPinValue(pinName as T, targetValue, timeout)
|
waitForValue: (targetValue: 0 | 1, timeout?: number) =>
|
||||||
|
this.#waitForPinValue(pinName as T, targetValue, timeout),
|
||||||
}),
|
}),
|
||||||
enumerable: true
|
enumerable: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,11 +107,8 @@ export class InputGroup<T extends string = string> {
|
||||||
if (this.#closed) throw new Error("InputGroup is closed")
|
if (this.#closed) throw new Error("InputGroup is closed")
|
||||||
|
|
||||||
const eventQueue: InputGroupEvent<T>[] = []
|
const eventQueue: InputGroupEvent<T>[] = []
|
||||||
let resolve: (() => void) | undefined
|
|
||||||
|
|
||||||
const listener = (event: InputGroupEvent<T>) => {
|
const listener = (event: InputGroupEvent<T>) => {
|
||||||
eventQueue.push(event)
|
eventQueue.push(event)
|
||||||
resolve?.()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#eventListeners.push(listener)
|
this.#eventListeners.push(listener)
|
||||||
|
|
@ -109,14 +116,14 @@ export class InputGroup<T extends string = string> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!this.#closed) {
|
while (!this.#closed) {
|
||||||
if (eventQueue.length === 0) {
|
if (eventQueue.length > 0) {
|
||||||
await new Promise<void>((r) => {
|
for (const event of eventQueue) {
|
||||||
resolve = r
|
yield event
|
||||||
})
|
}
|
||||||
|
eventQueue.length = 0
|
||||||
|
} else {
|
||||||
|
await Bun.sleep(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = eventQueue.shift()
|
|
||||||
if (event) yield event
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.#eventListeners = this.#eventListeners.filter((l) => l !== listener)
|
this.#eventListeners = this.#eventListeners.filter((l) => l !== listener)
|
||||||
|
|
@ -159,12 +166,16 @@ export class InputGroup<T extends string = string> {
|
||||||
if (!pinInfo) continue
|
if (!pinInfo) continue
|
||||||
|
|
||||||
const pressed = mapLibgpiodEdgeToPressedState(edgeType, pinInfo.pull)
|
const pressed = mapLibgpiodEdgeToPressedState(edgeType, pinInfo.pull)
|
||||||
const value = (pressed ? (pinInfo.pull === "up" ? 0 : 1) : (pinInfo.pull === "up" ? 1 : 0)) as 0 | 1
|
const value = (
|
||||||
|
pressed ? (pinInfo.pull === "up" ? 0 : 1) : pinInfo.pull === "up" ? 1 : 0
|
||||||
|
) as 0 | 1
|
||||||
const inputEvent: InputGroupEvent<T> = { pin: pinInfo.name as T, value, timestamp }
|
const inputEvent: InputGroupEvent<T> = { pin: pinInfo.name as T, value, timestamp }
|
||||||
|
|
||||||
for (const listener of this.#eventListeners) {
|
for (const listener of this.#eventListeners) {
|
||||||
listener(inputEvent)
|
listener(inputEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Bun.sleep(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
72
test.ts
72
test.ts
|
|
@ -1,72 +0,0 @@
|
||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic functionality test for Buzz library
|
|
||||||
* Tests device listing, player, recorder, and tone generation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Buzz from "./buzz"
|
|
||||||
|
|
||||||
console.log("🎵 Buzz Audio Library - Basic Test\n")
|
|
||||||
|
|
||||||
// Test 1: List devices
|
|
||||||
console.log("📋 Listing devices...")
|
|
||||||
const devices = await Buzz.listDevices()
|
|
||||||
console.log(`Found ${devices.length} device(s):`)
|
|
||||||
devices.forEach((d) => {
|
|
||||||
console.log(` ${d.type.padEnd(8)} ${d.label} (${d.id})`)
|
|
||||||
})
|
|
||||||
console.log("")
|
|
||||||
|
|
||||||
// Test 2: Create player
|
|
||||||
console.log("🔊 Creating default player...")
|
|
||||||
try {
|
|
||||||
const player = await Buzz.defaultPlayer()
|
|
||||||
console.log("✅ Player created\n")
|
|
||||||
|
|
||||||
// Test 3: Play sound file
|
|
||||||
console.log("🔊 Playing greeting sound...")
|
|
||||||
const playback = await player.play("./sounds/greeting/greet1.wav")
|
|
||||||
await playback.finished()
|
|
||||||
console.log("✅ Sound played\n")
|
|
||||||
|
|
||||||
// Test 4: Play tone
|
|
||||||
console.log("🎵 Playing 440Hz tone for 1 second...")
|
|
||||||
const tone = await player.playTone([440], 1000)
|
|
||||||
await tone.finished()
|
|
||||||
console.log("✅ Tone played\n")
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`⚠️ Skipping player tests: ${error instanceof Error ? error.message : error}\n`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 5: Create recorder
|
|
||||||
console.log("🎤 Creating default recorder...")
|
|
||||||
try {
|
|
||||||
const recorder = await Buzz.defaultRecorder()
|
|
||||||
console.log("✅ Recorder created\n")
|
|
||||||
|
|
||||||
// Test 6: Stream recording with RMS
|
|
||||||
console.log("📊 Recording for 2 seconds with RMS monitoring...")
|
|
||||||
const recording = recorder.start()
|
|
||||||
let chunkCount = 0
|
|
||||||
let maxRMS = 0
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await recording.stop()
|
|
||||||
}, 2000)
|
|
||||||
|
|
||||||
for await (const chunk of recording.stream()) {
|
|
||||||
chunkCount++
|
|
||||||
const rms = Buzz.calculateRMS(chunk)
|
|
||||||
if (rms > maxRMS) maxRMS = rms
|
|
||||||
if (chunkCount % 20 === 0) {
|
|
||||||
console.log(` RMS: ${Math.round(rms)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Recorded ${chunkCount} chunks, max RMS: ${Math.round(maxRMS)}\n`)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`⚠️ Skipping recorder tests: ${error instanceof Error ? error.message : error}\n`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ All tests complete!")
|
|
||||||
Loading…
Reference in New Issue
Block a user