├── website ├── logo.png ├── website_images │ ├── qfh.jpg │ ├── dipole.jpg │ ├── rpi1.jpg │ ├── rpi2.jpg │ ├── qfh-wiring.jpg │ └── helix_signal.jpg ├── s3-vars.js ├── LICENSE ├── index.html ├── rpi.html ├── qfh.html └── wx-ground-station.js ├── aws-api ├── .gitignore ├── api │ └── passes.js ├── serverless.yml └── package-lock.json ├── documentation └── example-mcir-precip.png ├── aws-s3 ├── .env ├── package.json ├── lib │ └── api │ │ └── discord.js ├── upload-upcoming-passes.js ├── upload_existing.js ├── remove-wx-images.js ├── upload-wx-images.js └── package-lock.json ├── schedule_all.sh ├── LICENSE ├── configure.sh ├── receive_and_process_satellite.sh ├── schedule_satellite.sh └── README.md /website/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/logo.png -------------------------------------------------------------------------------- /aws-api/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /website/website_images/qfh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/website_images/qfh.jpg -------------------------------------------------------------------------------- /website/website_images/dipole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/website_images/dipole.jpg -------------------------------------------------------------------------------- /website/website_images/rpi1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/website_images/rpi1.jpg -------------------------------------------------------------------------------- /website/website_images/rpi2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/website_images/rpi2.jpg -------------------------------------------------------------------------------- /documentation/example-mcir-precip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/documentation/example-mcir-precip.png -------------------------------------------------------------------------------- /website/website_images/qfh-wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/website_images/qfh-wiring.jpg -------------------------------------------------------------------------------- /website/website_images/helix_signal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alonsovargas3/wx-ground-station/HEAD/website/website_images/helix_signal.jpg -------------------------------------------------------------------------------- /aws-s3/.env: -------------------------------------------------------------------------------- 1 | #AWS 2 | AWS_REGION= 3 | AWS_BUCKET= 4 | 5 | 6 | #WATERMARK 7 | WATERMARK= 8 | STATION_LOCATION= 9 | 10 | #DIRECTORIES 11 | DISCORD_WEBHOOK = 12 | WEBSITE_ADDR = 13 | -------------------------------------------------------------------------------- /aws-s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-s3", 3 | "version": "1.0.0", 4 | "description": "scripts for manipulating and uploading wx images to S3 bucket", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "aws-sdk": "^2.539.0", 13 | "dateformat": "^3.0.3", 14 | "dotenv": "^8.2.0", 15 | "glob": "^7.1.4", 16 | "jimp": "^0.8.4", 17 | "request": "^2.88.2", 18 | "uuid": "^3.3.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /schedule_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update Satellite Information 4 | 5 | wget -qr https://www.celestrak.com/NORAD/elements/weather.txt -O INSTALL_DIR/weather.txt 6 | grep "NOAA 15" INSTALL_DIR/weather.txt -A 2 > INSTALL_DIR/weather.tle 7 | grep "NOAA 18" INSTALL_DIR/weather.txt -A 2 >> INSTALL_DIR/weather.tle 8 | grep "NOAA 19" INSTALL_DIR/weather.txt -A 2 >> INSTALL_DIR/weather.tle 9 | 10 | 11 | 12 | #Remove all AT jobs 13 | 14 | for i in `atq | awk '{print $1}'`;do atrm $i;done 15 | 16 | rm -f INSTALL_DIR/upcoming_passes.txt 17 | 18 | #Schedule Satellite Passes: 19 | 20 | INSTALL_DIR/schedule_satellite.sh "NOAA 19" 137.1000 21 | INSTALL_DIR/schedule_satellite.sh "NOAA 18" 137.9125 22 | INSTALL_DIR/schedule_satellite.sh "NOAA 15" 137.6200 23 | 24 | node INSTALL_DIR/aws-s3/upload-upcoming-passes.js INSTALL_DIR/upcoming_passes.txt 25 | -------------------------------------------------------------------------------- /website/s3-vars.js: -------------------------------------------------------------------------------- 1 | // 2 | // Replace BUCKET_NAME with the bucket name. 3 | // 4 | var bucketName = ''; 5 | // Replace this block of code with the sample code located at: 6 | // Cognito -- Manage Identity Pools -- [identity_pool_name] -- Sample Code -- JavaScript 7 | // 8 | // Initialize the Amazon Cognito credentials provider 9 | AWS.config.region = ''; // Region 10 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 11 | IdentityPoolId: '' 12 | }); 13 | 14 | // Create a mapbox.com account and get access token 15 | const MAP_BOX_ACCESS_TOKEN = ''; 16 | const GROUND_STATION_LAT = ''; 17 | const GROUND_STATION_LON = ''; 18 | const GROUND_STATION_NAME = ''; 19 | const MAX_CAPTURES = 10; 20 | const DIR_NAME = "images"; 21 | const PASS_URL = ""; 22 | 23 | // Create a new service object 24 | var s3 = new AWS.S3({ 25 | apiVersion: '2006-03-01', 26 | params: {Bucket: bucketName} 27 | }); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /website/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /configure.sh: -------------------------------------------------------------------------------- 1 | 2 | # Look for audio, images and logs directories 3 | # If they don't exist, create the directories 4 | if [ ! -d "audio" ] 5 | then 6 | mkdir audio 7 | fi 8 | 9 | if [ ! -d "images" ] 10 | then 11 | mkdir images 12 | fi 13 | 14 | if [ ! -d "logs" ] 15 | then 16 | mkdir logs 17 | fi 18 | 19 | # Define current directory and output to log 20 | currentDir=`echo $PWD` 21 | echo "configuring for" $currentDir 22 | 23 | # Replace INSTALL_DIR with the current working directory inside of each file 24 | # Assume current working directory will be final install path 25 | sed -i "s|INSTALL_DIR|$currentDir|g" schedule_all.sh 26 | sed -i "s|INSTALL_DIR|$currentDir|g" schedule_satellite.sh 27 | sed -i "s|INSTALL_DIR|$currentDir|g" receive_and_process_satellite.sh 28 | 29 | # Set execute rights for all 30 | chmod +x schedule_all.sh 31 | chmod +x schedule_satellite.sh 32 | chmod +x receive_and_process_satellite.sh 33 | 34 | # Add cronjob to run schedule_all.sh daily at mightnight 35 | cronjobcmd="$currentDir/schedule_all.sh" 36 | cronjob="0 0 * * * $cronjobcmd" 37 | ( crontab -l | grep -v -F "$cronjobcmd" ; echo "$cronjob" ) | crontab - 38 | -------------------------------------------------------------------------------- /aws-s3/lib/api/discord.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = function(satDetails,passKey,passElevation,passDirection,){ //pair,status,exchange,found,price,indicator,timeFrame 4 | var request = require("request"); 5 | 6 | var webhook = process.env.DISCORD_WEBHOOK; 7 | 8 | discordMsg = 9 | "\n" + satDetails + 10 | "\nelevation: "+passElevation+"°"+ 11 | "\ndirection: "+passDirection+ 12 | "\n["+process.env.WEBSITE_ADDR+"]("+process.env.WEBSITE_ADDR+")"+ 13 | "\n"+ 14 | "\n["+passKey+"]("+process.env.WEBSITE_ADDR+"/images/"+passKey+"-MCIR-precip.png \""+passKey+"\")"; 15 | 16 | request.post( 17 | webhook, 18 | { 19 | form: { 20 | content: discordMsg, 21 | username: "SatBot", 22 | embeds: [{ 23 | title: "Satellite Pass", 24 | description: "Hi! :thinking:" 25 | }] 26 | } 27 | }, 28 | function (error, response, body) { 29 | if (!error && response.statusCode == 200) { 30 | console.log("Message sent to Discord"); 31 | } 32 | } 33 | ); // End POST request first webhook 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /aws-api/api/passes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const AWS = require('aws-sdk'); 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | 5 | 6 | module.exports.list = (event, context, callback) => { 7 | var params = { 8 | TableName: process.env.PASS_TABLE, 9 | Limit: 100, 10 | ScanIndexForward: true, 11 | ProjectionExpression: "satellite,passDate,passTime,imageKey,images,chan_a,chan_b,direction,tle1,tle2,passDuration,frequency,elevation,gain" 12 | }; 13 | 14 | console.log("Scanning passes table."); 15 | const onScan = (err, data) => { 16 | 17 | if (err) { 18 | console.log('Scan failed to load data. Error JSON:', JSON.stringify(err, null, 2)); 19 | callback(err); 20 | } else { 21 | console.log("Scan succeeded."); 22 | return callback(null, { 23 | statusCode: 200, 24 | headers: { 25 | "Access-Control-Allow-Origin" : "*", 26 | 'Content-Type': 'application/json' 27 | }, 28 | body: JSON.stringify({ 29 | passes: data.Items 30 | }) 31 | }); 32 | } 33 | 34 | }; 35 | 36 | dynamoDb.scan(params, onScan); 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /aws-api/serverless.yml: -------------------------------------------------------------------------------- 1 | service: passes 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | stage: prod 7 | region: us-west-2 8 | environment: 9 | PASS_TABLE: ${self:service}-${opt:stage, self:provider.stage} 10 | iamRoleStatements: 11 | - Effect: Allow 12 | Action: 13 | - dynamodb:Query 14 | - dynamodb:Scan 15 | - dynamodb:GetItem 16 | - dynamodb:PutItem 17 | Resource: "*" 18 | 19 | functions: 20 | getPasses: 21 | handler: api/passes.list 22 | memorySize: 128 23 | description: Get all satellite passes 24 | events: 25 | - http: 26 | path: passes 27 | method: get 28 | cors: true 29 | 30 | resources: 31 | Resources: 32 | PassesDynamoDbTable: 33 | Type: 'AWS::DynamoDB::Table' 34 | DeletionPolicy: Retain 35 | Properties: 36 | AttributeDefinitions: 37 | - 38 | AttributeName: "passDate" 39 | AttributeType: "S" 40 | - 41 | AttributeName: "passTime" 42 | AttributeType: "S" 43 | KeySchema: 44 | - 45 | AttributeName: "passDate" 46 | KeyType: "HASH" 47 | - 48 | AttributeName: passTime 49 | KeyType: RANGE 50 | ProvisionedThroughput: 51 | ReadCapacityUnits: 1 52 | WriteCapacityUnits: 1 53 | StreamSpecification: 54 | StreamViewType: "NEW_AND_OLD_IMAGES" 55 | TableName: ${self:provider.environment.PASS_TABLE} 56 | -------------------------------------------------------------------------------- /aws-s3/upload-upcoming-passes.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({path:'/home/pi/wx-ground-station/aws-s3/.env'}); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var AWS = require('aws-sdk'); 5 | var uuid = require('uuid'); 6 | 7 | var REGION = process.env.AWS_REGION; 8 | var BUCKET = process.env.AWS_BUCKET; 9 | var IMAGE_DIR = "images/"; 10 | 11 | AWS.config.update({region: REGION}); 12 | var s3 = new AWS.S3(); 13 | 14 | uploadUpcomingPasses(process.argv[2]); 15 | 16 | function uploadUpcomingPasses(filename) { 17 | var upcomingPassesFilename = "upcoming_passes.json"; 18 | var lines = fs.readFileSync(filename).toString().split("\n"); 19 | var count = 0; 20 | var all_passes = []; 21 | lines.forEach((line) => { 22 | if (line.trim().length > 0) { 23 | var fields = line.split(','); 24 | var pass_info = { 25 | start: new Date(0).setUTCSeconds(fields[0]), 26 | end: new Date(0).setUTCSeconds(fields[1]), 27 | elevation: fields[2], 28 | direction: fields[3], 29 | satellite: fields[4], 30 | tle1: fields[5], 31 | tle2: fields[6] 32 | }; 33 | all_passes.push(pass_info); 34 | } 35 | if (++count == lines.length) { 36 | all_passes = all_passes.sort((a, b) => { return a.start-b.start }); 37 | console.log("uploading upcoming pass info"); 38 | var params = { 39 | ACL: "public-read", 40 | ContentType: "application/json", 41 | Bucket: BUCKET, 42 | Key: IMAGE_DIR + upcomingPassesFilename, 43 | Body: JSON.stringify(all_passes, null, 2) 44 | }; 45 | 46 | s3.putObject(params, function(err, data) { 47 | if (err) { 48 | console.log(err) 49 | } else { 50 | console.log(" successfully uploaded " + upcomingPassesFilename); 51 | } 52 | }); 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /receive_and_process_satellite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SAT=$1 4 | FREQ=$2 5 | FILEKEY=$3 6 | TLE_FILE=$4 7 | START_TIME=$5 8 | DURATION=$6 9 | ELEVATION=$7 10 | DIRECTION=$8 11 | 12 | AUDIO_DIR=INSTALL_DIR/audio 13 | IMAGE_DIR=INSTALL_DIR/images 14 | LOG_DIR=INSTALL_DIR/logs 15 | MAP_FILE=${IMAGE_DIR}/${FILEKEY}-map.png 16 | AUDIO_FILE=${AUDIO_DIR}/${FILEKEY}.wav 17 | LOGFILE=${LOG_DIR}/${FILEKEY}.log 18 | 19 | echo $@ >> $LOGFILE 20 | 21 | #/usr/local/bin/rtl_biast -b 1 2>> $LOGFILE 22 | sudo timeout $DURATION rtl_fm -f ${FREQ}M -s 60k -g 45 -p 0 -E wav -E deemp -F 9 - 2>> $LOGFILE | sox -t wav - $AUDIO_FILE rate 11025 23 | #/usr/local/bin/rtl_biast -b 0 2>> $LOGFILE 24 | 25 | PassStart=`expr $START_TIME + 90` 26 | 27 | if [ -e $AUDIO_FILE ] 28 | then 29 | /usr/local/bin/wxmap -T "${SAT}" -H $TLE_FILE -p 0 -l 0 -o $PassStart ${MAP_FILE} >> $LOGFILE 2>&1 30 | 31 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e ZA $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-ZA.png >> $LOGFILE 2>&1 32 | 33 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e NO $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-NO.png >> $LOGFILE 2>&1 34 | 35 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e MSA $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-MSA.png >> $LOGFILE 2>&1 36 | 37 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e MSA-precip $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-MSA-precip.png >> $LOGFILE 2>&1 38 | 39 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e MCIR $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-MCIR.png >> $LOGFILE 2>&1 40 | 41 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e MCIR-precip $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-MCIR-precip.png >> $LOGFILE 2>&1 42 | 43 | /usr/local/bin/wxtoimg -m ${MAP_FILE} -e therm $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-THERM.png >> $LOGFILE 2>&1 44 | 45 | /usr/local/bin/wxtoimg -p $AUDIO_FILE ${IMAGE_DIR}/${FILEKEY}-PRISTINE.png >> $LOGFILE 2>&1 46 | 47 | TLE1=`grep "$SAT" $TLE_FILE -A 2 | tail -2 | head -1 | tr -d '\r'` 48 | TLE2=`grep "$SAT" $TLE_FILE -A 2 | tail -2 | tail -1 | tr -d '\r'` 49 | GAIN=`grep Gain $LOGFILE | head -1` 50 | CHAN_A=`grep "Channel A" $LOGFILE | head -1` 51 | CHAN_B=`grep "Channel B" $LOGFILE | head -1` 52 | 53 | echo "node INSTALL_DIR/aws-s3/upload-wx-images.js \"$SAT\" $FREQ ${IMAGE_DIR}/${FILEKEY} $ELEVATION $DIRECTION $DURATION \"${TLE1}\" \"${TLE2}\" \"$GAIN\" \"${CHAN_A}\" \"${CHAN_B}\"" >> $LOGFILE 2>&1 54 | node INSTALL_DIR/aws-s3/upload-wx-images.js "$SAT" $FREQ ${IMAGE_DIR}/${FILEKEY} $ELEVATION $DIRECTION $DURATION "${TLE1}" "${TLE2}" "$GAIN" "${CHAN_A}" "${CHAN_B}" >> $LOGFILE 2>&1 55 | fi 56 | -------------------------------------------------------------------------------- /aws-s3/upload_existing.js: -------------------------------------------------------------------------------- 1 | // Read Synchrously 2 | require('dotenv').config(); 3 | 4 | // AWS Configuration 5 | var REGION = process.env.AWS_REGION; 6 | var BUCKET = process.env.AWS_BUCKET; 7 | var DIR_NAME = "images"; 8 | 9 | // Load the AWS SDK for Node.js 10 | var AWS = require('aws-sdk'); 11 | 12 | // Set the region 13 | AWS.config.update({ 14 | region: REGION 15 | }); 16 | 17 | var s3 = new AWS.S3({ 18 | apiVersion: '2006-03-01', 19 | params: {Bucket: BUCKET}, 20 | MaxKeys: 2500 21 | }); 22 | 23 | // Create the DynamoDB service object 24 | var docClient = new AWS.DynamoDB.DocumentClient(); 25 | 26 | s3.listObjects({Prefix: DIR_NAME}, function(err, data) { 27 | if (err){ 28 | console.log(err, err.stack); // an error occurred 29 | } else { 30 | 31 | var pattern = new RegExp(".+-[0-9]+[0-9]+\.json$"); 32 | var jsonFiles = data.Contents.filter(function (object) { 33 | return pattern.test(object.Key); 34 | }); 35 | 36 | jsonFiles.forEach(function (m) { 37 | //construct getParam 38 | var getParams = { 39 | Bucket: BUCKET, 40 | Key: m.Key 41 | } 42 | console.log(m.Key); 43 | //Fetch or read data from aws s3 44 | s3.getObject(getParams, function (err, data) { 45 | 46 | if (err) { 47 | console.log(err); 48 | } else { 49 | 50 | var content = data.Body.toString(); 51 | var db_content = JSON.parse(content); 52 | 53 | // Replaces key names for date, time, and duration 54 | // to avoid reserved term conflicts with DynamoDB 55 | db_content.passDate = db_content.date; 56 | db_content.passTime = db_content.time; 57 | db_content.passDuration = db_content.duration; 58 | delete db_content.date; 59 | delete db_content.time; 60 | delete db_content.duration; 61 | //console.log(db_content); 62 | 63 | var params = { 64 | TableName: 'passes-prod', 65 | Item: db_content, 66 | ReturnConsumedCapacity: "TOTAL" 67 | }; 68 | 69 | console.log("Adding a new item..."); 70 | docClient.put(params, function(err, data) { 71 | if (err) { 72 | console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2)); 73 | } else { 74 | console.log("Added item:", JSON.stringify(data, null, 2)); 75 | } 76 | }); 77 | } 78 | 79 | }) 80 | 81 | }); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /schedule_satellite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SAT=$1 4 | FREQ=$2 5 | TLE_FILE=INSTALL_DIR/weather.tle 6 | PREDICTION_START=`/usr/bin/predict -t $TLE_FILE -p "$SAT" | head -1` 7 | PREDICTION_END=`/usr/bin/predict -t $TLE_FILE -p "$SAT" | tail -1` 8 | 9 | END_EPOCH=`echo $PREDICTION_END | cut -d " " -f 1` 10 | END_EPOCH_DATE=`date --date="TZ=\"UTC\" @${END_EPOCH}" +%D` 11 | 12 | MAXELEV=`/usr/bin/predict -t $TLE_FILE -p "${SAT}" | awk -v max=0 '{if($5>max){max=$5}}END{print max}'` 13 | START_LAT=`echo $PREDICTION_START | awk '{print $8}'` 14 | END_LAT=`echo $PREDICTION_END | awk '{print $8}'` 15 | if [ $START_LAT -gt $END_LAT ] 16 | then 17 | DIR="southbound" 18 | else 19 | DIR="northbound" 20 | fi 21 | 22 | 23 | while [ $END_EPOCH_DATE == `date +%D` ] || [ $END_EPOCH_DATE == `date --date="tomorrow" +%D` ]; do 24 | 25 | START_TIME=`echo $PREDICTION_START | cut -d " " -f 3-4` 26 | START_EPOCH=`echo $PREDICTION_START | cut -d " " -f 1` 27 | 28 | SECONDS_REMAINDER=`echo $START_TIME | cut -d " " -f 2 | cut -d ":" -f 3` 29 | 30 | JOB_START=`date --date="TZ=\"UTC\" $START_TIME" +"%H:%M %D"` 31 | 32 | # at jobs can only be started on minute boundaries, so add the 33 | # seconds remainder to the duration of the pass because the 34 | # recording job will start early 35 | PASS_DURATION=`expr $END_EPOCH - $START_EPOCH` 36 | JOB_TIMER=`expr $PASS_DURATION + $SECONDS_REMAINDER` 37 | OUTDATE=`date --date="TZ=\"UTC\" $START_TIME" +%Y%m%d-%H%M%S` 38 | 39 | if [ $MAXELEV -ge 20 ] 40 | then 41 | FILEKEY="${SAT//" "}-${OUTDATE}" 42 | COMMAND="INSTALL_DIR/receive_and_process_satellite.sh \"${SAT}\" $FREQ $FILEKEY $TLE_FILE $START_EPOCH $JOB_TIMER $MAXELEV $DIR" 43 | echo $COMMAND 44 | echo $COMMAND | at $JOB_START 45 | 46 | TLE1=`grep "$SAT" $TLE_FILE -A 2 | tail -2 | head -1 | tr -d '\r'` 47 | TLE2=`grep "$SAT" $TLE_FILE -A 2 | tail -2 | tail -1 | tr -d '\r'` 48 | 49 | echo ${START_EPOCH},${END_EPOCH},${MAXELEV},${DIR},${SAT},"${TLE1}","${TLE2}" >> INSTALL_DIR/upcoming_passes.txt 50 | fi 51 | 52 | nextpredict=`expr $END_EPOCH + 60` 53 | 54 | PREDICTION_START=`/usr/bin/predict -t $TLE_FILE -p "${SAT}" $nextpredict | head -1` 55 | PREDICTION_END=`/usr/bin/predict -t $TLE_FILE -p "${SAT}" $nextpredict | tail -1` 56 | 57 | MAXELEV=`/usr/bin/predict -t $TLE_FILE -p "${SAT}" $nextpredict | awk -v max=0 '{if($5>max){max=$5}}END{print max}'` 58 | START_LAT=`echo $PREDICTION_START | awk '{print $8}'` 59 | END_LAT=`echo $PREDICTION_END | awk '{print $8}'` 60 | if [ $START_LAT -gt $END_LAT ] 61 | then 62 | DIR="southbound" 63 | else 64 | DIR="northbound" 65 | fi 66 | 67 | END_EPOCH=`echo $PREDICTION_END | cut -d " " -f 1` 68 | END_EPOCH_DATE=`date --date="TZ=\"UTC\" @${END_EPOCH}" +%D` 69 | 70 | done 71 | -------------------------------------------------------------------------------- /aws-s3/remove-wx-images.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({path:'/home/pi/wx-ground-station/aws-s3/.env'}); 2 | var fs = require('fs'); 3 | var AWS = require('aws-sdk'); 4 | var uuid = require('uuid'); 5 | 6 | var REGION = process.env.AWS_REGION; 7 | var BUCKET = process.env.AWS_BUCKET; 8 | var IMAGE_DIR = "images/"; 9 | 10 | AWS.config.update({ 11 | region: REGION 12 | }); 13 | 14 | var s3 = new AWS.S3(); 15 | var docClient = new AWS.DynamoDB.DocumentClient(); 16 | 17 | var table = "passes-prod"; 18 | 19 | // Get pass that will be deleted 20 | // Passed from command line 21 | var filebase = process.argv[2]; 22 | 23 | var theDate_yr = filebase.substring(7, 11); 24 | var theDate_mo = filebase.substring(11, 13); 25 | var theDate_day = filebase.substring(13, 15); 26 | var theTime_hr = filebase.substring(16, 18); 27 | var theTime_min = filebase.substring(18, 20); 28 | var theTime_sec = filebase.substring(20, 22); 29 | var passDate = theDate_yr + "-" + theDate_mo + "-" + theDate_day; 30 | var passTime = theTime_hr + ":" + theTime_min + ":" + theTime_sec + " +0000"; 31 | 32 | console.log("Removing files and pass information from S3 and DynamoDB for " + filebase + "*"); 33 | 34 | // All files that will be deleted 35 | var files = [ 36 | filebase + ".json", 37 | filebase + "-ZA.png", 38 | filebase + "-NO.png", 39 | filebase + "-MSA.png", 40 | filebase + "-MSA-precip.png", 41 | filebase + "-MCIR.png", 42 | filebase + "-MCIR-precip.png", 43 | filebase + "-THERM.png", 44 | "thumbs/" + filebase + "-ZA.png", 45 | "thumbs/" + filebase + "-NO.png", 46 | "thumbs/" + filebase + "-MSA.png", 47 | "thumbs/" + filebase + "-MSA-precip.png", 48 | "thumbs/" + filebase + "-MCIR.png", 49 | "thumbs/" + filebase + "-MCIR-precip.png", 50 | "thumbs/" + filebase + "-THERM.png" 51 | ]; 52 | 53 | // Iterate through each file and run removeFile function 54 | files.forEach(removeFile); 55 | 56 | deletePass(passDate,passTime); 57 | 58 | function deletePass(delDate,delTime){ 59 | var params = { 60 | TableName:table, 61 | Key:{ 62 | "passDate":delDate, 63 | "passTime":delTime 64 | } 65 | }; 66 | 67 | console.log("Attempting DynamoDB delete..."); 68 | docClient.delete(params, function(err, data) { 69 | if (err) { 70 | console.error("Unable to delete item. Error JSON:", JSON.stringify(err, null, 2)); 71 | } else { 72 | console.log("DeleteItem succeeded:", JSON.stringify(data, null, 2)); 73 | } 74 | }); 75 | } 76 | 77 | // Function removes files from S3 78 | // Designed to remove one file at a time 79 | function removeFile(filename) { 80 | // S3 params to delete files 81 | var params = { 82 | Bucket: BUCKET, 83 | Key: IMAGE_DIR + filename, 84 | }; 85 | 86 | // Delete from S3 87 | s3.deleteObject(params, (err, data) => { 88 | if (err) { 89 | console.log(err) 90 | } else { 91 | console.log(" successfully removed " + filename); 92 | } 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /aws-api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "aws-sdk": { 6 | "version": "2.635.0", 7 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.635.0.tgz", 8 | "integrity": "sha512-NlKqMB4HqMqSutY6YmPzQVa+mMhqo0655hYYl8G2zkUvrYy+YxDitvwDEUkSsNKVFkEvmHtZggFCgVYIUu/sXg==", 9 | "requires": { 10 | "buffer": "4.9.1", 11 | "events": "1.1.1", 12 | "ieee754": "1.1.13", 13 | "jmespath": "0.15.0", 14 | "querystring": "0.2.0", 15 | "sax": "1.2.1", 16 | "url": "0.10.3", 17 | "uuid": "3.3.2", 18 | "xml2js": "0.4.19" 19 | } 20 | }, 21 | "base64-js": { 22 | "version": "1.3.1", 23 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 24 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 25 | }, 26 | "buffer": { 27 | "version": "4.9.1", 28 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 29 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 30 | "requires": { 31 | "base64-js": "^1.0.2", 32 | "ieee754": "^1.1.4", 33 | "isarray": "^1.0.0" 34 | } 35 | }, 36 | "events": { 37 | "version": "1.1.1", 38 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 39 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 40 | }, 41 | "ieee754": { 42 | "version": "1.1.13", 43 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 44 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 45 | }, 46 | "isarray": { 47 | "version": "1.0.0", 48 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 49 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 50 | }, 51 | "jmespath": { 52 | "version": "0.15.0", 53 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 54 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 55 | }, 56 | "punycode": { 57 | "version": "1.3.2", 58 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 59 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 60 | }, 61 | "querystring": { 62 | "version": "0.2.0", 63 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 64 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 65 | }, 66 | "sax": { 67 | "version": "1.2.1", 68 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 69 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 70 | }, 71 | "url": { 72 | "version": "0.10.3", 73 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 74 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 75 | "requires": { 76 | "punycode": "1.3.2", 77 | "querystring": "0.2.0" 78 | } 79 | }, 80 | "uuid": { 81 | "version": "3.3.2", 82 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 83 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 84 | }, 85 | "xml2js": { 86 | "version": "0.4.19", 87 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 88 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 89 | "requires": { 90 | "sax": ">=0.6.0", 91 | "xmlbuilder": "~9.0.1" 92 | } 93 | }, 94 | "xmlbuilder": { 95 | "version": "9.0.7", 96 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 97 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | weather satellite ground station 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 51 | 92 | 93 | 94 | 95 | 96 | 97 |
98 | 124 | 125 | 126 |
127 | 128 | 129 |

weather satellite ground station

130 |
131 |

Receiver: RTL-SDR 132 |
Filter: SAWbird+ Filter & LNA / 137MHz center frequency 133 |
Antenna: Quadrifilar Helix

134 |
135 |
136 | 137 | 138 |
139 |
140 |
141 |

142 |
143 |
144 |
145 |

Recent captures:

146 |
147 |
148 |
149 | 153 |
154 |
155 | 156 | 157 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /website/rpi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | weather satellite ground station 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 86 | 87 | 88 |
89 | 90 | 91 |

weather satellite ground station

92 |
93 |

Receiver: RTL-SDR 94 |
Filter: SAWbird+ Filter & LNA / 137MHz center frequency 95 |
Antenna: Quadrifilar Helix

96 |
97 |
98 | 99 |
100 |

Raspberry Pi Hardware Setup

101 | 102 |

Transporting the Raspberry PI, RTL-SDR dongle, and filter can be a little difficult. I decided to create a case that would help make the kit easier to plug in and use. In the fall, winter, and even some parts of the spring, I leave this outside permanently but I would not advise leaving this out during the summer as it may lead to heat issues.

103 | 104 |

I made a case for my RTL-SDR, LNA, and Raspberry Pi, which make my NOAA ground station. This is attached to a QFH antenna and is setup to automatically capture, decode, and upload NOAA satellite passes. All the decoded images and pass details are uploaded to an AWS website hosted via S3. See here:

105 | 106 |

Raspberry Pi Case Open

107 | 108 |

As you can see the RTL-SDR is connected to the Nooelec Filter + LNA using a jumper cable. The antenna connection protrudes just a bit from the case to allow it to connect to an antenna when closed. All of the hardware is on the case using Commander Strips.

109 | 110 |

That jumper cable is bending a little more than I'd like but that's about as much distance as I could create between the two devices/connections.

111 | 112 |

The RTL-SDR is connected to the RPI with a 12 inch USB cable. I dislike this part about the case and open to any suggestions. The 6in cable was not enough distance and didn't allow the case to be opened. It also required a lot of bending. This way, it may stick out of the case but the bend is better and less restrictive.

113 | 114 |

Raspberry Pi Case Closed

115 | 116 |

Once closed you can see the SMA connector for connecting to an antenna and the USB cable that connects the RTL-SDR and RPI. In the background on the top left you can see where the power cable comes in for the RPI. I've been monitoring temperature and it's usually between 42 and 50 degress Celcius but I have seen it go all the way up to around 58 degrees when uploading images.

117 | 118 |

Parts List

119 | 120 | 130 | 131 |
132 |
133 | 134 | 135 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /website/qfh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | weather satellite ground station 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 40 | 41 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 86 | 87 | 88 |
89 | 90 | 91 |

weather satellite ground station

92 |
93 |

Receiver: RTL-SDR 94 |
Filter: SAWbird+ Filter & LNA / 137MHz center frequency 95 |
Antenna: Quadrifilar Helix

96 |
97 |
98 | 99 |
100 |

Making a Quadrifilar Helix Antenna

101 | 102 |

QFH Antenna

103 | 104 |

This odd looking thing is a quadrifilar helix antenna and it's specially designed to receive images from the NOAA weather satellites. It's actually two antennas that are formed by a larger loop and a smaller loop. These loops are shaped to receive a signal on 137MHz frequency and block out the rest of the noise. Below are some resources and my learnings on building a QFH antenna.

105 | 106 |

First to explain why such an odd shaped antenna is needed, it helps to understand how the radio signal that comes from the NOAA satellites look. See below:

107 | 108 |

Helix Radio Signal

109 | 110 |

There are a lot of tutorials out there and I would say that there is enough variation in each tutorial to make it confusing. Tutorials are mostly geared toward using copper tubing but a few also include a wire. version. Ultimately, I used this Instructables guide with a lot of help from the calculator on the John Coppens' website.

111 | 112 |

Many of the tutorials out there guide you to use the coax as part of the antenna but that ended up confusing me a bit and I decided to just make two loops instead. From a technical point of view, I didn’t see how it would make a big difference and that the most important thing to do was to get two loops that were each about 1λ. This below is the best diagram I found on how to wire it all together.

113 | 114 |

QFH Wiring

115 | 116 |

I used 14 AWG wire, which is pretty thick and hard to reliably measure. I went ahead and just created the loops a bit longer than I thought they should be. I probably could have measured out a string for measuring the wire more precisely but I guess I got lazy and just wanted the lengths at that point because I was in the homestretch for putting it all together.

117 | 118 |

Once I got the lengths cut, I cabled everything up with some electrical tape to test SWR with my NanoVNA. Of course, the lengths were too long and it was tuning to around 120 mHz. I started cutting down lengths of about one inch on each of the four ends and testing until I got closer to an SWR of about 1.7 at 137 mHz. If you don't have a VNA, don't worry. You can skip it as long as you made sure the wire lengths were about the right size. You may just want to take more time when measuring.

119 | 120 |

Next big task is to solder everything together. You'll just need a soldering iron, some solder, and flux. These can be bought rather inexpensively but you can also just borrow one from a friend or neighbor. However, the trick to making this easy is to purchase a cable that is already crimped for your SDR. These are typically SMA connectors. I purchased a 25ft cable with SMA connectors on the end and just cut one end off. This left the other end exposed for me to solder onto the anteanna. No need to crimp any connectors.

121 | 122 |

As with any antenna, the higher, the better. That said, my antenna is secured to a Christmas tree stand in my lawn and I get great images.

123 | 124 |

If you find that building a QFH antenna is a bit more than you want to do at the moment, you can always build a dipole. Below is a diagram on measurements, angle, and direction. You can also buy a simple kit from RTL-SDR that will allow you to just extend to the appropriate length.

125 | 126 |

Dipole Antenna

127 |
128 |
129 | 130 | 131 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /aws-s3/upload-wx-images.js: -------------------------------------------------------------------------------- 1 | // Required modules 2 | require('dotenv').config({path:'/home/pi/wx-ground-station/aws-s3/.env'}); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var glob = require("glob"); 6 | var AWS = require('aws-sdk'); 7 | var uuid = require('uuid'); 8 | var Jimp = require('jimp'); 9 | var dateFormat = require('dateformat'); 10 | var discord = require('./lib/api/discord'); 11 | 12 | // AWS Configuration 13 | var REGION = process.env.AWS_REGION; 14 | var BUCKET = process.env.AWS_BUCKET; 15 | 16 | // Title information printed over image 17 | var LOCATION = process.env.STATION_LOCATION; 18 | 19 | // Working directories for data 20 | var IMAGE_DIR = "images/"; 21 | var LOG_DIR = "logs/"; 22 | var AUDIO_DIR = "audio/"; 23 | var MAP_DIR = "maps/" 24 | 25 | // Set region 26 | AWS.config.update({region: REGION}); 27 | // Create S3 service object 28 | var s3 = new AWS.S3(); 29 | 30 | // Put the array of command line arguments into variables 31 | var satellite = process.argv[2]; 32 | var frequency = process.argv[3]; 33 | var filebase = process.argv[4]; 34 | var elevation = process.argv[5]; 35 | var direction = process.argv[6]; 36 | var duration = process.argv[7]; 37 | var tle1 = process.argv[8]; 38 | var tle2 = process.argv[9]; 39 | var gain = process.argv[10]; 40 | var chan_a = process.argv[11]; 41 | var chan_b = process.argv[12]; 42 | 43 | // Get the name of the files (such as NOAA15-20200227-141322) 44 | var basename = filebase.slice(filebase.lastIndexOf('/')+1); 45 | // Get the directory name 46 | var dirname = filebase.slice(0, filebase.lastIndexOf('/')+1); 47 | // Get the root directory without images directory 48 | var rootdirname = filebase.slice(0, filebase.lastIndexOf('/') - 6); 49 | // Get individual parts of the base name (such as NOAA15, 20200227, and 141322) 50 | var components = basename.split("-"); 51 | // Get date as the second part of the array and format as YYYY-MM-DD 52 | var date = components[1]; 53 | date = date.slice(0, 4) + '-' + date.slice(4, 6) + '-' + date.slice(6); 54 | // Get date as the third part of the array and format as HH:MM:ss 55 | var time = components[2]; 56 | time = time.slice(0, 2) + ':' + time.slice(2, 4) + ':' + time.slice(4) + ' ' + dateFormat(new Date, "o"); 57 | 58 | // Define gain by getting the desired value from the full string 59 | // example "Gain: 15.2" 60 | if (gain) { 61 | gain = gain.substring(gain.indexOf(": ") + 2) 62 | } 63 | 64 | // Define Channel A and B by getting the desired value from the full string 65 | // example "Channel A: 1 (visible)" 66 | if (chan_a) { 67 | chan_a = chan_a.substring(chan_a.indexOf(": ")+2); 68 | } 69 | // example "Channel B: 4 (thermal infrared)" 70 | if (chan_b) { 71 | chan_b = chan_b.substring(chan_b.indexOf(": ")+2); 72 | } 73 | 74 | console.log("Uploading files " + path.basename(filebase) + "* to S3..."); 75 | 76 | // Create an array with all the metadata for the datellite pass 77 | // This applies to all images 78 | var metadata = { 79 | satellite: satellite, 80 | date: date, 81 | time: time, 82 | elevation: elevation, 83 | direction: direction, 84 | duration: duration, 85 | imageKey: filebase.slice(filebase.lastIndexOf('/')+1), 86 | tle1: tle1, 87 | tle2: tle2, 88 | frequency: frequency, 89 | gain: gain, 90 | chan_a: chan_a, 91 | chan_b: chan_b, 92 | images: [] 93 | }; 94 | 95 | // Function to upload images to S3 96 | async function uploadImage(image, filename) { 97 | // Get width and height of the image 98 | var w = image.bitmap.width; 99 | var h = image.bitmap.height; 100 | var enhancement; 101 | 102 | // Define the enhancement variable depending on the name of the file 103 | if (filename.endsWith("-ZA.png")) enhancement = "normal infrared"; 104 | if (filename.endsWith("-NO.png")) enhancement = "color infrared"; 105 | if (filename.endsWith("-MSA.png")) enhancement = "multispectral analysis"; 106 | if (filename.endsWith("-MSA-precip.png")) enhancement = "multispectral precip"; 107 | if (filename.endsWith("-MCIR.png")) enhancement = "map color infrared"; 108 | if (filename.endsWith("-MCIR-precip.png")) enhancement = "map color infrared precip"; 109 | if (filename.endsWith("-THERM.png")) enhancement = "thermal"; 110 | if (filename.endsWith("-PRISTINE.png")) enhancement = "pristine view"; 111 | 112 | // Define imageInfo variable with details of the image 113 | var imageInfo = { 114 | filename: filename, 115 | width: w, 116 | height: h, 117 | thumbfilename: 'thumbs/' + filename, 118 | enhancement: enhancement 119 | }; 120 | 121 | // Upload image 122 | // Determine if watermark will be applied (0 = no watermark) 123 | if(process.env.WATERMARK==0){ 124 | // Read image from local directory 125 | var upContent = fs.readFileSync(rootdirname + "images/" + filename); 126 | // Parameters needed to upload file to S3 127 | var upParams = { 128 | ACL: "public-read", 129 | ContentType: "image/png", 130 | Bucket: BUCKET, 131 | Key: IMAGE_DIR + filename, 132 | Body: upContent 133 | }; 134 | // Upload file to S3 135 | uploadS3(upParams,filename); 136 | 137 | // Upload thumbs 138 | //Create a Jimp image that will be sized for thumbnails 139 | var newImage = await new Jimp(image.bitmap.width, image.bitmap.height+64, '#000000'); 140 | newImage.composite(image, 0, 0); 141 | image = newImage; 142 | // Clone the image to the thumb variable 143 | var thumb = image.clone(); 144 | // Scale the image to the given w & h, some parts of image may be clipped 145 | thumb.cover(260, 200); 146 | var thumbFilename = "thumbs/" + filename; 147 | // Put image in buffer to upload 148 | thumb.getBuffer(Jimp.MIME_PNG, (err, buffer) => { 149 | // Parameters needed to upload file to S3 150 | var params = { 151 | ACL: "public-read", 152 | ContentType: "image/png", 153 | Bucket: BUCKET, 154 | Key: IMAGE_DIR + thumbFilename, 155 | Body: buffer 156 | }; 157 | var thumbName = "thumb/"+filename; 158 | // Upload file to S3 159 | uploadS3(params,thumbName); 160 | }); 161 | 162 | } else { 163 | // Load font included in Jimp 164 | var font = await Jimp.loadFont(Jimp.FONT_SANS_16_WHITE); 165 | // Create a new image with width, height, and image background 166 | var newImage = await new Jimp(image.bitmap.width, image.bitmap.height+64, '#000000'); 167 | if (filename.endsWith("-PRISTINE.png")){ 168 | newImage.composite(image, 0, 0); 169 | image = newImage; 170 | console.log(filename + " image created (pristine)"); 171 | } else{ 172 | // Composites another Jimp image over image at x, y 173 | newImage.composite(image, 0, 48); 174 | image = newImage; 175 | // Print image details and location at the top of the image 176 | image.print(font, 5, 5, metadata.date + " " + metadata.time + " satellite: " + metadata.satellite + 177 | " elevation: " + metadata.elevation + '\xB0' + " enhancement: " + enhancement); 178 | image.print(font, 5, 25, LOCATION); 179 | console.log(filename + " image created"); 180 | } 181 | 182 | // Put image in buffer to upload 183 | image.getBuffer(Jimp.MIME_PNG, (err, buffer) => { 184 | // Parameters needed to upload file to S3 185 | var params = { 186 | ACL: "public-read", 187 | ContentType: "image/png", 188 | Bucket: BUCKET, 189 | Key: IMAGE_DIR + filename, 190 | Body: buffer 191 | }; 192 | // Upload file to S3 193 | uploadS3(params,filename); 194 | }); 195 | 196 | // Upload thumbs 197 | // Clone the image to the thumb variable 198 | var thumb = image.clone(); 199 | // Scale the image to the given w & h, some parts of image may be clipped 200 | thumb.cover(260, 200); 201 | var thumbFilename = "thumbs/" + filename; 202 | // Put image in buffer to upload 203 | thumb.getBuffer(Jimp.MIME_PNG, (err, buffer) => { 204 | // Parameters needed to upload file to S3 205 | var params = { 206 | ACL: "public-read", 207 | ContentType: "image/png", 208 | Bucket: BUCKET, 209 | Key: IMAGE_DIR + thumbFilename, 210 | Body: buffer 211 | }; 212 | // Upload file to S3 213 | uploadS3(params,filename); 214 | }); 215 | } 216 | 217 | // Return image information that was uploaded 218 | return imageInfo; 219 | } 220 | 221 | // Function to upload JSON file with all of the pass metadata 222 | function uploadMetadata(filebase) { 223 | // Create a promise to return 224 | return new Promise((resolve, reject)=> { 225 | // Upload JSON 226 | // Define name for the JSON file that contains all the metadata 227 | var metadataFilename = filebase + ".json"; 228 | // Parameters needed to upload file to S3 229 | var params = { 230 | ACL: "public-read", 231 | Bucket: BUCKET, 232 | Key: IMAGE_DIR + metadataFilename, 233 | Body: JSON.stringify(metadata, null, 2) 234 | }; 235 | var metaUp = uploadS3(params,metadataFilename); 236 | 237 | // Upload map 238 | // Define map file name and read from local directory 239 | var mapFilename = filebase + "-map.png"; 240 | var mapContent = fs.readFileSync(rootdirname + "images/" + mapFilename); 241 | // Parameters needed to upload file to S3 242 | var mapParams = { 243 | ACL: "public-read", 244 | ContentType: "image/png", 245 | Bucket: BUCKET, 246 | Key: MAP_DIR + mapFilename, 247 | Body: mapContent 248 | }; 249 | var mapUp = uploadS3(mapParams,mapFilename); 250 | 251 | //Upload Audio 252 | // Define wav file name and read from local directory 253 | var audioFilename = filebase + ".wav"; 254 | var audioContent = fs.readFileSync(rootdirname + "audio/" + audioFilename); 255 | // Parameters needed to upload file to S3 256 | var audioParams = { 257 | ACL: "public-read", 258 | Bucket: BUCKET, 259 | Key: AUDIO_DIR + audioFilename, 260 | Body: audioContent 261 | }; 262 | var audioUp = uploadS3(audioParams,audioFilename); 263 | 264 | //Upload Logs 265 | // Define log file name and read from local directory 266 | var logFilename = filebase + ".log"; 267 | var logContent = fs.readFileSync(rootdirname + "logs/" + logFilename); 268 | // Parameters needed to upload file to S3 269 | var logParams = { 270 | ACL: "public-read", 271 | Bucket: BUCKET, 272 | Key: LOG_DIR + logFilename, 273 | Body: logContent 274 | }; 275 | var logUp = uploadS3(logParams,logFilename); 276 | 277 | // Put all promises into a single promise array 278 | // Resolve when all promises have resolved 279 | Promise.all([metaUp, mapUp, audioUp, logUp]).then((result) => { 280 | resolve(); 281 | }); 282 | }); 283 | } 284 | 285 | // Function to upload file to S3 286 | function uploadS3(params,fileName){ 287 | return new Promise((resolve, reject)=> { 288 | s3.putObject(params, function(err, data) { 289 | if (err) { 290 | console.log(err) 291 | } else { 292 | console.log(" successfully uploaded to S3 - " + fileName); 293 | resolve(); 294 | } 295 | }); 296 | }); 297 | } 298 | 299 | // Function to upload pass information to DynamoD 300 | function uploadtoDynamo(keyname){ 301 | // Create the DynamoDB service object 302 | var docClient = new AWS.DynamoDB.DocumentClient(); 303 | // Key name (name of JSON file to upload) 304 | var s3key = "images/" + keyname + ".json"; 305 | console.log("Looking up log file for DynaoDB upload: " + s3key); 306 | //construct getParam 307 | var getParams = { 308 | Bucket: BUCKET, 309 | Key: s3key 310 | } 311 | 312 | //Fetch or read data from aws s3 313 | s3.getObject(getParams, function (err, data) { 314 | 315 | if (err) { 316 | console.log(err); 317 | } else { 318 | 319 | var content = data.Body.toString(); 320 | var db_content = JSON.parse(content); 321 | 322 | // Replaces key names for date, time, and duration 323 | // to avoid reserved term conflicts with DynamoDB 324 | db_content.passDate = db_content.date; 325 | db_content.passTime = db_content.time; 326 | db_content.passDuration = db_content.duration; 327 | delete db_content.date; 328 | delete db_content.time; 329 | delete db_content.duration; 330 | 331 | // Create params for DynamoDB 332 | var dbParams = { 333 | TableName: 'passes-prod', 334 | Item: db_content, 335 | ReturnConsumedCapacity: "TOTAL" 336 | }; 337 | 338 | console.log("Adding a new item to DynamoDB..."); 339 | 340 | // Put command to upload info to DynamoDB 341 | docClient.put(dbParams, function(err, data) { 342 | if (err) { 343 | console.error("Unable to add item to DyanamoDB. Error JSON:", JSON.stringify(err, null, 2)); 344 | } else { 345 | console.log("Added to DynamoDB - item:", JSON.stringify(keyname, null, 2)); 346 | } 347 | }); 348 | } 349 | 350 | }) 351 | } 352 | 353 | // Find all the files that match the filebase plus a wildcard of capital 354 | // letters followed by any character and a png extension such as 355 | // /home/pi/wx-ground-station/images/NOAA15-20200227-141322-MCIR.png 356 | // this will leave out map file as it is handled in the metadata upload 357 | glob(filebase + "-[A-Z]*.png", {}, function (err, files) { //<- Old version 358 | //Create an array to upload files and store promise 359 | var uploadPromises = []; 360 | 361 | var keyname = components[0] + "-" + components[1] + "-" + components[2]; 362 | //Iterate through each file 363 | files.forEach(function(filename) { 364 | // Get the last part of the path returned by filename 365 | // Ensures we only get file name such as NOAA15-20200227-141322-MCIR.png 366 | var basename = path.basename(filename); 367 | // Open the image file; using promise notation 368 | Jimp.read(filename) 369 | .then(image => { 370 | // Push new image to the uploadPromises array and call the uploadImage function 371 | uploadPromises.push(uploadImage(image, basename)); 372 | // If the uploadPromise array is equal to the 373 | // number of files proceed to uploading metadata 374 | if (uploadPromises.length == files.length) { 375 | // Returns a single Promise that fulfills when all of the promises passed 376 | Promise.all(uploadPromises).then((values) => { 377 | // Set value for metadata.values in the array to 378 | // the 'values' variable in the returned promise 379 | metadata.images = values; 380 | // Call function to upload metadata to S3 381 | uploadMetadata(path.basename(filebase)).then(function(result) { 382 | console.log("Images uploaded to S3: " + JSON.stringify(values, null, 2)); 383 | // Upload all information to DynamoDB 384 | uploadtoDynamo(keyname); 385 | // Call function to send a Discord message about the pass 386 | discord(satellite,keyname,elevation,direction); 387 | }); 388 | }); 389 | } 390 | }) 391 | }); 392 | }); 393 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automated NOAA Weather Satellite Ground Station 2 | 3 | ![Example Weather Capture](documentation/example-mcir-precip.png "MCIR with Precipitation") 4 | 5 | ## Introduction 6 | 7 | This project allows you to create a fully automated ground station that will receive and decode NOAA weather satellite images and upload them to your own website served from an Amazon AWS S3 bucket. [See my example S3 site](https://wximages.s3-us-west-1.amazonaws.com/index.html). 8 | 9 | This projected was originally adapted from an [Instructables article](https://www.instructables.com/id/Raspberry-Pi-NOAA-Weather-Satellite-Receiver/) by Jim Haslett that outlines how to customize a Raspberry Pi to receive weather images. The goal was to play the RPI close to the antenna to reduce feed-line loss. 10 | 11 | The project was then adapted in this [article](https://nootropicdesign.com/projectlab/2019/11/08/weather-satellite-ground-station/) where the images were automatically uploaded to S3 then displayed on a website. 12 | 13 | The goal of this branch is to extend the functionality that has been established and allow the weather station information to scale for weather stations that are constantly collecting information for server days, weeks, or months at a time. 14 | 15 | --- 16 | 17 | ## Installation Instructions 18 | 19 | Here’s what you’ll need: 20 | 21 | * Raspberry Pi (version 3 or 4) - you'll need Wi-Fi if you are using it outdoors or away from a network connection 22 | * Raspberry Pi case 23 | * MicroUSB power supply 24 | * 32GB SD card 25 | * SDR device - I recommend the RTL-SDR V3 dongle from RTL-SDR.COM blog 26 | * AWS account for hosting images and web content in an Amazon S3 bucket. You can sign up for the free tier for a year, and it’s still cheap after that 27 | * An antennea such as a dipole or a QFH 28 | * coaxial cable to go from your antenna to Raspberry Pi + RTL-SDR dongle. The dipole antenna kit comes with 3m of RG174 coax, but I used 10 feet of RG58 coax. 29 | 30 | >Dipole Antenna - You can build a simple dipole antenna with elements 21 inches (53.4 cm) long adjusted to a 120 degree angle between the elements. Here’s a great [article](http://lna4all.blogspot.com/2017/02/diy-137-mhz-wx-sat-v-dipole-antenna.html) on design or you can just buy [this dipole kit](https://www.rtl-sdr.com/product/rtl-sdr-blog-multipurpose-dipole-antenna-kit/) from the RTL-SDR.COM blog. 31 | 32 | ### Amazon AWS Setup 33 | 34 | The website for the ground station will be a serverless application hosted on S3. The RPI will use some Node.js scripts to upload all of the images and related assets. Here are the steps to setup your AWS account: 35 | 36 | #### AWS Credentials 37 | 38 | The scripts that run on the RPI are powered by Node.js and the AWS JavaScript SDK. To get them to all work, you will need to get credentials for your account. These two articles show you how to access or generate your credentials and store them for Node.js access: 39 | 40 | [Getting your credentials](https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html) 41 | [Loading Credentials in Node.js from the Shared Credentials File](https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html) 42 | 43 | Your credentials file on the Raspberry Pi `~/.aws/credentials` will look like this: 44 | 45 | ``` 46 | [default] 47 | aws_access_key_id = YOUR_ACCESS_KEY_ID 48 | aws_secret_access_key = YOUR_SECRET_ACCESS_KEY 49 | ``` 50 | 51 | Set the default region where your S3 bucket will reside in `~/.aws/config`. For example: 52 | 53 | ``` 54 | [default] 55 | output = json 56 | region = us-west-1 57 | ``` 58 | 59 | #### Create an S3 Bucket 60 | 61 | Now create an S3 bucket for public website hosting such as `wximages`. The following articles shows how to setup a bucket for public website hosting: 62 | [Setting up a Static Website](https://docs.aws.amazon.com/en_pv/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html) 63 | 64 | At this point you should be able to load a simple web site from your new bucket. You might want to upload a simple `index.html` file and try to load it in your browser with `http://BUCKETNAME.s3-website-REGION.amazonaws.com/`. 65 | 66 | ``` 67 | 68 | 69 | S3 test 70 | Hello from S3 71 | 72 | ``` 73 | 74 | #### Create an Identity Pool in Cognito 75 | 76 | To give public users the ability to access your S3 bucket using the AWS SDK, you need to set up an identity pool and create a policy allowing them read access to your bucket. This is done using Amazon Cognito. A good guide for granting public access to your bucket is described in [this article](https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/s3-example-photos-view.html) that shows how to serve images from an S3 bucket just like in this project. 77 | 78 | **Step 1:** [create an Amazon Cognito identity pool](https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/getting-started-browser#getting-started-browser-create-identity-pool) called `wx image users` and enable access to unauthenticated identities. Be sure to select the region in the upper right of the page that matches the region where your S3 bucket was created! Make note of the role name for unauthorized users, e.g. `Cognito_wximageusersUnauth_Role`. 79 | 80 | **Step 2:** on the Sample Code page, select JavaScript from the Platform list. Save this code somewhere, because we need to add it to the web content later. It looks something like this: 81 | 82 | ``` 83 | // Initialize the Amazon Cognito credentials provider 84 | AWS.config.region = 'us-west-1'; // Region 85 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 86 | IdentityPoolId: 'us-west-1:1d02ae39-3a06-497e-b63c-799a070dd09d', 87 | }); 88 | ``` 89 | 90 | **Step 3:** Add a Policy to the Created IAM Role. In [IAM console](https://console.aws.amazon.com/iam/), choose `Policies`. Click `Create Policy`, then click the JSON tab and add this, substituting BUCKET_NAME with your bucket name. 91 | 92 | ``` 93 | { 94 | "Version": "2012-10-17", 95 | "Statement": [ 96 | { 97 | "Effect": "Allow", 98 | "Action": [ 99 | "s3:ListBucket" 100 | ], 101 | "Resource": [ 102 | "arn:aws:s3:::BUCKET_NAME" 103 | ] 104 | } 105 | ] 106 | } 107 | ``` 108 | 109 | Click `Review policy` and give your policy a name, like `wxImagePolicy`. 110 | 111 | In IAM console, click `Roles`, then choose the unauthenticated user role previously created when the identity pool was created (e.g. `Cognito_wximageusersUnauth_Role`). Click `Attach Policies`. From the `Filter policies` menu, select `Customer managed`. This will show the policy you created above. Select it and click Attach policy. 112 | 113 | **Step 4.** Set CORS configuration on the S3 bucket. In the S3 console for your bucket, select `Permissions`, then `CORS configuration`. 114 | 115 | ``` 116 | 117 | 118 | 119 | * 120 | GET 121 | HEAD 122 | * 123 | 124 | 125 | ``` 126 | ### Raspberry Pi Setup 127 | 128 | Install Required Packages 129 | First, make sure your Raspberry Pi is up to date: 130 | 131 | ``` 132 | sudo apt-get update 133 | sudo apt-get upgrade 134 | sudo reboot 135 | ``` 136 | 137 | Then install a set of of required packages. 138 | 139 | ``` 140 | sudo apt-get install libusb-1.0 141 | sudo apt-get install cmake 142 | sudo apt-get install sox 143 | sudo apt-get install at 144 | sudo apt-get install predict 145 | ``` 146 | 147 | I used Node.js in some of the scripting, so if you don’t have `node` and `npm` installed, you’ll need to do that. In depth [details are here](https://github.com/nodesource/distributions/#deb), and I easily installed with: 148 | 149 | ``` 150 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 151 | sudo apt-get install -y nodejs 152 | ``` 153 | 154 | Using your favorite editor as root (e.g. `nano`), create a file `/etc/modprobe.d/no-rtl.conf` and add these contents: 155 | 156 | ``` 157 | blacklist dvb_usb_rtl28xxu 158 | blacklist rtl2832 159 | blacklist rtl2830 160 | ``` 161 | 162 | #### Build rtl-sdr 163 | 164 | Even if you have `rtl-sdr` already built and installed, it’s important to use the version in the GitHub repo [keenerd/rtl-sdr](https://github.com/keenerd/rtl-sdr), as this version’s `rtl_fm` command can create the WAV file header needed to decode the data with `sox`. 165 | 166 | ``` 167 | cd ~ 168 | git clone https://github.com/keenerd/rtl-sdr.git 169 | cd rtl-sdr/ 170 | mkdir build 171 | cd build 172 | cmake ../ -DINSTALL_UDEV_RULES=ON 173 | make 174 | sudo make install 175 | sudo ldconfig 176 | cd ~ 177 | sudo cp ./rtl-sdr/rtl-sdr.rules /etc/udev/rules.d/ 178 | sudo reboot 179 | ``` 180 | 181 | #### Install and Configure wxtoimg 182 | 183 | The program `wxtoimg` is what does the heavy lifting in this project. It decodes the audio files received by the RTL-SDR receiver and converts the data to images. The original author of wxtoimg has abandoned the project, but it is mirrored at [wxtoimgrestored.xyz](https://wxtoimgrestored.xyz/). 184 | 185 | ``` 186 | wget https://wxtoimgrestored.xyz/beta/wxtoimg-armhf-2.11.2-beta.deb 187 | sudo dpkg -i wxtoimg-armhf-2.11.2-beta.deb 188 | ``` 189 | 190 | Now run `wxtoimg` once to accept the license agreement. 191 | 192 | ``` 193 | wxtoimg 194 | ``` 195 | 196 | Create a file `~/.wxtoimgrc` with the location of your base station. As usual, negative latitude is southern hemisphere, and negative longitude is western hemisphere. Here is what it would look like for the White House as an example. 197 | 198 | ``` 199 | Latitude: 38.8977 200 | Longitude: -77.0365 201 | Altitude: 15 202 | ``` 203 | 204 | The program `predict` is used by the automated scripts to predict weather satellite orbits. Run `predict` to bring up the main menu. Select option ‘G’ from the menu to set your ground station location. 205 | 206 | You can enter whatever you want for the callsign such as your amateur radio callsign. **When entering the longitude, note that positive numbers are for the western hemisphere and negative numbers are for the eastern hemisphere.** This is opposite convention, so make sure you get this right or you’ll be listening when there’s no satellite overhead! 207 | 208 | #### Get the Automation Scripts and Configure 209 | 210 | The following scripts will automate the thumbnail images and then upload all images to S3. The git repo can be cloned anywhere on your Raspberry Pi. The `configure.sh` script sets the installation directory in the scripts and schedules a cron job to run the satellite pass scheduler job at midnight every night. The scheduler identifies times when each satellite will pass overhead and create an `at` one time job to start the recording, processing, and upload steps. 211 | 212 | ``` 213 | git clone https://github.com/alonsovargas3/wx-ground-station.git 214 | cd wx-ground-station 215 | sh configure.sh 216 | cd aws-s3 217 | npm install 218 | ``` 219 | 220 | In the file `aws-s3/.env` set REGION, BUCKET, and STATION_LOCATION to the correct values. The Node.js script prepares the images for upload by creating thumbnail images, optionally printing some metadata on the images, and creating a JSON metadata file for each image capture. If you choose to add a watermark the STATION_LOCATION string will be printed on the images that you capture. Here are my values just for reference. 221 | 222 | ``` 223 | #S3 224 | AWS_REGION=us-west-2 225 | AWS_BUCKET=wx.k6kzo.com 226 | 227 | #WATERMARK 228 | WATERMARK=0 229 | STATION_LOCATION="K6KZO Ground Station, Austin, Texas, USA" 230 | 231 | #DIRECTORIES 232 | DISCORD_WEBHOOK = [Enter webook here] 233 | WEBSITE_ADDR = https://wx.k6kzo.com 234 | ``` 235 | 236 | After that, you will need to setup the API that will provide all of the pass data. A serverless project is setup in the `aws-api` folder. You will just need to confirm one piece of information within the `serverless.yml` file. Open the file and specify the region. Make sure it is the same region as the rest of your other services like S3 as some of the functionality will depend on this configuration. I have used us-west-2 throughout. If you use the same region for your setup then you will not need to make any modifications. 237 | 238 | To deploy your API follow these commands: 239 | 240 | ``` 241 | cd aws-api 242 | npm install 243 | sls deploy 244 | ``` 245 | 246 | This will build your database in DynamoDB, create a Lambda service that will read the information from DynamoDB, and create an API Gateway to access the information from Lambda. When the process completes you should see the Service Information. You will need to find the following value. Note that your URL will be different. 247 | 248 | ``` 249 | endpoints: 250 | GET - https://e78uek07l4.execute-api.us-west-2.amazonaws.com/prod/passes 251 | ``` 252 | 253 | Record this value to add to the PASS_URL in the next steps when you update `s3-vars.js`. 254 | 255 | You'll also want to confirm you see the following value right under the endpoint: 256 | 257 | ``` 258 | functions: 259 | getPasses: passes-prod-getPasses 260 | ``` 261 | 262 | **If you run into any CORS issues with the API, you will need to enable CORS for the API endpoint.** CORS has been enabled in the serverless.yml file but sometimes issues can arise that are fixed by enabling CORS. After you enable CORS support on your resource, you must deploy or redeploy the API for the new settings to take effect. To do this, run `sls deploy` again in the `aws-api` directory. 263 | 264 | Next you will need to make some changes to the web content. The web interface uses Mapbox to draw the live maps of the next upcoming satellite pass. You’ll need to create an account at [Mapbox](https://mapbox.com/) to get an access token. Their free tier lets you load 50,000 maps/month, so you are not likely to have any real costs. When logged into Mapbox, get your account token from [https://account.mapbox.com/](https://account.mapbox.com/). 265 | 266 | Next, in the file `website/s3-vars.js`, set your bucket name, AWS region, AWS credentials (the Cognito identity pool info you saved above), Mapbox token, and your ground station info. Some of my values are shown here for reference. 267 | 268 | ``` 269 | var bucketName = 'wx.k6kzo.com'; 270 | AWS.config.region = 'us-west-2'; // Region 271 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 272 | IdentityPoolId: 'us-west-1:1d02ae39-30a6-497e-b066-795f070de089' 273 | }); 274 | 275 | // Create a mapbox.com account and get access token 276 | const MAP_BOX_ACCESS_TOKEN = 'YOUR_MAPBOX_TOKEN'; 277 | const GROUND_STATION_LAT = 38.8977; 278 | const GROUND_STATION_LON = -77.0365; 279 | const GROUND_STATION_NAME = 'K6KZO ground station'; 280 | const MAX_CAPTURES = 10; 281 | const DIR_NAME = "images"; 282 | const PASS_URL = "[Serverless API Endpoint]"; 283 | 284 | ``` 285 | 286 | #### Upload the Web Content to S3 287 | 288 | Upload the the contents of the `website` directory to your S3 bucket using the S3 console. Since you probably edited the files on your Raspberry Pi, you might need to copy them to your computer where you are accessing AWS using a browser. Whatever the case, these files need to be uploaded to the top level of your bucket. IMPORTANT: be sure to grant public access to the files when you upload them! 289 | 290 | ``` 291 | index.html 292 | wx-ground-station.js 293 | s3-vars.js 294 | tle.js 295 | moment.js 296 | logo.png 297 | ``` 298 | 299 | Of course, you can replace `logo.png` with your own, or just remove the `` tag from `index.html`. 300 | 301 | ### Test Everything Out 302 | 303 | Now that everything is configured, let’s run the scheduling script to schedule recording of upcoming satellite passes. This way you can have a look today instead of waiting until they get scheduled at midnight. This step will also upload a JSON file with the upcoming passes info to your website. 304 | 305 | ``` 306 | cd wx-ground-station 307 | ./schedule_all.sh 308 | ``` 309 | 310 | You can now visit your AWS S3 website endpoint at 311 | 312 | ``` 313 | http://BUCKETNAME.s3-website-REGION.amazonaws.com/ 314 | ``` 315 | 316 | Even though you don’t have any images captured, you should be able to see the next upcoming pass. The next thing to do is make sure the scripts work correctly to record the audio file, process it into images, and upload to your bucket. You can watch the logs in the `wx-ground-station/logs` to debug any errors. 317 | 318 | The `wxtoimg` enhancements that are displayed depends on what sensors were active when the images were captured. If sensors 3 and 4 were active (usually at night), then the thermal enhancement will be shown. Otherwise a multispectral analysis enhancement will be shown. 319 | 320 | Not all images you capture will be good; the satellite may be too low or you may not get a good signal. You can clean up bad ones by using the script `aws-s3/remove-wx-images.js` on the Raspberry Pi. Just provide the key to the particular capture as an argument to remove all the images and the metadata from the S3 bucket. 321 | 322 | ``` 323 | node aws-s3/remove-wx-images.js NOAA19-20191108-162650 324 | ``` 325 | 326 | In the next few hours you’ll be able to see some images uploaded, depending on when satellites are scheduled to fly over. You may get up to 12 passes per day, usually 2 for each of the NOAA satellites in the morning, then 2 more for each of them in the evening. 327 | 328 | **Note - If you are upgrading from a previous version follow the steps below** 329 | 330 | You may be upgrading from a previous version and need to upload all of your information. First you will need to replace your installation with the one in this project. After that is complete, manually upload all JSON files into the images folder of your S3 bucket. After that you will be able to use the file `aws-s3/upload-existing.js` on the Raspberry Pi. To do so run the following command: 331 | 332 | ``` 333 | node aws-s3/upload_existing.js 334 | ``` 335 | 336 | After a few moments all of the pass information in each JSON file will be uploaded to DynamoDB and they will be visible on your website. 337 | 338 | #### Optional CloudFront Setup with Custom URL 339 | 340 | As an optional step you can setup a CloudFront distribution to serve your website from a CDN. This also allows you to deploy a custom URL with an SSL certificate. To do so, see this [article](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html) for setting up CloudFront and this [article](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-alternate-domain-names.html) on how to setup a custom URL. 341 | 342 | ### Fine Tuning 343 | 344 | The script `receive_and_process_satellite.sh` uses the `rtl_fm` command to read the signal from the RTL-SDR receiver. The -p argument sets the PPM error correction. Most of the time it is set to 0, but you may want to adjust. See [this](https://davidnelson.me/?p=371) article for details. 345 | 346 | ### Low Noise Amplifier 347 | 348 | You can also install a low noise amplifier (LNA) to improve reception (results are mixed). Some LNAs can be powered with a bias tee circuit and controlled with the `rtl_biast` command. 349 | 350 | In Linux or MacOS download the source from git, compile it the same way you do the regular RTL-SDR drivers, and then run ./rtl_biast -b 1 to turn the bias tee on and ./rtl_biast -b 0 to turn the bias tee off. The procedure is: 351 | 352 | ``` 353 | git clone https://github.com/rtlsdrblog/rtl-sdr-blog 354 | cd rtl-sdr-blog 355 | mkdir build 356 | cd build 357 | cmake .. -DDETACH_KERNEL_DRIVER=ON 358 | make 359 | cd src 360 | ./rtl_biast -b 1 361 | ``` 362 | 363 | If you want to be able to run the bias tee program from anywhere on the command line you can also run "sudo make install". 364 | 365 | If you have trouble running the bias tee check with a multimeter if there is 4.5V at the SMA port. Also check that your powered device is actually capable of receiving power. Remember that not all LNA's can accept bias tee power. 366 | 367 | Once installed, uncomment the `rtl_biast` lines toward the top of `receive_and_process_satellite.sh`. This will turn the LNA on right before starting to record and off after capturing the signal. **Make sure that you also update the path if you have installed `rtl_biast` in a different location** 368 | -------------------------------------------------------------------------------- /website/wx-ground-station.js: -------------------------------------------------------------------------------- 1 | // Load new instance of TLE.js 2 | var tlejs = new TLEJS(); 3 | var lastPositionOfNextPass; 4 | var nextPass = null 5 | 6 | // Get SATCAT number for satellite and 7 | // return s n2yo link for the satellite 8 | function getSatelliteLink(tles) { 9 | var satNum = tlejs.getSatelliteNumber(tles); 10 | return "https://www.n2yo.com/satellite/?s=" + satNum; 11 | } 12 | 13 | // Get URL parameters 14 | // This will be use to get page numbers 15 | function getURLParameter(name, url) { 16 | if (!url) url = window.location.href; 17 | name = name.replace(/[\[\]]/g, '\\$&'); 18 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 19 | results = regex.exec(url); 20 | if (!results) return null; 21 | if (!results[2]) return ''; 22 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 23 | } 24 | 25 | function load() { 26 | $('#location').html(GROUND_STATION_LAT + ', ' + GROUND_STATION_LON); 27 | getUpcomingPassInfo(); 28 | getAllUpcomingPasses(); 29 | getDynamoPasses(function (metadata) { 30 | 31 | $('#messages').html(''); 32 | 33 | // show newest first 34 | var sortedMeta = metadata.sort(function (m1, m2) { 35 | var m1key = m1.passDate + "-" + m1.passTime; 36 | var m2key = m2.passDate + "-" + m2.passTime; 37 | return (m1key > m2key) ? -1 : 1; 38 | }); 39 | 40 | var captureCount = 0; 41 | 42 | // Function to convert time for each pass 43 | function convertToLocal(date,time){ 44 | var combinedDate = new Date(date+" "+time); 45 | var local = moment.utc(combinedDate).local().format('YYYY-MM-DD HH:mm:ss'); 46 | return local; 47 | } 48 | 49 | // Pagination 50 | // Get sorted JSON array 51 | var json = sortedMeta; 52 | // Find the total number of records in the array 53 | var totalJSON = json.length; 54 | // Determine number of pages that will need to be displayed 55 | // Rounding up to ensure the last set of records are visible 56 | var totalPages = Math.ceil(totalJSON/MAX_CAPTURES); 57 | // Current pages 58 | var page = getURLParameter('page'); 59 | if(page==null){page=1;} 60 | // Variable to add active class to list element 61 | var activeClass,disableClass,lastClass; 62 | // Variables for Prev and next 63 | var prevPage = parseInt(page) - 1; 64 | var nextPage = parseInt(page) + 1; 65 | // Create list elements 66 | for (let i=1; i<=totalPages; i++) { 67 | // If on the first page, disable previous 68 | if(page==1){ disableClass='disabled';} else { disableClass='';} 69 | // If on th elast page, disable next 70 | if(page==totalPages){ lastClass='disabled';} else { lastClass='';} 71 | // Display current page number as active 72 | if(page==i){ activeClass = 'active';} else {activeClass='';} 73 | // List elements 74 | if(i==1){$('#pages').append(['
  • Previous
  • '].join(''));} 75 | $('#pages').append(['
  • '+i+'
  • '].join('')); 76 | if(i==totalPages){$('#pages').append(['
  • Next
  • '].join(''));} 77 | } 78 | // Determine how many records to show per page 79 | var recPerPage = MAX_CAPTURES; 80 | // Use Math.max to ensure that we at least start from record 0 81 | var startRec = Math.max(page - 1, 0) * recPerPage; 82 | // Define end of array and stay within bounds of record set 83 | var endRec = Math.min(startRec + recPerPage, totalJSON); 84 | // Create JSON array for current page with appropriate records 85 | var recordsToShow = json.splice(startRec, endRec); 86 | 87 | // Displays each pass 88 | recordsToShow.forEach(function (m) { 89 | if (++captureCount > MAX_CAPTURES) return; 90 | if (m == null) return; 91 | var mapId = m.imageKey + '-gt'; 92 | var satLink = '' + m.satellite + ''; 93 | $('#previous_passes').append([ 94 | //'
    ', 95 | '

    ', convertToLocal(m.passDate,m.passTime), '

    ', 96 | '

    ', m.passDate, ' ', m.passTime, '

    ', 97 | '

    ', 98 | '
    ', 99 | '
    ', 100 | '
    ', 101 | '
    satellite: ', satLink, '
    ', 102 | '
    elevation: ', m.elevation, '°', '
    ', 103 | '
    direction: ', m.direction, '
    ', 104 | '
    downlink freq: ', m.frequency, ' MHz', '
    ', 105 | '
    gain: ', m.gain, '
    ', 106 | '
    channel A: ', m.chan_a, '
    ', 107 | '
    channel B: ', m.chan_b, '
    ', 108 | '
    pristine | map | audio
    ', 109 | '
    ', 110 | '
    '].join('')); 111 | $('#previous_passes').append([ 112 | '
    ', 113 | '
    ', 114 | '
    orbital elements:
    ', 115 | '
    ', m.tle1.replace(/ /g, " "), '
    ', 116 | '
    ', m.tle2.replace(/ /g, " "), '
    ', 117 | '
    ', 118 | '
    '].join('')); 119 | 120 | var mapOptions = { 121 | zoomControl: false, 122 | attributionControl: false, 123 | scrollWheelZoom: false, 124 | touchZoom: false, 125 | doubleClickZoom: false, 126 | dragging: false 127 | }; 128 | var groundTrackMap = L.map(mapId, mapOptions).setView([GROUND_STATION_LAT, GROUND_STATION_LON], 4); 129 | 130 | L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { 131 | attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', 132 | maxZoom: 18, 133 | id: 'mapbox.streets', 134 | accessToken: MAP_BOX_ACCESS_TOKEN 135 | }).addTo(groundTrackMap); 136 | var bounds = groundTrackMap.getBounds(); 137 | var marker = L.marker([GROUND_STATION_LAT, GROUND_STATION_LON], {title: GROUND_STATION_NAME}).addTo(groundTrackMap); 138 | 139 | var t = m.passTime.split(' ').join(''); 140 | var captureTime = new Date(m.passDate + 'T' + t).getTime(); 141 | if (m.passDuration) { 142 | // get the current orbit at the middle of the pass duration so it is the correct orbit for the ground station location. 143 | captureTime += (m.passDuration/2) * 1000; 144 | } 145 | 146 | const orbits = tlejs.getGroundTrackLatLng( 147 | [m.tle1, m.tle2], 148 | 10000, 149 | captureTime 150 | ); 151 | var orbit = orbits[1]; 152 | var polyline = L.polyline(orbit, {color: 'red'}).addTo(groundTrackMap); 153 | const lat = 0; 154 | const lon = 1; 155 | const tickLength = 0.5; 156 | for(var i=0;i bounds.getSouth())) { 159 | // draw two ticks to indicate direction of orbit 160 | /* 161 | 162 | directionAngle: | 163 | +135deg /|\ -135deg 164 | | 165 | */ 166 | var dlon = orbit[i+1][lon] - origin[lon]; 167 | var dlat = orbit[i+1][lat] - origin[lat]; 168 | 169 | // angle from point i and point i+1 170 | var directionAngle = Math.atan2(dlat,dlon); 171 | 172 | var tickAngle = directionAngle - (135 * (Math.PI/180)) 173 | var tick = [tickLength * Math.sin(tickAngle), tickLength * Math.cos(tickAngle)]; 174 | var tickPoints = [ [origin[lat], origin[lon]], [origin[lat]+tick[lat], origin[lon]+tick[lon]] ]; 175 | L.polyline(tickPoints, {color: 'red'}).addTo(groundTrackMap); 176 | 177 | tickAngle = directionAngle + (135 * (Math.PI/180)) 178 | tick = [tickLength * Math.sin(tickAngle), tickLength * Math.cos(tickAngle)]; 179 | tickPoints = [ [origin[lat], origin[lon]], [origin[lat]+tick[lat], origin[lon]+tick[lon]] ]; 180 | L.polyline(tickPoints, {color: 'red'}).addTo(groundTrackMap); 181 | } 182 | } 183 | 184 | m.images.forEach(function (i) { 185 | if (i.filename.endsWith("-ZA.png")) i.order = 1; 186 | if (i.filename.endsWith("-MCIR-precip.png")) i.order = 2; 187 | if (i.filename.endsWith("-MCIR.png")) i.order = 3; 188 | if (i.filename.endsWith("-NO.png")) i.order = 4; 189 | if (i.filename.endsWith("-MSA.png")) i.order = 5; 190 | if (i.filename.endsWith("-MSA-precip.png")) i.order = 6; 191 | if (i.filename.endsWith("-THERM.png")) i.order = 7; 192 | }); 193 | var images = m.images.sort(function (i1, i2) { 194 | return (i1.order < i2.order) ? -1 : 1; 195 | }); 196 | var imageHtml = [ 197 | '
    ' 198 | ]; 199 | images.forEach(function (i) { 200 | if (m.chan_a == '3/3B (mid infrared)') { 201 | // Show MSA image if sensor 3 was used. 202 | if (i.filename.endsWith('-MSA.png')) { 203 | return; 204 | } 205 | if (i.filename.endsWith('-MSA-precip.png')) { 206 | return; 207 | } 208 | } 209 | if (m.chan_a != '3/3B (mid infrared)') { 210 | // If no sensor 3 data, then show the thermal IR image. 211 | if (i.filename.endsWith('-THERM.png')) { 212 | return; 213 | } 214 | } 215 | var url = DIR_NAME + '/' + i.filename; 216 | var thumburl = DIR_NAME + '/' + i.thumbfilename; 217 | if(!i.filename.endsWith("-PRISTINE.png")){ 218 | imageHtml.push([ 219 | '
    ', 220 | '', 221 | '', 222 | '', 223 | '
    ', 224 | i.enhancement, 225 | '
    ', 226 | '
    '].join('')); 227 | } 228 | 229 | }); 230 | imageHtml.push('
    '); 231 | $('#previous_passes').append(imageHtml.join('')); 232 | }); 233 | }); 234 | } 235 | 236 | // Gets JSON file for a pass 237 | function getImageMetadata(DIR_NAME, cb) { 238 | var pattern = new RegExp(".+-[0-9]+[0-9]+\.json$"); 239 | s3.listObjects({Prefix: DIR_NAME}, function(err, data) { 240 | if (err) { 241 | return cb('There was an error viewing the directory: ' + err.message); 242 | } 243 | if (data && data.Contents && (data.Contents.length == 0)) { 244 | return cb('directory not found'); 245 | } 246 | var metadataFiles = data.Contents.filter(function (object) { 247 | return pattern.test(object.Key); 248 | }); 249 | 250 | var promises = metadataFiles.map(function(md) { 251 | var params = { 252 | Bucket: bucketName, 253 | Key: md.Key 254 | }; 255 | return s3.getObject(params).promise().then(function(data) { 256 | var s = JSON.parse(data.Body.toString()); 257 | return s; 258 | }); 259 | }); 260 | 261 | Promise.all(promises).then(function(results) { 262 | //console.log(results); 263 | cb(null, results); 264 | }) 265 | 266 | }); 267 | } 268 | 269 | // Get passes from API 270 | function getDynamoPasses(cb){ 271 | const fetchPromise = fetch(PASS_URL); 272 | fetchPromise.then(response => { 273 | return response.json(); 274 | }).then(passes => { 275 | const passList = passes.passes.map(satPass => satPass.passDate).join("\n"); 276 | cb(passes.passes); 277 | }); 278 | } 279 | 280 | // Get upcoming passes to show all passes 281 | function getAllUpcomingPasses() { 282 | var timeNow = new Date(); 283 | // Load upcoming_passes.json file using a HTTP GET request 284 | $.get(DIR_NAME + "/upcoming_passes.json", function(data) { 285 | // Loop through all upcoming passes to find next pass by looking at the end 286 | // time of each pass and determining if it is later than the current time 287 | // Note - upcoming passes file is in order of time and is loaded the same way 288 | // Note2 - using end time ensures next pass will not show until current is complete 289 | for(var i=0;i' + nextPassTime.satellite + ''; 294 | var curPass; 295 | 296 | if ((!bgSet) && (passEndDate > timeNow)) { 297 | curPass = 'style=\"background-color: #EDEDED;\"'; 298 | var bgSet = 1; 299 | } else { 300 | curPass = ''; 301 | } 302 | 303 | $("#all_passes").append([ 304 | '
    ', 305 | passSatLink, 306 | '
    ', 307 | passStartDate.toDateString(), 308 | ' ', 309 | '
    ', 310 | nextPassTime.direction, 311 | ' at ', 312 | nextPassTime.elevation, 313 | '° elevation', 314 | '
    ', 315 | 'capture begins at: ', 316 | ("0" + passStartDate.getHours()).slice(-2) + ":" + ("0" + passStartDate.getMinutes()).slice(-2), 317 | '
    ', 318 | 'imagery approx: ', 319 | ("0" + passEndDate.getHours()).slice(-2) + ":" + ("0" + passEndDate.getMinutes()).slice(-2), 320 | '

    '].join('') 321 | ); 322 | } 323 | }); 324 | } 325 | 326 | // Gets all upcoming satellite passes for the given LAT / LONG 327 | // This is used to display upcoming pass 328 | function getUpcomingPassInfo() { 329 | 330 | // Load upcoming_passes.json file using a HTTP GET request 331 | $.get(DIR_NAME + "/upcoming_passes.json", function(data) { 332 | var now = new Date(); 333 | var processingTime = 240000; // approx 4 minutes to process and upload images. 334 | // Loop through all upcoming passes to find next pass by looking at the end 335 | // time of each pass and determining if it is later than the current time 336 | // Note - upcoming passes file is in order of time and is loaded the same way 337 | // Note2 - using end time ensures next pass will not show until current is complete 338 | for(var i=0;i now)) { 341 | nextPass = data[i]; 342 | } 343 | } 344 | // Link to satellite for next pass 345 | var satLink = '' + nextPass.satellite + ''; 346 | // Start and end time for next pass 347 | var startDate = new Date(nextPass.start); 348 | var endDate = new Date(nextPass.end + processingTime); 349 | // Populates upcoming_passes
    with next pass information 350 | $("#upcoming_passes").append([ 351 | '
    ', 352 | '
    next image capture: ', 353 | satLink, 354 | ' ', 355 | nextPass.direction, 356 | ' at ', 357 | nextPass.elevation, 358 | '° elevation', 359 | '
    ', 360 | '
    capture begins at: ', 361 | ("0" + startDate.getHours()).slice(-2) + ":" + ("0" + startDate.getMinutes()).slice(-2), 362 | '
    ', 363 | '
    imagery approx: ', 364 | ("0" + endDate.getHours()).slice(-2) + ":" + ("0" + endDate.getMinutes()).slice(-2), 365 | '
    ', 366 | '
    '].join('') 367 | ); 368 | 369 | // Get location of satellite for next pass for the current time 370 | lastPositionOfNextPass = tlejs.getLatLon([nextPass.tle1, nextPass.tle2], new Date().getTime()); 371 | // Set mabox access token for Mapbox GL 372 | mapboxgl.accessToken = MAP_BOX_ACCESS_TOKEN; 373 | // Intializes a new flyover map 374 | var flyoverMap = new mapboxgl.Map({ 375 | container: 'flyover_map', 376 | style: 'mapbox://styles/mapbox/satellite-streets-v10', 377 | center: [lastPositionOfNextPass.lng, lastPositionOfNextPass.lat], 378 | pitch: 60, 379 | bearing: 0, 380 | zoom: 3 381 | }); 382 | // Initializes a new static map 383 | var staticMap = new mapboxgl.Map({ 384 | container: 'static_map', 385 | style: 'mapbox://styles/mapbox/streets-v11', 386 | center: [0, 0], 387 | zoom: 0 388 | }); 389 | // Get satelite location for current time and return 390 | // Mapbox GL object with LAT / LON for static map 391 | function getSatLocation() { 392 | var location = tlejs.getLatLon([nextPass.tle1, nextPass.tle2], new Date().getTime()); 393 | return new mapboxgl.LngLat(location.lng, location.lat); 394 | } 395 | // Get satellite location for current time 396 | // and return flyover map coordinate 397 | function getSatLocationPoint() { 398 | var l = getSatLocation(); 399 | return { 400 | "type": "Point", 401 | "coordinates": [l.lng, l.lat] 402 | }; 403 | } 404 | 405 | function getCurrentOrbit() { 406 | var orbits = tlejs.getGroundTrackLatLng( 407 | [nextPass.tle1, nextPass.tle2], 408 | 10000, 409 | new Date().getTime() 410 | ); 411 | var currentOrbit = orbits[1]; // [lat, lng] ordering 412 | var r = []; 413 | // Convert to [lng, lat] ordering as required by MapBox APIs 414 | for(var i=0;i { 460 | var currentLocation = getSatLocation(); 461 | var bearing = getBearing(currentLocation); 462 | flyoverMap.setCenter([currentLocation.lng, currentLocation.lat]); 463 | flyoverMap.setBearing(bearing); 464 | }, 500); 465 | }); 466 | 467 | 468 | staticMap.on('load', function() { 469 | staticMap.addSource('satellite-location', { 470 | "type": "geojson", 471 | "data": getSatLocationPoint() 472 | }); 473 | 474 | staticMap.addSource('current-orbit', { 475 | "type": "geojson", 476 | "data": getOrbitData() 477 | }); 478 | 479 | 480 | staticMap.addLayer({ 481 | 'id': 'orbit', 482 | 'type': 'line', 483 | 'source': 'current-orbit', 484 | 'layout': { 485 | 'line-cap': 'round', 486 | 'line-join': 'round' 487 | }, 488 | 'paint': { 489 | 'line-color': '#eeee00', 490 | 'line-width': 5, 491 | 'line-opacity': .8 492 | } 493 | }); 494 | 495 | staticMap.addLayer({ 496 | "id": "ground-station", 497 | "type": "circle", 498 | "source": { 499 | "type": "geojson", 500 | "data": { 501 | "type": "Point", 502 | "coordinates": [GROUND_STATION_LON, GROUND_STATION_LAT] 503 | } 504 | }, 505 | "paint": { 506 | "circle-radius": 10, 507 | "circle-color": "#ff0000" 508 | } 509 | }); 510 | 511 | staticMap.addLayer({ 512 | "id": "satellites", 513 | "source": "satellite-location", 514 | "type": "circle", 515 | "paint": { 516 | "circle-radius": 10, 517 | "circle-color": "#007cbf" 518 | } 519 | }); 520 | 521 | // Moves static map to center of satellite when function is called 522 | staticMap.flyTo({ 523 | center: [ 524 | getSatLocationPoint().coordinates[0], 525 | getSatLocationPoint().coordinates[1] 526 | ], 527 | }); 528 | 529 | // Move static map to center of satellite every 25 seconds 530 | setInterval(() => { 531 | staticMap.flyTo({ 532 | center: [ 533 | getSatLocationPoint().coordinates[0], 534 | getSatLocationPoint().coordinates[1] 535 | ], 536 | }); 537 | }, 25000); 538 | 539 | // Finds satellite location for a given time every half second 540 | setInterval(() => { 541 | staticMap.getSource('satellite-location').setData(getSatLocationPoint()); 542 | }, 500); 543 | 544 | // Finds satellite orbit and sets it every minute 545 | setInterval(() => { 546 | staticMap.getSource('current-orbit').setData(getOrbitData()); 547 | }, 60000); 548 | 549 | 550 | }); 551 | 552 | }); 553 | } 554 | -------------------------------------------------------------------------------- /aws-s3/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-s3", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@jimp/bmp": { 8 | "version": "0.8.5", 9 | "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.8.5.tgz", 10 | "integrity": "sha512-o/23j1RODQGGjvb2xg+9ZQCHc9uXa5XIoJuXHN8kh8AJBGD7JZYiHMwNHaxJRJvadimCKUeA5udZUJAoaPwrYg==", 11 | "requires": { 12 | "@jimp/utils": "^0.8.5", 13 | "bmp-js": "^0.1.0", 14 | "core-js": "^2.5.7" 15 | } 16 | }, 17 | "@jimp/core": { 18 | "version": "0.8.5", 19 | "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.8.5.tgz", 20 | "integrity": "sha512-Jto1IdL5HYg7uE15rpQjK6dfZJ6d6gRjUsVCPW50nIfXgWizaTibFEov90W9Bj+irwKrX2ntG3e3pZUyOC0COg==", 21 | "requires": { 22 | "@jimp/utils": "^0.8.5", 23 | "any-base": "^1.1.0", 24 | "buffer": "^5.2.0", 25 | "core-js": "^2.5.7", 26 | "exif-parser": "^0.1.12", 27 | "file-type": "^9.0.0", 28 | "load-bmfont": "^1.3.1", 29 | "mkdirp": "0.5.1", 30 | "phin": "^2.9.1", 31 | "pixelmatch": "^4.0.2", 32 | "tinycolor2": "^1.4.1" 33 | }, 34 | "dependencies": { 35 | "buffer": { 36 | "version": "5.5.0", 37 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", 38 | "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", 39 | "requires": { 40 | "base64-js": "^1.0.2", 41 | "ieee754": "^1.1.4" 42 | } 43 | } 44 | } 45 | }, 46 | "@jimp/custom": { 47 | "version": "0.8.5", 48 | "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.8.5.tgz", 49 | "integrity": "sha512-hS4qHOcOIL+N93IprsIhFgr8F4XnC2oYd+lRaOKEOg3ptS2vQnceSTtcXsC0//mhq8AV6lNjpbfs1iseEZuTqg==", 50 | "requires": { 51 | "@jimp/core": "^0.8.5", 52 | "core-js": "^2.5.7" 53 | } 54 | }, 55 | "@jimp/gif": { 56 | "version": "0.8.5", 57 | "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.8.5.tgz", 58 | "integrity": "sha512-Mj8jmv4AS76OY+Hx/Xoyihj02SUZ2ELk+O5x89pODz1+NeGtSWHHjZjnSam9HYAjycvVI/lGJdk/7w0nWIV/yQ==", 59 | "requires": { 60 | "@jimp/utils": "^0.8.5", 61 | "core-js": "^2.5.7", 62 | "omggif": "^1.0.9" 63 | } 64 | }, 65 | "@jimp/jpeg": { 66 | "version": "0.8.5", 67 | "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.8.5.tgz", 68 | "integrity": "sha512-7kjTY0BiCpwRywk+oPfpLto7cLI+9G0mf4N1bv1Hn+VLQwcXFy2fHyl4qjqLbbY6u4cyZgqN+R8Pg6GRRzv0kw==", 69 | "requires": { 70 | "@jimp/utils": "^0.8.5", 71 | "core-js": "^2.5.7", 72 | "jpeg-js": "^0.3.4" 73 | } 74 | }, 75 | "@jimp/plugin-blit": { 76 | "version": "0.8.5", 77 | "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.8.5.tgz", 78 | "integrity": "sha512-r8Z1CwazaJwZCRbucQgrfprlGyH91tX7GubUsbWr+zy5/dRJAAgaPj/hcoHDwbh3zyiXp5BECKKzKW0x4reL4w==", 79 | "requires": { 80 | "@jimp/utils": "^0.8.5", 81 | "core-js": "^2.5.7" 82 | } 83 | }, 84 | "@jimp/plugin-blur": { 85 | "version": "0.8.5", 86 | "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.8.5.tgz", 87 | "integrity": "sha512-UH5ywpV4YooUh9HXEsrNKDtojLCvIAAV0gywqn8EQeFyzwBJyXAvRNARJp7zr5OPLr9uGXkRLDCO9YyzdlXZng==", 88 | "requires": { 89 | "@jimp/utils": "^0.8.5", 90 | "core-js": "^2.5.7" 91 | } 92 | }, 93 | "@jimp/plugin-color": { 94 | "version": "0.8.5", 95 | "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.8.5.tgz", 96 | "integrity": "sha512-7XHqcTQ8Y1zto1b9P1y8m1dzSjnOpBsD9OZG0beTpeJ5bgPX+hF5ZLmvcM6c5ljkINw5EUF1it07BYbkCxiGQA==", 97 | "requires": { 98 | "@jimp/utils": "^0.8.5", 99 | "core-js": "^2.5.7", 100 | "tinycolor2": "^1.4.1" 101 | } 102 | }, 103 | "@jimp/plugin-contain": { 104 | "version": "0.8.5", 105 | "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.8.5.tgz", 106 | "integrity": "sha512-ZkiPFx9L0yITiKtYTYLWyBsSIdxo/NARhNPRZXyVF9HmTWSLDUw1c2c1uvETKxDZTAVK+souYT14DwFWWdhsYA==", 107 | "requires": { 108 | "@jimp/utils": "^0.8.5", 109 | "core-js": "^2.5.7" 110 | } 111 | }, 112 | "@jimp/plugin-cover": { 113 | "version": "0.8.5", 114 | "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.8.5.tgz", 115 | "integrity": "sha512-OdT4YAopLOhbhTUQV3R1v5ZZqIaUt3n3vJi/OfTbsak1t9UkPBVdmYPyhoont8zJdtdkF5dW16Ro1FTshytcww==", 116 | "requires": { 117 | "@jimp/utils": "^0.8.5", 118 | "core-js": "^2.5.7" 119 | } 120 | }, 121 | "@jimp/plugin-crop": { 122 | "version": "0.8.5", 123 | "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.8.5.tgz", 124 | "integrity": "sha512-E1Hb+gfu2k74Gkqh96apAyVljsP5MjCH4TY6lECAAEcYKGH/XRhz6lY2dSEjCYE7KtiqjTZzWwYkgAvkwojj9Q==", 125 | "requires": { 126 | "@jimp/utils": "^0.8.5", 127 | "core-js": "^2.5.7" 128 | } 129 | }, 130 | "@jimp/plugin-displace": { 131 | "version": "0.8.5", 132 | "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.8.5.tgz", 133 | "integrity": "sha512-fVgVYTS1HZzAXkg8Lg06PuirSUG5oXYaYYGL+3ZU4tmZn1pyZ+mZyfejpwtymETEYZnmymHoCT4xto19E/IRvA==", 134 | "requires": { 135 | "@jimp/utils": "^0.8.5", 136 | "core-js": "^2.5.7" 137 | } 138 | }, 139 | "@jimp/plugin-dither": { 140 | "version": "0.8.5", 141 | "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.8.5.tgz", 142 | "integrity": "sha512-KSj2y8E3yK7tldjT/8ejqAWw5HFBjtWW6QkcxfW7FdV4c/nsXZXDkMbhqMZ7FkDuSYoAPeWUFeddrH4yipC5iA==", 143 | "requires": { 144 | "@jimp/utils": "^0.8.5", 145 | "core-js": "^2.5.7" 146 | } 147 | }, 148 | "@jimp/plugin-flip": { 149 | "version": "0.8.5", 150 | "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.8.5.tgz", 151 | "integrity": "sha512-2QbGDkurPNAXZUeHLo/UA3tjh+AbAXWZKSdtoa1ArlASovRz8rqtA45YIRIkKrMH82TA3PZk8bgP2jaLKLrzww==", 152 | "requires": { 153 | "@jimp/utils": "^0.8.5", 154 | "core-js": "^2.5.7" 155 | } 156 | }, 157 | "@jimp/plugin-gaussian": { 158 | "version": "0.8.5", 159 | "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.8.5.tgz", 160 | "integrity": "sha512-2zReC5GJcVAXtf3UgzFcHSYN277i02K9Yrhc1xJf3mti00s43uD++B5Ho7/mIo+HrntVvWhxqar7PARdq0lVIg==", 161 | "requires": { 162 | "@jimp/utils": "^0.8.5", 163 | "core-js": "^2.5.7" 164 | } 165 | }, 166 | "@jimp/plugin-invert": { 167 | "version": "0.8.5", 168 | "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.8.5.tgz", 169 | "integrity": "sha512-GyMXPGheHdS14xfDceuZ9hrGm6gE9UG3PfTEjQbJmHMWippLC6yf8kombSudJlUf8q72YYSSXsSFKGgkHa67vA==", 170 | "requires": { 171 | "@jimp/utils": "^0.8.5", 172 | "core-js": "^2.5.7" 173 | } 174 | }, 175 | "@jimp/plugin-mask": { 176 | "version": "0.8.5", 177 | "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.8.5.tgz", 178 | "integrity": "sha512-inD/++XO+MkmwXl9JGYQ8X2deyOZuq9i+dmugH/557p16B9Q6tvUQt5X1Yg5w7hhkLZ00BKOAJI9XoyCC1NFvQ==", 179 | "requires": { 180 | "@jimp/utils": "^0.8.5", 181 | "core-js": "^2.5.7" 182 | } 183 | }, 184 | "@jimp/plugin-normalize": { 185 | "version": "0.8.5", 186 | "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.8.5.tgz", 187 | "integrity": "sha512-8YRWJWBT4NoSAbPhnjQJXGeaeWVrJAlGDv39A54oNH8Ry47fHcE0EN6zogQNpBuM34M6hRnZl4rOv1FIisaWdg==", 188 | "requires": { 189 | "@jimp/utils": "^0.8.5", 190 | "core-js": "^2.5.7" 191 | } 192 | }, 193 | "@jimp/plugin-print": { 194 | "version": "0.8.5", 195 | "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.8.5.tgz", 196 | "integrity": "sha512-BviNpCiA/fEieOqsrWr1FkqyFuiG2izdyyg7zUqyeUTHPwqrTLvXO9cfP/ThG4hZpu5wMQ5QClWSqhZu1fAwxA==", 197 | "requires": { 198 | "@jimp/utils": "^0.8.5", 199 | "core-js": "^2.5.7", 200 | "load-bmfont": "^1.4.0" 201 | } 202 | }, 203 | "@jimp/plugin-resize": { 204 | "version": "0.8.5", 205 | "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.8.5.tgz", 206 | "integrity": "sha512-gIdmISuNmZQ1QwprnRC5VXVWQfKIiWineVQGebpMAG/aoFOLDXrVl939Irg7Fb/uOlSFTzpAbt1zpJ8YG/Mi2w==", 207 | "requires": { 208 | "@jimp/utils": "^0.8.5", 209 | "core-js": "^2.5.7" 210 | } 211 | }, 212 | "@jimp/plugin-rotate": { 213 | "version": "0.8.5", 214 | "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.8.5.tgz", 215 | "integrity": "sha512-8T9wnL3gb+Z0ogMZmtyI6h3y7TuqW2a5SpFbzFUVF+lTZoAabXjEfX3CAozizCLaT+Duc5H2FJVemAHiyr+Dbw==", 216 | "requires": { 217 | "@jimp/utils": "^0.8.5", 218 | "core-js": "^2.5.7" 219 | } 220 | }, 221 | "@jimp/plugin-scale": { 222 | "version": "0.8.5", 223 | "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.8.5.tgz", 224 | "integrity": "sha512-G+CDH9s7BsxJ4b+mKZ5SsiXwTAynBJ+7/9SwZFnICZJJvLd79Tws6VPXfSaKJZuWnGIX++L8jTGmFORCfLNkdg==", 225 | "requires": { 226 | "@jimp/utils": "^0.8.5", 227 | "core-js": "^2.5.7" 228 | } 229 | }, 230 | "@jimp/plugins": { 231 | "version": "0.8.5", 232 | "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.8.5.tgz", 233 | "integrity": "sha512-52na0wqfQ3uItIA+C9cJ1EXffhSmABgK7ETClDseUh9oGtynHzxZ97smnFf1ydLjXLrF89Gt+YBxWLyiBGgiZQ==", 234 | "requires": { 235 | "@jimp/plugin-blit": "^0.8.5", 236 | "@jimp/plugin-blur": "^0.8.5", 237 | "@jimp/plugin-color": "^0.8.5", 238 | "@jimp/plugin-contain": "^0.8.5", 239 | "@jimp/plugin-cover": "^0.8.5", 240 | "@jimp/plugin-crop": "^0.8.5", 241 | "@jimp/plugin-displace": "^0.8.5", 242 | "@jimp/plugin-dither": "^0.8.5", 243 | "@jimp/plugin-flip": "^0.8.5", 244 | "@jimp/plugin-gaussian": "^0.8.5", 245 | "@jimp/plugin-invert": "^0.8.5", 246 | "@jimp/plugin-mask": "^0.8.5", 247 | "@jimp/plugin-normalize": "^0.8.5", 248 | "@jimp/plugin-print": "^0.8.5", 249 | "@jimp/plugin-resize": "^0.8.5", 250 | "@jimp/plugin-rotate": "^0.8.5", 251 | "@jimp/plugin-scale": "^0.8.5", 252 | "core-js": "^2.5.7", 253 | "timm": "^1.6.1" 254 | } 255 | }, 256 | "@jimp/png": { 257 | "version": "0.8.5", 258 | "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.8.5.tgz", 259 | "integrity": "sha512-zT89ucu8I2rsD3FIMIPLgr1OyKn4neD+5umwD3MY8AOB8+6tX5bFtnmTm3FzGJaJuibkK0wFl87eiaxnb+Megw==", 260 | "requires": { 261 | "@jimp/utils": "^0.8.5", 262 | "core-js": "^2.5.7", 263 | "pngjs": "^3.3.3" 264 | } 265 | }, 266 | "@jimp/tiff": { 267 | "version": "0.8.5", 268 | "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.8.5.tgz", 269 | "integrity": "sha512-Z7uzDcbHuwDg+hy2+UJQ2s5O6sqYXmv6H1fmSf/2dxBrlGMzl8yTc2/BxLrGREeoidDDMcKmXYGAOp4uCsdJjw==", 270 | "requires": { 271 | "core-js": "^2.5.7", 272 | "utif": "^2.0.1" 273 | } 274 | }, 275 | "@jimp/types": { 276 | "version": "0.8.5", 277 | "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.8.5.tgz", 278 | "integrity": "sha512-XUvpyebZGd1vyFiJyxUT4H9A3mKD7MV2MxjXnay3fNTrcow0UJJspmFw/w+G3TP/1dgrVC4K++gntjR6QWTzvg==", 279 | "requires": { 280 | "@jimp/bmp": "^0.8.5", 281 | "@jimp/gif": "^0.8.5", 282 | "@jimp/jpeg": "^0.8.5", 283 | "@jimp/png": "^0.8.5", 284 | "@jimp/tiff": "^0.8.5", 285 | "core-js": "^2.5.7", 286 | "timm": "^1.6.1" 287 | } 288 | }, 289 | "@jimp/utils": { 290 | "version": "0.8.5", 291 | "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.8.5.tgz", 292 | "integrity": "sha512-D3+H4BiopDkhUKvKkZTPPJ53voqOkfMuk3r7YZNcLtXGLkchjjukC4056lNo7B0DzjBgowTYsQM3JjKnYNIYeg==", 293 | "requires": { 294 | "core-js": "^2.5.7" 295 | } 296 | }, 297 | "ajv": { 298 | "version": "6.12.0", 299 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", 300 | "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", 301 | "requires": { 302 | "fast-deep-equal": "^3.1.1", 303 | "fast-json-stable-stringify": "^2.0.0", 304 | "json-schema-traverse": "^0.4.1", 305 | "uri-js": "^4.2.2" 306 | } 307 | }, 308 | "any-base": { 309 | "version": "1.1.0", 310 | "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", 311 | "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==" 312 | }, 313 | "asn1": { 314 | "version": "0.2.4", 315 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 316 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 317 | "requires": { 318 | "safer-buffer": "~2.1.0" 319 | } 320 | }, 321 | "assert-plus": { 322 | "version": "1.0.0", 323 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 324 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 325 | }, 326 | "asynckit": { 327 | "version": "0.4.0", 328 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 329 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 330 | }, 331 | "aws-sdk": { 332 | "version": "2.635.0", 333 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.635.0.tgz", 334 | "integrity": "sha512-NlKqMB4HqMqSutY6YmPzQVa+mMhqo0655hYYl8G2zkUvrYy+YxDitvwDEUkSsNKVFkEvmHtZggFCgVYIUu/sXg==", 335 | "requires": { 336 | "buffer": "4.9.1", 337 | "events": "1.1.1", 338 | "ieee754": "1.1.13", 339 | "jmespath": "0.15.0", 340 | "querystring": "0.2.0", 341 | "sax": "1.2.1", 342 | "url": "0.10.3", 343 | "uuid": "3.3.2", 344 | "xml2js": "0.4.19" 345 | }, 346 | "dependencies": { 347 | "uuid": { 348 | "version": "3.3.2", 349 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 350 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 351 | } 352 | } 353 | }, 354 | "aws-sign2": { 355 | "version": "0.7.0", 356 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 357 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 358 | }, 359 | "aws4": { 360 | "version": "1.9.1", 361 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 362 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 363 | }, 364 | "balanced-match": { 365 | "version": "1.0.0", 366 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 367 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 368 | }, 369 | "base64-js": { 370 | "version": "1.3.1", 371 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 372 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 373 | }, 374 | "bcrypt-pbkdf": { 375 | "version": "1.0.2", 376 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 377 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 378 | "requires": { 379 | "tweetnacl": "^0.14.3" 380 | } 381 | }, 382 | "bmp-js": { 383 | "version": "0.1.0", 384 | "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", 385 | "integrity": "sha1-4Fpj95amwf8l9Hcex62twUjAcjM=" 386 | }, 387 | "brace-expansion": { 388 | "version": "1.1.11", 389 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 390 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 391 | "requires": { 392 | "balanced-match": "^1.0.0", 393 | "concat-map": "0.0.1" 394 | } 395 | }, 396 | "buffer": { 397 | "version": "4.9.1", 398 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 399 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 400 | "requires": { 401 | "base64-js": "^1.0.2", 402 | "ieee754": "^1.1.4", 403 | "isarray": "^1.0.0" 404 | } 405 | }, 406 | "buffer-equal": { 407 | "version": "0.0.1", 408 | "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", 409 | "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" 410 | }, 411 | "caseless": { 412 | "version": "0.12.0", 413 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 414 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 415 | }, 416 | "combined-stream": { 417 | "version": "1.0.8", 418 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 419 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 420 | "requires": { 421 | "delayed-stream": "~1.0.0" 422 | } 423 | }, 424 | "concat-map": { 425 | "version": "0.0.1", 426 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 427 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 428 | }, 429 | "core-js": { 430 | "version": "2.6.11", 431 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", 432 | "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" 433 | }, 434 | "core-util-is": { 435 | "version": "1.0.2", 436 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 437 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 438 | }, 439 | "dashdash": { 440 | "version": "1.14.1", 441 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 442 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 443 | "requires": { 444 | "assert-plus": "^1.0.0" 445 | } 446 | }, 447 | "dateformat": { 448 | "version": "3.0.3", 449 | "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", 450 | "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" 451 | }, 452 | "delayed-stream": { 453 | "version": "1.0.0", 454 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 455 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 456 | }, 457 | "dom-walk": { 458 | "version": "0.1.1", 459 | "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", 460 | "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" 461 | }, 462 | "dotenv": { 463 | "version": "8.2.0", 464 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 465 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" 466 | }, 467 | "ecc-jsbn": { 468 | "version": "0.1.2", 469 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 470 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 471 | "requires": { 472 | "jsbn": "~0.1.0", 473 | "safer-buffer": "^2.1.0" 474 | } 475 | }, 476 | "events": { 477 | "version": "1.1.1", 478 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 479 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 480 | }, 481 | "exif-parser": { 482 | "version": "0.1.12", 483 | "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", 484 | "integrity": "sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=" 485 | }, 486 | "extend": { 487 | "version": "3.0.2", 488 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 489 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 490 | }, 491 | "extsprintf": { 492 | "version": "1.3.0", 493 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 494 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 495 | }, 496 | "fast-deep-equal": { 497 | "version": "3.1.1", 498 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 499 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 500 | }, 501 | "fast-json-stable-stringify": { 502 | "version": "2.1.0", 503 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 504 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 505 | }, 506 | "file-type": { 507 | "version": "9.0.0", 508 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", 509 | "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==" 510 | }, 511 | "forever-agent": { 512 | "version": "0.6.1", 513 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 514 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 515 | }, 516 | "form-data": { 517 | "version": "2.3.3", 518 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 519 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 520 | "requires": { 521 | "asynckit": "^0.4.0", 522 | "combined-stream": "^1.0.6", 523 | "mime-types": "^2.1.12" 524 | } 525 | }, 526 | "fs.realpath": { 527 | "version": "1.0.0", 528 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 529 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 530 | }, 531 | "getpass": { 532 | "version": "0.1.7", 533 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 534 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 535 | "requires": { 536 | "assert-plus": "^1.0.0" 537 | } 538 | }, 539 | "glob": { 540 | "version": "7.1.6", 541 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 542 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 543 | "requires": { 544 | "fs.realpath": "^1.0.0", 545 | "inflight": "^1.0.4", 546 | "inherits": "2", 547 | "minimatch": "^3.0.4", 548 | "once": "^1.3.0", 549 | "path-is-absolute": "^1.0.0" 550 | } 551 | }, 552 | "global": { 553 | "version": "4.3.2", 554 | "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", 555 | "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", 556 | "requires": { 557 | "min-document": "^2.19.0", 558 | "process": "~0.5.1" 559 | } 560 | }, 561 | "har-schema": { 562 | "version": "2.0.0", 563 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 564 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 565 | }, 566 | "har-validator": { 567 | "version": "5.1.3", 568 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 569 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 570 | "requires": { 571 | "ajv": "^6.5.5", 572 | "har-schema": "^2.0.0" 573 | } 574 | }, 575 | "http-signature": { 576 | "version": "1.2.0", 577 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 578 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 579 | "requires": { 580 | "assert-plus": "^1.0.0", 581 | "jsprim": "^1.2.2", 582 | "sshpk": "^1.7.0" 583 | } 584 | }, 585 | "ieee754": { 586 | "version": "1.1.13", 587 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 588 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 589 | }, 590 | "inflight": { 591 | "version": "1.0.6", 592 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 593 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 594 | "requires": { 595 | "once": "^1.3.0", 596 | "wrappy": "1" 597 | } 598 | }, 599 | "inherits": { 600 | "version": "2.0.4", 601 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 602 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 603 | }, 604 | "is-function": { 605 | "version": "1.0.1", 606 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 607 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=" 608 | }, 609 | "is-typedarray": { 610 | "version": "1.0.0", 611 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 612 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 613 | }, 614 | "isarray": { 615 | "version": "1.0.0", 616 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 617 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 618 | }, 619 | "isstream": { 620 | "version": "0.1.2", 621 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 622 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 623 | }, 624 | "jimp": { 625 | "version": "0.8.5", 626 | "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.8.5.tgz", 627 | "integrity": "sha512-BW7t/+TCgKpqZw/wHFwqF/A/Tyk43RmzRHyMBdqfOepqunUrajt0RTqowdWyFo4CS2FmD8pFiYfefWjpXFWrCA==", 628 | "requires": { 629 | "@jimp/custom": "^0.8.5", 630 | "@jimp/plugins": "^0.8.5", 631 | "@jimp/types": "^0.8.5", 632 | "core-js": "^2.5.7", 633 | "regenerator-runtime": "^0.13.3" 634 | } 635 | }, 636 | "jmespath": { 637 | "version": "0.15.0", 638 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 639 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 640 | }, 641 | "jpeg-js": { 642 | "version": "0.3.7", 643 | "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz", 644 | "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==" 645 | }, 646 | "jsbn": { 647 | "version": "0.1.1", 648 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 649 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 650 | }, 651 | "json-schema": { 652 | "version": "0.2.3", 653 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 654 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 655 | }, 656 | "json-schema-traverse": { 657 | "version": "0.4.1", 658 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 659 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 660 | }, 661 | "json-stringify-safe": { 662 | "version": "5.0.1", 663 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 664 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 665 | }, 666 | "jsprim": { 667 | "version": "1.4.1", 668 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 669 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 670 | "requires": { 671 | "assert-plus": "1.0.0", 672 | "extsprintf": "1.3.0", 673 | "json-schema": "0.2.3", 674 | "verror": "1.10.0" 675 | } 676 | }, 677 | "load-bmfont": { 678 | "version": "1.4.0", 679 | "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.0.tgz", 680 | "integrity": "sha512-kT63aTAlNhZARowaNYcY29Fn/QYkc52M3l6V1ifRcPewg2lvUZDAj7R6dXjOL9D0sict76op3T5+odumDSF81g==", 681 | "requires": { 682 | "buffer-equal": "0.0.1", 683 | "mime": "^1.3.4", 684 | "parse-bmfont-ascii": "^1.0.3", 685 | "parse-bmfont-binary": "^1.0.5", 686 | "parse-bmfont-xml": "^1.1.4", 687 | "phin": "^2.9.1", 688 | "xhr": "^2.0.1", 689 | "xtend": "^4.0.0" 690 | } 691 | }, 692 | "mime": { 693 | "version": "1.6.0", 694 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 695 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 696 | }, 697 | "mime-db": { 698 | "version": "1.43.0", 699 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 700 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" 701 | }, 702 | "mime-types": { 703 | "version": "2.1.26", 704 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 705 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 706 | "requires": { 707 | "mime-db": "1.43.0" 708 | } 709 | }, 710 | "min-document": { 711 | "version": "2.19.0", 712 | "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", 713 | "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", 714 | "requires": { 715 | "dom-walk": "^0.1.0" 716 | } 717 | }, 718 | "minimatch": { 719 | "version": "3.0.4", 720 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 721 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 722 | "requires": { 723 | "brace-expansion": "^1.1.7" 724 | } 725 | }, 726 | "minimist": { 727 | "version": "0.0.8", 728 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 729 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 730 | }, 731 | "mkdirp": { 732 | "version": "0.5.1", 733 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 734 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 735 | "requires": { 736 | "minimist": "0.0.8" 737 | } 738 | }, 739 | "oauth-sign": { 740 | "version": "0.9.0", 741 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 742 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 743 | }, 744 | "omggif": { 745 | "version": "1.0.10", 746 | "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", 747 | "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==" 748 | }, 749 | "once": { 750 | "version": "1.4.0", 751 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 752 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 753 | "requires": { 754 | "wrappy": "1" 755 | } 756 | }, 757 | "pako": { 758 | "version": "1.0.11", 759 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 760 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" 761 | }, 762 | "parse-bmfont-ascii": { 763 | "version": "1.0.6", 764 | "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", 765 | "integrity": "sha1-Eaw8P/WPfCAgqyJ2kHkQjU36AoU=" 766 | }, 767 | "parse-bmfont-binary": { 768 | "version": "1.0.6", 769 | "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", 770 | "integrity": "sha1-0Di0dtPp3Z2x4RoLDlOiJ5K2kAY=" 771 | }, 772 | "parse-bmfont-xml": { 773 | "version": "1.1.4", 774 | "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz", 775 | "integrity": "sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==", 776 | "requires": { 777 | "xml-parse-from-string": "^1.0.0", 778 | "xml2js": "^0.4.5" 779 | } 780 | }, 781 | "parse-headers": { 782 | "version": "2.0.3", 783 | "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", 784 | "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==" 785 | }, 786 | "path-is-absolute": { 787 | "version": "1.0.1", 788 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 789 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 790 | }, 791 | "performance-now": { 792 | "version": "2.1.0", 793 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 794 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 795 | }, 796 | "phin": { 797 | "version": "2.9.3", 798 | "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", 799 | "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==" 800 | }, 801 | "pixelmatch": { 802 | "version": "4.0.2", 803 | "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", 804 | "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", 805 | "requires": { 806 | "pngjs": "^3.0.0" 807 | } 808 | }, 809 | "pngjs": { 810 | "version": "3.4.0", 811 | "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", 812 | "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" 813 | }, 814 | "process": { 815 | "version": "0.5.2", 816 | "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", 817 | "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" 818 | }, 819 | "psl": { 820 | "version": "1.7.0", 821 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", 822 | "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" 823 | }, 824 | "punycode": { 825 | "version": "1.3.2", 826 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 827 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 828 | }, 829 | "qs": { 830 | "version": "6.5.2", 831 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 832 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 833 | }, 834 | "querystring": { 835 | "version": "0.2.0", 836 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 837 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 838 | }, 839 | "regenerator-runtime": { 840 | "version": "0.13.3", 841 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", 842 | "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" 843 | }, 844 | "request": { 845 | "version": "2.88.2", 846 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 847 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 848 | "requires": { 849 | "aws-sign2": "~0.7.0", 850 | "aws4": "^1.8.0", 851 | "caseless": "~0.12.0", 852 | "combined-stream": "~1.0.6", 853 | "extend": "~3.0.2", 854 | "forever-agent": "~0.6.1", 855 | "form-data": "~2.3.2", 856 | "har-validator": "~5.1.3", 857 | "http-signature": "~1.2.0", 858 | "is-typedarray": "~1.0.0", 859 | "isstream": "~0.1.2", 860 | "json-stringify-safe": "~5.0.1", 861 | "mime-types": "~2.1.19", 862 | "oauth-sign": "~0.9.0", 863 | "performance-now": "^2.1.0", 864 | "qs": "~6.5.2", 865 | "safe-buffer": "^5.1.2", 866 | "tough-cookie": "~2.5.0", 867 | "tunnel-agent": "^0.6.0", 868 | "uuid": "^3.3.2" 869 | } 870 | }, 871 | "safe-buffer": { 872 | "version": "5.2.0", 873 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 874 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 875 | }, 876 | "safer-buffer": { 877 | "version": "2.1.2", 878 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 879 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 880 | }, 881 | "sax": { 882 | "version": "1.2.1", 883 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 884 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 885 | }, 886 | "sshpk": { 887 | "version": "1.16.1", 888 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 889 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 890 | "requires": { 891 | "asn1": "~0.2.3", 892 | "assert-plus": "^1.0.0", 893 | "bcrypt-pbkdf": "^1.0.0", 894 | "dashdash": "^1.12.0", 895 | "ecc-jsbn": "~0.1.1", 896 | "getpass": "^0.1.1", 897 | "jsbn": "~0.1.0", 898 | "safer-buffer": "^2.0.2", 899 | "tweetnacl": "~0.14.0" 900 | } 901 | }, 902 | "timm": { 903 | "version": "1.6.2", 904 | "resolved": "https://registry.npmjs.org/timm/-/timm-1.6.2.tgz", 905 | "integrity": "sha512-IH3DYDL1wMUwmIlVmMrmesw5lZD6N+ZOAFWEyLrtpoL9Bcrs9u7M/vyOnHzDD2SMs4irLkVjqxZbHrXStS/Nmw==" 906 | }, 907 | "tinycolor2": { 908 | "version": "1.4.1", 909 | "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", 910 | "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" 911 | }, 912 | "tough-cookie": { 913 | "version": "2.5.0", 914 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 915 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 916 | "requires": { 917 | "psl": "^1.1.28", 918 | "punycode": "^2.1.1" 919 | }, 920 | "dependencies": { 921 | "punycode": { 922 | "version": "2.1.1", 923 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 924 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 925 | } 926 | } 927 | }, 928 | "tunnel-agent": { 929 | "version": "0.6.0", 930 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 931 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 932 | "requires": { 933 | "safe-buffer": "^5.0.1" 934 | } 935 | }, 936 | "tweetnacl": { 937 | "version": "0.14.5", 938 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 939 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 940 | }, 941 | "uri-js": { 942 | "version": "4.2.2", 943 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 944 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 945 | "requires": { 946 | "punycode": "^2.1.0" 947 | }, 948 | "dependencies": { 949 | "punycode": { 950 | "version": "2.1.1", 951 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 952 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 953 | } 954 | } 955 | }, 956 | "url": { 957 | "version": "0.10.3", 958 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 959 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 960 | "requires": { 961 | "punycode": "1.3.2", 962 | "querystring": "0.2.0" 963 | } 964 | }, 965 | "utif": { 966 | "version": "2.0.1", 967 | "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", 968 | "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", 969 | "requires": { 970 | "pako": "^1.0.5" 971 | } 972 | }, 973 | "uuid": { 974 | "version": "3.4.0", 975 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 976 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 977 | }, 978 | "verror": { 979 | "version": "1.10.0", 980 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 981 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 982 | "requires": { 983 | "assert-plus": "^1.0.0", 984 | "core-util-is": "1.0.2", 985 | "extsprintf": "^1.2.0" 986 | } 987 | }, 988 | "wrappy": { 989 | "version": "1.0.2", 990 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 991 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 992 | }, 993 | "xhr": { 994 | "version": "2.5.0", 995 | "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", 996 | "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", 997 | "requires": { 998 | "global": "~4.3.0", 999 | "is-function": "^1.0.1", 1000 | "parse-headers": "^2.0.0", 1001 | "xtend": "^4.0.0" 1002 | } 1003 | }, 1004 | "xml-parse-from-string": { 1005 | "version": "1.0.1", 1006 | "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", 1007 | "integrity": "sha1-qQKekp09vN7RafPG4oI42VpdWig=" 1008 | }, 1009 | "xml2js": { 1010 | "version": "0.4.19", 1011 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 1012 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 1013 | "requires": { 1014 | "sax": ">=0.6.0", 1015 | "xmlbuilder": "~9.0.1" 1016 | } 1017 | }, 1018 | "xmlbuilder": { 1019 | "version": "9.0.7", 1020 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 1021 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 1022 | }, 1023 | "xtend": { 1024 | "version": "4.0.2", 1025 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1026 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1027 | } 1028 | } 1029 | } 1030 | --------------------------------------------------------------------------------