Compare commits
No commits in common. "79d8beb942885ae8e1f35f33e5749f9dd6b31668" and "c219ec2fcf8bc7e6a7f161b152e3a4986cded171" have entirely different histories.
79d8beb942
...
c219ec2fcf
@ -306,73 +306,6 @@ class DeviceViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
return success(message='WiFi 配置成功')
|
return success(message='WiFi 配置成功')
|
||||||
|
|
||||||
@action(
|
|
||||||
detail=False, methods=['get'],
|
|
||||||
url_path='stories',
|
|
||||||
authentication_classes=[], permission_classes=[AllowAny]
|
|
||||||
)
|
|
||||||
def stories_by_mac(self, request):
|
|
||||||
"""
|
|
||||||
获取设备关联用户的随机故事(公开接口,无需认证)
|
|
||||||
GET /api/v1/devices/stories/?mac_address=AA:BB:CC:DD:EE:FF
|
|
||||||
供 hw-ws-service 调用。
|
|
||||||
优先返回用户自己的故事,无则兜底返回系统默认故事(is_default=True)。
|
|
||||||
"""
|
|
||||||
mac = request.query_params.get('mac_address', '').strip()
|
|
||||||
if not mac:
|
|
||||||
return error(message='mac_address 参数不能为空')
|
|
||||||
|
|
||||||
mac = mac.upper().replace('-', ':')
|
|
||||||
|
|
||||||
try:
|
|
||||||
device = Device.objects.get(mac_address=mac)
|
|
||||||
except Device.DoesNotExist:
|
|
||||||
return error(
|
|
||||||
code=ErrorCode.DEVICE_NOT_FOUND,
|
|
||||||
message='未找到对应设备',
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
user_device = (
|
|
||||||
UserDevice.objects
|
|
||||||
.filter(device=device, is_active=True, bind_type='owner')
|
|
||||||
.select_related('user')
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not user_device:
|
|
||||||
return error(
|
|
||||||
code=ErrorCode.NOT_FOUND,
|
|
||||||
message='该设备尚未绑定用户',
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
from apps.stories.models import Story
|
|
||||||
# 优先随机取用户自己有 audio_url 的故事
|
|
||||||
story = (
|
|
||||||
Story.objects
|
|
||||||
.filter(user=user_device.user)
|
|
||||||
.exclude(audio_url='')
|
|
||||||
.order_by('?')
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
# 兜底:用户暂无故事时使用系统默认故事
|
|
||||||
if not story:
|
|
||||||
story = (
|
|
||||||
Story.objects
|
|
||||||
.filter(is_default=True)
|
|
||||||
.exclude(audio_url='')
|
|
||||||
.order_by('?')
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not story:
|
|
||||||
return error(
|
|
||||||
code=ErrorCode.STORY_NOT_FOUND,
|
|
||||||
message='暂无可播放的故事',
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
return success(data={'title': story.title, 'audio_url': story.audio_url})
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='report-status',
|
@action(detail=False, methods=['post'], url_path='report-status',
|
||||||
authentication_classes=[], permission_classes=[AllowAny])
|
authentication_classes=[], permission_classes=[AllowAny])
|
||||||
def report_status(self, request):
|
def report_status(self, request):
|
||||||
|
|||||||
@ -12,14 +12,12 @@ import (
|
|||||||
// Connection 保存单个硬件连接的状态,所有方法并发安全。
|
// Connection 保存单个硬件连接的状态,所有方法并发安全。
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
WS *websocket.Conn
|
WS *websocket.Conn
|
||||||
DeviceID string // MAC 地址,来自 URL 参数 device-id
|
DeviceID string // MAC 地址,来自 URL 参数 device-id
|
||||||
ClientID string // 来自 URL 参数 client-id
|
ClientID string // 来自 URL 参数 client-id
|
||||||
SessionID string // 握手后分配的会话 ID
|
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
handshaked bool // 是否已完成 hello 握手
|
isPlaying bool
|
||||||
isPlaying bool
|
abortCh chan struct{} // close(abortCh) 通知流控 goroutine 中止播放
|
||||||
abortCh chan struct{} // close(abortCh) 通知流控 goroutine 中止播放
|
|
||||||
|
|
||||||
writeMu sync.Mutex // gorilla/websocket 写操作不并发安全,需独立锁
|
writeMu sync.Mutex // gorilla/websocket 写操作不并发安全,需独立锁
|
||||||
}
|
}
|
||||||
@ -33,30 +31,6 @@ func New(ws *websocket.Conn, deviceID, clientID string) *Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handshake 完成 hello 握手,存储 session_id。
|
|
||||||
func (c *Connection) Handshake(sessionID string) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.SessionID = sessionID
|
|
||||||
c.handshaked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsHandshaked 返回连接是否已完成 hello 握手。
|
|
||||||
func (c *Connection) IsHandshaked() bool {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.handshaked
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendCmd 向硬件发送控制指令,并发安全。
|
|
||||||
func (c *Connection) SendCmd(action string, params any) error {
|
|
||||||
return c.SendJSON(map[string]any{
|
|
||||||
"type": "cmd",
|
|
||||||
"action": action,
|
|
||||||
"params": params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartPlayback 开始新一轮播放,返回 abortCh 供流控 goroutine 监听。
|
// StartPlayback 开始新一轮播放,返回 abortCh 供流控 goroutine 监听。
|
||||||
// 若已在播放,先中止上一轮再开始新的。
|
// 若已在播放,先中止上一轮再开始新的。
|
||||||
func (c *Connection) StartPlayback() <-chan struct{} {
|
func (c *Connection) StartPlayback() <-chan struct{} {
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/qy/hw-ws-service/internal/connection"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandleAbort 处理硬件发来的 {"type":"abort"} 指令,中止当前播放。
|
|
||||||
func HandleAbort(conn *connection.Connection) {
|
|
||||||
log.Printf("[abort][%s] stopping playback", conn.DeviceID)
|
|
||||||
conn.StopPlayback()
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/qy/hw-ws-service/internal/connection"
|
|
||||||
)
|
|
||||||
|
|
||||||
// helloMessage 是硬件发来的 hello 握手消息。
|
|
||||||
type helloMessage struct {
|
|
||||||
MAC string `json:"mac"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleHello 处理硬件的 hello 握手消息。
|
|
||||||
// 校验 MAC 地址,分配 session_id,返回握手响应。
|
|
||||||
func HandleHello(conn *connection.Connection, raw []byte) error {
|
|
||||||
var msg helloMessage
|
|
||||||
if err := json.Unmarshal(raw, &msg); err != nil {
|
|
||||||
return fmt.Errorf("hello: invalid json: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAC 地址与 URL 参数不一致时记录警告,但不拒绝连接
|
|
||||||
if msg.MAC != "" && !strings.EqualFold(msg.MAC, conn.DeviceID) {
|
|
||||||
log.Printf("[hello][%s] MAC mismatch: url=%s body=%s", conn.DeviceID, conn.DeviceID, msg.MAC)
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID := newSessionID()
|
|
||||||
conn.Handshake(sessionID)
|
|
||||||
|
|
||||||
return conn.SendJSON(map[string]string{
|
|
||||||
"type": "hello",
|
|
||||||
"status": "ok",
|
|
||||||
"session_id": sessionID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSessionID() string {
|
|
||||||
b := make([]byte, 4)
|
|
||||||
rand.Read(b) //nolint:errcheck // crypto/rand.Read 在标准库中不会返回错误
|
|
||||||
return fmt.Sprintf("%x", b)
|
|
||||||
}
|
|
||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/qy/hw-ws-service/internal/connection"
|
"github.com/qy/hw-ws-service/internal/connection"
|
||||||
@ -23,8 +22,6 @@ const (
|
|||||||
maxConnections = 500
|
maxConnections = 500
|
||||||
// maxMessageBytes WebSocket 单条消息上限(4KB),防止内存耗尽攻击。
|
// maxMessageBytes WebSocket 单条消息上限(4KB),防止内存耗尽攻击。
|
||||||
maxMessageBytes = 4 * 1024
|
maxMessageBytes = 4 * 1024
|
||||||
// helloTimeout 握手超时:连接建立后必须在此时间内发送 hello,否则断开。
|
|
||||||
helloTimeout = 10 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@ -134,17 +131,7 @@ func (s *Server) handleConn(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
log.Printf("server: device %s connected, active=%d", deviceID, s.activeCount())
|
log.Printf("server: device %s connected, active=%d", deviceID, s.activeCount())
|
||||||
|
|
||||||
// 阶段1:等待 hello 握手(超时 helloTimeout)
|
// 消息读取循环
|
||||||
ws.SetReadDeadline(time.Now().Add(helloTimeout)) //nolint:errcheck
|
|
||||||
if !s.waitForHello(conn) {
|
|
||||||
log.Printf("server: device %s hello timeout or failed", deviceID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ws.SetReadDeadline(time.Time{}) //nolint:errcheck // 握手成功,取消读超时
|
|
||||||
|
|
||||||
log.Printf("server: device %s handshaked, session=%s", deviceID, conn.SessionID)
|
|
||||||
|
|
||||||
// 阶段2:正常消息循环
|
|
||||||
for {
|
for {
|
||||||
msgType, raw, err := ws.ReadMessage()
|
msgType, raw, err := ws.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -156,7 +143,7 @@ func (s *Server) handleConn(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只处理文本消息
|
// 只处理文本消息(二进制为上行音频,本服务暂不处理)
|
||||||
if msgType != websocket.TextMessage {
|
if msgType != websocket.TextMessage {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -172,40 +159,12 @@ func (s *Server) handleConn(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch envelope.Type {
|
switch envelope.Type {
|
||||||
case "story":
|
case "story":
|
||||||
go handler.HandleStory(conn, s.client)
|
go handler.HandleStory(conn, s.client)
|
||||||
case "abort":
|
|
||||||
handler.HandleAbort(conn)
|
|
||||||
default:
|
default:
|
||||||
log.Printf("server: unhandled message type %q from %s", envelope.Type, deviceID)
|
log.Printf("server: unhandled message type %q from %s", envelope.Type, deviceID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForHello 等待并处理第一条 hello 消息,成功返回 true。
|
|
||||||
func (s *Server) waitForHello(conn *connection.Connection) bool {
|
|
||||||
msgType, raw, err := conn.WS.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if msgType != websocket.TextMessage {
|
|
||||||
log.Printf("server: device %s sent non-text as first message", conn.DeviceID)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var envelope struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(raw, &envelope); err != nil || envelope.Type != "hello" {
|
|
||||||
log.Printf("server: device %s first message is not hello (got %q)", conn.DeviceID, envelope.Type)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := handler.HandleHello(conn, raw); err != nil {
|
|
||||||
log.Printf("server: device %s hello failed: %v", conn.DeviceID, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// register 注册连接,若同一设备已有连接则踢掉旧连接。
|
// register 注册连接,若同一设备已有连接则踢掉旧连接。
|
||||||
func (s *Server) register(conn *connection.Connection) error {
|
func (s *Server) register(conn *connection.Connection) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@ -225,21 +184,6 @@ func (s *Server) register(conn *connection.Connection) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendCmd 向指定设备发送控制指令。
|
|
||||||
// 若设备不在线或未握手,返回 error。
|
|
||||||
func (s *Server) SendCmd(deviceID, action string, params any) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
conn, ok := s.conns[deviceID]
|
|
||||||
s.mu.Unlock()
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("server: device %s not connected", deviceID)
|
|
||||||
}
|
|
||||||
if !conn.IsHandshaked() {
|
|
||||||
return fmt.Errorf("server: device %s not handshaked", deviceID)
|
|
||||||
}
|
|
||||||
return conn.SendCmd(action, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) unregister(deviceID string) {
|
func (s *Server) unregister(deviceID string) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user