This commit is contained in:
seydx
2026-01-12 19:57:38 +01:00
parent 3a587c9cee
commit 039e916030
5 changed files with 145 additions and 141 deletions
+33 -32
View File
@@ -33,7 +33,7 @@ wyze:
password: "yourpassword" # or MD5 triple-hash with "md5:" prefix password: "yourpassword" # or MD5 triple-hash with "md5:" prefix
streams: streams:
wyze_cam: wyze://192.168.1.123?uid=WYZEUID1234567890AB&enr=xxx&mac=AABBCCDDEEFF&dtls=true wyze_cam: wyze://192.168.1.123?uid=WYZEUID1234567890AB&enr=xxx&mac=AABBCCDDEEFF&model=HL_CAM4&dtls=true
``` ```
## Stream URL Format ## Stream URL Format
@@ -41,7 +41,7 @@ streams:
The stream URL is automatically generated when you add cameras via the WebUI: The stream URL is automatically generated when you add cameras via the WebUI:
``` ```
wyze://[IP]?uid=[P2P_ID]&enr=[ENR]&mac=[MAC]&dtls=true wyze://[IP]?uid=[P2P_ID]&enr=[ENR]&mac=[MAC]&model=[MODEL]&subtype=[hd|sd]&dtls=true
``` ```
| Parameter | Description | | Parameter | Description |
@@ -50,18 +50,20 @@ wyze://[IP]?uid=[P2P_ID]&enr=[ENR]&mac=[MAC]&dtls=true
| `uid` | P2P identifier (20 chars) | | `uid` | P2P identifier (20 chars) |
| `enr` | Encryption key for DTLS | | `enr` | Encryption key for DTLS |
| `mac` | Device MAC address | | `mac` | Device MAC address |
| `model` | Camera model (e.g., HL_CAM4) |
| `dtls` | Enable DTLS encryption (default: true) | | `dtls` | Enable DTLS encryption (default: true) |
| `subtype` | Camera resolution: `hd` or `sd` (default: `hd`) |
## Configuration ## Configuration
### Resolution ### Resolution
You can change the camera's resolution using the `quality` parameter: You can change the camera's resolution using the `subtype` parameter:
```yaml ```yaml
streams: streams:
wyze_hd: wyze://...&quality=hd wyze_hd: wyze://...&subtype=hd
wyze_sd: wyze://...&quality=sd wyze_sd: wyze://...&subtype=sd
``` ```
### Two-Way Audio ### Two-Way Audio
@@ -74,30 +76,29 @@ Two-way audio (intercom) is supported automatically. When a consumer sends audio
|------|-------|----------|----------|------------|--------| |------|-------|----------|----------|------------|--------|
| Wyze Cam v4 | HL_CAM4 | 4.52.9.4188 | TUTK | TransCode | hevc, aac | | Wyze Cam v4 | HL_CAM4 | 4.52.9.4188 | TUTK | TransCode | hevc, aac |
| | | 4.52.9.5332 | TUTK | HMAC-SHA1 | hevc, aac | | | | 4.52.9.5332 | TUTK | HMAC-SHA1 | hevc, aac |
| Wyze Cam v3 Pro | | | | | | | Wyze Cam v3 Pro | | | TUTK | | |
| Wyze Cam v3 | | | | | | | Wyze Cam v3 | | | TUTK | | |
| Wyze Cam v2 | | | | | | | Wyze Cam v2 | | | TUTK | | |
| Wyze Cam v1 | | | | | | | Wyze Cam v1 | | | TUTK | | |
| Wyze Cam Pan v4 | | | | | | | Wyze Cam Pan v4 | | | Gwell | | |
| Wyze Cam Pan v3 | | | | | | | Wyze Cam Pan v3 | | | TUTK | | |
| Wyze Cam Pan v2 | | | | | | | Wyze Cam Pan v2 | | | TUTK | | |
| Wyze Cam Pan v1 | | | | | | | Wyze Cam Pan v1 | | | TUTK | | |
| Wyze Cam OG | | | | | | | Wyze Cam OG | | | Gwell | | |
| Wyze Cam OG Telephoto | | | | | | | Wyze Cam OG Telephoto | | | Gwell | | |
| Wyze Cam OG (2025) | | | | | | | Wyze Cam OG (2025) | | | Gwell | | |
| Wyze Cam Outdoor v2 | | | | | | | Wyze Cam Outdoor v2 | | | TUTK | | |
| Wyze Cam Outdoor v1 | | | | | | | Wyze Cam Outdoor v1 | | | TUTK | | |
| Wyze Cam Outdoor Base Station | | | | | | | Wyze Cam Floodlight Pro | | | ? | | |
| Wyze Cam Floodlight Pro | | | | | | | Wyze Cam Floodlight v2 | | | TUTK | | |
| Wyze Cam Floodlight v2 | | | | | | | Wyze Cam Floodlight | | | TUTK | | |
| Wyze Cam Floodlight | | | | | | | Wyze Video Doorbell v2 | | | TUTK | | |
| Wyze Video Doorbell v2 | | | | | | | Wyze Video Doorbell v1 | | | TUTK | | |
| Wyze Video Doorbell v1 | | | | | | | Wyze Video Doorbell Pro | | | ? | | |
| Wyze Video Doorbell Pro | | | | | | | Wyze Battery Video Doorbell | | | ? | | |
| Wyze Battery Video Doorbell | | | | | | | Wyze Duo Cam Doorbell | | | ? | | |
| Wyze Duo Cam Doorbell | | | | | | | Wyze Battery Cam Pro | | | ? | | |
| Wyze Battery Cam Pro | | | | | | | Wyze Solar Cam Pan | | | ? | | |
| Wyze Solar Cam Pan | | | | | | | Wyze Duo Cam Pan | | | ? | | |
| Wyze Duo Cam Pan | | | | | | | Wyze Window Cam | | | ? | | |
| Wyze Window Cam | | | | | | | Wyze Bulb Cam | | | ? | | |
| Wyze Bulb Cam | | | | | |
+1 -1
View File
@@ -300,7 +300,7 @@ func (c *Client) doAVLogin() error {
} }
if err := c.conn.AVClientStart(5 * time.Second); err != nil { if err := c.conn.AVClientStart(5 * time.Second); err != nil {
return fmt.Errorf("wyze: AV login failed: %w", err) return fmt.Errorf("wyze: av login failed: %w", err)
} }
if c.verbose { if c.verbose {
+109 -80
View File
@@ -31,6 +31,12 @@ type Conn struct {
conn *net.UDPConn conn *net.UDPConn
addr *net.UDPAddr addr *net.UDPAddr
// DTLS
clientConn *dtls.Conn
serverConn *dtls.Conn
clientBuf chan []byte
serverBuf chan []byte
// Identity // Identity
uid string uid string
authKey string authKey string
@@ -45,25 +51,17 @@ type Conn struct {
avResp *AVLoginResponse avResp *AVLoginResponse
// Protocol // Protocol
newProto bool newProto bool
seq uint16 seq uint16
seqCmd uint16 seqCmd uint16
avSeq uint32 avSeq uint32
kaSeq uint32 kaSeq uint32
audioSeq uint32
// DTLS audioFrameNo uint32
main *dtls.Conn
speaker *dtls.Conn
mainBuf chan []byte
speakBuf chan []byte
// Channels // Channels
rawCmd chan []byte rawCmd chan []byte
// Audio TX
audioSeq uint32
audioFrame uint32
// Frame assembly // Frame assembly
frames *FrameHandler frames *FrameHandler
ackFlags uint16 ackFlags uint16
@@ -91,12 +89,6 @@ func Dial(host, uid, authKey, enr, mac string, verbose bool) (*Conn, error) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
psk := derivePSK(enr) psk := derivePSK(enr)
if verbose {
hash := sha256.Sum256([]byte(enr))
fmt.Printf("[PSK] ENR: %q → SHA256: %x\n", enr, hash)
fmt.Printf("[PSK] PSK: %x\n", psk)
}
c := &Conn{ c := &Conn{
conn: udp, conn: udp,
addr: &net.UDPAddr{IP: net.ParseIP(host), Port: DefaultPort}, addr: &net.UDPAddr{IP: net.ParseIP(host), Port: DefaultPort},
@@ -116,8 +108,8 @@ func Dial(host, uid, authKey, enr, mac string, verbose bool) (*Conn, error) {
return nil, err return nil, err
} }
c.mainBuf = make(chan []byte, 64) c.clientBuf = make(chan []byte, 64)
c.speakBuf = make(chan []byte, 64) c.serverBuf = make(chan []byte, 64)
c.rawCmd = make(chan []byte, 16) c.rawCmd = make(chan []byte, 16)
c.frames = NewFrameHandler(c.verbose) c.frames = NewFrameHandler(c.verbose)
@@ -141,14 +133,14 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
pkt2 := c.buildAVLoginPacket(MagicAVLogin2, 572, 0x0000, randomID) pkt2 := c.buildAVLoginPacket(MagicAVLogin2, 572, 0x0000, randomID)
pkt2[20]++ // pkt2 has randomID incremented by 1 pkt2[20]++ // pkt2 has randomID incremented by 1
if _, err := c.main.Write(pkt1); err != nil { if _, err := c.clientConn.Write(pkt1); err != nil {
return fmt.Errorf("AV login 1 failed: %w", err) return fmt.Errorf("av login 1 failed: %w", err)
} }
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
if _, err := c.main.Write(pkt2); err != nil { if _, err := c.clientConn.Write(pkt2); err != nil {
return fmt.Errorf("AV login 2 failed: %w", err) return fmt.Errorf("av login 2 failed: %w", err)
} }
// Wait for response // Wait for response
@@ -167,12 +159,8 @@ func (c *Conn) AVClientStart(timeout time.Duration) error {
TwoWayStreaming: int32(data[31]), TwoWayStreaming: int32(data[31]),
} }
if c.verbose {
fmt.Printf("[TUTK] AV Login Response: two_way_streaming=%d\n", c.avResp.TwoWayStreaming)
}
ack := c.buildACK() ack := c.buildACK()
c.main.Write(ack) c.clientConn.Write(ack)
return nil return nil
} }
@@ -195,7 +183,7 @@ func (c *Conn) AVServStart() error {
} }
c.mu.Lock() c.mu.Lock()
c.speaker = conn c.serverConn = conn
c.mu.Unlock() c.mu.Unlock()
if c.verbose { if c.verbose {
@@ -204,7 +192,7 @@ func (c *Conn) AVServStart() error {
// Wait for and respond to AV Login request from camera // Wait for and respond to AV Login request from camera
if err := c.handleSpeakerAVLogin(); err != nil { if err := c.handleSpeakerAVLogin(); err != nil {
return fmt.Errorf("speaker AV login failed: %w", err) return fmt.Errorf("speaker av login failed: %w", err)
} }
return nil return nil
@@ -216,11 +204,11 @@ func (c *Conn) AVServStop() error {
// Reset audio TX state // Reset audio TX state
c.audioSeq = 0 c.audioSeq = 0
c.audioFrame = 0 c.audioFrameNo = 0
if c.speaker != nil { if c.serverConn != nil {
err := c.speaker.Close() err := c.serverConn.Close()
c.speaker = nil c.serverConn = nil
return err return err
} }
return nil return nil
@@ -240,7 +228,7 @@ func (c *Conn) AVRecvFrameData() (*Packet, error) {
func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error { func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {
c.mu.Lock() c.mu.Lock()
conn := c.speaker conn := c.serverConn
if conn == nil { if conn == nil {
c.mu.Unlock() c.mu.Unlock()
return fmt.Errorf("speaker channel not connected") return fmt.Errorf("speaker channel not connected")
@@ -253,15 +241,19 @@ func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32,
n, err := conn.Write(frame) n, err := conn.Write(frame)
if c.verbose { if c.verbose {
if err != nil { if err != nil {
fmt.Printf("[AUDIO TX] DTLS Write ERROR: %v\n", err) fmt.Printf("[SPEAKER TX] DTLS Write ERROR: %v\n", err)
} else { } else {
fmt.Printf("[AUDIO TX] DTLS Write OK: %d bytes\n", n) fmt.Printf("[SPEAKER TX] len=%d, data:\n%s", n, hexDump(frame))
} }
} }
return err return err
} }
func (c *Conn) Write(data []byte) error { func (c *Conn) Write(data []byte) error {
if c.verbose {
fmt.Printf("[UDP TX] to=%s, len=%d, data:\n%s", c.addr.String(), len(data), hexDump(data))
}
if c.newProto { if c.newProto {
_, err := c.conn.WriteToUDP(data, c.addr) _, err := c.conn.WriteToUDP(data, c.addr)
return err return err
@@ -277,6 +269,11 @@ func (c *Conn) WriteDTLS(payload []byte, channel byte) error {
} else { } else {
frame = c.buildTxData(payload, channel) frame = c.buildTxData(payload, channel)
} }
if c.verbose {
fmt.Printf("[DTLS TX] to=%s, len=%d, channel=%d, data:\n%s", c.addr.String(), len(frame), channel, hexDump(frame))
}
return c.Write(frame) return c.Write(frame)
} }
@@ -320,7 +317,7 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16,
frame := c.buildIOCtrlFrame(payload) frame := c.buildIOCtrlFrame(payload)
var t *time.Timer var t *time.Timer
t = time.AfterFunc(1, func() { t = time.AfterFunc(1, func() {
if _, err := c.main.Write(frame); err == nil && t != nil { if _, err := c.clientConn.Write(frame); err == nil && t != nil {
t.Reset(time.Second) t.Reset(time.Second)
} }
}) })
@@ -337,7 +334,7 @@ func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16,
} }
ack := c.buildACK() ack := c.buildACK()
c.main.Write(ack) c.clientConn.Write(ack)
if len(data) >= 6 { if len(data) >= 6 {
if binary.LittleEndian.Uint16(data[4:]) == expectCmd { if binary.LittleEndian.Uint16(data[4:]) == expectCmd {
@@ -357,7 +354,7 @@ func (c *Conn) GetAVLoginResponse() *AVLoginResponse {
func (c *Conn) IsBackchannelReady() bool { func (c *Conn) IsBackchannelReady() bool {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
return c.speaker != nil return c.serverConn != nil
} }
func (c *Conn) RemoteAddr() *net.UDPAddr { func (c *Conn) RemoteAddr() *net.UDPAddr {
@@ -376,13 +373,13 @@ func (c *Conn) Close() error {
c.cancel() c.cancel()
c.mu.Lock() c.mu.Lock()
if c.main != nil { if c.clientConn != nil {
c.main.Close() c.clientConn.Close()
c.main = nil c.clientConn = nil
} }
if c.speaker != nil { if c.serverConn != nil {
c.speaker.Close() c.serverConn.Close()
c.speaker = nil c.serverConn = nil
} }
if c.frames != nil { if c.frames != nil {
c.frames.Close() c.frames.Close()
@@ -449,7 +446,6 @@ func (c *Conn) discovery() error {
func (c *Conn) oldDiscoDone() error { func (c *Conn) oldDiscoDone() error {
c.Write(c.buildDisco(2)) c.Write(c.buildDisco(2))
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
_, err := c.WriteAndWait(c.buildSession(), SessionTimeout, func(res []byte) bool { _, err := c.WriteAndWait(c.buildSession(), SessionTimeout, func(res []byte) bool {
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == CmdSessionRes return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == CmdSessionRes
}) })
@@ -482,7 +478,7 @@ func (c *Conn) connect() error {
} }
c.mu.Lock() c.mu.Lock()
c.main = conn c.clientConn = conn
c.mu.Unlock() c.mu.Unlock()
if c.verbose { if c.verbose {
@@ -504,7 +500,7 @@ func (c *Conn) worker() {
default: default:
} }
n, err := c.main.Read(buf) n, err := c.clientConn.Read(buf)
if err != nil { if err != nil {
c.err = err c.err = err
return return
@@ -517,6 +513,10 @@ func (c *Conn) worker() {
data := buf[:n] data := buf[:n]
magic := binary.LittleEndian.Uint16(data) magic := binary.LittleEndian.Uint16(data)
if c.verbose {
fmt.Printf("[DTLS RX] from=%s, len=%d, data:\n%s", c.addr.String(), n, hexDump(data))
}
switch magic { switch magic {
case MagicAVLoginResp: case MagicAVLoginResp:
c.queue(c.rawCmd, data) c.queue(c.rawCmd, data)
@@ -578,7 +578,14 @@ func (c *Conn) reader() {
return return
} }
if c.verbose {
fmt.Printf("[UDP RX] from=%s, len=%d, data:\n%s", addr.String(), n, hexDump(buf[:n]))
}
if !addr.IP.Equal(c.addr.IP) { if !addr.IP.Equal(c.addr.IP) {
if c.verbose {
fmt.Printf("Ignored packet from unknown IP: %s\n", addr.IP.String())
}
continue continue
} }
if addr.Port != c.addr.Port { if addr.Port != c.addr.Port {
@@ -599,9 +606,9 @@ func (c *Conn) reader() {
dtls := buf[NewHeaderSize : n-NewAuthSize] dtls := buf[NewHeaderSize : n-NewAuthSize]
switch ch { switch ch {
case IOTCChannelMain: case IOTCChannelMain:
c.queue(c.mainBuf, dtls) c.queue(c.clientBuf, dtls)
case IOTCChannelBack: case IOTCChannelBack:
c.queue(c.speakBuf, dtls) c.queue(c.serverBuf, dtls)
} }
} }
} }
@@ -624,9 +631,9 @@ func (c *Conn) reader() {
ch := data[14] ch := data[14]
switch ch { switch ch {
case IOTCChannelMain: case IOTCChannelMain:
c.queue(c.mainBuf, data[28:]) c.queue(c.clientBuf, data[28:])
case IOTCChannelBack: case IOTCChannelBack:
c.queue(c.speakBuf, data[28:]) c.queue(c.serverBuf, data[28:])
} }
} }
} }
@@ -653,18 +660,18 @@ func (c *Conn) handleSpeakerAVLogin() error {
} }
buf := make([]byte, 1024) buf := make([]byte, 1024)
c.speaker.SetReadDeadline(time.Now().Add(5 * time.Second)) c.serverConn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := c.speaker.Read(buf) n, err := c.serverConn.Read(buf)
if err != nil { if err != nil {
return fmt.Errorf("read AV login: %w", err) return fmt.Errorf("read av login: %w", err)
} }
if c.verbose { if c.verbose {
fmt.Printf("[SPEAK] Received AV Login request: %d bytes\n", n) fmt.Printf("[SPEAK] AV Login request len=%d data:\n%s", n, hexDump(buf[:n]))
} }
if n < 24 { if n < 24 {
return fmt.Errorf("AV login too short: %d bytes", n) return fmt.Errorf("av login too short: %d bytes", n)
} }
checksum := binary.LittleEndian.Uint32(buf[20:]) checksum := binary.LittleEndian.Uint32(buf[20:])
@@ -674,20 +681,20 @@ func (c *Conn) handleSpeakerAVLogin() error {
fmt.Printf("[SPEAK] Sending AV Login response: %d bytes\n", len(resp)) fmt.Printf("[SPEAK] Sending AV Login response: %d bytes\n", len(resp))
} }
if _, err = c.speaker.Write(resp); err != nil { if _, err = c.serverConn.Write(resp); err != nil {
return fmt.Errorf("write AV login response: %w", err) return fmt.Errorf("write AV login response: %w", err)
} }
// Camera may resend, respond again // Camera may resend, respond again
c.speaker.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) c.serverConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
if n, _ = c.speaker.Read(buf); n > 0 { if n, _ = c.serverConn.Read(buf); n > 0 {
if c.verbose { if c.verbose {
fmt.Printf("[SPEAK] Received AV Login resend: %d bytes\n", n) fmt.Printf("[SPEAK] Received AV Login resend: %d bytes\n", n)
} }
c.speaker.Write(resp) c.serverConn.Write(resp)
} }
c.speaker.SetReadDeadline(time.Time{}) c.serverConn.SetReadDeadline(time.Time{})
if c.verbose { if c.verbose {
fmt.Printf("[SPEAK] AV Login complete, ready for audio\n") fmt.Printf("[SPEAK] AV Login complete, ready for audio\n")
@@ -767,9 +774,10 @@ func (c *Conn) buildAVLoginPacket(magic uint16, size int, flags uint16, randomID
binary.LittleEndian.PutUint16(b[16:], uint16(size-24)) // payload size binary.LittleEndian.PutUint16(b[16:], uint16(size-24)) // payload size
binary.LittleEndian.PutUint16(b[18:], flags) binary.LittleEndian.PutUint16(b[18:], flags)
copy(b[20:], randomID[:4]) copy(b[20:], randomID[:4])
copy(b[24:], DefaultUser) // username copy(b[24:], DefaultUser) // username
copy(b[280:], c.enr) // password (ENR) copy(b[280:], c.enr) // password/ENR
binary.LittleEndian.PutUint32(b[540:], 2) // security_mode=AV_SECURITY_AUTO // binary.LittleEndian.PutUint32(b[536:], 1) // resend
binary.LittleEndian.PutUint32(b[540:], 4) // security_mode ?
binary.LittleEndian.PutUint32(b[552:], DefaultCaps) // capabilities binary.LittleEndian.PutUint32(b[552:], DefaultCaps) // capabilities
return b return b
} }
@@ -792,10 +800,10 @@ func (c *Conn) buildAVLoginResponse(checksum uint32) []byte {
func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16, sampleRate uint32, channels uint8) []byte { func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16, sampleRate uint32, channels uint8) []byte {
c.audioSeq++ c.audioSeq++
c.audioFrame++ c.audioFrameNo++
prevFrame := uint32(0) prevFrame := uint32(0)
if c.audioFrame > 1 { if c.audioFrameNo > 1 {
prevFrame = c.audioFrame - 1 prevFrame = c.audioFrameNo - 1
} }
totalPayload := len(payload) + 16 // payload + frameinfo totalPayload := len(payload) + 16 // payload + frameinfo
@@ -807,7 +815,7 @@ func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16,
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
binary.LittleEndian.PutUint32(b[4:], c.audioSeq) binary.LittleEndian.PutUint32(b[4:], c.audioSeq)
binary.LittleEndian.PutUint32(b[8:], timestampUS) binary.LittleEndian.PutUint32(b[8:], timestampUS)
if c.audioFrame == 1 { if c.audioFrameNo == 1 {
binary.LittleEndian.PutUint32(b[12:], 0x00000001) binary.LittleEndian.PutUint32(b[12:], 0x00000001)
} else { } else {
binary.LittleEndian.PutUint32(b[12:], 0x00100001) binary.LittleEndian.PutUint32(b[12:], 0x00100001)
@@ -821,13 +829,13 @@ func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16,
binary.LittleEndian.PutUint16(b[22:], 0x0010) // flags binary.LittleEndian.PutUint16(b[22:], 0x0010) // flags
binary.LittleEndian.PutUint32(b[24:], uint32(totalPayload)) binary.LittleEndian.PutUint32(b[24:], uint32(totalPayload))
binary.LittleEndian.PutUint32(b[28:], prevFrame) binary.LittleEndian.PutUint32(b[28:], prevFrame)
binary.LittleEndian.PutUint32(b[32:], c.audioFrame) binary.LittleEndian.PutUint32(b[32:], c.audioFrameNo)
copy(b[36:], payload) // Payload + FrameInfo copy(b[36:], payload) // Payload + FrameInfo
fi := b[36+len(payload):] fi := b[36+len(payload):]
binary.LittleEndian.PutUint16(fi, codec) binary.LittleEndian.PutUint16(fi, codec)
fi[2] = BuildAudioFlags(sampleRate, true, channels == 2) fi[2] = BuildAudioFlags(sampleRate, true, channels == 2)
fi[4] = 1 // online fi[4] = 1 // online
binary.LittleEndian.PutUint32(fi[12:], (c.audioFrame-1)*GetSamplesPerFrame(codec)*1000/sampleRate) binary.LittleEndian.PutUint32(fi[12:], (c.audioFrameNo-1)*GetSamplesPerFrame(codec)*1000/sampleRate)
return b return b
} }
@@ -916,10 +924,8 @@ func (c *Conn) buildIOCtrlFrame(payload []byte) []byte {
func derivePSK(enr string) []byte { func derivePSK(enr string) []byte {
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR) // TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
// contains a 0x00 byte, the PSK is truncated at that position. // contains a 0x00 byte, the PSK is truncated at that position.
// This matches iOS Wyze app behavior discovered via Frida instrumentation. // bytes after the first 0x00 are padded with zeros to make a 32-byte key.
hash := sha256.Sum256([]byte(enr)) hash := sha256.Sum256([]byte(enr))
pskLen := 32 pskLen := 32
for i := range 32 { for i := range 32 {
if hash[i] == 0x00 { if hash[i] == 0x00 {
@@ -928,7 +934,6 @@ func derivePSK(enr string) []byte {
} }
} }
// bytes up to first 0x00, rest padded with zeros
psk := make([]byte, 32) psk := make([]byte, 32)
copy(psk[:pskLen], hash[:pskLen]) copy(psk[:pskLen], hash[:pskLen])
return psk return psk
@@ -939,3 +944,27 @@ func genRandomID() []byte {
_, _ = rand.Read(b) _, _ = rand.Read(b)
return b return b
} }
func hexDump(data []byte) string {
const maxBytes = 650
totalLen := len(data)
truncated := totalLen > maxBytes
if truncated {
data = data[:maxBytes]
}
var result string
for i := 0; i < len(data); i += 16 {
end := min(i+16, len(data))
line := fmt.Sprintf(" %04x:", i)
for j := i; j < end; j++ {
line += fmt.Sprintf(" %02x", data[j])
}
result += line + "\n"
}
if truncated {
result += fmt.Sprintf(" ... (truncated, showing %d of %d bytes)\n", maxBytes, totalLen)
}
return result
}
+2 -2
View File
@@ -47,9 +47,9 @@ type ChannelAdapter struct {
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) { func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
var buf chan []byte var buf chan []byte
if a.channel == IOTCChannelMain { if a.channel == IOTCChannelMain {
buf = a.conn.mainBuf buf = a.conn.clientBuf
} else { } else {
buf = a.conn.speakBuf buf = a.conn.serverBuf
} }
select { select {
-26
View File
@@ -258,19 +258,11 @@ func (h *FrameHandler) Handle(data []byte) {
return return
} }
if h.verbose {
h.logWireHeader(data, hdr)
}
payload, fi := h.extractPayload(data, hdr.Channel) payload, fi := h.extractPayload(data, hdr.Channel)
if payload == nil { if payload == nil {
return return
} }
if h.verbose {
h.logAVPacket(hdr.Channel, hdr.FrameType, payload, fi)
}
switch hdr.Channel { switch hdr.Channel {
case ChannelAudio: case ChannelAudio:
h.handleAudio(payload, fi) h.handleAudio(payload, fi)
@@ -485,21 +477,3 @@ func (h *FrameHandler) queue(pkt *Packet) {
h.output <- pkt h.output <- pkt
} }
} }
func (h *FrameHandler) logWireHeader(data []byte, hdr *PacketHeader) {
fmt.Printf("[WIRE] ch=0x%02x type=0x%02x len=%d pkt=%d/%d frame=%d\n",
hdr.Channel, hdr.FrameType, len(data), hdr.PktIdx, hdr.PktTotal, hdr.FrameNo)
fmt.Printf(" RAW[0..35]: ")
for i := 0; i < 36 && i < len(data); i++ {
fmt.Printf("%02x ", data[i])
}
fmt.Printf("\n")
}
func (h *FrameHandler) logAVPacket(channel, frameType byte, payload []byte, fi *FrameInfo) {
fmt.Printf("[AV] ch=0x%02x type=0x%02x len=%d", channel, frameType, len(payload))
if fi != nil {
fmt.Printf(" fi={codec=0x%04x flags=0x%02x ts=%d}", fi.CodecID, fi.Flags, fi.Timestamp)
}
fmt.Printf("\n")
}