Receive data in dashboard

This commit is contained in:
Afonso Baldo 2022-12-05 22:25:36 +00:00
parent a9214847c4
commit 12f414b166
56 changed files with 2690 additions and 2813 deletions

View File

@ -5,19 +5,7 @@ import (
) )
func basePressure(stream FlightData) float64 { func basePressure(stream FlightData) float64 {
pressures := make([]float64, 0)
for _, segment := range stream.AllSegments() {
if segment.Computed.SmoothedPressure > 0 {
pressures = append(pressures, segment.Computed.SmoothedPressure)
}
if len(pressures) >= 10 {
var sum float64 = 0
for _, v := range pressures {
sum += v
}
return nanSafe(sum / float64(len(pressures)))
}
}
return 0 return 0
} }
@ -25,38 +13,31 @@ func altitude(bp float64, raw RawDataSegment) float64 {
if bp == 0 { if bp == 0 {
return 0 return 0
} }
return nanSafe(44307.7 * (1 - math.Pow((raw.Pressure/100)/bp, 0.190284))) return nanSafe(0.0)
} }
func normalizedPressure(raw RawDataSegment) float64 { func normalizedPressure(raw RawDataSegment) float64 {
return nanSafe(raw.Pressure / 100.0) return nanSafe(0.0)
} }
func velocity(stream FlightData, bp float64, raw RawDataSegment) float64 { func velocity(stream FlightData, bp float64, raw RawDataSegment) float64 {
altitude := altitude(bp, raw)
segments := stream.AllSegments()
for i := len(segments) - 1; i >= 0; i -= 1 {
if segments[i].Computed.Altitude != altitude {
return nanSafe((altitude - segments[i].Computed.Altitude) / (raw.Timestamp - segments[i].Raw.Timestamp))
}
}
return 0.0 return 0.0
} }
func yaw(raw RawDataSegment) float64 { func yaw(raw RawDataSegment) float64 {
return nanSafe(math.Atan2(-1.0*raw.Acceleration.X, raw.Acceleration.Z) * (180.0 / math.Pi)) return nanSafe(0.0)
} }
func pitch(raw RawDataSegment) float64 { func pitch(raw RawDataSegment) float64 {
return nanSafe(math.Atan2(-1.0*raw.Acceleration.Y, raw.Acceleration.Z) * (180.0 / math.Pi)) return nanSafe(0.0)
} }
func toRadians(degrees float64) float64 { func toRadians(degrees float64) float64 {
return nanSafe(degrees * math.Pi / 180) return nanSafe(0.0)
} }
func toDegrees(radians float64) float64 { func toDegrees(radians float64) float64 {
return nanSafe(radians * 180 / math.Pi) return nanSafe(0.0)
} }
func bearing(origin Coordinate, raw RawDataSegment) float64 { func bearing(origin Coordinate, raw RawDataSegment) float64 {
@ -77,121 +58,60 @@ func bearing(origin Coordinate, raw RawDataSegment) float64 {
} }
func distance(origin Coordinate, raw RawDataSegment) float64 { func distance(origin Coordinate, raw RawDataSegment) float64 {
if origin.Lat == 0 || origin.Lon == 0 || raw.Coordinate.Lat == 0 || raw.Coordinate.Lon == 0 { return 0.0
return 0
}
R := 6371e3
φ1 := origin.Lat * math.Pi / 180
φ2 := raw.Coordinate.Lat * math.Pi / 180
Δφ := (raw.Coordinate.Lat - origin.Lat) * math.Pi / 180
Δλ := (raw.Coordinate.Lon - origin.Lon) * math.Pi / 180
a := math.Sin(Δφ/2)*math.Sin(Δφ/2) + math.Cos(φ1)*math.Cos(φ2)*math.Sin(Δλ/2)*math.Sin(Δλ/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return nanSafe(R * c)
} }
func dataRate(stream FlightData) float64 { func dataRate(stream FlightData) float64 {
totalsMap := make(map[int]float64)
for _, timestamp := range stream.Time() { return nanSafe(0.0)
second := int(math.Floor(timestamp))
if total, ok := totalsMap[second]; ok {
totalsMap[second] = total + 1
} else {
totalsMap[second] = 1
}
}
total := 0.0
for _, secondTotal := range totalsMap {
total += secondTotal
}
return nanSafe(total / float64(len(totalsMap)))
} }
func averageComputedValue(seconds float64, stream FlightData, raw RawDataSegment, computed ComputedDataSegment, accessor func(seg ComputedDataSegment) float64) float64 { func averageComputedValue(seconds float64, stream FlightData, raw RawDataSegment, computed ComputedDataSegment, accessor func(seg ComputedDataSegment) float64) float64 {
total := accessor(computed) total := accessor(computed)
n := 1.0 return nanSafe(total)
i := len(stream.AllSegments()) - 1
for i >= 0 && raw.Timestamp-stream.Time()[i] <= seconds {
total += accessor(stream.AllSegments()[i].Computed)
n++
i--
}
return nanSafe(total / n)
} }
func determineFlightMode(stream FlightData, raw RawDataSegment, computed ComputedDataSegment) FlightMode { func determineFlightMode(stream FlightData, raw RawDataSegment, computed ComputedDataSegment) FlightMode {
length := len(stream.AllSegments()) switch int(raw.FlightPhase) {
if length == 0 { case 1:
return ModePrelaunch return ModeWaitingLaunch
case 2:
return ModeLaunch
case 4:
return ModeLowVelocity
case 5:
return ModeNoseOver
case 6:
return ModeDrogueFired
case 7:
return ModeMainFired
case 8:
return ModeFailsafe
case 9:
return ModeLanding
default:
return "UNKNOWN";
} }
lastMode := stream.AllSegments()[length-1].Computed.FlightMode
avgVelocity := averageComputedValue(1, stream, raw, computed, func(seg ComputedDataSegment) float64 {
return seg.SmoothedVelocity
})
avgAcceleration := averageComputedValue(1, stream, raw, computed, func(seg ComputedDataSegment) float64 {
return seg.SmoothedVerticalAcceleration
})
if lastMode == ModePrelaunch && avgVelocity > 5 {
return ModeAscentPowered
}
if lastMode == ModeAscentPowered && avgAcceleration < 0 && avgVelocity > 0 {
return ModeAscentUnpowered
}
if (lastMode == ModeAscentPowered || lastMode == ModeAscentUnpowered) && avgVelocity < 0 {
return ModeDescentFreefall
}
if lastMode == ModeDescentFreefall && math.Abs(avgAcceleration) < 0.5 {
return ModeDescentParachute
}
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() bp := 0.0
if bp == 0 {
bp = basePressure(stream)
}
origin := stream.Origin() origin := raw.Coordinate
if origin.Lat == 0 && origin.Lon == 0 && raw.Coordinate.Lat != 0 && raw.Coordinate.Lon != 0 {
origin = raw.Coordinate
}
alt := altitude(bp, raw)
vel := velocity(stream, bp, raw)
press := normalizedPressure(raw)
smoothedAlt := alt
smoothedVel := vel
smoothedVertAccel := 0.0
smoothedPress := press
smoothedTemp := raw.Temperature
s := len(stream.AllSegments())
if s > 0 {
alpha := 0.5
smoothedAlt = smoothed(alpha, alt, stream.SmoothedAltitude()[s-1])
smoothedVel = smoothed(alpha, vel, stream.SmoothedVelocity()[s-1])
smoothedPress = smoothed(alpha, press, stream.SmoothedPressure()[s-1])
smoothedTemp = smoothed(alpha, raw.Temperature, stream.SmoothedTemperature()[s-1])
smoothedVertAccel = (smoothedVel - stream.SmoothedVelocity()[s-1]) / (raw.Timestamp - stream.Time()[s-1])
}
computed := ComputedDataSegment{ computed := ComputedDataSegment{
Altitude: alt, Altitude: raw.Altitude,
Velocity: vel, Velocity: raw.Velocity,
Yaw: yaw(raw), Yaw: 0,
Pitch: pitch(raw), Pitch: 0,
Bearing: bearing(origin, raw), Bearing: 0,
Distance: distance(origin, raw), Distance: 0,
DataRate: dataRate(stream), DataRate: dataRate(stream),
SmoothedAltitude: smoothedAlt, SmoothedAltitude: raw.Altitude,
SmoothedVelocity: smoothedVel, SmoothedVelocity: raw.Velocity,
SmoothedPressure: smoothedPress, SmoothedPressure: 0.0,
SmoothedTemperature: smoothedTemp, SmoothedTemperature: raw.Temperature,
SmoothedVerticalAcceleration: smoothedVertAccel, SmoothedVerticalAcceleration: 0.0,
} }
computed.FlightMode = determineFlightMode(stream, raw, computed) computed.FlightMode = determineFlightMode(stream, raw, computed)

View File

@ -1,176 +0,0 @@
package core
import (
"math"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBasePressureSet(t *testing.T) {
segments, avg := makeDataSeries(0)
b := basePressure(&FlightDataConcrete{
Base: 0,
Segments: segments,
OriginCoordinate: Coordinate{},
})
assert.Equal(t, b, avg)
}
func TestAltitudeNoBase(t *testing.T) {
alt := altitude(0, RawDataSegment{
Pressure: rand.Float64(),
})
assert.Equal(t, alt, 0.0)
}
func TestAltitudeBase(t *testing.T) {
alt := altitude(1012, RawDataSegment{
Pressure: 1010,
})
assert.Equal(t, alt, 25868.260058108597)
}
func TestNormalizedPressure(t *testing.T) {
p := rand.Float64() * 1000
v := normalizedPressure(RawDataSegment{Pressure: p})
assert.Equal(t, v, p/100)
}
func TestVelocity(t *testing.T) {
bp := 1012.0
segments, _ := makeDataSeries(bp)
val := (rand.Float64()*20 + 1000) * 100.0
seg := RawDataSegment{
Timestamp: float64(len(segments)),
Pressure: val,
}
vel := velocity(&FlightDataConcrete{
Base: 0,
Segments: segments,
OriginCoordinate: Coordinate{},
}, bp, seg)
vel1 := (altitude(bp, seg) - segments[len(segments)-1].Computed.Altitude) / (seg.Timestamp - segments[len(segments)-1].Raw.Timestamp)
assert.Equal(t, vel, vel1)
}
func TestYaw(t *testing.T) {
val := yaw(RawDataSegment{
Acceleration: XYZ{
X: 100,
Y: 110,
Z: 120,
},
})
assert.Equal(t, val, -39.80557109226519)
}
func TestPitch(t *testing.T) {
val := pitch(RawDataSegment{
Acceleration: XYZ{
X: 100,
Y: 110,
Z: 120,
},
})
assert.Equal(t, val, -42.51044707800084)
}
func TestToDegrees(t *testing.T) {
val := toDegrees(math.Pi)
assert.Equal(t, val, 180.0)
}
func TestToRadians(t *testing.T) {
val := toRadians(90)
assert.Equal(t, val, math.Pi/2)
}
func TestBearing(t *testing.T) {
origin := Coordinate{
38.811423646113546,
-77.054951464077,
}
seg := RawDataSegment{
Coordinate: Coordinate{
38,
-77,
},
}
b := bearing(origin, seg)
assert.Equal(t, b, 179.9862686631269)
}
func TestDistance(t *testing.T) {
origin := Coordinate{
38.811423646113546,
-77.054951464077,
}
seg := RawDataSegment{
Coordinate: Coordinate{
38,
-77,
},
}
b := distance(origin, seg)
assert.Equal(t, b, 90353.15173806295)
}
func TestDataRate(t *testing.T) {
segments, _ := makeDataSeries(0)
rate := dataRate(&FlightDataConcrete{
Segments: segments,
})
assert.Equal(t, rate, 1.0)
}
func TestComputeDataSegment(t *testing.T) {
segments, avg := makeDataSeries(0)
segment, bp, origin := ComputeDataSegment(&FlightDataConcrete{
Segments: segments,
OriginCoordinate: Coordinate{37, -76},
}, RawDataSegment{
WriteProgress: 1.0,
Timestamp: float64(len(segments) + 1),
Pressure: 1014.0,
Temperature: 30.0,
Acceleration: XYZ{1, 2, 3},
Magnetic: XYZ{1, 2, 3},
Coordinate: Coordinate{38, -77},
GPSInfo: GPSInfo{0.0, 0.0},
Rssi: 0,
})
assert.Equal(t, bp, avg)
assert.NotEqual(t, origin.Lat, 0.0)
assert.NotEqual(t, origin.Lon, 0.0)
assert.NotEqual(t, segment.Altitude, 0.0)
assert.NotEqual(t, segment.Velocity, 0.0)
assert.NotEqual(t, segment.Yaw, 0.0)
assert.NotEqual(t, segment.Pitch, 0.0)
assert.NotEqual(t, segment.Bearing, 0.0)
assert.NotEqual(t, segment.Distance, 0.0)
assert.NotEqual(t, segment.DataRate, 0.0)
}
func makeDataSeries(bp float64) ([]DataSegment, float64) {
series := make([]DataSegment, 10)
total := 0.0
for i := 0; i < len(series); i++ {
val := rand.Float64()*20 + 1000
total += val
series[i] = DataSegment{
RawDataSegment{
Timestamp: float64(i),
Pressure: val * 100.0,
},
ComputedDataSegment{
Altitude: altitude(bp, RawDataSegment{
Pressure: val * 100.0,
}),
SmoothedPressure: val,
},
}
}
return series, total / float64(len(series))
}

View File

@ -1,26 +1,25 @@
package core package core
const ( const (
ModePrelaunch = "P" ModeWaitingLaunch = "WT"
ModeAscentPowered = "AP" ModeLaunch = "LD"
ModeAscentUnpowered = "AU" ModeLowVelocity = "LV"
ModeDescentFreefall = "DF" ModeNoseOver = "AP"
ModeDescentParachute = "DP" ModeDrogueFired = "DR"
ModeRecovery = "R" ModeMainFired = "MN"
ModeFailsafe = "FS"
ModeLanding = "TD"
) )
const ( const (
IndexTimestamp = 0 IndexFlightTime = 0
IndexPressure = 1 IndexFightPhase = 1
IndexTemperature = 2 IndexAltitude = 2
IndexAccelerationX = 3 IndexVelocity = 3
IndexAccelerationY = 4 IndexAcceleration = 4
IndexAccelerationZ = 5 IndexTemperature = 5
IndexMagneticX = 6 IndexCoordinateLat = 6
IndexMagneticY = 7 IndexCoordinateLon = 7
IndexMagneticZ = 8 IndexGpsQuality = 8
IndexCoordinateLat = 9 IndexGpsSats = 9
IndexCoordinateLon = 10
IndexGpsQuality = 11
IndexGpsSats = 12
) )

View File

@ -62,7 +62,7 @@ func (f *FlightDataConcrete) GpsSats() []float64 {
func (f *FlightDataConcrete) Time() []float64 { func (f *FlightDataConcrete) Time() []float64 {
return singleFlightDataElement(f, func(segment DataSegment) float64 { return singleFlightDataElement(f, func(segment DataSegment) float64 {
return segment.Raw.Timestamp return 0.0
}) })
} }

View File

@ -19,15 +19,15 @@ type XYZ struct {
} }
type RawDataSegment struct { type RawDataSegment struct {
WriteProgress float64 `json:"writeProgress"` FlightTime float64 `json:"flightTime"`
Timestamp float64 `json:"timestamp"` FlightPhase float64 `json:"flightPhase"`
Pressure float64 `json:"pressure"` Altitude float64 `json:"altitude"`
Temperature float64 `json:"temperature"` Velocity float64 `json:"velocity"`
Acceleration XYZ `json:"acceleration"` Acceleration float64 `json:"accelereation"`
Magnetic XYZ `json:"magnetic"` Temperature float64 `json:"temperature"`
Coordinate Coordinate `json:"coordinate"` Coordinate Coordinate `json:"coordinate"`
GPSInfo GPSInfo `json:"gpsInfo"` GPSInfo GPSInfo `json:"gpsInfo"`
Rssi int16 `json:"rssi"` Rssi int16 `json:"rssi"`
} }
type ComputedDataSegment struct { type ComputedDataSegment struct {

View File

@ -1,5 +1,5 @@
package main package main
const ( const (
PointsPerDataFrame = 2 PointsPerDataFrame = 1
) )

View File

@ -37,18 +37,15 @@ func StartTextLogger(p DataProvider, ds core.FlightData, logger LoggerControl) e
headers := []string{ headers := []string{
"Time", "Time",
"Prog", "Phase",
"Pressure", "Altitude",
"Temp", "Velocity",
"Accel X", "Acceleration",
"Accel Y", "Temperature",
"Accel Z", "Latitude",
"Mag X", "longitude",
"Mag Y", "GPS Quality",
"Mag Z", "GPS Sats",
"Lat",
"Lon",
"Sats",
"Qual", "Qual",
"RSSI", "RSSI",
} }
@ -99,16 +96,12 @@ func StartTextLogger(p DataProvider, ds core.FlightData, logger LoggerControl) e
j := len(data) - nRows + 1 + i j := len(data) - nRows + 1 + i
seg := data[j] seg := data[j]
rows[i+1] = []string{ rows[i+1] = []string{
fmt.Sprintf("%0.2f", seg.Raw.Timestamp), fmt.Sprintf("%0.2f", seg.Raw.FlightTime),
fmt.Sprintf("%0.2f", seg.Raw.WriteProgress), fmt.Sprintf("%0.2f", seg.Raw.FlightPhase),
fmt.Sprintf("%0.2f", seg.Raw.Pressure), fmt.Sprintf("%0.2f", seg.Raw.Altitude),
fmt.Sprintf("%0.2f", seg.Raw.Velocity),
fmt.Sprintf("%0.2f", seg.Raw.Acceleration),
fmt.Sprintf("%0.2f", seg.Raw.Temperature), 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.Lat),
fmt.Sprintf("%0.2f", seg.Raw.Coordinate.Lon), fmt.Sprintf("%0.2f", seg.Raw.Coordinate.Lon),
fmt.Sprintf("%0.2f", seg.Raw.GPSInfo.Sats), fmt.Sprintf("%0.2f", seg.Raw.GPSInfo.Sats),
@ -229,5 +222,6 @@ func StartWeb(p DataProvider, ds core.FlightData, logger LoggerControl) error {
} }
} }
}() }()
return http.ListenAndServe(":8080", nil) return http.ListenAndServe(":8080", nil)
} }

View File

@ -5,6 +5,7 @@
const webSocket = new WebSocket(`ws://${window.location.host}/api/data`) const webSocket = new WebSocket(`ws://${window.location.host}/api/data`)
webSocket.onmessage = (e) => { webSocket.onmessage = (e) => {
data = data.concat(JSON.parse(e.data)) data = data.concat(JSON.parse(e.data))
console.log( "DATA :" + data)
dashboard.update(data) dashboard.update(data)
} }
})() })()

View File

@ -1,10 +1,12 @@
const modeMap = { const modeMap = {
'P': 'Prelaunch', "WT": "Waiting for Launch",
'AP': 'Powered Ascent', "LD": "Launch Detected",
'AU': 'Unpowered Ascent', "LV": "Low velocity Detected",
'DF': 'Freefall Descent', "AP": "Nose-Over",
'DP': 'Parachute Descent', "DR": "Drogue Fired",
'R': 'Recovery' "MN": "Main Fired",
"FS": "Failsafe Triggered ",
"TD": "Landing Detected"
} }
class MissionInfoWidget extends Widget { class MissionInfoWidget extends Widget {

View File

@ -1,6 +1,6 @@
function makeXYExtractor(propType, propName) { function makeXYExtractor(propType, propName) {
return (data) => data.map(segment => ({ return (data) => data.map(segment => ({
x: segment.raw.timestamp, x: segment.raw.flightTime,
y: segment[propType][propName] y: segment[propType][propName]
})) }))
} }
@ -15,8 +15,7 @@ function makeCoordinateExtractor() {
function makeInfoExtractor() { function makeInfoExtractor() {
return (data) => ({ return (data) => ({
pcnt: data[data.length - 1].raw.cameraProgress, time: data[data.length - 1].raw.flightTime,
time: data[data.length - 1].raw.timestamp,
mode: data[data.length - 1].computed.flightMode mode: data[data.length - 1].computed.flightMode
}) })
} }

View File

@ -33,11 +33,13 @@ func telemetryIntFromBytes(b []byte) int16 {
} }
func decodeTelemetryBytes(bytes []byte) ([]byte, []byte, error) { func decodeTelemetryBytes(bytes []byte) ([]byte, []byte, error) {
parts := strings.Split(string(bytes), ",") parts := strings.Split(string(bytes), ",")
if len(parts) != 3 || parts[0] != "T" { if len(parts) != 3 || parts[0] != "T" {
return nil, nil, errors.New("bad telemetry") return nil, nil, errors.New(string(bytes))
} }
telemetryBytes, err := base64.StdEncoding.DecodeString(parts[1]) telemetryBytes, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -58,22 +60,15 @@ func bytesToDataSegment(stream core.FlightData, bytes []byte) ([]core.DataSegmen
var basePressure float64 var basePressure float64
var origin core.Coordinate var origin core.Coordinate
for i := len(segments) - 1; i >= 0; i-- { for i := len(segments) - 1; i >= 0; i-- {
offset := 1 + (i * 13) offset := (i * 10)
raw := core.RawDataSegment{ raw := core.RawDataSegment{
WriteProgress: telemetryFloatFromByteIndex(telemetryBytes, 0), FlightTime: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexFlightTime),
Timestamp: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexTimestamp), FlightPhase: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexFightPhase),
Pressure: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexPressure), Altitude: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAltitude),
Temperature: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexTemperature), Velocity: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexVelocity),
Acceleration: core.XYZ{ Acceleration: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAcceleration),
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAccelerationX), Temperature: telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexTemperature),
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAccelerationY),
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexAccelerationZ),
},
Magnetic: core.XYZ{
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexMagneticX),
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexMagneticY),
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexMagneticZ),
},
Coordinate: core.Coordinate{ Coordinate: core.Coordinate{
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexCoordinateLat), telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexCoordinateLat),
telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexCoordinateLon), telemetryFloatFromByteIndex(telemetryBytes, offset+core.IndexCoordinateLon),

View File

@ -1,27 +0,0 @@
package main
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTelemetryFloatFromByteIndex(t *testing.T) {
b64 := "AAAAAAAA8D8AAAAAAAAAQAAAAAAAAAhA"
data, _ := base64.StdEncoding.DecodeString(b64)
n := telemetryFloatFromByteIndex(data, 1)
assert.Equal(t, n, 2.0)
}
func TestTelemetryIntFromBytes(t *testing.T) {
n := telemetryIntFromBytes([]byte{0x01, 0x00})
assert.Equal(t, n, int16(1))
}
func TestDecodeTelemetryBytes(t *testing.T) {
telemetryBytes, rssiBytes, err := decodeTelemetryBytes([]byte("T,wcqhRbbz3T8NAIDUU3JoQGzPymub5/dAAQAM3C/rIkCoKRPINnrovxg/EbSXB+Y/qdDM1YdeMMD+XHTRRRctQAAAAAAAgELAT6wPjfWhL0D2QZYFE2dDQCC4yhMIQ1PAAAAAAAAAAAAAAAAAAAAAAA==,//8="))
assert.NotNil(t, telemetryBytes)
assert.NotNil(t, rssiBytes)
assert.Nil(t, err)
}

View File

@ -21,7 +21,7 @@ func (c AltitudeChart) Generate(offsetSeconds float64, fd []core.DataSegment) er
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Name: "Altitude", Name: "Altitude",
XValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Raw.Timestamp - offsetSeconds }), XValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Raw.flightTime - offsetSeconds }),
YValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Computed.SmoothedAltitude }), YValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Computed.SmoothedAltitude }),
}, },
}, },

View File

@ -21,7 +21,7 @@ func (c VelocityChart) Generate(offsetSeconds float64, fd []core.DataSegment) er
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Name: "Velocity", Name: "Velocity",
XValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Raw.Timestamp - offsetSeconds }), XValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Raw.flightTime - offsetSeconds }),
YValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Computed.SmoothedVelocity }), YValues: singleFlightDataElement(fd, func(d core.DataSegment) float64 { return d.Computed.SmoothedVelocity }),
}, },
}, },

View File

@ -6,9 +6,9 @@ func timeInMode(fd []core.DataSegment, mode core.FlightMode) float64 {
startTime := -1.0 startTime := -1.0
for _, d := range fd { for _, d := range fd {
if d.Computed.FlightMode == mode && startTime < 0 { if d.Computed.FlightMode == mode && startTime < 0 {
startTime = d.Raw.Timestamp startTime = d.Raw.flightTime
} else if d.Computed.FlightMode != mode && startTime > 0 { } else if d.Computed.FlightMode != mode && startTime > 0 {
return d.Raw.Timestamp - startTime return d.Raw.flightTime - startTime
} }
} }
return 0 return 0

View File

@ -24,8 +24,8 @@ func flightDataFromFile(input string) ([]core.DataSegment, error) {
func determineOffsetSeconds(ds []core.DataSegment) float64 { func determineOffsetSeconds(ds []core.DataSegment) float64 {
for _, d := range ds { for _, d := range ds {
if d.Computed.FlightMode == core.ModeAscentPowered { if d.Computed.FlightMode == core.ModeLaunch {
return d.Raw.Timestamp return d.Raw.flightTime
} }
} }
return 0 return 0

View File

@ -1,6 +1,26 @@
#include <SPI.h> #include <SPI.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
#include <string.h> #include <string.h>
#include <stdlib.h>
#include <base64.hpp>
#define BIT(n) (1 << (n))
#define BIT_FLIGHT_TIME BIT(0)
#define BIT_ALTITUDE_100 BIT(1)
#define BIT_ALTITUDE BIT(2)
#define BIT_VELOCITY BIT(3)
#define BIT_ACCELERATION BIT(4)
#define BIT_FLIGHT_PHASE BIT(5)
#define BIT_CHANNEL BIT(6)
#define BIT_TEMPERATURE_10 BIT(7)
#define BIT_NAME BIT(8)
#define BIT_BATERY_10 BIT(9)
#define BIT_APOGEE BIT(10)
#define BIT_MAX_VELOCITY BIT(11)
#define BIT_MAX_ACCELERATION BIT(12)
#define ALL_ELEMENTS 0b1111111111111
#define TRIGGER_FLIGHT_TIME '#' #define TRIGGER_FLIGHT_TIME '#'
#define TRIGGER_ALTITUDE_100 '{' #define TRIGGER_ALTITUDE_100 '{'
@ -17,60 +37,25 @@
#define TRIGGER_MAX_ACCELERATION '[' #define TRIGGER_MAX_ACCELERATION '['
#define TRIGGER_END_PACKET '>' #define TRIGGER_END_PACKET '>'
typedef union EggtimerData #define MAX_DATA_SIZE 100
{
int fight_time;
int altitude_100;
int altitude;
int velocity;
int acceleration_10;
int fight_phase;
char channel[6];
int temperature_10;
char name[9];
int battery_voltage_10;
int apogee;
int max_velocity;
int max_acceleration;
}EggtimerData;
typedef struct EggtimerElementPacket{ typedef struct WhiteVestsPacket{
double flight_time;
double flight_phase;
double altitude;
double velocity;
double acceleration;
double temperature;
double latitude;
double longitude;
double gpsSignal; // GPS Signal Strength
double gpsstat; // GPS Status
}WhiteVestsPacket;
typedef struct EggtimerElement{
char type; char type;
EggtimerData data; char data[MAX_DATA_SIZE];
} EggtimerElementPacket; } EggtimerElement;
static char _last_state = 0;
static size_t counter = 0;
/**
* @brief this function stores the data in the element packet
*/
void _save_data(char byte_received, EggtimerElementPacket *element){
switch (_last_state)
{
case TRIGGER_FLIGHT_TIME:
case TRIGGER_ALTITUDE_100:
case TRIGGER_ALTITUDE:
case TRIGGER_VELOCITY:
case TRIGGER_ACCELERATION_10:
case TRIGGER_FLIGHT_PHASE:
case TRIGGER_TEMPERATURE_10:
case TRIGGER_BATERY_10:
case TRIGGER_APOGEE:
case TRIGGER_MAX_VELOCITY:
case TRIGGER_MAX_ACCELERATION:
element->data.altitude *= 10;
element->data.altitude += byte_received - '0';
break;
case TRIGGER_CHANNEL:
case TRIGGER_NAME:
element->data.name[counter++] = byte_received;
break;
default:
break;
}
}
/** /**
* @brief This function is used to parse the data received from the Eggtimer * @brief This function is used to parse the data received from the Eggtimer
@ -79,7 +64,9 @@ void _save_data(char byte_received, EggtimerElementPacket *element){
* *
* @return true if receive new data packet, false otherwise * @return true if receive new data packet, false otherwise
*/ */
bool decode_eggtimer_data(char byte_received, EggtimerElementPacket * packet){ bool decode_eggtimer_data(char byte_received, EggtimerElement * packet){
static size_t counter = 0;
static char last_state = 0;
switch (byte_received) switch (byte_received)
{ {
@ -97,59 +84,242 @@ bool decode_eggtimer_data(char byte_received, EggtimerElementPacket * packet){
case TRIGGER_MAX_VELOCITY: case TRIGGER_MAX_VELOCITY:
case TRIGGER_MAX_ACCELERATION: case TRIGGER_MAX_ACCELERATION:
_last_state = byte_received; last_state = byte_received;
packet->type = byte_received; packet->type = byte_received;
memset(&packet->data, 0, sizeof packet->data); memset(&packet->data, 0, sizeof packet->data);
Serial.println( byte_received);
counter = 0; counter = 0;
break; break;
case TRIGGER_END_PACKET: case TRIGGER_END_PACKET:
if (_last_state){ if (last_state){
packet->type = _last_state; packet->type = last_state;
packet->data[counter] = '\0';
_last_state = 0; last_state = 0;
return true; return true;
} }
_last_state = 0; last_state = 0;
break; break;
default: default:
_save_data(byte_received, packet); // save data
if(last_state)
packet->data[counter++] = byte_received;
break; break;
} }
return false; return false;
} }
bool test(){ bool buildPacket(EggtimerElement *eggt_packet, WhiteVestsPacket *wv_packet){
char str[] = "{004>@5>#0132>~1B---->(00023>\\000>%04679>^0660>[025>?079>!201>=KM6ZFL>"; static int16_t elements_received = 0;
char *p = str; static WhiteVestsPacket nextPacket; // packet used when the packet is sent but it is not complete
EggtimerElementPacket packet; static bool sentIncomplete = false;
for (; *p; p++)
{
if(decode_eggtimer_data(*p, &packet)){
Serial.print("Data packet: ");
if(packet.type == TRIGGER_NAME || packet.type == TRIGGER_CHANNEL){
Serial.println(packet.data.name);
}
else{
Serial.println(packet.data.altitude);
}
} if(sentIncomplete){
memcpy(wv_packet, &nextPacket, sizeof(WhiteVestsPacket));
sentIncomplete = false;
}
int16_t element_bit = NULL;
double *element_pointer = NULL;
double *nextElement_pointer = NULL;
// convert data to double
bool converted = true;
char *endptr;
double element = strtol((char*)&eggt_packet->data, &endptr, 10);
if(endptr == (char*)&eggt_packet->data|| *endptr != NULL){ // error
converted = false;
element = 0;
}
switch (eggt_packet->type){
case TRIGGER_FLIGHT_TIME:
element_bit = BIT_FLIGHT_TIME;
element_pointer = &wv_packet->flight_time;
nextElement_pointer = &nextPacket.flight_time;
break;
case TRIGGER_ALTITUDE_100:
element_bit = BIT_ALTITUDE_100;
element_pointer = &wv_packet->altitude;
nextElement_pointer = &nextPacket.altitude;
element *= 100.0;
break;
case TRIGGER_ALTITUDE:
element_bit = BIT_ALTITUDE;
element_pointer = &wv_packet->altitude;
nextElement_pointer = &nextPacket.altitude;
break;
case TRIGGER_VELOCITY:
element_bit = BIT_VELOCITY;
element_pointer = &wv_packet->velocity;
nextElement_pointer = &nextPacket.velocity;
break;
case TRIGGER_ACCELERATION_10:
element_bit = BIT_ACCELERATION;
element_pointer = &wv_packet->acceleration;
nextElement_pointer = &nextPacket.acceleration;
element /= 10;
break;
case TRIGGER_FLIGHT_PHASE:
element_bit = BIT_FLIGHT_PHASE;
element_pointer = &wv_packet->flight_phase;
nextElement_pointer = &nextPacket.flight_phase;
break;
case TRIGGER_TEMPERATURE_10:
element_bit = BIT_TEMPERATURE_10;
element_pointer = &wv_packet->temperature;
nextElement_pointer = &nextPacket.temperature;
element /= 10;
break;
case TRIGGER_BATERY_10:
element_bit = BIT_BATERY_10;
break;
case TRIGGER_APOGEE:
element_bit = BIT_APOGEE;
break;
case TRIGGER_MAX_VELOCITY:
element_bit = BIT_MAX_VELOCITY;
break;
case TRIGGER_MAX_ACCELERATION:
element_bit = BIT_MAX_ACCELERATION;
break;
case TRIGGER_CHANNEL:
element_bit = BIT_CHANNEL;
break;
case TRIGGER_NAME:
element_bit = BIT_NAME;
break;
default:
break;
}
// check if the data received is from current packet, if not, it will be stored in the next packet
if((elements_received & element_bit) && element_pointer != NULL && nextElement_pointer != NULL && converted){
memcpy(&nextPacket, wv_packet, sizeof(WhiteVestsPacket));
*nextElement_pointer = element; // stores element in nextPacket
elements_received = element_bit;
sentIncomplete = true;
return true;
}
if( element_pointer != NULL){
if(converted)
*element_pointer = element;
else
return false; // error converting data
}
elements_received |= element_bit;
// check if the packet is complete
if(elements_received == ALL_ELEMENTS){
elements_received = 0;
return true;
} }
return false; return false;
}
/**
* @brief This function send the packet to the server
*/
void sendPacket(WhiteVestsPacket * packet){
unsigned char data_base64[255];
encode_base64((unsigned char*)packet, sizeof(WhiteVestsPacket), data_base64);
char rssi_base64[] = "//8=";
Serial.print("T,");
Serial.print((char *) data_base64);
Serial.print(",");
Serial.print((char *) rssi_base64);
Serial.print("\n");
}
bool test(){
char str[] ="{000>@1>#0000>~AB---->?079>!211>=KM6ZFL>"
"{000>@1>#0000>~AB---->?079>!211>=KM6ZFL>"
"{040>@2>#0010>~AB---->(0213>\\-001>?079>!212>=KM6ZFL>01>?079>!212>=KM6ZFL>"
"{045>@2>#0014>~AB---->(0072>\\-001>?079>!212>=KM6ZFL>"
"{046>@4>#0016>~AB---->(0033>\\-001>?079>!212>=KM6ZFL>"
"{046>@5>#0018>~1B---->(2787>\\000>%04679>^0660>[025>?079>!210>=KM6ZFL> "
"{045>@5>#0020>~1B---->(-0354>\\0=KM6ZFL>"
"{043>@5>#0022>~1B---->(-0029>\\004>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{043>@5>#0024>~1B---->(-0023>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{042>@5>#0026>~1B---->(-0028>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{042>@5>#0028>~1B---->(-0035>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{041>@5>#0030>~1B---->(-0029>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{040>@5>#0032>~1B---->(-0032>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{040>@5>#0034>~1B---->(-0021>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{039>@5>#0036>~1B---->(-0028>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{038>@5>#0038>~1B---->(-0028>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{038>@5>#0040>~1B---->(-0031>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{037>@5>#0042>~1B---->(-0043>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{037>@5>#0044>~1B---->(-0040>\\000>%04679>^0660>[025>?079>!209>=KM6ZFL>"
"{036>@5>#0046>~1B---->(-0022>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{035>@5>#0048>~1B---->(-0038>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{034>@5>#0050>~1B---->(-0028>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{034>@5>#0052>~1B---->(-0043>\\0=KM6ZFL>"
"{033>@5>#0054>~1B---->(-0033>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{032>@5>#0056>~1B---->(-0041>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{032>@5>#0058>~1B---->(-0026>\\000>%04679>^0660>[025>?079>!207>=KM6ZFL>"
"{031>@5>#0060>~1B---->(-0035>\\000>%04679>^0660>[025>?079>!208>=KM6ZFL>"
"{030>@5>#0062>~1B---->(-0037>\\000>%04679>^0660>[025>?079>!207>=KM6ZFL>"
"{029>@5>#0064>~1B---->(-0032>\\000>%04679>^0660>[025>?079>!207>=KM6ZFL>"
"{029>@5>#0066>~1B---->(-0035>\\000>%04679>^0660>[025>?079>!207>=KM6ZFL>"
"{028>@5>#0068>~1B---->(-0044>\\000>%04679>^0660>[025>?079>!207>=KM6ZFL>"
"{027>@5>#0070>~1B---->(-0045>\\000>%04679>^0660>[025>?079>!207>=KM6ZFL>"
"{026>@5>#0072>~1B---->(-0035>\\000>%04679>^0660>[025>?079>!206>=KM6ZFL>"
"{025>@5>#0074>~1B---->(-0045>\\000>%04679>^0660>[025>?079>!206>=KM6ZFL>"
"{025>@5>#0076>~1B---->(-0043>\\000>%04679>^0660>[025>?079>!206>=KM6ZFL>"
"{024>@5>#0078>~1B---->(-0046>\\000>%04679>^0660>[025>?079>!206>=KM6ZFL>"
"{023>@5>#0080>~1B---->(-0046>\\000>%04679>^0660>[025>?079>!206>=KM6ZFL>"
"{022>@5>#0082>~1B---->(-0037>\\000>%04679>^0660>[025>?079>!206>=KM6ZFL>"
"{021>@5>#0084>~1B---->(-0039>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{020>@5>#0086>~1B---->(-0043>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{020>@5>#0088>~1B---->(-0046>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{019>@5>#0090>~1B---->(-0037>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{018>@5>#0092>~1B---->(-0046>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{017>@5>#0094>~1B---->(-0035>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{016>@5>#0096>~1B---->(-0046>\\000>%04679>^0660>[025>?079>!205>=KM6ZFL>"
"{016>@5>#0098>~1B---->(-0032>\\000>%04679>^0660>[025>?079>!204>=KM6ZFL>"
"{015>@5>#0100>~1B---->(-0043>\\000>%04679>^0660>[025>?079>!204>=KM6ZFL>"
"{014>@5>#0102>~1B---->(-0037>\\000>%04679>^0660>[025>?079>!204>=KM6ZFL>"
"{013>@5>#0104>~1B---->(-0033>\\000>%04679>^0660>[025>?079>!204>=KM6ZFL>"
"{012>@5>#0106>~1B---->(-0026>\\000>%04679>^0660>[025>?079>!204>=KM6ZFL>"
"{012>@5>#0108>~1B---->(-0029>\\000>%04679>^0660>[025>?079>!204>=KM6ZFL>"
"{011>@5>#0110>~1B---->(-0042>\\000>%04679>^0660>[025>?079>!203>=KM6ZFL>"
"{010>@5>#0112>~1B---->(-0041>\\000>%04679>^0660>[025>?079>!203>=KM6ZFL>"
"{009>@5>#0114>~1B---->(-0037>\\000>%04679>^0660>[025>?079>!203>=KM6ZFL>"
"{009>@5>#0116>~1B---->(-0040>\\000>%04679>^0660>[025>?079>!203>=KM6ZFL>"
"{008>@5>#0118>~1B---->(-0043>\\000>%04679>^0660>[025>?079>!202>=KM6ZFL>";
char *p = str;
EggtimerElement eggt_packet;
WhiteVestsPacket wv_packet;
memset(&wv_packet, 0, sizeof(WhiteVestsPacket));
for (; *p; p++){
if(decode_eggtimer_data(*p, &eggt_packet) && buildPacket(&eggt_packet, &wv_packet)){
sendPacket(&wv_packet);
delay(2000);
}
}
return true;
} }
void setup() { void setup() {
Serial.begin(9600); Serial.begin(9600);
Serial.flush();
while (!Serial); while (!Serial);
Serial.println();
Serial.println("Ready!");
test(); test();
} }