diff --git a/.github/workflows/gr26.yml b/.github/workflows/gr26.yml new file mode 100644 index 0000000..ab7e603 --- /dev/null +++ b/.github/workflows/gr26.yml @@ -0,0 +1,153 @@ +name: gr26 +run-name: Triggered by ${{ github.event_name }} to ${{ github.ref }} by @${{ github.actor }} + +on: + push: + branches: + - "**" + tags: + - "**" + +jobs: + build: + runs-on: ${{ matrix.runner }} + name: Build ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-24.04 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate platform pair + id: platform + run: | + platform=${{ matrix.platform }} + echo "pair=${platform//\//-}" >> $GITHUB_OUTPUT + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v5 + with: + context: gr26 + platforms: ${{ matrix.platform }} + outputs: type=image,name=ghcr.io/gaucho-racing/mapache/gr26,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=build-${{ steps.platform.outputs.pair }} + cache-to: type=gha,scope=build-${{ steps.platform.outputs.pair }},mode=max + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ steps.platform.outputs.pair }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + name: Merge manifests + needs: build + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if this commit has a release tag + id: release + run: | + tag=$(git tag --points-at HEAD | grep '^v' | head -n1) + if [ -n "$tag" ]; then + echo "Found tag: $tag" + if gh release view "$tag" --json tagName > /dev/null 2>&1; then + echo "release_tag=$tag" >> $GITHUB_OUTPUT + echo "is_release=true" >> $GITHUB_OUTPUT + exit 0 + fi + fi + echo "is_release=false" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate tag list + id: tags + shell: bash + run: | + TAGS="type=sha" + + if [ "${GITHUB_REF_TYPE}" = "branch" ] && [ "${GITHUB_REF_NAME}" = "main" ]; then + TAGS="${TAGS}\ntype=raw,value=latest" + fi + + if [ "${{ steps.release.outputs.is_release }}" = "true" ]; then + CLEAN_TAG=$(echo "${{ steps.release.outputs.release_tag }}" | sed 's/^v//') + TAGS="${TAGS}\ntype=raw,value=${CLEAN_TAG}" + fi + + echo -e "tags<> $GITHUB_OUTPUT + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/gaucho-racing/mapache/gr26 + tags: ${{ steps.tags.outputs.tags }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/gaucho-racing/mapache/gr26@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ghcr.io/gaucho-racing/mapache/gr26:${{ steps.meta.outputs.version }} diff --git a/.gitignore b/.gitignore index d97c9a7..e299bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ !example.env *.env +tmp/ # === K8S === k8s/db-config.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml index 11361a9..76776e9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -100,32 +100,39 @@ services: RINCON_PASSWORD: "admin" RINCON_ENDPOINT: "http://rincon:10311" - # gr25: - # image: gauchoracing/mp_gr25:latest - # container_name: gr25 - # restart: unless-stopped - # depends_on: - # - nanomq - # - rincon - # ports: - # - "7005:7005" - # environment: - # ENV: "DEV" - # PORT: "7005" - # SERVICE_ENDPOINT: "http://gr25:7005" - # SERVICE_HEALTH_CHECK: "http://gr25:7005/gr25/ping" - # MQTT_HOST: "nanomq" - # MQTT_PORT: "1883" - # MQTT_USER: "gr25" - # MQTT_PASSWORD: "gr25" - # DATABASE_HOST: ${DATABASE_HOST} - # DATABASE_PORT: ${DATABASE_PORT} - # DATABASE_NAME: ${DATABASE_NAME} - # DATABASE_USER: ${DATABASE_USER} - # DATABASE_PASSWORD: ${DATABASE_PASSWORD} - # RINCON_USER: "admin" - # RINCON_PASSWORD: "admin" - # RINCON_ENDPOINT: "http://rincon:10311" + gr26: + build: + context: . + dockerfile: gr26/Dockerfile.dev + container_name: gr26 + restart: unless-stopped + depends_on: + - rincon + - nanomq + ports: + - "7005:7005" + volumes: + - ./gr26:/app/gr26 + - ./mapache-go:/app/mapache-go + - gr26_go_cache:/go + environment: + ENV: "DEV" + PORT: "7005" + SERVICE_ENDPOINT: "http://gr26:7005" + SERVICE_HEALTH_CHECK: "http://gr26:7005/gr26/ping" + MQTT_HOST: "nanomq" + MQTT_PORT: "1883" + MQTT_USER: "gr26" + MQTT_PASSWORD: "gr26" + DATABASE_HOST: ${DATABASE_HOST} + DATABASE_PORT: ${DATABASE_PORT} + DATABASE_NAME: ${DATABASE_NAME} + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + RINCON_USER: "admin" + RINCON_PASSWORD: "admin" + RINCON_ENDPOINT: "http://rincon:10311" + SKIP_AUTH_CHECK: "true" # query: # image: gauchoracing/mp_query:latest @@ -172,4 +179,5 @@ services: volumes: pgdata: auth_go_cache: - vehicle_go_cache: \ No newline at end of file + vehicle_go_cache: + gr26_go_cache: \ No newline at end of file diff --git a/gr26/.air.toml b/gr26/.air.toml new file mode 100644 index 0000000..3d5c83f --- /dev/null +++ b/gr26/.air.toml @@ -0,0 +1,21 @@ +root = "." +tmp_dir = "tmp" + +[build] + bin = "./tmp/main" + cmd = "go mod tidy && go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["tmp", "vendor"] + exclude_regex = ["_test.go"] + include_ext = ["go", "toml"] + kill_delay = "0s" + send_interrupt = false + poll = true + poll_interval = 500 + stop_on_error = true + +[log] + time = false + +[misc] + clean_on_exit = true diff --git a/gr26/.gitignore b/gr26/.gitignore new file mode 100644 index 0000000..f29bf02 --- /dev/null +++ b/gr26/.gitignore @@ -0,0 +1,168 @@ +.env + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage.html + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/ + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/gr26/Dockerfile b/gr26/Dockerfile new file mode 100644 index 0000000..debc391 --- /dev/null +++ b/gr26/Dockerfile @@ -0,0 +1,29 @@ +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder + +RUN apk --no-cache add ca-certificates +RUN apk add --no-cache tzdata + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download + +COPY . ./ +ARG TARGETOS +ARG TARGETARCH +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /gr26 + +## +## Deploy +## +FROM alpine:3.21 + +WORKDIR / + +COPY --from=builder /gr26 /gr26 + +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +ENV TZ=America/Los_Angeles + +ENTRYPOINT ["/gr26"] diff --git a/gr26/Dockerfile.dev b/gr26/Dockerfile.dev new file mode 100644 index 0000000..64261b4 --- /dev/null +++ b/gr26/Dockerfile.dev @@ -0,0 +1,7 @@ +FROM golang:1.26-alpine + +RUN go install github.com/air-verse/air@latest + +WORKDIR /app/gr26 + +CMD ["air", "-c", ".air.toml"] diff --git a/gr26/api/api.go b/gr26/api/api.go new file mode 100644 index 0000000..bca039b --- /dev/null +++ b/gr26/api/api.go @@ -0,0 +1,39 @@ +package api + +import ( + "time" + + "github.com/gaucho-racing/mapache/gr26/config" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func Run() { + api := InitializeRouter() + InitializeRoutes(api) + err := api.Run(":" + config.Port) + if err != nil { + logger.SugarLogger.Fatalf("Failed to start server: %v", err) + } +} + +func InitializeRouter() *gin.Engine { + if config.IsProduction() { + gin.SetMode(gin.ReleaseMode) + } + r := gin.Default() + r.Use(cors.New(cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + MaxAge: 12 * time.Hour, + AllowCredentials: true, + })) + return r +} + +func InitializeRoutes(router *gin.Engine) { + router.GET("/gr26/ping", Ping) + router.GET("/gr26/live", GetLatestSignalWebSocket) +} diff --git a/gr26/api/ping.go b/gr26/api/ping.go new file mode 100644 index 0000000..71857b6 --- /dev/null +++ b/gr26/api/ping.go @@ -0,0 +1,12 @@ +package api + +import ( + "net/http" + + "github.com/gaucho-racing/mapache/gr26/config" + "github.com/gin-gonic/gin" +) + +func Ping(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": config.Service.FormattedNameWithVersion() + " is online!"}) +} diff --git a/gr26/api/signal.go b/gr26/api/signal.go new file mode 100644 index 0000000..fc7aa73 --- /dev/null +++ b/gr26/api/signal.go @@ -0,0 +1,69 @@ +package api + +import ( + "net/http" + "strconv" + "strings" + + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + "github.com/gaucho-racing/mapache/gr26/service" + + mapache "github.com/gaucho-racing/mapache/mapache-go" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func GetLatestSignalWebSocket(c *gin.Context) { + vehicleID := c.Query("vehicle_id") + signals := strings.Split(c.Query("signals"), ",") + + if vehicleID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "vehicle_id is required"}) + return + } + + if len(signals) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "signals are required"}) + return + } + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + defer conn.Close() + + client := &service.Client{ + Conn: conn, + Send: make(chan mapache.Signal, 64), + } + + service.Hub.Subscribe(vehicleID, signals, client) + + go func() { + defer close(client.Send) + defer service.Hub.Unsubscribe(vehicleID, signals, client) + for { + messageType, p, err := conn.ReadMessage() + if err != nil { + logger.SugarLogger.Errorln("[WS - gr26/live] error while reading message\n", err.Error()) + return + } + logger.SugarLogger.Infoln("[WS - gr26/live] Received message ("+strconv.Itoa(messageType)+"): ", string(p)) + } + }() + + for signal := range client.Send { + if err := conn.WriteJSON(signal); err != nil { + break + } + } +} diff --git a/gr26/config/banner.go b/gr26/config/banner.go new file mode 100644 index 0000000..ab55908 --- /dev/null +++ b/gr26/config/banner.go @@ -0,0 +1,20 @@ +package config + +import "github.com/fatih/color" + +var Banner = ` +███╗ ███╗ █████╗ ██████╗ █████╗ ██████╗██╗ ██╗███████╗ +████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔════╝██║ ██║██╔════╝ +██╔████╔██║███████║██████╔╝███████║██║ ███████║█████╗ +██║╚██╔╝██║██╔══██║██╔═══╝ ██╔══██║██║ ██╔══██║██╔══╝ +██║ ╚═╝ ██║██║ ██║██║ ██║ ██║╚██████╗██║ ██║███████╗ +╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ +` + +func PrintStartupBanner() { + banner := color.New(color.Bold, color.FgHiMagenta).PrintlnFunc() + banner(Banner) + version := color.New(color.Bold, color.FgMagenta).PrintlnFunc() + version("Running " + Service.FormattedNameWithVersion() + " [ENV: " + Env + "]") + println() +} diff --git a/gr26/config/config.go b/gr26/config/config.go new file mode 100644 index 0000000..8620616 --- /dev/null +++ b/gr26/config/config.go @@ -0,0 +1,42 @@ +package config + +import ( + "os" + + "github.com/bk1031/rincon-go/v2" +) + +var Service rincon.Service = rincon.Service{ + Name: "GR26", + Version: "1.0.0", + Endpoint: os.Getenv("SERVICE_ENDPOINT"), + HealthCheck: os.Getenv("SERVICE_HEALTH_CHECK"), +} + +var Routes = []rincon.Route{ + { + Route: "/gr26/**", + Method: "*", + }, +} + +var SkipAuthCheck = os.Getenv("SKIP_AUTH_CHECK") == "true" +var VehicleUploadKeyCacheTTL = os.Getenv("VEHICLE_UPLOAD_KEY_CACHE_TTL") + +var Env = os.Getenv("ENV") +var Port = os.Getenv("PORT") + +var DatabaseHost = os.Getenv("DATABASE_HOST") +var DatabasePort = os.Getenv("DATABASE_PORT") +var DatabaseUser = os.Getenv("DATABASE_USER") +var DatabasePassword = os.Getenv("DATABASE_PASSWORD") +var DatabaseName = os.Getenv("DATABASE_NAME") + +var MQTTHost = os.Getenv("MQTT_HOST") +var MQTTPort = os.Getenv("MQTT_PORT") +var MQTTUser = os.Getenv("MQTT_USER") +var MQTTPassword = os.Getenv("MQTT_PASSWORD") + +func IsProduction() bool { + return Env == "PROD" +} diff --git a/gr26/config/verify.go b/gr26/config/verify.go new file mode 100644 index 0000000..fcebe9b --- /dev/null +++ b/gr26/config/verify.go @@ -0,0 +1,46 @@ +package config + +import "github.com/gaucho-racing/mapache/gr26/pkg/logger" + +func Verify() { + if Env == "" { + Env = "PROD" + logger.SugarLogger.Infof("ENV is not set, defaulting to %s", Env) + } + if Port == "" { + Port = "7005" + logger.SugarLogger.Infof("PORT is not set, defaulting to %s", Port) + } + if DatabaseHost == "" { + DatabaseHost = "localhost" + logger.SugarLogger.Infof("DATABASE_HOST is not set, defaulting to %s", DatabaseHost) + } + if DatabasePort == "" { + DatabasePort = "5432" + logger.SugarLogger.Infof("DATABASE_PORT is not set, defaulting to %s", DatabasePort) + } + if DatabaseUser == "" { + DatabaseUser = "postgres" + logger.SugarLogger.Infof("DATABASE_USER is not set, defaulting to %s", DatabaseUser) + } + if DatabasePassword == "" { + DatabasePassword = "password" + logger.SugarLogger.Infof("DATABASE_PASSWORD is not set, defaulting to %s", DatabasePassword) + } + if DatabaseName == "" { + DatabaseName = "mapache" + logger.SugarLogger.Infof("DATABASE_NAME is not set, defaulting to %s", DatabaseName) + } + if MQTTHost == "" { + MQTTHost = "localhost" + logger.SugarLogger.Infof("MQTT_HOST is not set, defaulting to %s", MQTTHost) + } + if MQTTPort == "" { + MQTTPort = "1883" + logger.SugarLogger.Infof("MQTT_PORT is not set, defaulting to %s", MQTTPort) + } + if VehicleUploadKeyCacheTTL == "" { + VehicleUploadKeyCacheTTL = "600" + logger.SugarLogger.Infof("VEHICLE_UPLOAD_KEY_CACHE_TTL is not set, defaulting to %s", VehicleUploadKeyCacheTTL) + } +} diff --git a/gr26/database/db.go b/gr26/database/db.go new file mode 100644 index 0000000..f5f9b11 --- /dev/null +++ b/gr26/database/db.go @@ -0,0 +1,37 @@ +package database + +import ( + "fmt" + "time" + + "github.com/gaucho-racing/mapache/gr26/config" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + "github.com/gaucho-racing/mapache/mapache-go" + + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var DB *gorm.DB + +var dbRetries = 0 + +func Init() { + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", config.DatabaseHost, config.DatabaseUser, config.DatabasePassword, config.DatabaseName, config.DatabasePort) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + if dbRetries < 5 { + dbRetries++ + logger.SugarLogger.Errorln("failed to connect database, retrying in 5s... ") + time.Sleep(time.Second * 5) + Init() + } else { + logger.SugarLogger.Fatalf("failed to connect database after 5 attempts") + } + } else { + logger.SugarLogger.Infoln("Connected to database") + db.AutoMigrate(&mapache.Signal{}, &mapache.Ping{}) + logger.SugarLogger.Infoln("AutoMigration complete") + DB = db + } +} diff --git a/gr26/go.mod b/gr26/go.mod new file mode 100644 index 0000000..fbab8dc --- /dev/null +++ b/gr26/go.mod @@ -0,0 +1,60 @@ +module github.com/gaucho-racing/mapache/gr26 + +go 1.26 + +require ( + github.com/bk1031/rincon-go/v2 v2.0.0 + github.com/eclipse/paho.mqtt.golang v1.5.0 + github.com/fatih/color v1.18.0 + github.com/gaucho-racing/mapache/mapache-go v0.0.0 + github.com/gaucho-racing/ulid-go v1.1.0 + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.12.0 + github.com/gorilla/websocket v1.5.3 + go.uber.org/zap v1.27.1 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) + +replace github.com/gaucho-racing/mapache/mapache-go => ../mapache-go diff --git a/gr26/go.sum b/gr26/go.sum new file mode 100644 index 0000000..510b998 --- /dev/null +++ b/gr26/go.sum @@ -0,0 +1,130 @@ +github.com/bk1031/rincon-go/v2 v2.0.0 h1:nmDHQNZI/AFfW+ZGTGoxpNPrv3OYXQ09anX+fCoiQsQ= +github.com/bk1031/rincon-go/v2 v2.0.0/go.mod h1:287Zc8PvUNnJuAwpt9XVuYUL8k4wrXg3Fa3L0KEmAB4= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gaucho-racing/ulid-go v1.1.0 h1:x00XM8EjlegfhlLYIob+U8ba5iX0gDRUr8mgBsjCunk= +github.com/gaucho-racing/ulid-go v1.1.0/go.mod h1:HwqoC27UtvXHrmhTO7K2GnXZ1VAeR6tg6EjrSEP5JUU= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/gr26/main.go b/gr26/main.go new file mode 100644 index 0000000..3b485c7 --- /dev/null +++ b/gr26/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/gaucho-racing/mapache/gr26/api" + "github.com/gaucho-racing/mapache/gr26/config" + "github.com/gaucho-racing/mapache/gr26/database" + "github.com/gaucho-racing/mapache/gr26/mqtt" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + "github.com/gaucho-racing/mapache/gr26/pkg/rincon" + "github.com/gaucho-racing/mapache/gr26/service" +) + +func main() { + logger.Init(config.IsProduction()) + defer logger.Logger.Sync() + + config.Verify() + config.PrintStartupBanner() + rincon.Init(&config.Service, &config.Routes) + database.Init() + mqtt.Init() + service.SubscribeTopics() + + api.Run() +} diff --git a/gr26/model/gps.go b/gr26/model/gps.go new file mode 100644 index 0000000..d94db26 --- /dev/null +++ b/gr26/model/gps.go @@ -0,0 +1,172 @@ +package model + +import ( + "math" + + mp "github.com/gaucho-racing/mapache/mapache-go" +) + +var GPSLatitude = mp.Message{ + mp.NewField("gps_latitude", 8, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gps_latitude", + Value: math.Float64frombits(uint64(f.Value)), + RawValue: f.Value, + }) + return signals + }), +} + +var GPSLongitude = mp.Message{ + mp.NewField("gps_longitude", 8, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gps_longitude", + Value: math.Float64frombits(uint64(f.Value)), + RawValue: f.Value, + }) + return signals + }), +} + +var GPSAltitude = mp.Message{ + mp.NewField("gps_altitude", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gps_altitude", + Value: float64(math.Float32frombits(uint32(f.Value))), + RawValue: f.Value, + }) + return signals + }), + mp.NewField("gps_status", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gps_altitude", + Value: float64(f.Value), + RawValue: f.Value, + }) + return signals + }), +} + +var GPSPx = mp.Message{ + mp.NewField("X_Theta", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "x_theta", + Value: (float64(f.Value) * 0.001), + RawValue: f.Value, + }) + return signals + }), + mp.NewField("X_Acc", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "x_acc", + Value: (float64(f.Value) * 0.01), + RawValue: f.Value, + }) + return signals + }), + mp.NewField("Status", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "status", + Value: float64(f.Value), + RawValue: f.Value, + }) + return signals + }), +} + +var GPSQy = mp.Message{ + mp.NewField("Y_Theta", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "y_theta", + Value: float64(f.Value) * 0.001, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("Y_Acc", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "y_acc", + Value: float64(f.Value) * 0.01, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("Status", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "status", + Value: float64(f.Value), + RawValue: f.Value, + }) + return signals + }), +} + +var GPSRz = mp.Message{ + mp.NewField("Z_Theta", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "z_theta", + Value: float64(f.Value) * 0.001, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("Z_Acc", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "z_acc", + Value: float64(f.Value) * 0.01, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("Status", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "status", + Value: float64(f.Value), + RawValue: f.Value, + }) + return signals + }), +} + +var DGPS_UVW = mp.Message{ + mp.NewField("DGPS_u", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "dgps_u", + Value: float64(f.Value) * 0.01, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("DGPS_v", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "dgps_v", + Value: float64(f.Value) * 0.01, + RawValue: f.Value, + }) + return signals + }), + mp.NewField("DGPS_w", 2, mp.Signed, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "dgps_w", + Value: float64(f.Value) * 0.01, + RawValue: f.Value, + }) + return signals + }), +} diff --git a/gr26/model/message.go b/gr26/model/message.go new file mode 100644 index 0000000..ff2c802 --- /dev/null +++ b/gr26/model/message.go @@ -0,0 +1,21 @@ +package model + +import mp "github.com/gaucho-racing/mapache/mapache-go" + +var messageMap = map[int]mp.Message{ + 0x02A: TCMResourceUtil, + 0x030: DGPS_UVW, + 0x031: GPSLatitude, + 0x032: GPSLongitude, + 0x033: GPSAltitude, + 0x034: GPSPx, + 0x035: GPSQy, + 0x036: GPSRz, +} + +func GetMessage(id int) mp.Message { + if msg, ok := messageMap[id]; ok { + return msg + } + return nil +} diff --git a/gr26/model/tcm.go b/gr26/model/tcm.go new file mode 100644 index 0000000..9b1572d --- /dev/null +++ b/gr26/model/tcm.go @@ -0,0 +1,240 @@ +package model + +import mp "github.com/gaucho-racing/mapache/mapache-go" + +var TCMResourceUtil = mp.Message{ + mp.NewField("cpu0_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu0_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu0_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu0_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu1_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu1_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu1_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu1_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu2_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu2_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu2_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu2_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu3_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu3_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu3_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu3_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu4_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu4_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu4_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu4_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu5_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu5_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu5_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu5_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu_total_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu_total_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("ram_total", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ram_total", + Value: float64(f.Value), // MB + RawValue: f.Value, + }) + return signals + }), + mp.NewField("ram_used", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ram_used", + Value: float64(f.Value), // MB + RawValue: f.Value, + }) + return signals + }), + mp.NewField("ram_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "ram_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("gpu_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gpu_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("gpu_freq", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gpu_freq", + Value: float64(f.Value), // MHz + RawValue: f.Value, + }) + return signals + }), + mp.NewField("disk_total", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "disk_total", + Value: float64(f.Value), // GB + RawValue: f.Value, + }) + return signals + }), + mp.NewField("disk_used", 4, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "disk_used", + Value: float64(f.Value), // GB + RawValue: f.Value, + }) + return signals + }), + mp.NewField("disk_util", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "disk_util", + Value: float64(f.Value), // Percentage + RawValue: f.Value, + }) + return signals + }), + mp.NewField("cpu_temp", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "cpu_temp", + Value: float64(f.Value), // Celsius + RawValue: f.Value, + }) + return signals + }), + mp.NewField("gpu_temp", 1, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "gpu_temp", + Value: float64(f.Value), // Celsius + RawValue: f.Value, + }) + return signals + }), + mp.NewField("voltage_draw", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "voltage_draw", + Value: float64(f.Value) / 1000, // Volts (assuming raw is in millivolts) + RawValue: f.Value, + }) + return signals + }), + mp.NewField("current_draw", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "current_draw", + Value: float64(f.Value) / 1000, // Amps (assuming raw is in milliamps) + RawValue: f.Value, + }) + return signals + }), + mp.NewField("power_draw", 2, mp.Unsigned, mp.LittleEndian, func(f mp.Field) []mp.Signal { + signals := []mp.Signal{} + signals = append(signals, mp.Signal{ + Name: "power_draw", + Value: float64(f.Value) / 1000, // Watts (assuming raw is in milliWatts) + RawValue: f.Value, + }) + return signals + }), +} diff --git a/gr26/mqtt/mqtt.go b/gr26/mqtt/mqtt.go new file mode 100644 index 0000000..5b55b3e --- /dev/null +++ b/gr26/mqtt/mqtt.go @@ -0,0 +1,28 @@ +package mqtt + +import ( + "fmt" + + "github.com/gaucho-racing/mapache/gr26/config" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + + mq "github.com/eclipse/paho.mqtt.golang" +) + +var Client mq.Client + +func Init() { + opts := mq.NewClientOptions() + opts.AddBroker(fmt.Sprintf("tcp://%s:%s", config.MQTTHost, config.MQTTPort)) + opts.SetUsername(config.MQTTUser) + opts.SetPassword(config.MQTTPassword) + opts.SetCleanSession(true) + opts.SetOrderMatters(false) + opts.SetAutoReconnect(true) + opts.SetClientID(fmt.Sprintf("%s-%d", config.Service.Name, config.Service.ID)) + Client = mq.NewClient(opts) + if token := Client.Connect(); token.Wait() && token.Error() != nil { + logger.SugarLogger.Fatalln("[MQ] Failed to connect to MQTT", token.Error()) + } + logger.SugarLogger.Infoln("[MQ] Connected to MQTT broker") +} diff --git a/gr26/pkg/logger/logger.go b/gr26/pkg/logger/logger.go new file mode 100644 index 0000000..2722d06 --- /dev/null +++ b/gr26/pkg/logger/logger.go @@ -0,0 +1,16 @@ +package logger + +import ( + "go.uber.org/zap" +) + +var Logger *zap.Logger +var SugarLogger *zap.SugaredLogger + +func Init(production bool) { + Logger = zap.Must(zap.NewProduction()) + if !production { + Logger = zap.Must(zap.NewDevelopment(zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))) + } + SugarLogger = Logger.Sugar() +} diff --git a/gr26/pkg/rincon/rincon.go b/gr26/pkg/rincon/rincon.go new file mode 100644 index 0000000..d99599b --- /dev/null +++ b/gr26/pkg/rincon/rincon.go @@ -0,0 +1,60 @@ +package rincon + +import ( + "os" + "time" + + "github.com/bk1031/rincon-go/v2" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" +) + +var RinconClient *rincon.Client +var RinconUser = os.Getenv("RINCON_USER") +var RinconPassword = os.Getenv("RINCON_PASSWORD") +var RinconEndpoint = os.Getenv("RINCON_ENDPOINT") + +func Init(service *rincon.Service, routes *[]rincon.Route) { + if RinconUser == "" || RinconPassword == "" || RinconEndpoint == "" { + logger.SugarLogger.Errorln("Rincon user, password, or endpoint is not set") + return + } + client := createClient(RinconEndpoint, RinconUser, RinconPassword) + if client == nil { + return + } + id, err := client.Register(*service, *routes) + if err != nil { + logger.SugarLogger.Fatalf("Failed to register service with Rincon: %v", err) + return + } + logger.SugarLogger.Infof("Registered service with ID: %d", id) + RinconClient = client + *service = *client.Service() +} + +func createClient(endpoint string, user string, password string) *rincon.Client { + rinconRetries := 0 + for rinconRetries < 5 { + client, err := rincon.NewClient(rincon.Config{ + BaseURL: endpoint, + HeartbeatMode: rincon.ServerHeartbeat, + HeartbeatInterval: 60, + AuthUser: user, + AuthPassword: password, + }) + if err != nil { + if rinconRetries < 5 { + logger.SugarLogger.Errorf("Failed to create Rincon client with %s: %v, retrying in 5s...", endpoint, err) + rinconRetries++ + time.Sleep(time.Second * 5) + } else { + logger.SugarLogger.Errorln("Failed to create Rincon client after 5 attempts") + return nil + } + } else { + logger.SugarLogger.Infof("Created Rincon client with endpoint %s", endpoint) + return client + } + } + return nil +} diff --git a/gr26/service/message.go b/gr26/service/message.go new file mode 100644 index 0000000..f0a124e --- /dev/null +++ b/gr26/service/message.go @@ -0,0 +1,95 @@ +package service + +import ( + "encoding/binary" + "fmt" + "strconv" + "strings" + "time" + + "github.com/gaucho-racing/mapache/gr26/model" + "github.com/gaucho-racing/mapache/gr26/mqtt" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + + mq "github.com/eclipse/paho.mqtt.golang" +) + +func SubscribeTopics() { + mqtt.Client.Subscribe("gr26/#", 0, func(client mq.Client, msg mq.Message) { + topic := msg.Topic() + if len(strings.Split(topic, "/")) != 4 { + logger.SugarLogger.Infof("[MQ] Received invalid topic: %s, ignoring", topic) + return + } + vehicleID := strings.Split(topic, "/")[1] + nodeID := strings.Split(topic, "/")[2] + canID := strings.Split(topic, "/")[3] + + if vehicleID == "" { + logger.SugarLogger.Infof("[MQ] Received invalid vehicle id: %s, ignoring", topic) + return + } + if nodeID == "" { + logger.SugarLogger.Infof("[MQ] Received invalid node id: %s, ignoring", topic) + return + } + + if canID == "ping" { + go HandlePing(vehicleID, nodeID, msg.Payload()) + return + } else if canID == "pong" { + return + } + + message := msg.Payload() + canID = strings.TrimPrefix(canID, "0x") + canIDInt, err := strconv.ParseInt(canID, 16, 64) + if err != nil { + logger.SugarLogger.Infof("[MQ] Received invalid can id: %s, ignoring", canID) + return + } + logger.SugarLogger.Infof("[MQ] Received message: %s", topic) + go HandleMessage(vehicleID, nodeID, int(canIDInt), message) + }) +} + +func HandleMessage(vehicleID string, nodeID string, canID int, message []byte) { + if len(message) < 11 { + logger.SugarLogger.Infof("[MQ] Message too short, ignoring %d bytes", len(message)) + return + } + timestamp := message[:8] + uploadKey := message[8:10] + data := message[10:] + + if !ValidateUploadKey(vehicleID, int(binary.BigEndian.Uint16(uploadKey))) { + logger.SugarLogger.Infof("Upload key validation failed for vehicle %s, ignoring", vehicleID) + return + } + + messageStruct := model.GetMessage(canID) + if messageStruct == nil { + logger.SugarLogger.Infof("Received unknown message id: %d, ignoring", canID) + return + } + + err := messageStruct.FillFromBytes(data) + if err != nil { + logger.SugarLogger.Infof("Error deserializing message: %s", err) + return + } + + signals := messageStruct.ExportSignals() + ts := int(binary.BigEndian.Uint64(timestamp)) + now := time.Now().Truncate(time.Microsecond) + for i := range signals { + signals[i].Name = fmt.Sprintf("%s_%s", nodeID, signals[i].Name) + signals[i].Timestamp = ts + signals[i].VehicleID = vehicleID + signals[i].ProducedAt = time.UnixMicro(int64(ts)) + signals[i].CreatedAt = now + } + if err := CreateSignals(signals); err != nil { + logger.SugarLogger.Infof("Error creating signals: %s", err) + } +} diff --git a/gr26/service/ping.go b/gr26/service/ping.go new file mode 100644 index 0000000..f94638f --- /dev/null +++ b/gr26/service/ping.go @@ -0,0 +1,72 @@ +package service + +import ( + "encoding/binary" + "fmt" + "strings" + "time" + + "github.com/gaucho-racing/mapache/gr26/database" + "github.com/gaucho-racing/mapache/gr26/mqtt" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + + "github.com/gaucho-racing/mapache/mapache-go" +) + +func HandlePing(vehicleID string, nodeID string, payload []byte) { + logger.SugarLogger.Infof("[MQ] Received ping from gr26/%s/%s", vehicleID, nodeID) + ping := binary.BigEndian.Uint64(payload[:8]) + uploadKey := binary.BigEndian.Uint16(payload[8:10]) + if !ValidateUploadKey(vehicleID, int(uploadKey)) { + logger.SugarLogger.Infof("Upload key validation failed for vehicle %s, ignoring", vehicleID) + return + } + go SendPong(vehicleID, nodeID, ping) +} + +func SendPong(vehicleID string, nodeID string, ping uint64) { + topic := fmt.Sprintf("gr26/%s/%s/pong", vehicleID, nodeID) + pong := uint64(time.Now().UnixMicro()) + latency := pong - ping + + payload := make([]byte, 16) + binary.BigEndian.PutUint64(payload, ping) + binary.BigEndian.PutUint64(payload[8:], pong) + + mqtt.Client.Publish(topic, 0, false, payload) + logger.SugarLogger.Infof("[PING] Received ping from gr26/%s/%s in %dms", vehicleID, nodeID, latency/1000) + + err := CreatePing(mapache.Ping{ + VehicleID: vehicleID, + Ping: int(ping), + Pong: int(pong), + Latency: int(latency), + }) + if err != nil { + logger.SugarLogger.Infof("Error creating ping: %s", err) + } +} + +func GetPing(vehicleID string, micros int) mapache.Ping { + var ping mapache.Ping + database.DB.Where("vehicle_id = ? AND ping = ?", vehicleID, micros).First(&ping) + return ping +} + +func CreatePing(ping mapache.Ping) error { + result := database.DB.Create(&ping) + if result.Error != nil { + if strings.Contains(result.Error.Error(), "Duplicate entry") { + logger.SugarLogger.Infof("Duplicate ping entry") + result = database.DB.Where("vehicle_id = ? AND ping = ?", ping.VehicleID, ping.Ping).Updates(&ping) + } + } else { + logger.SugarLogger.Infow("[DB] New ping created", + "vehicle_id", ping.VehicleID, + "ping", ping.Ping, + "pong", ping.Pong, + "latency", ping.Latency, + ) + } + return result.Error +} diff --git a/gr26/service/signal.go b/gr26/service/signal.go new file mode 100644 index 0000000..ae398d4 --- /dev/null +++ b/gr26/service/signal.go @@ -0,0 +1,75 @@ +package service + +import ( + "fmt" + + "github.com/gaucho-racing/mapache/gr26/database" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + + mapache "github.com/gaucho-racing/mapache/mapache-go" + ulid "github.com/gaucho-racing/ulid-go" + "gorm.io/gorm/clause" +) + +func GetSignal(timestamp int, vehicleID string, name string) mapache.Signal { + var signal mapache.Signal + database.DB.Where("timestamp = ?", timestamp).Where("vehicle_id = ?", vehicleID).Where("name = ?", name).First(&signal) + return signal +} + +func CreateSignal(signal mapache.Signal) error { + if signal.Timestamp == 0 { + return fmt.Errorf("signal timestamp cannot be 0") + } + if signal.VehicleID == "" { + return fmt.Errorf("signal vehicle id cannot be empty") + } + if signal.Name == "" { + return fmt.Errorf("signal name cannot be empty") + } + signal.ID = ulid.Make().Prefixed("sgnl") + Hub.Publish(signal) + if database.DB.Where("timestamp = ?", signal.Timestamp).Where("vehicle_id = ?", signal.VehicleID).Where("name = ?", signal.Name).Updates(&signal).RowsAffected == 0 { + logger.SugarLogger.Infow("[DB] New signal created", + "timestamp", signal.Timestamp, + "vehicle_id", signal.VehicleID, + "name", signal.Name, + ) + if result := database.DB.Create(&signal); result.Error != nil { + return result.Error + } + } else { + logger.SugarLogger.Infow("[DB] Existing signal updated", + "timestamp", signal.Timestamp, + "vehicle_id", signal.VehicleID, + "name", signal.Name, + ) + } + return nil +} + +func CreateSignals(signals []mapache.Signal) error { + for i := range signals { + if signals[i].Timestamp == 0 { + return fmt.Errorf("signal timestamp cannot be 0") + } + if signals[i].VehicleID == "" { + return fmt.Errorf("signal vehicle id cannot be empty") + } + if signals[i].Name == "" { + return fmt.Errorf("signal name cannot be empty") + } + signals[i].ID = ulid.Make().Prefixed("sgnl") + } + for _, signal := range signals { + Hub.Publish(signal) + } + result := database.DB.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "timestamp"}, {Name: "vehicle_id"}, {Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{"id", "value", "raw_value", "produced_at"}), + }).Create(&signals) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/gr26/service/signal_hub.go b/gr26/service/signal_hub.go new file mode 100644 index 0000000..7d0cfc4 --- /dev/null +++ b/gr26/service/signal_hub.go @@ -0,0 +1,85 @@ +package service + +import ( + "sync" + + mapache "github.com/gaucho-racing/mapache/mapache-go" + "github.com/gorilla/websocket" +) + +type Client struct { + Conn *websocket.Conn + Send chan mapache.Signal +} + +type SignalHub struct { + mu sync.RWMutex + subscribers map[string]map[string]map[*Client]struct{} +} + +var Hub *SignalHub + +func init() { + Hub = NewSignalHub() +} + +func NewSignalHub() *SignalHub { + return &SignalHub{ + subscribers: make(map[string]map[string]map[*Client]struct{}), + } +} + +func (h *SignalHub) Subscribe(vehicleID string, signalNames []string, client *Client) { + h.mu.Lock() + defer h.mu.Unlock() + if h.subscribers[vehicleID] == nil { + h.subscribers[vehicleID] = make(map[string]map[*Client]struct{}) + } + for _, name := range signalNames { + if h.subscribers[vehicleID][name] == nil { + h.subscribers[vehicleID][name] = make(map[*Client]struct{}) + } + h.subscribers[vehicleID][name][client] = struct{}{} + } +} + +func (h *SignalHub) Unsubscribe(vehicleID string, signalNames []string, client *Client) { + h.mu.Lock() + defer h.mu.Unlock() + signals, ok := h.subscribers[vehicleID] + if !ok { + return + } + for _, name := range signalNames { + clients, ok := signals[name] + if !ok { + continue + } + delete(clients, client) + if len(clients) == 0 { + delete(signals, name) + } + } + if len(signals) == 0 { + delete(h.subscribers, vehicleID) + } +} + +func (h *SignalHub) Publish(signal mapache.Signal) { + h.mu.RLock() + defer h.mu.RUnlock() + signals, ok := h.subscribers[signal.VehicleID] + if !ok { + return + } + clients, ok := signals[signal.Name] + if !ok { + return + } + for client := range clients { + select { + case client.Send <- signal: + default: + } + } +} diff --git a/gr26/service/vehicle.go b/gr26/service/vehicle.go new file mode 100644 index 0000000..cfc4115 --- /dev/null +++ b/gr26/service/vehicle.go @@ -0,0 +1,113 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "sync" + "time" + + "github.com/gaucho-racing/mapache/gr26/config" + "github.com/gaucho-racing/mapache/gr26/pkg/logger" + "github.com/gaucho-racing/mapache/gr26/pkg/rincon" + + "github.com/gaucho-racing/mapache/mapache-go" +) + +type uploadKeyCacheEntry struct { + UploadKey int + Found bool + ExpiresAt time.Time +} + +var uploadKeyCache sync.Map + +func ValidateUploadKey(vehicleID string, key int) bool { + if config.SkipAuthCheck { + return true + } + + if entry, ok := uploadKeyCache.Load(vehicleID); ok { + cached := entry.(uploadKeyCacheEntry) + if time.Now().Before(cached.ExpiresAt) { + if !cached.Found { + return false + } + return cached.UploadKey == key + } + } + + if rincon.RinconClient == nil { + logger.SugarLogger.Warnf("Rincon client is nil, cannot validate upload key for vehicle %s", vehicleID) + return false + } + + hitTTL, err := strconv.Atoi(config.VehicleUploadKeyCacheTTL) + if err != nil { + hitTTL = 600 + } + + svc, err := rincon.RinconClient.MatchRoute(fmt.Sprintf("/vehicles/%s", vehicleID), "GET") + if err != nil { + logger.SugarLogger.Warnf("Failed to resolve vehicle service via Rincon: %v", err) + uploadKeyCache.Store(vehicleID, uploadKeyCacheEntry{ + Found: false, + ExpiresAt: time.Now().Add(time.Minute), + }) + return false + } + + resp, err := http.Get(fmt.Sprintf("%s/vehicles/%s", svc.Endpoint, vehicleID)) + if err != nil { + logger.SugarLogger.Warnf("Failed to fetch vehicle %s: %v", vehicleID, err) + uploadKeyCache.Store(vehicleID, uploadKeyCacheEntry{ + Found: false, + ExpiresAt: time.Now().Add(time.Minute), + }) + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.SugarLogger.Warnf("Vehicle service returned %d for vehicle %s", resp.StatusCode, vehicleID) + uploadKeyCache.Store(vehicleID, uploadKeyCacheEntry{ + Found: false, + ExpiresAt: time.Now().Add(time.Minute), + }) + return false + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + logger.SugarLogger.Warnf("Failed to read vehicle response for %s: %v", vehicleID, err) + uploadKeyCache.Store(vehicleID, uploadKeyCacheEntry{ + Found: false, + ExpiresAt: time.Now().Add(time.Minute), + }) + return false + } + + var vehicle mapache.Vehicle + if err := json.Unmarshal(body, &vehicle); err != nil { + logger.SugarLogger.Warnf("Failed to unmarshal vehicle %s: %v", vehicleID, err) + uploadKeyCache.Store(vehicleID, uploadKeyCacheEntry{ + Found: false, + ExpiresAt: time.Now().Add(time.Minute), + }) + return false + } + + uploadKeyCache.Store(vehicleID, uploadKeyCacheEntry{ + UploadKey: vehicle.UploadKey, + Found: true, + ExpiresAt: time.Now().Add(time.Duration(hitTTL) * time.Second), + }) + + if vehicle.UploadKey != key { + logger.SugarLogger.Infof("Upload key mismatch for vehicle %s: expected %d, got %d", vehicleID, vehicle.UploadKey, key) + return false + } + return true +} diff --git a/mapache-go/signal.go b/mapache-go/signal.go index edecc72..edc1e99 100644 --- a/mapache-go/signal.go +++ b/mapache-go/signal.go @@ -24,20 +24,14 @@ const ( // This can be something like a sensor reading, a boolean flag, or a status code. // Timestamp, VehicleID, and Name are together used to uniquely identify a signal row entry. type Signal struct { - // Timestamp is the Unix microseconds of the signal. - Timestamp int `json:"timestamp"` - // VehicleID is the unique identifier for the vehicle that the signal is associated with. - VehicleID string `json:"vehicle_id"` - // Name represents the name of the signal. - Name string `json:"name"` - // Value is the value of the signal post-scaling. - Value float64 `json:"value"` - // RawValue is the raw value of the signal before scaling. - RawValue int `json:"raw_value"` - // ProducedAt is the time at which the signal was produced by the vehicle. + ID string `json:"id" gorm:"primaryKey"` + Timestamp int `json:"timestamp" gorm:"uniqueIndex:idx_signal_unique"` + VehicleID string `json:"vehicle_id" gorm:"uniqueIndex:idx_signal_unique"` + Name string `json:"name" gorm:"uniqueIndex:idx_signal_unique"` + Value float64 `json:"value"` + RawValue int `json:"raw_value"` ProducedAt time.Time `json:"produced_at" gorm:"precision:6"` - // CreatedAt is the time at which the signal was actually stored in the database. - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;precision:6"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;precision:6"` } func (Signal) TableName() string {