Skip to content

Commit 552e2dc

Browse files
khavrksclaude
andcommitted
Add Go source, CI, and open-source scaffolding
- Add relay, ws, and queue packages with WebSocket hub, session manager, and 4 queue backends - Add typed MessageType constants for BLE protocol messages - Add README with badges, architecture docs, and quick start - Add .gitignore, CHANGELOG.md, and GitHub Actions CI workflow - Add example server in examples/basic/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e79b25 commit 552e2dc

18 files changed

Lines changed: 2516 additions & 0 deletions

File tree

.github/workflows/go.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
go-version: ['1.22', '1.23']
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version: ${{ matrix.go-version }}
23+
24+
- name: Build
25+
run: go build ./...
26+
27+
- name: Vet
28+
run: go vet ./...

.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Binaries
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary
9+
*.test
10+
11+
# Output of go coverage
12+
*.out
13+
*.prof
14+
15+
# Go workspace
16+
go.work
17+
go.work.sum
18+
19+
# IDE
20+
.idea/
21+
.vscode/
22+
*.swp
23+
*.swo
24+
*~
25+
26+
# OS
27+
.DS_Store
28+
Thumbs.db
29+
30+
# Environment
31+
.env
32+
.env.local
33+
.env.*.local
34+
35+
# Vendor (uncomment if you vendor)
36+
# vendor/

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Changelog
2+
3+
## v0.1.0 — 2026-03-26
4+
5+
Initial release.
6+
7+
### Features
8+
9+
- **WebSocket hub** (`ws`) — multi-connection per user, single relay per device, first-connect/last-disconnect lifecycle hooks, subscription-based device broadcasting
10+
- **Session manager** (`relay`) — request-response correlation with timeouts over async WebSocket, BLE device config registry
11+
- **Offline command queue** (`queue`) — TTL-based expiration, command dedup by type, confirmation workflow, drain-on-reconnect
12+
- Memory backend — for testing and development
13+
- Postgres backend — durable, with UPSERT dedup
14+
- Redis backend — fast, with natural TTL support
15+
- RabbitMQ backend — reliable delivery with per-device queues and real-time consumption
16+
- Framework-agnostic WebSocket handler (works with net/http, Chi, Gin, Echo, Fiber)
17+
- Per-connection rate limiting

README.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,219 @@
11
# bridgekit
2+
3+
[![Go Reference](https://pkg.go.dev/badge/github.com/khavrks/bridgekit.svg)](https://pkg.go.dev/github.com/khavrks/bridgekit)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/khavrks/bridgekit)](https://goreportcard.com/report/github.com/khavrks/bridgekit)
5+
[![CI](https://github.com/khavrks/bridgekit/actions/workflows/go.yml/badge.svg)](https://github.com/khavrks/bridgekit/actions/workflows/go.yml)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7+
28
Go toolkit for server-to-device communication through intermediary bridges. WebSocket hub, request-response correlation, offline command queue.
9+
10+
## The Problem
11+
12+
Your server needs to send commands to a BLE device, but BLE is a local radio protocol — servers can't reach devices directly. A phone physically near the device acts as a bridge.
13+
14+
```
15+
Server ←──WebSocket──→ Phone App ←──BLE──→ Device
16+
```
17+
18+
This library handles the server side of that architecture:
19+
20+
- **WebSocket hub** — manages connections from phones and users, with lifecycle hooks and subscription-based broadcasting
21+
- **Session manager** — correlates outgoing commands with responses using request IDs and timeouts
22+
- **Offline queue** — stores commands when a device is offline, delivers them on reconnect
23+
- **Device config registry** — maps device types to their BLE service/characteristic UUIDs
24+
25+
## Install
26+
27+
```bash
28+
go get github.com/khavrks/bridgekit
29+
```
30+
31+
## Packages
32+
33+
| Package | What it does |
34+
|---------|-------------|
35+
| `relay` | BLE protocol types, session manager, device config registry |
36+
| `ws` | WebSocket hub, client lifecycle, read/write pumps, rate limiter |
37+
| `queue` | Offline command queue interface + 4 backends (Memory, Postgres, Redis, RabbitMQ) |
38+
39+
## Quick Start
40+
41+
```go
42+
package main
43+
44+
import (
45+
"github.com/khavrks/bridgekit/relay"
46+
"github.com/khavrks/bridgekit/ws"
47+
"github.com/khavrks/bridgekit/queue"
48+
)
49+
50+
func main() {
51+
// 1. Create and run the WebSocket hub
52+
hub := ws.NewHub()
53+
go hub.Run()
54+
55+
// 2. Register device BLE configs
56+
configs := relay.NewConfigRegistry(relay.DeviceConfig{
57+
ServiceUUID: "0000fff0-0000-1000-8000-00805f9b34fb",
58+
WriteUUID: "0000fff1-0000-1000-8000-00805f9b34fb",
59+
ListenUUID: "0000fff2-0000-1000-8000-00805f9b34fb",
60+
})
61+
62+
// 3. Create session manager (sends commands through the hub)
63+
sessions := relay.NewSessionManager(func(deviceID string, req relay.WriteRequest) bool {
64+
return hub.SendToDevice(deviceID, ws.Message{Type: "ble_write", DeviceID: deviceID})
65+
}, configs)
66+
67+
// 4. Send a command (blocks until response or timeout)
68+
resp, err := sessions.SendCommand("device-123", "smart-lock", "aabbccdd", 10*time.Second)
69+
}
70+
```
71+
72+
## Architecture
73+
74+
### WebSocket Hub
75+
76+
The hub manages two types of connections:
77+
78+
- **User connections** — a user can have multiple (tabs, devices). The hub detects first-connect and last-disconnect.
79+
- **Device connections** — one per device. This is the phone bridging BLE.
80+
81+
```go
82+
hub := ws.NewHub()
83+
hub.SetOnDeviceConnect(func(deviceID string) {
84+
log.Printf("device %s is online", deviceID)
85+
})
86+
hub.SetOnDeviceDisconnect(func(deviceID string) {
87+
log.Printf("device %s went offline", deviceID)
88+
})
89+
90+
// Subscription-based broadcasting: notify all users who care about a device
91+
hub.SetSubscriptionLoader(func(userID string) []string {
92+
return db.GetDeviceIDsForUser(userID) // your DB query
93+
})
94+
hub.BroadcastToDevice("device-123", ws.Message{Type: "state_changed"})
95+
```
96+
97+
### Session Manager
98+
99+
Request-response correlation over an async WebSocket channel:
100+
101+
```go
102+
// Server sends command → phone writes to BLE → device responds → phone sends back
103+
resp, err := sessions.SendCommand("device-123", "lock-type", "hex-payload", 10*time.Second)
104+
105+
// In your WebSocket message handler, route responses back:
106+
sessions.HandleResponse(bleResponse)
107+
```
108+
109+
### Offline Queue
110+
111+
Commands for offline devices are stored and delivered on reconnect:
112+
113+
```go
114+
// Pick your backend — all implement queue.Store
115+
q := queue.NewMemoryStore() // dev/testing
116+
q := queue.NewPostgresStore(pgPool) // durable, battle-tested
117+
q := queue.NewRedisStore(redisClient) // fast, natural TTL
118+
q, _ := queue.NewRabbitMQStore(amqpConn, queue.RabbitMQConfig{}) // reliable delivery
119+
120+
// Queue a command (with dedup — same type overwrites)
121+
q.EnqueueOrUpdate(ctx, "device-123", "lock", payload, 1*time.Hour)
122+
123+
// On reconnect, drain all pending commands
124+
payloads, _ := q.DrainPending(ctx, "device-123")
125+
126+
// Commands requiring approval before execution
127+
cmdID, _ := q.EnqueueWithConfirmation(ctx, "device-123", "unlock", payload, 1*time.Hour)
128+
q.ConfirmCommand(ctx, cmdID) // user approves
129+
// or: q.CancelCommand(ctx, cmdID) // user denies
130+
```
131+
132+
#### RabbitMQ Consumer
133+
134+
RabbitMQ also supports real-time consumption from per-device queues:
135+
136+
```go
137+
rmq, _ := queue.NewRabbitMQStore(conn, queue.RabbitMQConfig{})
138+
139+
// When a device connects, start consuming its queue
140+
payloads, _ := rmq.Consume(ctx, "device-123")
141+
go func() {
142+
for payload := range payloads {
143+
sessions.SendCommand("device-123", "lock-type", string(payload), 10*time.Second)
144+
}
145+
}()
146+
```
147+
148+
#### Choosing a Backend
149+
150+
| Backend | Durability | Speed | Best for |
151+
|---------|-----------|-------|----------|
152+
| **Memory** | None (lost on restart) | Fastest | Testing, development |
153+
| **Postgres** | Full | Moderate | Primary store, complex queries, existing PG infra |
154+
| **Redis** | Configurable (AOF/RDB) | Fast | High throughput, natural TTL, existing Redis infra |
155+
| **RabbitMQ** | Full (persistent msgs) | Fast | Reliable delivery, fan-out, existing AMQP infra |
156+
157+
### Framework-Agnostic WebSocket Handler
158+
159+
The handler works with any HTTP framework — just pass `http.ResponseWriter` and `*http.Request`:
160+
161+
```go
162+
// net/http
163+
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
164+
ws.HandleWebSocket(hub, w, r, userID, deviceID, onMessage)
165+
})
166+
167+
// Fiber (via fasthttpadaptor)
168+
// Chi, Gin, Echo — same pattern, extract w and r from your framework
169+
```
170+
171+
## Postgres Schema
172+
173+
If using `PostgresStore`, create this table:
174+
175+
```sql
176+
CREATE TABLE command_queue (
177+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
178+
device_id TEXT NOT NULL,
179+
command_type TEXT,
180+
payload BYTEA NOT NULL,
181+
expires_at TIMESTAMPTZ NOT NULL,
182+
delivered BOOLEAN NOT NULL DEFAULT false,
183+
requires_confirmation BOOLEAN NOT NULL DEFAULT false,
184+
confirmed BOOLEAN NOT NULL DEFAULT false,
185+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
186+
);
187+
188+
CREATE UNIQUE INDEX idx_command_queue_dedup
189+
ON command_queue (device_id, command_type)
190+
WHERE delivered = false;
191+
```
192+
193+
## Phone App Side
194+
195+
Your phone app needs to:
196+
197+
1. Connect to the WebSocket server with `?userId=X&deviceId=Y`
198+
2. Connect to the BLE device
199+
3. When receiving a `ble_write` message:
200+
- Write `payload` (hex-decoded) to the device's write characteristic
201+
- Read the response from the listen characteristic
202+
- Send back a `ble_response` with the same `requestId`
203+
4. Send `ble_status` messages when BLE connectivity changes
204+
205+
See the protocol types in `relay/protocol.go` for message formats.
206+
207+
## Dependencies
208+
209+
| Dependency | Purpose |
210+
|-----------|---------|
211+
| [gorilla/websocket](https://github.com/gorilla/websocket) | WebSocket protocol |
212+
| [google/uuid](https://github.com/google/uuid) | Request correlation IDs |
213+
| [jackc/pgx](https://github.com/jackc/pgx) | Postgres queue backend |
214+
| [redis/go-redis](https://github.com/redis/go-redis) | Redis queue backend |
215+
| [rabbitmq/amqp091-go](https://github.com/rabbitmq/amqp091-go) | RabbitMQ queue backend |
216+
217+
## License
218+
219+
MIT

0 commit comments

Comments
 (0)