diff --git a/boards/ARDUINO_GIGA/omv_boardconfig.h b/boards/ARDUINO_GIGA/omv_boardconfig.h index ca8aca70b..3ee127dd9 100644 --- a/boards/ARDUINO_GIGA/omv_boardconfig.h +++ b/boards/ARDUINO_GIGA/omv_boardconfig.h @@ -74,6 +74,11 @@ #define OMV_USB_IRQN (OTG_FS_IRQn) #define OMV_USB_ULPI (0) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (4096) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE( \ + HAS_JPEG, HAS_DRAM, HAS_CRC, HAS_WIFI, HAS_BT, HAS_SD, HAS_ETH, HAS_USB_HS) + // PLL1 480MHz/48MHz SDMMC and FDCAN // USB and RNG are clocked from the HSI48 #define OMV_OSC_PLL1M (4) @@ -138,7 +143,7 @@ #define OMV_FB_SIZE (3M) // FB memory: header + VGA/GS image #define OMV_FB_ALLOC_SIZE (1M) // minimum fb alloc size #define OMV_FB_OVERLAY_MEMORY SRAM0 // Fast fb_alloc memory. -#define OMV_FB_OVERLAY_SIZE (448K) // Fast fb_alloc memory size. +#define OMV_FB_OVERLAY_SIZE (443K) // Fast fb_alloc memory size. #define OMV_SB_MEMORY DRAM // Streaming buffer memory. #define OMV_SB_SIZE (1M) // Streaming buffer size. #define OMV_DMA_MEMORY SRAM3 // Misc DMA buffers memory. diff --git a/boards/ARDUINO_GIGA/omv_boardconfig.mk b/boards/ARDUINO_GIGA/omv_boardconfig.mk index 90437fba7..cc852f4cf 100755 --- a/boards/ARDUINO_GIGA/omv_boardconfig.mk +++ b/boards/ARDUINO_GIGA/omv_boardconfig.mk @@ -13,6 +13,7 @@ OMV_BOARD_CFLAGS = -DCORE_CM7 \ -DCYW43_BT_FIRMWARE_ADDRESS=0x90FC0000 \ -DCYW43_BT_FIRMWARE_INCLUDE_FILE="\"$(TOP_DIR)/$(CYW4343_FW_DIR)/cyw4343_btfw.h\"" OMV_ENABLE_BL=0 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/ARDUINO_NANO_33_BLE_SENSE/omv_boardconfig.h b/boards/ARDUINO_NANO_33_BLE_SENSE/omv_boardconfig.h index b431347ac..51ffc9e8d 100644 --- a/boards/ARDUINO_NANO_33_BLE_SENSE/omv_boardconfig.h +++ b/boards/ARDUINO_NANO_33_BLE_SENSE/omv_boardconfig.h @@ -50,10 +50,6 @@ #define OMV_FIR_MLX90641_ENABLE (1) #define OMV_FIR_AMG8833_ENABLE (1) -// Debugging configuration. -#define OMV_TUSBDBG_ENABLE (1) -#define OMV_TUSBDBG_BUFFER (512) - // UMM heap block size #define OMV_UMM_BLOCK_SIZE 16 diff --git a/boards/ARDUINO_NANO_RP2040_CONNECT/omv_boardconfig.h b/boards/ARDUINO_NANO_RP2040_CONNECT/omv_boardconfig.h index ba0eb1515..d3d061878 100644 --- a/boards/ARDUINO_NANO_RP2040_CONNECT/omv_boardconfig.h +++ b/boards/ARDUINO_NANO_RP2040_CONNECT/omv_boardconfig.h @@ -49,10 +49,6 @@ extern unsigned char *OMV_BOARD_UID_ADDR; // Unique address. // FIR drivers configuration. #define OMV_FIR_AMG8833_ENABLE (1) -// Debugging configuration. -#define OMV_TUSBDBG_ENABLE (1) -#define OMV_TUSBDBG_BUFFER (2048) - // UMM heap block size #define OMV_UMM_BLOCK_SIZE 16 diff --git a/boards/ARDUINO_NICLA_VISION/omv_boardconfig.h b/boards/ARDUINO_NICLA_VISION/omv_boardconfig.h index 93ea36d14..70ab10c00 100644 --- a/boards/ARDUINO_NICLA_VISION/omv_boardconfig.h +++ b/boards/ARDUINO_NICLA_VISION/omv_boardconfig.h @@ -61,6 +61,11 @@ #define OMV_USB_ULPI_STP_PIN (&omv_pin_C0_OTG_HS) #define OMV_USB_ULPI_DIR_PIN (&omv_pin_C2_OTG_HS) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (4096) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE( \ + HAS_JPEG, HAS_DRAM, HAS_CRC, HAS_WIFI, HAS_BT, HAS_SD, HAS_ETH, HAS_USB_HS) + // Defined for cpu frequency scaling to override the revid. #define OMV_MAX_CPU_FREQ (400) @@ -131,7 +136,7 @@ #define OMV_CM4_BOOT_MEMORY SRAM4 // Use to boot CM4 for low-power mode. #define OMV_CM4_BOOT_SIZE 1K #define OMV_GC_BLOCK0_MEMORY DTCM // Main GC block 0. -#define OMV_GC_BLOCK0_SIZE (32K) +#define OMV_GC_BLOCK0_SIZE (31K) #define OMV_GC_BLOCK1_MEMORY SRAM4 // Extra GC block 1. #define OMV_GC_BLOCK1_SIZE (63K) #define OMV_GC_BLOCK2_MEMORY SRAM1 // Extra GC block 2. diff --git a/boards/ARDUINO_NICLA_VISION/omv_boardconfig.mk b/boards/ARDUINO_NICLA_VISION/omv_boardconfig.mk index 45a04fea8..000e33f4f 100755 --- a/boards/ARDUINO_NICLA_VISION/omv_boardconfig.mk +++ b/boards/ARDUINO_NICLA_VISION/omv_boardconfig.mk @@ -15,6 +15,7 @@ OMV_BOARD_CFLAGS = -DCORE_CM7 \ -DCYW43_BT_FIRMWARE_INCLUDE_FILE="\"$(TOP_DIR)/$(CYW4343_FW_DIR)/cyw4343_btfw.h\"" OMV_ENABLE_BL = 0 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.h b/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.h index 98b2fe2cf..0740cb9b9 100644 --- a/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.h +++ b/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.h @@ -70,6 +70,11 @@ #define OMV_USB_ULPI_STP_PIN (&omv_pin_C0_OTG_HS) #define OMV_USB_ULPI_DIR_PIN (&omv_pin_I11_OTG_HS) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (4096) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE( \ + HAS_JPEG, HAS_DRAM, HAS_CRC, HAS_WIFI, HAS_BT, HAS_SD, HAS_ETH, HAS_USB_HS) + // Defined for cpu frequency scaling to override the revid. #define OMV_MAX_CPU_FREQ (400) diff --git a/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.mk b/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.mk index cec8cb4bb..0128a9be1 100755 --- a/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.mk +++ b/boards/ARDUINO_PORTENTA_H7/omv_boardconfig.mk @@ -14,6 +14,7 @@ OMV_BOARD_CFLAGS = -DCORE_CM7 \ -DCYW43_BT_FIRMWARE_INCLUDE_FILE="\"$(TOP_DIR)/$(CYW4343_FW_DIR)/cyw4343_btfw.h\"" OMV_ENABLE_BL=0 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV2/omv_boardconfig.h b/boards/OPENMV2/omv_boardconfig.h index 28f388f17..0c3304b13 100644 --- a/boards/OPENMV2/omv_boardconfig.h +++ b/boards/OPENMV2/omv_boardconfig.h @@ -60,6 +60,10 @@ // USB IRQn. #define OMV_USB_IRQN (OTG_FS_IRQn) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (512) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE(HAS_SD) + //PLL1 192MHz/48MHz #define OMV_OSC_PLL1M (12) #define OMV_OSC_PLL1N (384) diff --git a/boards/OPENMV2/omv_boardconfig.mk b/boards/OPENMV2/omv_boardconfig.mk index 64c46974e..bb0c50443 100755 --- a/boards/OPENMV2/omv_boardconfig.mk +++ b/boards/OPENMV2/omv_boardconfig.mk @@ -9,6 +9,7 @@ OMV_HSE_VALUE=12000000 DFU_DEVICE=0x0483:0xdf11 OMV_ENABLE_BL=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV3/omv_boardconfig.h b/boards/OPENMV3/omv_boardconfig.h index fe2ece51f..7608b0d5d 100644 --- a/boards/OPENMV3/omv_boardconfig.h +++ b/boards/OPENMV3/omv_boardconfig.h @@ -60,6 +60,10 @@ // USB IRQn. #define OMV_USB_IRQN (OTG_FS_IRQn) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (512) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE(HAS_JPEG, HAS_CRC, HAS_SD) + //PLL1 216MHz/48MHz #define OMV_OSC_PLL1M (12) #define OMV_OSC_PLL1N (432) @@ -87,7 +91,7 @@ #define OMV_FB_ALLOC_SIZE (84K) // minimum fb alloc size #define OMV_DMA_MEMORY DTCM // Misc DMA buffers #define OMV_GC_BLOCK0_MEMORY DTCM // Main GC block -#define OMV_GC_BLOCK0_SIZE (53K) +#define OMV_GC_BLOCK0_SIZE (52K) #define OMV_SB_SIZE (22K) // IDE JPEG buffer (header + data). #define OMV_MSC_BUF_SIZE (2K) // USB MSC bot data #define OMV_FFS_BUF_SIZE (32K) // Flash filesystem cache diff --git a/boards/OPENMV3/omv_boardconfig.mk b/boards/OPENMV3/omv_boardconfig.mk index 5f13d2305..8a202a6f1 100755 --- a/boards/OPENMV3/omv_boardconfig.mk +++ b/boards/OPENMV3/omv_boardconfig.mk @@ -9,6 +9,7 @@ OMV_HSE_VALUE=12000000 DFU_DEVICE=0x0483:0xdf11 OMV_ENABLE_BL=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV4/omv_boardconfig.h b/boards/OPENMV4/omv_boardconfig.h index 2a6a73839..5e6557b0e 100644 --- a/boards/OPENMV4/omv_boardconfig.h +++ b/boards/OPENMV4/omv_boardconfig.h @@ -74,6 +74,10 @@ // USB IRQn. #define OMV_USB_IRQN (OTG_FS_IRQn) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (512) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE(HAS_JPEG, HAS_CRC, HAS_SD) + //PLL1 480MHz/48MHz for USB, SDMMC and FDCAN #define OMV_OSC_PLL1M (3) #define OMV_OSC_PLL1N (240) diff --git a/boards/OPENMV4/omv_boardconfig.mk b/boards/OPENMV4/omv_boardconfig.mk index 189e84dc6..b9e3a32b0 100755 --- a/boards/OPENMV4/omv_boardconfig.mk +++ b/boards/OPENMV4/omv_boardconfig.mk @@ -10,6 +10,7 @@ OMV_BOOT_ADDR=0x08000000 OMV_HSE_VALUE=12000000 OMV_ENABLE_BL=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV4P/omv_boardconfig.h b/boards/OPENMV4P/omv_boardconfig.h index f3c8386ae..8728fef6a 100644 --- a/boards/OPENMV4P/omv_boardconfig.h +++ b/boards/OPENMV4P/omv_boardconfig.h @@ -60,6 +60,10 @@ // USB IRQn. #define OMV_USB_IRQN (OTG_FS_IRQn) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (4096) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE(HAS_JPEG, HAS_DRAM, HAS_CRC, HAS_SD) + //PLL1 48MHz for USB, SDMMC and FDCAN #define OMV_OSC_PLL1M (3) #define OMV_OSC_PLL1N (240) @@ -125,7 +129,7 @@ #define OMV_DMA_MEMORY_D2 SRAM3 // Domain 2 DMA buffers. #define OMV_DMA_MEMORY_D3 SRAM4 // Domain 3 DMA buffers. #define OMV_GC_BLOCK0_MEMORY SRAM1 // Main GC block -#define OMV_GC_BLOCK0_SIZE (239K) +#define OMV_GC_BLOCK0_SIZE (235K) #define OMV_GC_BLOCK1_MEMORY DRAM // Extra GC block 0. #define OMV_GC_BLOCK1_SIZE (4M) #define OMV_MSC_BUF_SIZE (2K) // USB MSC bot data diff --git a/boards/OPENMV4P/omv_boardconfig.mk b/boards/OPENMV4P/omv_boardconfig.mk index e9c85b90c..61cc65674 100755 --- a/boards/OPENMV4P/omv_boardconfig.mk +++ b/boards/OPENMV4P/omv_boardconfig.mk @@ -12,6 +12,7 @@ OMV_ENABLE_BL=1 OMV_BOSON_ENABLE=1 OMV_GENX320_ENABLE=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMVPT/omv_boardconfig.h b/boards/OPENMVPT/omv_boardconfig.h index 9cbb29691..310693a21 100644 --- a/boards/OPENMVPT/omv_boardconfig.h +++ b/boards/OPENMVPT/omv_boardconfig.h @@ -47,6 +47,10 @@ // USB IRQn. #define OMV_USB_IRQN (OTG_FS_IRQn) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (4096) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE(HAS_JPEG, HAS_DRAM, HAS_CRC, HAS_SD) + //PLL1 48MHz for USB, SDMMC and FDCAN #define OMV_OSC_PLL1M (3) #define OMV_OSC_PLL1N (240) diff --git a/boards/OPENMVPT/omv_boardconfig.mk b/boards/OPENMVPT/omv_boardconfig.mk index 189e84dc6..b9e3a32b0 100755 --- a/boards/OPENMVPT/omv_boardconfig.mk +++ b/boards/OPENMVPT/omv_boardconfig.mk @@ -10,6 +10,7 @@ OMV_BOOT_ADDR=0x08000000 OMV_HSE_VALUE=12000000 OMV_ENABLE_BL=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV_AE3/omv_boardconfig.h b/boards/OPENMV_AE3/omv_boardconfig.h index c5abddc75..f3876d7a2 100644 --- a/boards/OPENMV_AE3/omv_boardconfig.h +++ b/boards/OPENMV_AE3/omv_boardconfig.h @@ -67,11 +67,6 @@ extern unsigned char OMV_BOARD_UID_ADDR[12]; // Unique address. #define OMV_FIR_MLX90641_ENABLE (0) #define OMV_FIR_AMG8833_ENABLE (1) -// Debugging configuration. -#define OMV_TUSBDBG_ENABLE (CORE_M55_HP) -#define OMV_TUSBDBG_PACKET (512) -#define OMV_TUSBDBG_BUFFER (4096) - // UMM heap block size #define OMV_UMM_BLOCK_SIZE 256 @@ -80,6 +75,12 @@ extern unsigned char OMV_BOARD_UID_ADDR[12]; // Unique address. #define OMV_USB1_IRQ_HANDLER (USB_IRQHandler) #define OMV_USB_SWITCH_PIN (&omv_pin_USB_SWITCH) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (8192) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE( \ + HAS_GPU, HAS_NPU, HAS_JPEG, HAS_CRC, HAS_PMU, HAS_WIFI, \ + HAS_BT, HAS_SD, HAS_ETH, HAS_USB_HS, HAS_MULTICORE) + // Linker script constants (see the linker script template alif.ld.S). // Note: fb_alloc is a stack-based, dynamically allocated memory on FB. // The maximum available fb_alloc memory = FB_ALLOC_SIZE + FB_SIZE - (w*h*bpp). diff --git a/boards/OPENMV_AE3/omv_boardconfig.mk b/boards/OPENMV_AE3/omv_boardconfig.mk index 1f75f7886..3497ac4a7 100755 --- a/boards/OPENMV_AE3/omv_boardconfig.mk +++ b/boards/OPENMV_AE3/omv_boardconfig.mk @@ -13,6 +13,7 @@ OMV_FIRM_ADDR=0x80020000 OMV_HSE_VALUE=12000000 OMV_LEPTON_SDK_ENABLE=1 OMV_TOF_VL53L8CX_ENABLE=1 +OMV_USB_STACK_TINYUSB=$(CORE_M55_HP) MICROPY_PY_CSI = $(CORE_M55_HP) MICROPY_PY_CSI_NG = $(CORE_M55_HP) MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV_N6/omv_boardconfig.h b/boards/OPENMV_N6/omv_boardconfig.h index d936f0ecd..91007d19b 100644 --- a/boards/OPENMV_N6/omv_boardconfig.h +++ b/boards/OPENMV_N6/omv_boardconfig.h @@ -62,6 +62,12 @@ // USB IRQn. #define OMV_USB_IRQN (USB1_OTG_HS_IRQn) +// OpenMV protocol configuration. +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (8192) +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE( \ + HAS_GPU, HAS_NPU, HAS_ISP, HAS_VENC, HAS_JPEG, HAS_DRAM, \ + HAS_CRC, HAS_PMU, HAS_WIFI, HAS_BT, HAS_SD, HAS_ETH, HAS_USB_HS) + //PLL1 800MHz #define OMV_OSC_PLL1M (3) #define OMV_OSC_PLL1N (50) diff --git a/boards/OPENMV_N6/omv_boardconfig.mk b/boards/OPENMV_N6/omv_boardconfig.mk index 4ea2df2d5..ca7635059 100755 --- a/boards/OPENMV_N6/omv_boardconfig.mk +++ b/boards/OPENMV_N6/omv_boardconfig.mk @@ -26,6 +26,7 @@ OMV_ENABLE_BL=1 OMV_BOSON_ENABLE=1 OMV_GENX320_ENABLE=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_STMUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/boards/OPENMV_RT1060/omv_boardconfig.h b/boards/OPENMV_RT1060/omv_boardconfig.h index 3b0bf8390..61b8b34fb 100644 --- a/boards/OPENMV_RT1060/omv_boardconfig.h +++ b/boards/OPENMV_RT1060/omv_boardconfig.h @@ -47,13 +47,13 @@ #define OMV_FIR_MLX90641_ENABLE (1) #define OMV_FIR_AMG8833_ENABLE (1) -// Debugging configuration. -#define OMV_TUSBDBG_ENABLE (1) -#define OMV_TUSBDBG_BUFFER (2048) - // UMM heap block size #define OMV_UMM_BLOCK_SIZE 256 +// Protocol hardware capabilities +#define OMV_PROTOCOL_HW_CAPS OMV_PROTOCOL_HW_CAPS_MAKE( \ + HAS_DRAM, HAS_WIFI, HAS_BT, HAS_SD, HAS_ETH, HAS_USB_HS) + // USB config. #define OMV_USB_IRQN (USB_OTG1_IRQn) #define OMV_USB1_IRQ_HANDLER (USB_OTG1_IRQHandler) @@ -81,7 +81,7 @@ #define OMV_GC_BLOCK0_MEMORY OCRM2 // Extra GC block 0. #define OMV_GC_BLOCK0_SIZE (64K) #define OMV_GC_BLOCK1_MEMORY DTCM // Main GC block -#define OMV_GC_BLOCK1_SIZE (272K) +#define OMV_GC_BLOCK1_SIZE (271K) #define OMV_GC_BLOCK2_MEMORY DRAM // Extra GC block 1. #define OMV_GC_BLOCK2_SIZE (8M) #define OMV_RAMFUNC_MEMORY ITCM2 // RAM code memory. diff --git a/boards/OPENMV_RT1060/omv_boardconfig.mk b/boards/OPENMV_RT1060/omv_boardconfig.mk index 84e525cd5..a0d6a0323 100755 --- a/boards/OPENMV_RT1060/omv_boardconfig.mk +++ b/boards/OPENMV_RT1060/omv_boardconfig.mk @@ -11,6 +11,7 @@ OMV_HSE_VALUE=12000000 OMV_BOSON_ENABLE=1 OMV_GENX320_ENABLE=1 OMV_LEPTON_SDK_ENABLE=1 +OMV_USB_STACK_TINYUSB=1 MICROPY_PY_CSI = 1 MICROPY_PY_CSI_NG = 1 MICROPY_PY_FIR = 1 diff --git a/common/common.mk b/common/common.mk index 090db25f1..c68b6ce8e 100644 --- a/common/common.mk +++ b/common/common.mk @@ -33,11 +33,9 @@ COMMON_SRC_C += \ omv_csi.c \ omv_crc.c \ pendsv.c \ - tinyusb_debug.c \ trace.c \ umm_malloc.c \ unaligned_memcpy.c \ - usbdbg.c \ vospi.c \ queue.c \ omv_profiler.c \ diff --git a/common/mp_utils.c b/common/mp_utils.c index a45781d48..3fd82ce11 100644 --- a/common/mp_utils.c +++ b/common/mp_utils.c @@ -35,7 +35,6 @@ #include "shared/runtime/softtimer.h" #include "shared/runtime/pyexec.h" #include "omv_boardconfig.h" -#include "usbdbg.h" #include "mp_utils.h" void __attribute__((weak)) gc_collect(void) { @@ -59,35 +58,6 @@ void __attribute__((weak)) gc_collect(void) { gc_collect_end(); } -bool mp_exec_bootscript(const char *path, bool interruptible) { - nlr_buf_t nlr; - bool interrupted = false; - - if (nlr_push(&nlr) == 0) { - // Enable IDE interrupts if allowed. - if (interruptible) { - usbdbg_set_irq_enabled(true); - usbdbg_set_script_running(true); - } - - // Parse, compile and execute the script. - pyexec_file_if_exists(path, true); - nlr_pop(); - } else { - interrupted = true; - } - - // Disable IDE interrupts - usbdbg_set_irq_enabled(false); - usbdbg_set_script_running(false); - - if (interrupted) { - mp_obj_print_exception(&mp_plat_print, (mp_obj_t) nlr.ret_val); - } - - return interrupted; -} - void mp_init_gc_stack(void *sstack, void *estack, void *heap_start, void *heap_end, size_t stack_limit) { // Initialize the stack. mp_stack_set_top(estack); diff --git a/common/mp_utils.h b/common/mp_utils.h index 1ef9a194e..831fe674a 100644 --- a/common/mp_utils.h +++ b/common/mp_utils.h @@ -27,5 +27,4 @@ #define __MP_UTILS_H__ typedef struct _fs_user_mount_t fs_user_mount_t; void mp_init_gc_stack(void *stack_start, void *stack_end, void *heap_start, void *heap_end, size_t stack_limit); -bool mp_exec_bootscript(const char *path, bool interruptible); #endif // __MP_UTILS_H__ diff --git a/common/omv_buffer.h b/common/omv_buffer.h new file mode 100644 index 000000000..96e000f56 --- /dev/null +++ b/common/omv_buffer.h @@ -0,0 +1,94 @@ +#ifndef __OMV_BUFFER_H__ +#define __OMV_BUFFER_H__ + +#include +#include +#include +#include + +typedef struct { + size_t size; // Total buffer size + uint8_t *read_ptr; // Where to read/process from + uint8_t *write_ptr; // Where to write new data + size_t used; // Bytes currently in buffer (unprocessed) + uint8_t *data; // The actual buffer memory +} omv_buffer_t; + +// Initialize buffer with pre-allocated memory +static inline void omv_buffer_init(omv_buffer_t *buf, uint8_t *buffer, size_t size) { + buf->data = buffer; + buf->size = size; + buf->read_ptr = buffer; + buf->write_ptr = buffer; + buf->used = 0; +} + +// Reset buffer to empty state +static inline void omv_buffer_clear(omv_buffer_t *buf) { + buf->read_ptr = buf->data; + buf->write_ptr = buf->data; + buf->used = 0; +} + +// Get number of bytes available for reading +static inline size_t omv_buffer_avail(omv_buffer_t *buf) { + return buf->used; +} + +// Get number of bytes available for writing +static inline size_t omv_buffer_free(omv_buffer_t *buf) { + return buf->size - buf->used; +} + +// Get pointer to readable data +// Returns data pointer or NULL if buffer is empty +static inline void *omv_buffer_data(omv_buffer_t *buf) { + return (buf->used > 0) ? buf->read_ptr : NULL; +} + +// Peek at 16-bit value at start of buffer +// Caller must ensure omv_buffer_avail(buf) >= 2 +static inline uint16_t omv_buffer_peek16(omv_buffer_t *buf) { + return *((uint16_t *) buf->read_ptr); +} + +// Mark data as consumed and advance read pointer +// Resets buffer to beginning when empty +static inline void omv_buffer_consume(omv_buffer_t *buf, size_t consumed) { + buf->read_ptr += consumed; + buf->used -= consumed; + + // Reset to beginning if buffer is empty + if (buf->used == 0) { + buf->read_ptr = buf->data; + buf->write_ptr = buf->data; + } +} + +// Claim contiguous space for writing, compacting buffer if needed +static inline void *omv_buffer_claim(omv_buffer_t *buf, size_t requested_size) { + // Check if we have enough total space + if (buf->used + requested_size > buf->size) { + return NULL; // Buffer full + } + + size_t space_to_end = buf->size - (buf->write_ptr - buf->data); + + // If not enough contiguous space at the end, move unprocessed data to beginning + if (space_to_end < requested_size && buf->used > 0) { + // Move unprocessed data to the beginning + memmove(buf->data, buf->read_ptr, buf->used); + buf->read_ptr = buf->data; + buf->write_ptr = buf->data + buf->used; + } + + return buf->write_ptr; +} + +// Commit written data to the buffer +// Call this after successfully writing to the claimed space +static inline void omv_buffer_commit(omv_buffer_t *buf, size_t written) { + buf->write_ptr += written; + buf->used += written; +} +#endif // __OMV_BUFFER_H__ diff --git a/common/tinyusb_debug.c b/common/tinyusb_debug.c deleted file mode 100644 index 4cfc3585c..000000000 --- a/common/tinyusb_debug.c +++ /dev/null @@ -1,231 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * - * Copyright (C) 2022-2024 OpenMV, LLC. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * Tinyusb CDC debugger helper code. - */ - -#include "omv_boardconfig.h" - -#if OMV_TUSBDBG_ENABLE -#include "py/runtime.h" -#include "py/stream.h" -#include "py/mphal.h" -#include "py/ringbuf.h" -#include "pendsv.h" - -#include "tusb.h" -#include "usbdbg.h" -#include "tinyusb_debug.h" -#include "omv_common.h" -#include "cmsis_gcc.h" - -#define USBDBG_DATA_TIMEOUT (1000) -#define DEBUG_EP_SIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) - -typedef struct { - uint32_t timestamp; - uint8_t opcode; - uint32_t length; - bool debug_mode; - mp_sched_node_t sched_node; - ringbuf_t ringbuf; - uint8_t rawbuf[OMV_TUSBDBG_BUFFER]; -} debug_context_t; - -static debug_context_t ctx; - -uint32_t usb_cdc_buf_len() { - return ringbuf_avail(&ctx.ringbuf); -} - -uint32_t usb_cdc_get_buf(uint8_t *buf, uint32_t len) { - // Read all of the request bytes. - if (ringbuf_get_bytes(&ctx.ringbuf, buf, len) == 0) { - return len; - } - - // Try to read as much data as possible. - uint32_t bytes = 0; - for (; bytes < len; bytes++) { - int c = ringbuf_get(&ctx.ringbuf); - if (c == -1) { - break; - } - buf[bytes] = c; - } - - return bytes; -} - -void usb_cdc_reset_buffers(void) { - ctx.ringbuf.iget = 0; - ctx.ringbuf.iput = 0; -} - -void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *coding) { - usb_cdc_reset_buffers(); - - #if defined(MICROPY_BOARD_ENTER_BOOTLOADER) - if (coding->bit_rate == 1200) { - MICROPY_BOARD_ENTER_BOOTLOADER(0, 0); - } - #endif - - ctx.debug_mode = (coding->bit_rate == USBDBG_BAUDRATE_SLOW || - coding->bit_rate == USBDBG_BAUDRATE_FAST); -} - -// Wrapped MicroPython functions. -void __wrap_mp_usbd_task(void) { - extern void __real_mp_usbd_task(void); - - if (!tinyusb_debug_enabled()) { - __real_mp_usbd_task(); - } -} - -void __wrap_tud_cdc_rx_cb(uint8_t itf) { - extern void __real_tud_cdc_rx_cb(uint8_t itf); - - if (!tinyusb_debug_enabled()) { - __real_tud_cdc_rx_cb(itf); - } -} - -uintptr_t __wrap_mp_hal_stdio_poll(uintptr_t poll_flags) { - extern uintptr_t __real_mp_hal_stdio_poll(uintptr_t poll_flags); - - if (!tinyusb_debug_enabled()) { - return __real_mp_hal_stdio_poll(poll_flags); - } - return 0; -} - -mp_uint_t __wrap_mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) { - extern mp_uint_t __real_mp_hal_stdout_tx_strn(const char *str, mp_uint_t len); - - if (!tinyusb_debug_enabled()) { - return __real_mp_hal_stdout_tx_strn(str, len); - } else if (tud_cdc_connected()) { - for (int i = 0; i < len; i++) { - // On overflow, reset the ring buffer, if this string fits - // entirely in the buffer, to recover from broken strings. - if (ringbuf_put(&ctx.ringbuf, str[i]) == -1 && len <= ctx.ringbuf.size) { - usb_cdc_reset_buffers(); - ringbuf_put(&ctx.ringbuf, str[i]); - } - } - } - return len; -} - -int tinyusb_debug_init(void) { - ctx.opcode = 0; - ctx.length = 0; - if (!ctx.ringbuf.buf) { - ctx.ringbuf = (ringbuf_t) { ctx.rawbuf, sizeof(ctx.rawbuf), 0, 0 }; - } - return 0; -} - -bool tinyusb_debug_enabled(void) { - return ctx.debug_mode; -} - -void tinyusb_debug_task(mp_sched_node_t *node) { - tud_task_ext(0, false); - - if (!tinyusb_debug_enabled() || !tud_cdc_connected()) { - return; - } - - if (!ctx.length && tud_cdc_available() >= USBDBG_HEADER_SIZE) { - uint8_t cmdbuf[USBDBG_HEADER_SIZE]; - tud_cdc_read(cmdbuf, sizeof(cmdbuf)); - - if (cmdbuf[0] == 0x30) { - ctx.opcode = cmdbuf[1]; - ctx.length = *((uint32_t*)(cmdbuf+2)); - usbdbg_control(NULL, ctx.opcode, ctx.length); - } - - if (ctx.length > 0) { - ctx.timestamp = mp_hal_ticks_ms(); - } - } - - while (ctx.length) { - uint32_t bytes = 0; - - if (tud_task_event_ready()) { - tud_task_ext(0, false); - } - - if (ctx.opcode & 0x80) { - bytes = OMV_MIN(ctx.length, tud_cdc_write_available()); - if (bytes) { - ctx.length -= bytes; - usbdbg_data_in(bytes, tud_cdc_write); - } - if (bytes > 0 && bytes < DEBUG_EP_SIZE) { - tud_cdc_write_flush(); - } - } else { - bytes = OMV_MIN(ctx.length, tud_cdc_available()); - if (bytes) { - ctx.length -= bytes; - usbdbg_data_out(bytes, tud_cdc_read); - } - } - - if (bytes) { - ctx.timestamp = mp_hal_ticks_ms(); - } else if (__get_PRIMASK() & 1) { - break; - } else if (check_timeout_ms(ctx.timestamp, USBDBG_DATA_TIMEOUT)) { - tinyusb_debug_init(); - } - } -} - -// For the mimxrt, and nrf ports this replaces the weak USB IRQ handlers. -// For the RP2 port, this handler is installed in main.c -void OMV_USB1_IRQ_HANDLER(void) { - dcd_int_handler(0); - // If there are any event to process, schedule a call to cdc loop. - if (tud_task_event_ready()) { - mp_sched_schedule_node(&ctx.sched_node, tinyusb_debug_task); - } -} - -#if defined(OMV_USB2_IRQ_HANDLER) -void OMV_USB2_IRQ_HANDLER(void) { - dcd_int_handler(1); - // If there are any event to process, schedule a call to cdc loop. - if (tud_task_event_ready()) { - mp_sched_schedule_node(&ctx.sched_node, tinyusb_debug_task); - } -} -#endif - -#endif //OMV_TINYUSB_DEBUG diff --git a/common/tinyusb_debug.h b/common/tinyusb_debug.h deleted file mode 100644 index d8c92a966..000000000 --- a/common/tinyusb_debug.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * - * Copyright (C) 2022-2024 OpenMV, LLC. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * Tinyusb CDC debugger helper code. - */ -#ifndef __TUSBDBG_H__ -void USBD_IRQHandler(void); -int tinyusb_debug_init(void); -bool tinyusb_debug_enabled(void); -#endif // __TUSBDBG_H__ diff --git a/common/usbdbg.c b/common/usbdbg.c deleted file mode 100644 index 26fc5adff..000000000 --- a/common/usbdbg.c +++ /dev/null @@ -1,454 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * - * Copyright (C) 2013-2024 OpenMV, LLC. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * USB debugger. - */ -#include -#include -#include "py/nlr.h" -#include "py/gc.h" -#include "py/mphal.h" -#include "py/obj.h" -#include "py/objstr.h" -#include "py/runtime.h" - -#include "imlib.h" -#if MICROPY_PY_CSI || MICROPY_PY_CSI_NG -#include "omv_i2c.h" -#include "omv_csi.h" -#endif -#include "framebuffer.h" -#include "usbdbg.h" -#include "tinyusb_debug.h" -#include "omv_boardconfig.h" -#include "py_image.h" - -static int xfer_offs; -static int xfer_size; -static enum usbdbg_cmd cmd; - -static volatile bool script_ready; -static volatile bool script_running; -static volatile bool irq_enabled; -static vstr_t script_buf; - -// These functions must be implemented by the stack. -extern uint32_t usb_cdc_buf_len(); -extern uint32_t usb_cdc_get_buf(uint8_t *buf, uint32_t len); -extern void usb_cdc_reset_buffers(void); - -void usbdbg_init() { - cmd = USBDBG_NONE; - script_ready = false; - script_running = false; - irq_enabled = false; - vstr_init(&script_buf, 32); - #if OMV_TUSBDBG_ENABLE - tinyusb_debug_init(); - #endif - #if OMV_PROFILER_ENABLE - omv_profiler_init(); - #endif -} - -bool usbdbg_script_ready() { - return script_ready; -} - -vstr_t *usbdbg_get_script() { - return &script_buf; -} - -bool usbdbg_is_busy() { - return cmd != USBDBG_NONE; -} - -void usbdbg_set_script_running(bool running) { - script_running = running; -} - -inline void usbdbg_set_irq_enabled(bool enabled) { - if (enabled) { - NVIC_EnableIRQ(OMV_USB_IRQN); - } else { - NVIC_DisableIRQ(OMV_USB_IRQN); - } - __DSB(); __ISB(); - irq_enabled = enabled; -} - -static void usbdbg_interrupt_vm(bool ready) { - // Set script ready flag - script_ready = ready; - - // Set script running flag - script_running = ready; - - // Disable IDE IRQ (re-enabled by pyexec or in main). - usbdbg_set_irq_enabled(false); - - // Abort the VM. - mp_sched_vm_abort(); - - // When the VM runs again it will raise a KeyboardInterrupt. - mp_sched_keyboard_interrupt(); -} - -bool usbdbg_get_irq_enabled() { - return irq_enabled; -} - -void usbdbg_data_in(uint32_t size, usbdbg_write_callback_t write_callback) { - switch (cmd) { - case USBDBG_FW_VERSION: { - uint32_t buffer[3] = { - FIRMWARE_VERSION_MAJOR, - FIRMWARE_VERSION_MINOR, - FIRMWARE_VERSION_PATCH, - }; - cmd = USBDBG_NONE; - write_callback(&buffer, sizeof(buffer)); - break; - } - - case USBDBG_TX_BUF_LEN: { - uint32_t tx_buf_len = usb_cdc_buf_len(); - cmd = USBDBG_NONE; - write_callback(&tx_buf_len, sizeof(tx_buf_len)); - break; - } - - case USBDBG_SENSOR_ID: { - uint32_t buffer = 0xFF; - #if MICROPY_PY_CSI - omv_csi_t *csi = omv_csi_get(-1); - if (omv_csi_is_detected(csi) == true) { - buffer = omv_csi_get_id(csi); - } - #endif - cmd = USBDBG_NONE; - write_callback(&buffer, sizeof(buffer)); - break; - } - - case USBDBG_TX_BUF: { - uint8_t buffer[size]; - size = usb_cdc_get_buf(buffer, size); - xfer_offs += size; - if (xfer_offs == xfer_size) { - cmd = USBDBG_NONE; - } - if (size) { - write_callback(buffer, size); - } - break; - } - - case USBDBG_FRAME_SIZE: { - // Return 0 if FB is locked or not ready. - uint32_t buffer[3] = { 0 }; - // Try to lock FB. If header size == 0 frame is not ready - framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); - if (mutex_try_lock(&fb->lock, MUTEX_TID_IDE)) { - // If header size == 0 frame is not ready - if (fb->size == 0) { - // unlock FB - mutex_unlock(&fb->lock, MUTEX_TID_IDE); - } else { - // Return header w, h and size/bpp - buffer[0] = fb->w; - buffer[1] = fb->h; - buffer[2] = fb->size; - } - } - cmd = USBDBG_NONE; - write_callback(&buffer, sizeof(buffer)); - break; - } - - case USBDBG_FRAME_DUMP: - if (xfer_offs < xfer_size) { - framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); - write_callback(fb->raw_base + sizeof(framebuffer_header_t) + xfer_offs, size); - xfer_offs += size; - if (xfer_offs == xfer_size) { - cmd = USBDBG_NONE; - fb->w = 0; fb->h = 0; fb->size = 0; - mutex_unlock(&fb->lock, MUTEX_TID_IDE); - } - } - break; - - case USBDBG_ARCH_STR: { - uint8_t buffer[64]; - unsigned int uid[3] = { - #if (OMV_BOARD_UID_SIZE == 2) - 0U, - #else - *((unsigned int *) (OMV_BOARD_UID_ADDR + OMV_BOARD_UID_OFFSET * 2)), - #endif - *((unsigned int *) (OMV_BOARD_UID_ADDR + OMV_BOARD_UID_OFFSET * 1)), - *((unsigned int *) (OMV_BOARD_UID_ADDR + OMV_BOARD_UID_OFFSET * 0)), - }; - snprintf((char *) buffer, 64, "%s [%s:%08X%08X%08X]", - OMV_BOARD_ARCH, OMV_BOARD_TYPE, uid[0], uid[1], uid[2]); - cmd = USBDBG_NONE; - write_callback(&buffer, sizeof(buffer)); - break; - } - - case USBDBG_SCRIPT_RUNNING: { - cmd = USBDBG_NONE; - uint32_t buffer = script_running; - write_callback(&buffer, sizeof(buffer)); - break; - } - - case USBDBG_GET_STATE: { - // IDE will request 63/511 bytes of data to avoid ZLP packets. - // 64/512 packets still work too but generate ZLP packets. - size = OMV_MIN(size, 512); - uint8_t byte_buffer[size]; - memset(byte_buffer, 0, size); - uint32_t *buffer = (uint32_t *) byte_buffer; - static uint32_t last_update_ms = 0; - - // Set script running flag - if (script_running) { - buffer[0] |= USBDBG_FLAG_SCRIPT_RUNNING; - } - - // Set text buf valid flag. - uint32_t tx_buf_len = usb_cdc_buf_len(); - if (tx_buf_len) { - buffer[0] |= USBDBG_FLAG_TEXTBUF_NOTEMPTY; - } - - // Set code profiling flags - #if OMV_PROFILER_ENABLE - buffer[0] |= USBDBG_FLAG_PROFILE_ENABLED; - #if __PMU_PRESENT - buffer[0] |= USBDBG_FLAG_PROFILE_HAS_PMU; - #endif // __PMU_PRESENT - #endif // OMV_PROFILER_ENABLE - - // Limit the frames sent over USB to 20Hz. - framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); - if (check_timeout_ms(last_update_ms, 50) && - mutex_try_lock_fair(&fb->lock, MUTEX_TID_IDE)) { - // If header size == 0 frame is not ready - if (fb->size == 0) { - // unlock FB - mutex_unlock(&fb->lock, MUTEX_TID_IDE); - } else { - // Set valid frame flag. - buffer[0] |= USBDBG_FLAG_FRAMEBUF_LOCKED; - - // Set frame width, height and size/bpp - buffer[1] = fb->w; - buffer[2] = fb->h; - buffer[3] = fb->size; - last_update_ms = mp_hal_ticks_ms(); - } - } - - // The rest of this packet is packed with text buffer. - if (tx_buf_len) { - const uint32_t hdr = 16; - tx_buf_len = OMV_MIN(tx_buf_len, (size - hdr - 1)); - usb_cdc_get_buf(byte_buffer + hdr, tx_buf_len); - byte_buffer[hdr + tx_buf_len] = 0; // Null-terminate - } - - cmd = USBDBG_NONE; - write_callback(&byte_buffer, size); - break; - } - - case USBDBG_PROFILE_SIZE: { - // Return 0 if the profiling data is locked. - uint32_t buffer[3] = { 0 }; - #if OMV_PROFILER_ENABLE - if (mutex_try_lock(omv_profiler_lock(), MUTEX_TID_IDE)) { - size_t size = omv_profiler_get_size(); - buffer[0] = size / sizeof(omv_profiler_data_t); - buffer[1] = sizeof(omv_profiler_data_t); - buffer[2] = __PMU_NUM_EVENTCNT; - } - #endif - cmd = USBDBG_NONE; - write_callback(&buffer, sizeof(buffer)); - break; - } - - #if OMV_PROFILER_ENABLE - case USBDBG_PROFILE_DUMP: - if (xfer_offs < xfer_size) { - const char *data = omv_profiler_get_data(); - write_callback(data + xfer_offs, size); - xfer_offs += size; - if (xfer_offs == xfer_size) { - cmd = USBDBG_NONE; - mutex_unlock(omv_profiler_lock(), MUTEX_TID_IDE); - } - } - break; - #endif - - default: /* error */ - break; - } -} - -void usbdbg_data_out(uint32_t size, usbdbg_read_callback_t read_callback) { - switch (cmd) { - case USBDBG_FB_ENABLE: { - uint32_t enabled = 0; - framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); - read_callback(&enabled, 4); - framebuffer_set_enabled(fb, enabled); - if (fb->enabled == 0) { - // When disabling framebuffer, the IDE might still be holding FB lock. - // If the IDE is not the current lock owner, this operation is ignored. - mutex_unlock(&fb->lock, MUTEX_TID_IDE); - } - cmd = USBDBG_NONE; - break; - } - - case USBDBG_SCRIPT_EXEC: - // check if GC is locked before allocating memory for vstr. If GC was locked - // at least once before the script is fully uploaded xfer_offs will be less - // than the total size (xfer_size) and the script will Not be executed. - if (!script_running) { - nlr_buf_t nlr; - if (!gc_is_locked() && nlr_push(&nlr) == 0) { - char *buffer = vstr_add_len(&script_buf, size); - read_callback(buffer, size); - nlr_pop(); - } - xfer_offs += size; - if (xfer_offs == xfer_size) { - cmd = USBDBG_NONE; - // Schedule the IDE exception to interrupt the VM. - usbdbg_interrupt_vm(true); - } - } - break; - - case USBDBG_PROFILE_MODE: { - uint32_t buffer[1]; - read_callback(&buffer, sizeof(buffer)); - #if OMV_PROFILER_ENABLE - omv_profiler_set_mode(buffer[0]); - #endif - cmd = USBDBG_NONE; - break; - } - - case USBDBG_PROFILE_EVENT: { - uint32_t buffer[2]; - read_callback(&buffer, sizeof(buffer)); - #if OMV_PROFILER_ENABLE - omv_profiler_set_event(buffer[0], buffer[1]); - #endif - cmd = USBDBG_NONE; - break; - } - - default: /* error */ - size = OMV_MIN(size, 512); - uint8_t byte_buffer[size]; - read_callback(&byte_buffer, size); - break; - } -} - -void usbdbg_control(void *buffer, uint8_t request, uint32_t size) { - cmd = (enum usbdbg_cmd) request; - switch (cmd) { - case USBDBG_FW_VERSION: - case USBDBG_FRAME_SIZE: - case USBDBG_FRAME_DUMP: - case USBDBG_ARCH_STR: - case USBDBG_FB_ENABLE: - case USBDBG_TX_BUF: - case USBDBG_TX_BUF_LEN: - case USBDBG_SENSOR_ID: - case USBDBG_GET_STATE: - case USBDBG_PROFILE_SIZE: - case USBDBG_PROFILE_DUMP: - case USBDBG_PROFILE_MODE: - case USBDBG_PROFILE_EVENT: - case USBDBG_SCRIPT_EXEC: - xfer_offs = 0; - xfer_size = size; - break; - - case USBDBG_PROFILE_RESET: { - #if OMV_PROFILER_ENABLE - omv_profiler_reset(); - #endif - cmd = USBDBG_NONE; - break; - } - - case USBDBG_SCRIPT_RUNNING: - vstr_reset(&script_buf); - xfer_offs = 0; - xfer_size = size; - break; - - case USBDBG_SCRIPT_STOP: - if (script_running) { - // Reset CDC buffers. - usb_cdc_reset_buffers(); - - // Schedule the IDE exception to interrupt the VM. - usbdbg_interrupt_vm(false); - } - cmd = USBDBG_NONE; - break; - - case USBDBG_SYS_RESET: - #if defined(OMV_BOARD_RESET) - OMV_BOARD_RESET(); - #else - NVIC_SystemReset(); - #endif - break; - - case USBDBG_SYS_RESET_TO_BL: { - #if defined(MICROPY_BOARD_ENTER_BOOTLOADER) - MICROPY_BOARD_ENTER_BOOTLOADER(0, 0); - #else - NVIC_SystemReset(); - #endif - break; - } - - default: /* error */ - cmd = USBDBG_NONE; - break; - } -} diff --git a/common/usbdbg.h b/common/usbdbg.h deleted file mode 100644 index 4a0895ffa..000000000 --- a/common/usbdbg.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SPDX-License-Identifier: MIT - * - * Copyright (C) 2013-2024 OpenMV, LLC. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * USB debug support. - */ -#ifndef __USBDBG_H__ -#define __USBDBG_H__ - -/** - * Firmware version (major, minor and patch numbers). - */ -#define FIRMWARE_VERSION_MAJOR (4) -#define FIRMWARE_VERSION_MINOR (7) -#define FIRMWARE_VERSION_PATCH (0) - -#define USBDBG_BAUDRATE_SLOW (921600) -#define USBDBG_BAUDRATE_FAST (12000000) -#define USBDBG_HEADER_SIZE (6) - -/** - * To add a new debugging command, increment the last command value used. - * Set the MSB of the value if the request has a device-to-host data phase. - * Add the command to usr/openmv.py using the same value. - * Handle the command control and data in/out (if any) phases in usbdbg.c. - * - * See usbdbg.c for examples. - */ -enum usbdbg_cmd { - USBDBG_NONE = 0x00, - USBDBG_FW_VERSION = 0x80, - USBDBG_FRAME_SIZE = 0x81, - USBDBG_FRAME_DUMP = 0x82, - USBDBG_ARCH_STR = 0x83, - USBDBG_SCRIPT_EXEC = 0x05, - USBDBG_SCRIPT_STOP = 0x06, - USBDBG_SCRIPT_RUNNING = 0x87, - USBDBG_SYS_RESET = 0x0C, - USBDBG_SYS_RESET_TO_BL = 0x0E, - USBDBG_FB_ENABLE = 0x0D, - USBDBG_TX_BUF_LEN = 0x8E, - USBDBG_TX_BUF = 0x8F, - USBDBG_SENSOR_ID = 0x90, - USBDBG_GET_STATE = 0x93, - USBDBG_PROFILE_SIZE = 0x94, - USBDBG_PROFILE_DUMP = 0x95, - USBDBG_PROFILE_MODE = 0x16, - USBDBG_PROFILE_EVENT = 0x17, - USBDBG_PROFILE_RESET = 0x18, -}; - -typedef enum { - USBDBG_FLAG_SCRIPT_RUNNING = (1 << 0), - USBDBG_FLAG_TEXTBUF_NOTEMPTY = (1 << 1), - USBDBG_FLAG_FRAMEBUF_LOCKED = (1 << 2), - USBDBG_FLAG_PROFILE_ENABLED = (1 << 3), - USBDBG_FLAG_PROFILE_HAS_PMU = (1 << 4), -} usbdbg_flags_t; - -typedef uint32_t (*usbdbg_read_callback_t) (void *buf, uint32_t len); -typedef uint32_t (*usbdbg_write_callback_t) (const void *buf, uint32_t len); - -void usbdbg_init(); -void usbdbg_wait_for_command(uint32_t timeout); -bool usbdbg_script_ready(); -vstr_t *usbdbg_get_script(); -bool usbdbg_is_busy(); -bool usbdbg_get_irq_enabled(); -void usbdbg_set_irq_enabled(bool enabled); -void usbdbg_set_script_running(bool running); -void usbdbg_data_in(uint32_t size, usbdbg_write_callback_t write_callback); -void usbdbg_data_out(uint32_t size, usbdbg_read_callback_t read_callback); -void usbdbg_control(void *buffer, uint8_t brequest, uint32_t wlength); - -#endif /* __USBDBG_H__ */ diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 000000000..65a424e24 --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,570 @@ +# OpenMV Protocol Specification + +## 1. Overview + +The OpenMV Protocol is a channel-based communication protocol for OpenMV camera devices. It provides reliable command/response communication with error detection, packet sequencing, and optimized data channel operations. + +### 1.1 Key Features +- Channel-based architecture with 8-bit channel IDs (up to 32 channels) +- Transport-agnostic design (USB, UART, TCP/IP) +- Dual CRC-16-CCITT error detection (separate header and payload CRCs) +- Packet fragmentation for large transfers +- Zero-copy data transmission with readp() interface +- Channel-specific operations (read, write, ioctl, lock/unlock, shape) +- Configurable CRC and sequence number validation +- Configurable async event notifications +- Real-time stream processing with ring buffer +- Default configuration optimized for USB transport with CRC disabled + +### 1.2 Channel Architecture + +The protocol uses a channel-based model where every readable/writable resource is abstracted as a data channel (such as stdout, stdin, the frame buffer, etc.). Channels have flags that indicate if they're read-only, write-only, or read/write. The protocol provides these predefined channels, and it's possible to extend it with user-defined channels from Python. Each channel implements a standard interface with init, read, write, flush, ioctl, available, and locking operations as needed. + +## 2. Packet Structure + +### 2.1 Packet Format + +All packets follow this structure: + +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0 | SYNC | 2 bytes | Pattern | Synchronization pattern (0xAA55) | +| 2 | SEQ | 1 byte | Number | Sequence number (0-255), wraps | +| 3 | CHAN | 1 byte | ID | Channel ID for routing data ops | +| 4 | FLAGS | 1 byte | Flags | Packet flags (see section 2.2) | +| 5 | OPCODE | 1 byte | Code | Command/Response operation code | +| 6 | LENGTH | 2 bytes | Length | Length of data field only | +| 8 | CRC | 2 bytes | Checksum | CRC-16-CCITT over header fields | +| 10 | DATA | Variable | Payload | Optional payload data | +| 10+N | D_CRC | 2 bytes | Checksum | CRC-16-CCITT over data (if N > 0) | + +Total header size: 10 bytes (including header CRC) +Data CRC: 2 bytes (only present if LENGTH > 0) + +### 2.2 Flags Field + +``` +Bit 7 6 5 4 3 2 1 0 + │ │ │ │ │ │ │ └─ ACK: Acknowledgment packet + │ │ │ │ │ │ └───── NAK: Negative acknowledgment packet + │ │ │ │ │ └───────── FRAGMENT: More fragments follow + │ │ │ │ └───────────── EVENT: Event packet + │ │ │ └───────────────── Reserved + │ │ └───────────────────── Reserved + │ └───────────────────────── Reserved + └───────────────────────────── Reserved +``` + +- **ACK (bit 0)**: Set for acknowledgment packets +- **NAK (bit 1)**: Set for negative acknowledgment packets +- **FRAGMENT (bit 2)**: Set when more fragments follow, clear for last fragment +- **EVENT (bit 3)**: Set for event packets + +## 3. Protocol Operations + +### 3.1 Sequence Numbers + +- Each endpoint maintains separate sequence counters for TX and RX +- Sequence numbers increment for each new packet (not retransmissions) +- Used to detect duplicates and missing packets +- Wraps from 255 to 0 (8-bit counter) + +### 3.2 Acknowledgment Mechanism + +All response packets automatically include ACK flag unless NAK is explicitly set: +1. ACK packets indicate successful processing +2. NAK packets include status code indicating the error +3. Sequence numbers increment with each packet sent + +### 3.3 Fragmentation + +The protocol supports fragmentation of large data packets. For data larger than maximum payload size, the fragmentation flag will be set to indicate more fragments to follow, and cleared on the last fragment. + +## 4. Command Set + +### 4.1 Command Categories + +Commands are organized by functional area: + +| Range | Category | Description | +|-------|----------|-------------| +| 0x00-0x0F | Protocol Control | Sync, capabilities | +| 0x10-0x1F | System Control | Reset, boot, system info, events | +| 0x20-0x2F | Channel Operations | Channel list, poll, lock, size, read, write, ioctl | +| 0x30-0xFF | Reserved/Extensions | Future use | + +### 4.2 Command Summary + +#### Protocol Control (0x00-0x0F) +| Opcode | Command | Payload Size | Response Size | Description | +|--------|---------|-------------|---------------|-------------| +| 0x00 | PROTO_SYNC | 0 bytes | 2 bytes (status) | Synchronization request | +| 0x01 | PROTO_GET_CAPS | 0 bytes | 32 bytes | Get protocol capabilities | +| 0x02 | PROTO_SET_CAPS | 32 bytes | 32 bytes | Set protocol capabilities | +| 0x03 | PROTO_STATS | 0 bytes | 32 bytes | Get protocol statistics | + +#### System Control (0x10-0x1F) +| Opcode | Command | Payload Size | Response Size | Description | +|--------|---------|-------------|---------------|-------------| +| 0x10 | SYS_RESET | 0 bytes | No response | System reset | +| 0x11 | SYS_BOOT | 0 bytes | No response | Jump to bootloader | +| 0x12 | SYS_INFO | 0 bytes | 64 bytes | Get system information | +| 0x13 | SYS_EVENT | 4 bytes | No response | System event notification | + +#### Channel Operations (0x20-0x2F) +| Opcode | Command | Payload Size | Response Size | Description | +|--------|---------|-------------|---------------|-------------| +| 0x20 | CHANNEL_LIST | 0 bytes | Variable (16 bytes × channel count) | List registered channels | +| 0x21 | CHANNEL_POLL | 0 bytes | 4 bytes (flags) | Poll channels status | +| 0x22 | CHANNEL_LOCK | 0 bytes | 2 bytes (status) | Lock channel for exclusive access | +| 0x23 | CHANNEL_UNLOCK | 0 bytes | 2 bytes (status) | Unlock channel | +| 0x24 | CHANNEL_SHAPE | 0 bytes | Variable | Get data shape/dimensions | +| 0x25 | CHANNEL_SIZE | 0 bytes | 4 bytes | Get available data size | +| 0x26 | CHANNEL_READ | 8 bytes (offset + length) | Variable (requested data) | Read data from channel | +| 0x27 | CHANNEL_WRITE | 8+ bytes (offset + length + data) | 2 bytes (status) | Write data to channel | +| 0x28 | CHANNEL_IOCTL | 4+ bytes (request + data) | 2 bytes (status) | Channel-specific control operation | +| 0x29 | CHANNEL_EVENT | 4+ bytes (event + data) | No response | Channel-specific event notification | + +### 4.3 Detailed Command Formats + +#### Protocol Control Commands + +**PROTO_SYNC (0x00)** + +Request Format: (No payload) + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-1 | status | 2 bytes | Code | Status code (0x00 = success) | + + + +**PROTO_GET_CAPS (0x01)** + +Request Format: (No payload) + +Response Format: (16 bytes) +| Offset | Field | Size | Type | Description | +|--------|---------------|----------|----------------------|-----------------------------------| +| 0-3 | flags | 4 bytes | Bitfield | Capability flags (bits 0-3) | +| | | | | bit 0: CRC enabled | +| | | | | bit 1: Sequence enabled | +| | | | | bit 2: ACK enabled | +| | | | | bit 3: Events enabled | +| 4-5 | max_payload | 2 bytes | Size | Maximum payload size | +| 6-15 | reserved | 10 bytes | Padding | Reserved for future use | + + + +**PROTO_SET_CAPS (0x02)** + +Request Format: (16 bytes) +| Offset | Field | Size | Type | Description | +|--------|---------------|----------|----------------------|-----------------------------------| +| 0-3 | flags | 4 bytes | Bitfield | Capability flags to set | +| 4-5 | max_payload | 2 bytes | Size | Maximum payload size | +| 6-15 | reserved | 10 bytes | Padding | Reserved for future use | + +Response Format: (Same as request - echoes back the set values) + + + +**PROTO_STATS (0x03)** + +Request Format: (No payload) + +Response Format: (32 bytes) +| Offset | Field | Size | Type | Description | +|--------|----------------------|----------|----------------------|-----------------------------------| +| 0-3 | sent_packets | 4 bytes | Count | Number of packets sent | +| 4-7 | recv_packets | 4 bytes | Count | Number of packets received | +| 8-11 | checksum_errors | 4 bytes | Count | Number of CRC errors detected | +| 12-15 | sequence_errors | 4 bytes | Count | Number of sequence errors | +| 16-19 | retransmit | 4 bytes | Count | Number of packet retransmissions | +| 20-23 | transport_errors | 4 bytes | Count | Number of transport layer errors | +| 24-27 | sent_events | 4 bytes | Count | Number of events sent | +| 28-31 | max_ack_queue_depth | 4 bytes | Count | Maximum ACK queue depth reached | + + + +#### System Control Commands + +**SYS_RESET (0x10)** + +Request Format: (No payload) +Response: (No response - system resets immediately) + +**SYS_BOOT (0x11)** + +Request Format: (No payload) +Response: (No response - system enters bootloader) + +**SYS_EVENT (0x13)** + +Request Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-3 | event | 4 bytes | Event code | Event type and data | + +Response: (No response - events are notifications) + +Event types: +- 0x00: CHANNEL_REGISTERED - Channel dynamically registered +- 0x01: CHANNEL_UNREGISTERED - Channel unregistered +- 0x02: SOFT_REBOOT - System is going for a soft-reboot + +Note: Events are only sent when the events_enabled capability is set to true. + +**SYS_INFO (0x12)** + +Request Format: (No payload) + +Response Format: (80 bytes) +| Offset | Field | Size | Type | Description | +|--------|---------------------|----------|--------------|-----------------------------------| +| 0-3 | cpu_id | 4 bytes | Register | ARM CPUID register value | +| 4-15 | dev_id | 12 bytes | ID Array | Unique device ID (3×uint32) | +| 16-27 | chip_id | 12 bytes | ID Array | Camera sensor chip ID (3×uint32, supports multiple sensors) | +| 28-35 | id_reserved | 8 bytes | Padding | Reserved for future expansion | +| 36-43 | hw_caps | 8 bytes | Bitfield | Hardware capability flags (see omv_protocol_hw_caps.h): | +| | | | | bit 0: GPU, | +| | | | | bit 1: NPU, | +| | | | | bit 2: ISP, | +| | | | | bit 3: Video encoder, | +| | | | | bit 4: JPEG encoder, | +| | | | | bit 5: DRAM, | +| | | | | bit 6: Hardware CRC, | +| | | | | bit 7: PMU, | +| | | | | bits 8-15: PMU event count, | +| | | | | bit 16: WiFi, | +| | | | | bit 17: Bluetooth, | +| | | | | bit 18: SD card, | +| | | | | bit 19: Ethernet, | +| | | | | bit 20: USB High-Speed, | +| | | | | bit 21: Multi-core | +| 44-47 | flash_size_kb | 4 bytes | Size | Flash memory size (KB) - currently 0 | +| 48-51 | ram_size_kb | 4 bytes | Size | RAM size (KB) - currently 0 | +| 52-55 | framebuffer_size_kb | 4 bytes | Size | Main framebuffer size (KB) | +| 56-59 | stream_buffer_size_kb| 4 bytes | Size | Stream framebuffer size (KB) | +| 60-67 | memory_reserved | 8 bytes | Padding | Reserved for future expansion | +| 68-70 | firmware_version | 3 bytes | Version | Firmware version (4.7.0) | +| 71-73 | protocol_version | 3 bytes | Version | Protocol version (1.0.0) | +| 74-76 | bootloader_version | 3 bytes | Version | Bootloader version | +| 77-79 | reserved | 3 bytes | Padding | Padding to 80 bytes | + +#### Channel Operations Commands + +**CHANNEL_LIST (0x20)** + +Request Format: (No payload) + +Response Format: (Variable length - 16 bytes per channel) +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| N×0 | id | 1 byte | ID | Channel ID | +| N×1 | flags | 1 byte | Bitfield | Channel capability flags | +| N×2-15 | name | 14 bytes | String | Channel name (null-terminated) | + +(Repeat for each registered channel) + +**CHANNEL_POLL (0x21)** + +Request Format: (No payload) + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-3 | flags | 4 bytes | Bitfield | Channel status flags | + +**CHANNEL_LOCK (0x22) / CHANNEL_UNLOCK (0x23)** + +Request Format: (No payload) + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-1 | status | 2 bytes | Code | Status code | + +**CHANNEL_SIZE (0x25)** + +Request Format: (No payload) + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-3 | size | 4 bytes | Count | Available bytes in channel | + +**CHANNEL_READ (0x26)** + +Request Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-3 | offset | 4 bytes | Position | Starting position to read | +| 4-7 | length | 4 bytes | Count | Number of bytes to read | + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-N | data | N bytes | Payload | Requested channel data | + +**CHANNEL_WRITE (0x27)** + +Request Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-3 | offset | 4 bytes | Position | Starting position to write | +| 4-7 | length | 4 bytes | Count | Number of bytes to write | +| 8-N | data | N bytes | Payload | Data to write to channel | + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-1 | status | 2 bytes | Code | Status code | + +**CHANNEL_IOCTL (0x28)** + +Request Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-3 | request | 4 bytes | Code | IOCTL request code | +| 4-N | data | N bytes | Payload | Request-specific data | + +Response Format: +| Offset | Field | Size | Type | Description | +|--------|----------|----------|-------------------------|-----------------------------------| +| 0-1 | status | 2 bytes | Code | Status code | + +**CHANNEL_SHAPE (0x24)** + +Request Format: (No payload) + +Response Format: (Variable length - array of size_t values) +| Offset | Field | Size | Type | Description | +|--------|------------|----------|-----------------------|-----------------------------------| +| 0-3 | dimension0 | 4 bytes | size_t | First dimension (e.g., count) | +| 4-7 | dimension1 | 4 bytes | size_t | Second dimension (e.g., size) | +| ... | ... | 4 bytes | size_t | Additional dimensions (if any) | + +The response contains 1-4 size_t values depending on the channel type: +- Profile channel: 2 values (record_count, record_size) +- Stream channel: 1 value (total_size) for compressed, 3 values (width, height, bpp) for uncompressed +- Other channels: Channel-specific format + + +### 4.4 Async Events + +The protocol supports asynchronous event notifications that can be sent from device to host at any time when the `events_enabled` capability is set to true. There are two types of events: + +1. **System Events**: Use the SYS_EVENT opcode (0x13) on channel 0 and are marked with the EVENT flag (bit 3) +2. **Channel Events**: Use the CHANNEL_EVENT opcode (0x29) on the specific channel that generates the event + +Events do not require acknowledgment and do not affect sequence numbering. The device will only send events if: +1. The protocol transport is active +2. The ACK queue is not full +3. The events_enabled capability is set to true + +**System Events (SYS_EVENT - 0x13):** +- **CHANNEL_REGISTERED (0x00)**: Sent when a dynamic channel is registered +- **CHANNEL_UNREGISTERED (0x01)**: Sent when a dynamic channel is removed +- **SOFT_REBOOT (0x02)**: Sent when system is going for a soft-reboot + +**Channel Events (CHANNEL_EVENT - 0x29):** +Channel events are sent on the specific channel that generates the event and can include channel-specific event data in the payload. + +### 4.5 Status Codes + +Protocol status responses use these codes: + +| Code | Name | Value | Type | Description | +|------|----------|----------|---------------------------|-----------------------------------| +| 0x00 | SUCCESS | 0 | Status | Operation completed successfully | +| 0x01 | FAILED | 1 | Error | Command failed | +| 0x02 | INVALID | 2 | Error | Invalid command/argument | +| 0x03 | TIMEOUT | 3 | Error | Operation timeout | +| 0x04 | BUSY | 4 | Status | Device busy | +| 0x05 | CHECKSUM | 5 | Error | CRC error | +| 0x06 | SEQUENCE | 6 | Error | Sequence error | +| 0x07 | OVERFLOW | 7 | Error | Buffer overflow | +| 0x08 | FRAGMENT | 8 | Error | Fragmentation error | +| 0x09 | UNKNOWN | 9 | Error | Unknown error | + +## 5. Channel System + +The OpenMV Protocol uses a channel-based architecture where each channel represents a specialized data pipeline. Channels are identified by numeric IDs and provide a uniform interface for accessing different types of data and functionality. + +### 5.1 Channel Types + +**Channel 0 (transport)**: The physical transport layer (USB, UART, TCP/IP) that handles low-level data transmission. Not directly accessible via protocol commands. + +**Channel 1 (stdin)**: Script input channel (write-only) for sending Python code to the device for execution. + +**Channel 2 (stdout)**: Text output channel (read-only) for receiving console output and script results from the device. + +**Channel 3 (stream)**: Read-only channel for high-bandwidth data like image frames. Supports exclusive locking to prevent concurrent access conflicts. + +**Channel 4 (profile)**: Optional read-only channel providing performance metrics and diagnostic information. Profiling availability is determined by the presence of this channel rather than a capability flag. The channel is only registered when OMV_PROFILER_ENABLE is defined at compile time. The number of PMU event counters is embedded in the hardware capabilities bitfield (bits 7-12) in the system information. + +### 5.2 Channel Capabilities + +Each channel declares its supported operations through capability flags: +- **READ**: Channel supports read operations +- **WRITE**: Channel supports write operations +- **LOCK**: Channel supports exclusive locking +- **DYNAMIC**: Channel was dynamically created +- **PHYSICAL**: Transport-only channel (not accessible via protocol) + +### 5.3 Channel Operations + +**Discovery**: The CHANNEL_LIST command returns available channels with their IDs, capabilities, and names. + +**Data Access**: Read operations specify offset and length parameters. The offset meaning depends on the channel - it might be a byte position, frame number, or other logical addressing scheme. + +**Zero-Copy Reads**: The readp operation returns direct pointers to channel data, eliminating copy overhead for large transfers. + +**Control**: The ioctl operation handles channel-specific commands like configuration changes or status queries. + +**Flow Control**: Channels return appropriate status codes when data isn't ready, and use locking mechanisms to coordinate access between host and device. + +## 6. Error Handling + +### 6.1 CRC Verification +- CRC-16-CCITT polynomial (0x1021) with initial value 0xFFFF +- Header CRC: calculated over header fields (excluding CRC field itself) +- Data CRC: calculated over payload data (only if payload length > 0) +- Hardware acceleration available on STM32 and Alif platforms + +### 6.2 Stream Recovery +- On sync loss, scan for SYNC pattern (0xAA55) +- Validate packet header after SYNC +- Verify CRC before processing + +### 6.3 Sequence Numbers +- Increment after successful packet transmission +- Reset to 0 on PROTO_SYNC command +- Used for duplicate detection + +## 7. Protocol State Machine + +The state machine is driven by the transport layer when data is available: + +``` +┌──────┐ +│ IDLE │◄──────────────────────┐ +└──┬───┘ │ + │ Data available │ + ▼ │ +┌──────┐ │ +│ SYNC │───── Timeout ─────────┤ +└──┬───┘ │ + │ SYNC found │ + ▼ │ +┌────────┐ │ +│ HEADER │──── Invalid ────────┤ +└──┬─────┘ │ + │ Valid header │ + ▼ │ +┌──────┐ │ +│ DATA │───── Timeout ─────────┤ +└──┬───┘ │ + │ Data complete │ + ▼ │ +┌─────┐ │ +│ CRC │────── Invalid ─────────┤ +└──┬──┘ │ + │ Valid CRC │ + ▼ │ +┌─────────┐ │ +│ PROCESS │────────────────────┘ +└─────────┘ +``` + +## 8. Implementation Notes + +### 8.1 Zero-Copy Optimization +- readp() interface allows direct pointer access to data without copying +- Header, payload, and data CRC sent as separate transport writes +- Incremental CRC calculation supports non-contiguous data + +### 8.2 Buffer Requirements +- Maximum buffer size: 4096 bytes (configurable via OMV_PROTOCOL_MAX_BUFFER_SIZE) +- Maximum payload: 4086 bytes (4096 - 10 header - 2 data CRC) +- Minimum payload: 52 bytes (64 - 10 header - 2 data CRC) +- Ring buffer implementation for received data with double buffering (8192 bytes total) + +### 8.3 Channel Registration +- Maximum 32 channels supported (indices 0-31) +- Channels registered at protocol initialization +- Channel 0 is transport layer (not accessible via protocol) +- Protocol responses use channel 0 in packet header but are routed through transport + +## 9. Example Communication Flow + +### 9.1 Channel Read Operation +``` +Host Device + │ │ + ├─── CHANNEL_READ(CHAN=2) ────►│ // Read frame data + │ offset=0, length=1024 │ + │ │ + │◄─ Response(FRAGMENTED) ──────┤ // More fragments follow + │ │ + │◄─ Response(no flags) ────────┤ // Final fragment + │ │ +``` + +### 9.2 Channel Discovery +``` +Host Device + │ │ + ├─── CHANNEL_LIST ────────────►│ + │ │ + │◄─ Channel entries ───────────┤ // Array of channel info + │ │ + ├─── CHANNEL_SIZE(CHAN=2) ────►│ + │ │ + │◄─ Size response ─────────────┤ // Available bytes + │ │ +``` + +## 10. Protocol Configuration + +### 10.1 Protocol Capabilities + +The protocol supports negotiation of these capabilities between host and device: + +- **crc_enabled**: Enable/disable CRC error checking +- **seq_enabled**: Enable/disable sequence number validation +- **ack_enabled**: Enable/disable ACK packet requirement +- **events_enabled**: Enable/disable async event notifications +- **max_payload**: Maximum payload size supported + +### 10.2 Default Capabilities + +The protocol uses optimized defaults for USB transport: + +- **CRC enabled**: false (optimized for USB reliability) +- **Sequence validation**: false (optimized for USB in-order delivery) +- **ACK enabled**: false +- **Events enabled**: true +- **Max payload**: 4086 bytes (4096 - 10 header - 2 data CRC) +- **Soft reboot**: true +- **RTX retries**: 3 +- **RTX timeout**: 3000ms +- **Lock interval**: 10ms + +Fragmentation is always supported and handled automatically by the protocol when data exceeds the maximum payload size. + +### 10.3 Capability Negotiation + +The `PROTO_GET_CAPS` and `PROTO_SET_CAPS` commands allow dynamic configuration. See the detailed command formats above for the exact 32-byte payload structure. + +## 11. System Information + +The `SYS_INFO` command returns comprehensive system information in a 64-byte response. See the detailed command format above for the complete field layout including hardware identification, capabilities, memory information, and version data. + +## 12. Version History + +| Version | Date | Status | Type | Description | +|---------|----------|----------|-------------------------|----------------------------------------| +| 1.0.0 | 2025 | Current | Specification | OpenMV Protocol specification | diff --git a/lib/imlib/framebuffer.c b/lib/imlib/framebuffer.c index f6c022a98..5a69f3533 100644 --- a/lib/imlib/framebuffer.c +++ b/lib/imlib/framebuffer.c @@ -29,6 +29,7 @@ #include "fmath.h" #include "framebuffer.h" #include "omv_boardconfig.h" +#include "omv_protocol.h" // Main framebuffer memory extern char _fb_memory_start; @@ -45,11 +46,11 @@ void framebuffer_init0() { // Reuse the last enabled flag after resetting the state. bool enabled = framebuffer_get(FB_STREAM_ID)->enabled; - // Initialize the main framebuffer. + // Initialize the main framebuffer. framebuffer_init(framebuffer_get(FB_MAINFB_ID), &_fb_memory_start, &_fb_memory_end - &_fb_memory_start, false, true); - // Initialize the streaming buffer. + // Initialize the streaming buffer. framebuffer_init(framebuffer_get(FB_STREAM_ID), &_sb_memory_start, &_sb_memory_end - &_sb_memory_start, false, enabled); @@ -85,10 +86,10 @@ void framebuffer_to_image(framebuffer_t *fb, image_t *img) { img->h = fb->h; img->size = fb->size; img->pixfmt = fb->pixfmt; - + // For streaming buffers (no queues), use raw_base directly if (fb->used_queue == NULL) { - img->pixels = (uint8_t *)fb->raw_base; + img->pixels = (uint8_t *) fb->raw_base; } else { vbuffer_t *buffer = framebuffer_acquire(fb, FB_FLAG_USED | FB_FLAG_PEEK); img->pixels = (buffer == NULL) ? NULL : buffer->data; @@ -144,7 +145,7 @@ void framebuffer_flush(framebuffer_t *fb) { queue_flush(fb->used_queue); } - for (size_t i=0; ibuf_count; i++) { + for (size_t i = 0; i < fb->buf_count; i++) { vbuffer_t *buffer = framebuffer_pool_get(fb, i); // Reset the buffer's state. @@ -275,8 +276,8 @@ void framebuffer_update_preview(image_t *src) { } // Reserve space for header at the beginning - framebuffer_header_t *header = (framebuffer_header_t *)fb->raw_base; - uint8_t *frame_data = (uint8_t *)fb->raw_base + sizeof(framebuffer_header_t); + framebuffer_header_t *header = (framebuffer_header_t *) fb->raw_base; + uint8_t *frame_data = (uint8_t *) fb->raw_base + sizeof(framebuffer_header_t); size_t available_size = fb->raw_size - sizeof(framebuffer_header_t); if (src->is_compressed) { @@ -354,12 +355,14 @@ void framebuffer_update_preview(image_t *src) { framebuffer_from_image(fb, &dst); } + omv_protocol_send_event(OMV_PROTOCOL_CHANNEL_ID_STREAM, OMV_PROTOCOL_EVENT_NOTIFY, false); + exit_cleanup: header->width = fb->w; header->height = fb->h; header->pixfmt = fb->pixfmt; header->size = fb->is_compressed ? fb->size : fb->bpp; - + // Unlock the streaming buffer. mutex_unlock(&fb->lock, MUTEX_TID_OMV); } diff --git a/lib/micropython b/lib/micropython index b7efb956c..f5ece1cdf 160000 --- a/lib/micropython +++ b/lib/micropython @@ -1 +1 @@ -Subproject commit b7efb956cfb21afda9b71162eeefec8c31d3dff9 +Subproject commit f5ece1cdff2c4da3078e25cdff15594226f652e6 diff --git a/modules/py_omv.c b/modules/py_omv.c index baef18d2d..d28561d1d 100644 --- a/modules/py_omv.c +++ b/modules/py_omv.c @@ -27,17 +27,16 @@ #include #include #include "py/obj.h" -#include "usbdbg.h" #include "framebuffer.h" #include "omv_boardconfig.h" -#include "tinyusb_debug.h" +#include "omv_protocol.h" static mp_obj_t py_omv_version_string() { char str[12]; snprintf(str, 12, "%d.%d.%d", - FIRMWARE_VERSION_MAJOR, - FIRMWARE_VERSION_MINOR, - FIRMWARE_VERSION_PATCH); + OMV_FIRMWARE_VERSION_MAJOR, + OMV_FIRMWARE_VERSION_MINOR, + OMV_FIRMWARE_VERSION_PATCH); return mp_obj_new_str(str, strlen(str)); } static MP_DEFINE_CONST_FUN_OBJ_0(py_omv_version_string_obj, py_omv_version_string); @@ -69,22 +68,15 @@ static mp_obj_t py_omv_board_id() { static MP_DEFINE_CONST_FUN_OBJ_0(py_omv_board_id_obj, py_omv_board_id); static mp_obj_t py_omv_debug_mode() { - #if OMV_TUSBDBG_ENABLE - return mp_obj_new_bool(tinyusb_debug_enabled()); - #elif MICROPY_HW_ENABLE_USB - extern int usb_cdc_debug_mode_enabled(); - return mp_obj_new_bool(usb_cdc_debug_mode_enabled()); - #else - return mp_const_none; - #endif + return mp_obj_new_bool(omv_protocol_is_active()); } static MP_DEFINE_CONST_FUN_OBJ_0(py_omv_debug_mode_obj, py_omv_debug_mode); static const mp_rom_map_elem_t globals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_omv) }, - { MP_ROM_QSTR(MP_QSTR_version_major), MP_ROM_INT(FIRMWARE_VERSION_MAJOR) }, - { MP_ROM_QSTR(MP_QSTR_version_minor), MP_ROM_INT(FIRMWARE_VERSION_MINOR) }, - { MP_ROM_QSTR(MP_QSTR_version_patch), MP_ROM_INT(FIRMWARE_VERSION_PATCH) }, + { MP_ROM_QSTR(MP_QSTR_version_major), MP_ROM_INT(OMV_FIRMWARE_VERSION_MAJOR) }, + { MP_ROM_QSTR(MP_QSTR_version_minor), MP_ROM_INT(OMV_FIRMWARE_VERSION_MINOR) }, + { MP_ROM_QSTR(MP_QSTR_version_patch), MP_ROM_INT(OMV_FIRMWARE_VERSION_PATCH) }, { MP_ROM_QSTR(MP_QSTR_version_string), MP_ROM_PTR(&py_omv_version_string_obj) }, { MP_ROM_QSTR(MP_QSTR_arch), MP_ROM_PTR(&py_omv_arch_obj) }, { MP_ROM_QSTR(MP_QSTR_board_type), MP_ROM_PTR(&py_omv_board_type_obj) }, diff --git a/modules/py_protocol.c b/modules/py_protocol.c new file mode 100644 index 000000000..853e672f5 --- /dev/null +++ b/modules/py_protocol.c @@ -0,0 +1,495 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Protocol Python module - Provides bindings for OpenMV protocol system. + * Allows creating custom channels in Python that integrate with the C protocol. + */ +#include +#include +#include + +#include "py/obj.h" +#include "py/runtime.h" +#include "py/objstr.h" +#include "py/objint.h" + +#include "../protocol/omv_protocol.h" +#include "shared/runtime/softtimer.h" + +/*************************************************************************** +* Python Channel Delegates +***************************************************************************/ + +// Helper function to check if method exists on Python object +static bool py_channel_has_method(mp_obj_t obj, qstr method_name) { + mp_obj_t dest[2]; + mp_load_method_maybe(obj, method_name, dest); + return dest[0] != MP_OBJ_NULL; +} + +// Helper function to safely call Python method with exception handling +static mp_obj_t py_channel_call(mp_obj_t obj, qstr method_name, size_t n_args, const mp_obj_t *args) { + mp_obj_t dest[2 + n_args]; + mp_load_method_maybe(obj, method_name, dest); + + if (dest[0] == MP_OBJ_NULL) { + return MP_OBJ_NULL; // Method not found + } + + // Copy arguments to dest array after method and self + for (size_t i = 0; i < n_args; i++) { + dest[i + 2] = args[i]; + } + + // Call the method with exception handling + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t result = mp_call_method_n_kw(n_args, 0, dest); + nlr_pop(); + return result; + } else { + // Exception occurred + while (1) { + ; + } + return MP_OBJ_NULL; + } +} + +// Delegate function implementations +static int py_channel_init(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_init, 0, NULL); + return result == MP_OBJ_NULL ? -1 : 0; +} + +static bool py_channel_poll(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_poll, 0, NULL); + return result == MP_OBJ_NULL ? false : mp_obj_is_true(result); +} + +static int py_channel_lock(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_lock, 0, NULL); + return result == MP_OBJ_NULL ? -1 : (mp_obj_is_true(result) ? 0 : -1); +} + +static int py_channel_unlock(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_unlock, 0, NULL); + return result == MP_OBJ_NULL ? -1 : (mp_obj_is_true(result) ? 0 : -1); +} + +static size_t py_channel_size(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_size, 0, NULL); + return result == MP_OBJ_NULL ? 0 : mp_obj_get_int(result); +} + +static size_t py_channel_shape(const omv_protocol_channel_t *channel, size_t shape[4]) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_shape, 0, NULL); + + // Expect a tuple/list of 4 elements + if (result != MP_OBJ_NULL && mp_obj_is_type(result, &mp_type_tuple)) { + size_t len; + mp_obj_t *items; + mp_obj_get_array(result, &len, &items); + + for (size_t i = 0; i < 4 && i < len; i++) { + shape[i] = mp_obj_get_int(items[i]); + } + return len; + } + + return 0; +} + +static int py_channel_flush(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + mp_obj_t result = py_channel_call(obj, MP_QSTR_flush, 0, NULL); + return result == MP_OBJ_NULL ? -1 : 0; +} + +// Read delegate - calls Python read method and copies data to C buffer +static int py_channel_read(const omv_protocol_channel_t *channel, uint32_t offset, size_t size, void *data) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + + // Prepare arguments: offset, size + mp_obj_t args[2] = { + mp_obj_new_int(offset), + mp_obj_new_int(size) + }; + + // Call the Python read method + mp_obj_t result = py_channel_call(obj, MP_QSTR_read, 2, args); + + // Get buffer from Python result (should be bytes/bytearray) + mp_buffer_info_t bufinfo; + if (result != MP_OBJ_NULL && mp_get_buffer(result, &bufinfo, MP_BUFFER_READ)) { + size_t copy_size = (bufinfo.len < size) ? bufinfo.len : size; + memcpy(data, bufinfo.buf, copy_size); + return copy_size; // Return number of bytes read + } + + return -1; // Failed to get buffer +} + +// Write delegate - passes C buffer to Python as bytearray +static int py_channel_write(const omv_protocol_channel_t *channel, uint32_t offset, size_t size, const void *data) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + + // Prepare arguments: offset, data (as bytearray) + mp_obj_t args[2] = { + mp_obj_new_int(offset), + mp_obj_new_bytearray_by_ref(size, (void *) data) + }; + + // Call the Python write method + mp_obj_t result = py_channel_call(obj, MP_QSTR_write, 2, args); + + if (result == MP_OBJ_NULL) { + return -1; // Method not found or exception + } else if (mp_obj_is_int(result)) { + return mp_obj_get_int(result); + } + + return 0; // Default success +} + +// Readp delegate - calls Python readp method and returns pointer (dangerous but needed) +static const void *py_channel_readp(const omv_protocol_channel_t *channel, uint32_t offset, size_t size) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + + // Prepare arguments: offset, size + mp_obj_t args[2] = { + mp_obj_new_int(offset), + mp_obj_new_int(size) + }; + + // Call the Python readp method + mp_obj_t result = py_channel_call(obj, MP_QSTR_readp, 2, args); + + // Get buffer pointer directly from Python object + mp_buffer_info_t bufinfo; + if (result != MP_OBJ_NULL && mp_get_buffer(result, &bufinfo, MP_BUFFER_READ)) { + return bufinfo.buf; // Return direct pointer - DANGEROUS but needed for readp + } + + return NULL; +} + +// Ioctl delegate - passes command and data to Python +static int py_channel_ioctl(const omv_protocol_channel_t *channel, uint32_t cmd, size_t len, void *arg) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + + // Prepare arguments: command, length, arg (as bytearray if data present) + mp_obj_t args[3] = { + mp_obj_new_int(cmd), + mp_obj_new_int(len), + (arg == NULL || len == 0) ? mp_const_none : mp_obj_new_bytearray_by_ref(len, arg) + }; + + // Call the Python ioctl method + mp_obj_t result = py_channel_call(obj, MP_QSTR_ioctl, 3, args); + + if (result == MP_OBJ_NULL) { + return -1; // Method not found or exception + } else if (result == mp_const_none) { + return 0; // Success + } else if (mp_obj_is_int(result)) { + return mp_obj_get_int(result); + } + + return 0; // Default success +} + +// Active check delegate for transport channels +static bool py_channel_is_active(const omv_protocol_channel_t *channel) { + mp_obj_t obj = MP_OBJ_FROM_PTR(channel->priv); + + // Call the Python is_active method + mp_obj_t result = py_channel_call(obj, MP_QSTR_is_active, 0, NULL); + + return result != MP_OBJ_NULL && mp_obj_is_true(result); +} + + +/*************************************************************************** +* Protocol Channel Object +***************************************************************************/ + +// Protocol Channel handle object type +typedef struct _py_channel_obj_t { + mp_obj_base_t base; + omv_protocol_channel_t *channel; +} py_channel_obj_t; + +static mp_obj_t py_channel_deinit(mp_obj_t self_in) { + py_channel_obj_t *self = self_in; + (void) self; + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(py_channel_deinit_obj, py_channel_deinit); + +static void py_channel_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + py_channel_obj_t *self = self_in; + mp_printf(print, "", self->channel->name); +} + +static mp_obj_t py_channel_send_event(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + static const mp_arg_t allowed_args[] = { + { MP_QSTR_event, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_wait_ack, MP_ARG_BOOL, {.u_bool = false} }, + }; + + py_channel_obj_t *self = pos_args[0]; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + uint32_t event = args[0].u_int; + bool wait_ack = args[1].u_bool; + + if (omv_protocol_send_event(self->channel->id, event, wait_ack)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Failed to send channel event")); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(py_channel_send_event_obj, 2, py_channel_send_event); + +static const mp_rom_map_elem_t py_channel_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ProtocolChannel) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&py_channel_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_send_event), MP_ROM_PTR(&py_channel_send_event_obj) }, +}; +static MP_DEFINE_CONST_DICT(py_channel_locals_dict, py_channel_locals_dict_table); + +// Protocol Channel type definition +MP_DEFINE_CONST_OBJ_TYPE( + py_channel_type, + MP_QSTR_ProtocolChannel, + MP_TYPE_FLAG_NONE, + print, py_channel_print, + locals_dict, &py_channel_locals_dict + ); + +/*************************************************************************** +* Protocol Module +***************************************************************************/ + +static mp_obj_t py_protocol_poll(void); + +// Protocol task scheduling +static void py_protocol_task(mp_sched_node_t *node) { + omv_protocol_task(); +} + +static void py_protocol_schedule(void) { + static mp_sched_node_t protocol_task_node; + mp_sched_schedule_node(&protocol_task_node, py_protocol_task); +} + +// Soft timer callback to call py_protocol_poll +static void py_protocol_soft_timer_callback(soft_timer_entry_t *self) { + return py_protocol_schedule(); +} + +// Protocol active check +static mp_obj_t py_protocol_is_active(void) { + return mp_obj_new_bool(omv_protocol_is_active()); +} +static MP_DEFINE_CONST_FUN_OBJ_0(py_protocol_is_active_obj, py_protocol_is_active); + +// Schedule protocol task function +static mp_obj_t py_protocol_poll(void) { + py_protocol_schedule(); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_0(py_protocol_poll_obj, py_protocol_poll); + +// Protocol init function +static mp_obj_t py_protocol_init(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + static const mp_arg_t allowed_args[] = { + { MP_QSTR_crc, MP_ARG_BOOL, {.u_bool = true} }, + { MP_QSTR_seq, MP_ARG_BOOL, {.u_bool = true} }, + { MP_QSTR_ack, MP_ARG_BOOL, {.u_bool = false} }, + { MP_QSTR_events, MP_ARG_BOOL, {.u_bool = true} }, + { MP_QSTR_max_payload, MP_ARG_INT, {.u_int = OMV_PROTOCOL_MAX_PAYLOAD_SIZE} }, + { MP_QSTR_soft_reboot, MP_ARG_BOOL, {.u_bool = false} }, + { MP_QSTR_rtx_retries, MP_ARG_INT, {.u_int = OMV_PROTOCOL_DEF_RTX_RETRIES} }, + { MP_QSTR_rtx_timeout_ms, MP_ARG_INT, {.u_int = OMV_PROTOCOL_DEF_RTX_TIMEOUT_MS} }, + { MP_QSTR_lock_interval_ms, MP_ARG_INT, {.u_int = OMV_PROTOCOL_MIN_LOCK_INTERVAL_MS} }, + { MP_QSTR_timer_ms, MP_ARG_INT, {.u_int = 0} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Build configuration structure from arguments + omv_protocol_config_t config = { + .crc_enabled = args[0].u_bool, + .seq_enabled = args[1].u_bool, + .ack_enabled = args[2].u_bool, + .event_enabled = args[3].u_bool, + .max_payload = args[4].u_int, + .soft_reboot = args[5].u_bool, + .rtx_retries = args[6].u_int, + .rtx_timeout_ms = args[7].u_int, + .lock_intval_ms = args[8].u_int, + }; + + uint32_t timer_ms = args[9].u_int; + + if (omv_protocol_init(&config)) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Failed to initialize protocol")); + } + + // Register the default logical data channels + omv_protocol_register_channel(&omv_stdin_channel); + omv_protocol_register_channel(&omv_stdout_channel); + omv_protocol_register_channel(&omv_stream_channel); + + // Register the profiler channel (if enabled) + #if OMV_PROFILER_ENABLE + omv_protocol_register_channel(&omv_profile_channel); + #endif // OMV_PROFILER_ENABLE + + // Start periodic timer if timer_ms > 0 + if (timer_ms > 0) { + // Soft timer for periodic protocol task scheduling + static soft_timer_entry_t protocol_soft_timer; + + soft_timer_static_init(&protocol_soft_timer, SOFT_TIMER_MODE_PERIODIC, + timer_ms, py_protocol_soft_timer_callback); + soft_timer_insert(&protocol_soft_timer, timer_ms); + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(py_protocol_init_obj, 0, py_protocol_init); + +// Register channel function - dynamically allocates C channel for Python object +static mp_obj_t py_protocol_register(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + static const mp_arg_t allowed_args[] = { + { MP_QSTR_name, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_flags, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_backend, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_obj_t backend = args[2].u_obj; + const char *channel_name = mp_obj_str_get_str(args[0].u_obj); + + // Allocate C channel structure + omv_protocol_channel_t *channel = m_malloc0(sizeof(omv_protocol_channel_t)); + + // Initialize C channel + channel->priv = MP_OBJ_TO_PTR(backend); + channel->flags = args[1].u_int | OMV_PROTOCOL_CHANNEL_FLAG_DYNAMIC; + strncpy(channel->name, channel_name, OMV_PROTOCOL_CHANNEL_NAME_SIZE); + channel->name[OMV_PROTOCOL_CHANNEL_NAME_SIZE - 1] = '\0'; + + // Set delegate functions only if methods exist on the Python object + channel->init = py_channel_has_method(backend, MP_QSTR_init) ? py_channel_init : NULL; + channel->poll = py_channel_has_method(backend, MP_QSTR_poll) ? py_channel_poll : NULL; + channel->lock = py_channel_has_method(backend, MP_QSTR_lock) ? py_channel_lock : NULL; + channel->unlock = py_channel_has_method(backend, MP_QSTR_unlock) ? py_channel_unlock : NULL; + channel->size = py_channel_has_method(backend, MP_QSTR_size) ? py_channel_size : NULL; + channel->shape = py_channel_has_method(backend, MP_QSTR_shape) ? py_channel_shape : NULL; + channel->read = py_channel_has_method(backend, MP_QSTR_read) ? py_channel_read : NULL; + channel->write = py_channel_has_method(backend, MP_QSTR_write) ? py_channel_write : NULL; + channel->readp = py_channel_has_method(backend, MP_QSTR_readp) ? py_channel_readp : NULL; + channel->flush = py_channel_has_method(backend, MP_QSTR_flush) ? py_channel_flush : NULL; + channel->ioctl = py_channel_has_method(backend, MP_QSTR_ioctl) ? py_channel_ioctl : NULL; + channel->is_active = py_channel_has_method(backend, MP_QSTR_is_active) ? py_channel_is_active : NULL; + + // Automatically set flags based on available methods + if (channel->read || channel->readp) { + channel->flags |= OMV_PROTOCOL_CHANNEL_FLAG_READ; + } + if (channel->write) { + channel->flags |= OMV_PROTOCOL_CHANNEL_FLAG_WRITE; + } + if (channel->lock && channel->unlock) { + channel->flags |= OMV_PROTOCOL_CHANNEL_FLAG_LOCK; + } + + // Register with protocol system + if (omv_protocol_register_channel(channel) == -1) { + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Failed to register protocol channel")); + } + + // Create Python channel handle object + py_channel_obj_t *channel_obj = mp_obj_malloc_with_finaliser(py_channel_obj_t, &py_channel_type); + channel_obj->channel = channel; + + // Store dynamic channels in root pointers to prevent GC + if (OMV_PROTOCOL_CHANNEL_FLAG_GET(channel, DYNAMIC)) { + MP_STATE_PORT(protocol_channels)[channel->id] = MP_OBJ_FROM_PTR(channel_obj); + } + + return MP_OBJ_FROM_PTR(channel_obj); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(py_protocol_register_obj, 0, py_protocol_register); + +// Module globals table +static const mp_rom_map_elem_t protocol_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_protocol) }, + + // Protocol management functions + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&py_protocol_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_is_active), MP_ROM_PTR(&py_protocol_is_active_obj) }, + { MP_ROM_QSTR(MP_QSTR_poll), MP_ROM_PTR(&py_protocol_poll_obj) }, + { MP_ROM_QSTR(MP_QSTR_register), MP_ROM_PTR(&py_protocol_register_obj) }, + + // Channel flags constants + { MP_ROM_QSTR(MP_QSTR_CHANNEL_FLAG_READ), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_FLAG_READ) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_FLAG_WRITE), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_FLAG_WRITE) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_FLAG_LOCK), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_FLAG_LOCK) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_FLAG_PHYSICAL), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_FLAG_PHYSICAL) }, + + // Protocol channel IDs + { MP_ROM_QSTR(MP_QSTR_CHANNEL_ID_TRANSPORT), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_ID_TRANSPORT) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_ID_STDIN), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_ID_STDIN) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_ID_STDOUT), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_ID_STDOUT) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_ID_STREAM), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_ID_STREAM) }, + { MP_ROM_QSTR(MP_QSTR_CHANNEL_ID_PROFILE), MP_ROM_INT(OMV_PROTOCOL_CHANNEL_ID_PROFILE) }, +}; + +static MP_DEFINE_CONST_DICT(protocol_globals, protocol_globals_table); + +// Module object +const mp_obj_module_t protocol_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *) &protocol_globals, +}; + +// Register the module +MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_protocol, protocol_module); + +// Register root pointer for dynamic protocol channels +MP_REGISTER_ROOT_POINTER(mp_obj_t protocol_channels[OMV_PROTOCOL_MAX_CHANNELS]); diff --git a/ports/alif/alif.mk b/ports/alif/alif.mk index 8b9775885..241225a17 100644 --- a/ports/alif/alif.mk +++ b/ports/alif/alif.mk @@ -176,8 +176,8 @@ LDFLAGS = -mthumb \ ifeq ($(MCU_CORE),M55_HP) # Linker Flags -LDFLAGS += -Wl,--wrap=mp_usbd_task \ - -Wl,--wrap=tud_cdc_rx_cb \ +LDFLAGS += -Wl,--wrap=tud_cdc_rx_cb \ + -Wl,--wrap=tud_cdc_line_state_cb \ -Wl,--wrap=mp_hal_stdio_poll \ -Wl,--wrap=mp_hal_stdout_tx_strn endif @@ -186,6 +186,7 @@ endif include lib/cmsis/cmsis.mk include lib/alif/alif.mk include common/common.mk +include protocol/protocol.mk include drivers/drivers.mk include lib/imlib/imlib.mk include lib/tflm/tflm.mk diff --git a/ports/alif/main.c b/ports/alif/main.c index f976ee0b6..60e2f95f2 100644 --- a/ports/alif/main.c +++ b/ports/alif/main.c @@ -69,8 +69,6 @@ #include "omv_boardconfig.h" #include "framebuffer.h" -#include "usbdbg.h" -#include "tinyusb_debug.h" #include "fb_alloc.h" #include "file_utils.h" #include "mp_utils.h" @@ -81,6 +79,7 @@ #include "py_audio.h" #include "py_imu.h" #include "omv_gpio.h" +#include "omv_protocol.h" NORETURN void __fatal_error(const char *msg); extern void machine_pin_irq_deinit(void); @@ -125,9 +124,6 @@ soft_reset: #ifdef IMLIB_ENABLE_IMAGE_FILE_IO file_buffer_init0(); #endif - #if CORE_M55_HP - usbdbg_init(); - #endif #if MICROPY_PY_IMU py_imu_init(); #endif @@ -138,6 +134,11 @@ soft_reset: } #endif + #if CORE_M55_HP + // Initialize OpenMV protocol + omv_protocol_init_default(); + #endif + #if MICROPY_PY_LWIP // lwIP can only be initialized once, because the system timeout // list (next_timeout), is only ever reset by BSS clearing. @@ -168,17 +169,6 @@ soft_reset: mod_network_init(); #endif - // TODO: _boot on HP uses OSPI which is disabled right now. - #if MICROPY_HW_ENABLE_OSPI || CORE_M55_HE - // Execute _boot.py. - pyexec_frozen_module("_boot.py", false); - #endif - - #if MICROPY_HW_ENABLE_OSPI - // Mark the filesystem as an OpenMV storage. - file_ll_touch(".openmv_disk"); - #endif - // Initialize TinyUSB after the filesystem is mounted. #if MICROPY_HW_ENABLE_USBDEV if (!tusb_inited()) { @@ -186,26 +176,25 @@ soft_reset: } #endif - // Run boot.py script. - bool interrupted = mp_exec_bootscript("boot.py", true); + // Execute _boot.py. + pyexec_frozen_module("_boot.py", false); - // Run main.py script on first soft-reset. - if (first_soft_reset && !interrupted && mp_vfs_import_stat("main.py")) { - mp_exec_bootscript("main.py", true); - goto soft_reset_exit; + // Run boot.py every reset and main.py on first soft-reset + if (pyexec_file_if_exists("boot.py") && first_soft_reset) { + pyexec_file_if_exists("main.py"); } #if CORE_M55_HE goto soft_reset_exit; #endif - // If there's no script ready, just re-exec REPL - while (!usbdbg_script_ready()) { + while (!omv_protocol_exec_script()) { nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { - // enable IDE interrupt - usbdbg_set_irq_enabled(true); - // run REPL + // Enable Ctrl+C to interrupt script or REPL. + mp_hal_set_interrupt_char(CHAR_CTRL_C); + if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) { if (pyexec_raw_repl() != 0) { break; @@ -219,22 +208,10 @@ soft_reset: } } - if (usbdbg_script_ready()) { - nlr_buf_t nlr; - if (nlr_push(&nlr) == 0) { - // Enable IDE interrupt - usbdbg_set_irq_enabled(true); - // Execute the script. - pyexec_str(usbdbg_get_script(), true); - // Disable IDE interrupts - usbdbg_set_irq_enabled(false); - nlr_pop(); - } else { - mp_obj_print_exception(&mp_plat_print, (mp_obj_t) nlr.ret_val); - } - } - +#if CORE_M55_HE soft_reset_exit: +#endif + mp_hal_set_interrupt_char(-1); mp_printf(MP_PYTHON_PRINTER, "MPY: soft reboot\n"); #if MICROPY_PY_CSI omv_csi_abort_all(); diff --git a/ports/mimxrt/main.c b/ports/mimxrt/main.c index 0b92a3496..3a947fd9b 100644 --- a/ports/mimxrt/main.c +++ b/ports/mimxrt/main.c @@ -68,13 +68,12 @@ #include "omv_boardconfig.h" #include "framebuffer.h" #include "omv_csi.h" -#include "usbdbg.h" -#include "tinyusb_debug.h" #include "fb_alloc.h" #include "dma_alloc.h" #include "file_utils.h" #include "mp_utils.h" #include "mimxrt_hal.h" +#include "omv_protocol.h" int main(void) { bool first_soft_reset = true; @@ -110,7 +109,6 @@ soft_reset: #ifdef IMLIB_ENABLE_IMAGE_FILE_IO file_buffer_init0(); #endif - usbdbg_init(); machine_adc_init(); #if MICROPY_PY_MACHINE_SDCARD machine_sdcard_init0(); @@ -119,6 +117,7 @@ soft_reset: machine_i2s_init0(); #endif machine_rtc_start(); + omv_protocol_init_default(); #if MICROPY_PY_LWIP // lwIP can only be initialized once, because the system timeout @@ -173,22 +172,18 @@ soft_reset: tusb_init(); } - // Run boot.py script. - bool interrupted = mp_exec_bootscript("boot.py", true); - - // Run main.py script on first soft-reset. - if (first_soft_reset && !interrupted && mp_vfs_import_stat("main.py")) { - mp_exec_bootscript("main.py", true); - goto soft_reset_exit; + // Run boot.py every reset and main.py on first soft-reset + if (pyexec_file_if_exists("boot.py") && first_soft_reset) { + pyexec_file_if_exists("main.py"); } - // If there's no script ready, just re-exec REPL - while (!usbdbg_script_ready()) { + while (!omv_protocol_exec_script()) { nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { - // enable IDE interrupt - usbdbg_set_irq_enabled(true); - // run REPL + // Enable Ctrl+C to interrupt script or REPL. + mp_hal_set_interrupt_char(CHAR_CTRL_C); + if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) { if (pyexec_raw_repl() != 0) { break; @@ -202,22 +197,8 @@ soft_reset: } } - if (usbdbg_script_ready()) { - nlr_buf_t nlr; - if (nlr_push(&nlr) == 0) { - // Enable IDE interrupt - usbdbg_set_irq_enabled(true); - // Execute the script. - pyexec_str(usbdbg_get_script(), true); - // Disable IDE interrupts - usbdbg_set_irq_enabled(false); - nlr_pop(); - } else { - mp_obj_print_exception(&mp_plat_print, (mp_obj_t) nlr.ret_val); - } - } - -soft_reset_exit: + // soft reset + mp_hal_set_interrupt_char(-1); mp_printf(MP_PYTHON_PRINTER, "MPY: soft reboot\n"); #if MICROPY_PY_CSI omv_csi_abort_all(); diff --git a/ports/mimxrt/mimxrt.ld.S b/ports/mimxrt/mimxrt.ld.S index 975b6c1ce..4e98304d0 100644 --- a/ports/mimxrt/mimxrt.ld.S +++ b/ports/mimxrt/mimxrt.ld.S @@ -105,7 +105,7 @@ SECTIONS .text : { . = ALIGN(4); - *(EXCLUDE_FILE(*fsl_flexspi.o *gc.o *vm.o *parse*.o *runtime*.o *map.o *mpirq.o ) .text*) + *(EXCLUDE_FILE(*fsl_flexspi.o *gc.o *vm.o *parse*.o *runtime*.o *mpirq.o ) .text*) *(.rodata) *(.rodata*) *(.glue_7) diff --git a/ports/mimxrt/omv_mpconfigport.h b/ports/mimxrt/omv_mpconfigport.h index acc7eb752..7083d4dc3 100644 --- a/ports/mimxrt/omv_mpconfigport.h +++ b/ports/mimxrt/omv_mpconfigport.h @@ -29,6 +29,18 @@ fb_alloc_free_till_mark(); \ } while (0); +#define MICROPY_BOARD_BEFORE_PYTHON_EXEC(input_kind, exec_flags) \ + do { \ + extern void stdio_channel_pyexec_hook(bool); \ + stdio_channel_pyexec_hook(true); \ + } while (0); + +#define MICROPY_BOARD_AFTER_PYTHON_EXEC(input_kind, exec_flags, nlr, ret) \ + do { \ + extern void stdio_channel_pyexec_hook(bool); \ + stdio_channel_pyexec_hook(false); \ + } while (0); + #define MICROPY_ENABLE_VM_ABORT (1) #define MICROPY_PY_NETWORK_PPP_LWIP (0) #define MICROPY_PY_MACHINE_UART_IRQ (0) diff --git a/ports/mimxrt/omv_portconfig.mk b/ports/mimxrt/omv_portconfig.mk index e1e0a7a6e..e02d305f8 100644 --- a/ports/mimxrt/omv_portconfig.mk +++ b/ports/mimxrt/omv_portconfig.mk @@ -76,8 +76,8 @@ LDFLAGS = -mthumb \ -mabi=aapcs-linux \ -Wl,--print-memory-usage \ -Wl,--gc-sections \ - -Wl,--wrap=mp_usbd_task \ -Wl,--wrap=tud_cdc_rx_cb \ + -Wl,--wrap=tud_cdc_line_state_cb \ -Wl,--wrap=mp_hal_stdio_poll \ -Wl,--wrap=mp_hal_stdout_tx_strn \ -Wl,-T$(BUILD)/$(LDSCRIPT).lds \ @@ -138,6 +138,7 @@ include lib/imlib/imlib.mk include lib/tflm/tflm.mk include ports/ports.mk include common/micropy.mk +include protocol/protocol.mk # Firmware objects from port. MPY_FIRM_OBJ += $(addprefix $(BUILD)/$(MICROPY_DIR)/,\ diff --git a/ports/stm32/main.c b/ports/stm32/main.c index a3f631aaf..803c8fcdd 100644 --- a/ports/stm32/main.c +++ b/ports/stm32/main.c @@ -79,10 +79,10 @@ #include "omv_gpio.h" #include "omv_i2c.h" #include "omv_csi.h" +#include "omv_protocol.h" #include "mp_utils.h" #include "framebuffer.h" -#include "usbdbg.h" #include "sdram.h" #include "stm_xspi.h" #include "fb_alloc.h" @@ -225,7 +225,7 @@ soft_reset: #if MICROPY_HW_ENABLE_CAN pyb_can_init0(); #endif - #if MICROPY_PY_PYB_LEGACY && MICROPY_HW_ENABLE_HW_I2C + #if MICROPY_PY_PYB_LEGACY && MICROPY_HW_ENABLE_HW_I2C i2c_init0(); #endif spi_init0(); @@ -245,7 +245,6 @@ soft_reset: #if MICROPY_HW_ENABLE_SERVO servo_init(); #endif - usbdbg_init(); #if MICROPY_HW_ENABLE_SDCARD sdcard_init(); #endif @@ -297,6 +296,9 @@ soft_reset: mod_network_init(); #endif + // Initialize OpenMV protocol + omv_protocol_init_default(); + // Execute _boot.py to set up the filesystem. pyexec_frozen_module("_boot.py", false); @@ -326,24 +328,18 @@ soft_reset: } #endif - // Run boot.py script. - bool interrupted = mp_exec_bootscript("boot.py", true); - - // Run main.py script on first soft-reset. - if (first_soft_reset && !interrupted && mp_vfs_import_stat("main.py")) { - mp_exec_bootscript("main.py", true); - goto soft_reset_exit; + // Run boot.py every reset and main.py on first soft-reset + if (pyexec_file_if_exists("boot.py") && first_soft_reset) { + pyexec_file_if_exists("main.py"); } - // If there's no script ready, just re-exec REPL - while (!usbdbg_script_ready()) { + while (!omv_protocol_exec_script()) { nlr_buf_t nlr; if (nlr_push(&nlr) == 0) { - // enable IDE interrupt - usbdbg_set_irq_enabled(true); + // Enable Ctrl+C to interrupt script or REPL. + mp_hal_set_interrupt_char(CHAR_CTRL_C); - // run REPL if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) { if (pyexec_raw_repl() != 0) { break; @@ -353,31 +349,15 @@ soft_reset: break; } } - nlr_pop(); } } - if (usbdbg_script_ready()) { - nlr_buf_t nlr; - if (nlr_push(&nlr) == 0) { - // Enable IDE interrupts - usbdbg_set_irq_enabled(true); - // Execute the script. - pyexec_str(usbdbg_get_script(), true); - // Disable IDE interrupts - usbdbg_set_irq_enabled(false); - nlr_pop(); - } else { - mp_obj_print_exception(MP_PYTHON_PRINTER, (mp_obj_t) nlr.ret_val); - } - } - -soft_reset_exit: // soft reset + mp_hal_set_interrupt_char(-1); mp_printf(MP_PYTHON_PRINTER, "MPY: soft reboot\n"); #if MICROPY_PY_CSI - omv_csi_abort_all(); + omv_csi_abort_all(); #endif #if MICROPY_PY_LWIP systick_disable_dispatch(SYSTICK_DISPATCH_LWIP); diff --git a/ports/stm32/omv_mpconfigport.h b/ports/stm32/omv_mpconfigport.h index ba6c8f6df..0396368b2 100644 --- a/ports/stm32/omv_mpconfigport.h +++ b/ports/stm32/omv_mpconfigport.h @@ -29,6 +29,19 @@ fb_alloc_free_till_mark(); \ } while (0); + +#define MICROPY_BOARD_BEFORE_PYTHON_EXEC(input_kind, exec_flags) \ + do { \ + extern void stdio_channel_pyexec_hook(bool); \ + stdio_channel_pyexec_hook(true); \ + } while (0); + +#define MICROPY_BOARD_AFTER_PYTHON_EXEC(input_kind, exec_flags, nlr, ret) \ + do { \ + extern void stdio_channel_pyexec_hook(bool); \ + stdio_channel_pyexec_hook(false); \ + } while (0); + #define MICROPY_ENABLE_VM_ABORT (1) #define MICROPY_OPT_COMPUTED_GOTO (1) #define MICROPY_GC_SPLIT_HEAP (1) diff --git a/ports/stm32/omv_portconfig.mk b/ports/stm32/omv_portconfig.mk index 061998d18..e608b8619 100644 --- a/ports/stm32/omv_portconfig.mk +++ b/ports/stm32/omv_portconfig.mk @@ -80,6 +80,10 @@ LDFLAGS = -mthumb \ -mabi=aapcs-linux \ -Wl,--print-memory-usage \ -Wl,--gc-sections \ + -Wl,--wrap=usbd_cdc_control \ + -Wl,--wrap=usbd_cdc_receive \ + -Wl,--wrap=mp_os_dupterm_rx_chr \ + -Wl,--wrap=mp_hal_stdout_tx_strn \ -Wl,-T$(BUILD)/$(LDSCRIPT).lds \ -Wl,-Map=$(BUILD)/$(FIRMWARE).map @@ -141,6 +145,7 @@ CFLAGS += $(HAL_CFLAGS) $(MPY_CFLAGS) $(OMV_CFLAGS) include lib/cmsis/cmsis.mk include lib/stm32/stm32.mk include common/common.mk +include protocol/protocol.mk include drivers/drivers.mk include lib/imlib/imlib.mk include lib/tflm/tflm.mk diff --git a/protocol/omv_protocol.c b/protocol/omv_protocol.c new file mode 100644 index 000000000..2e65cd755 --- /dev/null +++ b/protocol/omv_protocol.c @@ -0,0 +1,858 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol - Transport-agnostic communication protocol + * This protocol provides reliable communication between host and + * device with support for multiple transport layers (USB, UART, TCP/IP). + */ +#include +#include +#include + +#include "py/mphal.h" +#include "omv_common.h" +#include "omv_csi.h" +#include "omv_crc.h" +#include "omv_protocol.h" +#include "omv_protocol_hw_caps.h" +#include "boot/include/version.h" + +#ifndef OMV_PROTOCOL_HW_CAPS +#define OMV_PROTOCOL_HW_CAPS (0) +#endif + +// Static global protocol context +static omv_protocol_context_t ctx; + +int omv_protocol_init(const omv_protocol_config_t *config) { + if (!config) { + return -1; + } + + // Validate config + if (config->max_payload < OMV_PROTOCOL_MIN_PAYLOAD_SIZE || + config->max_payload > OMV_PROTOCOL_MAX_PAYLOAD_SIZE || + config->lock_intval_ms < OMV_PROTOCOL_MIN_LOCK_INTERVAL_MS) { + return -1; + } + + // Initialize state + ctx.sequence = 0; + ctx.last_lock_ms = 0; + ctx.scan_offset = 0; + ctx.state = OMV_PROTOCOL_STATE_SYNC; + ctx.channels_count = 0; + memset(ctx.channels, 0, sizeof(ctx.channels)); + + // Use the config provided by this transport + // For USB, see defaults below. Uart could enable CRC, ACKs etc.. + ctx.config = *config; + + + // Initialize buffer + omv_buffer_init(&ctx.buffer, ctx.rawbuf, sizeof(ctx.rawbuf)); + + return 0; +} + +int omv_protocol_init_default(void) { + const omv_protocol_config_t *config, default_config = { + .crc_enabled = true, + .seq_enabled = true, + .ack_enabled = true, + .event_enabled = true, + .max_payload = OMV_PROTOCOL_MAX_PAYLOAD_SIZE, + .caps_negotiated = false, + .soft_reboot = true, + .rtx_retries = OMV_PROTOCOL_DEF_RTX_RETRIES, + .rtx_timeout_ms = OMV_PROTOCOL_DEF_RTX_TIMEOUT_MS, + .lock_intval_ms = OMV_PROTOCOL_MIN_LOCK_INTERVAL_MS, + }; + + // If capabilities were previously negotiated, preserve protocol + // capabilities but keep transport configuration as defaults. + if (ctx.config.caps_negotiated) { + config = &ctx.config; + } else { + config = &default_config; + } + + if (omv_protocol_init(config) != 0) { + return -1; + } + + #if OMV_PROTOCOL_DEFAULT_CHANNELS + // Register the physical transport as channel 0 + omv_protocol_register_channel(&omv_usb_channel); + + // Register the default logical data channels next + omv_protocol_register_channel(&omv_stdin_channel); + omv_protocol_register_channel(&omv_stdout_channel); + omv_protocol_register_channel(&omv_stream_channel); + + // Register the profiler channel (if enabled) + #if OMV_PROFILER_ENABLE + omv_protocol_register_channel(&omv_profile_channel); + #endif // OMV_PROFILER_ENABLE + + #endif // OMV_PROTOCOL_DEFAULT_CHANNELS + + return 0; +} + +int omv_protocol_deinit(void) { + // Deinitialize all channels (including transport at index 0) + for (int i = 0; i < OMV_PROTOCOL_MAX_CHANNELS; i++) { + if (ctx.channels[i] && ctx.channels[i]->deinit) { + ctx.channels[i]->deinit(ctx.channels[i]); + ctx.channels[i] = NULL; + } + } + return 0; +} + +void omv_protocol_reset_state(void) { + ctx.sequence = 0; + ctx.scan_offset = 0; + ctx.state = OMV_PROTOCOL_STATE_SYNC; + omv_buffer_clear(&ctx.buffer); + + // Reset any channels when losing sync + for (int i = 0; i < ctx.channels_count; i++) { + const omv_protocol_channel_t *channel = ctx.channels[i]; + if (channel && channel->unlock) { + channel->unlock(channel); + } + } +} + +bool omv_protocol_is_active(void) { + const omv_protocol_channel_t *transport = omv_protocol_find_transport(); + return transport && transport->is_active(transport); +} + +bool omv_protocol_exec_script(void) { + const omv_protocol_channel_t *channel = omv_protocol_find_channel(OMV_PROTOCOL_CHANNEL_ID_STDIN); + if (!channel || !channel->exec) { + return false; + } + + bool result = channel->exec(channel); + if (result) { + omv_protocol_send_event(0, OMV_PROTOCOL_EVENT_SOFT_REBOOT, false); + } + + if (result) { + // A script was executed - return true if the transport allows soft-reboot + return ctx.config.soft_reboot; + } + + return false; +} + +int omv_protocol_register_channel(const omv_protocol_channel_t *channel) { + int channel_id = -1; + + if (OMV_PROTOCOL_CHANNEL_IS_TRANSPORT(channel)) { + channel_id = 0; + } else if (!OMV_PROTOCOL_CHANNEL_FLAG_GET(channel, DYNAMIC)) { + // Use statically defined channel ID + channel_id = channel->id; + } else { + // Find the first free channel + for (size_t i = 1; i < OMV_PROTOCOL_MAX_CHANNELS; i++) { + if (ctx.channels[i] == NULL) { + channel_id = i; + break; + } + } + } + + // Initialize the channel + if (channel->init && channel->init(channel)) { + return -1; + } + + // Register channel at next available index + ctx.channels[channel_id] = channel; + + // Send channel registered event to host + if (OMV_PROTOCOL_CHANNEL_FLAG_GET(channel, DYNAMIC)) { + ((omv_protocol_channel_t *) channel)->id = channel_id; + omv_protocol_send_event(0, OMV_PROTOCOL_EVENT_CHANNEL_REGISTERED, false); + } + + // If no physical transport is ever registered, the count will be one + // less than the number of channels as they're offset by 1. However, + // channels_count is only used when the physical transport is active. + ctx.channels_count++; + + return channel_id; +} + +// Find and verify transport channel +const omv_protocol_channel_t *omv_protocol_find_transport(void) { + const omv_protocol_channel_t *transport = ctx.channels[OMV_PROTOCOL_CHANNEL_ID_TRANSPORT]; + return (transport && OMV_PROTOCOL_CHANNEL_IS_TRANSPORT(transport)) ? transport : NULL; +} + +const omv_protocol_channel_t *omv_protocol_find_channel(uint8_t channel_id) { + if (channel_id >= ctx.channels_count) { + return NULL; + } + + return ctx.channels[channel_id]; +} + +// Calculate and check if CRC matches the one stored in buffer +static inline bool omv_protocol_crc_check(void *buf, size_t size) { + return !size || !ctx.config.crc_enabled || omv_crc_check(buf, size); +} + +// Check if packet is a valid SYNC command +static inline bool omv_protocol_is_sync(const omv_protocol_packet_t *packet) { + return packet->sync == OMV_PROTOCOL_SYNC_WORD && + packet->channel == 0 && packet->length == 0 && + packet->opcode == OMV_PROTOCOL_OPCODE_PROTO_SYNC && + omv_protocol_crc_check((void *) packet, OMV_PROTOCOL_HEADER_SIZE); +} + +// Check if packet is a valid ACK for expected opcode/sequence +static inline bool omv_protocol_is_ack(const omv_protocol_packet_t *packet, uint8_t opcode, uint8_t sequence) { + return packet->sync == OMV_PROTOCOL_SYNC_WORD && + (packet->flags & OMV_PROTOCOL_FLAG_ACK) && + packet->opcode == opcode && packet->sequence == sequence && + omv_protocol_crc_check((void *) packet, OMV_PROTOCOL_HEADER_SIZE); +} + +// Protocol and system commands and events must arrive on channel 0 +static bool omv_protocol_channel_check(const omv_protocol_packet_t *packet) { + return packet->channel == 0 || packet->opcode > OMV_PROTOCOL_OPCODE_SYS_LAST; +} + +static inline bool omv_protocol_seq_check(omv_protocol_packet_t *packet) { + return !ctx.config.seq_enabled || + ctx.sequence == packet->sequence || + (packet->flags & OMV_PROTOCOL_FLAG_ACK) || + packet->opcode == OMV_PROTOCOL_OPCODE_SYS_EVENT || + packet->opcode == OMV_PROTOCOL_OPCODE_CHANNEL_EVENT || + packet->opcode == OMV_PROTOCOL_OPCODE_PROTO_SYNC; +} + +static bool omv_protocol_ioctl_check(uint8_t channel_id, uint32_t cmd, size_t len) { + static const struct { + uint8_t ch; uint32_t cmd; size_t size; + } ioctl_table[] = { + OMV_PROTOCOL_CHANNEL_IOCTL_TABLE + }; + for (int i = 0; i < sizeof(ioctl_table) / sizeof(ioctl_table[0]); i++) { + if (ioctl_table[i].ch == channel_id && ioctl_table[i].cmd == cmd) { + return len == ioctl_table[i].size; + } + } + return false; // Unknown ioctl on static channel +} + +static void omv_protocol_send_status(const omv_protocol_packet_t *packet, omv_protocol_status_t status) { + if (status == OMV_PROTOCOL_STATUS_SEQUENCE) { + ctx.stats.sequence_errors++; + } else if (status == OMV_PROTOCOL_STATUS_CHECKSUM) { + ctx.stats.checksum_errors++; + } + + if (status == OMV_PROTOCOL_STATUS_SUCCESS) { + omv_protocol_send_packet(packet->opcode, packet->channel, 0, NULL, OMV_PROTOCOL_FLAG_ACK); + } else { + omv_protocol_response_t resp = { + .status = status, + }; + omv_protocol_send_packet(packet->opcode, packet->channel, sizeof(resp), &resp, OMV_PROTOCOL_FLAG_NAK); + } +} + +int omv_protocol_send_event(uint8_t channel_id, uint16_t event, bool wait_ack) { + if (!ctx.config.event_enabled || !omv_protocol_is_active()) { + return -1; + } + + uint32_t flags = OMV_PROTOCOL_FLAG_EVENT | (wait_ack ? OMV_PROTOCOL_FLAG_ACK_REQ : 0); + uint8_t opcode = (channel_id == 0) ? OMV_PROTOCOL_OPCODE_SYS_EVENT: OMV_PROTOCOL_OPCODE_CHANNEL_EVENT; + + ctx.stats.sent_events++; + if (event == OMV_PROTOCOL_EVENT_NOTIFY) { + return omv_protocol_send_packet(opcode, channel_id, 0, NULL, flags); + } else { + return omv_protocol_send_packet(opcode, channel_id, sizeof(event), &event, flags); + } +} + +int omv_protocol_send_packet(uint8_t opcode, uint8_t channel_id, size_t size, const void *data, uint8_t flags) { + const omv_protocol_channel_t *transport = omv_protocol_find_transport(); + if (!transport || !transport->is_active(transport)) { + return -1; + } + + if (!ctx.config.ack_enabled) { + // ACK is disabled globall + flags &= ~OMV_PROTOCOL_FLAG_ACK_REQ; + } else if (!(flags & OMV_PROTOCOL_FLAG_NO_ACK)) { + flags |= OMV_PROTOCOL_FLAG_ACK_REQ; + } + + do { + int rtx_retries = ctx.config.rtx_retries; + uint32_t rtx_timeout = ctx.config.rtx_timeout_ms; + bool wait_for_ack = flags & OMV_PROTOCOL_FLAG_ACK_REQ; + + uint8_t crc16_bytes[2]; + size_t frag_len = (size <= ctx.config.max_payload) ? size : ctx.config.max_payload; + uint8_t frag_flags = (size <= ctx.config.max_payload) ? flags : (flags | OMV_PROTOCOL_FLAG_FRAGMENT); + + // Build packet header + omv_protocol_packet_t packet = { + .sync = OMV_PROTOCOL_SYNC_WORD, + .sequence = ctx.sequence, + .channel = channel_id, + .flags = frag_flags, + .opcode = opcode, + .length = frag_len, + }; + + // Calculate header CRC (excluding the CRC field itself) + if (ctx.config.crc_enabled) { + packet.crc = omv_crc_start(&packet, OMV_PROTOCOL_HEADER_SIZE - 2); + } + + // Calculate payload CRC (excluding the CRC field itself) + if (ctx.config.crc_enabled && size && data) { + omv_crc_t crc = omv_crc_start(data, frag_len); + crc16_bytes[0] = crc & 0xFF; + crc16_bytes[1] = (crc >> 8) & 0xFF; + } + + // Set up ACK waiting context + if (flags & OMV_PROTOCOL_FLAG_ACK_REQ) { + ctx.scan_offset = 0; + ctx.wait_ack_opcode = packet.opcode; + ctx.wait_ack_sequence = packet.sequence; + ctx.state = OMV_PROTOCOL_STATE_WAIT_ACK; + } + + do { + // Send packet header, payload and CRC + int sent = transport->write(transport, 0, OMV_PROTOCOL_HEADER_SIZE, &packet); + if (size && data) { + sent += transport->write(transport, 0, frag_len, data); + sent += transport->write(transport, 0, 2, crc16_bytes); + } + + if (transport->flush) { + transport->flush(transport); + } + + if (sent != OMV_PROTOCOL_HEADER_SIZE + frag_len + (frag_len > 0 ? 2 : 0)) { + ctx.stats.transport_errors++; + return -1; + } + + for (uint32_t start = mp_hal_ticks_ms(); wait_for_ack; mp_event_handle_nowait()) { + if (omv_protocol_task() == -1) { + return -1; + } + + // Check if state changed back to SYNC (ACK received) + wait_for_ack = (ctx.state == OMV_PROTOCOL_STATE_WAIT_ACK); + + if (wait_for_ack && check_timeout_ms(start, rtx_timeout)) { + rtx_timeout *= 2; + ctx.stats.retransmit++; + + // Set RTX and recalculate the CRC. + if (ctx.config.crc_enabled && !(packet.flags & OMV_PROTOCOL_FLAG_RTX)) { + packet.flags |= OMV_PROTOCOL_FLAG_RTX; + packet.crc = omv_crc_start(&packet, offsetof(omv_protocol_packet_t, crc)); + } + break; + } + } + } while (wait_for_ack && rtx_retries--); + + // Never received the ACK + if (wait_for_ack) { + omv_protocol_reset_state(); + return -1; + } + + if (size && data) { + size -= frag_len; + data = (uint8_t *) data + frag_len; + } + + if (!(flags & OMV_PROTOCOL_FLAG_EVENT)) { + ctx.sequence++; + ctx.stats.sent_packets++; + } + } while (size > 0); + + return 0; +} + +int omv_protocol_task(void) { + size_t available = 0; + const omv_protocol_channel_t *transport = omv_protocol_find_transport(); + + if (!transport || !transport->is_active(transport)) { + return -1; + } + + // Siphon off all available data from CDC buffer. + while ((available = transport->size(transport))) { + // Calculate read size: MIN(available, free_size) + size_t free_size = omv_buffer_free(&ctx.buffer); + size_t read_size = OMV_MIN(available, free_size); + uint8_t *write_ptr = omv_buffer_claim(&ctx.buffer, read_size); + + if (!write_ptr) { + break; + } + + // Write data directly into buffer + int bytes_read = transport->read(transport, 0, read_size, write_ptr); + if (bytes_read <= 0) { + break; + } + + // Commit the received data + omv_buffer_commit(&ctx.buffer, bytes_read); + } + + while (omv_buffer_avail(&ctx.buffer) >= OMV_PROTOCOL_SYNC_SIZE) { + uint8_t *buffer = omv_buffer_data(&ctx.buffer); + int32_t buffer_size = omv_buffer_avail(&ctx.buffer); + omv_protocol_packet_t *packet = omv_buffer_data(&ctx.buffer); + + switch (ctx.state) { + case OMV_PROTOCOL_STATE_SYNC: + // Look for sync pattern in the buffer + while (omv_buffer_avail(&ctx.buffer) >= OMV_PROTOCOL_SYNC_SIZE) { + if (omv_buffer_peek16(&ctx.buffer) == OMV_PROTOCOL_SYNC_WORD) { + ctx.state = OMV_PROTOCOL_STATE_HEADER; + break; + } + // Consume one byte and check the next word + omv_buffer_consume(&ctx.buffer, 1); + } + break; + + case OMV_PROTOCOL_STATE_HEADER: + // Check if we have a complete header + if (buffer_size < OMV_PROTOCOL_HEADER_SIZE) { + return 0; // Need more data + } + + // Validate packet header. + ctx.state = OMV_PROTOCOL_STATE_SYNC; + if (!omv_protocol_crc_check(packet, OMV_PROTOCOL_HEADER_SIZE)) { + // No further validation needed + } else if (packet->length > ctx.config.max_payload) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_OVERFLOW); + } else if (!omv_protocol_seq_check(packet)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SEQUENCE); + } else { + ctx.state = OMV_PROTOCOL_STATE_PAYLOAD; + } + + // Consume a byte if the header has been rejected. + if (ctx.state != OMV_PROTOCOL_STATE_PAYLOAD) { + omv_buffer_consume(&ctx.buffer, 1); + } + break; + + case OMV_PROTOCOL_STATE_PAYLOAD: + // HEADER + CRC + PAYLOAD + CRC + size_t packet_size = OMV_PROTOCOL_PACKET_GET_SIZE(packet); + size_t payload_size = packet_size - OMV_PROTOCOL_HEADER_SIZE; + + // Check if we have the complete packet + if (buffer_size < packet_size) { + // Transition to SYNC_RECOVERY to scan for SYNC commands + ctx.scan_offset = 0; + ctx.state = OMV_PROTOCOL_STATE_SYNC_RECOVERY; + break; + } + + ctx.state = OMV_PROTOCOL_STATE_SYNC; + // protocol_process may send a packet that expects an ACK. + // Clear the buffer first before calling protocol_process. + omv_buffer_consume(&ctx.buffer, packet_size); + + // Check data CRC if we have payload data + if (!omv_protocol_crc_check(packet->payload, payload_size)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_CHECKSUM); + } else if (!omv_protocol_channel_check(packet)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } else if (packet->flags & OMV_PROTOCOL_FLAG_ACK) { + // ACK packets are handled by WAIT_ACK state - ignore here + } else { + omv_protocol_process(packet); + } + + break; + + // Scan through the buffer looking for a complete SYNC command + // Prevents a partial packet from deadlocking the state machine + case OMV_PROTOCOL_STATE_SYNC_RECOVERY: + if (buffer_size >= OMV_PROTOCOL_PACKET_GET_SIZE(packet)) { + ctx.state = OMV_PROTOCOL_STATE_SYNC; + break; + } + + for (; ctx.scan_offset <= buffer_size - OMV_PROTOCOL_HEADER_SIZE; ctx.scan_offset++) { + omv_protocol_packet_t *packet = (omv_protocol_packet_t *) (buffer + ctx.scan_offset); + + if (omv_protocol_is_sync(packet)) { + // SYNC command is found - process SYNC and reset state + omv_protocol_process(packet); + return 0; + } + } + + // No SYNC command found - go back to PAYLOAD state + ctx.state = OMV_PROTOCOL_STATE_PAYLOAD; + return 0; + + // Scan through buffer looking for expected ACK packet + case OMV_PROTOCOL_STATE_WAIT_ACK: + for (; ctx.scan_offset <= buffer_size - OMV_PROTOCOL_HEADER_SIZE; ctx.scan_offset++) { + omv_protocol_packet_t *packet = (omv_protocol_packet_t *) (buffer + ctx.scan_offset); + + // SYNC found while waiting for ACK - process SYNC and reset state + if (omv_protocol_is_sync(packet)) { + omv_protocol_process(packet); + return -1; + } + + // Found the matching ACK - consume it and return to SYNC + if (omv_protocol_is_ack(packet, ctx.wait_ack_opcode, ctx.wait_ack_sequence)) { + if ((void *) packet == buffer) { + omv_buffer_consume(&ctx.buffer, OMV_PROTOCOL_HEADER_SIZE); + } + ctx.state = OMV_PROTOCOL_STATE_SYNC; + return 0; + } + } + // No matching ACK found - stay in WAIT_ACK state + return 0; + } + } + + return 0; +} + +void omv_protocol_process(const omv_protocol_packet_t *packet) { + ctx.stats.recv_packets++; + + switch (packet->opcode) { + case OMV_PROTOCOL_OPCODE_SYS_RESET: { + #if defined(OMV_BOARD_RESET) + OMV_BOARD_RESET(); + #else + NVIC_SystemReset(); + #endif + break; + } + + case OMV_PROTOCOL_OPCODE_SYS_BOOT: { + #if defined(MICROPY_BOARD_ENTER_BOOTLOADER) + MICROPY_BOARD_ENTER_BOOTLOADER(0, 0); + #else + NVIC_SystemReset(); + #endif + break; + } + + case OMV_PROTOCOL_OPCODE_PROTO_SYNC: { + ctx.sequence = 0; + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS); + omv_protocol_reset_state(); + break; + } + + case OMV_PROTOCOL_OPCODE_PROTO_GET_CAPS: { + // Convert internal config to wire caps format + omv_protocol_caps_t caps = {0}; + caps.crc_enabled = ctx.config.crc_enabled; + caps.seq_enabled = ctx.config.seq_enabled; + caps.ack_enabled = ctx.config.ack_enabled; + caps.event_enabled = ctx.config.event_enabled; + caps.max_payload = ctx.config.max_payload; + // Transport fields are not sent over wire + omv_protocol_send_packet(packet->opcode, packet->channel, sizeof(caps), &caps, 0); + break; + } + + case OMV_PROTOCOL_OPCODE_PROTO_SET_CAPS: { + omv_protocol_caps_t *caps = (void *) packet->payload; + + // Validate only the protocol capability fields + if (packet->length != sizeof(omv_protocol_caps_t) || + caps->max_payload < OMV_PROTOCOL_MIN_PAYLOAD_SIZE || + caps->max_payload > OMV_PROTOCOL_MAX_PAYLOAD_SIZE) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } else { + // Update only protocol capability fields, preserve transport config + ctx.config.crc_enabled = caps->crc_enabled; + ctx.config.seq_enabled = caps->seq_enabled; + ctx.config.ack_enabled = caps->ack_enabled; + ctx.config.event_enabled = caps->event_enabled; + ctx.config.max_payload = caps->max_payload; + // Mark capabilities as negotiated for soft reset preservation + ctx.config.caps_negotiated = true; + // Transport config fields remain unchanged + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS); + } + break; + } + + case OMV_PROTOCOL_OPCODE_PROTO_STATS: { + omv_protocol_send_packet(packet->opcode, packet->channel, sizeof(ctx.stats), &ctx.stats, 0); + break; + } + + case OMV_PROTOCOL_OPCODE_SYS_INFO: { + omv_protocol_sys_info_t sysinfo = { 0 }; + + // Hardware identification + sysinfo.cpu_id = SCB->CPUID; + + // Device ID from board UID + #if (OMV_BOARD_UID_SIZE > 2) + sysinfo.dev_id[0] = *((uint32_t *) (OMV_BOARD_UID_ADDR + OMV_BOARD_UID_OFFSET * 2)); + #endif + sysinfo.dev_id[1] = *((uint32_t *) (OMV_BOARD_UID_ADDR + OMV_BOARD_UID_OFFSET * 1)); + sysinfo.dev_id[2] = *((uint32_t *) (OMV_BOARD_UID_ADDR + OMV_BOARD_UID_OFFSET * 0)); + + // Camera sensor chip ID + #if MICROPY_PY_CSI + size_t chip_count = 0; + size_t max_chip_ids = OMV_ARRAY_SIZE(sysinfo.chip_id); + for (size_t i = 0; chip_count < max_chip_ids && i < OMV_CSI_MAX_DEVICES; i++) { + omv_csi_t *csi = &csi_all[i]; + if (csi->detected) { + sysinfo.chip_id[chip_count++] = omv_csi_get_id(csi); + } + } + #endif + + // Hardware capabilities + #ifdef OMV_PROTOCOL_HW_CAPS + sysinfo.hw_caps[0] = OMV_PROTOCOL_HW_CAPS; + #endif + + // Memory information + sysinfo.flash_size_kb = 0; + sysinfo.ram_size_kb = 0; + sysinfo.framebuffer_size_kb = framebuffer_get(FB_MAINFB_ID)->raw_size / 1024; + sysinfo.stream_buffer_size_kb = framebuffer_get(FB_STREAM_ID)->raw_size / 1024; + + // Firmware version + sysinfo.firmware_version[0] = OMV_FIRMWARE_VERSION_MAJOR; + sysinfo.firmware_version[1] = OMV_FIRMWARE_VERSION_MINOR; + sysinfo.firmware_version[2] = OMV_FIRMWARE_VERSION_PATCH; + + // Protocol version + sysinfo.protocol_version[0] = OMV_PROTOCOL_VERSION_MAJOR; + sysinfo.protocol_version[1] = OMV_PROTOCOL_VERSION_MINOR; + sysinfo.protocol_version[2] = OMV_PROTOCOL_VERSION_PATCH; + + // Bootloader version + sysinfo.bootloader_version[0] = OMV_BOOTLOADER_VERSION_MAJOR; + sysinfo.bootloader_version[1] = OMV_BOOTLOADER_VERSION_MINOR; + sysinfo.bootloader_version[2] = OMV_BOOTLOADER_VERSION_PATCH; + + omv_protocol_send_packet(OMV_PROTOCOL_OPCODE_SYS_INFO, packet->channel, sizeof(sysinfo), &sysinfo, 0); + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_LIST: { + // Build list of registered channels + int ch_count = 0; + omv_protocol_channel_entry_t ch_list[OMV_PROTOCOL_MAX_CHANNELS]; + + for (int i = 0; i < ctx.channels_count; i++) { + if (ctx.channels[i] != NULL) { + ch_list[ch_count].id = ctx.channels[i]->id; + ch_list[ch_count].flags = ctx.channels[i]->flags; + strncpy(ch_list[ch_count].name, ctx.channels[i]->name, OMV_PROTOCOL_CHANNEL_NAME_SIZE); + ch_list[ch_count].name[OMV_PROTOCOL_CHANNEL_NAME_SIZE - 1] = '\0'; + ch_count++; + } + } + + size_t ch_list_size = ch_count * sizeof(omv_protocol_channel_entry_t); + omv_protocol_send_packet(packet->opcode, packet->channel, ch_list_size, ch_list, 0); + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_POLL: { + omv_protocol_channel_poll_t response = { 0 }; + + for (int i = 0; i < ctx.channels_count; i++) { + const omv_protocol_channel_t *channel = ctx.channels[i]; + if (channel && channel->poll) { + response.flags |= channel->poll(channel) << i; + } + } + + omv_protocol_send_packet(packet->opcode, packet->channel, sizeof(response), &response, 0); + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_LOCK: { + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (!check_timeout_ms(ctx.last_lock_ms, ctx.config.lock_intval_ms)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_BUSY); + } else if (channel && channel->lock && channel->lock(channel) == 0) { + ctx.last_lock_ms = mp_hal_ticks_ms(); + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS); + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_BUSY); + } + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_UNLOCK: { + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (channel && channel->unlock && channel->unlock(channel) == 0) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS); + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_BUSY); + } + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_SIZE: { + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (channel && channel->size) { + omv_protocol_channel_size_t response; + response.size = channel->size(channel); + omv_protocol_send_packet(packet->opcode, packet->channel, sizeof(response), &response, 0); + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_SHAPE: { + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (channel && channel->shape) { + size_t shape_array[4]; + size_t shape_size = channel->shape(channel, shape_array) * sizeof(size_t); + omv_protocol_send_packet(packet->opcode, packet->channel, shape_size, shape_array, 0); + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_READ: { + omv_protocol_channel_io_t *request = (void *) packet->payload; + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (!request->length || !channel || !(channel->read || channel->readp)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } else if (channel->readp) { + const void *data = channel->readp(channel, request->offset, request->length); + if (!data) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_FAILED); + } else { + omv_protocol_send_packet(packet->opcode, packet->channel, request->length, data, 0); + } + } else { + uint8_t buffer[OMV_MIN(512, ctx.config.max_payload)]; + + while (request->length > 0) { + size_t size_rq = OMV_MIN(request->length, sizeof(buffer)); + int32_t size_rd = channel->read(channel, request->offset, size_rq, buffer); + + if (size_rd <= 0) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_FAILED); + break; + } + + request->length -= size_rd; + request->offset += size_rd; + uint8_t flags = (request->length == 0) ? 0 : OMV_PROTOCOL_FLAG_FRAGMENT; + omv_protocol_send_packet(packet->opcode, packet->channel, size_rd, buffer, flags); + } + } + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_WRITE: { + omv_protocol_channel_io_t *request = (void *) packet->payload; + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (channel && channel->write && request->length) { + if (channel->write(channel, request->offset, request->length, request->payload)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS); + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_FAILED); + } + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } + break; + } + + case OMV_PROTOCOL_OPCODE_CHANNEL_IOCTL: { + omv_protocol_channel_ioctl_t *ioctl = (void *) packet->payload; + size_t ioctl_len = packet->length - offsetof(omv_protocol_channel_ioctl_t, payload); + uint8_t *ioctl_arg = (ioctl_len) ? ioctl->payload : NULL; + const omv_protocol_channel_t *channel = omv_protocol_find_channel(packet->channel); + + if (channel && channel->ioctl) { + // Validate argument size for static channels only + if (!OMV_PROTOCOL_CHANNEL_FLAG_GET(channel, DYNAMIC) && // TODO + !omv_protocol_ioctl_check(packet->channel, ioctl->request, ioctl_len)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } else if (channel->ioctl(channel, ioctl->request, ioctl_len, ioctl_arg)) { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_FAILED); + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS); + } + } else { + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + } + break; + } + + default: + omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_INVALID); + break; + } +} diff --git a/protocol/omv_protocol.h b/protocol/omv_protocol.h new file mode 100644 index 000000000..b7873d8f1 --- /dev/null +++ b/protocol/omv_protocol.h @@ -0,0 +1,370 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol - Transport-agnostic communication protocol + * This protocol provides reliable communication between host and + * device with support for multiple transport layers (USB, UART, TCP/IP). + */ +#ifndef __OMV_PROTOCOL_H__ +#define __OMV_PROTOCOL_H__ + +#include +#include +#include +#include "omv_buffer.h" +#include "omv_protocol_channel.h" +#include "omv_boardconfig.h" + +/*************************************************************************** +* Protocol Constants +***************************************************************************/ + +#define OMV_FIRMWARE_VERSION_MAJOR (4) +#define OMV_FIRMWARE_VERSION_MINOR (7) +#define OMV_FIRMWARE_VERSION_PATCH (0) + +#define OMV_PROTOCOL_VERSION_MAJOR (1) +#define OMV_PROTOCOL_VERSION_MINOR (0) +#define OMV_PROTOCOL_VERSION_PATCH (0) + +#define OMV_PROTOCOL_SYNC_SIZE (2) +#define OMV_PROTOCOL_SYNC_WORD (0xD5AA) +#define OMV_PROTOCOL_CRC_SIZE (2) +#define OMV_PROTOCOL_HEADER_SIZE (10) // SYNC[2] SEQ[1] CHAN[1] FLAGS[1] OPCODE[1] LEN[2] CRC[2] + +#define OMV_PROTOCOL_MAX_CHANNELS (32) +#define OMV_PROTOCOL_DEF_RTX_RETRIES (3) +#define OMV_PROTOCOL_DEF_RTX_TIMEOUT_MS (500) // Doubled after each timeout +#define OMV_PROTOCOL_MIN_LOCK_INTERVAL_MS (10) +#define OMV_PROTOCOL_MAGIC_BAUDRATE (921600) + +#ifndef OMV_PROTOCOL_DEFAULT_CHANNELS +#define OMV_PROTOCOL_DEFAULT_CHANNELS (1) +#endif + +#ifndef OMV_PROTOCOL_MAX_BUFFER_SIZE +#define OMV_PROTOCOL_MAX_BUFFER_SIZE (4096) +#endif +#define OMV_PROTOCOL_MIN_PAYLOAD_SIZE (64 - OMV_PROTOCOL_HEADER_SIZE - 2) +#define OMV_PROTOCOL_MAX_PAYLOAD_SIZE (OMV_PROTOCOL_MAX_BUFFER_SIZE - OMV_PROTOCOL_HEADER_SIZE - 2) +#define OMV_PROTOCOL_MAX_PACKET_SIZE (OMV_PROTOCOL_HEADER_SIZE + OMV_PROTOCOL_MAX_PAYLOAD_SIZE + 2) + +#define OMV_PROTOCOL_PACKET_GET_SIZE(pkt) \ + (OMV_PROTOCOL_HEADER_SIZE + (pkt->length + (!!pkt->length * 2))) + +// Macro for protocol struct size validation +#define OMV_PROTOCOL_ASSERT_SIZE(type, size) \ + _Static_assert(sizeof(type) == (size), #type " must be exactly " #size " bytes") + +/*************************************************************************** +* Packet Flags +***************************************************************************/ + +typedef enum { + OMV_PROTOCOL_FLAG_ACK = (1 << 0), // ACK packet + OMV_PROTOCOL_FLAG_NAK = (1 << 1), // NAK packet + OMV_PROTOCOL_FLAG_RTX = (1 << 2), // RTX packet + OMV_PROTOCOL_FLAG_ACK_REQ = (1 << 3), // Packet requires an ACK + OMV_PROTOCOL_FLAG_FRAGMENT = (1 << 4), // More fragments to follow + OMV_PROTOCOL_FLAG_EVENT = (1 << 5), // Event packet + // Bits 6-7 reserved for future use +} omv_protocol_flags_t; + +#define OMV_PROTOCOL_FLAG_NO_ACK (OMV_PROTOCOL_FLAG_ACK | OMV_PROTOCOL_FLAG_NAK | OMV_PROTOCOL_FLAG_EVENT) + +/*************************************************************************** +* System Events (Channel 0) +***************************************************************************/ + +typedef enum { + OMV_PROTOCOL_EVENT_CHANNEL_REGISTERED = 0x00, // Channel dynamically registered + OMV_PROTOCOL_EVENT_CHANNEL_UNREGISTERED = 0x01, // Channel unregistered + OMV_PROTOCOL_EVENT_SOFT_REBOOT = 0x02, // System is going for a soft-reboot + OMV_PROTOCOL_EVENT_NOTIFY = 0xFFFF, // Notification event (no payload) +} omv_protocol_event_type_t; + +/*************************************************************************** +* Status Codes +***************************************************************************/ + +typedef enum { + OMV_PROTOCOL_STATUS_SUCCESS = 0x00, // Success + OMV_PROTOCOL_STATUS_FAILED = 0x01, // Command failed + OMV_PROTOCOL_STATUS_INVALID = 0x02, // Invalid command/arg + OMV_PROTOCOL_STATUS_TIMEOUT = 0x03, // Operation timeout + OMV_PROTOCOL_STATUS_BUSY = 0x04, // Device busy + OMV_PROTOCOL_STATUS_CHECKSUM = 0x05, // CRC error + OMV_PROTOCOL_STATUS_SEQUENCE = 0x06, // Sequence error + OMV_PROTOCOL_STATUS_OVERFLOW = 0x07, // Buffer overflow + OMV_PROTOCOL_STATUS_FRAGMENT = 0x08, // Fragmentation error + OMV_PROTOCOL_STATUS_UNKNOWN = 0x09, // Unknown error +} omv_protocol_status_t; + +/*************************************************************************** +* Protocol Opcodes +***************************************************************************/ + +typedef enum { + // Protocol Control Commands (0x00-0x0F) + OMV_PROTOCOL_OPCODE_PROTO_SYNC = 0x00, // Synchronization request + OMV_PROTOCOL_OPCODE_PROTO_GET_CAPS = 0x01, // Get capabilities + OMV_PROTOCOL_OPCODE_PROTO_SET_CAPS = 0x02, // Set capabilities + OMV_PROTOCOL_OPCODE_PROTO_STATS = 0x03, // Get protocol statistics + + // System Control Commands (0x10-0x1F) + OMV_PROTOCOL_OPCODE_SYS_RESET = 0x10, // System reset + OMV_PROTOCOL_OPCODE_SYS_BOOT = 0x11, // Jump to bootloader + OMV_PROTOCOL_OPCODE_SYS_INFO = 0x12, // Get system info + OMV_PROTOCOL_OPCODE_SYS_EVENT = 0x13, // System event + OMV_PROTOCOL_OPCODE_SYS_LAST = 0x13, // Last system command + + // Data Channels Commands (0x20-0x2F) + OMV_PROTOCOL_OPCODE_CHANNEL_LIST = 0x20, // List registered channels + OMV_PROTOCOL_OPCODE_CHANNEL_POLL = 0x21, // Poll channels status + OMV_PROTOCOL_OPCODE_CHANNEL_LOCK = 0x22, // Lock the data channel + OMV_PROTOCOL_OPCODE_CHANNEL_UNLOCK = 0x23, // Lock the data channel + OMV_PROTOCOL_OPCODE_CHANNEL_SHAPE = 0x24, // Available data size. + OMV_PROTOCOL_OPCODE_CHANNEL_SIZE = 0x25, // Available data size. + OMV_PROTOCOL_OPCODE_CHANNEL_READ = 0x26, // Dump the data channel + OMV_PROTOCOL_OPCODE_CHANNEL_WRITE = 0x27, // Write the data channel + OMV_PROTOCOL_OPCODE_CHANNEL_IOCTL = 0x28, // Perform ioctl on channel + OMV_PROTOCOL_OPCODE_CHANNEL_EVENT = 0x29, // System event +} omv_protocol_opcode_t; + +/*************************************************************************** +* Packet structure +***************************************************************************/ + +typedef struct __attribute__((packed)) { + uint16_t sync; // Synchronization word (0xAA55) + uint8_t sequence; // Sequence number + uint8_t channel; // Channel ID + uint8_t flags; // Packet flags + uint8_t opcode; // Command/response opcode + uint16_t length; // Length of data field only + uint16_t crc; // CRC of header fields + uint8_t payload[]; // Flexible array member +} +omv_protocol_packet_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_packet_t, OMV_PROTOCOL_HEADER_SIZE); + +/*************************************************************************** +* Packet Payloads +***************************************************************************/ + +// NAK respone structure +typedef struct __attribute__((packed)) { + uint16_t status; // Error status +} +omv_protocol_response_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_response_t, 2); + +// Channel size structure +typedef struct __attribute__((packed)) { + uint32_t size; +} +omv_protocol_channel_size_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_channel_size_t, 4); + +// Channel poll response structure +typedef struct __attribute__((packed)) { + uint32_t flags; +} +omv_protocol_channel_poll_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_channel_poll_t, 4); + +// Channel io structure +typedef struct __attribute__((packed)) { + uint32_t offset; + uint32_t length; + uint8_t payload[]; +} +omv_protocol_channel_io_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_channel_io_t, 8); + +// Channel ioctl structure +typedef struct __attribute__((packed)) { + uint32_t request; + uint8_t payload[]; +} +omv_protocol_channel_ioctl_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_channel_ioctl_t, 4); + +// Channel list entry structure +typedef struct __attribute__((packed)) { + uint8_t id; + uint8_t flags; + char name[OMV_PROTOCOL_CHANNEL_NAME_SIZE]; +} +omv_protocol_channel_entry_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_channel_entry_t, 16); + +// Protocol capabilities structure +typedef struct __attribute__((packed)) { + uint32_t crc_enabled : 1; + uint32_t seq_enabled : 1; + uint32_t ack_enabled : 1; + uint32_t event_enabled : 1; + uint32_t reserved1 : 28; + uint16_t max_payload; + uint8_t reserved2[10]; +} +omv_protocol_caps_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_caps_t, 16); + +// Protocol statistics structure +typedef struct __attribute__((packed)) { + uint32_t sent_packets; + uint32_t recv_packets; + uint32_t checksum_errors; + uint32_t sequence_errors; + uint32_t retransmit; + uint32_t transport_errors; + uint32_t sent_events; + uint32_t reserved; +} +omv_protocol_stats_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_stats_t, 32); + +// System information structure +typedef struct __attribute__((packed)) { + // Hardware identification + uint32_t cpu_id; // CPUID register value + uint32_t dev_id[3]; // Unique device ID register + uint32_t chip_id[3]; // Camera sensor chip ID + uint32_t id_reserved[2]; // Reserved for future expansion + + // Hardware capabilities + uint32_t hw_caps[2]; // Defined in hw_caps.h + + // Memory information + uint32_t flash_size_kb; // Flash memory size in KB + uint32_t ram_size_kb; // RAM size in KB + uint32_t framebuffer_size_kb; // Framebuffer size in KB + uint32_t stream_buffer_size_kb; // Streaming buffer size in KB + uint32_t memory_reserved[2]; // Reserved for future expansion + + // Version information + uint8_t firmware_version[3]; // Major, Minor, Patch + uint8_t protocol_version[3]; // Major, Minor, Patch + uint8_t bootloader_version[3]; // Bootloader version + + // Pad struct to 80 bytes + uint8_t reserved[3]; +} +omv_protocol_sys_info_t; +OMV_PROTOCOL_ASSERT_SIZE(omv_protocol_sys_info_t, 80); + +/*************************************************************************** +* Protocol Context +***************************************************************************/ + +// Protocol State Machine +typedef enum { + OMV_PROTOCOL_STATE_SYNC = 0x00, // Wait for sync bytes + OMV_PROTOCOL_STATE_HEADER = 0x01, // Reading header + OMV_PROTOCOL_STATE_PAYLOAD = 0x02, // Reading data + OMV_PROTOCOL_STATE_SYNC_RECOVERY = 0x03, // Scan for SYNC commands when stuck + OMV_PROTOCOL_STATE_WAIT_ACK = 0x04, // Wait for ACK response +} omv_protocol_state_t; + +// Protocol configuration structure (internal use) +typedef struct { + // Protocol capabilities (negotiated) + bool crc_enabled; + bool seq_enabled; + bool ack_enabled; + bool event_enabled; + uint16_t max_payload; + bool caps_negotiated; + // Transport configuration (local only) + bool soft_reboot; + uint16_t rtx_retries; + uint16_t rtx_timeout_ms; + uint16_t lock_intval_ms; +} omv_protocol_config_t; + +// Protocol context +typedef struct { + // State machine + uint8_t sequence; // Next sequence number + uint32_t last_lock_ms; // Timestamp of the last successful lock + int32_t scan_offset; + omv_protocol_state_t state; + + // ACK waiting context + uint8_t wait_ack_opcode; // Opcode we're waiting ACK for + uint8_t wait_ack_sequence; // Sequence we're waiting ACK for + + // Protocol configuration (capabilities + transport config) + omv_protocol_config_t config; + + // Buffer for received data + omv_buffer_t buffer; + uint8_t rawbuf[OMV_PROTOCOL_MAX_BUFFER_SIZE]; + + // Protocol physical/logical channels + uint8_t channels_count; + const omv_protocol_channel_t *channels[OMV_PROTOCOL_MAX_CHANNELS]; + + // Protocol Statistics + omv_protocol_stats_t stats; +} omv_protocol_context_t; + +// Initialize the protocol context +int omv_protocol_init(const omv_protocol_config_t *config); + +// Initialize the protocol using defaults (default config + default channels) +int omv_protocol_init_default(void); + +// Deinitialize protocol context +int omv_protocol_deinit(void); + +// Helper function to check if the transport is active. +bool omv_protocol_is_active(void); + +// Helper function to exec scripts in stdio buffer. +// Returns false, if no script is ready, true on executing the script. +bool omv_protocol_exec_script(void); + +// Register a channel +int omv_protocol_register_channel(const omv_protocol_channel_t *channel); + +// Find channel by ID. +const omv_protocol_channel_t *omv_protocol_find_channel(uint8_t channel_id); + +// Find transport channel. +const omv_protocol_channel_t *omv_protocol_find_transport(void); + +// Send event packet +int omv_protocol_send_event(uint8_t channel_id, uint16_t event, bool wait_ack); + +// Send response packet +int omv_protocol_send_packet(uint8_t opcode, uint8_t channel_id, size_t size, const void *data, uint8_t flags); + +// Call on events or periodically to process events +int omv_protocol_task(void); + +// Process assembled packet +void omv_protocol_process(const omv_protocol_packet_t *packet); +#endif // __OMV_PROTOCOL_H__ diff --git a/protocol/omv_protocol_channel.h b/protocol/omv_protocol_channel.h new file mode 100644 index 000000000..8dd7f56a6 --- /dev/null +++ b/protocol/omv_protocol_channel.h @@ -0,0 +1,150 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol - Channel Interface + * This header defines the channel abstraction layer for the OpenMV Protocol. + * Channels provide a unified interface for accessing different types of data + * and functionality through the protocol. + */ +#ifndef __OMV_PROTOCOL_CHANNEL_H__ +#define __OMV_PROTOCOL_CHANNEL_H__ + +#include +#include +#include + +/*************************************************************************** +* Channel Constants +***************************************************************************/ + +#define OMV_PROTOCOL_CHANNEL_NAME_SIZE (14) + +/*************************************************************************** +* Channel IOCTL Commands +***************************************************************************/ + +// Standard IOCTL commands for stdin channel +typedef enum { + OMV_CHANNEL_IOCTL_STDIN_STOP = 0x01, // Stop running script + OMV_CHANNEL_IOCTL_STDIN_EXEC = 0x02, // Execute script + OMV_CHANNEL_IOCTL_STDIN_RESET = 0x03, // Reset script buffer +} omv_channel_ioctl_stdin_t; + +// Standard IOCTL commands for stream channel +typedef enum { + OMV_CHANNEL_IOCTL_STREAM_CTRL = 0x00, // Enable/disable streaming + OMV_CHANNEL_IOCTL_STREAM_RAW_CTRL = 0x01, // Enable/disable raw streaming + OMV_CHANNEL_IOCTL_STREAM_RAW_CFG = 0x02, // Set raw stream resolution +} omv_channel_ioctl_stream_t; + +// Standard IOCTL commands for profile channel +typedef enum { + OMV_CHANNEL_IOCTL_PROFILE_MODE = 0x00, // Set profiling mode + OMV_CHANNEL_IOCTL_PROFILE_SET_EVENT = 0x01, // Set event type to profile + OMV_CHANNEL_IOCTL_PROFILE_RESET = 0x02, // Reset profiler data + OMV_CHANNEL_IOCTL_PROFILE_STATS = 0x03, // Get profiler statistics +} omv_channel_ioctl_profile_t; + +// IOCTL sizes lookup table entries +#define OMV_PROTOCOL_CHANNEL_IOCTL_TABLE \ + {OMV_PROTOCOL_CHANNEL_ID_STDIN, OMV_CHANNEL_IOCTL_STDIN_STOP, 0}, \ + {OMV_PROTOCOL_CHANNEL_ID_STDIN, OMV_CHANNEL_IOCTL_STDIN_EXEC, 0}, \ + {OMV_PROTOCOL_CHANNEL_ID_STDIN, OMV_CHANNEL_IOCTL_STDIN_RESET, 0}, \ + \ + {OMV_PROTOCOL_CHANNEL_ID_STREAM, OMV_CHANNEL_IOCTL_STREAM_CTRL, 4}, \ + {OMV_PROTOCOL_CHANNEL_ID_STREAM, OMV_CHANNEL_IOCTL_STREAM_RAW_CTRL, 4}, \ + {OMV_PROTOCOL_CHANNEL_ID_STREAM, OMV_CHANNEL_IOCTL_STREAM_RAW_CFG, 8}, \ + \ + {OMV_PROTOCOL_CHANNEL_ID_PROFILE, OMV_CHANNEL_IOCTL_PROFILE_MODE, 4}, \ + {OMV_PROTOCOL_CHANNEL_ID_PROFILE, OMV_CHANNEL_IOCTL_PROFILE_SET_EVENT, 8}, \ + {OMV_PROTOCOL_CHANNEL_ID_PROFILE, OMV_CHANNEL_IOCTL_PROFILE_RESET, 0}, \ + {OMV_PROTOCOL_CHANNEL_ID_PROFILE, OMV_CHANNEL_IOCTL_PROFILE_STATS, 0} + +/*************************************************************************** +* Channel Interface +***************************************************************************/ + +// Reserved/predefined channel IDs (usable as array indices) +typedef enum { + OMV_PROTOCOL_CHANNEL_ID_TRANSPORT = 0, // Transport layer (not accessible via protocol) + OMV_PROTOCOL_CHANNEL_ID_STDIN = 1, // Script input (write-only) + OMV_PROTOCOL_CHANNEL_ID_STDOUT = 2, // Text output (read-only) + OMV_PROTOCOL_CHANNEL_ID_STREAM = 3, // Stream data (read-only) + OMV_PROTOCOL_CHANNEL_ID_PROFILE = 4, // Profiling data (when enabled, read-only) +} omv_protocol_channel_id_t; + +// Channel flags +typedef enum { + OMV_PROTOCOL_CHANNEL_FLAG_READ = (1 << 0), // Channel supports read operations + OMV_PROTOCOL_CHANNEL_FLAG_WRITE = (1 << 1), // Channel supports write operations + OMV_PROTOCOL_CHANNEL_FLAG_EXEC = (1 << 2), // Executable channel + OMV_PROTOCOL_CHANNEL_FLAG_LOCK = (1 << 3), // Channel requires locking before read/write + OMV_PROTOCOL_CHANNEL_FLAG_STREAM = (1 << 4), // Streaming channel + OMV_PROTOCOL_CHANNEL_FLAG_DYNAMIC = (1 << 5), // Channel was dynamically created + OMV_PROTOCOL_CHANNEL_FLAG_PHYSICAL = (1 << 6), // Physical transport channel (not accessible via protocol) +} omv_protocol_channel_flags_t; + +// Helper macros +#define OMV_PROTOCOL_CHANNEL_FLAG_GET(channel, flag) \ + (((channel)->flags & OMV_PROTOCOL_CHANNEL_FLAG_##flag) != 0) + +#define OMV_PROTOCOL_CHANNEL_IS_TRANSPORT(channel) \ + (OMV_PROTOCOL_CHANNEL_FLAG_GET(channel, PHYSICAL) && \ + (channel)->read && (channel)->write && (channel)->size && \ + (channel)->is_active) + +// Forward declaration +typedef struct omv_protocol_channel omv_protocol_channel_t; + +// Channel interface (used for both transport and logical channels) +struct omv_protocol_channel { + void *priv; + uint8_t id; + uint32_t flags; + char name[OMV_PROTOCOL_CHANNEL_NAME_SIZE]; + int (*init) (const omv_protocol_channel_t *channel); + int (*deinit) (const omv_protocol_channel_t *channel); + bool (*poll) (const omv_protocol_channel_t *channel); + int (*lock) (const omv_protocol_channel_t *channel); + int (*unlock) (const omv_protocol_channel_t *channel); + size_t (*size) (const omv_protocol_channel_t *channel); + size_t (*shape) (const omv_protocol_channel_t *channel, size_t shape[4]); + int (*read) (const omv_protocol_channel_t *channel, uint32_t offset, size_t size, void *data); + int (*write) (const omv_protocol_channel_t *channel, uint32_t offset, size_t size, const void *data); + const void *(*readp) (const omv_protocol_channel_t *channel, uint32_t offset, size_t size); + int (*flush) (const omv_protocol_channel_t *channel); + int (*ioctl) (const omv_protocol_channel_t *channel, uint32_t cmd, size_t len, void *arg); + // Stdin-specific function to execute scripts internall (not exposed via ioctl). + bool (*exec) (const omv_protocol_channel_t *channel); + // Transport-specific functions (for channel ID 0 only) + bool (*is_active) (const omv_protocol_channel_t *channel); +}; + +// Default channels. +extern const omv_protocol_channel_t omv_usb_channel; +extern const omv_protocol_channel_t omv_stdin_channel; +extern const omv_protocol_channel_t omv_stdout_channel; +extern const omv_protocol_channel_t omv_stream_channel; +extern const omv_protocol_channel_t omv_profile_channel; + +#endif // __OMV_PROTOCOL_CHANNEL_H__ diff --git a/protocol/omv_protocol_channel_profile.c b/protocol/omv_protocol_channel_profile.c new file mode 100644 index 000000000..3c2b32136 --- /dev/null +++ b/protocol/omv_protocol_channel_profile.c @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2022-2024 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol Channels. + */ + +#include "omv_common.h" +#include "omv_profiler.h" +#include "omv_protocol.h" +#include "omv_boardconfig.h" + +#if OMV_PROFILER_ENABLE +static size_t profile_channel_size(const omv_protocol_channel_t *channel) { + return omv_profiler_get_size(); +} + +static size_t profile_channel_shape(const omv_protocol_channel_t *channel, size_t shape[4]) { + shape[0] = omv_profiler_get_size() / sizeof(omv_profiler_data_t); + shape[1] = sizeof(omv_profiler_data_t); + return 2; +} + +static int profile_channel_lock(const omv_protocol_channel_t *channel) { + size_t size = channel->size(channel); + return size && mutex_try_lock(omv_profiler_lock(), MUTEX_TID_IDE) ? 0 : -1; +} + +static int profile_channel_unlock(const omv_protocol_channel_t *channel) { + mutex_unlock(omv_profiler_lock(), MUTEX_TID_IDE); + return 0; +} + +static const void *profile_channel_readp(const omv_protocol_channel_t *channel, uint32_t offset, size_t size) { + const uint8_t *data = omv_profiler_get_data(); + if (offset + size > channel->size(channel)) { + return NULL; + } + return data + offset; +} + +static int profile_channel_ioctl(const omv_protocol_channel_t *channel, uint32_t cmd, size_t len, void *arg) { + union { + uint8_t bytes[16]; + uint32_t args[4]; + } u; + + memcpy(u.bytes, arg, len); + + switch (cmd) { + case OMV_CHANNEL_IOCTL_PROFILE_MODE: + omv_profiler_set_mode(u.args[0]); + return 0; + case OMV_CHANNEL_IOCTL_PROFILE_SET_EVENT: + omv_profiler_set_event(u.args[0], u.args[1]); + return 0; + case OMV_CHANNEL_IOCTL_PROFILE_RESET: + omv_profiler_reset(); + return 0; + default: + return -1; + } +} + +const omv_protocol_channel_t omv_profile_channel = { + .priv = NULL, + .id = OMV_PROTOCOL_CHANNEL_ID_PROFILE, + .name = "profile", + .flags = OMV_PROTOCOL_CHANNEL_FLAG_READ | + OMV_PROTOCOL_CHANNEL_FLAG_LOCK, + .lock = profile_channel_lock, + .unlock = profile_channel_unlock, + .size = profile_channel_size, + .shape = profile_channel_shape, + .readp = profile_channel_readp, + .ioctl = profile_channel_ioctl, +}; +#endif // OMV_PROFILER_ENABLE diff --git a/protocol/omv_protocol_channel_stdio.c b/protocol/omv_protocol_channel_stdio.c new file mode 100644 index 000000000..5a8afd258 --- /dev/null +++ b/protocol/omv_protocol_channel_stdio.c @@ -0,0 +1,219 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2022-2024 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol Channels. + */ + +#include "py/mphal.h" +#include "py/nlr.h" +#include "py/gc.h" +#include "py/ringbuf.h" +#include "py/runtime.h" +#include "shared/runtime/interrupt_char.h" +#include "shared/runtime/pyexec.h" + +#include "omv_common.h" +#include "omv_protocol.h" +#include "omv_boardconfig.h" + +#ifndef OMV_PROTOCOL_STDIO_BUFFER_SIZE +#define OMV_PROTOCOL_STDIO_BUFFER_SIZE (512) +#endif + + +typedef struct { + vstr_t vstrbuf; + bool script_running; + ringbuf_t ringbuf; + uint8_t rawbuf[OMV_PROTOCOL_STDIO_BUFFER_SIZE]; +} stdio_channel_context_t; + +static stdio_channel_context_t stdio_channel_ctx; + +static int stdin_channel_init(const omv_protocol_channel_t *channel) { + stdio_channel_context_t *ctx = channel->priv; + ctx->script_running = false; + vstr_init(&ctx->vstrbuf, 2048); + return 0; +} + +static int stdout_channel_init(const omv_protocol_channel_t *channel) { + stdio_channel_context_t *ctx = channel->priv; + + // Initialize ring buffer once to keep output from previous runs. + if (ctx->ringbuf.buf == NULL) { + ctx->ringbuf = (ringbuf_t) { + ctx->rawbuf, sizeof(ctx->rawbuf), 0, 0 + }; + } + return 0; +} + +void stdio_channel_pyexec_hook(bool running) { + stdio_channel_ctx.script_running = running; + omv_protocol_send_event(OMV_PROTOCOL_CHANNEL_ID_STDIN, running, false); +} + +static bool stdin_channel_poll(const omv_protocol_channel_t *channel) { + stdio_channel_context_t *ctx = channel->priv; + return ctx->script_running; +} + +static bool stdout_channel_poll(const omv_protocol_channel_t *channel) { + stdio_channel_context_t *ctx = channel->priv; + return ringbuf_avail(&ctx->ringbuf); +} + +static size_t stdio_channel_size(const omv_protocol_channel_t *channel) { + stdio_channel_context_t *ctx = channel->priv; + return ringbuf_avail(&ctx->ringbuf); +} + +static int stdio_channel_read(const omv_protocol_channel_t *channel, + uint32_t offset, size_t size, void *data) { + stdio_channel_context_t *ctx = channel->priv; + + size = OMV_MIN(size, ringbuf_avail(&ctx->ringbuf)); + return !ringbuf_get_bytes(&ctx->ringbuf, data, size) ? size : -1; +} + +static int stdio_channel_write(const omv_protocol_channel_t *channel, + uint32_t offset, size_t size, const void *data) { + stdio_channel_context_t *ctx = channel->priv; + + if (offset == 0) { + vstr_reset(&ctx->vstrbuf); + } + + nlr_buf_t nlr; + if (!gc_is_locked() && nlr_push(&nlr) == 0) { + char *buf = vstr_add_len(&ctx->vstrbuf, size); + memcpy(buf, data, size); + nlr_pop(); + return size; + } + + return -1; +} + +static bool stdin_channel_exec(const omv_protocol_channel_t *channel) { + stdio_channel_context_t *ctx = channel->priv; + + if (!vstr_len(&ctx->vstrbuf)) { + return false; + } + + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + // Execute the script. + pyexec_vstr(&ctx->vstrbuf, true); + nlr_pop(); + } else { + mp_obj_print_exception(&mp_plat_print, (mp_obj_t) nlr.ret_val); + } + + vstr_reset(&ctx->vstrbuf); + return true; +} + +static int stdio_channel_ioctl(const omv_protocol_channel_t *channel, uint32_t cmd, size_t len, void *arg) { + stdio_channel_context_t *ctx = channel->priv; + + switch (cmd) { + case OMV_CHANNEL_IOCTL_STDIN_STOP: + if (mp_interrupt_char != -1) { + mp_sched_vm_abort(); + mp_sched_keyboard_interrupt(); + } + break; + case OMV_CHANNEL_IOCTL_STDIN_EXEC: + if (!vstr_len(&ctx->vstrbuf)) { + return -1; + } + if (mp_interrupt_char != -1) { + mp_sched_vm_abort(); + mp_sched_keyboard_interrupt(); + } + break; + case OMV_CHANNEL_IOCTL_STDIN_RESET: + vstr_reset(&ctx->vstrbuf); + break; + default: + return -1; + } + + return 0; +} + +// Wrap MicroPython stdio functions to intercept REPL. +extern uintptr_t __real_mp_hal_stdio_poll(uintptr_t poll_flags); +uintptr_t __wrap_mp_hal_stdio_poll(uintptr_t poll_flags) { + if (!omv_protocol_is_active()) { + return __real_mp_hal_stdio_poll(poll_flags); + } + return 0; +} + +extern mp_uint_t __real_mp_hal_stdout_tx_strn(const char *str, mp_uint_t len); +mp_uint_t __wrap_mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) { + if (!omv_protocol_is_active()) { + return __real_mp_hal_stdout_tx_strn(str, len); + } + + const omv_protocol_channel_t *channel = omv_protocol_find_channel(OMV_PROTOCOL_CHANNEL_ID_STDOUT); + stdio_channel_context_t *ctx = channel->priv; + + // On overflow, reset the ring buffer, if this string fits + // entirely in the buffer, to recover from broken strings. + for (int i = 0; i < len; i++) { + if (ringbuf_put(&ctx->ringbuf, str[i]) == -1 && len <= ctx->ringbuf.size) { + ctx->ringbuf.iget = 0; + ctx->ringbuf.iput = 0; + ringbuf_put(&ctx->ringbuf, str[i]); + } + } + return len; +} + +const omv_protocol_channel_t omv_stdin_channel = { + .priv = &stdio_channel_ctx, + .id = OMV_PROTOCOL_CHANNEL_ID_STDIN, + .flags = OMV_PROTOCOL_CHANNEL_FLAG_WRITE | OMV_PROTOCOL_CHANNEL_FLAG_EXEC, + .name = "stdin", + .init = stdin_channel_init, + .poll = stdin_channel_poll, + .write = stdio_channel_write, + .ioctl = stdio_channel_ioctl, + .exec = stdin_channel_exec +}; + +const omv_protocol_channel_t omv_stdout_channel = { + .priv = &stdio_channel_ctx, + .id = OMV_PROTOCOL_CHANNEL_ID_STDOUT, + .flags = OMV_PROTOCOL_CHANNEL_FLAG_READ, + .name = "stdout", + .init = stdout_channel_init, + .poll = stdout_channel_poll, + .size = stdio_channel_size, + .read = stdio_channel_read, +}; diff --git a/protocol/omv_protocol_channel_stmusb.c b/protocol/omv_protocol_channel_stmusb.c new file mode 100644 index 000000000..3d94134cf --- /dev/null +++ b/protocol/omv_protocol_channel_stmusb.c @@ -0,0 +1,135 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2022-2024 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol USB Channel. + */ +#if OMV_USB_STACK_STMUSB +#include "py/stream.h" +#include "py/mphal.h" +#include "py/ringbuf.h" +#include "py/runtime.h" + +#include "omv_common.h" +#include "cmsis_gcc.h" +#include "omv_protocol.h" +#include "omv_boardconfig.h" +#undef MIN +#undef MAX +#include "usbd_cdc_msc_hid.h" +#include "usbd_cdc_interface.h" + +#ifndef OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS +#define OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS (1500) +#endif + +static bool usb_channel_active = false; + +static size_t usb_channel_size(const omv_protocol_channel_t *channel) { + usbd_cdc_itf_t *cdc = usb_vcp_get(0); + return usbd_cdc_rx_num(cdc); +} + +static bool usb_channel_is_active(const omv_protocol_channel_t *channel) { + return usb_channel_active; +} + +static int usb_channel_read(const omv_protocol_channel_t *channel, uint32_t offset, size_t size, void *data) { + usbd_cdc_itf_t *cdc = usb_vcp_get(0); + return usbd_cdc_rx(cdc, data, size, OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS); +} + +static int usb_channel_write(const omv_protocol_channel_t *channel, uint32_t offset, size_t size, const void *data) { + usbd_cdc_itf_t *cdc = usb_vcp_get(0); + return usbd_cdc_tx(cdc, data, size, OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS); +} + +static void usb_channel_task(mp_sched_node_t *node) { + if (usb_channel_active) { + omv_protocol_task(); + } +} + +int __real_mp_os_dupterm_rx_chr(void); +int __wrap_mp_os_dupterm_rx_chr(void) { + if (usb_channel_active) { + return -1; + } + return __real_mp_os_dupterm_rx_chr(); +} + +int8_t __real_usbd_cdc_receive(usbd_cdc_state_t *cdc_in, size_t len); +int8_t __wrap_usbd_cdc_receive(usbd_cdc_state_t *cdc_in, size_t len) { + static mp_sched_node_t usb_channel_node; + + // Ensure the CDC is detached from REPL, if the protocol is active, + // before calling receive as it raises exceptions on interrupt chars. + usbd_cdc_itf_t *cdc = usb_vcp_get(0); + if (usb_channel_active && cdc->attached_to_repl) { + cdc->attached_to_repl = false; + cdc->flow |= USBD_CDC_FLOWCONTROL_RTS | USBD_CDC_FLOWCONTROL_CTS; + } + + int8_t ret = __real_usbd_cdc_receive(cdc_in, len); + if (usb_channel_active) { + mp_sched_schedule_node(&usb_channel_node, usb_channel_task); + } + return ret; +} + +int8_t __real_usbd_cdc_control(usbd_cdc_state_t *cdc_in, uint8_t cmd, uint8_t *pbuf, uint16_t length); +int8_t __wrap_usbd_cdc_control(usbd_cdc_state_t *cdc_in, uint8_t cmd, uint8_t *pbuf, uint16_t length) { + usbd_cdc_itf_t *cdc = usb_vcp_get(0); + int8_t ret = __real_usbd_cdc_control(cdc_in, cmd, pbuf, length); + + if (cdc->bitrate != OMV_PROTOCOL_MAGIC_BAUDRATE || + cdc->connect_state == USBD_CDC_CONNECT_STATE_DISCONNECTED) { + // Reattach to REPL + cdc->rx_buf_put = 0; + cdc->rx_buf_get = 0; + cdc->rx_buf_full = false; + cdc->tx_need_empty_packet = 0; + cdc->attached_to_repl = true; + cdc->flow &= ~USBD_CDC_FLOWCONTROL_CTS; + } else if (cdc->bitrate == OMV_PROTOCOL_MAGIC_BAUDRATE) { + // Detach from REPL + cdc->attached_to_repl = false; + cdc->flow |= USBD_CDC_FLOWCONTROL_RTS | USBD_CDC_FLOWCONTROL_CTS; + } + + usb_channel_active = !cdc->attached_to_repl; + + return ret; +} + +// USB Channel +const omv_protocol_channel_t omv_usb_channel = { + .priv = NULL, + .id = OMV_PROTOCOL_CHANNEL_ID_TRANSPORT, + .name = "usb", + .flags = OMV_PROTOCOL_CHANNEL_FLAG_PHYSICAL, + .size = usb_channel_size, + .read = usb_channel_read, + .write = usb_channel_write, + .is_active = usb_channel_is_active +}; +#endif // OMV_USB_STACK_STM diff --git a/protocol/omv_protocol_channel_stream.c b/protocol/omv_protocol_channel_stream.c new file mode 100644 index 000000000..54bf91ec7 --- /dev/null +++ b/protocol/omv_protocol_channel_stream.c @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2022-2024 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol Channels. + */ + +#include "omv_common.h" +#include "omv_protocol.h" +#include "omv_boardconfig.h" +#include "framebuffer.h" + + +static int stream_channel_init(const omv_protocol_channel_t *channel) { + return channel->unlock(channel); +} + +static bool stream_channel_poll(const omv_protocol_channel_t *channel) { + return channel->size(channel) != 0; +} + +static int stream_channel_lock(const omv_protocol_channel_t *channel) { + framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); + size_t size = channel->size(channel); + // Attempt locking only if the stream is ready. + return size && mutex_try_lock(&fb->lock, MUTEX_TID_IDE) ? 0 : -1; +} + +static int stream_channel_unlock(const omv_protocol_channel_t *channel) { + framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); + if (mutex_unlock(&fb->lock, MUTEX_TID_IDE)) { + // Reset header even if we don't hold the lock + memset(fb->raw_base, 0, sizeof(framebuffer_header_t)); + } + return 0; +} + +static size_t stream_channel_size(const omv_protocol_channel_t *channel) { + framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); + framebuffer_header_t *hdr = (framebuffer_header_t *) fb->raw_base; + size_t size = hdr->is_compressed ? hdr->size : (hdr->width * hdr->height * hdr->bpp); + // Return header size + stream data size + return !size ? 0 : (size + sizeof(framebuffer_header_t)); +} + +static size_t stream_channel_shape(const omv_protocol_channel_t *channel, size_t shape[4]) { + framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); + + if (fb->is_compressed > 0) { + // Compressed: shape is (size,) + shape[0] = fb->size; + return 1; + } else { + // Uncompressed: shape is (w, h, bpp) + shape[0] = fb->w; + shape[1] = fb->h; + shape[2] = fb->bpp; + return 3; + } +} + +static int stream_channel_ioctl(const omv_protocol_channel_t *channel, uint32_t cmd, size_t len, void *arg) { + union { + uint8_t bytes[16]; + uint32_t args[4]; + } u; + + memcpy(u.bytes, arg, len); + framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); + + switch (cmd) { + case OMV_CHANNEL_IOCTL_STREAM_CTRL: + fb->enabled = u.args[0]; + // Reset stream buffer state + mutex_init0(&fb->lock); + memset(fb->raw_base, 0, sizeof(framebuffer_header_t)); + return 0; + case OMV_CHANNEL_IOCTL_STREAM_RAW_CFG: + fb->raw_w = u.args[0]; + fb->raw_h = u.args[1]; + return 0; + case OMV_CHANNEL_IOCTL_STREAM_RAW_CTRL: + fb->raw_enabled = u.args[0]; + return 0; + default: + return -1; + } +} + +static const void *stream_channel_readp(const omv_protocol_channel_t *channel, uint32_t offset, size_t size) { + framebuffer_t *fb = framebuffer_get(FB_STREAM_ID); + size_t available = channel->size(channel); + if (offset + size > available) { + return NULL; + } + // Return pointer to framebuffer data starting from header + return (uint8_t *) fb->raw_base + offset; +} + +const omv_protocol_channel_t omv_stream_channel = { + .priv = NULL, + .id = OMV_PROTOCOL_CHANNEL_ID_STREAM, + .name = "stream", + .flags = OMV_PROTOCOL_CHANNEL_FLAG_READ | + OMV_PROTOCOL_CHANNEL_FLAG_LOCK | + OMV_PROTOCOL_CHANNEL_FLAG_STREAM, + .init = stream_channel_init, + .poll = stream_channel_poll, + .lock = stream_channel_lock, + .unlock = stream_channel_unlock, + .size = stream_channel_size, + .shape = stream_channel_shape, + .readp = stream_channel_readp, + .ioctl = stream_channel_ioctl +}; diff --git a/protocol/omv_protocol_channel_tinyusb.c b/protocol/omv_protocol_channel_tinyusb.c new file mode 100644 index 000000000..db52f30fd --- /dev/null +++ b/protocol/omv_protocol_channel_tinyusb.c @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2022-2024 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol USB Channel. + */ +#if OMV_USB_STACK_TINYUSB +#include "py/stream.h" +#include "py/mphal.h" +#include "py/ringbuf.h" +#include "py/runtime.h" + +#include "omv_common.h" +#include "cmsis_gcc.h" +#include "omv_protocol.h" +#include "omv_boardconfig.h" +#include "tusb.h" + +#ifndef OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS +#define OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS (1500) +#endif + +static bool usb_channel_active; + +static size_t usb_channel_size(const omv_protocol_channel_t *channel) { + if (tud_task_event_ready()) { + tud_task_ext(0, false); + } + return tud_cdc_available(); +} + +static int usb_channel_flush(const omv_protocol_channel_t *channel) { + if (tud_task_event_ready()) { + tud_task_ext(0, false); + } + return tud_cdc_write_flush(); +} + +static bool usb_channel_is_active(const omv_protocol_channel_t *channel) { + return usb_channel_active; +} + +static int usb_channel_read(const omv_protocol_channel_t *channel, uint32_t offset, size_t size, void *data) { + size_t bytes = 0; + uint32_t start_ms = mp_hal_ticks_ms(); + while (bytes < size && !check_timeout_ms(start_ms, OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS)) { + bytes += tud_cdc_read((uint8_t *) data + bytes, size - bytes); + if (tud_task_event_ready()) { + tud_task_ext(0, false); + } + if (bytes < size) { + mp_event_handle_nowait(); + } + } + + return bytes; +} + +static int usb_channel_write(const omv_protocol_channel_t *channel, uint32_t offset, size_t size, const void *data) { + size_t bytes = 0; + uint32_t start_ms = mp_hal_ticks_ms(); + while (bytes < size && !check_timeout_ms(start_ms, OMV_PROTOCOL_USB_CHANNEL_TIMEOUT_MS)) { + bytes += tud_cdc_write((uint8_t *) data + bytes, size - bytes); + if (tud_task_event_ready()) { + tud_task_ext(0, false); + } + if (bytes < size) { + mp_event_handle_nowait(); + } + } + + return bytes; +} + +static void usb_channel_task(mp_sched_node_t *node) { + tud_task_ext(0, false); + if (omv_protocol_is_active()) { + omv_protocol_task(); + } +} + +// Wrap MicroPython TinyUSB functions to run our own task. +void __real_tud_cdc_rx_cb(uint8_t itf); +void __wrap_tud_cdc_rx_cb(uint8_t itf) { + if (!omv_protocol_is_active()) { + __real_tud_cdc_rx_cb(itf); + } +} + +void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *coding) { + usb_channel_active = (coding->bit_rate == OMV_PROTOCOL_MAGIC_BAUDRATE); +} + +void __real_tud_cdc_line_state_cb(uint8_t instance, bool dtr, bool rts); +void __wrap_tud_cdc_line_state_cb(uint8_t instance, bool dtr, bool rts) { + cdc_line_coding_t coding; + tud_cdc_get_line_coding(&coding); + + if (!dtr) { + usb_channel_active = false; + } else { + usb_channel_active = (coding.bit_rate == OMV_PROTOCOL_MAGIC_BAUDRATE); + } + + __real_tud_cdc_line_state_cb(instance, dtr, rts); +} + +// For the mimxrt, and nrf ports this replaces the weak USB IRQ handlers. +// For the RP2 port, this handler is installed in main.c +void OMV_USB1_IRQ_HANDLER(void) { + static mp_sched_node_t usb_channel_node; + + dcd_int_handler(0); + // If there are any event to process, schedule a call to channel task. + if (tud_task_event_ready()) { + mp_sched_schedule_node(&usb_channel_node, usb_channel_task); + } +} + +#ifdef OMV_USB2_IRQ_HANDLER +void OMV_USB2_IRQ_HANDLER(void) { + static mp_sched_node_t usb_channel_node; + + dcd_int_handler(1); + // If there are any event to process, schedule a call to channel task. + if (tud_task_event_ready()) { + mp_sched_schedule_node(&usb_channel_node, usb_channel_task); + } + +} +#endif // OMV_USB2_IRQ_HANDLER + +// USB Channel +const omv_protocol_channel_t omv_usb_channel = { + .priv = NULL, + .id = OMV_PROTOCOL_CHANNEL_ID_TRANSPORT, + .name = "usb", + .flags = OMV_PROTOCOL_CHANNEL_FLAG_PHYSICAL, + .size = usb_channel_size, + .read = usb_channel_read, + .write = usb_channel_write, + .flush = usb_channel_flush, + .is_active = usb_channel_is_active +}; +#endif // OMV_USB_STACK_TINYUSB diff --git a/protocol/omv_protocol_hw_caps.h b/protocol/omv_protocol_hw_caps.h new file mode 100644 index 000000000..a7b31dce6 --- /dev/null +++ b/protocol/omv_protocol_hw_caps.h @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV Protocol Hardware Capabilities Definitions + * This header provides macros for boards to easily define their hardware + * capabilities without manual bit manipulation. + */ +#ifndef __OMV_PROTOCOL_HW_CAPS_H__ +#define __OMV_PROTOCOL_HW_CAPS_H__ + +#include +#include "cmsis_gcc.h" + +#ifndef __PMU_NUM_EVENTCNT +#define __PMU_NUM_EVENTCNT 0 +#endif + +/*************************************************************************** +* Hardware Capability Bit Definitions +***************************************************************************/ + +#define OMV_PROTOCOL_HW_CAPS_HAS_GPU (1U << 0) // Graphics Processing Unit +#define OMV_PROTOCOL_HW_CAPS_HAS_NPU (1U << 1) // Neural Processing Unit +#define OMV_PROTOCOL_HW_CAPS_HAS_ISP (1U << 2) // Image Signal Processor +#define OMV_PROTOCOL_HW_CAPS_HAS_VENC (1U << 3) // Video encoder present +#define OMV_PROTOCOL_HW_CAPS_HAS_JPEG (1U << 4) // JPEG encoder present +#define OMV_PROTOCOL_HW_CAPS_HAS_DRAM (1U << 5) // DRAM present +#define OMV_PROTOCOL_HW_CAPS_HAS_CRC (1U << 6) // Hardware-accelerated CRC +#define OMV_PROTOCOL_HW_CAPS_HAS_PMU (1U << 7) // Performance Monitoring Unit +#define OMV_PROTOCOL_HW_CAPS_PMU_EVENTCNT ((__PMU_NUM_EVENTCNT & 0xFF) << 8) // PMU number of event counters +#define OMV_PROTOCOL_HW_CAPS_HAS_WIFI (1U << 16) // WiFi module present +#define OMV_PROTOCOL_HW_CAPS_HAS_BT (1U << 17) // Bluetooth available +#define OMV_PROTOCOL_HW_CAPS_HAS_SD (1U << 18) // SD card slot available +#define OMV_PROTOCOL_HW_CAPS_HAS_ETH (1U << 19) // Ethernet interface +#define OMV_PROTOCOL_HW_CAPS_HAS_USB_HS (1U << 20) // USB High-Speed capable +#define OMV_PROTOCOL_HW_CAPS_HAS_MULTICORE (1U << 21) // Multi-core processor + + +// Prefix paste +#define OMV_PROTOCOL_HW_CAPS_(x) OMV_PROTOCOL_HW_CAPS_##x + +// Expander helpers +#define EXPAND(x) x + +// Recursive apply with OR +#define FE_1(f, x) f(x) +#define FE_2(f, x, ...) f(x) | FE_1(f, __VA_ARGS__) +#define FE_3(f, x, ...) f(x) | FE_2(f, __VA_ARGS__) +#define FE_4(f, x, ...) f(x) | FE_3(f, __VA_ARGS__) +#define FE_5(f, x, ...) f(x) | FE_4(f, __VA_ARGS__) +#define FE_6(f, x, ...) f(x) | FE_5(f, __VA_ARGS__) +#define FE_7(f, x, ...) f(x) | FE_6(f, __VA_ARGS__) +#define FE_8(f, x, ...) f(x) | FE_7(f, __VA_ARGS__) +#define FE_9(f, x, ...) f(x) | FE_8(f, __VA_ARGS__) +#define FE_10(f, x, ...) f(x) | FE_9(f, __VA_ARGS__) +#define FE_11(f, x, ...) f(x) | FE_10(f, __VA_ARGS__) +#define FE_12(f, x, ...) f(x) | FE_11(f, __VA_ARGS__) +#define FE_13(f, x, ...) f(x) | FE_12(f, __VA_ARGS__) +#define FE_14(f, x, ...) f(x) | FE_13(f, __VA_ARGS__) +#define FE_15(f, x, ...) f(x) | FE_14(f, __VA_ARGS__) +#define FE_16(f, x, ...) f(x) | FE_15(f, __VA_ARGS__) +#define FE_17(f, x, ...) f(x) | FE_16(f, __VA_ARGS__) +#define FE_18(f, x, ...) f(x) | FE_17(f, __VA_ARGS__) +#define FE_19(f, x, ...) f(x) | FE_18(f, __VA_ARGS__) +#define FE_20(f, x, ...) f(x) | FE_19(f, __VA_ARGS__) +#define FE_21(f, x, ...) f(x) | FE_20(f, __VA_ARGS__) +#define FE_22(f, x, ...) f(x) | FE_21(f, __VA_ARGS__) +#define FE_23(f, x, ...) f(x) | FE_22(f, __VA_ARGS__) +#define FE_24(f, x, ...) f(x) | FE_23(f, __VA_ARGS__) +#define FE_25(f, x, ...) f(x) | FE_24(f, __VA_ARGS__) +#define FE_26(f, x, ...) f(x) | FE_25(f, __VA_ARGS__) +#define FE_27(f, x, ...) f(x) | FE_26(f, __VA_ARGS__) +#define FE_28(f, x, ...) f(x) | FE_27(f, __VA_ARGS__) +#define FE_29(f, x, ...) f(x) | FE_28(f, __VA_ARGS__) +#define FE_30(f, x, ...) f(x) | FE_29(f, __VA_ARGS__) +#define FE_31(f, x, ...) f(x) | FE_30(f, __VA_ARGS__) +#define FE_32(f, x, ...) f(x) | FE_31(f, __VA_ARGS__) + +// Dispatcher (up to 32 args) +#define GET_FE_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, \ + _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, \ + _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, \ + _31, _32, NAME, ...) NAME + +#define FOR_EACH(f, ...) \ + EXPAND(GET_FE_MACRO(__VA_ARGS__, \ + FE_32, FE_31, FE_30, FE_29, FE_28, FE_27, FE_26, FE_25, FE_24, FE_23, \ + FE_22, FE_21, FE_20, FE_19, FE_18, FE_17, FE_16, FE_15, FE_14, FE_13, \ + FE_12, FE_11, FE_10, FE_9, FE_8, FE_7, FE_6, FE_5, FE_4, FE_3, FE_2, FE_1 \ + ) (f, __VA_ARGS__)) + +// Main macro +#define OMV_PROTOCOL_HW_CAPS_MAKE(...) \ + FOR_EACH(OMV_PROTOCOL_HW_CAPS_, __VA_ARGS__) | OMV_PROTOCOL_HW_CAPS_PMU_EVENTCNT +#endif // __OMV_PROTOCOL_HW_CAPS_H__ diff --git a/protocol/protocol.mk b/protocol/protocol.mk new file mode 100644 index 000000000..db072ad55 --- /dev/null +++ b/protocol/protocol.mk @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (C) 2025 OpenMV, LLC. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# OpenMV Protocol Makefile + +PROTOCOL_SRC_C += \ + omv_protocol.c \ + omv_protocol_channel_stdio.c \ + omv_protocol_channel_stream.c \ + omv_protocol_channel_profile.c \ + +ifeq ($(OMV_USB_STACK_TINYUSB), 1) +CFLAGS += -DOMV_USB_STACK_TINYUSB=1 +PROTOCOL_SRC_C += omv_protocol_channel_tinyusb.c +endif + +ifeq ($(OMV_USB_STACK_STMUSB), 1) +CFLAGS += -DOMV_USB_STACK_STMUSB=1 +PROTOCOL_SRC_C += omv_protocol_channel_stmusb.c +endif + +CFLAGS += -I$(TOP_DIR)/protocol +OMV_FIRM_OBJ += $(addprefix $(BUILD)/protocol/, $(PROTOCOL_SRC_C:.c=.o)) + diff --git a/scripts/examples/12-Protocol/custom_channels.py b/scripts/examples/12-Protocol/custom_channels.py new file mode 100644 index 000000000..6a7a501ff --- /dev/null +++ b/scripts/examples/12-Protocol/custom_channels.py @@ -0,0 +1,72 @@ +import time +import protocol + + +class StaticChannel: + def __init__(self): + pass + + def size(self): + return len("HelloWorld!") + + def read(self, offset, size): + print(f"StaticChannel read {size} bytes from offset {offset}") + return "HelloWorld!" + + +class BufferChannel: + """A simple channel backed by a buffer that supports read/write operations.""" + + def __init__(self, buffer_size=1024): + self.buffer = bytearray(buffer_size) + self.data_size = 0 # Actual amount of valid data in buffer + + def init(self): + """Initialize the channel. Return None for success.""" + print(f"BufferChannel initialized with {len(self.buffer)} bytes") + + def deinit(self): + """Deinitialize the channel. Return None for success.""" + print("BufferChannel deinitialized") + + def poll(self): + """Check if channel has data available. Return bool.""" + return self.data_size > 0 + + def size(self): + """Return the amount of valid data in the buffer.""" + return self.data_size + + def read(self, offset, size): + """Read data from the buffer. Return bytes/bytearray.""" + if offset >= self.data_size: + return b"" # No data at this offset + + end_pos = min(offset + size, self.data_size) + data = bytes(self.buffer[offset:end_pos]) + print(f"BufferChannel read {len(data)} bytes from offset {offset}") + return data + + def write(self, offset, data): + """Write data to the buffer. data is a bytearray. Return bytes written or None.""" + if offset + len(data) > len(self.buffer): + # Truncate if it would exceed buffer size + available = len(self.buffer) - offset + data = data[:available] if available > 0 else b"" + + if len(data) > 0: + self.buffer[offset : offset + len(data)] = data + # Update data_size if we wrote beyond current end + self.data_size = max(self.data_size, offset + len(data)) + + print(f"BufferChannel wrote {len(data)} bytes to offset {offset}") + return len(data) # Return bytes written (or None for default success) + + +# Register custom channels +ch1 = protocol.register(name="static", backend=StaticChannel()) +ch2 = protocol.register(name="buffer", backend=BufferChannel()) + +while True: + ch1.send_event(0xAA, wait_ack=False) + time.sleep_ms(100) diff --git a/scripts/examples/12-Protocol/uart_transport.py b/scripts/examples/12-Protocol/uart_transport.py new file mode 100644 index 000000000..2c364a108 --- /dev/null +++ b/scripts/examples/12-Protocol/uart_transport.py @@ -0,0 +1,81 @@ +""" +UART Transport Example for OpenMV Protocol + +This example shows how to create a UART-based transport channel and register +built-in protocol channels in Python. + +The transport implements the physical layer interface required by the protocol: +- read(): Physical read from UART with timeout +- write(): Physical write to UART with timeout +- is_active(): Check if UART connection is available +- size(): Return number of bytes available to read +""" + +import time +import protocol +from machine import UART + +class StaticChannel: + def __init__(self): + pass + + def size(self): + return len("HelloWorld!") + + def read(self, offset, size): + print(f"StaticChannel read {size} bytes from offset {offset}") + return "HelloWorld!" + + +class UartTransport: + """UART-based transport channel for OpenMV Protocol""" + + def __init__(self, uart_id=1, baudrate=115200, timeout=1000, rxbuf=1024): + self.uart = UART( + uart_id, baudrate, rxbuf=rxbuf, timeout=timeout, timeout_char=500 + ) + self.buf = memoryview(bytearray(rxbuf)) + + def is_active(self): + return True + + def size(self): + return self.uart.any() + + def read(self, offset, size): + size = self.uart.readinto(self.buf, size) + return None if size is None else self.buf[:size] + + def write(self, offset, data): + return self.uart.write(data) + + def flush(self): + self.uart.flush() + + +if __name__ == "__main__": + MAX_PAYLOAD = 4096 - 10 - 2 + + # Initialize and configure the protocol + protocol.init( + crc=True, # Enable CRC + seq=True, # Enable sequence checking + ack=True, # Wait for CKs + events=True, # Enable async-events + soft_reboot=False, # Disable soft-reboots (required) + max_payload=MAX_PAYLOAD, # Max packet payload + rtx_retries=3, # Retransmission retry count + rtx_timeout_ms=500, # Timeout before retransmission (doubled after each try) + lock_interval_ms=10, # Minimum locking interval + timer_ms=10, # Schedules the protocol task every 10ms + ) + + # Register the transport + protocol.register( + name="uart", + flags=protocol.CHANNEL_FLAG_PHYSICAL, + backend=UartTransport(7, timeout=5000, rxbuf=8 * 1024, baudrate=921600), + ) + + # Register custom channel + protocol.register(name="static", backend=StaticChannel()) diff --git a/tools/openmv/__init__.py b/tools/openmv/__init__.py new file mode 100644 index 000000000..5a8919ec3 --- /dev/null +++ b/tools/openmv/__init__.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Protocol Package +# +# This package provides a Python implementation of the OpenMV Protocol +# for communicating with OpenMV cameras. +# +# Main classes: +# OMVCamera: High-level camera interface with channel operations +# +# Main exceptions: +# OMVPException: Base exception for protocol errors +# OMVPTimeoutException: Timeout during protocol operations +# OMVPChecksumException: CRC validation failures +# OMVPSequenceException: Sequence number validation failures + +from .camera import OMVCamera +from .exceptions import ( + OMVPException, + OMVPTimeoutException, + OMVPChecksumException, + OMVPSequenceException +) + +__version__ = "2.0.0" + +__all__ = [ + 'OMVCamera', + 'OMVPException', + 'OMVPTimeoutException', + 'OMVPChecksumException', + 'OMVPSequenceException' +] diff --git a/tools/openmv/buffer.py b/tools/openmv/buffer.py new file mode 100644 index 000000000..d2708a4b3 --- /dev/null +++ b/tools/openmv/buffer.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Protocol Ring Buffer +# +# This module provides an efficient ring buffer implementation for +# packet parsing with optimized memory operations using memoryview. + +import struct + +class OMVRingBuffer: + """Efficient ring buffer for packet parsing""" + def __init__(self, size=4096): + self.size = size + self.data = memoryview(bytearray(size)) + self.start = 0 # Read position + self.end = 0 # Write position + self.count = 0 # Number of bytes in buffer + + def __len__(self): + return self.count + + def extend(self, data): + """Add data to buffer - optimized with memoryview""" + data_len = len(data) + data_view = memoryview(data) + if data_len > self.size - self.count: + raise BufferError(f"Buffer overflow:") + + # Calculate contiguous space from end to buffer boundary + space_to_end = self.size - self.end + + if data_len <= space_to_end: + # All data fits without wrapping + self.data[self.end:self.end + data_len] = data_view[:data_len] + self.end = (self.end + data_len) % self.size + else: + # First part: from end to buffer boundary + self.data[self.end:self.size] = data_view[:space_to_end] + # Second part: from buffer start + remaining = data_len - space_to_end + self.data[:remaining] = data_view[space_to_end:data_len] + self.end = remaining + + self.count += data_len + + def peek(self, size): + """Peek at data without consuming - returns memoryview when possible""" + if size > self.count: + return None + + if self.start + size <= self.size: + # No wrap-around - return memoryview (zero-copy) + return self.data[self.start:self.start + size] + else: + # Wrap-around case - must create new bytes object + first_part = self.size - self.start + # Use tobytes() for memoryview concatenation + return (self.data[self.start:].tobytes() + + self.data[:size - first_part].tobytes()) + + def peek16(self): + """Peek at 16-bit value at start of buffer""" + if self.count < 2: + return None + elif self.start + 2 <= self.size: + # No wrap-around - use struct directly on memoryview + return struct.unpack(' 0: + self.pending_channel_events -= 1 + self.channels_by_id = self._channel_list() + self.channels_by_name = {ch['name']: cid for cid, ch in self.channels_by_id.items()} + logging.info(f"Registered channels ({len(self.channels_by_id)}):") + for cid, ch in self.channels_by_id.items(): + logging.info(f" ID: {cid}, Flags: 0x{ch['flags']:02X}, Name: {ch['name']}") + + def get_channel(self, name=None, channel_id=None): + """Get channel ID by name or channel name by ID with lazy loading""" + if self.pending_channel_events > 0: + self.update_channels() + + if name is not None: + # Return channel ID for given name + return self.channels_by_name.get(name) + elif channel_id is not None: + # Return channel name given ID + return self.channels_by_id.get(channel_id)["name"] + else: + raise ValueError("Must specify either name or channel_id") + + def connect(self): + """Establish connection to the OpenMV camera""" + try: + self._serial = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + + # Use the protocol defaults for the initial connection + self.transport = OMVTransport(self._serial, crc=True, seq=True, + max_payload=OMVProto.MIN_PAYLOAD_SIZE, timeout=self.timeout, + event_callback=self._handle_event, drop_rate=self.drop_rate) + + # Perform resync + self._resync() + + # Set protocol configuration to user-requested values + self.update_capabilities() + + # Update transport with final negotiated capabilities + self.transport.update_caps(self.caps['crc'], self.caps['seq'], + self.caps['ack'], self.caps['max_payload']) + + # Cache channel list + self.update_channels() + + # Cache system info + self.sysinfo = self.system_info() + + # Print system information + self.print_system_info() + except Exception as e: + self.disconnect() + raise OMVPException(f"Failed to connect: {e}") + + def disconnect(self): + """Close connection to the OpenMV camera""" + if self._serial: + self._serial.close() + self._serial = None + self.transport = None + + def is_connected(self): + """Check if connected to camera""" + return self._serial is not None and self._serial.is_open + + def host_stats(self): + """Get transport statistics""" + return self.transport.stats + + @retry_if_failed + def device_stats(self): + """Get protocol statistics""" + payload = self._send_cmd_wait_resp(OMVPOpcode.PROTO_STATS) + + if len(payload) < 32: + raise OMVPException(f"Invalid PROTO_STATS payload size: {len(payload)}") + + # Unpack the structure: 8 uint32_t fields (32 bytes total) + data = struct.unpack('<8I', payload) + + return { + 'sent': data[0], + 'received': data[1], + 'checksum': data[2], + 'sequence': data[3], + 'retransmit': data[4], + 'transport': data[5], + 'sent_events': data[6], + 'max_ack_queue_depth': data[7] + } + + @retry_if_failed + def reset(self): + """Reset the camera""" + self._send_cmd_wait_resp(OMVPOpcode.SYS_RESET) + + @retry_if_failed + def boot(self): + """Jump to bootloader""" + self._send_cmd_wait_resp(OMVPOpcode.SYS_BOOT) + + @retry_if_failed + def update_capabilities(self): + """Set device capabilities""" + payload = self._send_cmd_wait_resp(OMVPOpcode.PROTO_GET_CAPS) + flags, max_payload = struct.unpack(' 0: + self.update_channels() + for name, channel_id in self.channels_by_name.items(): + result[name] = bool(flags & (1 << channel_id)) + + return result + + @retry_if_failed + def profiler_reset(self): + """Reset the profiler data""" + if profile_id := self.get_channel(name="profile"): + self._channel_ioctl(profile_id, OMVPChannelIOCTL.PROFILE_RESET) + logging.debug("Profiler reset") + + @retry_if_failed + def profiler_mode(self, exclusive=False): + """Set profiler mode (exclusive=True for exclusive, False for inclusive)""" + if profile_id := self.get_channel(name="profile"): + mode = 1 if exclusive else 0 + self._channel_ioctl(profile_id, OMVPChannelIOCTL.PROFILE_MODE, 'I', mode) + logging.debug(f"Profile mode set to {'exclusive' if exclusive else 'inclusive'}") + + @retry_if_failed + def profiler_event_type(self, counter_num, event_id): + """Configure an event counter to monitor a specific event""" + if profile_id := self.get_channel(name="profile"): + self._channel_ioctl(profile_id, OMVPChannelIOCTL.PROFILE_SET_EVENT, 'II', counter_num, event_id) + logging.debug(f"Event counter {counter_num} set to event 0x{event_id:04X}") + + @retry_if_failed + def read_profile(self): + """Read profiler data from the profile channel""" + # Check if profile channel is available (replaces profile_enabled check) + profile_id = self.get_channel(name="profile") + if not profile_id: + return None + + # TODO just subtract the known record size + # Get event count from cached system info (pmu_eventcnt field) + event_count = self.sysinfo['pmu_eventcnt'] + + # Lock the profile channel + if not self._channel_lock(profile_id): + return None + + try: + # Get profile data shape and calculate size + shape = self._channel_shape(profile_id) + if len(shape) < 2: + return None + + profile_size = reduce(mul, shape) + if profile_size == 0: + return None + + record_count, record_size = shape[0], shape[1] + + # Read raw profile data using calculated size + data = self._channel_read(profile_id, 0, profile_size) + if len(data) == 0: + return None + + # Parse profile records + records = [] + record_format = f"<5I2Q{event_count}QI" + + for i in range(record_count): + offset = i * record_size + if offset + record_size > len(data): + break + + # Unpack the record + profile = struct.unpack(record_format, data[offset:offset + record_size]) + + # Parse the profile data + records.append({ + 'address': profile[0], + 'caller': profile[1], + 'call_count': profile[2], + 'min_ticks': profile[3], + 'max_ticks': profile[4], + 'total_ticks': profile[5], + 'total_cycles': profile[6], + 'events': profile[7:7 + event_count] + }) + + return records + finally: + self._channel_unlock(profile_id) + + @retry_if_failed + def read_stdout(self): + """Read text output buffer""" + stdout_id = self.get_channel(name="stdout") + if size := self._channel_size(stdout_id): + data = self._channel_read(stdout_id, 0, size) + return bytes(data).decode('utf-8', errors='ignore') + + @retry_if_failed + def read_frame(self): + """Read stream buffer data with header at the beginning and convert to RGB888""" + stream_id = self.get_channel(name="stream") + + # Lock the stream buffer + if not self._channel_lock(stream_id): + return None + + self.frame_event = False + try: + # Get total size (16-byte header + stream data) + if (size := self._channel_size(stream_id)) <= 16: + return None + + # Read all data (header + stream data) + data = self._channel_read(stream_id, 0, size) + if len(data) < 16: + return None + + # Parse stream header: width(4), height(4), pixformat(4), depth/size(4) + width, height, pixformat, depth = struct.unpack('> 8) & 0xFF, # 8 bits starting at bit 8 + 'wifi_present': bool(capabilities & (1 << 16)), + 'bt_present': bool(capabilities & (1 << 17)), + 'sd_present': bool(capabilities & (1 << 18)), + 'eth_present': bool(capabilities & (1 << 19)), + 'usb_highspeed': bool(capabilities & (1 << 20)), + 'multicore_present': bool(capabilities & (1 << 21)), + 'flash_size_kb': data[11], + 'ram_size_kb': data[12], + 'framebuffer_size_kb': data[13], + 'stream_buffer_size_kb': data[14], + 'firmware_version': data[17], + 'protocol_version': data[18], + 'bootloader_version': data[19] + } + + def print_system_info(self): + """Print formatted system information""" + logging.info("=== OpenMV System Information ===") + + # Print registered channels + logging.info(f"CPU ID: 0x{self.sysinfo['cpu_id']:08X}") + + # Device ID is now an array of 3 words + dev_id_hex = ''.join(f"{word:08X}" for word in self.sysinfo['device_id']) + logging.info(f"Device ID: {dev_id_hex}") + + # Sensor Chip IDs are now an array of 3 words + for i, chip_id in enumerate(self.sysinfo['sensor_chip_id']): + if chip_id != 0: # Only show non-zero chip IDs + logging.info(f"CSI{i}: 0x{chip_id:08X}") + + # Memory info + if self.sysinfo['flash_size_kb'] > 0: + logging.info(f"Flash: {self.sysinfo['flash_size_kb']} KB") + if self.sysinfo['ram_size_kb'] > 0: + logging.info(f"RAM: {self.sysinfo['ram_size_kb']} KB") + if self.sysinfo['framebuffer_size_kb'] > 0: + logging.info(f"Framebuffer: {self.sysinfo['framebuffer_size_kb']} KB") + if self.sysinfo['stream_buffer_size_kb'] > 0: + logging.info(f"Stream Buffer: {self.sysinfo['stream_buffer_size_kb']} KB") + + # Hardware capabilities + logging.info("Hardware capabilities:") + logging.info(f" GPU: {'Yes' if self.sysinfo['gpu_present'] else 'No'}") + logging.info(f" NPU: {'Yes' if self.sysinfo['npu_present'] else 'No'}") + logging.info(f" ISP: {'Yes' if self.sysinfo['isp_present'] else 'No'}") + logging.info(f" Video Encoder: {'Yes' if self.sysinfo['venc_present'] else 'No'}") + logging.info(f" JPEG Encoder: {'Yes' if self.sysinfo['jpeg_present'] else 'No'}") + logging.info(f" DRAM: {'Yes' if self.sysinfo['dram_present'] else 'No'}") + logging.info(f" CRC Hardware: {'Yes' if self.sysinfo['crc_present'] else 'No'}") + logging.info(f" PMU: {'Yes' if self.sysinfo['pmu_present'] else 'No'} " + f"({self.sysinfo['pmu_eventcnt']} counters)") + logging.info(f" Multi-core: {'Yes' if self.sysinfo['multicore_present'] else 'No'}") + logging.info(f" WiFi: {'Yes' if self.sysinfo['wifi_present'] else 'No'}") + logging.info(f" Bluetooth: {'Yes' if self.sysinfo['bt_present'] else 'No'}") + logging.info(f" SD Card: {'Yes' if self.sysinfo['sd_present'] else 'No'}") + logging.info(f" Ethernet: {'Yes' if self.sysinfo['eth_present'] else 'No'}") + logging.info(f" USB High-Speed: {'Yes' if self.sysinfo['usb_highspeed'] else 'No'}") + + # Profiler info - check if profile channel is available + profile_available = self.get_channel(name="profile") is not None + logging.info(f"Profiler: {'Available' if profile_available else 'Not available'}") + + # Version info + fw = self.sysinfo['firmware_version'] + proto = self.sysinfo['protocol_version'] + boot = self.sysinfo['bootloader_version'] + logging.info(f"Firmware version: {fw[0]}.{fw[1]}.{fw[2]}") + logging.info(f"Protocol version: {proto[0]}.{proto[1]}.{proto[2]}") + logging.info(f"Bootloader version: {boot[0]}.{boot[1]}.{boot[2]}") + logging.info(f"Protocol capabilities: CRC={self.caps['crc']}, SEQ={self.caps['seq']}, " + f"ACK={self.caps['ack']}, EVENTS={self.caps['events']}, " + f"PAYLOAD={self.caps['max_payload']}") + logging.info("=================================") diff --git a/tools/openmv/constants.py b/tools/openmv/constants.py new file mode 100644 index 000000000..4b8c63925 --- /dev/null +++ b/tools/openmv/constants.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Protocol Constants +# +# This module defines all the constants used in the OpenMV Protocol +# including opcodes, status codes, flags, and other definitions. + +from enum import IntEnum + +class OMVPFlags(IntEnum): + """OpenMV Protocol Packet Flags""" + ACK = (1 << 0) + NAK = (1 << 1) + RTX = (1 << 2) + ACK_REQ = (1 << 3) + FRAGMENT = (1 << 4) + EVENT = (1 << 5) + + +class OMVPState(IntEnum): + """OpenMV Protocol Parser State Machine States""" + SYNC = 0 + HEADER = 1 + PAYLOAD = 2 + + +class OMVProto(IntEnum): + """OpenMV Protocol Constants""" + SYNC_WORD = 0xD5AA + HEADER_SIZE = 10 + CRC_SIZE = 2 + MIN_PAYLOAD_SIZE = 64 - 10 - 2 # 64 - OMV_PROTOCOL_HEADER_SIZE - 2 = 52 + + +class OMVPStatus(IntEnum): + """OpenMV Protocol Status Codes""" + SUCCESS = 0x00 + FAILED = 0x01 + INVALID = 0x02 + TIMEOUT = 0x03 + BUSY = 0x04 + CHECKSUM = 0x05 + SEQUENCE = 0x06 + OVERFLOW = 0x07 + FRAGMENT = 0x08 + UNKNOWN = 0x09 + +class OMVPOpcode(IntEnum): + """OpenMV Protocol Operation Codes""" + # Protocol commands + PROTO_SYNC = 0x00 + PROTO_GET_CAPS = 0x01 + PROTO_SET_CAPS = 0x02 + PROTO_STATS = 0x03 + + # System commands + SYS_RESET = 0x10 + SYS_BOOT = 0x11 + SYS_INFO = 0x12 + SYS_EVENT = 0x13 + + # Channel commands + CHANNEL_LIST = 0x20 + CHANNEL_POLL = 0x21 + CHANNEL_LOCK = 0x22 + CHANNEL_UNLOCK = 0x23 + CHANNEL_SHAPE = 0x24 + CHANNEL_SIZE = 0x25 + CHANNEL_READ = 0x26 + CHANNEL_WRITE = 0x27 + CHANNEL_IOCTL = 0x28 + CHANNEL_EVENT = 0x29 + + +class OMVPEventType(IntEnum): + """OpenMV Protocol Event Types""" + CHANNEL_REGISTERED = 0x00 + CHANNEL_UNREGISTERED = 0x01 + SOFT_REBOOT = 0x02 + + +class OMVPChannelIOCTL(IntEnum): + """OpenMV Protocol Channel IOCTL Commands""" + # Stdin channel IOCTLs + STDIN_STOP = 0x01 # Stop running script + STDIN_EXEC = 0x02 # Execute script + STDIN_RESET = 0x03 # Reseet script buffer + + # Stream channel IOCTLs + STREAM_CTRL = 0x00 # Enable/disable streaming + STREAM_RAW_CTRL = 0x01 # Enable/disable raw streaming + STREAM_RAW_CFG = 0x02 # Set raw stream resolution + + # Profile channel IOCTLs + PROFILE_MODE = 0x00 # Set profiling mode + PROFILE_SET_EVENT = 0x01 # Set event type to profile + PROFILE_RESET = 0x02 # Reset profiler data + + diff --git a/tools/openmv/crc.py b/tools/openmv/crc.py new file mode 100644 index 000000000..51890a61c --- /dev/null +++ b/tools/openmv/crc.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# CRC-16 implementation for OpenMV Protocol +# +# This module provides CRC-16 calculation using polynomial 0xBAAD. + +# CRC-16 lookup table for polynomial 0xBAAD +_CRC16_TABLE = [ + 0x0000, 0xBAAD, 0xCFF7, 0x755A, 0x2543, 0x9FEE, 0xEAB4, 0x5019, + 0x4A86, 0xF02B, 0x8571, 0x3FDC, 0x6FC5, 0xD568, 0xA032, 0x1A9F, + 0x950C, 0x2FA1, 0x5AFB, 0xE056, 0xB04F, 0x0AE2, 0x7FB8, 0xC515, + 0xDF8A, 0x6527, 0x107D, 0xAAD0, 0xFAC9, 0x4064, 0x353E, 0x8F93, + 0x90B5, 0x2A18, 0x5F42, 0xE5EF, 0xB5F6, 0x0F5B, 0x7A01, 0xC0AC, + 0xDA33, 0x609E, 0x15C4, 0xAF69, 0xFF70, 0x45DD, 0x3087, 0x8A2A, + 0x05B9, 0xBF14, 0xCA4E, 0x70E3, 0x20FA, 0x9A57, 0xEF0D, 0x55A0, + 0x4F3F, 0xF592, 0x80C8, 0x3A65, 0x6A7C, 0xD0D1, 0xA58B, 0x1F26, + 0x9BC7, 0x216A, 0x5430, 0xEE9D, 0xBE84, 0x0429, 0x7173, 0xCBDE, + 0xD141, 0x6BEC, 0x1EB6, 0xA41B, 0xF402, 0x4EAF, 0x3BF5, 0x8158, + 0x0ECB, 0xB466, 0xC13C, 0x7B91, 0x2B88, 0x9125, 0xE47F, 0x5ED2, + 0x444D, 0xFEE0, 0x8BBA, 0x3117, 0x610E, 0xDBA3, 0xAEF9, 0x1454, + 0x0B72, 0xB1DF, 0xC485, 0x7E28, 0x2E31, 0x949C, 0xE1C6, 0x5B6B, + 0x41F4, 0xFB59, 0x8E03, 0x34AE, 0x64B7, 0xDE1A, 0xAB40, 0x11ED, + 0x9E7E, 0x24D3, 0x5189, 0xEB24, 0xBB3D, 0x0190, 0x74CA, 0xCE67, + 0xD4F8, 0x6E55, 0x1B0F, 0xA1A2, 0xF1BB, 0x4B16, 0x3E4C, 0x84E1, + 0x8D23, 0x378E, 0x42D4, 0xF879, 0xA860, 0x12CD, 0x6797, 0xDD3A, + 0xC7A5, 0x7D08, 0x0852, 0xB2FF, 0xE2E6, 0x584B, 0x2D11, 0x97BC, + 0x182F, 0xA282, 0xD7D8, 0x6D75, 0x3D6C, 0x87C1, 0xF29B, 0x4836, + 0x52A9, 0xE804, 0x9D5E, 0x27F3, 0x77EA, 0xCD47, 0xB81D, 0x02B0, + 0x1D96, 0xA73B, 0xD261, 0x68CC, 0x38D5, 0x8278, 0xF722, 0x4D8F, + 0x5710, 0xEDBD, 0x98E7, 0x224A, 0x7253, 0xC8FE, 0xBDA4, 0x0709, + 0x889A, 0x3237, 0x476D, 0xFDC0, 0xADD9, 0x1774, 0x622E, 0xD883, + 0xC21C, 0x78B1, 0x0DEB, 0xB746, 0xE75F, 0x5DF2, 0x28A8, 0x9205, + 0x16E4, 0xAC49, 0xD913, 0x63BE, 0x33A7, 0x890A, 0xFC50, 0x46FD, + 0x5C62, 0xE6CF, 0x9395, 0x2938, 0x7921, 0xC38C, 0xB6D6, 0x0C7B, + 0x83E8, 0x3945, 0x4C1F, 0xF6B2, 0xA6AB, 0x1C06, 0x695C, 0xD3F1, + 0xC96E, 0x73C3, 0x0699, 0xBC34, 0xEC2D, 0x5680, 0x23DA, 0x9977, + 0x8651, 0x3CFC, 0x49A6, 0xF30B, 0xA312, 0x19BF, 0x6CE5, 0xD648, + 0xCCD7, 0x767A, 0x0320, 0xB98D, 0xE994, 0x5339, 0x2663, 0x9CCE, + 0x135D, 0xA9F0, 0xDCAA, 0x6607, 0x361E, 0x8CB3, 0xF9E9, 0x4344, + 0x59DB, 0xE376, 0x962C, 0x2C81, 0x7C98, 0xC635, 0xB36F, 0x09C2 +] + +def crc16(data, init_value=0xFFFF): + """ + Calculate CRC-16 with polynomial 0xBAAD using lookup table. + + Args: + data: bytes-like object to calculate CRC for + init_value: initial CRC value (default: 0xFFFF) + + Returns: + 16-bit CRC value as integer + """ + crc = init_value + for byte in data: + index = (crc >> 8) ^ byte + crc = (crc << 8) ^ _CRC16_TABLE[index] + crc &= 0xFFFF # Keep it 16-bit + + return crc diff --git a/tools/openmv/examples/basic.py b/tools/openmv/examples/basic.py new file mode 100644 index 000000000..47cb2bc89 --- /dev/null +++ b/tools/openmv/examples/basic.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Protocol Test Script +# +# This module provides a test script for the OpenMV Protocol implementation. +# It demonstrates basic camera operations including script execution and data +# channel communication. + +import sys +import os +import argparse +import time +import logging +import random +import signal + +# Add parent directories to path for openmv module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) + +from openmv.camera import OMVCamera + +# Test script - +test_script = """ +import time +import csi +import image + +csi0 = csi.CSI() +csi0.reset() +csi0.pixformat(csi.RGB565) +csi0.framesize(csi.VGA) + +clock = time.clock() + +while(True): + clock.tick() + img = csi0.snapshot() + print(clock.fps(), " FPS") +""" + + +def str2bool(v): + """Convert string to boolean for argparse""" + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + +def signal_handler(sig, frame): + """Handle interrupt signals""" + logging.info("Interrupt signal received, stopping...") + sys.exit(0) + +def main(): + parser = argparse.ArgumentParser(description='OpenMV Protocol Test') + parser.add_argument('--port', action='store', help='Serial port (dev/ttyACM0)', default='/dev/ttyACM0') + parser.add_argument("--script", action="store", default=None, help="Script file") + parser.add_argument('--timeout', action='store', type=float, default=1.0, help='Protocol timeout in seconds') + parser.add_argument('--debug', action='store_true', help='Enable debug logging') + + # Camera configuration options + parser.add_argument('--baudrate', type=int, default=921600, help='Serial baudrate (default: 921600)') + parser.add_argument('--crc', type=str2bool, nargs='?', const=True, default=True, help='Enable CRC validation (default: true)') + parser.add_argument('--seq', type=str2bool, nargs='?', const=True, default=True, help='Enable sequence number validation (default: true)') + parser.add_argument('--ack', type=str2bool, nargs='?', const=True, default=False, help='Enable packet acknowledgment (default: false)') + parser.add_argument('--events', type=str2bool, nargs='?', const=True, default=True, help='Enable event notifications (default: true)') + parser.add_argument('--max-retry', type=int, default=3, help='Maximum number of retries (default: 3)') + parser.add_argument('--max-payload', type=int, default=4096, help='Maximum payload size in bytes (default: 4096)') + parser.add_argument('--drop-rate', type=float, default=0.0, help='Packet drop simulation rate (0.0-1.0, default: 0.0)') + parser.add_argument('--quiet', action='store_true', help='Suppress script output text') + + args = parser.parse_args() + + # Set up signal handler for graceful shutdown + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Configure logging + if args.quiet: + log_level = logging.WARNING + elif args.debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logging.basicConfig( + format="%(relativeCreated)010.3f - %(message)s", + level=log_level, + ) + + if args.script is None: + args.script = test_script + logging.info("Using built-in test script...") + else: + with open(args.script, 'r') as f: + args.script = f.read() + logging.info("Loaded script from file") + + try: + with OMVCamera(args.port, baudrate=args.baudrate, crc=args.crc, seq=args.seq, + ack=args.ack, events=args.events, timeout=args.timeout, max_retry=args.max_retry, + max_payload=args.max_payload, drop_rate=args.drop_rate) as camera: + logging.critical(f"Connected to OpenMV camera on {args.port}") + + data = camera.read_profile() + if data: + logging.info(f"Profile data ({len(data)} entries):") + # Print first 5 entries to avoid overwhelming output + for i, entry in enumerate(data[:min(5, len(data))]): + logging.info(f"Entry {i}: address=0x{entry['address']:08X}, " + f"caller=0x{entry['caller']:08X}, calls={entry['call_count']}, " + f"ticks={entry['total_ticks']}") + if len(data) > 5: + logging.info(f"... and {len(data) - min(5, len(data))} more entries") + + # Execute script + logging.info("Executing script...") + camera.streaming(True, raw=False, res=(256, 256)) + camera.exec(args.script) + logging.info("Script executed successfully") + + # Let script run for a few seconds + test_time = 1 + start_time = time.time() + while time.time() - start_time < test_time: + if not (status := camera.read_status()): + continue + + if status['stdout'] and (text := camera.read_stdout()): + if not args.quiet: + print(text, end='') + + if status['stream'] and (frame := camera.read_frame()): + logging.info(f"Frame: {frame['width']}x{frame['height']} depth={frame['depth']} " + f"format=0x{frame['format']:08X} length: {len(frame['data'])} bytes") + + # Test custom channel + if data := camera.channel_read("time"): + logging.info("Channel output: " + bytes(data).decode('utf-8', errors='ignore')) + + if camera.has_channel("buffer"): + random_data = bytes([random.randint(0, 255) for _ in range(10)]) + logging.info(f"Writing random data: {random_data.hex()}") + camera.channel_write("buffer", random_data) + if data := camera.channel_read("buffer"): + logging.info(f"Read back data: {bytes(data).hex()}") + if bytes(data) == random_data: + logging.info("✓ Data verification successful!") + else: + logging.warning("✗ Data mismatch!") + + # Stop the script + logging.info("Stopping script...") + camera.stop() + + # Read output for a while + start_time = time.time() + while time.time() - start_time < 1: + if status['stdout'] and (text := camera.read_stdout()): + if not args.quiet: + print(text, end='') + + print("") + host_stats = camera.host_stats() + logging.critical("=========== Host Statistics ===========") + logging.critical(f"{host_stats}") + logging.critical(f"==========================================") + + device_stats = camera.device_stats() + logging.critical("=========== Device Statistics ===========") + logging.critical(f"{device_stats}") + logging.critical(f"==========================================") + + except KeyboardInterrupt: + logging.info("Interrupted by user") + sys.exit(0) + except Exception as e: + logging.error(f"Error: {e}") + import traceback + logging.error(f"{traceback.format_exc()}") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/tools/openmv/examples/framebuffer.py b/tools/openmv/examples/framebuffer.py new file mode 100644 index 000000000..489181849 --- /dev/null +++ b/tools/openmv/examples/framebuffer.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Framebuffer Display Example +# +# This example demonstrates how to display live video from an OpenMV camera using pygame. +# It shows the basic functionality of connecting to a camera, executing a script, and +# displaying the framebuffer data in real-time with FPS counter. +# +# Controls: +# - C key: Capture screenshot to 'capture.png' +# - ESC key: Exit +# +# Dependencies: +# - pygame +# - numpy + +import sys +import os +import argparse +import time +import logging +import pygame +import numpy as np + +# Add parent directories to path for openmv module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) + +from openmv.camera import OMVCamera + +def str2bool(v): + """Convert string to boolean for argparse""" + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + +# Benchmark script for throughput testing +bench_script = """ +import sensor, image, time +sensor.reset() +sensor.set_pixformat(sensor.RGB565) +sensor.set_framesize(sensor.QVGA) +img = sensor.snapshot().compress() +while(True): + img.flush() +""" + +# Default test script for sensor-based cameras +test_script = """ +import sensor, image, time +sensor.reset() +sensor.set_pixformat(sensor.RGB565) +sensor.set_framesize(sensor.QVGA) +sensor.skip_frames(time = 2000) +clock = time.clock() + +while(True): + clock.tick() + img = sensor.snapshot() + print(clock.fps(), " FPS") +""" + +def main(): + parser = argparse.ArgumentParser(description='OpenMV Framebuffer Display') + parser.add_argument('--port', action='store', help='Serial port (default: /dev/ttyACM0)', default='/dev/ttyACM0') + parser.add_argument("--script", action="store", default=None, help="Script file") + parser.add_argument('--poll', action='store', help='Poll rate in ms (default: 4)', default=4, type=int) + parser.add_argument('--scale', action='store', help='Display scaling factor (default: 4)', default=4, type=int) + parser.add_argument('--bench', action='store_true', help='Run throughput benchmark', default=False) + parser.add_argument('--timeout', action='store', type=float, default=2.0, help='Protocol timeout in seconds') + parser.add_argument('--debug', action='store_true', help='Enable debug logging') + + # Camera configuration options + parser.add_argument('--baudrate', type=int, default=921600, help='Serial baudrate (default: 921600)') + parser.add_argument('--crc', type=str2bool, nargs='?', const=True, default=True, help='Enable CRC validation (default: true)') + parser.add_argument('--seq', type=str2bool, nargs='?', const=True, default=True, help='Enable sequence number validation (default: true)') + parser.add_argument('--ack', type=str2bool, nargs='?', const=True, default=True, help='Enable packet acknowledgment (default: false)') + parser.add_argument('--events', type=str2bool, nargs='?', const=True, default=True, help='Enable event notifications (default: true)') + parser.add_argument('--max-retry', type=int, default=3, help='Maximum number of retries (default: 3)') + parser.add_argument('--max-payload', type=int, default=4096, help='Maximum payload size in bytes (default: 4096)') + parser.add_argument('--drop-rate', type=float, default=0.0, help='Packet drop simulation rate (0.0-1.0, default: 0.0)') + parser.add_argument('--quiet', action='store_true', help='Suppress script output text') + + args = parser.parse_args() + + # Configure logging + if args.debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logging.basicConfig( + format="%(relativeCreated)010.3f - %(message)s", + level=log_level, + ) + + # Load script + if args.script is not None: + with open(args.script, 'r') as f: + script = f.read() + logging.info(f"Loaded script from {args.script}") + else: + script = bench_script if args.bench else test_script + logging.info("Using built-in script") + + # Initialize pygame + pygame.init() + + screen = None + clock = pygame.time.Clock() + fps_clock = pygame.time.Clock() + font = pygame.font.SysFont("monospace", 30) + + if not args.bench: + pygame.display.set_caption("OpenMV Camera") + else: + pygame.display.set_caption("OpenMV Camera (Benchmark)") + screen = pygame.display.set_mode((640, 120), pygame.DOUBLEBUF, 32) + + try: + with OMVCamera(args.port, baudrate=args.baudrate, crc=args.crc, seq=args.seq, + ack=args.ack, events=args.events, + timeout=args.timeout, max_retry=args.max_retry, + max_payload=args.max_payload, drop_rate=args.drop_rate) as camera: + logging.info(f"Connected to OpenMV camera on {args.port}") + + camera.stop() + time.sleep(0.500) # Wait for soft-reboot (if script is running) + camera.exec(script) + camera.streaming(True, raw=False, res=(512, 512)) + logging.info("Script executed, starting display...") + + while True: + # Handle pygame events first to keep UI responsive + for event in pygame.event.get(): + if event.type == pygame.QUIT: + raise KeyboardInterrupt + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + raise KeyboardInterrupt + elif event.key == pygame.K_c and not args.bench: + if 'image' in locals(): + pygame.image.save(image, "capture.png") + logging.info("Screenshot saved as capture.png") + + # Read camera status + status = camera.read_status() + + # Handle text output + if not args.bench and status and status.get('stdout'): + text = camera.read_stdout() + if text and not args.quiet: + print(text, end='') + + # Handle frame data + if frame := camera.read_frame(): + fps = fps_clock.get_fps() + w, h, data = frame['width'], frame['height'], frame['data'] + + # Create image from RGB888 data (always converted by camera module) + if not args.bench: + image = pygame.image.frombuffer(data, (w, h), 'RGB') + image = pygame.transform.smoothscale(image, (w * args.scale, h * args.scale)) + + # Create/resize screen if needed + if screen is None: + screen = pygame.display.set_mode((w * args.scale, h * args.scale), pygame.DOUBLEBUF, 32) + + # Draw frame + if args.bench: + screen.fill((0, 0, 0)) + else: + screen.blit(image, (0, 0)) + + # Draw FPS info with accurate data rate + current_mbps = (fps * frame['raw_size']) / 1024**2 + if current_mbps < 1.0: + rate_text = f"{current_mbps * 1024:.2f} KB/s" + else: + rate_text = f"{current_mbps:.2f} MB/s" + fps_text = f"{fps:.2f} FPS {rate_text} {w}x{h} RGB888" + screen.blit(font.render(fps_text, True, (255, 0, 0)), (0, 0)) + + # Update display + pygame.display.flip() + fps_clock.tick() + + # Control main loop timing + clock.tick(1000//args.poll) + + except KeyboardInterrupt: + logging.info("Interrupted by user") + sys.exit(0) + except Exception as e: + logging.error(f"Error: {e}") + import traceback + logging.error(f"{traceback.format_exc()}") + sys.exit(1) + finally: + pygame.quit() + +if __name__ == '__main__': + main() diff --git a/tools/pyopenmv_fb.py b/tools/openmv/examples/profiler.py old mode 100755 new mode 100644 similarity index 62% rename from tools/pyopenmv_fb.py rename to tools/openmv/examples/profiler.py index a4e53f8fe..094017eb4 --- a/tools/pyopenmv_fb.py +++ b/tools/openmv/examples/profiler.py @@ -1,20 +1,58 @@ #!/usr/bin/env python -# This file is part of the OpenMV project. +# SPDX-License-Identifier: MIT # -# Copyright (c) 2013-2025 Ibrahim Abdelkader -# Copyright (c) 2013-2025 Kwabena W. Agyeman +# Copyright (c) 2025 OpenMV, LLC. # -# This work is licensed under the MIT license, see the file LICENSE for details. +# OpenMV Framebuffer Display with Advanced Profiling # -# An example script using pyopenmv to grab the framebuffer. +# This example demonstrates advanced profiling features of OpenMV cameras, including: +# - Live framebuffer display with pygame +# - Performance profiling with function timing +# - Event counter profiling for hardware performance monitoring +# - Symbol loading from ELF firmware files for function name resolution +# - Interactive profiling controls and colorized performance visualization +# +# Controls: +# - P key: Toggle profiling overlay (off/profile/events) +# - M key: Toggle profiling mode (inclusive/exclusive) +# - R key: Reset profiler data +# - C key: Capture screenshot to 'capture.png' +# - ESC key: Exit +# +# Dependencies: +# - pygame +# - numpy +# - elftools (for symbol loading) import sys -import numpy as np -import pygame -import pyopenmv +import os import argparse import time +import logging +import pygame +import numpy as np +import struct +from functools import reduce +from operator import mul +# Add parent directories to path for openmv module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) + +from openmv.camera import OMVCamera + +def str2bool(v): + """Convert string to boolean for argparse""" + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + +# Default test script for sensor-based cameras test_script = """ import sensor, image, time sensor.reset() @@ -29,6 +67,7 @@ while(True): print(clock.fps(), " FPS") """ +# Benchmark script for throughput testing bench_script = """ import sensor, image, time sensor.reset() @@ -40,7 +79,7 @@ while(True): """ def addr_to_symbol(symbols, address): - # Binary search for speed + """Binary search for speed""" lo, hi = 0, len(symbols) - 1 while lo <= hi: mid = (lo + hi) // 2 @@ -54,9 +93,7 @@ def addr_to_symbol(symbols, address): return None def get_color_by_percentage(percentage, base_color=(220, 220, 220)): - """ - Return a color based on percentage with fine-grained intensity levels. - """ + """Return a color based on percentage with fine-grained intensity levels.""" def clamp(value): return max(0, min(255, int(value))) @@ -288,7 +325,7 @@ def draw_profile_table(overlay_surface, config, profile_data, profile_mode, symb total_ticks = record['total_ticks'] avg_cycles = record['total_cycles'] // max(1, call_count) avg_ticks = total_ticks // max(1, call_count) - percentage = (total_ticks / total_ticks_all * 100) + percentage = (total_ticks / total_ticks_all * 100) if total_ticks_all else 0 ticks_scale = "" if total_ticks > 1_000_000_000: @@ -344,6 +381,39 @@ def draw_profile_table(overlay_surface, config, profile_data, profile_mode, symb instruction_text = config['fonts']['instruction'].render(instruction_str, True, (180, 180, 180)) overlay_surface.blit(instruction_text, (0, summary_y + int(20 * config['scale_factor']))) +def handle_key_events(event, camera, profile_type, profile_mode, zoom_factor, base_width, base_height, screen, args): + """Handle keyboard events and return updated values""" + if event.key == pygame.K_ESCAPE: + raise KeyboardInterrupt + elif event.key == pygame.K_c and not args.bench: + if 'image' in locals(): + pygame.image.save(image, "capture.png") + logging.info("Screenshot saved as capture.png") + elif event.key == pygame.K_p: + profile_type = not profile_type + logging.info(f"Profile type: {'Performance' if profile_type else 'Events'}") + elif event.key == pygame.K_m: + profile_mode = not profile_mode + camera.profiler_mode(exclusive=profile_mode) + logging.info(f"Profile mode: {'Exclusive' if profile_mode else 'Inclusive'}") + elif event.key == pygame.K_r: + camera.profiler_reset() + logging.info("Profiler reset") + elif event.key == pygame.K_EQUALS and (pygame.key.get_pressed()[pygame.K_LCTRL] or pygame.key.get_pressed()[pygame.K_RCTRL]): + # Ctrl+ to zoom in + zoom_factor = min(zoom_factor * 1.25, 3.0) + new_width, new_height = int(base_width * zoom_factor), int(base_height * zoom_factor) + screen = pygame.display.set_mode((new_width, new_height), pygame.DOUBLEBUF, 32) + logging.info(f"Zoom: {zoom_factor:.2f}x") + elif event.key == pygame.K_MINUS and (pygame.key.get_pressed()[pygame.K_LCTRL] or pygame.key.get_pressed()[pygame.K_RCTRL]): + # Ctrl- to zoom out + zoom_factor = max(zoom_factor / 1.25, 0.5) + new_width, new_height = int(base_width * zoom_factor), int(base_height * zoom_factor) + screen = pygame.display.set_mode((new_width, new_height), pygame.DOUBLEBUF, 32) + logging.info(f"Zoom: {zoom_factor:.2f}x") + + return profile_type, profile_mode, zoom_factor, screen + def draw_profile_overlay(screen, screen_width, screen_height, profile_data, profile_mode, profile_type, scale, symbols, alpha=250): """Main entry point for drawing the profile overlay.""" @@ -387,178 +457,169 @@ def draw_profile_overlay(screen, screen_width, screen_height, profile_data, screen.blit(overlay_surface, (0, 0)) -def pygame_test(port, script, poll_rate, scale, benchmark, symbols): - # init pygame - pygame.init() - pyopenmv.disconnect() +def main(): + parser = argparse.ArgumentParser(description='OpenMV Framebuffer Display with Profiling') + parser.add_argument('--port', action='store', help='Serial port (default: /dev/ttyACM0)', default='/dev/ttyACM0') + parser.add_argument("--script", action="store", default=None, help="Script file") + parser.add_argument('--poll', action='store', help='Poll rate in ms (default: 4)', default=4, type=int) + parser.add_argument('--bench', action='store_true', help='Run throughput benchmark', default=False) + parser.add_argument('--firmware', action='store', help='Firmware ELF file for symbol resolution', default=None) + parser.add_argument('--timeout', action='store', type=float, default=2.0, help='Protocol timeout in seconds') + parser.add_argument('--debug', action='store_true', help='Enable debug logging') + + # Camera configuration options + parser.add_argument('--baudrate', type=int, default=921600, help='Serial baudrate (default: 921600)') + parser.add_argument('--crc', type=str2bool, nargs='?', const=True, default=True, help='Enable CRC validation (default: true)') + parser.add_argument('--seq', type=str2bool, nargs='?', const=True, default=True, help='Enable sequence number validation (default: true)') + parser.add_argument('--ack', type=str2bool, nargs='?', const=True, default=False, help='Enable packet acknowledgment (default: false)') + parser.add_argument('--events', type=str2bool, nargs='?', const=True, default=True, help='Enable event notifications (default: true)') + parser.add_argument('--max-retry', type=int, default=3, help='Maximum number of retries (default: 3)') + parser.add_argument('--max-payload', type=int, default=4096, help='Maximum payload size in bytes (default: 4096)') + parser.add_argument('--quiet', action='store_true', help='Suppress script output text') - connected = False - for i in range(10): + args = parser.parse_args() + + # Configure logging + if args.quiet: + log_level = logging.WARNING + elif args.debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logging.basicConfig( + format="%(relativeCreated)010.3f - %(message)s", + level=log_level, + ) + + # Load script + if args.script is not None: + with open(args.script, 'r') as f: + script = f.read() + logging.info(f"Loaded script from {args.script}") + else: + script = bench_script if args.bench else test_script + logging.info("Using built-in script") + + # Load symbols from firmware ELF file + symbols = [] + if args.firmware: try: - # opens CDC port. - # Set small timeout when connecting - pyopenmv.init(port, baudrate=921600, timeout=0.050) - connected = True - break + from elftools.elf.elffile import ELFFile + with open(args.firmware, 'rb') as f: + elf = ELFFile(f) + symtab = elf.get_section_by_name('.symtab') + if not symtab: + logging.warning("No symbol table found in ELF file") + else: + for sym in symtab.iter_symbols(): + addr = sym['st_value'] + size = sym['st_size'] + name = sym.name + if name and size > 0: # ignore empty symbols + symbols.append((addr, addr + size, name)) + symbols.sort() + logging.info(f"Loaded {len(symbols)} symbols from {args.firmware}") + except ImportError: + logging.error("elftools package not installed. Install with: pip install pyelftools") + sys.exit(1) except Exception as e: - connected = False - time.sleep(0.100) - - if not connected: - print("Failed to connect to OpenMV's serial port.\n" - "Please install OpenMV's udev rules first:\n" - "sudo cp openmv/udev/50-openmv.rules /etc/udev/rules.d/\n" - "sudo udevadm control --reload-rules\n\n") - sys.exit(1) - - # Set higher timeout after connecting for lengthy transfers. - pyopenmv.set_timeout(1*2) # SD Cards can cause big hicups. - pyopenmv.stop_script() - pyopenmv.enable_fb(True) - pyopenmv.reset_profiler() + logging.error(f"Failed to load symbols from {args.firmware}: {e}") - # Configure some event counters. - pyopenmv.set_event_counter(0, 0x0039) - pyopenmv.set_event_counter(1, 0x0023) - pyopenmv.set_event_counter(2, 0x0024) - pyopenmv.set_event_counter(3, 0x0001) - pyopenmv.set_event_counter(4, 0x0003) - pyopenmv.set_event_counter(5, 0xC102) - pyopenmv.set_event_counter(6, 0x02CC) - pyopenmv.set_event_counter(7, 0xC303) - - pyopenmv.exec_script(script) + # Initialize pygame with centered window + import os + os.environ['SDL_VIDEO_CENTERED'] = '1' + pygame.init() - # init screen running = True screen = None - - # Profiling control - profile_type = 0 - profile_mode = 0 - profile_data = [] - last_profile_read = 0 - clock = pygame.time.Clock() fps_clock = pygame.time.Clock() font = pygame.font.SysFont("monospace", 30) - if not benchmark: - pygame.display.set_caption("OpenMV Camera") - else: - pygame.display.set_caption("OpenMV Camera (Benchmark)") - screen = pygame.display.set_mode((640, 120), pygame.DOUBLEBUF, 32) + # Profiling control - start with performance profiling enabled + profile_type = True # True = performance profiling, False = event profiling + profile_mode = False # False = inclusive, True = exclusive + profile_data = [] + last_profile_read = 0 + + # Zoom control + zoom_factor = 1.0 + base_width, base_height = 1600, 1200 + + # Create window for profiling display + pygame.display.set_caption("OpenMV Profiler") + screen = pygame.display.set_mode((int(base_width * zoom_factor), + int(base_height * zoom_factor)), pygame.DOUBLEBUF, 32) + try: - while running: - # Read state - w, h, data, size, text, fmt, profiling = pyopenmv.read_state() - - if text is not None: - print(text, end="") - - # Read profiling data (maximum 10Hz) - if profiling and profile_type: + with OMVCamera(args.port, baudrate=args.baudrate, crc=args.crc, seq=args.seq, + ack=args.ack, events=args.events, timeout=args.timeout, max_retry=args.max_retry, + max_payload=args.max_payload) as camera: + logging.info(f"Connected to OpenMV camera on {args.port}") + + # Configure event counters for profiling + camera.profiler_event_type(0, 0x0039) # CPU cycles + camera.profiler_event_type(1, 0x0023) # L1I cache access + camera.profiler_event_type(2, 0x0024) # L1I cache refill + camera.profiler_event_type(3, 0x0001) # L1I cache TLB refill + camera.profiler_event_type(4, 0x0003) # L1D cache access + camera.profiler_event_type(5, 0xC102) # L1D cache refill + camera.profiler_event_type(6, 0x02CC) # L2D cache access + camera.profiler_event_type(7, 0xC303) # L2D cache refill + + camera.profiler_reset() + + camera.stop() + camera.exec(script) + camera.streaming(True) + logging.info("Script executed, starting display...") + + while True: + # Handle pygame events first to keep UI responsive + for event in pygame.event.get(): + if event.type == pygame.QUIT: + raise KeyboardInterrupt + elif event.type == pygame.KEYDOWN: + profile_type, profile_mode, zoom_factor, screen = handle_key_events( + event, camera, profile_type, profile_mode, zoom_factor, + base_width, base_height, screen, args) + + # Read profiling data (maximum 10Hz) current_time = time.time() if current_time - last_profile_read >= 0.1: # 10Hz = 0.1s interval - tmp_data = pyopenmv.read_profile() + tmp_data = camera.read_profile() if tmp_data: profile_data = tmp_data last_profile_read = current_time - #if profile_data: - # for r in profile_data: - # print(f"Func: {addr_to_symbol(symbols, r['address'])}@0x{r['address']:x} ") - # print(f"Call: {addr_to_symbol(symbols, r['caller'])}@0x{r['caller']:x}") - # sys.exit(0) + # Clear screen + screen.fill((0, 0, 0)) - if data is not None: - fps = fps_clock.get_fps() - - # Create image from RGB888 - if not benchmark: - image = pygame.image.frombuffer(data.flat[0:], (w, h), 'RGB') - image = pygame.transform.smoothscale(image, (w * scale, h * scale)) - - if screen is None: - screen = pygame.display.set_mode((w * scale, h * scale), pygame.DOUBLEBUF, 32) - - # blit stuff - if benchmark: - screen.fill((0, 0, 0)) - else: - screen.blit(image, (0, 0)) - - # FPS text - fps_text = f"{fps:.2f} FPS {fps * size / 1024**2:.2f} MB/s {w}x{h} {fmt}" - screen.blit(font.render(fps_text, 5, (255, 0, 0)), (0, 0)) - # Draw profile overlay if enabled - if profile_type and profile_data: - draw_profile_overlay(screen, w, h, profile_data, profile_mode, profile_type, scale, symbols) - - # update display - pygame.display.flip() - fps_clock.tick(1000//poll_rate) - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - running = False - elif event.key == pygame.K_c: - pygame.image.save(image, "capture.png") - elif event.key == pygame.K_p: - profile_type = (profile_type + 1) % 3 - elif event.key == pygame.K_m: - profile_mode = not profile_mode - pyopenmv.set_profile_mode(profile_mode) - elif event.key == pygame.K_r: - pyopenmv.reset_profiler() + if profile_data: + screen_width, screen_height = screen.get_size() + # Convert boolean to expected integer: True=1 (performance), False=2 (events) + overlay_type = 1 if profile_type else 2 + draw_profile_overlay(screen, screen_width, screen_height, + profile_data, profile_mode, overlay_type, 1, symbols) + + # Update display + pygame.display.flip() + + # Control main loop timing + clock.tick(1000//args.poll) - clock.tick(1000//poll_rate) - except KeyboardInterrupt: - pass - - pygame.quit() - pyopenmv.stop_script() + logging.info("Interrupted by user") + except Exception as e: + logging.error(f"Error: {e}") + import traceback + logging.error(f"{traceback.format_exc()}") + sys.exit(1) + finally: + pygame.quit() if __name__ == '__main__': - parser = argparse.ArgumentParser(description='pyopenmv module') - parser.add_argument('--port', action = 'store', help='Serial port (dev/ttyACM0)', default='/dev/ttyACM0') - parser.add_argument("--script", action = "store", default=None, help = "Script file") - parser.add_argument('--poll', action = 'store', help='Poll rate (default 4ms)', default=4, type=int) - parser.add_argument('--bench', action = 'store_true', help='Run throughput benchmark.', default=False) - parser.add_argument('--scale', action = 'store', help='Set frame scaling factor (default 4x).', default=4, type=int) - parser.add_argument('--firmware', action = 'store', help='Firmware for address to symbol', default=None) - - args = parser.parse_args() - if args.script is not None: - with open(args.script) as f: - args.script = f.read() - else: - args.script = bench_script if args.bench else test_script - - symbols = [] - - if args.firmware: - from elftools.elf.elffile import ELFFile - - with open(args.firmware, 'rb') as f: - elf = ELFFile(f) - symtab = elf.get_section_by_name('.symtab') - if not symtab: - raise ValueError("No symbol table found in ELF.") - - for sym in symtab.iter_symbols(): - addr = sym['st_value'] - size = sym['st_size'] - name = sym.name - if name and size > 0: # ignore empty symbols - symbols.append((addr, addr + size, name)) - - symbols.sort() - - pygame_test(args.port, args.script, args.poll, args.scale, args.bench, symbols) + main() diff --git a/tools/openmv/exceptions.py b/tools/openmv/exceptions.py new file mode 100644 index 000000000..95e60b836 --- /dev/null +++ b/tools/openmv/exceptions.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Protocol Exceptions +# +# This module defines all the custom exceptions used in the OpenMV +# Protocol implementation for proper error handling and debugging. + +import traceback + +class OMVPException(Exception): + """Base exception for OpenMV protocol errors""" + def __init__(self, message): + super().__init__(message) + self.traceback = traceback.format_exc() + +class OMVPTimeoutException(OMVPException): + """Raised when a protocol operation times out""" + def __init__(self, message): + super().__init__(message) + +class OMVPChecksumException(OMVPException): + """Raised when CRC validation fails""" + def __init__(self, message): + super().__init__(message) + +class OMVPSequenceException(OMVPException): + """Raised when sequence number validation fails""" + def __init__(self, message): + super().__init__(message) + +class OMVPResyncException(OMVPException): + """Raised to indicate that a resync was performed and operation should be retried""" + def __init__(self, message="Resync performed, retry operation"): + super().__init__(message) diff --git a/tools/openmv/image.py b/tools/openmv/image.py new file mode 100644 index 000000000..049003de2 --- /dev/null +++ b/tools/openmv/image.py @@ -0,0 +1,123 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Image Utilities +# +# This module provides image format conversion utilities for OpenMV camera data. +# Handles conversion from various pixel formats to RGB888 for display purposes. + +import logging +try: + import numpy as np +except ImportError: + np = None +try: + from PIL import Image +except ImportError: + Image = None + +# Pixel format constants +PIXFORMAT_GRAYSCALE = 0x08020001 # 1 byte per pixel +PIXFORMAT_RGB565 = 0x0C030002 # 2 bytes per pixel +PIXFORMAT_JPEG = 0x06060000 # Variable size JPEG + +def convert_to_rgb888(raw_data, width, height, pixformat): + """ + Convert various pixel formats to RGB888. + + Args: + raw_data (bytes): Raw image data + width (int): Image width + height (int): Image height + pixformat (int): Pixel format identifier + + Returns: + tuple: (rgb_data, format_string) where rgb_data is bytes or None on error + """ + + if pixformat == PIXFORMAT_GRAYSCALE: + return _convert_grayscale(raw_data, width, height) + elif pixformat == PIXFORMAT_RGB565: + return _convert_rgb565(raw_data, width, height) + elif pixformat == PIXFORMAT_JPEG: + return _convert_jpeg(raw_data, width, height) + else: + # Unknown format - return raw data and let caller handle it + fmt_str = f"0x{pixformat:08X}" + logging.warning(f"Unknown pixel format: {fmt_str}") + return raw_data, fmt_str + +def _convert_grayscale(raw_data, width, height): + """Convert grayscale to RGB888""" + fmt_str = "GRAY" + + if np is None: + logging.error("numpy required for grayscale conversion") + return None, fmt_str + + # Convert grayscale to RGB by duplicating the gray value + gray_array = np.frombuffer(raw_data, dtype=np.uint8) + if len(gray_array) != width * height: + logging.error(f"Grayscale data size mismatch: expected {width * height}, got {len(gray_array)}") + return None, fmt_str + + rgb_array = np.column_stack((gray_array, gray_array, gray_array)) + return rgb_array.tobytes(), fmt_str + +def _convert_rgb565(raw_data, width, height): + """Convert RGB565 to RGB888""" + fmt_str = "RGB565" + + if np is None: + logging.error("numpy required for RGB565 conversion") + return None, fmt_str + + # Convert RGB565 to RGB888 + rgb565_array = np.frombuffer(raw_data, dtype=np.uint16) + if len(rgb565_array) != width * height: + logging.error(f"RGB565 data size mismatch: expected {width * height}, got {len(rgb565_array)}") + return None, fmt_str + + # Extract RGB components from 16-bit RGB565 + r = (((rgb565_array & 0xF800) >> 11) * 255.0 / 31.0).astype(np.uint8) + g = (((rgb565_array & 0x07E0) >> 5) * 255.0 / 63.0).astype(np.uint8) + b = (((rgb565_array & 0x001F) >> 0) * 255.0 / 31.0).astype(np.uint8) + + rgb_array = np.column_stack((r, g, b)) + return rgb_array.tobytes(), fmt_str + +def _convert_jpeg(raw_data, width, height): + """Convert JPEG to RGB888""" + fmt_str = "JPEG" + + if Image is None: + logging.error("PIL/Pillow required for JPEG conversion") + return None, fmt_str + + try: + # Decode JPEG to RGB + image = Image.frombuffer("RGB", (width, height), raw_data, "jpeg", "RGB", "") + rgb_array = np.asarray(image) if np else None + + if rgb_array is not None: + if rgb_array.size != (width * height * 3): + logging.error(f"JPEG decode size mismatch: expected {width * height * 3}, got {rgb_array.size}") + return None, fmt_str + return rgb_array.tobytes(), fmt_str + else: + # Fallback without numpy + return image.tobytes(), fmt_str + + except Exception as e: + logging.error(f"JPEG decode error: {e}") + return None, fmt_str + +def get_format_string(pixformat): + """Get a human-readable format string from pixel format code""" + format_map = { + PIXFORMAT_GRAYSCALE: "GRAY", + PIXFORMAT_RGB565: "RGB565", + PIXFORMAT_JPEG: "JPEG" + } + return format_map.get(pixformat, f"0x{pixformat:08X}") \ No newline at end of file diff --git a/tools/openmv/transport.py b/tools/openmv/transport.py new file mode 100644 index 000000000..9ab2de729 --- /dev/null +++ b/tools/openmv/transport.py @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2025 OpenMV, LLC. +# +# OpenMV Protocol Transport Layer +# +# This module provides the low-level transport layer with the protocol state +# machine for packet parsing and communication management. + +import time +import logging +import struct +import random +from .constants import * +from .exceptions import * +from .crc import crc16 +from .buffer import OMVRingBuffer + +# Precompiled struct objects for efficiency +_crc_struct = struct.Struct(' self.max_payload: + raise OMVPException(f"Payload too large: {length} > {self.max_payload}") + + # Pack header without CRC first (10 bytes) + struct.pack_into(' 0: + self.pbuf[OMVProto.HEADER_SIZE:OMVProto.HEADER_SIZE + length] = data + struct.pack_into(' 0: + data = self.serial.read(self.serial.in_waiting) + self.buf.extend(data) + + # Process state machine + if not (packet := self._process()): + if poll_events: + return + time.sleep(0.001) + continue + + # Simulate packet drops by randomly dropping parsed packets + if self.drop_rate > 0.0 and random.random() < self.drop_rate: + self.log(packet=packet, direction="Drop") + continue + + # Handle retransmission + if (packet['flags'] & OMVPFlags.RTX) and (self.sequence != packet['sequence']): + if packet['flags'] & OMVPFlags.ACK_REQ: + self.send_packet(packet['opcode'], packet['channel'], + OMVPFlags.ACK, sequence=packet['sequence']) + continue # Skip further processing of duplicate packet + + self.stats['received'] += 1 + self.log(packet=packet, direction="Recv") + + # ACK the received packet + if packet['flags'] & OMVPFlags.ACK_REQ: + if self.drop_rate > 0.0 and random.random() < self.drop_rate: + self.log(packet['sequence'], packet['channel'], packet['opcode'], OMVPFlags.ACK, 0, "Drop") + else: + self.send_packet(packet['opcode'], packet['channel'], OMVPFlags.ACK) + + # Handle event packets + if packet['flags'] & OMVPFlags.EVENT: + self.event_callback(packet['channel'], 0xFFFF if not packet['length'] + else struct.unpack(' 2: + if self.state == OMVPState.SYNC: + # Find sync pattern + while len(self.buf) > 2: + sync = self.buf.peek16() + if sync == OMVProto.SYNC_WORD: + self.state = OMVPState.HEADER + break + self.buf.consume(1) + + elif self.state == OMVPState.HEADER: + # Wait for complete header + if len(self.buf) < OMVProto.HEADER_SIZE: + return None + + # Parse header + header = self.buf.peek(OMVProto.HEADER_SIZE) + sync, seq, chan, flags, opcode, length, crc = _hdr_struct.unpack(header[:OMVProto.HEADER_SIZE]) + + self.state = OMVPState.SYNC + if length > self.max_payload: + self.log(seq, chan, opcode, flags, length, "Rjct") + self.buf.consume(1) + elif not self._check_seq(seq, self.sequence, opcode, flags): + self.log(seq, chan, opcode, flags, length, "Rjct") + self.buf.consume(1) + elif not self._check_crc(crc, header[:OMVProto.HEADER_SIZE-2]): + self.log(seq, chan, opcode, flags, length, "Rjct") + self.buf.consume(1) + else: + self.state = OMVPState.PAYLOAD + self.plength = OMVProto.HEADER_SIZE + length + self.plength += OMVProto.CRC_SIZE if length else 0 + + elif self.state == OMVPState.PAYLOAD: + # Wait for a complete packet + if len(self.buf) < self.plength: + return None + + payload = None + self.state = OMVPState.SYNC + + # Parse packet + packet = self.buf.peek(self.plength) + sync, seq, chan, flags, opcode, length, crc = _hdr_struct.unpack(packet[:OMVProto.HEADER_SIZE]) + + # Parse payload + if length > 0: + payload = packet[OMVProto.HEADER_SIZE:-OMVProto.CRC_SIZE] + payload_crc = _crc_struct.unpack(packet[-OMVProto.CRC_SIZE:])[0] + + if not self._check_crc(payload_crc, payload): + self.stats['checksum'] += 1 + self.log(seq, chan, opcode, flags, length, "Rjct") + self.buf.consume(1) # Try next byte + continue + + self.buf.consume(self.plength) + + return { + 'sync': sync, + 'sequence': seq, + 'channel': chan, + 'flags': flags, + 'opcode': opcode, + 'length': length, + 'header_crc': crc, + 'payload': payload + } + + diff --git a/tools/pyopenmv.py b/tools/pyopenmv.py deleted file mode 100755 index 328aacc3d..000000000 --- a/tools/pyopenmv.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env python -# This file is part of the OpenMV project. -# -# Copyright (c) 2013-2021 Ibrahim Abdelkader -# Copyright (c) 2013-2021 Kwabena W. Agyeman -# -# This work is licensed under the MIT license, see the file LICENSE for details. -# -# Openmv module. - -import struct -import sys,time -import serial -import platform -import numpy as np -from PIL import Image - -__serial = None -__FB_HDR_SIZE = 12 - -# USB Debug commands -__USBDBG_CMD = 48 -__USBDBG_FW_VERSION = 0x80 -__USBDBG_FRAME_SIZE = 0x81 -__USBDBG_FRAME_DUMP = 0x82 -__USBDBG_ARCH_STR = 0x83 -__USBDBG_SCRIPT_EXEC = 0x05 -__USBDBG_SCRIPT_STOP = 0x06 -__USBDBG_SCRIPT_SAVE = 0x07 -__USBDBG_SCRIPT_RUNNING = 0x87 -__USBDBG_TEMPLATE_SAVE = 0x08 -__USBDBG_DESCRIPTOR_SAVE= 0x09 -__USBDBG_ATTR_READ = 0x8A -__USBDBG_ATTR_WRITE = 0x0B -__USBDBG_SYS_RESET = 0x0C -__USBDBG_SYS_RESET_TO_BL= 0x0E -__USBDBG_FB_ENABLE = 0x0D -__USBDBG_TX_BUF_LEN = 0x8E -__USBDBG_TX_BUF = 0x8F -__USBDBG_GET_STATE = 0x93 -__USBDBG_PROFILE_SIZE = 0x94 -__USBDBG_PROFILE_DUMP = 0x95 -__USBDBG_PROFILE_MODE = 0x16 -__USBDBG_PROFILE_EVENT = 0x17 -__USBDBG_PROFILE_RESET = 0x18 - -__USBDBG_FLAG_SCRIPT_RUNNING = (1 << 0) -__USBDBG_FLAG_TEXTBUF_NOTEMPTY = (1 << 1) -__USBDBG_FLAG_FRAMEBUF_LOCKED = (1 << 2) -__USBDBG_FLAG_PROFILE_ENABLED = (1 << 3) -__USBDBG_FLAG_PROFILE_HAS_PMU = (1 << 4) - -ATTR_CONTRAST = 0 -ATTR_BRIGHTNESS = 1 -ATTR_SATURATION = 2 -ATTR_GAINCEILING = 3 - -__BOOTLDR_START = 0xABCD0001 -__BOOTLDR_RESET = 0xABCD0002 -__BOOTLDR_ERASE = 0xABCD0004 -__BOOTLDR_WRITE = 0xABCD0008 - -def init(port, baudrate=921600, timeout=0.3): - global __serial - # open CDC port - __serial = serial.Serial(port, baudrate=baudrate, timeout=timeout) - -def disconnect(): - global __serial - try: - if (__serial): - __serial.close() - __serial = None - except: - pass - -def write_pack(fmt, *values): - __serial.write(struct.pack(fmt, *values)) - -def read_unpack(fmt): - return struct.unpack(fmt, __serial.read(struct.calcsize(fmt))) - -def set_timeout(timeout): - __serial.timeout = timeout - -def fb_size(): - # read fb header - write_pack(" 2 else (w * h * size) - - # read fb data - write_pack(">11)*255.0/31.0).astype(np.uint8) - g = (((arr & 0x07E0) >>5) *255.0/63.0).astype(np.uint8) - b = (((arr & 0x001F) >>0) *255.0/31.0).astype(np.uint8) - buff = np.column_stack((r,g,b)) - else: # JPEG - fmt = "JPEG" - try: - buff = np.asarray(Image.frombuffer("RGB", (w, h), buff, "jpeg", "RGB", "")) - except Exception as e: - raise ValueError(f"JPEG decode error (%e)") - - if (buff.size != (w*h*3)): - raise ValueError(f"Unexpected frame size. Expected: {w*h*3} received: {buff.size}") - - return w, h, buff.reshape((h, w, 3)), num_bytes, text, fmt, profile - - -def fb_dump(): - size = fb_size() - - if (not size[0]): - # frame not ready - return None - - if (size[2] > 2): #JPEG - num_bytes = size[2] - else: - num_bytes = size[0]*size[1]*size[2] - - # read fb data - write_pack(">11)*255.0/31.0).astype(np.uint8) - g = (((arr & 0x07E0) >>5) *255.0/63.0).astype(np.uint8) - b = (((arr & 0x001F) >>0) *255.0/31.0).astype(np.uint8) - buff = np.column_stack((r,g,b)) - else: # JPEG - try: - buff = np.asarray(Image.frombuffer("RGB", size[0:2], buff, "jpeg", "RGB", "")) - except Exception as e: - print ("JPEG decode error (%s)"%(e)) - return None - - if (buff.size != (size[0]*size[1]*3)): - return None - - return (size[0], size[1], buff.reshape((size[1], size[0], 3))) - -def read_profile(): - records = [] - - # Read and unpack profiling data size - write_pack(" 0 else [] - }) - - offset += record_size - - return records - -def set_profile_mode(mode): - write_pack("