File size: 8,476 Bytes
da590a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
package tools

import (
	"encoding/json"
	"fmt"
	"syscall"
	"unsafe"
)

// I2C ioctl constants from Linux kernel headers (<linux/i2c-dev.h>, <linux/i2c.h>)
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))
}