Last active
March 7, 2026 16:52
-
-
Save Staars/ce67a605ffe7ef819604cf8cd898ca2b to your computer and use it in GitHub Desktop.
Attempt to support a camera sensor just from the specs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # in preinit.be: load("/ov02c10.be") | |
| class CSI_Sensor | |
| var name | |
| var wire | |
| var addr | |
| static REG_END = 0xFFFF | |
| static REG_DELAY = 0xFFFE | |
| def init(name, addr) | |
| self.name = name | |
| self.addr = addr | |
| end | |
| def write_reg16(reg, val) | |
| if !self.wire return false end | |
| self.wire._begin_transmission(self.addr) | |
| self.wire._write((reg >> 8) & 0xFF) | |
| self.wire._write(reg & 0xFF) | |
| self.wire._write(val) | |
| return self.wire._end_transmission() == 0 | |
| end | |
| def read_reg16(reg) | |
| if !self.wire return nil end | |
| self.wire._begin_transmission(self.addr) | |
| self.wire._write((reg >> 8) & 0xFF) | |
| self.wire._write(reg & 0xFF) | |
| var err = self.wire._end_transmission(false) | |
| if err != 0 return nil end | |
| self.wire._request_from(self.addr, 1) | |
| if self.wire._available() | |
| return self.wire._read() | |
| end | |
| return nil | |
| end | |
| def write_array(reg_array) | |
| var i = 0 | |
| var sz = size(reg_array) | |
| while i < sz | |
| var reg = reg_array[i][0] | |
| var val = reg_array[i][1] | |
| if reg == self.REG_END | |
| break | |
| elif reg == self.REG_DELAY | |
| tasmota.delay(val) | |
| else | |
| if !self.write_reg16(reg, val) | |
| print(format("%s: FAILED at reg 0x%04X", self.name, reg)) | |
| return false | |
| end | |
| end | |
| i += 1 | |
| end | |
| return true | |
| end | |
| end | |
| class OV02C10 : CSI_Sensor | |
| static ADDR = 0x36 | |
| static CHIP_ID = 0x5602 | |
| var is_streaming | |
| var is_initialized | |
| var width, height | |
| var mipi_clock | |
| var format, bin_mode | |
| def init() | |
| super(self).init("OV02C10", self.ADDR) | |
| self.is_streaming = false | |
| self.is_initialized = false | |
| self.width = 1920 | |
| self.height = 1080 | |
| self.mipi_clock = 800 | |
| self.format = 1 | |
| self.bin_mode = 1 | |
| end | |
| def detect() | |
| self.wire = tasmota.wire_scan(self.addr) | |
| if !self.wire | |
| print("OV02C10: I2C scan failed") | |
| return false | |
| end | |
| tasmota.delay(10) | |
| var id_h = self.read_reg16(0x300A) | |
| var id_l = self.read_reg16(0x300B) | |
| if id_h == nil || id_l == nil return false end | |
| var chip_id = (id_h << 8) | id_l | |
| print(format("OV02C10: Chip ID = 0x%04X", chip_id)) | |
| return chip_id == self.CHIP_ID | |
| end | |
| def stream_on(on) | |
| if !self.wire return false end | |
| if on | |
| if self.is_streaming return true end | |
| if !self.write_reg16(0x0100, 0x01) return false end | |
| self.is_streaming = true | |
| print("OV02C10: Stream ON") | |
| else | |
| if !self.write_reg16(0x0100, 0x00) return false end | |
| self.is_streaming = false | |
| print("OV02C10: Stream OFF") | |
| end | |
| return true | |
| end | |
| def common_regs() | |
| return [ | |
| [0x0103, 0x01], | |
| [self.REG_DELAY, 10], | |
| [0x0100, 0x00], | |
| [self.REG_END, 0x00] | |
| ] | |
| end | |
| # Full baseline register init (2-lane) + dynamic sensor window/VTS | |
| # Matches Espressif esp-video-components PR #46 for OV02C10 | |
| def regs_custom(x, y, w, h, bin, fps, fmt) | |
| # Sensor constraints: no binning, RAW10 only | |
| bin = 1 | |
| fmt = 1 | |
| w = (w / 8) * 8 | |
| if w < 64 w = 64 end | |
| if w > 1920 w = 1920 end | |
| if h < 64 h = 64 end | |
| if h > 1080 h = 1080 end | |
| if fps < 1 fps = 1 end | |
| if fps > 60 fps = 60 end | |
| self.width = w | |
| self.height = h | |
| self.format = fmt | |
| self.bin_mode = bin | |
| self.mipi_clock = 800 | |
| # HTS=2280 per Espressif PR (0x08E8) | |
| var hts = 2280 | |
| # VTS: FPS = 80000000 / (HTS * VTS) | |
| var vts = 80000000 / (hts * fps) | |
| if vts < 1164 vts = 1164 end | |
| if vts > 65535 vts = 65535 end | |
| # Dynamic sensor window (0x3800-0x3807): center crop from 1928x1092 | |
| # Full array: x_start=0x0000, y_start=0x0004, x_end=0x078F, y_end=0x0443 | |
| var full_w = 1928 | |
| var full_h = 1088 | |
| var win_x_start = (full_w - w) / 2 + x | |
| var win_y_start = (full_h - h) / 2 + 4 + y | |
| if win_x_start < 0 win_x_start = 0 end | |
| if win_y_start < 4 win_y_start = 4 end | |
| var win_x_end = win_x_start + w + 7 | |
| var win_y_end = win_y_start + h + 3 | |
| if win_x_end > 0x078F win_x_end = 0x078F end | |
| if win_y_end > 0x0443 win_y_end = 0x0443 end | |
| # ISP offsets: fixed values per Espressif PR | |
| var isp_off_x = 0x07 | |
| var isp_off_y = 0x04 | |
| # Exposure: near-maximum for good indoor brightness | |
| # AEC/AGC (0x3503=0x00) will auto-adjust from this starting point | |
| var exposure = vts - 14 | |
| if exposure < 4 exposure = 4 end | |
| print(format("OV02C10: CFG %dx%d FPS=%d VTS=%d HTS=%d MIPI=%dMbps", w, h, fps, vts, hts, self.mipi_clock)) | |
| return [ | |
| # --- PLL & System --- | |
| [0x0301, 0x08], [0x0303, 0x06], [0x0304, 0x01], [0x0305, 0x90], | |
| [0x0313, 0x40], [0x031C, 0x4F], | |
| [0x301B, 0xF0], [0x3020, 0x97], [0x3022, 0x01], [0x3026, 0xB4], | |
| [0x3027, 0xF1], [0x303B, 0x00], [0x303C, 0x4F], [0x303D, 0xE6], | |
| [0x303E, 0x00], [0x303F, 0x03], [0x3021, 0x23], | |
| # --- Exposure & Gain (dynamic) --- | |
| [0x3501, (exposure >> 8) & 0xFF], [0x3502, exposure & 0xFF], [0x3504, 0x0C], | |
| [0x3507, 0x00], [0x3508, 0x40], [0x3509, 0x00], | |
| [0x350A, 0x01], [0x350B, 0x00], [0x350C, 0x41], | |
| # --- Analog --- | |
| [0x3600, 0x84], [0x3603, 0x08], [0x3610, 0x57], [0x3611, 0x1B], | |
| [0x3613, 0x78], [0x3623, 0x00], [0x3632, 0xA0], [0x3642, 0xE8], | |
| [0x364C, 0x70], [0x365F, 0x0F], | |
| # --- Sensor Core --- | |
| [0x3708, 0x30], [0x3714, 0x24], [0x3725, 0x02], [0x3737, 0x08], | |
| [0x3739, 0x28], [0x3749, 0x32], [0x374A, 0x32], [0x374B, 0x32], | |
| [0x374C, 0x32], [0x374D, 0x81], [0x374E, 0x81], [0x374F, 0x81], | |
| [0x3752, 0x36], [0x3753, 0x36], [0x3754, 0x36], [0x3761, 0x00], | |
| [0x376C, 0x81], [0x3774, 0x18], [0x3776, 0x08], [0x377C, 0x81], | |
| [0x377D, 0x81], [0x377E, 0x81], [0x37A0, 0x44], [0x37A6, 0x44], | |
| [0x37AA, 0x0D], [0x37AE, 0x00], [0x37CB, 0x03], [0x37CC, 0x01], | |
| [0x37D8, 0x02], [0x37D9, 0x10], [0x37E1, 0x10], [0x37E2, 0x18], | |
| [0x37E3, 0x08], [0x37E4, 0x08], [0x37E5, 0x02], [0x37E6, 0x08], | |
| # --- Geometry (dynamic: sensor window, output size, VTS) --- | |
| [0x3800, (win_x_start >> 8) & 0xFF], [0x3801, win_x_start & 0xFF], | |
| [0x3802, (win_y_start >> 8) & 0xFF], [0x3803, win_y_start & 0xFF], | |
| [0x3804, (win_x_end >> 8) & 0xFF], [0x3805, win_x_end & 0xFF], | |
| [0x3806, (win_y_end >> 8) & 0xFF], [0x3807, win_y_end & 0xFF], | |
| [0x3808, (w >> 8) & 0xFF], [0x3809, w & 0xFF], | |
| [0x380A, (h >> 8) & 0xFF], [0x380B, h & 0xFF], | |
| [0x380C, (hts >> 8) & 0xFF], [0x380D, hts & 0xFF], | |
| [0x380E, (vts >> 8) & 0xFF], [0x380F, vts & 0xFF], | |
| [0x3810, 0x00], [0x3811, isp_off_x], | |
| [0x3812, 0x00], [0x3813, isp_off_y], | |
| [0x3814, 0x01], [0x3815, 0x01], [0x3816, 0x01], [0x3817, 0x01], | |
| # --- ISP / Readout --- | |
| [0x3820, 0xA0], [0x3821, 0x00], [0x3822, 0x80], [0x3823, 0x08], | |
| [0x3824, 0x00], [0x3825, 0x20], [0x3826, 0x00], [0x3827, 0x08], | |
| [0x382A, 0x00], [0x382B, 0x08], [0x382D, 0x00], [0x382E, 0x00], | |
| [0x382F, 0x23], [0x3834, 0x00], [0x3839, 0x00], [0x383A, 0xD1], | |
| [0x383E, 0x03], | |
| # --- Phase / Timing --- | |
| [0x393D, 0x29], [0x393F, 0x6E], | |
| [0x394B, 0x06], [0x394C, 0x06], [0x394D, 0x08], [0x394E, 0x0A], | |
| [0x394F, 0x01], [0x3950, 0x01], [0x3951, 0x01], [0x3952, 0x01], | |
| [0x3953, 0x01], [0x3954, 0x01], [0x3955, 0x01], [0x3956, 0x01], | |
| [0x3957, 0x0E], [0x3958, 0x08], [0x3959, 0x08], [0x395A, 0x08], | |
| [0x395B, 0x13], [0x395C, 0x09], [0x395D, 0x05], [0x395E, 0x02], | |
| [0x395F, 0x00], [0x3960, 0x00], [0x3961, 0x00], [0x3962, 0x00], | |
| [0x3963, 0x00], [0x3964, 0x00], [0x3965, 0x00], [0x3966, 0x00], | |
| [0x3967, 0x00], [0x3968, 0x01], [0x3969, 0x01], [0x396A, 0x01], | |
| [0x396B, 0x01], [0x396C, 0x10], [0x396D, 0xF0], [0x396E, 0x11], | |
| [0x396F, 0x00], [0x3970, 0x37], [0x3971, 0x37], [0x3972, 0x37], | |
| [0x3973, 0x37], [0x3974, 0x00], [0x3975, 0x3C], [0x3976, 0x3C], | |
| [0x3977, 0x3C], [0x3978, 0x3C], | |
| # --- Clock / Debug --- | |
| [0x3C00, 0x0F], [0x3C20, 0x01], [0x3C21, 0x08], | |
| [0x3F00, 0x8B], [0x3F02, 0x0F], | |
| # --- BLC / ISP Control --- | |
| [0x4000, 0xC3], [0x4001, 0xE0], [0x4002, 0x00], [0x4003, 0x40], | |
| [0x4008, 0x04], [0x4009, 0x23], [0x400A, 0x04], [0x400B, 0x01], | |
| [0x4041, 0x20], | |
| [0x4077, 0x06], [0x4078, 0x00], [0x4079, 0x1A], [0x407A, 0x7F], | |
| [0x407B, 0x01], [0x4080, 0x03], [0x4081, 0x84], | |
| # --- Format --- | |
| [0x4308, 0x03], [0x4309, 0xFF], [0x430D, 0x00], | |
| # --- MIPI --- | |
| [0x4806, 0x00], [0x4813, 0x00], [0x4837, 0x10], [0x4857, 0x05], | |
| [0x4884, 0x04], | |
| [0x4500, 0x07], [0x4501, 0x00], [0x4503, 0x00], | |
| [0x450A, 0x04], [0x450E, 0x00], [0x450F, 0x00], | |
| [0x4800, 0x64], [0x4900, 0x00], [0x4901, 0x00], [0x4902, 0x01], | |
| # --- OTP / Misc --- | |
| [0x4D00, 0x03], [0x4D01, 0xD8], [0x4D02, 0xBA], [0x4D03, 0xA0], | |
| [0x4D04, 0xB7], [0x4D05, 0x34], [0x4D0D, 0x00], | |
| # --- ISP Pipeline --- | |
| [0x5000, 0xFD], [0x5001, 0x50], [0x5006, 0x00], [0x5080, 0x40], | |
| [0x5181, 0x2B], [0x5202, 0xA3], [0x5206, 0x01], [0x5207, 0x00], | |
| [0x520A, 0x01], [0x520B, 0x00], | |
| # --- 2-Lane Tail --- | |
| [0x365D, 0x00], [0x4815, 0x40], [0x4816, 0x12], [0x481F, 0x30], | |
| [0x4F00, 0x01], [0x0316, 0x90], [0x3016, 0x32], | |
| # Enable AEC (auto exposure) and AGC (auto gain) | |
| # Linux driver uses manual mode (V4L2 controls it); Tasmota needs on-sensor auto | |
| [0x3503, 0x00], | |
| [self.REG_END, 0x00] | |
| ] | |
| end | |
| def camera(cmd, idx, payload, raw) | |
| if cmd == "init" | |
| print("OV02C10: ========== INIT ==========") | |
| if !self.is_initialized | |
| if !self.detect() return 0 end | |
| self.is_initialized = true | |
| end | |
| if !self.write_array(self.common_regs()) return 0 end | |
| var req_w=1920; var req_h=1080; var req_bin=1; var req_fps=30; var req_fmt=1 | |
| var req_x=0; var req_y=0 | |
| var res_idx = 0 | |
| if idx != 0 | |
| import introspect | |
| var p = introspect.toptr(idx) | |
| var b = bytes(p, 28) | |
| res_idx = b[26] | |
| if res_idx == 255 | |
| req_w = b.get(0, 2); req_h = b.get(2, 2) | |
| req_fmt = b[8]; req_bin = b[16]; req_fps = b[17] | |
| req_x = b.get(12, 2); req_y = b.get(14, 2) | |
| elif res_idx == 0 | |
| req_w=640; req_h=480; req_bin=1; req_fmt=1 | |
| elif res_idx == 1 | |
| req_w=1280; req_h=720; req_bin=1; req_fmt=1 | |
| elif res_idx == 2 | |
| req_w=1920; req_h=1080; req_bin=1; req_fmt=1 | |
| end | |
| end | |
| var regs = self.regs_custom(req_x, req_y, req_w, req_h, req_bin, req_fps, req_fmt) | |
| if !self.write_array(regs) return 0 end | |
| if idx != 0 | |
| import introspect | |
| var p = introspect.toptr(idx) | |
| var b = bytes(p, 28) | |
| b.set(0, self.width, 2) | |
| b.set(2, self.height, 2) | |
| b.set(4, 1920, 2) | |
| b.set(6, 1080, 2) | |
| b[8] = self.format | |
| b[9] = 2 | |
| b.set(10, self.mipi_clock, 2) | |
| b[16] = self.bin_mode | |
| b[17] = req_fps | |
| b.setbytes(18, bytes().fromstring("OV02C10")) | |
| end | |
| return 1 | |
| elif cmd == "stream" | |
| return self.stream_on(idx == 1) ? 1 : 0 | |
| end | |
| return 0 | |
| end | |
| end | |
| var ov02c10 = OV02C10() | |
| tasmota.add_driver(ov02c10) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment