godcrm/god_frame/assets/lua_scripts/app.lua
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

215 lines
6.3 KiB
Lua

-- GOD Frame Application Loop
-- Handles: tap events, audio capture, photo capture, text display
--
-- Message flags (must match Dart FrameFlags):
-- 0x10 = tapFlag (Frame -> Phone)
-- 0x11 = startListeningFlag (Phone -> Frame)
-- 0x12 = stopListeningFlag (Phone -> Frame)
-- 0x20 = messageResponseFlag (Phone -> Frame) - display text
-- 0x22 = singleDataFlag (Phone -> Frame) - command dispatch
-- 0x23 = holdResponseFlag (Phone -> Frame) - keep awake
-- 0x05/0x06 = audio non-final/final (Frame -> Phone)
-- 0x07/0x08 = photo non-final/final (Frame -> Phone)
local TAP_FLAG = 0x10
local START_LISTENING = 0x11
local STOP_LISTENING = 0x12
local MSG_RESPONSE = 0x20
local SINGLE_DATA = 0x22
local HOLD_RESPONSE = 0x23
local state = "idle" -- idle, listening, wait_response, display
local last_msg_time = frame.time.utc()
local SLEEP_TIMEOUT = 120 -- seconds before auto-sleep (longer to prevent disconnect during recording)
local display_text = ""
local display_offset = 0
local events_text = "" -- upcoming calendar events (up to 3 lines)
-- Enable tap detection
local function enable_taps()
frame.imu.tap_callback(function()
-- Send tap event to phone (both raw flag and framed for compatibility)
-- Raw single byte — handled by Dart _handleRxData first-byte check
frame.bluetooth.send(string.char(TAP_FLAG))
last_msg_time = frame.time.utc()
-- If we're in listening state, show "stopping..." feedback
if state == "listening" then
frame.display.clear()
frame.display.text("stopping...", 170, 180)
frame.display.show()
end
end)
end
-- Display text on Frame screen with wrapping
local function show_text(text)
frame.display.clear()
if text == nil or text == "" then
frame.display.show()
return
end
local max_chars_per_line = 32 -- approximate for 640px width
local max_lines = 5 -- approximate for 400px height
local y = 10
local line_height = 75
local line = 0
local i = 1
while i <= #text and line < max_lines do
local end_pos = math.min(i + max_chars_per_line - 1, #text)
-- Find word boundary for wrapping
if end_pos < #text then
local space = text:sub(1, end_pos):match(".*()%s")
if space and space > i then
end_pos = space
end
end
local segment = text:sub(i, end_pos):gsub("^%s+", "")
frame.display.text(segment, 10, y)
y = y + line_height
i = end_pos + 1
line = line + 1
end
frame.display.show()
end
-- Show idle screen with optional calendar events
local function show_idle()
frame.display.clear()
if events_text ~= nil and events_text ~= "" then
-- Show upcoming events (3 lines max)
local y = 30
local line_height = 75
local line_count = 0
for line in events_text:gmatch("[^\n]+") do
if line_count >= 3 then break end
-- First event: brighter/larger, rest: dimmer
if line_count == 0 then
frame.display.text("> " .. line, 10, y)
else
frame.display.text(" " .. line, 10, y)
end
y = y + line_height
line_count = line_count + 1
end
-- Small hint at bottom
frame.display.text("tap to speak", 180, 340)
else
frame.display.text("tap me in", 180, 160)
end
frame.display.show()
end
-- Show listening indicator
local function show_listening()
frame.display.clear()
frame.display.text("listening...", 160, 160)
frame.display.text("tap to finish", 150, 240)
frame.display.show()
end
-- Show processing indicator
local function show_processing()
frame.display.clear()
frame.display.text("thinking...", 170, 180)
frame.display.show()
end
-- Handle incoming messages from phone
local function handle_messages(msg_code, data)
last_msg_time = frame.time.utc()
if msg_code == START_LISTENING then
state = "listening"
show_listening()
-- Start microphone recording
frame.microphone.start({
sample_rate = 8000,
bit_depth = 8
})
-- Auto-expose camera for photo capture
frame.camera.auto({})
elseif msg_code == STOP_LISTENING then
state = "wait_response"
show_processing()
-- Stop microphone - audio data will be sent automatically
frame.microphone.stop()
-- Capture photo
frame.camera.capture({})
elseif msg_code == MSG_RESPONSE then
state = "display"
display_text = data or ""
display_offset = 0
show_text(display_text)
elseif msg_code == HOLD_RESPONSE then
-- Just keep awake, don't change state
last_msg_time = frame.time.utc()
elseif msg_code == SINGLE_DATA then
-- Command dispatch based on first byte of data
if data and #data > 0 then
local cmd = string.byte(data, 1)
if cmd == 0x01 then
enable_taps()
elseif cmd == 0x02 then
-- Disable taps
frame.imu.tap_callback(nil)
elseif cmd == 0x03 then
-- Update calendar events display
events_text = #data > 1 and data:sub(2) or ""
-- Refresh idle screen if currently idle
if state == "idle" then
show_idle()
end
end
end
end
end
-- Register BLE message callback
frame.bluetooth.receive_callback(function(data)
if data and #data >= 2 then
local msg_code = string.byte(data, 1)
local payload = #data > 1 and data:sub(2) or ""
handle_messages(msg_code, payload)
end
end)
-- Main loop
enable_taps()
show_idle()
state = "idle"
while true do
-- Check for auto-sleep
local now = frame.time.utc()
if now - last_msg_time > SLEEP_TIMEOUT then
if state == "idle" or state == "display" then
frame.display.clear()
frame.display.show()
frame.sleep()
-- Wake up resets
last_msg_time = frame.time.utc()
state = "idle"
show_idle()
enable_taps()
end
end
-- Small delay to prevent busy loop
frame.sleep(0.05)
end