Skip to content

Commit 2e1941c

Browse files
authored
arduino-router: Add HCI API.
Signed-off-by: iabdalkader <i.abdalkader@gmail.com>
1 parent eabb1da commit 2e1941c

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

hciapi/hci-api.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package hciapi
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"log/slog"
9+
"strconv"
10+
"sync/atomic"
11+
12+
"golang.org/x/sys/unix"
13+
14+
"github.com/arduino/arduino-router/msgpackrouter"
15+
"github.com/arduino/arduino-router/msgpackrpc"
16+
)
17+
18+
var hciSocket atomic.Int32
19+
20+
//nolint:gochecknoinits
21+
func init() {
22+
hciSocket.Store(-1)
23+
}
24+
25+
// Register registers the HCI API methods with the router.
26+
func Register(router *msgpackrouter.Router) {
27+
_ = router.RegisterMethod("hci/open", HCIOpen)
28+
_ = router.RegisterMethod("hci/send", HCISend)
29+
_ = router.RegisterMethod("hci/recv", HCIRecv)
30+
_ = router.RegisterMethod("hci/avail", HCIAvail)
31+
_ = router.RegisterMethod("hci/close", HCIClose)
32+
}
33+
34+
// HCIOpen opens an HCI socket bound to the specified device (e.g. "hci0").
35+
func HCIOpen(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) {
36+
if len(params) != 1 {
37+
return nil, []any{1, "Expected one parameter: HCI device name (e.g., 'hci0')"}
38+
}
39+
40+
deviceName, ok := params[0].(string)
41+
if !ok {
42+
return nil, []any{1, "Invalid parameter type: expected string for device name"}
43+
}
44+
45+
if len(deviceName) < 4 || deviceName[:3] != "hci" {
46+
return nil, []any{1, "Invalid device name format, expected 'hciX' where X is device number"}
47+
}
48+
49+
devNum, err := strconv.Atoi(deviceName[3:])
50+
if err != nil || devNum < 0 || devNum > 0xFFFF {
51+
return nil, []any{1, "Invalid device number in device name"}
52+
}
53+
54+
// Close any existing socket
55+
if fd := hciSocket.Swap(-1); fd >= 0 {
56+
_ = unix.Close(int(fd))
57+
}
58+
59+
// Create raw HCI socket
60+
fd, err := unix.Socket(unix.AF_BLUETOOTH, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.BTPROTO_HCI)
61+
if err != nil {
62+
return nil, []any{3, fmt.Sprintf("Failed to create HCI socket: %v", err)}
63+
}
64+
65+
// Bring down the HCI device using ioctl (HCIDEVDOWN)
66+
const HCIDEVDOWN = 0x400448CA // from <bluetooth/hci.h>
67+
68+
if err := unix.IoctlSetInt(fd, HCIDEVDOWN, devNum); err != nil {
69+
unix.Close(fd)
70+
return nil, []any{3, "Failed to bring down HCI device: " + err.Error()}
71+
}
72+
slog.Info("Brought down HCI device", "device", deviceName)
73+
74+
// Bind to device (user channel)
75+
addr := &unix.SockaddrHCI{
76+
Dev: uint16(devNum), //nolint:gosec
77+
Channel: unix.HCI_CHANNEL_USER,
78+
}
79+
80+
if err := unix.Bind(fd, addr); err != nil {
81+
unix.Close(fd)
82+
return nil, []any{3, fmt.Sprintf("Failed to bind to HCI device: %v", err)}
83+
}
84+
85+
hciSocket.Store(int32(fd)) //nolint:gosec
86+
slog.Info("Opened HCI device", "device", deviceName, "fd", fd)
87+
return true, nil
88+
}
89+
90+
// HCIClose closes the currently open HCI socket.
91+
func HCIClose(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) {
92+
if len(params) != 0 {
93+
return nil, []any{1, "Expected no parameters"}
94+
}
95+
96+
if fd := hciSocket.Swap(-1); fd >= 0 {
97+
unix.Close(int(fd))
98+
}
99+
100+
slog.Info("Closed HCI device")
101+
return true, nil
102+
}
103+
104+
// HCISend transmits raw data to the open HCI socket.
105+
func HCISend(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) {
106+
if len(params) != 1 {
107+
return nil, []any{1, "Expected one parameter: data to send"}
108+
}
109+
110+
var data []byte
111+
switch v := params[0].(type) {
112+
case []byte:
113+
data = v
114+
case string:
115+
data = []byte(v)
116+
default:
117+
return nil, []any{1, "Invalid parameter type, expected []byte or string"}
118+
}
119+
120+
fd := hciSocket.Load()
121+
if fd < 0 {
122+
return nil, []any{2, "No HCI device open"}
123+
}
124+
125+
n, err := unix.Write(int(fd), data)
126+
if err != nil {
127+
slog.Error("Failed to send HCI packet", "err", err)
128+
return nil, []any{3, fmt.Sprintf("Failed to send HCI packet: %v", err)}
129+
}
130+
131+
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
132+
slog.Debug("Sent HCI packet", "bytes", n, "data", hex.EncodeToString(data))
133+
}
134+
return n, nil
135+
}
136+
137+
// HCIRecv reads available data from the HCI socket.
138+
func HCIRecv(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) {
139+
if len(params) != 1 {
140+
return nil, []any{1, "Expected one parameter: max bytes to receive"}
141+
}
142+
143+
size, ok := msgpackrpc.ToUint(params[0])
144+
if !ok {
145+
return nil, []any{1, "Invalid parameter type, expected uint for max bytes"}
146+
}
147+
148+
fd := hciSocket.Load()
149+
if fd < 0 {
150+
return nil, []any{2, "No HCI device open"}
151+
}
152+
153+
buffer := make([]byte, size)
154+
155+
// Short timeout (1ms) for non-blocking behavior
156+
tv := unix.Timeval{Usec: 1000}
157+
if err := unix.SetsockoptTimeval(int(fd), unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil {
158+
return nil, []any{3, fmt.Sprintf("Failed to set read timeout: %v", err)}
159+
}
160+
161+
n, err := unix.Read(int(fd), buffer)
162+
if err != nil {
163+
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) {
164+
slog.Debug("HCI recv timeout - no data available")
165+
return []byte{}, nil
166+
}
167+
slog.Error("Failed to receive HCI packet", "err", err)
168+
return nil, []any{3, fmt.Sprintf("Failed to receive HCI packet: %v", err)}
169+
}
170+
171+
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
172+
slog.Debug("Received HCI packet", "bytes", n, "data", hex.EncodeToString(buffer[:n]))
173+
}
174+
return buffer[:n], nil
175+
}
176+
177+
// HCIAvail checks whether data is available to read on the HCI socket.
178+
func HCIAvail(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) {
179+
if len(params) != 0 {
180+
return nil, []any{1, "Expected no parameters"}
181+
}
182+
183+
fd := hciSocket.Load()
184+
if fd < 0 {
185+
return nil, []any{2, "No HCI device open"}
186+
}
187+
188+
fds := []unix.PollFd{{
189+
Fd: fd,
190+
Events: unix.POLLIN,
191+
}}
192+
193+
n, err := unix.Poll(fds, 0)
194+
if err != nil {
195+
if errors.Is(err, unix.EINTR) {
196+
return false, nil
197+
}
198+
slog.Error("Failed to poll HCI socket", "err", err)
199+
return nil, []any{3, fmt.Sprintf("Poll failed: %v", err)}
200+
}
201+
202+
return n > 0 && (fds[0].Revents&unix.POLLIN) != 0, nil
203+
}

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"syscall"
1515
"time"
1616

17+
"github.com/arduino/arduino-router/hciapi"
1718
"github.com/arduino/arduino-router/monitorapi"
1819
"github.com/arduino/arduino-router/msgpackrouter"
1920
"github.com/arduino/arduino-router/msgpackrpc"
@@ -141,6 +142,9 @@ func startRouter(cfg Config) error {
141142
// Register TCP network API methods
142143
networkapi.Register(router)
143144

145+
// Register HCI API methods
146+
hciapi.Register(router)
147+
144148
// Register monitor API methods
145149
if err := monitorapi.Register(router, cfg.MonitorPortAddr); err != nil {
146150
slog.Error("Failed to register monitor API", "err", err)

0 commit comments

Comments
 (0)