diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/config/config.yml b/config/config.yml new file mode 100644 index 0000000..6d1ef87 --- /dev/null +++ b/config/config.yml @@ -0,0 +1,7 @@ +server: + port: 10230 +remote: + ws_loc: '' +led: + pin: 18 + count: 2 diff --git a/go.mod b/go.mod index 0eda161..c79dcce 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,17 @@ module rpi-ci-led go 1.14 -require github.com/jgarff/rpi_ws281x v0.0.0-20200319211106-6a720cbd42d3 +require ( + git.trj.tw/golang/argparse v1.0.1 + git.trj.tw/golang/utils v0.0.0-20190225142552-b019626f0349 + github.com/gorilla/websocket v1.4.2 + github.com/jesseduffield/yaml v2.1.0+incompatible + github.com/jgarff/rpi_ws281x v0.0.0-20200319211106-6a720cbd42d3 + github.com/joho/godotenv v1.3.0 + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/otakukaze/envconfig v1.0.4 + go.etcd.io/bbolt v1.3.4 + golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum index 81571e2..e5f6986 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,30 @@ +git.trj.tw/golang/argparse v1.0.1 h1:fLXwn8EUEXBwJEYvFgz0JTyy1q/UhucN6pIDIND87lI= +git.trj.tw/golang/argparse v1.0.1/go.mod h1:Ao2nzs4Fv+W/KgI8pDBgrcAC24IYE7DAzzUEOkTmqT4= +git.trj.tw/golang/utils v0.0.0-20190225142552-b019626f0349 h1:V6ifeiJ3ExnjaUylTOz37n6z5uLwm6fjKjnztbTCaQI= +git.trj.tw/golang/utils v0.0.0-20190225142552-b019626f0349/go.mod h1:yE+qbsUsijCTdwsaQRkPT1CXYk7ftMzXsCaaYx/0QI0= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE= +github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk= github.com/jgarff/rpi_ws281x v0.0.0-20200319211106-6a720cbd42d3 h1:Gmtr3J666u+wiChtQ49FYzcdd6UgX0aWoLciRPo3His= github.com/jgarff/rpi_ws281x v0.0.0-20200319211106-6a720cbd42d3/go.mod h1:xbXlgWZjA66nkwNqkT4ol2EqY7jL8v+1efK5ZnOT/MU= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/otakukaze/envconfig v1.0.4 h1:/rZ8xq1vFpgWzqsqUkk61doDGNv9pIXqrog/mCvSx8Y= +github.com/otakukaze/envconfig v1.0.4/go.mod h1:v2dNv5NX1Lakw3FTAkbxYURyaiOy68M8QpMTZz+ogfs= +go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 0a592e7..eb9a362 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,17 @@ package main import ( "fmt" + "log" "os" "os/signal" + "rpi-ci-led/pkg/config" + "rpi-ci-led/pkg/database" + "rpi-ci-led/pkg/led" + "rpi-ci-led/pkg/websocket" "rpi-ci-led/pkg/ws2812b" "syscall" + "git.trj.tw/golang/argparse" "github.com/joho/godotenv" ) @@ -15,14 +21,56 @@ func main() { godotenv.Load() - ws2812b.Init(18, 20, 255) + var configFile string + var dbPath string + + // argument parser + argParser := argparse.New() + argParser.Help("h", "help") + argParser.StringVar(&configFile, "", "f", "config", "config yaml file path", nil) + argParser.StringVar(&dbPath, "", "d", "db", "database file path", nil) + if err := argParser.Parse(os.Args); err != nil { + log.Fatal(err) + } + + if err := config.Load(configFile); err != nil { + log.Fatal(err) + } + + if err := database.New(dbPath); err != nil { + log.Fatal(err) + } + + conf := config.Get() + + socket, err := websocket.NewClient(conf.Remote.WSLoc) + if err != nil { + log.Fatal(err) + } + + ledsvc, err := led.Init(conf.LED.Pin, conf.LED.Count) + if err != nil { + log.Fatal(err) + } lock := make(chan os.Signal) signal.Notify(lock, syscall.SIGINT, syscall.SIGTERM) // main + go func() { + if err := socket.Listen(); err != nil { + log.Fatal(err) + } + lock <- syscall.SIGQUIT + }() + + go func() { ledsvc.Run() }() + + // start ws client connect to server <-lock + fmt.Printf("Before process exit, close all connection\n") + socket.Close() ws2812b.Close() } diff --git a/pkg/cihook/cihook.go b/pkg/cihook/cihook.go new file mode 100644 index 0000000..557b17e --- /dev/null +++ b/pkg/cihook/cihook.go @@ -0,0 +1,11 @@ +package cihook + +import "encoding/json" + +func Parse(b []byte) (*PipelineEvent, error) { + p := &PipelineEvent{} + if err := json.Unmarshal(b, p); err != nil { + return nil, err + } + return p, nil +} diff --git a/pkg/cihook/types.go b/pkg/cihook/types.go new file mode 100644 index 0000000..63ea116 --- /dev/null +++ b/pkg/cihook/types.go @@ -0,0 +1,71 @@ +package cihook + +import "time" + +type PipelineEvent struct { + ID string `json:"id"` + EventType string `json:"eventType"` + PublisherID string `json:"publisherId"` + Message struct { + Text string `json:"text"` + HTML string `json:"html"` + Markdown string `json:"markdown"` + } `json:"message"` + DetailedMessage struct { + Text string `json:"text"` + HTML string `json:"html"` + Markdown string `json:"markdown"` + } `json:"detailedMessage"` + Resource struct { + Run struct { + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + Web struct { + Href string `json:"href"` + } `json:"web"` + PipelineWeb struct { + Href string `json:"href"` + } `json:"pipeline.web"` + Pipeline struct { + Href string `json:"href"` + } `json:"pipeline"` + } `json:"_links"` + Pipeline struct { + URL string `json:"url"` + ID int `json:"id"` + Revision int `json:"revision"` + Name string `json:"name"` + Folder string `json:"folder"` + } `json:"pipeline"` + State string `json:"state"` + Result string `json:"result"` + CreatedDate time.Time `json:"createdDate"` + FinishedDate time.Time `json:"finishedDate"` + URL string `json:"url"` + ID int `json:"id"` + Name string `json:"name"` + } `json:"run"` + Pipeline struct { + URL string `json:"url"` + ID int `json:"id"` + Revision int `json:"revision"` + Name string `json:"name"` + Folder string `json:"folder"` + } `json:"pipeline"` + } `json:"resource"` + ResourceVersion string `json:"resourceVersion"` + ResourceContainers struct { + Collection struct { + ID string `json:"id"` + } `json:"collection"` + Account struct { + ID string `json:"id"` + } `json:"account"` + Project struct { + ID string `json:"id"` + } `json:"project"` + } `json:"resourceContainers"` + CreatedDate time.Time `json:"createdDate"` +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 3fb7032..e85b6a9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,16 +1,75 @@ package config +import ( + "errors" + "io/ioutil" + "os" + "path" + + "git.trj.tw/golang/utils" + "github.com/jesseduffield/yaml" + "github.com/otakukaze/envconfig" +) + type Server struct { Port int `yaml:"port" env:"SERVER_PORT"` } +type Remote struct { + WSLoc string `yaml:"ws_loc" env:"REMOTE_WS_LOC"` +} + +type LED struct { + Count int `yaml:"count" env:"LED_COUNT"` + Pin int `yaml:"pin" env:"LED_PIN"` +} + type Config struct { Server Server `yaml:"server"` + Remote Remote `yaml:"remote"` + LED LED `yaml:"led"` } var c *Config func Load(p ...string) error { + var fp string + if len(p) > 0 && p[0] != "" { + fp = p[0] + } else { + wd, err := os.Getwd() + if err != nil { + return err + } + fp = path.Join(wd, "config.yml") + } + + fp = utils.ParsePath(fp) + + if !utils.CheckExists(fp, false) { + return errors.New("config file not found") + } + + // read config file + b, err := ioutil.ReadFile(fp) + if err != nil { + return err + } + + c = &Config{} + + if err := yaml.Unmarshal(b, c); err != nil { + return err + } + + envconfig.Parse(c) return nil } + +func Get() *Config { + if c == nil { + panic(errors.New("config not init")) + } + return c +} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..92382d9 --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,43 @@ +package database + +import ( + "errors" + "os" + "path" + + "git.trj.tw/golang/utils" + "go.etcd.io/bbolt" +) + +var db *bbolt.DB + +func New(dbPath ...string) error { + var err error + var fp string + + if len(dbPath) > 0 && dbPath[0] != "" { + fp = dbPath[0] + } else { + wd, err := os.Getwd() + if err != nil { + return err + } + fp = path.Join(wd, "store.db") + } + + fp = utils.ParsePath(fp) + + db, err = bbolt.Open(fp, 0664, nil) + if err != nil { + return err + } + + return nil +} + +func Get() *bbolt.DB { + if db == nil { + panic(errors.New("database not init")) + } + return db +} diff --git a/pkg/led/led.go b/pkg/led/led.go new file mode 100644 index 0000000..dbafbf3 --- /dev/null +++ b/pkg/led/led.go @@ -0,0 +1,97 @@ +package led + +import ( + "errors" + "rpi-ci-led/pkg/ws2812b" + "time" +) + +type LEDSvc struct { + pin int + count int + blink map[int]chan struct{} + done chan struct{} +} + +var led *LEDSvc + +func Init(pin, count int) (*LEDSvc, error) { + _, err := ws2812b.Init(pin, count, 255) + if err != nil { + return nil, err + } + return &LEDSvc{ + pin: pin, + count: count, + blink: make(map[int]chan struct{}), + done: make(chan struct{}), + }, nil +} + +func Get() *LEDSvc { + if led == nil { + panic(errors.New("not init")) + } + return led +} + +func (p *LEDSvc) SetColor(pos int, color uint32) error { + if pos < 0 || pos > p.count { + return errors.New("out of range") + } + if ch, ok := p.blink[pos]; ok { + close(ch) + delete(p.blink, pos) + } + ws2812b.WriteColor(pos, color) + return nil +} + +func (p *LEDSvc) StopBlink(pos int) { + if ch, ok := p.blink[pos]; ok { + close(ch) + delete(p.blink, pos) + } +} + +func (p *LEDSvc) SetBlink(pos int, color uint32, interval time.Duration) { + if ch, ok := p.blink[pos]; ok { + close(ch) + delete(p.blink, pos) + } + go func() { + defer func() { + ws2812b.WriteColor(pos, 0) + delete(p.blink, pos) + }() + + ch := make(chan struct{}) + p.blink[pos] = ch + timer := time.NewTicker(interval) + on := false + for { + select { + case <-ch: + return + case <-timer.C: + if on { + ws2812b.WriteColor(pos, 0) + } else { + ws2812b.WriteColor(pos, color) + } + on = !on + } + } + }() +} + +func (p *LEDSvc) Run() { + ticker := time.NewTicker(time.Millisecond * 100) + for { + select { + case <-p.done: + return + case <-ticker.C: + } + } +} diff --git a/pkg/websocket/websocket.go b/pkg/websocket/websocket.go new file mode 100644 index 0000000..9d4a8c3 --- /dev/null +++ b/pkg/websocket/websocket.go @@ -0,0 +1,91 @@ +package websocket + +import ( + "errors" + "net/http" + "net/textproto" + + "github.com/gorilla/websocket" +) + +type Socket struct { + *websocket.Conn + Listeners map[string](chan<- []byte) + done chan struct{} + errd bool + errsig chan error +} + +var client *Socket + +func NewClient(remote string) (*Socket, error) { + if remote == "" { + return nil, errors.New("remote url is empty") + } + var err error + + headers := http.Header{} + + headers.Set( + textproto.CanonicalMIMEHeaderKey("X-Auth-Key"), + "rpi", + ) + c, resp, err := websocket.DefaultDialer.Dial(remote, headers) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + client = &Socket{ + Conn: c, + Listeners: make(map[string](chan<- []byte)), + done: make(chan struct{}, 0), + errd: false, + errsig: make(chan error), + } + + return client, nil +} + +func (p *Socket) Listen() error { + go func() { + defer close(p.done) + for { + _, msg, err := p.ReadMessage() + if err != nil { + p.errsig <- err + } + + for _, v := range p.Listeners { + v <- msg + } + } + }() + + select { + case <-p.done: + return nil + case err := <-p.errsig: + return err + } +} + +func (p *Socket) AddListener(alias string, c chan<- []byte) error { + if _, ok := p.Listeners[alias]; ok { + return errors.New("listener name exists") + } + p.Listeners[alias] = c + + return nil +} + +func (p *Socket) RemoveListener(alias string) error { + if v, ok := p.Listeners[alias]; !ok { + return errors.New("listener not exists") + } else { + close(v) + delete(p.Listeners, alias) + } + + return nil +} diff --git a/pkg/ws2812b/ws2812b.go b/pkg/ws2812b/ws2812b.go index 20e7f87..89655e8 100644 --- a/pkg/ws2812b/ws2812b.go +++ b/pkg/ws2812b/ws2812b.go @@ -23,6 +23,7 @@ func Init(gpio, count, brightness int) (*LED, error) { err := ws2811.Init(gpio, count, brightness) if err != nil { + led = nil return nil, err } @@ -36,15 +37,30 @@ func Init(gpio, count, brightness int) (*LED, error) { return led, nil } +func IsInit() bool { + if led == nil { + return false + } + return led.IsInit +} + func Close() { ws2811.Clear() ws2811.Fini() + led.IsInit = false } func ClearAll() { ws2811.Clear() } +func Len() int { + if led == nil { + return 0 + } + return led.Count +} + func WriteColor(pos int, color uint32) error { if pos < 0 || pos > led.Count { return errors.New("position out of range")