diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 371091a..35ef0e8 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -8,8 +8,9 @@ import ( "sort" "strings" + "github.com/golang/protobuf/jsonpb" + ipb "github.com/mtraver/rpi-ir-remote/irremotepb" "github.com/mtraver/rpi-ir-remote/remote" - "github.com/mtraver/rpi-ir-remote/remote/cambridgecxacn" ) var ( @@ -18,39 +19,47 @@ var ( sendCommand = flag.NewFlagSet("send", flag.ExitOnError) repeat int - remotes = []remote.Remote{cambridgecxacn.New()} + remotes = []ipb.Remote{} ) func init() { listCommand.Usage = func() { - fmt.Println(`list: list available remotes and their commands + fmt.Fprintln(flag.CommandLine.Output(), `list: list available remotes and their commands usage: list`) listCommand.PrintDefaults() } sendCommand.IntVar(&repeat, "repeat", 0, "number of times to repeat command") sendCommand.Usage = func() { - fmt.Println(`send: send an IR code + fmt.Fprintln(flag.CommandLine.Output(), `send: send an IR code usage: send [options] remote command`) sendCommand.PrintDefaults() } flag.Usage = func() { - fmt.Printf("usage: %s {list,send} [options] [args]\n", filepath.Base(os.Args[0])) - fmt.Println("\nCommands:") + message := `usage: %s remote_proto {list,send} [options] [args] + +Positional arguments (required): + remote_proto + path to file containing a JSON-encoded remote proto + +Commands: +` + + fmt.Fprintf(flag.CommandLine.Output(), message, filepath.Base(os.Args[0])) listCommand.Usage() sendCommand.Usage() } } -func getRemote(name string) (remote.Remote, error) { +func getRemote(name string) (ipb.Remote, error) { for _, r := range remotes { if r.Name == name { return r, nil } } - return remote.Remote{}, fmt.Errorf("cli: no remote with name %q", name) + return ipb.Remote{}, fmt.Errorf("cli: no remote with name %q", name) } func list() { @@ -58,7 +67,7 @@ func list() { strs := make([]string, len(remotes)) for i, r := range remotes { - strs[i] = strings.TrimRight(fmt.Sprintf("%v", r), "\n") + strs[i] = strings.TrimRight(remote.String(r), "\n") } fmt.Println(strings.Join(strs, "\n\n")) @@ -71,27 +80,42 @@ func send(remoteName, commandName string, repeat int) { os.Exit(2) } - if err := r.Send(commandName); err != nil { + if err := remote.Send(r, commandName); err != nil { fmt.Printf("%v\n", err) os.Exit(1) } } func main() { - if len(os.Args) < 2 { + if len(os.Args) < 3 { flag.Usage() os.Exit(2) } - switch subcmd := os.Args[1]; subcmd { + // Parse remote proto. + rp := os.Args[1] + file, err := os.Open(rp) + if err != nil { + fmt.Printf("Failed to open remote proto %s: %v\n", rp, err) + os.Exit(1) + } + defer file.Close() + var r ipb.Remote + if err := jsonpb.Unmarshal(file, &r); err != nil { + fmt.Printf("Failed to parse remote proto %s: %v\n", rp, err) + os.Exit(1) + } + remotes = append(remotes, r) + + switch subcmd := os.Args[2]; subcmd { case "list": - if err := listCommand.Parse(os.Args[2:]); err == flag.ErrHelp { + if err := listCommand.Parse(os.Args[3:]); err == flag.ErrHelp { listCommand.Usage() } list() case "send": - sendCommand.Parse(os.Args[2:]) + sendCommand.Parse(os.Args[3:]) if sendCommand.NArg() != 2 { sendCommand.Usage() diff --git a/cmd/server/main.go b/cmd/server/main.go index 47005b4..b4a5f70 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -16,6 +16,7 @@ import ( "time" mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" "github.com/mtraver/iotcore" @@ -23,7 +24,6 @@ import ( cpb "github.com/mtraver/rpi-ir-remote/cmd/server/configpb" ipb "github.com/mtraver/rpi-ir-remote/irremotepb" "github.com/mtraver/rpi-ir-remote/remote" - "github.com/mtraver/rpi-ir-remote/remote/cambridgecxacn" ) const ( @@ -41,8 +41,7 @@ var ( templates = template.Must(template.New("index").Parse(indexTemplate)) - // TODO(mtraver) Get supported remotes from protos given on the command line - remotes = make(map[string]remote.Remote) + remotes = make(map[string]ipb.Remote) ) func init() { @@ -50,9 +49,19 @@ func init() { flag.StringVar(&deviceFilePath, "device", "", "path to a file containing a JSON-encoded Device struct (see github.com/mtraver/iotcore)") flag.StringVar(&caCerts, "cacerts", "", "Path to a set of trustworthy CA certs.\nDownload Google's from https://pki.google.com/roots.pem.") - // TODO(mtraver) Get supported remotes from protos given on the command line - r := cambridgecxacn.New() - remotes[r.Name] = r + flag.Usage = func() { + message := `usage: server [options] remote_proto [remote_proto [remote_proto ...]]: + +Positional arguments (required): + remote_proto + path to file containing a JSON-encoded remote proto + +Options: +` + + fmt.Fprintf(flag.CommandLine.Output(), message) + flag.PrintDefaults() + } } type irRemoteRequest struct { @@ -61,7 +70,7 @@ type irRemoteRequest struct { } type irsendHandler struct { - Remote remote.Remote + Remote ipb.Remote Config cpb.Config Cmd string CheckToken bool @@ -101,7 +110,7 @@ func (h irsendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("Received request: %v %v", h.Remote.Name, h.Cmd) - if err := h.Remote.Send(h.Cmd); err != nil { + if err := remote.Send(h.Remote, h.Cmd); err != nil { log.Printf("Failed to send command %q to %q: %v", h.Cmd, h.Remote.Name, err) w.WriteHeader(http.StatusInternalServerError) return @@ -112,8 +121,8 @@ func (h irsendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } type indexHandler struct { - Remote remote.Remote - Config cpb.Config + Remotes map[string]ipb.Remote + Config cpb.Config } func (h indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -123,11 +132,11 @@ func (h indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } data := struct { - Remote remote.Remote + Remotes map[string]ipb.Remote Config cpb.Config FunFact string }{ - Remote: h.Remote, + Remotes: h.Remotes, Config: h.Config, FunFact: funFacts[rand.Intn(len(funFacts))], } @@ -158,7 +167,7 @@ func commandHandler(client mqtt.Client, msg mqtt.Message) { return } - if err := r.Send(action.GetCommand()); err != nil { + if err := remote.Send(r, action.GetCommand()); err != nil { log.Printf("commandHandler: failed to send command %q to %q: %v", action.GetCommand(), r.Name, err) return } @@ -220,11 +229,33 @@ func main() { os.Exit(2) } + if len(flag.Args()) < 1 { + fmt.Fprintf(flag.CommandLine.Output(), "At least one remote proto must be given\n") + flag.Usage() + os.Exit(2) + } + + // Parse config. config, err := serverconfig.Load(configFilePath) if err != nil { log.Fatalf("Failed to parse config file %s: %v", configFilePath, err) } + // Parse remote protos. + for _, rp := range flag.Args() { + file, err := os.Open(rp) + if err != nil { + log.Fatalf("Failed to open remote proto %s: %v", rp, err) + } + defer file.Close() + + var r ipb.Remote + if err := jsonpb.Unmarshal(file, &r); err != nil { + log.Fatalf("Failed to parse remote proto %s: %v", rp, err) + } + remotes[r.Name] = r + } + // If an MQTT device config was given, connect to the MQTT broker. In the connect handler // we'll subscribe to the commands topic. if deviceFilePath != "" { @@ -250,30 +281,29 @@ func main() { }() } - // TODO(mtraver) Get supported remotes from protos given on the command line - r := cambridgecxacn.New() - webuiMux := http.NewServeMux() webuiMux.Handle("/", indexHandler{ - Remote: r, - Config: config, + Remotes: remotes, + Config: config, }) apiMux := http.NewServeMux() - for name := range r.Commands { - apiMux.Handle(fmt.Sprintf("/%v", name), irsendHandler{ - Remote: r, - Config: config, - Cmd: name, - CheckToken: true, - }) - - webuiMux.Handle(fmt.Sprintf("/%v", name), irsendHandler{ - Remote: r, - Config: config, - Cmd: name, - CheckToken: false, - }) + for _, r := range remotes { + for _, code := range r.Code { + apiMux.Handle(fmt.Sprintf("/%v/%v", r.Name, code.Name), irsendHandler{ + Remote: r, + Config: config, + Cmd: code.Name, + CheckToken: true, + }) + + webuiMux.Handle(fmt.Sprintf("/%v/%v", r.Name, code.Name), irsendHandler{ + Remote: r, + Config: config, + Cmd: code.Name, + CheckToken: false, + }) + } } // If an MQTT device config was not given, start the API HTTP server. diff --git a/cmd/server/template.go b/cmd/server/template.go index fd9580a..810e352 100644 --- a/cmd/server/template.go +++ b/cmd/server/template.go @@ -19,10 +19,15 @@ const indexTemplate = `

Fun fact! {{ .FunFact }}

-

Remote: {{ .Remote.Name }}

+ {{ range $remoteName, $remote := .Remotes }} +

{{ $remoteName }}

- {{ range $name, $command := .Remote.Commands }} - + {{ range $i, $code := $remote.Code }} + +
+
+ {{ end }} +


{{ end }} diff --git a/config/cambridge_cxa.pb.json b/config/cambridge_cxa.pb.json new file mode 100644 index 0000000..8ef4079 --- /dev/null +++ b/config/cambridge_cxa.pb.json @@ -0,0 +1,33 @@ +{ + "name": "cambridge_cxa", + "code": [ + { + "name": "off", + "code": "KEY_POWER_OFF" + }, + { + "name": "music", + "code": "KEY_SOURCE_D1" + }, + { + "name": "tv", + "code": "KEY_SOURCE_D2" + }, + { + "name": "volup", + "code": "KEY_VOLUMEUP" + }, + { + "name": "voldown", + "code": "KEY_VOLUMEDOWN" + }, + { + "name": "direct", + "code": "KEY_DIRECT" + }, + { + "name": "lcd", + "code": "KEY_LCD" + } + ] +} diff --git a/irremotepb/irremote.pb.go b/irremotepb/irremote.pb.go index 4949899..2c3edec 100644 --- a/irremotepb/irremote.pb.go +++ b/irremotepb/irremote.pb.go @@ -130,25 +130,124 @@ func (m *Request) GetAction() *Action { return nil } +type Code struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Code) Reset() { *m = Code{} } +func (m *Code) String() string { return proto.CompactTextString(m) } +func (*Code) ProtoMessage() {} +func (*Code) Descriptor() ([]byte, []int) { + return fileDescriptor_f4a91b872702a0e6, []int{2} +} + +func (m *Code) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Code.Unmarshal(m, b) +} +func (m *Code) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Code.Marshal(b, m, deterministic) +} +func (m *Code) XXX_Merge(src proto.Message) { + xxx_messageInfo_Code.Merge(m, src) +} +func (m *Code) XXX_Size() int { + return xxx_messageInfo_Code.Size(m) +} +func (m *Code) XXX_DiscardUnknown() { + xxx_messageInfo_Code.DiscardUnknown(m) +} + +var xxx_messageInfo_Code proto.InternalMessageInfo + +func (m *Code) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Code) GetCode() string { + if m != nil { + return m.Code + } + return "" +} + +type Remote struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Code []*Code `protobuf:"bytes,2,rep,name=code,proto3" json:"code,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Remote) Reset() { *m = Remote{} } +func (m *Remote) String() string { return proto.CompactTextString(m) } +func (*Remote) ProtoMessage() {} +func (*Remote) Descriptor() ([]byte, []int) { + return fileDescriptor_f4a91b872702a0e6, []int{3} +} + +func (m *Remote) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Remote.Unmarshal(m, b) +} +func (m *Remote) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Remote.Marshal(b, m, deterministic) +} +func (m *Remote) XXX_Merge(src proto.Message) { + xxx_messageInfo_Remote.Merge(m, src) +} +func (m *Remote) XXX_Size() int { + return xxx_messageInfo_Remote.Size(m) +} +func (m *Remote) XXX_DiscardUnknown() { + xxx_messageInfo_Remote.DiscardUnknown(m) +} + +var xxx_messageInfo_Remote proto.InternalMessageInfo + +func (m *Remote) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Remote) GetCode() []*Code { + if m != nil { + return m.Code + } + return nil +} + func init() { proto.RegisterType((*Action)(nil), "irremote.Action") proto.RegisterType((*Request)(nil), "irremote.Request") + proto.RegisterType((*Code)(nil), "irremote.Code") + proto.RegisterType((*Remote)(nil), "irremote.Remote") } func init() { proto.RegisterFile("irremotepb/irremote.proto", fileDescriptor_f4a91b872702a0e6) } var fileDescriptor_f4a91b872702a0e6 = []byte{ - // 182 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x2c, 0x2a, 0x4a, - 0xcd, 0xcd, 0x2f, 0x49, 0x2d, 0x48, 0xd2, 0x87, 0x31, 0xf5, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85, - 0x38, 0x60, 0x7c, 0xa5, 0x70, 0x2e, 0x36, 0xc7, 0xe4, 0x92, 0xcc, 0xfc, 0x3c, 0x21, 0x69, 0x2e, - 0x4e, 0x88, 0x58, 0x7c, 0x66, 0x8a, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67, 0x10, 0x07, 0x44, 0xc0, - 0x33, 0x45, 0x48, 0x82, 0x8b, 0x3d, 0x39, 0x3f, 0x37, 0x37, 0x31, 0x2f, 0x45, 0x82, 0x09, 0x2c, - 0x05, 0xe3, 0x0a, 0x89, 0x71, 0xb1, 0x15, 0xa5, 0x16, 0xa4, 0x26, 0x96, 0x48, 0x30, 0x2b, 0x30, - 0x6a, 0xb0, 0x06, 0x41, 0x79, 0x4a, 0x49, 0x5c, 0xec, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, - 0x20, 0x93, 0x53, 0x52, 0xcb, 0x32, 0x93, 0x91, 0x4d, 0x86, 0x08, 0x78, 0xa6, 0x08, 0x09, 0x70, - 0x31, 0x67, 0x95, 0x97, 0x40, 0x4d, 0x05, 0x31, 0x85, 0x34, 0xb8, 0xd8, 0x12, 0xc1, 0x4e, 0x02, - 0x9b, 0xc8, 0x6d, 0x24, 0xa0, 0x07, 0x77, 0x3d, 0xc4, 0xa9, 0x41, 0x50, 0x79, 0x27, 0x9e, 0x28, - 0x2e, 0x84, 0x1f, 0x93, 0xd8, 0xc0, 0x7e, 0x33, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x96, 0x65, - 0x70, 0x5f, 0xf8, 0x00, 0x00, 0x00, + // 230 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0xbd, 0x4e, 0x03, 0x31, + 0x10, 0x84, 0x75, 0x5c, 0x70, 0x92, 0x0d, 0x42, 0xd1, 0x16, 0xe8, 0x10, 0x4d, 0xe4, 0xca, 0xd5, + 0x21, 0x85, 0x17, 0xe0, 0xa7, 0x4a, 0xeb, 0x06, 0x89, 0x06, 0xf9, 0xec, 0x2d, 0x8c, 0xe4, 0xf3, + 0x61, 0x0c, 0xbc, 0x3e, 0xf2, 0xcf, 0x25, 0x14, 0xe9, 0x66, 0x66, 0xb5, 0xdf, 0x8e, 0x16, 0x6e, + 0x6d, 0x08, 0xe4, 0x7c, 0xa4, 0x69, 0xb8, 0x9f, 0x65, 0x3f, 0x05, 0x1f, 0x3d, 0xae, 0x66, 0xcf, + 0x5f, 0x81, 0x3d, 0xe9, 0x68, 0xfd, 0x88, 0x77, 0xb0, 0x2e, 0xd9, 0xbb, 0x35, 0x5d, 0xb3, 0x6b, + 0xc4, 0x5a, 0xae, 0x4a, 0x70, 0x30, 0xd8, 0xc1, 0x52, 0x7b, 0xe7, 0xd4, 0x68, 0xba, 0x8b, 0x3c, + 0x9a, 0x2d, 0xde, 0x00, 0x0b, 0x34, 0x91, 0x8a, 0x5d, 0xbb, 0x6b, 0xc4, 0xa5, 0xac, 0x8e, 0x0f, + 0xb0, 0x94, 0xf4, 0xf9, 0x4d, 0x5f, 0x31, 0x91, 0x0d, 0xfd, 0x58, 0xfd, 0x9f, 0x5c, 0x82, 0x83, + 0xc1, 0x2d, 0xb4, 0x1f, 0xbf, 0xb1, 0x52, 0x93, 0x44, 0x01, 0x4c, 0xe5, 0x4a, 0x99, 0xb8, 0xd9, + 0x6f, 0xfb, 0x63, 0xfb, 0x52, 0x55, 0xd6, 0x39, 0xef, 0x61, 0xf1, 0xe2, 0x0d, 0x21, 0xc2, 0x62, + 0x54, 0x8e, 0x2a, 0x3b, 0xeb, 0x94, 0x69, 0x6f, 0xa8, 0x82, 0xb3, 0xe6, 0x8f, 0xc0, 0x64, 0x06, + 0x9d, 0xdd, 0xe0, 0xc7, 0x8d, 0x56, 0x6c, 0xf6, 0xd7, 0xa7, 0xab, 0xe9, 0x46, 0x21, 0x3c, 0x5f, + 0xbd, 0xc1, 0xe9, 0xab, 0x03, 0xcb, 0xdf, 0x7c, 0xf8, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xf9, 0xe6, + 0x0b, 0x2f, 0x6a, 0x01, 0x00, 0x00, } diff --git a/irremotepb/irremote.proto b/irremotepb/irremote.proto index 9c9d441..876a5fa 100644 --- a/irremotepb/irremote.proto +++ b/irremotepb/irremote.proto @@ -13,3 +13,13 @@ message Request { string jwt = 2; Action action = 3; } + +message Code { + string name = 1; + string code = 2; +} + +message Remote { + string name = 1; + repeated Code code = 2; +} diff --git a/remote/cambridgecxacn/cambridgecxacn.go b/remote/cambridgecxacn/cambridgecxacn.go deleted file mode 100644 index e1de35d..0000000 --- a/remote/cambridgecxacn/cambridgecxacn.go +++ /dev/null @@ -1,19 +0,0 @@ -package cambridgecxacn - -import ( - "github.com/mtraver/rpi-ir-remote/remote" -) - -func New() remote.Remote { - r := remote.NewRemote("cambridge_cxa") - - r.AddCommand("off", "KEY_POWER_OFF") - r.AddCommand("music", "KEY_SOURCE_D1") - r.AddCommand("tv", "KEY_SOURCE_D2") - r.AddCommand("volup", "KEY_VOLUMEUP") - r.AddCommand("voldown", "KEY_VOLUMEDOWN") - r.AddCommand("direct", "KEY_DIRECT") - r.AddCommand("lcd", "KEY_LCD") - - return r -} diff --git a/remote/remote.go b/remote/remote.go index b7e69d0..b8412b9 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -4,54 +4,37 @@ import ( "fmt" "strings" "text/tabwriter" - "time" + ipb "github.com/mtraver/rpi-ir-remote/irremotepb" "github.com/mtraver/rpi-ir-remote/irsend" ) -type Command struct { - Code string - RepeatInterval time.Duration -} - -type Remote struct { - Name string - Commands map[string]Command -} - -func NewRemote(name string) Remote { - return Remote{ - Name: name, - Commands: make(map[string]Command), - } -} - -func (r Remote) AddCommand(name string, code string) { - r.Commands[name] = Command{ - Code: code, - } -} - -func (r Remote) Send(command string) error { - c, ok := r.Commands[command] - if !ok { - return fmt.Errorf("remote: remote %q does not have command %q", r.Name, command) - } - - return irsend.Send(r.Name, c.Code) -} - -func (r Remote) String() string { +func String(r ipb.Remote) string { var b strings.Builder fmt.Fprintf(&b, "Name: %v\n", r.Name) w := tabwriter.NewWriter(&b, 0, 0, 1, ' ', 0) fmt.Fprintln(w, "Command name\tCode\t") - for name, command := range r.Commands { - fmt.Fprintln(w, strings.Join([]string{name, command.Code}, "\t")+"\t") + for _, code := range r.Code { + fmt.Fprintln(w, strings.Join([]string{code.Name, code.Code}, "\t")+"\t") } w.Flush() return b.String() } + +func Send(remote ipb.Remote, name string) error { + var c string + for _, code := range remote.Code { + if code.Name == name { + c = code.Code + } + } + + if c == "" { + return fmt.Errorf("remote: remote %q does not have command %q", remote.Name, name) + } + + return irsend.Send(remote.Name, c) +}