]> go.fuhry.dev Git - runtime.git/commitdiff
add gRPC client and server for Bryston autio device remote control
authorDan Fuhry <dan@fuhry.com>
Sat, 22 Mar 2025 01:29:53 +0000 (21:29 -0400)
committerDan Fuhry <dan@fuhry.com>
Sat, 22 Mar 2025 01:29:53 +0000 (21:29 -0400)
21 files changed:
.gitignore
automation/bryston_ctl/bryston_ctl.go [new file with mode: 0644]
automation/bryston_ctl/capture.txt [new file with mode: 0644]
automation/bryston_ctl/cli/Makefile [new file with mode: 0644]
automation/bryston_ctl/cli/main.go [new file with mode: 0644]
automation/bryston_ctl/client/Makefile [new file with mode: 0644]
automation/bryston_ctl/client/main.go [new file with mode: 0644]
automation/bryston_ctl/server.go [new file with mode: 0644]
automation/bryston_ctl/server/Makefile [new file with mode: 0644]
automation/bryston_ctl/server/bryston_ctl_acl.yaml [new file with mode: 0644]
automation/bryston_ctl/server/main.go [new file with mode: 0644]
proto/service/bryston_ctl/GPBMetadata/BrystonCtl.php [new file with mode: 0644]
proto/service/bryston_ctl/Makefile [new file with mode: 0644]
proto/service/bryston_ctl/bryston_ctl.pb.go [new file with mode: 0644]
proto/service/bryston_ctl/bryston_ctl.proto [new file with mode: 0644]
proto/service/bryston_ctl/bryston_ctl_grpc.pb.go [new file with mode: 0644]
proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/AbsoluteVolumeRequest.php [new file with mode: 0644]
proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/B100Command.php [new file with mode: 0644]
proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/BrystonModel.php [new file with mode: 0644]
proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/CommandRequest.php [new file with mode: 0644]
proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/CommandResponse.php [new file with mode: 0644]

index 187165e7a70e5678e816121e1996583c51a490ff..de7cd732be648641e24a36fa633c990e4836c4e1 100644 (file)
@@ -50,5 +50,9 @@ ldap/health_exporter/health_exporter
 envoy/xds/envoy_xds/envoy_xds
 mtls/mtls_exporter/mtls_exporter
 http/samlproxy/samlproxy
+automation/bryston_ctl/cli/cli
+automation/bryston_ctl/client/client
+automation/bryston_ctl/server/server
 
 /vendor/
+
diff --git a/automation/bryston_ctl/bryston_ctl.go b/automation/bryston_ctl/bryston_ctl.go
new file mode 100644 (file)
index 0000000..2488fce
--- /dev/null
@@ -0,0 +1,275 @@
+package bryston_ctl
+
+import (
+       "flag"
+       "fmt"
+       "os"
+       "strconv"
+       "time"
+
+       "go.bug.st/serial"
+
+       bryston_ctl_pb "go.fuhry.dev/runtime/proto/service/bryston_ctl"
+       "go.fuhry.dev/runtime/utils/hashset"
+)
+
+const VOLUME_STEPS = 32
+
+var baudRates = hashset.FromSlice([]uint{
+       9600,
+       38400,
+       115200,
+})
+
+var defaultBaudRate uint = 9600
+var defaultSerialPort string = "/dev/ttyS0"
+
+type BrystonControlOptions struct {
+       BaudRate uint
+       Port     string
+}
+
+type BrystonController interface {
+       Command(int32) (string, error)
+       AbsoluteVolume(bryston_ctl_pb.BrystonModel, int) error
+       Close() error
+}
+
+type brystonCtl struct {
+       port      serial.Port
+       absVolume int
+}
+
+func NewController() (BrystonController, error) {
+       opts := BrystonControlOptions{
+               Port:     defaultSerialPort,
+               BaudRate: defaultBaudRate,
+       }
+
+       return NewControllerWithOpts(opts)
+}
+
+func NewControllerWithOpts(opts BrystonControlOptions) (BrystonController, error) {
+       mode := &serial.Mode{
+               BaudRate: int(opts.BaudRate),
+               DataBits: 8,
+               Parity:   serial.NoParity,
+               StopBits: serial.OneStopBit,
+       }
+       port, err := serial.Open(opts.Port, mode)
+       if err != nil {
+               return nil, err
+       }
+       port.SetReadTimeout(250 * time.Millisecond)
+
+       return &brystonCtl{
+               port:      port,
+               absVolume: -1,
+       }, nil
+}
+
+func setBaudRate(flagValue string) error {
+       baudRate, err := strconv.Atoi(flagValue)
+       if err != nil {
+               return err
+       }
+
+       if !baudRates.Contains(uint(baudRate)) {
+               return fmt.Errorf("baudRate %d is invalid, expected one of: %v",
+                       baudRate, baudRates.AsSortedSlice())
+       }
+
+       defaultBaudRate = uint(baudRate)
+
+       return nil
+}
+
+func setSerialPort(flagValue string) error {
+       stat, err := os.Stat(flagValue)
+       if err != nil {
+               return err
+       }
+
+       if (stat.Mode().Type() & os.ModeCharDevice) != os.ModeCharDevice {
+               return fmt.Errorf("device %s is not a character device", flagValue)
+       }
+
+       defaultSerialPort = flagValue
+       return nil
+}
+
+func NewBrystonModelFlag() (*bryston_ctl_pb.BrystonModel, func(string) error) {
+       model := bryston_ctl_pb.BrystonModel_INVALID
+       modelP := &model
+
+       flagFunc := func(flagValue string) error {
+               value, ok := bryston_ctl_pb.BrystonModel_value[flagValue]
+               if !ok || bryston_ctl_pb.BrystonModel(value) == bryston_ctl_pb.BrystonModel_INVALID {
+                       return fmt.Errorf("invalid Bryston model ID: %q", flagValue)
+               }
+
+               *modelP = bryston_ctl_pb.BrystonModel(value)
+               return nil
+       }
+
+       return modelP, flagFunc
+}
+
+func NewBrystonCommandFlag(model *bryston_ctl_pb.BrystonModel) (*bryston_ctl_pb.CommandRequest, func(string) error) {
+       cmd := &bryston_ctl_pb.CommandRequest{}
+
+       flagFunc := func(flagValue string) error {
+               switch *model {
+               case bryston_ctl_pb.BrystonModel_INVALID:
+                       return fmt.Errorf("invalid model %v", *model)
+               case bryston_ctl_pb.BrystonModel_B100:
+                       value, ok := bryston_ctl_pb.B100Command_value[flagValue]
+                       if !ok {
+                               return fmt.Errorf("invalid Bryston B100 command: %q", flagValue)
+                       }
+
+                       cmd.Model = *model
+                       cmd.Command = &bryston_ctl_pb.CommandRequest_B100Command{
+                               B100Command: bryston_ctl_pb.B100Command(value),
+                       }
+                       return nil
+               }
+
+               panic("unreachable code")
+       }
+
+       return cmd, flagFunc
+}
+
+func (ctl *brystonCtl) Command(cmd int32) (string, error) {
+       buf := []byte(fmt.Sprintf("%03d\r", cmd))
+       nw, err := ctl.port.Write(buf)
+       if nw != len(buf) {
+               return "", fmt.Errorf("write failed")
+       }
+       if err != nil {
+               return "", err
+       }
+
+       xtra := []byte{}
+       name, _ := bryston_ctl_pb.B100Command_name[cmd]
+
+       switch name {
+       case "POWER_ON":
+               // power-on commands require special handling.
+               // the receiver sends 3 CRs:
+               //   1. ack power-on request
+               //   2. initial microcontroller startup (done writing s/w version)
+               //   3. powerup done, mute relay disengaged
+               ctl.port.SetReadTimeout(5 * time.Second)
+               defer ctl.port.SetReadTimeout(250 * time.Millisecond)
+               crCount := 0
+               b := make([]byte, 1)
+       readLoop:
+               for {
+                       nr, err := ctl.port.Read(b)
+                       if nr > 0 && b[0] == '\r' {
+                               crCount++
+                               if crCount == 3 {
+                                       break readLoop
+                               }
+                       } else if nr > 0 && b[0] == '>' {
+                               continue readLoop
+                       } else if nr > 0 && b[0] == '!' {
+                               return "", fmt.Errorf("%q command failed", name)
+                       } else if nr == 0 || err != nil {
+                               break readLoop
+                       } else {
+                               xtra = append(xtra, b[:nr]...)
+                       }
+               }
+       case "VOLUME_UP", "VOLUME_DOWN":
+               if err := ctl.readResult(); err != nil {
+                       return "", err
+               }
+               // wait for volume knob to actually move
+               time.Sleep(250 * time.Millisecond)
+       default:
+               if err := ctl.readResult(); err != nil {
+                       return "", err
+               }
+       }
+       return string(xtra), nil
+}
+func (ctl *brystonCtl) readResult() error {
+       out := make([]byte, 1)
+       nr, err := ctl.port.Read(out)
+       if err != nil {
+               return err
+       }
+       if nr < 1 {
+               return fmt.Errorf("no data was able to be read")
+       }
+       // read the carriage return
+       ctl.port.Read(make([]byte, 1))
+
+       if out[0] == '!' {
+               return fmt.Errorf("device reports invalid/failed command")
+       } else if out[0] != '>' {
+               return fmt.Errorf("unexpected result from device: %+v", out)
+       }
+
+       return nil
+}
+func (ctl *brystonCtl) AbsoluteVolume(model bryston_ctl_pb.BrystonModel, level int) error {
+       if level < 0 || level > VOLUME_STEPS {
+               return fmt.Errorf("volume level out of range (0-%d): %d", VOLUME_STEPS, level)
+       }
+       var upCmd, downCmd int32
+
+       switch model {
+       case bryston_ctl_pb.BrystonModel_B100:
+               downCmd = bryston_ctl_pb.B100Command_value["VOLUME_DOWN"]
+               upCmd = bryston_ctl_pb.BrystonModel_value["VOLUME_UP"]
+       default:
+               return fmt.Errorf("invalid model: %q", model)
+       }
+
+       if ctl.absVolume == -1 {
+               for i := 0; i < VOLUME_STEPS; i++ {
+                       _, err := ctl.Command(downCmd)
+                       if err != nil {
+                               return err
+                       }
+               }
+               ctl.absVolume = 0
+       }
+
+       diff := level - ctl.absVolume
+       if diff == 0 {
+               return nil
+       } else if diff > 0 {
+               // increase volume
+               for i := 0; i < diff; i++ {
+                       _, err := ctl.Command(upCmd)
+                       if err != nil {
+                               return err
+                       }
+                       ctl.absVolume++
+               }
+       } else if diff < 0 {
+               // decrease volume
+               for i := 0; i < (-1 * diff); i++ {
+                       _, err := ctl.Command(downCmd)
+                       if err != nil {
+                               return err
+                       }
+                       ctl.absVolume--
+               }
+       }
+       return nil
+}
+
+func (ctl *brystonCtl) Close() error {
+       return ctl.port.Close()
+}
+
+func init() {
+       flag.Func("bryston.baud-rate", "baud rate for communication with Bryston devices", setBaudRate)
+       flag.Func("bryston.port", "serial port for communication with Bryston devices", setSerialPort)
+}
diff --git a/automation/bryston_ctl/capture.txt b/automation/bryston_ctl/capture.txt
new file mode 100644 (file)
index 0000000..a1dde68
--- /dev/null
@@ -0,0 +1 @@
+>\r>\r000419 B100 R01.5 2007-02-13 \r>\r>\r
\ No newline at end of file
diff --git a/automation/bryston_ctl/cli/Makefile b/automation/bryston_ctl/cli/Makefile
new file mode 100644 (file)
index 0000000..bfab546
--- /dev/null
@@ -0,0 +1,14 @@
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+       rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+       go build $(GOBUILDFLAGS) -o $@ $<
+
diff --git a/automation/bryston_ctl/cli/main.go b/automation/bryston_ctl/cli/main.go
new file mode 100644 (file)
index 0000000..2b9f6a7
--- /dev/null
@@ -0,0 +1,52 @@
+package main
+
+import (
+       "flag"
+
+       "go.fuhry.dev/runtime/automation/bryston_ctl"
+       bryston_ctl_pb "go.fuhry.dev/runtime/proto/service/bryston_ctl"
+       "go.fuhry.dev/runtime/utils/log"
+)
+
+func main() {
+       model, modelFF := bryston_ctl.NewBrystonModelFlag()
+       flag.Func("model", "Bryston device model to control", modelFF)
+       cmd, cmdFF := bryston_ctl.NewBrystonCommandFlag(model)
+       flag.Func("command", "Command to send to Bryston device", cmdFF)
+       absVol := flag.Int("absvol", -1, "Set volume to absolute value")
+
+       flag.Parse()
+
+       ctl, err := bryston_ctl.NewController()
+       if err != nil {
+               log.Panic(err)
+               return
+       }
+       defer ctl.Close()
+
+       if cmd != nil && cmd.Model != bryston_ctl_pb.BrystonModel_INVALID {
+               var cmdInt int32
+               switch cmd.GetModel() {
+               case bryston_ctl_pb.BrystonModel_B100:
+                       if b100cmd, ok := cmd.Command.(*bryston_ctl_pb.CommandRequest_B100Command); ok {
+                               cmdInt = int32(b100cmd.B100Command)
+                       } else {
+                               log.Panic("B100 command unset")
+                       }
+               }
+               xtra, err := ctl.Command(cmdInt)
+               if err != nil {
+                       log.Panic(err)
+               }
+               log.Default().Notice("OK")
+               if xtra != "" {
+                       log.Default().Infof("extra data: %s", xtra)
+               }
+       } else if absVol != nil && *absVol != -1 {
+               err = ctl.AbsoluteVolume(*model, *absVol)
+               if err != nil {
+                       log.Panic(err)
+               }
+               log.Default().Notice("OK")
+       }
+}
diff --git a/automation/bryston_ctl/client/Makefile b/automation/bryston_ctl/client/Makefile
new file mode 100644 (file)
index 0000000..bfab546
--- /dev/null
@@ -0,0 +1,14 @@
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+       rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+       go build $(GOBUILDFLAGS) -o $@ $<
+
diff --git a/automation/bryston_ctl/client/main.go b/automation/bryston_ctl/client/main.go
new file mode 100644 (file)
index 0000000..723d8f6
--- /dev/null
@@ -0,0 +1,49 @@
+package main
+
+import (
+       "context"
+       "flag"
+       "os/signal"
+       "syscall"
+
+       "go.fuhry.dev/runtime/automation/bryston_ctl"
+       "go.fuhry.dev/runtime/grpc"
+       "go.fuhry.dev/runtime/mtls"
+       bryston_ctl_pb "go.fuhry.dev/runtime/proto/service/bryston_ctl"
+       "go.fuhry.dev/runtime/utils/log"
+)
+
+func main() {
+       ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+       model, modelFF := bryston_ctl.NewBrystonModelFlag()
+       flag.Func("model", "Bryston device model to control", modelFF)
+       cmd, cmdFF := bryston_ctl.NewBrystonCommandFlag(model)
+       flag.Func("command", "Command to send to Bryston device", cmdFF)
+
+       flag.Parse()
+       logger := log.Default().WithPrefix("BrystonCtlClient")
+
+       clientId := mtls.DefaultIdentity()
+       serverId := mtls.NewServiceIdentity("bryston_ctl")
+       client, err := grpc.NewGrpcClient(ctx, serverId, clientId)
+       if err != nil {
+               logger.Panic(err)
+       }
+
+       conn, err := client.Conn()
+       if err != nil {
+               logger.Panic(err)
+       }
+
+       defer conn.Close()
+       brystonCl := bryston_ctl_pb.NewBrystonRemoteControlServiceClient(conn)
+
+       logger.Noticef("client sending command: %+v", cmd)
+       result, err := brystonCl.Command(ctx, cmd)
+       if err != nil {
+               logger.Panic(err)
+       }
+
+       logger.Noticef("server replied: %+v", result)
+}
diff --git a/automation/bryston_ctl/server.go b/automation/bryston_ctl/server.go
new file mode 100644 (file)
index 0000000..0d0cb90
--- /dev/null
@@ -0,0 +1,47 @@
+package bryston_ctl
+
+import (
+       "context"
+       "fmt"
+
+       bryston_ctl_pb "go.fuhry.dev/runtime/proto/service/bryston_ctl"
+)
+
+type server struct {
+       bryston_ctl_pb.BrystonRemoteControlServiceServer
+
+       ctl BrystonController
+}
+
+func NewBrystonControlServer(ctl BrystonController) bryston_ctl_pb.BrystonRemoteControlServiceServer {
+       return &server{
+               ctl: ctl,
+       }
+}
+
+func (s *server) Command(ctx context.Context, req *bryston_ctl_pb.CommandRequest) (*bryston_ctl_pb.CommandResponse, error) {
+       var cmd int32
+       switch req.GetModel() {
+       case bryston_ctl_pb.BrystonModel_INVALID:
+               return nil, fmt.Errorf("invalid model")
+       case bryston_ctl_pb.BrystonModel_B100:
+               if b100cmd, ok := req.Command.(*bryston_ctl_pb.CommandRequest_B100Command); ok {
+                       cmd = int32(b100cmd.B100Command)
+               } else {
+                       return nil, fmt.Errorf("B100 command unset")
+               }
+       }
+       xtra, err := s.ctl.Command(cmd)
+       return &bryston_ctl_pb.CommandResponse{
+               Success: err == nil,
+               Message: xtra,
+       }, nil
+}
+
+func (s *server) AbsoluteVolume(ctx context.Context, req *bryston_ctl_pb.AbsoluteVolumeRequest) (*bryston_ctl_pb.CommandResponse, error) {
+       err := s.ctl.AbsoluteVolume(req.GetModel(), int(req.GetLevel()))
+       return &bryston_ctl_pb.CommandResponse{
+               Success: err == nil,
+               Message: "",
+       }, nil
+}
diff --git a/automation/bryston_ctl/server/Makefile b/automation/bryston_ctl/server/Makefile
new file mode 100644 (file)
index 0000000..bfab546
--- /dev/null
@@ -0,0 +1,14 @@
+GOSRC = $(wildcard *.go)
+GOEXE = $(shell basename `pwd`)
+GOBUILDFLAGS := -buildmode=pie -trimpath
+
+all: $(GOEXE)
+
+clean:
+       rm -fv $(GOEXE)
+
+.PHONY: all clean
+
+$(GOEXE): %: $(GOSRC)
+       go build $(GOBUILDFLAGS) -o $@ $<
+
diff --git a/automation/bryston_ctl/server/bryston_ctl_acl.yaml b/automation/bryston_ctl/server/bryston_ctl_acl.yaml
new file mode 100644 (file)
index 0000000..1021773
--- /dev/null
@@ -0,0 +1,6 @@
+DEFAULT:
+  - service: bryston_ctl
+
+fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService/Command:
+  - user: dan
+  - service: bryston_ctl
diff --git a/automation/bryston_ctl/server/main.go b/automation/bryston_ctl/server/main.go
new file mode 100644 (file)
index 0000000..d93fe73
--- /dev/null
@@ -0,0 +1,53 @@
+package main
+
+import (
+       "context"
+       "flag"
+       "os/signal"
+       "syscall"
+
+       google_grpc "google.golang.org/grpc"
+
+       "go.fuhry.dev/runtime/automation/bryston_ctl"
+       "go.fuhry.dev/runtime/grpc"
+       "go.fuhry.dev/runtime/mtls"
+       bryston_ctl_pb "go.fuhry.dev/runtime/proto/service/bryston_ctl"
+       "go.fuhry.dev/runtime/utils/log"
+)
+
+func main() {
+       var err error
+
+       flag.Parse()
+
+       serverIdentity := mtls.DefaultIdentity()
+       s, err := grpc.NewGrpcServer(serverIdentity)
+       if err != nil {
+               log.Panic(err)
+       }
+
+       ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+
+       ctl, err := bryston_ctl.NewController()
+       if err != nil {
+               log.Panic(err)
+               return
+       }
+       defer ctl.Close()
+
+       bcs := bryston_ctl.NewBrystonControlServer(ctl)
+
+       err = s.PublishAndServe(ctx, func(s *google_grpc.Server) {
+               bryston_ctl_pb.RegisterBrystonRemoteControlServiceServer(s, bcs)
+       })
+       if err != nil {
+               panic(err)
+       }
+       defer s.Stop()
+
+       <-ctx.Done()
+}
+
+func init() {
+       mtls.SetDefaultIdentity("bryston_ctl")
+}
diff --git a/proto/service/bryston_ctl/GPBMetadata/BrystonCtl.php b/proto/service/bryston_ctl/GPBMetadata/BrystonCtl.php
new file mode 100644 (file)
index 0000000..f176951
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: bryston_ctl.proto
+
+namespace GPBMetadata;
+
+class BrystonCtl
+{
+    public static $is_initialized = false;
+
+    public static function initOnce() {
+        $pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
+
+        if (static::$is_initialized == true) {
+          return;
+        }
+        $pool->internalAddGeneratedFile(
+            "\x0A\xEE\x09\x0A\x11bryston_ctl.proto\x12!fuhry.runtime.service.bryston_ctl\"\xA3\x01\x0A\x0ECommandRequest\x12>\x0A\x05model\x18\x01 \x01(\x0E2/.fuhry.runtime.service.bryston_ctl.BrystonModel\x12F\x0A\x0Cb100_command\x18\x02 \x01(\x0E2..fuhry.runtime.service.bryston_ctl.B100CommandH\x00B\x09\x0A\x07command\"f\x0A\x15AbsoluteVolumeRequest\x12>\x0A\x05model\x18\x01 \x01(\x0E2/.fuhry.runtime.service.bryston_ctl.BrystonModel\x12\x0D\x0A\x05level\x18\x02 \x01(\x0D\"3\x0A\x0FCommandResponse\x12\x0F\x0A\x07success\x18\x01 \x01(\x08\x12\x0F\x0A\x07message\x18\x02 \x01(\x09*\xD9\x03\x0A\x0BB100Command\x12\x0D\x0A\x09POWER_OFF\x10\x00\x12\x10\x0A\x0CSRC_ANALOG_1\x10\x01\x12\x10\x0A\x0CSRC_ANALOG_2\x10\x02\x12\x10\x0A\x0CSRC_ANALOG_3\x10\x03\x12\x10\x0A\x0CSRC_ANALOG_4\x10\x04\x12\x10\x0A\x0CSRC_ANALOG_5\x10\x05\x12\x10\x0A\x0CSRC_ANALOG_6\x10\x06\x12\x0D\x0A\x09VOLUME_UP\x10\x07\x12\x0F\x0A\x0BVOLUME_DOWN\x10\x08\x12\x10\x0A\x0CPOWER_TOGGLE\x10\x0F\x12\x10\x0A\x0CBALANCE_LEFT\x10\x13\x12\x11\x0A\x0DBALANCE_RIGHT\x10\x14\x12\x0C\x0A\x08POWER_ON\x10\x1D\x12\x0B\x0A\x07MUTE_ON\x10:\x12\x0C\x0A\x08MUTE_OFF\x10;\x12\x11\x0A\x0DRECORD_TOGGLE\x10=\x12\x0D\x0A\x09RECORD_ON\x10>\x12\x0E\x0A\x0ARECORD_OFF\x10?\x12\x11\x0A\x0DSRC_DIGITAL_1\x10Q\x12\x11\x0A\x0DSRC_DIGITAL_2\x10R\x12\x11\x0A\x0DSRC_DIGITAL_3\x10S\x12\x11\x0A\x0DSRC_DIGITAL_4\x10T\x12\x14\x0A\x0FPASSTHROUGH_SET\x10\xF5\x01\x12\x12\x0A\x0DTRIGGER_2_SET\x10\xF7\x01\x12\x14\x0A\x0FTRIGGER_2_CLEAR\x10\xF8\x01\x12\x11\x0A\x0CMASTER_RESET\x10\xFF\x01*%\x0A\x0CBrystonModel\x12\x0B\x0A\x07INVALID\x10\x00\x12\x08\x0A\x04B100\x10\x012\x94\x02\x0A\x1BBrystonRemoteControlService\x12r\x0A\x07Command\x121.fuhry.runtime.service.bryston_ctl.CommandRequest\x1A2.fuhry.runtime.service.bryston_ctl.CommandResponse\"\x00\x12\x80\x01\x0A\x0EAbsoluteVolume\x128.fuhry.runtime.service.bryston_ctl.AbsoluteVolumeRequest\x1A2.fuhry.runtime.service.bryston_ctl.CommandResponse\"\x00BQZ.go.fuhry.dev/runtime/proto/service/bryston_ctl\xCA\x02\x1Efuhry\\Proto\\Service\\BrystonCtlb\x06proto3"
+        , true);
+
+        static::$is_initialized = true;
+    }
+}
+
diff --git a/proto/service/bryston_ctl/Makefile b/proto/service/bryston_ctl/Makefile
new file mode 100644 (file)
index 0000000..55b35ee
--- /dev/null
@@ -0,0 +1,12 @@
+PROTO_SRCS := $(wildcard *.proto)
+PROTO_GO_OUTPUT := $(PROTO_SRCS:.proto=.pb.go)
+
+$(PROTO_GO_OUTPUT): $(PROTO_SRCS)
+       protoc --go_out=. --go_opt=paths=source_relative \
+               --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+               --php_out=. \
+               $(PROTO_SRCS)
+
+pb_go: $(PROTO_GO_OUTPUT)
+
+all: pb_go
\ No newline at end of file
diff --git a/proto/service/bryston_ctl/bryston_ctl.pb.go b/proto/service/bryston_ctl/bryston_ctl.pb.go
new file mode 100644 (file)
index 0000000..53e2f65
--- /dev/null
@@ -0,0 +1,516 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+//     protoc-gen-go v1.36.5
+//     protoc        v5.29.2
+// source: bryston_ctl.proto
+
+package bryston_ctl
+
+import (
+       protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+       protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+       reflect "reflect"
+       sync "sync"
+       unsafe "unsafe"
+)
+
+const (
+       // Verify that this generated code is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+       // Verify that runtime/protoimpl is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type B100Command int32
+
+const (
+       B100Command_POWER_OFF       B100Command = 0
+       B100Command_SRC_ANALOG_1    B100Command = 1
+       B100Command_SRC_ANALOG_2    B100Command = 2
+       B100Command_SRC_ANALOG_3    B100Command = 3
+       B100Command_SRC_ANALOG_4    B100Command = 4
+       B100Command_SRC_ANALOG_5    B100Command = 5
+       B100Command_SRC_ANALOG_6    B100Command = 6
+       B100Command_VOLUME_UP       B100Command = 7
+       B100Command_VOLUME_DOWN     B100Command = 8
+       B100Command_POWER_TOGGLE    B100Command = 15
+       B100Command_BALANCE_LEFT    B100Command = 19
+       B100Command_BALANCE_RIGHT   B100Command = 20
+       B100Command_POWER_ON        B100Command = 29
+       B100Command_MUTE_ON         B100Command = 58
+       B100Command_MUTE_OFF        B100Command = 59
+       B100Command_RECORD_TOGGLE   B100Command = 61
+       B100Command_RECORD_ON       B100Command = 62
+       B100Command_RECORD_OFF      B100Command = 63
+       B100Command_SRC_DIGITAL_1   B100Command = 81
+       B100Command_SRC_DIGITAL_2   B100Command = 82
+       B100Command_SRC_DIGITAL_3   B100Command = 83
+       B100Command_SRC_DIGITAL_4   B100Command = 84
+       B100Command_PASSTHROUGH_SET B100Command = 245
+       B100Command_TRIGGER_2_SET   B100Command = 247
+       B100Command_TRIGGER_2_CLEAR B100Command = 248
+       B100Command_MASTER_RESET    B100Command = 255
+)
+
+// Enum value maps for B100Command.
+var (
+       B100Command_name = map[int32]string{
+               0:   "POWER_OFF",
+               1:   "SRC_ANALOG_1",
+               2:   "SRC_ANALOG_2",
+               3:   "SRC_ANALOG_3",
+               4:   "SRC_ANALOG_4",
+               5:   "SRC_ANALOG_5",
+               6:   "SRC_ANALOG_6",
+               7:   "VOLUME_UP",
+               8:   "VOLUME_DOWN",
+               15:  "POWER_TOGGLE",
+               19:  "BALANCE_LEFT",
+               20:  "BALANCE_RIGHT",
+               29:  "POWER_ON",
+               58:  "MUTE_ON",
+               59:  "MUTE_OFF",
+               61:  "RECORD_TOGGLE",
+               62:  "RECORD_ON",
+               63:  "RECORD_OFF",
+               81:  "SRC_DIGITAL_1",
+               82:  "SRC_DIGITAL_2",
+               83:  "SRC_DIGITAL_3",
+               84:  "SRC_DIGITAL_4",
+               245: "PASSTHROUGH_SET",
+               247: "TRIGGER_2_SET",
+               248: "TRIGGER_2_CLEAR",
+               255: "MASTER_RESET",
+       }
+       B100Command_value = map[string]int32{
+               "POWER_OFF":       0,
+               "SRC_ANALOG_1":    1,
+               "SRC_ANALOG_2":    2,
+               "SRC_ANALOG_3":    3,
+               "SRC_ANALOG_4":    4,
+               "SRC_ANALOG_5":    5,
+               "SRC_ANALOG_6":    6,
+               "VOLUME_UP":       7,
+               "VOLUME_DOWN":     8,
+               "POWER_TOGGLE":    15,
+               "BALANCE_LEFT":    19,
+               "BALANCE_RIGHT":   20,
+               "POWER_ON":        29,
+               "MUTE_ON":         58,
+               "MUTE_OFF":        59,
+               "RECORD_TOGGLE":   61,
+               "RECORD_ON":       62,
+               "RECORD_OFF":      63,
+               "SRC_DIGITAL_1":   81,
+               "SRC_DIGITAL_2":   82,
+               "SRC_DIGITAL_3":   83,
+               "SRC_DIGITAL_4":   84,
+               "PASSTHROUGH_SET": 245,
+               "TRIGGER_2_SET":   247,
+               "TRIGGER_2_CLEAR": 248,
+               "MASTER_RESET":    255,
+       }
+)
+
+func (x B100Command) Enum() *B100Command {
+       p := new(B100Command)
+       *p = x
+       return p
+}
+
+func (x B100Command) String() string {
+       return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (B100Command) Descriptor() protoreflect.EnumDescriptor {
+       return file_bryston_ctl_proto_enumTypes[0].Descriptor()
+}
+
+func (B100Command) Type() protoreflect.EnumType {
+       return &file_bryston_ctl_proto_enumTypes[0]
+}
+
+func (x B100Command) Number() protoreflect.EnumNumber {
+       return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use B100Command.Descriptor instead.
+func (B100Command) EnumDescriptor() ([]byte, []int) {
+       return file_bryston_ctl_proto_rawDescGZIP(), []int{0}
+}
+
+type BrystonModel int32
+
+const (
+       BrystonModel_INVALID BrystonModel = 0
+       BrystonModel_B100    BrystonModel = 1
+)
+
+// Enum value maps for BrystonModel.
+var (
+       BrystonModel_name = map[int32]string{
+               0: "INVALID",
+               1: "B100",
+       }
+       BrystonModel_value = map[string]int32{
+               "INVALID": 0,
+               "B100":    1,
+       }
+)
+
+func (x BrystonModel) Enum() *BrystonModel {
+       p := new(BrystonModel)
+       *p = x
+       return p
+}
+
+func (x BrystonModel) String() string {
+       return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (BrystonModel) Descriptor() protoreflect.EnumDescriptor {
+       return file_bryston_ctl_proto_enumTypes[1].Descriptor()
+}
+
+func (BrystonModel) Type() protoreflect.EnumType {
+       return &file_bryston_ctl_proto_enumTypes[1]
+}
+
+func (x BrystonModel) Number() protoreflect.EnumNumber {
+       return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use BrystonModel.Descriptor instead.
+func (BrystonModel) EnumDescriptor() ([]byte, []int) {
+       return file_bryston_ctl_proto_rawDescGZIP(), []int{1}
+}
+
+type CommandRequest struct {
+       state protoimpl.MessageState `protogen:"open.v1"`
+       Model BrystonModel           `protobuf:"varint,1,opt,name=model,proto3,enum=fuhry.runtime.service.bryston_ctl.BrystonModel" json:"model,omitempty"`
+       // Types that are valid to be assigned to Command:
+       //
+       //      *CommandRequest_B100Command
+       Command       isCommandRequest_Command `protobuf_oneof:"command"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *CommandRequest) Reset() {
+       *x = CommandRequest{}
+       mi := &file_bryston_ctl_proto_msgTypes[0]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *CommandRequest) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CommandRequest) ProtoMessage() {}
+
+func (x *CommandRequest) ProtoReflect() protoreflect.Message {
+       mi := &file_bryston_ctl_proto_msgTypes[0]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use CommandRequest.ProtoReflect.Descriptor instead.
+func (*CommandRequest) Descriptor() ([]byte, []int) {
+       return file_bryston_ctl_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CommandRequest) GetModel() BrystonModel {
+       if x != nil {
+               return x.Model
+       }
+       return BrystonModel_INVALID
+}
+
+func (x *CommandRequest) GetCommand() isCommandRequest_Command {
+       if x != nil {
+               return x.Command
+       }
+       return nil
+}
+
+func (x *CommandRequest) GetB100Command() B100Command {
+       if x != nil {
+               if x, ok := x.Command.(*CommandRequest_B100Command); ok {
+                       return x.B100Command
+               }
+       }
+       return B100Command_POWER_OFF
+}
+
+type isCommandRequest_Command interface {
+       isCommandRequest_Command()
+}
+
+type CommandRequest_B100Command struct {
+       B100Command B100Command `protobuf:"varint,2,opt,name=b100_command,json=b100Command,proto3,enum=fuhry.runtime.service.bryston_ctl.B100Command,oneof"`
+}
+
+func (*CommandRequest_B100Command) isCommandRequest_Command() {}
+
+type AbsoluteVolumeRequest struct {
+       state         protoimpl.MessageState `protogen:"open.v1"`
+       Model         BrystonModel           `protobuf:"varint,1,opt,name=model,proto3,enum=fuhry.runtime.service.bryston_ctl.BrystonModel" json:"model,omitempty"`
+       Level         uint32                 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *AbsoluteVolumeRequest) Reset() {
+       *x = AbsoluteVolumeRequest{}
+       mi := &file_bryston_ctl_proto_msgTypes[1]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *AbsoluteVolumeRequest) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AbsoluteVolumeRequest) ProtoMessage() {}
+
+func (x *AbsoluteVolumeRequest) ProtoReflect() protoreflect.Message {
+       mi := &file_bryston_ctl_proto_msgTypes[1]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use AbsoluteVolumeRequest.ProtoReflect.Descriptor instead.
+func (*AbsoluteVolumeRequest) Descriptor() ([]byte, []int) {
+       return file_bryston_ctl_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AbsoluteVolumeRequest) GetModel() BrystonModel {
+       if x != nil {
+               return x.Model
+       }
+       return BrystonModel_INVALID
+}
+
+func (x *AbsoluteVolumeRequest) GetLevel() uint32 {
+       if x != nil {
+               return x.Level
+       }
+       return 0
+}
+
+type CommandResponse struct {
+       state         protoimpl.MessageState `protogen:"open.v1"`
+       Success       bool                   `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+       Message       string                 `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *CommandResponse) Reset() {
+       *x = CommandResponse{}
+       mi := &file_bryston_ctl_proto_msgTypes[2]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *CommandResponse) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CommandResponse) ProtoMessage() {}
+
+func (x *CommandResponse) ProtoReflect() protoreflect.Message {
+       mi := &file_bryston_ctl_proto_msgTypes[2]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use CommandResponse.ProtoReflect.Descriptor instead.
+func (*CommandResponse) Descriptor() ([]byte, []int) {
+       return file_bryston_ctl_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *CommandResponse) GetSuccess() bool {
+       if x != nil {
+               return x.Success
+       }
+       return false
+}
+
+func (x *CommandResponse) GetMessage() string {
+       if x != nil {
+               return x.Message
+       }
+       return ""
+}
+
+var File_bryston_ctl_proto protoreflect.FileDescriptor
+
+var file_bryston_ctl_proto_rawDesc = string([]byte{
+       0x0a, 0x11, 0x62, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e, 0x70, 0x72,
+       0x6f, 0x74, 0x6f, 0x12, 0x21, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69,
+       0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x62, 0x72, 0x79, 0x73, 0x74,
+       0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x22, 0xb7, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
+       0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x45, 0x0a, 0x05, 0x6d, 0x6f, 0x64,
+       0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79,
+       0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+       0x2e, 0x62, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e, 0x42, 0x72, 0x79,
+       0x73, 0x74, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c,
+       0x12, 0x53, 0x0a, 0x0c, 0x62, 0x31, 0x30, 0x30, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
+       0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72,
+       0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x62,
+       0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e, 0x42, 0x31, 0x30, 0x30, 0x43,
+       0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0b, 0x62, 0x31, 0x30, 0x30, 0x43, 0x6f,
+       0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
+       0x22, 0x74, 0x0a, 0x15, 0x41, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75,
+       0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x45, 0x0a, 0x05, 0x6d, 0x6f, 0x64,
+       0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79,
+       0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+       0x2e, 0x62, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e, 0x42, 0x72, 0x79,
+       0x73, 0x74, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c,
+       0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
+       0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x45, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
+       0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63,
+       0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63,
+       0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02,
+       0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0xd9, 0x03,
+       0x0a, 0x0b, 0x42, 0x31, 0x30, 0x30, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0d, 0x0a,
+       0x09, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x4f, 0x46, 0x46, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c,
+       0x53, 0x52, 0x43, 0x5f, 0x41, 0x4e, 0x41, 0x4c, 0x4f, 0x47, 0x5f, 0x31, 0x10, 0x01, 0x12, 0x10,
+       0x0a, 0x0c, 0x53, 0x52, 0x43, 0x5f, 0x41, 0x4e, 0x41, 0x4c, 0x4f, 0x47, 0x5f, 0x32, 0x10, 0x02,
+       0x12, 0x10, 0x0a, 0x0c, 0x53, 0x52, 0x43, 0x5f, 0x41, 0x4e, 0x41, 0x4c, 0x4f, 0x47, 0x5f, 0x33,
+       0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x52, 0x43, 0x5f, 0x41, 0x4e, 0x41, 0x4c, 0x4f, 0x47,
+       0x5f, 0x34, 0x10, 0x04, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x52, 0x43, 0x5f, 0x41, 0x4e, 0x41, 0x4c,
+       0x4f, 0x47, 0x5f, 0x35, 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x52, 0x43, 0x5f, 0x41, 0x4e,
+       0x41, 0x4c, 0x4f, 0x47, 0x5f, 0x36, 0x10, 0x06, 0x12, 0x0d, 0x0a, 0x09, 0x56, 0x4f, 0x4c, 0x55,
+       0x4d, 0x45, 0x5f, 0x55, 0x50, 0x10, 0x07, 0x12, 0x0f, 0x0a, 0x0b, 0x56, 0x4f, 0x4c, 0x55, 0x4d,
+       0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x08, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x4f, 0x57, 0x45,
+       0x52, 0x5f, 0x54, 0x4f, 0x47, 0x47, 0x4c, 0x45, 0x10, 0x0f, 0x12, 0x10, 0x0a, 0x0c, 0x42, 0x41,
+       0x4c, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x13, 0x12, 0x11, 0x0a, 0x0d,
+       0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x14, 0x12,
+       0x0c, 0x0a, 0x08, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x4f, 0x4e, 0x10, 0x1d, 0x12, 0x0b, 0x0a,
+       0x07, 0x4d, 0x55, 0x54, 0x45, 0x5f, 0x4f, 0x4e, 0x10, 0x3a, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x55,
+       0x54, 0x45, 0x5f, 0x4f, 0x46, 0x46, 0x10, 0x3b, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x45, 0x43, 0x4f,
+       0x52, 0x44, 0x5f, 0x54, 0x4f, 0x47, 0x47, 0x4c, 0x45, 0x10, 0x3d, 0x12, 0x0d, 0x0a, 0x09, 0x52,
+       0x45, 0x43, 0x4f, 0x52, 0x44, 0x5f, 0x4f, 0x4e, 0x10, 0x3e, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x45,
+       0x43, 0x4f, 0x52, 0x44, 0x5f, 0x4f, 0x46, 0x46, 0x10, 0x3f, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x52,
+       0x43, 0x5f, 0x44, 0x49, 0x47, 0x49, 0x54, 0x41, 0x4c, 0x5f, 0x31, 0x10, 0x51, 0x12, 0x11, 0x0a,
+       0x0d, 0x53, 0x52, 0x43, 0x5f, 0x44, 0x49, 0x47, 0x49, 0x54, 0x41, 0x4c, 0x5f, 0x32, 0x10, 0x52,
+       0x12, 0x11, 0x0a, 0x0d, 0x53, 0x52, 0x43, 0x5f, 0x44, 0x49, 0x47, 0x49, 0x54, 0x41, 0x4c, 0x5f,
+       0x33, 0x10, 0x53, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x52, 0x43, 0x5f, 0x44, 0x49, 0x47, 0x49, 0x54,
+       0x41, 0x4c, 0x5f, 0x34, 0x10, 0x54, 0x12, 0x14, 0x0a, 0x0f, 0x50, 0x41, 0x53, 0x53, 0x54, 0x48,
+       0x52, 0x4f, 0x55, 0x47, 0x48, 0x5f, 0x53, 0x45, 0x54, 0x10, 0xf5, 0x01, 0x12, 0x12, 0x0a, 0x0d,
+       0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x5f, 0x32, 0x5f, 0x53, 0x45, 0x54, 0x10, 0xf7, 0x01,
+       0x12, 0x14, 0x0a, 0x0f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x5f, 0x32, 0x5f, 0x43, 0x4c,
+       0x45, 0x41, 0x52, 0x10, 0xf8, 0x01, 0x12, 0x11, 0x0a, 0x0c, 0x4d, 0x41, 0x53, 0x54, 0x45, 0x52,
+       0x5f, 0x52, 0x45, 0x53, 0x45, 0x54, 0x10, 0xff, 0x01, 0x2a, 0x25, 0x0a, 0x0c, 0x42, 0x72, 0x79,
+       0x73, 0x74, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56,
+       0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x42, 0x31, 0x30, 0x30, 0x10, 0x01,
+       0x32, 0x94, 0x02, 0x0a, 0x1b, 0x42, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x52, 0x65, 0x6d, 0x6f,
+       0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+       0x12, 0x72, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x31, 0x2e, 0x66, 0x75,
+       0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76,
+       0x69, 0x63, 0x65, 0x2e, 0x62, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e,
+       0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32,
+       0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73,
+       0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x62, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63,
+       0x74, 0x6c, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+       0x73, 0x65, 0x22, 0x00, 0x12, 0x80, 0x01, 0x0a, 0x0e, 0x41, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74,
+       0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x38, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e,
+       0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e,
+       0x62, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e, 0x41, 0x62, 0x73, 0x6f,
+       0x6c, 0x75, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+       0x74, 0x1a, 0x32, 0x2e, 0x66, 0x75, 0x68, 0x72, 0x79, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d,
+       0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x62, 0x72, 0x79, 0x73, 0x74, 0x6f,
+       0x6e, 0x5f, 0x63, 0x74, 0x6c, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73,
+       0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x51, 0x5a, 0x2e, 0x67, 0x6f, 0x2e, 0x66, 0x75,
+       0x68, 0x72, 0x79, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f,
+       0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x62, 0x72,
+       0x79, 0x73, 0x74, 0x6f, 0x6e, 0x5f, 0x63, 0x74, 0x6c, 0xca, 0x02, 0x1e, 0x66, 0x75, 0x68, 0x72,
+       0x79, 0x5c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5c,
+       0x42, 0x72, 0x79, 0x73, 0x74, 0x6f, 0x6e, 0x43, 0x74, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+       0x6f, 0x33,
+})
+
+var (
+       file_bryston_ctl_proto_rawDescOnce sync.Once
+       file_bryston_ctl_proto_rawDescData []byte
+)
+
+func file_bryston_ctl_proto_rawDescGZIP() []byte {
+       file_bryston_ctl_proto_rawDescOnce.Do(func() {
+               file_bryston_ctl_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bryston_ctl_proto_rawDesc), len(file_bryston_ctl_proto_rawDesc)))
+       })
+       return file_bryston_ctl_proto_rawDescData
+}
+
+var file_bryston_ctl_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_bryston_ctl_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_bryston_ctl_proto_goTypes = []any{
+       (B100Command)(0),              // 0: fuhry.runtime.service.bryston_ctl.B100Command
+       (BrystonModel)(0),             // 1: fuhry.runtime.service.bryston_ctl.BrystonModel
+       (*CommandRequest)(nil),        // 2: fuhry.runtime.service.bryston_ctl.CommandRequest
+       (*AbsoluteVolumeRequest)(nil), // 3: fuhry.runtime.service.bryston_ctl.AbsoluteVolumeRequest
+       (*CommandResponse)(nil),       // 4: fuhry.runtime.service.bryston_ctl.CommandResponse
+}
+var file_bryston_ctl_proto_depIdxs = []int32{
+       1, // 0: fuhry.runtime.service.bryston_ctl.CommandRequest.model:type_name -> fuhry.runtime.service.bryston_ctl.BrystonModel
+       0, // 1: fuhry.runtime.service.bryston_ctl.CommandRequest.b100_command:type_name -> fuhry.runtime.service.bryston_ctl.B100Command
+       1, // 2: fuhry.runtime.service.bryston_ctl.AbsoluteVolumeRequest.model:type_name -> fuhry.runtime.service.bryston_ctl.BrystonModel
+       2, // 3: fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService.Command:input_type -> fuhry.runtime.service.bryston_ctl.CommandRequest
+       3, // 4: fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService.AbsoluteVolume:input_type -> fuhry.runtime.service.bryston_ctl.AbsoluteVolumeRequest
+       4, // 5: fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService.Command:output_type -> fuhry.runtime.service.bryston_ctl.CommandResponse
+       4, // 6: fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService.AbsoluteVolume:output_type -> fuhry.runtime.service.bryston_ctl.CommandResponse
+       5, // [5:7] is the sub-list for method output_type
+       3, // [3:5] is the sub-list for method input_type
+       3, // [3:3] is the sub-list for extension type_name
+       3, // [3:3] is the sub-list for extension extendee
+       0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_bryston_ctl_proto_init() }
+func file_bryston_ctl_proto_init() {
+       if File_bryston_ctl_proto != nil {
+               return
+       }
+       file_bryston_ctl_proto_msgTypes[0].OneofWrappers = []any{
+               (*CommandRequest_B100Command)(nil),
+       }
+       type x struct{}
+       out := protoimpl.TypeBuilder{
+               File: protoimpl.DescBuilder{
+                       GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+                       RawDescriptor: unsafe.Slice(unsafe.StringData(file_bryston_ctl_proto_rawDesc), len(file_bryston_ctl_proto_rawDesc)),
+                       NumEnums:      2,
+                       NumMessages:   3,
+                       NumExtensions: 0,
+                       NumServices:   1,
+               },
+               GoTypes:           file_bryston_ctl_proto_goTypes,
+               DependencyIndexes: file_bryston_ctl_proto_depIdxs,
+               EnumInfos:         file_bryston_ctl_proto_enumTypes,
+               MessageInfos:      file_bryston_ctl_proto_msgTypes,
+       }.Build()
+       File_bryston_ctl_proto = out.File
+       file_bryston_ctl_proto_goTypes = nil
+       file_bryston_ctl_proto_depIdxs = nil
+}
diff --git a/proto/service/bryston_ctl/bryston_ctl.proto b/proto/service/bryston_ctl/bryston_ctl.proto
new file mode 100644 (file)
index 0000000..bbec022
--- /dev/null
@@ -0,0 +1,62 @@
+syntax = "proto3";
+
+option go_package = "go.fuhry.dev/runtime/proto/service/bryston_ctl";
+option php_namespace = "fuhry\\Proto\\Service\\BrystonCtl";
+
+package fuhry.runtime.service.bryston_ctl;
+
+enum B100Command {
+    POWER_OFF = 0;
+    SRC_ANALOG_1 = 1;
+    SRC_ANALOG_2 = 2;
+    SRC_ANALOG_3 = 3;
+    SRC_ANALOG_4 = 4;
+    SRC_ANALOG_5 = 5;
+    SRC_ANALOG_6 = 6;
+    VOLUME_UP = 7;
+    VOLUME_DOWN = 8;
+    POWER_TOGGLE = 15;
+    BALANCE_LEFT = 19;
+    BALANCE_RIGHT = 20;
+    POWER_ON = 29;
+    MUTE_ON = 58;
+    MUTE_OFF = 59;
+    RECORD_TOGGLE = 61;
+    RECORD_ON = 62;
+    RECORD_OFF = 63;
+    SRC_DIGITAL_1 = 81;
+    SRC_DIGITAL_2 = 82;
+    SRC_DIGITAL_3 = 83;
+    SRC_DIGITAL_4 = 84;
+    PASSTHROUGH_SET = 245;
+    TRIGGER_2_SET = 247;
+    TRIGGER_2_CLEAR = 248;
+    MASTER_RESET = 255;
+}
+
+enum BrystonModel {
+    INVALID = 0;
+    B100 = 1;
+}
+
+message CommandRequest {
+    BrystonModel model = 1;
+    oneof command {
+        B100Command b100_command = 2;
+    }
+}
+
+message AbsoluteVolumeRequest {
+    BrystonModel model = 1;
+    uint32 level = 2;
+}
+
+message CommandResponse {
+    bool success = 1;
+    string message = 2;
+}
+
+service BrystonRemoteControlService {
+    rpc Command(CommandRequest) returns (CommandResponse) {};
+    rpc AbsoluteVolume(AbsoluteVolumeRequest) returns (CommandResponse) {};
+}
diff --git a/proto/service/bryston_ctl/bryston_ctl_grpc.pb.go b/proto/service/bryston_ctl/bryston_ctl_grpc.pb.go
new file mode 100644 (file)
index 0000000..feafffe
--- /dev/null
@@ -0,0 +1,147 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc             v5.29.2
+// source: bryston_ctl.proto
+
+package bryston_ctl
+
+import (
+       context "context"
+       grpc "google.golang.org/grpc"
+       codes "google.golang.org/grpc/codes"
+       status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+       BrystonRemoteControlService_Command_FullMethodName        = "/fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService/Command"
+       BrystonRemoteControlService_AbsoluteVolume_FullMethodName = "/fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService/AbsoluteVolume"
+)
+
+// BrystonRemoteControlServiceClient is the client API for BrystonRemoteControlService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type BrystonRemoteControlServiceClient interface {
+       Command(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (*CommandResponse, error)
+       AbsoluteVolume(ctx context.Context, in *AbsoluteVolumeRequest, opts ...grpc.CallOption) (*CommandResponse, error)
+}
+
+type brystonRemoteControlServiceClient struct {
+       cc grpc.ClientConnInterface
+}
+
+func NewBrystonRemoteControlServiceClient(cc grpc.ClientConnInterface) BrystonRemoteControlServiceClient {
+       return &brystonRemoteControlServiceClient{cc}
+}
+
+func (c *brystonRemoteControlServiceClient) Command(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (*CommandResponse, error) {
+       out := new(CommandResponse)
+       err := c.cc.Invoke(ctx, BrystonRemoteControlService_Command_FullMethodName, in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+func (c *brystonRemoteControlServiceClient) AbsoluteVolume(ctx context.Context, in *AbsoluteVolumeRequest, opts ...grpc.CallOption) (*CommandResponse, error) {
+       out := new(CommandResponse)
+       err := c.cc.Invoke(ctx, BrystonRemoteControlService_AbsoluteVolume_FullMethodName, in, out, opts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+// BrystonRemoteControlServiceServer is the server API for BrystonRemoteControlService service.
+// All implementations must embed UnimplementedBrystonRemoteControlServiceServer
+// for forward compatibility
+type BrystonRemoteControlServiceServer interface {
+       Command(context.Context, *CommandRequest) (*CommandResponse, error)
+       AbsoluteVolume(context.Context, *AbsoluteVolumeRequest) (*CommandResponse, error)
+       mustEmbedUnimplementedBrystonRemoteControlServiceServer()
+}
+
+// UnimplementedBrystonRemoteControlServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedBrystonRemoteControlServiceServer struct {
+}
+
+func (UnimplementedBrystonRemoteControlServiceServer) Command(context.Context, *CommandRequest) (*CommandResponse, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method Command not implemented")
+}
+func (UnimplementedBrystonRemoteControlServiceServer) AbsoluteVolume(context.Context, *AbsoluteVolumeRequest) (*CommandResponse, error) {
+       return nil, status.Errorf(codes.Unimplemented, "method AbsoluteVolume not implemented")
+}
+func (UnimplementedBrystonRemoteControlServiceServer) mustEmbedUnimplementedBrystonRemoteControlServiceServer() {
+}
+
+// UnsafeBrystonRemoteControlServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to BrystonRemoteControlServiceServer will
+// result in compilation errors.
+type UnsafeBrystonRemoteControlServiceServer interface {
+       mustEmbedUnimplementedBrystonRemoteControlServiceServer()
+}
+
+func RegisterBrystonRemoteControlServiceServer(s grpc.ServiceRegistrar, srv BrystonRemoteControlServiceServer) {
+       s.RegisterService(&BrystonRemoteControlService_ServiceDesc, srv)
+}
+
+func _BrystonRemoteControlService_Command_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+       in := new(CommandRequest)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(BrystonRemoteControlServiceServer).Command(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: BrystonRemoteControlService_Command_FullMethodName,
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+               return srv.(BrystonRemoteControlServiceServer).Command(ctx, req.(*CommandRequest))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+func _BrystonRemoteControlService_AbsoluteVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+       in := new(AbsoluteVolumeRequest)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(BrystonRemoteControlServiceServer).AbsoluteVolume(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: BrystonRemoteControlService_AbsoluteVolume_FullMethodName,
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+               return srv.(BrystonRemoteControlServiceServer).AbsoluteVolume(ctx, req.(*AbsoluteVolumeRequest))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+// BrystonRemoteControlService_ServiceDesc is the grpc.ServiceDesc for BrystonRemoteControlService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var BrystonRemoteControlService_ServiceDesc = grpc.ServiceDesc{
+       ServiceName: "fuhry.runtime.service.bryston_ctl.BrystonRemoteControlService",
+       HandlerType: (*BrystonRemoteControlServiceServer)(nil),
+       Methods: []grpc.MethodDesc{
+               {
+                       MethodName: "Command",
+                       Handler:    _BrystonRemoteControlService_Command_Handler,
+               },
+               {
+                       MethodName: "AbsoluteVolume",
+                       Handler:    _BrystonRemoteControlService_AbsoluteVolume_Handler,
+               },
+       },
+       Streams:  []grpc.StreamDesc{},
+       Metadata: "bryston_ctl.proto",
+}
diff --git a/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/AbsoluteVolumeRequest.php b/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/AbsoluteVolumeRequest.php
new file mode 100644 (file)
index 0000000..44c7cf0
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: bryston_ctl.proto
+
+namespace fuhry\Proto\Service\BrystonCtl;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>fuhry.runtime.service.bryston_ctl.AbsoluteVolumeRequest</code>
+ */
+class AbsoluteVolumeRequest extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.BrystonModel model = 1;</code>
+     */
+    protected $model = 0;
+    /**
+     * Generated from protobuf field <code>uint32 level = 2;</code>
+     */
+    protected $level = 0;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $model
+     *     @type int $level
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\BrystonCtl::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.BrystonModel model = 1;</code>
+     * @return int
+     */
+    public function getModel()
+    {
+        return $this->model;
+    }
+
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.BrystonModel model = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setModel($var)
+    {
+        GPBUtil::checkEnum($var, \fuhry\Proto\Service\BrystonCtl\BrystonModel::class);
+        $this->model = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>uint32 level = 2;</code>
+     * @return int
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Generated from protobuf field <code>uint32 level = 2;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setLevel($var)
+    {
+        GPBUtil::checkUint32($var);
+        $this->level = $var;
+
+        return $this;
+    }
+
+}
+
diff --git a/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/B100Command.php b/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/B100Command.php
new file mode 100644 (file)
index 0000000..729e9ad
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: bryston_ctl.proto
+
+namespace fuhry\Proto\Service\BrystonCtl;
+
+use UnexpectedValueException;
+
+/**
+ * Protobuf type <code>fuhry.runtime.service.bryston_ctl.B100Command</code>
+ */
+class B100Command
+{
+    /**
+     * Generated from protobuf enum <code>POWER_OFF = 0;</code>
+     */
+    const POWER_OFF = 0;
+    /**
+     * Generated from protobuf enum <code>SRC_ANALOG_1 = 1;</code>
+     */
+    const SRC_ANALOG_1 = 1;
+    /**
+     * Generated from protobuf enum <code>SRC_ANALOG_2 = 2;</code>
+     */
+    const SRC_ANALOG_2 = 2;
+    /**
+     * Generated from protobuf enum <code>SRC_ANALOG_3 = 3;</code>
+     */
+    const SRC_ANALOG_3 = 3;
+    /**
+     * Generated from protobuf enum <code>SRC_ANALOG_4 = 4;</code>
+     */
+    const SRC_ANALOG_4 = 4;
+    /**
+     * Generated from protobuf enum <code>SRC_ANALOG_5 = 5;</code>
+     */
+    const SRC_ANALOG_5 = 5;
+    /**
+     * Generated from protobuf enum <code>SRC_ANALOG_6 = 6;</code>
+     */
+    const SRC_ANALOG_6 = 6;
+    /**
+     * Generated from protobuf enum <code>VOLUME_UP = 7;</code>
+     */
+    const VOLUME_UP = 7;
+    /**
+     * Generated from protobuf enum <code>VOLUME_DOWN = 8;</code>
+     */
+    const VOLUME_DOWN = 8;
+    /**
+     * Generated from protobuf enum <code>POWER_TOGGLE = 15;</code>
+     */
+    const POWER_TOGGLE = 15;
+    /**
+     * Generated from protobuf enum <code>BALANCE_LEFT = 19;</code>
+     */
+    const BALANCE_LEFT = 19;
+    /**
+     * Generated from protobuf enum <code>BALANCE_RIGHT = 20;</code>
+     */
+    const BALANCE_RIGHT = 20;
+    /**
+     * Generated from protobuf enum <code>POWER_ON = 29;</code>
+     */
+    const POWER_ON = 29;
+    /**
+     * Generated from protobuf enum <code>MUTE_ON = 58;</code>
+     */
+    const MUTE_ON = 58;
+    /**
+     * Generated from protobuf enum <code>MUTE_OFF = 59;</code>
+     */
+    const MUTE_OFF = 59;
+    /**
+     * Generated from protobuf enum <code>RECORD_TOGGLE = 61;</code>
+     */
+    const RECORD_TOGGLE = 61;
+    /**
+     * Generated from protobuf enum <code>RECORD_ON = 62;</code>
+     */
+    const RECORD_ON = 62;
+    /**
+     * Generated from protobuf enum <code>RECORD_OFF = 63;</code>
+     */
+    const RECORD_OFF = 63;
+    /**
+     * Generated from protobuf enum <code>SRC_DIGITAL_1 = 81;</code>
+     */
+    const SRC_DIGITAL_1 = 81;
+    /**
+     * Generated from protobuf enum <code>SRC_DIGITAL_2 = 82;</code>
+     */
+    const SRC_DIGITAL_2 = 82;
+    /**
+     * Generated from protobuf enum <code>SRC_DIGITAL_3 = 83;</code>
+     */
+    const SRC_DIGITAL_3 = 83;
+    /**
+     * Generated from protobuf enum <code>SRC_DIGITAL_4 = 84;</code>
+     */
+    const SRC_DIGITAL_4 = 84;
+    /**
+     * Generated from protobuf enum <code>PASSTHROUGH_SET = 245;</code>
+     */
+    const PASSTHROUGH_SET = 245;
+    /**
+     * Generated from protobuf enum <code>TRIGGER_2_SET = 247;</code>
+     */
+    const TRIGGER_2_SET = 247;
+    /**
+     * Generated from protobuf enum <code>TRIGGER_2_CLEAR = 248;</code>
+     */
+    const TRIGGER_2_CLEAR = 248;
+    /**
+     * Generated from protobuf enum <code>MASTER_RESET = 255;</code>
+     */
+    const MASTER_RESET = 255;
+
+    private static $valueToName = [
+        self::POWER_OFF => 'POWER_OFF',
+        self::SRC_ANALOG_1 => 'SRC_ANALOG_1',
+        self::SRC_ANALOG_2 => 'SRC_ANALOG_2',
+        self::SRC_ANALOG_3 => 'SRC_ANALOG_3',
+        self::SRC_ANALOG_4 => 'SRC_ANALOG_4',
+        self::SRC_ANALOG_5 => 'SRC_ANALOG_5',
+        self::SRC_ANALOG_6 => 'SRC_ANALOG_6',
+        self::VOLUME_UP => 'VOLUME_UP',
+        self::VOLUME_DOWN => 'VOLUME_DOWN',
+        self::POWER_TOGGLE => 'POWER_TOGGLE',
+        self::BALANCE_LEFT => 'BALANCE_LEFT',
+        self::BALANCE_RIGHT => 'BALANCE_RIGHT',
+        self::POWER_ON => 'POWER_ON',
+        self::MUTE_ON => 'MUTE_ON',
+        self::MUTE_OFF => 'MUTE_OFF',
+        self::RECORD_TOGGLE => 'RECORD_TOGGLE',
+        self::RECORD_ON => 'RECORD_ON',
+        self::RECORD_OFF => 'RECORD_OFF',
+        self::SRC_DIGITAL_1 => 'SRC_DIGITAL_1',
+        self::SRC_DIGITAL_2 => 'SRC_DIGITAL_2',
+        self::SRC_DIGITAL_3 => 'SRC_DIGITAL_3',
+        self::SRC_DIGITAL_4 => 'SRC_DIGITAL_4',
+        self::PASSTHROUGH_SET => 'PASSTHROUGH_SET',
+        self::TRIGGER_2_SET => 'TRIGGER_2_SET',
+        self::TRIGGER_2_CLEAR => 'TRIGGER_2_CLEAR',
+        self::MASTER_RESET => 'MASTER_RESET',
+    ];
+
+    public static function name($value)
+    {
+        if (!isset(self::$valueToName[$value])) {
+            throw new UnexpectedValueException(sprintf(
+                    'Enum %s has no name defined for value %s', __CLASS__, $value));
+        }
+        return self::$valueToName[$value];
+    }
+
+
+    public static function value($name)
+    {
+        $const = __CLASS__ . '::' . strtoupper($name);
+        if (!defined($const)) {
+            throw new UnexpectedValueException(sprintf(
+                    'Enum %s has no value defined for name %s', __CLASS__, $name));
+        }
+        return constant($const);
+    }
+}
+
diff --git a/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/BrystonModel.php b/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/BrystonModel.php
new file mode 100644 (file)
index 0000000..217cfa5
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: bryston_ctl.proto
+
+namespace fuhry\Proto\Service\BrystonCtl;
+
+use UnexpectedValueException;
+
+/**
+ * Protobuf type <code>fuhry.runtime.service.bryston_ctl.BrystonModel</code>
+ */
+class BrystonModel
+{
+    /**
+     * Generated from protobuf enum <code>INVALID = 0;</code>
+     */
+    const INVALID = 0;
+    /**
+     * Generated from protobuf enum <code>B100 = 1;</code>
+     */
+    const B100 = 1;
+
+    private static $valueToName = [
+        self::INVALID => 'INVALID',
+        self::B100 => 'B100',
+    ];
+
+    public static function name($value)
+    {
+        if (!isset(self::$valueToName[$value])) {
+            throw new UnexpectedValueException(sprintf(
+                    'Enum %s has no name defined for value %s', __CLASS__, $value));
+        }
+        return self::$valueToName[$value];
+    }
+
+
+    public static function value($name)
+    {
+        $const = __CLASS__ . '::' . strtoupper($name);
+        if (!defined($const)) {
+            throw new UnexpectedValueException(sprintf(
+                    'Enum %s has no value defined for name %s', __CLASS__, $name));
+        }
+        return constant($const);
+    }
+}
+
diff --git a/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/CommandRequest.php b/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/CommandRequest.php
new file mode 100644 (file)
index 0000000..74a5411
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: bryston_ctl.proto
+
+namespace fuhry\Proto\Service\BrystonCtl;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>fuhry.runtime.service.bryston_ctl.CommandRequest</code>
+ */
+class CommandRequest extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.BrystonModel model = 1;</code>
+     */
+    protected $model = 0;
+    protected $command;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type int $model
+     *     @type int $b100_command
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\BrystonCtl::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.BrystonModel model = 1;</code>
+     * @return int
+     */
+    public function getModel()
+    {
+        return $this->model;
+    }
+
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.BrystonModel model = 1;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setModel($var)
+    {
+        GPBUtil::checkEnum($var, \fuhry\Proto\Service\BrystonCtl\BrystonModel::class);
+        $this->model = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.B100Command b100_command = 2;</code>
+     * @return int
+     */
+    public function getB100Command()
+    {
+        return $this->readOneof(2);
+    }
+
+    public function hasB100Command()
+    {
+        return $this->hasOneof(2);
+    }
+
+    /**
+     * Generated from protobuf field <code>.fuhry.runtime.service.bryston_ctl.B100Command b100_command = 2;</code>
+     * @param int $var
+     * @return $this
+     */
+    public function setB100Command($var)
+    {
+        GPBUtil::checkEnum($var, \fuhry\Proto\Service\BrystonCtl\B100Command::class);
+        $this->writeOneof(2, $var);
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getCommand()
+    {
+        return $this->whichOneof("command");
+    }
+
+}
+
diff --git a/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/CommandResponse.php b/proto/service/bryston_ctl/fuhry/Proto/Service/BrystonCtl/CommandResponse.php
new file mode 100644 (file)
index 0000000..61b0346
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: bryston_ctl.proto
+
+namespace fuhry\Proto\Service\BrystonCtl;
+
+use Google\Protobuf\Internal\GPBType;
+use Google\Protobuf\Internal\RepeatedField;
+use Google\Protobuf\Internal\GPBUtil;
+
+/**
+ * Generated from protobuf message <code>fuhry.runtime.service.bryston_ctl.CommandResponse</code>
+ */
+class CommandResponse extends \Google\Protobuf\Internal\Message
+{
+    /**
+     * Generated from protobuf field <code>bool success = 1;</code>
+     */
+    protected $success = false;
+    /**
+     * Generated from protobuf field <code>string message = 2;</code>
+     */
+    protected $message = '';
+
+    /**
+     * Constructor.
+     *
+     * @param array $data {
+     *     Optional. Data for populating the Message object.
+     *
+     *     @type bool $success
+     *     @type string $message
+     * }
+     */
+    public function __construct($data = NULL) {
+        \GPBMetadata\BrystonCtl::initOnce();
+        parent::__construct($data);
+    }
+
+    /**
+     * Generated from protobuf field <code>bool success = 1;</code>
+     * @return bool
+     */
+    public function getSuccess()
+    {
+        return $this->success;
+    }
+
+    /**
+     * Generated from protobuf field <code>bool success = 1;</code>
+     * @param bool $var
+     * @return $this
+     */
+    public function setSuccess($var)
+    {
+        GPBUtil::checkBool($var);
+        $this->success = $var;
+
+        return $this;
+    }
+
+    /**
+     * Generated from protobuf field <code>string message = 2;</code>
+     * @return string
+     */
+    public function getMessage()
+    {
+        return $this->message;
+    }
+
+    /**
+     * Generated from protobuf field <code>string message = 2;</code>
+     * @param string $var
+     * @return $this
+     */
+    public function setMessage($var)
+    {
+        GPBUtil::checkString($var, True);
+        $this->message = $var;
+
+        return $this;
+    }
+
+}
+