diff --git a/Camera.go b/Camera.go new file mode 100644 index 0000000..0b9de53 --- /dev/null +++ b/Camera.go @@ -0,0 +1,378 @@ +package main + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "net" + "strconv" + "strings" + "time" +) + +// Camera contains all information and features on a single IP Camera +type Camera struct { + ipAddress net.IP + port int + username string + password string + connected bool + disconnect bool + verbose bool + connection net.Conn + isLoggedIn bool + messageHandlers map[uint32][]MessageHandler +} + +// MessageHandler is used to process incoming messages from the camera +type MessageHandler func(camera *Camera, message *Message) (bool, error) + +const ( + LOGIN_ACCEPT = 0x0111 + START_PREVIEW = 0x01FF + REQUEST_FILE_LIST = 0xA025 + FILE_LIST_CONTENT = 0xA026 + REQUEST_FIRMWARE_INFO = 0xA034 + FIRMWARE_INFORMATION = 0xA035 + TAKE_PICTURE = 0xA038 + PICTURE_SAVED = 0xA039 + CONTROL_RECORDING = 0xA03A +) + +const ( + // RemoveHandler instructs the network code to remove this handler after execution + RemoveHandler = true + // KeepHandler instructs the network code to keep this handler after execution + KeepHandler = false +) + +// StoredFile is a file stored on the cameras sd-card +type StoredFile struct { + Path string + Size uint64 +} + +// CreateCamera creates a new Camera instance +func CreateCamera(ipAddress net.IP, port int, username, password string, verbose bool) *Camera { + camera := &Camera{ + ipAddress: ipAddress, + port: port, + username: username, + password: password, + messageHandlers: make(map[uint32][]MessageHandler, 0), + verbose: verbose, + } + return camera +} + +// Connect to the camera and start responding to keepalive packets +func (c *Camera) Connect() { + if c.verbose { + log.Printf("Connecting to %s:%d using username=%s, password=%s\n", c.ipAddress, c.port, c.username, c.password) + } + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.ipAddress, c.port)) + if err != nil { + log.Printf("ERROR: %s\n", err) + return + } + c.connection = conn + + c.HandleFirst(0x0112, aliveRequestHandler) + + go c.handleConnection() + + c.Login() +} + +// Login will try to login to the camera control service +func (c *Camera) Login() error { + loginAccept := make(chan bool, 1) + c.Handle(LOGIN_ACCEPT, func(c *Camera, m *Message) (bool, error) { + _, err := loginResultHandler(c, m) + if err != nil { + return false, err + } + loginAccept <- true + return KeepHandler, nil + }) + + //TODO: Handle login error messages + c.SendPacket(CreateLoginPacket(c.username, c.password)) + + select { + case loginSuccess := <-loginAccept: + if loginSuccess { + return nil + } + case <-time.After(5 * time.Second): + return errors.New("Login request timed out") + } + + return errors.New("Login failed") +} + +// IsConnected returns true if the camera connection has not been disconnected +func (c *Camera) IsConnected() bool { + return c.connected +} + +func (c *Camera) handleConnection() { + header := Header{} + var payload []byte + + for { + // Read the header from the wire + err := binary.Read(c.connection, binary.BigEndian, &header) + if err != nil { + log.Printf("ERROR Reading from Camera: %s\n", err) + break + } + + // Check the Magic bytes + if header.Magic != 0xABCD { + log.Printf("Received message with invalid magic (%x)\n", header.Magic) + break + } + + // Read the payload from the wire (if any) + if header.Length > 0 { + payload = make([]byte, header.Length) + bytesRead, err := io.ReadFull(c.connection, payload) + if err != nil || (uint16(bytesRead) != header.Length) { + log.Printf("ERROR Reading Payload from Camera: %s, expected %d Bytes, got %d\n", err, header.Length, bytesRead) + break + } + } else { + payload = []byte{} + } + + message := &Message{ + Header: header, + Payload: payload, + } + + // If there is not registered handler, dump the message + if len(c.messageHandlers[header.MessageType]) == 0 { + log.Printf("Received Unknown Message (no handler registered):\n%s\n", message) + continue + } + + // Run all registered handlers for this message type + remainingMessageHandlers := make([]MessageHandler, 0) + for _, handler := range c.messageHandlers[header.MessageType] { + remove, err := handler(c, message) + if !remove { + remainingMessageHandlers = append(remainingMessageHandlers, handler) + } + + if err != nil { + log.Printf("ERROR running message handler (%v): %s\n", handler, err) + break + } + } + // replace handlers with all but the one-shot handlers + c.messageHandlers[header.MessageType] = remainingMessageHandlers + + if c.disconnect { + break + } + } + c.Log("Disconnecting") + c.connected = false +} + +// Handle adds a new message handler to the list of message handlers for a given message type +func (c *Camera) Handle(messageType uint32, handleFunc MessageHandler) { + c.addHandler(messageType, handleFunc, false) +} + +// HandleFirst adds a new message handler to the start of the list of message handlers +// for a given message type +func (c *Camera) HandleFirst(messageType uint32, handleFunc MessageHandler) { + c.addHandler(messageType, handleFunc, true) +} + +func (c *Camera) addHandler(messageType uint32, handleFunc MessageHandler, prepend bool) { + if c.messageHandlers[messageType] == nil { + c.messageHandlers[messageType] = make([]MessageHandler, 0) + } + + if prepend { + c.messageHandlers[messageType] = append([]MessageHandler{handleFunc}, c.messageHandlers[messageType]...) + } else { + c.messageHandlers[messageType] = append(c.messageHandlers[messageType], handleFunc) + } +} + +// Log will write to stdout if this camera has been set to be verbose +func (c *Camera) Log(format string, data ...interface{}) { + if c.verbose { + log.Printf(format+"\n", data) + } +} + +// GetFileList retrieves a list of files stored on the cameras SD-Card +func (c *Camera) GetFileList() ([]StoredFile, error) { + fileListComplete := make(chan []StoredFile, 1) + fileListData := "" + + c.Handle(FILE_LIST_CONTENT, func(c *Camera, m *Message) (bool, error) { + numParts := binary.LittleEndian.Uint32(m.Payload[:4]) + currentPart := binary.LittleEndian.Uint32(m.Payload[4:8]) + fileListData += string(m.Payload[8:]) + if currentPart+1 >= numParts { + fileListComplete <- parseFileList(fileListData) + return RemoveHandler, nil + } + return KeepHandler, nil + }) + + err := c.SendPacket(CreatePacket(CreateCommandHeader(REQUEST_FILE_LIST), []byte{0x01, 0x00, 0x00, 0x00})) + if err != nil { + return nil, err + } + + select { + case result := <-fileListComplete: + return result, nil + case <-time.After(10 * time.Second): + return nil, errors.New("Timed out while loading file list") + } +} + +func parseFileList(input string) []StoredFile { + files := strings.Split(input, ";") + stored := make([]StoredFile, len(files)-1) + for i, file := range files { + parts := strings.Split(file, ":") + if len(parts) == 2 { + size, err := strconv.ParseUint(parts[1], 10, 64) + + if err == nil && size > 0 && len(parts[0]) > 0 { + stored[i] = StoredFile{ + Path: parts[0], + Size: size, + } + } + } + } + return stored +} + +// GetFirmwareInfo will request firmware information from the camera +func (c *Camera) GetFirmwareInfo() (string, error) { + if !c.isLoggedIn { + return "", errors.New("Camera Login required") + } + + firmwareInfo := make(chan string, 1) + c.Handle(FIRMWARE_INFORMATION, func(c *Camera, m *Message) (bool, error) { + firmwareInfo <- string(m.Payload) + return RemoveHandler, nil + }) + err := c.SendPacket(CreateCommandPacket(REQUEST_FIRMWARE_INFO)) + if err != nil { + return "", err + } + + select { + case result := <-firmwareInfo: + return result, nil + case <-time.After(5 * time.Second): + return "", errors.New("Firmware information request timed out") + } +} + +// SendPacket sends a raw packet to the camera +func (c *Camera) SendPacket(packet []byte) error { + _, err := c.connection.Write(packet) + return err +} + +// TakePicture instructs the camera to take a still image +func (c *Camera) TakePicture() error { + if !c.isLoggedIn { + return errors.New("Camera Login required") + } + + pictureTaken := make(chan bool, 1) + c.Handle(PICTURE_SAVED, func(c *Camera, m *Message) (bool, error) { + c.Log("Picture has been saved to SD-Card") + pictureTaken <- true + return RemoveHandler, nil + }) + + err := c.SendPacket(CreateCommandPacket(TAKE_PICTURE)) + if err != nil { + return err + } + + select { + case <-pictureTaken: + return nil + case <-time.After(5 * time.Second): + return errors.New("TAKE_PICTURE request timed out") + } +} + +// StartPreviewStream starts streaming video to this host +func (c *Camera) StartPreviewStream() error { + if !c.isLoggedIn { + return errors.New("Camera Login required") + } + c.Log("Starting Preview Stream") + return c.SendPacket(CreateCommandPacket(START_PREVIEW)) +} + +// StartRecording starts recording video to SD-Card +func (c *Camera) StartRecording() error { + if !c.isLoggedIn { + return errors.New("Camera Login required") + } + c.Log("Starting to record video") + return c.SendPacket(CreatePacket(CreateCommandHeader(CONTROL_RECORDING), []byte{0x01, 0x00, 0x00, 0x00})) +} + +// StopRecording stops recording video to SD-Card +func (c *Camera) StopRecording() error { + if !c.isLoggedIn { + return errors.New("Camera Login required") + } + c.Log("Stopping to record video") + return c.SendPacket(CreatePacket(CreateCommandHeader(CONTROL_RECORDING), []byte{0x00, 0x00, 0x00, 0x00})) +} + +// Disconnect from the camera +func (c *Camera) Disconnect() { + c.disconnect = true + c.connected = false + c.connection.Close() +} + +func aliveRequestHandler(camera *Camera, message *Message) (bool, error) { + camera.Log("Received Alive Request") + responseHeader := CreateCommandHeader(0x0113) // Alive Response + response := CreatePacket(responseHeader, []byte{}) + camera.Log("Sending Alive Response") + return KeepHandler, camera.SendPacket(response) +} + +// OnMessage handles an incoming firmware info message +func firmwareInfoHandler(camera *Camera, message *Message) (bool, error) { + camera.Log("Received Firmware Information") + camera.Log("Firmware Version: %s", string(message.Payload)) + return KeepHandler, nil +} + +func loginResultHandler(camera *Camera, message *Message) (bool, error) { + if message.Header.MessageType == 0x0111 { + camera.isLoggedIn = true + camera.Log("Login accepted") + } else if message.Header.MessageType == 0x1234 { // TODO: RE error code + camera.Log("Login failed") + return RemoveHandler, errors.New("There is already a client connected to the camera") + } + return RemoveHandler, nil +} diff --git a/RTPRelay.go b/RTPRelay.go new file mode 100644 index 0000000..de42b2f --- /dev/null +++ b/RTPRelay.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "io" + "log" + "net" +) + +// RTPRelay holds information on the relaying stream listener +type RTPRelay struct { + close bool + targetIP net.IP + targetPort int +} + +// CreateRTPRelay creates a UDP listener that handles live data +// from the camera and forwards it as an RTP stream +func CreateRTPRelay(targetAddress net.IP, targetPort int) RTPRelay { + conn, err := net.ListenPacket("udp", ":6669") + + if err != nil { + log.Printf("ERROR: %s\n", err) + } + + relay := RTPRelay{ + targetIP: targetAddress, + targetPort: targetPort, + } + if err != nil { + log.Printf("ERROR: %s\n", err) + } + + go handleCameraStream(relay, conn) + + return relay +} + +func handleCameraStream(relay RTPRelay, conn net.PacketConn) { + buffer := make([]byte, 2048) + header := StreamHeader{} + var payload []byte + + rtpTarget := net.UDPAddr{ + IP: relay.targetIP, + Port: relay.targetPort, + } + rtpSource, _ := net.ResolveUDPAddr("udp", "127.0.0.1:5000") + rtpConn, _ := net.DialUDP("udp", rtpSource, &rtpTarget) + + var sequenceNumber uint16 + var elapsed uint32 + + frameBuffer := []byte{} + + for { + conn.ReadFrom(buffer) + packetReader := bytes.NewReader(buffer) + binary.Read(packetReader, binary.BigEndian, &header) + + if header.Magic != 0xBCDE { + log.Printf("Received message with invalid magic (%x).", header.Magic) + break + } + if header.Length > 0 { + payload = make([]byte, header.Length) + bytesRead, err := io.ReadFull(packetReader, payload) + if err != nil { + log.Printf("Read Error: %s, %d bytes\n", err, bytesRead) + break + } + } else { + payload = []byte{} + } + + switch header.MessageType { + case 0x0001: // H.264 Data + frameBuffer = append(frameBuffer, payload...) + case 0x0002: // Time + packet := bytes.Buffer{} + packet.Write([]byte{0x80, 0x63}) + binary.Write(&packet, binary.BigEndian, sequenceNumber) + binary.Write(&packet, binary.BigEndian, (uint32)(elapsed*90)) + binary.Write(&packet, binary.BigEndian, (uint64(0))) + packet.Write(frameBuffer) + rtpConn.Write(packet.Bytes()) + frameBuffer = []byte{} + sequenceNumber++ + elapsed = binary.LittleEndian.Uint32(payload[12:]) + //log.Printf("Elapsed: %d (%x)\n", elapsed, payload[12:]) + default: + log.Printf("Received Unknown Message: %+v\n", header) + log.Printf("Payload:\n%s\n", hex.Dump(payload)) + } + if relay.close { + break + } + } +} + +// Stop stops listening for packets +func (r *RTPRelay) Stop() { + r.close = true +} diff --git a/ipcamera.go b/ipcamera.go index fc8ec8b..6a4dd73 100644 --- a/ipcamera.go +++ b/ipcamera.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/binary" "encoding/hex" + "fmt" "io" "log" "net" @@ -27,11 +28,15 @@ func main() { relay := CreateRTPRelay(net.ParseIP("127.0.0.1"), 5220) defer relay.Stop() - camera := Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, true) + defer camera.Disconnect() log.Printf("Using Camera: %+v\n", camera) - camera.Connect(username, password) + camera.Connect() + camera.Login() + camera.StartPreviewStream() + bufio.NewReader(os.Stdin).ReadBytes('\n') }, } @@ -45,12 +50,20 @@ func main() { Short: "List files stored on the cameras SD-Card", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - camera := Camera{IPAddress: args[0], Port: (int)(port), Verbose: false} + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, false) + defer camera.Disconnect() - camera.Connect(username, password) - camera.RequestFileList() - camera.WaitForMessage(0xA027) - //camera.StoredFiles + camera.Connect() + camera.Login() + files, err := camera.GetFileList() + if err != nil { + log.Printf("ERROR Receiving File List: %s\n", err) + return + } + + for _, file := range files { + fmt.Printf("%s\t%d\n", file.Path, file.Size) + } }, } @@ -59,10 +72,13 @@ func main() { Short: "Take a still image and save to SD-Card", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - camera := Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} - camera.Connect(username, password) + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, true) + defer camera.Disconnect() + + camera.Connect() + camera.Login() + camera.TakePicture() - camera.WaitForMessage(0xA039) }, } @@ -71,10 +87,13 @@ func main() { Short: "Start recording video to SD-Card", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - camera := Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} - camera.Connect(username, password) + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, true) + defer camera.Disconnect() + + camera.Connect() + camera.Login() + camera.StartRecording() - camera.WaitForMessage(0xA03B) }, } @@ -83,10 +102,13 @@ func main() { Short: "Stop recording video to SD-Card", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - camera := Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} - camera.Connect(username, password) + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, true) + defer camera.Disconnect() + + camera.Connect() + camera.Login() + camera.StopRecording() - camera.WaitForMessage(0xA03B) }, } @@ -95,9 +117,12 @@ func main() { Short: "Send a raw command to the camera", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - camera := Camera{IPAddress: args[1], Port: (int)(port), Verbose: true} + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, true) + defer camera.Disconnect() + + camera.Connect() + camera.Login() - camera.Connect(username, password) command, err := hex.DecodeString(args[0]) if err != nil { log.Printf("ERROR: %s\n", err) @@ -107,13 +132,12 @@ func main() { if len(command) >= 2 { header := CreateCommandHeader(uint32(binary.BigEndian.Uint16(command[:2]))) payload := command[2:] - log.Printf("Waiting for Login to finish") - camera.WaitForMessage(0x0111) packet := CreatePacket(header, payload) log.Printf("Sending Command: %X\n", packet) camera.SendPacket(packet) } + log.Printf("Waiting for Data, press ENTER to quit") bufio.NewReader(os.Stdin).ReadBytes('\n') }, } @@ -123,12 +147,19 @@ func main() { Short: "List files stored on the cameras SD-Card", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - camera := Camera{IPAddress: args[0], Port: (int)(port), Verbose: false} + camera := CreateCamera(net.ParseIP(args[0]), int(port), username, password, true) + defer camera.Disconnect() - camera.Connect(username, password) - camera.RequestFileList() - camera.WaitForMessage(0xA027) - newestFile := camera.StoredFiles[len(camera.StoredFiles)-1].Path + camera.Connect() + camera.Login() + + files, err := camera.GetFileList() + if err != nil { + log.Printf("ERROR Receiving File List: %s\n", err) + return + } + + newestFile := files[len(files)-1].Path url := "http://" + args[0] + newestFile log.Printf("Downloading latest File: %s\n", url) downloadFile(filepath.Base(newestFile), url) diff --git a/protocol.go b/protocol.go index 213ce89..3953ff3 100644 --- a/protocol.go +++ b/protocol.go @@ -2,6 +2,8 @@ package main import ( "bytes" + "encoding/hex" + "fmt" "github.com/icza/bitio" ) @@ -13,6 +15,20 @@ type Header struct { MessageType uint32 } +func (h *Header) String() string { + return fmt.Sprintf("{ Header Magic=0x%X, Length=%d, MessageType=0x%X }", h.Magic, h.Length, h.MessageType) +} + +// Message represents a complete message from/to the camera +type Message struct { + Header Header + Payload []byte +} + +func (m *Message) String() string { + return fmt.Sprintf("{ Message Header=%s, Payload=\n%s\n }", m.Header, hex.Dump(m.Payload)) +} + // StreamHeader is a live preview message header type StreamHeader struct { Magic uint16