Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
## 变更内容
### k8s/ingress.yaml
- 新增 /xiaozhi/v1/ 路径规则,将 WebSocket 流量路由到 hw-ws-svc:8888
- Traefik 最长前缀优先,/xiaozhi/v1/ 不影响 / 下的 Django 路由
### hw_service_go/k8s/service.yaml
- Service 类型由 LoadBalancer 改为 ClusterIP
- 移除阿里云 SLB 注解(通过 Traefik Ingress 统一暴露,不再需要独立公网 IP)
### hw_service_go/k8s/deployment.yaml
- 镜像地址改为 ${CI_REGISTRY_IMAGE}/hw-ws-service:latest 占位符
- CI/CD 部署时统一通过 sed 替换为华为云 SWR 实际地址
### hw_service_go/internal/server/server.go
- 新增 GET /xiaozhi/v1/healthz 接口,返回 {"status":"ok","active_connections":N}
- 用于部署后验证服务存活及当前连接数
### .gitea/workflows/deploy.yaml
- 新增 Build and Push HW WebSocket Service 步骤,构建并推送 hw_service_go 镜像
- 部署步骤新增 kubectl apply hw_service_go/k8s/deployment.yaml 和 service.yaml
- 新增 kubectl rollout restart deployment/hw-ws-service
### run.sh
- 本地同时启动 Django + hw_service_go 的开发脚本
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
2.1 KiB
Go
86 lines
2.1 KiB
Go
// Package connection 管理单个 ESP32 硬件 WebSocket 连接的状态。
|
|
package connection
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// Connection 保存单个硬件连接的状态,所有方法并发安全。
|
|
type Connection struct {
|
|
WS *websocket.Conn
|
|
DeviceID string // MAC 地址,来自 URL 参数 device-id
|
|
ClientID string // 来自 URL 参数 client-id
|
|
|
|
mu sync.Mutex
|
|
isPlaying bool
|
|
abortCh chan struct{} // close(abortCh) 通知流控 goroutine 中止播放
|
|
|
|
writeMu sync.Mutex // gorilla/websocket 写操作不并发安全,需独立锁
|
|
}
|
|
|
|
// New 创建新连接对象。
|
|
func New(ws *websocket.Conn, deviceID, clientID string) *Connection {
|
|
return &Connection{
|
|
WS: ws,
|
|
DeviceID: deviceID,
|
|
ClientID: clientID,
|
|
}
|
|
}
|
|
|
|
// StartPlayback 开始新一轮播放,返回 abortCh 供流控 goroutine 监听。
|
|
// 若已在播放,先中止上一轮再开始新的。
|
|
func (c *Connection) StartPlayback() <-chan struct{} {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// 中止上一轮播放(若有)
|
|
if c.isPlaying && c.abortCh != nil {
|
|
close(c.abortCh)
|
|
}
|
|
|
|
c.abortCh = make(chan struct{})
|
|
c.isPlaying = true
|
|
return c.abortCh
|
|
}
|
|
|
|
// StopPlayback 结束播放状态。
|
|
func (c *Connection) StopPlayback() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.isPlaying = false
|
|
}
|
|
|
|
// IsPlaying 返回当前是否正在播放。
|
|
func (c *Connection) IsPlaying() bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.isPlaying
|
|
}
|
|
|
|
// SendJSON 序列化 v 并以文本帧发送给设备,并发安全。
|
|
func (c *Connection) SendJSON(v any) error {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Errorf("connection: marshal json: %w", err)
|
|
}
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
return c.WS.WriteMessage(websocket.TextMessage, data)
|
|
}
|
|
|
|
// SendBinary 以二进制帧发送 Opus 数据,并发安全。
|
|
func (c *Connection) SendBinary(data []byte) error {
|
|
c.writeMu.Lock()
|
|
defer c.writeMu.Unlock()
|
|
return c.WS.WriteMessage(websocket.BinaryMessage, data)
|
|
}
|
|
|
|
// Close 关闭底层 WebSocket 连接。
|
|
func (c *Connection) Close() {
|
|
c.WS.Close()
|
|
}
|