refactoring, added sequential triggers execution per component

This commit is contained in:
sqshq 2019-03-09 23:41:23 -05:00
parent 22fbf3e796
commit 7db33312b6
11 changed files with 246 additions and 183 deletions

View File

@ -7,28 +7,41 @@ import (
"log" "log"
) )
func Beep() error { type AudioPlayer struct {
player *oto.Player
beep []byte
}
func NewAudioPlayer() *AudioPlayer {
bytes, err := Asset("quindar-tone.mp3") bytes, err := Asset("quindar-tone.mp3")
if err != nil { if err != nil {
log.Fatal("Can't find asset file") log.Fatal("Can't find audio file")
} }
d, err := mp3.NewDecoder(NewAssetFile(bytes)) player, err := oto.NewPlayer(44100, 2, 2, 8192)
if err != nil { if err != nil {
return err panic(err)
}
defer d.Close()
p, err := oto.NewPlayer(d.SampleRate(), 2, 2, 8192)
if err != nil {
return err
}
defer p.Close()
if _, err := io.Copy(p, d); err != nil {
return err
} }
return nil return &AudioPlayer{
player: player,
beep: bytes,
}
}
func (a *AudioPlayer) Beep() {
decoder, err := mp3.NewDecoder(NewAssetFile(a.beep))
if err != nil {
panic(err)
}
if _, err := io.Copy(a.player, decoder); err != nil {
panic(err)
}
}
func (a *AudioPlayer) Close() {
_ = a.player.Close()
} }

View File

@ -5,7 +5,6 @@ import (
"github.com/sqshq/sampler/config" "github.com/sqshq/sampler/config"
"github.com/sqshq/sampler/console" "github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data" "github.com/sqshq/sampler/data"
"github.com/sqshq/sampler/trigger"
"image" "image"
"math" "math"
"strconv" "strconv"
@ -39,7 +38,6 @@ const (
type RunChart struct { type RunChart struct {
ui.Block ui.Block
data.Consumer data.Consumer
triggers []trigger.Trigger
lines []TimeLine lines []TimeLine
grid ChartGrid grid ChartGrid
timescale time.Duration timescale time.Duration

View File

@ -7,13 +7,13 @@ runcharts:
w: 53 w: 53
h: 16 h: 16
triggers: triggers:
- title: PROCESSING STARTED # ${prev} ${cur} ${lavel} echo $(( 3 < 4 && 1 > 2 )) - title: LATENCY
condition: ((${prev} == 0 && ${cur} > 0)) condition: echo "$prev < 0.4 && $cur > 0.4" |bc -l
actions: actions:
terminal-bell: true terminal-bell: true
sound: false sound: true
visual: true visual: true
script: say ${lavel} initiated with value ${cur} script: 'say alert: ${label} latency exceeded ${cur} second'
scale: 3 scale: 3
items: items:
- label: GOOGLE - label: GOOGLE
@ -100,6 +100,12 @@ gauges:
size: size:
w: 27 w: 27
h: 2 h: 2
triggers:
- title: CLOCK BELL EVERY MINUTE
condition: '[ $label == "cur" ] && [ $cur -eq 0 ] && echo 1'
actions:
sound: true
script: say -v samantha `date +%I:%M%p`
values: values:
cur: date +%S cur: date +%S
max: echo 60 max: echo 60

View File

@ -104,9 +104,9 @@ func (c *Config) setDefaultValues() {
func setDefaultTriggersValues(triggers []TriggerConfig) { func setDefaultTriggersValues(triggers []TriggerConfig) {
defaultTerminalBell := true defaultTerminalBell := false
defaultSound := false defaultSound := false
defaultVisual := true defaultVisual := false
for i, trigger := range triggers { for i, trigger := range triggers {
@ -137,20 +137,20 @@ func (c *Config) setDefaultColors() {
palette := console.GetPalette(*c.Theme) palette := console.GetPalette(*c.Theme)
colorsCount := len(palette.Colors) colorsCount := len(palette.Colors)
for _, chart := range c.RunCharts { for _, ch := range c.RunCharts {
for j, item := range chart.Items { for j, item := range ch.Items {
if item.Color == nil { if item.Color == nil {
item.Color = &palette.Colors[j%colorsCount] item.Color = &palette.Colors[j%colorsCount]
chart.Items[j] = item ch.Items[j] = item
} }
} }
} }
for _, chart := range c.BarCharts { for _, b := range c.BarCharts {
for j, item := range chart.Items { for j, item := range b.Items {
if item.Color == nil { if item.Color == nil {
item.Color = &palette.Colors[j%colorsCount] item.Color = &palette.Colors[j%colorsCount]
chart.Items[j] = item b.Items[j] = item
} }
} }
} }

View File

@ -23,6 +23,10 @@ const (
type Console struct{} type Console struct{}
const (
BellCharacter = "\a"
)
func (self *Console) Init() { func (self *Console) Init() {
fmt.Printf("\033]0;%s\007", AppTitle) fmt.Printf("\033]0;%s\007", AppTitle)

View File

@ -11,7 +11,8 @@ type Sample struct {
} }
type Alert struct { type Alert struct {
Text string Title string
Text string
} }
func NewConsumer() Consumer { func NewConsumer() Consumer {

View File

@ -1,6 +1,7 @@
package data package data
import ( import (
"github.com/sqshq/sampler/config"
ui "github.com/sqshq/termui" ui "github.com/sqshq/termui"
"os/exec" "os/exec"
"strings" "strings"
@ -9,7 +10,19 @@ import (
type Item struct { type Item struct {
Label string Label string
Script string Script string
Color ui.Color Color *ui.Color
}
func NewItems(cfgs []config.Item) []Item {
items := make([]Item, 0)
for _, i := range cfgs {
item := Item{Label: *i.Label, Script: i.Script, Color: i.Color}
items = append(items, item)
}
return items
} }
func (i *Item) nextValue() (value string, err error) { func (i *Item) nextValue() (value string, err error) {

View File

@ -1,43 +1,63 @@
package data package data
import ( import (
"github.com/sqshq/sampler/trigger"
"time" "time"
) )
type Sampler struct { type Sampler struct {
consumer Consumer consumer Consumer
item Item items []Item
triggers []trigger.Trigger triggers []Trigger
triggersChannel chan Sample
} }
func NewSampler(consumer Consumer, item Item, triggers []trigger.Trigger, rateMs int) Sampler { func NewSampler(consumer Consumer, items []Item, triggers []Trigger, rateMs int) Sampler {
ticker := time.NewTicker(time.Duration(rateMs * int(time.Millisecond))) ticker := time.NewTicker(
sampler := Sampler{consumer, item, triggers} time.Duration(rateMs * int(time.Millisecond)),
)
sampler := Sampler{
consumer,
items,
triggers,
make(chan Sample),
}
go func() { go func() {
sampler.sample()
for ; true; <-ticker.C { for ; true; <-ticker.C {
sampler.sample() for _, item := range sampler.items {
go sampler.sample(item)
}
}
}()
go func() {
for {
select {
case sample := <-sampler.triggersChannel:
for _, t := range sampler.triggers {
t.Execute(sample)
}
}
} }
}() }()
return sampler return sampler
} }
func (s *Sampler) sample() { func (s *Sampler) sample(item Item) {
value, err := s.item.nextValue()
val, err := item.nextValue()
if err == nil { if err == nil {
sample := Sample{Label: item.Label, Value: val}
sample := Sample{Value: value, Label: s.item.Label}
s.consumer.SampleChannel <- sample s.consumer.SampleChannel <- sample
s.triggersChannel <- sample
for _, t := range s.triggers {
t.Execute(s.item.Label, value)
}
} else { } else {
s.consumer.AlertChannel <- Alert{Text: err.Error()} s.consumer.AlertChannel <- Alert{
Title: "SAMPLING FAILURE",
Text: err.Error(),
}
} }
} }

120
data/trigger.go Normal file
View File

@ -0,0 +1,120 @@
package data
import (
"fmt"
"github.com/sqshq/sampler/asset"
"github.com/sqshq/sampler/config"
"github.com/sqshq/sampler/console"
"os"
"os/exec"
"regexp"
)
const (
TrueIndicator = "1"
InitialValue = "0"
)
type Trigger struct {
title string
condition string
actions Actions
consumer Consumer
valuesByLabel map[string]Values
player *asset.AudioPlayer
digitsRegexp *regexp.Regexp
}
type Actions struct {
terminalBell bool
sound bool
visual bool
script *string
}
type Values struct {
current string
previous string
}
func NewTriggers(cfgs []config.TriggerConfig, consumer Consumer, player *asset.AudioPlayer) []Trigger {
triggers := make([]Trigger, 0)
for _, cfg := range cfgs {
triggers = append(triggers, NewTrigger(cfg, consumer, player))
}
return triggers
}
func NewTrigger(config config.TriggerConfig, consumer Consumer, player *asset.AudioPlayer) Trigger {
return Trigger{
title: config.Title,
condition: config.Condition,
consumer: consumer,
valuesByLabel: make(map[string]Values),
player: player,
digitsRegexp: regexp.MustCompile("[^0-9]+"),
actions: Actions{
terminalBell: *config.Actions.TerminalBell,
sound: *config.Actions.Sound,
visual: *config.Actions.Visual,
script: config.Actions.Script,
},
}
}
func (t *Trigger) Execute(sample Sample) {
if t.evaluate(sample) {
if t.actions.terminalBell {
fmt.Print(console.BellCharacter)
}
if t.actions.sound {
t.player.Beep()
}
if t.actions.visual {
//t.consumer.AlertChannel <- Alert{
// Title: "TRIGGER ALERT", Text: sample.Label,
//}
}
if t.actions.script != nil {
_, _ = runScript(*t.actions.script, sample.Label, t.valuesByLabel[sample.Label])
}
}
}
func (t *Trigger) evaluate(sample Sample) bool {
if values, ok := t.valuesByLabel[sample.Label]; ok {
values.previous = values.current
values.current = sample.Value
t.valuesByLabel[sample.Label] = values
} else {
t.valuesByLabel[sample.Label] = Values{previous: InitialValue, current: sample.Value}
}
output, err := runScript(t.condition, sample.Label, t.valuesByLabel[sample.Label])
if err != nil {
//t.consumer.AlertChannel <- Alert{Title: "TRIGGER CONDITION FAILURE", Text: err.Error()}
}
return t.digitsRegexp.ReplaceAllString(string(output), "") == TrueIndicator
}
func runScript(script, label string, data Values) ([]byte, error) {
cmd := exec.Command("sh", "-c", script)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("prev=%v", data.previous),
fmt.Sprintf("cur=%v", data.current),
fmt.Sprintf("label=%v", label))
return cmd.Output()
}

35
main.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"github.com/sqshq/sampler/asset"
"github.com/sqshq/sampler/component" "github.com/sqshq/sampler/component"
"github.com/sqshq/sampler/component/asciibox" "github.com/sqshq/sampler/component/asciibox"
"github.com/sqshq/sampler/component/barchart" "github.com/sqshq/sampler/component/barchart"
@ -11,7 +12,6 @@ import (
"github.com/sqshq/sampler/console" "github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data" "github.com/sqshq/sampler/data"
"github.com/sqshq/sampler/event" "github.com/sqshq/sampler/event"
"github.com/sqshq/sampler/trigger"
ui "github.com/sqshq/termui" ui "github.com/sqshq/termui"
) )
@ -25,51 +25,50 @@ func main() {
width, height := ui.TerminalDimensions() width, height := ui.TerminalDimensions()
lout := layout.NewLayout(width, height, component.NewStatusLine(flg.ConfigFileName), component.NewMenu()) lout := layout.NewLayout(width, height, component.NewStatusLine(flg.ConfigFileName), component.NewMenu())
player := asset.NewAudioPlayer()
defer player.Close()
for _, c := range cfg.RunCharts { for _, c := range cfg.RunCharts {
legend := runchart.Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details} legend := runchart.Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details}
chart := runchart.NewRunChart(c, legend) chart := runchart.NewRunChart(c, legend)
lout.AddComponent(config.TypeRunChart, chart, c.Title, c.Position, c.Size, *c.RefreshRateMs) lout.AddComponent(config.TypeRunChart, chart, c.Title, c.Position, c.Size, *c.RefreshRateMs)
triggers := trigger.NewTriggers(c.Triggers) triggers := data.NewTriggers(c.Triggers, chart.Consumer, player)
items := data.NewItems(c.Items)
data.NewSampler(chart.Consumer, items, triggers, *c.RefreshRateMs)
for _, i := range c.Items { for _, i := range c.Items {
item := data.Item{Label: *i.Label, Script: i.Script, Color: *i.Color} chart.AddLine(*i.Label, *i.Color)
chart.AddLine(item.Label, item.Color)
data.NewSampler(chart.Consumer, item, triggers, *c.RefreshRateMs)
} }
} }
for _, a := range cfg.AsciiBoxes { for _, a := range cfg.AsciiBoxes {
box := asciibox.NewAsciiBox(a) box := asciibox.NewAsciiBox(a)
item := data.Item{Label: *a.Label, Script: a.Script, Color: *a.Color} item := data.Item{Label: *a.Label, Script: a.Script, Color: a.Color}
triggers := trigger.NewTriggers(a.Triggers)
lout.AddComponent(config.TypeAsciiBox, box, a.Title, a.Position, a.Size, *a.RefreshRateMs) lout.AddComponent(config.TypeAsciiBox, box, a.Title, a.Position, a.Size, *a.RefreshRateMs)
data.NewSampler(box.Consumer, item, triggers, *a.RefreshRateMs) triggers := data.NewTriggers(a.Triggers, box.Consumer, player)
data.NewSampler(box.Consumer, []data.Item{item}, triggers, *a.RefreshRateMs)
} }
for _, b := range cfg.BarCharts { for _, b := range cfg.BarCharts {
chart := barchart.NewBarChart(b.Title, *b.Scale) chart := barchart.NewBarChart(b.Title, *b.Scale)
triggers := trigger.NewTriggers(b.Triggers) triggers := data.NewTriggers(b.Triggers, chart.Consumer, player)
lout.AddComponent(config.TypeBarChart, chart, b.Title, b.Position, b.Size, *b.RefreshRateMs) lout.AddComponent(config.TypeBarChart, chart, b.Title, b.Position, b.Size, *b.RefreshRateMs)
items := data.NewItems(b.Items)
data.NewSampler(chart.Consumer, items, triggers, *b.RefreshRateMs)
for _, i := range b.Items { for _, i := range b.Items {
item := data.Item{Label: *i.Label, Script: i.Script, Color: *i.Color}
chart.AddBar(*i.Label, *i.Color) chart.AddBar(*i.Label, *i.Color)
data.NewSampler(chart.Consumer, item, triggers, *b.RefreshRateMs)
} }
} }
for _, gc := range cfg.Gauges { for _, gc := range cfg.Gauges {
g := gauge.NewGauge(gc.Title, *gc.Scale, *gc.Color) g := gauge.NewGauge(gc.Title, *gc.Scale, *gc.Color)
triggers := trigger.NewTriggers(gc.Triggers) triggers := data.NewTriggers(gc.Triggers, g.Consumer, player)
lout.AddComponent(config.TypeGauge, g, gc.Title, gc.Position, gc.Size, *gc.RefreshRateMs) lout.AddComponent(config.TypeGauge, g, gc.Title, gc.Position, gc.Size, *gc.RefreshRateMs)
items := data.NewItems(gc.Items)
for _, i := range gc.Items { data.NewSampler(g.Consumer, items, triggers, *gc.RefreshRateMs)
item := data.Item{Label: *i.Label, Script: i.Script}
data.NewSampler(g.Consumer, item, triggers, *gc.RefreshRateMs)
}
} }
handler := event.NewHandler(lout) handler := event.NewHandler(lout)

View File

@ -1,111 +0,0 @@
package trigger
import (
"fmt"
"github.com/sqshq/sampler/asset"
"github.com/sqshq/sampler/config"
"os"
"os/exec"
)
const (
TrueIndicator = "1"
BellCharacter = "\a"
)
type Trigger struct {
title string
condition string
actions Actions
data map[string]Data
}
type Actions struct {
terminalBell bool
sound bool
visual bool
script *string
}
type Data struct {
previousValue interface{}
currentValue interface{}
}
func NewTriggers(configs []config.TriggerConfig) []Trigger {
triggers := make([]Trigger, len(configs))
for _, c := range configs {
triggers = append(triggers, NewTrigger(c))
}
return triggers
}
func NewTrigger(config config.TriggerConfig) Trigger {
return Trigger{
title: config.Title,
condition: config.Condition,
data: make(map[string]Data),
actions: Actions{
terminalBell: *config.Actions.TerminalBell,
sound: *config.Actions.Sound,
visual: *config.Actions.Visual,
script: config.Actions.Script,
},
}
}
func (t Trigger) Execute(label string, value interface{}) {
if data, ok := t.data[label]; ok {
data.previousValue = data.currentValue
data.currentValue = value
} else {
t.data[label] = Data{previousValue: nil, currentValue: value}
}
t.evaluate(label, t.data[label])
}
func (t Trigger) evaluate(label string, data Data) {
output, err := runScript(t.condition, label, data)
if err != nil {
//println(err) // TODO visual notification
}
if string(output) != TrueIndicator {
return
}
if t.actions.terminalBell {
fmt.Print(BellCharacter)
}
if t.actions.sound {
_ = asset.Beep()
}
if t.actions.visual {
// TODO visual notification
}
if t.actions.script != nil {
_, _ = runScript(*t.actions.script, label, data)
}
}
func runScript(script, label string, data Data) ([]byte, error) {
cmd := exec.Command("sh", "-c", script)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("prev=%v", data.previousValue),
fmt.Sprintf("cur=%v", data.currentValue),
fmt.Sprintf("label=%v", label))
return cmd.Output()
}