From 5f367c63cf437cbed9a672a376d34aa7085b8233 Mon Sep 17 00:00:00 2001 From: John Jones Date: Tue, 12 Oct 2021 08:17:32 -0400 Subject: [PATCH 1/5] updating tooling --- dashboard/dashboard/render.go | 355 ------------------ dashboard/go.mod | 12 - {dashboard => ground}/.gitignore | 0 {dashboard => ground}/Makefile | 0 .../dashboard => ground/core}/computed.go | 8 +- .../core}/computed_test.go | 4 +- .../dashboard => ground/core}/consts.go | 24 +- .../dashboard => ground/core}/flight_data.go | 27 +- ground/core/go.mod | 5 + ground/core/go.sum | 10 + {dashboard/dashboard => ground/core}/types.go | 36 +- {dashboard/dashboard => ground/core}/util.go | 2 +- ground/dashboard/consts.go | 5 + .../dashboard/data_provider.go | 2 +- ground/dashboard/go.mod | 14 + {dashboard => ground/dashboard}/go.sum | 7 +- {dashboard => ground}/dashboard/logger.go | 10 +- {dashboard => ground/dashboard}/main.go | 5 +- ground/dashboard/render.go | 233 ++++++++++++ .../dashboard/static/index.html | 0 .../dashboard/static/js/attitudeWidget.js | 0 .../dashboard/static/js/dashboard.js | 0 .../dashboard/static/js/kvTableWidget.js | 0 .../dashboard/static/js/lineChartWidget.js | 0 .../dashboard/static/js/main.js | 0 .../dashboard/static/js/mapWidget.js | 0 .../dashboard/static/js/missionInfoWidget.js | 0 .../dashboard/static/js/util.js | 0 .../dashboard/static/js/widget.js | 0 .../dashboard/static/style/style.css | 0 .../dashboard/telemetryio.go | 54 +-- .../dashboard/telemetryio_test.go | 2 +- ground/dashboard/types.go | 31 ++ .../generate_test_data/generate_test_data.go | 0 ground/reporter/charts/altitude.go | 47 +++ ground/reporter/charts/types.go | 7 + ground/reporter/charts/util.go | 11 + ground/reporter/charts/velocity.go | 47 +++ ground/reporter/conversion/inboard_reader.go | 82 ++++ ground/reporter/conversion/types.go | 7 + ground/reporter/go.mod | 12 + ground/reporter/go.sum | 39 ++ ground/reporter/main.go | 27 ++ ground/reporter/summerizers/altitude.go | 33 ++ ground/reporter/summerizers/mode_time.go | 47 +++ ground/reporter/summerizers/origin.go | 36 ++ ground/reporter/summerizers/touchdown.go | 36 ++ ground/reporter/summerizers/travel.go | 33 ++ ground/reporter/summerizers/types.go | 9 + ground/reporter/summerizers/util.go | 15 + ground/reporter/summerizers/velocity.go | 34 ++ ground/reporter/task_chart.go | 55 +++ ground/reporter/task_convert.go | 72 ++++ ground/reporter/task_summary.go | 64 ++++ ground/reporter/types.go | 11 + ground/reporter/util.go | 32 ++ ground/ground.ino => receiver/receiver.ino | 0 57 files changed, 1137 insertions(+), 465 deletions(-) delete mode 100644 dashboard/dashboard/render.go delete mode 100644 dashboard/go.mod rename {dashboard => ground}/.gitignore (100%) rename {dashboard => ground}/Makefile (100%) rename {dashboard/dashboard => ground/core}/computed.go (96%) rename {dashboard/dashboard => ground/core}/computed_test.go (97%) rename {dashboard/dashboard => ground/core}/consts.go (90%) rename {dashboard/dashboard => ground/core}/flight_data.go (79%) create mode 100644 ground/core/go.mod create mode 100644 ground/core/go.sum rename {dashboard/dashboard => ground/core}/types.go (82%) rename {dashboard/dashboard => ground/core}/util.go (94%) create mode 100644 ground/dashboard/consts.go rename {dashboard => ground}/dashboard/data_provider.go (98%) create mode 100644 ground/dashboard/go.mod rename {dashboard => ground/dashboard}/go.sum (86%) rename {dashboard => ground}/dashboard/logger.go (82%) rename {dashboard => ground/dashboard}/main.go (90%) create mode 100644 ground/dashboard/render.go rename {dashboard => ground}/dashboard/static/index.html (100%) rename {dashboard => ground}/dashboard/static/js/attitudeWidget.js (100%) rename {dashboard => ground}/dashboard/static/js/dashboard.js (100%) rename {dashboard => ground}/dashboard/static/js/kvTableWidget.js (100%) rename {dashboard => ground}/dashboard/static/js/lineChartWidget.js (100%) rename {dashboard => ground}/dashboard/static/js/main.js (100%) rename {dashboard => ground}/dashboard/static/js/mapWidget.js (100%) rename {dashboard => ground}/dashboard/static/js/missionInfoWidget.js (100%) rename {dashboard => ground}/dashboard/static/js/util.js (100%) rename {dashboard => ground}/dashboard/static/js/widget.js (100%) rename {dashboard => ground}/dashboard/static/style/style.css (100%) rename {dashboard => ground}/dashboard/telemetryio.go (51%) rename {dashboard => ground}/dashboard/telemetryio_test.go (97%) create mode 100644 ground/dashboard/types.go rename {dashboard => ground}/generate_test_data/generate_test_data.go (100%) create mode 100644 ground/reporter/charts/altitude.go create mode 100644 ground/reporter/charts/types.go create mode 100644 ground/reporter/charts/util.go create mode 100644 ground/reporter/charts/velocity.go create mode 100644 ground/reporter/conversion/inboard_reader.go create mode 100644 ground/reporter/conversion/types.go create mode 100644 ground/reporter/go.mod create mode 100644 ground/reporter/go.sum create mode 100644 ground/reporter/main.go create mode 100644 ground/reporter/summerizers/altitude.go create mode 100644 ground/reporter/summerizers/mode_time.go create mode 100644 ground/reporter/summerizers/origin.go create mode 100644 ground/reporter/summerizers/touchdown.go create mode 100644 ground/reporter/summerizers/travel.go create mode 100644 ground/reporter/summerizers/types.go create mode 100644 ground/reporter/summerizers/util.go create mode 100644 ground/reporter/summerizers/velocity.go create mode 100644 ground/reporter/task_chart.go create mode 100644 ground/reporter/task_convert.go create mode 100644 ground/reporter/task_summary.go create mode 100644 ground/reporter/types.go create mode 100644 ground/reporter/util.go rename ground/ground.ino => receiver/receiver.ino (100%) diff --git a/dashboard/dashboard/render.go b/dashboard/dashboard/render.go deleted file mode 100644 index 1617c0b..0000000 --- a/dashboard/dashboard/render.go +++ /dev/null @@ -1,355 +0,0 @@ -package dashboard - -import ( - "embed" - "fmt" - "io/fs" - "net/http" - "os" - "path" - "sync" - "time" - - ui "github.com/gizak/termui/v3" - "github.com/gizak/termui/v3/widgets" - "github.com/gorilla/websocket" -) - -const SecondsWindow = 20 - -//go:embed static/** -var static embed.FS - -type staticFS struct { - content embed.FS -} - -func (c staticFS) Open(name string) (fs.File, error) { - return c.content.Open(path.Join("static", name)) -} - -func StartTextLogger(p DataProvider, ds FlightData, logger LoggerControl) error { - if err := ui.Init(); err != nil { - return err - } - defer ui.Close() - - headers := []string{ - "Time", - "Prog", - "Pressure", - "Temp", - "Accel X", - "Accel Y", - "Accel Z", - "Mag X", - "Mag Y", - "Mag Z", - "Lat", - "Lon", - "Sats", - "Qual", - "RSSI", - } - - grid := ui.NewGrid() - termWidth, termHeight := ui.TerminalDimensions() - grid.SetRect(0, 0, termWidth, termHeight) - - table := widgets.NewTable() - table.Title = "Data Stream" - table.Rows = [][]string{headers} - - errorsui := widgets.NewList() - errorsui.Title = "Errors" - errorsui.Rows = []string{} - errorsui.WrapText = false - errorsList := make([]error, 0) - - grid.Set( - ui.NewRow(0.8, - ui.NewCol(1.0, table), - ), - ui.NewRow(0.2, - ui.NewCol(1.0, errorsui), - ), - ) - - uiEvents := ui.PollEvents() - streamChannel := p.Stream() - - renderTable := func() { - data := ds.AllSegments() - - if len(data) == 0 { - ui.Render(grid) - return - } - nRows := (table.Inner.Dy() + 1) / 2 - if nRows <= 0 { - nRows = 10 - } - if nRows > len(data)+1 { - nRows = len(data) + 1 - } - rows := make([][]string, nRows) - rows[0] = headers - for i := 0; i < nRows-1; i++ { - j := len(data) - nRows + 1 + i - seg := data[j] - rows[i+1] = []string{ - fmt.Sprintf("%0.2f", seg.Raw.Timestamp), - fmt.Sprintf("%0.2f", seg.Raw.WriteProgress), - fmt.Sprintf("%0.2f", seg.Raw.Pressure), - fmt.Sprintf("%0.2f", seg.Raw.Temperature), - fmt.Sprintf("%0.2f", seg.Raw.Acceleration.X), - fmt.Sprintf("%0.2f", seg.Raw.Acceleration.Y), - fmt.Sprintf("%0.2f", seg.Raw.Acceleration.Z), - fmt.Sprintf("%0.2f", seg.Raw.Magnetic.X), - fmt.Sprintf("%0.2f", seg.Raw.Magnetic.Y), - fmt.Sprintf("%0.2f", seg.Raw.Magnetic.Z), - fmt.Sprintf("%0.2f", seg.Raw.Coordinate.Lat), - fmt.Sprintf("%0.2f", seg.Raw.Coordinate.Lon), - fmt.Sprintf("%0.2f", seg.Raw.GPSInfo.Sats), - fmt.Sprintf("%0.2f", seg.Raw.GPSInfo.Quality), - fmt.Sprintf("%d", seg.Raw.Rssi), - } - } - table.Rows = rows - ui.Render(grid) - } - - renderErrors := func() { - if len(errorsList) == 0 { - ui.Render(grid) - return - } - - nRows := errorsui.Inner.Dy() - if nRows <= 0 { - nRows = 10 - } - if nRows > len(errorsList) { - nRows = len(errorsList) - } - rows := make([]string, nRows) - for i := 0; i < nRows; i++ { - j := len(errorsList) - nRows + i - rows[i] = fmt.Sprint(errorsList[j]) - } - errorsui.Rows = rows - - ui.Render(grid) - } - - renderTable() - - for { - select { - case e := <-uiEvents: - switch e.ID { - case "q", "": - return nil - } - case bytes := <-streamChannel: - latestSegments, err := ds.IngestNewSegment(bytes) - if err != nil { - errorsList = append(errorsList, err) - renderErrors() - } else { - renderTable() - for _, seg := range latestSegments { - logger.Log(seg) - } - } - } - } -} - -func StartWeb(p DataProvider, ds FlightData, logger LoggerControl) error { - var mutex sync.Mutex - var upgrader = websocket.Upgrader{} - http.HandleFunc("/api/data", func(w http.ResponseWriter, req *http.Request) { - c, err := upgrader.Upgrade(w, req, nil) - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - mutex.Lock() - data := ds.AllSegments() - lastLength := len(data) - err = c.WriteJSON(data) - mutex.Unlock() - if err != nil { - fmt.Println(err) - return - } - for { - mutex.Lock() - data = ds.AllSegments() - if len(data) > lastLength { - err = c.WriteJSON(data[lastLength:]) - if err != nil { - fmt.Println(err) - mutex.Unlock() - return - } - lastLength = len(data) - } - mutex.Unlock() - time.Sleep(1 * time.Second) - } - }) - if os.Getenv("DEV_MODE") == "" { - http.Handle("/", http.FileServer(http.FS(staticFS{static}))) - } else { - http.Handle("/", http.FileServer(http.Dir("dashboard/static"))) - } - go func() { - streamChannel := p.Stream() - for { - bytes := <-streamChannel - mutex.Lock() - latestSegments, err := ds.IngestNewSegment(bytes) - mutex.Unlock() - if err != nil { - fmt.Println(err) - } else { - for _, seg := range latestSegments { - logger.Log(seg) - } - } - } - }() - return http.ListenAndServe(":8080", nil) -} - -// return nil -// if err := ui.Init(); err != nil { -// return err -// } -// defer ui.Close() - -// altitude := widgets.NewPlot() -// altitude.Data = make([][]float64, 1) - -// velocity := widgets.NewPlot() -// velocity.Data = make([][]float64, 1) - -// rssi := widgets.NewPlot() -// rssi.Data = make([][]float64, 1) - -// temp := widgets.NewSparkline() -// pressure := widgets.NewSparkline() -// tempPress := widgets.NewSparklineGroup(temp, pressure) -// tempPress.Title = "Temperature & Pressure" - -// gpsQuality := widgets.NewSparkline() -// gpsSats := widgets.NewSparkline() -// gps := widgets.NewSparklineGroup(gpsQuality, gpsSats) -// gps.Title = "GPS Info" - -// bearingDistance := widgets.NewParagraph() -// bearingDistance.Title = "Bearing & Distance" - -// pitchYaw := widgets.NewParagraph() -// pitchYaw.Title = "Pitch & Yaw" - -// gauge := widgets.NewGauge() - -// dataStats := widgets.NewParagraph() -// dataStats.Title = "Signal Stats" - -// grid := ui.NewGrid() -// termWidth, termHeight := ui.TerminalDimensions() -// grid.SetRect(0, 0, termWidth, termHeight) - -// grid.Set( -// ui.NewRow(1.0/2, -// ui.NewCol(1.0/3, altitude), -// ui.NewCol(1.0/3, velocity), -// ui.NewCol(1.0/3, rssi), -// ), -// ui.NewRow(5.0/16, -// ui.NewCol(1.0/3, gps), -// ui.NewCol(1.0/3, tempPress), -// ui.NewCol(1.0/3, dataStats), -// ), -// ui.NewRow(3.0/16, -// ui.NewCol(1.0/3, bearingDistance), -// ui.NewCol(1.0/3, pitchYaw), -// ui.NewCol(1.0/3, gauge), -// ), -// ) - -// uiEvents := ui.PollEvents() -// streamChannel := p.Stream() -// ticker := time.NewTicker(time.Second).C -// lastStreamEvent := time.Now() -// lastEventAge := 0.0 -// lastLatestSegment := DataSegment{} - -// renderDashboard := func() { -// if len(ds.AllSegments()) > 1 { -// curtime := ds.Time() - -// altitude.Data[0] = captureEndFrameOfData(curtime, ds.Altitude(), altitude.Inner.Dx()-10, SecondsWindow) -// altitude.Title = fmt.Sprintf("Altitude (%.2f)", lastLatestSegment.Computed.Altitude) - -// velocity.Data[0] = captureEndFrameOfData(curtime, ds.Velocity(), velocity.Inner.Dx()-10, SecondsWindow) -// velocity.Title = fmt.Sprintf("Velocity (%.2f)", lastLatestSegment.Computed.Velocity) - -// temp.Title = fmt.Sprintf("Temperature: %.2f°", lastLatestSegment.Raw.Temperature) -// temp.Data = normalize(captureEndFrameOfData(curtime, ds.Temperature(), tempPress.Inner.Dx(), SecondsWindow)) - -// pressure.Title = fmt.Sprintf("Pressure: %.2f mBar", lastLatestSegment.Computed.NormalizedPressure) -// pressure.Data = normalize(captureEndFrameOfData(curtime, ds.Pressure(), tempPress.Inner.Dx(), SecondsWindow)) - -// gpsQuality.Title = fmt.Sprintf("GPS Signal Quality: %.2f", lastLatestSegment.Raw.GPSInfo.Quality) -// gpsQuality.Data = normalize(captureEndFrameOfData(curtime, ds.GpsQuality(), gps.Inner.Dx(), SecondsWindow)) - -// gpsSats.Title = fmt.Sprintf("GPS Sats: %.0f", lastLatestSegment.Raw.GPSInfo.Sats) -// gpsSats.Data = captureEndFrameOfData(curtime, ds.GpsSats(), gps.Inner.Dx(), SecondsWindow) - -// bearingDistance.Text = fmt.Sprintf("Bearing: %.2f\nDistance: %.2f", lastLatestSegment.Computed.Bearing, lastLatestSegment.Computed.Distance) - -// pitchYaw.Text = fmt.Sprintf("Pitch: %.2f\nYaw: %.2f", lastLatestSegment.Computed.Pitch, lastLatestSegment.Computed.Yaw) - -// rssi.Data[0] = captureEndFrameOfData(curtime, ds.Rssi(), rssi.Inner.Dx()-10, SecondsWindow) -// rssi.Title = fmt.Sprintf("RSSI (%d)", lastLatestSegment.Raw.Rssi) - -// gauge.Title = fmt.Sprintf("Mission Time: %s", timeString(lastLatestSegment.Raw.Timestamp)) -// gauge.Percent = int(100 * lastLatestSegment.Raw.CameraProgress) - -// receiving := lastEventAge < 5.0 -// dataStats.Text = fmt.Sprintf("Data Points: %d\nData Rate: %.2f/s\nLast Event Age: %.2fs\nReceiving: %t", len(ds.AllSegments()), lastLatestSegment.Computed.DataRate, lastEventAge, receiving) - -// ui.Render(grid) -// } -// } - -// for { -// select { -// case e := <-uiEvents: -// switch e.ID { -// case "q", "": -// return nil -// } -// case bytes := <-streamChannel: -// latestSegments, err := ds.IngestNewSegment(bytes) -// if err == nil { -// lastStreamEvent = time.Now() -// if len(latestSegments) > 0 { -// lastLatestSegment = latestSegments[len(latestSegments)-1] -// for _, seg := range latestSegments { -// logger.Log(seg) -// } -// } -// renderDashboard() -// } -// case <-ticker: -// lastEventAge = float64(time.Since(lastStreamEvent)) / float64(time.Second) -// renderDashboard() -// } -// } diff --git a/dashboard/go.mod b/dashboard/go.mod deleted file mode 100644 index e70e47d..0000000 --- a/dashboard/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module main - -go 1.16 - -require ( - github.com/gizak/termui/v3 v3.1.0 - github.com/gorilla/websocket v1.4.2 - github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 - github.com/johnjones4/termui v0.1.0 - github.com/stretchr/testify v1.7.0 - golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 // indirect -) diff --git a/dashboard/.gitignore b/ground/.gitignore similarity index 100% rename from dashboard/.gitignore rename to ground/.gitignore diff --git a/dashboard/Makefile b/ground/Makefile similarity index 100% rename from dashboard/Makefile rename to ground/Makefile diff --git a/dashboard/dashboard/computed.go b/ground/core/computed.go similarity index 96% rename from dashboard/dashboard/computed.go rename to ground/core/computed.go index 6f3dc7e..49c513e 100644 --- a/dashboard/dashboard/computed.go +++ b/ground/core/computed.go @@ -1,4 +1,4 @@ -package dashboard +package core import ( "math" @@ -135,7 +135,7 @@ func determineFlightMode(stream FlightData, raw RawDataSegment, computed Compute avgAcceleration := averageComputedValue(1, stream, raw, computed, func(seg ComputedDataSegment) float64 { return seg.SmoothedVerticalAcceleration }) - if lastMode == ModePrelaunch && avgVelocity > 1 { + if lastMode == ModePrelaunch && avgVelocity > 5 { return ModeAscentPowered } if lastMode == ModeAscentPowered && avgAcceleration < 0 && avgVelocity > 0 { @@ -147,13 +147,13 @@ func determineFlightMode(stream FlightData, raw RawDataSegment, computed Compute if lastMode == ModeDescentFreefall && math.Abs(avgAcceleration) < 0.5 { return ModeDescentParachute } - if (lastMode == ModeDescentFreefall || lastMode == ModeDescentParachute) && math.Abs(avgVelocity) < 0.5 { + if (lastMode == ModeDescentFreefall || lastMode == ModeDescentParachute) && math.Abs(avgAcceleration) < 0.5 && math.Abs(avgVelocity) < 0.5 { return ModeRecovery } return lastMode } -func computeDataSegment(stream FlightData, raw RawDataSegment) (ComputedDataSegment, float64, Coordinate) { +func ComputeDataSegment(stream FlightData, raw RawDataSegment) (ComputedDataSegment, float64, Coordinate) { bp := stream.BasePressure() if bp == 0 { bp = basePressure(stream) diff --git a/dashboard/dashboard/computed_test.go b/ground/core/computed_test.go similarity index 97% rename from dashboard/dashboard/computed_test.go rename to ground/core/computed_test.go index b50f950..1786982 100644 --- a/dashboard/dashboard/computed_test.go +++ b/ground/core/computed_test.go @@ -1,4 +1,4 @@ -package dashboard +package core import ( "math" @@ -127,7 +127,7 @@ func TestDataRate(t *testing.T) { func TestComputeDataSegment(t *testing.T) { segments, avg := makeDataSeries(0) - segment, bp, origin := computeDataSegment(&FlightDataConcrete{ + segment, bp, origin := ComputeDataSegment(&FlightDataConcrete{ Segments: segments, OriginCoordinate: Coordinate{37, -76}, }, RawDataSegment{ diff --git a/dashboard/dashboard/consts.go b/ground/core/consts.go similarity index 90% rename from dashboard/dashboard/consts.go rename to ground/core/consts.go index 0659916..219f9ee 100644 --- a/dashboard/dashboard/consts.go +++ b/ground/core/consts.go @@ -1,4 +1,13 @@ -package dashboard +package core + +const ( + ModePrelaunch = "P" + ModeAscentPowered = "AP" + ModeAscentUnpowered = "AU" + ModeDescentFreefall = "DF" + ModeDescentParachute = "DP" + ModeRecovery = "R" +) const ( IndexTimestamp = 0 @@ -15,16 +24,3 @@ const ( IndexGpsQuality = 11 IndexGpsSats = 12 ) - -const ( - PointsPerDataFrame = 2 -) - -const ( - ModePrelaunch = "P" - ModeAscentPowered = "AP" - ModeAscentUnpowered = "AU" - ModeDescentFreefall = "DF" - ModeDescentParachute = "DP" - ModeRecovery = "R" -) diff --git a/dashboard/dashboard/flight_data.go b/ground/core/flight_data.go similarity index 79% rename from dashboard/dashboard/flight_data.go rename to ground/core/flight_data.go index 2ebfd3c..001f2a1 100644 --- a/dashboard/dashboard/flight_data.go +++ b/ground/core/flight_data.go @@ -1,18 +1,19 @@ -package dashboard +package core func NewFlightData() FlightDataConcrete { return FlightDataConcrete{0, make([]DataSegment, 0), Coordinate{}} } -func (f *FlightDataConcrete) IngestNewSegment(bytes []byte) ([]DataSegment, error) { - segments, basePressure, origin, err := bytesToDataSegment(f, bytes) - if err != nil { - return segments, err - } +func (f *FlightDataConcrete) AppendData(segments []DataSegment) { f.Segments = append(f.Segments, segments...) - f.Base = basePressure - f.OriginCoordinate = origin - return segments, nil +} + +func (f *FlightDataConcrete) SetBasePressure(bp float64) { + f.Base = bp +} + +func (f *FlightDataConcrete) SetOrigin(coord Coordinate) { + f.OriginCoordinate = coord } func (f *FlightDataConcrete) AllSegments() []DataSegment { @@ -74,3 +75,11 @@ func (f *FlightDataConcrete) Rssi() []float64 { func (f *FlightDataConcrete) Origin() Coordinate { return f.OriginCoordinate } + +func (f *FlightDataConcrete) FlightModes() []FlightMode { + data := make([]FlightMode, len(f.AllSegments())) + for i, segment := range f.AllSegments() { + data[i] = segment.Computed.FlightMode + } + return data +} diff --git a/ground/core/go.mod b/ground/core/go.mod new file mode 100644 index 0000000..72183b1 --- /dev/null +++ b/ground/core/go.mod @@ -0,0 +1,5 @@ +module core + +go 1.16 + +require github.com/stretchr/testify v1.7.0 diff --git a/ground/core/go.sum b/ground/core/go.sum new file mode 100644 index 0000000..b380ae4 --- /dev/null +++ b/ground/core/go.sum @@ -0,0 +1,10 @@ +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/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/dashboard/dashboard/types.go b/ground/core/types.go similarity index 82% rename from dashboard/dashboard/types.go rename to ground/core/types.go index 40348d7..13ffa2f 100644 --- a/dashboard/dashboard/types.go +++ b/ground/core/types.go @@ -1,9 +1,4 @@ -package dashboard - -import ( - "io" - "sync" -) +package core type FlightMode string @@ -56,26 +51,17 @@ type DataSegment struct { Computed ComputedDataSegment `json:"computed"` } -type DataProvider interface { - Stream() <-chan []byte -} - type FlightDataConcrete struct { Base float64 Segments []DataSegment OriginCoordinate Coordinate } -type DataProviderFile struct { - Bytes [][]byte -} - -type DataProviderSerial struct { - Port io.ReadWriteCloser -} - type FlightData interface { - IngestNewSegment(bytes []byte) ([]DataSegment, error) + // IngestNewSegment(bytes []byte) ([]DataSegment, error) + AppendData(segments []DataSegment) + SetBasePressure(bp float64) + SetOrigin(coord Coordinate) AllSegments() []DataSegment BasePressure() float64 Origin() Coordinate @@ -87,15 +73,5 @@ type FlightData interface { GpsQuality() []float64 GpsSats() []float64 Rssi() []float64 -} - -type Logger struct { - DataChannel chan DataSegment - ContinueRunning bool - Mutex sync.Mutex -} - -type LoggerControl interface { - Kill() - Log(DataSegment) + FlightModes() []FlightMode } diff --git a/dashboard/dashboard/util.go b/ground/core/util.go similarity index 94% rename from dashboard/dashboard/util.go rename to ground/core/util.go index d7fcf10..24e48e8 100644 --- a/dashboard/dashboard/util.go +++ b/ground/core/util.go @@ -1,4 +1,4 @@ -package dashboard +package core func singleFlightDataElement(ds FlightData, accessor func(DataSegment) float64) []float64 { data := make([]float64, len(ds.AllSegments())) diff --git a/ground/dashboard/consts.go b/ground/dashboard/consts.go new file mode 100644 index 0000000..e6420b2 --- /dev/null +++ b/ground/dashboard/consts.go @@ -0,0 +1,5 @@ +package main + +const ( + PointsPerDataFrame = 2 +) diff --git a/dashboard/dashboard/data_provider.go b/ground/dashboard/data_provider.go similarity index 98% rename from dashboard/dashboard/data_provider.go rename to ground/dashboard/data_provider.go index 5e9beaf..018058f 100644 --- a/dashboard/dashboard/data_provider.go +++ b/ground/dashboard/data_provider.go @@ -1,4 +1,4 @@ -package dashboard +package main import ( "bytes" diff --git a/ground/dashboard/go.mod b/ground/dashboard/go.mod new file mode 100644 index 0000000..b1fe659 --- /dev/null +++ b/ground/dashboard/go.mod @@ -0,0 +1,14 @@ +module main + +go 1.16 + +replace github.com/johnjones4/model-rocket-telemetry/dashboard/core => ../core + +require ( + github.com/gizak/termui/v3 v3.1.0 + github.com/gorilla/websocket v1.4.2 + github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 + github.com/johnjones4/model-rocket-telemetry/dashboard/core v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.7.0 + golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect +) diff --git a/dashboard/go.sum b/ground/dashboard/go.sum similarity index 86% rename from dashboard/go.sum rename to ground/dashboard/go.sum index 16e2f62..b1e7dd6 100644 --- a/dashboard/go.sum +++ b/ground/dashboard/go.sum @@ -6,8 +6,6 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0= github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs= -github.com/johnjones4/termui v0.1.0 h1:js88oav+7ePj5frAsaO2wJSsttvEctiw+w2WUlRRl7g= -github.com/johnjones4/termui v0.1.0/go.mod h1:kDMgCVkOCMdq5mQvbwJebFv6M/a10tlLEnT2aH+JIXA= github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= @@ -19,8 +17,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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= -golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c= -golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/dashboard/dashboard/logger.go b/ground/dashboard/logger.go similarity index 82% rename from dashboard/dashboard/logger.go rename to ground/dashboard/logger.go index 3fa9b5a..a48c1f4 100644 --- a/dashboard/dashboard/logger.go +++ b/ground/dashboard/logger.go @@ -1,4 +1,4 @@ -package dashboard +package main import ( "encoding/json" @@ -7,9 +7,11 @@ import ( "path" "sync" "time" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" ) -func dataSegmentToString(ds DataSegment) string { +func dataSegmentToString(ds core.DataSegment) string { bytes, err := json.Marshal(ds) if err != nil { return "" @@ -30,7 +32,7 @@ func generateLogFilePath() (string, error) { func NewLogger() LoggerControl { logger := Logger{ - DataChannel: make(chan DataSegment, 100), + DataChannel: make(chan core.DataSegment, 100), ContinueRunning: true, Mutex: sync.Mutex{}, } @@ -66,6 +68,6 @@ func (l *Logger) Kill() { l.Mutex.Unlock() } -func (l *Logger) Log(ds DataSegment) { +func (l *Logger) Log(ds core.DataSegment) { l.DataChannel <- ds } diff --git a/dashboard/main.go b/ground/dashboard/main.go similarity index 90% rename from dashboard/main.go rename to ground/dashboard/main.go index 3daf3fb..1af4c22 100644 --- a/dashboard/main.go +++ b/ground/dashboard/main.go @@ -2,8 +2,9 @@ package main import ( "flag" - . "main/dashboard" "strings" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" ) const ( @@ -31,7 +32,7 @@ func main() { if err != nil { panic(err) } - df := NewFlightData() + df := core.NewFlightData() logger := NewLogger() defer logger.Kill() switch *output { diff --git a/ground/dashboard/render.go b/ground/dashboard/render.go new file mode 100644 index 0000000..ce54607 --- /dev/null +++ b/ground/dashboard/render.go @@ -0,0 +1,233 @@ +package main + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "os" + "path" + "sync" + "time" + + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" + "github.com/gorilla/websocket" + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +const SecondsWindow = 20 + +//go:embed static/** +var static embed.FS + +type staticFS struct { + content embed.FS +} + +func (c staticFS) Open(name string) (fs.File, error) { + return c.content.Open(path.Join("static", name)) +} + +func StartTextLogger(p DataProvider, ds core.FlightData, logger LoggerControl) error { + if err := ui.Init(); err != nil { + return err + } + defer ui.Close() + + headers := []string{ + "Time", + "Prog", + "Pressure", + "Temp", + "Accel X", + "Accel Y", + "Accel Z", + "Mag X", + "Mag Y", + "Mag Z", + "Lat", + "Lon", + "Sats", + "Qual", + "RSSI", + } + + grid := ui.NewGrid() + termWidth, termHeight := ui.TerminalDimensions() + grid.SetRect(0, 0, termWidth, termHeight) + + table := widgets.NewTable() + table.Title = "Data Stream" + table.Rows = [][]string{headers} + + errorsui := widgets.NewList() + errorsui.Title = "Errors" + errorsui.Rows = []string{} + errorsui.WrapText = false + errorsList := make([]error, 0) + + grid.Set( + ui.NewRow(0.8, + ui.NewCol(1.0, table), + ), + ui.NewRow(0.2, + ui.NewCol(1.0, errorsui), + ), + ) + + uiEvents := ui.PollEvents() + streamChannel := p.Stream() + + renderTable := func() { + data := ds.AllSegments() + + if len(data) == 0 { + ui.Render(grid) + return + } + nRows := (table.Inner.Dy() + 1) / 2 + if nRows <= 0 { + nRows = 10 + } + if nRows > len(data)+1 { + nRows = len(data) + 1 + } + rows := make([][]string, nRows) + rows[0] = headers + for i := 0; i < nRows-1; i++ { + j := len(data) - nRows + 1 + i + seg := data[j] + rows[i+1] = []string{ + fmt.Sprintf("%0.2f", seg.Raw.Timestamp), + fmt.Sprintf("%0.2f", seg.Raw.WriteProgress), + fmt.Sprintf("%0.2f", seg.Raw.Pressure), + fmt.Sprintf("%0.2f", seg.Raw.Temperature), + fmt.Sprintf("%0.2f", seg.Raw.Acceleration.X), + fmt.Sprintf("%0.2f", seg.Raw.Acceleration.Y), + fmt.Sprintf("%0.2f", seg.Raw.Acceleration.Z), + fmt.Sprintf("%0.2f", seg.Raw.Magnetic.X), + fmt.Sprintf("%0.2f", seg.Raw.Magnetic.Y), + fmt.Sprintf("%0.2f", seg.Raw.Magnetic.Z), + fmt.Sprintf("%0.2f", seg.Raw.Coordinate.Lat), + fmt.Sprintf("%0.2f", seg.Raw.Coordinate.Lon), + fmt.Sprintf("%0.2f", seg.Raw.GPSInfo.Sats), + fmt.Sprintf("%0.2f", seg.Raw.GPSInfo.Quality), + fmt.Sprintf("%d", seg.Raw.Rssi), + } + } + table.Rows = rows + ui.Render(grid) + } + + renderErrors := func() { + if len(errorsList) == 0 { + ui.Render(grid) + return + } + + nRows := errorsui.Inner.Dy() + if nRows <= 0 { + nRows = 10 + } + if nRows > len(errorsList) { + nRows = len(errorsList) + } + rows := make([]string, nRows) + for i := 0; i < nRows; i++ { + j := len(errorsList) - nRows + i + rows[i] = fmt.Sprint(errorsList[j]) + } + errorsui.Rows = rows + + ui.Render(grid) + } + + renderTable() + + for { + select { + case e := <-uiEvents: + switch e.ID { + case "q", "": + return nil + } + case bytes := <-streamChannel: + latestSegments, basePressure, origin, err := bytesToDataSegment(ds, bytes) + if err != nil { + errorsList = append(errorsList, err) + renderErrors() + } else { + ds.AppendData(latestSegments) + ds.SetBasePressure(basePressure) + ds.SetOrigin(origin) + renderTable() + for _, seg := range latestSegments { + logger.Log(seg) + } + } + } + } +} + +func StartWeb(p DataProvider, ds core.FlightData, logger LoggerControl) error { + var mutex sync.Mutex + var upgrader = websocket.Upgrader{} + http.HandleFunc("/api/data", func(w http.ResponseWriter, req *http.Request) { + c, err := upgrader.Upgrade(w, req, nil) + if err != nil { + fmt.Println(err) + return + } + defer c.Close() + mutex.Lock() + data := ds.AllSegments() + lastLength := len(data) + err = c.WriteJSON(data) + mutex.Unlock() + if err != nil { + fmt.Println(err) + return + } + for { + mutex.Lock() + data = ds.AllSegments() + if len(data) > lastLength { + err = c.WriteJSON(data[lastLength:]) + if err != nil { + fmt.Println(err) + mutex.Unlock() + return + } + lastLength = len(data) + } + mutex.Unlock() + time.Sleep(1 * time.Second) + } + }) + if os.Getenv("DEV_MODE") == "" { + http.Handle("/", http.FileServer(http.FS(staticFS{static}))) + } else { + http.Handle("/", http.FileServer(http.Dir("dashboard/static"))) + } + go func() { + streamChannel := p.Stream() + for { + bytes := <-streamChannel + latestSegments, basePressure, origin, err := bytesToDataSegment(ds, bytes) + if err != nil { + fmt.Println(err) + } else { + mutex.Lock() + ds.AppendData(latestSegments) + ds.SetBasePressure(basePressure) + ds.SetOrigin(origin) + mutex.Unlock() + for _, seg := range latestSegments { + logger.Log(seg) + } + } + } + }() + return http.ListenAndServe(":8080", nil) +} diff --git a/dashboard/dashboard/static/index.html b/ground/dashboard/static/index.html similarity index 100% rename from dashboard/dashboard/static/index.html rename to ground/dashboard/static/index.html diff --git a/dashboard/dashboard/static/js/attitudeWidget.js b/ground/dashboard/static/js/attitudeWidget.js similarity index 100% rename from dashboard/dashboard/static/js/attitudeWidget.js rename to ground/dashboard/static/js/attitudeWidget.js diff --git a/dashboard/dashboard/static/js/dashboard.js b/ground/dashboard/static/js/dashboard.js similarity index 100% rename from dashboard/dashboard/static/js/dashboard.js rename to ground/dashboard/static/js/dashboard.js diff --git a/dashboard/dashboard/static/js/kvTableWidget.js b/ground/dashboard/static/js/kvTableWidget.js similarity index 100% rename from dashboard/dashboard/static/js/kvTableWidget.js rename to ground/dashboard/static/js/kvTableWidget.js diff --git a/dashboard/dashboard/static/js/lineChartWidget.js b/ground/dashboard/static/js/lineChartWidget.js similarity index 100% rename from dashboard/dashboard/static/js/lineChartWidget.js rename to ground/dashboard/static/js/lineChartWidget.js diff --git a/dashboard/dashboard/static/js/main.js b/ground/dashboard/static/js/main.js similarity index 100% rename from dashboard/dashboard/static/js/main.js rename to ground/dashboard/static/js/main.js diff --git a/dashboard/dashboard/static/js/mapWidget.js b/ground/dashboard/static/js/mapWidget.js similarity index 100% rename from dashboard/dashboard/static/js/mapWidget.js rename to ground/dashboard/static/js/mapWidget.js diff --git a/dashboard/dashboard/static/js/missionInfoWidget.js b/ground/dashboard/static/js/missionInfoWidget.js similarity index 100% rename from dashboard/dashboard/static/js/missionInfoWidget.js rename to ground/dashboard/static/js/missionInfoWidget.js diff --git a/dashboard/dashboard/static/js/util.js b/ground/dashboard/static/js/util.js similarity index 100% rename from dashboard/dashboard/static/js/util.js rename to ground/dashboard/static/js/util.js diff --git a/dashboard/dashboard/static/js/widget.js b/ground/dashboard/static/js/widget.js similarity index 100% rename from dashboard/dashboard/static/js/widget.js rename to ground/dashboard/static/js/widget.js diff --git a/dashboard/dashboard/static/style/style.css b/ground/dashboard/static/style/style.css similarity index 100% rename from dashboard/dashboard/static/style/style.css rename to ground/dashboard/static/style/style.css diff --git a/dashboard/dashboard/telemetryio.go b/ground/dashboard/telemetryio.go similarity index 51% rename from dashboard/dashboard/telemetryio.go rename to ground/dashboard/telemetryio.go index c2d77c2..ec9c1a1 100644 --- a/dashboard/dashboard/telemetryio.go +++ b/ground/dashboard/telemetryio.go @@ -1,4 +1,4 @@ -package dashboard +package main import ( "bytes" @@ -7,6 +7,8 @@ import ( "errors" "math" "strings" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" ) func telemetryFloatFromByteIndex(bytes []byte, index int) float64 { @@ -46,47 +48,47 @@ func decodeTelemetryBytes(bytes []byte) ([]byte, []byte, error) { return telemetryBytes, rssiBytes, nil } -func bytesToDataSegment(stream FlightData, bytes []byte) ([]DataSegment, float64, Coordinate, error) { +func bytesToDataSegment(stream core.FlightData, bytes []byte) ([]core.DataSegment, float64, core.Coordinate, error) { telemetryBytes, rssiBytes, err := decodeTelemetryBytes(bytes) if err != nil { - return nil, 0, Coordinate{}, err + return nil, 0, core.Coordinate{}, err } - segments := make([]DataSegment, PointsPerDataFrame) + segments := make([]core.DataSegment, PointsPerDataFrame) var basePressure float64 - var origin Coordinate + var origin core.Coordinate for i := len(segments) - 1; i >= 0; i-- { offset := 1 + (i * 13) - raw := RawDataSegment{ + raw := core.RawDataSegment{ WriteProgress: telemetryFloatFromByteIndex(telemetryBytes, 0), - Timestamp: telemetryFloatFromByteIndex(telemetryBytes, offset+IndexTimestamp), - Pressure: telemetryFloatFromByteIndex(telemetryBytes, offset+IndexPressure), - Temperature: telemetryFloatFromByteIndex(telemetryBytes, offset+IndexTemperature), - Acceleration: XYZ{ - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexAccelerationX), - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexAccelerationY), - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexAccelerationZ), + Timestamp: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexTimestamp), + Pressure: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexPressure), + Temperature: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexTemperature), + Acceleration: core.XYZ{ + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAccelerationX), + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAccelerationY), + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAccelerationZ), }, - Magnetic: XYZ{ - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexMagneticX), - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexMagneticY), - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexMagneticZ), + Magnetic: core.XYZ{ + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexMagneticX), + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexMagneticY), + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexMagneticZ), }, - Coordinate: Coordinate{ - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexCoordinateLat), - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexCoordinateLon), + Coordinate: core.Coordinate{ + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexCoordinateLat), + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexCoordinateLon), }, - GPSInfo: GPSInfo{ - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexGpsQuality), - telemetryFloatFromByteIndex(telemetryBytes, offset+IndexGpsSats), + GPSInfo: core.GPSInfo{ + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexGpsQuality), + telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexGpsSats), }, Rssi: telemetryIntFromBytes(rssiBytes), } - var computed ComputedDataSegment - computed, basePressure, origin = computeDataSegment(stream, raw) + var computed core.ComputedDataSegment + computed, basePressure, origin = core.ComputeDataSegment(stream, raw) - segments[i] = DataSegment{ + segments[i] = core.DataSegment{ raw, computed, } diff --git a/dashboard/dashboard/telemetryio_test.go b/ground/dashboard/telemetryio_test.go similarity index 97% rename from dashboard/dashboard/telemetryio_test.go rename to ground/dashboard/telemetryio_test.go index 28e6dfb..e62bdff 100644 --- a/dashboard/dashboard/telemetryio_test.go +++ b/ground/dashboard/telemetryio_test.go @@ -1,4 +1,4 @@ -package dashboard +package main import ( "encoding/base64" diff --git a/ground/dashboard/types.go b/ground/dashboard/types.go new file mode 100644 index 0000000..f004ac3 --- /dev/null +++ b/ground/dashboard/types.go @@ -0,0 +1,31 @@ +package main + +import ( + "io" + "sync" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type Logger struct { + DataChannel chan core.DataSegment + ContinueRunning bool + Mutex sync.Mutex +} + +type LoggerControl interface { + Kill() + Log(core.DataSegment) +} + +type DataProvider interface { + Stream() <-chan []byte +} + +type DataProviderFile struct { + Bytes [][]byte +} + +type DataProviderSerial struct { + Port io.ReadWriteCloser +} diff --git a/dashboard/generate_test_data/generate_test_data.go b/ground/generate_test_data/generate_test_data.go similarity index 100% rename from dashboard/generate_test_data/generate_test_data.go rename to ground/generate_test_data/generate_test_data.go diff --git a/ground/reporter/charts/altitude.go b/ground/reporter/charts/altitude.go new file mode 100644 index 0000000..4cbadc4 --- /dev/null +++ b/ground/reporter/charts/altitude.go @@ -0,0 +1,47 @@ +package charts + +import ( + "bytes" + "os" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + "github.com/wcharczuk/go-chart/v2" +) + +type AltitudeChart struct { + filePath string +} + +func NewAltitudeChart(f string) ChartTask { + return AltitudeChart{f} +} + +func (c AltitudeChart) Generate(offsetSeconds float64, fd []core.DataSegment) error { + graph := chart.Chart{ + Series: []chart.Series{ + chart.ContinuousSeries{ + Name: "Altitude", + XValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Raw.Timestamp - offsetSeconds }), + YValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Computed.SmoothedAltitude }), + }, + }, + XAxis: chart.XAxis{ + Name: "Seconds", + }, + YAxis: chart.YAxis{ + Name: "Meters", + }, + } + + buffer := bytes.NewBuffer([]byte{}) + err := graph.Render(chart.PNG, buffer) + if err != nil { + return err + } + err = os.WriteFile(c.filePath, buffer.Bytes(), 0777) + if err != nil { + return err + } + + return nil +} diff --git a/ground/reporter/charts/types.go b/ground/reporter/charts/types.go new file mode 100644 index 0000000..920bc86 --- /dev/null +++ b/ground/reporter/charts/types.go @@ -0,0 +1,7 @@ +package charts + +import "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + +type ChartTask interface { + Generate(offsetSeconds float64, fd []core.DataSegment) error +} diff --git a/ground/reporter/charts/util.go b/ground/reporter/charts/util.go new file mode 100644 index 0000000..79e3016 --- /dev/null +++ b/ground/reporter/charts/util.go @@ -0,0 +1,11 @@ +package charts + +import "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + +func singleFlightDataElement(fd []core.DataSegment, accessor func(core.DataSegment) float64) []float64 { + data := make([]float64, len(fd)) + for i, segment := range fd { + data[i] = accessor(segment) + } + return data +} diff --git a/ground/reporter/charts/velocity.go b/ground/reporter/charts/velocity.go new file mode 100644 index 0000000..8d5068f --- /dev/null +++ b/ground/reporter/charts/velocity.go @@ -0,0 +1,47 @@ +package charts + +import ( + "bytes" + "os" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + "github.com/wcharczuk/go-chart/v2" +) + +type VelocityChart struct { + filePath string +} + +func NewVelocityChart(f string) ChartTask { + return VelocityChart{f} +} + +func (c VelocityChart) Generate(offsetSeconds float64, fd []core.DataSegment) error { + graph := chart.Chart{ + Series: []chart.Series{ + chart.ContinuousSeries{ + Name: "Velocity", + XValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Raw.Timestamp - offsetSeconds }), + YValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Computed.SmoothedVelocity }), + }, + }, + XAxis: chart.XAxis{ + Name: "Seconds", + }, + YAxis: chart.YAxis{ + Name: "Meters/Second", + }, + } + + buffer := bytes.NewBuffer([]byte{}) + err := graph.Render(chart.PNG, buffer) + if err != nil { + return err + } + err = os.WriteFile(c.filePath, buffer.Bytes(), 0777) + if err != nil { + return err + } + + return nil +} diff --git a/ground/reporter/conversion/inboard_reader.go b/ground/reporter/conversion/inboard_reader.go new file mode 100644 index 0000000..7f204df --- /dev/null +++ b/ground/reporter/conversion/inboard_reader.go @@ -0,0 +1,82 @@ +package conversion + +import ( + "encoding/csv" + "os" + "strconv" + + "github.com/cheggaaa/pb/v3" + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type InboardReader struct { + filePath string +} + +func NewInboardReader(f string) InboardReader { + return InboardReader{f} +} + +func (i InboardReader) Read(showProgress bool) (core.FlightData, error) { + f, err := os.Open(i.filePath) + if err != nil { + return nil, err + } + defer f.Close() + csvReader := csv.NewReader(f) + data, err := csvReader.ReadAll() + if err != nil { + return nil, err + } + fd := core.NewFlightData() + var bar *pb.ProgressBar + if showProgress { + bar = pb.StartNew(len(data)) + } + for i, row := range data { + rawSeg := core.RawDataSegment{ + WriteProgress: 0, + Timestamp: quietParseFloat(row, core.IndexTimestamp), + Pressure: quietParseFloat(row, core.IndexPressure), + Temperature: quietParseFloat(row, core.IndexTemperature), + Acceleration: core.XYZ{ + X: quietParseFloat(row, core.IndexAccelerationX), + Y: quietParseFloat(row, core.IndexAccelerationY), + Z: quietParseFloat(row, core.IndexAccelerationZ), + }, + Magnetic: core.XYZ{ + X: quietParseFloat(row, core.IndexMagneticX), + Y: quietParseFloat(row, core.IndexMagneticY), + Z: quietParseFloat(row, core.IndexMagneticZ), + }, + Coordinate: core.Coordinate{ + Lat: quietParseFloat(row, core.IndexCoordinateLat), + Lon: quietParseFloat(row, core.IndexCoordinateLon), + }, + GPSInfo: core.GPSInfo{ + Quality: quietParseFloat(row, core.IndexGpsQuality), + Sats: quietParseFloat(row, core.IndexGpsSats), + }, + Rssi: 0, + } + computed, basePressure, origin := core.ComputeDataSegment(&fd, rawSeg) + fd.AppendData([]core.DataSegment{{ + Raw: rawSeg, + Computed: computed, + }}) + fd.SetBasePressure(basePressure) + fd.SetOrigin(origin) + if showProgress { + bar.SetCurrent(int64(i)) + } + } + if showProgress { + bar.Finish() + } + return &fd, nil +} + +func quietParseFloat(row []string, i int) float64 { + f, _ := strconv.ParseFloat(row[i], 64) + return f +} diff --git a/ground/reporter/conversion/types.go b/ground/reporter/conversion/types.go new file mode 100644 index 0000000..5599f30 --- /dev/null +++ b/ground/reporter/conversion/types.go @@ -0,0 +1,7 @@ +package conversion + +import "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + +type Reader interface { + Read(showProgress bool) (core.FlightData, error) +} diff --git a/ground/reporter/go.mod b/ground/reporter/go.mod new file mode 100644 index 0000000..8d4f0bb --- /dev/null +++ b/ground/reporter/go.mod @@ -0,0 +1,12 @@ +module main + +go 1.16 + +replace github.com/johnjones4/model-rocket-telemetry/dashboard/core => ../core + +require ( + github.com/cheggaaa/pb/v3 v3.0.8 + github.com/johnjones4/model-rocket-telemetry/dashboard/core v0.0.0-00010101000000-000000000000 + github.com/wcharczuk/go-chart/v2 v2.1.0 + golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect +) diff --git a/ground/reporter/go.sum b/ground/reporter/go.sum new file mode 100644 index 0000000..fb81ebd --- /dev/null +++ b/ground/reporter/go.sum @@ -0,0 +1,39 @@ +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= +github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= +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/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +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= +github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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/ground/reporter/main.go b/ground/reporter/main.go new file mode 100644 index 0000000..1dcc5c7 --- /dev/null +++ b/ground/reporter/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "os" +) + +func main() { + if len(os.Args) < 2 { + log.Fatal("not enough arguments") + } + cmds := []task{ + newTaskConvert(), + newTaskSummary(), + newTaskChart(), + } + for _, cmd := range cmds { + if cmd.name() == os.Args[1] { + cmd.FlagSet().Parse(os.Args[2:]) + err := cmd.run() + if err != nil { + log.Panic(err) + } + return + } + } +} diff --git a/ground/reporter/summerizers/altitude.go b/ground/reporter/summerizers/altitude.go new file mode 100644 index 0000000..37d6761 --- /dev/null +++ b/ground/reporter/summerizers/altitude.go @@ -0,0 +1,33 @@ +package summerizers + +import ( + "errors" + "fmt" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type ApogeeSummerizer struct { + altitude float64 +} + +func (s *ApogeeSummerizer) Generate(fd []core.DataSegment) error { + s.altitude = 0 + for _, d := range fd { + if d.Computed.SmoothedAltitude > s.altitude && d.Computed.FlightMode == core.ModeAscentUnpowered { + s.altitude = d.Computed.SmoothedAltitude + } + } + if s.altitude == 0 { + return errors.New("cannot determine apogee") + } + return nil +} + +func (s *ApogeeSummerizer) Value() string { + return fmt.Sprintf("%f m", s.altitude) +} + +func (s *ApogeeSummerizer) Name() string { + return "Apogee" +} diff --git a/ground/reporter/summerizers/mode_time.go b/ground/reporter/summerizers/mode_time.go new file mode 100644 index 0000000..ca363e1 --- /dev/null +++ b/ground/reporter/summerizers/mode_time.go @@ -0,0 +1,47 @@ +package summerizers + +import ( + "fmt" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type ModeTimeSummerizer struct { + time float64 + mode core.FlightMode +} + +func NewModeTimeSummerizer(m core.FlightMode) Summarizer { + return &ModeTimeSummerizer{0, m} +} + +func (s *ModeTimeSummerizer) Generate(fd []core.DataSegment) error { + s.time = timeInMode(fd, s.mode) + if s.time == 0 { + return fmt.Errorf("cannot determine time in %s", s.mode) + } + return nil +} + +func (s *ModeTimeSummerizer) Value() string { + return fmt.Sprintf("%f s", s.time) +} + +func (s *ModeTimeSummerizer) Name() string { + switch s.mode { + case core.ModePrelaunch: + return "Prelaunch" + case core.ModeAscentPowered: + return "Powered Ascent" + case core.ModeAscentUnpowered: + return "Unpowered Ascent" + case core.ModeDescentFreefall: + return "Freefall Descent" + case core.ModeDescentParachute: + return "Controlled Descent" + case core.ModeRecovery: + return "Recovery" + default: + return "" + } +} diff --git a/ground/reporter/summerizers/origin.go b/ground/reporter/summerizers/origin.go new file mode 100644 index 0000000..be3bdc3 --- /dev/null +++ b/ground/reporter/summerizers/origin.go @@ -0,0 +1,36 @@ +package summerizers + +import ( + "errors" + "fmt" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type OriginSummerizer struct { + origin core.Coordinate + originSet bool +} + +func (s *OriginSummerizer) Generate(fd []core.DataSegment) error { + s.origin = core.Coordinate{} + s.originSet = false + for _, d := range fd { + if !s.originSet && d.Computed.FlightMode == core.ModeAscentPowered { + s.origin = d.Raw.Coordinate + s.originSet = true + } + } + if !s.originSet { + return errors.New("cannot determine origin") + } + return nil +} + +func (s *OriginSummerizer) Value() string { + return fmt.Sprintf("%f/%f", s.origin.Lat, s.origin.Lon) +} + +func (s *OriginSummerizer) Name() string { + return "Origin" +} diff --git a/ground/reporter/summerizers/touchdown.go b/ground/reporter/summerizers/touchdown.go new file mode 100644 index 0000000..48c5616 --- /dev/null +++ b/ground/reporter/summerizers/touchdown.go @@ -0,0 +1,36 @@ +package summerizers + +import ( + "errors" + "fmt" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type TouchdownSummerizer struct { + touchdown core.Coordinate + touchdownSet bool +} + +func (s *TouchdownSummerizer) Generate(fd []core.DataSegment) error { + s.touchdown = core.Coordinate{} + s.touchdownSet = false + for _, d := range fd { + if !s.touchdownSet && d.Computed.FlightMode == core.ModeRecovery { + s.touchdown = d.Raw.Coordinate + s.touchdownSet = true + } + } + if !s.touchdownSet { + return errors.New("cannot determine touchdown") + } + return nil +} + +func (s *TouchdownSummerizer) Value() string { + return fmt.Sprintf("%f/%f", s.touchdown.Lat, s.touchdown.Lon) +} + +func (s *TouchdownSummerizer) Name() string { + return "Touchdown" +} diff --git a/ground/reporter/summerizers/travel.go b/ground/reporter/summerizers/travel.go new file mode 100644 index 0000000..f23ff76 --- /dev/null +++ b/ground/reporter/summerizers/travel.go @@ -0,0 +1,33 @@ +package summerizers + +import ( + "errors" + "fmt" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type TravelSummerizer struct { + distance float64 +} + +func (s *TravelSummerizer) Generate(fd []core.DataSegment) error { + s.distance = 0 + for _, d := range fd { + if d.Computed.Distance > s.distance && d.Computed.FlightMode == core.ModeRecovery { + s.distance = d.Computed.Distance + } + } + if s.distance == 0 { + return errors.New("cannot determine travel distance") + } + return nil +} + +func (s *TravelSummerizer) Value() string { + return fmt.Sprintf("%f m", s.distance) +} + +func (s *TravelSummerizer) Name() string { + return "Travel Distance" +} diff --git a/ground/reporter/summerizers/types.go b/ground/reporter/summerizers/types.go new file mode 100644 index 0000000..2a0ede4 --- /dev/null +++ b/ground/reporter/summerizers/types.go @@ -0,0 +1,9 @@ +package summerizers + +import "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + +type Summarizer interface { + Generate(fd []core.DataSegment) error + Name() string + Value() string +} diff --git a/ground/reporter/summerizers/util.go b/ground/reporter/summerizers/util.go new file mode 100644 index 0000000..5318c67 --- /dev/null +++ b/ground/reporter/summerizers/util.go @@ -0,0 +1,15 @@ +package summerizers + +import "github.com/johnjones4/model-rocket-telemetry/dashboard/core" + +func timeInMode(fd []core.DataSegment, mode core.FlightMode) float64 { + startTime := -1.0 + for _, d := range fd { + if d.Computed.FlightMode == mode && startTime < 0 { + startTime = d.Raw.Timestamp + } else if d.Computed.FlightMode != mode && startTime > 0 { + return d.Raw.Timestamp - startTime + } + } + return 0 +} diff --git a/ground/reporter/summerizers/velocity.go b/ground/reporter/summerizers/velocity.go new file mode 100644 index 0000000..03bb5c8 --- /dev/null +++ b/ground/reporter/summerizers/velocity.go @@ -0,0 +1,34 @@ +package summerizers + +import ( + "errors" + "fmt" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type VelocitySummerizer struct { + mV float64 +} + +func (s *VelocitySummerizer) Generate(fd []core.DataSegment) error { + s.mV = 0 + for _, d := range fd { + //TODO fix mode checking + if d.Computed.SmoothedVelocity > s.mV && d.Computed.FlightMode == core.ModeAscentPowered { + s.mV = d.Computed.SmoothedVelocity + } + } + if s.mV == 0 { + return errors.New("cannot determine max v") + } + return nil +} + +func (s *VelocitySummerizer) Value() string { + return fmt.Sprintf("%f m/s", s.mV) +} + +func (s *VelocitySummerizer) Name() string { + return "Max Velocity" +} diff --git a/ground/reporter/task_chart.go b/ground/reporter/task_chart.go new file mode 100644 index 0000000..346cbb3 --- /dev/null +++ b/ground/reporter/task_chart.go @@ -0,0 +1,55 @@ +package main + +import ( + "flag" + "log" + "main/charts" + "path" +) + +type taskChart struct { + fs *flag.FlagSet + input *string + output *string +} + +func newTaskChart() taskChart { + tc := taskChart{ + fs: flag.NewFlagSet("chart", flag.ContinueOnError), + } + tc.input = tc.fs.String("input", "", "Input file path") + tc.output = tc.fs.String("output", "", "Output folder") + return tc +} + +func (t taskChart) FlagSet() *flag.FlagSet { + return t.fs +} + +func (t taskChart) name() string { + return t.fs.Name() +} + +func (t taskChart) run() error { + log.Printf("Reading light data from %s\n", *t.input) + fd, err := flightDataFromFile(*t.input) + if err != nil { + return err + } + + charts := []charts.ChartTask{ + charts.NewAltitudeChart(path.Join(*t.output, "altitude.png")), + charts.NewVelocityChart(path.Join(*t.output, "velocity.png")), + } + + offset := determineOffsetSeconds(fd) + + for _, c := range charts { + err = c.Generate(offset, fd) + if err != nil { + return err + } + } + + return nil +} diff --git a/ground/reporter/task_convert.go b/ground/reporter/task_convert.go new file mode 100644 index 0000000..a6d44cc --- /dev/null +++ b/ground/reporter/task_convert.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "log" + "main/conversion" + "os" +) + +const ( + DataTypeInboard = "inboard" +) + +type taskConvert struct { + fs *flag.FlagSet + input *string + output *string + dtype *string + progress *bool +} + +func newTaskConvert() taskConvert { + tc := taskConvert{ + fs: flag.NewFlagSet("convert", flag.ContinueOnError), + } + tc.input = tc.fs.String("input", "", "Input file path") + tc.dtype = tc.fs.String("type", DataTypeInboard, "Data type (inboard or ground)") + tc.output = tc.fs.String("output", "converted_flight_data.json", "Output file path") + tc.progress = tc.fs.Bool("progress", true, "Show progress") + return tc +} + +func (t taskConvert) FlagSet() *flag.FlagSet { + return t.fs +} + +func (t taskConvert) name() string { + return t.fs.Name() +} + +func (t taskConvert) run() error { + var readerInst conversion.Reader + switch *t.dtype { + case DataTypeInboard: + log.Printf("Will read inboard data from %s\n", *t.input) + readerInst = conversion.NewInboardReader(*t.input) + } + if readerInst == nil { + return errors.New("data type not supported") + } + + fd, err := readerInst.Read(*t.progress) + if err != nil { + return err + } + + log.Println("Converting data") + fdData, err := json.Marshal(fd.AllSegments()) + if err != nil { + return err + } + + log.Println("Writing data") + err = os.WriteFile(*t.output, fdData, 0777) + if err != nil { + return err + } + + return nil +} diff --git a/ground/reporter/task_summary.go b/ground/reporter/task_summary.go new file mode 100644 index 0000000..cbf76ca --- /dev/null +++ b/ground/reporter/task_summary.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "log" + + "main/summerizers" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +type taskSummary struct { + fs *flag.FlagSet + input *string +} + +func newTaskSummary() taskSummary { + tc := taskSummary{ + fs: flag.NewFlagSet("summary", flag.ContinueOnError), + } + tc.input = tc.fs.String("input", "", "Input file path") + return tc +} + +func (t taskSummary) FlagSet() *flag.FlagSet { + return t.fs +} + +func (t taskSummary) name() string { + return t.fs.Name() +} + +func (t taskSummary) run() error { + log.Printf("Reading light data from %s\n", *t.input) + fd, err := flightDataFromFile(*t.input) + if err != nil { + return err + } + + summerizers := []summerizers.Summarizer{ + &summerizers.ApogeeSummerizer{}, + &summerizers.VelocitySummerizer{}, + &summerizers.TravelSummerizer{}, + &summerizers.OriginSummerizer{}, + &summerizers.TouchdownSummerizer{}, + summerizers.NewModeTimeSummerizer(core.ModeAscentPowered), + summerizers.NewModeTimeSummerizer(core.ModeAscentUnpowered), + summerizers.NewModeTimeSummerizer(core.ModeDescentFreefall), + summerizers.NewModeTimeSummerizer(core.ModeDescentParachute), + } + for _, s := range summerizers { + log.Printf("Calculating %s\n", s.Name()) + err = s.Generate(fd) + if err != nil { + return err + } + } + + for _, s := range summerizers { + log.Printf("%s: %s\n", s.Name(), s.Value()) + } + + return nil +} diff --git a/ground/reporter/types.go b/ground/reporter/types.go new file mode 100644 index 0000000..3c33d6d --- /dev/null +++ b/ground/reporter/types.go @@ -0,0 +1,11 @@ +package main + +import ( + "flag" +) + +type task interface { + FlagSet() *flag.FlagSet + name() string + run() error +} diff --git a/ground/reporter/util.go b/ground/reporter/util.go new file mode 100644 index 0000000..ff250c2 --- /dev/null +++ b/ground/reporter/util.go @@ -0,0 +1,32 @@ +package main + +import ( + "encoding/json" + "os" + + "github.com/johnjones4/model-rocket-telemetry/dashboard/core" +) + +func flightDataFromFile(input string) ([]core.DataSegment, error) { + bytes, err := os.ReadFile(input) + if err != nil { + return nil, err + } + + var segs []core.DataSegment + err = json.Unmarshal(bytes, &segs) + if err != nil { + return nil, err + } + + return segs, err +} + +func determineOffsetSeconds(ds []core.DataSegment) float64 { + for _, d := range ds { + if d.Computed.FlightMode == core.ModeAscentPowered { + return d.Raw.Timestamp + } + } + return 0 +} diff --git a/ground/ground.ino b/receiver/receiver.ino similarity index 100% rename from ground/ground.ino rename to receiver/receiver.ino From a396f17f892e6e74763f8726e115895335562474 Mon Sep 17 00:00:00 2001 From: John Jones Date: Tue, 12 Oct 2021 09:05:53 -0400 Subject: [PATCH 2/5] fixing build --- .github/workflows/build-test-dashboard.yml | 4 +- ground/{ => dashboard}/Makefile | 3 - .../generate_test_data/generate_test_data.go | 83 ------------------- 3 files changed, 2 insertions(+), 88 deletions(-) rename ground/{ => dashboard}/Makefile (72%) delete mode 100644 ground/generate_test_data/generate_test_data.go diff --git a/.github/workflows/build-test-dashboard.yml b/.github/workflows/build-test-dashboard.yml index 8c193e2..6c296db 100644 --- a/.github/workflows/build-test-dashboard.yml +++ b/.github/workflows/build-test-dashboard.yml @@ -19,7 +19,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Install Go Dependencies, Test, and Build run: | - cd dashboard + cd ground/dashboard make install make test make build @@ -30,4 +30,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | - ./dashboard/build/* + ./ground/dashboard/build/* diff --git a/ground/Makefile b/ground/dashboard/Makefile similarity index 72% rename from ground/Makefile rename to ground/dashboard/Makefile index afa2a29..bfb4def 100644 --- a/ground/Makefile +++ b/ground/dashboard/Makefile @@ -10,9 +10,6 @@ install: run: go run . $(source) -generate_test_data: - go run generate_test_data/generate_test_data.go $(file) > data/data.txt - build: go build -o build/dashboard-$(OS)-$(ARCH) . diff --git a/ground/generate_test_data/generate_test_data.go b/ground/generate_test_data/generate_test_data.go deleted file mode 100644 index c1fcc0e..0000000 --- a/ground/generate_test_data/generate_test_data.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "bytes" - b64 "encoding/base64" - "encoding/binary" - "encoding/csv" - "fmt" - "io" - "math/rand" - "os" - "strconv" - "strings" -) - -func main() { - file := os.Args[1] - f, err := os.Open(file) - if err != nil { - fmt.Println(err) - return - } - defer f.Close() - - csvr := csv.NewReader(f) - - var buf bytes.Buffer - records := 0 - skipped := 0 - pcnt := 0.0 - for { - row, err := csvr.Read() - if err != nil { - if err == io.EOF { - return - } - fmt.Println(err) - return - } - if skipped == 3 { - skipped = 0 - if records == 0 { - _ = binary.Write(&buf, binary.LittleEndian, pcnt) - pcnt += 0.0001 - } - for _, el := range row { - p, err := strconv.ParseFloat(el, 64) - if err != nil { - fmt.Println(err) - return - } else { - err := binary.Write(&buf, binary.LittleEndian, p) - if err != nil { - fmt.Println(err) - return - } - } - } - records++ - if records == 2 { - sEnc := b64.StdEncoding.EncodeToString(buf.Bytes()) - - var buf1 bytes.Buffer - rssi := int16(rand.Intn(100) * -1) - err = binary.Write(&buf1, binary.LittleEndian, rssi) - if err != nil { - fmt.Println(err) - return - } - sEnc1 := b64.StdEncoding.EncodeToString(buf1.Bytes()) - - line := []string{"T", sEnc, sEnc1} - - fmt.Println(strings.Join(line[:], ",")) - - records = 0 - buf = *bytes.NewBuffer([]byte{}) - } - } else { - skipped++ - } - } -} From 966f3589bb6671b3380b618598000cc10d66eec3 Mon Sep 17 00:00:00 2001 From: John Jones Date: Tue, 12 Oct 2021 09:08:31 -0400 Subject: [PATCH 3/5] fixing build --- ground/dashboard/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ground/dashboard/Makefile b/ground/dashboard/Makefile index bfb4def..2929d26 100644 --- a/ground/dashboard/Makefile +++ b/ground/dashboard/Makefile @@ -14,7 +14,7 @@ build: go build -o build/dashboard-$(OS)-$(ARCH) . test: - cd dashboard && go test . + go test . clean: rm -rf build From b3e42848045975b14ccd3ec37fd97b1a602fba42 Mon Sep 17 00:00:00 2001 From: John Jones Date: Tue, 12 Oct 2021 09:11:37 -0400 Subject: [PATCH 4/5] fixing build 3 --- ...st-dashboard.yml => build-test-ground.yml} | 5 ++++- ground/dashboard/Makefile | 2 +- ground/tool/Makefile | 20 +++++++++++++++++++ ground/{reporter => tool}/charts/altitude.go | 0 ground/{reporter => tool}/charts/types.go | 0 ground/{reporter => tool}/charts/util.go | 0 ground/{reporter => tool}/charts/velocity.go | 0 .../conversion/inboard_reader.go | 0 ground/{reporter => tool}/conversion/types.go | 0 ground/{reporter => tool}/go.mod | 0 ground/{reporter => tool}/go.sum | 0 ground/{reporter => tool}/main.go | 0 .../summerizers/altitude.go | 0 .../summerizers/mode_time.go | 0 .../{reporter => tool}/summerizers/origin.go | 0 .../summerizers/touchdown.go | 0 .../{reporter => tool}/summerizers/travel.go | 0 .../{reporter => tool}/summerizers/types.go | 0 ground/{reporter => tool}/summerizers/util.go | 0 .../summerizers/velocity.go | 0 ground/{reporter => tool}/task_chart.go | 0 ground/{reporter => tool}/task_convert.go | 0 ground/{reporter => tool}/task_summary.go | 0 ground/{reporter => tool}/types.go | 0 ground/{reporter => tool}/util.go | 0 25 files changed, 25 insertions(+), 2 deletions(-) rename .github/workflows/{build-test-dashboard.yml => build-test-ground.yml} (89%) create mode 100644 ground/tool/Makefile rename ground/{reporter => tool}/charts/altitude.go (100%) rename ground/{reporter => tool}/charts/types.go (100%) rename ground/{reporter => tool}/charts/util.go (100%) rename ground/{reporter => tool}/charts/velocity.go (100%) rename ground/{reporter => tool}/conversion/inboard_reader.go (100%) rename ground/{reporter => tool}/conversion/types.go (100%) rename ground/{reporter => tool}/go.mod (100%) rename ground/{reporter => tool}/go.sum (100%) rename ground/{reporter => tool}/main.go (100%) rename ground/{reporter => tool}/summerizers/altitude.go (100%) rename ground/{reporter => tool}/summerizers/mode_time.go (100%) rename ground/{reporter => tool}/summerizers/origin.go (100%) rename ground/{reporter => tool}/summerizers/touchdown.go (100%) rename ground/{reporter => tool}/summerizers/travel.go (100%) rename ground/{reporter => tool}/summerizers/types.go (100%) rename ground/{reporter => tool}/summerizers/util.go (100%) rename ground/{reporter => tool}/summerizers/velocity.go (100%) rename ground/{reporter => tool}/task_chart.go (100%) rename ground/{reporter => tool}/task_convert.go (100%) rename ground/{reporter => tool}/task_summary.go (100%) rename ground/{reporter => tool}/types.go (100%) rename ground/{reporter => tool}/util.go (100%) diff --git a/.github/workflows/build-test-dashboard.yml b/.github/workflows/build-test-ground.yml similarity index 89% rename from .github/workflows/build-test-dashboard.yml rename to .github/workflows/build-test-ground.yml index 6c296db..643b58b 100644 --- a/.github/workflows/build-test-dashboard.yml +++ b/.github/workflows/build-test-ground.yml @@ -21,7 +21,9 @@ jobs: run: | cd ground/dashboard make install - make test + make build + cd ../tool + make install make build - name: Publish Artifacts uses: softprops/action-gh-release@v1 @@ -31,3 +33,4 @@ jobs: with: files: | ./ground/dashboard/build/* + ./ground/tool/build/* diff --git a/ground/dashboard/Makefile b/ground/dashboard/Makefile index 2929d26..c52167d 100644 --- a/ground/dashboard/Makefile +++ b/ground/dashboard/Makefile @@ -11,7 +11,7 @@ run: go run . $(source) build: - go build -o build/dashboard-$(OS)-$(ARCH) . + go build -o build/white-vest-dashboard-$(OS)-$(ARCH) . test: go test . diff --git a/ground/tool/Makefile b/ground/tool/Makefile new file mode 100644 index 0000000..35366ba --- /dev/null +++ b/ground/tool/Makefile @@ -0,0 +1,20 @@ +.PHONY: generate_test_data run + +OS=$(shell uname) +ARCH=$(shell arch) + +install: + go get ./... + go get -t ./... + +run: + go run . $(source) + +build: + go build -o build/white-vest-tools-$(OS)-$(ARCH) . + +test: + go test . + +clean: + rm -rf build diff --git a/ground/reporter/charts/altitude.go b/ground/tool/charts/altitude.go similarity index 100% rename from ground/reporter/charts/altitude.go rename to ground/tool/charts/altitude.go diff --git a/ground/reporter/charts/types.go b/ground/tool/charts/types.go similarity index 100% rename from ground/reporter/charts/types.go rename to ground/tool/charts/types.go diff --git a/ground/reporter/charts/util.go b/ground/tool/charts/util.go similarity index 100% rename from ground/reporter/charts/util.go rename to ground/tool/charts/util.go diff --git a/ground/reporter/charts/velocity.go b/ground/tool/charts/velocity.go similarity index 100% rename from ground/reporter/charts/velocity.go rename to ground/tool/charts/velocity.go diff --git a/ground/reporter/conversion/inboard_reader.go b/ground/tool/conversion/inboard_reader.go similarity index 100% rename from ground/reporter/conversion/inboard_reader.go rename to ground/tool/conversion/inboard_reader.go diff --git a/ground/reporter/conversion/types.go b/ground/tool/conversion/types.go similarity index 100% rename from ground/reporter/conversion/types.go rename to ground/tool/conversion/types.go diff --git a/ground/reporter/go.mod b/ground/tool/go.mod similarity index 100% rename from ground/reporter/go.mod rename to ground/tool/go.mod diff --git a/ground/reporter/go.sum b/ground/tool/go.sum similarity index 100% rename from ground/reporter/go.sum rename to ground/tool/go.sum diff --git a/ground/reporter/main.go b/ground/tool/main.go similarity index 100% rename from ground/reporter/main.go rename to ground/tool/main.go diff --git a/ground/reporter/summerizers/altitude.go b/ground/tool/summerizers/altitude.go similarity index 100% rename from ground/reporter/summerizers/altitude.go rename to ground/tool/summerizers/altitude.go diff --git a/ground/reporter/summerizers/mode_time.go b/ground/tool/summerizers/mode_time.go similarity index 100% rename from ground/reporter/summerizers/mode_time.go rename to ground/tool/summerizers/mode_time.go diff --git a/ground/reporter/summerizers/origin.go b/ground/tool/summerizers/origin.go similarity index 100% rename from ground/reporter/summerizers/origin.go rename to ground/tool/summerizers/origin.go diff --git a/ground/reporter/summerizers/touchdown.go b/ground/tool/summerizers/touchdown.go similarity index 100% rename from ground/reporter/summerizers/touchdown.go rename to ground/tool/summerizers/touchdown.go diff --git a/ground/reporter/summerizers/travel.go b/ground/tool/summerizers/travel.go similarity index 100% rename from ground/reporter/summerizers/travel.go rename to ground/tool/summerizers/travel.go diff --git a/ground/reporter/summerizers/types.go b/ground/tool/summerizers/types.go similarity index 100% rename from ground/reporter/summerizers/types.go rename to ground/tool/summerizers/types.go diff --git a/ground/reporter/summerizers/util.go b/ground/tool/summerizers/util.go similarity index 100% rename from ground/reporter/summerizers/util.go rename to ground/tool/summerizers/util.go diff --git a/ground/reporter/summerizers/velocity.go b/ground/tool/summerizers/velocity.go similarity index 100% rename from ground/reporter/summerizers/velocity.go rename to ground/tool/summerizers/velocity.go diff --git a/ground/reporter/task_chart.go b/ground/tool/task_chart.go similarity index 100% rename from ground/reporter/task_chart.go rename to ground/tool/task_chart.go diff --git a/ground/reporter/task_convert.go b/ground/tool/task_convert.go similarity index 100% rename from ground/reporter/task_convert.go rename to ground/tool/task_convert.go diff --git a/ground/reporter/task_summary.go b/ground/tool/task_summary.go similarity index 100% rename from ground/reporter/task_summary.go rename to ground/tool/task_summary.go diff --git a/ground/reporter/types.go b/ground/tool/types.go similarity index 100% rename from ground/reporter/types.go rename to ground/tool/types.go diff --git a/ground/reporter/util.go b/ground/tool/util.go similarity index 100% rename from ground/reporter/util.go rename to ground/tool/util.go From b0e6cc3d2325d3265fdb54bebf8de18001cf551c Mon Sep 17 00:00:00 2001 From: John Jones Date: Tue, 12 Oct 2021 09:20:07 -0400 Subject: [PATCH 5/5] readme update --- Readme.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 5a86f6e..0c587a7 100644 --- a/Readme.md +++ b/Readme.md @@ -105,18 +105,47 @@ The ground Arduino software receives transmitted packets and echos them out to s ### Dashboard -The dashboard is a text-based tool for tracking and logging received telemetry. To activate it, download a release from the GitHub project or build the dashboard using the following steps: +The dashboard is a text-based tool for tracking and logging received telemetry. To use it, download a release from the GitHub project or build the dashboard using the following steps: ```bash $ cd ~ $ git clone git@github.com:johnjones4/white-vest.git -$ cd white-vest/dashboard +$ cd white-vest/ground/dashboard $ make install $ make build ``` Then, run the dashboard using the following `build/dashboard-Darwin-i386 --input /dev/cu.usbmodem143101 --output text`. Note that `dashboard-Darwin-i386` will change based on the system you are using and `/dev/cu.usbmodem143101` is the path to the Arduino serial connection. To view the web dashboard, pass in `web` for the `--output` option and open [http://localhost:8080/](http://localhost:8080/). +### Tool + +The tool is a utility for after-flight work. To use it, download a release from the GitHub project or build the dashboard using the following steps: + +```bash +$ cd ~ +$ git clone git@github.com:johnjones4/white-vest.git +$ cd white-vest/ground/tool +$ make install +$ make build +``` + +The overall command structure is: + +```bash +$ white-vest-tool [TASK] [FLAGS] +``` + +Taks/Flags: +* `convert`: Converts telemtry data to plain old JSON for analysis + * `--input`: The file path to the input file + * `--output`: The file path to output to + * `--type`: The "type" of data. Right now the only valid option, and the default value, is `inboard`. This is the CSV file generated on the telemetry module during flight. I recommend reviewing the data and clipping the pre-launch and post-landing useless data for faster conversion. + * `--progress`: `true` or `false`, print a progress bar during conversion +* `summary`: Analyze the data and print a few summary data points like apogee, max v, etc + * `--input`: The file path to the input file (use the file made during `convert`) +* `chart`: Generate altitude and velocity charts + * `--input`: The file path to the input file (use the file made during `convert`) + * `--ouput`: The directory to save the charts to ### Air This software requires [I2C](https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-i2c), [SPI0](https://www.raspberrypi-spy.co.uk/2014/08/enabling-the-spi-interface-on-the-raspberry-pi/), and [Serial](https://maker.pro/raspberry-pi/tutorial/how-to-use-a-gps-receiver-with-raspberry-pi-4) to be enabled on a Raspberry Pi.