package tools import ( "encoding/json" "fmt" "syscall" "unsafe" ) // I2C ioctl constants from Linux kernel headers (, ) const ( i2cSlave = 0x0703 // Set slave address (fails if in use by driver) i2cFuncs = 0x0705 // Query adapter functionality bitmask i2cSmbus = 0x0720 // Perform SMBus transaction // I2C_FUNC capability bits i2cFuncSmbusQuick = 0x00010000 i2cFuncSmbusReadByte = 0x00020000 // SMBus transaction types i2cSmbusRead = 0 i2cSmbusWrite = 1 // SMBus protocol sizes i2cSmbusQuick = 0 i2cSmbusByte = 1 ) // i2cSmbusData matches the kernel union i2c_smbus_data (34 bytes max). // For quick and byte transactions only the first byte is used (if at all). type i2cSmbusData [34]byte // i2cSmbusArgs matches the kernel struct i2c_smbus_ioctl_data. type i2cSmbusArgs struct { readWrite uint8 command uint8 size uint32 data *i2cSmbusData } // smbusProbe performs a single SMBus probe at the given address. // Uses SMBus Quick Write (safest) or falls back to SMBus Read Byte for // EEPROM address ranges where quick write can corrupt AT24RF08 chips. // This matches i2cdetect's MODE_AUTO behavior. func smbusProbe(fd int, addr int, hasQuick bool) bool { // EEPROM ranges: use read byte (quick write can corrupt AT24RF08) useReadByte := (addr >= 0x30 && addr <= 0x37) || (addr >= 0x50 && addr <= 0x5F) if !useReadByte && hasQuick { // SMBus Quick Write: [START] [ADDR|W] [ACK/NACK] [STOP] // Safest probe — no data transferred args := i2cSmbusArgs{ readWrite: i2cSmbusWrite, command: 0, size: i2cSmbusQuick, data: nil, } _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args))) return errno == 0 } // SMBus Read Byte: [START] [ADDR|R] [ACK/NACK] [DATA] [STOP] var data i2cSmbusData args := i2cSmbusArgs{ readWrite: i2cSmbusRead, command: 0, size: i2cSmbusByte, data: &data, } _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSmbus, uintptr(unsafe.Pointer(&args))) return errno == 0 } // scan probes valid 7-bit addresses on a bus for connected devices. // Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO: // SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges. func (t *I2CTool) scan(args map[string]interface{}) *ToolResult { bus, errResult := parseI2CBus(args) if errResult != nil { return errResult } devPath := fmt.Sprintf("/dev/i2c-%s", bus) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) if err != nil { return ErrorResult(fmt.Sprintf("failed to open %s: %v (check permissions and i2c-dev module)", devPath, err)) } defer syscall.Close(fd) // Query adapter capabilities to determine available probe methods. // I2C_FUNCS writes an unsigned long, which is word-sized on Linux. var funcs uintptr _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cFuncs, uintptr(unsafe.Pointer(&funcs))) if errno != 0 { return ErrorResult(fmt.Sprintf("failed to query I2C adapter capabilities on %s: %v", devPath, errno)) } hasQuick := funcs&i2cFuncSmbusQuick != 0 hasReadByte := funcs&i2cFuncSmbusReadByte != 0 if !hasQuick && !hasReadByte { return ErrorResult(fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath)) } type deviceEntry struct { Address string `json:"address"` Status string `json:"status,omitempty"` } var found []deviceEntry // Scan 0x08-0x77, skipping I2C reserved addresses 0x00-0x07 for addr := 0x08; addr <= 0x77; addr++ { // Set slave address — EBUSY means a kernel driver owns this address _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr)) if errno != 0 { if errno == syscall.EBUSY { found = append(found, deviceEntry{ Address: fmt.Sprintf("0x%02x", addr), Status: "busy (in use by kernel driver)", }) } continue } if smbusProbe(fd, addr, hasQuick) { found = append(found, deviceEntry{ Address: fmt.Sprintf("0x%02x", addr), }) } } if len(found) == 0 { return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath)) } result, _ := json.MarshalIndent(map[string]interface{}{ "bus": devPath, "devices": found, "count": len(found), }, "", " ") return SilentResult(fmt.Sprintf("Scan of %s:\n%s", devPath, string(result))) } // readDevice reads bytes from an I2C device, optionally at a specific register func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult { bus, errResult := parseI2CBus(args) if errResult != nil { return errResult } addr, errResult := parseI2CAddress(args) if errResult != nil { return errResult } length := 1 if l, ok := args["length"].(float64); ok { length = int(l) } if length < 1 || length > 256 { return ErrorResult("length must be between 1 and 256") } devPath := fmt.Sprintf("/dev/i2c-%s", bus) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) if err != nil { return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err)) } defer syscall.Close(fd) // Set slave address _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr)) if errno != 0 { return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno)) } // If register is specified, write it first if regFloat, ok := args["register"].(float64); ok { reg := int(regFloat) if reg < 0 || reg > 255 { return ErrorResult("register must be between 0x00 and 0xFF") } _, err := syscall.Write(fd, []byte{byte(reg)}) if err != nil { return ErrorResult(fmt.Sprintf("failed to write register 0x%02x: %v", reg, err)) } } // Read data buf := make([]byte, length) n, err := syscall.Read(fd, buf) if err != nil { return ErrorResult(fmt.Sprintf("failed to read from device 0x%02x: %v", addr, err)) } // Format as hex bytes hexBytes := make([]string, n) intBytes := make([]int, n) for i := 0; i < n; i++ { hexBytes[i] = fmt.Sprintf("0x%02x", buf[i]) intBytes[i] = int(buf[i]) } result, _ := json.MarshalIndent(map[string]interface{}{ "bus": devPath, "address": fmt.Sprintf("0x%02x", addr), "bytes": intBytes, "hex": hexBytes, "length": n, }, "", " ") return SilentResult(string(result)) } // writeDevice writes bytes to an I2C device, optionally at a specific register func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult { confirm, _ := args["confirm"].(bool) if !confirm { return ErrorResult("write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.") } bus, errResult := parseI2CBus(args) if errResult != nil { return errResult } addr, errResult := parseI2CAddress(args) if errResult != nil { return errResult } dataRaw, ok := args["data"].([]interface{}) if !ok || len(dataRaw) == 0 { return ErrorResult("data is required for write (array of byte values 0-255)") } if len(dataRaw) > 256 { return ErrorResult("data too long: maximum 256 bytes per I2C transaction") } data := make([]byte, 0, len(dataRaw)+1) // If register is specified, prepend it to the data if regFloat, ok := args["register"].(float64); ok { reg := int(regFloat) if reg < 0 || reg > 255 { return ErrorResult("register must be between 0x00 and 0xFF") } data = append(data, byte(reg)) } for i, v := range dataRaw { f, ok := v.(float64) if !ok { return ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i)) } b := int(f) if b < 0 || b > 255 { return ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b)) } data = append(data, byte(b)) } devPath := fmt.Sprintf("/dev/i2c-%s", bus) fd, err := syscall.Open(devPath, syscall.O_RDWR, 0) if err != nil { return ErrorResult(fmt.Sprintf("failed to open %s: %v", devPath, err)) } defer syscall.Close(fd) // Set slave address _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), i2cSlave, uintptr(addr)) if errno != 0 { return ErrorResult(fmt.Sprintf("failed to set I2C address 0x%02x: %v", addr, errno)) } // Write data n, err := syscall.Write(fd, data) if err != nil { return ErrorResult(fmt.Sprintf("failed to write to device 0x%02x: %v", addr, err)) } return SilentResult(fmt.Sprintf("Wrote %d byte(s) to device 0x%02x on %s", n, addr, devPath)) }