mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-27 16:43:59 +08:00
418 lines
13 KiB
Go
418 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math"
|
|
"time"
|
|
|
|
"capnproto.org/go/capnp/v3"
|
|
"pfeifer.dev/mapd/cereal"
|
|
"pfeifer.dev/mapd/cereal/car"
|
|
"pfeifer.dev/mapd/cereal/custom"
|
|
"pfeifer.dev/mapd/cereal/log"
|
|
"pfeifer.dev/mapd/maps"
|
|
m "pfeifer.dev/mapd/math"
|
|
ms "pfeifer.dev/mapd/settings"
|
|
)
|
|
|
|
const mapLoadRetryDelay = time.Second
|
|
|
|
type MapLoader func(m.Position) (maps.Offline, error)
|
|
|
|
type State struct {
|
|
Publisher *cereal.Publisher[custom.MapdOut]
|
|
ShadowOnly bool
|
|
SnapshotID string
|
|
Data maps.Offline
|
|
Car CarState
|
|
CurrentWay CurrentWay
|
|
SpeedLimit SpeedLimitState
|
|
NextWays []maps.NextWayResult
|
|
Position m.Position
|
|
Curvatures []m.Curvature
|
|
TargetVelocities []Velocity
|
|
DistanceSinceLastPosition float32
|
|
VisionCurveSpeed float32
|
|
MapCurveSpeed float32
|
|
VisionCurveMA m.MovingAverage
|
|
NextAdvisorySpeed Upcoming[float32]
|
|
NextHazard Upcoming[string]
|
|
RoadStatus custom.MapdOut_SampleStatus
|
|
GpsSource custom.MapdOut_GpsSource
|
|
GpsMonoTime uint64
|
|
ComputedMonoTime uint64
|
|
SourceGeneration uint64
|
|
ProducerSession uint64
|
|
MatchedLimitMps float32
|
|
lastMapLoadAttempt time.Time
|
|
bootNow func() uint64
|
|
// The fix the current road result was computed from. The loop runs at
|
|
// 20 Hz and GPS arrives at 1-10 Hz; a repeated fix has the same answer.
|
|
roadFixMonoTime uint64
|
|
roadFixGeneration uint64
|
|
}
|
|
|
|
func (s *State) Init() {
|
|
s.Car.Init()
|
|
s.VisionCurveMA.Init(20)
|
|
s.NextHazard = NewUpcoming(10, "", checkWayForHazardChange)
|
|
s.NextAdvisorySpeed = NewUpcoming(10, 0, checkWayForAdvisorySpeedChange)
|
|
s.SpeedLimit.Init()
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noGps
|
|
for s.ProducerSession == 0 {
|
|
var bytes [8]byte
|
|
if _, err := rand.Read(bytes[:]); err != nil {
|
|
panic(err)
|
|
}
|
|
s.ProducerSession = binary.LittleEndian.Uint64(bytes[:])
|
|
}
|
|
}
|
|
|
|
func (s *State) RoadMatched() bool {
|
|
return s.RoadStatus == custom.MapdOut_SampleStatus_matchedNoLimit ||
|
|
s.RoadStatus == custom.MapdOut_SampleStatus_matchedLimit
|
|
}
|
|
|
|
func (s *State) clearSpeedLimitEvidence() {
|
|
s.SpeedLimit = SpeedLimitState{}
|
|
s.SpeedLimit.Init()
|
|
if !s.ShadowOnly {
|
|
ms.Settings.ResetSpeedLimitAccepted()
|
|
}
|
|
}
|
|
|
|
func (s *State) clearRoadDerived() {
|
|
s.CurrentWay = CurrentWay{}
|
|
s.NextWays = nil
|
|
s.Curvatures = nil
|
|
s.TargetVelocities = nil
|
|
s.MapCurveSpeed = 0
|
|
s.MatchedLimitMps = 0
|
|
s.NextHazard.Reset()
|
|
s.NextAdvisorySpeed.Reset()
|
|
s.clearSpeedLimitEvidence()
|
|
}
|
|
|
|
func (s *State) ResetRoadEvidence() {
|
|
s.clearRoadDerived()
|
|
s.roadFixMonoTime = 0
|
|
s.Data = maps.Offline{}
|
|
s.Position = m.Position{}
|
|
s.DistanceSinceLastPosition = 0
|
|
s.lastMapLoadAttempt = time.Time{}
|
|
}
|
|
|
|
// expireRoadEvidence drops everything derived from an expired fix but keeps
|
|
// the verified tile. A dense tile can take longer to load than the fix TTL;
|
|
// discarding it here made every following fix reload it, so the loop never
|
|
// published a match (~600 ms per load, ~60 % CPU, in Las Vegas 2026-10-15).
|
|
// Tile currency is still checked every loop by invalidateChangedTile.
|
|
func (s *State) expireRoadEvidence() {
|
|
s.clearRoadDerived()
|
|
s.roadFixMonoTime = 0
|
|
s.Position = m.Position{}
|
|
s.DistanceSinceLastPosition = 0
|
|
}
|
|
|
|
func (s *State) invalidateChangedTile() bool {
|
|
if s.Data.Loaded && !s.Data.SourceCurrentFor(s.SnapshotID) {
|
|
s.ResetRoadEvidence()
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noCoverage
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *State) bootTime() uint64 {
|
|
if s.bootNow != nil {
|
|
return s.bootNow()
|
|
}
|
|
return cereal.GetTime()
|
|
}
|
|
|
|
// ExpireGpsEvidence gates output using the same source-specific TTL as the
|
|
// GPS selector. Source generation and original fix time remain unchanged
|
|
// until the selector reports a transition.
|
|
func (s *State) ExpireGpsEvidence() {
|
|
s.expireGpsEvidenceAt(s.bootTime())
|
|
}
|
|
|
|
func (s *State) expireGpsEvidenceAt(now uint64) {
|
|
if !cereal.GpsFixFreshAt(cereal.GpsSource(s.GpsSource), s.GpsMonoTime, now) && s.RoadStatus != custom.MapdOut_SampleStatus_noGps {
|
|
s.expireRoadEvidence()
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noGps
|
|
}
|
|
}
|
|
|
|
// ProcessGps updates road evidence before any output is serialized. A cached
|
|
// fix may be used across map loops, but its original timestamp is preserved.
|
|
func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader, now time.Time) {
|
|
defer func() {
|
|
s.ComputedMonoTime = s.bootTime()
|
|
s.expireGpsEvidenceAt(s.ComputedMonoTime)
|
|
}()
|
|
s.GpsMonoTime = sample.FixMonoTime
|
|
s.SourceGeneration = sample.SourceGeneration
|
|
s.GpsSource = custom.MapdOut_GpsSource(sample.Source)
|
|
if sample.SourceChanged {
|
|
s.ResetRoadEvidence()
|
|
}
|
|
if !success {
|
|
s.ResetRoadEvidence()
|
|
s.GpsSource = custom.MapdOut_GpsSource_none
|
|
s.GpsMonoTime = 0
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noGps
|
|
return
|
|
}
|
|
// A replaced cached tile cannot lend its old road or accepted limit to
|
|
// either a new fix or another loop using the same fix.
|
|
s.invalidateChangedTile()
|
|
if sample.NewFix || sample.SourceChanged {
|
|
s.DistanceSinceLastPosition = 0
|
|
}
|
|
location := sample.Location
|
|
pos := m.PosFromLocation(location)
|
|
s.Position = pos
|
|
// Unknown CAN direction or speed uncertainty cannot create confident road evidence.
|
|
if (sample.Source == cereal.GpsSourceCar || sample.Source == cereal.GpsSourcePhone) && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
|
|
s.clearRoadDerived()
|
|
s.roadFixMonoTime = 0
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noMatch
|
|
return
|
|
}
|
|
if !sample.NewFix && !sample.SourceChanged && s.Data.Loaded && s.roadFixMonoTime != 0 &&
|
|
s.roadFixMonoTime == sample.FixMonoTime && s.roadFixGeneration == sample.SourceGeneration {
|
|
// Same fix, same verified tile: matching again would scan the tile for
|
|
// an identical answer. Freshness still expires in the deferred gate.
|
|
return
|
|
}
|
|
s.roadFixMonoTime = 0
|
|
box := s.Data.Box()
|
|
if !s.Data.Loaded || !box.PosInside(pos) {
|
|
// Retry missing coverage at most once a second while a fix remains in
|
|
// the same region. A changed source resets the retry timer above.
|
|
if s.Data.Loaded || s.lastMapLoadAttempt.IsZero() || now.Sub(s.lastMapLoadAttempt) >= mapLoadRetryDelay {
|
|
loaded, err := load(pos)
|
|
if err != nil {
|
|
s.Data = maps.Offline{}
|
|
} else {
|
|
s.Data = loaded
|
|
}
|
|
s.lastMapLoadAttempt = now
|
|
}
|
|
}
|
|
box = s.Data.Box()
|
|
if !s.Data.Loaded || !box.PosInside(pos) {
|
|
s.clearRoadDerived()
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noCoverage
|
|
return
|
|
}
|
|
|
|
s.roadFixMonoTime, s.roadFixGeneration = sample.FixMonoTime, sample.SourceGeneration
|
|
way, err := GetCurrentWay(s.CurrentWay, s.NextWays, &s.Data, location)
|
|
if err != nil || way.Way.Nodes.Len() < 2 {
|
|
s.clearRoadDerived()
|
|
if errors.Is(err, ErrAmbiguousWay) {
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_unknown
|
|
} else {
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noMatch
|
|
}
|
|
return
|
|
}
|
|
s.CurrentWay = way
|
|
if !s.ShadowOnly {
|
|
s.NextWays, err = NextWays(location, way, &s.Data, way.OnWay.IsForward)
|
|
if err != nil {
|
|
s.NextWays = nil
|
|
}
|
|
s.Curvatures, err = GetStateCurvatures(s)
|
|
if err != nil {
|
|
s.Curvatures = nil
|
|
s.TargetVelocities = nil
|
|
s.MapCurveSpeed = 0
|
|
} else {
|
|
s.TargetVelocities = GetTargetVelocities(s.Curvatures, s.TargetVelocities)
|
|
}
|
|
} else {
|
|
s.NextWays = nil
|
|
s.Curvatures = nil
|
|
s.TargetVelocities = nil
|
|
s.MapCurveSpeed = 0
|
|
}
|
|
limit := way.EffectiveMaxSpeed()
|
|
if math.IsNaN(limit) || math.IsInf(limit, 0) || limit < 0 || limit > math.MaxFloat32 {
|
|
s.clearRoadDerived()
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_noMatch
|
|
return
|
|
}
|
|
if limit == 0 {
|
|
s.clearSpeedLimitEvidence()
|
|
s.MatchedLimitMps = 0
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_matchedNoLimit
|
|
} else {
|
|
s.MatchedLimitMps = float32(limit)
|
|
s.RoadStatus = custom.MapdOut_SampleStatus_matchedLimit
|
|
}
|
|
}
|
|
|
|
func (s *State) SuggestedSpeed() float32 {
|
|
suggestedSpeed := min(s.Car.VCruise*ms.KPH_TO_MS, ms.MAX_OP_SPEED)
|
|
|
|
if ms.Settings.SpeedLimitControlEnabled || ms.Settings.ExternalSpeedLimitControlEnabled {
|
|
slSuggestedSpeed := s.SpeedLimit.SpeedLimitFinalSuggestion(s.Car.EnableSpeedActive, s.Car.SetSpeedChanging, s.Car.VEgo)
|
|
if suggestedSpeed > slSuggestedSpeed && slSuggestedSpeed > 0 {
|
|
suggestedSpeed = slSuggestedSpeed
|
|
}
|
|
}
|
|
if ms.Settings.VisionCurveSpeedControlEnabled && s.VisionCurveSpeed > 0 && (s.VisionCurveSpeed < suggestedSpeed || suggestedSpeed == 0) && (!ms.Settings.VisionCurveUseEnableSpeed || s.Car.EnableSpeedActive) {
|
|
suggestedSpeed = s.VisionCurveSpeed
|
|
}
|
|
if ms.Settings.MapCurveSpeedControlEnabled && s.MapCurveSpeed > 0 && (s.MapCurveSpeed < suggestedSpeed || suggestedSpeed == 0) && (!ms.Settings.MapCurveUseEnableSpeed || s.Car.EnableSpeedActive) {
|
|
suggestedSpeed = s.MapCurveSpeed
|
|
}
|
|
if suggestedSpeed < 0 {
|
|
suggestedSpeed = 0
|
|
}
|
|
return suggestedSpeed
|
|
}
|
|
|
|
func (s *State) UpdateCarState(carData car.CarState) {
|
|
s.Car.Update(carData)
|
|
s.DistanceSinceLastPosition += float32(s.Car.UpdateTime.DiffMA.Estimate) * s.Car.VEgo
|
|
}
|
|
|
|
func (s *State) UpdateRoadDependentCar() {
|
|
s.SpeedLimit.NextLimit.Update(s)
|
|
s.NextAdvisorySpeed.Update(s)
|
|
s.NextHazard.Update(s)
|
|
if s.RoadStatus == custom.MapdOut_SampleStatus_matchedLimit {
|
|
s.SpeedLimit.Update(s.CurrentWay, s.Car)
|
|
}
|
|
}
|
|
|
|
func (s *State) Send() error {
|
|
msg, err := s.BuildMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// mapdOut is sent from this loop, so a valid sample cannot wait in an
|
|
// asynchronous queue after its final freshness check.
|
|
return s.Publisher.Send(msg)
|
|
}
|
|
|
|
func (s *State) BuildMessage() (*capnp.Message, error) {
|
|
now := s.bootTime()
|
|
s.expireGpsEvidenceAt(now)
|
|
s.invalidateChangedTile()
|
|
msg, err := s.buildMessageAt(now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Text/list serialization can be expensive with a large selected way.
|
|
// If it crossed the source TTL, discard the valid message and encode an
|
|
// invalid one with neutral road fields.
|
|
endNow := s.bootTime()
|
|
if s.invalidateChangedTile() {
|
|
return s.buildMessageAt(endNow)
|
|
}
|
|
if s.RoadStatus != custom.MapdOut_SampleStatus_noGps && !cereal.GpsFixFreshAt(cereal.GpsSource(s.GpsSource), s.GpsMonoTime, endNow) {
|
|
s.expireGpsEvidenceAt(endNow)
|
|
return s.buildMessageAt(endNow)
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
func (s *State) buildMessageAt(now uint64) (*capnp.Message, error) {
|
|
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
event, err := log.NewRootEvent(seg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
event.SetLogMonoTime(now)
|
|
event.SetValid(s.RoadMatched())
|
|
output, err := event.NewMapdOut()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
output.SetSampleVersion(2)
|
|
output.SetRoadStatus(s.RoadStatus)
|
|
output.SetGpsSource(s.GpsSource)
|
|
output.SetGpsMonoTime(s.GpsMonoTime)
|
|
output.SetComputedMonoTime(s.ComputedMonoTime)
|
|
output.SetSourceGeneration(s.SourceGeneration)
|
|
output.SetProducerSession(s.ProducerSession)
|
|
if !s.RoadMatched() {
|
|
return msg, nil
|
|
}
|
|
id := s.CurrentWay.Way.Id()
|
|
output.SetWayId(id)
|
|
|
|
name := s.CurrentWay.Way.WayName()
|
|
output.SetWayName(name)
|
|
|
|
ref := s.CurrentWay.Way.WayRef()
|
|
output.SetWayRef(ref)
|
|
|
|
output.SetRoadName(s.CurrentWay.Way.Name())
|
|
|
|
output.SetSpeedLimit(s.MatchedLimitMps)
|
|
|
|
output.SetConditionalSpeedLimit(s.CurrentWay.ConditionalMaxSpeedRaw())
|
|
|
|
if !s.ShadowOnly {
|
|
output.SetSpeedLimitSuggestedSpeed(s.SpeedLimit.Suggestion.Value)
|
|
output.SetNextSpeedLimit(s.SpeedLimit.NextLimit.Value)
|
|
output.SetNextSpeedLimitDistance(s.SpeedLimit.NextLimit.Distance)
|
|
}
|
|
|
|
hazard := s.CurrentWay.Way.Hazard()
|
|
output.SetHazard(hazard)
|
|
|
|
if !s.ShadowOnly {
|
|
output.SetNextHazard(s.NextHazard.Value)
|
|
output.SetNextHazardDistance(s.NextHazard.Distance)
|
|
}
|
|
|
|
advisorySpeed := s.CurrentWay.Way.AdvisorySpeed()
|
|
output.SetAdvisorySpeed(float32(advisorySpeed))
|
|
|
|
if !s.ShadowOnly {
|
|
output.SetNextAdvisorySpeed(s.NextAdvisorySpeed.Value)
|
|
output.SetNextAdvisorySpeedDistance(s.NextAdvisorySpeed.Distance)
|
|
}
|
|
|
|
oneWay := s.CurrentWay.Way.OneWay()
|
|
output.SetOneWay(oneWay)
|
|
|
|
lanes := s.CurrentWay.Way.Lanes()
|
|
output.SetLanes(uint8(lanes))
|
|
|
|
output.SetTileLoaded(s.Data.Loaded)
|
|
|
|
output.SetRoadContext(custom.RoadContext(s.CurrentWay.Way.Context()))
|
|
output.SetHighwayClass(custom.HighwayClass(s.CurrentWay.Way.HighwayClass()))
|
|
output.SetEstimatedRoadWidth(s.CurrentWay.Way.Width())
|
|
if !s.ShadowOnly {
|
|
output.SetVisionCurveSpeed(s.VisionCurveSpeed)
|
|
output.SetMapCurveSpeed(s.MapCurveSpeed)
|
|
output.SetSuggestedSpeed(s.SuggestedSpeed())
|
|
}
|
|
output.SetDistanceFromWayCenter(float32(s.CurrentWay.OnWay.Distance.Distance))
|
|
|
|
output.SetWaySelectionType(s.CurrentWay.SelectionType)
|
|
if !s.ShadowOnly {
|
|
output.SetSpeedLimitAccepted(ms.Settings.SpeedLimitAccepted())
|
|
}
|
|
|
|
if s.RoadStatus == custom.MapdOut_SampleStatus_matchedNoLimit {
|
|
output.SetSpeedLimitSuggestedSpeed(0)
|
|
output.SetSpeedLimitAccepted(false)
|
|
}
|
|
return msg, nil
|
|
}
|