Move from polling to worker thread #3
662
src/phone.ts
662
src/phone.ts
|
|
@ -5,6 +5,9 @@ import { sleep } from "bun"
|
|||
import { processStderr, processStdout } from "./utils/stdio"
|
||||
import Buzz from "./buzz"
|
||||
import { join } from "path"
|
||||
import { GPIO } from "./pins"
|
||||
import { Agent } from "./agent"
|
||||
import { searchWeb } from "./agent/tools"
|
||||
|
||||
type CancelableTask = () => void
|
||||
|
||||
|
|
@ -19,327 +22,424 @@ type PhoneContext = {
|
|||
cancelAgent?: CancelableTask
|
||||
}
|
||||
|
||||
export const pins = {
|
||||
ringer: 17,
|
||||
hook: 27,
|
||||
rotaryInUse: 22,
|
||||
rotaryNumber: 23,
|
||||
}
|
||||
const gpio = new GPIO({ resetOnClose: true })
|
||||
using ringer = gpio.output(17)
|
||||
using inputs = gpio.inputGroup({
|
||||
hook: { pin: 27, debounce: 50 },
|
||||
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)
|
||||
log.info(`📞 Hook ${inputs.pins.hook.value}`)
|
||||
await handleInputEvents()
|
||||
}
|
||||
|
||||
const baresipConfig = join(import.meta.dir, "..", "baresip")
|
||||
const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
|
||||
const handleInputEvents = async () => {
|
||||
let digit = 0
|
||||
for await (const event of inputs.events()) {
|
||||
switch (event.pin) {
|
||||
case "hook":
|
||||
const type = event.value == 0 ? "hang_up" : "pick_up"
|
||||
log.info(`📞 Hook ${event.value} sending ${type}`)
|
||||
if (type === "hang_up") {
|
||||
ringer.value = 1
|
||||
} else {
|
||||
ringer.value = 0
|
||||
}
|
||||
break
|
||||
|
||||
baresip.registrationSuccess.connect(async () => {
|
||||
log.info("🐻 server connected")
|
||||
const result = await gpio.get(pins.hook)
|
||||
if (result.state === "low") {
|
||||
phoneService.send({ type: "initialized" })
|
||||
} else {
|
||||
phoneService.send({ type: "pick_up" })
|
||||
}
|
||||
})
|
||||
case "rotaryInUse":
|
||||
if (event.value === 0) {
|
||||
digit = 0
|
||||
} else {
|
||||
log.info(`📞 Dialed digit: ${digit}`)
|
||||
}
|
||||
break
|
||||
|
||||
baresip.callReceived.connect(({ contact }) => {
|
||||
log.info(`🐻 incoming call from ${contact}`)
|
||||
phoneService.send({ type: "incoming_call", from: contact })
|
||||
})
|
||||
case "rotaryNumber":
|
||||
if (event.value === 1) {
|
||||
digit += 1
|
||||
}
|
||||
break
|
||||
|
||||
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)
|
||||
}
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
let agentProcess: Bun.Subprocess
|
||||
|
||||
const initializeAgent = () => {
|
||||
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")
|
||||
}
|
||||
default:
|
||||
log.error(`📞 Unknown pin event: ${event.pin}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = (initial?: Partial<PhoneContext>): PhoneContext => ({
|
||||
numberDialed: 0,
|
||||
baresip,
|
||||
startAgent,
|
||||
...initial,
|
||||
})
|
||||
const apiKey = process.env.ELEVEN_API_KEY
|
||||
const agentId = process.env.ELEVEN_AGENT_ID
|
||||
|
||||
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
|
||||
if (!apiKey) {
|
||||
console.error("❌ Error: ELEVEN_API_KEY environment variable is required")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!agentId) {
|
||||
console.error(
|
||||
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required"
|
||||
)
|
||||
|
||||
const phoneService = interpret(phoneMachine, () => {})
|
||||
|
||||
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)
|
||||
console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const incomingCallRing = (): CancelableTask => {
|
||||
let abortController = new AbortController()
|
||||
await startPhone(agentId, apiKey)
|
||||
|
||||
const playRingtone = async () => {
|
||||
while (!abortController.signal.aborted) {
|
||||
await ring(2000, abortController.signal)
|
||||
await sleep(4000)
|
||||
}
|
||||
}
|
||||
playRingtone().catch((error) => log.error("Ringer error:", error))
|
||||
// log.info("📞 GPIO inputs initialized")
|
||||
|
||||
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 }) => {
|
||||
ctx.lastError = event.message
|
||||
log.error(`Phone error: ${event.message}`)
|
||||
return ctx
|
||||
}
|
||||
// // baresip.registrationSuccess.connect(async () => {
|
||||
// // log.info("🐻 server connected")
|
||||
// // const result = await gpio.get(pins.hook)
|
||||
// // if (result.state === "low") {
|
||||
// // phoneService.send({ type: "initialized" })
|
||||
// // } else {
|
||||
// // phoneService.send({ type: "pick_up" })
|
||||
// // }
|
||||
// // })
|
||||
|
||||
const incomingCall = (ctx: PhoneContext, event: { type: "incoming_call"; from?: string }) => {
|
||||
ctx.peer = event.from
|
||||
ctx.cancelRinger = incomingCallRing()
|
||||
log.info(`Incoming call from ${event.from}`)
|
||||
// // baresip.callReceived.connect(({ contact }) => {
|
||||
// // log.info(`🐻 incoming call from ${contact}`)
|
||||
// // phoneService.send({ type: "incoming_call", from: contact })
|
||||
// // })
|
||||
|
||||
return ctx
|
||||
}
|
||||
// // baresip.callEstablished.connect(({ contact }) => {
|
||||
// // log.info(`🐻 call established with ${contact}`)
|
||||
// // phoneService.send({ type: "answered" })
|
||||
// // })
|
||||
|
||||
const stopRinger = (ctx: PhoneContext) => {
|
||||
ctx.cancelRinger?.()
|
||||
ctx.cancelRinger = undefined
|
||||
return ctx
|
||||
}
|
||||
// // baresip.hungUp.connect(() => {
|
||||
// // log.info("🐻 call hung up")
|
||||
// // phoneService.send({ type: "remote_hang_up" })
|
||||
// // })
|
||||
|
||||
const playDialTone = (ctx: PhoneContext) => {
|
||||
const tone = new ToneGenerator()
|
||||
// // baresip.connect().catch((error) => {
|
||||
// // 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 = () => {
|
||||
tone.stop()
|
||||
}
|
||||
// const agent = new Agent({
|
||||
// agentId,
|
||||
// apiKey,
|
||||
// tools: {
|
||||
// search_web: (args: { query: string }) => searchWeb(args.query),
|
||||
// },
|
||||
// })
|
||||
|
||||
return ctx
|
||||
}
|
||||
// handleAgentEvents(agent)
|
||||
|
||||
const playOutgoingTone = () => {
|
||||
const tone = new ToneGenerator()
|
||||
let canceled = false
|
||||
// const startAgent = () => {
|
||||
// log.info("☎️ Starting agent conversation")
|
||||
|
||||
const play = async () => {
|
||||
while (!canceled) {
|
||||
await tone.playTone([440, 480], 2000)
|
||||
await sleep(4000)
|
||||
}
|
||||
}
|
||||
// if (agentProcess?.stdin) {
|
||||
// agentProcess.stdin.write("start\n")
|
||||
// } else {
|
||||
// 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 () => {
|
||||
tone.stop()
|
||||
canceled = true
|
||||
}
|
||||
}
|
||||
// const context = (initial?: Partial<PhoneContext>): PhoneContext => ({
|
||||
// numberDialed: 0,
|
||||
// baresip,
|
||||
// startAgent,
|
||||
// ...initial,
|
||||
// })
|
||||
|
||||
const dialStart = (ctx: PhoneContext) => {
|
||||
ctx.numberDialed = 0
|
||||
ctx = stopDialTone(ctx)
|
||||
// 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
|
||||
// )
|
||||
|
||||
return ctx
|
||||
}
|
||||
// const phoneService = interpret(phoneMachine, () => {})
|
||||
|
||||
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))
|
||||
}
|
||||
// d._onEnter = function (machine, to, state, prevState, event) {
|
||||
// log.info(`📱 ${machine.current} -> ${to} (${JSON.stringify(event)})`)
|
||||
// }
|
||||
|
||||
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) => {
|
||||
log.info(`Calling agent`)
|
||||
ctx.cancelAgent = ctx.startAgent()
|
||||
// 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 })
|
||||
// })
|
||||
|
||||
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) => {
|
||||
return ctx.numberDialed === 10
|
||||
}
|
||||
// // Graceful shutdown handling
|
||||
// const cleanup = () => {
|
||||
// log.info("🛑 Shutting down, stopping agent process")
|
||||
// if (agentProcess?.stdin) {
|
||||
// agentProcess.stdin.write("quit\n")
|
||||
// }
|
||||
// }
|
||||
|
||||
const callAnswered = (ctx: PhoneContext) => {
|
||||
ctx.baresip.accept()
|
||||
// process.on("SIGINT", cleanup)
|
||||
// process.on("SIGTERM", cleanup)
|
||||
// process.on("exit", cleanup)
|
||||
// }
|
||||
|
||||
ctx.cancelDialTone?.()
|
||||
ctx.cancelDialTone = undefined
|
||||
// const handleAgentEvents = (agent: Agent) => {
|
||||
// agent.events.connect(async (event) => {
|
||||
// switch (event.type) {
|
||||
// case "connected":
|
||||
// console.log("✅ Connected to AI agent\n")
|
||||
// break
|
||||
|
||||
ctx.cancelRinger?.()
|
||||
ctx.cancelRinger = undefined
|
||||
// case "user_transcript":
|
||||
// console.log(`👤 You: ${event.transcript}`)
|
||||
// break
|
||||
|
||||
return ctx
|
||||
}
|
||||
// case "agent_response":
|
||||
// console.log(`🤖 Agent: ${event.response}`)
|
||||
// break
|
||||
|
||||
const stopCall = (ctx: PhoneContext) => {
|
||||
ctx.baresip.hangUp()
|
||||
return ctx
|
||||
}
|
||||
// case "audio":
|
||||
// await waitingIndicator.stop()
|
||||
// const audioBuffer = Buffer.from(event.audioBase64, "base64")
|
||||
// streamPlayback.write(audioBuffer)
|
||||
// break
|
||||
|
||||
const stopAgent = (ctx: PhoneContext) => {
|
||||
log.info("🛑 Stopping agent")
|
||||
ctx.cancelAgent?.()
|
||||
ctx.cancelAgent = undefined
|
||||
return ctx
|
||||
}
|
||||
// case "interruption":
|
||||
// console.log("🛑 User interrupted")
|
||||
// streamPlayback?.stop()
|
||||
// streamPlayback = player.playStream() // Reset playback stream
|
||||
// break
|
||||
|
||||
const stopDialTone = (ctx: PhoneContext) => {
|
||||
ctx.cancelDialTone?.()
|
||||
ctx.cancelDialTone = undefined
|
||||
// case "tool_call":
|
||||
// waitingIndicator.start(streamPlayback)
|
||||
// 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) => {
|
||||
ctx.numberDialed += 1
|
||||
return ctx
|
||||
}
|
||||
// case "tool_error":
|
||||
// console.error(`❌ Tool error: ${event.error}`)
|
||||
// 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> }> {
|
||||
const result = {} as Record<T, { readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }>
|
||||
get pins(): Record<
|
||||
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) {
|
||||
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")
|
||||
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")
|
||||
|
||||
const eventQueue: InputGroupEvent<T>[] = []
|
||||
let resolve: (() => void) | undefined
|
||||
|
||||
const listener = (event: InputGroupEvent<T>) => {
|
||||
eventQueue.push(event)
|
||||
resolve?.()
|
||||
}
|
||||
|
||||
this.#eventListeners.push(listener)
|
||||
|
|
@ -109,14 +116,14 @@ export class InputGroup<T extends string = string> {
|
|||
|
||||
try {
|
||||
while (!this.#closed) {
|
||||
if (eventQueue.length === 0) {
|
||||
await new Promise<void>((r) => {
|
||||
resolve = r
|
||||
})
|
||||
if (eventQueue.length > 0) {
|
||||
for (const event of eventQueue) {
|
||||
yield event
|
||||
}
|
||||
eventQueue.length = 0
|
||||
} else {
|
||||
await Bun.sleep(0)
|
||||
}
|
||||
|
||||
const event = eventQueue.shift()
|
||||
if (event) yield event
|
||||
}
|
||||
} finally {
|
||||
this.#eventListeners = this.#eventListeners.filter((l) => l !== listener)
|
||||
|
|
@ -159,12 +166,16 @@ export class InputGroup<T extends string = string> {
|
|||
if (!pinInfo) continue
|
||||
|
||||
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 }
|
||||
|
||||
for (const listener of this.#eventListeners) {
|
||||
listener(inputEvent)
|
||||
}
|
||||
|
||||
await Bun.sleep(0)
|
||||
}
|
||||
}
|
||||
} 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