From da5b432bf95474ec659a944079d83cbbfae03feb Mon Sep 17 00:00:00 2001 From: Saikrishna Bairamoni <84093461+SaikrishnaBairamoni@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:09:17 -0400 Subject: [PATCH 01/35] Update dockerhub.yml --- .github/workflows/dockerhub.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 81f0790..c6e8c10 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -18,9 +18,14 @@ jobs: uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Replcae Docker tag + id: set_tag + run: echo "TAG=$(echo ${GITHUB_REF##*/} | sed 's/\//-/g')" >> $GITHUB_ENV + - name: Build uses: docker/build-push-action@v5 with: push: true - tags: usdotjpoode/jpo-cvdp:${{ github.ref_name }} + tags: usdotjpoode/jpo-cvdp:${{ env.TAG }} From aad819bffeca610905891cad95e343187910fe6f Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Thu, 20 Jun 2024 11:03:41 -0600 Subject: [PATCH 02/35] Corrected typo in `dockerhub.yml` --- .github/workflows/dockerhub.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index c6e8c10..21f9b2f 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -20,7 +20,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Replcae Docker tag + - name: Replace Docker tag id: set_tag run: echo "TAG=$(echo ${GITHUB_REF##*/} | sed 's/\//-/g')" >> $GITHUB_ENV From 2b2c7179958e11cab568c1d5d51fc92244034078 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Mon, 24 Jun 2024 13:41:54 -0600 Subject: [PATCH 03/35] Added supported message types to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index bcd81ac..5828079 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ certain speed restrictions.** Do not assume this strategy will work in general. There are alternative strategies that must be employed to handle cases where loitering locations can aid in learning the identity of the driver. +## Supported Message Types +- Basic Safety Message (BSM) + ## Table of Contents 1. [Release Notes](#release-notes) From a919904a93f1f9f6a39a11835e5ef785ec95ea82 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Mon, 24 Jun 2024 13:44:03 -0600 Subject: [PATCH 04/35] Added explanation regarding PPM not being designed to handle other message types to README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5828079..a80c4c0 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ trajectories with individuals is the objective of this project. # The Operational Data Environment (ODE) Privacy Protection Module (PPM) -The PPM operates on streams of raw BSMs generated by the ODE. It determines +The PPM operates on streams of raw BSMs processed by the ODE. It determines whether individual BSMs should be retained or suppressed (deleted) based on the information in that BSM and auxiliary map information used to define a geofence. BSM geoposition (latitude and longitude) and speed are used to determine the @@ -42,6 +42,8 @@ loitering locations can aid in learning the identity of the driver. ## Supported Message Types - Basic Safety Message (BSM) +It should be noted that the PPM is not designed to handle other message types at this time. Future versions of the PPM may support additional message types. + ## Table of Contents 1. [Release Notes](#release-notes) From 27655baddd0d754aa7abec19b4eb77dfb7322bf7 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Mon, 24 Jun 2024 14:33:48 -0600 Subject: [PATCH 05/35] Removed all references to TIMs --- Dockerfile.standalone | 53 ------- README.md | 8 +- config/ppmTim.properties | 42 ----- config/tim-test/I_80_vel_filter.properties | 37 ----- config/tim-test/c1.properties | 41 ----- config/tim-test/c2.properties | 41 ----- config/tim-test/c3.properties | 41 ----- do_kafka_test.sh | 39 +---- docker-compose-confluent-cloud.yml | 20 +-- docker-compose-kafka.yml | 2 +- docker-compose-standalone.yml | 54 ------- docker-compose.yml | 4 +- docker-test/do_tim_test.sh | 59 -------- docker-test/test_in.py | 39 ++--- docker-test/test_out.py | 39 ++--- docs/testing.md | 2 +- src/tests.cpp | 8 - test-scripts/standalone.sh | 32 +--- test-scripts/standalone_multi.sh | 143 ------------------ unit-test-data/test-case.all.good.tims.json | 3 - unit-test-data/test-case.bad.speed.tims.json | 3 - .../test-case.inside.geofence.tims.json | 7 - .../test-case.outside.geofence.tims.json | 6 - 23 files changed, 48 insertions(+), 675 deletions(-) delete mode 100644 Dockerfile.standalone delete mode 100644 config/ppmTim.properties delete mode 100644 config/tim-test/I_80_vel_filter.properties delete mode 100644 config/tim-test/c1.properties delete mode 100644 config/tim-test/c2.properties delete mode 100644 config/tim-test/c3.properties delete mode 100644 docker-compose-standalone.yml delete mode 100755 docker-test/do_tim_test.sh delete mode 100755 test-scripts/standalone_multi.sh delete mode 100644 unit-test-data/test-case.all.good.tims.json delete mode 100644 unit-test-data/test-case.bad.speed.tims.json delete mode 100644 unit-test-data/test-case.inside.geofence.tims.json delete mode 100644 unit-test-data/test-case.outside.geofence.tims.json diff --git a/Dockerfile.standalone b/Dockerfile.standalone deleted file mode 100644 index bc786cd..0000000 --- a/Dockerfile.standalone +++ /dev/null @@ -1,53 +0,0 @@ -# === RUNTIME DEPENDENCIES IMAGE === -FROM alpine:3.12 as runtime-deps -USER root - -WORKDIR /cvdi-stream - -# update the package manager -RUN apk update - -# add runtime dependencies -RUN apk add --upgrade --no-cache \ - bash \ - librdkafka \ - librdkafka-dev - -# === BUILDER IMAGE === -FROM runtime-deps as builder -USER root - -WORKDIR /cvdi-stream - -ENV DEBIAN_FRONTEND=noninteractive - -# add build dependencies -RUN apk add --upgrade --no-cache --virtual .build-deps \ - cmake \ - g++ \ - make - -# add the source and build files -ADD CMakeLists.txt /cvdi-stream -ADD ./src /cvdi-stream/src -ADD ./cv-lib /cvdi-stream/cv-lib -ADD ./include /cvdi-stream/include -ADD ./kafka-test /cvdi-stream/kafka-test -ADD ./unit-test-data /cvdi-stream/unit-test-data -ADD ./config /cvdi-stream/config - -# do the build -RUN export LD_LIBRARY_PATH=/usr/local/lib && mkdir /cvdi-stream-build && cd /cvdi-stream-build && cmake /cvdi-stream && make - -# === RUNTIME IMAGE === -FROM runtime-deps -USER root - -WORKDIR /cvdi-stream - -# copy the built files from the builder -COPY --from=builder /cvdi-stream-build/ /cvdi-stream-build/ -COPY --from=builder /cvdi-stream /cvdi-stream - -# add test data (this changes frequently so keep it low in the file) -ADD ./docker-test /cvdi-stream/docker-test diff --git a/README.md b/README.md index a80c4c0..44e7470 100644 --- a/README.md +++ b/README.md @@ -163,10 +163,10 @@ Unit tests can be built and executed using the build_and_run_unit_tests.sh file The unit tests are also built when the solution is compiled. For information on that, check out [this section](./docs/testing.md#unit-testing). ## Standalone Cluster -The docker-compose-standalone.yml file is meant for local testing/troubleshooting. +The docker-compose.yml file is meant for local testing/troubleshooting. To utilize this, pass the -f flag to the docker-compose command as follows: -> docker-compose -f docker-compose-standalone.yml up +> docker-compose -f docker-compose.yml up Sometimes kafka will fail to start up properly. If this happens, spin down the containers and try again. @@ -195,16 +195,14 @@ This script should be run outside of the dev container in an environment where D It should be noted that this script needs to use the LF end-of-line sequence. ## Kafka Test Script -The [do_kafka_test.sh](./do_kafka_test.sh) script is designed to perform integration tests on a Kafka instance. To execute the tests, this script relies on the following scripts: standalone.sh, standalone_multi.sh, do_bsm_test.sh, and do_tim_test.sh. +The [do_kafka_test.sh](./do_kafka_test.sh) script is designed to perform integration tests on a Kafka instance. To execute the tests, this script relies on the following scripts: standalone.sh and do_bsm_test.sh. To ensure proper execution, it is recommended to run this script outside of the dev container where docker is available. This is because the script will spin up a standalone kafka instance and will not be able to access the docker daemon from within the dev container. It should be noted that this script and any dependent scripts need to use the LF end-of-line sequence. These include the following: - do_kafka_test.sh - standalone.sh -- standalone_multi.sh - do_bsm_test.sh -- do_tim_test.sh - test_in.py - test_out.py diff --git a/config/ppmTim.properties b/config/ppmTim.properties deleted file mode 100644 index 3b2707a..0000000 --- a/config/ppmTim.properties +++ /dev/null @@ -1,42 +0,0 @@ -# Configuration details for the velocity filter. -# min and max velocity values are in units m/s per the J2735 specification. -privacy.filter.velocity=OFF -privacy.filter.velocity.min=2.235 -privacy.filter.velocity.max=35.763 - -# Configuration details for privacy ID redaction. -privacy.redaction.id=OFF -privacy.redaction.id.value=FFFFFFFF -privacy.redaction.id.inclusions=OFF -privacy.redaction.id.included=BEA10000,BEA10001 - -# Configuration details for general redaction -privacy.redaction.general=OFF - -# Configuration details for geofencing. -privacy.filter.geofence=OFF -privacy.filter.geofence.mapfile=/ppm_data/I_80.edges -privacy.filter.geofence.sw.lat=40.997 -privacy.filter.geofence.sw.lon=-111.041 -privacy.filter.geofence.ne.lat=42.085 -privacy.filter.geofence.ne.lon=-104.047 - -# ODE / PPM Kafka topics. -privacy.topic.consumer=topic.OdeTimJson -privacy.topic.producer=topic.FilteredOdeTimJson - -group.id=PPM_TIM - -# max number of bytes per topic+partition to request from brokers -# defaults to 1 MiB, here we set it to 20 MiB -max.partition.fetch.bytes=20971520 - -# For testing purposes, use one partition. -# privacy.kafka.partition=0 - -# The host ip address for the Broker. -metadata.broker.list=your.kafka.broker.ip:9092 - -# specify the compression codec for all data generated: none, gzip, snappy, lz4 -compression.type=none - diff --git a/config/tim-test/I_80_vel_filter.properties b/config/tim-test/I_80_vel_filter.properties deleted file mode 100644 index c62ac9a..0000000 --- a/config/tim-test/I_80_vel_filter.properties +++ /dev/null @@ -1,37 +0,0 @@ -# min and max velocity values to filter below and above (units m/s) -privacy.filter.velocity=ON -privacy.filter.velocity.min=2.235 -privacy.filter.velocity.max=35.763 - -# value to replace existing id for filtering. -privacy.redaction.id=OFF -privacy.redaction.id.value=FFFFFFFF -privacy.redaction.id.inclusions=ON -privacy.redaction.id.included=BEA10000,BEA10001 - -# geofence map fitting boundaries. -privacy.filter.geofence=OFF -privacy.filter.geofence.mapfile=/ppm_data/road_file.csv - -# geographic bounds that can further restrict the map information. -privacy.filter.geofence.sw.lat=40.970819 -privacy.filter.geofence.sw.lon=-111.090454 -privacy.filter.geofence.ne.lat=42.136908 -privacy.filter.geofence.ne.lon=-103.793068 - -# topics for the sanitization module to consume and produce. -privacy.topic.consumer=j2735BsmRawJson -privacy.topic.producer=j2735BsmFilteredJson - -group.id=0 - -privacy.kafka.partition=0 - -#metadata.broker.list=160.91.216.129:9092 -#metadata.broker.list=192.168.1.228:9092 -metadata.broker.list=172.17.0.1:9092 - -# specify the compression codec for all data generated: none, gzip, snappy, lz4 -compression.type=none - -debug=0 diff --git a/config/tim-test/c1.properties b/config/tim-test/c1.properties deleted file mode 100644 index 3fcb249..0000000 --- a/config/tim-test/c1.properties +++ /dev/null @@ -1,41 +0,0 @@ -# min and max velocity values to filter below and above (units m/s) -privacy.filter.velocity=OFF -privacy.filter.velocity.min=2.235 -privacy.filter.velocity.max=35.763 - -# value to replace existing id for filtering. -privacy.redaction.id=OFF -privacy.redaction.id.value=FFFFFFFF -privacy.redaction.id.inclusions=ON -privacy.redaction.id.included=BEA10000,BEA10001 - -# geofence map fitting boundaries. -privacy.filter.geofence=OFF -privacy.filter.geofence.mapfile=/ppm_data/road_file.csv - -# geographic bounds that can further restrict the map information. -privacy.filter.geofence.sw.lat=40.970819 -privacy.filter.geofence.sw.lon=-111.090454 -privacy.filter.geofence.ne.lat=42.136908 -privacy.filter.geofence.ne.lon=-103.793068 -privacy.filter.geofence.extension=10.0 - -# topics for the sanitization module to consume and produce. -privacy.topic.consumer=topic.OdeTimJson -privacy.topic.producer=topic.FilteredOdeTimJson - -# Amount of time to wait when no message is available (milliseconds) -privacy.consumer.timeout=500 - -group.id=1 - -privacy.kafka.partition=0 - -#metadata.broker.list=160.91.216.129:9092 -#metadata.broker.list=192.168.1.228:9092 -metadata.broker.list=172.17.0.1:9092 - -# specify the compression codec for all data generated: none, gzip, snappy, lz4 -compression.type=none - -debug=0 diff --git a/config/tim-test/c2.properties b/config/tim-test/c2.properties deleted file mode 100644 index 642ddfa..0000000 --- a/config/tim-test/c2.properties +++ /dev/null @@ -1,41 +0,0 @@ -# min and max velocity values to filter below and above (units m/s) -privacy.filter.velocity=ON -privacy.filter.velocity.min=2.235 -privacy.filter.velocity.max=35.763 - -# value to replace existing id for filtering. -privacy.redaction.id=OFF -privacy.redaction.id.value=FFFFFFFF -privacy.redaction.id.inclusions=ON -privacy.redaction.id.included=BEA10000,BEA10001 - -# geofence map fitting boundaries. -privacy.filter.geofence=OFF -privacy.filter.geofence.mapfile=/ppm_data/road_file.csv - -# geographic bounds that can further restrict the map information. -privacy.filter.geofence.sw.lat=40.970819 -privacy.filter.geofence.sw.lon=-111.090454 -privacy.filter.geofence.ne.lat=42.136908 -privacy.filter.geofence.ne.lon=-103.793068 -privacy.filter.geofence.extension=10.0 - -# topics for the sanitization module to consume and produce. -privacy.topic.consumer=topic.OdeTimJson -privacy.topic.producer=topic.FilteredOdeTimJson - -# Amount of time to wait when no message is available (milliseconds) -privacy.consumer.timeout=500 - -group.id=1 - -privacy.kafka.partition=0 - -#metadata.broker.list=160.91.216.129:9092 -#metadata.broker.list=192.168.1.228:9092 -metadata.broker.list=172.17.0.1:9092 - -# specify the compression codec for all data generated: none, gzip, snappy, lz4 -compression.type=none - -debug=0 diff --git a/config/tim-test/c3.properties b/config/tim-test/c3.properties deleted file mode 100644 index fc73e31..0000000 --- a/config/tim-test/c3.properties +++ /dev/null @@ -1,41 +0,0 @@ -# min and max velocity values to filter below and above (units m/s) -privacy.filter.velocity=OFF -privacy.filter.velocity.min=2.235 -privacy.filter.velocity.max=35.763 - -# value to replace existing id for filtering. -privacy.redaction.id=OFF -privacy.redaction.id.value=FFFFFFFF -privacy.redaction.id.inclusions=ON -privacy.redaction.id.included=BEA10000,BEA10001 - -# geofence map fitting boundaries. -privacy.filter.geofence=ON -privacy.filter.geofence.mapfile=/ppm_data/road_file.csv - -# geographic bounds that can further restrict the map information. -privacy.filter.geofence.sw.lat=40.970819 -privacy.filter.geofence.sw.lon=-111.090454 -privacy.filter.geofence.ne.lat=42.136908 -privacy.filter.geofence.ne.lon=-103.793068 -privacy.filter.geofence.extension=10.0 - -# topics for the sanitization module to consume and produce. -privacy.topic.consumer=topic.OdeTimJson -privacy.topic.producer=topic.FilteredOdeTimJson - -# Amount of time to wait when no message is available (milliseconds) -privacy.consumer.timeout=500 - -group.id=2 - -privacy.kafka.partition=0 - -#metadata.broker.list=160.91.216.129:9092 -#metadata.broker.list=192.168.1.228:9092 -metadata.broker.list=172.17.0.1:9092 - -# specify the compression codec for all data generated: none, gzip, snappy, lz4 -compression.type=none - -debug=0 diff --git a/do_kafka_test.sh b/do_kafka_test.sh index 6d7c063..420d2c6 100755 --- a/do_kafka_test.sh +++ b/do_kafka_test.sh @@ -15,7 +15,6 @@ NC='\033[0m' # No Color MAP_FILE=data/I_80.edges BSM_DATA_FILE=data/I_80_test.json -TIM_DATA_FILE=data/I_80_test_TIMS.json PPM_CONTAINER_NAME=test_ppm_instance PPM_IMAGE_TAG=do-kafka-test-ppm-image PPM_IMAGE_NAME=jpo-cvdp_ppm @@ -38,7 +37,6 @@ setup() { echo "KAFKA_CONTAINER_NAME is resolved dynamically" echo "MAP_FILE: $MAP_FILE" echo "BSM_DATA_FILE: $BSM_DATA_FILE" - echo "TIM_DATA_FILE: $TIM_DATA_FILE" echo "PPM_CONTAINER_NAME: $PPM_CONTAINER_NAME" echo "PPM_IMAGE_TAG: $PPM_IMAGE_TAG" echo "PPM_IMAGE_NAME: $PPM_IMAGE_NAME" @@ -63,12 +61,8 @@ waitForKafkaToCreateTopics() { allTopicsCreated=true if [ $(echo $ltopics | grep "topic.FilteredOdeBsmJson" | wc -l) == "0" ]; then allTopicsCreated=false - elif [ $(echo $ltopics | grep "topic.FilteredOdeTimJson" | wc -l) == "0" ]; then - allTopicsCreated=false elif [ $(echo $ltopics | grep "topic.OdeBsmJson" | wc -l) == "0" ]; then allTopicsCreated=false - elif [ $(echo $ltopics | grep "topic.OdeTimJson" | wc -l) == "0" ]; then - allTopicsCreated=false fi if [ $allTopicsCreated == true ]; then @@ -94,57 +88,38 @@ run_tests() { echo "--- File Being Used ---" echo $MAP_FILE echo $BSM_DATA_FILE - echo $TIM_DATA_FILE echo "-----------------" - numberOfTests=10 + numberOfTests=6 echo -e $YELLOW"Test 1/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c1.properties $BSM_DATA_FILE BSM 0 + ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c1.properties $BSM_DATA_FILE 0 echo "" echo "" echo -e $YELLOW"Test 2/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c2.properties $BSM_DATA_FILE BSM 10 + ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c2.properties $BSM_DATA_FILE 10 echo "" echo "" echo -e $YELLOW"Test 3/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c3.properties $BSM_DATA_FILE BSM 18 + ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c3.properties $BSM_DATA_FILE 18 echo "" echo "" echo -e $YELLOW"Test 4/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c4.properties $BSM_DATA_FILE BSM 23 + ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c4.properties $BSM_DATA_FILE 23 echo "" echo "" echo -e $YELLOW"Test 5/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c5.properties $BSM_DATA_FILE BSM 33 + ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c5.properties $BSM_DATA_FILE 33 echo "" echo "" echo -e $YELLOW"Test 6/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c6.properties $BSM_DATA_FILE BSM 43 - echo "" - echo "" - - echo -e $YELLOW"Test 7/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/tim-test/c1.properties $TIM_DATA_FILE TIM 0 - echo "" - echo "" - - echo -e $YELLOW"Test 8/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/tim-test/c2.properties $TIM_DATA_FILE TIM 10 - echo "" - echo "" - - echo -e $YELLOW"Test 9/$numberOfTests"$NC - ./test-scripts/standalone.sh $MAP_FILE config/tim-test/c3.properties $TIM_DATA_FILE TIM 18 + ./test-scripts/standalone.sh $MAP_FILE config/bsm-test/c6.properties $BSM_DATA_FILE 43 echo "" echo "" - - echo -e $YELLOW"Test 10/$numberOfTests (2 tests in one)"$NC - ./test-scripts/standalone_multi.sh $MAP_FILE config/bsm-test/c6.properties config/tim-test/c3.properties $BSM_DATA_FILE $TIM_DATA_FILE 48 23 } cleanup() { diff --git a/docker-compose-confluent-cloud.yml b/docker-compose-confluent-cloud.yml index 0a358b7..24d21b7 100644 --- a/docker-compose-confluent-cloud.yml +++ b/docker-compose-confluent-cloud.yml @@ -1,6 +1,6 @@ version: '2' services: - ppmbsm: + ppm: build: context: . dockerfile: Dockerfile @@ -15,23 +15,5 @@ services: PPM_LOG_TO_CONSOLE: ${PPM_LOG_TO_CONSOLE} RPM_DEBUG: ${RPM_DEBUG} PPM_LOG_LEVEL: ${PPM_LOG_LEVEL} - volumes: - - ${DOCKER_SHARED_VOLUME}:/ppm_data - - ppmtim: - build: - context: . - dockerfile: Dockerfile - environment: - DOCKER_HOST_IP: ${DOCKER_HOST_IP} - KAFKA_TYPE: ${KAFKA_TYPE} - CONFLUENT_KEY: ${CONFLUENT_KEY} - CONFLUENT_SECRET: ${CONFLUENT_SECRET} - PPM_CONFIG_FILE: ppmTim.properties - REDACTION_PROPERTIES_PATH: ${REDACTION_PROPERTIES_PATH} - PPM_LOG_TO_FILE: ${PPM_LOG_TO_FILE} - PPM_LOG_TO_CONSOLE: ${PPM_LOG_TO_CONSOLE} - RPM_DEBUG: ${RPM_DEBUG} - PPM_LOG_LEVEL: ${PPM_LOG_LEVEL} volumes: - ${DOCKER_SHARED_VOLUME}:/ppm_data \ No newline at end of file diff --git a/docker-compose-kafka.yml b/docker-compose-kafka.yml index 7d9a61f..94fb651 100644 --- a/docker-compose-kafka.yml +++ b/docker-compose-kafka.yml @@ -11,6 +11,6 @@ services: environment: KAFKA_ADVERTISED_HOST_NAME: ${DOCKER_HOST_IP} KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1,topic.OdeTimJson:1:1,topic.FilteredOdeTimJson:1:1" + KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1" volumes: - /var/run/docker.sock:/var/run/docker.sock \ No newline at end of file diff --git a/docker-compose-standalone.yml b/docker-compose-standalone.yml deleted file mode 100644 index 4199265..0000000 --- a/docker-compose-standalone.yml +++ /dev/null @@ -1,54 +0,0 @@ -version: '2' -services: - zookeeper: - image: wurstmeister/zookeeper - ports: - - "2181:2181" - - kafka: - image: wurstmeister/kafka - ports: - - "9092:9092" - environment: - KAFKA_ADVERTISED_HOST_NAME: ${DOCKER_HOST_IP} - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1,topic.OdeTimJson:1:1,topic.FilteredOdeTimJson:1:1" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ppm_bsm: - build: - context: . - dockerfile: Dockerfile - environment: - DOCKER_HOST_IP: ${DOCKER_HOST_IP} - KAFKA_TYPE: ${KAFKA_TYPE} - CONFLUENT_KEY: ${CONFLUENT_KEY} - CONFLUENT_SECRET: ${CONFLUENT_SECRET} - PPM_CONFIG_FILE: ppmBsm.properties - REDACTION_PROPERTIES_PATH: ${REDACTION_PROPERTIES_PATH} - PPM_LOG_TO_FILE: ${PPM_LOG_TO_FILE} - PPM_LOG_TO_CONSOLE: ${PPM_LOG_TO_CONSOLE} - RPM_DEBUG: ${RPM_DEBUG} - PPM_LOG_LEVEL: ${PPM_LOG_LEVEL} - depends_on: - - kafka - volumes: - - ${DOCKER_SHARED_VOLUME}:/ppm_data - - ppm_tim: - build: - context: . - dockerfile: Dockerfile - environment: - DOCKER_HOST_IP: ${DOCKER_HOST_IP} - PPM_CONFIG_FILE: ppmTim.properties - REDACTION_PROPERTIES_PATH: ${REDACTION_PROPERTIES_PATH} - PPM_LOG_TO_FILE: ${PPM_LOG_TO_FILE} - PPM_LOG_TO_CONSOLE: ${PPM_LOG_TO_CONSOLE} - RPM_DEBUG: ${RPM_DEBUG} - PPM_LOG_LEVEL: ${PPM_LOG_LEVEL} - depends_on: - - kafka - volumes: - - ${DOCKER_SHARED_VOLUME}:/ppm_data \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e4d530c..273493c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,14 +11,14 @@ services: environment: KAFKA_ADVERTISED_HOST_NAME: ${DOCKER_HOST_IP} KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1,topic.OdeTimJson:1:1,topic.FilteredOdeTimJson:1:1" + KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1" volumes: - /var/run/docker.sock:/var/run/docker.sock ppm: build: context: . - dockerfile: Dockerfile.standalone + dockerfile: Dockerfile ports: - "8080:8080" - "9090:9090" diff --git a/docker-test/do_tim_test.sh b/docker-test/do_tim_test.sh deleted file mode 100755 index 6df20b2..0000000 --- a/docker-test/do_tim_test.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -# This script produces and consumes messages from Kafka topics. It reads a JSON file containing raw -# TIM data, processes it with a Python script, and then sends the output to a Kafka topic. Then it -# consumes the filtered messages from the Kafka topic, using a specified offset, and checks if -# any messages were received. If no messages were received after a certain number of attempts, the -# script exits with an error message. Otherwise, the script exits with a success message. - -export LD_LIBRARY_PATH=/usr/local/lib - -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' # No Color - -broker=$DOCKER_HOST_IP:9092 - -echo "**************************" -echo "Producing Raw TIMs..." -echo "**************************" -cat /ppm_data/tim_test.json | /cvdi-stream/docker-test/test_in.py | /cvdi-stream-build/kafka-test/kafka_tool -P -b $broker -p 0 -t topic.OdeTimJson 2> priv.err - -# Start the DI consumer. -offset=$1 - -echo "**************************" -echo "Consuming Filtered TIMs at offset "$offset "..." -echo "**************************" - -attempts=0 -max_attempts=5 -while true; do - attempts=$((attempts+1)) - - timeout 5 /cvdi-stream-build/kafka-test/kafka_tool -C -b $broker -p 0 -t topic.FilteredOdeTimJson -e -o $offset 2> con.err | /cvdi-stream/docker-test/test_out.py > tmp.out - if [[ $? != 0 ]]; then - echo "Error: Kafka consumer timed out." - echo "~~~~~~~~~~~~~~~~~~~~~~~~~~" - echo -e $RED"TEST FAILED!"$NC - echo "~~~~~~~~~~~~~~~~~~~~~~~~~~" - exit 1 - fi - - lines=$(cat tmp.out | wc -l) - if [[ $lines != "0" ]]; then - cat tmp.out - break - else - if [[ $attempts > $max_attempts ]]; then - echo "No data received after $max_attempts attempts. Exiting..." - echo "~~~~~~~~~~~~~~~~~~~~~~~~~~" - echo -e $RED"TEST FAILED!"$NC - echo "~~~~~~~~~~~~~~~~~~~~~~~~~~" - exit 1 - fi - fi -done -echo "~~~~~~~~~~~~~~~~~~~~~~~~~~" -echo -e $GREEN"TEST PASSED!"$NC -echo "~~~~~~~~~~~~~~~~~~~~~~~~~~" \ No newline at end of file diff --git a/docker-test/test_in.py b/docker-test/test_in.py index 743713e..d65fe3d 100755 --- a/docker-test/test_in.py +++ b/docker-test/test_in.py @@ -6,39 +6,26 @@ import sys def print_bsm_data(d): - if d['metadata']['payloadType'] == 'us.dot.its.jpo.ode.model.OdeTimPayload': - speed = d['metadata']['receivedMessageDetails']['locationData']['speed'] - lat = d['metadata']['receivedMessageDetails']['locationData']['latitude'] - lng = d['metadata']['receivedMessageDetails']['locationData']['longitude'] + id_ = d['payload']['data']['coreData']['id'] + speed = d['payload']['data']['coreData']['speed'] + lat = d['payload']['data']['coreData']['position']['latitude'] + lng = d['payload']['data']['coreData']['position']['longitude'] - print('Producing TIMS with speed={}, position={}, {}'.format(speed, lat, lng), file=sys.stderr) + if not d['payload']['data']['coreData']['size']: + print('Consuming BSM with ID={}, speed={}, position={}, {}'.format(id_, speed, lat, lng)) return - elif d['metadata']['payloadType'] == 'us.dot.its.jpo.ode.model.OdeBsmPayload': - id_ = d['payload']['data']['coreData']['id'] - speed = d['payload']['data']['coreData']['speed'] - lat = d['payload']['data']['coreData']['position']['latitude'] - lng = d['payload']['data']['coreData']['position']['longitude'] - if not d['payload']['data']['coreData']['size']: - print('Consuming BSM with ID={}, speed={}, position={}, {}'.format(id_, speed, lat, lng)) + length = 0 + width = 0 - return + if d['payload']['data']['coreData']['size']['length']: + length = d['payload']['data']['coreData']['size']['length'] - length = 0 - width = 0 + if d['payload']['data']['coreData']['size']['width']: + width = d['payload']['data']['coreData']['size']['width'] - if d['payload']['data']['coreData']['size']['length']: - length = d['payload']['data']['coreData']['size']['length'] - - if d['payload']['data']['coreData']['size']['width']: - width = d['payload']['data']['coreData']['size']['width'] - - print('Producing BSM with ID={}, speed={}, position={}, {}, size=l:{}, w:{}'.format(id_, speed, lat, lng, length, width), file=sys.stderr) - - return - - raise IOError() + print('Producing BSM with ID={}, speed={}, position={}, {}, size=l:{}, w:{}'.format(id_, speed, lat, lng, length, width), file=sys.stderr) for l in sys.stdin: diff --git a/docker-test/test_out.py b/docker-test/test_out.py index 326133b..3a4d47c 100755 --- a/docker-test/test_out.py +++ b/docker-test/test_out.py @@ -6,39 +6,26 @@ import sys def print_bsm_data(d): - if d['metadata']['payloadType'] == 'us.dot.its.jpo.ode.model.OdeTimPayload': - speed = d['metadata']['receivedMessageDetails']['locationData']['speed'] - lat = d['metadata']['receivedMessageDetails']['locationData']['latitude'] - lng = d['metadata']['receivedMessageDetails']['locationData']['longitude'] + id_ = d['payload']['data']['coreData']['id'] + speed = d['payload']['data']['coreData']['speed'] + lat = d['payload']['data']['coreData']['position']['latitude'] + lng = d['payload']['data']['coreData']['position']['longitude'] - print('Consuming TIMS with speed={}, position={}, {}'.format(speed, lat, lng)) + if not d['payload']['data']['coreData']['size']: + print('Consuming BSM with ID={}, speed={}, position={}, {}'.format(id_, speed, lat, lng)) return - elif d['metadata']['payloadType'] == 'us.dot.its.jpo.ode.model.OdeBsmPayload': - id_ = d['payload']['data']['coreData']['id'] - speed = d['payload']['data']['coreData']['speed'] - lat = d['payload']['data']['coreData']['position']['latitude'] - lng = d['payload']['data']['coreData']['position']['longitude'] - if not d['payload']['data']['coreData']['size']: - print('Consuming BSM with ID={}, speed={}, position={}, {}'.format(id_, speed, lat, lng)) + length = 0 + width = 0 - return + if d['payload']['data']['coreData']['size']['length']: + length = d['payload']['data']['coreData']['size']['length'] - length = 0 - width = 0 + if d['payload']['data']['coreData']['size']['width']: + width = d['payload']['data']['coreData']['size']['width'] - if d['payload']['data']['coreData']['size']['length']: - length = d['payload']['data']['coreData']['size']['length'] - - if d['payload']['data']['coreData']['size']['width']: - width = d['payload']['data']['coreData']['size']['width'] - - print('Consuming BSM with ID={}, speed={}, position={}, {}, size=l:{}, w:{}'.format(id_, speed, lat, lng, length, width)) - - return - - raise IOError() + print('Consuming BSM with ID={}, speed={}, position={}, {}, size=l:{}, w:{}'.format(id_, speed, lat, lng, length, width)) for l in sys.stdin: try: diff --git a/docs/testing.md b/docs/testing.md index fb62c8c..9ec5b50 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -9,7 +9,7 @@ There are several ways to test the capabilities of the PPM. ## Test Files Several example JSON message test files are in the [jpo-cvdp/data](../data) directory. These files can be edited to generate -your own test cases. Each line in the file should be a well-formed BSM or TIM JSON +your own test cases. Each line in the file should be a well-formed BSM JSON object. **Each message should be on a separate line in the file.** **If a JSON object cannot be parsed it is suppressed.** ## Docker Testing diff --git a/src/tests.cpp b/src/tests.cpp index 55250ca..2df3ce2 100644 --- a/src/tests.cpp +++ b/src/tests.cpp @@ -1309,10 +1309,6 @@ TEST_CASE( "BSMHandler JSON Id Redaction Only", "[ppm][filtering][idonly]" ) { REQUIRE ( loadTestCases( "unit-test-data/test-case.bad.speed.json", json_test_cases ) ); REQUIRE ( loadTestCases( "unit-test-data/test-case.inside.geofence.json", json_test_cases ) ); REQUIRE ( loadTestCases( "unit-test-data/test-case.outside.geofence.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.all.good.tims.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.bad.speed.tims.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.inside.geofence.tims.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.outside.geofence.tims.json", json_test_cases ) ); for ( auto& test_case : json_test_cases ) { CHECK( handler.process( test_case ) ); CHECK( handler.get_result_string() == "success" ); @@ -1351,9 +1347,6 @@ TEST_CASE( "BSMHandler JSON Speed Only Filtering", "[ppm][filtering][speedonly]" REQUIRE ( loadTestCases( "unit-test-data/test-case.inside.geofence.json", json_test_cases ) ); REQUIRE ( loadTestCases( "unit-test-data/test-case.bad.id.json", json_test_cases ) ); REQUIRE ( loadTestCases( "unit-test-data/test-case.outside.geofence.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.all.good.tims.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.inside.geofence.tims.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.outside.geofence.tims.json", json_test_cases ) ); for ( auto& test_case : json_test_cases ) { CHECK( handler.process( test_case ) ); CHECK( handler.get_result_string() == "success" ); @@ -1362,7 +1355,6 @@ TEST_CASE( "BSMHandler JSON Speed Only Filtering", "[ppm][filtering][speedonly]" // get rid of previous cases. json_test_cases.clear(); REQUIRE ( loadTestCases( "unit-test-data/test-case.bad.speed.json", json_test_cases ) ); - REQUIRE ( loadTestCases( "unit-test-data/test-case.bad.speed.tims.json", json_test_cases ) ); for ( auto& test_case : json_test_cases ) { CHECK_FALSE( handler.process( test_case ) ); CHECK( handler.get_result_string() == "speed" ); diff --git a/test-scripts/standalone.sh b/test-scripts/standalone.sh index df64e33..2751ec4 100755 --- a/test-scripts/standalone.sh +++ b/test-scripts/standalone.sh @@ -7,9 +7,8 @@ # directory. If the OFFSET argument is provided, it is used as the offset in the topic that # will be consumed and displayed in the output. If not, the default value of 0 is used. -# The script then produces the test data by executing either do_bsm_test.sh or do_tim_test.sh -# depending on the type argument, passing in the OFFSET value as an argument. The PPM container -# is then stopped, and the script ends. +# The script then produces the test data by executing do_bsm_test.sh, passing in the OFFSET +# value as an argument. The PPM container is then stopped, and the script ends. # This script should only be used for testing or demo purposes, as it may hang if the offsets are wrong. It also # checks if the required configuration and test data files exist before proceeding with the test. @@ -64,7 +63,7 @@ stopPPMContainer() { docker rm -f $PPM_CONTAINER_NAME > /dev/null } -USAGE="standalone.sh [MAP_FILE] [CONFIG] [TEST_FILE] [BSM | TIM] [OFFSET]" +USAGE="standalone.sh [MAP_FILE] [CONFIG] [TEST_FILE] [OFFSET]" if [ -z $1 ] || [ ! -f $1 ]; then echo "Map file: "$1" not found!" @@ -85,15 +84,9 @@ if [ -z $3 ] || [ ! -f $3 ]; then fi if [ -z $4 ]; then - echo "Must include type (BSM or TIM)!" - echo $USAGE - exit 1 -fi - -if [ -z $5 ]; then OFFSET=0 else - OFFSET=$5 + OFFSET=$4 fi mkdir -p /tmp/docker-test/data @@ -105,26 +98,15 @@ cp $1 /tmp/docker-test/data/road_file.csv # TODO replace map file line: sed -i '/TEXT_TO_BE_REPLACED/c\This line is removed by the admin.' /tmp/foo cp $2 /tmp/docker-test/data/config.properties -# Copy the data. -if [ $4 = "BSM" ]; then - cp $3 /tmp/docker-test/data/bsm_test.json -elif [ $4 = "TIM" ]; then - cp $3 /tmp/docker-test/data/tim_test.json -else - echo "Type must be BSM or TIM!" -fi +cp $3 /tmp/docker-test/data/bsm_test.json echo "**************************" -echo "Running standalone test in $PPM_CONTAINER_NAME container with "$1 $2 $3 $4 +echo "Running standalone test in $PPM_CONTAINER_NAME container with "$1 $2 $3 echo "**************************" startPPMContainer # Produce the test data. -if [ $4 = "BSM" ]; then - docker exec $PPM_CONTAINER_NAME /cvdi-stream/docker-test/do_bsm_test.sh $OFFSET -elif [ $4 = "TIM" ]; then - docker exec $PPM_CONTAINER_NAME /cvdi-stream/docker-test/do_tim_test.sh $OFFSET -fi +docker exec $PPM_CONTAINER_NAME /cvdi-stream/docker-test/do_bsm_test.sh $OFFSET stopPPMContainer \ No newline at end of file diff --git a/test-scripts/standalone_multi.sh b/test-scripts/standalone_multi.sh deleted file mode 100755 index d9616ab..0000000 --- a/test-scripts/standalone_multi.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash - -# This script starts two PPM containers, one for BSMs and one for TIMs, in separate Docker containers. -# It first checks if all necessary input files exist, creates necessary directories, and copies the input -# files to those directories. It then starts the PPM containers, waits for them to spin up, produces test -# data, and finally stops the containers. - -# This script should only be used for testing or demo purposes, as it may hang if the offsets are wrong. It also -# checks if the required configuration and test data files exist before proceeding with the test. - -PPM_BSM_CONTAINER_NAME=test_ppm_bsm_instance -PPM_TIM_CONTAINER_NAME=test_ppm_tim_instance -PPM_IMAGE_TAG=do-kafka-test-ppm-image -PPM_IMAGE_NAME=jpo-cvdp_ppm - -USAGE="standalone_multi.sh [MAP_FILE] [BSM_CONFIG] [TIM_CONFIG] [BSM_TEST_FILE] [TIM_TEST_FILE] [BSM_OFFSET] [TIM_OFSET]" - -startPPMContainer() { - # Start the PPM in a new container. - dockerHostIp=$DOCKER_HOST_IP - PPM_CONTAINER_NAME=$1 - data_source=$2 - ppm_container_port=$3 - - stopPPMContainer $PPM_CONTAINER_NAME - - # make sure ip can be pinged - while true; do - if ping -c 1 $dockerHostIp &> /dev/null; then - break - else - echo "Docker host ip $dockerHostIp is not pingable. Exiting." - exit 1 - fi - done - echo "Starting PPM in new container" - docker run --name $PPM_CONTAINER_NAME --env DOCKER_HOST_IP=$dockerHostIp --env PPM_LOG_TO_CONSOLE=true --env PPM_LOG_TO_FILE=true --env PPM_LOG_LEVEL=DEBUG -v $data_source:/ppm_data -d -p $ppm_container_port':8080' $PPM_IMAGE_NAME:$PPM_IMAGE_TAG /cvdi-stream/docker-test/ppm_standalone.sh - - echo "Waiting for $PPM_CONTAINER_NAME to spin up" - # while num lines of docker logs is less than 100, sleep 1 - secondsWaited=0 - while [ $(docker logs $PPM_CONTAINER_NAME | wc -l) -lt 100 ]; do - sleep 1 - secondsWaited=$((secondsWaited+1)) - done - echo "$PPM_CONTAINER_NAME is ready after $secondsWaited seconds" - - - if [ $(docker ps | grep $PPM_CONTAINER_NAME | wc -l) == "0" ]; then - echo "PPM container '$PPM_CONTAINER_NAME' is not running. Exiting." - exit 1 - fi - - container_logs=$(docker logs $PPM_CONTAINER_NAME 2>&1) - if [ $(echo $container_logs | grep "Failed to make shape" | wc -l) != "0" ]; then - echo "Warning: PPM failed to make shape." - fi -} - -stopPPMContainer() { - PPM_CONTAINER_NAME=$1 - if [ $(docker ps | grep $PPM_CONTAINER_NAME | wc -l) != "0" ]; then - echo "Stopping existing PPM container" - docker stop $PPM_CONTAINER_NAME > /dev/null - fi - docker rm -f $PPM_CONTAINER_NAME > /dev/null -} - -if [ -z $1 ] || [ ! -f $1 ]; then - echo "Map file: "$1" not found!" - echo $USAGE - exit 1 -fi - -if [ -z $2 ] || [ ! -f $2 ]; then - echo "BSM config file: "$2" not found!" - echo $USAGE - exit 1 -fi - -if [ -z $3 ] || [ ! -f $3 ]; then - echo "TIM config file: "$3" not found!" - echo $USAGE - exit 1 -fi - -if [ -z $4 ] || [ ! -f $4 ]; then - echo "BSM test file: "$4" not found!" - echo $USAGE - exit 1 -fi - -if [ -z $5 ] || [ ! -f $5 ]; then - echo "TIM test file: "$5" not found!" - echo $USAGE - exit 1 -fi - -if [ -z $6 ]; then - BSM_OFFSET=0 -else - BSM_OFFSET=$6 -fi - -if [ -z $7 ]; then - TIM_OFFSET=0 -else - TIM_OFFSET=$7 -fi - -mkdir -p /tmp/docker-test/bsm-data -mkdir -p /tmp/docker-test/tim-data - -# Copy the road files to the docker test data. -cp $1 /tmp/docker-test/bsm-data/road_file.csv -cp $1 /tmp/docker-test/tim-data/road_file.csv - -# Copy the configs to the test data. -# TODO replace map file line: sed -i '/TEXT_TO_BE_REPLACED/c\This line is removed by the admin.' /tmp/foo -cp $2 /tmp/docker-test/bsm-data/config.properties -cp $3 /tmp/docker-test/tim-data/config.properties - -# Copy the data. -cp $4 /tmp/docker-test/bsm-data/bsm_test.json -cp $5 /tmp/docker-test/tim-data/tim_test.json - -echo "**************************" -echo "Running standalone multi PPM test with "$1 $2 $3 $4 $5 $6 $7 -echo "**************************" - -dockerHostIp=$DOCKER_HOST_IP -# Start the BSM PPM in a new container. -startPPMContainer $PPM_BSM_CONTAINER_NAME /tmp/docker-test/bsm-data 8080 - -# Start the TIM PPM in a new container. -startPPMContainer $PPM_TIM_CONTAINER_NAME /tmp/docker-test/tim-data 8081 - -# Produce the test data. -docker exec $PPM_BSM_CONTAINER_NAME /cvdi-stream/docker-test/do_bsm_test.sh $BSM_OFFSET -docker exec $PPM_TIM_CONTAINER_NAME /cvdi-stream/docker-test/do_tim_test.sh $TIM_OFFSET - -stopPPMContainer $PPM_BSM_CONTAINER_NAME -stopPPMContainer $PPM_TIM_CONTAINER_NAME diff --git a/unit-test-data/test-case.all.good.tims.json b/unit-test-data/test-case.all.good.tims.json deleted file mode 100644 index e87a51e..0000000 --- a/unit-test-data/test-case.all.good.tims.json +++ /dev/null @@ -1,3 +0,0 @@ -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.94911, "longitude": -83.928343, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949821, "longitude": -83.936279, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.951501, "longitude": -83.935851, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} diff --git a/unit-test-data/test-case.bad.speed.tims.json b/unit-test-data/test-case.bad.speed.tims.json deleted file mode 100644 index 484facc..0000000 --- a/unit-test-data/test-case.bad.speed.tims.json +++ /dev/null @@ -1,3 +0,0 @@ -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949811, "longitude": -83.92909, "speed": 0.5}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.951084, "longitude": -83.930725, "speed": 99.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949915, "longitude": -83.936186, "speed": 2.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} diff --git a/unit-test-data/test-case.inside.geofence.tims.json b/unit-test-data/test-case.inside.geofence.tims.json deleted file mode 100644 index c180272..0000000 --- a/unit-test-data/test-case.inside.geofence.tims.json +++ /dev/null @@ -1,7 +0,0 @@ -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.94911, "longitude": -83.928343, "speed": 3.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.952555, "longitude": -83.932468, "speed": 4.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949821, "longitude": -83.936279, "speed": 5.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.951501, "longitude": -83.935851, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949915, "longitude": -83.936186, "speed": 10.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949811, "longitude": -83.92909, "speed": 34.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.951084, "longitude": -83.930725, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} diff --git a/unit-test-data/test-case.outside.geofence.tims.json b/unit-test-data/test-case.outside.geofence.tims.json deleted file mode 100644 index 19bc928..0000000 --- a/unit-test-data/test-case.outside.geofence.tims.json +++ /dev/null @@ -1,6 +0,0 @@ -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.9493, "longitude": -83.927489, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.950668, "longitude": -83.931295, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.962259, "longitude": -83.914569, "speed": 22.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.949271, "longitude": -83.928893, "speed": 5.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.948337, "longitude": -83.928826, "speed": 32.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} -{"metadata": {"logFileName": "tim.uper", "odeReceivedAt": "2017-09-26T20:00:08.48Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 101, "heading": 40, "latitude": 35.953634, "longitude": -83.931646, "speed": 5.0}, "rxFrom": 0}, "recordGeneratedAt": "2017-07-14T15:46:47.707Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "receivedMsgRecord", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 0, "bundleSize": 1, "recordId": 0, "serialNumber": 0, "streamId": "90b148a2-4b30-46a1-9947-4084506847e8"}, "validSignature": true}, "payload": {"data": {"dataframes": [{"content": "Advisory", "crc": "0000000000000000", "durationTime": 1, "frameType": 1, "items": ["513"], "msgID": "RoadSignID", "mutcd": 5, "position": {"elevation": 917.1432, "latitude": 41.678473, "longitude": -108.782775}, "priority": 0, "regions": [{"anchorPosition": {"elevation": 2020.6969900289998, "latitude": 41.2500807, "longitude": -111.0093847}, "closedPath": false, "description": "path", "direction": "0000000000001010", "directionality": 3, "laneWidth": 7, "name": "Testing TIM", "path": {"nodes": [{"delta": "node-LL3", "nodeLat": 0.0014506, "nodeLong": 0.0031024}, {"delta": "node-LL3", "nodeLat": 0.0014568, "nodeLong": 0.0030974}, {"delta": "node-LL3", "nodeLat": 0.0014559, "nodeLong": 0.0030983}, {"delta": "node-LL3", "nodeLat": 0.0014563, "nodeLong": 0.003098}, {"delta": "node-LL3", "nodeLat": 0.0014562, "nodeLong": 0.0030982}], "scale": 0, "type": "ll"}, "regulatorID": 0, "segmentID": 33}], "sspLocationRights": 3, "sspMsgContent": 3, "sspMsgTypes": 2, "sspTimRights": 0, "startDateTime": "2017-08-02T22:25:00.000Z", "url": "null", "viewAngle": "1010101010101010"}], "index": 13, "msgCnt": 1, "packetID": 0, "timeStamp": "2016-08-03T22:25:36.297Z", "urlB": "null"}, "dataType": "us.dot.its.jpo.ode.plugin.j2735.J2735TravelerInformationMessage"}, "schemaVersion": 3} From 369969f40c07241a3f71ee7d82ff74b7db739956 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Mon, 24 Jun 2024 14:36:02 -0600 Subject: [PATCH 06/35] Added default value for PPM_CONFIG_FILE to `sample.env` --- sample.env | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sample.env b/sample.env index f23c2d0..36a36bb 100644 --- a/sample.env +++ b/sample.env @@ -5,9 +5,7 @@ DOCKER_HOST_IP= DOCKER_SHARED_VOLUME= # The config file to use. -# This only needs to be set when using `docker-compose.yml`. -# The `docker-compose-standalone.yml` & `docker-compose-confluent-cloud.yml` files directly set values for this. -PPM_CONFIG_FILE= +PPM_CONFIG_FILE=ppmBsm.properties # An directory that contains the fieldsToRedact.txt file needed by the RedactionPropertiesManager class. REDACTION_PROPERTIES_PATH= From 3e2861d788af20731bb03990b21d7aa3895ac866 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Mon, 24 Jun 2024 14:49:24 -0600 Subject: [PATCH 07/35] Added default value for REDACTION_PROPERTIES_PATH in `sample.env` --- .gitignore | 2 ++ sample.env | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f35d2df..e793742 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build docs/html /.env + +ppm_data/* \ No newline at end of file diff --git a/sample.env b/sample.env index 36a36bb..bb52c8d 100644 --- a/sample.env +++ b/sample.env @@ -4,11 +4,11 @@ DOCKER_HOST_IP= # An external directory that contains the necessary edges and properties files. DOCKER_SHARED_VOLUME= -# The config file to use. +# The config file to use. This file must be in the shared volume. PPM_CONFIG_FILE=ppmBsm.properties -# An directory that contains the fieldsToRedact.txt file needed by the RedactionPropertiesManager class. -REDACTION_PROPERTIES_PATH= +# The path to the fieldsToRedact.txt file needed by the RedactionPropertiesManager class. This file must be in the shared volume. +REDACTION_PROPERTIES_PATH=fieldsToRedact.txt # Logging related flags for file/console logging & log level PPM_LOG_TO_FILE=false From c090b6dc0cf50fb06302e967c9c289fff3601911 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Tue, 25 Jun 2024 08:29:40 -0600 Subject: [PATCH 08/35] Simplified docker compose instructions in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 44e7470..17ae7a3 100644 --- a/README.md +++ b/README.md @@ -165,8 +165,8 @@ The unit tests are also built when the solution is compiled. For information on ## Standalone Cluster The docker-compose.yml file is meant for local testing/troubleshooting. -To utilize this, pass the -f flag to the docker-compose command as follows: -> docker-compose -f docker-compose.yml up +To utilize this, run the following command in the root directory of the project: +> docker compose up Sometimes kafka will fail to start up properly. If this happens, spin down the containers and try again. @@ -195,7 +195,7 @@ This script should be run outside of the dev container in an environment where D It should be noted that this script needs to use the LF end-of-line sequence. ## Kafka Test Script -The [do_kafka_test.sh](./do_kafka_test.sh) script is designed to perform integration tests on a Kafka instance. To execute the tests, this script relies on the following scripts: standalone.sh and do_bsm_test.sh. +The [do_kafka_test.sh](./do_kafka_test.sh) script is designed to perform integration tests on a Kafka instance. To execute the tests, this script relies on the `standalone.sh` and `do_bsm_test.sh` scripts. To ensure proper execution, it is recommended to run this script outside of the dev container where docker is available. This is because the script will spin up a standalone kafka instance and will not be able to access the docker daemon from within the dev container. From a037927da3498977dad2b9c080319ca3efa939a7 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Tue, 25 Jun 2024 09:26:45 -0600 Subject: [PATCH 09/35] Removed OdeTimPayload check from `bsmHandler.cpp` --- src/bsmHandler.cpp | 51 +++------------------------------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/src/bsmHandler.cpp b/src/bsmHandler.cpp index 1101a2f..d39853e 100644 --- a/src/bsmHandler.cpp +++ b/src/bsmHandler.cpp @@ -136,7 +136,7 @@ bool BSMHandler::isWithinEntity(BSM &bsm) const { return false; } -bool BSMHandler::process( const std::string& bsm_json ) { +bool BSMHandler::process( const std::string& message_json ) { double speed = 0.0; double latitude = 0.0; double longitude = 0.0; @@ -150,7 +150,7 @@ bool BSMHandler::process( const std::string& bsm_json ) { // create the DOM // check for errors - if (document.Parse(bsm_json.c_str()).HasParseError()) { + if (document.Parse(message_json.c_str()).HasParseError()) { result_ = ResultStatus::PARSE; return false; @@ -318,54 +318,9 @@ bool BSMHandler::process( const std::string& bsm_json ) { handleGeneralRedaction(document); // uses fieldsToRedact.txt } - else if (payload_type_str == "us.dot.its.jpo.ode.model.OdeTimPayload") { - if (!metadata.HasMember("receivedMessageDetails")) { - result_ = ResultStatus::MISSING; - - return false; - } - - rapidjson::Value& received_details = metadata["receivedMessageDetails"]; - - if (!received_details.HasMember("locationData")) { - result_ = ResultStatus::MISSING; - - return false; - } - - rapidjson::Value& location = received_details["locationData"]; - - if (!location.HasMember("latitude") || !location.HasMember("longitude") || !location.HasMember("speed")) { - result_ = ResultStatus::MISSING; - - return false; - } - - if (!location["latitude"].IsDouble() || !location["longitude"].IsDouble() || !location["speed"].IsDouble()) { - result_ = ResultStatus::OTHER; - - return false; - } - - latitude = location["latitude"].GetDouble(); - longitude = location["longitude"].GetDouble(); - speed = location["speed"].GetDouble(); - - bsm_.set_latitude(latitude); - bsm_.set_longitude(longitude); - bsm_.set_velocity(speed); - - if (is_active() && !isWithinEntity(bsm_)) { - result_ = ResultStatus::GEOPOSITION; - } - - if (is_active() && vf_.suppress(speed)) { - result_ = ResultStatus::SPEED; - } - } else { + // Unsupported payload type result_ = ResultStatus::MISSING; - return false; } From 4707571cf0d75531a1eb9217f396f3df0bdaf742 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Tue, 25 Jun 2024 09:27:28 -0600 Subject: [PATCH 10/35] Removed a couple more references to TIMs in data & docs directories --- data/I_80_test_TIMS.json | 10 ---------- docs/dockerhub.md | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 data/I_80_test_TIMS.json diff --git a/data/I_80_test_TIMS.json b/data/I_80_test_TIMS.json deleted file mode 100644 index f6706b4..0000000 --- a/data/I_80_test_TIMS.json +++ /dev/null @@ -1,10 +0,0 @@ -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 41.738136, "longitude": -106.587029, "speed": 7.02}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 41.608656, "longitude": -109.226824, "speed": 7.12}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 41.311097, "longitude": -110.512927, "speed": 7.16}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 41.246647, "longitude": -111.027436, "speed": 7.44}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 41.600371, "longitude": -106.22341, "speed": 7.44}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 42.29789, "longitude": -83.72035, "speed": 1.78}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 42.29789, "longitude": -83.72034, "speed": 0.7}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 42.24576, "longitude": -83.62337, "speed": 6.86}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 42.24576, "longitude": -83.62337, "speed": 6.84}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} -{"metadata": {"logFileName": "rxMsg_1507764909_2001_3A470_3A41af_3A1_3A226_3Aadff_3Afe05_3A2561.csv", "odeReceivedAt": "2017-12-06T23:15:59.596Z[UTC]", "payloadType": "us.dot.its.jpo.ode.model.OdeTimPayload", "receivedMessageDetails": {"locationData": {"elevation": 1484, "heading": 193.6875, "latitude": 42.24576, "longitude": -83.62338, "speed": 6.74}, "rxSource": "RSU"}, "recordGeneratedAt": "2017-10-11T23:35:09.924Z[UTC]", "recordGeneratedBy": "OBU", "recordType": "rxMsg", "sanitized": false, "schemaVersion": 3, "serialId": {"bundleId": 6, "bundleSize": 1, "recordId": 2, "serialNumber": 0, "streamId": "fef1b210-858b-4444-b0a9-72d4da9ad275"}, "validSignature": false}, "payload": {"data": {"MessageFrame": {"messageId": 31, "value": {"TravelerInformation": {"dataFrames": {"TravelerDataFrame": {"content": {"advisory": {"SEQUENCE": {"item": {"itis": 513}}}}, "duratonTime": 1440, "frameType": {"advisory": ""}, "msgId": {"roadSignID": {"mutcdCode": {"warning": ""}, "position": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "viewAngle": 1111111111111111}}, "priority": 5, "regions": {"GeographicalPath": {"anchor": {"elevation": 4096, "lat": 404725418, "long": -1049700560}, "closedPath": {"false": ""}, "description": {"path": {"offset": {"xy": {"nodes": {"NodeXY": [{"delta": {"node-LatLon": {"lat": 404735394, "lon": -1049701440}}}, {"delta": {"node-LatLon": {"lat": 404732984, "lon": -1049700032}}}, {"delta": {"node-LatLon": {"lat": 404730975, "lon": -1049699856}}}, {"delta": {"node-LatLon": {"lat": 404718186, "lon": -1049701616}}}, {"delta": {"node-LatLon": {"lat": 404716111, "lon": -1049702321}}}, {"delta": {"node-LatLon": {"lat": 404715307, "lon": -1049702937}}}]}}}}}, "direction": 1111111111111111, "directionality": {"both": ""}, "laneWidth": 800}}, "sspLocationRights": 1, "sspMsgRights1": 1, "sspMsgRights2": 1, "sspTimRights": 1, "startTime": 401108, "startYear": 2017}}, "msgCnt": 1, "packetID": "000000000018099C39"}}}}, "dataType": "TravelerInformation"}} diff --git a/docs/dockerhub.md b/docs/dockerhub.md index 931d8a3..d474afd 100644 --- a/docs/dockerhub.md +++ b/docs/dockerhub.md @@ -46,7 +46,7 @@ services: environment: KAFKA_ADVERTISED_HOST_NAME: ${DOCKER_HOST_IP} KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1,topic.OdeTimJson:1:1,topic.FilteredOdeTimJson:1:1" + KAFKA_CREATE_TOPICS: "topic.OdeBsmJson:1:1,topic.FilteredOdeBsmJson:1:1" volumes: - /var/run/docker.sock:/var/run/docker.sock From a17ba424b31de8f0ea00de5e8d50ff7a3f306ed7 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 12:27:31 -0600 Subject: [PATCH 11/35] Removed broken Travis / SONAR badges at top of README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 17ae7a3..a8294aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -Master: [![Build Status](https://travis-ci.org/usdot-jpo-ode/jpo-cvdp.svg?branch=master)](https://travis-ci.org/usdot-jpo-ode/jpo-cvdp) [![Quality Gate](https://sonarqube.com/api/badges/gate?key=jpo-cvdp-key)](https://sonarqube.com/dashboard?id=jpo-cvdp-key) - # jpo-cvdp The United States Department of Transportation Joint Program Office (JPO) From 7b1327ac6bd15e6888388593a551c41808b9c6d3 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 12:37:51 -0600 Subject: [PATCH 12/35] Adjusted headers & updated table of contents in README --- README.md | 87 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index a8294aa..aa63e1a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # jpo-cvdp - The United States Department of Transportation Joint Program Office (JPO) -Connected Vehicle Data Privacy (CVDP) Project is developing a variety of methods -to enhance the privacy of individuals who generated connected vehicle data. +Connected Vehicle Data Privacy (CVDP) project is developing a variety of methods +to enhance the privacy of individuals who generate connected vehicle data. Connected vehicle technology uses in-vehicle wireless transceivers to broadcast and receive basic safety messages (BSMs) that include accurate spatiotemporal @@ -17,15 +16,14 @@ important.** Developing procedures that minimize the risk of associating trajectories with individuals is the objective of this project. # The Operational Data Environment (ODE) Privacy Protection Module (PPM) - The PPM operates on streams of raw BSMs processed by the ODE. It determines whether individual BSMs should be retained or suppressed (deleted) based on the information in that BSM and auxiliary map information used to define a geofence. BSM geoposition (latitude and longitude) and speed are used to determine the -disposition of each BSM processed. The PPM also redacts other BSM fields. +disposition of each BSM processed. Additionally, the PPM redacts a configurable +set of fields from the BSMs that are retained to further protect privacy. ## PPM Limitations - Protecting against inference-based privacy attacks on spatiotemporal trajectories (i.e., sequences of BSMs from a single vehicle) in **general** is a challenging task. An example of an inference-based privacy attack is @@ -43,21 +41,24 @@ loitering locations can aid in learning the identity of the driver. It should be noted that the PPM is not designed to handle other message types at this time. Future versions of the PPM may support additional message types. ## Table of Contents - 1. [Release Notes](#release-notes) 2. [Documentation](#documentation) 3. [Development and Collaboration Tools](#development-and-collaboration-tools) -3. [Getting Started](#getting-started) -4. [Installation](docs/installation.md) -5. [Configuration and Operation](docs/configuration.md) -6. [Testing](docs/testing.md) -7. [Development](docs/coding-standards.md) -8. [Confluent Cloud Integration](#confluent-cloud-integration) +4. [Getting Started](#getting-started) +5. [Confluent Cloud Integration](#confluent-cloud-integration) +6. [Testing/Troubleshooting](#testingtroubleshooting) +7. [General Redaction](#general-redaction) + +### Additional Resources +1. [Installation](docs/installation.md) +2. [Configuration and Operation](docs/configuration.md) +3. [Testing](docs/testing.md) +4. [Development](docs/coding-standards.md) ## Release Notes The current version and release history of the Jpo-cvdp: [Jpo-cvdp Release Notes]() -# Documentation +## Documentation The following document will help practitioners build, test, deploy, and understand the PPM's functions: @@ -68,7 +69,7 @@ to the JPO Product Owner at DOT, FHWA, or JPO. To provide feedback, we recommend repository (https://github.com/usdot-jpo-ode/jpo-cvdp/issues). You will need a GitHub account to create an issue. If you don’t have an account, a dialog will be presented to you to create one at no cost. -## Code Documentation +### Code Documentation Code documentation can be generated using [Doxygen](https://www.doxygen.org) by following the commands below: @@ -81,28 +82,28 @@ $ doxygen The documentation is in HTML and is written to the `/jpo-cvdp/docs/html` directory. Open `index.html` in a browser. -## Class Usage Diagram +### Class Usage Diagram ![class usage](./docs/diagrams/class-usage/PPM%20Class%20Usage.drawio.png) This diagram displays how the different classes in the project are used. If one class uses another class, there will be a black arrow pointing to the class it uses. The Tool class is extended by the PPM class, which is represented by a white arrow. -# Development and Collaboration Tools +## Development and Collaboration Tools -## Source Repositories - GitHub +### Source Repositories - GitHub - https://github.com/usdot-jpo-ode/jpo-cvdp - `git@github.com:usdot-jpo-ode/jpo-cvdp.git` -## Agile Project Management - Jira +### Agile Project Management - Jira https://usdotjpoode.atlassian.net/secure/Dashboard.jspa -## Continuous Integration and Delivery +### Continuous Integration and Delivery The PPM is tested using [Travis Continuous Integration](https://travis-ci.org). -# Getting Started +## Getting Started -## Prerequisites +### Prerequisites You will need Git to obtain the code and documents in this repository. Furthermore, we recommend using Docker to build the necessary containers to @@ -113,7 +114,7 @@ build, test, and experiment with the PPM. The [Docker](#docker) instructions can You can find more information in our [installation and setup](docs/installation.md) directions. -## Getting the Source Code +### Getting the Source Code See the installation and setup instructions unless you just want to examine the code. @@ -133,34 +134,34 @@ git config --global core.autocrlf false git clone https://github.com/usdot-jpo-ode/jpo-cvdp.git ``` -# Confluent Cloud Integration +## Confluent Cloud Integration Rather than using a local kafka instance, this project can utilize an instance of kafka hosted by Confluent Cloud via SASL. -## Environment variables -### Purpose & Usage +### Environment variables +#### Purpose & Usage - The DOCKER_HOST_IP environment variable is used to communicate with the bootstrap server that the instance of Kafka is running on. - The KAFKA_TYPE environment variable specifies what type of kafka connection will be attempted and is used to check if Confluent should be utilized. If this is not set to "CONFLUENT", the PPM will attempt to connect to a local kafka instance. - The CONFLUENT_KEY and CONFLUENT_SECRET environment variables are used to authenticate with the bootstrap server. These are the API key and secret that are generated when a new API key is created in Confluent Cloud. These are only used if the KAFKA_TYPE environment variable is set to "CONFLUENT". -### Values +#### Values - DOCKER_HOST_IP must be set to the bootstrap server address (excluding the port) - KAFKA_TYPE must be set to "CONFLUENT" - CONFLUENT_KEY must be set to the API key being utilized for CC - CONFLUENT_SECRET must be set to the API secret being utilized for CC -## CC Docker Compose File +### CC Docker Compose File There is a provided docker-compose file (docker-compose-confluent-cloud.yml) that passes the above environment variables into the container that gets created. Further, this file doesn't spin up a local kafka instance since it is not required. -## Note +### Note This has only been tested with Confluent Cloud but technically all SASL authenticated Kafka brokers can be reached using this method. -# Testing/Troubleshooting -## Unit Tests +## Testing/Troubleshooting +### Unit Tests Unit tests can be built and executed using the build_and_run_unit_tests.sh file inside of the dev container for the project. More information about this can be found [here](./docs/testing.md#utilizing-the-build_and_run_unit_testssh-script). The unit tests are also built when the solution is compiled. For information on that, check out [this section](./docs/testing.md#unit-testing). -## Standalone Cluster +### Standalone Cluster The docker-compose.yml file is meant for local testing/troubleshooting. To utilize this, run the following command in the root directory of the project: @@ -168,7 +169,7 @@ To utilize this, run the following command in the root directory of the project: Sometimes kafka will fail to start up properly. If this happens, spin down the containers and try again. -### Data & Config Files +#### Data & Config Files Data and config files are expected to be in a location pointed to by the DOCKER_SHARED_VOLUME environment variable. At this time, the PPM assumes that this location is the /ppm_data directory. When run in a docker or k8s solution, an external drive/directory can be mounted to this directory. @@ -177,22 +178,22 @@ In a BSM configuration, the PPM requires the following files to be present in th - *.edges - ppmBsm.properties -#### fieldsToRedact.txt +##### fieldsToRedact.txt The path to this file is specified by the REDACTION_PROPERTIES_PATH environment variable. If this is not set, field redaction will not take place but the PPM will continue to function. If this is set and the file is not found, the same behavior will occur. When running the project in the provided dev container, the REDACTION_PROPERTIES_PATH environment variable should be set to the project-level fieldsToRedact.txt file for debugging/experimentation purposes. This is located in /workspaces/jpo-cvdp/config/fieldsToRedact.txt from the perspective of the dev container. -#### RPM Debug +##### RPM Debug If the RPM_DEBUG environment variable is set to true, debug messages will be logged to a file by the RedactionPropertiesManager class. This will allow developers to see whether the environment variable is set, whether the file was found and whether a non-zero number of redaction fields were loaded in. -## Build & Exec Script +### Build & Exec Script The [`build_and_exec.sh`](./build_and_exec.sh) script can be used to build a tagged image of the PPM, run the container & enter it with an interactive shell. This script can be used to test the PPM in a standalone environment. This script should be run outside of the dev container in an environment where Docker is available. It should be noted that this script needs to use the LF end-of-line sequence. -## Kafka Test Script +### Kafka Test Script The [do_kafka_test.sh](./do_kafka_test.sh) script is designed to perform integration tests on a Kafka instance. To execute the tests, this script relies on the `standalone.sh` and `do_bsm_test.sh` scripts. To ensure proper execution, it is recommended to run this script outside of the dev container where docker is available. This is because the script will spin up a standalone kafka instance and will not be able to access the docker daemon from within the dev container. @@ -212,20 +213,20 @@ export DOCKER_HOST_IP=$(ifconfig | zgrep -m 1 -oP '(?<=inet\s)\d+(\.\d+){3}') WSL will sometimes hang while the script waits for kafka to create topics. The script should exit after a number of attempts, but if it does not, running `wsl --shutdown` in a windows command prompt and restarting the docker services is recommended. -## Some Notes +### Some Notes - The tests for this project can be run after compilation by running the "ppm_tests" executable. - When manually compiling with WSL, librdkafka will sometimes not be recognized. This can be resolved by utilizing the provided dev environment. -# General Redaction +## General Redaction General redaction refers to redaction functionality in the BSMHandler that utilizes the 'fieldsToRedact.txt' file to redact specified fields from BSM messages. -## How to specify the fields to redact +### How to specify the fields to redact The fieldsToRedact.txt file is used by the BSMHandler and lists the paths to the fields to be redacted. It should be noted that this file needs to use the LF end-of-line sequence. -### How are fields redacted? +#### How are fields redacted? The paths in the fieldsToRedact.txt file area are added to a list and then used to search for the fields in the BSM message. If a member is found, the default behavior is to remove it with rapidjson's RemoveMember() function. It should be noted that by default, only leaf members are able to be removed. There are some exceptions to this which are listed in the [Overridden Redaction Behavior](#overridden-redaction-behavior) section. -## Overridden Redaction Behavior +### Overridden Redaction Behavior Some values will be treated differently than others when redacted. For example, the 'coreData.angle' field will be set to 127 instead of being removed since it is a required field. The following table lists the overridden redaction behavior. | Field | Redaction Behavior | @@ -242,5 +243,5 @@ Some values will be treated differently than others when redacted. For example, | brakeBoost | Set to "unavailable" | | auxBrakes | Set to "unavailable" | -### Bitstrings +#### Bitstrings Since it would be incorrect for a bitstring to be missing bits, the PPM will remove the entire bitstring if any of its bits are redacted. This is done by removing the parent object. For example, if the 'partII.value.lights.lowBeamHeadlightsOn' field is redacted, the 'partII.value.lights' object will be removed. From 534262e0d2b1b202b625a42a31bf06046e41822b Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 12:50:34 -0600 Subject: [PATCH 13/35] Updated doxygen installation instructions in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aa63e1a..5afe798 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,8 @@ don’t have an account, a dialog will be presented to you to create one at no c Code documentation can be generated using [Doxygen](https://www.doxygen.org) by following the commands below: ```bash -$ sudo apt install doxygen +$ sudo apt-get update +$ sudo apt-get install doxygen $ cd /jpo-cvdp $ doxygen ``` From 78ef6e67cd16170d69927b97b83b8fcb10655341 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 12:54:32 -0600 Subject: [PATCH 14/35] Updated PPM Class Usage diagram --- docs/.gitignore | 1 + .../class-usage/PPM Class Usage.drawio | 174 +++++++++++++++++- .../class-usage/PPM Class Usage.drawio.png | Bin 39684 -> 64694 bytes 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 docs/.gitignore diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..6092ad9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +*/*/*.bkp \ No newline at end of file diff --git a/docs/diagrams/class-usage/PPM Class Usage.drawio b/docs/diagrams/class-usage/PPM Class Usage.drawio index 64f6f9f..48d7095 100644 --- a/docs/diagrams/class-usage/PPM Class Usage.drawio +++ b/docs/diagrams/class-usage/PPM Class Usage.drawio @@ -1 +1,173 @@ -7VxRd6o4EP41nrP74B5JQPSxWm27t73rvd617VNPKqnSAqEQq95fv0FAIKHU2kJk7VPJkCD5vsnMZDK0Afv26sxD7vyKGNhqgJaxasDTBgBAaWnsTyBZh5K22g0FM880QpGSCMbmbxwJW5F0YRrYz3SkhFjUdLPCKXEcPKUZGfI8ssx2eyBW9lddNMOCYDxFlii9Ng06D6UdoCfyc2zO5vEvK+1ofjaKO0cz8efIIMuUCA4asO8RQsMre9XHVgBejMv1xfraunxqn/39w39G//a+/fo+aYYPG75nyHYKHnbo3o+mmj+50YbG5Z07GoG76/vhzVn86BdkLSK8Bs7CPkf+PJozXcdAemThGDh4WKsBe8u5SfHYRdPg7pKpDpPNqW2xlsIuH0zL6hOLeJuxsN8faEP2Yr0dZxK/FvYoXqV4jGZ2homNqbdmXaK7sKWGQyIthe2ItGXCuRITOU/xHfdDkZrNto9OoGQXEZrvQBYKyF4YP7GBppSBUi9sY9xiC9CSja0qYDvBFpmadD00LYprhq/S1g4MX03Atze+qheoTQCyqMK4XQWqVn/xAGeg0713bXPyT/vMhaQJBASxwVxV1CQenZMZcZA1SKS9LMZJn0tC3AjZR0zpOvK7aEFJFne8MulNMPwvNv+weZu6dbqKHr1prOOGw+YbjtLi5m36XjJs04rHvY9Mnyy8KS5Qw8gRU+TNMC0ANnpegGahanjYQtR8yQYGeTRvhp54HlqnOrjEdKifevIoEKSWsZI1kxrg/DHXH2pF3dlF+AKJvm1nsr8KQqkqmNK/lDrmq2D5qlRk+SSrUpPzuFDXClVJ6K9p5euSLtec7W7NJKmS+tmq9CHn05HLlv5/cD5Fcb5s56NnLYDaecP5dAr7Zy2GMFrlRmsaFzOFqEWjOM39BNOjKDK0WZKCfXp0s6sd+T2+O9Hh7Tlmi+zGVskLfdQPKYIoDiC2ZkTJGJHEpkgzI7m4goMwI02VCyS6xWaE719oRvZY/UWQZnen58gxrLrt/DkzWmnSKn9/IMWwpjy+/i6XfwDRoKLuaMa7B7HAARcnQKV4gfNxBde/nJ2FmJT+iVzTePSJU88MalPbIYOqV7rQ5fjxZKGDvRa60tg9ANh/oS/BuKsub4E21CYX183Tl7U5jg5XZK1zMQTPZo8UPgIP51NaBA6PYPf/ITUoJ5EkBmB81hGouynCuyPDNvc7+huRIX+qwQ0oyXMAwXXgFcWO4Qvqykw3zWqXTz3yhGO/4BAHc64iEiHLnDmsOWUaxSJO2AscgTlF1kl0wzYNY6PweW4ouwjK8ji8n9+GkSmP085xOLAsh6OI56ELHx8dLa+t1gpoyd12tgRWZNj1/W10vq5psoz0x5aIeOx6hEukCbkdR4VLJJ+W9hctCeRxogfKZkX/YoXd7WaDLFWTTUvnixbRzcOOWHtSLS1izvQIaVE417I9xpJFCxAL2QYrZLsWA9zxFzbu3wsUHXaii09pS89oA9FPXK2D+tYRYvpJTaa9Xu1R3uYOq4C5edLXhqNT7blFJ9bUfT5f3Tzm7COO0L5wVThV7rlzSRHz6EdISpPLhGx9szRf3BIYqGCL7c+RG9x5sPDqJPisooSjsCj2e7uiQVpplDHWv3de1McrY2kPyGR1sQST+Og9fdy0OWVivmHkETdQaexfIQfN6na+3AS7lJaXdfCUC7YY8RyjTeID0VZ1+4NcVg6oqkeXXRdcZCNkl+eAfL3ZtS44rtN7/dDmlcK+zzq0KSp9SpmEkWtfklkNzS14Ox1TWlyei+2XuQ146XLLJudToLLMbWEAlFb5Uc0+sOJzXJXuQQvjzxSqvwixagprFTDmVjjIKQHcJxb4WIkv58vLrREpyE1VsO3JfUtwODy/8S1Y7XlWZfIspua+oYcnFGWYSw2xBLO3K/ivu3KFT7IpFX7Wm4tvDQtbyuCFK3Gt8MQrl5UaBr4VsKLokmkRS1z+sJHpMNHUQr7/55ERBLnvZ5ScZZOXntuDH9ZM/lVNuHVP/uEPHPwH \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/diagrams/class-usage/PPM Class Usage.drawio.png b/docs/diagrams/class-usage/PPM Class Usage.drawio.png index 9741bdcdfb059664e7ae6a066baed97efd4d65da..d86749bd98e0dbed6e097346e74760d74fed95ed 100644 GIT binary patch literal 64694 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 NlDwK+(M{u^{{^#TK4bs@ literal 39684 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 T_2|+m@K0V^8CH1j(ewWUw#4Rk From 6ad4f1508f9a6379aa526cd5bcd6d9fda3e4bf59 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 12:56:39 -0600 Subject: [PATCH 15/35] Removed sections on Jira & Travis from README --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 5afe798..7414cc2 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,6 @@ This diagram displays how the different classes in the project are used. If one - https://github.com/usdot-jpo-ode/jpo-cvdp - `git@github.com:usdot-jpo-ode/jpo-cvdp.git` -### Agile Project Management - Jira -https://usdotjpoode.atlassian.net/secure/Dashboard.jspa - -### Continuous Integration and Delivery - -The PPM is tested using [Travis Continuous Integration](https://travis-ci.org). - ## Getting Started ### Prerequisites From a0052b92f8cb8e0268fd0edb2816e0a240566b42 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:02:48 -0600 Subject: [PATCH 16/35] Added note on running unit tests from inside deployed container to README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7414cc2..19fa511 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ This diagram displays how the different classes in the project are used. If one You will need Git to obtain the code and documents in this repository. Furthermore, we recommend using Docker to build the necessary containers to -build, test, and experiment with the PPM. The [Docker](#docker) instructions can be found in that section. +build, test, and experiment with the PPM. - [Git](https://git-scm.com/) - [Docker](https://www.docker.com) @@ -208,8 +208,8 @@ export DOCKER_HOST_IP=$(ifconfig | zgrep -m 1 -oP '(?<=inet\s)\d+(\.\d+){3}') WSL will sometimes hang while the script waits for kafka to create topics. The script should exit after a number of attempts, but if it does not, running `wsl --shutdown` in a windows command prompt and restarting the docker services is recommended. ### Some Notes -- The tests for this project can be run after compilation by running the "ppm_tests" executable. -- When manually compiling with WSL, librdkafka will sometimes not be recognized. This can be resolved by utilizing the provided dev environment. +- The tests for this project can be run after compilation by running the "ppm_tests" executable. An easy way to do this is to run the `build_and_exec.sh` script and then run the executable from within the container, which should be located in the /cvdi-stream-build directory. +- When manually compiling with WSL, librdkafka will sometimes not be recognized. This can be avoided by utilizing the provided dev environment. ## General Redaction General redaction refers to redaction functionality in the BSMHandler that utilizes the 'fieldsToRedact.txt' file to redact specified fields from BSM messages. From 29946d8a28f29c6fbd56061157a2e810a6dc945e Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:24:36 -0600 Subject: [PATCH 17/35] Adjusted headers & added a table of contents in `configuration.md` --- docs/configuration.md | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 586cac4..6616b07 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,4 +1,15 @@ -# PPM Operation +# Configuration +## Table of Contents +1. [PPM Operation](#ppm-operation) +2. [PPM Command Line Options](#ppm-command-line-options) +3. [PPM Deployment](#ppm-deployment) +4. [PPM Kafka Limitations](#ppm-kafka-limitations) +5. [Multiple PPM Instances with Different Configurations](#multiple-ppm-instances-with-different-configurations) +6. [PPM Logging](#ppm-logging) +7. [PPM Configuration](#ppm-configuration) +8. [Map Files](#map-files) + +## PPM Operation The messages suppressed and sanitized by the PPM are documented [here](https://github.com/usdot-jpo-ode/jpo-ode/blob/develop/docs/metadata_standards.md). @@ -9,7 +20,7 @@ The PPM suppresses BSMs and TIMs message and redacts BSM ID fields based on seve 3. Message location is outside of a prescribed geofence. 4. BSM TemporaryID can be redacted (rendered indistinct). -## PPM Command Line Options +### PPM Command Line Options The PPM can be started by specifying only the configuration file. Command line options are also available. **Command line options override parameters specified in the configuration file.** The following command line options are available: @@ -31,7 +42,7 @@ line options override parameters specified in the configuration file.** The foll -m | --mapfile : The path to the map file to use to build the geofence. ``` -# PPM Deployment +## PPM Deployment Once the PPM is [installed and configured](installation.md) it operates as a background service. The PPM can be started before or after other services. If started before the other services, it may produce some error messages while it waits @@ -43,7 +54,7 @@ $ ./ppm -c We recommend reviewing the [testing documentation](testing.md) for more details on running the PPM. -# PPM Kafka Limitations +## PPM Kafka Limitations With regard to the Apache Kafka architecture, each PPM process does **not** provide a way to take advantage of Kafka's scalable architecture. In other words, each PPM process will consume data from a single Kafka topic and a single partition within @@ -51,13 +62,13 @@ that topic. One way to consume topics with multiple partitions is to launch one configuration file will allow you to designate the partition. In the future, the PPM may be updated to automatically handle multiple partitions within a single topic. -# Multiple PPM Instances with Different Configurations +## Multiple PPM Instances with Different Configurations Nothing prevents a users from launching multiple PPM instances where each uses a different configuration file. This strategy would allow various degrees of privacy protection. It would also allow a user to publish various versions of the data to different "filtered" topics. -# PPM Logging +## PPM Logging PPM operations are optionally logged to the console or a file. The file is a rotating log file, i.e., a set number of log files will be used to record the PPM's information. By default, the file is in a `logs` directory from where the ACM is launched and the file is @@ -84,7 +95,7 @@ with a date and time stamp and the level of the log message. [170613 12:25:47.443150] [info] BSM [SUPPRESSED-speed]: (ON-VBL--,36712,41.116496,-104.888494,1.000000) ``` -# PPM Configuration +## PPM Configuration The PPM configuration file is a text file with a specific format. It can be used to configure Kafka as well as the PPM. Comments can be added to the configuration file by starting a line with the '#' character. Configuration lines consist @@ -103,7 +114,7 @@ Example configuration files can be found in the [jpo-cvdp/config](../config) dir The details of the settings and how they affect the function of the PPM follow: -## Sanitization Flag +### Sanitization Flag The current JSON data object sent to the PPM and published by the PPM contains the following three named components: parts: @@ -123,7 +134,7 @@ for features that may cause it to be suppressed. The same analysis is done on th The JSON format published by the PPM follows the format received. It may be completely suppressed or certain fields may be modifed as described in this second and the sections that follow. -## Velocity Filtering +### Velocity Filtering - `privacy.filter.velocity` : enables or disables message filtering based on the speed within the message. - `ON` : enables message filtering. @@ -135,7 +146,7 @@ be modifed as described in this second and the sections that follow. - `privacy.filter.velocity.max` : *When velocity fitering is enabled*, messages having velocities above this value will be suppressed. The units are in meters per second. -## BSM Identifier Redaction +### BSM Identifier Redaction If required, the `TemporaryID` field in the BSM can be redacted and replaced with a randomly chosen identifier. The following configuration parameters control identifier redaction. @@ -156,7 +167,7 @@ control identifier redaction. - Similar to the `privacy.redaction.id.value`, these are 4 hexadecimal-encoded bytes. - More than one id can be specified by separating them by commas. -## BSM Vehicle Size Redaction +### BSM Vehicle Size Redaction If required, the `VehicleLength` and `VehicleWidth` fields in the BSM can be redacted and replaced with a **0** value. The following configuration parameters control vehicle size redaction. @@ -165,7 +176,7 @@ control vehicle size redaction. - `ON` : enables redaction - Any other value : disables redaction. -## Geofencing +### Geofencing Messages can be suppressed based on latitude and longitude attributes. If this capability is turned one through the configuration file, each edge defined in the @@ -187,7 +198,7 @@ determine the size of the rectange. of the controls that determines the size of the component geofences that surround road segments. See the [Map Files](#geofencing) section. -### Geofence Region Boundaries +#### Geofence Region Boundaries Geofence Boundary Configuration Parameters: The geofence is stored in a geographically-defined data structured called a quadtree. The following bounding box coordinates define the quadtree's region. The data that is stored in this data @@ -202,7 +213,7 @@ instead of having to modify the mapfile. - `privacy.filter.geofence.ne.lat` : The latitude of the upper-right corner of the quadtree region. - `privacy.filter.geofence.ne.lon` : The longitude of the upper-right corner of the quadtree region. -## ODE Kafka Interface +### ODE Kafka Interface - `privacy.topic.producer` : The Kafka topic name where the PPM will write the filtered messages. **The name is case sensitive.** @@ -227,7 +238,7 @@ instead of having to modify the mapfile. - `compression.type` : The type of compression to use for writing to Kafka topics. Currently, this should be set to none. -# Map Files +## Map Files The map file is used to define the geofence. It defines a set of shapes, one per line. For road geofence use, the edge shape is used. The map file for the @@ -254,5 +265,5 @@ This file has four comma-separated elements: For the WYDOT use case, WYDOT provided a set of edge definitions for I-80 that were converted into the above format. -## See Also: Data & Config Files +### See Also: Data & Config Files More information on config files can be found in the [Data & Config Files](../README.md#data--config-files) section of the README. \ No newline at end of file From 57d02143f78d510dd59df0271d5a4a4ef901ed0a Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:27:54 -0600 Subject: [PATCH 18/35] Fixed broken link in 'PPM Operation' section of `configuration.md` --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6616b07..b42ac06 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,7 @@ ## PPM Operation -The messages suppressed and sanitized by the PPM are documented [here](https://github.com/usdot-jpo-ode/jpo-ode/blob/develop/docs/metadata_standards.md). +The messages suppressed and sanitized by the PPM are documented [here](../README.md#supported-message-types). The PPM suppresses BSMs and TIMs message and redacts BSM ID fields based on several conditions. These conditions are determined by a set of configuration parameters. The following conditions will result in a message being suppressed, or deleted, from the stream. From fbe1cc5ad4437371909d1828f5201390b8b63d10 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:28:44 -0600 Subject: [PATCH 19/35] Removed mention of TIMs from `configuration.md` --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 586cac4..ddebab0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,7 +2,7 @@ The messages suppressed and sanitized by the PPM are documented [here](https://github.com/usdot-jpo-ode/jpo-ode/blob/develop/docs/metadata_standards.md). -The PPM suppresses BSMs and TIMs message and redacts BSM ID fields based on several conditions. These conditions are determined by a set of configuration parameters. The following conditions will result in a message being suppressed, or deleted, from the stream. +The PPM suppresses BSMs and redacts BSM ID fields based on several conditions. These conditions are determined by a set of configuration parameters. The following conditions will result in a message being suppressed, or deleted, from the stream. 1. Message JSON record cannot be parsed. 2. Message speed is outside of prescribed limits. From 6bd348a6fcb3c0ef08f22df34981f13693a639eb Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:30:01 -0600 Subject: [PATCH 20/35] Corrected fourth suppression condition in `configuration.md` --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index cdab891..9b3c1dc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -18,7 +18,7 @@ The PPM suppresses BSMs and redacts BSM ID fields based on several conditions. T 1. Message JSON record cannot be parsed. 2. Message speed is outside of prescribed limits. 3. Message location is outside of a prescribed geofence. -4. BSM TemporaryID can be redacted (rendered indistinct). +4. BSM TemporaryID cannot be redacted (rendered indistinct). ### PPM Command Line Options From 101c2dcef7ec392a98801286b2807f83ea3f89c9 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:41:13 -0600 Subject: [PATCH 21/35] Corrected some typos in `configuration.md` --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9b3c1dc..1d3cbc9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -179,7 +179,7 @@ control vehicle size redaction. ### Geofencing Messages can be suppressed based on latitude and longitude attributes. If this -capability is turned one through the configuration file, each edge defined in the +capability is turned on through the configuration file, each edge defined in the map file is used to infer a *component* geofence that surrounds that segment of the road. The image below illustrates how a *rectange* is drawn to form the segment's geofence. The aforementioned edge attributes and PPM configuration parameters @@ -228,7 +228,7 @@ instead of having to modify the mapfile. themselves with a consumer group name, and each record published to a topic is delivered to one consumer instance within each subscribing consumer group. Consumer instances can be in separate processes or on separate machines. **Due to the way the kafka library - internally updates its topic offsets, the group ID must be unique for each the topic.** + internally updates its topic offsets, the group ID must be unique for each topic.** - `privacy.kafka.partition` : The partition(s) that this PPM will consume records from. A Kafka topic can be divided, or partitioned, into several "parallel" streams. A topic may have many partitions so it can handle an arbitrary From 09b12378588867e4f191b10a3d6377d733a4b39d Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 13:49:24 -0600 Subject: [PATCH 22/35] Adjusted headers & added table of contents in `installation.md` --- docs/installation.md | 47 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index b5d2806..4559d0c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,15 +1,24 @@ # Installation and Setup +## Table of Contents +1. [Docker Installation](#docker-installation) +1. [Manual Installation](#manual-installation) +1. [Integrating with the ODE](#integrating-with-the-ode) +1. [CDOT Integration with K8s](#cdot-integration-with-k8s) +## Docker Installation +(TBD) + +## Manual Installation The following instructions represent the "hard" way to install and test the PPM. A docker image can be built to make this easier: [Using the Docker Container](#using-the-docker-container). *The directions that follow were developed for a clean installation of Ubuntu.* -## 1. Install [Git](https://git-scm.com/) +### 1. Install [Git](https://git-scm.com/) ```bash $ sudo apt install git ``` -## 2. Install Oracle’s Java +### 2. Install Oracle’s Java ```bash $ sudo add-apt-repository -y ppa:webupd8team/java @@ -18,13 +27,13 @@ $ sudo apt install oracle-java8-installer -y $ sudo java -version ``` -## 3. Install [CMake](https://cmake.org) to build the PPM +### 3. Install [CMake](https://cmake.org) to build the PPM ```bash $ sudo apt install cmake ``` -## 4. Install [Docker](https://www.docker.com) +### 4. Install [Docker](https://www.docker.com) - When following the website instructions, setup the Docker repos and follow the Linux post-install instructions. - The CE version seems to work fine. @@ -49,30 +58,30 @@ $ sudo apt install cmake ``` - NOTE: The DNS IP addresses are ORNL specific. -## 5. Restart the docker daemon to consume the new configuration file. +### 5. Restart the docker daemon to consume the new configuration file. ```bash $ service docker stop $ service docker start ``` -## 6. Check the configuration using the command below to confirm the updates above are taken if needed: +### 6. Check the configuration using the command below to confirm the updates above are taken if needed: ```bash $ docker info ``` -## 7. Install Docker Compose +### 7. Install Docker Compose - Comprehensive instructions can be found on this [website](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-16-04) - Follow steps 1 and 2. -## 8. Create a base directory from which to install all the necessary components to test the PPM. +### 8. Create a base directory from which to install all the necessary components to test the PPM. ```bash $ export BASE_PPM_DIR=~/some/dir/you/want/to/put/this/stuff ``` -## 9. Install [`kafka-docker`](https://github.com/wurstmeister/kafka-docker) so kafka and zookeeper can run in a separate container. +### 9. Install [`kafka-docker`](https://github.com/wurstmeister/kafka-docker) so kafka and zookeeper can run in a separate container. - Get your host IP address. The address is usually listed under an ethernet adapter, e.g., `en`. @@ -102,7 +111,7 @@ $ cd $BASE_PPM_DIR/kafka-docker $ docker-compose down ``` -## 10. Download and install the Kafka **binary**. +### 10. Download and install the Kafka **binary**. - The Kafka binary provides a producer and consumer tool that can act as surrogates for the ODE (among other items). - [Kafka Binary](https://kafka.apache.org/downloads) @@ -116,7 +125,7 @@ $ tar -xzf kafka_2.12-0.10.2.1.tgz // the kafka version may be $ mv kafka_2.12-0.10.2.1 kafka ``` -## 11. Download and install [`librdkafka`](https://github.com/edenhill/librdkafka), the C++ Kafka library we use to build the PPM. +### 11. Download and install [`librdkafka`](https://github.com/edenhill/librdkafka), the C++ Kafka library we use to build the PPM. ```bash $ cd $BASE_PPM_DIR @@ -130,7 +139,7 @@ $ sudo make install - **NOTE**: The header files for `librdkafka` should be located in `/usr/local/include/librdkafka` and the libraries (static and dynamic) should be located in `/usr/local/lib`. If you put them in another location the PPM may not build. -## 12. Download, Build, and Install the Privacy Protection Module (PPM) +### 12. Download, Build, and Install the Privacy Protection Module (PPM) ```bash $ cd $BASE_PPM_DIR @@ -141,15 +150,15 @@ $ cmake .. $ make ``` -## Additional information +### Additional information - The PPM uses [RapidJSON](https://github.com/miloyip/rapidjson), but it is a header-only library included in the repository. - The PPM uses [spdlog](https://github.com/gabime/spdlog) for logging; it is a header-only library and the headers are included in the repository. - The PPM uses [Catch](https://github.com/philsquared/Catch) for unit testing, but it is a header-only library included in the repository. -# Integrating with the ODE +## Integrating with the ODE -## Using the Docker Container +### Using the Docker Container This will run the PPM module in separate container. First set the required environmental variables. You need to tell the PPM container where the Kafka Docker container is running with the `DOCKER_HOST_IP` variable. Also tell the PPM container where to find the [map file](configuration.md#map-file) and [PPM Configuration file](configuration.md) by setting the `DOCKER_SHARED_VOLUME`: @@ -175,15 +184,15 @@ Add the following service to the end of the `docker-compose.yml` file in the `jp Start the ODE containers as normal. Note that the topics for raw BSMs must be created ahead of time. -# CDOT Integration with K8s +## CDOT Integration with K8s -## Overview +### Overview The Colorado Department of Transportation (CDOT) is deploying the various ODE services within a Kubernetes (K8s) environment. Details of this deployment can be found in the main ODE repository [documentation pages](https://github.com/usdot-jpo-ode/jpo-ode/docs). In general, each submodule image is built as a Docker image and then pushed to the CDOT registry. The images are pulled into containers running within the K8s environment, and additional containers are spun up as load requires. -## CDOT PPM Module Build +### CDOT PPM Module Build Several additional files have been added to this project to facilitate the CDOT integration. These files are: - cdot-scripts/build_cdot.sh - docker-test/ppm_no_map.sh -### Shell Scripts +#### Shell Scripts Two additional scripts have been added to facilitate the CDOT integration. The first, [`ppm_no_map.sh`](../docker-test/ppm_no_map.sh), is modeled after the existing [`ppm.sh`](../docker-test/ppm.sh) script and performs a similar function. This script is used to start the PPM module, but leaves out the hard-coded mapfile name in favor of the properties file configuration. The second script, [`build_cdot.sh`](../cdot-scripts/build_cdot.sh), is used to build the CDOT PPM Docker image, tag the image with a user provided tag, and push that image to a remote repository. This is a simple automation script used to help reduce complexity in the CDOT pipeline. \ No newline at end of file From ae49bb62ba4443a9e0c7d46b68c372595ad965f6 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 14:08:54 -0600 Subject: [PATCH 23/35] Added section on environment variables to `configuration.md` --- docs/configuration.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1d3cbc9..2ef0f4a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,6 +8,7 @@ 6. [PPM Logging](#ppm-logging) 7. [PPM Configuration](#ppm-configuration) 8. [Map Files](#map-files) +9. [Environment Variables](#environment-variables) ## PPM Operation @@ -266,4 +267,20 @@ This file has four comma-separated elements: For the WYDOT use case, WYDOT provided a set of edge definitions for I-80 that were converted into the above format. ### See Also: Data & Config Files -More information on config files can be found in the [Data & Config Files](../README.md#data--config-files) section of the README. \ No newline at end of file +More information on config files can be found in the [Data & Config Files](../README.md#data--config-files) section of the README. + +## Environment Variables +The following table lists the environment variables that can be used to configure the PPM: +| Variable | Description | +|----------|-------------| +| `DOCKER_HOST_IP` | The IP address of the Docker host. | +| `DOCKER_SHARED_VOLUME` | The path to the shared volume where the map file and configuration file are located. | +| `PPM_CONFIG_FILE` | The path to the PPM configuration file. | +| `REDACTION_PROPERTIES_PATH` | The path to the redaction properties file. | +| `PPM_LOG_TO_FILE` | The path to the log file. | +| `PPM_LOG_TO_CONSOLE` | The path to the console log file. | +| `PPM_LOG_LEVEL` | The log level. | +| `RPM_DEBUG` | When set to true, the Redaction Properties Manager will print debug messages to a file. | +| `KAFKA_TYPE` | The type of Kafka broker. If not set, a local Kafka broker will be used. | +| `CONFLUENT_KEY` | The Confluent key. Only used if `KAFKA_TYPE` is set to `CONFLUENT`. | +| `CONFLUENT_SECRET` | The Confluent secret. Only used if `KAFKA_TYPE` is set to `CONFLUENT`. | From b4977b4b42ff56f879d54c40cfd99a2ef09fe0a8 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Wed, 26 Jun 2024 14:10:08 -0600 Subject: [PATCH 24/35] Added Docker instructions to `installation.md` --- docs/installation.md | 62 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 4559d0c..8259726 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,67 @@ 1. [CDOT Integration with K8s](#cdot-integration-with-k8s) ## Docker Installation -(TBD) +### 1. Install [Docker](https://www.docker.com) + +- When following the website instructions, setup the Docker repos and follow the Linux post-install instructions. +- The CE version seems to work fine. +- [Docker installation instructions](https://docs.docker.com/engine/installation/linux/ubuntu/#install-using-the-repository) + +#### ORNL Specific Docker Configuration +- *ORNL specific, but may apply to others with organizational security* + - Correct for internal Google DNS blocking + - As root (`$ sudo su`), create a `daemon.json` file in the `/etc/docker` directory that contains the following information: +```bash + { + "debug": true, + "default-runtime": "runc", + "dns": ["160.91.126.23","160.91.126.28"], + "icc": true, + "insecure-registries": [], + "ip": "0.0.0.0", + "log-driver": "json-file", + "log-level": "info", + "max-concurrent-downloads": 3, + "max-concurrent-uploads": 5, + "oom-score-adjust": -500 + } +``` +- NOTE: The DNS IP addresses are ORNL specific. + +Be sure to restart the docker daemon to consume the new configuration file. + +```bash +$ service docker stop +$ service docker start +``` + +Check the configuration using the command below to confirm the updates above are taken if needed: + +```bash +$ docker info +``` + +### 2. Configure environment variables +Configure the environment variables for the PPM to communicate with the Kafka instance. Copy or rename the `sample.env` file to `.env`. + +```bash +$ cp sample.env .env +``` + +Edit the `.env` file to include the necessary information. + +```bash +$ vi .env +``` + +For more information on the environment variables, see the 'Environment Variables' section in the [configuration.md](configuration.md) file. + +### 3. Spin up Kafka & the PPM in Docker +To spin up the PPM and Kafka in Docker, use the following commands: + +```bash +docker compose up --build +``` ## Manual Installation The following instructions represent the "hard" way to install and test the PPM. A docker image can be built to make From ee8d864db7e0c0f313c0cedd13222b6ddacce08d Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Tue, 23 Jul 2024 14:46:54 -0400 Subject: [PATCH 25/35] Updated manual installation instructions in `installation.md` --- docs/installation.md | 153 ++++--------------------------------------- 1 file changed, 11 insertions(+), 142 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 8259726..052bbf7 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,7 +2,6 @@ ## Table of Contents 1. [Docker Installation](#docker-installation) 1. [Manual Installation](#manual-installation) -1. [Integrating with the ODE](#integrating-with-the-ode) 1. [CDOT Integration with K8s](#cdot-integration-with-k8s) ## Docker Installation @@ -78,128 +77,26 @@ this easier: [Using the Docker Container](#using-the-docker-container). *The dir $ sudo apt install git ``` -### 2. Install Oracle’s Java +### 2. Install librdkafka -```bash -$ sudo add-apt-repository -y ppa:webupd8team/java -$ sudo apt update -$ sudo apt install oracle-java8-installer -y -$ sudo java -version -``` - -### 3. Install [CMake](https://cmake.org) to build the PPM +Talking to a Kafka instance, subscribing and producting to topics requires the use of a third party library. We use the librdkafka library as a c/c++ implementation. This can be installed as a package. ```bash -$ sudo apt install cmake +$ sudo apt install -y libsasl2-dev +$ sudo apt install -y libsasl2-modules +$ sudo apt install -y libssl-dev +$ sudo apt install -y librdkafka-dev ``` -### 4. Install [Docker](https://www.docker.com) +### 3. Install [CMake](https://cmake.org), g++ & make to build the PPM -- When following the website instructions, setup the Docker repos and follow the Linux post-install instructions. -- The CE version seems to work fine. -- [Docker installation instructions](https://docs.docker.com/engine/installation/linux/ubuntu/#install-using-the-repository) -- *ORNL specific, but may apply to others with organizational security* - - Correct for internal Google DNS blocking - - As root (`$ sudo su`), create a `daemon.json` file in the `/etc/docker` directory that contains the following information: ```bash - { - "debug": true, - "default-runtime": "runc", - "dns": ["160.91.126.23","160.91.126.28”], - "icc": true, - "insecure-registries": [], - "ip": "0.0.0.0", - "log-driver": "json-file", - "log-level": "info", - "max-concurrent-downloads": 3, - "max-concurrent-uploads": 5, - "oom-score-adjust": -500 - } -``` -- NOTE: The DNS IP addresses are ORNL specific. - -### 5. Restart the docker daemon to consume the new configuration file. - -```bash -$ service docker stop -$ service docker start -``` - -### 6. Check the configuration using the command below to confirm the updates above are taken if needed: - -```bash -$ docker info -``` - -### 7. Install Docker Compose -- Comprehensive instructions can be found on this [website](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-16-04) -- Follow steps 1 and 2. - -### 8. Create a base directory from which to install all the necessary components to test the PPM. - -```bash -$ export BASE_PPM_DIR=~/some/dir/you/want/to/put/this/stuff -``` - -### 9. Install [`kafka-docker`](https://github.com/wurstmeister/kafka-docker) so kafka and zookeeper can run in a separate container. - -- Get your host IP address. The address is usually listed under an ethernet adapter, e.g., `en`. - -```bash -$ ifconfig -$ export DOCKER_HOST_IP= -``` -- Get the kafka and zookeeper images. - -```bash -$ cd $BASE_PPM_DIR -$ git clone https://github.com/wurstmeister/kafka-docker.git -$ cd kafka-docker -$ vim docker-compose.yml // Set karka: ports: to 9092:9092 -``` -- The `docker-compose.yml` file may need to be changed; the ports for kafka should be 9092:9092. -- Startup the kafka and zookeeper containers and make sure they are running. - -```bash -$ docker-compose up --no-recreate -d -$ docker-compose ps -``` -- **When you want to stop kafka and zookeeper, execute the following commands.** - -```bash -$ cd $BASE_PPM_DIR/kafka-docker -$ docker-compose down -``` - -### 10. Download and install the Kafka **binary**. - -- The Kafka binary provides a producer and consumer tool that can act as surrogates for the ODE (among other items). -- [Kafka Binary](https://kafka.apache.org/downloads) -- [Kafka Quickstart](https://kafka.apache.org/quickstart) is a very useful reference. -- Move and unpack the Kafka code as follows: - -```bash -$ cd $BASE_PPM_DIR -$ wget http://apache.claz.org/kafka/0.10.2.1/kafka_2.12-0.10.2.1.tgz // mirror and kafka version may change; check website. -$ tar -xzf kafka_2.12-0.10.2.1.tgz // the kafka version may be different. -$ mv kafka_2.12-0.10.2.1 kafka -``` - -### 11. Download and install [`librdkafka`](https://github.com/edenhill/librdkafka), the C++ Kafka library we use to build the PPM. - -```bash -$ cd $BASE_PPM_DIR -$ git clone https://github.com/edenhill/librdkafka.git -$ cd librdkafka -$ ./configure -$ make -$ sudo make install +$ sudo apt install cmake +$ sudo apt install g++ +$ sudo apt install make ``` -- **NOTE**: The header files for `librdkafka` should be located in `/usr/local/include/librdkafka` and the libraries - (static and dynamic) should be located in `/usr/local/lib`. If you put them in another location the PPM may not build. - -### 12. Download, Build, and Install the Privacy Protection Module (PPM) +### 4. Download, Build, and Install the Privacy Protection Module (PPM) ```bash $ cd $BASE_PPM_DIR @@ -216,34 +113,6 @@ $ make - The PPM uses [spdlog](https://github.com/gabime/spdlog) for logging; it is a header-only library and the headers are included in the repository. - The PPM uses [Catch](https://github.com/philsquared/Catch) for unit testing, but it is a header-only library included in the repository. -## Integrating with the ODE - -### Using the Docker Container - -This will run the PPM module in separate container. First set the required environmental variables. You need to tell the PPM container where the Kafka Docker container is running with the `DOCKER_HOST_IP` variable. Also tell the PPM container where to find the [map file](configuration.md#map-file) and [PPM Configuration file](configuration.md) by setting the `DOCKER_SHARED_VOLUME`: - -```bash -$ export DOCKER_HOST_IP=your.docker.host.ip -$ export DOCKER_SHARED_VOLUME=/your/shared/directory -``` - -Note that the map file and configuration file must be located in the `DOCKER_SHARED_VOLUME` root directory and named -`config.properties` and `road_file.csv` respectively. - -Add the following service to the end of the `docker-compose.yml` file in the `jpo-ode` installation directory. - -```bash - ppm: - build: /path/to/jpo-cvdp/repo - environment: - DOCKER_HOST_IP: ${DOCKER_HOST_IP} - volumes: - - ${DOCKER_SHARED_VOLUME}:/ppm_data -``` - -Start the ODE containers as normal. Note that the topics for raw BSMs must be created ahead of time. - - ## CDOT Integration with K8s ### Overview From e63b9d2656319e5fa87cdb57eae15dfe1e283448 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Tue, 23 Jul 2024 15:42:55 -0400 Subject: [PATCH 26/35] Revised `testing.md` --- README.md | 4 +- docs/testing.md | 212 +++++++++++------------------------------------- 2 files changed, 49 insertions(+), 167 deletions(-) diff --git a/README.md b/README.md index 19fa511..116fa1b 100644 --- a/README.md +++ b/README.md @@ -151,9 +151,7 @@ This has only been tested with Confluent Cloud but technically all SASL authenti ## Testing/Troubleshooting ### Unit Tests -Unit tests can be built and executed using the build_and_run_unit_tests.sh file inside of the dev container for the project. More information about this can be found [here](./docs/testing.md#utilizing-the-build_and_run_unit_testssh-script). - -The unit tests are also built when the solution is compiled. For information on that, check out [this section](./docs/testing.md#unit-testing). +Unit tests can be built and executed using the build_and_run_unit_tests.sh file inside of the dev container for the project. Alternatively, they can be run inside of the deployed PPM container. More information about this can be found [here](./docs/testing.md#unit-testing). ### Standalone Cluster The docker-compose.yml file is meant for local testing/troubleshooting. diff --git a/docs/testing.md b/docs/testing.md index 9ec5b50..6b93dcf 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,180 +1,67 @@ # Testing the PPM +This document describes how to test the PPM module. The PPM can be tested using unit tests, standalone tests, and Kafka integration tests. -There are several ways to test the capabilities of the PPM. - -- Testing as an individual application within [Docker](#docker-testing) -- Testing as an individual application on the [native client](#testing-on-the-native-client) +## Table of Contents - [Unit Testing](#unit-testing) +- [Standalone Testing](#standalone-testing) +- [Kafka Integration Testing](#kafka-integration-testing) +- [Test Files](#test-files) +- [See Also: Testing/Troubleshooting](#see-also-testingtroubleshooting) -## Test Files - -Several example JSON message test files are in the [jpo-cvdp/data](../data) directory. These files can be edited to generate -your own test cases. Each line in the file should be a well-formed BSM JSON -object. **Each message should be on a separate line in the file.** **If a JSON object cannot be parsed it is suppressed.** - -## Docker Testing - -To run a series of configuration tests: - - $ ./do_test.sh - -**Running this command for the first time can take awhile, as the dependencies need to be built for the PPM image.** -To run the standalone PPM test using Docker containers, from the jpo-cvdp root directory: - - $ ./start_kafka.sh - -This will build and start the required kafka containers, including the PPM image. -Next run: - - $ ./test-scripts/standalone.sh [MAP_FILE] [CONFIG] [TEST_FILE] [OFFSET] - -Where MAP_FILE is a [map file](configuration.md#map-file), CONFIG is [PPM Configuration file](configuration.md) and TEST_FILE is a [JSON message test file](#test-files). Offset refers to the offset in the filtered message topic; this is where the consumer will look for new output. The default offset is zero (the beginning of the topic), which should for the first time running the test. This will start the PPM Kafka container and use the supplied files to test the BSM filtering. For example, running: - - $ ./test-scripts/standalone.sh data/I_80.edges config/test/I_80_vel_filter.properties data/I_80_test.json - -yields: - -``` -************************** -Running standalone test with data/I_80.edges config/test/I_80_vel_filter.properties data/I_80_test.json -************************** -************************** -Producing Raw BSMs... -************************** -Producing BSM with ID=BEA10000, speed=7.02, position=41.738136, -106.587029 -Producing BSM with ID=BEA10000, speed=7.12, position=41.608656, -109.226824 -Producing BSM with ID=BEA10000, speed=7.16, position=41.311097, -110.512927 -Producing BSM with ID=BEA10000, speed=7.44, position=41.246647, -111.027436 -Producing BSM with ID=BEA10000, speed=7.44, position=41.600371, -106.22341 -Producing BSM with ID=BEA10000, speed=1.78, position=42.29789, -83.72035 -Producing BSM with ID=BEA10000, speed=0.7, position=42.29789, -83.72034 -Producing BSM with ID=BEA10000, speed=6.86, position=42.24576, -83.62337 -Producing BSM with ID=BEA10000, speed=6.84, position=42.24576, -83.62337 -Producing BSM with ID=BEA10000, speed=6.74, position=42.24576, -83.62338 -************************** -Consuming Filtered BSMs at offset 0 ... -************************** -Consuming BSM with ID=BEA10000, speed=7.02, position=41.738136, -106.587029 -Consuming BSM with ID=BEA10000, speed=7.12, position=41.608656, -109.226824 -Consuming BSM with ID=BEA10000, speed=7.16, position=41.311097, -110.512927 -Consuming BSM with ID=BEA10000, speed=7.44, position=41.246647, -111.027436 -Consuming BSM with ID=BEA10000, speed=7.44, position=41.600371, -106.22341 -Consuming BSM with ID=BEA10000, speed=6.86, position=42.24576, -83.62337 -Consuming BSM with ID=BEA10000, speed=6.84, position=42.24576, -83.62337 -Consuming BSM with ID=BEA10000, speed=6.74, position=42.24576, -83.62338 -``` - -## Testing on the Native Client - -The PPM can be tested as a component of the ODE, and it can be tested using a basic Kafka installation on the native -client. - -### ODE Integration Testing +## Unit Testing +### Testing On Local Machine +The build_and_run_unit_test.sh script provides an easy method to build and run the PPM's unit tests. It should be noted that this script needs to have the LF end-of-line sequence for it to work. -The PPM is meant to be a module that supports the ODE. The following instructions outline how to perform integration -testing on a single linux installation: +#### Steps +1. Pull the project into VSCode +1. Reopen the project in a dev container +1. Open the terminal. +1. Type "sudo su" to run commands as root +1. Type ./build_and_run_unit_tests.sh to run the script -1. Follow the [ODE installation instructions](https://github.com/usdot-jpo-ode/jpo-ode#documentation) -1. Start a terminal for launching the ODE containers. -1. Set the following environment variables: +### Testing Using Docker +1. Start by building the Docker image: ```bash -$ export DOCKER_HOST_IP= -$ export DOCKER_SHARED_VOLUME= +$ docker build -t ppm . ``` -1. Follow the Deploying ODE Application on a Docker Host directions +2. Then run unit tests inside the container with the following command: ```bash -$ docker-compose up --no-recreate -d +$ docker run -it -e PPM_LOG_TO_CONSOLE=true --name ppm ppm /cvdi-stream-build/ppm_tests ``` -1. Start a terminal for launching the PPM. +3. Remove the container: ```bash -$ cd $BASE_PPM_DIR/jpo-cvdp/build -$ ./ppm -c ../config/.properties +$ docker rm ppm ``` -1. Open a web browser, and enter the url: `localhost:8080` -1. Click on the **Connect** button. -1. Click on the **Browse** button, find a JSON test file with BSMs (one per line). -1. Click the **Upload** button. - -The BSMs from the file should be listed in the web browser: the BSMs section -lists all the BSMs; the Filtered BSMs section contains the BSMs that were -processed by the PPM and returned back to the ODE. - -### Testing without the ODE -These instructions describe how to run a collection of BSM test JSON objects through the PPM and examine its operation. -Using *GNU screen* for this work is really handy; you will need several shells. - -Startup `kafka-docker` in its own shell: - -```bash -$ cd $BASE_PPM_DIR/kafka-docker -$ docker-compose up --no-recreate -d // to startup kafka and zookeeper containers -$ docker-compose ps // to check that they are running. +## Standalone Testing +1. Spin up Kafka & the PPM ``` - -In another shell, create the simulated ODE produced topic (`j2735BsmRawJson`) and PPM produced topic (`j2735BsmFilteredJson`) - -```bash -$ cd $BASE_PPM_DIR/kafka -$ bin/kafka-topic.sh --create --zookeeper :2181 --replication-factor 1 --partitions 1 --topic j2735BsmRawJson -$ bin/kafka-topic.sh --create --zookeeper :2181 --replication-factor 1 --partitions 1 --topic j2735BsmFilteredJson +$ docker compose up -d --build ``` -Startup the simulated ODE consumer in the same shell you used to create the topics. - -```bash -$ cd $BASE_PPM_DIR/kafka -$ bin/kafka-console-consumer.sh --bootstrap-server :9092 --topic j2735BsmFilteredJson +2. View logs of PPM ``` - -- This process should just wait for input from the PPM module. - -In another shell, startup the PPM - -```bash -$ cd $BASE_PPM_DIR/jpo-cvdp/build -$ ./ppm -c ../config/.properties +$ docker compose logs -f ppm ``` -- At this point the PPM will wait for streaming messages from the simulated ODE - producer. When a message is received output will be generated describing how - the PPM handled the BSM. - -In another shell, send test JSON-encoded BSMs to the PPM. - -```bash -$ cd $BASE_PPM_DIR/kafka -$ cat $BASE_PPM_DIR/jpo-cvdp/data/ | bin/kafka-console-producer.sh --broker-list :9092 --topic j2735BsmRawJson +3. Listen to the output topic +``` +$ kafkacat -b localhost:9092 -t topic.OdeBsmJson -C ``` -- After the messages are written to the `j2735BsmRawJson` topic the shell process should return. - -You can confirm PPM operations in two ways: - -- Open the information log file in an editor and inspect the output. The `[datetimestamp] [info] BSM` messages should describe the actions the PPM is taking based on the input. -- Return to the simulated ODE consumer shell and examine the output JSON; this is more difficult because the JSON does not render well on the screen. - -## Testing All Capabilities - -- To execute the following tests, you will stop the PPM module, if running, with `-C` and then start it up with one of the following `.properties` files. - - `test.allon.properties` - - `test.geofenceonly.properties` - - `test.idredactonly.properties` - - `test.spdonly.properties` -- After starting the PPM module, you will use the shell you created above to send the [testfile](../data/bsm.wy.test.json) to the Kafka producer: - -```bash -$ cat $BASE_PPM_DIR/jpo-cvdp/data/bsm.wy.test.json | bin/kafka-console-producer.sh --broker-list :9092 --topic j2735BsmRawJson +4. Send a message to the PPM using kafkacat ``` +$ kafkacat -b localhost:9092 -t topic.OdeBsmJson -P +``` + +You can now paste a JSON message into the terminal and hit enter. The PPM should log the message and send it to the output topic if it is not suppressed. -- Open the information log file in an editor and inspect the `info BSM` message (an example of these messages is shown below). The message immediately -to the right of `BSM` indicates whether the message was RETAINED, or passed on to a filtered stream, or SUPPRESSED with the cause. The information in -parenthesis is the TemporaryID, secMark, lat, lon, and speed information in the message; this can be used to test and troubleshoot your configuration. +The message immediately to the right of `BSM` indicates whether the message was RETAINED, or passed on to a filtered stream, or SUPPRESSED with the cause. The information in parenthesis is the TemporaryID, secMark, lat, lon, and speed information in the message; this can be used to test and troubleshoot your configuration. ```bash [170613 12:30:47.057503] [info] BSM [RETAINED]: (ON-VG---,36710,41.116496,-104.888494,5.000000) @@ -197,25 +84,22 @@ parenthesis is the TemporaryID, secMark, lat, lon, and speed information in the [170613 12:30:47.064940] [info] BSM [RETAINED]: (OFFVG---,36727,43.313653,-111.799675,9.000000) ``` -- The above output will vary depending on which configuration file you use. - -## Unit Testing - -Unit tests are built when the PPM is compiled during installation. Those tests can be run using the following command: +5. To stop the PPM and Kafka, run the following command: +``` +$ docker compose down +``` +## Kafka Integration Testing +To run kafka integration tests, run the following command: ```bash -$ ./ppm_tests +$ ./do_kafka_test.sh ``` -### Utilizing the build_and_run_unit_tests.sh script -The build_and_run_unit_test.sh script provides an easy method to build and run the PPM's unit tests. It should be noted that this script needs to have the LF end-of-line sequence for it to work. +## Test Files -#### Steps -1. Pull the project into VSCode -1. Reopen the project in a dev container -1. Open the terminal. -1. Type "sudo su" to run commands as root -1. Type ./build_and_run_unit_tests.sh to run the script +Several example JSON message test files are in the [jpo-cvdp/data](../data) directory. These files can be edited to generate +your own test cases. Each line in the file should be a well-formed BSM JSON +object. **Each message should be on a separate line in the file.** **If a JSON object cannot be parsed it is suppressed.** # See Also: Testing/Troubleshooting -More information on testing can be found in the [Testing/Troubleshooting](../README.md#Testing/Troubleshooting) section of the README. \ No newline at end of file +More information on testing can be found in the [Testing/Troubleshooting](../README.md#testingtroubleshooting) section of the README. \ No newline at end of file From dad2348b5c7bf0e5e57492d37908e29076cb7e27 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Tue, 23 Jul 2024 15:47:39 -0400 Subject: [PATCH 27/35] Corrected a reference in `troubleshooting.md` --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 96d5791..05cbe6f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -32,4 +32,4 @@ This error occurs when the PPM is unable to connect to the local broker. This ca - The PPM is targeting the wrong IP address # See Also: Testing/Troubleshooting -More information on troubleshooting can be found in the [Testing/Troubleshooting](../README.md#Testing/Troubleshooting) section of the README. \ No newline at end of file +More information on troubleshooting can be found in the [Testing/Troubleshooting](../README.md#testingtroubleshooting) section of the README. \ No newline at end of file From 2a94d4d8a2a2701fe6b485e97a1e7a9c59ec9f88 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Thu, 5 Sep 2024 10:18:47 -0600 Subject: [PATCH 28/35] Removed TIM reference in `standalone.sh` --- test-scripts/standalone.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test-scripts/standalone.sh b/test-scripts/standalone.sh index 2751ec4..d7a681d 100755 --- a/test-scripts/standalone.sh +++ b/test-scripts/standalone.sh @@ -2,8 +2,7 @@ # This script sets up and runs a standalone test for a PPM container. The # PPM is started in a Docker container using a specified image and waits for it to become -# ready. The script takes in three input files: ROAD_FILE, CONFIG, TEST_DATA, and a type -# argument (BSM or TIM). It checks if the input files exist and copies them to a test data +# ready. The script takes in three input files: ROAD_FILE, CONFIG, & TEST_DATA. It checks if the input files exist and copies them to a test data # directory. If the OFFSET argument is provided, it is used as the offset in the topic that # will be consumed and displayed in the output. If not, the default value of 0 is used. From 05a21fcbd69dc78c25ed9a948fdb1845a2b0ba2a Mon Sep 17 00:00:00 2001 From: Michael7371 <40476797+Michael7371@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:12:44 -0600 Subject: [PATCH 29/35] fixing line endings for .py and making the auto behavior lf --- .gitattributes | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 6888c15..add50b0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # Set the default behavior, in case people don't have core.autocrlf set. -* text=auto +* text=auto eol=lf # Explicitly declare bash scripts to be set to LF *.sh text eol=lf -*.txt text eol=lf \ No newline at end of file +*.txt text eol=lf +*.py text eol=lf \ No newline at end of file From 8e7c4c9ecf1b4ba769095372cf0f60defff35a1d Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Thu, 5 Sep 2024 12:20:28 -0600 Subject: [PATCH 30/35] Changed `docker-compose` to `docker compose` in kafka scripts --- start_kafka.sh | 2 +- stop_kafka.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/start_kafka.sh b/start_kafka.sh index 8319216..0cfdbd1 100755 --- a/start_kafka.sh +++ b/start_kafka.sh @@ -3,4 +3,4 @@ ./stop_kafka.sh # start kafka -docker-compose -f docker-compose-kafka.yml up -d \ No newline at end of file +docker compose -f docker-compose-kafka.yml up -d \ No newline at end of file diff --git a/stop_kafka.sh b/stop_kafka.sh index 5b4123b..cf9f625 100644 --- a/stop_kafka.sh +++ b/stop_kafka.sh @@ -1,4 +1,4 @@ #!/bin/bash # stop kafka -docker-compose -f docker-compose-kafka.yml down --remove-orphans \ No newline at end of file +docker compose -f docker-compose-kafka.yml down --remove-orphans \ No newline at end of file From 98e5716c32c9d7bf1ee4092f58292892f47b1d55 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Fri, 6 Sep 2024 09:08:34 -0600 Subject: [PATCH 31/35] Added release notes for version 1.4.0 --- docs/Release_notes.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/Release_notes.md b/docs/Release_notes.md index 6dff78b..824088f 100644 --- a/docs/Release_notes.md +++ b/docs/Release_notes.md @@ -1,8 +1,19 @@ Jpo-cvdp Release Notes ---------------------------- -Version 1.3.0, released February 2024 +Version 1.4.0, released September 2024 +---------------------------------------- +### **Summary** +The changes for the jpo-cvdp 1.4.0 release involve the addition of a 'Supported Message Types' section to the README, the removal of TIM support since TIMs do not contain personally-identifiable information, and revisions to the documentation for accuracy & clarity. + +Enhancements in this release: +- CDOT PR 44: Added 'Supported Message Types' section to README +- CDOT PR 45: Removed TIM support since TIMs do not contain personally-identifiable information +- CDOT PR 46: Revised documentation for accuracy & clarity + +Version 1.3.0, released February 2024 +---------------------------------------- ### **Summary** The changes for the jpo-cvdp 1.3.0 release involve the optimization of dockerfiles, addition of dockerhub image documentation & some QoL changes to the `do_kafka_test.sh` script. From ee9bcd88e307c7e04eb03909a8efe48707b87b87 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Sun, 22 Sep 2024 09:04:13 -0600 Subject: [PATCH 32/35] Removed TIM reference from configuration documentation & fixed some typos --- docs/configuration.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 2ef0f4a..4b709be 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -129,11 +129,10 @@ if the message is published. The `metadata:sanitized` element is changed to `tru all messages published by the PPM should have a `metadata:sanitized` value of `true`. The `payload` component of the BSM has a `data` object containing the `coreData` object that is analyzed by the PPM -for features that may cause it to be suppressed. The same analysis is done on the `receivedDetails:loction` object in the TIMS -`metadata` component. Note that the `payload` for TIMS is not inspected. +for features that may cause it to be suppressed. The JSON format published by the PPM follows the format received. It may be completely suppressed or certain fields may -be modifed as described in this second and the sections that follow. +be modified as described in this second and the sections that follow. ### Velocity Filtering @@ -141,10 +140,10 @@ be modifed as described in this second and the sections that follow. - `ON` : enables message filtering. - Any other value : disables message filtering. -- `privacy.filter.velocity.min` : *When velocity fitering is enabled*, messages having velocities below this value will be +- `privacy.filter.velocity.min` : *When velocity filtering is enabled*, messages having velocities below this value will be suppressed. The units are in meters per second. -- `privacy.filter.velocity.max` : *When velocity fitering is enabled*, messages having velocities above this value will be +- `privacy.filter.velocity.max` : *When velocity filtering is enabled*, messages having velocities above this value will be suppressed. The units are in meters per second. ### BSM Identifier Redaction @@ -182,9 +181,9 @@ control vehicle size redaction. Messages can be suppressed based on latitude and longitude attributes. If this capability is turned on through the configuration file, each edge defined in the map file is used to infer a *component* geofence that surrounds that segment of the -road. The image below illustrates how a *rectange* is drawn to form the segment's +road. The image below illustrates how a *rectangle* is drawn to form the segment's geofence. The aforementioned edge attributes and PPM configuration parameters -determine the size of the rectange. +determine the size of the rectangle. ![Road Segment Geofence Dimensions](graphics/geofence-dimensions.png) From 8fee29e34ce43f091ec42a007be593ecb6533736 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Sun, 22 Sep 2024 09:15:50 -0600 Subject: [PATCH 33/35] Modified dev container to make all shell scripts executable upon being created --- .devcontainer/devcontainer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0543538..57e57d6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,8 +23,8 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "gcc -v", + "postCreateCommand": "sudo find /workspaces/ -type f -iname \"*.sh\" -exec chmod +x {} \\;" // Make all shell scripts executable // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + // "remoteUser": "vscode" } From d1dac9ea26302e867bb3a1587e96e83263a48586 Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Sun, 22 Sep 2024 09:21:19 -0600 Subject: [PATCH 34/35] Removed unnecessary 'version' element from docker-compose files --- docker-compose-confluent-cloud.yml | 1 - docker-compose-kafka.yml | 1 - docker-compose.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/docker-compose-confluent-cloud.yml b/docker-compose-confluent-cloud.yml index 24d21b7..3b37618 100644 --- a/docker-compose-confluent-cloud.yml +++ b/docker-compose-confluent-cloud.yml @@ -1,4 +1,3 @@ -version: '2' services: ppm: build: diff --git a/docker-compose-kafka.yml b/docker-compose-kafka.yml index 94fb651..610badd 100644 --- a/docker-compose-kafka.yml +++ b/docker-compose-kafka.yml @@ -1,4 +1,3 @@ -version: '2' services: zookeeper: image: wurstmeister/zookeeper diff --git a/docker-compose.yml b/docker-compose.yml index 273493c..33d4bfc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '2' services: zookeeper: image: wurstmeister/zookeeper From a39a863e4fc541f6e23f8e303563ba1e449b735f Mon Sep 17 00:00:00 2001 From: dmccoystephenson Date: Sun, 22 Sep 2024 09:32:25 -0600 Subject: [PATCH 35/35] Updated testing doc to include configuration for standalone testing --- docs/testing.md | 103 +++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 6b93dcf..7be1c47 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -39,55 +39,70 @@ $ docker rm ppm ``` ## Standalone Testing -1. Spin up Kafka & the PPM -``` -$ docker compose up -d --build -``` - -2. View logs of PPM -``` -$ docker compose logs -f ppm -``` - -3. Listen to the output topic -``` -$ kafkacat -b localhost:9092 -t topic.OdeBsmJson -C -``` +1. Rename `sample.env` to `.env` -4. Send a message to the PPM using kafkacat -``` -$ kafkacat -b localhost:9092 -t topic.OdeBsmJson -P -``` +1. Set `DOCKER_HOST_IP` in the `.env` file to the IP address of your Docker host. This is the IP address of the machine running Docker. -You can now paste a JSON message into the terminal and hit enter. The PPM should log the message and send it to the output topic if it is not suppressed. +1. Create a temporary directory called 'ppm_data' in the root of the project directory. -The message immediately to the right of `BSM` indicates whether the message was RETAINED, or passed on to a filtered stream, or SUPPRESSED with the cause. The information in parenthesis is the TemporaryID, secMark, lat, lon, and speed information in the message; this can be used to test and troubleshoot your configuration. +1. Set `DOCKER_SHARED_VOLUME` in the `.env` file to the full path of the 'ppm_data' directory. -```bash -[170613 12:30:47.057503] [info] BSM [RETAINED]: (ON-VG---,36710,41.116496,-104.888494,5.000000) -[170613 12:30:47.057566] [info] BSM [RETAINED]: (ON-VG-99,36711,41.116496,-104.888494,5.000000) -[170613 12:30:47.057584] [info] BSM [SUPPRESSED-speed]: (ON-VBL--,36712,41.116496,-104.888494,1.000000) -[170613 12:30:47.057593] [info] BSM [SUPPRESSED-speed]: (ON-VBH--,36713,41.116496,-104.888494,100.000000) -[170613 12:30:47.057623] [info] BSM [RETAINED]: (OFFVG---,36714,41.118110,-104.889282,5.000000) -[170613 12:30:47.057639] [info] BSM [SUPPRESSED-speed]: (OFFVBH--,36715,41.118110,-104.889282,99.000000) -[170613 12:30:47.057670] [info] BSM [RETAINED]: (OFFVGMID,36716,41.141742,-105.361760,9.000000) -[170613 12:30:47.057705] [info] BSM [RETAINED]: (ON-VGTOP,36717,41.143138,-105.361470,9.000000) -[170613 12:30:47.058086] [info] BSM [SUPPRESSED-speed]: (ON-VBTOP,36718,41.143138,-105.361470,1.000000) -[170613 12:30:47.058126] [info] BSM [RETAINED]: (ON-VGBOT,36719,41.140537,-105.362255,9.000000) -[170613 12:30:47.058147] [info] BSM [SUPPRESSED-speed]: (ON-VBBOT,36720,41.140537,-105.362255,50.000000) -[170613 12:30:47.058178] [info] BSM [RETAINED]: (ON-VG---,36721,41.411728,-110.137350,9.000000) -[170613 12:30:47.058213] [info] BSM [RETAINED]: (ON-VG-99,36722,41.411728,-110.137350,9.000000) -[170613 12:30:47.058293] [info] BSM [RETAINED]: (OFFVG---,36723,41.628687,-109.089771,9.000000) -[170613 12:30:47.058451] [info] BSM [RETAINED]: (OFFVG---,36724,41.627758,-109.091004,9.000000) -[170613 12:30:47.058494] [info] BSM [RETAINED]: (O??VG---,36725,41.627672,-109.089390,9.000000) -[170613 12:30:47.064880] [info] BSM [RETAINED]: (ON-VG---,36726,41.627467,-109.089251,9.000000) -[170613 12:30:47.064940] [info] BSM [RETAINED]: (OFFVG---,36727,43.313653,-111.799675,9.000000) -``` +1. Ensure that the following files are present in the 'ppm_data' directory: + - [fieldsToRedact.txt](../config/fieldsToRedact.txt) + - [ppmBsm.properties](../config/fieldsToSuppress.txt) + - [I_80.edges](../data/I_80.edges) -5. To stop the PPM and Kafka, run the following command: -``` -$ docker compose down -``` +1. Spin up Kafka & the PPM + ``` + $ docker compose up -d --build + ``` + +1. View logs of PPM + ``` + $ docker compose logs -f ppm + ``` + +1. Listen to the output topic + ``` + $ kafkacat -b localhost:9092 -t topic.OdeBsmJson -C + ``` + +1. Send a message to the PPM using kafkacat + ``` + $ kafkacat -b localhost:9092 -t topic.OdeBsmJson -P + ``` + + You can now paste a JSON message into the terminal and hit enter. The PPM should log the message and send it to the output topic if it is not suppressed. + + The message immediately to the right of `BSM` indicates whether the message was RETAINED, or passed on to a filtered stream, or SUPPRESSED with the cause. The information in parenthesis is the TemporaryID, secMark, lat, lon, and speed information in the message; this can be used to test and troubleshoot your configuration. + + ```bash + [170613 12:30:47.057503] [info] BSM [RETAINED]: (ON-VG---,36710,41.116496,-104.888494,5.000000) + [170613 12:30:47.057566] [info] BSM [RETAINED]: (ON-VG-99,36711,41.116496,-104.888494,5.000000) + [170613 12:30:47.057584] [info] BSM [SUPPRESSED-speed]: (ON-VBL--,36712,41.116496,-104.888494,1.000000) + [170613 12:30:47.057593] [info] BSM [SUPPRESSED-speed]: (ON-VBH--,36713,41.116496,-104.888494,100.000000) + [170613 12:30:47.057623] [info] BSM [RETAINED]: (OFFVG---,36714,41.118110,-104.889282,5.000000) + [170613 12:30:47.057639] [info] BSM [SUPPRESSED-speed]: (OFFVBH--,36715,41.118110,-104.889282,99.000000) + [170613 12:30:47.057670] [info] BSM [RETAINED]: (OFFVGMID,36716,41.141742,-105.361760,9.000000) + [170613 12:30:47.057705] [info] BSM [RETAINED]: (ON-VGTOP,36717,41.143138,-105.361470,9.000000) + [170613 12:30:47.058086] [info] BSM [SUPPRESSED-speed]: (ON-VBTOP,36718,41.143138,-105.361470,1.000000) + [170613 12:30:47.058126] [info] BSM [RETAINED]: (ON-VGBOT,36719,41.140537,-105.362255,9.000000) + [170613 12:30:47.058147] [info] BSM [SUPPRESSED-speed]: (ON-VBBOT,36720,41.140537,-105.362255,50.000000) + [170613 12:30:47.058178] [info] BSM [RETAINED]: (ON-VG---,36721,41.411728,-110.137350,9.000000) + [170613 12:30:47.058213] [info] BSM [RETAINED]: (ON-VG-99,36722,41.411728,-110.137350,9.000000) + [170613 12:30:47.058293] [info] BSM [RETAINED]: (OFFVG---,36723,41.628687,-109.089771,9.000000) + [170613 12:30:47.058451] [info] BSM [RETAINED]: (OFFVG---,36724,41.627758,-109.091004,9.000000) + [170613 12:30:47.058494] [info] BSM [RETAINED]: (O??VG---,36725,41.627672,-109.089390,9.000000) + [170613 12:30:47.064880] [info] BSM [RETAINED]: (ON-VG---,36726,41.627467,-109.089251,9.000000) + [170613 12:30:47.064940] [info] BSM [RETAINED]: (OFFVG---,36727,43.313653,-111.799675,9.000000) + ``` + + +1. To stop the PPM and Kafka, run the following command: + + ``` + $ docker compose down + ``` ## Kafka Integration Testing To run kafka integration tests, run the following command: