-- =============================================================================================== -- UPDATED VERSION - FULLY COMPATIBLE WITH FLEXTECH AKT APRIL 2025 API v1.0 -- -- Author: Sebastian Langer -- Company: SLSS Sebastian Langer Software Solutions UG (haftungsbeschraenkt) -- In cooperation with: SeRoSys-Technology LLC -- -- Copyright (c) 2026 Sebastian Langer Software Solutions UG (haftungsbeschraenkt) -- All rights reserved. -- -- E-Mail: SLSS_Official@langer-sebastian.de -- Web: https://www.langer-sebastian.de -- -- Usage rights: -- SeRoSys-Technology LLC is granted a non-exclusive, perpetual right to use, -- modify, reproduce and distribute this software as part of its own products, -- systems and customer solutions. -- Copyright and ownership of the original software remain with -- Sebastian Langer Software Solutions UG (haftungsbeschraenkt). -- -- Version: see in-code version number below -- -- Features and changes: -- + Periph I2C transactions truncated to fit SeRoSys 70-byte I2C buffer -- + Original I2C length preserved (lenByte, capped to 255) -- + Triggered A2B / CAN logging -- + Circular A2B audio pre-trigger buffer -- + Read current electrical states of both BusMonitor GPIO inputs -- + Send an A2B trigger message to the SeRoSys CAN dongle -- + I2C connection check on script start -- =============================================================================================== -- USER CONFIGURATION -- Only modify the settings in this section -- =============================================================================================== local used_trigger_gpio = 0 -- the Bus Monitor GPIO pin for the standalone trigger activation local debug = false -- enables [true] or disables [false] serial debug output for troubleshooting -- =============================================================================================== -- END OF USER CONFIGURATION -- DO NOT MODIFY ANYTHING BELOW THIS LINE -- =============================================================================================== local version = "1.0.1.4" local bm = require('bm') local qwiic = require('qwiic') local term = require('term') local drive = require('drive') local pio = require('pio') local cbuf = require('cbuf') local route = require('route') local led = require('led') local system = require('system') -- Limits for SeRoSys hardware side (65 bytes total buffer per transaction) -- Packet format: [evt_type] + payload -- For evt 0x06 payload: node, addr, rw, lenByte, data... -- => 1 (id) + 4 (header) + data <= 69 => data <= 64 local MAX_PERIPH_DATA = 64 local SLAVE_ADDR = 0x08 -- I2C slave address -- Constants and fallback to numeric values local I2C_REG = (bm.i2c.I2C_REG) or 0 local I2C_PER = (bm.i2c.I2C_PERIPHERAL) or 1 local I2C_COND = (bm.i2c.I2C_PERIPHERAL_CONDITION) or 2 local C_START = (bm.i2c.I2C_START) or 0 local C_RPT = (bm.i2c.I2C_RPTSTART) or 1 local C_ACK = (bm.i2c.I2C_ACK) or 2 local C_NACK = (bm.i2c.I2C_NACK) or 3 local C_STOP = (bm.i2c.I2C_STOP) or 5 local C_ERROR = (bm.i2c.I2C_ERROR) or 6 -- Variables for Wav-file log counter similar to the CAN log method local TRIGGER_FILE_PATH = "sd:SLSS_wav_files/" local TRIGGER_FILE_PREFIX = "SLSS_Triggered_Log_" local TRIGGER_FILE_MAX_NUMBER = 65535 local A2B_TRIGGER_ID = 32 local TRIGGERLOGNUMBER = 0 local ACTIVETRIGGERLOGNUMBER = nil -- GPIO detection and debouncing local GPIO0 = 0 local GPIO1 = 1 local TRIGGERLASTSTATE = false local TRIGGERCANDIDATESTATE = false local TRIGGERCANDIDATETIME = 0 local TRIGGER_DEBOUNCE_TIME = 0.030 -- 30 ms -- Buffered WAV file for SD card logging local AUDIO_CHANNELS = 2 -- audio channels local AUDIO_WORD_SIZE = 2 -- 16 bit = 2 bytes local AUDIO_SAMPLE_RATE = 48000 -- sample rate local AUDIO_BUFFER_TIME = 30 -- seconds local AUDIO_BUFFER_SIZE = AUDIO_CHANNELS * AUDIO_WORD_SIZE * AUDIO_SAMPLE_RATE * AUDIO_BUFFER_TIME local AUDIOBUFFERACTIVE = false -- LED states for user feedback local LED_ERROR = false local LED_ERROR_STATE = false local LED_ERROR_TIMER = 0 local INITIALIZATION_ERROR = false local LED_TRIGGER_ACTIVE = false local LED_TRIGGER_TIMER = 0 -- Initialize led functionality local function initLEDs() led.override() -- Take control over BusMonitor leds led.set('status', 'all', 0) -- Clear status led led.set('io', 'all', 0) -- Clear I/O led end -- Set led to ready state local function setLEDReady() LED_ERROR = false LED_ERROR_STATE = false led.set('status', 'all', 0) led.set('status', 'green', 1) end -- Set led to error state local function setLEDError() LED_ERROR = true LED_ERROR_STATE = true LED_ERROR_TIMER = os.clock() led.set('status', 'all', 0) led.set('status', 'red', 1) end -- Set I/O led state when trigger is set local function setLEDTrigger() LED_TRIGGER_ACTIVE = true LED_TRIGGER_TIMER = os.clock() led.set('io', 'all', 0) led.set('io', 'red', 1) end -- Update function for led states without blocking main loop execution local function updateLEDs() local now = os.clock() -- Error led if LED_ERROR then if (now - LED_ERROR_TIMER) >= 0.5 then LED_ERROR_TIMER = now LED_ERROR_STATE = not LED_ERROR_STATE led.set('status', 'all', 0) if LED_ERROR_STATE then led.set('status', 'red', 1) end end end -- Trigger led if LED_TRIGGER_ACTIVE then if (now - LED_TRIGGER_TIMER) >= 1.0 then LED_TRIGGER_ACTIVE = false led.set('io', 'all', 0) end end end -- Safe number conversion (number/string/boolean -> number) local function N(x) local t = type(x) if t == "number" then return x end if t == "string" then return tonumber(x) or 0 end if t == "boolean" then return x and 1 or 0 end return 0 end -- Helper: normalize strings (trim + uppercase) local function normStr(x) local s = tostring(x or "") s = s:gsub("^%s+", ""):gsub("%s+$", "") return s:upper() end -- State for batching one peripheral I2C transaction local periph = { active = false, nodeAddr = 0, addr = 0, rw = 0, bytes = {}, totalLen = 0 } -- Helper: push one data byte (0..255), but store only up to MAX_PERIPH_DATA local function pushPeriphByte(b) if #periph.bytes < MAX_PERIPH_DATA then periph.bytes[#periph.bytes + 1] = N(b) & 0xFF end end -- Reset peripheral transaction state local function resetPeriph() periph.active = false periph.nodeAddr = 0 periph.addr = 0 periph.rw = 0 periph.bytes = {} periph.totalLen = 0 end -- Test qwiic (I2C) availability local function testI2CBusReadiness() -- Send reserved I2C test message (0xFF) local callOk, i2cOk = pcall(qwiic.i2c, SLAVE_ADDR, { 0xFF }, 0) -- I2C communication is working if callOk and i2cOk then if debug then print("I2C startup check successful") end return true end -- I2C communication failed if debug then print("ERROR: I2C startup check failed") print("Restarting Bus Monitor in 10 seconds...") end setLEDError() print("==========================================") print("= QWIIC STARTUP ERROR DETECTED =") print("= Please check the qwiic cabling between =") print("= the devices and try again! For further =") print("= help visit @ https://serosys-tech.com/ =") print("==========================================\n\n") local startTime = os.clock() while (os.clock() - startTime) < 5.0 do updateLEDs() end system.reset("hard") end -- Send packet to SeRoSys hardware: [evt_type] + payload bytes local function send(evt_type, payload) local pkt = { evt_type } if payload then for _, v in ipairs(payload) do pkt[#pkt + 1] = N(v) & 0xFF end end local callOk, i2cOk, ret = pcall(qwiic.i2c, SLAVE_ADDR, pkt, 0) -- Lua/API exception if not callOk then if debug then print("QWIIC API error:", tostring(i2cOk)) end setLEDError() return false end -- I2C transaction failed if not i2cOk then if debug then print("QWIIC I2C transaction failed") end setLEDError() return false end return true end -- get the GPIO states for A2B logging local function getA2BGPIOStatus() local gpio0State = pio.gpio(GPIO0) local gpio1State = pio.gpio(GPIO1) if debug then print("GPIO0 raw:", gpio0State, " | GPIO1 raw:", gpio1State) end local status = 0 if gpio0State then status = status | 0x02 end if gpio1State then status = status | 0x01 end return status end -- Build and send the A2B trigger message to local function sendA2BTrigger(status, logNumber) if logNumber == nil then if debug then print("Error in sendA2BTrigger()") end setLEDError() return false end logNumber = logNumber & 0xFFFF local highByte = (logNumber >> 8) & 0xFF local lowByte = logNumber & 0xFF send(A2B_TRIGGER_ID, {status & 0xFF, highByte, lowByte}) return true end -- Search the next usable log file number local function findNextFreeTriggerLogNumber() local usedNumbers = {} local dir = drive.dir(TRIGGER_FILE_PATH) while true do local entry = dir() if entry == nil then break end local number = string.match(entry.fname, "^SLSS_Triggered_Log_(%d+)%.wav$") if number then number = tonumber(number) if number and number >= 1 and number <= TRIGGER_FILE_MAX_NUMBER then usedNumbers[number] = true end end end -- Search first unused 16-bit number for number = 1, TRIGGER_FILE_MAX_NUMBER do if not usedNumbers[number] then return number end end -- No free number available return nil end -- Initialize the circular audio buffer for dual logging local function initAudioBuffer() cbuf.reset() local ok = cbuf.config(AUDIO_CHANNELS, AUDIO_WORD_SIZE, AUDIO_BUFFER_SIZE, AUDIO_SAMPLE_RATE) if not ok then if debug then print("Error in initAudioBuffer() on cbuf.config()") end setLEDError() return false end route.clear() local routeOk = route.set(0, 'a2b-dn', 0, 'cbuf', 0, AUDIO_CHANNELS) if not routeOk then if debug then print("Error in initAudioBuffer() on route.set()") end setLEDError() return false end local enableOk = cbuf.enable() if not enableOk then if debug then print("Error in initAudioBuffer() on cbuf.enable()") end setLEDError() return false end return true end -- Check if the sub-directory on the sd card exist local function directoryExists(directoryName) local dir = drive.dir("sd:") while true do local entry = dir() if entry == nil then break end if entry.flags == 1 and entry.fname == directoryName then return true end end return false end -- Save circular audio buffer to audio file on sd card local function saveTriggeredAudio(logNumber) if not AUDIOBUFFERACTIVE then if debug then print("Error in saveTriggeredAudio() on AUDIOBUFFERACTIVE") end setLEDError() return false end if logNumber == nil then if debug then print("Error in saveTriggeredAudio() on logNumber") end setLEDError() return false end local fileName = string.format("%s%s%d.wav", TRIGGER_FILE_PATH, TRIGGER_FILE_PREFIX, logNumber) local ok = cbuf.save(fileName) if not ok then if debug then print("Error in saveTriggeredAudio() on cbuf.save()") end setLEDError() return false end -- clear to start a fresh circular buffer and return success cbuf.clear() return true end -- Process the trigger GPIO message for the specific dongle message local function processSLSSTriggerGPIO() local gpioState = pio.gpio(used_trigger_gpio) -- Input is active LOW: -- GPIO connected to GND = trigger active local currentState = (gpioState == 0 or gpioState == false) if currentState ~= TRIGGERCANDIDATESTATE then -- GPIO state change detected TRIGGERCANDIDATESTATE = currentState TRIGGERCANDIDATETIME = os.clock() return end if currentState ~= TRIGGERLASTSTATE then -- State is unchanged -> check debounce time if (os.clock() - TRIGGERCANDIDATETIME) >= TRIGGER_DEBOUNCE_TIME then TRIGGERLASTSTATE = currentState if currentState then local currentLogNumber = TRIGGERLOGNUMBER local status = getA2BGPIOStatus() if status ~= nil and currentLogNumber ~= nil then ACTIVETRIGGERLOGNUMBER = currentLogNumber setLEDTrigger() -- Trigger I/O LED for one second sendA2BTrigger(status, ACTIVETRIGGERLOGNUMBER) -- send trigger message local saved = saveTriggeredAudio(currentLogNumber) if saved then TRIGGERLOGNUMBER = findNextFreeTriggerLogNumber() else if debug then print("Error in processSLSSTriggerGPIO() on saved") end setLEDError() end end else local status = getA2BGPIOStatus() if status ~= nil and ACTIVETRIGGERLOGNUMBER ~= nil then sendA2BTrigger(status, ACTIVETRIGGERLOGNUMBER) -- trigger released ACTIVETRIGGERLOGNUMBER = nil end end end end end -- Subscribe to all events from the specification bm.subscribe(bm.event.BIAS_OK_DETECTED) bm.subscribe(bm.event.BIAS_OK_REMOVED) bm.subscribe(bm.event.BIAS_REV_DETECTED) bm.subscribe(bm.event.BIAS_REV_REMOVED) bm.subscribe(bm.event.BUS_LOCK_LOCKED) bm.subscribe(bm.event.BUS_LOCK_UNLOCKED) bm.subscribe(bm.event.DISCOVERY_MODE) bm.subscribe(bm.event.NEWSTRCT) bm.subscribe(bm.event.I2C) bm.subscribe(bm.event.DOWNSTREAM_SCF_ERROR) bm.subscribe(bm.event.UPSTREAM_SRF_ERROR) bm.subscribe(bm.event.SLAVE_ERROR) bm.subscribe(bm.event.IRQ) bm.subscribe(bm.event.SEQUENCE_ERROR) bm.subscribe(bm.event.SPI) -- Initialize LED control initLEDs() -- check if I2C bus is ready to transfer data testI2CBusReadiness() print("==========================================") print("= SeRoSys - Flextech AKT A2B Interface =") print("= Script version: " .. version .. " =") print("= Copyright: SeRoSys-Technology (C) 2026 =") print("= visit us @ https://serosys-tech.com/ =") print("==========================================\n\n") print("Press 'q' to quit\n") -- check if sub-directory exist on sd card local directoryName = TRIGGER_FILE_PATH :gsub("^sd:", "") :gsub("/$", "") if not directoryExists(directoryName) then if debug then print("A2B directory not found - using SD root directory") end TRIGGER_FILE_PATH = "sd:" end -- get the first usable trigger number for the wav-file TRIGGERLOGNUMBER = findNextFreeTriggerLogNumber() if not TRIGGERLOGNUMBER then if debug then print("Error in init on TRIGGERLOGNUMBER") end INITIALIZATION_ERROR = true setLEDError() end -- configure GPIOs as input I/Os pio.gpio(GPIO0, "dir", "in") pio.gpio(GPIO1, "dir", "in") local gpioState = pio.gpio(used_trigger_gpio) -- get initial GPIO state -- GPIO is active LOW TRIGGERLASTSTATE = (gpioState == 0 or gpioState == false) TRIGGERCANDIDATESTATE = TRIGGERLASTSTATE -- initialize log file buffer AUDIOBUFFERACTIVE = initAudioBuffer() if not AUDIOBUFFERACTIVE then if debug then print("Error in init on initAudioBuffer()") end INITIALIZATION_ERROR = true setLEDError() end -- Everything initialized successfully if not INITIALIZATION_ERROR then setLEDReady() end -- Main loop while true do if bm.status().events > 0 then local e = bm.getEvent() if e.event == bm.event.BIAS_OK_DETECTED then send(0x01) elseif e.event == bm.event.BIAS_OK_REMOVED then send(0x02) elseif e.event == bm.event.BIAS_REV_DETECTED then send(0x0D) elseif e.event == bm.event.BIAS_REV_REMOVED then send(0x0E) elseif e.event == bm.event.BUS_LOCK_LOCKED then send(0x0F) elseif e.event == bm.event.BUS_LOCK_UNLOCKED then send(0x10) elseif e.event == bm.event.DISCOVERY_MODE then local status_val = (normStr(e.status) == "START") and 1 or 0 local rc = (status_val == 1) and N(e.respCycles) or 0 send(0x03, { status_val, rc }) elseif e.event == bm.event.NEWSTRCT then local dn_slots = N(e.dn and e.dn.slots) or 0 local up_slots = N(e.up and e.up.slots) or 0 local dn_dtOfs = N(e.dn and e.dn.dtOfs) or 0 local up_dtOfs = N(e.up and e.up.dtOfs) or 0 local dn_dtSlots = N(e.dn and e.dn.dtSlots) or 0 local up_dtSlots = N(e.up and e.up.dtSlots) or 0 send(0x04, { dn_slots & 0xFF, (dn_slots >> 8) & 0xFF, up_slots & 0xFF, (up_slots >> 8) & 0xFF, dn_dtOfs, up_dtOfs, dn_dtSlots, up_dtSlots }) elseif e.event == bm.event.I2C then local t = N(e.type) if t == I2C_REG then local rw = (e.rw and 1) or 0 send(0x05, { N(e.nodeAddr), N(e.addr), N(e.data), rw }) elseif t == I2C_PER then -- Start of a peripheral I2C transaction periph.active = true periph.nodeAddr = N(e.nodeAddr) periph.addr = N(e.addr) -- e.g. 0x39 periph.rw = (e.rw and 1) or 0 periph.bytes = {} periph.totalLen = 0 elseif t == I2C_COND then -- Peripheral transaction condition / byte events if not periph.active then -- Fallback: if we missed the I2C_PER start, create minimal context periph.active = true periph.nodeAddr = N(e.nodeAddr) periph.addr = 0 periph.rw = (e.rw and 1) or 0 periph.bytes = {} periph.totalLen = 0 end local c = N(e.condition) if c == C_ACK or c == C_NACK then -- Count true length always, but store only up to MAX_PERIPH_DATA periph.totalLen = periph.totalLen + 1 pushPeriphByte(e.data) elseif c == C_STOP or c == C_ERROR then -- lenByte = true length, capped to 255 local lenByte = periph.totalLen if lenByte > 255 then lenByte = 255 end -- Payload: nodeAddr, addr, rw, lenByte, then up to MAX_PERIPH_DATA bytes local payload = { periph.nodeAddr, periph.addr, periph.rw, lenByte } for i = 1, #periph.bytes do payload[#payload + 1] = periph.bytes[i] end send(0x06, payload) resetPeriph() end end elseif e.event == bm.event.DOWNSTREAM_SCF_ERROR then local typ = normStr(e.type) -- "SCF" or "SRF" local error_type = (typ == "SCF") and 0 or 1 send(0x07, { error_type, N(e.missed), N(e.hdcnt), N(e.crc) }) elseif e.event == bm.event.UPSTREAM_SRF_ERROR then local typ = normStr(e.type) local error_type = (typ == "SRF") and 1 or 0 send(0x08, { error_type, N(e.missed), N(e.hdcnt), N(e.crc) }) elseif e.event == bm.event.SLAVE_ERROR then local err = normStr(e.errorStr) local error_code = 0 if err == "SRFMISSED_ERROR" then error_code = 1 elseif err == "BROADCAST_ACK" then error_code = 2 elseif err == "DISCOVERY_ERROR" then error_code = 3 elseif err == "DOWNSTREAM_CRC_ERROR" then error_code = 4 elseif err == "UNSPECIFIED_ERROR" then error_code = 5 end send(0x09, { N(e.nodeAddr), error_code }) elseif e.event == bm.event.IRQ then send(0x0A, { N(e.nodeAddr), N(e.status) }) elseif e.event == bm.event.SEQUENCE_ERROR then send(0x0B, { N(e.seq) }) elseif e.event == bm.event.SPI then local tS = normStr(e.type) local sS = normStr(e.ss) local spi_type = 0 if tS == "ATOMIC_READ" then spi_type = 1 elseif tS == "ATOMIC_WRITE" then spi_type = 2 elseif tS == "FULL_DUPLEX" then spi_type = 3 elseif tS == "BULK_WRITE" then spi_type = 4 end local ss_code = 0 if sS == "ADR1" then ss_code = 1 elseif sS == "SIO2" then ss_code = 2 elseif sS == "ADR2" then ss_code = 3 end send(0x0C, { N(e.respNode), spi_type, ss_code, N(e.wLen or 0), N(e.rLen or 0) }) end end -- Check external GPIO trigger processSLSSTriggerGPIO() -- Update led states updateLEDs() -- Simple non-blocking quit check local key = term.getchar(term.NOWAIT) if key == string.byte('q') or key == string.byte('Q') then led.release() print("\n=== Stopped by user ===") break end end print("SeRoSys - Flextech AKT A2B Interface stopped cleanly. Goodbye!")