Compare commits

...

18 Commits

Author SHA1 Message Date
iabdalkader
2b7ab635e3 lib/stai: Pull events during inference.
Ensures pending events gets services more often.

Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
6c3b31ab5f scripts/examples: Add Protocol examples.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
57d8bc84a0 lib/micropython: Use temporary branch.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
7c8d300cf4 tools/openmv: OpenMV Protocol V2 Implementation.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
549c407da1 ports/stm32: Switch to new protocol.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
bb517e5cdd ports/alif: Switch to new protocol.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
0718c4bfbd ports/mimxrt: Switch to new protocol.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
35d0d6c363 boards/all: Switch to new protocol.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
d5c1bd59c9 modules/omv: Switch to new protocol.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
05ff2fff32 lib/imlib: Send stream events.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
c51fc4b267 modules: Add OpenMV protocol bindings.
Enables creating transports and data channels from Python.

Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
cc879d15b4 protocol: Introduce OMV protocol V2.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
91a01ee978 common/usbdbg: Remove obsolete protocol files.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
f205fe0219 common/mp_utils: Remove exec_bootscript.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
e80a7d00b8 tools/pyopenmv: Remove obsolete tools.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
iabdalkader
896e4164d1 common: Add circular buffer API for data streaming
Add a simple ring buffer implementation with claiming/committing,
consumption tracking, and automatic compaction when needed.

A better solution would be to implement Bip Buffer:
https://www.codeproject.com/Articles/3479/The-Bip-Buffer-The-Circular-Buffer-with-a-Twist

Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 20:26:49 +02:00
Ibrahim Abdelkader
87b01fac51
Merge pull request #2855 from openmv/fix_raw_stream_size
Some checks failed
🔥 Firmware Build / build-firmware (0, ARDUINO_GIGA) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, ARDUINO_NANO_33_BLE_SENSE) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, ARDUINO_NANO_RP2040_CONNECT) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, ARDUINO_NICLA_VISION) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, ARDUINO_PORTENTA_H7) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, DOCKER) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV2) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV3) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV4) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV4P) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMVPT) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV_AE3) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV_N6) (push) Waiting to run
🔥 Firmware Build / build-firmware (0, OPENMV_RT1060) (push) Waiting to run
🔥 Firmware Build / build-firmware (1, OPENMV_N6) (push) Waiting to run
🔥 Firmware Build / code-size-report (push) Blocked by required conditions
🔥 Firmware Build / stable-release (push) Blocked by required conditions
🔥 Firmware Build / development-release (push) Blocked by required conditions
🔎 Check Code Formatting / formatting-check (push) Has been cancelled
lib/imlib: Fix raw stream buffer size check.
2025-09-21 21:26:28 +03:00
iabdalkader
552f4ada8e lib/imlib: Fix raw stream buffer size check.
Check frame dimensions as well as total image size
top scale the image if needed.

Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
2025-09-21 16:41:34 +02:00
73 changed files with 5804 additions and 1827 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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 \

View File

@ -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);

View File

@ -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__

94
common/omv_buffer.h Normal file
View File

@ -0,0 +1,94 @@
#ifndef __OMV_BUFFER_H__
#define __OMV_BUFFER_H__
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
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__

View File

@ -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

View File

@ -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__

View File

@ -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 <string.h>
#include <stdio.h>
#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;
}
}

View File

@ -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__ */

570
docs/protocol.md Normal file
View File

@ -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 |

View File

@ -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; i<fb->buf_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) {
@ -305,7 +306,7 @@ void framebuffer_update_preview(image_t *src) {
// Down-scale the frame (if necessary) and send the raw frame.
dst.size = src->bpp;
dst.pixfmt = src->pixfmt;
if (image_size(&dst) <= available_size) {
if (src->w <= fb->raw_w && src->h <= fb->raw_h && image_size(&dst) <= available_size) {
memcpy(dst.pixels, src->pixels, image_size(src));
} else {
float scale = IM_MIN((fb->raw_w / (float) src->w),
@ -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);
}

@ -1 +1 @@
Subproject commit b7efb956cfb21afda9b71162eeefec8c31d3dff9
Subproject commit f5ece1cdff2c4da3078e25cdff15594226f652e6

View File

@ -234,6 +234,7 @@ int ml_backend_init_model(py_ml_model_obj_t *model) {
}
int ml_backend_run_inference(py_ml_model_obj_t *model) {
LL_ATON_RT_RetValues_t ll_aton_rt_ret;
ml_backend_state_t *state = (ml_backend_state_t *) model->state;
// Flush input buffers.
@ -242,7 +243,25 @@ int ml_backend_run_inference(py_ml_model_obj_t *model) {
SCB_CleanDCache_by_Addr(LL_Buffer_addr_start(buf), LL_Buffer_len(buf));
}
LL_ATON_RT_Main(&state->nn_inst);
LL_ATON_RT_RuntimeInit();
LL_ATON_RT_Init_Network(&state->nn_inst);
do {
// Execute first/next runtime step
ll_aton_rt_ret = LL_ATON_RT_RunEpochBlock(&state->nn_inst);
// Handle pending events (TinyUSB, OMV Protocol etc..)
mp_handle_pending(false);
// Wait for the next event
if (ll_aton_rt_ret == LL_ATON_RT_WFE) {
LL_ATON_OSAL_WFE();
}
} while (ll_aton_rt_ret != LL_ATON_RT_DONE);
LL_ATON_RT_DeInit_Network(&state->nn_inst);
LL_ATON_RT_RuntimeDeInit();
return 0;
}

View File

@ -27,17 +27,16 @@
#include <stdio.h>
#include <stdbool.h>
#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) },

495
modules/py_protocol.c Normal file
View File

@ -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 <string.h>
#include <stdio.h>
#include <stdbool.h>
#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, "<ProtocolChannel: %s>", 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]);

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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)

View File

@ -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)

View File

@ -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)/,\

View File

@ -74,15 +74,16 @@
#include "extmod/vfs_fat.h"
#include "shared/runtime/pyexec.h"
#include "shared/readline/readline.h"
#include "shared/runtime/softtimer.h"
#include "omv_boardconfig.h"
#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 +226,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 +246,6 @@ soft_reset:
#if MICROPY_HW_ENABLE_SERVO
servo_init();
#endif
usbdbg_init();
#if MICROPY_HW_ENABLE_SDCARD
sdcard_init();
#endif
@ -297,6 +297,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 +329,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 +350,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);
@ -410,6 +391,7 @@ soft_reset_exit:
py_audio_deinit();
#endif
imlib_deinit_all();
soft_timer_deinit();
gc_sweep_all();
mp_deinit();
first_soft_reset = false;

View File

@ -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)

View File

@ -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

854
protocol/omv_protocol.c Normal file
View File

@ -0,0 +1,854 @@
/*
* 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 <string.h>
#include <stdlib.h>
#include <stddef.h>
#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;
static const omv_protocol_config_t 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,
};
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() {
if (omv_protocol_init(&default_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;
}
void 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;
}
}
}
void omv_protocol_reset(void) {
// Reset state
ctx.sequence = 0;
ctx.scan_offset = 0;
ctx.state = OMV_PROTOCOL_STATE_SYNC;
omv_buffer_clear(&ctx.buffer);
// Restore default config
ctx.config = default_config;
// Unlock channels
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();
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();
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 {
// ACK the updated caps first before changing them.
omv_protocol_send_status(packet, OMV_PROTOCOL_STATUS_SUCCESS);
// Mark capabilities as negotiated for soft reset preservation
ctx.config.caps_negotiated = true;
// 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;
}
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;
}
}

370
protocol/omv_protocol.h Normal file
View File

@ -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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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
void 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__

View File

@ -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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
/***************************************************************************
* 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__

View File

@ -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

View File

@ -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,
};

View File

@ -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

View File

@ -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
};

View File

@ -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

View File

@ -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 <stdint.h>
#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__

43
protocol/protocol.mk Normal file
View File

@ -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))

View File

@ -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)

View File

@ -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())

35
tools/openmv/__init__.py Normal file
View File

@ -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'
]

85
tools/openmv/buffer.py Normal file
View File

@ -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('<H', self.data[self.start:self.start + 2])[0]
else:
# Wrap-around case - need to combine bytes
return self.data[self.start] | (self.data[(self.start + 1) % self.size] << 8)
def consume(self, count):
"""Consume count bytes from buffer"""
count = min(count, self.count)
self.start = (self.start + count) % self.size
self.count -= count
def read(self, size):
"""Read and consume data"""
data = self.peek(size)
if data is not None:
self.consume(size)
return data

631
tools/openmv/camera.py Normal file
View File

@ -0,0 +1,631 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2025 OpenMV, LLC.
#
# OpenMV Protocol Camera Interface
#
# This module provides the high-level OMVCamera class that handles all
# camera operations and channel communications using the OpenMV Protocol.
import struct
import time
import sys
import serial
import logging
from functools import reduce
from operator import mul
from .constants import OMVPOpcode, OMVProto, OMVPEventType, OMVPChannelIOCTL
from .exceptions import OMVPException, OMVPTimeoutException, OMVPResyncException
from .transport import OMVTransport
from . import image as omv_image
class OMVCamera:
"""OpenMV Camera Protocol Implementation"""
def __init__(self, port, baudrate=921600,
crc=True, seq=True, ack=True, events=True,
timeout=1.0, max_retry=3, max_payload=4096,
drop_rate=0.0):
# Serial connection
self._serial = None
self.port = port
self.baudrate = baudrate
self.timeout = timeout
self.max_retry = max_retry
self.drop_rate = drop_rate
# Configuration stored in dictionary
self.caps = {
'crc': crc,
'seq': seq,
'ack': ack,
'events': events,
'max_payload': max_payload,
}
# Protocol components
self.channels_by_name = {}
self.channels_by_id = {}
self.pending_channel_events = 0
self.sysinfo = {}
self.transport = None
self.frame_event = False
def __enter__(self):
"""Context manager entry"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit"""
self.disconnect()
@staticmethod
def retry_if_failed(func):
"""Decorator to automatically retry on resync"""
# Note all held locks are released on resync in the firmware.
def wrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except OMVPResyncException:
return func(self, *args, **kwargs)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
def poll_events(self):
self.transport.recv_packet(poll_events=True)
def _send_cmd_wait_resp(self, opcode, channel=0, data=b''):
"""Send a command and wait for response (ACK/NAK or data)"""
if not self.is_connected():
raise OMVPException("Not connected")
# Special handling for reset commands - they never return
if opcode in [OMVPOpcode.SYS_RESET, OMVPOpcode.SYS_BOOT]:
self.transport.send_packet(opcode, channel, 0, data)
self.disconnect() # Device will reset, connection lost
return None
try:
self.transport.send_packet(opcode, channel, 0, data)
# Returns payload/None or raises exception
return self.transport.recv_packet()
except KeyboardInterrupt:
sys.exit(0)
except:
self._resync()
raise OMVPResyncException()
def _handle_event(self, channel_id, event):
"""Handle events from the device"""
if channel_id == 0:
# System events
event_name = OMVPEventType(event).name if event in OMVPEventType else f"0x{event:04X}"
logging.info(f"🔔 System Event: channel=system, event={event_name}")
# Handle system events
if event == OMVPEventType.SOFT_REBOOT:
logging.info("🔥 Soft Reboot triggered")
elif event == OMVPEventType.CHANNEL_REGISTERED:
self.pending_channel_events += 1
elif channel_id in self.channels_by_id:
# Channel events
event_type = ""
channel = self.channels_by_id[channel_id]["name"]
if channel == "stream":
self.frame_event = True
event_type = " (Frame Ready)"
elif channel == "stdin":
event_type = " (Script Started)" if event == 1 else " (Script Stopped)"
logging.info(f"🔔 Channel Event: channel={channel}, event=0x{event:04X}{event_type}")
else:
# Unknown channel
logging.warning(f"⚠️ Unknown Event: channel={channel_id}, event=0x{event:04X}")
def _resync(self):
logging.info("🔁 Resynchronizing")
# Perform resync sequence on timeout
for attempt in range(self.max_retry):
try:
# Send SYNC command and wait for response
self.transport.reset_sequence()
self.transport.send_packet(OMVPOpcode.PROTO_SYNC, 0, 0)
if self.transport.recv_packet():
self.transport.reset_sequence()
break
except OMVPException:
if attempt < self.max_retry - 1:
logging.warning(f"⚠️ Sync attempt {attempt + 1} failed, retrying...")
continue
else:
logging.error("❌ Failed to resync after maximum attempts")
raise OMVPTimeoutException("Resync failed - unable to synchronize with device")
def _channel_lock(self, channel_id):
"""Lock a data channel"""
return self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_LOCK, channel_id)
def _channel_unlock(self, channel_id):
"""Lock a data channel"""
return self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_UNLOCK, channel_id)
def _channel_size(self, channel_id):
"""Get available data size for a channel"""
payload = self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_SIZE, channel_id)
return struct.unpack('<I', payload[:4])[0]
def _channel_shape(self, channel_id):
"""Get available data size for a channel"""
payload = self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_SHAPE, channel_id)
return struct.unpack(f'<{len(payload)//4}I', payload)
def _channel_read(self, channel_id, offset, length):
"""Read data from a channel (protocol handles fragmentation automatically)"""
payload = struct.pack('<II', offset, length)
data = self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_READ, channel_id, payload)
return bytes(data)
def _channel_write(self, channel_id, data, offset=0):
"""Write data to a channel with automatic packet splitting"""
# Maximum payload size - 8-byte header (offset + length)
chunk_size = self.caps['max_payload'] - 8
for start in range(0, len(data), chunk_size):
chunk = data[start:start + chunk_size]
# Payload: offset + length + data
payload = struct.pack('<II', offset + start, len(chunk)) + chunk
self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_WRITE, channel_id, payload)
def _channel_ioctl(self, channel_id, cmd, fmt=None, *args):
"""Perform ioctl operation on a channel"""
payload = struct.pack('<I', cmd)
if fmt and args:
payload += struct.pack('<' + fmt, *args)
return self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_IOCTL, channel_id, payload)
def _channel_list(self):
"""List registered channels on the device"""
channels = {}
entry_size = 16 # bytes: 1 (id) + 1 (flags) + 14 (name)
payload = self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_LIST)
num_channels = len(payload) // entry_size
for i in range(num_channels):
offset = i * entry_size
# Unpack id, flags, and raw name bytes in one go
cid, flags, raw_name = struct.unpack_from("<BB14s", payload, offset)
# Strip at first null byte, then decode
name = raw_name.split(b"\x00", 1)[0].decode("utf-8", errors="ignore")
channels[cid] = {"name": name, "flags": flags}
return channels
def update_channels(self):
"""Update channel list from device"""
if self.pending_channel_events > 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('<IH', payload[:6])
flags = (self.caps['crc'] << 0 |
self.caps['seq'] << 1 |
self.caps['ack'] << 2 |
self.caps['events'] << 3)
self.caps['max_payload'] = min(max_payload, self.caps['max_payload'])
payload = struct.pack('<IH10x', flags, self.caps['max_payload'])
response = self._send_cmd_wait_resp(OMVPOpcode.PROTO_SET_CAPS, 0, payload)
@retry_if_failed
def stop(self):
"""Stop running script"""
# Stop running script
stdin_id = self.get_channel(name="stdin")
self._channel_ioctl(stdin_id, OMVPChannelIOCTL.STDIN_STOP)
@retry_if_failed
def exec(self, script):
"""Write and execute a script"""
stdin_id = self.get_channel(name="stdin")
# Reset script buffer
self._channel_ioctl(stdin_id, OMVPChannelIOCTL.STDIN_RESET)
# Upload script data
self._channel_write(stdin_id, memoryview(script.encode('utf-8')))
# Execute the script
self._channel_ioctl(stdin_id, OMVPChannelIOCTL.STDIN_EXEC)
@retry_if_failed
def streaming(self, enable, raw=False, res=None):
"""Enable or disable streaming"""
stream_id = self.get_channel(name="stream")
if raw:
self._channel_ioctl(stream_id, OMVPChannelIOCTL.STREAM_RAW_CFG, 'II', *res)
self._channel_ioctl(stream_id, OMVPChannelIOCTL.STREAM_RAW_CTRL, 'I', raw)
self._channel_ioctl(stream_id, OMVPChannelIOCTL.STREAM_CTRL, 'I', enable)
@retry_if_failed
def read_status(self):
"""Poll channels status and return a dictionary of channel readiness"""
payload = self._send_cmd_wait_resp(OMVPOpcode.CHANNEL_POLL)
flags = struct.unpack('<I', payload[:4])[0]
# Build a dictionary mapping channel names to their poll status
result = {}
if self.pending_channel_events > 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('<IIII', data[:16])
# Extract raw frame data
raw_data = data[16:]
# Convert to RGB888 using image module
rgb_data, fmt_str = omv_image.convert_to_rgb888(raw_data, width, height, pixformat)
if rgb_data is None:
return None
return {
'width': width,
'height': height,
'format': pixformat,
'depth': depth,
'data': rgb_data,
'raw_size': len(raw_data)
}
finally:
self._channel_unlock(stream_id)
@retry_if_failed
def channel_size(self, channel):
"""Get size of data available in a custom channel"""
channel_id = self.get_channel(name=channel)
return 0 if channel_id is None else self._channel_size(channel_id)
@retry_if_failed
def channel_read(self, channel, size=None):
"""Read data from a custom channel"""
if channel_id := self.get_channel(name=channel):
if size is None:
size = self._channel_size(channel_id)
return self._channel_read(channel_id, 0, size)
return None
@retry_if_failed
def channel_write(self, channel, data):
"""Write data to a custom channel"""
channel_id = self.get_channel(name=channel)
if channel_id:
self._channel_write(channel_id, data)
return channel_id is not None
def has_channel(self, channel):
"""Check if a channel exists"""
return self.get_channel(name=channel) is not None
@retry_if_failed
def system_info(self):
"""Get system information"""
payload = self._send_cmd_wait_resp(OMVPOpcode.SYS_INFO)
if len(payload) < 80:
raise OMVPException(f"Invalid SYS_INFO payload size: {len(payload)}")
# Unpack the structure:
# cpu_id[1] + dev_id[3] + chip_id[3] + id_reserved[2] + hw_caps[2] + memory[6] + versions[9] + padding[3]
data = struct.unpack('<I 3I 3I 2I 2I 6I 3s3s3s 3x', payload)
# Extract capability bitfield (always 2 words)
capabilities = data[9] # First hw_caps word
capabilities2 = data[10] # Second hw_caps word
return {
'cpu_id': data[0],
'device_id': data[1:4], # Now 3 words
'sensor_chip_id': data[4:7], # Now 3 words
'gpu_present': bool(capabilities & (1 << 0)),
'npu_present': bool(capabilities & (1 << 1)),
'isp_present': bool(capabilities & (1 << 2)),
'venc_present': bool(capabilities & (1 << 3)),
'jpeg_present': bool(capabilities & (1 << 4)),
'dram_present': bool(capabilities & (1 << 5)),
'crc_present': bool(capabilities & (1 << 6)),
'pmu_present': bool(capabilities & (1 << 7)),
'pmu_eventcnt': (capabilities >> 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("=================================")

101
tools/openmv/constants.py Normal file
View File

@ -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

62
tools/openmv/crc.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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 <iabdalkader@openmv.io>
# Copyright (c) 2013-2025 Kwabena W. Agyeman <kwagyeman@openmv.io>
# 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()

View File

@ -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)

123
tools/openmv/image.py Normal file
View File

@ -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}")

321
tools/openmv/transport.py Normal file
View File

@ -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('<H')
_hdr_struct = struct.Struct('<HBBBBHH')
class OMVTransport:
"""Low-level transport layer with state machine"""
def __init__(self, serial, crc, seq, max_payload, timeout, event_callback, drop_rate=0.0):
self.serial = serial
self.timeout = timeout
self.max_payload = max_payload
# Protocol state
self.sequence = 0
self.state = OMVPState.SYNC
self.crc_enabled = crc
self.seq_enabled = seq
# Event callback
self.event_callback = event_callback
# Packet simulation
self.drop_rate = drop_rate
# Packet buffers for send/recv
self.buf = OMVRingBuffer(max(max_payload * 4, 4 * 1024 * 1024))
self.pbuf = memoryview(bytearray(max_payload + OMVProto.HEADER_SIZE + OMVProto.CRC_SIZE))
# Statistics
self.stats = {
'sent': 0,
'received': 0,
'checksum': 0,
'sequence': 0,
}
def reset_sequence(self):
"""Reset sequence counter to 0"""
self.sequence = 0
def update_caps(self, crc, seq, ack, max_payload):
"""Update transport capabilities"""
self.crc_enabled = crc
self.seq_enabled = seq
self.max_payload = max_payload
# Reallocate buffers to accommodate new max_payload
self.buf = OMVRingBuffer(max(max_payload * 4, 4 * 1024 * 1024))
self.pbuf = memoryview(bytearray(max_payload + OMVProto.HEADER_SIZE + OMVProto.CRC_SIZE))
def _crc(self, data):
"""Calculate CRC-16 with polynomial 0xBAAD"""
return 0 if not self.crc_enabled else crc16(data)
def _check_crc(self, crc, buffer):
"""Check if CRC matches the calculated value or CRC is disabled"""
return not self.crc_enabled or crc == self._crc(buffer)
def _check_seq(self, sequence, expected_sequence, opcode, flags):
"""Check if sequence is valid or sequence checking is disabled"""
return (not self.seq_enabled or
(flags & OMVPFlags.EVENT) or
(flags & OMVPFlags.RTX) or
sequence == expected_sequence or
opcode == OMVPOpcode.PROTO_SYNC)
def _format_flags(self, flags):
"""Get human-readable name for packet flags"""
if flags == 0:
return "0x00"
flag_parts = []
if flags & OMVPFlags.ACK:
flag_parts.append("ACK")
if flags & OMVPFlags.NAK:
flag_parts.append("NAK")
if flags & OMVPFlags.RTX:
flag_parts.append("RTX")
if flags & OMVPFlags.FRAGMENT:
flag_parts.append("FRAG")
if flags & OMVPFlags.EVENT:
flag_parts.append("EVT")
if flags & OMVPFlags.ACK_REQ:
flag_parts.append("ACK_REQ")
return "|".join(flag_parts) if flag_parts else f"0x{flags:02X}"
def log(self, seq=None, ch=None, opcode=None, flags=None, length=None, direction=None, packet=None):
"""Log packet information for debugging"""
if packet is not None:
seq = packet['sequence']
ch = packet['channel']
opcode = packet['opcode']
flags = packet['flags']
length = packet['length']
opcode_str = OMVPOpcode(opcode).name if opcode in OMVPOpcode else f"0x{opcode:02X}"
flags_str = self._format_flags(flags)
# Add emoji based on packet type and direction
if direction == "Drop":
emoji = "🎲"
elif direction == "Rjct":
emoji = "🚫"
elif flags & OMVPFlags.ACK:
emoji = ""
elif flags & OMVPFlags.NAK:
emoji = ""
elif direction == "Send":
emoji = "➡️"
else:
emoji = "⬅️"
logging.debug(f"{emoji} {direction}: seq={seq:03d},"
f" chan={ch}, opcode={opcode_str},"
f" flags={flags_str}, length={length}")
def send_packet(self, opcode, channel, flags, data=None, sequence=None):
"""Send a packet to the camera"""
if not self.serial or not self.serial.is_open:
raise OMVPTimeoutException("Serial connection not open")
sequence = self.sequence if sequence is None else sequence
length = 0 if data is None else len(data)
if length > self.max_payload:
raise OMVPException(f"Payload too large: {length} > {self.max_payload}")
# Pack header without CRC first (10 bytes)
struct.pack_into('<HBBBBH', self.pbuf, 0, OMVProto.SYNC_WORD,
sequence, channel, flags, opcode, length)
struct.pack_into('<H', self.pbuf, OMVProto.HEADER_SIZE - 2,
self._crc(self.pbuf[:OMVProto.HEADER_SIZE - 2]))
# Pack data if present
if length > 0:
self.pbuf[OMVProto.HEADER_SIZE:OMVProto.HEADER_SIZE + length] = data
struct.pack_into('<H', self.pbuf, OMVProto.HEADER_SIZE + length, self._crc(data))
packet_size = OMVProto.HEADER_SIZE + length + (OMVProto.CRC_SIZE if length else 0)
self.log(sequence, channel, opcode, flags, length, "Send")
self.serial.write(self.pbuf[:packet_size])
self.stats['sent'] += 1
def recv_packet(self, poll_events=False):
"""Receive and parse a packet from the camera with NAK handling"""
if not self.serial or not self.serial.is_open:
raise OMVPException("Serial connection not open")
fragments = bytearray() # Collect fragment payloads
start_time = time.time()
while time.time() - start_time < self.timeout:
if self.serial.in_waiting > 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('<H', packet['payload'])[0])
start_time = time.time()
continue # Not ACK'd
# Update sequence after each packet (including fragments)
self.sequence = (self.sequence + 1) & 0xFF
# Check if this is a fragmented packet
if packet['flags'] & OMVPFlags.FRAGMENT:
fragments.extend(packet['payload'])
start_time = time.time()
continue # Continue collecting fragments
# Either last fragment or non-fragmented packet
if fragments:
# This is the last fragment - combine all
fragments.extend(packet['payload'])
packet['payload'] = bytes(fragments)
packet['length'] = len(fragments)
# Handle NAK flags
if packet['flags'] & OMVPFlags.NAK:
status = struct.unpack('<H', packet['payload'][:2])[0]
logging.debug(f"Command failed with status: {OMVPStatus(status).name}")
# Raise specific exception for all NAK statuses except BUSY
if status == OMVPStatus.CHECKSUM:
raise OMVPChecksumException("")
elif status == OMVPStatus.SEQUENCE:
raise OMVPSequenceException("")
elif status == OMVPStatus.TIMEOUT:
raise OMVPTimeoutException("")
elif status != OMVPStatus.BUSY:
raise OMVPException(f"Command failed with status: {OMVPStatus(status).name}")
return False
# Return payload or True for ACK
return True if not packet['length'] else bytes(packet['payload'])
if not poll_events:
raise OMVPTimeoutException("Packet receive timeout")
def _process(self):
"""Process the protocol state machine"""
while len(self.buf) > 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
}

View File

@ -1,305 +0,0 @@
#!/usr/bin/env python
# This file is part of the OpenMV project.
#
# Copyright (c) 2013-2021 Ibrahim Abdelkader <iabdalkader@openmv.io>
# Copyright (c) 2013-2021 Kwabena W. Agyeman <kwagyeman@openmv.io>
#
# 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("<BBI", __USBDBG_CMD, __USBDBG_FRAME_SIZE, __FB_HDR_SIZE)
return read_unpack("III")
def read_state():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_GET_STATE, 64)
flags, w, h, size, text_buf = read_unpack("IIII48s")
text = None
profile = False
if flags & __USBDBG_FLAG_PROFILE_ENABLED:
profile = True
if flags & __USBDBG_FLAG_TEXTBUF_NOTEMPTY:
text = text_buf.split(b'\0', 1)[0].decode()
if flags & __USBDBG_FLAG_FRAMEBUF_LOCKED == 0:
return 0, 0, None, 0, text, "", profile
num_bytes = size if size > 2 else (w * h * size)
# read fb data
write_pack("<BBI", __USBDBG_CMD, __USBDBG_FRAME_DUMP, num_bytes)
buff = __serial.read(num_bytes)
if size == 1: # Grayscale
fmt = "GRAY"
y = np.fromstring(buff, dtype=np.uint8)
buff = np.column_stack((y, y, y))
elif size == 2: # RGB565
fmt = "RGB"
arr = np.fromstring(buff, dtype=np.uint16)
r = (((arr & 0xF800) >>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("<BBI", __USBDBG_CMD, __USBDBG_FRAME_DUMP, num_bytes)
buff = __serial.read(num_bytes)
if size[2] == 1: # Grayscale
y = np.fromstring(buff, dtype=np.uint8)
buff = np.column_stack((y, y, y))
elif size[2] == 2: # RGB565
arr = np.fromstring(buff, dtype=np.uint16)
r = (((arr & 0xF800) >>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("<BBI", __USBDBG_CMD, __USBDBG_PROFILE_SIZE, 12)
record_count, record_size, event_count = read_unpack("<III")
if record_count:
offset = 0
record_format = f"<5I2Q{event_count}QI"
profile_size = record_count * record_size
# Read profiling data
write_pack("<BBI", __USBDBG_CMD, __USBDBG_PROFILE_DUMP, profile_size)
profile_data = __serial.read(profile_size)
for i in range(record_count):
# Unpack the record
profile = struct.unpack(record_format, profile_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] if event_count > 0 else []
})
offset += record_size
return records
def set_profile_mode(mode):
write_pack("<BBI", __USBDBG_CMD, __USBDBG_PROFILE_MODE, 4)
write_pack("<I", mode)
def set_event_counter(event_num, event_type):
write_pack("<BBI", __USBDBG_CMD, __USBDBG_PROFILE_EVENT, 8)
write_pack("<II", event_num, event_type)
def reset_profiler():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_PROFILE_RESET, 0)
def exec_script(buf):
write_pack("<BBI", __USBDBG_CMD, __USBDBG_SCRIPT_EXEC, len(buf))
__serial.write(buf.encode())
def stop_script():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_SCRIPT_STOP, 0)
def script_running():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_SCRIPT_RUNNING, 4)
return read_unpack("I")[0]
def save_template(x, y, w, h, path):
buf = struct.pack("IIII", x, y, w, h) + path
write_pack("<BBI", __USBDBG_CMD, __USBDBG_TEMPLATE_SAVE, len(buf))
__serial.write(buf)
def save_descriptor(x, y, w, h, path):
buf = struct.pack("HHHH", x, y, w, h) + path
write_pack("<BBI", __USBDBG_CMD, __USBDBG_DESCRIPTOR_SAVE, len(buf))
__serial.write(buf)
def set_attr(attr, value):
write_pack("<BBI", __USBDBG_CMD, __USBDBG_ATTR_WRITE, 8)
write_pack("<II", attr, value)
def get_attr(attr):
write_pack("<BBIh", __USBDBG_CMD, __USBDBG_ATTR_READ, 1, attr)
return __serial.read(1)
def reset():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_SYS_RESET, 0)
def reset_to_bl():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_SYS_RESET_TO_BL, 0)
def bootloader_start():
write_pack("<I", __BOOTLDR_START)
return read_unpack("I")[0] == __BOOTLDR_START
def bootloader_reset():
write_pack("<I", __BOOTLDR_RESET)
def flash_erase(sector):
write_pack("<II", __BOOTLDR_ERASE, sector)
def flash_write(buf):
write_pack("<I", __BOOTLDR_WRITE) + buf
def tx_buf_len():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_TX_BUF_LEN, 4)
return read_unpack("I")[0]
def tx_buf(bytes):
write_pack("<BBI", __USBDBG_CMD, __USBDBG_TX_BUF, bytes)
return __serial.read(bytes)
def fw_version():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_FW_VERSION, 12)
return read_unpack("III")
def enable_fb(enable):
write_pack("<BBI", __USBDBG_CMD, __USBDBG_FB_ENABLE, 4)
write_pack("<I", enable)
def arch_str():
write_pack("<BBI", __USBDBG_CMD, __USBDBG_ARCH_STR, 64)
return __serial.read(64).split(b'\0', 1)[0]
if __name__ == '__main__':
if len(sys.argv)!= 3:
print ('usage: pyopenmv.py <port> <script>')
sys.exit(1)
with open(sys.argv[2], 'r') as fin:
buf = fin.read()
disconnect()
init(sys.argv[1])
stop_script()
exec_script(buf)
tx_len = tx_buf_len()
time.sleep(0.250)
if (tx_len):
print(tx_buf(tx_len).decode())
disconnect()

View File

@ -1,281 +0,0 @@
#!/usr/bin/env python2
# This file is part of the OpenMV project.
#
# Copyright (c) 2013-2021 Ibrahim Abdelkader <iabdalkader@openmv.io>
# Copyright (c) 2013-2021 Kwabena W. Agyeman <kwagyeman@openmv.io>
#
# This work is licensed under the MIT license, see the file LICENSE for details.
#
# Openmv module with support for multiple cams.
import struct
import sys,time
import serial
import platform
import numpy as np
from PIL import Image
__serial = []
__port = []
__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_FB_ENABLE = 0x0D
__USBDBG_TX_BUF_LEN = 0x8E
__USBDBG_TX_BUF = 0x8F
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
global __port
# open CDC port
__serial.append(serial.Serial(port, baudrate=baudrate, timeout=timeout))
__port.append(port)
def disconnect(port):
global __serial
global __port
try:
idx = __port.index(port)
__serial[idx].close()
__serial.pop(idx)
__port.pop(idx)
except:
pass
def set_timeout(port, timeout):
try:
idx = __port.index(port)
__serial[idx].timeout = timeout
except:
pass
def fb_size(port):
# read fb header
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_FRAME_SIZE, __FB_HDR_SIZE))
return struct.unpack("III", __serial[idx].read(12))
except:
return None
def fb_dump(port):
try:
idx = __port.index(port)
size = fb_size(port)
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
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_FRAME_DUMP, num_bytes))
buff = __serial[idx].read(num_bytes)
if size[2] == 1: # Grayscale
y = np.fromstring(buff, dtype=np.uint8)
buff = np.column_stack((y, y, y))
elif size[2] == 2: # RGB565
arr = np.fromstring(buff, dtype=np.uint16).newbyteorder('S')
r = (((arr & 0xF800) >>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)))
except:
return None
def exec_script(port, buf):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_SCRIPT_EXEC, len(buf)))
__serial[idx].write(buf.encode())
except:
pass
def stop_script(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_SCRIPT_STOP, 0))
except:
pass
def script_running(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_SCRIPT_RUNNING, 4))
return struct.unpack("I", __serial[idx].read(4))[0]
except:
return None
def save_template(port, x, y, w, h, path):
try:
idx = __port.index(port)
buf = struct.pack("IIII", x, y, w, h) + path
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_TEMPLATE_SAVE, len(buf)))
__serial[idx].write(buf)
except:
pass
def save_descriptor(port, x, y, w, h, path):
try:
idx = __port.index(port)
buf = struct.pack("HHHH", x, y, w, h) + path
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_DESCRIPTOR_SAVE, len(buf)))
__serial[idx].write(buf)
except:
pass
def set_attr(port, attr, value):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_ATTR_WRITE, 8))
__serial[idx].write(struct.pack("<II", attr, value))
except:
pass
def get_attr(port, attr):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBIh", __USBDBG_CMD, __USBDBG_ATTR_READ, 1, attr))
return __serial[idx].read(1)
except:
return None
def reset(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_SYS_RESET, 0))
except:
pass
def bootloader_start(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<I", __BOOTLDR_START))
return struct.unpack("I", __serial[idx].read(4))[0] == __BOOTLDR_START
except:
pass
def bootloader_reset(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<I", __BOOTLDR_RESET))
except:
pass
def flash_erase(port, sector):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<II", __BOOTLDR_ERASE, sector))
except:
pass
def flash_write(port, buf):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<I", __BOOTLDR_WRITE) + buf)
except:
pass
def tx_buf_len(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_TX_BUF_LEN, 4))
return struct.unpack("I", __serial[idx].read(4))[0]
except:
return None
def tx_buf(port, bytes):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_TX_BUF, bytes))
return __serial[idx].read(bytes)
except:
return None
def fw_version(port):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_FW_VERSION, 12))
return struct.unpack("III", __serial[idx].read(12))
except:
return None
def enable_fb(port, enable):
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_FB_ENABLE, 4))
__serial[idx].write(struct.pack("<I", enable))
except:
pass
def arch_str():
try:
idx = __port.index(port)
__serial[idx].write(struct.pack("<BBI", __USBDBG_CMD, __USBDBG_ARCH_STR, 64))
return __serial[idx].read(64).split('\0', 1)[0]
except:
return None
if __name__ == '__main__':
if len(sys.argv)!= 3:
print ('usage: pyopenmv.py <port> <script>')
sys.exit(1)
with open(sys.argv[2], 'r') as fin:
buf = fin.read()
portname = sys.argv[1]
disconnect(portname)
init(portname)
stop_script(portname)
exec_script(portname, buf)
tx_len = tx_buf_len(portname)
time.sleep(0.250)
if (tx_len):
print(tx_buf(portname, tx_len).decode())
disconnect(portname)

View File

@ -1,77 +0,0 @@
#!/usr/bin/env python2
# This file is part of the OpenMV project.
#
# Copyright (c) 2013-2021 Ibrahim Abdelkader <iabdalkader@openmv.io>
# Copyright (c) 2013-2021 Kwabena W. Agyeman <kwagyeman@openmv.io>
#
# This work is licensed under the MIT license, see the file LICENSE for details.
#
# This script stress-tests script execution.
import sys, os
import pyopenmv
import argparse
from time import sleep
from random import randint
def main():
# CMD args parser
parser = argparse.ArgumentParser(description='openmv stress test')
parser.add_argument("-j", "--disable_fb", action = "store_true", help = "Disable FB JPEG compression")
parser.add_argument("-p", "--port", action = "store", help = "OpenMV serial port")
parser.add_argument("-t", "--time", action = "store", default = 100, help = "Max time before stopping the script")
parser.add_argument("-s", "--script", action = "store",\
default="../scripts/examples/00-HelloWorld/helloworld.py", help = "OpenMV script file")
# Parse CMD args
args = parser.parse_args()
# init openmv
if (args.port):
portname = args.port
elif 'darwin' in sys.platform:
portname = "/dev/cu.usbmodem14221"
else:
portname = "/dev/openmvcam"
print("\n>>>Reading script: %s\n" %(args.script))
with open(args.script, "r") as f:
script = f.read()
print("%s\n" %(script))
connected = False
for i in range(10):
try:
# Open serial port.
# Set small timeout when connecting
pyopenmv.init(portname, baudrate=921600, timeout=0.050)
connected = True
break
except Exception as e:
connected = False
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.
pyopenmv.set_timeout(0.500)
# Enable/Disable framebuffer compression.
print(">>>Enable FB JPEG compression %s" %(str(not args.disable_fb)))
pyopenmv.enable_fb(not args.disable_fb)
# Interrupt running script.
pyopenmv.stop_script()
max_timeout = int(args.time)
for i in range(1000):
pyopenmv.exec_script(script)
sleep(randint(0, max_timeout)/1000)
pyopenmv.stop_script()
if __name__ == '__main__':
main()