diff --git a/pkg/pcic/frame.go b/pkg/pcic/frame.go new file mode 100644 index 0000000..f0f7c5a --- /dev/null +++ b/pkg/pcic/frame.go @@ -0,0 +1,7 @@ +package pcic + +import "github.com/graugans/go-ovp8xx/pkg/chunk" + +type Frame struct { + Chunks []chunk.ChunkData +} diff --git a/pkg/pcic/protocol.go b/pkg/pcic/protocol.go new file mode 100644 index 0000000..f2afde0 --- /dev/null +++ b/pkg/pcic/protocol.go @@ -0,0 +1,102 @@ +package pcic + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/graugans/go-ovp8xx/pkg/chunk" +) + +type PCIC struct { +} + +const ( + headerSize int = 20 + minimumContentLength int = 6 + ticketFieldLength int = 4 + lengthFieldLength int = 10 + delimiterFieldLength int = 2 +) + +const ( + firstTicketOffset int = 0 + lengthOffset int = 4 + secondTicketOffset int = 16 + delimiterOffset int = 14 + dataOffset int = 20 +) + +const ( + startMarker string = "star" + endMarker string = "stop" +) + +func (p *PCIC) Receive(reader io.Reader) (Frame, error) { + frame := Frame{} + header := make([]byte, headerSize) + n, err := io.ReadFull(reader, header) + if err != nil { + return frame, err + } + if n < headerSize { + return frame, fmt.Errorf("not enough data received: %d", n) + } + firstTicket := header[:ticketFieldLength] + secondTicket := header[secondTicketOffset:dataOffset] + if !bytes.Equal(firstTicket, secondTicket) { + return frame, fmt.Errorf("mismatch in the tickets %s != %s ", + string(firstTicket), + string(secondTicket), + ) + } + lengthBuffer := string(header[lengthOffset:secondTicketOffset]) + if lengthBuffer[0] != 'L' { + return frame, fmt.Errorf("the length field does not start with 'L': %v", lengthBuffer) + } + length := 0 + n, err = fmt.Sscanf(lengthBuffer, "L%09d\r\n", &length) + if err != nil { + return frame, err + } + if n != 1 { + return frame, errors.New("no length in the length field detected") + } + if length < minimumContentLength { + return frame, errors.New("the length information is too short") + } + data := make([]byte, length-ticketFieldLength) + if _, err = io.ReadFull(reader, data); err != nil { + return frame, err + } + trailer := data[len(data)-delimiterFieldLength:] + if !bytes.Equal(trailer, []byte{'\r', '\n'}) { + return frame, errors.New("invalid trailer detected") + } + contentDecorated := data[:len(data)-delimiterFieldLength] + if len(startMarker)+len(endMarker) > len(contentDecorated) { + return frame, fmt.Errorf("missing start (%s) and end markers (%s) buffer length: %d", + startMarker, + endMarker, + len(contentDecorated), + ) + } + content := contentDecorated[len(endMarker) : len(contentDecorated)-len(endMarker)] + if len(content) == 0 { + // no content is available + return frame, nil + } + remainingBytes := len(content) + offset := 0 + for remainingBytes > 0 { + c := chunk.ChunkData{} + if err := c.Parse(content[offset:]); err != nil { + return frame, err + } + frame.Chunks = append(frame.Chunks, c) + offset += c.Size() + remainingBytes -= c.Size() + } + return frame, err +} diff --git a/pkg/pcic/protocol_test.go b/pkg/pcic/protocol_test.go new file mode 100644 index 0000000..2c261a0 --- /dev/null +++ b/pkg/pcic/protocol_test.go @@ -0,0 +1,72 @@ +package pcic_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/graugans/go-ovp8xx/pkg/chunk" + "github.com/graugans/go-ovp8xx/pkg/pcic" + "github.com/stretchr/testify/assert" +) + +const miniMalContentLength int = 14 + +func TestMinimalReceive(t *testing.T) { + r := strings.NewReader("Hello, Reader!") + p := pcic.PCIC{} + _, err := p.Receive(r) + assert.Error(t, err, "We expect an error while receiving malformed data") + + // Test the minimal possible PCIC message + r = strings.NewReader("0001L000000014\r\n0001starstop\r\n") + _, err = p.Receive(r) + assert.NoError(t, err, "We expect no error while receiving data") +} + +func TestReceiveWithChunk(t *testing.T) { + c := chunk.ChunkData{} + chunkData := []byte{ + 0x69, 0x00, 0x00, 0x00, /* CHUNK_TYPE */ + 0x34, 0x00, 0x00, 0x00, /* CHUNK_SIZE */ + 0x30, 0x00, 0x00, 0x00, /* HEADER_SIZE */ + 0x02, 0x00, 0x00, 0x00, /* HEADER_VERSION */ + 0x04, 0x00, 0x00, 0x00, /* IMAGE_WIDTH */ + 0x01, 0x00, 0x00, 0x00, /* IMAGE_HEIGTH */ + 0x00, 0x00, 0x00, 0x00, /* DATA_FORMAT */ + 0x00, 0x00, 0x00, 0x00, /* TIME_STAMP */ + 0x00, 0x00, 0x00, 0x00, /* FRAME_COUNT */ + 0x00, 0x00, 0x00, 0x00, /* STATUS_CODE */ + 0x00, 0x01, 0x00, 0x00, /* TIME_STAMP_SEC */ + 0x01, 0x01, 0x00, 0x00, /* TIME_STAMP_NSEC */ + 0xFF, 0xFF, 0xFF, 0xBB, /* DATA */ + } + assert.NoError(t, + c.Parse(chunkData), + "A successful parse expected", + ) + p := pcic.PCIC{} + buffer := fmt.Sprintf( + "0001L%09d\r\n0001star%sstop\r\n", + miniMalContentLength+len(chunkData), + string(chunkData), + ) + // Test the PCIC message with single chunk + r := strings.NewReader(buffer) + f, err := p.Receive(r) + assert.NoError(t, err, "We expect no error while receiving data") + + assert.Equal(t, chunk.RADIAL_DISTANCE_NOISE, f.Chunks[0].Type()) + + // test with trailing XX after the chunk + buffer = fmt.Sprintf( + "0001L%09d\r\n0001star%sXXstop\r\n", + miniMalContentLength+len(chunkData)+2, + string(chunkData), + ) + // Test the PCIC message with single chunk + r = strings.NewReader(buffer) + _, err = p.Receive(r) + assert.Error(t, err, "We expect an error while receiving malformed data") + +}