diff --git a/Makefile b/Makefile index 4a9ccac..2798784 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ all: bin/ambient-glance bin/ambient-glance: $(SOURCES) go build -o bin/ambient-glance . +bin/obactl: $(SOURCES) + go build -o bin/obactl ./cmd/obactl + .PHONY: test test: go test -v ./... diff --git a/README.md b/README.md index 2753605..d4c89b8 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ supporting both an automatic carousel between apps and preemption. The following apps are implemented today: * Clock * Fortune - based on the fortune(6) command +* 8 clap - The UCLA 8 clap * ADS-B - to display the closest detected aircraft within a small radius of a receiver +* [OneBusAway](https://onebusaway.org/) - Transit arrival data > This is not an officially supported Google product. This project is not > eligible for the [Google Open Source Software Vulnerability Rewards @@ -21,3 +23,21 @@ The following apps are implemented today: ![LD220-HP pole display showing SKW3853](docs/skw3853.jpg) +## Sample config + +```json +{ + "adsb": { + "tar1090_endpoint":"http://localhost:30152", + "lat":"", + "lon":"", + "radius":"3" + }, + "oba": { + "stops": ["stop code"], + "route_alias": { + "route code": "NAME" + } + } +} +``` diff --git a/apps/oba.go b/apps/oba.go new file mode 100644 index 0000000..c2246f8 --- /dev/null +++ b/apps/oba.go @@ -0,0 +1,257 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package apps + +import ( + "context" + "errors" + "fmt" + "log" + "sort" + "strings" + "sync" + "time" + + onebusaway "github.com/OneBusAway/go-sdk" + "github.com/OneBusAway/go-sdk/option" + "go.sbk.wtf/ambient-glance/display" + "go.sbk.wtf/ambient-glance/scheduler" +) + +type oba struct { + log *log.Logger + loc *time.Location + intents chan<- scheduler.Intent + stops []string + alias map[string]string + cache obaCache +} + +type obaCache struct { + l sync.RWMutex + nextArrivals []obaArrival +} + +type obaArrival struct { + shortName string + time time.Time + headsign string + agency string +} + +func NewOBA(stops []string, alias map[string]string, log *log.Logger) *oba { + loc, err := time.LoadLocation("America/Los_Angeles") + if err != nil { + log.Fatal(err) + } + return &oba{ + log: log, + loc: loc, + stops: stops, + alias: alias, + } +} + +func (o *oba) WithIntents(intents chan<- scheduler.Intent) IntentApp { + o.intents = intents + return o +} + +func (o *oba) Name() string { + return "onebusaway" +} + +func (o *oba) Stop(id string) error { + return nil +} + +func (o *oba) Run(ctx context.Context) error { + client := onebusaway.NewClient( + option.WithAPIKey("TEST"), + ) + first := true + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if first { + first = false + break + } + time.Sleep(30 * time.Second) + } + arrivals := make([]obaArrival, 0) + for _, s := range o.stops { + res, err := client.ArrivalAndDeparture.List(ctx, s, onebusaway.ArrivalAndDepartureListParams{ + MinutesAfter: onebusaway.Int(20), + }) + if err != nil { + o.log.Printf("error listing arrivals for %q: %v", s, err) + continue + } + o.log.Printf("oba: got %d arrivals for stop %q\n", len(res.Data.Entry.ArrivalsAndDepartures), s) + agencies := make(map[string]string) + for _, a := range res.Data.References.Agencies { + agencies[a.ID] = a.Name + } + routes := make(map[string]string) + for _, r := range res.Data.References.Routes { + routes[r.ID] = r.AgencyID + } + for _, a := range res.Data.Entry.ArrivalsAndDepartures { + dep := time.UnixMilli(a.PredictedDepartureTime).In(o.loc) + now := time.Now() + if now.After(dep) || now.Add(20*time.Minute).Before(dep) { + continue + } + name := a.RouteShortName + if n, ok := o.alias[a.RouteID]; ok { + name = n + } + arrivals = append(arrivals, obaArrival{ + shortName: name, + headsign: a.TripHeadsign, + time: dep, + agency: agencies[routes[a.RouteID]], + }) + o.log.Printf("oba: added arrival for %q at %q dep %s\n", name, s, dep.Format(time.Kitchen)) + } + } + sort.Slice(arrivals, func(i, j int) bool { + return arrivals[i].time.Before(arrivals[j].time) + }) + o.cache.l.Lock() + o.cache.nextArrivals = arrivals + o.cache.l.Unlock() + } +} + +func (o *oba) SignalIntent() error { + if o.intents == nil { + return errors.New("no intents set") + } + o.intents <- scheduler.Intent{ + Name: o.Name(), + Activity: &obaActivity{ + id: "intent", + log: o.log, + cache: &o.cache, + }, + } + return nil +} + +func (o *oba) Activate(id string) (scheduler.Activity, error) { + return &obaActivity{ + id: id, + log: o.log, + cache: &o.cache, + }, nil +} + +type obaActivity struct { + id string + log *log.Logger + cache *obaCache +} + +func (o *obaActivity) Run(ctx context.Context, d display.Display) error { + if err := d.Reset(); err != nil { + return err + } + first := true + for i := 0; i < 3; i++ { + select { + case <-ctx.Done(): + return nil + default: + if first { + first = false + break + } + time.Sleep(2 * time.Second) + } + var arrivals []obaArrival + o.cache.l.RLock() + arrivals = o.cache.nextArrivals + o.cache.l.RUnlock() + if len(arrivals) == 0 { + return nil + } + for j := 0; j < len(arrivals) && j < 10; j += 2 { + if j > 0 { + time.Sleep(2 * time.Second) + } + var one, two string + one = formatArrival(arrivals[j]) + if j+1 < len(arrivals) { + two = formatArrival(arrivals[j+1]) + } else { + two = strings.Repeat(" ", 20) + } + if err := d.MoveCursor(display.CursorTopLeft); err != nil { + return err + } + if _, err := d.Write([]byte(one)); err != nil { + return err + } + if err := d.MoveCursor(display.CursorBottomLeft); err != nil { + return err + } + if _, err := d.Write([]byte(two)); err != nil { + return err + } + } + } + return nil +} + +func formatArrival(arrival obaArrival) string { + in := arrival.time.Sub(time.Now()) + inFmt := fmt.Sprintf("%dm", int(in.Round(time.Minute).Minutes())) + if in.Minutes() < 0 { + inFmt = "NOW" + } + shortName := fmtField(arrival.shortName, 5, leftPad) + headSign := fmtField(arrival.headsign, 10, rightPad) + inFmt = fmtField(inFmt, 3, rightPad) + + // 12345 7890123456 890 + return fmt.Sprintf("%s %s %s", shortName, headSign, inFmt) +} + +type padDir int + +const ( + leftPad padDir = iota + rightPad +) + +func fmtField(s string, size int, pad padDir) string { + switch { + case len(s) == size: + return s + case len(s) > size: + return s[:size] + case pad == leftPad: + return strings.Repeat(" ", size-len(s)) + s + case pad == rightPad: + return s + strings.Repeat(" ", size-len(s)) + } + return "" +} diff --git a/cmd/obactl/main.go b/cmd/obactl/main.go new file mode 100644 index 0000000..27b526f --- /dev/null +++ b/cmd/obactl/main.go @@ -0,0 +1,160 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "errors" + "fmt" + "os" + "strings" + "text/tabwriter" + "time" + + oba "github.com/OneBusAway/go-sdk" + "github.com/OneBusAway/go-sdk/option" + "github.com/spf13/cobra" +) + +func main() { + rootCmd := &cobra.Command{ + Use: "obactl", + Short: "obactl is a tool for querying OneBusAway", + } + rootCmd.AddCommand(stopsCommand()) + rootCmd.AddCommand(arrivalCommand()) + err := rootCmd.Execute() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func stopsCommand() *cobra.Command { + stops := &cobra.Command{ + Use: "stops", + Short: "Finds stops for a given location", + } + var ( + lat float64 + lon float64 + ) + flags := stops.Flags() + flags.Float64Var(&lat, "lat", 0, "Latitude to search for stops for") + flags.Float64Var(&lon, "lon", 0, "Longitude to search for stops for") + stops.MarkFlagRequired("lat") + stops.MarkFlagRequired("lon") + stops.RunE = func(cmd *cobra.Command, args []string) error { + client := oba.NewClient( + option.WithAPIKey("TEST"), + ) + + res, err := client.StopsForLocation.List(stops.Context(), oba.StopsForLocationListParams{ + Lat: oba.F(lat), + Lon: oba.F(lon), + }) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return err + } + if res == nil { + return errors.New("nil response") + } + fmt.Println("AGENCIES") + tw := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', tabwriter.TabIndent|tabwriter.DiscardEmptyColumns) + fmt.Fprintln(tw, "ID\tNAME") + for _, a := range res.Data.References.Agencies { + fmt.Fprintf(tw, "%s\t%s\n", a.ID, a.Name) + } + tw.Flush() + fmt.Println() + fmt.Println("ROUTES") + tw = tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', tabwriter.TabIndent|tabwriter.DiscardEmptyColumns) + fmt.Fprintln(tw, "ID\tSHORT\tNAME") + shorts := make(map[string]string) + for _, r := range res.Data.References.Routes { + fmt.Fprintf(tw, "%s\t%s\t%s\n", r.ID, r.ShortName, r.LongName) + shorts[r.ID] = r.ShortName + } + tw.Flush() + + fmt.Println() + fmt.Println("STOPS") + tw = tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', tabwriter.TabIndent|tabwriter.DiscardEmptyColumns) + fmt.Fprintln(tw, "ID\tDIR\tNAME\tROUTES") + for _, s := range res.Data.List { + routes := make([]string, 0) + for _, r := range s.RouteIDs { + routes = append(routes, shorts[r]) + } + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", s.ID, s.Direction, s.Name, strings.Join(routes, ",")) + } + tw.Flush() + + return nil + } + return stops +} + +func arrivalCommand() *cobra.Command { + arrival := &cobra.Command{ + Use: "arrival", + Short: "Finds arrival for a given location", + } + var ( + stopID string + ) + flags := arrival.Flags() + flags.StringVarP(&stopID, "id", "i", "", "stop ID") + arrival.MarkFlagRequired("id") + arrival.RunE = func(cmd *cobra.Command, args []string) error { + client := oba.NewClient( + option.WithAPIKey("TEST"), + ) + + res, err := client.ArrivalAndDeparture.List(arrival.Context(), stopID, oba.ArrivalAndDepartureListParams{}) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return err + } + if res == nil { + return errors.New("nil response") + } + loc, err := time.LoadLocation("America/Los_Angeles") + if err != nil { + fmt.Fprintln(os.Stderr, err) + return err + } + agencies := make(map[string]string) + for _, a := range res.Data.References.Agencies { + agencies[a.ID] = a.Name + } + routes := make(map[string]string) + for _, r := range res.Data.References.Routes { + routes[r.ID] = r.AgencyID + } + tw := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', tabwriter.TabIndent|tabwriter.DiscardEmptyColumns) + fmt.Fprintln(tw, "ID\tSHORT\tPREDICTED ARRIVAL\tDEPARTURE\tHEADSIGN\tAGENCY") + for _, a := range res.Data.Entry.ArrivalsAndDepartures { + arr := time.UnixMilli(a.PredictedArrivalTime).In(loc) + dep := time.UnixMilli(a.PredictedDepartureTime).In(loc) + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", a.RouteID, a.RouteShortName, arr.Format(time.Kitchen), dep.Format(time.Kitchen), a.TripHeadsign, agencies[routes[a.RouteID]]) + } + tw.Flush() + return nil + } + return arrival +} diff --git a/config.go b/config.go index ec600b4..719d00e 100644 --- a/config.go +++ b/config.go @@ -22,10 +22,20 @@ import ( ) type Config struct { - ADSBTar1090Endpoint string `json:"adsb_tar1090_endpoint"` - ADSBLat string `json:"adsb_lat"` - ADSBLon string `json:"adsb_lon"` - ADSBRadius string `json:"adsb_radius"` + ADSB ADSBConfig `json:"adsb"` + OBA OBAConfig `json:"oba"` +} + +type ADSBConfig struct { + Tar1090Endpoint string `json:"tar1090_endpoint"` + Lat string `json:"lat"` + Lon string `json:"lon"` + Radius string `json:"radius"` +} + +type OBAConfig struct { + Stops []string `json:"stops"` + RouteAlias map[string]string `json:"route_alias"` } func LoadConfig(path string) (*Config, error) { diff --git a/go.mod b/go.mod index 969b5b4..7f4a903 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,20 @@ require ( ) require ( + github.com/OneBusAway/go-sdk v1.4.8 // indirect github.com/containerd/ltag v0.3.0 // indirect github.com/creack/goselect v0.1.3 // indirect github.com/gdamore/encoding v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect diff --git a/go.sum b/go.sum index 69e39b2..ceba090 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,8 @@ +github.com/OneBusAway/go-sdk v1.4.8 h1:/qhf1fD1KECauO0Aamss2awv3gB1GAmgKtEGxFPDfQ0= +github.com/OneBusAway/go-sdk v1.4.8/go.mod h1:7Rj5b+lGJROO+UqrkHPEjwJcXddbhwL0CQSJrLaAWSA= github.com/containerd/ltag v0.3.0 h1:AbeBQAGLwWxWVkgtLblT5Zd5fFW1+45On3+RvuZO+Go= github.com/containerd/ltag v0.3.0/go.mod h1:VEpXtwQK+FDdhegH7NLRJM5gzdHtHWDztP1YoZxWJlQ= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc= @@ -11,6 +14,8 @@ github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JX github.com/gdamore/tcell/v2 v2.13.1 h1:Ca2N6mHxhXuElCgn+nfKuZjS7gwNiIRKHFiljrZQ26A= github.com/gdamore/tcell/v2 v2.13.1/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= @@ -25,9 +30,26 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -98,3 +120,4 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index bf4bcbc..7f63989 100644 --- a/main.go +++ b/main.go @@ -111,8 +111,9 @@ func main() { playApp := apps.NewPlay() fortuneApp := apps.NewFortune() eightClapApp := apps.New8Clap() + obaApp := apps.NewOBA(config.OBA.Stops, config.OBA.RouteAlias, l) - sch, _, intents := scheduler.NewScheduler(schedulerDisp, l, clockApp, fortuneApp, eightClapApp) + sch, _, intents := scheduler.NewScheduler(schedulerDisp, l, clockApp, obaApp, fortuneApp, eightClapApp) var ( runningScheduler = false schedCtx context.Context @@ -138,8 +139,9 @@ func main() { sharkApp := apps.NewBabyShark(intents) eightClapIntentApp := apps.New8ClapIntent(intents) + obaIntentApp := obaApp.WithIntents(intents) - adsb := apps.NewADSB(config.ADSBTar1090Endpoint, config.ADSBLat, config.ADSBLon, config.ADSBRadius, l, intents) + adsb := apps.NewADSB(config.ADSB.Tar1090Endpoint, config.ADSB.Lat, config.ADSB.Lon, config.ADSB.Radius, l, intents) var ( runningADSB = false adsbCtx context.Context @@ -163,6 +165,29 @@ func main() { } toggleADSB() + var ( + runningOBA = false + obaCtx context.Context + obaCancel context.CancelFunc + ) + toggleOBA := func() { + if runningOBA { + l.Println("Stop oba") + obaCancel() + runningOBA = false + return + } + l.Println("Run oba") + runningOBA = true + obaCtx, obaCancel = context.WithCancel(context.Background()) + go func() { + if err := obaApp.Run(obaCtx); err != nil { + l.Println("oba err", err) + } + }() + } + toggleOBA() + list := tview.NewList() list.ShowSecondaryText(false) list.AddItem("Scheduler", "", 's', toggleScheduler) @@ -177,6 +202,16 @@ func main() { }) list.AddItem("ADSB", "", 'a', toggleADSB) + list.AddItem("One Bus Away", "", 'o', toggleOBA) + list.AddItem("One Bus Away Now", "", 'g', func() { + go func() { + if err := obaIntentApp.SignalIntent(); err != nil { + l.Println("oba err", err) + return + } + l.Println("OBA activated") + }() + }) list.AddItem("Play", "", 'p', func() { go func() { diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 68b1c12..a2b1479 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -129,7 +129,7 @@ func (s scheduler) Run(ctx context.Context) error { cancel() } } - // return nil + return nil } func (s scheduler) runActivity(ctx context.Context, name string, id string, a Activity) error {