├── ip ├── testdata │ └── preview.jpg ├── mockresponder_generic_test.go ├── log.go ├── internal │ └── util.go ├── mockresponder_test.go ├── packets_test.go ├── vendor_extensions.go └── mockresponder_fuji_test.go ├── ptp ├── doc.go ├── events_test.go ├── util_test.go ├── util.go ├── vendors_test.go ├── vendors.go ├── device_test.go ├── modes.go ├── storage.go └── events.go ├── go.mod ├── cmd ├── testdata │ ├── test_fail1.conf │ ├── test_ok1.conf │ ├── test_ok2.conf │ └── test_fail2.conf ├── flags_test.go ├── command_unknown.go ├── ishell.go ├── command_noliveview.go ├── commands_test.go ├── server.go ├── command_info.go ├── command_state.go ├── command_help.go ├── command_get.go ├── texture.go ├── command_describe.go ├── format_test.go ├── command_set.go ├── command_opreq.go ├── window.go ├── flags.go ├── config.go ├── main.go ├── command_capture.go ├── commands.go ├── command_liveview.go ├── config_test.go └── format.go ├── Makefile ├── go.sum ├── LICENSE.txt ├── fmt ├── string.go ├── json.go ├── string_test.go ├── testdata │ └── plain.json ├── json_test.go ├── string_fuji.go ├── string_generic.go ├── string_fuji_test.go └── string_generic_test.go ├── viewfinder ├── viewfinder.go └── fuji_xt1.go └── docs └── code-examples └── findAllProperties.php /ip/testdata/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malc0mn/ptp-ip/HEAD/ip/testdata/preview.jpg -------------------------------------------------------------------------------- /ptp/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ptp implements the Picture Transfer Protocol (ISO-15740) basics in a transport agnostic way. 3 | The structs and constants it exposes can be used in various implementations. 4 | 5 | TODO: complete docs. 6 | */ 7 | package ptp 8 | -------------------------------------------------------------------------------- /ptp/events_test.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | import "testing" 4 | 5 | func TestEvent_Session(t *testing.T) { 6 | event := &Event{ 7 | SessionID: 5, 8 | } 9 | 10 | got := event.Session() 11 | want := SessionID(5) 12 | if got != want { 13 | t.Errorf("Session() return = %d, want %d", got, want) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/malc0mn/ptp-ip 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 7 | github.com/go-gl/glfw v0.0.0-20200707082815-5321531c36a2 8 | github.com/go-ini/ini v1.56.0 9 | github.com/google/uuid v1.1.1 10 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76 11 | ) 12 | -------------------------------------------------------------------------------- /cmd/testdata/test_fail1.conf: -------------------------------------------------------------------------------- 1 | ; This is us 2 | [initiator] 3 | friendly_name = "Golang test client" 4 | ; Generate a new random one using uuidgen or some other tool! 5 | ; Or simply do cat /proc/sys/kernel/random/uuid 6 | guid = "cca455de-79ac-4b12-9731-91e433a899cf" 7 | 8 | ; The target we will be connecting to 9 | [responder] 10 | host = "192.168.0.2" 11 | port = 0 12 | 13 | ; Config when running as a daemon 14 | [server] 15 | ; Setting this to true will enable server mode 16 | enabled = true 17 | address = "127.0.0.2" 18 | port = 0 19 | -------------------------------------------------------------------------------- /cmd/flags_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUint16Value(t *testing.T) { 8 | u := uint16Value(55) 9 | 10 | want := "55" 11 | got := u.String() 12 | if got != want { 13 | t.Errorf("uint16Value String() = %s; want %s", got, want) 14 | } 15 | 16 | err := u.Set("0") 17 | if err == nil { 18 | t.Errorf("uint16Value Set() = %s; want value out of range", err) 19 | } 20 | 21 | err = u.Set("65536") 22 | if err == nil { 23 | t.Errorf("uint16Value Set() = %s; want value out of range", err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ptp/util_test.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | import ( 4 | "encoding/binary" 5 | "testing" 6 | ) 7 | 8 | func TestByteArrayToInt64(t *testing.T) { 9 | b := make([]byte, 4) 10 | binary.LittleEndian.PutUint16(b, 0x6140) 11 | 12 | got := byteArrayToInt64(b, 2) 13 | want := int64(0x6140) 14 | if got != want { 15 | t.Errorf("byteArrayToInt64() return = %d, want %d", got, want) 16 | } 17 | 18 | got = byteArrayToInt64(b, 7) 19 | want = int64(0x6140) 20 | if got != want { 21 | t.Errorf("byteArrayToInt64() return = %d, want %d", got, want) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/testdata/test_ok1.conf: -------------------------------------------------------------------------------- 1 | ; This is us 2 | [initiator] 3 | friendly_name = "Golang test OK1 client" 4 | ; Generate a new random one using uuidgen or some other tool! 5 | ; Or simply do cat /proc/sys/kernel/random/uuid 6 | guid = "cca455de-79ac-4b12-9731-91e433a899cf" 7 | 8 | ; The target we will be connecting to 9 | [responder] 10 | vendor = "fuji" 11 | host = "192.168.0.2" 12 | port = 35740 13 | 14 | ; Config when running as a daemon 15 | [server] 16 | ; Setting this to true will enable server mode 17 | enabled = true 18 | address = "127.0.0.2" 19 | port = 25740 20 | -------------------------------------------------------------------------------- /cmd/command_unknown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | ) 6 | 7 | // No init function here!!! 8 | 9 | type unknown struct{} 10 | 11 | func (unknown) name() string { 12 | return "unknown" 13 | } 14 | 15 | func (unknown) alias() []string { 16 | return []string{} 17 | } 18 | 19 | func (unknown) execute(_ *ip.Client, _ []string, _ chan<- string) string { 20 | return "unknown command\n" 21 | } 22 | 23 | func (c unknown) help() string { 24 | return "" 25 | } 26 | 27 | func (unknown) arguments() []string { 28 | return []string{} 29 | } 30 | -------------------------------------------------------------------------------- /cmd/testdata/test_ok2.conf: -------------------------------------------------------------------------------- 1 | ; This is us 2 | [initiator] 3 | friendly_name = "Golang test OK2 client" 4 | ; Generate a new random one using uuidgen or some other tool! 5 | ; Or simply do cat /proc/sys/kernel/random/uuid 6 | guid = "9fe5160c-4951-404d-9505-10baaf725606" 7 | 8 | ; The target we will be connecting to 9 | [responder] 10 | vendor = "fuji" 11 | host = "192.168.0.2" 12 | cmd_data_port = 55740 13 | event_port = 55741 14 | stream_port = 55742 15 | 16 | ; Config when running as a daemon 17 | [server] 18 | ; Setting this to true will enable server mode 19 | enabled = true 20 | address = "127.0.0.3" 21 | port = 35740 22 | -------------------------------------------------------------------------------- /ptp/util.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | import "encoding/binary" 4 | 5 | // byteArrayToInt64 converts a byte array to an int64 where l is the number of significant bytes in the byte array. 6 | // Setting l to 0 will cause l to be set to the length of the byte array passed in. 7 | func byteArrayToInt64(b []byte, l int) int64 { 8 | if l == 0 || l > len(b) { 9 | l = len(b) 10 | } 11 | 12 | if l < 8 { 13 | pad := make([]byte, 8-l) 14 | b = append(b, pad...) 15 | } 16 | 17 | // Converting between uint64 and int64 does not change the sign bit, only the way it is interpreted. 18 | return int64(binary.LittleEndian.Uint64(b)) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/testdata/test_fail2.conf: -------------------------------------------------------------------------------- 1 | ; This is us 2 | [initiator] 3 | friendly_name = "Golang test client" 4 | ; Generate a new random one using uuidgen or some other tool! 5 | ; Or simply do cat /proc/sys/kernel/random/uuid 6 | guid = "cca455de-79ac-4b12-9731-91e433a899cf" 7 | 8 | ; The target we will be connecting to 9 | [responder] 10 | vendor = "fuji" 11 | host = "192.168.0.2" 12 | port = 35740 13 | comm_data_port = 55740 14 | event_port = 55741 15 | stream_port = 55742 16 | 17 | ; Config when running as a daemon 18 | [server] 19 | ; Setting this to true will enable server mode 20 | enabled = true 21 | address = "127.0.0.2" 22 | port = 25740 23 | -------------------------------------------------------------------------------- /cmd/ishell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/malc0mn/ptp-ip/ip" 7 | "os" 8 | "time" 9 | ) 10 | 11 | func iShell(c *ip.Client) { 12 | rw := bufio.NewReadWriter(bufio.NewReader(os.Stdin), bufio.NewWriter(os.Stdout)) 13 | fmt.Print("Interactive shell ready to receive commands.\n") 14 | for { 15 | // TODO: find a good way (not sleep) to "separate" the outputs so that the '> ' below does not get 'mixed' with 16 | // the Dial() debug output from the client... 17 | time.Sleep(1 * time.Second) 18 | 19 | fmt.Print("> ") 20 | readAndExecuteCommand(rw, c, "[iShell]") 21 | fmt.Print("\n\n") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCEDIR=cmd 2 | BINARY=ptpip 3 | BINARY_NOLV=${BINARY}-nolv 4 | VERSION := $(shell git describe --tags) 5 | BUILD_TIME := $(shell date +%FT%T%z) 6 | 7 | LDFLAGS=-ldflags "-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" 8 | TAGS=-tags with_lv 9 | 10 | .DEFAULT_GOAL: all 11 | 12 | .PHONY: all 13 | all: ptpip 14 | 15 | ptpip: 16 | cd cmd; go build -trimpath ${LDFLAGS} ${TAGS} -o ../${BINARY} 17 | 18 | nolv: 19 | cd cmd; go build -trimpath ${LDFLAGS} -o ../${BINARY_NOLV} 20 | 21 | .PHONY: test 22 | test: 23 | go test ./... 24 | 25 | .PHONY: install 26 | install: 27 | cd cmd; GOBIN=/usr/local/bin/ go install ${LDFLAGS} ${TAGS} 28 | 29 | .PHONY: clean 30 | clean: 31 | if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi ; if [ -f ${BINARY_NOLV} ] ; then rm ${BINARY_NOLV} ; fi 32 | -------------------------------------------------------------------------------- /cmd/command_noliveview.go: -------------------------------------------------------------------------------- 1 | // +build !with_lv 2 | 3 | package main 4 | 5 | import "github.com/malc0mn/ptp-ip/ip" 6 | 7 | var nolv = "Binary not compiled with live view support!" 8 | 9 | func init() { 10 | registerCommand(&liveview{}) 11 | } 12 | 13 | type liveview struct{} 14 | 15 | func (liveview) name() string { 16 | return "liveview" 17 | } 18 | 19 | func (liveview) alias() []string { 20 | return []string{} 21 | } 22 | 23 | func (liveview) execute(_ *ip.Client, _ []string, _ chan<- string) string { 24 | return nolv + "\n" 25 | } 26 | 27 | func (l liveview) help() string { 28 | return `"` + l.name() + `" is not supported in this build!` 29 | } 30 | 31 | func (liveview) arguments() []string { 32 | return []string{} 33 | } 34 | 35 | func mainThread() { 36 | return 37 | } 38 | 39 | func preview(_ []byte) string { 40 | return nolv 41 | } 42 | -------------------------------------------------------------------------------- /cmd/commands_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | "testing" 7 | ) 8 | 9 | func TestCommandByName(t *testing.T) { 10 | cmds := map[string]command{ 11 | "capture": &capture{}, 12 | "describe": &describe{}, 13 | "get": &get{}, 14 | "help": &help{}, 15 | "info": &info{}, 16 | "liveview": &liveview{}, 17 | "opreq": &opreq{}, 18 | "shoot": &capture{}, 19 | "shutter": &capture{}, 20 | "snap": &capture{}, 21 | "set": &set{}, 22 | "state": &state{}, 23 | } 24 | for name, want := range cmds { 25 | got := commandByName(name) 26 | if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) { 27 | t.Errorf("commandByName(%s) got = %v; want %v", name, got, want) 28 | } 29 | } 30 | } 31 | 32 | func TestUnknown(t *testing.T) { 33 | got := unknown{}.execute(&ip.Client{}, []string{}, make(chan string)) 34 | want := "unknown command\n" 35 | if got != want { 36 | t.Errorf("got = '%s'; want '%s'", got, want) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= 2 | github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= 3 | github.com/go-gl/glfw v0.0.0-20200707082815-5321531c36a2 h1:tCvD9jzwA40XAvO3wIhY748dWrXyNJ0mDQ3pTvlHlXQ= 4 | github.com/go-gl/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 5 | github.com/go-ini/ini v1.56.0 h1:6HjxSjqdmgnujDPhlzR4a44lxK3w03WPN8te0SoUSeM= 6 | github.com/go-ini/ini v1.56.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 7 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 8 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= 10 | golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 11 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 12 | -------------------------------------------------------------------------------- /ptp/vendors_test.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | import "testing" 4 | 5 | func TestVendorStringToType(t *testing.T) { 6 | check := map[string]VendorExtension{ 7 | "kodak": VE_EastmanKodakCompany, 8 | "epson": VE_SeikoEpson, 9 | "agilent": VE_AgilentTechnologiesInc, 10 | "polaroid": VE_PolaroidCorporation, 11 | "agfa": VE_AgfaGevaert, 12 | "ms": VE_MicrosoftCorporation, 13 | "equinox": VE_EquinoxResearchLtd, 14 | "vq": VE_ViewQuestTechnologies, 15 | "st": VE_STMicroelectronics, 16 | "nikon": VE_NikonCorporation, 17 | "canon": VE_CanonInc, 18 | "fn": VE_FotoNationInc, 19 | "pentax": VE_PENTAXCorporation, 20 | "fuji": VE_FujiPhotoFilmCoLtd, 21 | "ndd": VE_NddMedicalTechnologies, 22 | "samsung": VE_SamsungElectronicsCoLtd, 23 | "parrot": VE_ParrotDronesSAS, 24 | "panasonic": VE_PanasonicCorporation, 25 | "generic": 0, 26 | } 27 | 28 | for code, want := range check { 29 | got := VendorStringToType(code) 30 | if got != want { 31 | t.Errorf("VendorStringToType() return = %#x, want %#x", got, want) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 malc0mn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | "log" 7 | "net" 8 | ) 9 | 10 | func validateAddress() { 11 | if ip := net.ParseIP(conf.srvAddr); ip == nil { 12 | log.Fatalf("Invalid IP address '%s'", conf.srvAddr) 13 | } 14 | } 15 | 16 | func launchServer(c *ip.Client) { 17 | validateAddress() 18 | 19 | lmp := "[Local server]" 20 | sock, err := net.Listen("tcp", net.JoinHostPort(conf.srvAddr, conf.srvPort.String())) 21 | defer sock.Close() 22 | if err != nil { 23 | log.Printf("%s error %s...", lmp, err) 24 | return 25 | } 26 | log.Printf("%s listening on %s...", lmp, sock.Addr().String()) 27 | log.Printf("%s awaiting messages... (CTRL+C to quit)", lmp) 28 | 29 | for { 30 | conn, err := sock.Accept() 31 | if err != nil { 32 | log.Printf("%s accept error %s...", lmp, err) 33 | continue 34 | } 35 | go handleMessages(conn, c, lmp) 36 | } 37 | } 38 | 39 | func handleMessages(conn net.Conn, c *ip.Client, lmp string) { 40 | defer conn.Close() 41 | rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) 42 | 43 | readAndExecuteCommand(rw, c, lmp) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/command_info.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | ) 6 | 7 | func init() { 8 | registerCommand(&info{}) 9 | } 10 | 11 | type info struct{} 12 | 13 | func (info) name() string { 14 | return "info" 15 | } 16 | 17 | func (info) alias() []string { 18 | return []string{} 19 | } 20 | 21 | func (info) execute(c *ip.Client, f []string, _ chan<- string) string { 22 | res, err := c.GetDeviceInfo() 23 | 24 | if err != nil { 25 | res = err.Error() 26 | } 27 | 28 | return formatDeviceInfo(c.ResponderVendor(), res, f) 29 | } 30 | 31 | func (i info) help() string { 32 | help := `"` + i.name() + `" displays the device info. The data returned can vary from vendor to vendor.` + "\n" 33 | 34 | if args := i.arguments(); len(args) > 0 { 35 | help += helpAddArgumentsTitle() 36 | for i, arg := range args { 37 | switch i { 38 | case 0: 39 | help += "\t- " + `"` + arg + `" to output the data in parsable json format` + "\n" 40 | case 1: 41 | help += "\t- " + `"` + arg + `" to be used together with "` + args[0] + `": format the output in a human readable way` + "\n" 42 | } 43 | } 44 | } 45 | 46 | return help 47 | } 48 | 49 | func (info) arguments() []string { 50 | return []string{"json", "pretty"} 51 | } 52 | -------------------------------------------------------------------------------- /cmd/command_state.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | ) 6 | 7 | func init() { 8 | registerCommand(&state{}) 9 | } 10 | 11 | type state struct{} 12 | 13 | func (state) name() string { 14 | return "state" 15 | } 16 | 17 | func (state) alias() []string { 18 | return []string{} 19 | } 20 | 21 | func (state) execute(c *ip.Client, f []string, _ chan<- string) string { 22 | res, err := c.GetDeviceState() 23 | 24 | if err != nil { 25 | res = err.Error() 26 | } 27 | 28 | return formatDeviceInfo(c.ResponderVendor(), res, f) 29 | } 30 | 31 | func (i state) help() string { 32 | help := `"` + i.name() + `" displays the current device state. This currently is a Fuji specific command!` + "\n" 33 | 34 | if args := i.arguments(); len(args) > 0 { 35 | help += helpAddArgumentsTitle() 36 | for i, arg := range args { 37 | switch i { 38 | case 0: 39 | help += "\t- " + `"` + arg + `" to output the data in parsable json format` + "\n" 40 | case 1: 41 | help += "\t- " + `"` + arg + `" to be used together with "` + args[0] + `": format the output in a human readable way` + "\n" 42 | } 43 | } 44 | } 45 | 46 | return help 47 | } 48 | 49 | func (state) arguments() []string { 50 | return []string{"json", "pretty"} 51 | } 52 | -------------------------------------------------------------------------------- /cmd/command_help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | "sort" 6 | ) 7 | 8 | func init() { 9 | registerCommand(&help{}) 10 | } 11 | 12 | type help struct{} 13 | 14 | func (help) name() string { 15 | return "help" 16 | } 17 | 18 | func (help) alias() []string { 19 | return []string{} 20 | } 21 | 22 | func (help) execute(_ *ip.Client, f []string, _ chan<- string) string { 23 | if len(f) == 0 { 24 | names := make([]string, 0, len(commands)) 25 | for name := range commands { 26 | names = append(names, name) 27 | } 28 | sort.Strings(names) 29 | 30 | txt := "\nSupported commands:\n\n" 31 | for _, name := range names { 32 | txt += commands[name].help() + "\n\n" 33 | } 34 | return txt 35 | } 36 | 37 | if cmd, exists := commands[f[0]]; exists { 38 | return "\n" + cmd.help() 39 | } 40 | 41 | return "\nUnknown command " + f[0] + "!\n" 42 | } 43 | 44 | func (h help) help() string { 45 | help := `"` + h.name() + `" displays help for all commands or for a single one.` + "\n" 46 | 47 | if args := h.arguments(); len(args) > 0 { 48 | help += helpAddArgumentsTitle() 49 | for i, arg := range args { 50 | switch i { 51 | case 0: 52 | help += "\t- " + arg + " to get help for\n" 53 | } 54 | } 55 | } 56 | 57 | return help 58 | } 59 | 60 | func (help) arguments() []string { 61 | return []string{"command"} 62 | } 63 | -------------------------------------------------------------------------------- /cmd/command_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ptpfmt "github.com/malc0mn/ptp-ip/fmt" 6 | "github.com/malc0mn/ptp-ip/ip" 7 | ) 8 | 9 | func init() { 10 | registerCommand(&get{}) 11 | } 12 | 13 | type get struct{} 14 | 15 | func (get) name() string { 16 | return "get" 17 | } 18 | 19 | func (get) alias() []string { 20 | return []string{} 21 | } 22 | 23 | func (get) execute(c *ip.Client, f []string, _ chan<- string) string { 24 | errorFmt := "get error: %s\n" 25 | 26 | cod, err := formatDeviceProperty(c, f[0]) 27 | if err != nil { 28 | return fmt.Sprintf(errorFmt, err) 29 | } 30 | 31 | v, err := c.GetDevicePropertyValue(cod) 32 | if err != nil { 33 | return fmt.Sprintf(errorFmt, err) 34 | } 35 | 36 | return ptpfmt.DevicePropValAsString(c.ResponderVendor(), cod, int64(v)) + fmt.Sprintf(" (%#x)", v) 37 | } 38 | 39 | func (g get) help() string { 40 | help := `"` + g.name() + `" gets the current value for the given property.` + "\n" 41 | 42 | if args := g.arguments(); len(args) > 0 { 43 | help += helpAddArgumentsTitle() 44 | for i, arg := range args { 45 | switch i { 46 | case 0: 47 | help += "\t- " + arg + ": a hexadecimal field code in the form of '0x5001' or one of the supported unified field names:\n" + helpAddUnifiedFieldNames() 48 | } 49 | } 50 | } 51 | 52 | return help 53 | } 54 | 55 | func (get) arguments() []string { 56 | return []string{"property"} 57 | } 58 | -------------------------------------------------------------------------------- /cmd/texture.go: -------------------------------------------------------------------------------- 1 | // Taken from github.com/fogleman/imview and slightly customized. 2 | 3 | // +build with_lv 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/go-gl/gl/v2.1/gl" 9 | "image" 10 | "image/draw" 11 | ) 12 | 13 | type texture struct { 14 | handle uint32 15 | } 16 | 17 | func newTexture() *texture { 18 | var handle uint32 19 | gl.GenTextures(1, &handle) 20 | t := &texture{handle} 21 | t.setMinFilter(gl.LINEAR) 22 | t.setMagFilter(gl.NEAREST) 23 | t.setWrapS(gl.CLAMP_TO_EDGE) 24 | t.setWrapT(gl.CLAMP_TO_EDGE) 25 | return t 26 | } 27 | 28 | func (t *texture) bind() { 29 | gl.BindTexture(gl.TEXTURE_2D, t.handle) 30 | } 31 | 32 | func (t *texture) setImage(im image.Image) { 33 | rgba, ok := im.(*image.RGBA) 34 | if !ok { 35 | rgba = image.NewRGBA(im.Bounds()) 36 | draw.Draw(rgba, rgba.Rect, im, image.Point{}, draw.Src) 37 | } 38 | t.setRGBA(rgba) 39 | } 40 | 41 | func (t *texture) setRGBA(im *image.RGBA) { 42 | t.bind() 43 | size := im.Rect.Size() 44 | gl.TexImage2D( 45 | gl.TEXTURE_2D, 0, gl.RGBA, int32(size.X), int32(size.Y), 46 | 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(im.Pix), 47 | ) 48 | } 49 | 50 | func (t *texture) setMinFilter(x int32) { 51 | t.bind() 52 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, x) 53 | } 54 | 55 | func (t *texture) setMagFilter(x int32) { 56 | t.bind() 57 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, x) 58 | } 59 | 60 | func (t *texture) setWrapS(x int32) { 61 | t.bind() 62 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, x) 63 | } 64 | 65 | func (t *texture) setWrapT(x int32) { 66 | t.bind() 67 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, x) 68 | } 69 | -------------------------------------------------------------------------------- /cmd/command_describe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | ) 7 | 8 | func init() { 9 | registerCommand(&describe{}) 10 | } 11 | 12 | type describe struct{} 13 | 14 | func (describe) name() string { 15 | return "describe" 16 | } 17 | 18 | func (describe) alias() []string { 19 | return []string{} 20 | } 21 | 22 | func (describe) execute(c *ip.Client, f []string, _ chan<- string) string { 23 | errorFmt := "describe error: %s\n" 24 | 25 | cod, err := formatDeviceProperty(c, f[0]) 26 | if err != nil { 27 | return fmt.Sprintf(errorFmt, err) 28 | } 29 | 30 | res, err := c.GetDevicePropertyDescription(cod) 31 | if err != nil { 32 | return fmt.Sprintf(errorFmt, err) 33 | } 34 | 35 | if res == nil { 36 | return fmt.Sprintf(errorFmt, fmt.Sprintf("cannot describe property %#x", cod)) 37 | } 38 | 39 | return fujiFormatDeviceProperty(res, f[1:]) 40 | } 41 | 42 | func (d describe) help() string { 43 | help := `"` + d.name() + `" describes the given property.` + "\n" 44 | 45 | if args := d.arguments(); len(args) > 0 { 46 | help += helpAddArgumentsTitle() 47 | for i, arg := range args { 48 | switch i { 49 | case 0: 50 | help += "\t- " + arg + ": a hexadecimal field code in the form of '0x5005' or one of the supported unified field names:\n" + helpAddUnifiedFieldNames() 51 | case 1: 52 | help += "\t- " + `"` + arg + `" to output the data in parsable json format` + "\n" 53 | case 2: 54 | help += "\t- " + `"` + arg + `" to be used together with "` + args[1] + `": format the output in a human readable way` + "\n" 55 | } 56 | } 57 | } 58 | 59 | return help 60 | } 61 | 62 | func (describe) arguments() []string { 63 | return []string{"property", "json", "pretty"} 64 | } 65 | -------------------------------------------------------------------------------- /cmd/format_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | "testing" 7 | ) 8 | 9 | func TestFormatDeviceProperty(t *testing.T) { 10 | c, err := ip.NewClient(ip.DefaultVendor, ip.DefaultIpAddress, ip.DefaultPort, "", "", ip.LevelSilent) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | want := ptp.DevicePropCode(0) 16 | wantE := "error converting: strconv.ParseUint: parsing \"test\": invalid syntax or unknown field name 'test'" 17 | got, err := formatDeviceProperty(c, "test") 18 | if err.Error() != wantE { 19 | t.Errorf("formatDeviceProperty() error = %s; want %s", err, wantE) 20 | } 21 | if got != want { 22 | t.Errorf("formatDeviceProperty() got %#x; want %#x", got, want) 23 | } 24 | 25 | want = ptp.DevicePropCode(0x5005) 26 | got, err = formatDeviceProperty(c, "0x5005") 27 | if err != nil { 28 | t.Errorf("formatDeviceProperty() error = %s; want ", err) 29 | } 30 | if got != want { 31 | t.Errorf("formatDeviceProperty() got %#x; want %#x", got, want) 32 | } 33 | 34 | want = ptp.DevicePropCode(0x500f) 35 | got, err = formatDeviceProperty(c, "iso") 36 | if err != nil { 37 | t.Errorf("formatDeviceProperty() error = %s; want ", err) 38 | } 39 | if got != want { 40 | t.Errorf("formatDeviceProperty() got %#x; want %#x", got, want) 41 | } 42 | 43 | c, err = ip.NewClient("fuji", ip.DefaultIpAddress, ip.DefaultPort, "", "", ip.LevelSilent) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | want = ptp.DevicePropCode(0xd02a) 49 | got, err = formatDeviceProperty(c, "iso") 50 | if err != nil { 51 | t.Errorf("formatDeviceProperty() error = %s; want ", err) 52 | } 53 | if got != want { 54 | t.Errorf("formatDeviceProperty() got %#x; want %#x", got, want) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/command_set.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ptpfmt "github.com/malc0mn/ptp-ip/fmt" 6 | "github.com/malc0mn/ptp-ip/ip" 7 | ) 8 | 9 | func init() { 10 | registerCommand(&set{}) 11 | } 12 | 13 | type set struct{} 14 | 15 | func (set) name() string { 16 | return "set" 17 | } 18 | 19 | func (set) alias() []string { 20 | return []string{} 21 | } 22 | 23 | func (set) execute(c *ip.Client, f []string, _ chan<- string) string { 24 | errorFmt := "set error: %s\n" 25 | 26 | cod, err := formatDeviceProperty(c, f[0]) 27 | if err != nil { 28 | return fmt.Sprintf(errorFmt, err) 29 | } 30 | 31 | // TODO: add support for "string" values such as "astia" for film simulation. 32 | val, err := ptpfmt.HexStringToUint64(f[1], 32) 33 | if err != nil { 34 | return fmt.Sprintf(errorFmt, err) 35 | } 36 | c.Debugf("Converted value to: %#x", val) 37 | 38 | err = c.SetDeviceProperty(cod, uint32(val)) 39 | if err != nil { 40 | return fmt.Sprintf(errorFmt, err) 41 | } 42 | 43 | return fmt.Sprintf("property %s successfully set to %#x\n", f[0], val) 44 | } 45 | 46 | func (s set) help() string { 47 | help := `"` + s.name() + `" sets the given value for the given property. Depending on the camera operation mode (aperture priority, shutter priority, manual or auto), not all properties might be settable!` + "\n" 48 | 49 | if args := s.arguments(); len(args) > 0 { 50 | help += helpAddArgumentsTitle() 51 | for i, arg := range args { 52 | switch i { 53 | case 0: 54 | help += "\t- " + arg + " is a hexadecimal field code in the form of '0x5001' or one of the supported unified field names:\n" + helpAddUnifiedFieldNames() 55 | case 1: 56 | help += "\t- " + arg + " is a hexadecimal value to set the field to. E.g. '0x6'\n" 57 | } 58 | } 59 | } 60 | 61 | return help 62 | } 63 | 64 | func (set) arguments() []string { 65 | return []string{"property", "value"} 66 | } 67 | -------------------------------------------------------------------------------- /ip/mockresponder_generic_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | "io" 7 | "net" 8 | ) 9 | 10 | func handleGenericMessages(conn net.Conn, _ chan uint32, lmp string) { 11 | // NO defer conn.Close() here since we need to mock a real responder and thus need to keep the connections open when 12 | // established and continuously listen for messages in a loop. 13 | for { 14 | h, pkt, err := readMessage(conn, lmp) 15 | if err == io.EOF { 16 | conn.Close() 17 | break 18 | } 19 | if pkt == nil { 20 | continue 21 | } 22 | 23 | var msg string 24 | var res PacketIn 25 | switch h.PacketType { 26 | case PKT_InitCommandRequest: 27 | msg, res = genericInitCommandRequestResponse(lmp, PV_VersionOnePointZero) 28 | case PKT_InitEventRequest: 29 | msg, res = genericInitEventRequestResponse() 30 | case PKT_OperationRequest: 31 | msg, res = genericOperationRequestResponse() 32 | default: 33 | lgr.Errorf("%s unknown packet type %#x", lmp, h.PacketType) 34 | continue 35 | } 36 | 37 | if res != nil { 38 | if msg != "" { 39 | lgr.Infof("%s responding to %s", lmp, msg) 40 | } 41 | sendMessage(conn, res, nil, lmp) 42 | } 43 | } 44 | } 45 | 46 | func genericInitCommandRequestResponse(friendlyName string, pv ProtocolVersion) (string, PacketIn) { 47 | uuid, _ := uuid.Parse(MockResponderGUID) 48 | return "InitCommandRequest", 49 | &InitCommandAckPacket{ 50 | ConnectionNumber: 1, 51 | ResponderGUID: uuid, 52 | ResponderFriendlyName: friendlyName, 53 | ResponderProtocolVersion: uint32(pv), 54 | } 55 | } 56 | 57 | func genericInitEventRequestResponse() (string, PacketIn) { 58 | return "InitEventRequest", &InitEventAckPacket{} 59 | } 60 | 61 | func genericOperationRequestResponse() (string, PacketIn) { 62 | return "OperationRequest", &OperationResponsePacket{ 63 | OperationResponse: ptp.OperationResponse{ 64 | TransactionID: 2, 65 | }, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cmd/command_opreq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | ptpfmt "github.com/malc0mn/ptp-ip/fmt" 7 | "github.com/malc0mn/ptp-ip/ip" 8 | "github.com/malc0mn/ptp-ip/ptp" 9 | ) 10 | 11 | func init() { 12 | registerCommand(&opreq{}) 13 | } 14 | 15 | type opreq struct{} 16 | 17 | func (opreq) name() string { 18 | return "opreq" 19 | } 20 | 21 | func (opreq) alias() []string { 22 | return []string{} 23 | } 24 | 25 | func (opreq) execute(c *ip.Client, f []string, _ chan<- string) string { 26 | var res string 27 | errorFmt := "opreq error: %s\n" 28 | 29 | cod, err := ptpfmt.HexStringToUint64(f[0], 16) 30 | if err != nil { 31 | return fmt.Sprintf(errorFmt, err) 32 | } 33 | c.Debugf("Converted uint16: %#x", cod) 34 | 35 | params := f[1:] 36 | p := make([]uint32, len(params)) 37 | for i, param := range params { 38 | conv, err := ptpfmt.HexStringToUint64(param, 64) 39 | if err != nil { 40 | return fmt.Sprintf(errorFmt, err) 41 | } 42 | p[i] = uint32(conv) 43 | } 44 | 45 | c.Debugf("Converted params: %#x", p) 46 | 47 | d, err := c.OperationRequestRaw(ptp.OperationCode(cod), p) 48 | if err != nil { 49 | return fmt.Sprintf(errorFmt, err) 50 | } 51 | 52 | for _, raw := range d { 53 | res += fmt.Sprintf("\nReceived %d bytes. HEX dump:\n%s", len(raw), hex.Dump(raw)) 54 | } 55 | 56 | return res 57 | } 58 | 59 | func (o opreq) help() string { 60 | help := `"` + o.name() + `" This command is intended for reverse engineering and/or debugging purposes. The output will always be a hexadecimal dump of the packets received from the responder.` + "\n" 61 | 62 | if args := o.arguments(); len(args) > 0 { 63 | help += helpAddArgumentsTitle() 64 | for i, arg := range args { 65 | switch i { 66 | case 0: 67 | help += "\t- " + arg + ": a hexadecimal operation code in the form of '0x1014'. The supported operation codes will vary from vendor to vendor.\n" 68 | case 1: 69 | help += "\t- " + arg + ": depending on the operation code, an additional parameter might be required. It is expected to be in hexadecimal form, e.g. '0x5003'\n" 70 | } 71 | } 72 | } 73 | 74 | return help 75 | } 76 | 77 | func (opreq) arguments() []string { 78 | return []string{"opcode", "param"} 79 | } 80 | -------------------------------------------------------------------------------- /ptp/vendors.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | type VendorExtension uint32 4 | 5 | const ( 6 | VE_EastmanKodakCompany VendorExtension = 0x00000001 7 | VE_SeikoEpson VendorExtension = 0x00000002 8 | VE_AgilentTechnologiesInc VendorExtension = 0x00000003 9 | VE_PolaroidCorporation VendorExtension = 0x00000004 10 | VE_AgfaGevaert VendorExtension = 0x00000005 11 | VE_MicrosoftCorporation VendorExtension = 0x00000006 12 | VE_EquinoxResearchLtd VendorExtension = 0x00000007 13 | VE_ViewQuestTechnologies VendorExtension = 0x00000008 14 | VE_STMicroelectronics VendorExtension = 0x00000009 15 | VE_NikonCorporation VendorExtension = 0x0000000A 16 | VE_CanonInc VendorExtension = 0x0000000B 17 | VE_FotoNationInc VendorExtension = 0x0000000C 18 | VE_PENTAXCorporation VendorExtension = 0x0000000D 19 | VE_FujiPhotoFilmCoLtd VendorExtension = 0x0000000E 20 | VE_NddMedicalTechnologies VendorExtension = 0x00000012 21 | VE_SamsungElectronicsCoLtd VendorExtension = 0x0000001A 22 | VE_ParrotDronesSAS VendorExtension = 0x0000001B 23 | VE_PanasonicCorporation VendorExtension = 0x0000001C 24 | ) 25 | 26 | func VendorStringToType(vendor string) VendorExtension { 27 | switch vendor { 28 | case "kodak": 29 | return VE_EastmanKodakCompany 30 | case "epson": 31 | return VE_SeikoEpson 32 | case "agilent": 33 | return VE_AgilentTechnologiesInc 34 | case "polaroid": 35 | return VE_PolaroidCorporation 36 | case "agfa": 37 | return VE_AgfaGevaert 38 | case "ms": 39 | return VE_MicrosoftCorporation 40 | case "equinox": 41 | return VE_EquinoxResearchLtd 42 | case "vq": 43 | return VE_ViewQuestTechnologies 44 | case "st": 45 | return VE_STMicroelectronics 46 | case "nikon": 47 | return VE_NikonCorporation 48 | case "canon": 49 | return VE_CanonInc 50 | case "fn": 51 | return VE_FotoNationInc 52 | case "pentax": 53 | return VE_PENTAXCorporation 54 | case "fuji": 55 | return VE_FujiPhotoFilmCoLtd 56 | case "ndd": 57 | return VE_NddMedicalTechnologies 58 | case "samsung": 59 | return VE_SamsungElectronicsCoLtd 60 | case "parrot": 61 | return VE_ParrotDronesSAS 62 | case "panasonic": 63 | return VE_PanasonicCorporation 64 | default: 65 | return 0 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /fmt/string.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | PRP_Delay string = "delay" 12 | PRP_Effect string = "effect" 13 | PRP_Exposure string = "exposure" 14 | PRP_ExpBias string = "exp-bias" 15 | PRP_FlashMode string = "flashmode" 16 | PRP_FocusMeteringMode string = "focusmtr" 17 | PRP_ISO string = "iso" 18 | PRP_WhiteBalance string = "whitebalance" 19 | ) 20 | 21 | var UnifiedFieldNames = []string{ 22 | PRP_Delay, 23 | PRP_Effect, 24 | PRP_Exposure, 25 | PRP_ExpBias, 26 | PRP_FlashMode, 27 | PRP_FocusMeteringMode, 28 | PRP_ISO, 29 | PRP_WhiteBalance, 30 | } 31 | 32 | func ConvertToHexString(v interface{}) string { 33 | return fmt.Sprintf("%#x", v) 34 | } 35 | 36 | // HexStringToUint64 will convert a string in hexadecimal notation to an unsigned 64 bit integer. String values may 37 | // start with 0x but this is not mandatory. 38 | func HexStringToUint64(code string, bitSize int) (uint64, error) { 39 | cod, err := strconv.ParseUint(strings.Replace(code, "0x", "", -1), 16, bitSize) 40 | if err != nil { 41 | return 0, fmt.Errorf("error converting: %s", err) 42 | } 43 | 44 | return cod, nil 45 | } 46 | 47 | // TODO: how to do this better without the need to pass in a vendor? This is called from MarshalJSON() which cannot 48 | // accept parameters. 49 | func DevicePropCodeAsString(code ptp.DevicePropCode) string { 50 | res := GenericDevicePropCodeAsString(code) 51 | if res == "" { 52 | res = FujiDevicePropCodeAsString(code) 53 | } 54 | 55 | return res 56 | } 57 | 58 | // PropNameToDevicePropCode converts a string to a device property code. 59 | func PropNameToDevicePropCode(vendor ptp.VendorExtension, param string) (ptp.DevicePropCode, error) { 60 | switch vendor { 61 | case ptp.VE_FujiPhotoFilmCoLtd: 62 | return FujiPropToDevicePropCode(param) 63 | default: 64 | return GenericPropToDevicePropCode(param) 65 | } 66 | } 67 | 68 | func DevicePropValAsString(vendor ptp.VendorExtension, code ptp.DevicePropCode, v int64) string { 69 | switch vendor { 70 | case ptp.VE_FujiPhotoFilmCoLtd: 71 | return FujiDevicePropValueAsString(code, v) 72 | default: 73 | return DevicePropValueAsString(code, v) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /cmd/window.go: -------------------------------------------------------------------------------- 1 | // Taken from github.com/fogleman/imview and slightly customized. 2 | 3 | // +build with_lv 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/go-gl/gl/v2.1/gl" 9 | "github.com/go-gl/glfw/v3.1/glfw" 10 | "image" 11 | _ "image/jpeg" 12 | "runtime" 13 | ) 14 | 15 | func init() { 16 | runtime.LockOSThread() 17 | } 18 | 19 | type window struct { 20 | *glfw.Window 21 | image image.Image 22 | texture *texture 23 | } 24 | 25 | func newWindow(im image.Image, title string) (*window, error) { 26 | const maxSize = 1200 27 | w := im.Bounds().Size().X 28 | h := im.Bounds().Size().Y 29 | a := float64(w) / float64(h) 30 | if a >= 1 { 31 | if w > maxSize { 32 | w = maxSize 33 | h = int(maxSize / a) 34 | } 35 | } else { 36 | if h > maxSize { 37 | h = maxSize 38 | w = int(maxSize * a) 39 | } 40 | } 41 | 42 | glfw.WindowHint(glfw.Resizable, glfw.False) 43 | glfw.WindowHint(glfw.ContextVersionMajor, 2) 44 | glfw.WindowHint(glfw.ContextVersionMinor, 1) 45 | win, err := glfw.CreateWindow(w, h, title, nil, nil) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | win.MakeContextCurrent() 51 | glfw.SwapInterval(1) 52 | 53 | texture := newTexture() 54 | texture.setImage(im) 55 | result := &window{win, im, texture} 56 | result.SetRefreshCallback(result.onRefresh) 57 | 58 | return result, nil 59 | } 60 | 61 | func (window *window) setImage(im image.Image) { 62 | window.image = im 63 | window.texture.setImage(im) 64 | window.draw() 65 | } 66 | 67 | func (window *window) onRefresh(_ *glfw.Window) { 68 | window.draw() 69 | } 70 | 71 | func (window *window) draw() { 72 | window.MakeContextCurrent() 73 | gl.Clear(gl.COLOR_BUFFER_BIT) 74 | window.drawImage() 75 | window.SwapBuffers() 76 | } 77 | 78 | func (window *window) drawImage() { 79 | const padding = 0 80 | iw := window.image.Bounds().Size().X 81 | ih := window.image.Bounds().Size().Y 82 | w, h := window.GetFramebufferSize() 83 | s1 := float32(w) / float32(iw) 84 | s2 := float32(h) / float32(ih) 85 | f := float32(1 - padding) 86 | var x, y float32 87 | if s1 >= s2 { 88 | x = f * s2 / s1 89 | y = f 90 | } else { 91 | x = f 92 | y = f * s1 / s2 93 | } 94 | gl.Enable(gl.TEXTURE_2D) 95 | window.texture.bind() 96 | gl.Begin(gl.QUADS) 97 | gl.TexCoord2f(0, 1) 98 | gl.Vertex2f(-x, -y) 99 | gl.TexCoord2f(1, 1) 100 | gl.Vertex2f(x, -y) 101 | gl.TexCoord2f(1, 0) 102 | gl.Vertex2f(x, y) 103 | gl.TexCoord2f(0, 0) 104 | gl.Vertex2f(-x, y) 105 | gl.End() 106 | } 107 | -------------------------------------------------------------------------------- /cmd/flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "github.com/malc0mn/ptp-ip/ip" 8 | "strconv" 9 | ) 10 | 11 | const ( 12 | defaultIp = "127.0.0.1" 13 | ) 14 | 15 | var ( 16 | valueOutOfRange = errors.New("value out of range") 17 | 18 | cmd string 19 | file string 20 | 21 | interactive bool 22 | server bool 23 | 24 | showHelp bool 25 | showVersion bool 26 | 27 | verbosity ip.LogLevel 28 | ) 29 | 30 | // Custom flag type that will only accept uint16 values, ideal for ports! 31 | type uint16Value uint64 32 | 33 | func (i *uint16Value) Set(s string) error { 34 | v, err := strconv.ParseUint(s, 0, 16) 35 | if err != nil || v == 0 { 36 | err = valueOutOfRange 37 | } 38 | *i = uint16Value(v) 39 | 40 | return err 41 | } 42 | 43 | func (i *uint16Value) String() string { 44 | return strconv.FormatInt(int64(*i), 10) 45 | } 46 | 47 | func initFlags() { 48 | flag.StringVar(&conf.vendor, "t", ip.DefaultVendor, "The vendor of the responder that will be connected to.") 49 | flag.StringVar(&conf.host, "h", ip.DefaultIpAddress, "The responder host to connect to.") 50 | flag.Var(&conf.port, "p", "The responder port to connect to. Use this flag when the responder has only ONE port for all channels!") 51 | flag.Var(&conf.cport, "pc", "The responder port used for the Command/Data connection.") 52 | flag.Var(&conf.eport, "pe", "The responder port used for the Event connection.") 53 | flag.Var(&conf.sport, "ps", "The responder port used for the streamer or 'live view' connection.") 54 | flag.StringVar(&conf.fname, "n", "", "A custom friendly name to use for the initiator.") 55 | flag.StringVar(&conf.guid, "g", "", "A custom GUID to use for the initiator. (default random)") 56 | 57 | flag.BoolVar(&interactive, "i", false, fmt.Sprintf("This will run the %s command with an interactive shell.", exe)) 58 | 59 | flag.StringVar(&cmd, "c", "", "The command to send to the responder.") 60 | flag.StringVar(&file, "f", "", "Read all settings from a config file. The config file will override any command line flags present.") 61 | 62 | flag.BoolVar(&server, "s", false, fmt.Sprintf("This will run the %s command as a server", exe)) 63 | flag.StringVar(&conf.srvAddr, "sa", defaultIp, "To be used in combination with '-s': this defines the server address to listen on.") 64 | flag.Var(&conf.srvPort, "sp", "To be used in combination with '-s': this defines the server port to listen on.") 65 | 66 | flag.BoolVar(&showHelp, "?", false, "Display usage information.") 67 | flag.BoolVar(&showVersion, "version", false, "Display version info.") 68 | 69 | flag.Var(&verbosity, "v", "PTP/IP log level verbosity: ranges from v to vvv.") 70 | 71 | // Set a custom usage function. 72 | flag.Usage = printUsage 73 | 74 | flag.Parse() 75 | } 76 | 77 | // TODO: customise. 78 | func printUsage() { 79 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", exe) 80 | flag.PrintDefaults() 81 | } 82 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/go-ini/ini" 7 | "github.com/malc0mn/ptp-ip/ip" 8 | "log" 9 | "os" 10 | ) 11 | 12 | type config struct { 13 | vendor string 14 | host string 15 | port uint16Value 16 | cport uint16Value 17 | eport uint16Value 18 | sport uint16Value 19 | fname string 20 | guid string 21 | 22 | srvAddr string 23 | srvPort uint16Value 24 | } 25 | 26 | var ( 27 | portSpecAmbiguous = errors.New("ambiguous port specification: use a single port OR define multiple ports") 28 | 29 | conf = &config{ 30 | vendor: ip.DefaultVendor, 31 | host: ip.DefaultIpAddress, 32 | port: uint16Value(ip.DefaultPort), 33 | srvAddr: defaultIp, 34 | srvPort: uint16Value(ip.DefaultPort), 35 | } 36 | ) 37 | 38 | func loadConfig() { 39 | f, err := ini.Load(file) 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "Error opening config file - %s\n", err) 42 | os.Exit(errOpenConfig) 43 | } 44 | 45 | // Initiator 46 | if i, err := f.GetSection("initiator"); err == nil { 47 | if k, err := i.GetKey("friendly_name"); err == nil { 48 | conf.fname = k.String() 49 | } 50 | if k, err := i.GetKey("guid"); err == nil { 51 | conf.guid = k.String() 52 | } 53 | } 54 | 55 | // Responder 56 | if i, err := f.GetSection("responder"); err == nil { 57 | if k, err := i.GetKey("vendor"); err == nil { 58 | conf.vendor = k.String() 59 | } 60 | if k, err := i.GetKey("host"); err == nil { 61 | conf.host = k.String() 62 | } 63 | if k, err := i.GetKey("port"); err == nil { 64 | if err := conf.port.Set(k.String()); err != nil { 65 | log.Fatal(valueOutOfRange) 66 | } 67 | } 68 | if k, err := i.GetKey("cmd_data_port"); err == nil { 69 | if err := conf.cport.Set(k.String()); err != nil { 70 | log.Fatal(valueOutOfRange) 71 | } 72 | } 73 | if k, err := i.GetKey("event_port"); err == nil { 74 | if err := conf.eport.Set(k.String()); err != nil { 75 | log.Fatal(valueOutOfRange) 76 | } 77 | } 78 | if k, err := i.GetKey("stream_port"); err == nil { 79 | if err := conf.sport.Set(k.String()); err != nil { 80 | log.Fatal(valueOutOfRange) 81 | } 82 | } 83 | } 84 | 85 | // Server 86 | if i, err := f.GetSection("server"); err == nil { 87 | if k, err := i.GetKey("enabled"); err == nil { 88 | if v, err := k.Bool(); err == nil { 89 | server = v 90 | } 91 | } 92 | if k, err := i.GetKey("address"); err == nil { 93 | conf.srvAddr = k.String() 94 | } 95 | if k, err := i.GetKey("port"); err == nil { 96 | if err := conf.srvPort.Set(k.String()); err != nil { 97 | log.Fatal(valueOutOfRange) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func checkPorts() { 104 | if conf.cport != 0 && conf.eport != 0 { 105 | conf.port = 0 106 | } 107 | 108 | if conf.port != 0 && (conf.cport != 0 || conf.eport != 0) { 109 | log.Fatal(portSpecAmbiguous) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /fmt/json.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | ) 7 | 8 | type DevicePropDescJSON struct { 9 | *ptp.DevicePropDesc 10 | } 11 | 12 | type ValueLabel struct { 13 | Value string `json:"value"` 14 | Label string `json:"label"` 15 | } 16 | 17 | type CodeLabel struct { 18 | Code string `json:"code"` 19 | Label string `json:"label"` 20 | } 21 | 22 | func (dpdj *DevicePropDescJSON) MarshalJSON() ([]byte, error) { 23 | var form ptp.Form 24 | switch dpdj.FormFlag { 25 | case ptp.DPF_FormFlag_Range: 26 | form = &RangeFormJSON{ 27 | RangeForm: dpdj.Form.(*ptp.RangeForm), 28 | } 29 | case ptp.DPF_FormFlag_Enum: 30 | form = &EnumerationFormJSON{ 31 | EnumerationForm: dpdj.Form.(*ptp.EnumerationForm), 32 | } 33 | } 34 | 35 | return json.Marshal(&struct { 36 | DevicePropertyCode CodeLabel 37 | DataType string `json:"dataType"` 38 | GetSet bool `json:"readOnly"` 39 | FactoryDefaultValue ValueLabel 40 | CurrentValue ValueLabel 41 | FormFlag string `json:"formType"` 42 | Form interface{} `json:"form"` 43 | }{ 44 | DevicePropertyCode: CodeLabel{ 45 | Code: ConvertToHexString(dpdj.DevicePropertyCode), 46 | Label: DevicePropCodeAsString(dpdj.DevicePropertyCode), 47 | }, 48 | DataType: DataTypeCodeAsString(dpdj.DataType), 49 | GetSet: dpdj.GetSet != ptp.DPD_GetSet, 50 | FactoryDefaultValue: ValueLabel{ 51 | Value: ConvertToHexString(dpdj.FactoryDefaultValueAsInt64()), 52 | Label: FujiDevicePropValueAsString(dpdj.DevicePropertyCode, dpdj.FactoryDefaultValueAsInt64()), 53 | }, 54 | CurrentValue: ValueLabel{ 55 | Value: ConvertToHexString(dpdj.CurrentValueAsInt64()), 56 | Label: FujiDevicePropValueAsString(dpdj.DevicePropertyCode, dpdj.CurrentValueAsInt64()), 57 | }, 58 | FormFlag: FormFlagAsString(dpdj.FormFlag), 59 | Form: form, 60 | }) 61 | } 62 | 63 | type RangeFormJSON struct { 64 | *ptp.RangeForm 65 | } 66 | 67 | func (rfj *RangeFormJSON) MarshalJSON() ([]byte, error) { 68 | return json.Marshal(&struct { 69 | MinimumValue string `json:"min"` 70 | MaximumValue string `json:"max"` 71 | StepSize string `json:"step"` 72 | }{ 73 | MinimumValue: ConvertToHexString(rfj.MinimumValueAsInt64()), 74 | MaximumValue: ConvertToHexString(rfj.MaximumValueAsInt64()), 75 | StepSize: ConvertToHexString(rfj.StepSizeAsInt64()), 76 | }) 77 | } 78 | 79 | type EnumerationFormJSON struct { 80 | *ptp.EnumerationForm 81 | } 82 | 83 | func (ef *EnumerationFormJSON) MarshalJSON() ([]byte, error) { 84 | values := ef.SupportedValuesAsInt64Array() 85 | hex := make([]ValueLabel, len(values)) 86 | for i := 0; i < len(values); i++ { 87 | hex[i] = ValueLabel{ 88 | Value: ConvertToHexString(values[i]), 89 | Label: FujiDevicePropValueAsString(ef.DevicePropDesc.DevicePropertyCode, values[i]), 90 | } 91 | } 92 | 93 | return json.Marshal(&struct { 94 | SupportedValues []ValueLabel `json:"values"` 95 | }{ 96 | SupportedValues: hex, 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/malc0mn/ptp-ip/ip" 7 | "os" 8 | "os/signal" 9 | "path/filepath" 10 | "syscall" 11 | ) 12 | 13 | const ( 14 | ok = 0 15 | errGeneral = 1 16 | errInvalidArgs = 2 17 | errOpenConfig = 102 18 | errCreateClient = 104 19 | errResponderConnect = 105 20 | ) 21 | 22 | var ( 23 | version = "0.0.0" 24 | buildTime = "unknown" 25 | exe string 26 | quit = make(chan struct{}) // Should this be global or do we need to pass it along to all who need it? 27 | ) 28 | 29 | func main() { 30 | exe = filepath.Base(os.Args[0]) 31 | 32 | initFlags() 33 | 34 | if noArgs := len(os.Args) < 2; noArgs || showHelp { 35 | printUsage() 36 | exit := ok 37 | if noArgs { 38 | exit = errGeneral 39 | } 40 | os.Exit(exit) 41 | } 42 | 43 | if showVersion { 44 | fmt.Printf("%s version %s built on %s\n", exe, version, buildTime) 45 | os.Exit(ok) 46 | } 47 | 48 | if file != "" { 49 | loadConfig() 50 | } 51 | 52 | checkPorts() 53 | 54 | if cmd != "" && (interactive || server) || (interactive && server) { 55 | fmt.Fprintln(os.Stderr, "Too many arguments: either run in server mode OR interactive mode OR execute a single command; not all at once!") 56 | os.Exit(errInvalidArgs) 57 | } 58 | 59 | // TODO: finish this implementation so CTRL+C will also abort client.Dial() etc. properly. 60 | sigs := make(chan os.Signal, 1) 61 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 62 | go func() { 63 | sig := <-sigs 64 | fmt.Printf("Received signal %s, shutting down...\n", sig) 65 | close(quit) 66 | }() 67 | 68 | client, err := ip.NewClient(conf.vendor, conf.host, uint16(conf.port), conf.fname, conf.guid, verbosity) 69 | if err != nil { 70 | fmt.Fprintf(os.Stderr, "Error creating PTP/IP client - %s\n", err) 71 | os.Exit(errCreateClient) 72 | } 73 | defer client.Close() 74 | 75 | if conf.cport != 0 { 76 | client.SetCommandDataPort(uint16(conf.cport)) 77 | } 78 | if conf.eport != 0 { 79 | client.SetEventPort(uint16(conf.eport)) 80 | } 81 | if conf.sport != 0 { 82 | client.SetStreamerPort(uint16(conf.sport)) 83 | } 84 | 85 | fmt.Printf("Created new client with name '%s' and GUID '%s'.\n", client.InitiatorFriendlyName(), client.InitiatorGUIDAsString()) 86 | fmt.Printf("Attempting to connect to %s\n", client.CommandDataAddress()) 87 | err = client.Dial() 88 | if err != nil { 89 | fmt.Fprintf(os.Stderr, "Error connecting to responder - %s\n", err) 90 | os.Exit(errResponderConnect) 91 | } 92 | 93 | if cmd != "" { 94 | executeCommand(cmd, bufio.NewWriter(os.Stdout), client, "cli") 95 | } 96 | 97 | if server || interactive { 98 | if interactive { 99 | go iShell(client) 100 | } 101 | 102 | if server { 103 | go launchServer(client) 104 | } 105 | 106 | mainThread() 107 | 108 | <-quit 109 | fmt.Println("Bye bye!") 110 | } 111 | 112 | os.Exit(ok) 113 | } 114 | -------------------------------------------------------------------------------- /cmd/command_capture.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | func init() { 14 | registerCommand(&capture{}) 15 | } 16 | 17 | type capture struct{} 18 | 19 | func (capture) name() string { 20 | return "capture" 21 | } 22 | 23 | func (capture) alias() []string { 24 | return []string{"shoot", "shutter", "snap"} 25 | } 26 | 27 | func (cap capture) execute(c *ip.Client, f []string, asyncOut chan<- string) string { 28 | amount := 1 29 | if len(f) >= 1 { 30 | if val, err := strconv.Atoi(f[0]); err == nil { 31 | amount = val 32 | f = f[1:] // drop processed amount argument 33 | } 34 | } 35 | 36 | var ( 37 | imgs chan []byte 38 | wg sync.WaitGroup 39 | ) 40 | if len(f) >= 1 { 41 | imgs = make(chan []byte, 10) 42 | var path string 43 | if !cap.isView(f[0]) { 44 | ext := filepath.Ext(f[0]) 45 | path = strings.TrimSuffix(f[0], ext) + "-%d" + ext 46 | } 47 | 48 | wg.Add(1) 49 | go func() { 50 | i := 1 51 | for img := range imgs { 52 | if path != "" { 53 | file := fmt.Sprintf(path, i) 54 | if err := ioutil.WriteFile(file, img, 0644); err != nil { 55 | asyncOut <- err.Error() 56 | continue 57 | } 58 | asyncOut <- fmt.Sprintf("Image preview saved to %s", file) 59 | i++ 60 | } else { 61 | asyncOut <- preview(img) 62 | } 63 | } 64 | wg.Done() 65 | }() 66 | } 67 | 68 | if amount > 1 { 69 | asyncOut <- fmt.Sprintf("Capturing %d images...", amount) 70 | } 71 | for i := 0; i < amount; i++ { 72 | if amount > 1 { 73 | asyncOut <- fmt.Sprintf(" capturing image %d", i+1) 74 | } 75 | var err error 76 | img, err := c.InitiateCapture() 77 | if err != nil { 78 | return err.Error() 79 | } 80 | if imgs != nil { 81 | imgs <- img 82 | } 83 | } 84 | if imgs != nil { 85 | close(imgs) 86 | wg.Wait() 87 | return "" 88 | } 89 | 90 | plural := "" 91 | if amount > 1 { 92 | plural = "s" 93 | } 94 | 95 | return fmt.Sprintf("Image%s captured, check the camera\n", plural) 96 | } 97 | 98 | func (cap capture) help() string { 99 | help := `"` + cap.name() + `" will make the responder capture a single image.` + "\n" 100 | help += helpAddAliases(cap.alias()) 101 | 102 | if args := cap.arguments(); len(args) > 0 { 103 | help += helpAddArgumentsTitle() 104 | for i, arg := range args { 105 | switch i { 106 | case 0: 107 | help += "\t- " + arg + ": an integer value to indicate the amount of captures to make\n" 108 | case 1: 109 | help += "\t- " + `"` + arg + `" opens a window to display the capture preview if the camera returns it` + "\n\tOR\n" 110 | case 2: 111 | help += "\t- a " + arg + " to save the capture preview to\n" 112 | } 113 | } 114 | } 115 | 116 | return help 117 | } 118 | 119 | func (capture) arguments() []string { 120 | return []string{"amount", "view", "filepath"} 121 | } 122 | 123 | func (cap capture) isView(param string) bool { 124 | return param == cap.arguments()[1] 125 | } 126 | -------------------------------------------------------------------------------- /ptp/device_test.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | import ( 4 | "encoding/binary" 5 | "testing" 6 | ) 7 | 8 | func TestDevicePropDesc_SizeOfValueInBytes(t *testing.T) { 9 | check := map[DataTypeCode]int{ 10 | DTC_INT8: 1, 11 | DTC_UINT8: 1, 12 | DTC_INT16: 2, 13 | DTC_UINT16: 2, 14 | DTC_INT32: 4, 15 | DTC_UINT32: 4, 16 | DTC_INT64: 8, 17 | DTC_UINT64: 8, 18 | } 19 | 20 | for code, want := range check { 21 | dpd := DevicePropDesc{ 22 | DataType: code, 23 | } 24 | got := dpd.SizeOfValueInBytes() 25 | if got != want { 26 | t.Errorf("SizeOfValueInBytes() return = %d, want %d", got, want) 27 | } 28 | } 29 | } 30 | 31 | func TestDevicePropDesc_FactoryDefaultValueAsInt64(t *testing.T) { 32 | b := make([]byte, 2) 33 | binary.LittleEndian.PutUint16(b, 0x0140) 34 | 35 | dpd := DevicePropDesc{ 36 | FactoryDefaultValue: b, 37 | } 38 | 39 | got := dpd.FactoryDefaultValueAsInt64() 40 | want := int64(0x0140) 41 | if got != want { 42 | t.Errorf("FactoryDefaultValueAsInt64() return = %d, want %d", got, want) 43 | } 44 | } 45 | 46 | func TestDevicePropDesc_CurrentValueAsInt64(t *testing.T) { 47 | b := make([]byte, 2) 48 | binary.LittleEndian.PutUint16(b, 0x0340) 49 | 50 | dpd := DevicePropDesc{ 51 | CurrentValue: b, 52 | } 53 | 54 | got := dpd.CurrentValueAsInt64() 55 | want := int64(0x0340) 56 | if got != want { 57 | t.Errorf("CurrentValueAsInt64() return = %d, want %d", got, want) 58 | } 59 | } 60 | 61 | func TestRangeForm_MinimumValueAsInt64(t *testing.T) { 62 | b := make([]byte, 2) 63 | binary.LittleEndian.PutUint16(b, 0x0040) 64 | 65 | rf := RangeForm{ 66 | MinimumValue: b, 67 | } 68 | 69 | got := rf.MinimumValueAsInt64() 70 | want := int64(0x0040) 71 | if got != want { 72 | t.Errorf("MinimumValueAsInt64() return = %d, want %d", got, want) 73 | } 74 | } 75 | 76 | func TestRangeForm_MaximumValueAsInt64(t *testing.T) { 77 | b := make([]byte, 2) 78 | binary.LittleEndian.PutUint16(b, 0x9040) 79 | 80 | rf := RangeForm{ 81 | MaximumValue: b, 82 | } 83 | 84 | got := rf.MaximumValueAsInt64() 85 | want := int64(0x9040) 86 | if got != want { 87 | t.Errorf("MaximumValueAsInt64() return = %d, want %d", got, want) 88 | } 89 | } 90 | 91 | func TestRangeForm_StepSizeAsInt64(t *testing.T) { 92 | b := make([]byte, 2) 93 | binary.LittleEndian.PutUint16(b, 0x0010) 94 | 95 | rf := RangeForm{ 96 | StepSize: b, 97 | } 98 | 99 | got := rf.StepSizeAsInt64() 100 | want := int64(0x0010) 101 | if got != want { 102 | t.Errorf("StepSizeAsInt64() return = %d, want %d", got, want) 103 | } 104 | } 105 | 106 | func TestEnumerationForm_SupportedValuesAsInt64Array(t *testing.T) { 107 | total := 5 108 | b := make([][]byte, total) 109 | for i := 0; i < total; i++ { 110 | b[i] = make([]byte, 2) 111 | binary.LittleEndian.PutUint16(b[i], uint16(0x0010*i)) 112 | } 113 | 114 | rf := EnumerationForm{ 115 | NumberOfValues: total, 116 | SupportedValues: b, 117 | } 118 | 119 | got := rf.SupportedValuesAsInt64Array() 120 | want := []int64{0x0000, 0x0010, 0x0020, 0x0030, 0x0040} 121 | for i := 0; i < total; i++ { 122 | if got[i] != want[i] { 123 | t.Errorf("StepSizeAsInt64() %d = %d, want %d", i, got[i], want[i]) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cmd/commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | ptpfmt "github.com/malc0mn/ptp-ip/fmt" 6 | "github.com/malc0mn/ptp-ip/ip" 7 | "log" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | commandsMu sync.RWMutex 14 | commands = make(map[string]command) 15 | aliases = make(map[string]string) 16 | ) 17 | 18 | type command interface { 19 | name() string 20 | alias() []string 21 | // TODO: is there a more elegant solution to drop at least the async output channel argument here...? 22 | execute(*ip.Client, []string, chan<- string) string 23 | help() string 24 | arguments() []string 25 | } 26 | 27 | func registerCommand(cmd command) { 28 | commandsMu.Lock() 29 | defer commandsMu.Unlock() 30 | if cmd == nil { 31 | panic("cmd: registerCommand command is nil") 32 | } 33 | 34 | name := cmd.name() 35 | if _, dup := commands[name]; dup { 36 | panic("cmd: registerCommand called twice for command " + name) 37 | } 38 | commands[name] = cmd 39 | 40 | for _, alias := range cmd.alias() { 41 | if _, dup := aliases[alias]; dup { 42 | panic("cmd: registerCommand double alias " + alias) 43 | } 44 | aliases[alias] = name 45 | } 46 | } 47 | 48 | func helpAddAliases(aliases []string) string { 49 | var help string 50 | 51 | if len(aliases) > 0 { 52 | help += "\n\t" + `Possible aliases: "` + strings.Join(aliases, `", "`) + `"` + "\n" 53 | } 54 | 55 | return help 56 | } 57 | 58 | func helpAddArgumentsTitle() string { 59 | return "\tAllowed arguments:\n" 60 | } 61 | 62 | func helpAddUnifiedFieldNames() string { 63 | return "\t" + ` "` + strings.Join(ptpfmt.UnifiedFieldNames, `", "`) + `"` + "\n" 64 | } 65 | 66 | func readAndExecuteCommand(rw *bufio.ReadWriter, c *ip.Client, lmp string) { 67 | msg, err := rw.ReadString('\n') 68 | if err != nil { 69 | log.Printf("%s error reading message '%s'", lmp, err) 70 | return 71 | } 72 | msg = strings.TrimSuffix(msg, "\n") 73 | if msg == "" { 74 | log.Printf("%s ignoring empty message!", lmp) 75 | return 76 | } 77 | log.Printf("%s message received: '%s'", lmp, msg) 78 | 79 | executeCommand(msg, rw.Writer, c, lmp) 80 | } 81 | 82 | func executeCommand(msg string, w *bufio.Writer, c *ip.Client, lmp string) { 83 | var wg sync.WaitGroup 84 | f := strings.Fields(msg) 85 | asyncOut := make(chan string) 86 | 87 | // Launch async output routine. 88 | wg.Add(1) 89 | go func() { 90 | for msg := range asyncOut { 91 | if _, err := w.Write([]byte(msg + "\n")); err != nil { 92 | log.Printf("%s error writing response: '%s'", lmp, err) 93 | continue 94 | } 95 | if err := w.Flush(); err != nil { 96 | log.Printf("%s error flushing buffer: '%s'", lmp, err) 97 | } 98 | } 99 | wg.Done() 100 | }() 101 | 102 | _, err := w.Write([]byte(commandByName(f[0]).execute(c, f[1:], asyncOut))) 103 | close(asyncOut) 104 | wg.Wait() 105 | if err != nil { 106 | log.Printf("%s error writing response: '%s'", lmp, err) 107 | return 108 | } 109 | err = w.Flush() 110 | if err != nil { 111 | log.Printf("%s error flushing buffer: '%s'", lmp, err) 112 | } 113 | } 114 | 115 | func commandByName(n string) command { 116 | if name, exists := aliases[n]; exists { 117 | n = name 118 | } 119 | 120 | if cmd, exists := commands[n]; exists { 121 | return cmd 122 | } 123 | 124 | return &unknown{} 125 | } 126 | -------------------------------------------------------------------------------- /fmt/string_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | "testing" 7 | ) 8 | 9 | func TestConvertToHexString(t *testing.T) { 10 | want := "0x501f" 11 | got := ConvertToHexString(ptp.DPC_CopyrightInfo) 12 | 13 | if got != want { 14 | t.Errorf("HexStringToUint64() got = %s; want %s", got, want) 15 | } 16 | } 17 | 18 | func TestHexStringToUint64(t *testing.T) { 19 | got, err := HexStringToUint64("0x10G4", 13) 20 | wantE := "error converting: strconv.ParseUint: parsing \"10G4\": invalid syntax" 21 | if err.Error() != wantE { 22 | t.Errorf("HexStringToUint64() error = %s; want %s", err, wantE) 23 | } 24 | 25 | got, err = HexStringToUint64("0x1004", 12) 26 | wantE = "error converting: strconv.ParseUint: parsing \"1004\": value out of range" 27 | if err.Error() != wantE { 28 | t.Errorf("HexStringToUint64() error = %s; want %s", err, wantE) 29 | } 30 | 31 | got, err = HexStringToUint64("0x1004", 13) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | wantI := uint64(4100) 37 | if got != wantI { 38 | t.Errorf("HexStringToUint64() return = %d; want %d", got, wantI) 39 | } 40 | 41 | got, err = HexStringToUint64("1004", 13) 42 | if err != nil { 43 | t.Errorf("HexStringToUint64() error = %s; want ", err) 44 | } 45 | 46 | wantI = uint64(4100) 47 | if got != wantI { 48 | t.Errorf("HexStringToUint64() return = %d; want %d", got, wantI) 49 | } 50 | } 51 | 52 | func TestDevicePropCodeAsString(t *testing.T) { 53 | want := "ISO" 54 | got := DevicePropCodeAsString(ip.DPC_Fuji_ExposureIndex) 55 | if got != want { 56 | t.Errorf("DevicePropCodeAsString() got = %s; want %s", got, want) 57 | } 58 | 59 | got = DevicePropCodeAsString(ptp.DPC_ExposureIndex) 60 | if got != want { 61 | t.Errorf("DevicePropCodeAsString() got = %s; want %s", got, want) 62 | } 63 | } 64 | 65 | func TestPropNameToDevicePropCode(t *testing.T) { 66 | want := ip.DPC_Fuji_ExposureIndex 67 | got, err := PropNameToDevicePropCode(ptp.VE_FujiPhotoFilmCoLtd, "iso") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if got != want { 72 | t.Errorf("PropNameToDevicePropCode() got = %#x; want %#x", got, want) 73 | } 74 | 75 | want = ptp.DPC_ExposureIndex 76 | got, err = PropNameToDevicePropCode(ptp.VendorExtension(0), "iso") 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | if got != want { 81 | t.Errorf("PropNameToDevicePropCode() got = %#x; want %#x", got, want) 82 | } 83 | 84 | wantE := "unknown field name 'testing123'" 85 | got, err = PropNameToDevicePropCode(ptp.VendorExtension(0), "testing123") 86 | if err.Error() != wantE { 87 | t.Errorf("PropNameToDevicePropCode() err = %s; want %s", err, wantE) 88 | } 89 | if got != 0 { 90 | t.Errorf("PropNameToDevicePropCode() got = %#x; want %#x", got, want) 91 | } 92 | } 93 | 94 | func TestDevicePropValAsString(t *testing.T) { 95 | want := "PRO Neg. Hi" 96 | got := DevicePropValAsString(ptp.VE_FujiPhotoFilmCoLtd, ip.DPC_Fuji_FilmSimulation, 6) 97 | 98 | if got != want { 99 | t.Errorf("DevicePropValAsString() got = %s; want %s", got, want) 100 | } 101 | 102 | want = "center spot" 103 | got = DevicePropValAsString(ptp.VendorExtension(0), ptp.DPC_FocusMeteringMode, 1) 104 | 105 | if got != want { 106 | t.Errorf("DevicePropValAsString() got = %s; want %s", got, want) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ptp/modes.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | type EffectMode uint16 4 | type ExposureMeteringMode uint16 5 | type ExposureProgramMode uint16 6 | type FlashMode uint16 7 | type FocusMeteringMode uint16 8 | type FocusMode uint16 9 | type FunctionalMode uint16 10 | type SelfTestType uint16 11 | type StillCaptureMode uint16 12 | type WhiteBalance uint16 13 | 14 | const ( 15 | EMM_Undefined ExposureMeteringMode = 0x0000 16 | EMM_Avarage ExposureMeteringMode = 0x0001 17 | EMM_CenterWeightedAvarage ExposureMeteringMode = 0x0002 18 | EMM_MultiSpot ExposureMeteringMode = 0x0003 19 | EMM_CenterSpot ExposureMeteringMode = 0x0004 20 | 21 | EPM_Undefined ExposureProgramMode = 0x0000 22 | EPM_Manual ExposureProgramMode = 0x0001 23 | EPM_Automatic ExposureProgramMode = 0x0002 24 | EPM_AperturePriority ExposureProgramMode = 0x0003 25 | EPM_ShutterPriority ExposureProgramMode = 0x0004 26 | EPM_ProgramCreative ExposureProgramMode = 0x0005 27 | EPM_ProgramAction ExposureProgramMode = 0x0006 28 | EPM_Portrait ExposureProgramMode = 0x0007 29 | 30 | FCM_Undefined FocusMode = 0x0000 31 | FCM_Manual FocusMode = 0x0001 32 | FCM_Automatic FocusMode = 0x0002 33 | FCM_AutomaticMacro FocusMode = 0x0003 34 | 35 | FLM_Undefined FlashMode = 0x0000 36 | FLM_AutoFlash FlashMode = 0x0001 37 | FLM_FlashOff FlashMode = 0x0002 38 | FLM_FillFlash FlashMode = 0x0003 39 | FLM_RedEyeAuto FlashMode = 0x0004 40 | FLM_RedEyeFill FlashMode = 0x0005 41 | FLM_ExternalSync FlashMode = 0x0006 42 | 43 | FMM_Undefined FocusMeteringMode = 0x0000 44 | FMM_CenterSpot FocusMeteringMode = 0x0001 45 | FMM_MultiSpot FocusMeteringMode = 0x0002 46 | 47 | FUM_StandardMode FunctionalMode = 0x0000 48 | FUM_SleepState FunctionalMode = 0x0001 49 | 50 | FXM_Undefined EffectMode = 0x0000 51 | FXM_Standard EffectMode = 0x0001 52 | FXM_BlackWhite EffectMode = 0x0002 53 | FXM_Sepia EffectMode = 0x0003 54 | 55 | SCM_Undefined StillCaptureMode = 0x0000 56 | SCM_Normal StillCaptureMode = 0x0001 57 | SCM_Burst StillCaptureMode = 0x0002 58 | SCM_Timelapse StillCaptureMode = 0x0003 59 | 60 | // STT_Default is the default device-specific self-test. 61 | STT_Default SelfTestType = 0x0000 62 | 63 | WB_Undefined WhiteBalance = 0x0000 64 | // WB_Manual indicates the white balance is set directly using the RGB Gain property and is static until changed. 65 | WB_Manual WhiteBalance = 0x0001 66 | // WB_Automatic indicates the device attempts to set the white balance using some kind of automatic mechanism. 67 | WB_Automatic WhiteBalance = 0x0002 68 | // WB_OnePushAutomatic indicates the user must press the capture button while pointing the device at a white field, 69 | // at which time the device determines the white balance setting. 70 | WB_OnePushAutomatic WhiteBalance = 0x0003 71 | // WB_Daylight indicates the device attempts to set the white balance to a value that is appropriate for use in 72 | // daylight conditions. 73 | WB_Daylight WhiteBalance = 0x0004 74 | // WB_Fluorescent indicates the device attempts to set the white balance to a value that is appropriate for use in 75 | // with fluorescent lighting conditions. 76 | WB_Fluorescent WhiteBalance = 0x0005 77 | // WB_Tungsten indicates the device attempts to set the white balance to a value that is appropriate for use in 78 | // conditions with a tungsten light source. 79 | WB_Tungsten WhiteBalance = 0x0006 80 | // WB_Flash indicates the device attempts to set the white balance to a value that is appropriate for flash 81 | // conditions. 82 | WB_Flash WhiteBalance = 0x0007 83 | ) 84 | -------------------------------------------------------------------------------- /ptp/storage.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | type StorageType uint16 4 | type FilesystemType uint16 5 | type AccessCapability uint16 6 | type ProtectionStatus uint16 7 | type StorageID uint32 8 | 9 | const ( 10 | ST_Undefined StorageType = 0x0000 11 | ST_FixedROM StorageType = 0x0001 12 | ST_RemovableROM StorageType = 0x0002 13 | ST_FixedRAM StorageType = 0x0003 14 | ST_RemovableRAM StorageType = 0x0004 15 | 16 | FT_Undefined FilesystemType = 0x0000 17 | FT_GenericFlat FilesystemType = 0x0001 18 | FT_GenericHierarchical FilesystemType = 0x0002 19 | FT_DCF FilesystemType = 0x0003 20 | 21 | AC_ReadWrite AccessCapability = 0x0000 22 | AC_ReadOnly_NoDeletion AccessCapability = 0x0001 23 | AC_ReadOnly_Deletion AccessCapability = 0x0002 24 | 25 | PS_NoProtection ProtectionStatus = 0x0000 26 | PS_ReadOnly ProtectionStatus = 0x0001 27 | ) 28 | 29 | // This dataset is used to hold the state information for a storage device. 30 | type StorageInfo struct { 31 | // The code that identifies the type of storage, particularly whether the store is inherently random-access or 32 | // read-only memory, and whether it is fixed or removable media. 33 | StorageType StorageType 34 | 35 | // This optional code indicates the type of filesystem present on the device. This field may be used to determine 36 | // the filenaming convention used by the storage device, as well as to determine whether support for a hierarchical 37 | // system is present. If the storage device is DCF-conformant, it shall indicate so here. 38 | // All values having bit 31 set to zero are reserved for future use. If a proprietary implementation wishes to 39 | // extend the interpretation of this field, bit 31 should be set to 1. 40 | FilesystemType FilesystemType 41 | 42 | // This field indicates whether the store is read-write or read-only. If the store is read-only, deletion may or may 43 | // not be allowed. The allowed values are described in the following table. Read-Write is only valid if the 44 | // StorageType is nonROM, as described in the StorageType field above. 45 | // All values having bit 15 set to zero are reserved for future use. If a proprietary implementation wishes to 46 | // extend the interpretation of this field, bit 15 should be set to 1. 47 | AccessCapability AccessCapability 48 | 49 | // This is an optional field that indicates the total storage capacity of the store in bytes. If this field is 50 | // unused, it should report 0xFFFFFFFF. 51 | MaxCapacity uint64 52 | 53 | // The amount of free space that is available in the store in bytes. If this value is not useful for the device, it 54 | // may set this field to 0xFFFFFFFF and rely upon the FreeSpaceInImages field instead. 55 | FreeSpaceInBytes uint64 56 | 57 | // The number of images that may still be captured into this store according to the current image capture settings 58 | // of the device. If the device does not implement this capability, this field should be set to 0xFFFFFFFF. This 59 | // field may be used for devices that do not report FreeSpaceInBytes, or the two fields may be used in combination. 60 | FreeSpaceInImages uint32 61 | 62 | // An optional field that may be used for a human-readable text description of the storage device. This should be 63 | // used for storage-type specific information as opposed to volume-specific information. Examples would be "Type I 64 | // Compact Flash" or "3.5-inch 1.44 MB Floppy". If unused, this field should be set to the empty string. 65 | StorageDescription string 66 | 67 | // An optional field that may be used to hold the volume label of the storage device, if such a label exists and is 68 | // known. If unused, this field should be set to the empty string. 69 | VolumeLabel string 70 | } 71 | -------------------------------------------------------------------------------- /viewfinder/viewfinder.go: -------------------------------------------------------------------------------- 1 | package viewfinder 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ptp" 5 | "golang.org/x/image/font" 6 | "golang.org/x/image/font/basicfont" 7 | "golang.org/x/image/math/fixed" 8 | "image" 9 | "image/color" 10 | ) 11 | 12 | // WidgetDrawer defines the signature of the drawer function of a widget. 13 | type WidgetDrawer func(*Widget, int64) 14 | 15 | // Viewfinder holds a list of pointers to Widgets mapped to their ptp.DevicePropCode. 16 | type Viewfinder struct { 17 | Widgets map[ptp.DevicePropCode]*Widget 18 | } 19 | 20 | // DrawWidget draws the widget mapped to the given device property code on the given image with the given value. 21 | func (vf *Viewfinder) DrawWidget(img *image.RGBA, code ptp.DevicePropCode, val int64) { 22 | if w, ok := vf.Widgets[code]; ok { 23 | w.Dst = img 24 | w.Draw(w, val) 25 | } 26 | } 27 | 28 | // NewViewfinder creates a vendor specific viewfinder using the image passed in to allow each Widget to calibrate its 29 | // starting position. 30 | // When the vendor has no viewfinder defined, nothing will happen. 31 | func NewViewfinder(img *image.RGBA, v ptp.VendorExtension) *Viewfinder { 32 | switch v { 33 | case ptp.VE_FujiPhotoFilmCoLtd: 34 | return NewFujiXT1Viewfinder(img) 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // DrawViewfinder draws all the viewfinder widgets when present in the given ptp.DevicePropDesc list. 41 | func DrawViewfinder(vf *Viewfinder, img *image.RGBA, s []*ptp.DevicePropDesc) { 42 | for _, p := range s { 43 | vf.DrawWidget(img, p.DevicePropertyCode, p.CurrentValueAsInt64()) 44 | } 45 | } 46 | 47 | // Widget defines a viewfinder widget. 48 | type Widget struct { 49 | *font.Drawer 50 | origin fixed.Point26_6 51 | face font.Face 52 | colour *image.Uniform 53 | Draw WidgetDrawer 54 | } 55 | 56 | // SetColour sets the font colour to the given red, green and blue values. 57 | func (w *Widget) SetColour(r, g, b uint8) { 58 | w.Src = image.NewUniform(color.RGBA{R: r, G: g, B: b, A: 255}) 59 | } 60 | 61 | // ResetColour resets the colour to the original one when the widget was first made. 62 | func (w *Widget) ResetColour() { 63 | w.Src = w.colour 64 | } 65 | 66 | // ResetFace resets the font face to the original one when the widget was first made. 67 | func (w *Widget) ResetFace() { 68 | w.Face = w.face 69 | } 70 | 71 | // ResetToOrigin resets the start drawing position to the original point calculated when the widget was first made. 72 | func (w *Widget) ResetToOrigin() { 73 | w.Dot = w.origin 74 | } 75 | 76 | // NewWidget needs a colour to draw in and x/y coordinates to start drawing from. 77 | // Important: the destination image is NOT set but can be set later using Widget.SetImage()! 78 | func NewWidget(img *image.RGBA, r, g, b uint8, f *basicfont.Face, x, y int) *Widget { 79 | point := fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)} 80 | col := image.NewUniform(color.RGBA{R: r, G: g, B: b, A: 255}) 81 | 82 | return &Widget{ 83 | Drawer: &font.Drawer{ 84 | Dst: img, 85 | Src: col, 86 | Face: f, 87 | Dot: point, 88 | }, 89 | origin: point, 90 | face: f, 91 | colour: col, 92 | } 93 | } 94 | 95 | // NewFontWidget returns a new Widget using basicfont.Face7x13 for its basicfont.Face. 96 | func NewFontWidget(img *image.RGBA, r, g, b uint8, x, y int) *Widget { 97 | return NewWidget(img, r, g, b, basicfont.Face7x13, x, y) 98 | } 99 | 100 | // NewWhiteFontWidget returns a new Widget using basicfont.Face7x13 for its basicfont.Face and white (255, 255, 255) for 101 | // its drawing colour. 102 | func NewWhiteFontWidget(img *image.RGBA, x, y int) *Widget { 103 | return NewFontWidget(img, 255, 255, 255, x, y) 104 | } 105 | 106 | // NewGlyphWidget returns a new Widget using VFGlyphs6x13 for its basicfont.Face. 107 | func NewGlyphWidget(img *image.RGBA, r, g, b uint8, x, y int) *Widget { 108 | return NewWidget(img, r, g, b, VFGlyphs6x13, x, y) 109 | } 110 | 111 | // NewWhiteGlyphWidget returns a new Widget using VFGlyphs6x13 for its basicfont.Face and white (255, 255, 255) for 112 | // its drawing colour. 113 | func NewWhiteGlyphWidget(img *image.RGBA, x, y int) *Widget { 114 | return NewGlyphWidget(img, 255, 255, 255, x, y) 115 | } 116 | -------------------------------------------------------------------------------- /ip/log.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | ) 8 | 9 | const ( 10 | // LevelSilent disables all log messages. 11 | LevelSilent LogLevel = 0 12 | // LevelVerbose will output warning and error messages. 13 | LevelVerbose LogLevel = 1 14 | // LevelVeryVerbose will output info, warning and error messages. 15 | LevelVeryVerbose LogLevel = 2 16 | // LevelVeryVerbose will output debug, info, warning and error messages. 17 | LevelDebug LogLevel = 3 18 | ) 19 | 20 | type LogLevel byte 21 | 22 | // Set() implements flags.Value interface. 23 | func (l *LogLevel) Set(s string) error { 24 | *l = LevelSilent 25 | switch s { 26 | case "v": 27 | *l = LevelVerbose 28 | case "vv": 29 | *l = LevelVeryVerbose 30 | case "vvv": 31 | *l = LevelDebug 32 | default: 33 | return errors.New("unknown log level") 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // String() implement flags.Value interface. 40 | func (l *LogLevel) String() string { 41 | switch *l { 42 | case LevelVerbose: 43 | return "v" 44 | case LevelVeryVerbose: 45 | return "vv" 46 | case LevelDebug: 47 | return "vvv" 48 | } 49 | 50 | return "" 51 | } 52 | 53 | // Logger is the interface allowing you to create a custom logger. 54 | type Logger interface { 55 | Debug(v ...interface{}) 56 | Debugf(format string, v ...interface{}) 57 | Debugln(v ...interface{}) 58 | Error(v ...interface{}) 59 | Errorf(format string, v ...interface{}) 60 | Errorln(v ...interface{}) 61 | Fatal(v ...interface{}) 62 | Fatalf(format string, v ...interface{}) 63 | Fatalln(v ...interface{}) 64 | Info(v ...interface{}) 65 | Infof(format string, v ...interface{}) 66 | Infoln(v ...interface{}) 67 | Warn(v ...interface{}) 68 | Warnf(format string, v ...interface{}) 69 | Warnln(v ...interface{}) 70 | } 71 | 72 | // StdLogger is the standard logger, a wrapper around the golang log package. 73 | type StdLogger struct { 74 | level LogLevel 75 | *log.Logger 76 | } 77 | 78 | func (sl *StdLogger) Debug(v ...interface{}) { 79 | if sl.level >= LevelDebug { 80 | log.Print(v...) 81 | } 82 | } 83 | 84 | func (sl *StdLogger) Debugf(format string, v ...interface{}) { 85 | if sl.level >= LevelDebug { 86 | log.Printf(format, v...) 87 | } 88 | } 89 | 90 | func (sl *StdLogger) Debugln(v ...interface{}) { 91 | if sl.level >= LevelDebug { 92 | log.Println(v...) 93 | } 94 | } 95 | 96 | func (sl *StdLogger) Error(v ...interface{}) { 97 | if sl.level > LevelSilent { 98 | log.Print(v...) 99 | } 100 | } 101 | 102 | func (sl *StdLogger) Errorf(format string, v ...interface{}) { 103 | if sl.level > LevelSilent { 104 | log.Printf(format, v...) 105 | } 106 | } 107 | 108 | func (sl *StdLogger) Errorln(v ...interface{}) { 109 | if sl.level > LevelSilent { 110 | log.Println(v...) 111 | } 112 | } 113 | 114 | func (sl *StdLogger) Info(v ...interface{}) { 115 | if sl.level >= LevelVeryVerbose { 116 | log.Print(v...) 117 | } 118 | } 119 | 120 | func (sl *StdLogger) Infof(format string, v ...interface{}) { 121 | if sl.level >= LevelVeryVerbose { 122 | log.Printf(format, v...) 123 | } 124 | } 125 | 126 | func (sl *StdLogger) Infoln(v ...interface{}) { 127 | if sl.level >= LevelVeryVerbose { 128 | log.Println(v...) 129 | } 130 | } 131 | 132 | func (sl *StdLogger) Warn(v ...interface{}) { 133 | if sl.level >= LevelVerbose { 134 | log.Print(v...) 135 | } 136 | } 137 | 138 | func (sl *StdLogger) Warnf(format string, v ...interface{}) { 139 | if sl.level >= LevelVerbose { 140 | log.Printf(format, v...) 141 | } 142 | } 143 | 144 | func (sl *StdLogger) Warnln(v ...interface{}) { 145 | if sl.level >= LevelVerbose { 146 | log.Println(v...) 147 | } 148 | } 149 | 150 | // NewLogger creates a new StdLogger. The out variable sets the destination to which log data will be written. 151 | // The level determines the type of log messages being output. 152 | // The prefix appears at the beginning of each generated log line, or after the log header if the log.Lmsgprefix flag is 153 | // provided. 154 | // The flag argument defines the logging properties. 155 | func NewLogger(level LogLevel, out io.Writer, prefix string, flag int) Logger { 156 | return &StdLogger{ 157 | level: level, 158 | Logger: log.New(out, prefix, flag), 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /fmt/testdata/plain.json: -------------------------------------------------------------------------------- 1 | [{"DevicePropertyCode":{"code":"0x5012","label":"capture delay"},"dataType":"uint16","readOnly":false,"FactoryDefaultValue":{"value":"0x0","label":"off"},"CurrentValue":{"value":"0x0","label":"off"},"formType":"enum","form":{"values":[{"value":"0x0","label":"off"},{"value":"0x2","label":"2 seconds"},{"value":"0x4","label":"10 seconds"}]}},{"DevicePropertyCode":{"code":"0x500c","label":"flash mode"},"dataType":"uint16","readOnly":false,"FactoryDefaultValue":{"value":"0x2","label":"off"},"CurrentValue":{"value":"0x8009","label":"disabled"},"formType":"enum","form":{"values":[{"value":"0x8009","label":"disabled"},{"value":"0x800a","label":"enabled"}]}},{"DevicePropertyCode":{"code":"0x5005","label":"white balance"},"dataType":"uint16","readOnly":false,"FactoryDefaultValue":{"value":"0x2","label":"automatic"},"CurrentValue":{"value":"0x2","label":"automatic"},"formType":"enum","form":{"values":[{"value":"0x2","label":"automatic"},{"value":"0x4","label":"daylight"},{"value":"0x8006","label":"shade"},{"value":"0x8001","label":"fluorescent 1"},{"value":"0x8002","label":"fluorescent 2"},{"value":"0x8003","label":"fluorescent 3"},{"value":"0x6","label":"tungsten"},{"value":"0x800a","label":"underwater"},{"value":"0x800b","label":"temprerature"},{"value":"0x800c","label":"custom"}]}},{"DevicePropertyCode":{"code":"0x5010","label":"exposure bias compensation"},"dataType":"int16","readOnly":false,"FactoryDefaultValue":{"value":"0x0","label":"0"},"CurrentValue":{"value":"0x0","label":"0"},"formType":"enum","form":{"values":[{"value":"0xf448","label":"-3"},{"value":"0xf595","label":"-2 2/3"},{"value":"0xf6e3","label":"-2 1/3"},{"value":"0xf830","label":"-2"},{"value":"0xf97d","label":"-1 2/3"},{"value":"0xfacb","label":"-1 1/3"},{"value":"0xfc18","label":"-1"},{"value":"0xfd65","label":"-2/3"},{"value":"0xfeb3","label":"-1/3"},{"value":"0x0","label":"0"},{"value":"0x14d","label":"1/3"},{"value":"0x29b","label":"2/3"},{"value":"0x3e8","label":"1"},{"value":"0x535","label":"1 1/3"},{"value":"0x683","label":"1 2/3"},{"value":"0x7d0","label":"2"},{"value":"0x91d","label":"2 1/3"},{"value":"0xa6b","label":"2 2/3"},{"value":"0xbb8","label":"3"}]}},{"DevicePropertyCode":{"code":"0xd001","label":"film simulation"},"dataType":"uint16","readOnly":false,"FactoryDefaultValue":{"value":"0x1","label":"PROVIA"},"CurrentValue":{"value":"0x2","label":"Velvia"},"formType":"enum","form":{"values":[{"value":"0x1","label":"PROVIA"},{"value":"0x2","label":"Velvia"},{"value":"0x3","label":"ASTIA"},{"value":"0x4","label":"Monochrome"},{"value":"0x5","label":"Sepia"},{"value":"0x6","label":"PRO Neg. Hi"},{"value":"0x7","label":"PRO Neg. Std"},{"value":"0x8","label":"Monochrome + Ye Filter"},{"value":"0x9","label":"Monochrome + R Filter"},{"value":"0xa","label":"Monochrome + G Filter"},{"value":"0xb","label":"Classic Chrome"}]}},{"DevicePropertyCode":{"code":"0xd02a","label":"ISO"},"dataType":"uint32","readOnly":false,"FactoryDefaultValue":{"value":"0xffffffff","label":"auto"},"CurrentValue":{"value":"0x80001900","label":"S6400"},"formType":"enum","form":{"values":[{"value":"0x80000190","label":"S400"},{"value":"0x80000320","label":"S800"},{"value":"0x80000640","label":"S1600"},{"value":"0x80000c80","label":"S3200"},{"value":"0x80001900","label":"S6400"},{"value":"0x40000064","label":"L100"},{"value":"0xc8","label":"200"},{"value":"0xfa","label":"250"},{"value":"0x140","label":"320"},{"value":"0x190","label":"400"},{"value":"0x1f4","label":"500"},{"value":"0x280","label":"640"},{"value":"0x320","label":"800"},{"value":"0x3e8","label":"1000"},{"value":"0x4e2","label":"1250"},{"value":"0x640","label":"1600"},{"value":"0x7d0","label":"2000"},{"value":"0x9c4","label":"2500"},{"value":"0xc80","label":"3200"},{"value":"0xfa0","label":"4000"},{"value":"0x1388","label":"5000"},{"value":"0x1900","label":"6400"},{"value":"0x40003200","label":"H12800"},{"value":"0x40006400","label":"H25600"},{"value":"0x4000c800","label":"H51200"}]}},{"DevicePropertyCode":{"code":"0xd019","label":"rec mode"},"dataType":"uint16","readOnly":false,"FactoryDefaultValue":{"value":"0x1","label":""},"CurrentValue":{"value":"0x1","label":""},"formType":"enum","form":{"values":[{"value":"0x0","label":""},{"value":"0x1","label":""}]}},{"DevicePropertyCode":{"code":"0xd17c","label":"focus point"},"dataType":"uint32","readOnly":false,"FactoryDefaultValue":{"value":"0x0","label":"0x0"},"CurrentValue":{"value":"0x3020702","label":"7x2"},"formType":"range","form":{"min":"0x0","max":"0x10090707","step":"0x1"}}] -------------------------------------------------------------------------------- /docs/code-examples/findAllProperties.php: -------------------------------------------------------------------------------- 1 | MAX_VALUE || $start > MAX_VALUE || $end > MAX_VALUE) { 62 | printf('The maximum allowed value is %d!%s', MAX_VALUE, PHP_EOL); 63 | exit(1); 64 | } 65 | 66 | if ($port < MIN_VALUE || $start < MIN_VALUE || $end < MIN_VALUE) { 67 | printf('The minimum allowed value is %d!%s', MIN_VALUE, PHP_EOL); 68 | exit(1); 69 | } 70 | } 71 | 72 | function printUsage(): void 73 | { 74 | global $argv; 75 | 76 | printf('Usage for %s:%s', basename($argv[0]), PHP_EOL); 77 | 78 | $opts = explode('::', OPTIONS); 79 | foreach ($opts as $opt) { 80 | if (empty($opt)) { 81 | continue; 82 | } 83 | 84 | $message = ''; 85 | switch ($opt) { 86 | case 'h': 87 | $message = sprintf('The host to connect to (defaults to %s).', DEFAULT_HOST); 88 | break; 89 | case 'p': 90 | $message = sprintf('The port to connect to (defaults to %d).', DEFAULT_PORT); 91 | break; 92 | case 's': 93 | $message = sprintf('The decimal value to start eumeration from (defaults to %d).', MIN_VALUE); 94 | break; 95 | case 'e': 96 | $message = sprintf('The decimal value to enumerate to (defaults to %d).', MAX_VALUE); 97 | break; 98 | } 99 | printHelpLine($opt, $message); 100 | } 101 | 102 | foreach (LONG_OPTS as $opt) { 103 | $message = ''; 104 | switch ($opt) { 105 | case 'dry-run': 106 | $message = 'Do not actually execute the commands.'; 107 | break; 108 | case 'help': 109 | $message = 'Print this message.'; 110 | break; 111 | } 112 | printHelpLine($opt, $message, true); 113 | } 114 | } 115 | 116 | function printHelpLine(string $option, string $message, bool $long = false): void 117 | { 118 | $prefix = $long ? '--' : '-'; 119 | printf("%s%s%s\t%s%s", $prefix, $option, PHP_EOL, $message, PHP_EOL); 120 | } 121 | 122 | /** 123 | * @param mixed $default The value to return when the option is not set. 124 | * 125 | * @return mixed 126 | */ 127 | function getOption(string $option, $default = null, $longOpt = false) 128 | { 129 | global $options; 130 | 131 | if (array_key_exists($option, $options)) { 132 | if ($longOpt) { 133 | return true; 134 | } 135 | return $options[$option]; 136 | } 137 | 138 | return $default; 139 | } 140 | 141 | function sendSocketMessageAndPrintResult(string $host, int $port, string $message): void 142 | { 143 | global $dryRun; 144 | 145 | if ($dryRun) { 146 | echo 'Dry run active, not sending message!' . PHP_EOL; 147 | 148 | return; 149 | } 150 | 151 | $fp = @fsockopen($host, $port, $errno, $errstr, 30); 152 | if (!$fp) { 153 | printf("%s (%d)%s", $errstr, $errno, PHP_EOL); 154 | exit(1); 155 | } 156 | 157 | fwrite($fp, $message . "\n"); 158 | while (!feof($fp)) { 159 | echo fgets($fp, 128); 160 | } 161 | fclose($fp); 162 | } 163 | 164 | function printSeparator(int $length): void 165 | { 166 | echo str_repeat('-', $length) . PHP_EOL; 167 | } 168 | -------------------------------------------------------------------------------- /cmd/command_liveview.go: -------------------------------------------------------------------------------- 1 | // +build with_lv 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "github.com/go-gl/gl/v2.1/gl" 9 | "github.com/go-gl/glfw/v3.1/glfw" 10 | "github.com/malc0mn/ptp-ip/ip" 11 | "github.com/malc0mn/ptp-ip/ptp" 12 | "github.com/malc0mn/ptp-ip/viewfinder" 13 | "image" 14 | "image/draw" 15 | "time" 16 | ) 17 | 18 | var ( 19 | lvState bool 20 | mainStack = make(chan func()) 21 | ) 22 | 23 | func init() { 24 | registerCommand(&liveview{}) 25 | } 26 | 27 | type liveview struct{} 28 | 29 | func (liveview) name() string { 30 | return "liveview" 31 | } 32 | 33 | func (liveview) alias() []string { 34 | return []string{} 35 | } 36 | 37 | func (l liveview) execute(c *ip.Client, f []string, _ chan<- string) string { 38 | errorFmt := "liveview error: %s\n" 39 | 40 | if lvState { 41 | return "already enabled!\n" 42 | } 43 | 44 | lvState = true 45 | 46 | if err := c.ToggleLiveView(lvState); err != nil { 47 | return fmt.Sprintf(errorFmt, err) 48 | } 49 | 50 | withVf := true 51 | if len(f) >= 1 { 52 | withVf = !l.isNoVf(f[0]) 53 | } 54 | 55 | runOnMain(func() { liveViewUI(c, withVf) }) 56 | 57 | return "enabled\n" 58 | } 59 | 60 | func (l liveview) help() string { 61 | help := `"` + l.name() + `" opens a window and displays a live view through the camera lens. Not all vendors support this!` + "\n" 62 | 63 | if args := l.arguments(); len(args) > 0 { 64 | help += helpAddArgumentsTitle() 65 | for i, arg := range args { 66 | switch i { 67 | case 0: 68 | help += "\t- " + `"` + arg + `" disables the viewfinder overlay which eliminates camera state polling` + "\n" 69 | } 70 | } 71 | } 72 | 73 | return help 74 | } 75 | 76 | func (liveview) arguments() []string { 77 | return []string{"novf"} 78 | } 79 | 80 | func (l liveview) isNoVf(param string) bool { 81 | return param == l.arguments()[0] 82 | } 83 | 84 | // mainThread is used to execute on the main thread, which is what OpenGL requires. 85 | func mainThread() { 86 | for { 87 | select { 88 | case f := <-mainStack: 89 | f() 90 | case <-quit: 91 | return 92 | } 93 | } 94 | } 95 | 96 | // runOnMain executes f on the main thread but does not wait for it to finish. 97 | func runOnMain(f func()) { 98 | mainStack <- f 99 | } 100 | 101 | func liveViewUI(c *ip.Client, withVf bool) error { 102 | if err := gl.Init(); err != nil { 103 | return err 104 | } 105 | 106 | if err := glfw.Init(); err != nil { 107 | return err 108 | } 109 | defer glfw.Terminate() 110 | 111 | img := <-c.StreamChan 112 | window, err := showImage(img, "Live view") 113 | if err != nil { 114 | return err 115 | } 116 | 117 | // TODO: add support to allow toggling the viewfinder on or off. 118 | var ( 119 | vf *viewfinder.Viewfinder 120 | s interface{} 121 | ) 122 | ticker := time.NewTicker(1 * time.Second) 123 | if withVf { 124 | s, err = c.GetDeviceState() 125 | if err != nil { 126 | s = []*ptp.DevicePropDesc{} 127 | } 128 | 129 | im, _, err := image.Decode(bytes.NewReader(img)) 130 | if err == nil { 131 | vf = viewfinder.NewViewfinder(toRGBA(im), c.ResponderVendor()) 132 | } 133 | } else { 134 | ticker.Stop() 135 | } 136 | 137 | poller: 138 | for !window.ShouldClose() { 139 | select { 140 | case img := <-c.StreamChan: 141 | im, _, err := image.Decode(bytes.NewReader(img)) 142 | if err == nil { 143 | rgba := toRGBA(im) 144 | if vf != nil { 145 | if data, ok := s.([]*ptp.DevicePropDesc); ok { 146 | viewfinder.DrawViewfinder(vf, rgba, data) 147 | } 148 | } 149 | window.setImage(rgba) 150 | } 151 | case <-ticker.C: 152 | s, _ = c.GetDeviceState() 153 | case <-quit: 154 | break poller 155 | } 156 | glfw.PollEvents() 157 | } 158 | 159 | window.Destroy() 160 | lvState = false 161 | if err := c.ToggleLiveView(lvState); err != nil { 162 | return err 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func toRGBA(img image.Image) *image.RGBA { 169 | rgba, ok := img.(*image.RGBA) 170 | if !ok { 171 | rgba = image.NewRGBA(img.Bounds()) 172 | draw.Draw(rgba, rgba.Rect, img, image.Point{}, draw.Src) 173 | } 174 | 175 | return rgba 176 | } 177 | 178 | func preview(img []byte) string { 179 | // TODO: figure out how to cleanly have multiple windows open at the same time 'on the main thread' by introducing some 180 | // sort of extremely simple window manager. 181 | if lvState { 182 | return "can currently not display preview while liveview is active" 183 | } 184 | 185 | runOnMain(func() { previewUI(img) }) 186 | 187 | return "preview window opened" 188 | } 189 | 190 | func previewUI(img []byte) error { 191 | if err := gl.Init(); err != nil { 192 | return err 193 | } 194 | 195 | if err := glfw.Init(); err != nil { 196 | return err 197 | } 198 | defer glfw.Terminate() 199 | 200 | window, err := showImage(img, "Capture preview") 201 | if err != nil { 202 | return err 203 | } 204 | 205 | poller: 206 | for !window.ShouldClose() { 207 | select { 208 | case <-quit: 209 | break poller 210 | default: 211 | glfw.WaitEvents() 212 | } 213 | } 214 | 215 | window.Destroy() 216 | 217 | return nil 218 | } 219 | 220 | func showImage(img []byte, title string) (*window, error) { 221 | im, _, err := image.Decode(bytes.NewReader(img)) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | window, err := newWindow(im, title) 227 | if err != nil { 228 | return nil, err 229 | } 230 | window.draw() 231 | 232 | return window, nil 233 | } 234 | -------------------------------------------------------------------------------- /cmd/config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/malc0mn/ptp-ip/ip" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func TestDefaultConfig(t *testing.T) { 11 | want := "generic" 12 | if conf.vendor != want { 13 | t.Errorf("conf.vendor = %s; want %s", conf.vendor, want) 14 | } 15 | 16 | want = "192.168.0.1" 17 | if conf.host != want { 18 | t.Errorf("conf.host = %s; want %s", conf.host, want) 19 | } 20 | 21 | wantPort := uint16Value(15740) 22 | if conf.port != wantPort { 23 | t.Errorf("conf.port = %d; want %d", conf.port, wantPort) 24 | } 25 | 26 | want = "127.0.0.1" 27 | if conf.srvAddr != want { 28 | t.Errorf("conf.srvAddr = %s; want %s", conf.srvAddr, want) 29 | } 30 | 31 | if conf.srvPort != wantPort { 32 | t.Errorf("conf.srvPort = %d; want %d", conf.srvPort, wantPort) 33 | } 34 | } 35 | 36 | func TestLoadconfigOk1(t *testing.T) { 37 | file = "testdata/test_ok1.conf" 38 | loadConfig() 39 | 40 | want := "Golang test OK1 client" 41 | if conf.fname != want { 42 | t.Errorf("loadConfig() fname = %s; want %s", conf.fname, want) 43 | } 44 | 45 | want = "cca455de-79ac-4b12-9731-91e433a899cf" 46 | if conf.guid != want { 47 | t.Errorf("loadConfig() guid = %s; want %s", conf.guid, want) 48 | } 49 | 50 | want = "fuji" 51 | if conf.vendor != want { 52 | t.Errorf("loadConfig() vendor = %s; want %s", conf.host, want) 53 | } 54 | 55 | want = "192.168.0.2" 56 | if conf.host != want { 57 | t.Errorf("loadConfig() host = %s; want %s", conf.host, want) 58 | } 59 | 60 | wantPort := uint16Value(35740) 61 | if conf.port != wantPort { 62 | t.Errorf("loadConfig() port = %d; want %d", conf.port, wantPort) 63 | } 64 | 65 | wantEnabled := true 66 | if server != wantEnabled { 67 | t.Errorf("loadConfig() server = %v; want %v", server, wantEnabled) 68 | } 69 | 70 | want = "127.0.0.2" 71 | if conf.srvAddr != want { 72 | t.Errorf("loadConfig() saddr = %s; want %s", conf.srvAddr, want) 73 | } 74 | 75 | wantPort = uint16Value(25740) 76 | if conf.srvPort != wantPort { 77 | t.Errorf("loadConfig() sport = %d; want %d", conf.srvPort, wantPort) 78 | } 79 | } 80 | 81 | func TestLoadconfigOk2(t *testing.T) { 82 | conf = &config{ 83 | vendor: ip.DefaultVendor, 84 | host: ip.DefaultIpAddress, 85 | port: uint16Value(ip.DefaultPort), 86 | srvAddr: defaultIp, 87 | srvPort: uint16Value(ip.DefaultPort), 88 | } 89 | 90 | file = "testdata/test_ok2.conf" 91 | loadConfig() 92 | 93 | want := "Golang test OK2 client" 94 | if conf.fname != want { 95 | t.Errorf("loadConfig() fname = %s; want %s", conf.fname, want) 96 | } 97 | 98 | want = "9fe5160c-4951-404d-9505-10baaf725606" 99 | if conf.guid != want { 100 | t.Errorf("loadConfig() guid = %s; want %s", conf.guid, want) 101 | } 102 | 103 | want = "fuji" 104 | if conf.vendor != want { 105 | t.Errorf("loadConfig() vendor = %s; want %s", conf.host, want) 106 | } 107 | 108 | want = "192.168.0.2" 109 | if conf.host != want { 110 | t.Errorf("loadConfig() host = %s; want %s", conf.host, want) 111 | } 112 | 113 | wantPort := uint16Value(15740) 114 | if conf.port != wantPort { 115 | t.Errorf("loadConfig() port = %d; want %d", conf.port, wantPort) 116 | } 117 | 118 | wantPort = uint16Value(55740) 119 | if conf.cport != wantPort { 120 | t.Errorf("loadConfig() cport = %d; want %d", conf.cport, wantPort) 121 | } 122 | 123 | wantPort = uint16Value(55741) 124 | if conf.eport != wantPort { 125 | t.Errorf("loadConfig() eport = %d; want %d", conf.eport, wantPort) 126 | } 127 | 128 | wantPort = uint16Value(55742) 129 | if conf.sport != wantPort { 130 | t.Errorf("loadConfig() sport = %d; want %d", conf.sport, wantPort) 131 | } 132 | 133 | wantEnabled := true 134 | if server != wantEnabled { 135 | t.Errorf("loadConfig() server = %v; want %v", server, wantEnabled) 136 | } 137 | 138 | want = "127.0.0.3" 139 | if conf.srvAddr != want { 140 | t.Errorf("loadConfig() saddr = %s; want %s", conf.srvAddr, want) 141 | } 142 | 143 | wantPort = uint16Value(35740) 144 | if conf.srvPort != wantPort { 145 | t.Errorf("loadConfig() sport = %d; want %d", conf.srvPort, wantPort) 146 | } 147 | } 148 | 149 | func TestLoadConfigWrongPath(t *testing.T) { 150 | if os.Getenv("CONF_FAIL") == "1" { 151 | file = "does-not-exist.conf" 152 | loadConfig() 153 | return 154 | } 155 | 156 | want := 102 157 | cmd := exec.Command(os.Args[0], "-test.run=TestLoadConfigWrongPath") 158 | cmd.Env = append(os.Environ(), "CONF_FAIL=1") 159 | err := cmd.Run() 160 | if e, ok := err.(*exec.ExitError); ok && !e.Success() && e.ExitCode() != want { 161 | t.Fatalf("loadConfig() ran with err %v, want exit status %d", err, want) 162 | } 163 | } 164 | 165 | func TestLoadConfigFail1(t *testing.T) { 166 | if os.Getenv("CONF_FAIL") == "1" { 167 | file = "testdata/test_fail1.conf" 168 | loadConfig() 169 | return 170 | } 171 | 172 | want := 1 173 | cmd := exec.Command(os.Args[0], "-test.run=TestLoadConfigFail") 174 | cmd.Env = append(os.Environ(), "CONF_FAIL=1") 175 | err := cmd.Run() 176 | if e, ok := err.(*exec.ExitError); ok && !e.Success() && e.ExitCode() != want { 177 | t.Fatalf("loadConfig() ran with err %v, want exit status %d", err, want) 178 | } 179 | } 180 | 181 | func TestLoadConfigFail2(t *testing.T) { 182 | if os.Getenv("CONF_FAIL") == "1" { 183 | file = "testdata/test_fail2.conf" 184 | loadConfig() 185 | return 186 | } 187 | 188 | want := 1 189 | cmd := exec.Command(os.Args[0], "-test.run=TestLoadConfigFail") 190 | cmd.Env = append(os.Environ(), "CONF_FAIL=1") 191 | err := cmd.Run() 192 | if e, ok := err.(*exec.ExitError); ok && !e.Success() && e.ExitCode() != want { 193 | t.Fatalf("loadConfig() ran with err %v, want exit status %d", err, want) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /cmd/format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | ptpfmt "github.com/malc0mn/ptp-ip/fmt" 8 | "github.com/malc0mn/ptp-ip/ip" 9 | "github.com/malc0mn/ptp-ip/ptp" 10 | "strconv" 11 | "strings" 12 | "text/tabwriter" 13 | ) 14 | 15 | func formatDeviceProperty(c *ip.Client, param string) (ptp.DevicePropCode, error) { 16 | var cod ptp.DevicePropCode 17 | 18 | conv, errH := ptpfmt.HexStringToUint64(param, 16) 19 | if errH != nil { 20 | var errS error 21 | cod, errS = ptpfmt.PropNameToDevicePropCode(c.ResponderVendor(), param) 22 | if errS != nil { 23 | return 0, fmt.Errorf("%s or %s", errH, errS) 24 | } else { 25 | c.Debugf("Converted %s: %#x", param, cod) 26 | } 27 | } else { 28 | cod = ptp.DevicePropCode(conv) 29 | c.Debugf("Converted uint16: %#x", cod) 30 | } 31 | 32 | return cod, nil 33 | } 34 | 35 | func formatDeviceInfo(vendor ptp.VendorExtension, data interface{}, f []string) string { 36 | switch vendor { 37 | case ptp.VE_FujiPhotoFilmCoLtd: 38 | return fujiFormatDeviceInfo(data.([]*ptp.DevicePropDesc), f) 39 | default: 40 | // TODO: add generic device info formatting. 41 | return "" 42 | } 43 | } 44 | 45 | func fujiFormatDeviceProperty(dpd *ptp.DevicePropDesc, f []string) string { 46 | if len(f) >= 1 && f[0] == "json" { 47 | var opt string 48 | if len(f) > 1 { 49 | opt = f[1] 50 | } 51 | 52 | return fujiFormatJson(&ptpfmt.DevicePropDescJSON{ 53 | DevicePropDesc: dpd, 54 | }, opt) 55 | } 56 | 57 | return fujiFormatTable(dpd) 58 | } 59 | 60 | func fujiFormatDeviceInfo(list []*ptp.DevicePropDesc, f []string) string { 61 | if len(f) >= 1 && f[0] == "json" { 62 | var opt string 63 | if len(f) > 1 { 64 | opt = f[1] 65 | } 66 | 67 | return fujiFormatJsonList(list, opt) 68 | } 69 | 70 | return fujiFormatListAsTable(list) 71 | } 72 | 73 | func fujiFormatJsonList(list []*ptp.DevicePropDesc, opt string) string { 74 | lj := make([]*ptpfmt.DevicePropDescJSON, len(list)) 75 | for i := 0; i < len(list); i++ { 76 | lj[i] = &ptpfmt.DevicePropDescJSON{ 77 | DevicePropDesc: list[i], 78 | } 79 | } 80 | 81 | return fujiFormatJson(lj, opt) 82 | } 83 | 84 | func fujiFormatJson(v interface{}, opt string) string { 85 | var err error 86 | var res []byte 87 | if opt == "pretty" { 88 | res, err = json.MarshalIndent(v, "", " ") 89 | } else { 90 | res, err = json.Marshal(v) 91 | } 92 | if err != nil { 93 | return err.Error() 94 | } 95 | 96 | return string(res) 97 | } 98 | 99 | func fujiFormatTable(dpd *ptp.DevicePropDesc) string { 100 | w, buf := newTabWriter() 101 | rows := longHeader() 102 | rows = append(rows, longPropDescFormat(dpd)) 103 | formatRows(w, rows) 104 | 105 | return "\n" + buf.String() 106 | } 107 | 108 | func fujiFormatListAsTable(list []*ptp.DevicePropDesc) string { 109 | w, buf := newTabWriter() 110 | rows := shortHeader() 111 | for _, dpd := range list { 112 | rows = append(rows, shortPropDescFormat(dpd)) 113 | } 114 | formatRows(w, rows) 115 | 116 | return "\n" + buf.String() 117 | } 118 | 119 | func newTabWriter() (*tabwriter.Writer, *bytes.Buffer) { 120 | buf := new(bytes.Buffer) 121 | 122 | return tabwriter.NewWriter(buf, 8, 4, 2, ' ', tabwriter.TabIndent), buf 123 | } 124 | 125 | func shortHeader() [][]string { 126 | return [][]string{ 127 | {"DevicePropCode", "Property name", "Value as string", "Value as int64", "Value in hex"}, 128 | {"--------------", "-------------", "---------------", "--------------", "------------"}, 129 | } 130 | } 131 | 132 | func shortPropDescFormat(dpd *ptp.DevicePropDesc) []string { 133 | return []string{ 134 | fmt.Sprintf("%0#4x", dpd.DevicePropertyCode), 135 | ptpfmt.FujiDevicePropCodeAsString(dpd.DevicePropertyCode), 136 | ptpfmt.FujiDevicePropValueAsString(dpd.DevicePropertyCode, dpd.CurrentValueAsInt64()), 137 | strconv.FormatInt(dpd.CurrentValueAsInt64(), 10), 138 | fmt.Sprintf("%0#8x", dpd.CurrentValueAsInt64()), 139 | } 140 | } 141 | 142 | func longHeader() [][]string { 143 | return [][]string{ 144 | {"DevicePropCode", "Prop name", "Dflt val as str", "Dflt val as int64", "Dflt val in hex", "Cur val as str", "Cur val as int64", "Cur val in hex", "Vals allowed"}, 145 | {"--------------", "---------", "---------------", "-----------------", "---------------", "--------------", "----------------", "--------------", "------------"}, 146 | } 147 | } 148 | 149 | func longPropDescFormat(dpd *ptp.DevicePropDesc) []string { 150 | var allowed string 151 | 152 | switch form := dpd.Form.(type) { 153 | case *ptp.RangeForm: 154 | allowed = fmt.Sprintf( 155 | "Min: %#x, max: %#x, stepszie: %#x", 156 | form.MinimumValueAsInt64(), form.MaximumValueAsInt64(), form.StepSizeAsInt64(), 157 | ) 158 | case *ptp.EnumerationForm: 159 | vals := form.SupportedValuesAsInt64Array() 160 | str := make([]string, len(vals)) 161 | for i, val := range vals { 162 | str[i] = ptpfmt.ConvertToHexString(val) 163 | } 164 | allowed = strings.Join(str, ", ") 165 | } 166 | 167 | return []string{ 168 | fmt.Sprintf("%0#4x", dpd.DevicePropertyCode), 169 | ptpfmt.FujiDevicePropCodeAsString(dpd.DevicePropertyCode), 170 | ptpfmt.FujiDevicePropValueAsString(dpd.DevicePropertyCode, dpd.FactoryDefaultValueAsInt64()), 171 | strconv.FormatInt(dpd.FactoryDefaultValueAsInt64(), 10), 172 | fmt.Sprintf("%0#8x", dpd.FactoryDefaultValueAsInt64()), 173 | ptpfmt.FujiDevicePropValueAsString(dpd.DevicePropertyCode, dpd.CurrentValueAsInt64()), 174 | strconv.FormatInt(dpd.CurrentValueAsInt64(), 10), 175 | fmt.Sprintf("%0#8x", dpd.CurrentValueAsInt64()), 176 | allowed, 177 | } 178 | } 179 | 180 | func formatRows(w *tabwriter.Writer, rows [][]string) { 181 | for _, row := range rows { 182 | fmt.Fprintln(w, strings.Join(row, "\t")) 183 | } 184 | w.Flush() 185 | } 186 | -------------------------------------------------------------------------------- /ip/internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "github.com/malc0mn/ptp-ip/ptp" 7 | "io" 8 | "net" 9 | "reflect" 10 | "strings" 11 | "time" 12 | "unicode/utf16" 13 | ) 14 | 15 | func marshal(s interface{}, bo binary.ByteOrder, b *bytes.Buffer) { 16 | // binary.Write() can only cope with fixed length values so we'll need to handle anything else ourselves. 17 | if _, hasSession := s.(ptp.Session); binary.Size(s) < 0 || hasSession { 18 | v := reflect.Indirect(reflect.ValueOf(s)) 19 | 20 | for i := 0; i < v.NumField(); i++ { 21 | // When a dataset has a SessionID, we must skip sending it according to the PTP/IP protocol. 22 | if v.Type().Field(i).Name == "SessionID" { 23 | continue 24 | } 25 | 26 | f := v.Field(i) 27 | switch f.Kind() { 28 | case reflect.Struct: 29 | marshal(f.Addr().Interface(), bo, b) 30 | case reflect.String: 31 | // TODO: the PTP protocol sets a limit of 255 characters per string including the terminating null 32 | // character. We must still enforce this limit here. 33 | // A rune in Go is an alias for uint32 but the PTP protocol expects 2 byte Unicode characters according 34 | // to the ISO10646 standard, so we convert them to utf16 (which is uint16) here. 35 | binary.Write(b, bo, utf16.Encode([]rune(f.String()))) 36 | // Strings must be null terminated. 37 | binary.Write(b, bo, uint16(0)) 38 | default: 39 | binary.Write(b, bo, f.Addr().Interface()) 40 | } 41 | } 42 | } else { 43 | binary.Write(b, bo, s) 44 | } 45 | } 46 | 47 | // Marshal data to a byte array, Little Endian formant, for transport. 48 | func MarshalLittleEndian(s interface{}) []byte { 49 | var b bytes.Buffer 50 | marshal(s, binary.LittleEndian, &b) 51 | 52 | return b.Bytes() 53 | } 54 | 55 | // We always read using reflection to fill each field of s as we go along. This way, we can fill structs like the 56 | // ptp.OperationResponsePacket which does not necessarily receive all parameter fields 'over the wire'. According to the 57 | // protocol we should, but unfortunately it depends on the vendor's implementation. So we need to make sure this 58 | // unmarshal function is usable by all future implementations. 59 | // The int returned is the left over length of the data that has NOT been unmarshalled. It is the responsibility of the 60 | // caller to handle it. 61 | func unmarshal(r io.Reader, s interface{}, l int, vs int, bo binary.ByteOrder) (int, error) { 62 | v := reflect.Indirect(reflect.ValueOf(s)) 63 | 64 | for i := 0; i < v.NumField(); i++ { 65 | // When a dataset has a SessionID, we must skip it since the PTP/IP protocol does not send it. 66 | if v.Type().Field(i).Name == "SessionID" { 67 | continue 68 | } 69 | 70 | f := v.Field(i) 71 | switch f.Kind() { 72 | case reflect.Struct: 73 | var err error 74 | l, err = unmarshal(r, f.Addr().Interface(), l, vs, bo) 75 | if err != nil { 76 | return 0, err 77 | } 78 | case reflect.String: 79 | // The PTP protocol expects 2 byte Unicode characters according to the ISO10646 standard, so we convert 80 | // them to string here. 81 | b := make([]uint16, vs / 2) 82 | if err := binary.Read(r, bo, b); err != nil { 83 | return 0, err 84 | } 85 | // The slice operation happening here is to drop the null terminator. 86 | f.SetString(string(utf16.Decode(b[:len(b) - 1]))) 87 | l -= vs 88 | default: 89 | if err := binary.Read(r, bo, f.Addr().Interface()); err != nil { 90 | return 0, err 91 | } 92 | l -= binary.Size(f.Addr().Interface()) 93 | } 94 | 95 | if l == 0 { 96 | return l, nil 97 | } 98 | } 99 | 100 | return l, nil 101 | } 102 | 103 | // Unmarshal a byte array, Little Endian formant, upon reception. 104 | // We need a reader, a destination container, the total expected length and a "variable size" integer indicating the 105 | // variable sized portion of the packet. 106 | // Any data that is left over after reading to s will be returned as as a byte array to be dealt with by the caller. 107 | func UnmarshalLittleEndian(r io.Reader, s interface{}, l int, vs int) ([]byte, error) { 108 | var xs []byte 109 | 110 | left, err := unmarshal(r, s, l, vs, binary.LittleEndian) 111 | if left > 0 { 112 | xs = make([]byte, left) 113 | binary.Read(r, binary.LittleEndian, &xs) 114 | } 115 | 116 | return xs, err 117 | } 118 | 119 | func TotalSizeOfFixedFields(s interface{}) int { 120 | tfs := binary.Size(s) 121 | 122 | // The SessionID Field is dropped in the PTP/IP implementation. 123 | if _, hasSession := s.(ptp.Session); hasSession { 124 | tfs -= 4 125 | } 126 | 127 | if tfs < 0 { 128 | tfs = 0 129 | v := reflect.Indirect(reflect.ValueOf(s)) 130 | for i := 0; i < v.NumField(); i++ { 131 | f := v.Field(i) 132 | switch f.Kind() { 133 | case reflect.String: 134 | // Skip string fields, we do not calculate their size. 135 | continue 136 | default: 137 | tfs += binary.Size(f.Addr().Interface()) 138 | } 139 | } 140 | } 141 | 142 | return tfs 143 | } 144 | 145 | // A wrapper around net.Dial() that will retry dialing 10 times on a "connection refused" error with a 500ms delay 146 | // between retries. 147 | // TODO: make this loop cancelable! 148 | func RetryDialer(network, address string, timeout time.Duration) (net.Conn, error) { 149 | var err error 150 | var retries = 10 151 | var wait = 500 * time.Millisecond 152 | var conn net.Conn 153 | 154 | for { 155 | conn, err = net.DialTimeout(network, address, timeout) 156 | // Insane isn't it? No sentinel errors from net.Dial()! 157 | if err != nil && strings.Contains(err.Error(), "connection refused") && retries > 0 { 158 | retries-- 159 | time.Sleep(wait) 160 | continue 161 | } 162 | break 163 | } 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | return conn, nil 169 | } 170 | -------------------------------------------------------------------------------- /fmt/json_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/malc0mn/ptp-ip/ip" 7 | "github.com/malc0mn/ptp-ip/ptp" 8 | "io/ioutil" 9 | "testing" 10 | ) 11 | 12 | func TestMarshalJSON(t *testing.T) { 13 | list := []*ptp.DevicePropDesc{ 14 | { 15 | DevicePropertyCode: ptp.DPC_CaptureDelay, 16 | DataType: ptp.DTC_UINT16, 17 | GetSet: ptp.DPD_GetSet, 18 | FactoryDefaultValue: []uint8{0x0, 0x0}, 19 | CurrentValue: []uint8{0x0, 0x0}, 20 | FormFlag: ptp.DPF_FormFlag_Enum, 21 | Form: &ptp.EnumerationForm{ 22 | NumberOfValues: 3, 23 | SupportedValues: [][]uint8{ 24 | {0x00, 0x00}, {0x02, 0x00}, {0x04, 0x00}, 25 | }, 26 | }, 27 | }, 28 | { 29 | DevicePropertyCode: ptp.DPC_FlashMode, 30 | DataType: ptp.DTC_UINT16, 31 | GetSet: ptp.DPD_GetSet, 32 | FactoryDefaultValue: []uint8{0x2, 0x0}, 33 | CurrentValue: []uint8{0x9, 0x80}, 34 | FormFlag: ptp.DPF_FormFlag_Enum, 35 | Form: &ptp.EnumerationForm{ 36 | NumberOfValues: 2, 37 | SupportedValues: [][]uint8{ 38 | {0x09, 0x80}, {0x0a, 0x80}, 39 | }, 40 | }, 41 | }, 42 | { 43 | DevicePropertyCode: ptp.DPC_WhiteBalance, 44 | DataType: ptp.DTC_UINT16, 45 | GetSet: ptp.DPD_GetSet, 46 | FactoryDefaultValue: []uint8{0x2, 0x0}, 47 | CurrentValue: []uint8{0x2, 0x0}, 48 | FormFlag: ptp.DPF_FormFlag_Enum, 49 | Form: &ptp.EnumerationForm{ 50 | NumberOfValues: 10, 51 | SupportedValues: [][]uint8{ 52 | {0x02, 0x00}, {0x04, 0x00}, {0x06, 0x80}, {0x01, 0x80}, {0x02, 0x80}, {0x03, 0x80}, {0x06, 0x00}, 53 | {0x0a, 0x80}, {0x0b, 0x80}, {0x0c, 0x80}, 54 | }, 55 | }, 56 | }, 57 | { 58 | DevicePropertyCode: ptp.DPC_ExposureBiasCompensation, 59 | DataType: ptp.DTC_INT16, 60 | GetSet: ptp.DPD_GetSet, 61 | FactoryDefaultValue: []uint8{0x0, 0x0}, 62 | CurrentValue: []uint8{0x0, 0x0}, 63 | FormFlag: ptp.DPF_FormFlag_Enum, 64 | Form: &ptp.EnumerationForm{ 65 | NumberOfValues: 19, 66 | SupportedValues: [][]uint8{ 67 | {0x48, 0xf4}, {0x95, 0xf5}, {0xe3, 0xf6}, {0x30, 0xf8}, {0x7d, 0xf9}, {0xcb, 0xfa}, {0x18, 0xfc}, 68 | {0x65, 0xfd}, {0xb3, 0xfe}, {0x00, 0x00}, {0x4d, 0x01}, {0x9b, 0x02}, {0xe8, 0x03}, {0x35, 0x05}, 69 | {0x83, 0x06}, {0xd0, 0x07}, {0x1d, 0x09}, {0x6b, 0x0a}, {0xb8, 0x0b}, 70 | }, 71 | }, 72 | }, 73 | { 74 | DevicePropertyCode: ip.DPC_Fuji_FilmSimulation, 75 | DataType: ptp.DTC_UINT16, 76 | GetSet: ptp.DPD_GetSet, 77 | FactoryDefaultValue: []uint8{0x1, 0x0}, 78 | CurrentValue: []uint8{0x2, 0x0}, 79 | FormFlag: ptp.DPF_FormFlag_Enum, 80 | Form: &ptp.EnumerationForm{ 81 | NumberOfValues: 11, 82 | SupportedValues: [][]uint8{ 83 | {0x01, 0x00}, {0x02, 0x00}, {0x03, 0x00}, {0x04, 0x00}, {0x05, 0x00}, {0x06, 0x00}, {0x07, 0x00}, {0x08, 0x00}, 84 | {0x09, 0x00}, {0x0a, 0x00}, {0x0b, 0x00}, 85 | }, 86 | }, 87 | }, 88 | { 89 | DevicePropertyCode: ip.DPC_Fuji_ExposureIndex, 90 | DataType: ptp.DTC_UINT32, 91 | GetSet: ptp.DPD_GetSet, 92 | FactoryDefaultValue: []uint8{0xff, 0xff, 0xff, 0xff}, 93 | CurrentValue: []uint8{0x0, 0x19, 0x0, 0x80}, 94 | FormFlag: ptp.DPF_FormFlag_Enum, 95 | Form: &ptp.EnumerationForm{ 96 | NumberOfValues: 25, 97 | SupportedValues: [][]uint8{ 98 | {0x90, 0x01, 0x00, 0x80}, {0x20, 0x03, 0x00, 0x80}, {0x40, 0x06, 0x00, 0x80}, {0x80, 0x0c, 0x00, 0x80}, 99 | {0x00, 0x19, 0x00, 0x80}, {0x64, 0x00, 0x00, 0x40}, {0xc8, 0x00, 0x00, 0x00}, {0xfa, 0x00, 0x00, 0x00}, 100 | {0x40, 0x01, 0x00, 0x00}, {0x90, 0x01, 0x00, 0x00}, {0xf4, 0x01, 0x00, 0x00}, {0x80, 0x02, 0x00, 0x00}, 101 | {0x20, 0x03, 0x00, 0x00}, {0xe8, 0x03, 0x00, 0x00}, {0xe2, 0x04, 0x00, 0x00}, {0x40, 0x06, 0x00, 0x00}, 102 | {0xd0, 0x07, 0x00, 0x00}, {0xc4, 0x09, 0x00, 0x00}, {0x80, 0x0c, 0x00, 0x00}, {0xa0, 0x0f, 0x00, 0x00}, 103 | {0x88, 0x13, 0x00, 0x00}, {0x00, 0x19, 0x00, 0x00}, {0x00, 0x32, 0x00, 0x40}, {0x00, 0x64, 0x00, 0x40}, 104 | {0x00, 0xc8, 0x00, 0x40}, 105 | }, 106 | }, 107 | }, 108 | { 109 | DevicePropertyCode: ip.DPC_Fuji_RecMode, 110 | DataType: ptp.DTC_UINT16, 111 | GetSet: ptp.DPD_GetSet, 112 | FactoryDefaultValue: []uint8{0x1, 0x0}, 113 | CurrentValue: []uint8{0x1, 0x0}, 114 | FormFlag: ptp.DPF_FormFlag_Enum, 115 | Form: &ptp.EnumerationForm{ 116 | NumberOfValues: 2, 117 | SupportedValues: [][]uint8{ 118 | {0x0, 0x0}, {0x1, 0x0}, 119 | }, 120 | }, 121 | }, 122 | { 123 | DevicePropertyCode: ip.DPC_Fuji_FocusMeteringMode, 124 | DataType: ptp.DTC_UINT32, 125 | GetSet: ptp.DPD_GetSet, 126 | FactoryDefaultValue: []uint8{0x0, 0x0, 0x0, 0x0}, 127 | CurrentValue: []uint8{0x2, 0x7, 0x2, 0x3}, 128 | FormFlag: ptp.DPF_FormFlag_Range, 129 | Form: &ptp.RangeForm{ 130 | MinimumValue: []uint8{0x00, 0x00, 0x00, 0x00}, 131 | MaximumValue: []uint8{0x07, 0x07, 0x09, 0x10}, 132 | StepSize: []uint8{0x01, 0x00, 0x00, 0x00}, 133 | }, 134 | }, 135 | } 136 | 137 | for _, f := range list { 138 | f.Form.SetDevicePropDesc(f) 139 | } 140 | 141 | lj := make([]*DevicePropDescJSON, len(list)) 142 | for i := 0; i < len(list); i++ { 143 | lj[i] = &DevicePropDescJSON{ 144 | DevicePropDesc: list[i], 145 | } 146 | } 147 | 148 | want, err := ioutil.ReadFile("testdata/plain.json") 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | got, err := json.Marshal(lj) 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | if bytes.Compare(got, want) != 0 { 158 | t.Errorf("MarshalJSON() got = %s; want %s", got, want) 159 | } 160 | 161 | want, err = ioutil.ReadFile("testdata/pretty.json") 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | got, err = json.MarshalIndent(lj, "", " ") 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | if bytes.Compare(got, want) != 0 { 170 | t.Errorf("MarshalJSON() got = %s; want %s", got, want) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /ip/mockresponder_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "encoding/binary" 5 | "flag" 6 | "fmt" 7 | "github.com/malc0mn/ptp-ip/ip/internal" 8 | "github.com/malc0mn/ptp-ip/ptp" 9 | "io" 10 | "log" 11 | "net" 12 | "os" 13 | "testing" 14 | ) 15 | 16 | const MockResponderGUID string = "3e8626cc-5059-4225-bdd6-d160b2e6a60f" 17 | 18 | var ( 19 | address = "127.0.0.1" 20 | okPort = DefaultPort 21 | fujiCmdPort uint16 = 55740 22 | fujiEvtPort uint16 = 55741 23 | failPort uint16 = 25740 24 | logLevel = LevelSilent 25 | lgr Logger 26 | ) 27 | 28 | func TestMain(m *testing.M) { 29 | flag.Parse() 30 | 31 | if testing.Verbose() { 32 | logLevel = LevelDebug 33 | } 34 | 35 | lgr = NewLogger(logLevel, os.Stderr, "", log.LstdFlags) 36 | 37 | newLocalOkResponder(DefaultVendor, address, []uint16{okPort}) 38 | newLocalOkResponder("fuji", address, []uint16{fujiCmdPort, fujiEvtPort}) 39 | newLocalFailResponder(address, failPort) 40 | os.Exit(m.Run()) 41 | } 42 | 43 | type msgHandler func(net.Conn, chan uint32, string) 44 | 45 | type MockResponder struct { 46 | vendor ptp.VendorExtension 47 | address string 48 | ports []uint16 49 | handlers []msgHandler 50 | lmp string 51 | } 52 | 53 | func runResponder(vendor ptp.VendorExtension, address string, ports []uint16, handlers []msgHandler, lmp string) { 54 | mr := &MockResponder{ 55 | vendor: vendor, 56 | address: address, 57 | ports: ports, 58 | handlers: handlers, 59 | lmp: lmp, 60 | } 61 | 62 | evtChan := make(chan uint32, 10) 63 | for i, _ := range mr.handlers { 64 | go mr.run(i, evtChan) 65 | } 66 | } 67 | 68 | func newLocalOkResponder(vendor string, address string, ports []uint16) { 69 | var handlers []msgHandler 70 | switch vendor { 71 | case "fuji": 72 | handlers = []msgHandler{handleFujiMessages, handleFujiEvents} 73 | default: 74 | handlers = []msgHandler{handleGenericMessages} 75 | } 76 | 77 | runResponder(ptp.VendorStringToType(vendor), address, ports, handlers, fmt.Sprintf("[Mocked %s OK responder]", vendor)) 78 | } 79 | 80 | func newLocalFailResponder(address string, port uint16) { 81 | runResponder(ptp.VendorExtension(0), address, []uint16{port}, []msgHandler{alwaysFailMessage}, "[Mocked FAIL responder]") 82 | } 83 | 84 | func (mr *MockResponder) run(i int, evtChan chan uint32) { 85 | ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", mr.address, mr.ports[i])) 86 | defer ln.Close() 87 | if err != nil { 88 | lgr.Errorf("%s error %s...", mr.lmp, err) 89 | return 90 | } 91 | lgr.Infof("%s listening on %s...", mr.lmp, ln.Addr().String()) 92 | 93 | for { 94 | conn, err := ln.Accept() 95 | if err != nil { 96 | lgr.Errorf("%s accept error %s...", mr.lmp, err) 97 | continue 98 | } 99 | lgr.Infof("%s new connection %v...", mr.lmp, conn) 100 | go mr.handlers[i](conn, evtChan, mr.lmp) 101 | } 102 | } 103 | 104 | func readMessage(r io.Reader, lmp string) (Header, PacketOut, error) { 105 | var err error 106 | 107 | var h Header 108 | lgr.Infof("%s awaiting packet header...", lmp) 109 | err = binary.Read(r, binary.LittleEndian, &h) 110 | if err != nil { 111 | if err == io.EOF { 112 | lgr.Infof("%s client disconnected", lmp) 113 | } else { 114 | lgr.Errorf("%s error reading header: %s", lmp, err) 115 | } 116 | return h, nil, err 117 | } 118 | pkt, err := NewPacketOutFromPacketType(h.PacketType) 119 | if err != nil { 120 | lgr.Errorf("%s error creating packet: %s", lmp, err) 121 | return h, nil, err 122 | } 123 | 124 | vs := int(h.Length) - HeaderSize - internal.TotalSizeOfFixedFields(pkt) 125 | _, err = internal.UnmarshalLittleEndian(r, pkt, int(h.Length)-HeaderSize, vs) 126 | // TODO: handle byte array being returned? 127 | if err != nil { 128 | lgr.Errorf("%s error reading packet %T data %s", lmp, pkt, err) 129 | return h, nil, err 130 | } 131 | 132 | return h, pkt, nil 133 | } 134 | 135 | func readMessageRaw(r io.Reader, lmp string) (uint32, []byte, error) { 136 | var err error 137 | 138 | var l uint32 139 | lgr.Infof("%s awaiting packet length...", lmp) 140 | err = binary.Read(r, binary.LittleEndian, &l) 141 | if err != nil { 142 | if err == io.EOF { 143 | lgr.Infof("%s client disconnected", lmp) 144 | } else { 145 | lgr.Errorf("%s error reading packet length: %s", lmp, err) 146 | } 147 | return l, nil, err 148 | } 149 | 150 | b := make([]byte, int(l)-4) 151 | if err := binary.Read(r, binary.LittleEndian, &b); err != nil { 152 | lgr.Errorf("%s error reading payload: %s", lmp, err) 153 | return l, nil, err 154 | } 155 | 156 | return l, b, nil 157 | } 158 | 159 | func sendMessage(w io.Writer, pkt Packet, extra []byte, lmp string) { 160 | err := sendAnyPacket(w, pkt, extra, lmp) 161 | if err != nil { 162 | lgr.Errorf("%s error responding: %s", lmp, err) 163 | } 164 | } 165 | 166 | func alwaysFailMessage(conn net.Conn, _ chan uint32, lmp string) { 167 | // TCP connections are closed by the Responder on failure! 168 | defer conn.Close() 169 | if _, pkt, _ := readMessage(conn, lmp); pkt == nil { 170 | return 171 | } 172 | 173 | sendMessage(conn, &InitFailPacket{ 174 | Reason: FR_FailRejectedInitiator, 175 | }, nil, lmp) 176 | } 177 | 178 | func sendAnyPacket(w io.Writer, p Packet, extra []byte, lmp string) error { 179 | lgr.Infof("%s sendAnyPacket() %T", lmp, p) 180 | 181 | pl := internal.MarshalLittleEndian(p) 182 | pll := len(pl) 183 | if extra != nil { 184 | pll += len(extra) 185 | } 186 | 187 | // An invalid packet type means it does not adhere to the PTP/IP standard, so we only send the length field here. 188 | if p.PacketType() == PKT_Invalid { 189 | // Send length only. The length must include the size of the length field, so we add 4 bytes for that! 190 | if _, err := w.Write(internal.MarshalLittleEndian(uint32(pll + 4))); err != nil { 191 | return err 192 | } 193 | } else { 194 | // The packet length MUST include the header, so we add 8 bytes for that! 195 | h := internal.MarshalLittleEndian(Header{uint32(pll + HeaderSize), p.PacketType()}) 196 | 197 | // Send header. 198 | n, err := w.Write(h) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | if n != HeaderSize { 204 | return fmt.Errorf(BytesWrittenMismatch, n, HeaderSize) 205 | } 206 | lgr.Infof("%s sendAnyPacket() header bytes written %d", lmp, n) 207 | } 208 | 209 | // Send payload. 210 | if pll == 0 { 211 | lgr.Infof("%s sendAnyPacket() packet has no payload", lmp) 212 | return nil 213 | } 214 | 215 | n, err := w.Write(pl) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | if extra != nil { 221 | nn, err := w.Write(extra) 222 | if err != nil { 223 | return err 224 | } 225 | n += nn 226 | } 227 | 228 | if n != pll { 229 | return fmt.Errorf(BytesWrittenMismatch, n, pll) 230 | } 231 | 232 | lgr.Infof("%s sendAnyPacket() payload bytes written %d", lmp, n) 233 | 234 | return nil 235 | } 236 | -------------------------------------------------------------------------------- /ip/packets_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | "github.com/malc0mn/ptp-ip/ptp" 7 | "testing" 8 | ) 9 | 10 | func TestNewInitCommandRequestPacket(t *testing.T) { 11 | uuid, _ := uuid.NewRandom() 12 | got := NewInitCommandRequestPacket(uuid, "têst") 13 | want := "têst" 14 | 15 | if got.GetFriendlyName() != want { 16 | t.Errorf("NewInitCommandRequestPacket() FriendlyName = %s; want %s", got.GetFriendlyName(), want) 17 | } 18 | if got.GetProtocolVersion() != PV_VersionOnePointZero { 19 | t.Errorf("NewInitCommandRequestPacket() ProtocolVersion = %#x; want %#x", got.GetProtocolVersion(), PV_VersionOnePointZero) 20 | } 21 | } 22 | 23 | func TestNewInitCommandRequestPacketForClient(t *testing.T) { 24 | c, err := NewClient(DefaultVendor, DefaultIpAddress, DefaultPort, "test", "", logLevel) 25 | if err != nil { 26 | t.Errorf("NewClient() err = %s; want ", err) 27 | } 28 | 29 | got := NewInitCommandRequestPacketForClient(c) 30 | want := "test" 31 | 32 | if got.GetFriendlyName() != want { 33 | t.Errorf("NewInitCommandRequestPacketForClient() FriendlyName = %s; want %s", got.GetFriendlyName(), want) 34 | } 35 | if got.GetProtocolVersion() != PV_VersionOnePointZero { 36 | t.Errorf("NewInitCommandRequestPacketForClient() ProtocolVersion = %#x; want %#x", got.GetProtocolVersion(), PV_VersionOnePointZero) 37 | } 38 | } 39 | 40 | func TestNewInitCommandRequestPacketWithVersion(t *testing.T) { 41 | uuid, _ := uuid.NewRandom() 42 | got := NewInitCommandRequestPacketWithVersion(uuid, "versíon", 0x00020005) 43 | wantName := "versíon" 44 | wantVersion := ProtocolVersion(0x00020005) 45 | 46 | if got.GetFriendlyName() != wantName { 47 | t.Errorf("NewInitCommandRequestPacketWithVersion() FriendlyName = %s; want %s", got.GetFriendlyName(), wantName) 48 | } 49 | if got.GetProtocolVersion() != wantVersion { 50 | t.Errorf("NewInitCommandRequestPacketWithVersion() ProtocolVersion = %#x; want %#x", got.GetProtocolVersion(), wantVersion) 51 | } 52 | } 53 | 54 | func TestNewInitEventRequestPacket(t *testing.T) { 55 | got := NewInitEventRequestPacket(5) 56 | want := uint32(5) 57 | if got.GetConnectionNumber() != want { 58 | t.Errorf("NewInitEventRequestPacket() ConnectionNumber = %#x; want %#x", got.GetConnectionNumber(), want) 59 | } 60 | } 61 | 62 | func TestInitFailPacket_ReasonAsError(t *testing.T) { 63 | errs := map[FailReason]string{ 64 | FR_FailBusy: "busy: too many active connections", 65 | FR_FailRejectedInitiator: "rejected: device not allowed", 66 | FR_FailUnspecified: "reason unspecified", 67 | FR_Fuji_DeviceBusy: "fuji: invalid friendly name or camera state: allow to 'change' client or 'reset' connection", 68 | FR_Fuji_InvalidParameter: "fuji: unknown protocol version", 69 | FailReason(0x5032000): "unknown failure reason returned 0x5032000", 70 | } 71 | 72 | for reason, want := range errs { 73 | ifp := InitFailPacket{ 74 | Reason: reason, 75 | } 76 | got := ifp.ReasonAsError().Error() 77 | if got != want { 78 | t.Errorf("ReasonAsError() Reason = '%s'; want '%s'", got, want) 79 | } 80 | } 81 | } 82 | 83 | func TestNewPacketOutFromPacketType(t *testing.T) { 84 | types := map[PacketType]string{ 85 | PKT_InitCommandRequest: "GenericInitCommandRequest", 86 | PKT_InitEventRequest: "GenericInitEventRequest", 87 | PKT_OperationRequest: "OperationRequest", 88 | PKT_StartData: "StartData", 89 | PKT_Data: "Data", 90 | PKT_Cancel: "Cancel", 91 | PKT_EndData: "EndData", 92 | PKT_ProbeRequest: "ProbeRequest", 93 | PKT_ProbeResponse: "ProbeResponse", 94 | } 95 | 96 | for typ, want := range types { 97 | got, err := NewPacketOutFromPacketType(typ) 98 | want := fmt.Sprintf("*ip.%sPacket", want) 99 | if err != nil { 100 | t.Errorf("NewPacketOutFromPacketType() err = %s; want ", err) 101 | } 102 | 103 | name := fmt.Sprintf("%T", got) 104 | if name != want { 105 | t.Errorf("NewPacketOutFromPacketType() returned %s; want %s", name, want) 106 | } 107 | 108 | if got.PacketType() != typ { 109 | t.Errorf("NewPacketOutFromPacketType() type = %#x; want %#x", got.PacketType(), typ) 110 | } 111 | } 112 | } 113 | 114 | func TestNewPacketOutFromPacketTypeFail(t *testing.T) { 115 | types := []PacketType{ 116 | PKT_InitCommandAck, 117 | PKT_InitEventAck, 118 | PKT_InitFail, 119 | PKT_OperationResponse, 120 | PKT_Event, 121 | } 122 | for _, typ := range types { 123 | got, err := NewPacketOutFromPacketType(typ) 124 | if err == nil { 125 | t.Errorf("NewPacketOutFromPacketType() err = %s; want unknown packet type %#x", err, typ) 126 | } 127 | if got != nil { 128 | t.Errorf("NewPacketOutFromPacketType() got = %T; want ", got) 129 | } 130 | } 131 | } 132 | 133 | func TestNewPacketInFromPacketType(t *testing.T) { 134 | types := map[PacketType]string{ 135 | PKT_InitCommandAck: "InitCommandAck", 136 | PKT_InitEventAck: "InitEventAck", 137 | PKT_InitFail: "InitFail", 138 | PKT_OperationResponse: "OperationResponse", 139 | PKT_Event: "GenericEvent", 140 | PKT_StartData: "StartData", 141 | PKT_Data: "Data", 142 | PKT_Cancel: "Cancel", 143 | PKT_EndData: "EndData", 144 | PKT_ProbeRequest: "ProbeRequest", 145 | PKT_ProbeResponse: "ProbeResponse", 146 | } 147 | 148 | for typ, want := range types { 149 | got, err := NewPacketInFromPacketType(typ) 150 | want := fmt.Sprintf("*ip.%sPacket", want) 151 | if err != nil { 152 | t.Errorf("NewPacketInFromPacketType() err = %s; want ", err) 153 | } 154 | 155 | name := fmt.Sprintf("%T", got) 156 | if name != want { 157 | t.Errorf("NewPacketInFromPacketType() returned %s; want %s", name, want) 158 | } 159 | 160 | if got.PacketType() != typ { 161 | t.Errorf("NewPacketInFromPacketType() type = %#x; want %#x", got.PacketType(), typ) 162 | } 163 | } 164 | } 165 | 166 | func TestNewPacketInFromPacketTypeFail(t *testing.T) { 167 | types := []PacketType{ 168 | PKT_InitCommandRequest, 169 | PKT_InitEventRequest, 170 | PKT_OperationRequest, 171 | } 172 | for _, typ := range types { 173 | got, err := NewPacketInFromPacketType(typ) 174 | if err == nil { 175 | t.Errorf("NewPacketInFromPacketType() err = %s; want unknown packet type %#x", err, typ) 176 | } 177 | if got != nil { 178 | t.Errorf("NewPacketInFromPacketType() got = %T; want ", got) 179 | } 180 | } 181 | } 182 | 183 | func TestOperationRequestPacket_Payload(t *testing.T) { 184 | oreq := &OperationRequestPacket{ 185 | DataPhaseInfo: DP_NoDataOrDataIn, 186 | OperationRequest: ptp.GetDeviceInfo(2), 187 | } 188 | 189 | pl := oreq.Payload() 190 | got := fmt.Sprintf("%.8b", pl) 191 | want := "[00000001 00000000 00000000 00000000 00000001 00010000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000]" 192 | if got != want { 193 | t.Errorf("payload() buffer = %s; want %s", got, want) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /ptp/events.go: -------------------------------------------------------------------------------- 1 | package ptp 2 | 3 | // The most significant nibble (4 bits) is used to indicate the category of the code and whether the code value is 4 | // standard or vendor-extended: 0100 = standard, 1100 = vendor-extended. 5 | type EventCode uint16 6 | 7 | const ( 8 | EC_Undefined EventCode = 0x4000 9 | 10 | // EC_CancelTransaction is used to cancel a transaction for transports that do not have a specified or standard way 11 | // of canceling transactions. The particular method used to cancel transactions may be ip-specific. When an 12 | // Initiator or Responder receives a CancelTransaction event, it should abort the transaction referred to by the 13 | // TransactionID in the event dataset. If that transaction is already complete, the event should be ignored. 14 | // After receiving a CancelTransfer event from the Initiator, the Responder shall send an IncompleteTransfer 15 | // response for the operation that was cancelled. Both devices will then be ready for the next transaction. 16 | EC_CancelTransaction EventCode = 0x4001 17 | 18 | // EC_ObjectAdded indicates a new data object was added to the device. The new handle assigned by the device to the 19 | // new object should be passed in the Parameter1 field of the event. If more than one object was added, each new 20 | // object should generate a separate ObjectAdded event. The appearance of a new store on the device should not cause 21 | // the creation of new ObjectAdded events for the new objects present on the new store, but should instead cause the 22 | // generation of a StoreAdded event. 23 | EC_ObjectAdded EventCode = 0x4002 24 | 25 | // EC_ObjectRemoved informs a data object was removed from the device unexpectedly due to something external to the 26 | // current session. The handle of the object that was removed should be passed in the Parameter1 field of the event. 27 | // If more than one image was removed, the separate ObjectRemoved events should be generated for each. If the data 28 | // object that was removed was removed because of a previous operation that is a part of this session, no event 29 | // needs to be sent to the opposing device. The removal of a store on the device should not cause the creation of 30 | // ObjectRemoved events for the objects present on the removed store, but should instead cause the generation of one 31 | // StoreRemoved event with the appropriate PhysicalStorageID. 32 | EC_ObjectRemoved EventCode = 0x4003 33 | 34 | // EC_StoreAdded tells a new store was added to the device. If this is a new physical store that contains only one 35 | // logical store, then the complete StorageID of the new store should be indicated in the first parameter. If the 36 | // new store contains more than one logical store, then the first parameter should be set to 0x00000000. This 37 | // indicates that the list of StorageIDs should be re-obtained using the GetStorageIDs operation, and examined 38 | // appropriately. Any new StorageIDs discovered should result in the appropriate invocations of GetStorageInfo 39 | // operations. 40 | EC_StoreAdded EventCode = 0x4004 41 | 42 | // EC_StoreRemoved indicates the stores that are no longer available. The opposing device may assume that the 43 | // StorageInfo datasets and ObjectHandles associated with those stores are no longer valid. The first parameter is 44 | // used to indicate the StorageID of the store that is no longer available. If the store removed is only a single 45 | // logical store within a physical store, the entire StorageID should be sent, which indicates that any other 46 | // logical stores on that physical store are still available. If the physical store and all logical stores upon it 47 | // are removed (e.g. removal of an ejectable media with multiple partitions), the first parameter should contain the 48 | // PhysicalStorageID in the most significant sixteen bits, with the least significant sixteen bits set to 0xFFFF. 49 | EC_StoreRemoved EventCode = 0x4005 50 | 51 | // EC_DevicePropChanged informs that a property changed on the device due to something external to this session. The 52 | // appropriate property dataset should be requested from the opposing device. 53 | EC_DevicePropChanged EventCode = 0x4006 54 | 55 | // Indicates that the ObjectInfo dataset for a particular object has changed, and that it should be re-requested. 56 | EC_ObjectInfoChanged EventCode = 0x4007 57 | 58 | // EC_DeviceInfoChanged indicates that the capabilities of the device have changed, and that the DeviceInfo should 59 | // be re-requested. This may be caused by the device going into or out of a sleep state, or by the device losing or 60 | // gaining some functionality in some way. 61 | EC_DeviceInfoChanged EventCode = 0x4008 62 | 63 | // EC_RequestObjectTransfer can be used by a Responder to ask the Initiator to initiate a GetObject operation on the 64 | // handle specified in the first parameter. This allows for push-mode to be enabled on devices/transports that are 65 | // intrinsically pull mode. 66 | EC_RequestObjectTransfer EventCode = 0x4009 67 | 68 | // EC_StoreFull shall be sent when a store becomes full. Any multi-object capture that may be occurring should 69 | // retain the objects that were written to a store before the store became full. 70 | EC_StoreFull EventCode = 0x400A 71 | 72 | // EC_DeviceReset needs only to be supported for devices that support multiple sessions or in the case if the device 73 | // is capable of resetting itself automatically or manually through user intervention while connected. This event 74 | // shall be sent to all open sessions other than the session that initiated the operation. This event shall be 75 | // interpreted as indicating that the sessions are about to be closed. 76 | EC_DeviceReset EventCode = 0x400B 77 | 78 | // EC_StorageInfoChanged is used when information in the StorageInfo dataset for a store changes. This can occur due 79 | // to device properties changing, such as ImageSize, which can cause changes in fields such as FreeSpaceInImages. 80 | // This event is typically not needed if the change is caused by an in-session operation that affects whole objects 81 | // in a deterministic manner. This includes changes in FreeSpaceInImages or FreeSpaceInBytes caused by operations 82 | // such as InitiateCapture or CopyObject, where the Initiator can recognize the changes due to the successful 83 | // response code of the operation, and/or related required events. 84 | EC_StorageInfoChanged EventCode = 0x400C 85 | 86 | // EC_CaptureComplete is used to indicate that a capture session, previously initiated by the InitiateCapture 87 | // operation, is complete, and that no more ObjectAdded events will occur as the result of this asynchronous 88 | // operation. This operation is not used for InitiateOpenCapture operations. 89 | EC_CaptureComplete EventCode = 0x400D 90 | 91 | // EC_UnreportedStatus may be implemented for certain transports where situations can arise where the Responder was 92 | // unable to report events to the Initiator regarding changes in its internal status. When an Initiator receives 93 | // this event, it is responsible for doing whatever is necessary to ensure that its knowledge of the Responder is up 94 | // to date. This may include re-obtaining individual datasets, ObjectHandle lists, etc., or may even result in the 95 | // session being closed and re-opened. This event is typically only needed in situations where the ip used by the 96 | // device supports a suspend/resume/remote-wakeup feature and the Responder has gone into a suspend state and has 97 | // been unable to report state changes during that time period. This prevents the need for queuing of these 98 | // unreportable events. The details of the use of this event are ip-specific and should be fully specified in 99 | // the specific ip implementation specification. 100 | EC_UnreportedStatus EventCode = 0x400E 101 | ) 102 | 103 | type Event struct { 104 | // Indicates the event. 105 | EventCode EventCode 106 | 107 | // Indicates the SessionID of the session for which the event is relevant. If the event is relevant to all open 108 | // sessions, this field should be set to 0xFFFFFFFF. 109 | SessionID SessionID 110 | 111 | // If the event corresponds to a previously initiated transaction, this field shall hold the TransactionID of that 112 | // operation. If the event is not specific to a particular transaction, this field shall be set to 0xFFFFFFFF. 113 | TransactionID TransactionID 114 | 115 | // These fields hold the event-specific nth parameter. Events may have at most three parameters. The interpretation 116 | // of any parameter is dependent upon the EventCode. Any unused parameter fields should be set to 0x00000000. If a 117 | // parameter holds a value that is less than 32 bits, the lowest significant bits shall be used to store the value, 118 | // with the most significant bits being set to zeros. 119 | Parameter1 []byte 120 | Parameter2 []byte 121 | Parameter3 []byte 122 | } 123 | 124 | func (e *Event) Session() SessionID { 125 | return e.SessionID 126 | } 127 | -------------------------------------------------------------------------------- /fmt/string_fuji.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | "github.com/malc0mn/ptp-ip/ptp" 7 | "strconv" 8 | ) 9 | 10 | func FujiDevicePropCodeAsString(code ptp.DevicePropCode) string { 11 | switch code { 12 | case ip.DPC_Fuji_FilmSimulation: 13 | return "film simulation" 14 | case ip.DPC_Fuji_ImageQuality: 15 | return "image quality" 16 | case ip.DPC_Fuji_RecMode: 17 | return "rec mode" 18 | case ip.DPC_Fuji_CommandDialMode: 19 | return "command dial mode" 20 | case ip.DPC_Fuji_ExposureIndex: 21 | return "ISO" 22 | case ip.DPC_Fuji_MovieISO: 23 | return "movie ISO" 24 | case ip.DPC_Fuji_FocusMeteringMode: 25 | return "focus point" 26 | case ip.DPC_Fuji_FocusLock: 27 | return "focus lock" 28 | case ip.DPC_Fuji_DeviceError: 29 | return "device error" 30 | case ip.DPC_Fuji_CapturesRemaining: 31 | return "captures remaining" 32 | case ip.DPC_Fuji_MovieRemainingTime: 33 | return "movie remaining time" 34 | case ip.DPC_Fuji_ShutterSpeed: 35 | return "shutter speed" 36 | case ip.DPC_Fuji_ImageAspectRatio: 37 | return "image size" 38 | case ip.DPC_Fuji_BatteryLevel: 39 | return "battery level" 40 | case ip.DPC_Fuji_InitSequence: 41 | return "init sequence" 42 | case ip.DPC_Fuji_AppVersion: 43 | return "app version" 44 | default: 45 | return GenericDevicePropCodeAsString(code) 46 | } 47 | } 48 | 49 | // FujiPropToDevicePropCode converts a standardised property string to a valid ptp.DevicePropertyCode. 50 | func FujiPropToDevicePropCode(field string) (ptp.DevicePropCode, error) { 51 | switch field { 52 | case PRP_ISO: 53 | return ip.DPC_Fuji_ExposureIndex, nil 54 | case PRP_Effect: 55 | return ip.DPC_Fuji_FilmSimulation, nil 56 | case "recmode": 57 | return ip.DPC_Fuji_RecMode, nil 58 | case PRP_FocusMeteringMode: 59 | return ip.DPC_Fuji_FocusMeteringMode, nil 60 | default: 61 | return GenericPropToDevicePropCode(field) 62 | } 63 | } 64 | 65 | func FujiDevicePropValueAsString(code ptp.DevicePropCode, v int64) string { 66 | switch code { 67 | case ptp.DPC_BatteryLevel, ip.DPC_Fuji_BatteryLevel: 68 | return FujiBatteryLevelAsString(ip.FujiBatteryLevel(v)) 69 | case ip.DPC_Fuji_CommandDialMode: 70 | return FujiCommandDialModeAsString(ip.FujiCommandDialMode(v)) 71 | case ip.DPC_Fuji_DeviceError: 72 | return FujiDeviceErrorAsString(ip.FujiDeviceError(v)) 73 | case ip.DPC_Fuji_ExposureIndex: 74 | return FujiExposureIndexAsString(ip.FujiExposureIndex(v)) 75 | case ip.DPC_Fuji_FilmSimulation: 76 | return FujiFilmSimulationAsString(ip.FujiFilmSimulation(v)) 77 | case ptp.DPC_FlashMode: 78 | return FujiFlashModeAsString(ptp.FlashMode(v)) 79 | case ip.DPC_Fuji_FocusLock: 80 | return FujiFocusLockAsString(ip.FujiFocusLock(v)) 81 | case ip.DPC_Fuji_FocusMeteringMode: 82 | return FujiFocusMeteringModeAsString(uint32(v)) 83 | case ptp.DPC_FocusMode: 84 | return FujiFocusModeAsString(ptp.FocusMode(v)) 85 | case ip.DPC_Fuji_ImageAspectRatio: 86 | return FujiImageAspectRatioAsString(ip.FujiImageSize(v)) 87 | case ip.DPC_Fuji_ImageQuality: 88 | return FujiImageQualityAsString(ip.FujiImageQuality(v)) 89 | case ptp.DPC_WhiteBalance: 90 | return FujiWhiteBalanceAsString(ptp.WhiteBalance(v)) 91 | case ptp.DPC_CaptureDelay: 92 | return FujiSelfTimerAsString(ip.FujiSelfTimer(v)) 93 | default: 94 | return DevicePropValueAsString(code, v) 95 | } 96 | } 97 | 98 | func FujiBatteryLevelAsString(bat ip.FujiBatteryLevel) string { 99 | switch bat { 100 | case ip.BAT_Fuji_3bOne: 101 | return "1/3" 102 | case ip.BAT_Fuji_3bTwo: 103 | return "2/3" 104 | case ip.BAT_Fuji_3bFull: 105 | return "3/3" 106 | case ip.BAT_Fuji_5bCritical: 107 | return "critical" 108 | case ip.BAT_Fuji_5bOne: 109 | return "1/5" 110 | case ip.BAT_Fuji_5bTwo: 111 | return "2/5" 112 | case ip.BAT_Fuji_5bThree: 113 | return "3/5" 114 | case ip.BAT_Fuji_5bFour: 115 | return "4/5" 116 | case ip.BAT_Fuji_5bFull: 117 | return "5/5" 118 | default: 119 | return "" 120 | } 121 | } 122 | 123 | func FujiCommandDialModeAsString(cmd ip.FujiCommandDialMode) string { 124 | switch cmd { 125 | case ip.CMD_Fuji_Both: 126 | return "both" 127 | case ip.CMD_Fuji_Aperture: 128 | return "aperture" 129 | case ip.CMD_Fuji_ShutterSpeed: 130 | return "shutter speed" 131 | case ip.CMD_Fuji_None: 132 | return "none" 133 | default: 134 | return "" 135 | } 136 | } 137 | 138 | func FujiDeviceErrorAsString(de ip.FujiDeviceError) string { 139 | switch de { 140 | case ip.DE_Fuji_None: 141 | return "none" 142 | default: 143 | return "" 144 | } 145 | } 146 | 147 | func FujiExposureIndexAsString(edx ip.FujiExposureIndex) string { 148 | if edx == ip.EDX_Fuji_Auto { 149 | return "auto" 150 | } 151 | 152 | prefix := "" 153 | val := int64(edx & 0x0000FFFF) 154 | 155 | switch uint16(edx >> 16) { 156 | case ip.EDX_Fuji_Extended: 157 | prefix = "H" 158 | if val < 200 { 159 | prefix = "L" 160 | } 161 | case ip.EDX_Fuji_MaxSensitivity: 162 | prefix = "S" 163 | } 164 | 165 | if prefix == "" { 166 | return strconv.FormatInt(val, 10) 167 | } 168 | 169 | return fmt.Sprintf("%s%d", prefix, val) 170 | } 171 | 172 | func FujiFilmSimulationAsString(fs ip.FujiFilmSimulation) string { 173 | switch fs { 174 | case ip.FS_Fuji_Provia: 175 | return "PROVIA" 176 | case ip.FS_Fuji_Velvia: 177 | return "Velvia" 178 | case ip.FS_Fuji_Astia: 179 | return "ASTIA" 180 | case ip.FS_Fuji_Monochrome: 181 | return "Monochrome" 182 | case ip.FS_Fuji_Sepia: 183 | return "Sepia" 184 | case ip.FS_Fuji_ProNegHigh: 185 | return "PRO Neg. Hi" 186 | case ip.FS_Fuji_ProNegStandard: 187 | return "PRO Neg. Std" 188 | case ip.FS_Fuji_MonochromeYeFilter: 189 | return "Monochrome + Ye Filter" 190 | case ip.FS_Fuji_MonochromeRFilter: 191 | return "Monochrome + R Filter" 192 | case ip.FS_Fuji_MonochromeGFilter: 193 | return "Monochrome + G Filter" 194 | case ip.FS_Fuji_ClassicChrome: 195 | return "Classic Chrome" 196 | case ip.FS_Fuji_ACROS: 197 | return "ACROS" 198 | case ip.FS_Fuji_ACROSYe: 199 | return "ACROS Ye" 200 | case ip.FS_Fuji_ACROSR: 201 | return "ACROS R" 202 | case ip.FS_Fuji_ACROSG: 203 | return "ACROS G" 204 | case ip.FS_Fuji_ETERNA: 205 | return "ETERNA" 206 | default: 207 | return "" 208 | } 209 | } 210 | 211 | func FujiFlashModeAsString(mode ptp.FlashMode) string { 212 | switch mode { 213 | case ip.FM_Fuji_On: 214 | return "on" 215 | case ip.FM_Fuji_RedEye: 216 | return "red eye" 217 | case ip.FM_Fuji_RedEyeOn: 218 | return "red eye on" 219 | case ip.FM_Fuji_RedEyeSync: 220 | return "red eye sync" 221 | case ip.FM_Fuji_RedEyeRear: 222 | return "red eye rear" 223 | case ip.FM_Fuji_SlowSync: 224 | return "slow sync" 225 | case ip.FM_Fuji_RearSync: 226 | return "rear sync" 227 | case ip.FM_Fuji_Commander: 228 | return "commander" 229 | case ip.FM_Fuji_Disabled: 230 | return "disabled" 231 | case ip.FM_Fuji_Enabled: 232 | return "enabled" 233 | default: 234 | return FlashModeAsString(mode) 235 | } 236 | } 237 | 238 | func FujiFocusLockAsString(fl ip.FujiFocusLock) string { 239 | switch fl { 240 | case ip.FL_Fuji_On: 241 | return "on" 242 | case ip.FL_Fuji_Off: 243 | return "off" 244 | default: 245 | return "" 246 | } 247 | } 248 | 249 | func FujiFocusMeteringModeAsString(fmm uint32) string { 250 | // TODO: what are the 4 msb? 251 | mask := uint32(0x000000FF) 252 | x := fmm >> 8 & mask 253 | y := fmm & mask 254 | 255 | return fmt.Sprintf("%dx%d", x, y) 256 | } 257 | 258 | func FujiFocusModeAsString(fm ptp.FocusMode) string { 259 | switch fm { 260 | case ip.FCM_Fuji_Single_Auto: 261 | return "single auto" 262 | case ip.FCM_Fuji_Continuous_Auto: 263 | return "continuous auto" 264 | default: 265 | return "" 266 | } 267 | } 268 | 269 | func FujiImageAspectRatioAsString(is ip.FujiImageSize) string { 270 | switch is { 271 | case ip.IS_Fuji_Small_3x2: 272 | return "S 3:2" 273 | case ip.IS_Fuji_Small_16x9: 274 | return "S 16:9" 275 | case ip.IS_Fuji_Small_1x1: 276 | return "S 1:1" 277 | case ip.IS_Fuji_Medium_3x2: 278 | return "M 3:2" 279 | case ip.IS_Fuji_Medium_16x9: 280 | return "M 16:9" 281 | case ip.IS_Fuji_Medium_1x1: 282 | return "M 1:1" 283 | case ip.IS_Fuji_Large_3x2: 284 | return "L 3:2" 285 | case ip.IS_Fuji_Large_16x9: 286 | return "L 16:9" 287 | case ip.IS_Fuji_Large_1x1: 288 | return "L 1:1" 289 | default: 290 | return "" 291 | } 292 | } 293 | 294 | func FujiImageQualityAsString(iq ip.FujiImageQuality) string { 295 | switch iq { 296 | case ip.IQ_Fuji_Fine: 297 | return "fine" 298 | case ip.IQ_Fuji_Normal: 299 | return "normal" 300 | case ip.IQ_Fuji_FineAndRAW: 301 | return "fine + RAW" 302 | case ip.IQ_Fuji_NormalAndRAW: 303 | return "normal + RAW" 304 | default: 305 | return "" 306 | } 307 | } 308 | 309 | func FujiWhiteBalanceAsString(wb ptp.WhiteBalance) string { 310 | switch wb { 311 | case ip.WB_Fuji_Fluorescent1: 312 | return "fluorescent 1" 313 | case ip.WB_Fuji_Fluorescent2: 314 | return "fluorescent 2" 315 | case ip.WB_Fuji_Fluorescent3: 316 | return "fluorescent 3" 317 | case ip.WB_Fuji_Shade: 318 | return "shade" 319 | case ip.WB_Fuji_Underwater: 320 | return "underwater" 321 | case ip.WB_Fuji_Temperature: 322 | return "temprerature" 323 | case ip.WB_Fuji_Custom: 324 | return "custom" 325 | default: 326 | return WhiteBalanceAsString(wb) 327 | } 328 | } 329 | 330 | // TODO: FujiRecModeAsString(rm ip.FujiRecMode) 331 | 332 | func FujiSelfTimerAsString(st ip.FujiSelfTimer) string { 333 | switch st { 334 | case ip.ST_Fuji_1Sec: 335 | return "1 second" 336 | case ip.ST_Fuji_2Sec: 337 | return "2 seconds" 338 | case ip.ST_Fuji_5Sec: 339 | return "5 seconds" 340 | case ip.ST_Fuji_10Sec: 341 | return "10 seconds" 342 | case ip.ST_Fuji_Off: 343 | return "off" 344 | default: 345 | return "" 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /ip/vendor_extensions.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "github.com/google/uuid" 8 | "github.com/malc0mn/ptp-ip/ip/internal" 9 | "github.com/malc0mn/ptp-ip/ptp" 10 | ) 11 | 12 | // TODO: This solution is not OK, vendors can differ massively so it seems. Should this become an interface that all 13 | // vendors need to implement...? It would turn out to be a huge interface, so there will no doubt be a better solution? 14 | // Embedding ip.Client in a struct ip.FujiClient wasn't that good either. Take the Dial() method for example, this 15 | // calls the initCommandDataConn() and initEventConn() methods but when using embedding the methods on ip.Client get 16 | // called and not the ones on ip.FujiClient so you would also have to "override" the Dial() as well. 17 | type VendorExtensions struct { 18 | cmdDataInit func(*Client) error 19 | eventInit func(*Client) error 20 | processStreamData func(*Client) error 21 | newCmdDataInitPacket func(uuid.UUID, string) InitCommandRequestPacket 22 | newEventInitPacket func(uint32) InitEventRequestPacket 23 | newEventPacket func() EventPacket 24 | extractTransactionId func([]byte, connectionType) (ptp.TransactionID, error) 25 | getDeviceInfo func(*Client) (interface{}, error) 26 | getDeviceState func(*Client) (interface{}, error) 27 | getDevicePropertyDesc func(*Client, ptp.DevicePropCode) (*ptp.DevicePropDesc, error) 28 | getDevicePropertyValue func(*Client, ptp.DevicePropCode) (uint32, error) 29 | setDeviceProperty func(*Client, ptp.DevicePropCode, uint32) error 30 | operationRequestRaw func(*Client, ptp.OperationCode, []uint32) ([][]byte, error) 31 | initiateCapture func(*Client) ([]byte, error) 32 | } 33 | 34 | func (c *Client) loadVendorExtensions() { 35 | c.vendorExtensions = &VendorExtensions{ 36 | cmdDataInit: GenericInitCommandDataConn, 37 | eventInit: GenericInitEventConn, 38 | processStreamData: GenericProcessStreamData, 39 | newCmdDataInitPacket: NewInitCommandRequestPacket, 40 | newEventInitPacket: NewInitEventRequestPacket, 41 | newEventPacket: NewEventPacket, 42 | extractTransactionId: GenericExtractTransactionId, 43 | getDeviceInfo: GenericGetDeviceInfo, 44 | getDeviceState: GenericGetDeviceState, 45 | getDevicePropertyDesc: GenericGetDevicePropertyDesc, 46 | getDevicePropertyValue: GenericGetDevicePropertyValue, 47 | setDeviceProperty: GenericSetDeviceProperty, 48 | operationRequestRaw: GenericOperationRequestRaw, 49 | initiateCapture: GenericInitiateCapture, 50 | } 51 | 52 | switch c.ResponderVendor() { 53 | case ptp.VE_FujiPhotoFilmCoLtd: 54 | c.vendorExtensions.cmdDataInit = FujiInitCommandDataConn 55 | c.vendorExtensions.processStreamData = FujiProcessStreamData 56 | c.vendorExtensions.newCmdDataInitPacket = NewFujiInitCommandRequestPacket 57 | c.vendorExtensions.newEventInitPacket = NewFujiInitEventRequestPacket 58 | c.vendorExtensions.newEventPacket = NewFujiEventPacket 59 | c.vendorExtensions.extractTransactionId = FujiExtractTransactionId 60 | c.vendorExtensions.getDeviceInfo = FujiGetDeviceInfo 61 | c.vendorExtensions.getDeviceState = FujiGetDeviceState 62 | c.vendorExtensions.getDevicePropertyDesc = FujiGetDevicePropertyDesc 63 | c.vendorExtensions.getDevicePropertyValue = FujiGetDevicePropertyValue 64 | c.vendorExtensions.setDeviceProperty = FujiSetDeviceProperty 65 | c.vendorExtensions.operationRequestRaw = FujiSendOperationRequestAndGetRawResponse 66 | c.vendorExtensions.initiateCapture = FujiInitiateCapture 67 | } 68 | } 69 | 70 | // GenericInitCommandDataConn initiates the command/data connection. It expects an open TCP connection to the 71 | // command/data port to be present. 72 | func GenericInitCommandDataConn(c *Client) error { 73 | err := c.SendPacketToCmdDataConn(c.newCmdDataInitPacket()) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | res, _, err := c.waitForPacketFromCmdDataConn(nil) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | switch pkt := res.(type) { 84 | case *InitFailPacket: 85 | err = pkt.ReasonAsError() 86 | case *InitCommandAckPacket: 87 | c.connectionNumber = pkt.ConnectionNumber 88 | c.responder.GUID = pkt.ResponderGUID 89 | c.responder.FriendlyName = pkt.ResponderFriendlyName 90 | c.responder.ProtocolVersion = pkt.ResponderProtocolVersion 91 | go c.responseListener() 92 | return nil 93 | default: 94 | err = fmt.Errorf("unexpected packet received %T", res) 95 | } 96 | 97 | c.Infoln("Closing Command/Data connection!") 98 | c.commandDataConn.Close() 99 | return err 100 | } 101 | 102 | // GenericInitEventConn initiates the event connection. 103 | func GenericInitEventConn(c *Client) error { 104 | var err error 105 | 106 | c.eventConn, err = internal.RetryDialer(c.Network(), c.EventAddress(), DefaultDialTimeout) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | c.configureTcpConn(eventConnection) 112 | 113 | ierp := c.newEventInitPacket() 114 | if ierp == nil { 115 | c.Info("No further event channel init required.") 116 | return nil 117 | } 118 | err = c.SendPacketToEventConn(ierp) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | res, _, err := c.waitForPacketFromEventConn(nil) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | switch pkt := res.(type) { 129 | case *InitFailPacket: 130 | err = pkt.ReasonAsError() 131 | case *InitEventAckPacket: 132 | c.incrementTransactionId() 133 | return nil 134 | default: 135 | err = fmt.Errorf("unexpected packet received %T", res) 136 | } 137 | 138 | c.Infoln("Closing Event connection!") 139 | c.eventConn.Close() 140 | return err 141 | } 142 | 143 | // GenericProcessStreamData does absolutely nothing since the standard PTP/IP protocol does not have a streamer 144 | // connection. 145 | func GenericProcessStreamData(_ *Client) error { 146 | return nil 147 | } 148 | 149 | // GenericExtractTransactionId extracts the transaction ID from a full raw inbound packet. This packet must include the 150 | // full header containing length and packet type. 151 | func GenericExtractTransactionId(p []byte, _ connectionType) (ptp.TransactionID, error) { 152 | if len(p) < 13 { 153 | return 0, fmt.Errorf("packet too small: got length %d", len(p)) 154 | } 155 | 156 | var data []byte 157 | pt := PacketType(binary.LittleEndian.Uint32(p[4:8])) 158 | switch pt { 159 | case PKT_OperationResponse, PKT_Event: 160 | data = p[10:14] 161 | case PKT_StartData, PKT_Data, PKT_EndData, PKT_Cancel: 162 | data = p[8:12] 163 | // TODO: PKT_ProbeRequest and PKT_ProbeResponse do not have a transaction ID, how to handle those? 164 | } 165 | 166 | return ptp.TransactionID(binary.LittleEndian.Uint32(data)), nil 167 | } 168 | 169 | // Request the Responder's device information. 170 | func GenericGetDeviceInfo(c *Client) (interface{}, error) { 171 | tid := c.incrementTransactionId() 172 | 173 | resCh := make(chan []byte, 2) 174 | defer close(resCh) 175 | if err := c.subscribe(tid, resCh); err != nil { 176 | return nil, err 177 | } 178 | 179 | err := c.SendPacketToCmdDataConn(&OperationRequestPacket{ 180 | DataPhaseInfo: DP_NoDataOrDataIn, 181 | OperationRequest: ptp.GetDeviceInfo(tid), 182 | }) 183 | 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | res, _, err := c.WaitForPacketFromCommandDataSubscriber(resCh, nil) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | switch pkt := res.(type) { 194 | case *OperationResponsePacket: 195 | return pkt, nil 196 | default: 197 | err = fmt.Errorf("unexpected packet received %T", res) 198 | } 199 | 200 | return nil, err 201 | } 202 | 203 | // GenericGetDeviceState requests the Responder's device status. 204 | func GenericGetDeviceState(_ *Client) (interface{}, error) { 205 | return nil, errors.New("command not supported") 206 | } 207 | 208 | // GenericGetDevicePropertyValue requests the value for the given property from the Responder's. 209 | func GenericGetDevicePropertyDesc(c *Client, dpc ptp.DevicePropCode) (*ptp.DevicePropDesc, error) { 210 | return nil, errors.New("command not YET supported") 211 | } 212 | 213 | // GenericGetDevicePropertyValue requests the value for the given property from the Responder's. 214 | func GenericGetDevicePropertyValue(c *Client, dpc ptp.DevicePropCode) (uint32, error) { 215 | return 0, errors.New("command not YET supported") 216 | } 217 | 218 | // GenericSetDeviceProperty sets the value for the given property on the Responder. 219 | func GenericSetDeviceProperty(c *Client, dpc ptp.DevicePropCode, val uint32) error { 220 | return errors.New("command not YET supported") 221 | } 222 | 223 | func GenericOperationRequestRaw(c *Client, code ptp.OperationCode, params []uint32) ([][]byte, error) { 224 | tid := c.incrementTransactionId() 225 | 226 | or := ptp.OperationRequest{ 227 | OperationCode: code, 228 | TransactionID: tid, 229 | } 230 | 231 | // TODO: how to eliminate this crazyness WITHOUT reflection? Rework the OperationRequest struct perhaps with a 232 | // [5]interface{} instead of 5 separate fields...? 233 | if len(params) >= 1 { 234 | or.Parameter1 = params[0] 235 | } 236 | if len(params) >= 2 { 237 | or.Parameter2 = params[1] 238 | } 239 | if len(params) >= 3 { 240 | or.Parameter3 = params[2] 241 | } 242 | if len(params) >= 4 { 243 | or.Parameter4 = params[3] 244 | } 245 | if len(params) == 5 { 246 | or.Parameter5 = params[4] 247 | } 248 | resCh := make(chan []byte, 2) 249 | if err := c.subscribe(tid, resCh); err != nil { 250 | return nil, err 251 | } 252 | defer c.unsubscribe(tid) 253 | 254 | err := c.SendPacketToCmdDataConn(&OperationRequestPacket{ 255 | DataPhaseInfo: DP_NoDataOrDataIn, 256 | OperationRequest: or, 257 | }) 258 | 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | var raw [][]byte 264 | raw[0], err = c.WaitForRawPacketFromCommandDataSubscriber(resCh) 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | // TODO: handle possible followup packets depending on the data phase returned. 270 | 271 | return raw, err 272 | } 273 | 274 | func GenericInitiateCapture(c *Client) ([]byte, error) { 275 | return nil, errors.New("command not YET supported") 276 | } 277 | -------------------------------------------------------------------------------- /fmt/string_generic.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | "math" 7 | "strconv" 8 | ) 9 | 10 | // GenericDevicePropCodeAsString returns the DevicePropCode as string. When the DevicePropCode is unknown, it returns an empty 11 | // string. 12 | func GenericDevicePropCodeAsString(code ptp.DevicePropCode) string { 13 | switch code { 14 | case ptp.DPC_BatteryLevel: 15 | return "battery level" 16 | case ptp.DPC_FunctionalMode: 17 | return "functional mode" 18 | case ptp.DPC_ImageSize: 19 | return "image size" 20 | case ptp.DPC_CompressionSetting: 21 | return "compression setting" 22 | case ptp.DPC_WhiteBalance: 23 | return "white balance" 24 | case ptp.DPC_RGBGain: 25 | return "RGB gain" 26 | case ptp.DPC_FNumber: 27 | return "F-number" 28 | case ptp.DPC_FocalLength: 29 | return "focal length" 30 | case ptp.DPC_FocusDistance: 31 | return "focus distance" 32 | case ptp.DPC_FocusMode: 33 | return "focus mode" 34 | case ptp.DPC_ExposureMeteringMode: 35 | return "exposure metering mode" 36 | case ptp.DPC_FlashMode: 37 | return "flash mode" 38 | case ptp.DPC_ExposureTime: 39 | return "exposure time" 40 | case ptp.DPC_ExposureProgramMode: 41 | return "exposure program mode" 42 | case ptp.DPC_ExposureIndex: 43 | return "ISO" 44 | case ptp.DPC_ExposureBiasCompensation: 45 | return "exposure bias compensation" 46 | case ptp.DPC_DateTime: 47 | return "date time" 48 | case ptp.DPC_CaptureDelay: 49 | return "capture delay" 50 | case ptp.DPC_StillCaptureMode: 51 | return "still capture mode" 52 | case ptp.DPC_Contrast: 53 | return "contrast" 54 | case ptp.DPC_Sharpness: 55 | return "sharpness" 56 | case ptp.DPC_DigitalZoom: 57 | return "digital zoom" 58 | case ptp.DPC_EffectMode: 59 | return "effect mode" 60 | case ptp.DPC_BurstNumber: 61 | return "burst number" 62 | case ptp.DPC_BurstInterval: 63 | return "burst interval" 64 | case ptp.DPC_TimelapseNumber: 65 | return "timelapse number" 66 | case ptp.DPC_TimelapseInterval: 67 | return "timelapse interval" 68 | case ptp.DPC_FocusMeteringMode: 69 | return "focus metering mode" 70 | case ptp.DPC_UploadURL: 71 | return "upload URL" 72 | case ptp.DPC_Artist: 73 | return "artist" 74 | case ptp.DPC_CopyrightInfo: 75 | return "copyright info" 76 | default: 77 | return "" 78 | } 79 | } 80 | 81 | // GenericPropToDevicePropCode converts a standardised property string to a valid DevicePropertyCode. 82 | func GenericPropToDevicePropCode(field string) (ptp.DevicePropCode, error) { 83 | switch field { 84 | case PRP_Delay: 85 | return ptp.DPC_CaptureDelay, nil 86 | case PRP_Effect: 87 | return ptp.DPC_EffectMode, nil 88 | case PRP_Exposure: 89 | return ptp.DPC_ExposureTime, nil 90 | case PRP_ExpBias: 91 | return ptp.DPC_ExposureBiasCompensation, nil 92 | case PRP_FlashMode: 93 | return ptp.DPC_FlashMode, nil 94 | case PRP_ISO: 95 | return ptp.DPC_ExposureIndex, nil 96 | case PRP_WhiteBalance: 97 | return ptp.DPC_WhiteBalance, nil 98 | case PRP_FocusMeteringMode: 99 | return ptp.DPC_FocusMeteringMode, nil 100 | default: 101 | return 0, fmt.Errorf("unknown field name '%s'", field) 102 | } 103 | } 104 | 105 | func FormFlagAsString(flag ptp.DevicePropFormFlag) string { 106 | switch flag { 107 | case ptp.DPF_FormFlag_None: 108 | return "none" 109 | case ptp.DPF_FormFlag_Range: 110 | return "range" 111 | case ptp.DPF_FormFlag_Enum: 112 | return "enum" 113 | default: 114 | return "" 115 | } 116 | } 117 | 118 | func DataTypeCodeAsString(code ptp.DataTypeCode) string { 119 | switch code { 120 | case ptp.DTC_UNDEF: 121 | return "undefined" 122 | case ptp.DTC_INT8: 123 | return "int8" 124 | case ptp.DTC_UINT8: 125 | return "uint8" 126 | case ptp.DTC_INT16: 127 | return "int16" 128 | case ptp.DTC_UINT16: 129 | return "uint16" 130 | case ptp.DTC_INT32: 131 | return "int32" 132 | case ptp.DTC_UINT32: 133 | return "uint32" 134 | case ptp.DTC_INT64: 135 | return "int64" 136 | case ptp.DTC_UINT64: 137 | return "uint64" 138 | case ptp.DTC_INT128: 139 | return "int128" 140 | case ptp.DTC_UINT128: 141 | return "uint128" 142 | case ptp.DTC_AINT8: 143 | return "aint8" 144 | case ptp.DTC_AUINT8: 145 | return "auint8" 146 | case ptp.DTC_AINT16: 147 | return "aint16" 148 | case ptp.DTC_AUINT16: 149 | return "auint16" 150 | case ptp.DTC_AINT32: 151 | return "aint32" 152 | case ptp.DTC_AUINT32: 153 | return "auint32" 154 | case ptp.DTC_AINT64: 155 | return "aint64" 156 | case ptp.DTC_AUINT64: 157 | return "auint64" 158 | case ptp.DTC_AINT128: 159 | return "aint128" 160 | case ptp.DTC_AUINT128: 161 | return "auint128" 162 | case ptp.DTC_STR: 163 | return "string" 164 | default: 165 | return "" 166 | } 167 | } 168 | 169 | func DevicePropValueAsString(code ptp.DevicePropCode, v int64) string { 170 | switch code { 171 | case ptp.DPC_EffectMode: 172 | return EffectModeAsString(ptp.EffectMode(v)) 173 | case ptp.DPC_ExposureBiasCompensation: 174 | return ExposureBiasCompensationAsString(int16(v)) 175 | case ptp.DPC_ExposureMeteringMode: 176 | return ExposureMeteringModeAsString(ptp.ExposureMeteringMode(v)) 177 | case ptp.DPC_ExposureProgramMode: 178 | return ExposureProgramModeAsString(ptp.ExposureProgramMode(v)) 179 | case ptp.DPC_FlashMode: 180 | return FlashModeAsString(ptp.FlashMode(v)) 181 | case ptp.DPC_FNumber: 182 | return FNumberAsString(uint16(v)) 183 | case ptp.DPC_FocusMeteringMode: 184 | return FocusMeteringModeAsString(ptp.FocusMeteringMode(v)) 185 | case ptp.DPC_FocusMode: 186 | return FocusModeAsString(ptp.FocusMode(v)) 187 | case ptp.DPC_FunctionalMode: 188 | return FunctionalModeAsString(ptp.FunctionalMode(v)) 189 | case ptp.DPC_StillCaptureMode: 190 | return StillCaptureModeAsString(ptp.StillCaptureMode(v)) 191 | case ptp.DPC_WhiteBalance: 192 | return WhiteBalanceAsString(ptp.WhiteBalance(v)) 193 | default: 194 | return "" 195 | } 196 | } 197 | 198 | func FNumberAsString(fn uint16) string { 199 | if fn == 0xffff { 200 | return "automatic" 201 | } 202 | 203 | return fmt.Sprintf("f/%.1f", float32(fn)/100) 204 | } 205 | 206 | func EffectModeAsString(fxm ptp.EffectMode) string { 207 | switch fxm { 208 | case ptp.FXM_Undefined: 209 | return "undefined" 210 | case ptp.FXM_Standard: 211 | return "standard" 212 | case ptp.FXM_BlackWhite: 213 | return "black and white" 214 | case ptp.FXM_Sepia: 215 | return "sepia" 216 | default: 217 | return "" 218 | } 219 | } 220 | 221 | func ExposureBiasCompensationAsString(ebv int16) string { 222 | i, f := math.Modf(float64(ebv) / float64(1000)) 223 | 224 | if f == 0 { 225 | return strconv.FormatInt(int64(i), 10) 226 | } 227 | 228 | // Tried to use big.Rat to do the conversion, but it's trying to be "too precise" :/ 229 | frac := "1/3" 230 | if math.Abs(f) > 0.4 { 231 | frac = "2/3" 232 | } 233 | 234 | if i == 0 { 235 | sign := "" 236 | if f < 0 { 237 | sign = "-" 238 | } 239 | return sign + frac 240 | } 241 | 242 | return fmt.Sprintf("%d %s", int(i), frac) 243 | } 244 | 245 | func ExposureMeteringModeAsString(emm ptp.ExposureMeteringMode) string { 246 | switch emm { 247 | case ptp.EMM_Undefined: 248 | return "undefined" 249 | case ptp.EMM_Avarage: 250 | return "average" 251 | case ptp.EMM_CenterWeightedAvarage: 252 | return "center weighted average" 253 | case ptp.EMM_MultiSpot: 254 | return "multi spot" 255 | case ptp.EMM_CenterSpot: 256 | return "center spot" 257 | default: 258 | return "" 259 | } 260 | } 261 | 262 | func ExposureProgramModeAsString(epm ptp.ExposureProgramMode) string { 263 | switch epm { 264 | case ptp.EPM_Undefined: 265 | return "undefined" 266 | case ptp.EPM_Manual: 267 | return "manual" 268 | case ptp.EPM_Automatic: 269 | return "automatic" 270 | case ptp.EPM_AperturePriority: 271 | return "aperture priority" 272 | case ptp.EPM_ShutterPriority: 273 | return "shutter priority" 274 | case ptp.EPM_ProgramCreative: 275 | return "program creative" 276 | case ptp.EPM_ProgramAction: 277 | return "program action" 278 | case ptp.EPM_Portrait: 279 | return "portrait" 280 | default: 281 | return "" 282 | } 283 | } 284 | 285 | func FlashModeAsString(flm ptp.FlashMode) string { 286 | switch flm { 287 | case ptp.FLM_Undefined: 288 | return "undefined" 289 | case ptp.FLM_AutoFlash: 290 | return "auto flash" 291 | case ptp.FLM_FlashOff: 292 | return "off" 293 | case ptp.FLM_FillFlash: 294 | return "fill" 295 | case ptp.FLM_RedEyeAuto: 296 | return "red eye auto" 297 | case ptp.FLM_RedEyeFill: 298 | return "red eye fill" 299 | case ptp.FLM_ExternalSync: 300 | return "external sync" 301 | default: 302 | return "" 303 | } 304 | } 305 | 306 | func FocusMeteringModeAsString(fmm ptp.FocusMeteringMode) string { 307 | switch fmm { 308 | case ptp.FMM_Undefined: 309 | return "undefined" 310 | case ptp.FMM_CenterSpot: 311 | return "center spot" 312 | case ptp.FMM_MultiSpot: 313 | return "multi spot" 314 | default: 315 | return "" 316 | } 317 | } 318 | 319 | func FocusModeAsString(fcm ptp.FocusMode) string { 320 | switch fcm { 321 | case ptp.FCM_Undefined: 322 | return "undefined" 323 | case ptp.FCM_Manual: 324 | return "manual" 325 | case ptp.FCM_Automatic: 326 | return "automatic" 327 | case ptp.FCM_AutomaticMacro: 328 | return "automatic macro" 329 | default: 330 | return "" 331 | } 332 | } 333 | 334 | func FunctionalModeAsString(fum ptp.FunctionalMode) string { 335 | switch fum { 336 | case ptp.FUM_StandardMode: 337 | return "standard" 338 | case ptp.FUM_SleepState: 339 | return "sleep" 340 | default: 341 | return "" 342 | } 343 | } 344 | 345 | func SelfTestTypeAsString(stt ptp.SelfTestType) string { 346 | switch stt { 347 | case ptp.STT_Default: 348 | return "default" 349 | default: 350 | return "" 351 | } 352 | } 353 | 354 | func StillCaptureModeAsString(scm ptp.StillCaptureMode) string { 355 | switch scm { 356 | case ptp.SCM_Undefined: 357 | return "undefined" 358 | case ptp.SCM_Normal: 359 | return "normal" 360 | case ptp.SCM_Burst: 361 | return "burst" 362 | case ptp.SCM_Timelapse: 363 | return "timelapse" 364 | default: 365 | return "" 366 | } 367 | } 368 | 369 | func WhiteBalanceAsString(wb ptp.WhiteBalance) string { 370 | switch wb { 371 | case ptp.WB_Undefined: 372 | return "undefined" 373 | case ptp.WB_Manual: 374 | return "manual" 375 | case ptp.WB_Automatic: 376 | return "automatic" 377 | case ptp.WB_OnePushAutomatic: 378 | return "one push automatic" 379 | case ptp.WB_Daylight: 380 | return "daylight" 381 | case ptp.WB_Fluorescent: 382 | return "fluorescent" 383 | case ptp.WB_Tungsten: 384 | return "tungsten" 385 | case ptp.WB_Flash: 386 | return "flash" 387 | default: 388 | return "" 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /fmt/string_fuji_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | "github.com/malc0mn/ptp-ip/ptp" 7 | "testing" 8 | ) 9 | 10 | func TestFujiDevicePropCodeAsString(t *testing.T) { 11 | check := map[ptp.DevicePropCode]string{ 12 | ip.DPC_Fuji_FilmSimulation: "film simulation", 13 | ip.DPC_Fuji_ImageQuality: "image quality", 14 | ip.DPC_Fuji_RecMode: "rec mode", 15 | ip.DPC_Fuji_CommandDialMode: "command dial mode", 16 | ip.DPC_Fuji_ExposureIndex: "ISO", 17 | ip.DPC_Fuji_MovieISO: "movie ISO", 18 | ip.DPC_Fuji_FocusMeteringMode: "focus point", 19 | ip.DPC_Fuji_FocusLock: "focus lock", 20 | ip.DPC_Fuji_DeviceError: "device error", 21 | ip.DPC_Fuji_CapturesRemaining: "captures remaining", 22 | ip.DPC_Fuji_MovieRemainingTime: "movie remaining time", 23 | ip.DPC_Fuji_ShutterSpeed: "shutter speed", 24 | ip.DPC_Fuji_ImageAspectRatio: "image size", 25 | ip.DPC_Fuji_BatteryLevel: "battery level", 26 | ip.DPC_Fuji_InitSequence: "init sequence", 27 | ip.DPC_Fuji_AppVersion: "app version", 28 | ptp.DevicePropCode(0): "", 29 | } 30 | 31 | for code, want := range check { 32 | got := FujiDevicePropCodeAsString(code) 33 | if got != want { 34 | t.Errorf("FujiDevicePropCodeAsString() return = '%s', want '%s'", got, want) 35 | } 36 | } 37 | } 38 | 39 | func TestFujiPropToDevicePropCode(t *testing.T) { 40 | check := map[string]ptp.DevicePropCode{ 41 | PRP_Effect: ip.DPC_Fuji_FilmSimulation, 42 | PRP_FocusMeteringMode: ip.DPC_Fuji_FocusMeteringMode, 43 | PRP_ISO: ip.DPC_Fuji_ExposureIndex, 44 | "recmode": ip.DPC_Fuji_RecMode, 45 | } 46 | 47 | for prop, want := range check { 48 | got, err := FujiPropToDevicePropCode(prop) 49 | if err != nil { 50 | t.Errorf("FujiPropToDevicePropCode() error = %s, want ", err) 51 | } 52 | if got != want { 53 | t.Errorf("FujiPropToDevicePropCode() return = '%#x', want '%#x'", got, want) 54 | } 55 | } 56 | 57 | prop := "test" 58 | got, err := FujiPropToDevicePropCode(prop) 59 | wantE := fmt.Sprintf("unknown field name '%s'", prop) 60 | if err.Error() != wantE { 61 | t.Errorf("FujiPropToDevicePropCode() error = %s, want %s", err, wantE) 62 | } 63 | wantC := ptp.DevicePropCode(0) 64 | if got != wantC { 65 | t.Errorf("FujiPropToDevicePropCode() return = %d, want %d", got, wantC) 66 | } 67 | } 68 | 69 | func TestFujiBatteryLevelAsString(t *testing.T) { 70 | check := map[ip.FujiBatteryLevel]string{ 71 | ip.BAT_Fuji_3bOne: "1/3", 72 | ip.BAT_Fuji_3bTwo: "2/3", 73 | ip.BAT_Fuji_3bFull: "3/3", 74 | ip.BAT_Fuji_5bCritical: "critical", 75 | ip.BAT_Fuji_5bOne: "1/5", 76 | ip.BAT_Fuji_5bTwo: "2/5", 77 | ip.BAT_Fuji_5bThree: "3/5", 78 | ip.BAT_Fuji_5bFour: "4/5", 79 | ip.BAT_Fuji_5bFull: "5/5", 80 | ip.FujiBatteryLevel(0): "", 81 | } 82 | 83 | for code, want := range check { 84 | got := FujiBatteryLevelAsString(code) 85 | if got != want { 86 | t.Errorf("FujiBatteryLevelAsString() return = '%s', want '%s'", got, want) 87 | } 88 | } 89 | } 90 | 91 | func TestFujiCommandDialModeAsString(t *testing.T) { 92 | check := map[ip.FujiCommandDialMode]string{ 93 | ip.CMD_Fuji_Both: "both", 94 | ip.CMD_Fuji_Aperture: "aperture", 95 | ip.CMD_Fuji_ShutterSpeed: "shutter speed", 96 | ip.CMD_Fuji_None: "none", 97 | ip.FujiCommandDialMode(4): "", 98 | } 99 | 100 | for code, want := range check { 101 | got := FujiCommandDialModeAsString(code) 102 | if got != want { 103 | t.Errorf("FujiCommandDialModeAsString() return = '%s', want '%s'", got, want) 104 | } 105 | } 106 | } 107 | 108 | func TestFujiDeviceErrorAsString(t *testing.T) { 109 | check := map[ip.FujiDeviceError]string{ 110 | ip.DE_Fuji_None: "none", 111 | ip.FujiDeviceError(4): "", 112 | } 113 | 114 | for code, want := range check { 115 | got := FujiDeviceErrorAsString(code) 116 | if got != want { 117 | t.Errorf("FujiDeviceErrorAsString() return = '%s', want '%s'", got, want) 118 | } 119 | } 120 | } 121 | 122 | func TestFujiExposureIndexAsString(t *testing.T) { 123 | check := map[uint32]string{ 124 | uint32(ip.EDX_Fuji_Auto): "auto", 125 | 0x00000140: "320", 126 | 0x00001900: "6400", 127 | 0x40000064: "L100", 128 | 0x40003200: "H12800", 129 | 0x40006400: "H25600", 130 | 0x4000C800: "H51200", 131 | 0x800000C8: "S200", 132 | 0x80000190: "S400", 133 | 0x80000320: "S800", 134 | 0x80000640: "S1600", 135 | 0x80000C80: "S3200", 136 | 0x80001900: "S6400", 137 | } 138 | 139 | for code, want := range check { 140 | got := FujiExposureIndexAsString(ip.FujiExposureIndex(code)) 141 | if got != want { 142 | t.Errorf("FujiExposureIndexAsString() return = '%s', want '%s'", got, want) 143 | } 144 | } 145 | } 146 | 147 | func TestFujiFilmSimulationAsString(t *testing.T) { 148 | check := map[ip.FujiFilmSimulation]string{ 149 | ip.FS_Fuji_Provia: "PROVIA", 150 | ip.FS_Fuji_Velvia: "Velvia", 151 | ip.FS_Fuji_Astia: "ASTIA", 152 | ip.FS_Fuji_Monochrome: "Monochrome", 153 | ip.FS_Fuji_Sepia: "Sepia", 154 | ip.FS_Fuji_ProNegHigh: "PRO Neg. Hi", 155 | ip.FS_Fuji_ProNegStandard: "PRO Neg. Std", 156 | ip.FS_Fuji_MonochromeYeFilter: "Monochrome + Ye Filter", 157 | ip.FS_Fuji_MonochromeRFilter: "Monochrome + R Filter", 158 | ip.FS_Fuji_MonochromeGFilter: "Monochrome + G Filter", 159 | ip.FS_Fuji_ClassicChrome: "Classic Chrome", 160 | ip.FS_Fuji_ACROS: "ACROS", 161 | ip.FS_Fuji_ACROSYe: "ACROS Ye", 162 | ip.FS_Fuji_ACROSR: "ACROS R", 163 | ip.FS_Fuji_ACROSG: "ACROS G", 164 | ip.FS_Fuji_ETERNA: "ETERNA", 165 | ip.FujiFilmSimulation(0): "", 166 | } 167 | 168 | for code, want := range check { 169 | got := FujiFilmSimulationAsString(code) 170 | if got != want { 171 | t.Errorf("FujiFilmSimulationAsString() return = '%s', want '%s'", got, want) 172 | } 173 | } 174 | } 175 | 176 | func TestFujiFlashModeAsString(t *testing.T) { 177 | check := map[ptp.FlashMode]string{ 178 | ip.FM_Fuji_On: "on", 179 | ip.FM_Fuji_RedEye: "red eye", 180 | ip.FM_Fuji_RedEyeOn: "red eye on", 181 | ip.FM_Fuji_RedEyeSync: "red eye sync", 182 | ip.FM_Fuji_RedEyeRear: "red eye rear", 183 | ip.FM_Fuji_SlowSync: "slow sync", 184 | ip.FM_Fuji_RearSync: "rear sync", 185 | ip.FM_Fuji_Commander: "commander", 186 | ip.FM_Fuji_Disabled: "disabled", 187 | ip.FM_Fuji_Enabled: "enabled", 188 | ptp.FlashMode(0): "undefined", 189 | } 190 | 191 | for code, want := range check { 192 | got := FujiFlashModeAsString(code) 193 | if got != want { 194 | t.Errorf("FujiFlashModeAsString() return = '%s', want '%s'", got, want) 195 | } 196 | } 197 | } 198 | 199 | func TestFujiFocusLockAsString(t *testing.T) { 200 | check := map[ip.FujiFocusLock]string{ 201 | ip.FL_Fuji_On: "on", 202 | ip.FL_Fuji_Off: "off", 203 | ip.FujiFocusLock(2): "", 204 | } 205 | 206 | for code, want := range check { 207 | got := FujiFocusLockAsString(code) 208 | if got != want { 209 | t.Errorf("FujiFocusLockAsString() return = '%s', want '%s'", got, want) 210 | } 211 | } 212 | } 213 | 214 | func TestFujiFocusMeteringModeAsString(t *testing.T) { 215 | check := map[uint32]string{ 216 | 0x10000101: "1x1", 217 | 0x02000203: "2x3", 218 | 0x00300504: "5x4", 219 | 0x00040707: "7x7", 220 | } 221 | 222 | for code, want := range check { 223 | got := FujiFocusMeteringModeAsString(code) 224 | if got != want { 225 | t.Errorf("FujiFocusMeteringModeAsString() return = '%s', want '%s'", got, want) 226 | } 227 | } 228 | } 229 | 230 | func TestFujiFocusModeAsString(t *testing.T) { 231 | check := map[ptp.FocusMode]string{ 232 | ip.FCM_Fuji_Single_Auto: "single auto", 233 | ip.FCM_Fuji_Continuous_Auto: "continuous auto", 234 | ptp.FocusMode(2): "", 235 | } 236 | 237 | for code, want := range check { 238 | got := FujiFocusModeAsString(code) 239 | if got != want { 240 | t.Errorf("FujiFocusModeAsString() return = '%s', want '%s'", got, want) 241 | } 242 | } 243 | } 244 | 245 | func TestFujiImageAspectRatioAsString(t *testing.T) { 246 | check := map[ip.FujiImageSize]string{ 247 | ip.IS_Fuji_Small_3x2: "S 3:2", 248 | ip.IS_Fuji_Small_16x9: "S 16:9", 249 | ip.IS_Fuji_Small_1x1: "S 1:1", 250 | ip.IS_Fuji_Medium_3x2: "M 3:2", 251 | ip.IS_Fuji_Medium_16x9: "M 16:9", 252 | ip.IS_Fuji_Medium_1x1: "M 1:1", 253 | ip.IS_Fuji_Large_3x2: "L 3:2", 254 | ip.IS_Fuji_Large_16x9: "L 16:9", 255 | ip.IS_Fuji_Large_1x1: "L 1:1", 256 | ip.FujiImageSize(0): "", 257 | } 258 | 259 | for code, want := range check { 260 | got := FujiImageAspectRatioAsString(code) 261 | if got != want { 262 | t.Errorf("FujiImageAspectRatioAsString() return = '%s', want '%s'", got, want) 263 | } 264 | } 265 | } 266 | 267 | func TestFujiImageQualityAsString(t *testing.T) { 268 | check := map[ip.FujiImageQuality]string{ 269 | ip.IQ_Fuji_Fine: "fine", 270 | ip.IQ_Fuji_Normal: "normal", 271 | ip.IQ_Fuji_FineAndRAW: "fine + RAW", 272 | ip.IQ_Fuji_NormalAndRAW: "normal + RAW", 273 | ip.FujiImageQuality(0): "", 274 | } 275 | 276 | for code, want := range check { 277 | got := FujiImageQualityAsString(code) 278 | if got != want { 279 | t.Errorf("FujiImageQualityAsString() return = '%s', want '%s'", got, want) 280 | } 281 | } 282 | } 283 | 284 | func TestFujiWhiteBalanceAsString(t *testing.T) { 285 | check := map[ptp.WhiteBalance]string{ 286 | ip.WB_Fuji_Fluorescent1: "fluorescent 1", 287 | ip.WB_Fuji_Fluorescent2: "fluorescent 2", 288 | ip.WB_Fuji_Fluorescent3: "fluorescent 3", 289 | ip.WB_Fuji_Shade: "shade", 290 | ip.WB_Fuji_Underwater: "underwater", 291 | ip.WB_Fuji_Temperature: "temprerature", 292 | ip.WB_Fuji_Custom: "custom", 293 | ptp.WhiteBalance(0): "undefined", 294 | } 295 | 296 | for code, want := range check { 297 | got := FujiWhiteBalanceAsString(code) 298 | if got != want { 299 | t.Errorf("FujiWhiteBalanceAsString() return = '%s', want '%s'", got, want) 300 | } 301 | } 302 | } 303 | 304 | func TestFujiSelfTimerAsString(t *testing.T) { 305 | check := map[ip.FujiSelfTimer]string{ 306 | ip.ST_Fuji_1Sec: "1 second", 307 | ip.ST_Fuji_2Sec: "2 seconds", 308 | ip.ST_Fuji_5Sec: "5 seconds", 309 | ip.ST_Fuji_10Sec: "10 seconds", 310 | ip.ST_Fuji_Off: "off", 311 | ip.FujiSelfTimer(5): "", 312 | } 313 | 314 | for code, want := range check { 315 | got := FujiSelfTimerAsString(code) 316 | if got != want { 317 | t.Errorf("FujiSelfTimerAsString() return = '%s', want '%s'", got, want) 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /ip/mockresponder_fuji_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "github.com/malc0mn/ptp-ip/ptp" 7 | "io" 8 | "io/ioutil" 9 | "net" 10 | "os" 11 | ) 12 | 13 | func handleFujiMessages(conn net.Conn, evtChan chan uint32, lmp string) { 14 | // NO defer conn.Close() here since we need to mock a real Fuji responder and thus need to keep the connections open 15 | // when established and continuously listen for messages in a loop. 16 | for { 17 | l, raw, err := readMessageRaw(conn, lmp) 18 | if err == io.EOF { 19 | conn.Close() 20 | break 21 | } 22 | if raw == nil { 23 | continue 24 | } 25 | 26 | lgr.Infof("%s read %d raw bytes", lmp, l) 27 | 28 | var ( 29 | msg string 30 | resp PacketIn 31 | data []byte 32 | evt uint32 33 | ) 34 | eodp := false 35 | 36 | // This construction is thanks to the Fuji decision of not properly using packet types. Watch out for the caveat 37 | // here: we need to swap the order of the DataPhase and the OperationRequestCode because we are reading what are 38 | // actually two uint16 numbers as if they were a single uint32! 39 | switch binary.LittleEndian.Uint32(raw[0:4]) { 40 | case uint32(PKT_InitCommandRequest): 41 | msg, resp = genericInitCommandRequestResponse(lmp, ProtocolVersion(0)) 42 | case constructPacketType(OC_Fuji_GetCapturePreview): 43 | msg, resp, data = fujiGetCapturePreview(raw[4:8]) 44 | evt = constructEventData(OC_Fuji_GetCapturePreview, raw[4:8]) 45 | eodp = true 46 | case constructPacketType(OC_Fuji_GetDeviceInfo): 47 | msg, resp, data = fujiGetDeviceInfo(raw[4:8]) 48 | eodp = true 49 | case constructPacketType(ptp.OC_GetDevicePropDesc): 50 | msg, resp, data = fujiGetDevicePropDescResponse(raw[4:8], raw[8:10]) 51 | eodp = true 52 | case constructPacketType(ptp.OC_GetDevicePropValue): 53 | msg, resp, data = fujiGetDevicePropValueResponse(raw[4:8], raw[8:10]) 54 | eodp = true 55 | case constructPacketType(ptp.OC_InitiateCapture): 56 | msg, resp = fujiInitiateCaptureResponse(raw[4:8]) 57 | evt = constructEventData(ptp.OC_InitiateCapture, raw[4:8]) 58 | case constructPacketType(ptp.OC_InitiateOpenCapture): 59 | msg, resp = fujiInitiateOpenCaptureResponse(raw[4:8]) 60 | case constructPacketType(ptp.OC_OpenSession): 61 | msg, resp = fujiOpenSessionResponse(raw[4:8]) 62 | case constructPacketTypeWithDataPhase(ptp.OC_SetDevicePropValue, DP_DataOut): 63 | // SetDevicePropValue involves two messages, only the second one needs a response from us! 64 | msg, resp = fujiSetDevicePropValue(raw[4:8]) 65 | } 66 | 67 | if resp != nil { 68 | if msg != "" { 69 | lgr.Infof("%s responding to %s", lmp, msg) 70 | } 71 | sendMessage(conn, resp, data, lmp) 72 | if eodp { 73 | lgr.Infof("%s sending end of data packet", lmp) 74 | sendMessage(conn, fujiEndOfDataPacket(raw[4:8]), nil, lmp) 75 | } 76 | } 77 | 78 | if evt != 0 { 79 | evtChan <- evt 80 | lgr.Infof("%s requested event dispatch for oc|tid %#x...", lmp, evt) 81 | } 82 | } 83 | } 84 | 85 | func handleFujiEvents(conn net.Conn, evtChan chan uint32, lmp string) { 86 | for { 87 | var evts []*FujiEventPacket 88 | data := <-evtChan 89 | lgr.Infof("%s received event request %#x", lmp, data) 90 | oc := ptp.OperationCode(data & uint32(0xFFFF0000) >> 16) 91 | tid := ptp.TransactionID(data & uint32(0x0000FFFF)) 92 | lgr.Infof("%s operation code %#x with transaction ID %#x", lmp, oc, tid) 93 | 94 | switch oc { 95 | case ptp.OC_InitiateCapture: 96 | fSize, _ := os.Stat("testdata/preview.jpg") 97 | evts = append( 98 | evts, 99 | &FujiEventPacket{ 100 | DataPhase: 0x0004, 101 | EventCode: EC_Fuji_ObjectAdded, 102 | Amount: 1, // No clue what this is, always seems to be set to 1 103 | TransactionID: tid, 104 | Parameter1: uint32(tid), // Yes, it is always set to the transaction ID! 105 | }, 106 | &FujiEventPacket{ 107 | DataPhase: 0x0004, 108 | EventCode: EC_Fuji_PreviewAvailable, 109 | Amount: 1, // No clue what this is, always seems to be set to 1 110 | TransactionID: tid, 111 | Parameter1: uint32(tid), // Yes, it is always set to the transaction ID! 112 | Parameter2: uint32(fSize.Size()), 113 | }, 114 | ) 115 | case OC_Fuji_GetCapturePreview: 116 | evts = append(evts, &FujiEventPacket{ 117 | DataPhase: 0x0004, 118 | EventCode: ptp.EC_CaptureComplete, 119 | Amount: 1, 120 | TransactionID: tid, 121 | Parameter1: uint32(tid), 122 | }) 123 | } 124 | 125 | for _, evt := range evts { 126 | sendMessage(conn, evt, nil, lmp) 127 | } 128 | } 129 | } 130 | 131 | func constructPacketType(code ptp.OperationCode) uint32 { 132 | return constructPacketTypeWithDataPhase(code, DP_NoDataOrDataIn) 133 | } 134 | 135 | func constructPacketTypeWithDataPhase(code ptp.OperationCode, dp DataPhase) uint32 { 136 | return uint32(code)<<16 | uint32(dp) 137 | } 138 | 139 | // Don't try this at home: it is fine for testing as the transaction ID will always be quite low. 140 | func constructEventData(code ptp.OperationCode, tid []byte) uint32 { 141 | return uint32(code)<<16 | binary.LittleEndian.Uint32(tid) 142 | } 143 | 144 | func fujiGetCapturePreview(tid []byte) (string, *FujiOperationResponsePacket, []byte) { 145 | dat, _ := ioutil.ReadFile("testdata/preview.jpg") 146 | return "GetCapturePreview", 147 | fujiOperationResponsePacket(DP_DataOut, RC_Fuji_GetCapturePreview, tid), 148 | dat 149 | } 150 | 151 | func fujiGetDeviceInfo(tid []byte) (string, *FujiOperationResponsePacket, []byte) { 152 | return "GetDeviceInfo", 153 | fujiOperationResponsePacket(DP_DataOut, RC_Fuji_GetDeviceInfo, tid), 154 | []byte{ 155 | 0x08, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x12, 0x50, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 156 | 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0c, 0x50, 0x04, 0x00, 0x01, 0x02, 157 | 0x00, 0x09, 0x80, 0x02, 0x02, 0x00, 0x09, 0x80, 0x0a, 0x80, 0x24, 0x00, 0x00, 0x00, 0x05, 0x50, 0x04, 0x00, 158 | 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x0a, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x80, 0x01, 0x80, 0x02, 0x80, 159 | 0x03, 0x80, 0x06, 0x00, 0x0a, 0x80, 0x0b, 0x80, 0x0c, 0x80, 0x36, 0x00, 0x00, 0x00, 0x10, 0x50, 0x03, 0x00, 160 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0x00, 0x48, 0xf4, 0x95, 0xf5, 0xe3, 0xf6, 0x30, 0xf8, 0x7d, 0xf9, 161 | 0xcb, 0xfa, 0x18, 0xfc, 0x65, 0xfd, 0xb3, 0xfe, 0x00, 0x00, 0x4d, 0x01, 0x9b, 0x02, 0xe8, 0x03, 0x35, 0x05, 162 | 0x83, 0x06, 0xd0, 0x07, 0x1d, 0x09, 0x6b, 0x0a, 0xb8, 0x0b, 0x26, 0x00, 0x00, 0x00, 0x01, 0xd0, 0x04, 0x00, 163 | 0x01, 0x01, 0x00, 0x02, 0x00, 0x02, 0x0b, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 164 | 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x78, 0x00, 0x00, 0x00, 0x2a, 0xd0, 165 | 0x06, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x00, 0x19, 0x00, 0x80, 0x02, 0x19, 0x00, 0x90, 0x01, 0x00, 0x80, 166 | 0x20, 0x03, 0x00, 0x80, 0x40, 0x06, 0x00, 0x80, 0x80, 0x0c, 0x00, 0x80, 0x00, 0x19, 0x00, 0x80, 0x64, 0x00, 167 | 0x00, 0x40, 0xc8, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 168 | 0xf4, 0x01, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0xe2, 0x04, 169 | 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, 0xd0, 0x07, 0x00, 0x00, 0xc4, 0x09, 0x00, 0x00, 0x80, 0x0c, 0x00, 0x00, 170 | 0xa0, 0x0f, 0x00, 0x00, 0x88, 0x13, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x32, 0x00, 0x40, 0x00, 0x64, 171 | 0x00, 0x40, 0x00, 0xc8, 0x00, 0x40, 0x14, 0x00, 0x00, 0x00, 0x19, 0xd0, 0x04, 0x00, 0x01, 0x01, 0x00, 0x01, 172 | 0x00, 0x02, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x7c, 0xd1, 0x06, 0x00, 0x01, 0x00, 173 | 0x00, 0x00, 0x00, 0x02, 0x07, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x09, 0x10, 0x01, 0x00, 174 | 0x00, 0x00, 175 | } 176 | } 177 | 178 | func fujiGetDevicePropDescResponse(tid []byte, prop []byte) (string, *FujiOperationResponsePacket, []byte) { 179 | var p []byte 180 | 181 | switch binary.LittleEndian.Uint16(prop) { 182 | case uint16(DPC_Fuji_FocusMeteringMode): 183 | p = []byte{0x7c, 0xd1, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 184 | 0x00, 0x07, 0x07, 0x09, 0x10, 0x01, 0x00, 0x00, 0x00, 185 | } 186 | case uint16(ptp.DPC_WhiteBalance): 187 | p = []byte{0x05, 0x50, 0x04, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x0a, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 188 | 0x80, 0x01, 0x80, 0x02, 0x80, 0x03, 0x80, 0x06, 0x00, 0x0a, 0x80, 0x0b, 0x80, 0x0c, 0x80, 189 | } 190 | case uint16(DPC_Fuji_FilmSimulation): 191 | p = []byte{0x01, 0xd0, 0x04, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x02, 0x0b, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 192 | 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 193 | } 194 | } 195 | 196 | return fmt.Sprintf("GetDevicePropDesc %#x", binary.LittleEndian.Uint16(prop)), 197 | fujiOperationResponsePacket(DP_DataOut, RC_Fuji_GetDevicePropDesc, tid), 198 | p 199 | } 200 | 201 | func fujiGetDevicePropValueResponse(tid []byte, prop []byte) (string, *FujiOperationResponsePacket, []byte) { 202 | var p []byte 203 | 204 | switch binary.LittleEndian.Uint16(prop) { 205 | case uint16(DPC_Fuji_AppVersion): 206 | p = make([]byte, 4) 207 | binary.LittleEndian.PutUint32(p, PM_Fuji_AppVersion) 208 | case uint16(DPC_Fuji_CurrentState): 209 | p = []byte{0x11, 0x00, 0x01, 0x50, 0x02, 0x00, 0x00, 0x00, 0x41, 0xd2, 0x0a, 0x00, 0x00, 0x00, 0x05, 0x50, 0x02, 210 | 0x00, 0x00, 0x00, 0x0a, 0x50, 0x01, 0x80, 0x00, 0x00, 0x0c, 0x50, 0x0a, 0x80, 0x00, 0x00, 0x0e, 0x50, 0x02, 211 | 0x00, 0x00, 0x00, 0x10, 0x50, 0xb3, 0xfe, 0x00, 0x00, 0x12, 0x50, 0x00, 0x00, 0x00, 0x00, 0x01, 0xd0, 0x02, 212 | 0x00, 0x00, 0x00, 0x18, 0xd0, 0x04, 0x00, 0x00, 0x00, 0x28, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xd0, 0x00, 213 | 0x19, 0x00, 0x80, 0x7c, 0xd1, 0x02, 0x07, 0x02, 0x03, 0x09, 0xd2, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xd2, 0x00, 214 | 0x00, 0x00, 0x00, 0x29, 0xd2, 0xd6, 0x05, 0x00, 0x00, 0x2a, 0xd2, 0x8f, 0x06, 0x00, 0x00, 215 | } 216 | } 217 | 218 | return fmt.Sprintf("GetDevicePropValue %#x", binary.LittleEndian.Uint16(prop)), 219 | fujiOperationResponsePacket(DP_DataOut, RC_Fuji_GetDevicePropValue, tid), 220 | p 221 | } 222 | 223 | func fujiInitiateCaptureResponse(tid []byte) (string, *FujiOperationResponsePacket) { 224 | return "InitiateCapture", 225 | fujiEndOfDataPacket(tid) 226 | } 227 | 228 | func fujiInitiateOpenCaptureResponse(tid []byte) (string, *FujiOperationResponsePacket) { 229 | return "InitiateOpenCapture", 230 | fujiEndOfDataPacket(tid) 231 | } 232 | 233 | func fujiOpenSessionResponse(tid []byte) (string, *FujiOperationResponsePacket) { 234 | return "OpenSession", 235 | fujiEndOfDataPacket(tid) 236 | } 237 | 238 | func fujiSetDevicePropValue(tid []byte) (string, *FujiOperationResponsePacket) { 239 | return "SetDevicePropValue", 240 | fujiEndOfDataPacket(tid) 241 | } 242 | 243 | func fujiEndOfDataPacket(tid []byte) *FujiOperationResponsePacket { 244 | return fujiOperationResponsePacket(DP_Unknown, ptp.RC_OK, tid) 245 | } 246 | 247 | func fujiOperationResponsePacket(dp DataPhase, orc ptp.OperationResponseCode, tid []byte) *FujiOperationResponsePacket { 248 | return &FujiOperationResponsePacket{ 249 | DataPhase: uint16(dp), 250 | OperationResponseCode: orc, 251 | TransactionID: ptp.TransactionID(binary.LittleEndian.Uint32(tid)), 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /viewfinder/fuji_xt1.go: -------------------------------------------------------------------------------- 1 | package viewfinder 2 | 3 | import ( 4 | ptpfmt "github.com/malc0mn/ptp-ip/fmt" 5 | "github.com/malc0mn/ptp-ip/ip" 6 | "github.com/malc0mn/ptp-ip/ptp" 7 | "golang.org/x/image/font/basicfont" 8 | "golang.org/x/image/math/fixed" 9 | "image" 10 | "math" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // NewFujiXT1Viewfinder returns a new Fuji X-T1 viewfinder containing a Widget list mimicking the real viewfinder. 16 | // The image is needed for the widgets to calibrate their origin so they can render in their own designated place. 17 | func NewFujiXT1Viewfinder(img *image.RGBA) *Viewfinder { 18 | return &Viewfinder{ 19 | Widgets: map[ptp.DevicePropCode]*Widget{ 20 | ptp.DPC_BatteryLevel: NewFujiBatteryLevelWidget(img), 21 | ptp.DPC_CaptureDelay: NewFujiCaptureDelayWidget(img), 22 | ip.DPC_Fuji_CapturesRemaining: NewFujiCapturesRemainingWidget(img), 23 | ptp.DPC_ExposureBiasCompensation: NewFujiExposureBiasCompensationWidget(img), 24 | ptp.DPC_ExposureProgramMode: NewFujiExposureProgramModeWidget(img), 25 | ip.DPC_Fuji_ExposureIndex: NewFujiISOWidget(img), 26 | ip.DPC_Fuji_FilmSimulation: NewFujiFilmSimulationWidget(img), 27 | ptp.DPC_FNumber: NewFujiFNumberWidget(img), 28 | ip.DPC_Fuji_ImageAspectRatio: NewFujiImageSizeWidget(img), 29 | ip.DPC_Fuji_ImageQuality: NewFujiImageQualityWidget(img), 30 | ptp.DPC_WhiteBalance: NewFujiWhiteBalanceWidget(img), 31 | }, 32 | } 33 | } 34 | 35 | func NewFujiBatteryLevelWidget(img *image.RGBA) *Widget { 36 | // Calculate starting position. 37 | x := float64(img.Bounds().Max.X) - (float64(img.Bounds().Max.X) * 0.1) 38 | y := img.Bounds().Max.Y - 8 39 | 40 | w := NewWhiteGlyphWidget(img, int(x), y) 41 | w.Draw = drawFujiBattery3Bars 42 | 43 | return w 44 | } 45 | 46 | func drawFujiBattery3Bars(w *Widget, val int64) { 47 | w.ResetToOrigin() 48 | 49 | var lvl string 50 | switch ip.FujiBatteryLevel(val) { 51 | case ip.BAT_Fuji_3bOne: 52 | w.SetColour(255, 0, 0) // red 53 | lvl = "baU" 54 | case ip.BAT_Fuji_3bTwo: 55 | lvl = "bCT" 56 | case ip.BAT_Fuji_3bFull: 57 | lvl = "BAT" 58 | } 59 | 60 | w.DrawString(lvl) 61 | } 62 | 63 | func NewFujiCaptureDelayWidget(img *image.RGBA) *Widget { 64 | // Calculate starting position. 65 | x := float64(img.Bounds().Min.X) + (float64(img.Bounds().Max.X) * 0.2) 66 | y := 18 67 | 68 | w := NewWhiteGlyphWidget(img, int(x), y) 69 | w.Draw = drawFujiCaptureDelay 70 | 71 | return w 72 | } 73 | 74 | func drawFujiCaptureDelay(w *Widget, val int64) { 75 | w.ResetToOrigin() 76 | 77 | var icon string 78 | 79 | switch ip.FujiSelfTimer(val) { 80 | case ip.ST_Fuji_2Sec: 81 | icon = "uv" 82 | case ip.ST_Fuji_10Sec: 83 | icon = "uw" 84 | } 85 | 86 | w.DrawString(icon) 87 | } 88 | 89 | func NewFujiCapturesRemainingWidget(img *image.RGBA) *Widget { 90 | // Calculate starting position. 91 | x := float64(img.Bounds().Max.X) - (float64(img.Bounds().Max.X) * 0.25) 92 | y := 18 93 | 94 | w := NewWhiteFontWidget(img, int(x), y) 95 | w.Draw = drawFujiCapturesRemaining 96 | 97 | return w 98 | } 99 | 100 | func drawFujiCapturesRemaining(w *Widget, val int64) { 101 | w.ResetToOrigin() 102 | 103 | w.DrawString(strconv.FormatInt(val, 10)) 104 | } 105 | 106 | func NewFujiExposureBiasCompensationWidget(img *image.RGBA) *Widget { 107 | // Make sure the center point of our bias widget is in the center of the image. 108 | offset := VFGlyphs6x13.Width * len(getBias()) / 2 109 | 110 | x := float64(img.Bounds().Max.X) - (float64(img.Bounds().Max.X) * 0.5) - float64(offset) 111 | y := img.Bounds().Max.Y - 10 112 | 113 | w := NewWhiteGlyphWidget(img, int(x), y) 114 | w.Draw = drawFujiExposureBiasCompensation 115 | 116 | return w 117 | } 118 | 119 | func getBias() []rune { 120 | return []rune("6..5..4..0..1..2..3") 121 | } 122 | 123 | func drawFujiExposureBiasCompensation(w *Widget, val int64) { 124 | w.ResetToOrigin() 125 | w.ResetColour() 126 | 127 | zero := 9 // don't forget: zero indexed! 128 | stops := 3 // bias dial is per 3 stops 129 | fStop := 0 // default stop is '0' meaning no fractional stop 130 | bias := getBias() 131 | marker := []rune(" ") 132 | 133 | // Draw the leading +/- icon 134 | w.Dot.X -= fixed.Int26_6(VFGlyphs6x13.Width * 3 * 64) // offset icon 3 glyphs to the left 135 | w.DrawString("+-") 136 | w.ResetToOrigin() 137 | 138 | // Calculate marker position. 139 | i, f := math.Modf(float64(int16(val)) / float64(1000)) 140 | onZero := i == 0 && f == 0 141 | if f != 0 { 142 | fStop = 1 143 | if math.Abs(f) > 0.4 { 144 | fStop = 2 145 | } 146 | if math.Signbit(f) { 147 | fStop = -fStop 148 | } 149 | } 150 | pos := zero + fStop + int(i*float64(stops)) 151 | 152 | // When we're not on a fractional number, replace the number marker with an 'empty' marker. 153 | if f == 0 { 154 | bias[pos] = '"' 155 | } 156 | 157 | // When the marker is on 0, the widget must be drawn in grey. 158 | if onZero { 159 | w.SetColour(100, 100, 100) // grey 160 | } 161 | 162 | // Now draw the basic exposure bias compensation widget. 163 | w.DrawString(string(bias)) 164 | 165 | // When the marker is on 0, the the marker and '0' position must be drawn in white. 166 | if onZero { 167 | w.SetColour(255, 255, 255) // white 168 | for _, r := range []rune{'"', '!'} { 169 | w.ResetToOrigin() 170 | marker[pos] = r 171 | w.DrawString(string(marker)) 172 | } 173 | 174 | return 175 | } 176 | 177 | // Draw the marker on the the calculated position in yellow! 178 | marker[pos] = '!' 179 | w.SetColour(255, 185, 10) // yellow 180 | w.ResetToOrigin() 181 | w.DrawString(string(marker)) 182 | } 183 | 184 | func NewFujiExposureProgramModeWidget(img *image.RGBA) *Widget { 185 | // Calculate starting position. 186 | x := float64(img.Bounds().Min.X) + (float64(img.Bounds().Max.X) * 0.1) 187 | y := img.Bounds().Max.Y - 10 188 | 189 | w := NewWhiteGlyphWidget(img, int(x), y) 190 | w.Draw = drawFujiExposureProgramMode 191 | 192 | return w 193 | } 194 | 195 | func drawFujiExposureProgramMode(w *Widget, val int64) { 196 | w.ResetToOrigin() 197 | 198 | icon := " " 199 | switch ptp.ExposureProgramMode(val) { 200 | case ptp.EPM_Manual: 201 | icon = "Mm" 202 | case ptp.EPM_Automatic: 203 | icon = "Pp" 204 | case ptp.EPM_AperturePriority: 205 | icon = "Nn" 206 | case ptp.EPM_ShutterPriority: 207 | icon = "Ll" 208 | } 209 | 210 | w.DrawString(icon) 211 | } 212 | 213 | func NewFujiISOWidget(img *image.RGBA) *Widget { 214 | // Calculate starting position. 215 | x := float64(img.Bounds().Max.X) - (float64(img.Bounds().Max.X) * 0.2) 216 | y := img.Bounds().Max.Y - 10 217 | 218 | w := NewWhiteGlyphWidget(img, int(x), y) 219 | w.Draw = drawFujiISO 220 | 221 | return w 222 | } 223 | 224 | func drawFujiISO(w *Widget, val int64) { 225 | w.ResetToOrigin() 226 | w.ResetFace() 227 | 228 | iso := ptpfmt.FujiExposureIndexAsString(ip.FujiExposureIndex(val)) 229 | 230 | w.DrawString("is") // iso icon 231 | 232 | if strings.HasPrefix(iso, "S") { 233 | w.Dot.X -= fixed.Int26_6(18 * 64) // offset to the left 234 | w.Dot.Y -= fixed.Int26_6(8 * 64) 235 | w.DrawString("ISO") // auto icon 236 | w.Dot.Y += fixed.Int26_6(8 * 64) // reset Y axis 237 | iso = string([]rune(iso)[1:]) // drop the leading S 238 | } 239 | 240 | w.Face = basicfont.Face7x13 241 | w.Dot.X += fixed.Int26_6(6 * 64) 242 | w.Dot.Y += fixed.Int26_6(2 * 64) 243 | 244 | w.DrawString(iso) // actual value 245 | } 246 | 247 | func NewFujiFilmSimulationWidget(img *image.RGBA) *Widget { 248 | // Calculate starting position. 249 | x := float64(img.Bounds().Min.X) + (float64(img.Bounds().Max.X) * 0.3) 250 | y := 18 251 | 252 | w := NewWhiteGlyphWidget(img, int(x), y) 253 | w.Draw = drawFujiFilmSimulation 254 | 255 | return w 256 | } 257 | 258 | func drawFujiFilmSimulation(w *Widget, val int64) { 259 | w.ResetToOrigin() 260 | 261 | var flm string 262 | 263 | switch ip.FujiFilmSimulation(val) { 264 | case ip.FS_Fuji_Provia: 265 | flm = "()*" 266 | case ip.FS_Fuji_Velvia: 267 | flm = "#$%" 268 | case ip.FS_Fuji_Astia: 269 | flm = "(&%" 270 | case ip.FS_Fuji_Monochrome: 271 | flm = "'&%" 272 | case ip.FS_Fuji_Sepia: 273 | flm = "9DE" 274 | case ip.FS_Fuji_ProNegHigh: 275 | flm = "=>;" 276 | case ip.FS_Fuji_ProNegStandard: 277 | flm = "=><" 278 | case ip.FS_Fuji_MonochromeYeFilter: 279 | flm = "'?@" 280 | case ip.FS_Fuji_MonochromeRFilter: 281 | flm = "'?7" 282 | case ip.FS_Fuji_MonochromeGFilter: 283 | flm = "'?8" 284 | case ip.FS_Fuji_ClassicChrome: 285 | flm = ":,/" 286 | } 287 | 288 | w.DrawString(flm) 289 | } 290 | 291 | func NewFujiFNumberWidget(img *image.RGBA) *Widget { 292 | // Calculate starting position. 293 | x := float64(img.Bounds().Min.X) + (float64(img.Bounds().Max.X) * 0.25) 294 | y := img.Bounds().Max.Y - 10 295 | 296 | w := NewWhiteFontWidget(img, int(x), y) 297 | w.Draw = drawFujiFNumber 298 | 299 | return w 300 | } 301 | 302 | func drawFujiFNumber(w *Widget, val int64) { 303 | w.ResetToOrigin() 304 | 305 | w.DrawString(strings.Replace(ptpfmt.FNumberAsString(uint16(val)), "f/", "F", 1)) 306 | } 307 | 308 | func NewFujiImageSizeWidget(img *image.RGBA) *Widget { 309 | // Calculate starting position. 310 | x := float64(img.Bounds().Max.X) - (float64(img.Bounds().Max.X) * 0.15) + float64(VFGlyphs6x13.Width*3) + 1 311 | y := 18 312 | 313 | w := NewWhiteGlyphWidget(img, int(x), y) 314 | w.Draw = drawFujiImageSize 315 | 316 | return w 317 | } 318 | 319 | func drawFujiImageSize(w *Widget, val int64) { 320 | w.ResetToOrigin() 321 | 322 | var icon string 323 | 324 | switch []rune(ptpfmt.FujiImageAspectRatioAsString(ip.FujiImageSize(val)))[0] { 325 | case 'S': 326 | icon = "jko" 327 | case 'M': 328 | icon = "jqo" 329 | case 'L': 330 | icon = "jro" 331 | } 332 | 333 | w.DrawString(icon) 334 | } 335 | 336 | func NewFujiImageQualityWidget(img *image.RGBA) *Widget { 337 | // Calculate starting position. 338 | x := float64(img.Bounds().Max.X) - (float64(img.Bounds().Max.X) * 0.15) 339 | y := 18 340 | 341 | w := NewWhiteGlyphWidget(img, int(x), y) 342 | w.Draw = drawFujiImageQuality 343 | 344 | return w 345 | } 346 | 347 | func drawFujiImageQuality(w *Widget, val int64) { 348 | w.ResetToOrigin() 349 | w.ResetFace() 350 | 351 | icon := " " // three spaces as default value to represent 'no icon' 352 | var qual string 353 | 354 | switch ip.FujiImageQuality(val) { 355 | case ip.IQ_Fuji_Fine: 356 | qual = "F" 357 | case ip.IQ_Fuji_Normal: 358 | qual = "N" 359 | case ip.IQ_Fuji_FineAndRAW: 360 | icon = "fgh" 361 | qual = "F" 362 | case ip.IQ_Fuji_NormalAndRAW: 363 | icon = "fgh" 364 | qual = "N" 365 | } 366 | 367 | w.DrawString(icon) 368 | w.Face = basicfont.Face7x13 369 | w.DrawString(" " + qual) 370 | } 371 | 372 | func NewFujiWhiteBalanceWidget(img *image.RGBA) *Widget { 373 | // Calculate starting position. 374 | x := float64(img.Bounds().Min.X) + (float64(img.Bounds().Max.X) * 0.26) 375 | y := 18 376 | 377 | w := NewWhiteGlyphWidget(img, int(x), y) 378 | w.Draw = drawFujiWhiteBalance 379 | 380 | return w 381 | } 382 | 383 | func drawFujiWhiteBalance(w *Widget, val int64) { 384 | w.ResetToOrigin() 385 | 386 | var icon string 387 | 388 | switch ptp.WhiteBalance(val) { 389 | case ptp.WB_Daylight: 390 | icon = "XY" 391 | case ptp.WB_Tungsten: 392 | icon = "QR" 393 | case ip.WB_Fuji_Fluorescent1: 394 | icon = "FGH" 395 | case ip.WB_Fuji_Fluorescent2: 396 | icon = "FGJ" 397 | case ip.WB_Fuji_Fluorescent3: 398 | icon = "FGK" 399 | case ip.WB_Fuji_Shade: 400 | icon = "de" 401 | case ip.WB_Fuji_Underwater: 402 | icon = "VW" 403 | case ip.WB_Fuji_Temperature: 404 | icon = "]^" 405 | case ip.WB_Fuji_Custom: 406 | icon = "Z[" 407 | } 408 | 409 | w.DrawString(icon) 410 | } 411 | -------------------------------------------------------------------------------- /fmt/string_generic_test.go: -------------------------------------------------------------------------------- 1 | package fmt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/malc0mn/ptp-ip/ptp" 6 | "testing" 7 | ) 8 | 9 | func TestGenericDevicePropCodeAsString(t *testing.T) { 10 | check := map[ptp.DevicePropCode]string{ 11 | ptp.DPC_BatteryLevel: "battery level", 12 | ptp.DPC_FunctionalMode: "functional mode", 13 | ptp.DPC_ImageSize: "image size", 14 | ptp.DPC_CompressionSetting: "compression setting", 15 | ptp.DPC_WhiteBalance: "white balance", 16 | ptp.DPC_RGBGain: "RGB gain", 17 | ptp.DPC_FNumber: "F-number", 18 | ptp.DPC_FocalLength: "focal length", 19 | ptp.DPC_FocusDistance: "focus distance", 20 | ptp.DPC_FocusMode: "focus mode", 21 | ptp.DPC_ExposureMeteringMode: "exposure metering mode", 22 | ptp.DPC_FlashMode: "flash mode", 23 | ptp.DPC_ExposureTime: "exposure time", 24 | ptp.DPC_ExposureProgramMode: "exposure program mode", 25 | ptp.DPC_ExposureIndex: "ISO", 26 | ptp.DPC_ExposureBiasCompensation: "exposure bias compensation", 27 | ptp.DPC_DateTime: "date time", 28 | ptp.DPC_CaptureDelay: "capture delay", 29 | ptp.DPC_StillCaptureMode: "still capture mode", 30 | ptp.DPC_Contrast: "contrast", 31 | ptp.DPC_Sharpness: "sharpness", 32 | ptp.DPC_DigitalZoom: "digital zoom", 33 | ptp.DPC_EffectMode: "effect mode", 34 | ptp.DPC_BurstNumber: "burst number", 35 | ptp.DPC_BurstInterval: "burst interval", 36 | ptp.DPC_TimelapseNumber: "timelapse number", 37 | ptp.DPC_TimelapseInterval: "timelapse interval", 38 | ptp.DPC_FocusMeteringMode: "focus metering mode", 39 | ptp.DPC_UploadURL: "upload URL", 40 | ptp.DPC_Artist: "artist", 41 | ptp.DPC_CopyrightInfo: "copyright info", 42 | ptp.DevicePropCode(0): "", 43 | } 44 | 45 | for code, want := range check { 46 | got := GenericDevicePropCodeAsString(code) 47 | if got != want { 48 | t.Errorf("GenericDevicePropCodeAsString() return = '%s', want '%s'", got, want) 49 | } 50 | } 51 | } 52 | 53 | func TestPropToDevicePropCode(t *testing.T) { 54 | check := map[string]ptp.DevicePropCode{ 55 | PRP_Delay: ptp.DPC_CaptureDelay, 56 | PRP_Effect: ptp.DPC_EffectMode, 57 | PRP_Exposure: ptp.DPC_ExposureTime, 58 | PRP_ExpBias: ptp.DPC_ExposureBiasCompensation, 59 | PRP_FlashMode: ptp.DPC_FlashMode, 60 | PRP_FocusMeteringMode: ptp.DPC_FocusMeteringMode, 61 | PRP_ISO: ptp.DPC_ExposureIndex, 62 | PRP_WhiteBalance: ptp.DPC_WhiteBalance, 63 | } 64 | 65 | for prop, want := range check { 66 | got, err := GenericPropToDevicePropCode(prop) 67 | if err != nil { 68 | t.Errorf("GenericPropToDevicePropCode() error = %s, want ", err) 69 | } 70 | if got != want { 71 | t.Errorf("GenericPropToDevicePropCode() return = '%#x', want '%#x'", got, want) 72 | } 73 | } 74 | 75 | prop := "test" 76 | got, err := GenericPropToDevicePropCode(prop) 77 | wantE := fmt.Sprintf("unknown field name '%s'", prop) 78 | if err.Error() != wantE { 79 | t.Errorf("GenericPropToDevicePropCode() error = %s, want %s", err, wantE) 80 | } 81 | wantC := ptp.DevicePropCode(0) 82 | if got != wantC { 83 | t.Errorf("GenericPropToDevicePropCode() return = %d, want %d", got, wantC) 84 | } 85 | } 86 | 87 | func TestFormFlagAsString(t *testing.T) { 88 | check := map[ptp.DevicePropFormFlag]string{ 89 | ptp.DPF_FormFlag_None: "none", 90 | ptp.DPF_FormFlag_Range: "range", 91 | ptp.DPF_FormFlag_Enum: "enum", 92 | ptp.DevicePropFormFlag(3): "", 93 | } 94 | 95 | for code, want := range check { 96 | got := FormFlagAsString(code) 97 | if got != want { 98 | t.Errorf("FormFlagAsString() return = '%s', want '%s'", got, want) 99 | } 100 | } 101 | } 102 | 103 | func TestDataTypeCodeAsString(t *testing.T) { 104 | check := map[ptp.DataTypeCode]string{ 105 | ptp.DTC_UNDEF: "undefined", 106 | ptp.DTC_INT8: "int8", 107 | ptp.DTC_UINT8: "uint8", 108 | ptp.DTC_INT16: "int16", 109 | ptp.DTC_UINT16: "uint16", 110 | ptp.DTC_INT32: "int32", 111 | ptp.DTC_UINT32: "uint32", 112 | ptp.DTC_INT64: "int64", 113 | ptp.DTC_UINT64: "uint64", 114 | ptp.DTC_INT128: "int128", 115 | ptp.DTC_UINT128: "uint128", 116 | ptp.DTC_AINT8: "aint8", 117 | ptp.DTC_AUINT8: "auint8", 118 | ptp.DTC_AINT16: "aint16", 119 | ptp.DTC_AUINT16: "auint16", 120 | ptp.DTC_AINT32: "aint32", 121 | ptp.DTC_AUINT32: "auint32", 122 | ptp.DTC_AINT64: "aint64", 123 | ptp.DTC_AUINT64: "auint64", 124 | ptp.DTC_AINT128: "aint128", 125 | ptp.DTC_AUINT128: "auint128", 126 | ptp.DTC_STR: "string", 127 | ptp.DataTypeCode(0xF000): "", 128 | } 129 | 130 | for code, want := range check { 131 | got := DataTypeCodeAsString(code) 132 | if got != want { 133 | t.Errorf("DataTypeCodeAsString() return = '%s', want '%s'", got, want) 134 | } 135 | } 136 | } 137 | 138 | var modes = map[ptp.DevicePropCode]map[int64]string{ 139 | ptp.DPC_EffectMode: { 140 | int64(ptp.FXM_Undefined): "undefined", 141 | int64(ptp.FXM_Standard): "standard", 142 | int64(ptp.FXM_BlackWhite): "black and white", 143 | int64(ptp.FXM_Sepia): "sepia", 144 | int64(ptp.EffectMode(4)): "", 145 | }, 146 | ptp.DPC_ExposureBiasCompensation: { 147 | int64(-3000): "-3", 148 | int64(-2667): "-2 2/3", 149 | int64(-2334): "-2 1/3", 150 | int64(-2000): "-2", 151 | int64(-1667): "-1 2/3", 152 | int64(-1334): "-1 1/3", 153 | int64(-1000): "-1", 154 | int64(0): "0", 155 | int64(334): "1/3", 156 | int64(667): "2/3", 157 | int64(1000): "1", 158 | int64(1334): "1 1/3", 159 | int64(1667): "1 2/3", 160 | int64(2000): "2", 161 | int64(2334): "2 1/3", 162 | int64(2667): "2 2/3", 163 | int64(3000): "3", 164 | }, 165 | ptp.DPC_ExposureMeteringMode: { 166 | int64(ptp.EMM_Undefined): "undefined", 167 | int64(ptp.EMM_Avarage): "average", 168 | int64(ptp.EMM_CenterWeightedAvarage): "center weighted average", 169 | int64(ptp.EMM_MultiSpot): "multi spot", 170 | int64(ptp.EMM_CenterSpot): "center spot", 171 | int64(ptp.ExposureMeteringMode(5)): "", 172 | }, 173 | ptp.DPC_ExposureProgramMode: { 174 | int64(ptp.EPM_Undefined): "undefined", 175 | int64(ptp.EPM_Manual): "manual", 176 | int64(ptp.EPM_Automatic): "automatic", 177 | int64(ptp.EPM_AperturePriority): "aperture priority", 178 | int64(ptp.EPM_ShutterPriority): "shutter priority", 179 | int64(ptp.EPM_ProgramCreative): "program creative", 180 | int64(ptp.EPM_ProgramAction): "program action", 181 | int64(ptp.EPM_Portrait): "portrait", 182 | int64(ptp.ExposureProgramMode(8)): "", 183 | }, 184 | ptp.DPC_FlashMode: { 185 | int64(ptp.FLM_Undefined): "undefined", 186 | int64(ptp.FLM_AutoFlash): "auto flash", 187 | int64(ptp.FLM_FlashOff): "off", 188 | int64(ptp.FLM_FillFlash): "fill", 189 | int64(ptp.FLM_RedEyeAuto): "red eye auto", 190 | int64(ptp.FLM_RedEyeFill): "red eye fill", 191 | int64(ptp.FLM_ExternalSync): "external sync", 192 | int64(ptp.FlashMode(7)): "", 193 | }, 194 | ptp.DPC_FocusMeteringMode: { 195 | int64(ptp.FMM_Undefined): "undefined", 196 | int64(ptp.FMM_CenterSpot): "center spot", 197 | int64(ptp.FMM_MultiSpot): "multi spot", 198 | int64(ptp.FocusMeteringMode(3)): "", 199 | }, 200 | ptp.DPC_FocusMode: { 201 | int64(ptp.FCM_Undefined): "undefined", 202 | int64(ptp.FCM_Manual): "manual", 203 | int64(ptp.FCM_Automatic): "automatic", 204 | int64(ptp.FCM_AutomaticMacro): "automatic macro", 205 | int64(ptp.FocusMode(4)): "", 206 | }, 207 | ptp.DPC_FunctionalMode: { 208 | int64(ptp.FUM_StandardMode): "standard", 209 | int64(ptp.FUM_SleepState): "sleep", 210 | int64(ptp.FunctionalMode(2)): "", 211 | }, 212 | ptp.DPC_StillCaptureMode: { 213 | int64(ptp.SCM_Undefined): "undefined", 214 | int64(ptp.SCM_Normal): "normal", 215 | int64(ptp.SCM_Burst): "burst", 216 | int64(ptp.SCM_Timelapse): "timelapse", 217 | int64(ptp.StillCaptureMode(4)): "", 218 | }, 219 | ptp.DPC_WhiteBalance: { 220 | int64(ptp.WB_Undefined): "undefined", 221 | int64(ptp.WB_Manual): "manual", 222 | int64(ptp.WB_Automatic): "automatic", 223 | int64(ptp.WB_OnePushAutomatic): "one push automatic", 224 | int64(ptp.WB_Daylight): "daylight", 225 | int64(ptp.WB_Fluorescent): "fluorescent", 226 | int64(ptp.WB_Tungsten): "tungsten", 227 | int64(ptp.WB_Flash): "flash", 228 | int64(ptp.WhiteBalance(8)): "", 229 | }, 230 | } 231 | 232 | func TestDevicePropValueAsString(t *testing.T) { 233 | for dpc, vals := range modes { 234 | for val, want := range vals { 235 | got := DevicePropValueAsString(dpc, int64(val)) 236 | if got != want { 237 | t.Errorf("DevicePropValueAsString() return = '%s', want '%s'", got, want) 238 | } 239 | } 240 | } 241 | } 242 | 243 | func TestFNumberAsString(t *testing.T) { 244 | got := FNumberAsString(0x015e) 245 | want := "f/3.5" 246 | if got != want { 247 | t.Errorf("FNumberAsString() return = '%s', want '%s'", got, want) 248 | } 249 | 250 | got = FNumberAsString(0xffff) 251 | want = "automatic" 252 | if got != want { 253 | t.Errorf("FNumberAsString() return = '%s', want '%s'", got, want) 254 | } 255 | } 256 | 257 | func TestEffectModeAsString(t *testing.T) { 258 | for code, want := range modes[ptp.DPC_EffectMode] { 259 | got := EffectModeAsString(ptp.EffectMode(code)) 260 | if got != want { 261 | t.Errorf("EffectModeAsString() return = '%s', want '%s'", got, want) 262 | } 263 | } 264 | } 265 | 266 | func TestExposureBiasCompensationAsString(t *testing.T) { 267 | for ebv, want := range modes[ptp.DPC_ExposureBiasCompensation] { 268 | got := ExposureBiasCompensationAsString(int16(ebv)) 269 | if got != want { 270 | t.Errorf("ExposureBiasCompensationAsString() return = '%s', want '%s'", got, want) 271 | } 272 | } 273 | } 274 | 275 | func TestExposureMeteringModeAsString(t *testing.T) { 276 | for code, want := range modes[ptp.DPC_ExposureMeteringMode] { 277 | got := ExposureMeteringModeAsString(ptp.ExposureMeteringMode(code)) 278 | if got != want { 279 | t.Errorf("ExposureMeteringModeAsString() return = '%s', want '%s'", got, want) 280 | } 281 | } 282 | } 283 | 284 | func TestExposureProgramModeAsString(t *testing.T) { 285 | for code, want := range modes[ptp.DPC_ExposureProgramMode] { 286 | got := ExposureProgramModeAsString(ptp.ExposureProgramMode(code)) 287 | if got != want { 288 | t.Errorf("ExposureProgramModeAsString() return = '%s', want '%s'", got, want) 289 | } 290 | } 291 | } 292 | 293 | func TestFlashModeAsString(t *testing.T) { 294 | for code, want := range modes[ptp.DPC_FlashMode] { 295 | got := FlashModeAsString(ptp.FlashMode(code)) 296 | if got != want { 297 | t.Errorf("FlashModeAsString() return = '%s', want '%s'", got, want) 298 | } 299 | } 300 | } 301 | 302 | func TestFocusMeteringModeAsString(t *testing.T) { 303 | for code, want := range modes[ptp.DPC_FocusMeteringMode] { 304 | got := FocusMeteringModeAsString(ptp.FocusMeteringMode(code)) 305 | if got != want { 306 | t.Errorf("FocusMeteringModeAsString() return = '%s', want '%s'", got, want) 307 | } 308 | } 309 | } 310 | 311 | func TestFocusModeAsString(t *testing.T) { 312 | for code, want := range modes[ptp.DPC_FocusMode] { 313 | got := FocusModeAsString(ptp.FocusMode(code)) 314 | if got != want { 315 | t.Errorf("FocusModeAsString() return = '%s', want '%s'", got, want) 316 | } 317 | } 318 | } 319 | 320 | func TestFunctionalModeAsString(t *testing.T) { 321 | for code, want := range modes[ptp.DPC_FunctionalMode] { 322 | got := FunctionalModeAsString(ptp.FunctionalMode(code)) 323 | if got != want { 324 | t.Errorf("FunctionalModeAsString() return = '%s', want '%s'", got, want) 325 | } 326 | } 327 | } 328 | 329 | func TestSelfTestTypeAsString(t *testing.T) { 330 | check := map[ptp.SelfTestType]string{ 331 | ptp.STT_Default: "default", 332 | ptp.SelfTestType(1): "", 333 | } 334 | 335 | for code, want := range check { 336 | got := SelfTestTypeAsString(code) 337 | if got != want { 338 | t.Errorf("SelfTestTypeAsString() return = '%s', want '%s'", got, want) 339 | } 340 | } 341 | } 342 | 343 | func TestStillCaptureModeAsString(t *testing.T) { 344 | for code, want := range modes[ptp.DPC_StillCaptureMode] { 345 | got := StillCaptureModeAsString(ptp.StillCaptureMode(code)) 346 | if got != want { 347 | t.Errorf("StillCaptureModeAsString() return = '%s', want '%s'", got, want) 348 | } 349 | } 350 | } 351 | 352 | func TestWhiteBalanceAsString(t *testing.T) { 353 | for code, want := range modes[ptp.DPC_WhiteBalance] { 354 | got := WhiteBalanceAsString(ptp.WhiteBalance(code)) 355 | if got != want { 356 | t.Errorf("WhiteBalanceAsString() return = '%s', want '%s'", got, want) 357 | } 358 | } 359 | } 360 | --------------------------------------------------------------------------------