From bd9ab973b296e41fee6225449d360d0b8d5cc237 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 20 Nov 2025 16:16:47 -0800 Subject: [PATCH 1/6] whatever --- .vscode/settings.json | 3 + baresip/config | 2 +- bun.lock | 13 +- package.json | 5 +- scripts/bootstrap.ts | 11 +- sounds/stalling/sigh2.wav | Bin 80734 -> 0 bytes sounds/typing/{typing2.wav => typing.wav} | Bin sounds/typing/typing1.wav | Bin 128078 -> 0 bytes src/agent/index.ts | 2 +- src/buzz/index.ts | 33 +- src/main.ts | 26 + src/phone.ts | 703 +++++++++++----------- src/pins/index.ts | 2 + src/sip.ts | 2 +- src/test-operator.ts | 5 +- src/test-pins.ts | 2 +- src/utils/index.ts | 16 + src/utils/log.ts | 26 +- src/utils/signal.ts | 47 +- src/utils/stdio.ts | 2 +- src/utils/waiting-sounds.ts | 13 +- 21 files changed, 505 insertions(+), 408 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 sounds/stalling/sigh2.wav rename sounds/typing/{typing2.wav => typing.wav} (100%) delete mode 100644 sounds/typing/typing1.wav create mode 100644 src/main.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/baresip/config b/baresip/config index 9485572..26b8161 100644 --- a/baresip/config +++ b/baresip/config @@ -6,7 +6,6 @@ # Core poll_method epoll # poll, select, epoll .. -ring_aufile none # Call call_local_timeout 120 @@ -18,6 +17,7 @@ audio_source alsa,default audio_alert none audio_alert_enable no audio_level no +ring_aufile /dev/null ausrc_format s16 # s16, float, .. auplay_format s16 # s16, float, .. auenc_format s16 # s16, float, .. diff --git a/bun.lock b/bun.lock index b555245..0fb9ba4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,10 +6,11 @@ "dependencies": { "hono": "^4.10.4", "openai": "^6.9.0", - "robot3": "^1.2.0", + "robot3": "1.1.1", }, "devDependencies": { "@types/bun": "latest", + "prettier": "^3.6.2", }, "peerDependencies": { "typescript": "^5", @@ -21,17 +22,19 @@ "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="], + "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="], "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], - "csstype": ["csstype@3.2.2", "", {}, "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], - "openai": ["openai@6.9.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-n2sJRYmM+xfJ0l3OfH8eNnIyv3nQY7L08gZQu3dw6wSdfPtKAk92L83M2NIP5SS8Cl/bsBBG3yKzEOjkx0O+7A=="], + "openai": ["openai@6.9.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg=="], - "robot3": ["robot3@1.2.0", "", {}, "sha512-Xin8KHqCKrD9Rqk1ZzZQYjsb6S9DRggcfwBqnVPeM3DLtNCJLxWWTrPJDYm3E+ZiTO7H3VMdgyPSkIbuYnYP2Q=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "robot3": ["robot3@1.1.1", "", {}, "sha512-kuD0oQg2KUE74FCQ1a5uoRsEJ/bUKrU1D3vnluop9X7LSiGLndejQgjUEcMqJMVzUA836HSXhtY7XNtQiPTCLQ=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/package.json b/package.json index 8c0ca93..cc164c2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "bun run src/operator.ts" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "prettier": "^3.6.2" }, "peerDependencies": { "typescript": "^5" @@ -15,7 +16,7 @@ "dependencies": { "hono": "^4.10.4", "openai": "^6.9.0", - "robot3": "^1.2.0" + "robot3": "1.1.1" }, "prettier": { "semi": false, diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts index de73d65..905c196 100755 --- a/scripts/bootstrap.ts +++ b/scripts/bootstrap.ts @@ -21,15 +21,18 @@ const WEB_SERVICE_FILE = "/etc/systemd/system/phone-web.service" console.log(`Install directory: ${INSTALL_DIR}`) -console.log("\nStep 1: Ensuring directory exists...") +console.log("\nEnsuring directory exists...") await $`mkdir -p ${INSTALL_DIR}` console.log(`✓ Directory ready: ${INSTALL_DIR}`) -console.log("\nStep 2: Installing dependencies...") +console.log("\nInstalling dependencies...") await $`cd ${INSTALL_DIR} && bun install` console.log(`✓ Dependencies installed`) -console.log("\nStep 3: Installing systemd services...") +console.log("\nInstalling Baresip...") +await $`sudo apt install -y baresip` + +console.log("\nInstalling systemd services...") // Find where bun is installed const bunPath = await $`which bun` .quiet() @@ -87,7 +90,7 @@ await $`systemctl enable phone-ap.service` await $`systemctl enable phone-web.service` console.log("✓ Services enabled") -console.log("\nStep 4: Starting the services...") +console.log("\nStarting the services...") await $`systemctl start phone-ap.service` await $`systemctl start phone-web.service` console.log("✓ Services started") diff --git a/sounds/stalling/sigh2.wav b/sounds/stalling/sigh2.wav deleted file mode 100644 index 365a6d34f5f6fc80518193c71b136aac54cb0069..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80734 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%0AG8rDtUQ#K@MTYPkSW9cz&OO+qf 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�vR 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 diff --git a/src/agent/index.ts b/src/agent/index.ts index a03a95a..913f2a2 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -255,7 +255,7 @@ export class Agent { }) } - #handleClose = (): void => { + #handleClose = (event: CloseEvent): void => { this.#cleanup() this.events.emit({ type: "disconnected" }) } diff --git a/src/buzz/index.ts b/src/buzz/index.ts index f70d0b8..fdccb1e 100644 --- a/src/buzz/index.ts +++ b/src/buzz/index.ts @@ -1,20 +1,25 @@ -import { Player } from "./player.js" -import { Recorder } from "./recorder.js" +import { Player as PlayerClass } from "./player.js" +import { Recorder as RecorderClass } from "./recorder.js" import { listDevices, calculateRMS, findDeviceByLabel, - type AudioFormat, - type Device, + type AudioFormat as AudioFormatType, + type Device as DeviceType, + type Playback as PlaybackType, + type StreamingPlayback as StreamingPlaybackType, + type StreamingRecording as StreamingRecordingType, + type FileRecording as FileRecordingType, } from "./utils.js" -const defaultPlayer = (format?: AudioFormat) => Player.create({ format }) +const defaultPlayer = (format?: AudioFormatType) => PlayerClass.create({ format }) -const player = (label: string, format?: AudioFormat) => Player.create({ label, format }) +const player = (label: string, format?: AudioFormatType) => PlayerClass.create({ label, format }) -const defaultRecorder = (format?: AudioFormat) => Recorder.create({ format }) +const defaultRecorder = (format?: AudioFormatType) => RecorderClass.create({ format }) -const recorder = (label: string, format?: AudioFormat) => Recorder.create({ label, format }) +const recorder = (label: string, format?: AudioFormatType) => + RecorderClass.create({ label, format }) const getVolumeControl = async (cardNumber?: number): Promise => { const output = cardNumber @@ -89,7 +94,13 @@ const Buzz = { calculateRMS, } +declare namespace Buzz { + export type Playback = PlaybackType + export type StreamingPlayback = StreamingPlaybackType + export type StreamingRecording = StreamingRecordingType + export type FileRecording = FileRecordingType + export type Player = PlayerClass + export type Recorder = RecorderClass +} + export default Buzz -export type { Device, AudioFormat } -export { type Player } from "./player.js" -export { type Recorder } from "./recorder.js" diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..eeedf87 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,26 @@ +import { runPhone } from "./phone" + +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) +} + +if (!agentId) { + console.error( + "❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required", + ) + console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai") + process.exit(1) +} + +console.log(`☎️ Starting phone with pid=${process.pid}`) +try { + await runPhone(agentId, apiKey) +} catch (error) { + console.error(`❌ Error starting phone: ${(error as Error).message}`) + process.exit(1) +} +console.log(`👋 Goodbye!`) diff --git a/src/phone.ts b/src/phone.ts index c2f677b..47939c5 100644 --- a/src/phone.ts +++ b/src/phone.ts @@ -1,15 +1,24 @@ -import { d, reduce, createMachine, state, transition, interpret, guard } from "robot3" +import { + d, + reduce, + createMachine, + state, + transition, + interpret, + action, + invoke, + type Service, +} from "robot3" import { Baresip } from "./sip" -import { log } from "./utils/log" +import log from "./utils/log" 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" - -// TODO: Kill baresip process on exit +import { ring } from "./utils" +import { getSound, WaitingSounds } from "./utils/waiting-sounds" type CancelableTask = () => void @@ -17,422 +26,408 @@ type PhoneContext = { lastError?: string peer?: string numberDialed: number - cancelDialTone?: CancelableTask cancelRinger?: CancelableTask baresip: Baresip - startAgent: () => CancelableTask - cancelAgent?: CancelableTask + stopAgent?: CancelableTask + ringer: GPIO.Output + agentId: string + agentKey: string } -const gpio = new GPIO() -using ringer = gpio.output(17, { resetOnClose: true }) -using hook = gpio.input(27, { pull: "up", debounce: 3 }) -using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 }) -using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 }) +type PhoneService = Service + +const player = await Buzz.defaultPlayer() +let dialTonePlayback: Buzz.Playback | undefined + +export const runPhone = async (agentId: string, agentKey: string) => { + const gpio = new GPIO() + using ringer = gpio.output(17, { resetOnClose: true }) + using hook = gpio.input(27, { pull: "up", debounce: 3 }) + using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 }) + using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 }) -export const startPhone = async (agentId: string, apiKey: string) => { await Buzz.setVolume(0.4) - log.info(`📞 Hook ${hook.value}`) + log(`📞 Phone is ${hook.value ? "off hook" : "on hook"}`) - let digit = 0 + const phoneService = interpret(phoneMachine, () => {}) + listenForPhoneEvents(phoneService, hook, rotaryInUse, rotaryNumber) + const baresip = await startBaresip(phoneService, hook, ringer) + phoneService.send({ type: "config", baresip, agentId, agentKey, ringer }) - hook.onChange((event) => { - 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 - } - }) - - rotaryInUse.onChange((event) => { - if (event.value === 0) { - digit = 0 - } else { - log.info(`📞 Dialed digit: ${digit}`) - } - }) - - rotaryNumber.onChange((event) => { - if (event.value === 1) { - digit += 1 - } - }) + process.on("SIGINT", () => cleanup(baresip)) + process.on("SIGTERM", () => cleanup(baresip)) // Keep process running await new Promise(() => {}) } -const apiKey = process.env.ELEVEN_API_KEY -const agentId = process.env.ELEVEN_AGENT_ID +const listenForPhoneEvents = ( + phoneService: PhoneService, + hook: GPIO.Input, + rotaryInUse: GPIO.Input, + rotaryNumber: GPIO.Input, +) => { + hook.onChange((event) => { + const type = event.value == 0 ? "hang-up" : "pick-up" + log(`📞 Hook ${event.value} sending ${type}`) + phoneService.send({ type }) + }) -if (!apiKey) { - console.error("❌ Error: ELEVEN_API_KEY environment variable is required") - process.exit(1) + rotaryInUse.onChange((event) => { + if (event.value === 0) { + phoneService.send({ type: "dial-start" }) + } else { + phoneService.send({ type: "dial-stop" }) + } + }) + + rotaryNumber.onChange((event) => { + if (event.value === 1) { + phoneService.send({ type: "digit_increment" }) + } + }) } -if (!agentId) { - console.error( - "❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required" - ) - console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai") - process.exit(1) +const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer: GPIO.Output) => { + const baresipConfig = join(import.meta.dir, "..", "baresip") + const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig]) + + baresip.registrationSuccess.connect(async () => { + log("🐻 server connected") + if (hook.value === 0) { + phoneService.send({ type: "initialized" }) + } else { + phoneService.send({ type: "pick-up" }) + } + }) + + baresip.callReceived.connect(({ contact }) => { + log(`🐻 incoming call from ${contact}`) + phoneService.send({ type: "incoming-call", from: contact }) + }) + + baresip.callEstablished.connect(({ contact }) => { + log(`🐻 call established with ${contact}`) + phoneService.send({ type: "answered" }) + }) + + baresip.hungUp.connect(() => { + log("🐻 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(ringer, 500) + await sleep(250) + } + process.exit(1) + }) + + return baresip } -await startPhone(agentId, apiKey) - -const startBaresip = async (hook: GPIO.InputPin) => { - // const baresipConfig = join(import.meta.dir, "..", "baresip") - // const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig]) - - // 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" }) - // } - // }) - - // baresip.callReceived.connect(({ contact }) => { - // log.info(`🐻 incoming call from ${contact}`) - // phoneService.send({ type: "incoming_call", from: contact }) - // }) - - // 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) - // }) - - // const agent = new Agent({ - // agentId, - // apiKey, - // tools: { - // search_web: (args: { query: string }) => searchWeb(args.query), - // }, - // }) +const cleanup = (baresip: Baresip) => { + try { + log("🛑 Shutting down, stopping agent process") + baresip.kill() + } catch (error) { + log.error("Error during shutdown:", error) + } finally { + process.exit(0) + } } -// handleAgentEvents(agent) +const handleError = (ctx: PhoneContext, event: { type: "error"; message?: string }) => { + ctx.lastError = event.message + log.error(`Phone Error: ${event.message}`) + return ctx +} -// const startAgent = () => { -// log.info("☎️ Starting agent conversation") +const config = ( + ctx: PhoneContext, + event: { baresip: Baresip; agentId: string; agentKey: string; ringer: GPIO.Output }, +) => { + ctx.baresip = event.baresip + ctx.agentId = event.agentId + ctx.agentKey = event.agentKey + ctx.ringer = event.ringer + return ctx +} -// if (agentProcess?.stdin) { -// agentProcess.stdin.write("start\n") -// } else { -// log.error("☎️ No agent process stdin available") -// phoneService.send({ type: "remote_hang_up" }) -// } +const startAgent = (service: Service, ctx: PhoneContext) => { + let streamPlayback = player.playStream() -// return () => { -// log.info("☎️ Stopping agent conversation") -// if (agentProcess?.stdin) { -// agentProcess.stdin.write("stop\n") -// } -// } -// } + const agent = new Agent({ + agentId: ctx.agentId, + apiKey: ctx.agentKey, + tools: { + search_web: (args: { query: string }) => searchWeb(args.query), + }, + }) -// const context = (initial?: Partial): PhoneContext => ({ -// numberDialed: 0, -// baresip, -// startAgent, -// ...initial, -// }) + handleAgentEvents(service, agent, streamPlayback) + const stopListening = startListening(service, agent) -// 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 -// ) + ctx.stopAgent = () => { + stopListening() + dialTonePlayback?.stop() + streamPlayback.stop() + } -// const phoneService = interpret(phoneMachine, () => {}) + return ctx +} -// d._onEnter = function (machine, to, state, prevState, event) { -// log.info(`📱 ${machine.current} -> ${to} (${JSON.stringify(event)})`) -// } +const startListening = (service: Service, agent: Agent) => { + const abortAgent = new AbortController() -// 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 }) -// }) + new Promise(async (resolve) => { + const recorder = await Buzz.defaultRecorder() + const listenPlayback = recorder.start() + let backgroundNoisePlayback: Buzz.Playback | undefined + let waitingForVoice = true + const maxPreBufferChunks = 4 // Keep ~1 second of audio before speech detection -// 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 }) -// }) + let preConnectionBuffer: Uint8Array[] = [] -// gpio.monitor(pins.rotaryNumber, { bias: "pull-up", throttleMs: 90 }, (event) => { -// if (event.edge !== "rising") return -// phoneService.send({ type: "digit_increment" }) -// }) + agent.events.connect(async (event) => { + if (event.type === "disconnected") abortAgent.abort() + }) -// // Graceful shutdown handling -// const cleanup = () => { -// log.info("🛑 Shutting down, stopping agent process") -// if (agentProcess?.stdin) { -// agentProcess.stdin.write("quit\n") -// } -// } + for await (const chunk of listenPlayback.stream()) { + if (abortAgent.signal.aborted) { + agent.stop() + listenPlayback.stop() + backgroundNoisePlayback?.stop() -// process.on("SIGINT", cleanup) -// process.on("SIGTERM", cleanup) -// process.on("exit", cleanup) -// } + resolve() + break + } -// const handleAgentEvents = (agent: Agent) => { -// agent.events.connect(async (event) => { -// switch (event.type) { -// case "connected": -// console.log("✅ Connected to AI agent\n") -// break + if (waitingForVoice) { + preConnectionBuffer.push(chunk) + if (preConnectionBuffer.length > maxPreBufferChunks) { + preConnectionBuffer.shift() + } -// case "user_transcript": -// console.log(`👤 You: ${event.transcript}`) -// break + const rms = Buzz.calculateRMS(chunk) + if (rms > 5000) { + dialTonePlayback?.stop() + service.send({ type: "start-agent" }) + waitingForVoice = false -// case "agent_response": -// console.log(`🤖 Agent: ${event.response}`) -// break + backgroundNoisePlayback = await player.play(getSound("background"), { repeat: true }) -// case "audio": -// await waitingIndicator.stop() -// const audioBuffer = Buffer.from(event.audioBase64, "base64") -// streamPlayback.write(audioBuffer) -// break + await agent.start() -// case "interruption": -// console.log("🛑 User interrupted") -// streamPlayback?.stop() -// streamPlayback = player.playStream() // Reset playback stream -// break + // Send pre-buffered audio + for (const chunk of preConnectionBuffer) agent.sendAudio(chunk) + preConnectionBuffer = [] + } + } else { + agent.sendAudio(chunk) + } + } + }) -// case "tool_call": -// waitingIndicator.start(streamPlayback) -// console.log(`🔧 Tool call: ${event.name}(${JSON.stringify(event.args)})`) -// break + return () => abortAgent.abort() +} -// case "tool_result": -// console.log(`✅ Tool result: ${JSON.stringify(event.result)}`) -// break +const handleAgentEvents = ( + service: Service, + agent: Agent, + streamPlayback: Buzz.StreamingPlayback, +) => { + const waitingIndicator = new WaitingSounds(player) -// case "tool_error": -// console.error(`❌ Tool error: ${event.error}`) -// break + agent.events.connect(async (event) => { + switch (event.type) { + case "connected": + log("🤖 Connected to AI agent\n") + 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 "user_transcript": + log(`🤖 You: ${event.transcript}`) + break -// case "error": -// console.error("Agent error:", event.error) -// break + case "agent_response": + log(`🤖 Agent: ${event.response}`) + break -// case "ping": -// break + case "audio": + await waitingIndicator.stop() + const audioBuffer = Buffer.from(event.audioBase64, "base64") + streamPlayback.write(audioBuffer) + break -// default: -// console.log(`😵‍💫 ${event.type}`) -// break -// } -// }) -// } + case "interruption": + log("🤖 User interrupted") + streamPlayback?.stop() + streamPlayback = player.playStream() // Reset playback stream + break -// const incomingCallRing = (): CancelableTask => { -// let abortController = new AbortController() + case "tool_call": + waitingIndicator.start(streamPlayback) + log(`🤖 Tool call: ${event.name}(${JSON.stringify(event.args)})`) + break -// const playRingtone = async () => { -// while (!abortController.signal.aborted) { -// await ring(2000, abortController.signal) -// await sleep(4000) -// } -// } -// playRingtone().catch((error) => log.error("Ringer error:", error)) + case "tool_result": + log(`🤖 Tool result: ${JSON.stringify(event.result)}`) + break -// return () => abortController.abort() -// } + case "tool_error": + console.error(`❌ Tool error: ${event.error}`) + break -// const handleError = (ctx: PhoneContext, event: { type: "error"; message?: string }) => { -// ctx.lastError = event.message -// log.error(`Phone error: ${event.message}`) -// return ctx -// } + case "disconnected": + log(`🤖 👋 Conversation ended, returning to dialtone`) + streamPlayback?.stop() + service.send({ type: "remote-hang-up" }) + break -// const incomingCall = (ctx: PhoneContext, event: { type: "incoming_call"; from?: string }) => { -// ctx.peer = event.from -// ctx.cancelRinger = incomingCallRing() -// log.info(`Incoming call from ${event.from}`) + case "error": + log.error("🤖 Agent error:", event.error) + break -// return ctx -// } + case "ping": + break -// const stopRinger = (ctx: PhoneContext) => { -// ctx.cancelRinger?.() -// ctx.cancelRinger = undefined -// return ctx -// } + default: + log.debug(`😵 Unknown agent event ${event.type}`) + break + } + }) +} -// const playDialTone = (ctx: PhoneContext) => { -// const tone = new ToneGenerator() +const stopAgent = (ctx: PhoneContext) => { + ctx.stopAgent?.() + ctx.stopAgent = undefined + return ctx +} -// tone.loopTone([350, 440]) +const incomingCall = (ctx: PhoneContext, event: { type: "incoming-call"; from?: string }) => { + ctx.peer = event.from + return ctx +} -// ctx.cancelDialTone = () => { -// tone.stop() -// } +const hangUp = (ctx: PhoneContext) => { + console.log(`📞 Hanging up call`) + ctx.baresip.hangUp() +} -// return ctx -// } +const answerCall = (ctx: PhoneContext) => { + log(`📞 Answering call`) + ctx.baresip.accept() +} -// const playOutgoingTone = () => { -// const tone = new ToneGenerator() -// let canceled = false +const makeCall = async (ctx: PhoneContext) => { + log(`Dialing number: ${ctx.numberDialed}`) + if (ctx.numberDialed === 1) { + ctx.baresip.dial("+13476229543") + } else if (ctx.numberDialed === 2) { + ctx.baresip.dial("+18109643563") + } else { + log.error(`No contact for number dialed: ${ctx.numberDialed}`) + } -// const play = async () => { -// while (!canceled) { -// await tone.playTone([440, 480], 2000) -// await sleep(4000) -// } -// } + return ctx +} -// play().catch((error) => log.error("Outgoing tone error:", error)) +const startRinger = async (ctx: PhoneContext) => { + let abortController = new AbortController() + const keepRinging = async () => { + while (!abortController.signal.aborted) { + await ring(ctx.ringer, 2000, abortController.signal) + await sleep(4000) + } + } + keepRinging().catch((error) => log.error("Ringer error:", error)) -// return () => { -// tone.stop() -// canceled = true -// } -// } + ctx.cancelRinger = () => abortController.abort() -// const dialStart = (ctx: PhoneContext) => { -// ctx.numberDialed = 0 -// ctx = stopDialTone(ctx) + return ctx +} -// return ctx -// } +const stopRinger = (ctx: PhoneContext) => { + ctx.cancelRinger?.() + ctx.cancelRinger = undefined + 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)) -// } +async function startDialToneAndAgent(this: any, ctx: PhoneContext) { + ctx = await startAgent(this, ctx) -// return ctx -// } + await dialTonePlayback?.stop() + dialTonePlayback = await player.playTone([350, 440], Infinity) -// const makeAgentCall = (ctx: PhoneContext) => { -// log.info(`Calling agent`) -// ctx.cancelAgent = ctx.startAgent() + return ctx +} -// return ctx -// } +const stopDialTone = () => { + dialTonePlayback?.stop() +} -// const callAgentGuard = (ctx: PhoneContext) => { -// return ctx.numberDialed === 10 -// } +const dialStart = (ctx: PhoneContext) => { + ctx.numberDialed = 0 + return ctx +} -// const callAnswered = (ctx: PhoneContext) => { -// ctx.baresip.accept() +const digitIncrement = (ctx: PhoneContext) => { + ctx.numberDialed += 1 + return ctx +} -// ctx.cancelDialTone?.() -// ctx.cancelDialTone = undefined +const t = transition +const r = reduce +const a = action -// ctx.cancelRinger?.() -// ctx.cancelRinger = undefined +const phoneMachine = createMachine( + "booting", + // prettier-ignore + { + booting: state( + t("config", "initializing", r(config)) + ), + initializing: state( + t("initialized", "idle"), + t("pick-up", "ready"), + t("error", "fault", r(handleError))), + idle: state( + t("incoming-call", "incoming", r(incomingCall)), + t("pick-up", "ready")), + incoming: invoke(startRinger, + t("remote-hang-up", "idle", r(stopRinger)), + t("pick-up", "connected", r(stopRinger), a(answerCall))), + connected: state( + t("remote-hang-up", "ready"), + t("hang-up", "idle", a(hangUp))), + ready: invoke(startDialToneAndAgent, + t("dial-start", "dialing", a(stopDialTone), r(dialStart), a(stopAgent)), + t("hang-up", "idle", a(stopDialTone), a(stopAgent)), + t("start-agent", "connectToAgent", a(stopDialTone))), + connectToAgent: state( + t("hang-up", "idle", r(stopAgent)), + t("remote-hang-up", "ready", r(stopAgent))), + dialing: state( + t("dial-stop", "outgoing"), + t("digit_increment", "dialing", r(digitIncrement)), + t("hang-up", "idle")), + outgoing: invoke(makeCall, + t("answered", "connected"), + t("hang-up", "idle", a(hangUp))), + aborted: state( + t("hang-up", "idle")), + fault: state(), + }, +) -// 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 -// } +d._onEnter = function (machine, to, state, prevState, event) { + log(`📱 ${machine.current} -> ${to} (${(event as any).type})`) +} diff --git a/src/pins/index.ts b/src/pins/index.ts index 34b0606..a856869 100644 --- a/src/pins/index.ts +++ b/src/pins/index.ts @@ -82,6 +82,8 @@ namespace GPIO { export type InputOptions = Type.InputOptions export type OutputOptions = Type.OutputOptions export type InputEvent = Type.InputEvent + export type Input = import("./input").Input + export type Output = import("./output").Output } export default GPIO diff --git a/src/sip.ts b/src/sip.ts index afbe9a6..722a4fe 100644 --- a/src/sip.ts +++ b/src/sip.ts @@ -1,4 +1,4 @@ -import { log } from "./utils/log.ts" +import log from "./utils/log.ts" import { Signal } from "./utils/signal.ts" import { processStdout, processStderr } from "./utils/stdio.ts" diff --git a/src/test-operator.ts b/src/test-operator.ts index ad33fd4..dc04b9b 100755 --- a/src/test-operator.ts +++ b/src/test-operator.ts @@ -1,5 +1,4 @@ import Buzz from "./buzz/index.ts" -import type { Playback } from "./buzz/utils.ts" import { Agent } from "./agent/index.ts" import { searchWeb } from "./agent/tools.ts" import { getSound, WaitingSounds } from "./utils/waiting-sounds.ts" @@ -19,8 +18,8 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => { }, }) - let currentDialtone: Playback | undefined - let currentBackgroundNoise: Playback | undefined + let currentDialtone: Buzz.Playback | undefined + let currentBackgroundNoise: Buzz.Playback | undefined let streamPlayback = player.playStream() const waitingIndicator = new WaitingSounds(player) diff --git a/src/test-pins.ts b/src/test-pins.ts index 0ff95c2..561c9e0 100644 --- a/src/test-pins.ts +++ b/src/test-pins.ts @@ -1,4 +1,4 @@ -import { GPIO } from "./pins" +import GPIO from "./pins" console.log(`kill -9 ${process.pid}`) diff --git a/src/utils/index.ts b/src/utils/index.ts index 0d0fcd6..4a06864 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +import type GPIO from "../pins" + export const ensure = (value: T, message: string): T => { if (value === undefined || value === null) { throw new Error(message) @@ -9,3 +11,17 @@ export const ensure = (value: T, message: string): T => { export const random = (arr: ReadonlyArray): T => { return arr[Math.floor(Math.random() * arr.length)]! } + +export const ring = async (ringer: GPIO.Output, duration: number, signal?: AbortSignal) => { + try { + const endAt = performance.now() + duration + while (performance.now() < endAt && !signal?.aborted) { + ringer.value = 1 + await Bun.sleep(50) + ringer.value = 0 + await Bun.sleep(50) + } + } finally { + ringer.value = 0 + } +} diff --git a/src/utils/log.ts b/src/utils/log.ts index 301611f..33e1c00 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,21 +1,21 @@ let showDebug = true let showInfo = true -let showError = true -export function setLogLevel(level: "debug" | "info" | "error" | "none") { +export function setLogLevel(level: "debug" | "info" | "error") { showDebug = level === "debug" showInfo = level === "debug" || level === "info" - showError = level !== "none" } -export const log = { - debug: (...args: any[]) => { - if (showDebug) console.debug("DEBUG: ", ...args) - }, - info: (...args: any[]) => { - if (showInfo) console.log("INFO: ", ...args) - }, - error: (...args: any[]) => { - if (showError) console.error("ERROR: ", ...args) - }, +const log = (...args: any[]) => { + if (showInfo) console.log("👁️‍🗨️ INFO: ", ...args) } + +log.debug = (...args: any[]) => { + if (showDebug) console.debug("🪲 DEBUG: ", ...args) +} + +log.error = (...args: any[]) => { + console.error("💥 ERROR: ", ...args) +} + +export default log diff --git a/src/utils/signal.ts b/src/utils/signal.ts index c10609c..e537562 100644 --- a/src/utils/signal.ts +++ b/src/utils/signal.ts @@ -13,10 +13,6 @@ * Emit a signal: * chatSignal.emit({ username: "Chad", message: "Hey everyone, how's it going?" }); * - * Forward a signal: - * const relaySignal = new Signal<{ username: string, message: string }>() - * const disconnectRelay = chatSignal.connect(relaySignal) - * // Now, when chatSignal emits, relaySignal will also emit the same data * * Disconnect a single listener: * disconnect(); // The disconnect function is returned when you connect to a signal @@ -55,3 +51,46 @@ export class Signal { this.listeners = [] } } + +/** + * How to use Emitter: + * + * Create an emitter: + * const chat = new Emitter<{ username: string, message: string }>() + * + * Listen to events: + * const off = chat.on((data) => { + * const {username, message} = data; + * console.log(`${username} said "${message}"`); + * }) + * + * Emit an event: + * chat.emit({ username: "Chad", message: "Hey everyone, how's it going?" }); + * + * Remove a specific listener: + * off(); // The off function is returned when you add a listener + * + * Remove all listeners: + * chat.removeAllListeners() + */ +export class Emitter { + private listeners: Array<(data: T) => void> = [] + + on(listener: (data: T) => void) { + this.listeners.push(listener) + + return () => { + this.listeners = this.listeners.filter((l) => l !== listener) + } + } + + emit(data: T) { + for (const listener of this.listeners) { + listener(data) + } + } + + removeAllListeners() { + this.listeners = [] + } +} diff --git a/src/utils/stdio.ts b/src/utils/stdio.ts index 0165074..a1d2888 100644 --- a/src/utils/stdio.ts +++ b/src/utils/stdio.ts @@ -1,4 +1,4 @@ -import { log } from "./log.ts" +import log from "./log.ts" export const LineSplitter = () => { let buffer = "" diff --git a/src/utils/waiting-sounds.ts b/src/utils/waiting-sounds.ts index 5d4e4e2..2d66f93 100644 --- a/src/utils/waiting-sounds.ts +++ b/src/utils/waiting-sounds.ts @@ -1,15 +1,14 @@ -import { type Player } from "../buzz/index.ts" +import Buzz from "../buzz/index.ts" import { join } from "path" -import type { Playback, StreamingPlayback } from "../buzz/utils.ts" import { random } from "./index.ts" export class WaitingSounds { - typingPlayback?: Playback - speakingPlayback?: Playback + typingPlayback?: Buzz.Playback + speakingPlayback?: Buzz.Playback - constructor(private player: Player) {} + constructor(private player: Buzz.Player) {} - async start(operatorStream: StreamingPlayback) { + async start(operatorStream: Buzz.StreamingPlayback) { if (this.typingPlayback) return // Already playing this.#startTypingSounds() @@ -35,7 +34,7 @@ export class WaitingSounds { }) } - async #startSpeakingSounds(operatorStream: StreamingPlayback) { + async #startSpeakingSounds(operatorStream: Buzz.StreamingPlayback) { const playedSounds = new Set() let dir: SoundDir | undefined return new Promise(async (resolve) => { -- 2.50.1 From 28186bc0ce3244cb96ab1fc6ea988eca450ac774 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 20 Nov 2025 18:18:47 -0800 Subject: [PATCH 2/6] wip --- scripts/bootstrap-services.ts | 98 ++++ scripts/bootstrap.ts | 81 +-- scripts/{cli.sh => cli.ts} | 0 scripts/deploy.ts | 20 +- scripts/setup-services.ts | 14 + src/agent/README.md | 6 +- src/buzz/README.md | 528 +++++++++++++++++++ src/buzz/index.ts | 10 +- src/phone.ts | 34 +- src/pins/FFI-LEARNINGS.md | 173 ------ src/services/ap-monitor.ts | 14 +- src/services/server/components/IndexPage.tsx | 5 - src/services/server/components/Layout.tsx | 13 +- src/services/server/components/LogsPage.tsx | 80 +-- src/services/server/server.tsx | 33 +- src/test-buzz.ts | 4 +- src/test-operator.ts | 4 +- 17 files changed, 744 insertions(+), 373 deletions(-) create mode 100644 scripts/bootstrap-services.ts rename scripts/{cli.sh => cli.ts} (100%) create mode 100644 scripts/setup-services.ts create mode 100644 src/buzz/README.md delete mode 100644 src/pins/FFI-LEARNINGS.md diff --git a/scripts/bootstrap-services.ts b/scripts/bootstrap-services.ts new file mode 100644 index 0000000..82b6c2b --- /dev/null +++ b/scripts/bootstrap-services.ts @@ -0,0 +1,98 @@ +import { $ } from "bun" +import { writeFileSync } from "fs" + +const AP_SERVICE_FILE = "/etc/systemd/system/phone-ap.service" +const WEB_SERVICE_FILE = "/etc/systemd/system/phone-web.service" +const PHONE_SERVICE_FILE = "/etc/systemd/system/phone.service" + +export const setupServices = async (installDir: string) => { + console.log("\nInstalling systemd services...") + + // Find where bun is installed + const bunPath = await $`which bun` + .quiet() + .nothrow() + .text() + .then((p) => p.trim()) + + if (!bunPath) { + console.error("Error: bun not found in PATH. Please ensure bun is available system-wide.") + process.exit(1) + } + console.log(`Using bun at: ${bunPath}`) + + // Create AP monitor service + const apServiceContent = `[Unit] +Description=Phone WiFi AP Monitor +After=network.target +Before=phone-web.service + +[Service] +Type=simple +ExecStart=${bunPath} ${installDir}/src/services/ap-monitor.ts +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +` + writeFileSync(AP_SERVICE_FILE, apServiceContent, "utf8") + console.log("✓ Created phone-ap.service") + + // Create web server service + const webServiceContent = `[Unit] +Description=Phone Web Server +After=network.target phone-ap.service + +[Service] +Type=simple +ExecStart=${bunPath} ${installDir}/src/services/server/server.tsx +WorkingDirectory=${installDir} +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +` + writeFileSync(WEB_SERVICE_FILE, webServiceContent, "utf8") + console.log("✓ Created phone-web.service") + + // Create phone service + const phoneServiceContent = `[Unit] +Description=Phone Application +After=network.target sound.target +Requires=sound.target + +[Service] +Type=simple +User=corey +ExecStart=${bunPath} ${installDir}/src/main.ts +WorkingDirectory=${installDir} +EnvironmentFile=${installDir}/.env +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +` + writeFileSync(PHONE_SERVICE_FILE, phoneServiceContent, "utf8") + console.log("✓ Created phone.service") + + await $`systemctl daemon-reload` + await $`systemctl enable phone-ap.service` + await $`systemctl enable phone-web.service` + await $`systemctl enable phone.service` + console.log("✓ Services enabled") + + console.log("\nStarting the services...") + await $`systemctl start phone-ap.service` + await $`systemctl start phone-web.service` + await $`systemctl start phone.service` + console.log("✓ Services started") +} diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts index 905c196..d78bdbf 100755 --- a/scripts/bootstrap.ts +++ b/scripts/bootstrap.ts @@ -1,5 +1,4 @@ import { $ } from "bun" -import { writeFileSync } from "fs" console.log(` ========================================== @@ -16,8 +15,6 @@ if (process.getuid && process.getuid() !== 0) { // Get install directory from argument or use default const INSTALL_DIR = process.argv[2] || "/home/corey/phone" -const AP_SERVICE_FILE = "/etc/systemd/system/phone-ap.service" -const WEB_SERVICE_FILE = "/etc/systemd/system/phone-web.service" console.log(`Install directory: ${INSTALL_DIR}`) @@ -32,82 +29,6 @@ console.log(`✓ Dependencies installed`) console.log("\nInstalling Baresip...") await $`sudo apt install -y baresip` -console.log("\nInstalling systemd services...") -// Find where bun is installed -const bunPath = await $`which bun` - .quiet() - .nothrow() - .text() - .then((p) => p.trim()) -if (!bunPath) { - console.error("Error: bun not found in PATH. Please ensure bun is available system-wide.") - process.exit(1) -} -console.log(`Using bun at: ${bunPath}`) - -// Create AP monitor service -const apServiceContent = `[Unit] -Description=Phone WiFi AP Monitor -After=network.target -Before=phone-web.service - -[Service] -Type=simple -ExecStart=${bunPath} ${INSTALL_DIR}/services/ap-monitor.ts -Restart=on-failure -RestartSec=5 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target -` -writeFileSync(AP_SERVICE_FILE, apServiceContent, "utf8") -console.log("✓ Created phone-ap.service") - -// Create web server service -const webServiceContent = `[Unit] -Description=Phone Web Server -After=network.target phone-ap.service - -[Service] -Type=simple -ExecStart=${bunPath} ${INSTALL_DIR}/services/server/server.tsx -WorkingDirectory=${INSTALL_DIR} -Restart=on-failure -RestartSec=5 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target -` -writeFileSync(WEB_SERVICE_FILE, webServiceContent, "utf8") -console.log("✓ Created phone-web.service") - -await $`systemctl daemon-reload` -await $`systemctl enable phone-ap.service` -await $`systemctl enable phone-web.service` -console.log("✓ Services enabled") - -console.log("\nStarting the services...") -await $`systemctl start phone-ap.service` -await $`systemctl start phone-web.service` -console.log("✓ Services started") - console.log(` -========================================== -✓ Bootstrap complete! -========================================== - -Both services are now running and will start automatically on boot: -- phone-ap.service: Monitors WiFi and manages AP -- phone-web.service: Web server for configuration - -How it works: -- If connected to WiFi: Access at http://phone.local -- If NOT connected: WiFi AP "phone-setup" will start automatically - Connect to the AP at the same address http://phone.local - -To check status use ./cli +✅ Bootstrap complete! `) diff --git a/scripts/cli.sh b/scripts/cli.ts similarity index 100% rename from scripts/cli.sh rename to scripts/cli.ts diff --git a/scripts/deploy.ts b/scripts/deploy.ts index aa1a894..e4b6314 100755 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -40,22 +40,10 @@ if (shouldBootstrap) { // make console beep await $`afplay /System/Library/Sounds/Blow.aiff` -// Always check if services exist and restart them (whether we bootstrapped or not) -console.log("Checking for existing services...") -const apServiceExists = await $`ssh ${PI_HOST} "systemctl is-enabled phone-ap.service"` - .nothrow() - .quiet() -const webServiceExists = await $`ssh ${PI_HOST} "systemctl is-enabled phone-web.service"` - .nothrow() - .quiet() - -if (apServiceExists.exitCode === 0 && webServiceExists.exitCode === 0) { - console.log("Restarting services...") - await $`ssh ${PI_HOST} "sudo systemctl restart phone-ap.service phone-web.service"` - console.log("✓ Services restarted\n") -} else if (!shouldBootstrap) { - console.log("Services not installed. Run with --bootstrap to install.\n") -} +// Always set up services on every deploy +console.log("Setting up services...") +await $`ssh ${PI_HOST} "sudo bun ${PI_DIR}/scripts/setup-services.ts ${PI_DIR}"` +console.log("✓ Services configured and running\n") console.log(` ✓ Deploy complete! diff --git a/scripts/setup-services.ts b/scripts/setup-services.ts new file mode 100644 index 0000000..f3f397a --- /dev/null +++ b/scripts/setup-services.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env bun + +import { setupServices } from "./bootstrap-services" + +// Get install directory from argument or use default +const INSTALL_DIR = process.argv[2] || "/home/corey/phone" + +console.log(`Setting up services for: ${INSTALL_DIR}`) + +await setupServices(INSTALL_DIR) + +console.log(` +✓ Services configured and running! +`) diff --git a/src/agent/README.md b/src/agent/README.md index 7166f44..93a4020 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -19,7 +19,7 @@ const agent = new Agent({ }) // Set up event handlers -const player = await Buzz.defaultPlayer() +const player = await Buzz.player() let playback = player.playStream() agent.events.connect((event) => { @@ -43,7 +43,7 @@ agent.events.connect((event) => { await agent.start() // Continuously stream audio -const recorder = await Buzz.defaultRecorder() +const recorder = await Buzz.recorder() const recording = recorder.start() for await (const chunk of recording.stream()) { agent.sendAudio(chunk) @@ -53,7 +53,7 @@ for await (const chunk of recording.stream()) { ## VAD Pattern ```typescript -const recorder = await Buzz.defaultRecorder() +const recorder = await Buzz.recorder() const recording = recorder.start() const buffer = new RollingBuffer() diff --git a/src/buzz/README.md b/src/buzz/README.md new file mode 100644 index 0000000..1d9f419 --- /dev/null +++ b/src/buzz/README.md @@ -0,0 +1,528 @@ +# Buzz + +High-level audio library for Bun using ALSA with streaming support and voice activity detection. + +## Features + +- Play audio files with repeat option +- Generate and play multi-frequency tones (dial tones, DTMF, etc.) +- Stream audio playback with buffer tracking +- Record audio to stream or file (WAV) +- Volume control via ALSA mixer +- Device discovery and selection +- Voice activity detection via RMS calculation +- Type-safe TypeScript API with namespace types +- Zero external dependencies (uses ALSA `aplay` and `arecord`) + +## Requirements + +- Bun 1.0+ +- ALSA utilities (`aplay`, `arecord`, `amixer`) +- Linux system with ALSA support +- TypeScript 5.2+ + +## Quick Start + +```typescript +import Buzz from "./buzz" + +// Play an audio file +const player = await Buzz.player() +const playback = await player.play("./sounds/greeting.wav") +await playback.finished() + +// Generate a dial tone +const dialTone = await player.playTone([350, 440], Infinity) // infinite duration +await Buzz.sleep(3000) +await dialTone.stop() + +// Record audio +const recorder = await Buzz.recorder() +const recording = recorder.start() + +for await (const chunk of recording.stream()) { + const rms = Buzz.calculateRMS(chunk) + if (rms > 5000) { + console.log("Speech detected!") + } +} +``` + +## API + +### Buzz Module + +#### `Buzz.player(label?, format?)` + +Create a player. Omit `label` to use the default playback device. + +```typescript +const player = await Buzz.player() // default device +const player = await Buzz.player(undefined, { sampleRate: 16000 }) // default device with custom format +const player = await Buzz.player("USB Audio") // specific device +const player = await Buzz.player("Speaker", { sampleRate: 44100 }) // specific device with format +``` + +#### `Buzz.recorder(label?, format?)` + +Create a recorder. Omit `label` to use the default capture device. + +```typescript +const recorder = await Buzz.recorder() // default device +const recorder = await Buzz.recorder(undefined, { sampleRate: 16000 }) // default device with custom format +const recorder = await Buzz.recorder("USB Microphone") // specific device +``` + +#### `Buzz.setVolume(volume, label?)` + +Set playback volume (0.0 to 1.0). + +```typescript +await Buzz.setVolume(0.5) // 50% on default device +await Buzz.setVolume(0.8, "Speaker") // 80% on specific device +``` + +#### `Buzz.getVolume(label?)` + +Get current playback volume. + +```typescript +const volume = await Buzz.getVolume() // returns 0.0 to 1.0 +``` + +#### `Buzz.listDevices()` + +List all available audio devices. + +```typescript +const devices = await Buzz.listDevices() +// [ +// { id: 'plughw:0,0', card: 0, device: 0, label: 'bcm2835 Headphones', type: 'playback' }, +// { id: 'plughw:1,0', card: 1, device: 0, label: 'USB Audio', type: 'capture' } +// ] +``` + +#### `Buzz.calculateRMS(audioChunk)` + +Calculate root mean square (RMS) for voice activity detection. + +```typescript +const chunk: Uint8Array = // ... audio data +const rms = Buzz.calculateRMS(chunk) +if (rms > 5000) { + console.log("Voice detected!") +} +``` + +### Player + +#### `player.play(filePath, options?)` + +Play an audio file (WAV format). + +```typescript +const playback = await player.play("./sounds/beep.wav") +const playback = await player.play("./music.wav", { repeat: true }) + +// Wait for playback to finish +await playback.finished() + +// Stop playback +await playback.stop() +``` + +Returns: `Buzz.Playback` + +Options: +- `repeat?: boolean` - Loop the file indefinitely (default: false) + +#### `player.playTone(frequencies, duration)` + +Generate and play a tone with one or more frequencies. + +```typescript +// Dial tone (350 Hz + 440 Hz) +const dialTone = await player.playTone([350, 440], Infinity) + +// DTMF "1" key (697 Hz + 1209 Hz) for 200ms +const dtmf = await player.playTone([697, 1209], 200) + +// Single frequency beep +const beep = await player.playTone([440], 1000) // 440 Hz for 1 second +``` + +Returns: `Buzz.Playback` + +#### `player.playStream()` + +Create a streaming playback handle for real-time audio. + +```typescript +const stream = player.playStream() + +// Write audio chunks +stream.write(audioChunk1) +stream.write(audioChunk2) + +// Check if buffer is empty +if (stream.bufferEmptyFor > 1000) { + console.log("Buffer empty for 1+ seconds") +} + +// Stop streaming +await stream.stop() +``` + +Returns: `Buzz.StreamingPlayback` + +### Recorder + +#### `recorder.start()` + +Start recording to a stream. + +```typescript +const recording = recorder.start() + +for await (const chunk of recording.stream()) { + // Process audio chunks (Uint8Array) + console.log("Received", chunk.byteLength, "bytes") +} +``` + +Returns: `Buzz.StreamingRecording` + +#### `recorder.start(outputFile)` + +Start recording to a WAV file. + +```typescript +const recording = recorder.start("./output.wav") + +// Stop when done +await Bun.sleep(5000) +await recording.stop() +``` + +Returns: `Buzz.FileRecording` + +## Types + +All types are available under the `Buzz` namespace: + +```typescript +Buzz.AudioFormat // { format?, sampleRate?, channels? } +Buzz.Device // { id, card, device, label, type } +Buzz.Playback // { isPlaying, stop(), finished() } +Buzz.StreamingPlayback // { isPlaying, write(), stop(), bufferEmptyFor } +Buzz.StreamingRecording // { isRecording, stream(), stop() } +Buzz.FileRecording // { isRecording, stop() } +Buzz.Player // Player class type +Buzz.Recorder // Recorder class type +``` + +## Audio Format + +Default format: `S16_LE`, 16000 Hz, mono + +```typescript +type AudioFormat = { + format?: string // e.g., "S16_LE", "S32_LE" + sampleRate?: number // e.g., 16000, 44100, 48000 + channels?: number // 1 = mono, 2 = stereo +} +``` + +Common formats: +- **Phone quality**: `{ sampleRate: 8000, channels: 1 }` +- **Voice/AI**: `{ sampleRate: 16000, channels: 1 }` (default) +- **CD quality**: `{ sampleRate: 44100, channels: 2 }` +- **Professional**: `{ sampleRate: 48000, channels: 2 }` + +## Examples + +### Voice Activity Detection + +```typescript +import Buzz from "./buzz" + +const recorder = await Buzz.recorder() +const player = await Buzz.player() + +const recording = recorder.start() +let talking = false + +for await (const chunk of recording.stream()) { + const rms = Buzz.calculateRMS(chunk) + + if (rms > 5000 && !talking) { + console.log("🗣️ Started talking") + talking = true + } else if (rms < 1000 && talking) { + console.log("🤫 Stopped talking") + talking = false + } +} +``` + +### Streaming Playback with Buffer Tracking + +```typescript +import Buzz from "./buzz" + +const player = await Buzz.player() +const stream = player.playStream() + +// Simulate receiving audio chunks from network +const chunks = [chunk1, chunk2, chunk3] // Uint8Array[] + +for (const chunk of chunks) { + stream.write(chunk) + + // Wait until buffer is nearly empty before requesting more + while (stream.bufferEmptyFor < 500) { + await Bun.sleep(100) + } +} + +await stream.stop() +``` + +### Dial Tone with Voice Detection + +```typescript +import Buzz from "./buzz" + +await Buzz.setVolume(0.4) + +const player = await Buzz.player() +const recorder = await Buzz.recorder() + +// Play dial tone +const dialTone = await player.playTone([350, 440], Infinity) + +// Wait for voice +const recording = recorder.start() +const vadThreshold = 5000 + +for await (const chunk of recording.stream()) { + const rms = Buzz.calculateRMS(chunk) + + if (rms > vadThreshold) { + console.log("Voice detected, stopping dial tone") + await dialTone.stop() + break + } +} +``` + +### Play Sound Effects + +```typescript +import Buzz from "./buzz" + +const player = await Buzz.player() + +// Play multiple sounds in sequence +const sounds = ["./start.wav", "./beep.wav", "./end.wav"] + +for (const sound of sounds) { + const playback = await player.play(sound) + await playback.finished() +} +``` + +### Background Music Loop + +```typescript +import Buzz from "./buzz" + +const player = await Buzz.player() + +// Play background music on repeat +const bgMusic = await player.play("./background.wav", { repeat: true }) + +// Stop after 30 seconds +await Bun.sleep(30000) +await bgMusic.stop() +``` + +### Record to File + +```typescript +import Buzz from "./buzz" + +const recorder = await Buzz.recorder() + +console.log("Recording for 10 seconds...") +const recording = recorder.start("./output.wav") + +await Bun.sleep(10000) +await recording.stop() + +console.log("Saved to output.wav") +``` + +### Multi-Device Setup + +```typescript +import Buzz from "./buzz" + +// List all devices +const devices = await Buzz.listDevices() +console.log("Available devices:", devices) + +// Use specific devices +const speaker = await Buzz.player("Speaker") +const mic = await Buzz.recorder("USB Microphone") + +// Independent volume control +await Buzz.setVolume(0.8, "Speaker") +await Buzz.setVolume(1.0, "Headphones") +``` + +## Architecture + +### ALSA Backend + +Buzz wraps ALSA command-line tools (`aplay`, `arecord`) via Bun's subprocess API: + +- **Playback**: Spawns `aplay` with stdin pipe for streaming or file path for file playback +- **Recording**: Spawns `arecord` with stdout pipe for streaming or file path for WAV output +- **Volume**: Uses `amixer` for volume control + +**Benefits:** + +- **Simple**: No C bindings or FFI required +- **Reliable**: ALSA tools are battle-tested +- **Flexible**: Full format support (sample rates, channels, encodings) +- **Portable**: Works on any Linux system with ALSA + +### Streaming Architecture + +Streaming playback uses Bun's subprocess stdin pipe: + +1. Spawn `aplay` with raw audio format and stdin input +2. Write audio chunks to process stdin as they arrive +3. Track buffer duration based on bytes written +4. Calculate `bufferEmptyFor` using performance timestamps + +This enables: +- Real-time playback of network streams (WebSocket, API responses) +- Buffer management for smooth playback +- Low-latency audio (<100ms with proper buffering) + +### Voice Activity Detection + +`calculateRMS()` computes the root mean square of audio samples: + +``` +RMS = sqrt(sum(sample²) / count) +``` + +This provides a simple but effective measure of audio energy: +- Silence: RMS < 1000 +- Noise: RMS 1000-5000 +- Speech: RMS > 5000 + +Adjust thresholds based on your microphone and environment. + +## Device Selection + +### By Default (Recommended) + +```typescript +const player = await Buzz.player() +const recorder = await Buzz.recorder() +``` + +Uses ALSA default device (usually correct). + +### By Label + +```typescript +const devices = await Buzz.listDevices() +// Find device with label containing "USB" +const usbDevice = devices.find(d => d.label.includes("USB")) + +const player = await Buzz.player(usbDevice.label) +``` + +Useful for multi-device setups (USB audio, HDMI, headphones). + +## Error Handling + +```typescript +try { + const player = await Buzz.player() +} catch (err) { + if (err.message.includes("No playback devices found")) { + console.error("No audio output devices available") + } +} + +try { + await Buzz.setVolume(0.5) +} catch (err) { + if (err.message.includes("Failed to set volume")) { + console.error("Could not control volume (check mixer permissions)") + } +} +``` + +## Troubleshooting + +### No devices found + +Check ALSA devices: + +```bash +aplay -l # list playback devices +arecord -l # list capture devices +``` + +### Volume control fails + +Check mixer controls: + +```bash +amixer scontrols +amixer sget Master +``` + +### Crackling or distortion + +Try different buffer sizes by adjusting format: + +```typescript +const player = await Buzz.player(undefined, { + sampleRate: 16000, + channels: 1, + format: "S16_LE" +}) +``` + +### Device already in use + +Only one process can use an ALSA device at a time. Stop other audio applications or use PulseAudio/PipeWire for mixing. + +## Design Philosophy + +- **Simple by default** - `player()` and `recorder()` work out of the box without arguments +- **Streaming-first** - Built for real-time audio (AI voice, telephony, WebRTC) +- **Type-safe** - Namespace types provide autocomplete and compile-time safety +- **Flexible** - Support for files, tones, and streams +- **Minimal dependencies** - Uses standard ALSA tools, no native bindings + +## Performance + +- **Latency**: ~50-100ms for streaming playback (depends on buffering) +- **CPU**: Minimal overhead (subprocess spawning + pipe I/O) +- **Memory**: Efficient streaming (no need to load entire files) +- **Voice detection**: `calculateRMS()` is fast (~1µs per chunk on modern hardware) + +## References + +- [ALSA documentation](https://www.alsa-project.org/wiki/Main_Page) +- [Bun subprocess API](https://bun.sh/docs/api/spawn) +- [Audio sample formats](https://en.wikipedia.org/wiki/Audio_bit_depth) diff --git a/src/buzz/index.ts b/src/buzz/index.ts index fdccb1e..33d8c60 100644 --- a/src/buzz/index.ts +++ b/src/buzz/index.ts @@ -12,13 +12,9 @@ import { type FileRecording as FileRecordingType, } from "./utils.js" -const defaultPlayer = (format?: AudioFormatType) => PlayerClass.create({ format }) +const player = (label?: string, format?: AudioFormatType) => PlayerClass.create({ label, format }) -const player = (label: string, format?: AudioFormatType) => PlayerClass.create({ label, format }) - -const defaultRecorder = (format?: AudioFormatType) => RecorderClass.create({ format }) - -const recorder = (label: string, format?: AudioFormatType) => +const recorder = (label?: string, format?: AudioFormatType) => RecorderClass.create({ label, format }) const getVolumeControl = async (cardNumber?: number): Promise => { @@ -85,9 +81,7 @@ const getVolume = async (label?: string): Promise => { const Buzz = { listDevices, - defaultPlayer, player, - defaultRecorder, recorder, setVolume, getVolume, diff --git a/src/phone.ts b/src/phone.ts index 47939c5..ab191ab 100644 --- a/src/phone.ts +++ b/src/phone.ts @@ -36,7 +36,7 @@ type PhoneContext = { type PhoneService = Service -const player = await Buzz.defaultPlayer() +const player = await Buzz.player() let dialTonePlayback: Buzz.Playback | undefined export const runPhone = async (agentId: string, agentKey: string) => { @@ -49,13 +49,15 @@ export const runPhone = async (agentId: string, agentKey: string) => { await Buzz.setVolume(0.4) log(`📞 Phone is ${hook.value ? "off hook" : "on hook"}`) + playStartRing(ringer) + const phoneService = interpret(phoneMachine, () => {}) listenForPhoneEvents(phoneService, hook, rotaryInUse, rotaryNumber) const baresip = await startBaresip(phoneService, hook, ringer) phoneService.send({ type: "config", baresip, agentId, agentKey, ringer }) - process.on("SIGINT", () => cleanup(baresip)) - process.on("SIGTERM", () => cleanup(baresip)) + process.on("SIGINT", () => cleanup(baresip, ringer)) + process.on("SIGTERM", () => cleanup(baresip, ringer)) // Keep process running await new Promise(() => {}) @@ -134,9 +136,10 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer return baresip } -const cleanup = (baresip: Baresip) => { +const cleanup = (baresip: Baresip, ringer: GPIO.Output) => { try { log("🛑 Shutting down, stopping agent process") + playExitRing(ringer) baresip.kill() } catch (error) { log.error("Error during shutdown:", error) @@ -189,7 +192,7 @@ const startListening = (service: Service, agent: Agent) => const abortAgent = new AbortController() new Promise(async (resolve) => { - const recorder = await Buzz.defaultRecorder() + const recorder = await Buzz.recorder() const listenPlayback = recorder.start() let backgroundNoisePlayback: Buzz.Playback | undefined let waitingForVoice = true @@ -384,6 +387,27 @@ const digitIncrement = (ctx: PhoneContext) => { return ctx } +const playStartRing = async (ringer: GPIO.Output) => { + // Three quick beeps, getting faster = energetic/welcoming + ringer.value = 1 + await Bun.sleep(80) + ringer.value = 0 + await Bun.sleep(120) + + ringer.value = 1 + await Bun.sleep(80) + ringer.value = 0 + await Bun.sleep(100) + + ringer.value = 1 + await Bun.sleep(80) + ringer.value = 0 +} + +const playExitRing = async (ringer: GPIO.Output) => { + ringer.value = 0 // Always try and turn it off! +} + const t = transition const r = reduce const a = action diff --git a/src/pins/FFI-LEARNINGS.md b/src/pins/FFI-LEARNINGS.md deleted file mode 100644 index 1e3adb0..0000000 --- a/src/pins/FFI-LEARNINGS.md +++ /dev/null @@ -1,173 +0,0 @@ -# Bun FFI Learnings - -After researching GitHub examples and Bun's FFI documentation, here's what I found surprising and helpful. - -## Surprising Discoveries - -### 1. **String Handling is Simpler Than Expected** -I initially thought you'd need `CString` everywhere, but: -- For **args**: `FFIType.cstring` just needs `ptr(Buffer.from(str + "\0"))` -- For **returns**: `FFIType.cstring` automatically converts pointers to JS strings -- `CString` is mainly for **reading** C strings from pointers, not passing them - -**Example from real code:** -```javascript -const str = Buffer.from("hello\0", "utf8"); -myFunction(ptr(str)); // Clean and simple! -``` - -### 2. **No Type Wrappers Needed** -Unlike Node-FFI, Bun doesn't require defining structs or complex type wrappers. Just: -```javascript -add: { - args: [FFIType.i32, FFIType.i32], - returns: FFIType.i32, -} -``` - -### 3. **TinyCC JIT Compilation** -Bun embeds TinyCC and JIT-compiles C bindings on the fly. This means: -- 2-6x faster than Node-API -- Zero build step for type conversions -- Direct memory access without serialization - -## Helpful Patterns - -### Pattern 1: String Helper -```typescript -import { ptr } from "bun:ffi" -const cstr = (s: string) => ptr(Buffer.from(s + "\0")) - -// Usage: -gpiod.open(cstr("/dev/gpiochip0")) -``` - -### Pattern 2: Resource Cleanup -Always use cleanup handlers: -```javascript -const cleanup = () => { - lib.symbols.release(resource) - lib.symbols.close(chip) -} -process.on("SIGINT", cleanup) -process.on("SIGTERM", cleanup) -``` - -### Pattern 3: Destructuring Symbols -```javascript -const { - symbols: { functionName } -} = dlopen(path, { /* defs */ }) - -// Call directly: -functionName(arg1, arg2) -``` - -## Common Mistakes to Avoid - -1. **Don't forget null terminators** - `Buffer.from(str + "\0")` not `Buffer.from(str)` -2. **Pointer lifetime** - Keep TypedArrays alive while C code uses them -3. **Type mismatches** - `FFIType.i32` vs `FFIType.u32` matters! -4. **Missing cleanup** - C libraries don't have garbage collection - -## Best Practices from Real Examples - -1. **Use `suffix` for cross-platform library loading:** - ```javascript - import { suffix } from "bun:ffi" - dlopen(`libname.${suffix}`, { /* ... */ }) - ``` - -2. **Check for null on resource creation:** - ```javascript - const chip = lib.gpiod_chip_open(cstr(path)) - if (!chip) { - console.error("Failed to open") - process.exit(1) - } - ``` - -3. **Free configs after use:** - ```javascript - const config = lib.create_config() - // ... use config ... - lib.free_config(config) // Don't leak! - ``` - -## What Makes Bun FFI Special - -- **Performance**: JIT compilation beats traditional FFI -- **Simplicity**: No build tools, no gyp, no node-gyp nightmares -- **TypeScript native**: Works seamlessly with TS type system -- **Built-in**: Ships with Bun, zero dependencies - -## Hard-Won Lessons from GPIO Implementation - -### 1. **Enum values MUST match the C header exactly** -We spent hours debugging because our constants were off by one: -```typescript -// WRONG - missing GPIOD_LINE_BIAS_AS_IS -export const GPIOD_LINE_BIAS_UNKNOWN = 1 // Actually should be 2! -export const GPIOD_LINE_BIAS_DISABLED = 2 // Actually should be 3! -export const GPIOD_LINE_BIAS_PULL_UP = 3 // Actually should be 4! - -// CORRECT - includes AS_IS at position 1 -export const GPIOD_LINE_BIAS_AS_IS = 1 -export const GPIOD_LINE_BIAS_UNKNOWN = 2 -export const GPIOD_LINE_BIAS_DISABLED = 3 -export const GPIOD_LINE_BIAS_PULL_UP = 4 -export const GPIOD_LINE_BIAS_PULL_DOWN = 5 -``` -**Lesson:** Always grep the header file for the complete enum, don't assume! - -### 2. **Hardware debouncing requires correct constants** -With wrong constants, we were accidentally passing `BIAS_DISABLED` instead of `BIAS_PULL_UP`, which meant: -- No pull resistor (pin floated) -- Debouncing didn't work at all -- Got 6+ events per button press - -After fixing: **Clean single events with 1ms debounce via kernel!** - -### 3. **Edge detection is event-driven, not polling** -Don't poll `get_value()` in a loop! Use: -- `gpiod_line_request_wait_edge_events()` - blocks until interrupt -- `gpiod_line_request_read_edge_events()` - reads queued events -- Much more efficient, CPU sleeps until hardware event - -### 4. **TypedArray to pointer needs `ptr()`** -When passing arrays to C functions: -```typescript -const offsets = new Uint32Array([21]) -gpiod.gpiod_line_config_add_line_settings( - lineConfig, - ptr(offsets), // Need ptr() wrapper! - 1, - lineSettings -) -``` - -### 5. **Signal handling for clean shutdown** -Generators don't run `finally` blocks if abandoned. Need: -```typescript -let shouldExit = false -process.on("SIGINT", () => { shouldExit = true }) - -while (!shouldExit) { - const ret = wait_edge_events(request, 100_000_000) // Use timeout! - // ... -} -``` - -### 6. **Button wiring determines logic** -- **GND button + pull-UP**: Press = FALLING edge (HIGH→LOW) -- **VCC button + pull-DOWN**: Press = RISING edge (LOW→HIGH) - -Always check initial pin state to verify wiring! - -## Resources Used - -- Official Bun FFI docs: https://bun.com/docs/runtime/ffi -- libgpiod v2 C API: https://libgpiod.readthedocs.io/en/latest/core_api.html -- Python bindings examples: https://github.com/brgl/libgpiod/tree/master/bindings/python/examples -- Real examples: GitHub searches for bun FFI projects -- Community discussions: Bun issue tracker and HN threads diff --git a/src/services/ap-monitor.ts b/src/services/ap-monitor.ts index 6b9dab4..131126b 100644 --- a/src/services/ap-monitor.ts +++ b/src/services/ap-monitor.ts @@ -115,12 +115,6 @@ async function stopAP() { async function checkAndManageAP() { const connected = await isConnectedToWiFi() - console.log( - `[checkAndManageAP] WiFi: ${connected ? "connected" : "disconnected"}, AP: ${ - apRunning ? "running" : "stopped" - }` - ) - if (connected && apRunning) { console.log("[checkAndManageAP] WiFi connected and AP running → stopping AP") await stopAP() @@ -134,7 +128,7 @@ async function checkAndManageAP() { const savedNetwork = await findAvailableSavedNetwork() if (savedNetwork) { console.log( - `[checkAndManageAP] Found available saved network: ${savedNetwork}, attempting connection...` + `[checkAndManageAP] Found available saved network: ${savedNetwork}, attempting connection...`, ) // Try to connect first @@ -230,6 +224,12 @@ async function tryConnect(connectionName: string): Promise { } // Initial check +const connected = await isConnectedToWiFi() +console.log( + `[checkAndManageAP] WiFi: ${connected ? "connected" : "disconnected"}, AP: ${ + apRunning ? "running" : "stopped" + }`, +) await checkAndManageAP() // Check periodically diff --git a/src/services/server/components/IndexPage.tsx b/src/services/server/components/IndexPage.tsx index e258767..3a5cd66 100644 --- a/src/services/server/components/IndexPage.tsx +++ b/src/services/server/components/IndexPage.tsx @@ -16,11 +16,6 @@ export const IndexPage = () => ( - ); diff --git a/src/services/server/server.tsx b/src/services/server/server.tsx index d949c94..0fad238 100644 --- a/src/services/server/server.tsx +++ b/src/services/server/server.tsx @@ -1,5 +1,3 @@ -#!/usr/bin/env bun - import { Hono } from "hono" import { join } from "node:path" import { $ } from "bun" @@ -36,30 +34,24 @@ app.get("/api/networks", async (c) => { } }) -// API endpoint to get logs (for auto-refresh) -app.get("/api/logs", async (c) => { - try { - const logs = - await $`journalctl -u phone-ap.service -u phone-web.service -n 200 --no-pager`.text() - return c.json({ logs: logs.trim() }) - } catch (error) { - return c.json({ logs: "", error: String(error) }, 500) - } -}) - // Main WiFi configuration page app.get("/", (c) => { return c.html() }) -// Service logs with auto-refresh +// Service logs app.get("/logs", async (c) => { + const service = c.req.query("service") || "phone-ap" + const validServices = ["phone-ap", "phone-web", "phone"] + + // Default to phone-ap if invalid service + const selectedService = validServices.includes(service) ? service : "phone-ap" + try { - const logs = - await $`journalctl -u phone-ap.service -u phone-web.service -n 200 --no-pager`.text() - return c.html() + const logs = await $`journalctl -u ${selectedService}.service -n 200 --no-pager --no-hostname`.text() + return c.html() } catch (error) { - throw new Error(`Failed to fetch logs: ${error}`) + return c.html() } }) @@ -93,7 +85,8 @@ app.post("/save", async (c) => { return response }) -export default { port: 80, fetch: app.fetch } +const port = process.env.PORT ? Number(process.env.PORT) : 80 +export default { port, fetch: app.fetch } -console.log("Server running on http://0.0.0.0:80") +console.log(`Server running on http://0.0.0.0:${port}`) console.log("Access via WiFi or AP at http://phone.local") diff --git a/src/test-buzz.ts b/src/test-buzz.ts index 3dffde2..2851fa2 100755 --- a/src/test-buzz.ts +++ b/src/test-buzz.ts @@ -21,7 +21,7 @@ console.log("") // Test 2: Create player console.log("🔊 Creating default player...") try { - const player = await Buzz.defaultPlayer() + const player = await Buzz.player() console.log("✅ Player created\n") // Test 3: Play sound file @@ -42,7 +42,7 @@ try { // Test 5: Create recorder console.log("🎤 Creating default recorder...") try { - const recorder = await Buzz.defaultRecorder() + const recorder = await Buzz.recorder() console.log("✅ Recorder created\n") // Test 6: Stream recording with RMS diff --git a/src/test-operator.ts b/src/test-operator.ts index dc04b9b..5a56b8a 100755 --- a/src/test-operator.ts +++ b/src/test-operator.ts @@ -7,8 +7,8 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => { console.log("📞 Phone System Starting\n") await Buzz.setVolume(0.4) - const recorder = await Buzz.defaultRecorder() - const player = await Buzz.defaultPlayer() + const recorder = await Buzz.recorder() + const player = await Buzz.player() const agent = new Agent({ agentId, -- 2.50.1 From c07cb297e32eb0aec722df023c23f561426a0008 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 21 Nov 2025 10:59:40 -0800 Subject: [PATCH 3/6] wip --- baresip/accounts | 2 +- baresip/config | 8 ++++++-- scripts/bootstrap-services.ts | 24 +++++++++++++++++------- scripts/bootstrap.ts | 3 ++- scripts/deploy.ts | 3 ++- scripts/setup-services.ts | 3 ++- src/phone.ts | 2 +- src/utils/waiting-sounds.ts | 30 +++++++++++++++++------------- 8 files changed, 48 insertions(+), 27 deletions(-) diff --git a/baresip/accounts b/baresip/accounts index 39cce39..d581311 100644 --- a/baresip/accounts +++ b/baresip/accounts @@ -1 +1 @@ -;auth_pass=zgm-kwx2bug5hwf3YGF;unregister_on_exit=yes;regint=300 \ No newline at end of file +;auth_pass=zgm-kwx2bug5hwf3YGF;unregister_on_exit=yes \ No newline at end of file diff --git a/baresip/config b/baresip/config index 26b8161..8cafe39 100644 --- a/baresip/config +++ b/baresip/config @@ -51,13 +51,17 @@ module stun.so module turn.so module ice.so +# STUN Server +stun_host stun.l.google.com +stun_port 19302 + module httpd.so #------------------------------------------------------------------------------ # Temporary Modules (loaded then unloaded) -module_tmp uuid.so -module_tmp account.so +module uuid.so +module account.so #------------------------------------------------------------------------------ diff --git a/scripts/bootstrap-services.ts b/scripts/bootstrap-services.ts index 82b6c2b..1a26163 100644 --- a/scripts/bootstrap-services.ts +++ b/scripts/bootstrap-services.ts @@ -8,6 +8,13 @@ const PHONE_SERVICE_FILE = "/etc/systemd/system/phone.service" export const setupServices = async (installDir: string) => { console.log("\nInstalling systemd services...") + // Detect user from environment or use default + // SUDO_USER is set when running with sudo, which is what we want + const serviceUser = process.env.SERVICE_USER || process.env.SUDO_USER || process.env.USER || "corey" + const userUid = await $`id -u ${serviceUser}`.text().then((s) => s.trim()) + + console.log(`Setting up services for user: ${serviceUser} (UID: ${userUid})`) + // Find where bun is installed const bunPath = await $`which bun` .quiet() @@ -61,7 +68,7 @@ WantedBy=multi-user.target writeFileSync(WEB_SERVICE_FILE, webServiceContent, "utf8") console.log("✓ Created phone-web.service") - // Create phone service + // Create phone service (system service with environment variables for audio access) const phoneServiceContent = `[Unit] Description=Phone Application After=network.target sound.target @@ -69,7 +76,10 @@ Requires=sound.target [Service] Type=simple -User=corey +User=${serviceUser} +Group=audio +Environment=XDG_RUNTIME_DIR=/run/user/${userUid} +Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${userUid}/bus ExecStart=${bunPath} ${installDir}/src/main.ts WorkingDirectory=${installDir} EnvironmentFile=${installDir}/.env @@ -90,9 +100,9 @@ WantedBy=multi-user.target await $`systemctl enable phone.service` console.log("✓ Services enabled") - console.log("\nStarting the services...") - await $`systemctl start phone-ap.service` - await $`systemctl start phone-web.service` - await $`systemctl start phone.service` - console.log("✓ Services started") + console.log("\nRestarting the services...") + await $`systemctl restart phone-ap.service` + await $`systemctl restart phone-web.service` + await $`systemctl restart phone.service` + console.log("✓ Services restarted") } diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts index d78bdbf..63e3180 100755 --- a/scripts/bootstrap.ts +++ b/scripts/bootstrap.ts @@ -14,7 +14,8 @@ if (process.getuid && process.getuid() !== 0) { } // Get install directory from argument or use default -const INSTALL_DIR = process.argv[2] || "/home/corey/phone" +const defaultUser = process.env.USER || "corey" +const INSTALL_DIR = process.argv[2] || `/home/${defaultUser}/phone` console.log(`Install directory: ${INSTALL_DIR}`) diff --git a/scripts/deploy.ts b/scripts/deploy.ts index e4b6314..ac7a03d 100755 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -2,8 +2,9 @@ import { $ } from "bun" +const defaultUser = process.env.USER ?? "corey" const PI_HOST = process.env.PI_HOST ?? "phone.local" -const PI_DIR = process.env.PI_DIR ?? "/home/corey/phone" +const PI_DIR = process.env.PI_DIR ?? `/home/${defaultUser}/phone` // Parse command line arguments const shouldBootstrap = process.argv.includes("--bootstrap") diff --git a/scripts/setup-services.ts b/scripts/setup-services.ts index f3f397a..11033a4 100644 --- a/scripts/setup-services.ts +++ b/scripts/setup-services.ts @@ -3,7 +3,8 @@ import { setupServices } from "./bootstrap-services" // Get install directory from argument or use default -const INSTALL_DIR = process.argv[2] || "/home/corey/phone" +const defaultUser = process.env.USER || "corey" +const INSTALL_DIR = process.argv[2] || `/home/${defaultUser}/phone` console.log(`Setting up services for: ${INSTALL_DIR}`) diff --git a/src/phone.ts b/src/phone.ts index ab191ab..a1f5ab3 100644 --- a/src/phone.ts +++ b/src/phone.ts @@ -46,7 +46,7 @@ export const runPhone = async (agentId: string, agentKey: string) => { using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 }) using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 }) - await Buzz.setVolume(0.4) + await Buzz.setVolume(0.2) log(`📞 Phone is ${hook.value ? "off hook" : "on hook"}`) playStartRing(ringer) diff --git a/src/utils/waiting-sounds.ts b/src/utils/waiting-sounds.ts index 2d66f93..4a316ec 100644 --- a/src/utils/waiting-sounds.ts +++ b/src/utils/waiting-sounds.ts @@ -1,6 +1,7 @@ import Buzz from "../buzz/index.ts" import { join } from "path" import { random } from "./index.ts" +import { log } from "console" export class WaitingSounds { typingPlayback?: Buzz.Playback @@ -38,39 +39,42 @@ export class WaitingSounds { const playedSounds = new Set() let dir: SoundDir | undefined return new Promise(async (resolve) => { + // Don't start playing speaking sounds until the operator stream has been silent for a bit while (operatorStream.bufferEmptyFor < 1500) { await Bun.sleep(100) } do { const lastSoundDir = dir - const value = Math.random() * 100 if (lastSoundDir === "body-noises") { dir = "apology" - } else if (value > 99 && !lastSoundDir) { - dir = "body-noises" - } else if (value > 75 && !lastSoundDir) { - dir = "stalling" } else { - dir = undefined - await Bun.sleep(1000) + // sleep for 4-6 seconds + await Bun.sleep(4000 + Math.random() * 2000) + const value = Math.random() * 100 + + if (value > 95) { + dir = "body-noises" + } else { + dir = "stalling" + } } - if (dir) { - const speakingSound = getSound(dir, Array.from(playedSounds)) - this.speakingPlayback = await this.player.play(speakingSound) - playedSounds.add(speakingSound) - await this.speakingPlayback.finished() - } + const speakingSound = getSound(dir, Array.from(playedSounds)) + this.speakingPlayback = await this.player.play(speakingSound) + playedSounds.add(speakingSound) + await this.speakingPlayback.finished() } while (this.typingPlayback) resolve() }) } async stop() { + log(`🛑 Stopping waiting sounds. Has typingPlayback: ${!!this.typingPlayback}`) if (!this.typingPlayback) return await Promise.all([this.typingPlayback.stop(), this.speakingPlayback?.finished()]) + log("🛑 Waiting sounds stopped") this.typingPlayback = undefined } } -- 2.50.1 From 61eb2bc895016cc8d0affbf7235aa334f08f7623 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Sun, 23 Nov 2025 15:40:22 -0800 Subject: [PATCH 4/6] wip --- baresip-test-config.tar.gz | Bin 0 -> 1890 bytes baresip/accounts | 2 +- baresip/old-accounts | 1 + baresip/old-config | 24 ++++++++++++++ src/utils/waiting-sounds.ts | 7 ++-- test-baresip-calls.sh | 64 ++++++++++++++++++++++++++++++++++++ 6 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 baresip-test-config.tar.gz create mode 100644 baresip/old-accounts create mode 100644 baresip/old-config create mode 100755 test-baresip-calls.sh diff --git a/baresip-test-config.tar.gz b/baresip-test-config.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..ce97ecabafce77f08878bc4e38e7950e81c56e83 GIT binary patch literal 1890 zcmV-o2c7sIiwFR`IU{KR1MOVhZrjKeb_*1R*z5MTgmkyCs3AEdC2N&hal45Z@pilR z1__FS7?DGXG09=cp=BlL1N5Tzy)F7UeTKeE&yf1DWXDnfQR&9>*)qvFXMWC*XXZOc zgf*9V9xD=_AJqngkd2Tkj*e}WM`YmYSe=`yZs|J0*fDfnMY?Gse59iKhO|PFbCSVO zJeJYfBYL0~1dqroQ`F`0r-EPn=9fo5KRUV~p8Cg2^^H1*C_8Gv$AHgY;Ulj9y<3yR ztE(U8r=rF`;q$8vm7lH3f9(sXnZ1sQSZ1}&0Fgj9i_dk^Tc;fzV^wgM#F&*fRZFViVPd&}Cx(4c5mSQ>T ztMli>AI@H$zy5=EOE}N8hrD$#{L^qa!zc)S^p0Il72H!Vq0+0jk5#(k;>zcqh9`^q zcmM7FuRCaS|3|hh_y0rS>pVyv^{+?3?` z;QcsmUr%q1Q8BhA)4+WD;+uX($6>+;HtHvYr(rT-5gp8E-Y*g%$~nzkmbmmb>rN%E9se88i8 zTQmT5Aq*I>f{)c6>9*X2JV!FL*q8 zlV6)J&!fxfi&rLp&O6<~ooK4=@JVw$mIUE=Zw)?xPs;yWBK|iVQ^x;?f#m;B8Y})^ z-#V~6|6lk&#x|Dx{}9;V|2D$9W;hOpd(gv@|9|nQfA`<&|E8{6oBO{d`Tt?iQd;VW zWE5DqT0ILzMtH~)rKKpX_Fe%Es%J3GO3I?hjVYfnztLz=aVcb%=ie(Z4mT2fHvtLK z%AXSpo}W_UksypHNIH~zoji{^9?iIx(zp}@*AIi4Te1Fz;K`ztvm9zPbZ7%Q`Hu1_ z%dQpR`R+BPDT%@xXwfW%X9Py#!5=qbaB#)S2(b%PHc(X_lUrAuh$eAWG+~kNGCl!{ zUP(m9r38X&nsU*3!bJ1K!Vj53#9ud!$P5PAXq1;l67pl82Be7iTp?#g=FvwKNCdVw z9#M$s4UOi#lOoSNH(*&zM4eo>p-7Bjvy3M9)O>os2$~-iK~P#?pf*fyqtR01d`;%Y z%m)jC@Rs@utKj}6rCDv$(#lFj zTIvP!iwGVbB`!}OzUC9rTZg5*Ghs0uWpvt!!ck}LvYmKQqow|MzN}xBmXnfmut>{S zj~{qB+|P$p$BwSAX@al8JUpaI3Ro*{gz{P;jEThQS{r@_?)8y2=1=Wh|BS_8Z#aU$ zoX^zweubh4{kiUi$O@?64X+;3E3*OXLn~wc^`g+DRbzX$V!wLIBgsiT8lOr=w!*da-|B6w{=r$&_mNYDzK!%Qelw5Ex;;TA^1q{@eZE#+&|cB1h(b4uV#dcTpG5fg%ciALb%8kO1jd%P)wl5G4+GR*kX{&b%PQxkThd@_g`th_9TFM-lU` zmm(1n4nBkl3I|DI_&mLd877mxaP|Ly*djC=b(K<@vCzyl2Ua}P=4 zfY4m=wOYM>?9Bg7TgMI-?|)!Rm;QecNdEt%vEu*rtpi*AfARkxmi~_oWJ&&i2(17A z2LmDiq*(_1IJQjxKLOOg`|rU2LAbI18>T7u|3kpT)ap7|Z(DxPYj<_zx4WKawN2Y5 cc0g>OI`-!tqlAQngv9>wKPfmZUjR}70Q;TeLI3~& literal 0 HcmV?d00001 diff --git a/baresip/accounts b/baresip/accounts index d581311..8493601 100644 --- a/baresip/accounts +++ b/baresip/accounts @@ -1 +1 @@ -;auth_pass=zgm-kwx2bug5hwf3YGF;unregister_on_exit=yes \ No newline at end of file +;auth_pass=zgm-kwx2bug5hwf3YGF;unregister_on_exit=yes \ No newline at end of file diff --git a/baresip/old-accounts b/baresip/old-accounts new file mode 100644 index 0000000..8493601 --- /dev/null +++ b/baresip/old-accounts @@ -0,0 +1 @@ +;auth_pass=zgm-kwx2bug5hwf3YGF;unregister_on_exit=yes \ No newline at end of file diff --git a/baresip/old-config b/baresip/old-config new file mode 100644 index 0000000..b5d910e --- /dev/null +++ b/baresip/old-config @@ -0,0 +1,24 @@ +call_max_calls 4 +call_local_timeout 120 + +# Audio +audio_player alsa,default +audio_source alsa,default +audio_alert alsa,default + +# Modules +#------------------------------------------------------------------------------ + +module_path /usr/lib/baresip/modules + +# Audio codec Modules +module g711.so + +# Audio driver Modules +module alsa.so + +# Application Modules +module_app account.so +module_app menu.so + +module httpd.so \ No newline at end of file diff --git a/src/utils/waiting-sounds.ts b/src/utils/waiting-sounds.ts index 4a316ec..3b0c49a 100644 --- a/src/utils/waiting-sounds.ts +++ b/src/utils/waiting-sounds.ts @@ -73,9 +73,12 @@ export class WaitingSounds { log(`🛑 Stopping waiting sounds. Has typingPlayback: ${!!this.typingPlayback}`) if (!this.typingPlayback) return - await Promise.all([this.typingPlayback.stop(), this.speakingPlayback?.finished()]) - log("🛑 Waiting sounds stopped") + // Quicky undefine this to stop the loops + const typingPlayback = this.typingPlayback this.typingPlayback = undefined + + await Promise.all([typingPlayback.stop(), this.speakingPlayback?.finished()]) + log("🛑 Waiting sounds stopped") } } diff --git a/test-baresip-calls.sh b/test-baresip-calls.sh new file mode 100755 index 0000000..adcad3e --- /dev/null +++ b/test-baresip-calls.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Test script to call the Pi's baresip every 5 minutes +# This will help verify the NAT connection stays alive + +ACCOUNT_SID="AC1290bf86af061f24406c6762f48fa24e" +AUTH_TOKEN="6bfac57e5e26ea52c841594ef8d99ed1" +FROM_NUMBER="+13476229543" +TO_SIP="sip:yellow@probablycorey.sip.us1.twilio.com" +LOG_FILE="baresip-test-results.log" + +echo "=== Baresip Call Test Started at $(date) ===" | tee -a $LOG_FILE +echo "Will call every 5 minutes. Press Ctrl+C to stop." | tee -a $LOG_FILE +echo "" | tee -a $LOG_FILE + +call_count=0 + +while true; do + call_count=$((call_count + 1)) + echo "[$(date)] Test call #$call_count" | tee -a $LOG_FILE + + # Make the call + CALL_SID=$(curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls.json" \ + -u "$ACCOUNT_SID:$AUTH_TOKEN" \ + --data-urlencode "Url=http://demo.twilio.com/docs/voice.xml" \ + --data-urlencode "To=$TO_SIP" \ + --data-urlencode "From=$FROM_NUMBER" \ + | jq -r '.sid') + + echo " Call SID: $CALL_SID" | tee -a $LOG_FILE + + # Wait for call to complete + sleep 10 + + # Check status + STATUS=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls/$CALL_SID.json" \ + -u "$ACCOUNT_SID:$AUTH_TOKEN" \ + | jq -r '.status') + + echo " Status: $STATUS" | tee -a $LOG_FILE + + # Check for errors + ERROR=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls/$CALL_SID/Notifications.json" \ + -u "$ACCOUNT_SID:$AUTH_TOKEN" \ + | jq -r '.notifications[0].error_code // "none"') + + if [ "$ERROR" != "none" ]; then + echo " ❌ ERROR: $ERROR" | tee -a $LOG_FILE + ERROR_MSG=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls/$CALL_SID/Notifications.json" \ + -u "$ACCOUNT_SID:$AUTH_TOKEN" \ + | jq -r '.notifications[0].message_text') + echo " Message: $ERROR_MSG" | tee -a $LOG_FILE + elif [ "$STATUS" = "no-answer" ] || [ "$STATUS" = "completed" ]; then + echo " ✓ Success" | tee -a $LOG_FILE + else + echo " ⚠ Unexpected status: $STATUS" | tee -a $LOG_FILE + fi + + echo "" | tee -a $LOG_FILE + + # Wait 5 minutes (minus the 10 seconds we already waited) + echo " Waiting 5 minutes until next call..." | tee -a $LOG_FILE + sleep 290 +done -- 2.50.1 From 9ddb54d319151fd43a9836fe0c030645f487d381 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Sun, 23 Nov 2025 16:03:40 -0800 Subject: [PATCH 5/6] YES --- baresip/config | 75 ++++++++------------------------------------ baresip/old-accounts | 1 - baresip/old-config | 24 -------------- 3 files changed, 13 insertions(+), 87 deletions(-) delete mode 100644 baresip/old-accounts delete mode 100644 baresip/old-config diff --git a/baresip/config b/baresip/config index 8cafe39..28e956c 100644 --- a/baresip/config +++ b/baresip/config @@ -1,75 +1,26 @@ -# -# baresip configuration -# - -#------------------------------------------------------------------------------ - -# Core -poll_method epoll # poll, select, epoll .. - -# Call +call_max_calls 4 call_local_timeout 120 -call_max_calls 4 # Audio -audio_player alsa,default -audio_source alsa,default -audio_alert none -audio_alert_enable no -audio_level no +audio_player alsa,default +audio_source alsa,default +audio_alert alsa,default + ring_aufile /dev/null -ausrc_format s16 # s16, float, .. -auplay_format s16 # s16, float, .. -auenc_format s16 # s16, float, .. -audec_format s16 # s16, float, .. -audio_buffer 20-160 # ms -# AVT - Audio/Video Transport -rtp_tos 184 -rtcp_mux no -jitter_buffer_delay 5-10 # frames -rtp_stats no - - -#------------------------------------------------------------------------------ # Modules +#------------------------------------------------------------------------------ -module_path /usr/lib/baresip/modules - -# UI Modules -#module stdio.so - -# Audio codec Modules (in order) -module g711.so +module_path /usr/lib/baresip/modules +# Audio codec Modules +module g711.so # Audio driver Modules -module alsa.so +module alsa.so -# Media NAT modules -module stun.so -module turn.so -module ice.so - -# STUN Server -stun_host stun.l.google.com -stun_port 19302 - -module httpd.so - -#------------------------------------------------------------------------------ -# Temporary Modules (loaded then unloaded) - -module uuid.so -module account.so - - -#------------------------------------------------------------------------------ # Application Modules +module_app account.so +module_app menu.so -module_app contact.so -module_app debug_cmd.so -module_app menu.so - - -http_listen 0.0.0.0:8000 # httpd - HTTP Serve \ No newline at end of file +module httpd.so \ No newline at end of file diff --git a/baresip/old-accounts b/baresip/old-accounts deleted file mode 100644 index 8493601..0000000 --- a/baresip/old-accounts +++ /dev/null @@ -1 +0,0 @@ -;auth_pass=zgm-kwx2bug5hwf3YGF;unregister_on_exit=yes \ No newline at end of file diff --git a/baresip/old-config b/baresip/old-config deleted file mode 100644 index b5d910e..0000000 --- a/baresip/old-config +++ /dev/null @@ -1,24 +0,0 @@ -call_max_calls 4 -call_local_timeout 120 - -# Audio -audio_player alsa,default -audio_source alsa,default -audio_alert alsa,default - -# Modules -#------------------------------------------------------------------------------ - -module_path /usr/lib/baresip/modules - -# Audio codec Modules -module g711.so - -# Audio driver Modules -module alsa.so - -# Application Modules -module_app account.so -module_app menu.so - -module httpd.so \ No newline at end of file -- 2.50.1 From 8e8c884586d9f36db415c8a27b8b39825db2c204 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Sun, 23 Nov 2025 16:05:02 -0800 Subject: [PATCH 6/6] clean up --- baresip-test-config.tar.gz | Bin 1890 -> 0 bytes test-baresip-calls.sh | 64 ------------------------------------- 2 files changed, 64 deletions(-) delete mode 100644 baresip-test-config.tar.gz delete mode 100755 test-baresip-calls.sh diff --git a/baresip-test-config.tar.gz b/baresip-test-config.tar.gz deleted file mode 100644 index ce97ecabafce77f08878bc4e38e7950e81c56e83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1890 zcmV-o2c7sIiwFR`IU{KR1MOVhZrjKeb_*1R*z5MTgmkyCs3AEdC2N&hal45Z@pilR z1__FS7?DGXG09=cp=BlL1N5Tzy)F7UeTKeE&yf1DWXDnfQR&9>*)qvFXMWC*XXZOc zgf*9V9xD=_AJqngkd2Tkj*e}WM`YmYSe=`yZs|J0*fDfnMY?Gse59iKhO|PFbCSVO zJeJYfBYL0~1dqroQ`F`0r-EPn=9fo5KRUV~p8Cg2^^H1*C_8Gv$AHgY;Ulj9y<3yR ztE(U8r=rF`;q$8vm7lH3f9(sXnZ1sQSZ1}&0Fgj9i_dk^Tc;fzV^wgM#F&*fRZFViVPd&}Cx(4c5mSQ>T ztMli>AI@H$zy5=EOE}N8hrD$#{L^qa!zc)S^p0Il72H!Vq0+0jk5#(k;>zcqh9`^q zcmM7FuRCaS|3|hh_y0rS>pVyv^{+?3?` z;QcsmUr%q1Q8BhA)4+WD;+uX($6>+;HtHvYr(rT-5gp8E-Y*g%$~nzkmbmmb>rN%E9se88i8 zTQmT5Aq*I>f{)c6>9*X2JV!FL*q8 zlV6)J&!fxfi&rLp&O6<~ooK4=@JVw$mIUE=Zw)?xPs;yWBK|iVQ^x;?f#m;B8Y})^ z-#V~6|6lk&#x|Dx{}9;V|2D$9W;hOpd(gv@|9|nQfA`<&|E8{6oBO{d`Tt?iQd;VW zWE5DqT0ILzMtH~)rKKpX_Fe%Es%J3GO3I?hjVYfnztLz=aVcb%=ie(Z4mT2fHvtLK z%AXSpo}W_UksypHNIH~zoji{^9?iIx(zp}@*AIi4Te1Fz;K`ztvm9zPbZ7%Q`Hu1_ z%dQpR`R+BPDT%@xXwfW%X9Py#!5=qbaB#)S2(b%PHc(X_lUrAuh$eAWG+~kNGCl!{ zUP(m9r38X&nsU*3!bJ1K!Vj53#9ud!$P5PAXq1;l67pl82Be7iTp?#g=FvwKNCdVw z9#M$s4UOi#lOoSNH(*&zM4eo>p-7Bjvy3M9)O>os2$~-iK~P#?pf*fyqtR01d`;%Y z%m)jC@Rs@utKj}6rCDv$(#lFj zTIvP!iwGVbB`!}OzUC9rTZg5*Ghs0uWpvt!!ck}LvYmKQqow|MzN}xBmXnfmut>{S zj~{qB+|P$p$BwSAX@al8JUpaI3Ro*{gz{P;jEThQS{r@_?)8y2=1=Wh|BS_8Z#aU$ zoX^zweubh4{kiUi$O@?64X+;3E3*OXLn~wc^`g+DRbzX$V!wLIBgsiT8lOr=w!*da-|B6w{=r$&_mNYDzK!%Qelw5Ex;;TA^1q{@eZE#+&|cB1h(b4uV#dcTpG5fg%ciALb%8kO1jd%P)wl5G4+GR*kX{&b%PQxkThd@_g`th_9TFM-lU` zmm(1n4nBkl3I|DI_&mLd877mxaP|Ly*djC=b(K<@vCzyl2Ua}P=4 zfY4m=wOYM>?9Bg7TgMI-?|)!Rm;QecNdEt%vEu*rtpi*AfARkxmi~_oWJ&&i2(17A z2LmDiq*(_1IJQjxKLOOg`|rU2LAbI18>T7u|3kpT)ap7|Z(DxPYj<_zx4WKawN2Y5 cc0g>OI`-!tqlAQngv9>wKPfmZUjR}70Q;TeLI3~& diff --git a/test-baresip-calls.sh b/test-baresip-calls.sh deleted file mode 100755 index adcad3e..0000000 --- a/test-baresip-calls.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash - -# Test script to call the Pi's baresip every 5 minutes -# This will help verify the NAT connection stays alive - -ACCOUNT_SID="AC1290bf86af061f24406c6762f48fa24e" -AUTH_TOKEN="6bfac57e5e26ea52c841594ef8d99ed1" -FROM_NUMBER="+13476229543" -TO_SIP="sip:yellow@probablycorey.sip.us1.twilio.com" -LOG_FILE="baresip-test-results.log" - -echo "=== Baresip Call Test Started at $(date) ===" | tee -a $LOG_FILE -echo "Will call every 5 minutes. Press Ctrl+C to stop." | tee -a $LOG_FILE -echo "" | tee -a $LOG_FILE - -call_count=0 - -while true; do - call_count=$((call_count + 1)) - echo "[$(date)] Test call #$call_count" | tee -a $LOG_FILE - - # Make the call - CALL_SID=$(curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls.json" \ - -u "$ACCOUNT_SID:$AUTH_TOKEN" \ - --data-urlencode "Url=http://demo.twilio.com/docs/voice.xml" \ - --data-urlencode "To=$TO_SIP" \ - --data-urlencode "From=$FROM_NUMBER" \ - | jq -r '.sid') - - echo " Call SID: $CALL_SID" | tee -a $LOG_FILE - - # Wait for call to complete - sleep 10 - - # Check status - STATUS=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls/$CALL_SID.json" \ - -u "$ACCOUNT_SID:$AUTH_TOKEN" \ - | jq -r '.status') - - echo " Status: $STATUS" | tee -a $LOG_FILE - - # Check for errors - ERROR=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls/$CALL_SID/Notifications.json" \ - -u "$ACCOUNT_SID:$AUTH_TOKEN" \ - | jq -r '.notifications[0].error_code // "none"') - - if [ "$ERROR" != "none" ]; then - echo " ❌ ERROR: $ERROR" | tee -a $LOG_FILE - ERROR_MSG=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Calls/$CALL_SID/Notifications.json" \ - -u "$ACCOUNT_SID:$AUTH_TOKEN" \ - | jq -r '.notifications[0].message_text') - echo " Message: $ERROR_MSG" | tee -a $LOG_FILE - elif [ "$STATUS" = "no-answer" ] || [ "$STATUS" = "completed" ]; then - echo " ✓ Success" | tee -a $LOG_FILE - else - echo " ⚠ Unexpected status: $STATUS" | tee -a $LOG_FILE - fi - - echo "" | tee -a $LOG_FILE - - # Wait 5 minutes (minus the 10 seconds we already waited) - echo " Waiting 5 minutes until next call..." | tee -a $LOG_FILE - sleep 290 -done -- 2.50.1