add wyze support

This commit is contained in:
seydx
2026-01-01 05:24:45 +01:00
parent 212def9ceb
commit a42ab88dbd
21 changed files with 5371 additions and 1 deletions
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
package tutk
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/aac"
)
const FrameInfoSize = 40
// Wire format (little-endian) - Wyze extended FRAMEINFO:
//
// [0-1] codec_id uint16 (0x004e=H.264, 0x0050=H.265, 0x0088=AAC)
// [2] flags uint8 (Video: 0=P/1=I, Audio: sr_idx<<2|bits16<<1|ch)
// [3] cam_index uint8
// [4] online_num uint8
// [5] framerate uint8 (FPS, e.g. 20)
// [6] frame_size uint8 (Resolution: 1=1080P, 2=360P, 4=2K)
// [7] bitrate uint8 (e.g. 0xF0=240)
// [8-11] timestamp_us uint32 (microseconds component)
// [12-15] timestamp uint32 (Unix timestamp in seconds)
// [16-19] payload_sz uint32 (frame payload size)
// [20-23] frame_no uint32 (frame number)
// [24-39] device_id 16 bytes (MAC address + padding)
type FrameInfo struct {
CodecID uint16
Flags uint8
CamIndex uint8
OnlineNum uint8
Framerate uint8 // FPS (e.g. 20)
FrameSize uint8 // Resolution: 1=1080P, 2=360P, 4=2K
Bitrate uint8 // Bitrate value (e.g. 240)
TimestampUS uint32
Timestamp uint32
PayloadSize uint32
FrameNo uint32
}
// Resolution constants (as received in FrameSize field)
// Note: Some cameras only support 2K + 360P, others support 1080P + 360P
// The actual resolution depends on camera model!
const (
ResolutionUnknown = 0
ResolutionSD = 1 // 360P (640x360) on 2K cameras, or 1080P on older cams
Resolution360P = 2 // 360P (640x360)
Resolution2K = 4 // 2K (2560x1440)
)
func (fi *FrameInfo) IsKeyframe() bool {
return fi.Flags == 0x01
}
// Resolution returns a human-readable resolution string
func (fi *FrameInfo) Resolution() string {
switch fi.FrameSize {
case ResolutionSD:
return "SD" // Could be 360P or 1080P depending on camera
case Resolution360P:
return "360P"
case Resolution2K:
return "2K"
default:
return "unknown"
}
}
func (fi *FrameInfo) SampleRate() uint32 {
srIdx := (fi.Flags >> 2) & 0x0F
return uint32(SampleRateValue(srIdx))
}
func (fi *FrameInfo) Channels() uint8 {
if fi.Flags&0x01 == 1 {
return 2
}
return 1
}
func (fi *FrameInfo) IsVideo() bool {
return IsVideoCodec(fi.CodecID)
}
func (fi *FrameInfo) IsAudio() bool {
return IsAudioCodec(fi.CodecID)
}
func ParseFrameInfo(data []byte) *FrameInfo {
if len(data) < FrameInfoSize {
return nil
}
offset := len(data) - FrameInfoSize
fi := data[offset:]
return &FrameInfo{
CodecID: binary.LittleEndian.Uint16(fi[0:2]),
Flags: fi[2],
CamIndex: fi[3],
OnlineNum: fi[4],
Framerate: fi[5],
FrameSize: fi[6],
Bitrate: fi[7],
TimestampUS: binary.LittleEndian.Uint32(fi[8:12]),
Timestamp: binary.LittleEndian.Uint32(fi[12:16]),
PayloadSize: binary.LittleEndian.Uint32(fi[16:20]),
FrameNo: binary.LittleEndian.Uint32(fi[20:24]),
}
}
func ParseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channels uint8) {
// Try ADTS header first (more reliable than FRAMEINFO flags)
if aac.IsADTS(payload) {
codec := aac.ADTSToCodec(payload)
if codec != nil {
return codec.ClockRate, codec.Channels
}
}
// Fallback to FRAMEINFO flags
if fi != nil {
return fi.SampleRate(), fi.Channels()
}
// Default values
return 16000, 1
}
+64
View File
@@ -0,0 +1,64 @@
package tutk
import (
"fmt"
"net"
"time"
)
type ChannelAdapter struct {
conn *Conn
channel uint8
}
func (a *ChannelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
var buf chan []byte
if a.channel == IOTCChannelMain {
buf = a.conn.mainBuf
} else {
buf = a.conn.speakerBuf
}
select {
case data := <-buf:
n = copy(p, data)
if a.conn.verbose && len(data) >= 1 {
fmt.Printf("[ChannelAdapter] ch=%d ReadFrom: len=%d contentType=%d\n",
a.channel, len(data), data[0])
}
return n, a.conn.addr, nil
case <-a.conn.done:
return 0, nil, net.ErrClosed
}
}
func (a *ChannelAdapter) WriteTo(p []byte, addr net.Addr) (n int, err error) {
if a.conn.verbose {
fmt.Printf("[IOTC TX] channel=%d size=%d\n", a.channel, len(p))
}
_, err = a.conn.sendIOTC(p, a.channel)
if err != nil {
return 0, err
}
return len(p), nil
}
func (a *ChannelAdapter) Close() error {
return nil
}
func (a *ChannelAdapter) LocalAddr() net.Addr {
return &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 0}
}
func (a *ChannelAdapter) SetDeadline(time.Time) error {
return nil
}
func (a *ChannelAdapter) SetReadDeadline(time.Time) error {
return nil
}
func (a *ChannelAdapter) SetWriteDeadline(time.Time) error {
return nil
}
+218
View File
@@ -0,0 +1,218 @@
package tutk
import (
"crypto/cipher"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
"golang.org/x/crypto/chacha20poly1305"
)
const CipherSuiteID_CCAC dtls.CipherSuiteID = 0xCCAC
const (
chachaTagLength = 16
chachaNonceLength = 12
)
var (
errDecryptPacket = &protocol.TemporaryError{Err: errors.New("failed to decrypt packet")}
errCipherSuiteNotInit = &protocol.TemporaryError{Err: errors.New("CipherSuite not initialized")}
)
type ChaCha20Poly1305Cipher struct {
localCipher, remoteCipher cipher.AEAD
localWriteIV, remoteWriteIV []byte
}
func NewChaCha20Poly1305Cipher(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*ChaCha20Poly1305Cipher, error) {
localCipher, err := chacha20poly1305.New(localKey)
if err != nil {
return nil, err
}
remoteCipher, err := chacha20poly1305.New(remoteKey)
if err != nil {
return nil, err
}
return &ChaCha20Poly1305Cipher{
localCipher: localCipher,
localWriteIV: localWriteIV,
remoteCipher: remoteCipher,
remoteWriteIV: remoteWriteIV,
}, nil
}
func generateAEADAdditionalData(h *recordlayer.Header, payloadLen int) []byte {
var additionalData [13]byte
binary.BigEndian.PutUint64(additionalData[:], h.SequenceNumber)
binary.BigEndian.PutUint16(additionalData[:], h.Epoch)
additionalData[8] = byte(h.ContentType)
additionalData[9] = h.Version.Major
additionalData[10] = h.Version.Minor
binary.BigEndian.PutUint16(additionalData[11:], uint16(payloadLen))
return additionalData[:]
}
func computeNonce(iv []byte, epoch uint16, sequenceNumber uint64) []byte {
nonce := make([]byte, chachaNonceLength)
binary.BigEndian.PutUint64(nonce[4:], sequenceNumber)
binary.BigEndian.PutUint16(nonce[4:], epoch)
for i := 0; i < chachaNonceLength; i++ {
nonce[i] ^= iv[i]
}
return nonce
}
func (c *ChaCha20Poly1305Cipher) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
payload := raw[pkt.Header.Size():]
raw = raw[:pkt.Header.Size()]
nonce := computeNonce(c.localWriteIV, pkt.Header.Epoch, pkt.Header.SequenceNumber)
additionalData := generateAEADAdditionalData(&pkt.Header, len(payload))
encryptedPayload := c.localCipher.Seal(nil, nonce, payload, additionalData)
r := make([]byte, len(raw)+len(encryptedPayload))
copy(r, raw)
copy(r[len(raw):], encryptedPayload)
binary.BigEndian.PutUint16(r[pkt.Header.Size()-2:], uint16(len(r)-pkt.Header.Size()))
return r, nil
}
func (c *ChaCha20Poly1305Cipher) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
err := header.Unmarshal(in)
switch {
case err != nil:
return nil, err
case header.ContentType == protocol.ContentTypeChangeCipherSpec:
return in, nil
case len(in) <= header.Size()+chachaTagLength:
return nil, fmt.Errorf("ciphertext too short: %d <= %d", len(in), header.Size()+chachaTagLength)
}
nonce := computeNonce(c.remoteWriteIV, header.Epoch, header.SequenceNumber)
out := in[header.Size():]
additionalData := generateAEADAdditionalData(&header, len(out)-chachaTagLength)
out, err = c.remoteCipher.Open(out[:0], nonce, out, additionalData)
if err != nil {
return nil, fmt.Errorf("%w: %v", errDecryptPacket, err)
}
return append(in[:header.Size()], out...), nil
}
type TLSEcdhePskWithChacha20Poly1305Sha256 struct {
aead atomic.Value
}
func NewTLSEcdhePskWithChacha20Poly1305Sha256() *TLSEcdhePskWithChacha20Poly1305Sha256 {
return &TLSEcdhePskWithChacha20Poly1305Sha256{}
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) CertificateType() clientcertificate.Type {
return clientcertificate.Type(0)
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) KeyExchangeAlgorithm() dtls.CipherSuiteKeyExchangeAlgorithm {
return dtls.CipherSuiteKeyExchangeAlgorithmPsk | dtls.CipherSuiteKeyExchangeAlgorithmEcdhe
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) ECC() bool {
return true
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) ID() dtls.CipherSuiteID {
return CipherSuiteID_CCAC
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) String() string {
return "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256"
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) HashFunc() func() hash.Hash {
return sha256.New
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) AuthenticationType() dtls.CipherSuiteAuthenticationType {
return dtls.CipherSuiteAuthenticationTypePreSharedKey
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) IsInitialized() bool {
return c.aead.Load() != nil
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 0
prfKeyLen = 32
prfIvLen = 12
)
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom,
prfMacLen, prfKeyLen, prfIvLen,
c.HashFunc(),
)
if err != nil {
return err
}
var aead *ChaCha20Poly1305Cipher
if isClient {
aead, err = NewChaCha20Poly1305Cipher(
keys.ClientWriteKey, keys.ClientWriteIV,
keys.ServerWriteKey, keys.ServerWriteIV,
)
} else {
aead, err = NewChaCha20Poly1305Cipher(
keys.ServerWriteKey, keys.ServerWriteIV,
keys.ClientWriteKey, keys.ClientWriteIV,
)
}
if err != nil {
return err
}
c.aead.Store(aead)
return nil
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
aead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)
if !ok {
return nil, fmt.Errorf("%w: unable to encrypt", errCipherSuiteNotInit)
}
return aead.Encrypt(pkt, raw)
}
func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
aead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)
if !ok {
return nil, fmt.Errorf("%w: unable to decrypt", errCipherSuiteNotInit)
}
return aead.Decrypt(h, raw)
}
func CustomCipherSuites() []dtls.CipherSuite {
return []dtls.CipherSuite{
NewTLSEcdhePskWithChacha20Poly1305Sha256(),
}
}
File diff suppressed because it is too large Load Diff
+282
View File
@@ -0,0 +1,282 @@
package tutk
const (
CodecUnknown uint16 = 0x00 // Unknown codec
CodecMPEG4 uint16 = 0x4C // 76 - MPEG4
CodecH263 uint16 = 0x4D // 77 - H.263
CodecH264 uint16 = 0x4E // 78 - H.264/AVC (common for Wyze)
CodecMJPEG uint16 = 0x4F // 79 - MJPEG
CodecH265 uint16 = 0x50 // 80 - H.265/HEVC (common for Wyze)
)
const (
AudioCodecAACRaw uint16 = 0x86 // 134 - AAC raw format
AudioCodecAACADTS uint16 = 0x87 // 135 - AAC with ADTS header
AudioCodecAACLATM uint16 = 0x88 // 136 - AAC with LATM format
AudioCodecG711U uint16 = 0x89 // 137 - G.711 μ-law (PCMU)
AudioCodecG711A uint16 = 0x8A // 138 - G.711 A-law (PCMA)
AudioCodecADPCM uint16 = 0x8B // 139 - ADPCM
AudioCodecPCM uint16 = 0x8C // 140 - PCM 16-bit signed LE
AudioCodecSPEEX uint16 = 0x8D // 141 - Speex
AudioCodecMP3 uint16 = 0x8E // 142 - MP3
AudioCodecG726 uint16 = 0x8F // 143 - G.726
// Wyze extensions (not in official SDK)
AudioCodecAACWyze uint16 = 0x90 // 144 - Wyze AAC
AudioCodecOpus uint16 = 0x92 // 146 - Opus codec
)
const (
SampleRate8K uint8 = 0x00 // 8000 Hz
SampleRate11K uint8 = 0x01 // 11025 Hz
SampleRate12K uint8 = 0x02 // 12000 Hz
SampleRate16K uint8 = 0x03 // 16000 Hz
SampleRate22K uint8 = 0x04 // 22050 Hz
SampleRate24K uint8 = 0x05 // 24000 Hz
SampleRate32K uint8 = 0x06 // 32000 Hz
SampleRate44K uint8 = 0x07 // 44100 Hz
SampleRate48K uint8 = 0x08 // 48000 Hz
)
var SampleRates = map[uint8]int{
SampleRate8K: 8000,
SampleRate11K: 11025,
SampleRate12K: 12000,
SampleRate16K: 16000,
SampleRate22K: 22050,
SampleRate24K: 24000,
SampleRate32K: 32000,
SampleRate44K: 44100,
SampleRate48K: 48000,
}
var SamplesPerFrame = map[uint16]uint32{
AudioCodecAACRaw: 1024, // AAC frame = 1024 samples
AudioCodecAACADTS: 1024,
AudioCodecAACLATM: 1024,
AudioCodecAACWyze: 1024,
AudioCodecG711U: 160, // G.711 typically 20ms = 160 samples at 8kHz
AudioCodecG711A: 160,
AudioCodecPCM: 160,
AudioCodecADPCM: 160,
AudioCodecSPEEX: 160,
AudioCodecMP3: 1152, // MP3 frame = 1152 samples
AudioCodecG726: 160,
AudioCodecOpus: 960, // Opus typically 20ms = 960 samples at 48kHz
}
const (
IOTypeVideoStart = 0x01FF
IOTypeVideoStop = 0x02FF
IOTypeAudioStart = 0x0300
IOTypeAudioStop = 0x0301
IOTypeSpeakerStart = 0x0350
IOTypeSpeakerStop = 0x0351
IOTypeGetAudioOutFormatReq = 0x032A
IOTypeGetAudioOutFormatRes = 0x032B
IOTypeSetStreamCtrlReq = 0x0320
IOTypeSetStreamCtrlRes = 0x0321
IOTypeGetStreamCtrlReq = 0x0322
IOTypeGetStreamCtrlRes = 0x0323
IOTypeDevInfoReq = 0x0340
IOTypeDevInfoRes = 0x0341
IOTypeGetSupportStreamReq = 0x0344
IOTypeGetSupportStreamRes = 0x0345
IOTypeSetRecordReq = 0x0310
IOTypeSetRecordRes = 0x0311
IOTypeGetRecordReq = 0x0312
IOTypeGetRecordRes = 0x0313
IOTypePTZCommand = 0x1001
IOTypeReceiveFirstFrame = 0x1002
IOTypeGetEnvironmentReq = 0x030A
IOTypeGetEnvironmentRes = 0x030B
IOTypeSetVideoModeReq = 0x030C
IOTypeSetVideoModeRes = 0x030D
IOTypeGetVideoModeReq = 0x030E
IOTypeGetVideoModeRes = 0x030F
IOTypeSetTimeReq = 0x0316
IOTypeSetTimeRes = 0x0317
IOTypeGetTimeReq = 0x0318
IOTypeGetTimeRes = 0x0319
IOTypeSetWifiReq = 0x0102
IOTypeSetWifiRes = 0x0103
IOTypeGetWifiReq = 0x0104
IOTypeGetWifiRes = 0x0105
IOTypeListWifiAPReq = 0x0106
IOTypeListWifiAPRes = 0x0107
IOTypeSetMotionDetectReq = 0x0306
IOTypeSetMotionDetectRes = 0x0307
IOTypeGetMotionDetectReq = 0x0308
IOTypeGetMotionDetectRes = 0x0309
)
const (
CmdDiscoReq uint16 = 0x0601
CmdDiscoRes uint16 = 0x0602
CmdSessionReq uint16 = 0x0402
CmdSessionRes uint16 = 0x0404
CmdDataTX uint16 = 0x0407
CmdDataRX uint16 = 0x0408
CmdKeepaliveReq uint16 = 0x0427
CmdKeepaliveRes uint16 = 0x0428
)
const (
MagicAVLoginResp uint16 = 0x2100
MagicIOCtrl uint16 = 0x7000
MagicChannelMsg uint16 = 0x1000
MagicACK uint16 = 0x0009
MagicAVLogin1 uint16 = 0x0000
MagicAVLogin2 uint16 = 0x2000
)
const (
ProtocolVersion uint16 = 0x000c // Version 12
)
const (
DefaultCapabilities uint32 = 0x001f07fb
)
const (
KCmdAuth = 10000
KCmdChallenge = 10001
KCmdChallengeResp = 10002
KCmdAuthResult = 10003
KCmdControlChannel = 10010
KCmdControlChannelResp = 10011
KCmdSetResolution = 10056
KCmdSetResolutionResp = 10057
)
const (
MediaTypeVideo = 1
MediaTypeAudio = 2
MediaTypeReturnAudio = 3
MediaTypeRDT = 4
)
const (
IOTCChannelMain = 0 // Main AV channel (we = DTLS Client, camera = Server)
IOTCChannelBack = 1 // Backchannel for Return Audio (we = DTLS Server, camera = Client)
)
const (
BitrateMax uint16 = 0xF0 // 240 KB/s
BitrateSD uint16 = 0x3C // 60 KB/s
)
const (
FrameSize1080P = 0
FrameSize360P = 1
FrameSize720P = 2
FrameSize2K = 3
)
const (
QualityUnknown = 0
QualityMax = 1
QualityHigh = 2
QualityMiddle = 3
QualityLow = 4
QualityMin = 5
)
func CodecName(id uint16) string {
switch id {
case CodecH264:
return "H264"
case CodecH265:
return "H265"
case CodecMPEG4:
return "MPEG4"
case CodecH263:
return "H263"
case CodecMJPEG:
return "MJPEG"
default:
return "Unknown"
}
}
func AudioCodecName(id uint16) string {
switch id {
case AudioCodecG711U:
return "PCMU"
case AudioCodecG711A:
return "PCMA"
case AudioCodecPCM:
return "PCM"
case AudioCodecAACLATM, AudioCodecAACRaw, AudioCodecAACADTS, AudioCodecAACWyze:
return "AAC"
case AudioCodecOpus:
return "Opus"
case AudioCodecSPEEX:
return "Speex"
case AudioCodecMP3:
return "MP3"
case AudioCodecG726:
return "G726"
case AudioCodecADPCM:
return "ADPCM"
default:
return "Unknown"
}
}
func SampleRateValue(enum uint8) int {
if rate, ok := SampleRates[enum]; ok {
return rate
}
return 16000 // Default
}
func SampleRateIndex(hz uint32) uint8 {
switch hz {
case 8000:
return SampleRate8K
case 11025:
return SampleRate11K
case 12000:
return SampleRate12K
case 16000:
return SampleRate16K
case 22050:
return SampleRate22K
case 24000:
return SampleRate24K
case 32000:
return SampleRate32K
case 44100:
return SampleRate44K
case 48000:
return SampleRate48K
default:
return SampleRate16K // Default
}
}
func BuildAudioFlags(sampleRate uint32, bits16 bool, stereo bool) uint8 {
flags := SampleRateIndex(sampleRate) << 2
if bits16 {
flags |= 0x02
}
if stereo {
flags |= 0x01
}
return flags
}
func IsVideoCodec(id uint16) bool {
return id >= CodecMPEG4 && id <= CodecH265
}
func IsAudioCodec(id uint16) bool {
return id >= AudioCodecAACRaw && id <= AudioCodecOpus
}
func GetSamplesPerFrame(codecID uint16) uint32 {
if samples, ok := SamplesPerFrame[codecID]; ok {
return samples
}
return 1024 // Default to AAC
}
+155
View File
@@ -0,0 +1,155 @@
package tutk
const (
// Start packets - first fragment of a frame
// 0x08: Extended start (36-byte header, no FrameInfo)
// 0x09: StartAlt (36-byte header, FrameInfo only if pkt_total==1)
FrameTypeStart uint8 = 0x08
FrameTypeStartAlt uint8 = 0x09
// Continuation packets - middle fragment (28-byte header, no FrameInfo)
FrameTypeCont uint8 = 0x00
FrameTypeContAlt uint8 = 0x04
// End packets - last fragment (with 40-byte FrameInfo)
// 0x01: Single-packet frame (28-byte header)
// 0x05: Multi-packet end (28-byte header)
// 0x0d: Extended end (36-byte header)
FrameTypeEndSingle uint8 = 0x01
FrameTypeEndMulti uint8 = 0x05
FrameTypeEndExt uint8 = 0x0d
)
const (
ChannelIVideo uint8 = 0x05
ChannelAudio uint8 = 0x03
ChannelPVideo uint8 = 0x07
)
type Packet struct {
Channel uint8
Codec uint16
Timestamp uint32
Payload []byte
IsKeyframe bool
FrameNo uint32
SampleRate uint32
Channels uint8
}
func (p *Packet) IsVideo() bool {
return p.Channel == ChannelIVideo || p.Channel == ChannelPVideo
}
func (p *Packet) IsAudio() bool {
return p.Channel == ChannelAudio
}
type AuthResponse struct {
ConnectionRes string `json:"connectionRes"`
CameraInfo map[string]any `json:"cameraInfo"`
}
type AVLoginResponse struct {
ServerType uint32
Resend int32
TwoWayStreaming int32
SyncRecvData int32
SecurityMode uint32
VideoOnConnect int32
AudioOnConnect int32
}
func IsStartFrame(frameType uint8) bool {
return frameType == FrameTypeStart || frameType == FrameTypeStartAlt
}
func IsEndFrame(frameType uint8) bool {
return frameType == FrameTypeEndSingle ||
frameType == FrameTypeEndMulti ||
frameType == FrameTypeEndExt
}
func IsContinuationFrame(frameType uint8) bool {
return frameType == FrameTypeCont || frameType == FrameTypeContAlt
}
type PacketHeader struct {
Channel byte
FrameType byte
HeaderSize int // 28 or 36
FrameNo uint32 // Frame number (from [24-27] for 28-byte, [32-35] for 36-byte)
PktIdx uint16 // Packet index within frame (0-based)
PktTotal uint16 // Total packets in this frame
PayloadSize uint16
HasFrameInfo bool // true if [14-15] or [22-23] == 0x0028
}
func ParsePacketHeader(data []byte) *PacketHeader {
if len(data) < 28 {
return nil
}
frameType := data[1]
hdr := &PacketHeader{
Channel: data[0],
FrameType: frameType,
}
// Header size based on FrameType (NOT magic bytes!)
switch frameType {
case FrameTypeStart, FrameTypeStartAlt, FrameTypeEndExt: // 0x08, 0x09, 0x0d
hdr.HeaderSize = 36
default: // 0x00, 0x01, 0x04, 0x05
hdr.HeaderSize = 28
}
if len(data) < hdr.HeaderSize {
return nil
}
if hdr.HeaderSize == 28 {
// 28-Byte Header Layout:
// [12-13] pkt_total
// [14-15] pkt_idx OR 0x0028 (FrameInfo marker) - ONLY 0x0028 in End packets!
// [16-17] payload_size
// [24-27] frame_no (uint32)
hdr.PktTotal = uint16(data[12]) | uint16(data[13])<<8
pktIdxOrMarker := uint16(data[14]) | uint16(data[15])<<8
hdr.PayloadSize = uint16(data[16]) | uint16(data[17])<<8
hdr.FrameNo = uint32(data[24]) | uint32(data[25])<<8 | uint32(data[26])<<16 | uint32(data[27])<<24
// 0x0028 is FrameInfo marker ONLY in End packets, otherwise it's pkt_idx=40
if IsEndFrame(hdr.FrameType) && pktIdxOrMarker == 0x0028 {
hdr.HasFrameInfo = true
if hdr.PktTotal > 0 {
hdr.PktIdx = hdr.PktTotal - 1 // Last packet
}
} else {
hdr.PktIdx = pktIdxOrMarker
}
} else {
// 36-Byte Header Layout:
// [20-21] pkt_total
// [22-23] pkt_idx OR 0x0028 (FrameInfo marker) - ONLY 0x0028 in End packets!
// [24-25] payload_size
// [32-35] frame_no (uint32) - GLOBAL frame counter, matches 28-byte [24-27]
// NOTE: [18-19] is channel-specific frame index, NOT used for reassembly!
hdr.PktTotal = uint16(data[20]) | uint16(data[21])<<8
pktIdxOrMarker := uint16(data[22]) | uint16(data[23])<<8
hdr.PayloadSize = uint16(data[24]) | uint16(data[25])<<8
hdr.FrameNo = uint32(data[32]) | uint32(data[33])<<8 | uint32(data[34])<<16 | uint32(data[35])<<24
// 0x0028 is FrameInfo marker ONLY in End packets, otherwise it's pkt_idx=40
if IsEndFrame(hdr.FrameType) && pktIdxOrMarker == 0x0028 {
hdr.HasFrameInfo = true
if hdr.PktTotal > 0 {
hdr.PktIdx = hdr.PktTotal - 1
}
} else {
hdr.PktIdx = pktIdxOrMarker
}
}
return hdr
}