commit 030942c3e957ed88d8d834f8fccb70588e6cb7d8 Author: Przemko Date: Fri Nov 19 14:15:31 2021 +0100 Initial commit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b031cd4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - name: Build + run: go build -v . + + - name: Test + run: go test -v . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3550a17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/go-satel.iml +/.idea/ diff --git a/change_type.go b/change_type.go new file mode 100644 index 0000000..3016888 --- /dev/null +++ b/change_type.go @@ -0,0 +1,78 @@ +package satel + +type ChangeType byte + +const ( + ZoneViolation ChangeType = iota + ZoneTamper + ZoneAlarm + ZoneTamperAlarm + ZoneAlarmMemory + ZoneTamperAlarmMemory + ZoneBypass + ZoneNoViolationTrouble + ZoneLongViolationTrouble + ArmedPartitionSuppressed + ArmedPartition + PartitionArmedInMode2 + PartitionArmedInMode3 + PartitionWith1stCodeEntered + PartitionEntryTime + PartitionExitTimeOver10s + PartitionExitTimeUnder10s + PartitionTemporaryBlocked + PartitionBlockedForGuardRound + PartitionAlarm + PartitionFireAlarm + PartitionAlarmMemory + PartitionFireAlarmMemory + Output + DoorOpened + DoorOpenedLong + StatusBit + TroublePart1 + TroublePart2 + TroublePart3 + TroublePart4 + TroublePart5 + TroubleMemoryPart1 + TroubleMemoryPart2 + TroubleMemoryPart3 + TroubleMemoryPart4 + TroubleMemoryPart5 + PartitionWithViolatedZones + ZoneIsolate +) + +func (c ChangeType) String() string { + strings := [...]string{ + "zone-violation", + "zone-tamper", + "zone-alarm", + "zone-tamper-alarm", + "zone-alarm-memory", + "zone-tamper-alarm-memory", + "zone-bypass", + "zone-no-violation-trouble", + "zone-long-violation-trouble", + "armed-partition-suppressed", + "armed-partition", + "partition-armed-mode-2", + "partition-armed-mode-3", + "partition-with-1st-code-entered", + "partition-entry-time", + "partition-exit-time-over-10s", + "partition-exit-time-under-10s", + "partition-temporary-blocked", + "partition-blocked-guard-round", + "partition-alarm", + "partition-fire-alarm", + "partition-alarm-memory", + "partition-fire-alarm-memory", + "output"} + if int(c) < len(strings) { + return strings[c] + } else { + return "unknown" + } +} diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..9645411 --- /dev/null +++ b/frame.go @@ -0,0 +1,24 @@ +package satel + +import "math/bits" + +const seed uint16 = 0x147A + +func frame(data ...byte) []byte { + f := append([]byte{0xFE, 0xFE}, data...) + f = append(f, crc(data)...) + return append(f, 0xFE, 0x0D) +} + +func crc(data []byte) []byte { + c := seed + for _, b := range data { + c = update(c, b) + } + return []byte{byte(c >> 8), byte(c & 0xFF)} +} +func update(c uint16, b byte) uint16 { + c = bits.RotateLeft16(c, 1) + c ^= 0xFFFF + return c + c>>8 + uint16(b) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c5a6529 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go-satel + +go 1.16 + +require github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb88a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/satel.go b/satel.go new file mode 100644 index 0000000..dfb7603 --- /dev/null +++ b/satel.go @@ -0,0 +1,184 @@ +package satel + +import ( + "bufio" + "errors" + "net" + "sync" + "time" +) + +type Event struct { + Type ChangeType + Index int + Value bool +} + +type Config struct { + EventsQueueSize int + LongCommands bool +} + +type Satel struct { + conn net.Conn + mu sync.Mutex + cmdSize int + cmdChan chan int + Events chan Event +} + +func New(conn net.Conn) *Satel { + return NewConfig(conn, Config{}) +} + +func NewConfig(conn net.Conn, config Config) *Satel { + s := &Satel{ + conn: conn, + cmdChan: make(chan int), + Events: make(chan Event, config.EventsQueueSize), + } + if config.LongCommands { + s.cmdSize = 32 + } else { + s.cmdSize = 16 + } + go s.read() + err := s.sendCmd(0x7F, 0x01, 0x04, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + if err != nil { + close(s.Events) + return s + } + go func() { + for { + err = s.sendCmd(0x0A) + if err != nil { + return + } + time.Sleep(5 * time.Second) + } + }() + return s +} + +func (s *Satel) ArmPartition(code string, mode, index int) error { + data := make([]byte, 4) + data[index/8] = 1 << (index % 8) + bytes := prepareCommand(code, byte(0x80+mode), data...) + return s.sendCmd(bytes...) +} + +func (s *Satel) ForceArmPartition(code string, mode, index int) error { + data := make([]byte, 4) + data[index/8] = 1 << (index % 8) + bytes := prepareCommand(code, byte(0xA0+mode), data...) + return s.sendCmd(bytes...) +} + +func (s *Satel) DisarmPartition(code string, index int) error { + data := make([]byte, 4) + data[index/8] = 1 << (index % 8) + bytes := prepareCommand(code, byte(0x84), data...) + return s.sendCmd(bytes...) +} + +func (s *Satel) SetOutput(code string, index int, value bool) error { + cmd := byte(0x89) + if value { + cmd = 0x88 + } + data := make([]byte, s.cmdSize) + data[index/8] = 1 << (index % 8) + bytes := prepareCommand(code, cmd, data...) + return s.sendCmd(bytes...) +} + +func prepareCommand(code string, cmd byte, data ...byte) []byte { + bytes := append([]byte{cmd}, transformCode(code)...) + return append(bytes, data...) +} + +func (s *Satel) Close() error { + return s.conn.Close() +} + +type command struct { + prev [32]byte + initialized bool +} + +func (s *Satel) read() { + scanner := bufio.NewScanner(s.conn) + scanner.Split(scan) + commands := make(map[byte]command) + + for ok := scanner.Scan(); ok; ok = scanner.Scan() { + bytes := scanner.Bytes() + cmd := bytes[0] + bytes = bytes[1 : len(bytes)-2] + s.cmdRes() + if cmd == 0xEF { + continue + } + c := commands[cmd] + for i, bb := range bytes { + change := bb ^ c.prev[i] + for j := 0; j < 8; j++ { + index := byte(1 << j) + if !c.initialized || change&index != 0 { + s.Events <- Event{ + Type: ChangeType(cmd), + Index: i*8 + j, + Value: bb&index != 0, + } + } + } + c.prev[i] = bytes[i] + } + c.initialized = true + commands[cmd] = c + } + close(s.Events) + _ = s.conn.Close() +} + +func (s *Satel) cmdRes() { + select { + case s.cmdChan <- 0: + default: + } +} + +func (s *Satel) sendCmd(data ...byte) (err error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.conn == nil { + return errors.New("no connection") + } + _, err = s.conn.Write(frame(data...)) + if err == nil { + select { + case <-s.cmdChan: + case <-time.After(3 * time.Second): + } + } + return +} + +func transformCode(code string) []byte { + bytes := make([]byte, 8) + for i := 0; i < 16; i++ { + if i < len(code) { + digit := code[i] + if i%2 == 0 { + bytes[i/2] = (digit - '0') << 4 + } else { + bytes[i/2] |= digit - '0' + } + } else if i%2 == 0 { + bytes[i/2] = 0xFF + } else if i == len(code) { + bytes[i/2] |= 0x0F + } + } + return bytes +} diff --git a/satel_test.go b/satel_test.go new file mode 100644 index 0000000..df9ca0b --- /dev/null +++ b/satel_test.go @@ -0,0 +1,16 @@ +package satel + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestTransformCode(t *testing.T) { + assert := assert.New(t) + assert.Equal([]byte{0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, transformCode("0")) + assert.Equal([]byte{0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, transformCode("0000")) + assert.Equal([]byte{0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, transformCode("000")) + assert.Equal([]byte{0x12, 0x34, 0x56, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, transformCode("123456")) + assert.Equal([]byte{0x98, 0x12, 0x4F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, transformCode("98124")) + assert.Equal([]byte{0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0xFF}, transformCode("12345678901234")) +} diff --git a/scanner.go b/scanner.go new file mode 100644 index 0000000..17f1d5f --- /dev/null +++ b/scanner.go @@ -0,0 +1,21 @@ +package satel + +import "bytes" + +func scan(data []byte, _ bool) (advance int, token []byte, err error) { + i := 0 + for ; i < len(data) && data[i] == 0xFE; i++ { + } + if i > 0 { + data = data[i:] + } + startIndex := bytes.Index(data, []byte{0xFE, 0xFE}) + index := bytes.Index(data, []byte{0xFE, 0x0D}) + if startIndex > 0 && (index < 0 || startIndex < index) { + return i + startIndex + 2, nil, nil + } + if index > 0 { + return i + index + 2, data[:index], nil + } + return 0, nil, nil +}