├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitlab-ci.yml ├── .make ├── dummyhal │ └── main.make ├── general.make ├── git.make ├── go │ ├── build.make │ ├── main.make │ └── quality.make ├── halv1 │ ├── build.make │ ├── config.make │ └── main.make └── log.make ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── configure.go ├── root.go ├── start.go └── version.go ├── docs ├── IMPLEMENTATION │ ├── DOWNLINKS.md │ ├── HAL.md │ └── README.md └── INSTALL_INSTRUCTIONS │ ├── FTDI.md │ ├── IMST_RPI.md │ ├── KERLINK.md │ ├── MULTITECH.md │ ├── PARAMETERS.md │ ├── SPI.md │ ├── TOOLCHAINS.md │ └── console.gif ├── main.go ├── pktfwd.gif ├── pktfwd ├── configuration.go ├── downlinks.go ├── gpio.go ├── gps.go ├── manager.go ├── network.go ├── run.go ├── status.go └── uplinks.go ├── scripts ├── kerlink │ ├── build-kerlink.sh │ └── create-package.sh ├── multitech │ ├── create-package.sh │ └── multitech-installer.sh ├── rpi │ └── install-systemd.sh └── toolchains │ ├── Dockerfile.kerlink-iot-station │ └── Dockerfile.multitech ├── util ├── config.go ├── logger.go └── system.go ├── vendor └── vendor.json └── wrapper ├── common.go ├── concentrator_dummy.go ├── concentrator_halV1.go ├── downlinks_dummy.go ├── downlinks_halV1.go ├── gps_HALV1.go ├── gps_dummy.go ├── uplinks_HALV1.go └── uplinks_dummy.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This is a **{bug report/feature request/question/...}** for **{the packet forwarder}**. 2 | 3 | - Explain what you want to do 4 | - Explain either: 5 | * which build you used, or 6 | * if you build the packet forwarder yourself, which steps you took to build it 7 | - Explain what steps you took to run it 8 | - Explain what went wrong or what is missing 9 | 10 | ## Environment 11 | 12 | - ` version` returns: `Commit={...}` 13 | - My gateway: 14 | - Manufacturer: `{...}` 15 | - Type of concentrator: `{...}` 16 | - Type of interface with the concentrator: `{...}` 17 | - Is connected to: `{The TTN community network|My private TTN network}` 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */cover.html 2 | */cover.out 3 | lora_gateway*/ 4 | release/ 5 | vendor/*/ 6 | 7 | # macOS 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - sign 4 | - package 5 | 6 | cache: 7 | key: "$CI_PROJECT_PATH" 8 | paths: 9 | - .govendor 10 | - go 11 | 12 | before_script: 13 | # Creating release path 14 | - mkdir release 15 | # Go build environment variables 16 | - export GOROOT=$PWD/go 17 | - export GOPATH=$PWD/gopath 18 | - export PATH=$PATH:$GOROOT/bin:$GOPATH/bin 19 | # Creating govendor cache folder 20 | - mkdir -p $PWD/.govendor 21 | - rm -rf $GOPATH 22 | - mkdir -p $GOPATH/.cache && ln -s $PWD/.govendor $GOPATH/.cache/govendor 23 | # Downloading go if not installed yet 24 | - apt-get update -y && apt-get install make git tar -y 25 | - "([[ ! $(go version) =~ \"version\" ]] && apt-get install wget -y && wget https://storage.googleapis.com/golang/go1.8.1.linux-amd64.tar.gz && tar -C $PWD -xvzf go1.8.1.linux-amd64.tar.gz) || echo \"Expected Go toolset available in cache\"" 26 | # Copying the packet-forwarder in the gopath 27 | - mkdir -p $GOPATH/src/github.com/TheThingsNetwork 28 | - ln -s $PWD $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 29 | # Build environment variables 30 | - export CI_BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) 31 | - echo "date $CI_BUILD_DATE" >> release/info 32 | - echo "commit $CI_BUILD_DATE" >> release/info 33 | # Downloading dependencies 34 | - pushd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 35 | - make dev-deps 36 | - make deps 37 | - popd 38 | 39 | after_script: 40 | - cp -r release release_files 41 | - pushd release_files 42 | # Change name of the binary 43 | - mv packet-forwarder* packet-forwarder 44 | # Create archive 45 | - tar -cvzf $CI_JOB_NAME.tar.gz * 46 | - popd 47 | - rm -rf release/* 48 | - cp release_files/$CI_JOB_NAME.tar.gz release 49 | 50 | multitech-conduit-pktfwd: 51 | stage: build 52 | image: registry.gitlab.com/thethingsnetwork/packet_forwarder/multitech-toolchain 53 | script: 54 | # Remove the toolchain's CFLAGS 55 | - "sed 's/.*CFLAGS.*//g' /opt/mlinux/3.2.0/environment-setup-arm926ejste-mlinux-linux-gnueabi -i.bak" 56 | # Enable mLinux toolchain 57 | - sdk_enable_file=$(ls /opt/mlinux/*/*setup*) 58 | - source $sdk_enable_file 59 | # Go to packet forwarder file 60 | - pushd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 61 | - GOOS=linux GOARM=5 GOARCH=arm make build 62 | - cp scripts/multitech/* release 63 | - popd 64 | artifacts: 65 | paths: 66 | - release/ 67 | 68 | kerlink-iot-station-pktfwd: 69 | stage: build 70 | image: registry.gitlab.com/thethingsnetwork/packet_forwarder/klk-toolchain 71 | script: 72 | - pushd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 73 | - ./scripts/kerlink/build-kerlink.sh /opt 74 | - cp scripts/kerlink/create-package.sh release 75 | - popd 76 | artifacts: 77 | paths: 78 | - release/ 79 | 80 | imst-rpi-pktfwd: 81 | stage: build 82 | image: "ubuntu:xenial" 83 | script: 84 | - pushd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 85 | - apt install -y gcc-arm-linux-gnueabi # Installing cross-compiler 86 | - GOOS=linux GOARM=7 GOARCH=arm PLATFORM=imst_rpi CFG_SPI=native CC=arm-linux-gnueabi-gcc make build 87 | - cp scripts/rpi/install-systemd.sh release 88 | - popd 89 | artifacts: 90 | paths: 91 | - release/ 92 | 93 | sign: 94 | before_script: [] 95 | after_script: [] 96 | only: 97 | - develop@thethingsnetwork/packet_forwarder 98 | - master@thethingsnetwork/packet_forwarder 99 | stage: sign 100 | image: golang:latest 101 | script: 102 | - pushd release 103 | - shasum -a 256 $(ls) > checksums 104 | - gpg --no-tty --batch --import /gpg/signing.ci.gpg-key 105 | - gpg --no-tty --batch --no-use-agent --passphrase $GPG_PASSPHRASE --detach-sign checksums 106 | - popd 107 | artifacts: 108 | paths: 109 | - release/checksums 110 | - release/checksums.sig 111 | 112 | azure-binaries: 113 | before_script: [] 114 | after_script: [] 115 | only: 116 | - develop@thethingsnetwork/packet_forwarder 117 | - master@thethingsnetwork/packet_forwarder 118 | stage: package 119 | image: registry.gitlab.com/thethingsindustries/upload 120 | script: 121 | - cd release 122 | - export STORAGE_CONTAINER=packet-forwarder STORAGE_KEY=$AZURE_STORAGE_KEY ZIP=false TGZ=false PREFIX=$CI_BUILD_REF_NAME/ 123 | - upload * 124 | -------------------------------------------------------------------------------- /.make/dummyhal/main.make: -------------------------------------------------------------------------------- 1 | hal.build: 2 | 3 | hal.deps: 4 | 5 | hal.clean: 6 | 7 | hal.clean-deps: 8 | -------------------------------------------------------------------------------- /.make/general.make: -------------------------------------------------------------------------------- 1 | # Set shell 2 | SHELL = bash 3 | 4 | ## count the input 5 | count = wc -w 6 | -------------------------------------------------------------------------------- /.make/git.make: -------------------------------------------------------------------------------- 1 | .PHONY: hooks 2 | 3 | GIT_COMMIT = `git rev-parse HEAD 2>/dev/null` 4 | GIT_BRANCH = `git rev-parse --abbrev-ref HEAD 2>/dev/null` 5 | GIT_TAG = $(shell git describe --abbrev=0 --tags) 6 | BUILD_DATE = `date -u +%Y-%m-%dT%H:%M%SZ` 7 | 8 | # Get all files that are currently staged, except for deleted files 9 | STAGED_FILES = git diff --staged --name-only --diff-filter=d 10 | 11 | 12 | # Install git hooks 13 | hooks: 14 | @$(log) installing hooks 15 | @touch .git/hooks/pre-commit 16 | @chmod u+x .git/hooks/pre-commit 17 | @echo "make quality-staged" >> .git/hooks/pre-commit 18 | -------------------------------------------------------------------------------- /.make/go/build.make: -------------------------------------------------------------------------------- 1 | # Infer GOOS and GOARCH 2 | GOOS ?= $(or $(word 1,$(subst -, ,${TARGET_PLATFORM})), $(shell echo "`go env GOOS`")) 3 | GOARCH ?= $(or $(word 2,$(subst -, ,${TARGET_PLATFORM})), $(shell echo "`go env GOARCH`")) 4 | 5 | ifeq ($(GOOS),darwin) 6 | CGO_LDFLAGS := -lmpsse 7 | else 8 | ifeq ($(CFG_SPI),ftdi) 9 | CGO_LDFLAGS := -lrt -lmpsse 10 | else 11 | CGO_LDFLAGS := -lrt 12 | endif 13 | ifneq ($(SDKTARGETSYSROOT),) 14 | CGO_CFLAGS := -I$(SDKTARGETSYSROOT)/usr/include/libftdi1 -I$(SDKTARGETSYSROOT)/usr/include 15 | endif 16 | endif 17 | 18 | 19 | # build 20 | go.build: $(RELEASE_DIR)/$(NAME)-$(GOOS)-$(GOARCH)-$(PLATFORM) 21 | 22 | # default main file 23 | MAIN ?= ./main.go 24 | 25 | # Time margin in milliseconds 26 | ifeq ($(PLATFORM),multitech) 27 | SENDING_TIME_MARGIN = 100 28 | else ifeq ($(PLATFORM),kerlink) 29 | SENDING_TIME_MARGIN = 60 30 | endif 31 | 32 | LD_FLAGS = -ldflags "-w -X main.version=${PKTFWD_VERSION} -X main.gitCommit=${GIT_COMMIT} -X main.buildDate=${BUILD_DATE} -X github.com/TheThingsNetwork/packet_forwarder/pktfwd.platform=${PLATFORM} -X github.com/TheThingsNetwork/packet_forwarder/cmd.downlinksMargin=${SENDING_TIME_MARGIN}" 33 | 34 | # Build the executable 35 | $(RELEASE_DIR)/$(NAME)-%: $(shell $(GO_FILES)) vendor/vendor.json 36 | @$(log) "building" [$(GO_ENV) CC="$(CC)" GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) CGO_CFLAGS=$(CGO_CFLAGS) CGO_LDFLAGS=$(CGO_LDFLAGS) $(GO_ENV) $(GO) build -tags '$(HAL_CHOICE)' $(GO_FLAGS) $(LD_FLAGS) $(MAIN) ...] 37 | @$(GO_ENV) CC="$(CC)" GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" $(GO) build -tags '$(HAL_CHOICE)' -o "$(RELEASE_DIR)/$(NAME)-$(GOOS)-$(GOARCH)-$(PLATFORM)-$(CFG_SPI)" -v $(GO_FLAGS) $(LD_FLAGS) $(MAIN) 38 | 39 | # Build the executable in dev mode (much faster) 40 | go.dev: GO_FLAGS = 41 | go.dev: GO_ENV = 42 | go.dev: BUILD_TYPE = dev 43 | go.dev: $(RELEASE_DIR)/$(NAME)-$(GOOS)-$(GOARCH)-$(PLATFORM) 44 | 45 | ## link the executable to a simple name 46 | $(RELEASE_DIR)/$(NAME): $(RELEASE_DIR)/$(NAME)-$(GOOS)-$(GOARCH)-$(PLATFORM) 47 | @$(log) "linking binary" [ln -sfr $(RELEASE_DIR)/$(NAME)-$(GOOS)-$(GOARCH)-$(PLATFORM) $(RELEASE_DIR)/$(NAME)] 48 | @ln -sfr $(RELEASE_DIR)/$(NAME)-$(GOOS)-$(GOARCH)-$(PLATFORM) $(RELEASE_DIR)/$(NAME) 49 | 50 | go.link: $(RELEASE_DIR)/$(NAME) 51 | 52 | go.link-dev: GO_FLAGS = 53 | go.link-dev: GO_ENV = 54 | go.link-dev: BUILD_TYPE = dev 55 | go.link-dev: go.link 56 | 57 | ## initialize govendor 58 | vendor/vendor.json: 59 | @$(log) initializing govendor 60 | @govendor init 61 | 62 | # vim: ft=make 63 | -------------------------------------------------------------------------------- /.make/go/main.make: -------------------------------------------------------------------------------- 1 | # Programs 2 | GO = go 3 | GOLINT = golint 4 | 5 | # License keys 6 | # To add a license key to the binary, specify the variable name using 7 | # LICENSE_KEY_VAR, and add a LICENSE_KEY_FILE variable to the build. 8 | LICENSE_KEY_VAR ?= "main.licenseKey" 9 | LICENSE_KEY_STR ?= $(shell cat "$(LICENSE_KEY_FILE)" 2>/dev/null | head -n -1 | tail -n +2 | tr -d '\n') 10 | 11 | # Flags 12 | ## go 13 | GO_FLAGS = -a 14 | ifeq ($(HAL_CHOICE),dummy) 15 | GO_ENV = CGO_ENABLED=0 16 | else 17 | GO_ENV = CGO_ENABLED=1 18 | endif 19 | 20 | ## golint 21 | GOLINT_FLAGS = -set_exit_status 22 | 23 | ## test 24 | GO_TEST_FLAGS = -cover 25 | 26 | ## coverage 27 | GO_COVER_FILE = coverage.out 28 | GO_COVER_DIR = .coverage 29 | 30 | # Filters 31 | 32 | ## select only go files 33 | only_go = grep '.go$$' 34 | 35 | ## select/remove vendored files 36 | no_vendor = grep -v 'vendor' 37 | only_vendor = grep 'vendor' 38 | 39 | ## select/remove mock files 40 | no_mock = grep -v '_mock.go' 41 | only_mock = grep '_mock.go' 42 | 43 | ## select/remove protobuf generated files 44 | no_pb = grep -Ev '.pb.go$$|.pb.gw.go$$' 45 | only_pb = grep -E '.pb.go$$|.pb.gw.go$$' 46 | 47 | ## select/remove test files 48 | no_test = grep -v '_test.go$$' 49 | only_test = grep '_test.go$$' 50 | 51 | ## filter files to packages 52 | to_packages = sed 's:/[^/]*$$::' | sort | uniq 53 | 54 | ## make packages local (prefix with ./) 55 | to_local = sed 's:^:\./:' 56 | 57 | 58 | # Selectors 59 | 60 | ## find all go files 61 | GO_FILES = find . -name '*.go' | grep -v '.git' 62 | 63 | ## local go packages 64 | GO_PACKAGES = $(GO_FILES) | $(no_vendor) | $(to_packages) 65 | 66 | ## external go packages (in vendor) 67 | EXTERNAL_PACKAGES = $(GO_FILES) | $(only_vendor) | $(to_packages) 68 | 69 | ## staged local packages 70 | STAGED_PACKAGES = $(STAGED_FILES) | $(only_go) | $(no_vendor) | $(to_packages) | $(to_local) 71 | 72 | ## packages for testing 73 | TEST_PACKAGES = $(GO_FILES) | $(no_vendor) | $(only_test) | $(to_packages) 74 | 75 | # Rules 76 | 77 | ## get tools required for development 78 | go.dev-deps: 79 | @$(log) "fetching go tools" 80 | @command -v govendor > /dev/null || ($(log) Installing govendor && $(GO) get -v -u github.com/kardianos/govendor) 81 | @command -v golint > /dev/null || ($(log) Installing golint && $(GO) get -v -u github.com/golang/lint/golint) 82 | 83 | ## install dependencies 84 | go.deps: 85 | @$(log) "fetching go dependencies" 86 | @govendor sync -v 87 | 88 | ## install packages for faster rebuilds 89 | go.install: 90 | @$(log) "installing go packages" 91 | @$(EXTERNAL_PACKAGES) | xargs $(GO) install -v 92 | 93 | ## clean build files 94 | go.clean: 95 | @$(log) "cleaning release dir" [rm -rf $(RELEASE_DIR)] 96 | @rm -rf $(RELEASE_DIR) 97 | 98 | ## clean dependencies 99 | go.clean-deps: 100 | @$(log) "cleaning go dependencies" [rm -rf vendor/*/] 101 | @rm -rf vendor/*/ 102 | 103 | ## run tests 104 | go.test: 105 | @$(log) testing `$(TEST_PACKAGES) | $(count)` go packages 106 | @$(GO) test $(GO_TEST_FLAGS) `$(TEST_PACKAGES)` 107 | 108 | ## clean cover files 109 | go.cover.clean: 110 | rm -rf $(GO_COVER_DIR) $(GO_COVER_FILE) 111 | 112 | ## package coverage 113 | $(GO_COVER_DIR)/%.out: GO_TEST_FLAGS=-cover -coverprofile="$(GO_COVER_FILE)" 114 | $(GO_COVER_DIR)/%.out: % 115 | @$(log) testing "$<" 116 | @mkdir -p `dirname "$(GO_COVER_DIR)/$<"` 117 | @$(GO) test -cover -coverprofile="$@" "./$<" 118 | 119 | ## project coverage 120 | $(GO_COVER_FILE): go.cover.clean $(patsubst ./%,./$(GO_COVER_DIR)/%.out,$(shell $(TEST_PACKAGES))) 121 | @echo "mode: set" > $(GO_COVER_FILE) 122 | @cat $(patsubst ./%,./$(GO_COVER_DIR)/%.out,$(shell $(TEST_PACKAGES))) | grep -vE "mode: set" | sort >> $(GO_COVER_FILE) 123 | 124 | # vim: ft=make 125 | -------------------------------------------------------------------------------- /.make/go/quality.make: -------------------------------------------------------------------------------- 1 | .PHONY: go.fmt go.fmt-staged go.vet go.vet-staged go.lint go.lint-staged go.quality go.quality-staged 2 | 3 | # Fmt 4 | 5 | ## fmt all packages 6 | go.fmt: 7 | @$(log) "formatting `$(GO_PACKAGES) | $(count)` go packages" 8 | @[[ -z "`$(GO_PACKAGES) | xargs go fmt | tee -a /dev/stderr`" ]] 9 | 10 | ## fmt stages packages 11 | go.fmt-staged: GO_PACKAGES = $(STAGED_PACKAGES) 12 | go.fmt-staged: go.fmt 13 | 14 | # Vet 15 | 16 | ## vet all packages 17 | go.vet: 18 | @$(log) "vetting `$(GO_PACKAGES) | $(count)` go packages" 19 | @$(GO_PACKAGES) | xargs $(GO) vet 20 | 21 | ## vet staged packages 22 | go.vet-staged: GO_PACKAGES = $(STAGED_PACKAGES) 23 | go.vet-staged: go.vet 24 | 25 | 26 | # Linting 27 | 28 | ## lint all packages, exiting when errors occur 29 | GO_LINT_FILES = $(GO_FILES) | $(no_vendor) | $(no_mock) | $(no_pb) 30 | go.lint: 31 | @$(log) "linting `$(GO_LINT_FILES) | $(count)` go files" 32 | @(for pkg in `$(LINT_FILES)`; do $(GOLINT) $(GOLINT_FLAGS) $$pkg || exit 1; done) 33 | 34 | ## lint all packages, ignoring errors 35 | go.lint-all: GOLINT_FLAGS = 36 | go.lint-all: go.lint 37 | 38 | # lint staged files 39 | go.lint-staged: GO_LINT_FILES = $(STAGED_FILES) | $(only_go) | $(no_vendor) | $(no_mock) | $(no_pb) 40 | go.lint-staged: go.lint 41 | 42 | # Coveralls 43 | 44 | go.cover.dev-deps: 45 | @command -v goveralls > /dev/null || go get github.com/mattn/goveralls 46 | 47 | coveralls: go.cover.dev-deps $(GO_COVER_FILE) 48 | goveralls -coverprofile=$(GO_COVER_FILE) -service=travis-ci -repotoken $$COVERALLS_TOKEN 49 | 50 | # Quality 51 | 52 | ## run all quality on all files 53 | go.quality: go.fmt go.vet go.lint 54 | 55 | ## run all quality on staged files 56 | go.quality-staged: go.fmt-staged go.vet-staged go.lint-staged 57 | 58 | # vim: ft=make 59 | -------------------------------------------------------------------------------- /.make/halv1/build.make: -------------------------------------------------------------------------------- 1 | ## HAL depending on the platform 2 | ifeq ($(GOOS),darwin) 3 | # MAC 4 | CFG_SPI := mac 5 | PLATFORM := imst_rpi 6 | else 7 | CFG_SPI ?= native 8 | PLATFORM ?= default 9 | endif 10 | 11 | # Build the HAL 12 | hal.build: lora_gateway/libloragw/inc/$(PLATFORM).h lora_gateway/libloragw/libloragw.a 13 | 14 | ### library.cfg configuration file processing 15 | 16 | ifeq ($(CFG_SPI),native) 17 | CFG_SPI_MSG := Linux native SPI driver 18 | CFG_SPI_OPT := CFG_SPI_NATIVE 19 | else ifeq ($(CFG_SPI),ftdi) 20 | CFG_SPI_MSG := FTDI SPI-over-USB bridge using libmpsse/libftdi/libusb 21 | CFG_SPI_OPT := CFG_SPI_FTDI 22 | else ifeq ($(CFG_SPI),mac) 23 | CFG_SPI_MSG := FTDI SPI-over-USB bridge on the MAC using libmpsse/libftdi/libusb 24 | CFG_SPI_OPT := CFG_SPI_FTDI 25 | else 26 | $(error No SPI physical layer selected, check lora_gateway/target.cfg file) 27 | endif 28 | 29 | lora_gateway/libloragw/libloragw.a: 30 | CFG_SPI=$(CFG_SPI) PLATFORM=$(PLATFORM) $(MAKE) all -e -C lora_gateway/libloragw 31 | 32 | # Clean the HAL 33 | hal.clean: 34 | $(MAKE) clean -e -C lora_gateway/libloragw 35 | @rm -f lora_gateway/libloragw/inc/config.h 36 | @rm -f lora_gateway/libloragw/inc/default.h 37 | -------------------------------------------------------------------------------- /.make/halv1/config.make: -------------------------------------------------------------------------------- 1 | PLATFORM ?= default 2 | 3 | SPI_SPEED ?= 8000000 4 | SPIDEV ?= $(firstword $(wildcard /dev/spidev*)) 5 | SPI_CS_CHANGE ?= 0 6 | 7 | VID ?= 0x0403 8 | PID ?= 0x6014 9 | 10 | DEBUG_AUX ?= 0 11 | DEBUG_HAL ?= 0 12 | DEBUG_SPI ?= 0 13 | DEBUG_REG ?= 0 14 | DEBUG_GPS ?= 0 15 | DEBUG_GPIO ?= 0 16 | DEBUG_LBT ?= 0 17 | 18 | lora_gateway/libloragw/inc/default.h: 19 | @echo "Generating default spi header file" 20 | @echo "#ifndef _DEFAULT__H_" >> $@ 21 | @echo "#define _DEFAULT__H_" >> $@ 22 | @echo "#define DISPLAY_PLATFORM \"Auto-generated default SPI config\"" >> $@ 23 | @echo "#define SPI_SPEED $(SPI_SPEED)" >> $@ 24 | @echo "#define SPI_DEV_PATH \"$(SPIDEV)\"" >> $@ 25 | @echo "#define SPI_CS_CHANGE $(SPI_CS_CHANGE)" >> $@ 26 | @echo "#define VID $(VID)" >> $@ 27 | @echo "#define PID $(PID)" >> $@ 28 | @echo "#endif" >> $@ 29 | @echo "Generated default spi header file" 30 | 31 | ### transpose library.cfg into a C header file : config.h 32 | 33 | LIBLORAGW_VERSION := `cat lora_gateway/VERSION` 34 | 35 | lora_gateway/libloragw/inc/config.h: lora_gateway/VERSION lora_gateway/libloragw/library.cfg lora_gateway/libloragw/inc/$(PLATFORM).h 36 | @echo "*** Checking libloragw library configuration ***" 37 | @rm -f $@ 38 | #File initialization 39 | @echo "#ifndef _LORAGW_CONFIGURATION_H" >> $@ 40 | @echo "#define _LORAGW_CONFIGURATION_H" >> $@ 41 | # Release version 42 | @echo "Release version : $(LIBLORAGW_VERSION)" 43 | @echo " #define LIBLORAGW_VERSION "\"$(LIBLORAGW_VERSION)\""" >> $@ 44 | # SPI interface 45 | @echo "SPI interface : $(CFG_SPI_MSG)" 46 | @echo " #define $(CFG_SPI_OPT) 1" >> $@ 47 | # Debug options 48 | @echo " #define DEBUG_AUX $(DEBUG_AUX)" >> $@ 49 | @echo " #define DEBUG_SPI $(DEBUG_SPI)" >> $@ 50 | @echo " #define DEBUG_REG $(DEBUG_REG)" >> $@ 51 | @echo " #define DEBUG_HAL $(DEBUG_HAL)" >> $@ 52 | @echo " #define DEBUG_GPS $(DEBUG_GPS)" >> $@ 53 | @echo " #define DEBUG_GPIO $(DEBUG_GPIO)" >> $@ 54 | @echo " #define DEBUG_LBT $(DEBUG_LBT)" >> $@ 55 | # Platform selection 56 | @echo " #include \"$(PLATFORM).h\"" >> $@ 57 | # end of file 58 | @echo "#endif" >> $@ 59 | @echo "*** Configuration seems ok ***" 60 | -------------------------------------------------------------------------------- /.make/halv1/main.make: -------------------------------------------------------------------------------- 1 | ## HAL depending on the platform 2 | ifeq ($(GOOS),darwin) 3 | # MAC 4 | CFG_SPI := mac 5 | PLATFORM := imst_rpi 6 | endif 7 | 8 | HAL_REPO := https://github.com/TheThingsNetwork/lora_gateway.git 9 | 10 | ## install dependencies 11 | hal.deps: 12 | @$(log) "fetching HAL and dependencies" 13 | git clone -b master $(HAL_REPO) ./lora_gateway 14 | 15 | ## clean dependencies 16 | hal.clean-deps: 17 | @$(log) "cleaning HAL" [rm -rf lora_gateway/] 18 | @rm -rf lora_gateway/ 19 | -------------------------------------------------------------------------------- /.make/log.make: -------------------------------------------------------------------------------- 1 | # This makefile has tools for logging useful information 2 | # about build steps 3 | 4 | LOG_NAME = $(shell basename $$(pwd)) 5 | 6 | log = echo -e "\033[1;34m`basename $(LOG_NAME)` \033[0m" 7 | 8 | # vim: ft=make 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of The Things Network Packet Forwarder authors for copyright purposes. 2 | # 3 | # The copyright owners listed in this document agree to release their work under 4 | # the MIT license that can be found in the LICENSE file. 5 | # 6 | # Names should be added to this file as 7 | # Firstname Lastname 8 | # 9 | # Please keep the list sorted. 10 | 11 | Eric Gourlaouen 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | There are many ways you can contribute to The Things Network. 4 | 5 | - Share your work on the [Labs section of our website](https://www.thethingsnetwork.org/labs/) 6 | - Participate in [topics on our Forum](https://thethingsnetwork.org/forum/) 7 | - Talk with others on our [Slack](https://thethingsnetwork.slack.com/) ([_request invite_](https://account.thethingsnetwork.org)) 8 | - Contribute to our [open source projects on github](https://github.com/TheThingsNetwork) 9 | 10 | ## Submitting issues 11 | 12 | If something is wrong or missing, we want to know about it. Please submit an issue on Github explaining what exactly is the problem. **Give as many details as possible**. If you can (and want to) fix the issue, please tell us in the issue. 13 | 14 | ## Contributing pull requests 15 | 16 | We warmly welcome your pull requests. Be sure to follow some simple guidelines so that we can quickly accept your contributions. 17 | 18 | - Write tests 19 | - Write [good commit messages](https://chris.beams.io/posts/git-commit/) 20 | - Sign our [CLA](https://cla-assistant.io/TheThingsNetwork/packet_forwarder) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 The Things Network 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # name of the executable 2 | NAME = packet-forwarder 3 | 4 | # location of executable 5 | RELEASE_DIR = release 6 | 7 | # Version information 8 | GIT_COMMIT = $(or $(CI_BUILD_REF), `git rev-parse HEAD 2>/dev/null`) 9 | GIT_TAG = $(shell git describe --abbrev=0 --tags 2>/dev/null) 10 | 11 | ifeq ($(GIT_BRANCH), $(GIT_TAG)) 12 | PKTFWD_VERSION = $(GIT_TAG) 13 | else 14 | PKTFWD_VERSION = $(GIT_TAG)-dev 15 | endif 16 | 17 | # HAL choice 18 | HAL_CHOICE ?= halv1 19 | 20 | .PHONY: dev test quality quality-staged 21 | 22 | build: hal.build go.build 23 | 24 | dev: go.dev 25 | 26 | deps: go.deps hal.deps 27 | 28 | dev-deps: go.dev-deps 29 | 30 | test: go.test 31 | 32 | quality: go.quality 33 | 34 | quality-staged: go.quality-staged 35 | 36 | clean: go.clean hal.clean 37 | 38 | clean-deps: go.clean-deps hal.clean-deps 39 | 40 | install: go.install 41 | 42 | include ./.make/*.make 43 | include ./.make/go/*.make 44 | ifeq ($(HAL_CHOICE),halv1) 45 | include ./.make/halv1/*.make 46 | else ifeq ($(HAL_CHOICE),dummy) 47 | include ./.make/dummyhal/*.make 48 | endif 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Packet Forwarder 2 | 3 | **⚠️ The development of this packet forwarder has been put on hold. We're currently working on new tools to make gateways easier to manage, that we will make public when ready. In the meanwhile, we recommend to use other packet forwarders ([Semtech packet forwarder](https://github.com/Lora-net/packet_forwarder), [MP packet forwarder](https://github.com/kersing/packet_forwarder)). To learn more, join the discussion on [the forum](https://www.thethingsnetwork.org/forum/t/new-ttn-packet-forwarder-available/7644/46).** 4 | 5 | Packet forwarder to make the link between a LoRa concentrator and [The Things Network](https://www.thethingsnetwork.org)'s backend. 6 | 7 | This packet forwarder **isn't destined** to [The Things Gateway](https://www.thethingsnetwork.org/docs/gateways/gateway/)'s users, but for users who **already have a gateway** from another vendor, and that want to connect it to The Things Network. 8 | 9 | ![Demo GIF](https://github.com/TheThingsNetwork/packet_forwarder/raw/master/pktfwd.gif) 10 | 11 | * [Install](#install) 12 | * [Build](#build) 13 | * [Run](#run) 14 | + [Contributing](#contribute) 15 | + [License](#license) 16 | 17 | ## Install 18 | 19 | Installation manuals are available for main available gateways: 20 | 21 | + [Kerlink IoT Station installation manual](docs/INSTALL_INSTRUCTIONS/KERLINK.md) 22 | + [Multitech Conduit installation manual](docs/INSTALL_INSTRUCTIONS/MULTITECH.md) 23 | + [Raspberry Pi + IMST ic880a installation manual](docs/INSTALL_INSTRUCTIONS/IMST_RPI.md) 24 | 25 | ## Build 26 | 27 | If you have a custom-made gateway, or if you want to contribute to the development of the packet forwarder, you will have to build the binary yourself. 28 | 29 | + [Kerlink IoT Station build instructions](docs/INSTALL_INSTRUCTIONS/KERLINK.md#build) 30 | + [Multitech Conduit build instructions](docs/INSTALL_INSTRUCTIONS/MULTITECH.md#build) 31 | + [Raspberry Pi + IMST ic880a build instructions](docs/INSTALL_INSTRUCTIONS/IMST_RPI.md#build) 32 | + [SPI environment build instructions](docs/INSTALL_INSTRUCTIONS/SPI.md) 33 | + [FTDI environment build instructions](docs/INSTALL_INSTRUCTIONS/FTDI.md) *(Experimental)* 34 | 35 | + [General build parameters](docs/INSTALL_INSTRUCTIONS/PARAMETERS.md) 36 | 37 | ### Run 38 | 39 | ```bash 40 | $ packet-forwarder configure 41 | $ packet-forwarder start 42 | ``` 43 | 44 | #### Configuration format and flags 45 | 46 | The configuration file generated by `packet-forwarder configure` is stored at `$HOME/.pktfwd.yml`. You can specify a different config file with the `--config` flag, or specify runtime parameters with the different flags: 47 | 48 | * `--id`: Gateway ID of the present gateway. 49 | * `--key`: Gateway key of the present gateway. 50 | * `--router`: ID of the router with which communicate (optional ; default: account server-stored router) 51 | * `--auth-server`: URI of the account server (optional ; default: `https://account.thethingsnetwork.org`) 52 | * `--discovery-server`: Address and port of the discovery server (optional ; default: `discover.thethingsnetwork.org:1900`) 53 | * `--verbose` or `-v`: Show debugging information (optional) 54 | * `--downlink-send-margin`: Change downlink send margin, in milliseconds (optional ; [see documentation](docs/IMPLEMENTATION/DOWNLINKS.md)) 55 | * `--gps-path`: Set GPS path to enable GPS support (optional ; default: empty) 56 | * `--ignore-crc`: Ignore CRC check, and send uplink packets upstream even if they are CRC-invalid. 57 | 58 | ## Contributing 59 | 60 | Source code for this packet forwarder is MIT licensed. We encourage users to make contributions on [Github](https://github.com/TheThingsNetwork/packet-forwarder) and to participate in discussions on [Slack](https://www.thethingsnetwork.org/forum/t/slack-invitations/3037/4). 61 | 62 | If you encounter any problems, please check [open issues](https://github.com/TheThingsNetwork/packet-forwarder/issues) before [creating a new issue](https://github.com/TheThingsNetwork/packet-forwarder/issues/new). Please be specific and give a detailed description of the issue. Explain the steps to reproduce the problem. If you're able to fix the issue yourself, please help the community by forking the repository and submitting a pull request with your fix. 63 | 64 | For contributing a feature, please open an issue that explains what you're working on. Work in your own fork of the repository and submit a pull request when you're done. 65 | 66 | If you want to contribute, but don't know where to start, you could have a look at issues with the label [*help wanted*](https://github.com/TheThingsNetwork/packet-forwarder/labels/help%20wanted) or [*difficulty/easy*](https://github.com/TheThingsNetwork/packet-forwarder/labels/difficulty%2Feasy). 67 | 68 | ## License 69 | 70 | Source code for the packet forwarder is released under the MIT License, which can be found in the [LICENSE](LICENSE) file. 71 | -------------------------------------------------------------------------------- /cmd/configure.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package cmd 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/TheThingsNetwork/packet_forwarder/util" 9 | "github.com/segmentio/go-prompt" 10 | "github.com/spf13/cobra" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | var configureCmd = &cobra.Command{ 15 | Use: "configure [config-path]", 16 | Short: "Configure Packet Forwarder", 17 | Long: `packet-forwarder configure creates a YAML configuration file for the packet forwarder. 18 | 19 | The first argument is used as the storage location to the configuration file. If nothing is specified, the default configuration file path ($HOME/.pktfwd.yml) is used.`, 20 | 21 | Run: func(cmd *cobra.Command, args []string) { 22 | ctx := util.GetLogger() 23 | 24 | ctx.Info("If you haven't registered your gateway yet, you can register it either with the console, or with `ttnctl`.") 25 | 26 | var ( 27 | gatewayAuthServer string 28 | gatewayDiscoveryServer string 29 | ) 30 | 31 | if !prompt.Confirm("Is this gateway going to be used on the community network?") { 32 | gatewayDiscoveryServer = prompt.StringRequired("Enter the URL of the discovery server of your private network, in a format:") 33 | if prompt.Confirm("Are you using a private account server?") { 34 | gatewayAuthServer = prompt.StringRequired("Enter the URL of the account server (example: \"https://account.thethingsnetwork.org\"") 35 | } 36 | } 37 | 38 | gatewayID := prompt.StringRequired("Enter the ID of the gateway") 39 | gatewayKey := prompt.PasswordMasked("Enter the access key of the gateway") 40 | 41 | type yamlConfig struct { 42 | ID string `yaml:"id"` 43 | Key string `yaml:"key"` 44 | AuthServer string `yaml:"auth-server,omitempty"` 45 | DiscoveryServer string `yaml:"discovery-server,omitempty"` 46 | } 47 | 48 | newConfig := &yamlConfig{ 49 | ID: gatewayID, 50 | Key: gatewayKey, 51 | AuthServer: gatewayAuthServer, 52 | DiscoveryServer: gatewayDiscoveryServer, 53 | } 54 | 55 | output, err := yaml.Marshal(newConfig) 56 | if err != nil { 57 | util.GetLogger().WithError(err).Fatal("Failed to generate YAML") 58 | } 59 | 60 | f, err := os.Create(cfgFile) 61 | if err != nil { 62 | util.GetLogger().WithError(err).Fatal("Failed to create file") 63 | } 64 | defer f.Close() 65 | 66 | f.Write(output) 67 | ctx.WithField("ConfigFilePath", cfgFile).Info("New configuration file saved") 68 | }, 69 | } 70 | 71 | func init() { 72 | RootCmd.AddCommand(configureCmd) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/TheThingsNetwork/packet_forwarder/util" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | var cfgFile string 16 | 17 | var RootCmd = &cobra.Command{ 18 | Use: "packet-forwarder", 19 | Short: "The Things Network LoRa Packet Forwarder", 20 | Long: `The Things Network LoRa Packet Forwarder 21 | 22 | Every build is configured to interact with a kind of 23 | LoRa concentrator.`, 24 | } 25 | 26 | func Execute() { 27 | if err := RootCmd.Execute(); err != nil { 28 | fmt.Println(err) 29 | } 30 | } 31 | 32 | func init() { 33 | cobra.OnInitialize(initConfig) 34 | 35 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default \"$HOME/.pktfwd.yml\")") 36 | } 37 | 38 | func initConfig() { 39 | if cfgFile == "" { 40 | cfgFile = util.GetConfigFile() 41 | } 42 | 43 | viper.SetConfigType("yaml") 44 | viper.SetConfigName(".pktfwd") 45 | viper.AddConfigPath("$HOME") 46 | viper.SetEnvPrefix("pktfwd") 47 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 48 | viper.AutomaticEnv() 49 | 50 | if cfgFile != "" { 51 | viper.SetConfigFile(cfgFile) 52 | } 53 | 54 | if _, err := os.Stat(cfgFile); err == nil { 55 | err := viper.ReadInConfig() 56 | if err != nil { 57 | fmt.Println("Error when reading config file:", err, "; If the file doesn't exist yet, create .pktfwd.yml by using the `configure` command.") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/start.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package cmd 4 | 5 | import ( 6 | "os" 7 | "runtime/trace" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/TheThingsNetwork/packet_forwarder/pktfwd" 12 | "github.com/TheThingsNetwork/packet_forwarder/util" 13 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | // standardDownlinkSendMargin is the time we send a TX packet to the concentrator before its sending time. 19 | const standardDownlinkSendMargin = 100 20 | 21 | // downlinksMargin is specified at build. If it contains a numeric value, it is used as the number of 22 | // milliseconds of time margin. If no numeric value can be parsed, we use standardTimeMargin. 23 | var downlinksSendMargin = "" 24 | 25 | func getDefaultDownlinkSendMargin() int64 { 26 | margin, err := strconv.Atoi(downlinksSendMargin) 27 | if err != nil || margin == 0 { 28 | return standardDownlinkSendMargin 29 | } 30 | 31 | return int64(margin) 32 | } 33 | 34 | var config = viper.GetViper() 35 | 36 | var startCmd = &cobra.Command{ 37 | Use: "start", 38 | Short: "Start Packet Forwarding", 39 | Long: `packet-forwarder start connects to the LoRa concentrator, and starts redirecting the packets.`, 40 | 41 | Run: func(cmd *cobra.Command, args []string) { 42 | ctx := util.GetLogger() 43 | ctx.WithField("HALVersionInfo", wrapper.LoRaGatewayVersionInfo()).Info("Packet Forwarder for LoRa Gateway") 44 | 45 | if traceFilename := config.GetString("run-trace"); traceFilename != "" { 46 | f, err := os.Create(traceFilename) 47 | if err != nil { 48 | ctx.WithField("File", traceFilename).Fatal("Couldn't create trace file") 49 | } 50 | trace.Start(f) 51 | defer trace.Stop() 52 | ctx.WithField("File", traceFilename).Info("Trace writing active for this run") 53 | } 54 | 55 | if pin := config.GetInt("reset-pin"); pin != 0 { 56 | ctx.WithField("ResetPin", pin).Info("Reset pin specified, resetting concentrator...") 57 | if err := pktfwd.ResetPin(pin); err != nil { 58 | ctx.WithError(err).Fatal("Couldn't reset pin") 59 | } 60 | } 61 | 62 | ignoreCRC := config.GetBool("ignore-crc") 63 | if ignoreCRC { 64 | ctx.Warn("CRC check disabled, packets with invalid CRC will be sent upstream") 65 | } 66 | 67 | ttnConfig := &pktfwd.TTNConfig{ 68 | ID: config.GetString("id"), 69 | Key: config.GetString("key"), 70 | AuthServer: config.GetString("auth-server"), 71 | DiscoveryServer: config.GetString("discovery-server"), 72 | Router: config.GetString("router"), 73 | Version: config.GetString("version"), 74 | DownlinksSendMargin: time.Duration(config.GetInt64("downlink-send-margin")) * time.Millisecond, 75 | IgnoreCRC: ignoreCRC, 76 | } 77 | 78 | conf, err := pktfwd.FetchConfig(ctx, ttnConfig) 79 | if err != nil { 80 | ctx.WithError(err).Fatal("Couldn't read configuration") 81 | return 82 | } 83 | 84 | if err = pktfwd.Run(ctx, *conf, *ttnConfig, config.GetString("gps-path")); err != nil { 85 | ctx.WithError(err).Error("The program ended following a failure") 86 | } 87 | }, 88 | } 89 | 90 | func init() { 91 | startCmd.PersistentFlags().String("auth-server", "https://account.thethingsnetwork.org", "The account server the packet forwarder gets the gateway configuration from") 92 | startCmd.PersistentFlags().String("discovery-server", "discover.thethingsnetwork.org:1900", "The discovery server the packet forwarder uses to route the packets") 93 | startCmd.PersistentFlags().String("id", "", "The gateway ID to get its configuration from the account server") 94 | startCmd.PersistentFlags().String("key", "", "The gateway key to authenticate itself with the back-end") 95 | startCmd.PersistentFlags().String("router", "", "The router to communicate with (example: ttn-router-eu)") 96 | startCmd.PersistentFlags().String("gps-path", "", "The file system path to the GPS interface, if a GPS is available (example: /dev/nmea)") 97 | startCmd.PersistentFlags().Int64("downlink-send-margin", getDefaultDownlinkSendMargin(), "The margin, in milliseconds, between a downlink is sent to a concentrator and it is being sent by the concentrator") 98 | startCmd.PersistentFlags().String("run-trace", "", "File to which write the runtime trace of the packet forwarder. Can later be read with `go tool trace `.") 99 | startCmd.PersistentFlags().Int("reset-pin", 0, "GPIO pin associated to the reset pin of the board") 100 | startCmd.PersistentFlags().BoolP("verbose", "v", false, "Show debug logs") 101 | startCmd.PersistentFlags().Bool("ignore-crc", false, "Send packets upstream even if CRC validation is incorrect") 102 | 103 | viper.BindPFlags(startCmd.PersistentFlags()) 104 | 105 | RootCmd.AddCommand(startCmd) 106 | } 107 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/go-utils/log" 7 | "github.com/TheThingsNetwork/packet_forwarder/util" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var versionCmd = &cobra.Command{ 13 | Use: "version", 14 | Short: "Get build and version information", 15 | Long: "packet-forwarder version gets the build and version information of packet-forwarder", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | gitCommit := viper.GetString("gitCommit") 18 | buildDate := viper.GetString("buildDate") 19 | 20 | ctx := util.GetLogger() 21 | ctx.WithFields(log.Fields{ 22 | "Version": viper.GetString("version"), 23 | "Commit": gitCommit, 24 | "BuildDate": buildDate, 25 | }).Info("Got build information") 26 | }, 27 | } 28 | 29 | func init() { 30 | RootCmd.AddCommand(versionCmd) 31 | } 32 | -------------------------------------------------------------------------------- /docs/IMPLEMENTATION/DOWNLINKS.md: -------------------------------------------------------------------------------- 1 | # Downlinks implementation 2 | 3 | According to TTN specifications, gateways don't decide on which reception window should be sent a downlink. The TTN back-end decides, according to its own logic, when a downlink should be sent. The objectives of this implementation of this packet forwarder, in terms of downlink reception and transmission, are: 4 | 5 | * Transmitting every downlink packet within a reasonable timing to make sure that the reception window requirements of every one of them is met ; 6 | 7 | * Making an optimal use of the concentrator buffer, to be able to send as much downlinks as possible. 8 | 9 | Most concentrators only have a single downlink buffer - which means that the packet forwarder has to handle the logic of transmitting packets at the right moment to lose a minimum amount of packets. 10 | 11 | + [`sendingTimeMargin` values](#values) 12 | 13 | ## Downlink emission process 14 | 15 | To handle the distribution of a downlink, a packet forwarder must transmit to the concentrator, with the packet, the **internal clock time** at which the **concentrator should emit it**. This value is called `ExpectedSendingTimestamp`. The internal clock from the concentrator is initiated at 0 when the concentrator is started - during the `lgw_start()` HAL function call that starts the concentrator. This initialisation moment is called `ConcentratorBootTime`. The `lgw_start()` call lasting a few seconds, saving the time reference from the moment the HAL function was called is not precise enough. 16 | 17 | The method we use to find `ConcentratorBootTime` is through the first uplink. With every uplink, a value `count_us` is transmitted to the packet forwarder, that contains the **value of the concentrator's internal clock** at the uplink reception in the concentrator. In the manager (`pktfwd/manager.go`), during the first uplink reception, the calculation to find `ConcentratorBootTime` is made from this `count_us` value from the current time. 18 | 19 | It is important to note that because of the uplink polling rate, `count_us` only allows us to find `ConcentratorBootTime` within 100μs. When the packet forwarder starts, it polls for uplinks every 100μs, to have a higher degree of precision for the `ConcentratorBootTime` value calculation. When the first uplink has been received, the polling frequency is diminished to every 5ms, to avoid performance issues. 20 | 21 | * When a downlink is received, the packet forwarder schedules it in an internal queue system to be handled **100ms before `ExpectedSendingTimestamp`**. This means that the packet forwarder has then 100ms to perform its last computations on the downlink packet and to transmit it. This 100ms margin value is called `sendingTimeMargin`. 22 | 23 | * The value of `sendingTimeMargin` has a consequence of the gateway's downlink debit rate. Considering we need 100ms to transmit a downlink from the gateway's internal memory to emission, it means that we can only reasonably transmit 600 downlinks per minute - and that is making the assumption that receive windows won't overlap. 24 | 25 | * Having a 100ms `sendingTimeMargin` allows the packet forwarder to have a comfortable margin in case of performance issues on the system, or in case of transmission issues. For systems connected to a concentrator via USB, it usually takes 10ms to perform the last computations and to transmit the packet to the concentrator. However, one improvement to the packet forwarder would be setting `sendingTimeMargin` as a build or run parameter, to make use of the higher transmission speeds on SPI-connected devices. 26 | 27 | *Note:* The packet forwarder doesn't support GPS concentrators yet. GPS concentrators don't rely on an internal clock, and are able to transmit absolute timestamps for an uplink - meaning it is not necessary to know their internal clock value to transmit downlinks to such devices. 28 | 29 | ## Specific `sendingTimeMargin` values 30 | 31 | Depending on the hardware, we might change the value of `sendingTimeMargin` to adapt. A higher `sendingTimeMargin` value means more risk of having downlinks being deleted by the packet forwarder before sent, but can be necessary for the downlinks to be sent in time. 32 | 33 | |Build|`sendingTimeMargin` value| 34 | |---|---| 35 | |Kerlink IoT Station|50ms| 36 | |Multitech Conduit|100ms| 37 | -------------------------------------------------------------------------------- /docs/IMPLEMENTATION/HAL.md: -------------------------------------------------------------------------------- 1 | # HAL interface implementation 2 | 3 | The objective of this packet forwarder is to provide a lightweight implementation of the LoRaWAN specifications, adaptable to the different Hardware Abstraction Layers provided by Semtech and other actors. The logic behind the LoRaWAN protocol is thus loosely coupled to the specific concentrators interfaces. This means that it is possible for contributors to add compatibility to **new HALs**. For the moment, two HALs are available: 4 | 5 | + `halv1`, that interfaces with the classic SX1301 concentrator HAL. **This is the default HAL.** 6 | 7 | + `dummy`, that simulates an interaction with a concentrator. This HAL is to be reserved for testing purposes. 8 | 9 | To add an interface with a HAL, you need to implement, in the `wrapper` package, all the methods that are called by the rest of the packet forwarder. You can refer to the `*_dummy.go` files, that contain the code for the dummy HAL, for this. 10 | 11 | The classic process to add a new HAL is to add new files, in the `wrapper` package, that will **only build when the HAL identifier is passed as a build tag**. In Go, to specify this, you need to add a `// +build ` at the beginning of the file. For example, for a new HAL called `devHAL`, this is what `gps_devhal.go` would look like: 12 | 13 | ```go 14 | // +build devHAL 15 | 16 | package wrapper 17 | 18 | func LoRaGPSEnable(TTYPath string) error { 19 | return nil 20 | } 21 | 22 | // [...] 23 | ``` 24 | 25 | Once the development is over and you have implemented all `wrapper`'s functions for this new HAL, you can test the new HAL by building the packet forwarder by passing `HAL_CHOICE=` as environment variable: 26 | 27 | ```bash 28 | $ export HAL_CHOICE=devHAL 29 | $ make build 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/IMPLEMENTATION/README.md: -------------------------------------------------------------------------------- 1 | # Implementation documentation 2 | 3 | Available documentation: 4 | 5 | + [Downlinks implementation](DOWNLINKS.md) 6 | + [HAL interface implementation](HAL.md) 7 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/FTDI.md: -------------------------------------------------------------------------------- 1 | # Install the TTN Packet Forwarder on a FTDI environment 2 | 3 | *Note: Support of FTDI is experimental and not guaranteed, as Semtech has dropped FTDI support for the Hardware Abstraction Layer. macOS builds should only be used for development purposes.* 4 | 5 | + [Build procedure](#build) 6 | + [macOS troubleshooting](#macos) 7 | 8 | ## Build procedure 9 | 10 | Building the packet forwarder on a FTDI environment, with a USB connection with the concentrator, requires the `libmpsse` library. 11 | 12 | ### Install `libmpsse` 13 | 14 | 1. `brew install libftdi` or `apt install libftdi` 15 | 2. `wget https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/libmpsse/libmpsse-1.3.tar.gz` 16 | 3. `tar -xvzf libmpsse-1.3.tar.gz` 17 | 4. `cd libmpsse-1.3/src` 18 | 5. `./configure --disable-python && make && sudo make install` 19 | 20 | ### Download and build the packet forwarder 21 | 22 | Make sure you have [installed](https://golang.org/dl/) and [configured](https://golang.org/doc/code.html#GOPATH) your Go environment. 23 | 24 | ```bash 25 | $ go get -u github.com/TheThingsNetwork/packet_forwarder 26 | $ cd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 27 | $ make dev-deps 28 | $ make deps 29 | # If you are using Linux: 30 | $ CFG_SPI=ftdi PLATFORM=imst_rpi make build 31 | # If you are using macOS: 32 | $ CFG_SPI=mac PLATFORM=imst_rpi make build 33 | ``` 34 | 35 | The build will then be available in the `release/` folder. 36 | 37 | ## macOS troubleshooting 38 | 39 | On a macOS environment, you will need, at every reboot, to unload the native Apple Driver for FTDI devices: `sudo kextunload -b com.apple.driver.AppleUSBFTDI`. If you are unsure of the name of the driver, you can look for it with the command `kextstat | grep FTDI`. 40 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/IMST_RPI.md: -------------------------------------------------------------------------------- 1 | # Install the TTN Packet Forwarder on a Raspberry Pi with an IMST ic880a board 2 | 3 | To follow this manual, you must have a Raspberry Pi with an IMST ic880a board, connected through SPI. 4 | 5 | ## Download and run 6 | 7 | 1. Download the [Raspberry Pi + IMST build](https://ttnreleases.blob.core.windows.net/packet-forwarder/master/imst-rpi-pktfwd.tar.gz) of the packet forwarder. 8 | 9 | 2. Configure the packet forwarder: 10 | 11 | ```bash 12 | $ configure 13 | [...] 14 | INFO New configuration file saved ConfigFilePath=/root/.pktfwd.yml 15 | ``` 16 | 17 | 3. Run the packet forwarder: 18 | 19 | ```bash 20 | $ start 21 | ``` 22 | 23 | ### Permanent installation with systemd 24 | 25 | If you want a permanent installation of the packet forwarder on your Raspberry Pi, with `systemd` managing the packet forwarder on the background, we provide a basic systemd installation script, `install-systemd.sh`. 26 | 27 | 1. Select the build, and copy it in a permanent location - such as `/usr/bin`. 28 | 29 | 2. Create a configuration file in a permanent location, such as in a `/usr/config` directory: 30 | 31 | ```bash 32 | $ touch /usr/config/ttn-pkt-fwd.yml 33 | ``` 34 | 35 | 3. Set up this configuration file: 36 | 37 | ```bash 38 | $ configure /usr/config/ttn-pkt-fwd.yml 39 | ``` 40 | 41 | 4. Use the `install-systemd.sh` script, with the binary as a first argument and the config file as a second argument: 42 | 43 | ```bash 44 | $ ./install-systemd.sh 45 | ./install-systemd.sh: Installation of the systemd service complete. 46 | ``` 47 | 48 | 5. Reload the systemd daemon, and start the service: 49 | 50 | ```bash 51 | sudo systemctl daemon-reload 52 | sudo systemctl enable ttn-pkt-fwd 53 | sudo systemctl start ttn-pkt-fwd 54 | ``` 55 | 56 | ## Build 57 | 58 | If want to contribute to the development of the packet forwarder, you might want to build the TTN Packet Forwarder. You will need to use a Linux environment to run the toolchain necessary for the build. 59 | 60 | ### Getting the toolchain 61 | 62 | If you want to build the packet forwarder for a Raspberry Pi, you will need a **Raspberry Pi cross-compiler**. On some Linux distributions, such as Ubuntu, a toolchain is available as a package: `sudo apt install gcc-arm-linux-gnueabi -y`. 63 | 64 | ### Building the binary 65 | 66 | Make sure you have [installed](https://golang.org/dl/) and [configured](https://golang.org/doc/code.html#GOPATH) your Go environment. 67 | 68 | Follow these commands: 69 | 70 | ```bash 71 | $ make dev-deps 72 | $ make deps 73 | $ GOOS=linux GOARCH=arm GOARM=7 CC=gcc-arm-linux-gnueabi make build 74 | ``` 75 | 76 | The binary will then be available in the `release/` folder. 77 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/KERLINK.md: -------------------------------------------------------------------------------- 1 | # Install the TTN Packet Forwarder on a Kerlink IoT Station 2 | 3 | *Note: for the moment, the TTN Packet Forwarder is not compatible with the Kerlink iBTS.* 4 | 5 | Before installing the TTN Packet Forwarder, we recommend **updating the Station to the latest firmware available**. 6 | 7 | + [Download and test the TTN Packet Forwarder](#download-test) 8 | + [Install the TTN Packet Forwarder](#install) 9 | + [Build the TTN Packet Forwarder](#build) 10 | + [Troubleshooting](#troubleshooting) 11 | 12 | ## Download and test the TTN Packet Forwarder 13 | 14 | *Note: Before installing the new packet forwarder, make sure you removed any other packet forwarder installed on your Kerlink IoT Station. If you don't have any important files stored on the disk, the safest way to make sure of that is to update the Station to the latest firmware available, which will reset the file system in the process.* 15 | 16 | 1. Download the [Kerlink build](https://ttnreleases.blob.core.windows.net/packet-forwarder/master/kerlink-iot-station-pktfwd.tar.gz) of the packet forwarder. 17 | 18 | 2. In the folder, you will find several files: a `create-package.sh` script and a binary file, that we will call `packet-forwarder`. 19 | 20 | The binary is sufficient for a testing use - if you wish to try the TTN packet forwarder, just copy the binary on the Station, and execute: 21 | 22 | ```bash 23 | $ ./packet-forwarder configure 24 | # Follow the instructions of the wizard 25 | [...] 26 | INFO New configuration file saved ConfigFilePath=/root/config.yml 27 | $ ./packet-forwarder start 28 | INFO Packet Forwarder for LoRa Gateway HALVersionInfo=Version: 4.0.0; Options: native; 29 | [...] 30 | INFO Concentrator started, packets can now be received and sent 31 | ``` 32 | 33 | ## Install the TTN Packet Forwarder 34 | 35 | This section covers permanent installation of the TTN Packet Forwarder on a Kerlink IoT Station. 36 | 37 | ### Packaging the TTN Packet Forwarder 38 | 39 | Download the [Kerlink build](https://ttnreleases.blob.core.windows.net/packet-forwarder/master/kerlink-iot-station-pktfwd.tar.gz) of the packet forwarder. Execute the `create-package.sh` script with the binary inside as an argument: 40 | 41 | ```bash 42 | $ ./create-package.sh packet-forwarder 43 | [...] 44 | # The script will ask you several questions to configure the packet forwarder. 45 | ./create-package.sh: Kerlink DOTA package complete. 46 | ``` 47 | 48 | A `kerlink-release-` folder will appear in the folder you are in: 49 | 50 | ```bash 51 | $ cd kerlink-release- && tree 52 | . 53 | ├── dota_ttn-pkt-fwd.tar.gz 54 | ├── INSTALL.md 55 | └── produsb.sh 56 | ``` 57 | 58 | ### Transfering and installing the TTN Packet Forwarder 59 | 60 | You can consult the `INSTALL.md` to know how to install the package from here. The two options are **network transfer** and **USB stick transfer**. Depending on your configuration, choose the installation method that suits you best. Once the package has been transferred, the packet forwarder will be installed on your Kerlink IoT Station! You can monitor it from the [console](https://console.thethingsnetwork.org): 61 | 62 | ![Console demo](https://github.com/TheThingsNetwork/packet_forwarder/raw/master/docs/INSTALL_INSTRUCTIONS/console.gif) 63 | 64 | ## Build the TTN Packet Forwarder for the Kerlink IoT Station 65 | 66 | If you use a specific machine or want to contribute to the development of the packet forwarder, you might want to build the TTN Packet Forwarder. You might need to use a Linux environment to run the toolchain necessary for the build. 67 | 68 | ### Building the binary 69 | 70 | To build the packet forwarder for the Kerlink IoT Station, you will need access to Kerlink's Wirnet Station wiki. 71 | 72 | 1. On Kerlink's Wirnet Station Wiki, click on *Resources*, scroll down to *Tools*, then download the toolchain you need, depending on the firmware of your Station. 73 | 74 | 2. In most cases, the archive will hold a `arm-2011.03-wirgrid` folder. Copy this folder in `/opt`: 75 | 76 | ```bash 77 | $ mkdir -p /opt 78 | $ mv arm-2011.03-wirgrid /opt 79 | ``` 80 | 81 | 3. Execute the `scripts/build-kerlink.sh` script, indicating in argument the location of the toolchain: 82 | 83 | ```bash 84 | $ ./scripts/build-kerlink.sh "/opt/arm-2011.03-wirgrid" 85 | ``` 86 | 87 | This script will build the Kerlink IoT Station binary of the packet forwarder. 88 | 89 | ### Building the DOTA file 90 | 91 | This binary is sufficient for basic testing of the packet forwarder on a Kerlink IoT Station. However, for permanent installations, the packet forwarder is wrapped in a package called DOTA file. To create a DOTA file, use the `scripts/kerlink/create-kerlink-package.sh` script: 92 | 93 | ```bash 94 | $ ./scripts/kerlink/create-kerlink-package.sh 95 | [...] 96 | create-kerlink-package.sh: Kerlink DOTA package complete. The package is available in kerlink-release/. Consult the INSTALL.md file to know how to install the package on your Kerlink IoT Station! 97 | ``` 98 | 99 | ## Troubleshooting 100 | 101 | #### I've deleted my gateway from the console and added a new one, how can I change the configuration of the gateway? 102 | 103 | Connect remotely to the Kerlink IoT Station, and execute these commands: 104 | 105 | ```bash 106 | $ cd /mnt/fsuser-1/ttn-packet-forwarder 107 | $ ./ttn-pkt-fwd configure config.yml 108 | # Following the instructions of the wizard 109 | [...] 110 | INFO New configuration file saved ConfigFilePath=config.yml 111 | $ reboot 112 | # Reboot the gateway to apply the changes 113 | ``` 114 | 115 | #### I'm getting a "Concentrator boot time computation error: Absurd uptime received by the concentrator" error when starting the packet forwarder. 116 | 117 | The concentrator sometimes sends absurd uptime values to the packet forwarder, often because it hasn't been stopped properly. Restart the packet forwarder until this error disappears. 118 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/MULTITECH.md: -------------------------------------------------------------------------------- 1 | # Install the TTN Packet Forwarder on a Multitech Conduit 2 | 3 | *Note: if you're using an AEP model, you will need to configure the Conduit on the web interface before installing the Packet Forwarder. Consult [this guide](https://www.thethingsnetwork.org/docs/gateways/multitech/aep.html) to learn how to do this.* 4 | 5 | ## Download and install 6 | 7 | *Note: Before installing the new packet forwarder, make sure you removed any other packet forwarder installed on your Multitech Conduit.* 8 | 9 | 1. Download the [Multitech Conduit package](https://ttnreleases.blob.core.windows.net/packet-forwarder/master/multitech-conduit-pktfwd.tar.gz) of the packet forwarder. 10 | 11 | 2. In the archive, you will find an `create-package.sh` file, a `multitech-installer.sh`, as well as the executable binary. Execute the `create-package.sh` file, with the binary as a first argument: 12 | 13 | ```bash 14 | $ ./create-package.sh 15 | [...] 16 | # Following the instructions of the wizard 17 | ./create-package.sh: package available at ttn-pkt-fwd.ipk 18 | ``` 19 | 20 | 3. Copy the package on the Multitech Conduit, using either a USB key or `scp` if you have an SSH connection to the Multitech Conduit. Install the package, configure the packet forwarder, then start it: 21 | 22 | ```bash 23 | $ opkg install ttn-pkt-fwd.ipk 24 | Installing ttn-pkt-fwd (2.0.0) to root... 25 | Configuring ttn-pkt-fwd. 26 | $ /etc/init.d/ttn-pkt-fwd configure 27 | [...] 28 | # Following the instructions of the wizard 29 | INFO New configuration file saved ConfigFilePath=/var/config/ttn-pkt-fwd/config.yml 30 | $ /etc/init.d/ttn-pkt-fwd start 31 | Starting ttn-pkt-fwd: OK 32 | ``` 33 | 34 | ## Build the TTN Packet Forwarder for the Multitech Conduit 35 | 36 | If you use a specific machine or want to contribute to the development of the packet forwarder, you might want to build the TTN Packet Forwarder. You might need to use a Linux environment to run the toolchain necessary for the build. 37 | 38 | ### Downloading the Multitech toolchain 39 | 40 | To build the packet forwarder, you will need to download [Multitech's C toolchain](http://www.multitech.net/developer/software/mlinux/mlinux-software-development/mlinux-c-toolchain/). Download it, and install it by following the instructions on Multitech's website. 41 | 42 | ### Building the binary 43 | 44 | 1. Download the packet forwarder, along with its dependencies: 45 | 46 | ```bash 47 | $ go get -u github.com/TheThingsNetwork/packet_forwarder 48 | $ cd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 49 | $ make dev-deps 50 | $ make deps 51 | ``` 52 | 53 | 2. Enable the Multitech toolchain, then set those environment variables: 54 | 55 | ```bash 56 | $ source /path/to/sdk/environment-setup-arm926ejste-mlinux-linux-gnueabi 57 | # Usually /opt/mlinux/{version}/environment-setup-arm926ejste-mlinux-linux-gnueabi 58 | $ export GOARM=5 59 | $ export GOOS=linux 60 | $ export GOARCH=arm 61 | $ export CFG_SPI=ftdi 62 | $ export PLATFORM=multitech 63 | ``` 64 | 65 | 3. Build the binary: 66 | 67 | ```bash 68 | $ make build 69 | ``` 70 | 71 | The binary will then be available in the `release/` folder. 72 | 73 | ### Building the package 74 | 75 | To build the package, use the `scripts/multitech/create-package.sh` script: 76 | 77 | ```bash 78 | $ ./scripts/multitech/create-package.sh release/ 79 | [...] 80 | # Following the instructions of the wizard 81 | ./create-package.sh: package available at ttn-pkt-fwd.ipk 82 | ``` 83 | 84 | The package will then be available at the specified path. 85 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/PARAMETERS.md: -------------------------------------------------------------------------------- 1 | # Build configuration 2 | 3 | In addition to the hardware-specific parameters you can pass at build to enable or specify certain features, this document details the different parameters that can be used. 4 | 5 | ## HAL choice 6 | 7 | For the moment, only two HALs are available. To switch HALs, pass the identifier of this HAL to `HAL_CHOICE`: 8 | 9 | + `halv1`, that interfaces with the classic SX1301 concentrator HAL. **This is the default value.** 10 | 11 | + `dummy`, that simulates an interaction with a concentrator. This HAL is to be reserved for testing purposes, on testing network environments. 12 | 13 | To learn more about implementing an interface with another HAL, please consult the [implementation reference](../IMPLEMENTATION/HAL.md). 14 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/SPI.md: -------------------------------------------------------------------------------- 1 | # Install the TTN Packet Forwarder on a SPI environment 2 | 3 | + [Build procedure](#build) 4 | + [Cross-compilation](#crosscompilation) 5 | + [SPI configuration](#spi) 6 | + [GPS configuration](#gps) 7 | 8 | ## Build procedure 9 | 10 | Make sure you have [installed](https://golang.org/dl/) and [configured](https://golang.org/doc/code.html#GOPATH) Go environment 1.8.3 or higher. 11 | 12 | This procedure describes how to build the packet forwarder for a machine that can interact with a concentrator using SPI. If you build the packet forwarder on the machine itself, the SPI configuration will be dynamically determined. Otherwise, see the [SPI configuration section](#spi) to see how to specify the SPI configuration. 13 | 14 | ```bash 15 | $ go get -u github.com/TheThingsNetwork/packet_forwarder 16 | $ cd $GOPATH/src/github.com/TheThingsNetwork/packet_forwarder 17 | $ make dev-deps 18 | $ make deps 19 | $ make build 20 | ``` 21 | 22 | The build will then be available in the `release/` folder. 23 | 24 | ### Cross-compilation 25 | 26 | If the gateway you wish to build the packet forwarder for doesn't support compilation, you will need to specify the target platform through environment variables: 27 | 28 | * `GOOS`: OS of the target machine, following one of the values [supported by the Go toolchain](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Example: `GOARCH=linux`. 29 | * `GOARCH`: Architecture of the target machine, following one of the values [supported by the Go toolchain](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Example: `GOARCH=amd64`, `GOARCH=arm`. 30 | * `GOARM`: If building the packet forwarder for an `arm` architecture, the [ARM architecture to support](https://github.com/golang/go/wiki/GoArm). Example: `GOARM=5`, `GOARM=7`. 31 | * `CC`: the `cc` compiler collection to use. Example: `CC=arm-none-linux-gnueabi-gcc`, `CC=arm-mlinux-gnuneabi-gcc`. 32 | * `CROSS_COMPILE`: the prefix of the compiler collections to use. Example: `CROSS_COMPILE=arm-none-linux-gnueabi-`, `CROSS_COMPILE=arm-mlinux-gnuneabi-`. 33 | 34 | These environment variables need to be set **just after the `make dev-deps`** step of the build process. 35 | 36 | *Note: in several cases, toolchain setup scripts will set some of those variables for you, such as with the Multitech toolchain.* 37 | 38 | ## SPI configuration 39 | 40 | If the build machine is different from the target machine, the SPI configuration can't be determined during the build process. You will then have to specify the configuration as a parameter. 41 | 42 | * If the target machine is a `kerlink`, `imst_rpi`, `linklabs_blowfish_rpi` or `lorank` machine, the SPI configuration for those devices is already part of the HAL - you can build for those by specifying `PLATFORM=kerlink`, `PLATFORM=lorank`, and such. You can see `lora_gateway/libloragw/library.cfg`, once the dependencies have been installed. 43 | 44 | * Otherwise, you can specify the parameters of the SPI interface with those parameters: 45 | 46 | * `SPI_SPEED` (default: `8000000`) 47 | * `SPIDEV` (default: the first `/dev/spidev*` file found on the build machine) 48 | * `SPI_CS_CHANGE` (default: `0`) 49 | * `VID` (default: `0x0403`) 50 | * `PID` (default: `0x6014`) 51 | 52 | ## GPS configuration 53 | 54 | If the gateway you are running the packet forwarder has a GPS available, you can enable it by passing during the build the `GPS_PATH` environment variable. This `GPS_PATH` should point to the TTY path of the GPS. For example, on the Kerlink build, `GPS_PATH=/dev/nmea`. 55 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/TOOLCHAINS.md: -------------------------------------------------------------------------------- 1 | # Building toolchain images 2 | 3 | This document describes how to build the toolchain Docker images, to build them for development purposes. Indeed, some of the toolchains (such as the Kerlink IoT Station toolchain) are privately-licensed - we thus cannot publicly release them. The Dockerfiles mentioned in this document are located in the `scripts/toolchains` folder. 4 | 5 | Once the images are built on your personal machine, you can set up your own CI pipeline, using the [GitLab CI](.gitlab-ci.yml) configuration file in the repo. 6 | 7 | * [Kerlink IoT Station](#klk-iot-station) 8 | * [Multitech Conduit](#multitech) 9 | 10 | ## Kerlink IoT Station 11 | 12 | To build the `registry.gitlab.com/thethingsnetwork/packet_forwarder/klk-toolchain` image, you will need access to Kerlink's Wirnet Station wiki. 13 | 14 | 1. On Kerlink's Wirnet Station Wiki, click on *Resources*, scroll down to *Tools*, then download the toolchain you need, depending on the firmware of your Station. 15 | 16 | 2. In most cases, the archive will hold a `arm-2011.03-wirgrid` folder. Copy this folder in `packet-forwarder/scripts/toolchains`. 17 | 18 | 3. Build the image: `docker build . -t registry.gitlab.com/thethingsnetwork/packet_forwarder/klk-toolchain -f Dockerfile.kerlink-iot-station`. The content of the toolchain will be copied to form the image. 19 | 20 | ## Multitech Conduit mLinux 21 | 22 | The Multitech Conduit image, tagged `registry.gitlab.com/thethingsnetwork/packet_forwarder/multitech-toolchain`, does not need access to any private resource, and can be built solely with its Dockerfile: 23 | 24 | ```bash 25 | $ docker build . -t registry.gitlab.com/thethingsnetwork/packet_forwarder/multitech-toolchain -f Dockerfile.multitech 26 | ``` 27 | 28 | *Note: depending on the Docker storage driver on which the image is built, the SDK extraction can fail. In that case, you might want to change the storage driver to `aufs`.* 29 | -------------------------------------------------------------------------------- /docs/INSTALL_INSTRUCTIONS/console.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheThingsArchive/packet_forwarder/63127c64088e12e8e25d87687646dcc6f1acd987/docs/INSTALL_INSTRUCTIONS/console.gif -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/packet_forwarder/cmd" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | var ( 11 | version = "2.x.x" 12 | gitCommit = "unknown" 13 | buildDate = "unknown" 14 | ) 15 | 16 | func main() { 17 | viper.Set("version", version) 18 | viper.Set("gitCommit", gitCommit) 19 | viper.Set("buildDate", buildDate) 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /pktfwd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheThingsArchive/packet_forwarder/63127c64088e12e8e25d87687646dcc6f1acd987/pktfwd.gif -------------------------------------------------------------------------------- /pktfwd/configuration.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/go-account-lib/account" 7 | "github.com/TheThingsNetwork/go-utils/log" 8 | "github.com/TheThingsNetwork/packet_forwarder/util" 9 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 10 | ) 11 | 12 | // Multitech concentrators require a clksrc of 0, even if the frequency plan indicates a value of 1. 13 | // This value, modified at build to include the platform type, is currently useful as a flag to 14 | // ignore the frequency plan value of `clksrc`. 15 | var platform = "" 16 | 17 | func configureBoard(ctx log.Interface, conf util.Config, gpsPath string) error { 18 | if platform == "multitech" { 19 | ctx.Info("Forcing clock source to 0 (Multitech concentrator)") 20 | conf.Concentrator.Clksrc = 0 21 | } 22 | 23 | err := wrapper.SetBoardConf(ctx, conf) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | err = configureChannels(ctx, conf) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | err = enableGPS(ctx, gpsPath) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func configureIndividualChannels(ctx log.Interface, conf util.Config) error { 42 | // Configuring LoRa standard channel 43 | if lora := conf.Concentrator.LoraSTDChannel; lora != nil { 44 | err := wrapper.SetStandardChannel(ctx, *lora) 45 | if err != nil { 46 | return err 47 | } 48 | ctx.Info("LoRa standard channel configured") 49 | } else { 50 | ctx.Warn("No configuration for LoRa standard channel, ignoring") 51 | } 52 | 53 | // Configuring FSK channel 54 | if fsk := conf.Concentrator.FSKChannel; fsk != nil { 55 | err := wrapper.SetFSKChannel(ctx, *fsk) 56 | if err != nil { 57 | return err 58 | } 59 | ctx.Info("FSK channel configured") 60 | } else { 61 | ctx.Warn("No configuration for FSK standard channel, ignoring") 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func configureChannels(ctx log.Interface, conf util.Config) error { 68 | // Configuring the TX Gain Lut 69 | err := wrapper.SetTXGainConf(ctx, conf.Concentrator) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Configuring the RF and SF channels 75 | err = wrapper.SetRFChannels(ctx, conf) 76 | if err != nil { 77 | return err 78 | } 79 | wrapper.SetSFChannels(ctx, conf) 80 | 81 | // Configuring the individual LoRa standard and FSK channels 82 | err = configureIndividualChannels(ctx, conf) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | // FetchConfig reads the configuration from the distant server 90 | func FetchConfig(ctx log.Interface, ttnConfig *TTNConfig) (*util.Config, error) { 91 | a := account.New(ttnConfig.AuthServer) 92 | 93 | gw, err := a.FindGateway(ttnConfig.ID) 94 | ctx = ctx.WithFields(log.Fields{"GatewayID": ttnConfig.ID, "AuthServer": ttnConfig.AuthServer}) 95 | if err != nil { 96 | ctx.WithError(err).Error("Failed to find gateway specified as gateway ID") 97 | return nil, err 98 | } 99 | ctx.WithField("URL", gw.FrequencyPlanURL).Info("Found gateway parameters, getting frequency plans") 100 | if gw.Attributes.Description != nil { 101 | ttnConfig.GatewayDescription = *gw.Attributes.Description 102 | } 103 | 104 | config, err := util.FetchConfigFromURL(ctx, gw.FrequencyPlanURL) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | return &config, nil 110 | } 111 | -------------------------------------------------------------------------------- /pktfwd/downlinks.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "github.com/TheThingsNetwork/go-utils/log" 10 | "github.com/TheThingsNetwork/go-utils/queue" 11 | "github.com/TheThingsNetwork/packet_forwarder/util" 12 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 13 | "github.com/TheThingsNetwork/ttn/api/router" 14 | ) 15 | 16 | // BootTimeSetter is an interface that implements every type that needs receive boot time (mostly to 17 | // determine uptime afterwards) 18 | type BootTimeSetter interface { 19 | SetBootTime(t time.Time) 20 | } 21 | 22 | type multipleBootTimeSetter struct { 23 | list []BootTimeSetter 24 | t *time.Time 25 | } 26 | 27 | func NewMultipleBootTimeSetter() multipleBootTimeSetter { 28 | return multipleBootTimeSetter{ 29 | list: make([]BootTimeSetter, 0), 30 | } 31 | } 32 | 33 | func (b *multipleBootTimeSetter) SetBootTime(t time.Time) { 34 | for _, receiver := range b.list { 35 | receiver.SetBootTime(t) 36 | } 37 | b.t = &t 38 | } 39 | func (b *multipleBootTimeSetter) Add(t BootTimeSetter) { 40 | if b.t != nil { 41 | t.SetBootTime(*b.t) 42 | } 43 | b.list = append(b.list, t) 44 | } 45 | 46 | // DownlinkManager is an interface that starts scheduling every downlink that is given to it 47 | type DownlinkManager interface { 48 | BootTimeSetter 49 | ScheduleDownlink(d *router.DownlinkMessage) 50 | } 51 | 52 | type downlinkManager struct { 53 | queue queue.JIT 54 | ctx log.Interface 55 | conf util.Config 56 | bgCtx context.Context 57 | statusMgr StatusManager 58 | startupTime time.Time 59 | downlinkSendMargin time.Duration 60 | } 61 | 62 | func (d *downlinkManager) getTimeMargin() time.Duration { 63 | return d.downlinkSendMargin 64 | } 65 | 66 | // NewDownlinkManager returns a new downlink manager that runs as long as the context doesn't close 67 | func NewDownlinkManager(bgCtx context.Context, ctx log.Interface, conf util.Config, statusMgr StatusManager, sendingTimeMargin time.Duration) DownlinkManager { 68 | downlinkMgr := &downlinkManager{ 69 | queue: queue.NewJIT(), 70 | ctx: ctx, 71 | conf: conf, 72 | bgCtx: bgCtx, 73 | statusMgr: statusMgr, 74 | downlinkSendMargin: sendingTimeMargin, 75 | } 76 | ctx.WithField("SendingTimeMargin", sendingTimeMargin).Debug("Configured margin between downlink sent and concentrator processing") 77 | go downlinkMgr.handleDownlinks() 78 | return downlinkMgr 79 | } 80 | 81 | func (d *downlinkManager) SetBootTime(t time.Time) { 82 | d.startupTime = t 83 | } 84 | 85 | func (d *downlinkManager) handleDownlinks() { 86 | downlinks := d.nextDownlinks() 87 | for { 88 | select { 89 | case downlink := <-downlinks: 90 | d.ctx.WithField("ConcentratorUptime", time.Now().Sub(d.startupTime)).Info("Received downlink from JIT queue, transmitting to the concentrator") 91 | if err := wrapper.SendDownlink(downlink, d.conf, d.ctx); err == nil { 92 | d.statusMgr.SentTX() 93 | } 94 | case <-d.bgCtx.Done(): 95 | d.ctx.Info("Stopping downlink manager") 96 | return 97 | } 98 | } 99 | } 100 | 101 | func (d *downlinkManager) nextDownlinks() chan *router.DownlinkMessage { 102 | downlink := make(chan *router.DownlinkMessage) 103 | go func() { 104 | for { 105 | item := d.queue.Next() 106 | if item == nil { 107 | d.ctx.Warn("JIT queue closing, no more downlinks sent") 108 | break 109 | } 110 | next := item.(*router.DownlinkMessage) 111 | select { 112 | case downlink <- next: 113 | default: 114 | } 115 | } 116 | close(downlink) 117 | }() 118 | 119 | return downlink 120 | } 121 | 122 | func (d *downlinkManager) ScheduleDownlink(message *router.DownlinkMessage) { 123 | lora := message.ProtocolConfiguration.GetLorawan() 124 | if lora == nil { 125 | d.ctx.Warn("Received non-LORA downlink, ignoring") 126 | return 127 | } 128 | 129 | margin := d.getTimeMargin() 130 | 131 | schedulingTimestamp := util.TXTimestamp(message.GetGatewayConfiguration().GetTimestamp()) 132 | d.ctx.WithFields(log.Fields{ 133 | "ExpectedSendingTimestamp": schedulingTimestamp.GetAsDuration(), 134 | "ConcentratorBootTime": d.startupTime, 135 | "ConcentratorUptime": time.Now().Sub(d.startupTime), 136 | "SchedulingTimestamp": d.startupTime.Add(-margin).Add(schedulingTimestamp.GetAsDuration()), 137 | }).Info("Scheduled downlink") 138 | d.queue.Schedule(message, d.startupTime.Add(-margin).Add(schedulingTimestamp.GetAsDuration())) 139 | } 140 | -------------------------------------------------------------------------------- /pktfwd/gpio.go: -------------------------------------------------------------------------------- 1 | package pktfwd 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stianeikeland/go-rpio" 8 | ) 9 | 10 | const gpioTimeMargin = 100 * time.Millisecond 11 | 12 | // ResetPin resets the specified pin 13 | func ResetPin(pinNumber int) error { 14 | err := rpio.Open() 15 | if err != nil { 16 | return errors.Wrap(err, "couldn't get GPIO access") 17 | } 18 | 19 | pin := rpio.Pin(uint8(pinNumber)) 20 | pin.Output() 21 | time.Sleep(gpioTimeMargin) 22 | pin.Low() 23 | time.Sleep(gpioTimeMargin) 24 | pin.High() 25 | time.Sleep(gpioTimeMargin) 26 | pin.Low() 27 | time.Sleep(gpioTimeMargin) 28 | 29 | return errors.Wrap(rpio.Close(), "couldn't close GPIO access") 30 | } 31 | -------------------------------------------------------------------------------- /pktfwd/gps.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/go-utils/log" 7 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | /* 12 | GPS workflow with the TTN back-end: 13 | - if gps available, send that in the status message (no need to send it with every uplink) 14 | - if nothing available or set, get the coordinates from account server and use that in the status message 15 | */ 16 | 17 | // enableGPS checks if there is an available GPS for this build - if yes, 18 | // tries to activate it. 19 | func enableGPS(ctx log.Interface, gpsPath string) (err error) { 20 | if gpsPath == "" { 21 | ctx.Warn("No GPS chip configured, ignoring") 22 | return nil 23 | } 24 | 25 | ctx.WithField("GPSPath", gpsPath).Info("GPS path found, activating") 26 | err = wrapper.LoRaGPSEnable(gpsPath) 27 | if err != nil { 28 | return errors.Wrap(err, "GPS activation failed") 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pktfwd/manager.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/go-utils/log" 13 | "github.com/TheThingsNetwork/packet_forwarder/util" 14 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | const ( 19 | initUplinkPollingRate = 100 * time.Microsecond 20 | stableUplinkPollingRate = 5 * time.Millisecond 21 | statusRoutineSleepRate = 15 * time.Second 22 | gpsUpdateRate = 5 * time.Millisecond 23 | ) 24 | 25 | /* Manager struct manages the routines during runtime, once the gateways and network 26 | configuration have been set up. It startes a routine, that it only stopped when the 27 | users wants to close the program or that an error occurs. */ 28 | type Manager struct { 29 | ctx log.Interface 30 | conf util.Config 31 | netClient NetworkClient 32 | statusMgr StatusManager 33 | uplinkPollingRate time.Duration 34 | // Concentrator boot time 35 | bootTimeSetters multipleBootTimeSetter 36 | foundBootTime bool 37 | isGPS bool 38 | ignoreCRC bool 39 | downlinksSendMargin time.Duration 40 | } 41 | 42 | func NewManager(ctx log.Interface, conf util.Config, netClient NetworkClient, gpsPath string, runConfig TTNConfig) Manager { 43 | isGPS := gpsPath != "" 44 | statusMgr := NewStatusManager(ctx, netClient.FrequencyPlan(), runConfig.GatewayDescription, isGPS, netClient.DefaultLocation()) 45 | 46 | bootTimeSetters := NewMultipleBootTimeSetter() 47 | bootTimeSetters.Add(statusMgr) 48 | 49 | return Manager{ 50 | ctx: ctx, 51 | conf: conf, 52 | netClient: netClient, 53 | statusMgr: statusMgr, 54 | bootTimeSetters: bootTimeSetters, 55 | isGPS: isGPS, 56 | // At the beginning, until we get our first uplinks, we keep a high polling rate to the concentrator 57 | uplinkPollingRate: initUplinkPollingRate, 58 | downlinksSendMargin: runConfig.DownlinksSendMargin, 59 | ignoreCRC: runConfig.IgnoreCRC, 60 | } 61 | } 62 | 63 | func (m *Manager) run() error { 64 | runStart := time.Now() 65 | m.ctx.WithField("DateTime", runStart).Info("Starting concentrator...") 66 | err := wrapper.StartLoRaGateway() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | m.ctx.WithField("DateTime", time.Now()).Info("Concentrator started, packets can now be received and sent") 72 | err = m.handler(runStart) 73 | if shutdownErr := m.shutdown(); shutdownErr != nil { 74 | m.ctx.WithError(shutdownErr).Error("Couldn't stop concentrator gracefully") 75 | } 76 | return err 77 | } 78 | 79 | func (m *Manager) handler(runStart time.Time) (err error) { 80 | // First, we'll handle the case when the user wants to end the program 81 | c := make(chan os.Signal) 82 | defer close(c) 83 | signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGABRT) 84 | 85 | // We'll start the routines, and attach them a context 86 | bgCtx, cancel := context.WithCancel(context.Background()) 87 | defer cancel() 88 | routinesErr := m.startRoutines(bgCtx, runStart) 89 | defer close(routinesErr) 90 | 91 | // Finally, we'll listen to the different issues 92 | select { 93 | case sig := <-c: 94 | m.ctx.WithField("Signal", sig.String()).Info("Stopping packet forwarder") 95 | case err = <-routinesErr: 96 | m.ctx.Error("Program ended after one of the network links failed") 97 | } 98 | 99 | return err 100 | } 101 | 102 | func (m *Manager) findConcentratorBootTime(packets []wrapper.Packet, runStart time.Time) error { 103 | currentTime := time.Now() 104 | highestTimestamp := uint32(0) 105 | for _, p := range packets { 106 | if p.CountUS > highestTimestamp { 107 | highestTimestamp = p.CountUS 108 | } 109 | } 110 | if highestTimestamp == 0 { 111 | return nil 112 | } 113 | 114 | // Estimated boot time: highest timestamp (closest to current time) substracted to the current time 115 | highestTimestampDuration := time.Duration(highestTimestamp) * time.Microsecond 116 | bootTime := currentTime.Add(-highestTimestampDuration) 117 | if runStart.After(bootTime) || bootTime.After(time.Now()) { 118 | // Absurd timestamp 119 | return errors.New("Absurd uptime received by concentrator") 120 | } 121 | m.ctx.WithField("BootTime", bootTime).Info("Determined concentrator boot time") 122 | m.setBootTime(bootTime) 123 | return nil 124 | } 125 | 126 | func (m *Manager) setBootTime(bootTime time.Time) { 127 | m.bootTimeSetters.SetBootTime(bootTime) 128 | m.foundBootTime = true 129 | m.uplinkPollingRate = stableUplinkPollingRate 130 | } 131 | 132 | func (m *Manager) uplinkRoutine(bgCtx context.Context, runStart time.Time) chan error { 133 | errC := make(chan error) 134 | go func() { 135 | m.ctx.Info("Waiting for uplink packets") 136 | defer close(errC) 137 | for { 138 | packets, err := wrapper.Receive() 139 | if err != nil { 140 | errC <- errors.Wrap(err, "Uplink packets retrieval error") 141 | return 142 | } 143 | if len(packets) == 0 { // Empty payload => we sleep, then reiterate. 144 | time.Sleep(m.uplinkPollingRate) 145 | continue 146 | } 147 | 148 | m.ctx.WithField("NbPackets", len(packets)).Info("Received uplink packets") 149 | if !m.foundBootTime { 150 | // First packets received => find concentrator boot time 151 | err = m.findConcentratorBootTime(packets, runStart) 152 | if err != nil { 153 | m.ctx.WithError(err).Warn("Error when computing concentrator boot time - using packet forwarder run start time") 154 | m.setBootTime(runStart) 155 | } 156 | } 157 | 158 | validPackets := wrapUplinkPayload(m.ctx, packets, m.ignoreCRC, m.netClient.GatewayID()) 159 | m.statusMgr.HandledRXBatch(len(validPackets), len(packets)) 160 | if len(validPackets) == 0 { 161 | // Packets received, but with invalid CRC - ignoring 162 | time.Sleep(m.uplinkPollingRate) 163 | continue 164 | } 165 | 166 | m.ctx.WithField("NbValidPackets", len(validPackets)).Info("Sending valid uplink packets") 167 | m.netClient.SendUplinks(validPackets) 168 | 169 | select { 170 | case <-bgCtx.Done(): 171 | errC <- nil 172 | return 173 | default: 174 | continue 175 | } 176 | } 177 | }() 178 | return errC 179 | } 180 | 181 | func (m *Manager) gpsRoutine(bgCtx context.Context) chan error { 182 | errC := make(chan error) 183 | go func() { 184 | m.ctx.Info("Starting GPS update routine") 185 | defer close(errC) 186 | for { 187 | select { 188 | case <-bgCtx.Done(): 189 | return 190 | default: 191 | // The GPS time reference and coordinates are updated at `gpsUpdateRate` 192 | err := wrapper.UpdateGPSData(m.ctx) 193 | if err != nil { 194 | errC <- errors.Wrap(err, "GPS update error") 195 | } 196 | } 197 | } 198 | }() 199 | return errC 200 | } 201 | 202 | func (m *Manager) downlinkRoutine(bgCtx context.Context) { 203 | m.ctx.Info("Waiting for downlink messages") 204 | downlinkQueue := m.netClient.Downlinks() 205 | dManager := NewDownlinkManager(bgCtx, m.ctx, m.conf, m.statusMgr, m.downlinksSendMargin) 206 | m.bootTimeSetters.Add(dManager) 207 | for { 208 | select { 209 | case downlink := <-downlinkQueue: 210 | m.ctx.Info("Scheduling newly-received downlink packet") 211 | m.statusMgr.ReceivedTX() 212 | dManager.ScheduleDownlink(downlink) 213 | case <-bgCtx.Done(): 214 | return 215 | } 216 | } 217 | } 218 | 219 | func (m *Manager) statusRoutine(bgCtx context.Context) chan error { 220 | errC := make(chan error) 221 | go func() { 222 | defer close(errC) 223 | for { 224 | select { 225 | case <-time.After(statusRoutineSleepRate): 226 | rtt, err := m.netClient.Ping() 227 | m.ctx.WithField("RTT", rtt).Debug("Ping to the router successful") 228 | if err != nil { 229 | errC <- errors.Wrap(err, "Network server health check error") 230 | return 231 | } 232 | 233 | status, err := m.statusMgr.GenerateStatus(rtt) 234 | if err != nil { 235 | errC <- errors.Wrap(err, "Gateway status computation error") 236 | return 237 | } 238 | 239 | err = m.netClient.SendStatus(*status) 240 | if err != nil { 241 | errC <- errors.Wrap(err, "Gateway status transmission error") 242 | return 243 | } 244 | case <-bgCtx.Done(): 245 | return 246 | } 247 | } 248 | }() 249 | return errC 250 | } 251 | 252 | func (m *Manager) networkRoutine(bgCtx context.Context) chan error { 253 | errC := make(chan error) 254 | go func() { 255 | defer close(errC) 256 | if err := m.netClient.RefreshRoutine(bgCtx); err != nil { 257 | errC <- errors.Wrap(err, "Couldn't refresh account server token") 258 | } 259 | }() 260 | return errC 261 | } 262 | 263 | func (m *Manager) startRoutines(bgCtx context.Context, runTime time.Time) chan error { 264 | err := make(chan error) 265 | go func() { 266 | upCtx, upCancel := context.WithCancel(bgCtx) 267 | downCtx, downCancel := context.WithCancel(bgCtx) 268 | statusCtx, statusCancel := context.WithCancel(bgCtx) 269 | gpsCtx, gpsCancel := context.WithCancel(bgCtx) 270 | networkCtx, networkCancel := context.WithCancel(bgCtx) 271 | 272 | go m.downlinkRoutine(downCtx) 273 | uplinkErrors := m.uplinkRoutine(upCtx, runTime) 274 | statusErrors := m.statusRoutine(statusCtx) 275 | networkErrors := m.networkRoutine(networkCtx) 276 | var gpsErrors chan error 277 | if m.isGPS { 278 | gpsErrors = m.gpsRoutine(gpsCtx) 279 | } 280 | select { 281 | case uplinkError := <-uplinkErrors: 282 | err <- errors.Wrap(uplinkError, "Uplink routine error") 283 | case statusError := <-statusErrors: 284 | err <- errors.Wrap(statusError, "Status routine error") 285 | case networkError := <-networkErrors: 286 | err <- errors.Wrap(networkError, "Network routine error") 287 | case gpsError := <-gpsErrors: 288 | err <- errors.Wrap(gpsError, "GPS routine error") 289 | case <-bgCtx.Done(): 290 | err <- nil 291 | } 292 | upCancel() 293 | gpsCancel() 294 | downCancel() 295 | statusCancel() 296 | networkCancel() 297 | }() 298 | return err 299 | } 300 | 301 | func (m *Manager) shutdown() error { 302 | m.netClient.Stop() 303 | return stopGateway(m.ctx) 304 | } 305 | 306 | func stopGateway(ctx log.Interface) error { 307 | err := wrapper.StopLoRaGateway() 308 | if err != nil { 309 | return err 310 | } 311 | 312 | ctx.Info("Concentrator stopped gracefully") 313 | return nil 314 | } 315 | -------------------------------------------------------------------------------- /pktfwd/network.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "math" 9 | "sync" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/go-account-lib/account" 13 | "github.com/TheThingsNetwork/go-utils/log" 14 | "github.com/TheThingsNetwork/ttn/api/discovery" 15 | "github.com/TheThingsNetwork/ttn/api/fields" 16 | "github.com/TheThingsNetwork/ttn/api/gateway" 17 | "github.com/TheThingsNetwork/ttn/api/health" 18 | "github.com/TheThingsNetwork/ttn/api/router" 19 | "github.com/pkg/errors" 20 | "google.golang.org/grpc" 21 | ) 22 | 23 | const ( 24 | tokenRefreshMargin = -2 * time.Minute 25 | uplinksBufferSize = 32 26 | ) 27 | 28 | type TTNConfig struct { 29 | ID string 30 | Key string 31 | AuthServer string 32 | DiscoveryServer string 33 | Router string 34 | Version string 35 | GatewayDescription string 36 | DownlinksSendMargin time.Duration 37 | IgnoreCRC bool 38 | } 39 | 40 | type TTNClient struct { 41 | antennaLocation *account.AntennaLocation 42 | routerConn *grpc.ClientConn 43 | ctx log.Interface 44 | uplinkStream router.UplinkStream 45 | uplinkMutex sync.Mutex 46 | downlinkStream router.DownlinkStream 47 | statusStream router.GatewayStatusStream 48 | account *account.Account 49 | runConfig TTNConfig 50 | connected bool 51 | networkMutex *sync.Mutex 52 | streamsMutex *sync.Mutex 53 | token string 54 | tokenExpiry time.Time 55 | frequencyPlan string 56 | // Communication between internal goroutines 57 | stopDownlinkQueue chan bool 58 | stopUplinkQueue chan bool 59 | stopMainRouterReconnection chan bool 60 | downlinkStreamChange chan bool 61 | downlinkQueue chan *router.DownlinkMessage 62 | uplinkQueue chan *router.UplinkMessage 63 | routerChanges chan func(c *TTNClient) error 64 | } 65 | 66 | type NetworkClient interface { 67 | SendStatus(status gateway.Status) error 68 | SendUplinks(messages []router.UplinkMessage) 69 | FrequencyPlan() string 70 | Downlinks() <-chan *router.DownlinkMessage 71 | GatewayID() string 72 | Ping() (time.Duration, error) 73 | DefaultLocation() *account.AntennaLocation 74 | Stop() 75 | RefreshRoutine(ctx context.Context) error 76 | } 77 | 78 | func (c *TTNClient) GatewayID() string { 79 | return c.runConfig.ID 80 | } 81 | 82 | type RouterHealthCheck struct { 83 | Conn *grpc.ClientConn 84 | Duration time.Duration 85 | Err error 86 | } 87 | 88 | func connectionHealthCheck(conn *grpc.ClientConn) (time.Duration, error) { 89 | timeBefore := time.Now() 90 | ok, err := health.Check(conn) 91 | if !ok { 92 | err = errors.New("Health check with the router failed") 93 | } 94 | return time.Now().Sub(timeBefore), err 95 | } 96 | 97 | func connectToRouter(ctx log.Interface, discoveryClient discovery.Client, router string) (*grpc.ClientConn, error) { 98 | routerAccess, err := discoveryClient.Get("router", router) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | var announcement = *routerAccess 104 | 105 | ctx.WithField("RouterID", router).Info("Connecting to router...") 106 | return announcement.Dial() 107 | } 108 | 109 | func (c *TTNClient) DefaultLocation() *account.AntennaLocation { 110 | return c.antennaLocation 111 | } 112 | 113 | func (c *TTNClient) FrequencyPlan() string { 114 | return c.frequencyPlan 115 | } 116 | 117 | func reconnectionDelay(tries uint) time.Duration { 118 | return time.Duration(math.Exp(float64(tries)/2.0)) * time.Second 119 | } 120 | 121 | func (c *TTNClient) tryMainRouterReconnection(gw account.Gateway, discoveryClient discovery.Client) { 122 | tries := uint(0) 123 | for { 124 | select { 125 | case <-c.stopMainRouterReconnection: 126 | return 127 | case <-time.After(reconnectionDelay(tries)): 128 | break 129 | } 130 | c.ctx.Info("Trying to reconnect to main router") 131 | routerConn, err := connectToRouter(c.ctx, discoveryClient, gw.Router.ID) 132 | if err != nil { 133 | c.ctx.WithError(err).Warn("Couldn't connect to the main router") 134 | tries = tries + 1 135 | continue 136 | } 137 | 138 | c.routerChanges <- func(t *TTNClient) error { 139 | t.routerConn = routerConn 140 | return nil 141 | } 142 | c.ctx.Info("Connection to main router successful") 143 | break 144 | } 145 | } 146 | 147 | func (c *TTNClient) Ping() (time.Duration, error) { 148 | c.networkMutex.Lock() 149 | defer c.networkMutex.Unlock() 150 | t, err := connectionHealthCheck(c.routerConn) 151 | return t, err 152 | } 153 | 154 | func (c *TTNClient) getLowestLatencyRouter(discoveryClient discovery.Client, fallbackRouters []account.GatewayRouter) (*grpc.ClientConn, error) { 155 | routerAnnouncements := make([]*discovery.Announcement, 0) 156 | for _, router := range fallbackRouters { 157 | routerAnnouncement, err := discoveryClient.Get("router", router.ID) 158 | if err != nil { 159 | continue 160 | } 161 | routerAnnouncements = append(routerAnnouncements, routerAnnouncement) 162 | } 163 | return c.getLowestLatencyRouterFromAnnouncements(discoveryClient, routerAnnouncements) 164 | } 165 | 166 | func (c *TTNClient) getLowestLatencyRouterFromAnnouncements(discoveryClient discovery.Client, routerAnnouncements []*discovery.Announcement) (*grpc.ClientConn, error) { 167 | var routerConn *grpc.ClientConn 168 | routerHealthChannel := make(chan RouterHealthCheck) 169 | for _, routerAnnouncement := range routerAnnouncements { 170 | announcement := routerAnnouncement 171 | go func() { 172 | conn, err := announcement.Dial() 173 | if err != nil { 174 | routerHealthChannel <- RouterHealthCheck{Err: err} 175 | return 176 | } 177 | duration, err := connectionHealthCheck(conn) 178 | routerHealthChannel <- RouterHealthCheck{ 179 | Err: err, 180 | Duration: duration, 181 | Conn: conn, 182 | } 183 | }() 184 | } 185 | 186 | lowestPing := time.Duration(math.MaxInt64) 187 | routersChecked := 0 188 | for routerHealth := range routerHealthChannel { 189 | if routerHealth.Err == nil && routerHealth.Duration < lowestPing { 190 | if routerConn != nil { 191 | routerConn.Close() 192 | } 193 | routerConn = routerHealth.Conn 194 | } 195 | routersChecked++ 196 | if routersChecked == len(routerAnnouncements) { 197 | break 198 | } 199 | } 200 | if routerConn == nil { 201 | return nil, errors.New("Packet forwarder couldn't establish a healthy connection with any router") 202 | } 203 | c.ctx.Info("Identified the lowest latency router") 204 | return routerConn, nil 205 | } 206 | 207 | func (c *TTNClient) getRouterClient(ctx log.Interface) error { 208 | ctx.WithField("Address", c.runConfig.DiscoveryServer).Info("Connecting to TTN discovery server") 209 | discoveryClient, err := discovery.NewClient(c.runConfig.DiscoveryServer, &discovery.Announcement{ 210 | ServiceName: "ttn-packet-forwarder", 211 | ServiceVersion: c.runConfig.Version, 212 | Id: c.runConfig.ID, 213 | }, func() string { return "" }) 214 | if err != nil { 215 | return err 216 | } 217 | ctx.Info("Connected to discovery server - getting router address") 218 | 219 | defer discoveryClient.Close() 220 | 221 | var routerConn *grpc.ClientConn 222 | if c.runConfig.Router == "" { 223 | gw, err := c.account.FindGateway(c.GatewayID()) 224 | if err != nil { 225 | return errors.Wrap(err, "Couldn't fetch the gateway information from the account server") 226 | } 227 | 228 | if gw.Router.ID != "" { 229 | routerConn, err = connectToRouter(c.ctx.WithField("RouterID", gw.Router.ID), discoveryClient, gw.Router.ID) 230 | } 231 | if gw.Router.ID == "" || err != nil { 232 | if err != nil { 233 | ctx.WithError(err).WithField("RouterID", gw.Router.ID).Warn("Couldn't connect to main router - trying to connect to fallback routers") 234 | } 235 | fallbackRouters := gw.FallbackRouters 236 | if len(fallbackRouters) == 0 { 237 | ctx.Warn("No fallback routers in memory for this gateway - loading all routers") 238 | routers, err := discoveryClient.GetAll("router") 239 | if err != nil { 240 | ctx.WithError(err).Error("Couldn't retrieve routers") 241 | return err 242 | } 243 | routerConn, err = c.getLowestLatencyRouterFromAnnouncements(discoveryClient, routers) 244 | if err != nil { 245 | return errors.Wrap(err, "Couldn't figure out the lowest latency router") 246 | } 247 | } else { 248 | routerConn, err = c.getLowestLatencyRouter(discoveryClient, fallbackRouters) 249 | if err != nil { 250 | return errors.Wrap(err, "Couldn't figure out the lowest latency router") 251 | } 252 | } 253 | defer func() { 254 | // Wait for the function to be finished, to protect `c.routerConn` 255 | go c.tryMainRouterReconnection(gw, discoveryClient) 256 | }() 257 | } 258 | } else { 259 | routerConn, err = connectToRouter(ctx, discoveryClient, c.runConfig.Router) 260 | if err != nil { 261 | return errors.Wrap(err, "Couldn't connect to user-specified router") 262 | } 263 | ctx.Info("Connected to router") 264 | } 265 | 266 | c.routerConn = routerConn 267 | return nil 268 | } 269 | 270 | func (c *TTNClient) Downlinks() <-chan *router.DownlinkMessage { 271 | return c.downlinkQueue 272 | } 273 | 274 | func (c *TTNClient) queueUplinks() { 275 | for { 276 | select { 277 | case <-c.stopUplinkQueue: 278 | c.ctx.Info("Closing uplinks queue") 279 | close(c.uplinkQueue) 280 | return 281 | case uplink := <-c.uplinkQueue: 282 | ctx := c.ctx.WithFields(fields.Get(uplink)) 283 | if err := c.uplinkStream.Send(uplink); err != nil { 284 | ctx.WithError(err).Warn("Uplink message transmission to the back-end failed.") 285 | } else { 286 | ctx.Info("Uplink message transmission successful.") 287 | } 288 | } 289 | } 290 | } 291 | 292 | func (c *TTNClient) queueDownlinks() { 293 | c.ctx.Info("Downlinks queuing routine started") 294 | c.streamsMutex.Lock() 295 | downlinkStreamChannel := c.downlinkStream.Channel() 296 | c.streamsMutex.Unlock() 297 | for { 298 | select { 299 | case <-c.stopDownlinkQueue: 300 | c.ctx.Info("Closing downlinks queue") 301 | close(c.downlinkQueue) 302 | return 303 | case downlink := <-downlinkStreamChannel: 304 | c.ctx.Info("Received downlink packet") 305 | c.downlinkQueue <- downlink 306 | case <-c.downlinkStreamChange: 307 | c.streamsMutex.Lock() 308 | downlinkStreamChannel = c.downlinkStream.Channel() 309 | c.streamsMutex.Unlock() 310 | } 311 | } 312 | } 313 | 314 | func (c *TTNClient) fetchAccountServerInfo() error { 315 | c.account = account.NewWithKey(c.runConfig.AuthServer, c.runConfig.Key) 316 | gw, err := c.account.FindGateway(c.runConfig.ID) 317 | if err != nil { 318 | return errors.Wrap(err, "Account server error") 319 | } 320 | c.antennaLocation = gw.AntennaLocation 321 | c.token = gw.Token.AccessToken 322 | c.tokenExpiry = gw.Token.Expiry 323 | c.frequencyPlan = gw.FrequencyPlan 324 | c.ctx.WithField("TokenExpiry", c.tokenExpiry).Info("Refreshed account server information") 325 | return nil 326 | } 327 | 328 | func (c *TTNClient) RefreshRoutine(ctx context.Context) error { 329 | for { 330 | refreshTime := c.tokenExpiry.Add(tokenRefreshMargin) 331 | c.ctx.Debugf("Preparing to update network clients at %v", refreshTime) 332 | select { 333 | case <-time.After(refreshTime.Sub(time.Now())): 334 | c.routerChanges <- func(t *TTNClient) error { 335 | if err := t.fetchAccountServerInfo(); err != nil { 336 | return errors.Wrap(err, "Couldn't update account server info") 337 | } 338 | return nil 339 | } 340 | c.ctx.Debug("Refreshed network connection") 341 | case <-ctx.Done(): 342 | return nil 343 | } 344 | } 345 | } 346 | 347 | func CreateNetworkClient(ctx log.Interface, ttnConfig TTNConfig) (NetworkClient, error) { 348 | var client = &TTNClient{ 349 | ctx: ctx, 350 | runConfig: ttnConfig, 351 | downlinkQueue: make(chan *router.DownlinkMessage), 352 | uplinkQueue: make(chan *router.UplinkMessage, uplinksBufferSize), 353 | networkMutex: &sync.Mutex{}, 354 | streamsMutex: &sync.Mutex{}, 355 | stopDownlinkQueue: make(chan bool), 356 | stopUplinkQueue: make(chan bool), 357 | downlinkStreamChange: make(chan bool), 358 | routerChanges: make(chan func(c *TTNClient) error), 359 | } 360 | 361 | client.networkMutex.Lock() 362 | defer client.networkMutex.Unlock() 363 | 364 | // Get the first token 365 | err := client.fetchAccountServerInfo() 366 | if err != nil { 367 | return nil, err 368 | } 369 | 370 | // Updating with the initial RouterConn 371 | err = client.getRouterClient(ctx) 372 | if err != nil { 373 | return nil, err 374 | } 375 | 376 | client.connectToStreams(router.NewRouterClientForGateway(router.NewRouterClient(client.routerConn), client.runConfig.ID, client.token)) 377 | 378 | go client.watchRouterChanges() 379 | 380 | go client.queueDownlinks() 381 | go client.queueUplinks() 382 | 383 | return client, nil 384 | } 385 | 386 | func (c *TTNClient) watchRouterChanges() { 387 | for { 388 | select { 389 | case routerChange := <-c.routerChanges: 390 | if routerChange == nil { // Channel closed, shutting network client down 391 | return 392 | } 393 | c.networkMutex.Lock() 394 | if err := routerChange(c); err != nil { 395 | c.ctx.WithError(err).Warn("Couldn't operate network client change") 396 | } else { 397 | c.connectToStreams(router.NewRouterClientForGateway(router.NewRouterClient(c.routerConn), c.runConfig.ID, c.token)) 398 | c.downlinkStreamChange <- true 399 | } 400 | c.networkMutex.Unlock() 401 | } 402 | } 403 | } 404 | 405 | func (c *TTNClient) connectToStreams(routerClient router.RouterClientForGateway) { 406 | c.streamsMutex.Lock() 407 | defer c.streamsMutex.Unlock() 408 | if c.connected { 409 | c.disconnectOfStreams() 410 | } 411 | c.uplinkStream = router.NewMonitoredUplinkStream(routerClient) 412 | c.downlinkStream = router.NewMonitoredDownlinkStream(routerClient) 413 | c.statusStream = router.NewMonitoredGatewayStatusStream(routerClient) 414 | c.connected = true 415 | } 416 | 417 | func (c *TTNClient) disconnectOfStreams() { 418 | c.uplinkStream.Close() 419 | c.downlinkStream.Close() 420 | c.statusStream.Close() 421 | c.connected = false 422 | } 423 | 424 | func (c *TTNClient) SendUplinks(messages []router.UplinkMessage) { 425 | for _, message := range messages { 426 | c.uplinkQueue <- &message 427 | } 428 | } 429 | 430 | func (c *TTNClient) SendStatus(status gateway.Status) error { 431 | var uptimeString string 432 | status.Region = c.frequencyPlan 433 | uptimeDuration, err := time.ParseDuration(fmt.Sprintf("%dus", status.GetTimestamp())) 434 | if err == nil { 435 | uptimeString = uptimeDuration.String() 436 | } else { 437 | uptimeString = fmt.Sprintf("%fs", float32(status.GetTimestamp())/1000000.0) 438 | } 439 | c.ctx.WithFields(log.Fields{ 440 | "TXPacketsReceived": status.GetTxIn(), 441 | "TXPacketsValid": status.GetTxOk(), 442 | "RXPacketsReceived": status.GetRxIn(), 443 | "RXPacketsValid": status.GetRxOk(), 444 | "FrequencyPlan": status.GetRegion(), 445 | "Uptime": uptimeString, 446 | "Load1": status.GetOs().GetLoad_1(), 447 | "Load5": status.GetOs().GetLoad_5(), 448 | "Load15": status.GetOs().GetLoad_15(), 449 | "CpuPercentage": status.GetOs().GetCpuPercentage(), 450 | "MemoryPercentage": status.GetOs().GetMemoryPercentage(), 451 | "Latitude": status.GetGps().GetLatitude(), 452 | "Longitude": status.GetGps().GetLongitude(), 453 | "Altitude": status.GetGps().GetAltitude(), 454 | "RTT": status.GetRtt(), 455 | }).Info("Sending status to the network server") 456 | err = c.statusStream.Send(&status) 457 | if err != nil { 458 | return errors.Wrap(err, "Status stream error") 459 | } 460 | return nil 461 | } 462 | 463 | // Stop a running network client 464 | func (c *TTNClient) Stop() { 465 | c.stopDownlinkQueue <- true 466 | c.stopUplinkQueue <- true 467 | select { 468 | case c.stopMainRouterReconnection <- true: 469 | break 470 | default: 471 | break 472 | } 473 | close(c.routerChanges) 474 | c.streamsMutex.Lock() 475 | defer c.streamsMutex.Unlock() 476 | c.disconnectOfStreams() 477 | } 478 | -------------------------------------------------------------------------------- /pktfwd/run.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/go-utils/log" 7 | "github.com/TheThingsNetwork/packet_forwarder/util" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Init initiates the configuration, the network connection, and handles the manager 12 | func Run(ctx log.Interface, conf util.Config, ttnConfig TTNConfig, gpsPath string) error { 13 | networkCli, err := CreateNetworkClient(ctx, ttnConfig) 14 | if err != nil { 15 | return errors.Wrap(err, "Network configuration failure") 16 | } 17 | 18 | // applying configuration to the board 19 | if err := configureBoard(ctx, conf, gpsPath); err != nil { 20 | return errors.Wrap(err, "Board configuration failure") 21 | } 22 | 23 | // Creating manager 24 | var mgr = NewManager(ctx, conf, networkCli, gpsPath, ttnConfig) 25 | return mgr.run() 26 | } 27 | -------------------------------------------------------------------------------- /pktfwd/status.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "net" 7 | "runtime" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/TheThingsNetwork/go-account-lib/account" 12 | "github.com/TheThingsNetwork/go-utils/log" 13 | "github.com/TheThingsNetwork/packet_forwarder/util" 14 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 15 | "github.com/TheThingsNetwork/ttn/api/gateway" 16 | "github.com/pkg/errors" 17 | "github.com/shirou/gopsutil/cpu" 18 | "github.com/shirou/gopsutil/load" 19 | "github.com/shirou/gopsutil/mem" 20 | ) 21 | 22 | type StatusManager interface { 23 | BootTimeSetter 24 | HandledRXBatch(received, valid int) 25 | ReceivedTX() 26 | SentTX() 27 | GenerateStatus(rtt time.Duration) (*gateway.Status, error) 28 | } 29 | 30 | func NewStatusManager(ctx log.Interface, frequencyPlan string, gatewayDescription string, isGPSChip bool, antennaLocation *account.AntennaLocation) StatusManager { 31 | if antennaLocation == nil { 32 | ctx.Warn("Antenna location unavailable from the account server") 33 | } 34 | return &statusManager{ 35 | antennaLocation: antennaLocation, 36 | ctx: ctx, 37 | isGPSChip: isGPSChip, 38 | rxIn: 0, 39 | rxOk: 0, 40 | txIn: 0, 41 | txOk: 0, 42 | frequencyPlan: frequencyPlan, 43 | gatewayDescription: gatewayDescription, 44 | } 45 | } 46 | 47 | type statusManager struct { 48 | antennaLocation *account.AntennaLocation 49 | ctx log.Interface 50 | isGPSChip bool 51 | rxIn uint32 52 | rxOk uint32 53 | txIn uint32 54 | txOk uint32 55 | frequencyPlan string 56 | gatewayDescription string 57 | bootTime *time.Time 58 | } 59 | 60 | func (s *statusManager) SetBootTime(t time.Time) { 61 | s.bootTime = &t 62 | } 63 | 64 | func (s *statusManager) ReceivedTX() { 65 | atomic.AddUint32(&s.txIn, 1) 66 | } 67 | 68 | func (s *statusManager) SentTX() { 69 | atomic.AddUint32(&s.txOk, 1) 70 | } 71 | 72 | func (s *statusManager) HandledRXBatch(received, valid int) { 73 | atomic.AddUint32(&s.rxIn, uint32(received)) 74 | atomic.AddUint32(&s.rxOk, uint32(valid)) 75 | } 76 | 77 | func getOSInfo() *gateway.Status_OSMetrics { 78 | osInfo := &gateway.Status_OSMetrics{} 79 | /* Temperature not yet implemented due to disparities between 80 | platforms (no standard way of getting temperature from a platform 81 | to another: see https://github.com/shirou/gopsutil/issues/329) */ 82 | 83 | stats, err := cpu.Times(false) 84 | if err == nil && len(stats) > 0 { 85 | cpuStat := stats[0] 86 | cpuUsageTime := cpuStat.Total() - cpuStat.Idle 87 | osInfo.CpuPercentage = float32(cpuUsageTime / cpuStat.Total() * 100) 88 | } // CPU stats not available on every platform 89 | 90 | loadInfo, err := load.Avg() 91 | if err == nil { 92 | osInfo.Load_1 = float32(loadInfo.Load1) 93 | osInfo.Load_5 = float32(loadInfo.Load5) 94 | osInfo.Load_15 = float32(loadInfo.Load15) 95 | } 96 | 97 | virtualMemory, err := mem.VirtualMemory() 98 | if err == nil { 99 | osInfo.MemoryPercentage = float32(virtualMemory.UsedPercent) 100 | } 101 | 102 | return osInfo 103 | } 104 | 105 | func (s *statusManager) GenerateStatus(rtt time.Duration) (*gateway.Status, error) { 106 | var concentratorBootTime time.Duration 107 | if s.bootTime == nil { 108 | concentratorBootTime = 0 109 | } else { 110 | concentratorBootTime = time.Now().Sub(*s.bootTime) 111 | } 112 | 113 | osInfo := getOSInfo() 114 | addrs, err := net.InterfaceAddrs() 115 | if err != nil { 116 | return nil, errors.Wrap(err, "Net interfaces obtention error") 117 | } 118 | 119 | ips := make([]string, 0) 120 | for _, a := range addrs { 121 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 122 | if ipnet.IP.To4() != nil { 123 | ips = append(ips, ipnet.IP.String()) 124 | } 125 | } 126 | } 127 | 128 | status := &gateway.Status{ 129 | Timestamp: uint32(util.TXTimestampFromDuration(concentratorBootTime)), 130 | Time: time.Now().UnixNano(), 131 | GatewayTrusted: true, 132 | Region: s.frequencyPlan, 133 | Ip: ips, 134 | Platform: runtime.GOOS, 135 | // Contact-email: TODO once it has been implemented on the account server 136 | ContactEmail: "", 137 | Description: s.gatewayDescription, 138 | Rtt: uint32(rtt.Nanoseconds() / 1000000), 139 | RxIn: atomic.LoadUint32(&s.rxIn), 140 | RxOk: atomic.LoadUint32(&s.rxOk), 141 | TxIn: atomic.LoadUint32(&s.txIn), 142 | TxOk: atomic.LoadUint32(&s.txOk), 143 | Os: osInfo, 144 | } 145 | 146 | coordinates := new(gateway.GPSMetadata) 147 | if s.antennaLocation != nil { // Antenna location accessible from the account server 148 | if s.antennaLocation.Latitude != nil { 149 | coordinates.Latitude = float32(*s.antennaLocation.Latitude) 150 | } 151 | if s.antennaLocation.Longitude != nil { 152 | coordinates.Longitude = float32(*s.antennaLocation.Longitude) 153 | } 154 | if s.antennaLocation.Altitude != nil { 155 | coordinates.Altitude = int32(*s.antennaLocation.Altitude) 156 | } 157 | } 158 | 159 | if s.isGPSChip { // GPS chip available 160 | gpsChipCoordinates, err := wrapper.GetGPSCoordinates() 161 | if err != nil { 162 | s.ctx.WithError(err).Warn("Unable to retrieve GPS coordinates from the GPS hardware") 163 | } else { 164 | coordinates = &gateway.GPSMetadata{ 165 | Latitude: float32(gpsChipCoordinates.Latitude), 166 | Longitude: float32(gpsChipCoordinates.Longitude), 167 | Altitude: int32(gpsChipCoordinates.Altitude), 168 | } 169 | } 170 | } 171 | 172 | status.Gps = coordinates 173 | 174 | return status, nil 175 | } 176 | -------------------------------------------------------------------------------- /pktfwd/uplinks.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package pktfwd 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/TheThingsNetwork/go-utils/log" 9 | "github.com/TheThingsNetwork/packet_forwarder/wrapper" 10 | "github.com/TheThingsNetwork/ttn/api/gateway" 11 | "github.com/TheThingsNetwork/ttn/api/protocol" 12 | "github.com/TheThingsNetwork/ttn/api/protocol/lorawan" 13 | "github.com/TheThingsNetwork/ttn/api/router" 14 | ) 15 | 16 | func acceptedCRC(p wrapper.Packet) bool { 17 | // XX: Should retrieve the CRC configuration from the account server. 18 | if p.Status == wrapper.StatusCRCOK || p.Status == wrapper.StatusNOCRC { 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | func initGatewayMetadata(gatewayID string, packet wrapper.Packet) gateway.RxMetadata { 25 | var gateway = gateway.RxMetadata{ 26 | GatewayId: gatewayID, 27 | RfChain: uint32(packet.RFChain), 28 | Channel: uint32(packet.IFChain), 29 | Frequency: uint64(packet.Freq), 30 | Rssi: packet.RSSI, 31 | Snr: packet.SNR, 32 | Timestamp: packet.CountUS, 33 | Time: packet.Time, 34 | Gps: packet.Gps, 35 | } 36 | return gateway 37 | } 38 | 39 | func newLoRaMetadata(packet wrapper.Packet) (lorawan.Metadata, error) { 40 | var datarate, bandwidth, coderate string 41 | var err error 42 | 43 | var p = lorawan.Metadata{ 44 | Modulation: lorawan.Modulation_LORA, 45 | } 46 | 47 | if datarate, err = packet.DatarateString(); err != nil { 48 | return p, err 49 | } 50 | if bandwidth, err = packet.BandwidthString(); err != nil { 51 | return p, err 52 | } 53 | p.DataRate = datarate + bandwidth 54 | 55 | if coderate, err = packet.CoderateString(); err != nil { 56 | return p, err 57 | } 58 | p.CodingRate = coderate 59 | 60 | return p, nil 61 | } 62 | 63 | func newFSKMetadata(packet wrapper.Packet) lorawan.Metadata { 64 | var p = lorawan.Metadata{ 65 | Modulation: lorawan.Modulation_FSK, 66 | } 67 | p.BitRate = packet.Datarate 68 | return p 69 | } 70 | 71 | func initLoRaData(packet wrapper.Packet) (lorawan.Metadata, error) { 72 | var loRaData lorawan.Metadata 73 | if packet.Modulation == wrapper.ModulationLoRa { 74 | var err error 75 | loRaData, err = newLoRaMetadata(packet) 76 | if err != nil { 77 | return loRaData, err 78 | } 79 | } else if packet.Modulation == wrapper.ModulationFSK { 80 | loRaData = newFSKMetadata(packet) 81 | } else { 82 | return loRaData, fmt.Errorf("Received packet with unknown modulation code: %v", packet.Modulation) 83 | } 84 | 85 | return loRaData, nil 86 | } 87 | 88 | func createUplinkMessage(gatewayID string, packet wrapper.Packet) (router.UplinkMessage, error) { 89 | var uplink router.UplinkMessage 90 | 91 | gateway := initGatewayMetadata(gatewayID, packet) 92 | loraData, err := initLoRaData(packet) 93 | if err != nil { 94 | return uplink, err 95 | } 96 | var data = protocol.RxMetadata{ 97 | Protocol: &protocol.RxMetadata_Lorawan{ 98 | Lorawan: &loraData, 99 | }, 100 | } 101 | 102 | uplink = router.UplinkMessage{ 103 | ProtocolMetadata: &data, 104 | GatewayMetadata: &gateway, 105 | Payload: packet.Payload, 106 | } 107 | 108 | return uplink, nil 109 | } 110 | 111 | func wrapUplinkPayload(ctx log.Interface, packets []wrapper.Packet, ignoreCRC bool, gatewayID string) []router.UplinkMessage { 112 | var messages = make([]router.UplinkMessage, 0, wrapper.NbMaxPackets) 113 | // Iterating through every packet: 114 | for _, inspectedPacket := range packets { 115 | // First, we'll check the CRC is conform to the packets the gateway is configured to transmit 116 | if !ignoreCRC && !acceptedCRC(inspectedPacket) { 117 | ctx.Warn("Uplink packet received with an invalid CRC - ignoring") 118 | continue 119 | } 120 | 121 | // Creating and filling the uplink message 122 | message, err := createUplinkMessage(gatewayID, inspectedPacket) 123 | if err != nil { 124 | ctx.WithError(err).Error("Couldn't wrap uplink message to the TTN format") 125 | continue 126 | } 127 | messages = append(messages, message) 128 | } 129 | 130 | return messages 131 | } 132 | -------------------------------------------------------------------------------- /scripts/kerlink/build-kerlink.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ ! $(go env GOOS) == "linux" ]] ; then 4 | echo "$0: wrong os: Building a Kerlink IoT Station requires a Linux environment. Please retry on another environment." 5 | exit 1 6 | fi 7 | 8 | TOOLCHAIN_PATH="$1" 9 | 10 | usage_str="usage: build-kerlink.sh [path to the Kerlink toolchain] 11 | The Kerlink toolchain for the appropriate firmware can be downloaded on the Kerlink Wiki, at 12 | http://wikikerlink.fr/lora-station/doku.php?id=wiki:ressources 13 | at the \"Tools\" section. 14 | 15 | example: ./build-kerlink.sh /opt/arm-2011.03-wirgrid" 16 | 17 | if [[ -z "$TOOLCHAIN_PATH" ]] ; then 18 | echo "$0: $usage_str" 19 | exit 1 20 | fi 21 | 22 | pushd "$GOPATH/src/github.com/TheThingsNetwork/packet_forwarder" 23 | 24 | export CROSS_COMPILE=arm-none-linux-gnueabi- 25 | export CC=arm-none-linux-gnueabi-gcc 26 | export GOARM=5 27 | export GOOS=linux 28 | export GOARCH=arm 29 | export PLATFORM=kerlink 30 | export GPS_PATH="/dev/nmea" 31 | 32 | export PATH="$PATH:$TOOLCHAIN_PATH/bin" 33 | 34 | make dev-deps 35 | make deps 36 | make build 37 | if ! make build ; then 38 | echo "$0: Build of the packet forwarder has failed. Make sure the toolchain is available, and that you have installed the appropriate dependencies (with \`make dev-deps\` and \`make deps\`)." 39 | exit 1 40 | fi 41 | 42 | echo "$0: Build complete and available in $PWD/release." 43 | 44 | popd 45 | -------------------------------------------------------------------------------- /scripts/kerlink/create-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # What this script does: 4 | # - Downloads Let's Encrypt cert 5 | # - Creates a configuration file 6 | # - Generates a script and a readme file 7 | # - Packages everything with the kerlink build for DOTA install 8 | # Possible improvements: 9 | # - Custom certificate address 10 | # - TTN-hosted static url for produsb.sh and the Let's Encrypt cert 11 | 12 | RED='\033[0;31m' 13 | NC='\033[0m' # No Color 14 | 15 | usage_str="usage: create-package.sh [path to the Kerlink build] ([Gateway ID] [Gateway key]) 16 | 17 | example: ./create-package.sh packet-forwarder-kerlink" 18 | 19 | if [[ -z "$(which tar)" ]] ; then 20 | echo "$0: tar required to run this script." 21 | exit 1 22 | fi 23 | 24 | # Getting path to the kerlink binary 25 | BINARY_PATH="$1" 26 | if [[ -z "$BINARY_PATH" ]] ; then 27 | echo "$0: $usage_str" &> "$OUTPUT" 28 | exit 1 29 | fi 30 | 31 | if [[ $(which openssl) =~ "not found" ]] ; then 32 | random_string="-$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)" 33 | else 34 | random_string="-$(openssl rand -base64 15)" 35 | random_string="${random_string//\/}" 36 | fi 37 | 38 | gatewayID="$2" 39 | gatewayKey="$3" 40 | if [[ ! -z "$4" ]] ; then 41 | OUTPUT="/dev/null" 42 | else 43 | OUTPUT="/dev/stdout" 44 | fi 45 | 46 | WORKDIR="/tmp/packet-forwarder-kerlink$random_string" 47 | BASE="/mnt/fsuser-1/ttn-pkt-fwd" 48 | CFG_FILENAME="config.yml" 49 | PKTFWD_DESTDIR="$WORKDIR$BASE" 50 | 51 | mkdir -p "$PKTFWD_DESTDIR" 52 | 53 | cp "$BINARY_PATH" "$PKTFWD_DESTDIR/ttn-pkt-fwd" 54 | 55 | configure () { 56 | printf "%s: Gateway ID:\n> " "$0" 57 | 58 | read -r gatewayID 59 | 60 | printf "%s: Gateway Key:\n> " "$0" 61 | 62 | read -r -s gatewayKey 63 | } 64 | 65 | if [[ -z "$gatewayID" && -z "$gatewayKey" ]] ; then 66 | echo "$0: If you haven't registered your gateway yet, register it on the console or with \`ttnctl\`, using the gateway connector protocol." 67 | 68 | if [[ -f "$HOME/.pktfwd.yml" ]] ; then 69 | while true; do 70 | read -r -p "$0: Local packet forwarder configuration found (in $HOME/.pktfwd.yml). Do you want to include it in the package? " yn 71 | case $yn in 72 | [Yy]* ) cp "$HOME/.pktfwd.yml" "$PKTFWD_DESTDIR/$CFG_FILENAME"; COPIED_CONFIG="1"; echo "$0: Local configuration included."; break;; 73 | [Nn]* ) echo "$0: Local packet forwarder configuration not copied, please enter the new configuration."; configure "$PKTFWD_DESTDIR/$CFG_FILENAME"; break;; 74 | * ) echo "Please answer [y]es or [n]o.";; 75 | esac 76 | done 77 | else 78 | configure 79 | fi 80 | 81 | echo "$0: Configuration saved - see INSTALL.md if you wish to modify this configuration later" 82 | fi 83 | 84 | if [[ -z "$COPIED_CONFIG" ]] ; then 85 | echo "id: \"${gatewayID}\" 86 | key: \"${gatewayKey}\"" > "$PKTFWD_DESTDIR/$CFG_FILENAME" 87 | fi 88 | 89 | echo "$0: Fetching TLS root certificate" &> "$OUTPUT" 90 | SSL_WORKDIR="$WORKDIR/etc/ssl/certs" 91 | mkdir -p "$SSL_WORKDIR" 92 | pushd "$SSL_WORKDIR" &> /dev/null 93 | wget "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt" &> /dev/null 94 | popd &> /dev/null 95 | 96 | echo "$0: Generating startup script" &> "$OUTPUT" 97 | echo "#!/bin/sh 98 | 99 | BASE=\"$BASE\" 100 | cd \$BASE 101 | killall ttn-pkt-fwd 102 | modem_off.sh 103 | 104 | sleep 3 105 | modem_on.sh 106 | sleep 3 107 | export GOGC=30 108 | ./ttn-pkt-fwd start --config=config.yml" > "$PKTFWD_DESTDIR/ttn-pkt-fwd.sh" 109 | chmod +x "$PKTFWD_DESTDIR/ttn-pkt-fwd.sh" 110 | 111 | echo "$0: Generating DOTA manifest" &> "$OUTPUT" 112 | echo " 113 | 114 | 115 | 116 | 117 | 118 | " > "$PKTFWD_DESTDIR/manifest.xml" 119 | 120 | echo "$0: Startup and init scripts, build and manifests saved. Starting packaging" &> "$OUTPUT" 121 | 122 | release_folder="kerlink-release$random_string" 123 | mkdir "$release_folder" 124 | 125 | DOTA_ARCHIVE="dota_ttn-pkt-fwd$random_string.tar.gz" 126 | pushd "$WORKDIR" &> /dev/null 127 | tar -cvzf "$DOTA_ARCHIVE" "mnt" "etc" &> /dev/null 128 | popd &> /dev/null 129 | mv "$WORKDIR/$DOTA_ARCHIVE" "$release_folder" 130 | 131 | wget "https://cdn.rawgit.com/TheThingsNetwork/kerlink-station-firmware/16f6325e/dota/produsb.zip" &> /dev/null 132 | unzip produsb.zip &> /dev/null # Creates a produsb.sh 133 | mv produsb.sh "$release_folder" 134 | rm produsb.zip 135 | 136 | echo "# Install the TTN Packet Forwarder on a Kerlink IoT Station 137 | 138 | The Kerlink IoT Station build of the TTN packet forwarder is packaged within an archive, also called **DOTA file**. 139 | 140 | ## Method 1: USB stick 141 | 142 | 1. Copy \`$DOTA_ARCHIVE\` on an empty FAT or FAT32-formatted USB stick. 143 | 2. Copy \`produsb.sh\` on the USB stick. 144 | 3. Insert the stick in the Kerlink's USB port. Do not reboot the machine until the DOTA installation is complete! You can see the progress by pushing the \"Test\" button on the Station - as long as MOD1 and MOD2 are blinking, installation is in progress. It should take between 2 and 5 minutes. 145 | 146 | ## Method 2: Network transfer 147 | 148 | 1. Copy \`$DOTA_ARCHIVE\` in the \`/mnt/fsuser-1/dota\` folder on the Station, using \`scp\`. 149 | 2. Reboot the Station with \`reboot\` to trigger the DOTA installation. Do not try to shutdown the machine until the DOTA installation is complete! You can see the progress by pushing the \"Test\" button on the Station - as long as MOD1 and MOD2 are blinking, installation is in progress. It should take between 2 and 5 minutes." > "$release_folder/INSTALL.md" 150 | 151 | rm -rf "$WORKDIR" 152 | 153 | printf "%s: ${RED}Kerlink DOTA package ready.${NC} The package is available in %s/$release_folder. Consult the INSTALL.md file to know how to install the package on your Kerlink IoT Station!\n" "$0" "$PWD" &> "$OUTPUT" 154 | 155 | if [[ ! -z "$4" ]] ; then 156 | printf "%s/%s/%s" "$(pwd)" "$release_folder" "$DOTA_ARCHIVE" 157 | fi 158 | -------------------------------------------------------------------------------- /scripts/multitech/create-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Parts of the script based on installer.sh by Jac Kersing 4 | # 5 | # What this script does: 6 | # - Creates a configuration file 7 | # - Generates a script and a readme file 8 | # - Packages everything with the kerlink build for DOTA install 9 | # Possible improvements: 10 | # - Custom certificate address 11 | # - TTN-hosted static url for produsb.sh and the Let's Encrypt cert 12 | 13 | if [[ -z "$(which tar)" ]] ; then 14 | echo "$0: tar required to run this script." 15 | exit 1 16 | fi 17 | 18 | multitech_installer_file=$(echo "$0" | grep -o '^.*\/')/multitech-installer.sh 19 | if [[ ! -f "$multitech_installer_file" ]] ; then 20 | echo "$0: Can't find multitech-installer.sh at $multitech_installer_file, please check and restart this script." 21 | exit 1 22 | fi 23 | 24 | usage_str="usage: create-kerlink-package.sh [path to the Multitech build] ([Gateway ID] [Gateway key]) 25 | 26 | example: ./create-kerlink-package.sh packet-forwarder-multitech" 27 | 28 | # Getting path to the kerlink binary 29 | INITIAL_BINARY_PATH="$1" 30 | if [[ -z "$INITIAL_BINARY_PATH" ]] ; then 31 | echo "$0: $usage_str" 32 | exit 1 33 | fi 34 | 35 | if [[ $(which openssl) =~ "not found" ]] ; then 36 | random_string="-$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)" 37 | else 38 | random_string="-$(openssl rand -base64 15)" 39 | random_string="${random_string//\/}" 40 | fi 41 | 42 | gatewayID="$2" 43 | gatewayKey="$3" 44 | if [[ ! -z "$4" ]] ; then 45 | OUTPUT="/dev/null" 46 | else 47 | OUTPUT="/dev/stdout" 48 | fi 49 | 50 | echo "$0: Creating file tree" &> "$OUTPUT" 51 | 52 | WORKDIR="/tmp/packet-forwarder-multitech-$random_string" 53 | BASE="/usr/bin" 54 | PKTFWD_DESTDIR="$WORKDIR$BASE" 55 | BINARY_NAME="ttn-pkt-fwd" 56 | 57 | mkdir -p "$PKTFWD_DESTDIR" # /usr/bin 58 | mkdir -p "$WORKDIR/etc/init.d" # /etc/init.d 59 | mkdir -p "$WORKDIR/usr/cfg" 60 | local_config_file="/usr/cfg/config.yml" 61 | config_file="$WORKDIR$local_config_file" 62 | touch "$config_file" 63 | cp "$INITIAL_BINARY_PATH" "$PKTFWD_DESTDIR/$BINARY_NAME" 64 | chmod +x "$PKTFWD_DESTDIR/$BINARY_NAME" 65 | 66 | configure () { 67 | printf "%s: Gateway ID:\n> " "$0" 68 | 69 | read -r gatewayID 70 | 71 | printf "%s: Gateway Key:\n> " "$0" 72 | 73 | read -r -s gatewayKey 74 | } 75 | 76 | if [[ -z "$gatewayID" && -z "$gatewayKey" ]] ; then 77 | echo "$0: If you haven't registered your gateway yet, register it on the console or with \`ttnctl\`, using the gateway connector protocol." &> "$OUTPUT" 78 | 79 | if [[ -f "$HOME/.pktfwd.yml" ]] ; then 80 | while true; do 81 | read -r -p "$0: Local packet forwarder configuration found (in $HOME/.pktfwd.yml). Do you want to include it in the package? " yn 82 | case $yn in 83 | [Yy]* ) cp "$HOME/.pktfwd.yml" "$config_file"; COPIED_CONFIG="1"; echo "$0: Local configuration included." &> "$OUTPUT"; break;; 84 | [Nn]* ) echo "$0: Local packet forwarder configuration not copied, please enter the new configuration." &> "$OUTPUT"; configure "$PKTFWD_DESTDIR/$CFG_FILENAME"; break;; 85 | * ) echo "Please answer [y]es or [n]o." &> "$OUTPUT";; 86 | esac 87 | done 88 | else 89 | configure 90 | fi 91 | 92 | echo "$0: Configuration packaged." &> "$OUTPUT" 93 | fi 94 | 95 | if [[ -z "$COPIED_CONFIG" ]] ; then 96 | echo "id: \"${gatewayID}\" 97 | key: \"${gatewayKey}\"" > "$config_file" 98 | fi 99 | 100 | echo "$0: Generating control file" &> "$OUTPUT" 101 | echo "Package: ttn-pkt-fwd 102 | Version: 2.0.0 103 | Description: TTN Packet Forwarder 104 | Section: console/utils 105 | Priority: optional 106 | Maintainer: The Things Industries 107 | License: MIT 108 | Architecture: arm926ejste 109 | OE: ttn-pkt-fwd 110 | Homepage: https://github.com/TheThingsNetwork/packet_forwarder 111 | Depends: libmpsse (>= 1.3), libc6 (>= 2.19) 112 | Source: git://github.com/TheThingsNetwork/packet_forwarder.git;protocol=git" > "$WORKDIR/control" 113 | 114 | echo "$0: Generating service script" &> "$OUTPUT" 115 | echo "#!/bin/bash 116 | 117 | NAME=\"$BINARY_NAME\" 118 | ENABLED=\"yes\" 119 | 120 | [ -f /etc/default/\$NAME ] && source /etc/default/\$NAME 121 | 122 | run_dir=/var/run/ttn-pkt-fwd 123 | conf_dir=/usr/cfg 124 | pkt_fwd_dir=$BASE 125 | pkt_fwd=\$pkt_fwd_dir/$BINARY_NAME 126 | pkt_fwd_log=/var/log/ttn-pkt-fwd.log 127 | pkt_fwd_pidfile=\$run_dir/ttn-pkt-fwd.pid 128 | 129 | read_card_info() { 130 | # product-id of first lora card 131 | lora_id=\$(mts-io-sysfs show lora/product-id 2> /dev/null) 132 | lora_eui=\$(mts-io-sysfs show lora/eui 2> /dev/null) 133 | # remove all colons 134 | lora_eui_raw=\${lora_eui//:/} 135 | } 136 | 137 | card_found() { 138 | if [ \"\$lora_id\" = \"\$lora_us_id\" ] || [ \"\$lora_id\" = \"\$lora_eu_id\" ]; then 139 | echo \"Found lora card \$lora_id\" 140 | return 1 141 | else 142 | return 0 143 | fi 144 | } 145 | 146 | do_start() { 147 | read_card_info 148 | 149 | if ! card_found; then 150 | echo \"\$0: MTAC-LORA not detected\" 151 | exit 1 152 | fi 153 | 154 | # wait for internet connection to become available 155 | COUNTER=0 156 | while : ; do 157 | ping -c1 google.com > /dev/null 2> /dev/null 158 | if [ \$? -eq 0 ] 159 | then 160 | break 161 | else 162 | if [ \$COUNTER -gt 10 ] ; then 163 | echo \"Couldn't connect to Internet, aborting.\" 164 | exit 1 165 | fi 166 | echo \"No internet connection (\$COUNTER out of 10 tries), waiting...\" 167 | sleep 20 168 | let COUNTER=COUNTER+1 169 | fi 170 | done 171 | 172 | echo -n \"Starting \$NAME: \" 173 | mkdir -p \$run_dir 174 | 175 | start-stop-daemon --start --background --make-pidfile \ 176 | --pidfile \$pkt_fwd_pidfile --exec \$pkt_fwd -- start --config=\$conf_dir/config.yml 177 | echo \"OK\" 178 | } 179 | 180 | do_stop() { 181 | echo -n \"Stopping \$NAME: \" 182 | start-stop-daemon --stop --quiet --oknodo --pidfile \$pkt_fwd_pidfile --retry 5 183 | rm -f \$pkt_fwd_pidfile 184 | echo \"OK\" 185 | } 186 | 187 | if [ \"\$ENABLED\" != \"yes\" ]; then 188 | echo \"\$NAME: disabled in /etc/default\" 189 | exit 190 | fi 191 | 192 | configure() { 193 | multitech-installer.sh 194 | mkdir -p \$conf_dir 195 | if [[ ! -f \"\$conf_dir/config.yml\" ]] ; then 196 | touch \"\$conf_dir/config.yml\" 197 | \$pkt_fwd configure \"\$conf_dir/config.yml\" --config=\"\$conf_dir/config.yml\" 198 | fi 199 | update-rc.d ttn-pkt-fwd defaults 200 | exit 201 | } 202 | 203 | case \"\$1\" in 204 | \"start\") 205 | do_start 206 | ;; 207 | \"stop\") 208 | do_stop 209 | ;; 210 | \"restart\") 211 | ## Stop the service and regardless of whether it was 212 | ## running or not, start it again. 213 | do_stop 214 | do_start 215 | ;; 216 | \"configure\") 217 | ## Configure the service 218 | configure 219 | ;; 220 | *) 221 | ## If no parameters are given, print which are avaiable. 222 | echo \"Usage: \$0 {start|stop|restart|configure}\" 223 | exit 1 224 | ;; 225 | esac" > "$WORKDIR/etc/init.d/$BINARY_NAME" 226 | chmod +x "$WORKDIR/etc/init.d/$BINARY_NAME" 227 | 228 | echo "chmod +x \"$BASE/$BINARY_NAME\" 229 | echo \"********************************************** 230 | YOU NEED TO CONFIGURE YOUR GATEWAY BY EXECUTING /etc/init.d/$BINARY_NAME configure 231 | **********************************************\" 232 | update-rc.d -f ttn-pkt-fwd remove > /dev/null 2> /dev/null 233 | update-rc.d ttn-pkt-fwd defaults 95 30 > /dev/null 2> /dev/null" > "$WORKDIR/postinst" 234 | chmod +x "$WORKDIR/postinst" 235 | 236 | cp "$multitech_installer_file" "$PKTFWD_DESTDIR" 237 | chmod +x "$PKTFWD_DESTDIR/multitech-installer.sh" 238 | 239 | FILENAME="ttn-pkt-fwd$random_string.ipk" 240 | 241 | pushd "$WORKDIR" &> /dev/null 242 | tar -czvf "data.tar.gz" "etc" "var" "usr" &> /dev/null 243 | tar -czvf "control.tar.gz" "control" "postinst" &> /dev/null 244 | tar -czvf "$FILENAME" "data.tar.gz" "control.tar.gz" &> /dev/null 245 | popd &> /dev/null 246 | 247 | release_folder="multitech-release$random_string" 248 | mkdir "$release_folder" 249 | mv "$WORKDIR/$FILENAME" "$PWD/$release_folder" 250 | 251 | rm -rf "$WORKDIR" 252 | 253 | echo "$0: package available at $PWD/$release_folder/$FILENAME" &> "$OUTPUT" 254 | 255 | if [[ ! -z "$4" ]] ; then 256 | printf "%s/%s/%s" "$PWD" "$release_folder" "$FILENAME" 257 | fi 258 | -------------------------------------------------------------------------------- /scripts/multitech/multitech-installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to install packages for TTN on MultiTech Linux Conduit 4 | # 5 | # Written by Jac Kersing , with changes by Eric Gourlaouen 6 | # 7 | # Parts of the script based on tzselect by Paul Eggert. 8 | # 9 | 10 | STATUSFILE=/var/config/.installer 11 | # STATUSFILE allows us to keep track of the installation process. 12 | 13 | if [ ! -f $STATUSFILE ] ; then 14 | touch $STATUSFILE 15 | fi 16 | 17 | # now set the time zone 18 | # Output one argument as-is to standard output. 19 | # Safer than 'echo', which can mishandle '\' or leading '-'. 20 | say() { 21 | printf '%s\n' "$1" 22 | } 23 | 24 | # Ask the user to select from the function's arguments, 25 | # and assign the selected argument to the variable 'select_result'. 26 | # Exit on EOF or I/O error. Use the shell's 'select' builtin if available, 27 | # falling back on a less-nice but portable substitute otherwise. 28 | if 29 | case $BASH_VERSION in 30 | ?*) : ;; 31 | '') 32 | # '; exit' should be redundant, but Dash doesn't properly fail without it. 33 | (eval 'set --; select x; do break; done; exit') /dev/null 34 | esac 35 | then 36 | # Do this inside 'eval', as otherwise the shell might exit when parsing it 37 | # even though it is never executed. 38 | eval ' 39 | doselect() { 40 | select select_result 41 | do 42 | case $select_result in 43 | "") echo >&2 "Please enter a number in range." ;; 44 | ?*) break 45 | esac 46 | done || exit 47 | } 48 | 49 | # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout. 50 | case $BASH_VERSION in 51 | [01].*) 52 | case `echo 1 | (select x in x; do break; done) 2>/dev/null` in 53 | ?*) PS3= 54 | esac 55 | esac 56 | ' 57 | else 58 | doselect() { 59 | # Field width of the prompt numbers. 60 | select_width=`expr $# : '.*'` 61 | 62 | select_i= 63 | 64 | while : 65 | do 66 | case $select_i in 67 | '') 68 | select_i=0 69 | for select_word 70 | do 71 | select_i=`expr $select_i + 1` 72 | printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word" 73 | done ;; 74 | *[!0-9]*) 75 | echo >&2 'Please enter a number in range.' ;; 76 | *) 77 | if test 1 -le $select_i && test $select_i -le $#; then 78 | shift `expr $select_i - 1` 79 | select_result=$1 80 | break 81 | fi 82 | echo >&2 'Please enter a number in range.' 83 | esac 84 | 85 | # Prompt and read input. 86 | printf >&2 %s "${PS3-#? }" 87 | read select_i || exit 88 | done 89 | } 90 | fi 91 | 92 | # check for AEP model and ask user to skip network/timezone setup 93 | # /var/config/db.json is only present on AEP models, use it to detect AEP 94 | grep network $STATUSFILE > /dev/null 2> /dev/null 95 | if [ $? -ne 0 -a -f /var/config/db.json ] ; then 96 | # Securing the device should be done using the web interface 97 | echo "secure" >> $STATUSFILE 98 | echo "AEP Model detected, have time zone and network been setup?" 99 | doselect Yes No 100 | if [ "$select_result" = "No" ] ; then 101 | echo "Please configure \"network interfaces\" and \"time\" using the web interface and restart." 102 | echo "DO NOT configure \"LoRa Network Server\" in the web interface!" 103 | exit 104 | fi 105 | echo "timezone" >> $STATUSFILE 106 | echo "network" >> $STATUSFILE 107 | fi 108 | 109 | grep secure $STATUSFILE > /dev/null 2> /dev/null 110 | if [ $? -ne 0 ] ; then 111 | # Start by securing the device 112 | echo "Securing access to the device, enter the same password twice and" 113 | echo "make sure to save this password as the device requires factory" 114 | echo "reset when the password is lost!!" 115 | passwd root 116 | echo "secure" >> $STATUSFILE 117 | else 118 | echo "$0: Device already secured, ignoring" 119 | fi 120 | 121 | grep timezone $STATUSFILE > /dev/null 2> /dev/null 122 | if [ $? -ne 0 ] ; then 123 | echo "$0: Starting timezone configuration" 124 | # -------------------------------- from tzselect code: -------------------- 125 | # Interact with the user via stderr and stdin. 126 | # Contributed by Paul Eggert. This file is in the public domain. 127 | 128 | # Specify default values for environment variables if they are unset. 129 | AWK=awk 130 | TZDIR=/usr/share/zoneinfo 131 | 132 | coord= 133 | location_limit=10 134 | zonetabtype=zone 135 | 136 | # Make sure the tables are readable. 137 | TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab 138 | TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab 139 | for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE 140 | do 141 | <"$f" || { 142 | say >&2 "$0: time zone files are not set up correctly" 143 | exit 1 144 | } 145 | done 146 | 147 | # If the current locale does not support UTF-8, convert data to current 148 | # locale's format if possible, as the shell aligns columns better that way. 149 | # Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI. 150 | ! $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' && 151 | { tmp=`(mktemp -d) 2>/dev/null` || { 152 | tmp=${TMPDIR-/tmp}/tzselect.$$ && 153 | (umask 77 && mkdir -- "$tmp") 154 | };} && 155 | trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM && 156 | (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \ 157 | 2>/dev/null && 158 | TZ_COUNTRY_TABLE=$tmp/iso3166.tab && 159 | iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab && 160 | TZ_ZONE_TABLE=$tmp/$zonetabtype.tab 161 | 162 | newline=' 163 | ' 164 | IFS=$newline 165 | 166 | 167 | # Awk script to read a time zone table and output the same table, 168 | # with each column preceded by its distance from 'here'. 169 | output_distances=' 170 | BEGIN { 171 | FS = "\t" 172 | while (getline &2 'Please identify a location' \ 256 | 'so that time zone rules can be set correctly.' 257 | 258 | continent= 259 | country= 260 | region= 261 | 262 | case $coord in 263 | ?*) 264 | continent=coord;; 265 | '') 266 | 267 | # Ask the user for continent or ocean. 268 | 269 | echo >&2 'Please select a continent, ocean, "coord", or "TZ".' 270 | 271 | quoted_continents=` 272 | $AWK ' 273 | BEGIN { FS = "\t" } 274 | /^[^#]/ { 275 | entry = substr($3, 1, index($3, "/") - 1) 276 | if (entry == "America") 277 | entry = entry "s" 278 | if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/) 279 | entry = entry " Ocean" 280 | printf "'\''%s'\''\n", entry 281 | } 282 | ' <"$TZ_ZONE_TABLE" | 283 | sort -u | 284 | tr '\n' ' ' 285 | echo '' 286 | ` 287 | 288 | eval ' 289 | doselect '"$quoted_continents"' \ 290 | "coord - I want to use geographical coordinates." \ 291 | "TZ - I want to specify the time zone using the Posix TZ format." 292 | continent=$select_result 293 | case $continent in 294 | Americas) continent=America;; 295 | *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''` 296 | esac 297 | ' 298 | esac 299 | 300 | case $continent in 301 | TZ) 302 | # Ask the user for a Posix TZ string. Check that it conforms. 303 | while 304 | echo >&2 'Please enter the desired value' \ 305 | 'of the TZ environment variable.' 306 | echo >&2 'For example, GST-10 is a zone named GST' \ 307 | 'that is 10 hours ahead (east) of UTC.' 308 | read TZ 309 | $AWK -v TZ="$TZ" 'BEGIN { 310 | tzname = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})" 311 | time = "(2[0-4]|[0-1]?[0-9])" \ 312 | "(:[0-5][0-9](:[0-5][0-9])?)?" 313 | offset = "[-+]?" time 314 | mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]" 315 | jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \ 316 | "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])" 317 | datetime = ",(" mdate "|" jdate ")(/" time ")?" 318 | tzpattern = "^(:.*|" tzname offset "(" tzname \ 319 | "(" offset ")?(" datetime datetime ")?)?)$" 320 | if (TZ ~ tzpattern) exit 1 321 | exit 0 322 | }' 323 | do 324 | say >&2 "'$TZ' is not a conforming Posix time zone string." 325 | done 326 | TZ_for_date=$TZ;; 327 | *) 328 | case $continent in 329 | coord) 330 | case $coord in 331 | '') 332 | echo >&2 'Please enter coordinates' \ 333 | 'in ISO 6709 notation.' 334 | echo >&2 'For example, +4042-07403 stands for' 335 | echo >&2 '40 degrees 42 minutes north,' \ 336 | '74 degrees 3 minutes west.' 337 | read coord;; 338 | esac 339 | distance_table=`$AWK \ 340 | -v coord="$coord" \ 341 | -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \ 342 | "$output_distances" <"$TZ_ZONE_TABLE" | 343 | sort -n | 344 | sed "${location_limit}q" 345 | ` 346 | regions=`say "$distance_table" | $AWK ' 347 | BEGIN { FS = "\t" } 348 | { print $NF } 349 | '` 350 | echo >&2 'Please select one of the following' \ 351 | 'time zone regions,' 352 | echo >&2 'listed roughly in increasing order' \ 353 | "of distance from $coord". 354 | doselect $regions 355 | region=$select_result 356 | TZ=`say "$distance_table" | $AWK -v region="$region" ' 357 | BEGIN { FS="\t" } 358 | $NF == region { print $4 } 359 | '` 360 | ;; 361 | *) 362 | # Get list of names of countries in the continent or ocean. 363 | countries=`$AWK \ 364 | -v continent="$continent" \ 365 | -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \ 366 | ' 367 | BEGIN { FS = "\t" } 368 | /^#/ { next } 369 | $3 ~ ("^" continent "/") { 370 | ncc = split($1, cc, /,/) 371 | for (i = 1; i <= ncc; i++) 372 | if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i] 373 | } 374 | END { 375 | while (getline &2 'Please select a country' \ 393 | 'whose clocks agree with yours.' 394 | doselect $countries 395 | country=$select_result;; 396 | *) 397 | country=$countries 398 | esac 399 | 400 | 401 | # Get list of names of time zone rule regions in the country. 402 | regions=`$AWK \ 403 | -v country="$country" \ 404 | -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \ 405 | ' 406 | BEGIN { 407 | FS = "\t" 408 | cc = country 409 | while (getline &2 'Please select one of the following' \ 425 | 'time zone regions.' 426 | doselect $regions 427 | region=$select_result;; 428 | *) 429 | region=$regions 430 | esac 431 | 432 | # Determine TZ from country and region. 433 | TZ=`$AWK \ 434 | -v country="$country" \ 435 | -v region="$region" \ 436 | -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \ 437 | ' 438 | BEGIN { 439 | FS = "\t" 440 | cc = country 441 | while (getline &2 "$0: time zone files are not set up correctly" 457 | exit 1 458 | } 459 | esac 460 | 461 | # Output TZ info and ask the user to confirm. 462 | 463 | echo >&2 "" 464 | echo >&2 "The following information has been given:" 465 | echo >&2 "" 466 | case $country%$region%$coord in 467 | ?*%?*%) say >&2 " $country$newline $region";; 468 | ?*%%) say >&2 " $country";; 469 | %?*%?*) say >&2 " coord $coord$newline $region";; 470 | %%?*) say >&2 " coord $coord";; 471 | *) say >&2 " TZ='$TZ'" 472 | esac 473 | say >&2 "" 474 | say >&2 "Therefore TZ='$TZ' will be used." 475 | say >&2 "Is the above information OK?" 476 | 477 | doselect Yes No 478 | ok=$select_result 479 | case $ok in 480 | Yes) break 481 | esac 482 | do coord= 483 | done 484 | 485 | # -------------------------------- end tzselect -------------------- 486 | 487 | # link choosen timezone 488 | ln -sf /usr/share/zoneinfo/$TZ /etc/localtime 489 | echo "timezone" >> $STATUSFILE 490 | else 491 | echo "$0: Device timezone already configured, ignoring" 492 | fi 493 | 494 | # On to the network information 495 | grep network $STATUSFILE > /dev/null 2> /dev/null 496 | if [ $? -ne 0 ] ; then 497 | echo "" 498 | echo "NETWORK SETUP" 499 | echo "" 500 | echo "Do you want to use DHCP" 501 | doselect Yes No 502 | ok=$select_result 503 | case $ok in 504 | Yes) 505 | if [ ! -f /var/config/network/interfaces.org ] ; then 506 | mv /var/config/network/interfaces /etc/network/interfaces.org 507 | fi 508 | cat << _EOF_ > /var/config/network/interfaces 509 | # /etc/network/interfaces -- configuration file for ifup(8), ifdown(8) 510 | 511 | # The loopback interface 512 | auto lo 513 | iface lo inet loopback 514 | 515 | # Wired interface 516 | auto eth0 517 | iface eth0 inet dhcp 518 | #iface eth0 inet static 519 | #address 192.168.2.1 520 | #netmask 255.255.255.0 521 | #gateway 192.168.2.254 522 | 523 | # Bridge interface with eth0 (comment out eth0 lines above to use with bridge) 524 | # iface eth0 inet manual 525 | # 526 | # auto br0 527 | # iface br0 inet static 528 | # bridge_ports eth0 529 | # address 192.168.2.1 530 | # netmask 255.255.255.0 531 | 532 | # Wifi client 533 | # NOTE: udev rules will bring up wlan0 automatically if a wifi device is detected 534 | # and the wlan0 interface is defined, therefore an "auto wlan0" line is not needed. 535 | # If "auto wlan0" is also specified, startup conflicts may result. 536 | #iface wlan0 inet dhcp 537 | #wpa-conf /var/config/wpa_supplicant.conf 538 | #wpa-driver nl80211 539 | _EOF_ 540 | ;; 541 | No) 542 | got_it=No 543 | while [ $got_it != Yes ] ; do 544 | echo "Please provide network parameters" 545 | echo -n "IP address: " 546 | read ip 547 | echo -n "netmask: " 548 | read mask 549 | echo -n "gateway: " 550 | read gw 551 | echo -n "DNS IP (use 8.8.8.8 for Google DNS): " 552 | read dns 553 | echo 554 | echo "Supplied information:" 555 | echo "IP : $ip" 556 | echo "Netmask: $mask" 557 | echo "Gateway: $gw" 558 | echo "DNS IP : $dns" 559 | doselect Yes No 560 | got_it=$select_result 561 | done 562 | cat << _EOF_ > /var/config/network/interfaces 563 | # /etc/network/interfaces -- configuration file for ifup(8), ifdown(8) 564 | 565 | # The loopback interface 566 | auto lo 567 | iface lo inet loopback 568 | 569 | # Wired interface 570 | auto eth0 571 | iface eth0 inet static 572 | address $ip 573 | netmask $mask 574 | gateway $gw 575 | post-up echo 'nameserver $dns' >/etc/resolv.conf 576 | 577 | # Bridge interface with eth0 (comment out eth0 lines above to use with bridge) 578 | # iface eth0 inet manual 579 | # 580 | # auto br0 581 | # iface br0 inet static 582 | # bridge_ports eth0 583 | # address 192.168.2.1 584 | # netmask 255.255.255.0 585 | 586 | # Wifi client 587 | # NOTE: udev rules will bring up wlan0 automatically if a wifi device is detected 588 | # and the wlan0 interface is defined, therefore an "auto wlan0" line is not needed. 589 | # If "auto wlan0" is also specified, startup conflicts may result. 590 | #iface wlan0 inet dhcp 591 | #wpa-conf /var/config/wpa_supplicant.conf 592 | #wpa-driver nl80211 593 | _EOF_ 594 | esac 595 | 596 | echo "network" >> $STATUSFILE 597 | echo "Network configuration written" 598 | echo "" 599 | echo "The gateway will now shutdown. Remove power once the status led" 600 | echo "stopped blinking, connect the gateway to the new network and reapply" 601 | echo "power." 602 | echo "" 603 | echo "When the gateway reboots, continue the configuration process with" 604 | echo "the same /etc/init.d/ttn-pkt-fwd configure command." 605 | echo "" 606 | echo "Press enter to continue" 607 | read n 608 | sync;sync;sync 609 | shutdown -h now 610 | sleep 600 611 | else 612 | echo "$0: Device network already configured, ignoring" 613 | fi 614 | 615 | # Disable the MultiTech lora server processes 616 | # Do we want to remove the software as well?? 617 | grep disable-mtech $STATUSFILE > /dev/null 2> /dev/null 618 | if [ $? -ne 0 ] ; then 619 | echo "Disable MultiTech packet forwarder" 620 | /etc/init.d/lora-network-server stop 621 | cat << _EOF_ > /etc/default/lora-network-server 622 | # set to "yes" or "no" to control starting on boot 623 | ENABLED="no" 624 | _EOF_ 625 | echo "disable-mtech" >> $STATUSFILE 626 | else 627 | echo "$0: Multitech LoRa server already disabled, ignoring" 628 | fi 629 | 630 | # Set date and time using ntpdate 631 | grep date $STATUSFILE > /dev/null 2> /dev/null 632 | if [ $? -ne 0 ] ; then 633 | echo "$0: Date/time configuration" 634 | # Network should be configured allowing access to remote servers at this point 635 | wget http://www.thethingsnetwork.org/ --no-check-certificate -O /dev/null -o /dev/null 636 | if [ $? -ne 0 ] ; then 637 | echo "Error in network settings, cannot access www.thethingsnetwork.org" 638 | echo "Network settings are necessary to run the last step of the setup" 639 | echo "(update date and time info using ntpdate)." 640 | echo "Check network settings and rerun this script to correct the setup" 641 | grep -v network $STATUSFILE > $STATUSFILE.tmp 642 | mv $STATUSFILE.tmp $STATUSFILE 643 | exit 1 644 | fi 645 | ntpdate 0.europe.pool.ntp.org 646 | hwclock -u -w 647 | echo "date" >> $STATUSFILE 648 | else 649 | echo "$0: Date and time already configured, ignorin" 650 | fi 651 | -------------------------------------------------------------------------------- /scripts/rpi/install-systemd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "$1" ]] ; then 4 | # No binary specified 5 | echo "$0: No binary specified." 6 | exit 1 7 | fi 8 | 9 | binary="$1" 10 | binary_name=`basename "$binary"` 11 | binary_directory=`dirname "$binary"` 12 | pushd "$binary_directory" 13 | absolute_binary_directory="$(pwd)" 14 | absolute_binary_path="$absolute_binary_directory/$binary_name" 15 | popd 16 | 17 | config="$2" 18 | 19 | if [[ -z "$config" ]] ; then 20 | echo "$0: No configuration file to use specified." 21 | exit 1 22 | fi 23 | 24 | config_name=`basename "$config"` 25 | config_directory=`dirname "$config"` 26 | pushd "$config_directory" 27 | absolute_config_directory="$(pwd)" 28 | absolute_config_path="$absolute_config_directory/$config_name" 29 | popd 30 | 31 | echo "[Unit] 32 | Description=TTN Packet Forwarder Service 33 | 34 | [Install] 35 | WantedBy=multi-user.target 36 | 37 | [Service] 38 | TimeoutStartSec=infinity 39 | Type=simple 40 | TimeoutSec=infinity 41 | RestartSec=10 42 | WorkingDirectory=$absolute_binary_directory 43 | ExecStart=$absolute_binary_path start --config=\"$absolute_config_path\" 44 | Restart=always 45 | BusName=org.thethingsnetwork.ttn-pkt-fwd" > /etc/systemd/system/ttn-pkt-fwd.service 46 | 47 | echo "$0: Installation of the systemd service complete." 48 | -------------------------------------------------------------------------------- /scripts/toolchains/Dockerfile.kerlink-iot-station: -------------------------------------------------------------------------------- 1 | FROM ubuntu:12.04 2 | 3 | RUN apt-get -y update ; apt-get -y upgrade 4 | RUN apt-get -y install apt-utils make ia32-libs u-boot-tools lzma zlib1g-dev bison flex yodl 5 | RUN apt-get -y install git --fix-missing 6 | 7 | COPY arm-2011.03-wirgrid /opt 8 | ENV PATH=$PATH:/opt/bin 9 | ENV CROSS_COMPILE=arm-none-linux-gnueabi- 10 | -------------------------------------------------------------------------------- /scripts/toolchains/Dockerfile.multitech: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get -y update 4 | RUN apt-get -y install curl bzip2 python 5 | 6 | # Installing Multitech toolchain 7 | RUN curl http://www.multitech.net/mlinux/sdk/3.2.0/mlinux-eglibc-x86_64-mlinux-factory-image-arm926ejste-toolchain-3.2.0.sh > mlinux-toolchain-install.sh 8 | RUN chmod +x mlinux-toolchain-install.sh 9 | RUN ./mlinux-toolchain-install.sh 10 | 11 | ENV CFG_SPI=ftdi 12 | ENV PLATFORM=multitech 13 | -------------------------------------------------------------------------------- /util/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package util 4 | 5 | import ( 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "encoding/json" 10 | 11 | "github.com/TheThingsNetwork/go-utils/log" 12 | ) 13 | 14 | type ChannelConf struct { 15 | Enabled bool `json:"enable"` 16 | Description *string `json:"desc,omitempty"` 17 | Radio uint8 `json:"radio"` 18 | IfValue int32 `json:"if"` 19 | Bandwidth *uint32 `json:"bandwidth,omitempty"` 20 | Datarate *uint32 `json:"datarate,omitempty"` 21 | SpreadFactor *uint8 `json:"spread_factor,omitempty"` 22 | } 23 | 24 | type ChannelFreqConf struct { 25 | Freq int `json:"freq_hz"` // Frequency in hertz 26 | ScanTime int `json:"scan_time_us"` 27 | } 28 | 29 | // LBTConf wraps lbt configuration for SX1301 30 | type LbtConf struct { 31 | Enabled bool `json:"enable"` 32 | RssiTarget int `json:"rssi_target"` 33 | RssiOffset int `json:"sx127x_rssi_offset"` 34 | ChannelsConfig []ChannelFreqConf `json:"chan_cfg"` 35 | } 36 | 37 | type GainTableConf struct { 38 | PaGain uint8 `json:"pa_gain"` 39 | MixGain uint8 `json:"mix_gain"` 40 | RfPower int8 `json:"rf_power"` 41 | DigGain uint8 `json:"dig_gain"` 42 | Description *string `json:"desc,omitempty"` 43 | DacGain *uint8 `json:"dac_gain,omitempty"` 44 | } 45 | 46 | type RadioConf struct { 47 | Enabled bool `json:"enable"` 48 | RadioType string `json:"type"` 49 | Freq int `json:"freq"` 50 | RssiOffset float32 `json:"rssi_offset"` 51 | TxEnabled bool `json:"tx_enable"` 52 | TxNotchFreq *int `json:"tx_notch_freq,omitempty"` 53 | TxMinFreq *int `json:"tx_freq_min,omitempty"` 54 | TxMaxFreq *int `json:"tx_freq_max,omitempty"` 55 | } 56 | 57 | type SX1301Conf struct { 58 | LorawanPublic bool `json:"lorawan_public"` 59 | Clksrc int `json:"clksrc"` 60 | ClksrcDescription *string `json:"clksrc_desc,omitempty"` 61 | AntennaGain *int `json:"antenna_gain,omitempty"` 62 | AntennaGainDescription *string `json:"antenna_gain_desc,omitempty"` 63 | LbtConfig *LbtConf `json:"lbt_cfg,omitempty"` 64 | Radio0 *RadioConf `json:"radio_0,omitempty"` 65 | Radio1 *RadioConf `json:"radio_1,omitempty"` 66 | MultiSFChan0 *ChannelConf `json:"chan_multiSF_0,omitempty"` 67 | MultiSFChan1 *ChannelConf `json:"chan_multiSF_1,omitempty"` 68 | MultiSFChan2 *ChannelConf `json:"chan_multiSF_2,omitempty"` 69 | MultiSFChan3 *ChannelConf `json:"chan_multiSF_3,omitempty"` 70 | MultiSFChan4 *ChannelConf `json:"chan_multiSF_4,omitempty"` 71 | MultiSFChan5 *ChannelConf `json:"chan_multiSF_5,omitempty"` 72 | MultiSFChan6 *ChannelConf `json:"chan_multiSF_6,omitempty"` 73 | MultiSFChan7 *ChannelConf `json:"chan_multiSF_7,omitempty"` 74 | MultiSFChan8 *ChannelConf `json:"chan_multiSF_8,omitempty"` 75 | MultiSFChan9 *ChannelConf `json:"chan_multiSF_9,omitempty"` 76 | MultiSFChan10 *ChannelConf `json:"chan_multiSF_10,omitempty"` 77 | MultiSFChan11 *ChannelConf `json:"chan_multiSF_11,omitempty"` 78 | MultiSFChan12 *ChannelConf `json:"chan_multiSF_12,omitempty"` 79 | MultiSFChan13 *ChannelConf `json:"chan_multiSF_13,omitempty"` 80 | MultiSFChan14 *ChannelConf `json:"chan_multiSF_14,omitempty"` 81 | MultiSFChan15 *ChannelConf `json:"chan_multiSF_15,omitempty"` 82 | MultiSFChan16 *ChannelConf `json:"chan_multiSF_16,omitempty"` 83 | MultiSFChan17 *ChannelConf `json:"chan_multiSF_17,omitempty"` 84 | MultiSFChan18 *ChannelConf `json:"chan_multiSF_18,omitempty"` 85 | MultiSFChan19 *ChannelConf `json:"chan_multiSF_19,omitempty"` 86 | MultiSFChan20 *ChannelConf `json:"chan_multiSF_20,omitempty"` 87 | MultiSFChan21 *ChannelConf `json:"chan_multiSF_21,omitempty"` 88 | MultiSFChan22 *ChannelConf `json:"chan_multiSF_22,omitempty"` 89 | MultiSFChan23 *ChannelConf `json:"chan_multiSF_23,omitempty"` 90 | MultiSFChan24 *ChannelConf `json:"chan_multiSF_24,omitempty"` 91 | MultiSFChan25 *ChannelConf `json:"chan_multiSF_25,omitempty"` 92 | MultiSFChan26 *ChannelConf `json:"chan_multiSF_26,omitempty"` 93 | MultiSFChan27 *ChannelConf `json:"chan_multiSF_27,omitempty"` 94 | MultiSFChan28 *ChannelConf `json:"chan_multiSF_28,omitempty"` 95 | MultiSFChan29 *ChannelConf `json:"chan_multiSF_29,omitempty"` 96 | MultiSFChan30 *ChannelConf `json:"chan_multiSF_30,omitempty"` 97 | MultiSFChan31 *ChannelConf `json:"chan_multiSF_31,omitempty"` 98 | MultiSFChan32 *ChannelConf `json:"chan_multiSF_32,omitempty"` 99 | MultiSFChan33 *ChannelConf `json:"chan_multiSF_33,omitempty"` 100 | MultiSFChan34 *ChannelConf `json:"chan_multiSF_34,omitempty"` 101 | MultiSFChan35 *ChannelConf `json:"chan_multiSF_35,omitempty"` 102 | MultiSFChan36 *ChannelConf `json:"chan_multiSF_36,omitempty"` 103 | MultiSFChan37 *ChannelConf `json:"chan_multiSF_37,omitempty"` 104 | MultiSFChan38 *ChannelConf `json:"chan_multiSF_38,omitempty"` 105 | MultiSFChan39 *ChannelConf `json:"chan_multiSF_39,omitempty"` 106 | MultiSFChan40 *ChannelConf `json:"chan_multiSF_40,omitempty"` 107 | MultiSFChan41 *ChannelConf `json:"chan_multiSF_41,omitempty"` 108 | MultiSFChan42 *ChannelConf `json:"chan_multiSF_42,omitempty"` 109 | MultiSFChan43 *ChannelConf `json:"chan_multiSF_43,omitempty"` 110 | MultiSFChan44 *ChannelConf `json:"chan_multiSF_44,omitempty"` 111 | MultiSFChan45 *ChannelConf `json:"chan_multiSF_45,omitempty"` 112 | MultiSFChan46 *ChannelConf `json:"chan_multiSF_46,omitempty"` 113 | MultiSFChan47 *ChannelConf `json:"chan_multiSF_47,omitempty"` 114 | MultiSFChan48 *ChannelConf `json:"chan_multiSF_48,omitempty"` 115 | MultiSFChan49 *ChannelConf `json:"chan_multiSF_49,omitempty"` 116 | MultiSFChan50 *ChannelConf `json:"chan_multiSF_50,omitempty"` 117 | MultiSFChan51 *ChannelConf `json:"chan_multiSF_51,omitempty"` 118 | MultiSFChan52 *ChannelConf `json:"chan_multiSF_52,omitempty"` 119 | MultiSFChan53 *ChannelConf `json:"chan_multiSF_53,omitempty"` 120 | MultiSFChan54 *ChannelConf `json:"chan_multiSF_54,omitempty"` 121 | MultiSFChan55 *ChannelConf `json:"chan_multiSF_55,omitempty"` 122 | MultiSFChan56 *ChannelConf `json:"chan_multiSF_56,omitempty"` 123 | MultiSFChan57 *ChannelConf `json:"chan_multiSF_57,omitempty"` 124 | MultiSFChan58 *ChannelConf `json:"chan_multiSF_58,omitempty"` 125 | MultiSFChan59 *ChannelConf `json:"chan_multiSF_59,omitempty"` 126 | MultiSFChan60 *ChannelConf `json:"chan_multiSF_60,omitempty"` 127 | MultiSFChan61 *ChannelConf `json:"chan_multiSF_61,omitempty"` 128 | MultiSFChan62 *ChannelConf `json:"chan_multiSF_62,omitempty"` 129 | MultiSFChan63 *ChannelConf `json:"chan_multiSF_63,omitempty"` 130 | LoraSTDChannel *ChannelConf `json:"chan_Lora_std,omitempty"` 131 | FSKChannel *ChannelConf `json:"chan_FSK,omitempty"` 132 | TxLut0 *GainTableConf `json:"tx_lut_0,omitempty"` 133 | TxLut1 *GainTableConf `json:"tx_lut_1,omitempty"` 134 | TxLut2 *GainTableConf `json:"tx_lut_2,omitempty"` 135 | TxLut3 *GainTableConf `json:"tx_lut_3,omitempty"` 136 | TxLut4 *GainTableConf `json:"tx_lut_4,omitempty"` 137 | TxLut5 *GainTableConf `json:"tx_lut_5,omitempty"` 138 | TxLut6 *GainTableConf `json:"tx_lut_6,omitempty"` 139 | TxLut7 *GainTableConf `json:"tx_lut_7,omitempty"` 140 | TxLut8 *GainTableConf `json:"tx_lut_8,omitempty"` 141 | TxLut9 *GainTableConf `json:"tx_lut_9,omitempty"` 142 | TxLut10 *GainTableConf `json:"tx_lut_10,omitempty"` 143 | TxLut11 *GainTableConf `json:"tx_lut_11,omitempty"` 144 | TxLut12 *GainTableConf `json:"tx_lut_12,omitempty"` 145 | TxLut13 *GainTableConf `json:"tx_lut_13,omitempty"` 146 | TxLut14 *GainTableConf `json:"tx_lut_14,omitempty"` 147 | TxLut15 *GainTableConf `json:"tx_lut_15,omitempty"` 148 | } 149 | 150 | func (s SX1301Conf) GetRadios() []RadioConf { 151 | radios := make([]RadioConf, 0) 152 | for _, i := range []*RadioConf{s.Radio0, s.Radio1} { 153 | if i == nil { 154 | return radios 155 | } 156 | radios = append(radios, *i) 157 | } 158 | return radios 159 | } 160 | 161 | func (s SX1301Conf) GetTXLuts() []GainTableConf { 162 | gainTables := make([]GainTableConf, 0) 163 | for _, i := range []*GainTableConf{ 164 | s.TxLut0, s.TxLut1, s.TxLut2, s.TxLut3, s.TxLut4, s.TxLut5, s.TxLut6, s.TxLut7, s.TxLut8, s.TxLut9, 165 | s.TxLut10, s.TxLut11, s.TxLut12, s.TxLut13, s.TxLut14, s.TxLut15, 166 | } { 167 | if i == nil { 168 | return gainTables 169 | } 170 | gainTables = append(gainTables, *i) 171 | } 172 | return gainTables 173 | } 174 | 175 | func (s SX1301Conf) GetMultiSFChannels() []ChannelConf { 176 | channels := make([]ChannelConf, 0) 177 | for _, i := range []*ChannelConf{ 178 | s.MultiSFChan0, s.MultiSFChan1, s.MultiSFChan2, s.MultiSFChan3, s.MultiSFChan4, s.MultiSFChan5, s.MultiSFChan6, s.MultiSFChan7, s.MultiSFChan8, s.MultiSFChan9, 179 | s.MultiSFChan10, s.MultiSFChan11, s.MultiSFChan12, s.MultiSFChan13, s.MultiSFChan14, s.MultiSFChan15, s.MultiSFChan16, s.MultiSFChan17, s.MultiSFChan18, s.MultiSFChan19, 180 | s.MultiSFChan20, s.MultiSFChan21, s.MultiSFChan22, s.MultiSFChan23, s.MultiSFChan24, s.MultiSFChan25, s.MultiSFChan26, s.MultiSFChan27, s.MultiSFChan28, s.MultiSFChan29, 181 | s.MultiSFChan30, s.MultiSFChan31, s.MultiSFChan32, s.MultiSFChan33, s.MultiSFChan34, s.MultiSFChan35, s.MultiSFChan36, s.MultiSFChan37, s.MultiSFChan38, s.MultiSFChan39, 182 | s.MultiSFChan40, s.MultiSFChan41, s.MultiSFChan42, s.MultiSFChan43, s.MultiSFChan44, s.MultiSFChan45, s.MultiSFChan46, s.MultiSFChan47, s.MultiSFChan48, s.MultiSFChan49, 183 | s.MultiSFChan50, s.MultiSFChan51, s.MultiSFChan52, s.MultiSFChan53, s.MultiSFChan54, s.MultiSFChan55, s.MultiSFChan56, s.MultiSFChan57, s.MultiSFChan58, s.MultiSFChan59, 184 | s.MultiSFChan60, s.MultiSFChan61, s.MultiSFChan62, s.MultiSFChan63, 185 | } { 186 | if i == nil { 187 | return channels 188 | } 189 | channels = append(channels, *i) 190 | } 191 | return channels 192 | } 193 | 194 | type Config struct { 195 | Concentrator SX1301Conf `json:"SX1301_conf"` 196 | } 197 | 198 | func jsonParseConfig(frequencyPlan []byte) (Config, error) { 199 | conf := Config{} 200 | if err := json.Unmarshal(frequencyPlan, &conf); err != nil { 201 | return conf, err 202 | } 203 | return conf, nil 204 | } 205 | 206 | func FetchConfigFromURL(ctx log.Interface, url string) (Config, error) { 207 | c := Config{} 208 | 209 | resp, err := http.Get(url) 210 | if err != nil { 211 | ctx.Error("Couldn't get the frequency plans") 212 | return c, err 213 | } 214 | frequency, err := ioutil.ReadAll(resp.Body) 215 | if err != nil { 216 | ctx.Error("Failure to read the server response") 217 | return c, err 218 | } 219 | resp.Body.Close() 220 | 221 | return jsonParseConfig(frequency) 222 | } 223 | -------------------------------------------------------------------------------- /util/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package util 4 | 5 | import ( 6 | "os" 7 | 8 | cliHandler "github.com/TheThingsNetwork/go-utils/handlers/cli" 9 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 10 | "github.com/TheThingsNetwork/go-utils/log/apex" 11 | "github.com/apex/log" 12 | levelHandler "github.com/apex/log/handlers/level" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | func GetLogger() ttnlog.Interface { 17 | logLevel := log.InfoLevel 18 | if viper.GetBool("verbose") { 19 | logLevel = log.DebugLevel 20 | } 21 | ctx := apex.Wrap(&log.Logger{ 22 | Handler: levelHandler.New(cliHandler.New(os.Stdout), logLevel), 23 | }) 24 | return ctx 25 | } 26 | -------------------------------------------------------------------------------- /util/system.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package util 4 | 5 | import ( 6 | "os" 7 | "path" 8 | "time" 9 | 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | // TXTimestamp allows to wrap a router.DownlinkMessage.GatewayConfiguration.Timestamp 14 | type TXTimestamp uint32 15 | 16 | func (ts TXTimestamp) GetAsDuration() time.Duration { 17 | return time.Duration(ts) * time.Microsecond 18 | } 19 | 20 | func TXTimestampFromDuration(d time.Duration) TXTimestamp { 21 | return TXTimestamp(d.Nanoseconds() / 1000.0) 22 | } 23 | 24 | func GetConfigFile() string { 25 | flag := viper.GetString("config") 26 | 27 | home := os.Getenv("HOME") 28 | homeyml := "" 29 | homeyaml := "" 30 | 31 | if home != "" { 32 | homeyml = path.Join(home, ".pktfwd.yml") 33 | homeyaml = path.Join(home, ".pktfwd.yaml") 34 | } 35 | 36 | try_files := []string{ 37 | flag, 38 | homeyml, 39 | homeyaml, 40 | } 41 | 42 | // find a file that exists, and use that 43 | for _, file := range try_files { 44 | if file != "" { 45 | if _, err := os.Stat(file); err == nil { 46 | return file 47 | } 48 | } 49 | } 50 | 51 | // no file found, set up correct fallback 52 | return homeyml 53 | } 54 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "qtjd74+bErubh+qyv3s+lWmn9wc=", 7 | "path": "github.com/StackExchange/wmi", 8 | "revision": "ea383cf3ba6ec950874b8486cd72356d007c768f", 9 | "revisionTime": "2017-04-10T19:29:09Z" 10 | }, 11 | { 12 | "checksumSHA1": "J/wj5mkriXItehEQy/cezSlGSKs=", 13 | "path": "github.com/TheThingsNetwork/go-account-lib/account", 14 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 15 | "revisionTime": "2017-03-08T10:31:49Z" 16 | }, 17 | { 18 | "checksumSHA1": "b6pCShOzSh4N9LujOUFzQDzqhz8=", 19 | "path": "github.com/TheThingsNetwork/go-account-lib/auth", 20 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 21 | "revisionTime": "2017-03-08T10:31:49Z" 22 | }, 23 | { 24 | "checksumSHA1": "d1nFTGUlP4sNEf1lelyh6L59mjE=", 25 | "path": "github.com/TheThingsNetwork/go-account-lib/cache", 26 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 27 | "revisionTime": "2017-03-08T10:31:49Z" 28 | }, 29 | { 30 | "checksumSHA1": "8LlsoZGpmrUfqDNDJKU/IJ/x6TM=", 31 | "path": "github.com/TheThingsNetwork/go-account-lib/claims", 32 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 33 | "revisionTime": "2017-03-08T10:31:49Z" 34 | }, 35 | { 36 | "checksumSHA1": "SvUkgVuVVVssqpXbE8OfeWCm0KU=", 37 | "path": "github.com/TheThingsNetwork/go-account-lib/scope", 38 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 39 | "revisionTime": "2017-03-08T10:31:49Z" 40 | }, 41 | { 42 | "checksumSHA1": "RpKXQd5sp9/jsWM991S7OhE9/ME=", 43 | "path": "github.com/TheThingsNetwork/go-account-lib/tokenkey", 44 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 45 | "revisionTime": "2017-03-08T10:31:49Z" 46 | }, 47 | { 48 | "checksumSHA1": "48WYq5L+4Gkl5NXlhDJM0Uzt/7o=", 49 | "path": "github.com/TheThingsNetwork/go-account-lib/tokens", 50 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 51 | "revisionTime": "2017-03-08T10:31:49Z" 52 | }, 53 | { 54 | "checksumSHA1": "ceMHzBTkbEJGWevGmCq+QwSLRDE=", 55 | "path": "github.com/TheThingsNetwork/go-account-lib/util", 56 | "revision": "b4473965f75842ceed74f0ea8a3278d39bc45bc2", 57 | "revisionTime": "2017-03-08T10:31:49Z" 58 | }, 59 | { 60 | "checksumSHA1": "T7iFQUlCUAv4cJNDZC0//46Nbio=", 61 | "path": "github.com/TheThingsNetwork/go-utils/handlers/cli", 62 | "revision": "46d65ba58d30932498e46e4eb51da2a8cab7c424", 63 | "revisionTime": "2017-03-01T14:32:23Z" 64 | }, 65 | { 66 | "checksumSHA1": "aXt7ZSqIfsHWBbJPgHFjqtyxyQ0=", 67 | "path": "github.com/TheThingsNetwork/go-utils/log", 68 | "revision": "46d65ba58d30932498e46e4eb51da2a8cab7c424", 69 | "revisionTime": "2017-03-01T14:32:23Z" 70 | }, 71 | { 72 | "checksumSHA1": "RdI5upcV6MHSjr5Y9zogYvbeURw=", 73 | "path": "github.com/TheThingsNetwork/go-utils/log/apex", 74 | "revision": "46d65ba58d30932498e46e4eb51da2a8cab7c424", 75 | "revisionTime": "2017-03-01T14:32:23Z" 76 | }, 77 | { 78 | "checksumSHA1": "2/v0SMyHM5vgImOb1BEEDWeXZEY=", 79 | "path": "github.com/TheThingsNetwork/go-utils/pseudorandom", 80 | "revision": "7571e4b271c4c2676e7a2d917c4b102c2bd1674b", 81 | "revisionTime": "2017-03-03T09:55:16Z" 82 | }, 83 | { 84 | "checksumSHA1": "aW+EoKKmJasOwy4//7JgaaMDYJI=", 85 | "path": "github.com/TheThingsNetwork/go-utils/queue", 86 | "revision": "46d65ba58d30932498e46e4eb51da2a8cab7c424", 87 | "revisionTime": "2017-03-01T14:32:23Z" 88 | }, 89 | { 90 | "checksumSHA1": "iYa+qSqzqZwpmoibM8/1X+aC3sI=", 91 | "path": "github.com/TheThingsNetwork/go-utils/random", 92 | "revision": "7571e4b271c4c2676e7a2d917c4b102c2bd1674b", 93 | "revisionTime": "2017-03-03T09:55:16Z" 94 | }, 95 | { 96 | "checksumSHA1": "kLFTtAVcjZbHXybodGAqJ8wxflY=", 97 | "path": "github.com/TheThingsNetwork/go-utils/roots", 98 | "revision": "7571e4b271c4c2676e7a2d917c4b102c2bd1674b", 99 | "revisionTime": "2017-03-03T09:55:16Z" 100 | }, 101 | { 102 | "checksumSHA1": "yTg6AirOMwWmVUaMPLBASLmjRpc=", 103 | "path": "github.com/TheThingsNetwork/ttn/api", 104 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 105 | "revisionTime": "2017-03-02T15:44:46Z" 106 | }, 107 | { 108 | "checksumSHA1": "v2Z8XqrF655B4vRpxQn9bgUDNcU=", 109 | "path": "github.com/TheThingsNetwork/ttn/api/discovery", 110 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 111 | "revisionTime": "2017-03-02T15:44:46Z" 112 | }, 113 | { 114 | "checksumSHA1": "ARrfSGyNh+NPB+NHEJyUelYYDMQ=", 115 | "path": "github.com/TheThingsNetwork/ttn/api/fields", 116 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 117 | "revisionTime": "2017-03-02T15:44:46Z" 118 | }, 119 | { 120 | "checksumSHA1": "w7ZMwXnN6CF9/M+7tuD6FEvgZm0=", 121 | "path": "github.com/TheThingsNetwork/ttn/api/gateway", 122 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 123 | "revisionTime": "2017-03-02T15:44:46Z" 124 | }, 125 | { 126 | "checksumSHA1": "5S+Thlh+eahe9eHq8U2LSyqYpNY=", 127 | "path": "github.com/TheThingsNetwork/ttn/api/health", 128 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 129 | "revisionTime": "2017-03-02T15:44:46Z" 130 | }, 131 | { 132 | "checksumSHA1": "96h6E+qIc8bz1pAlNxDoFrDsK7E=", 133 | "path": "github.com/TheThingsNetwork/ttn/api/protocol", 134 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 135 | "revisionTime": "2017-03-02T15:44:46Z" 136 | }, 137 | { 138 | "checksumSHA1": "94T8g0xLVjgFugZlHklxjJ7Bnbc=", 139 | "path": "github.com/TheThingsNetwork/ttn/api/protocol/lorawan", 140 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 141 | "revisionTime": "2017-03-02T15:44:46Z" 142 | }, 143 | { 144 | "checksumSHA1": "C2MSiyTg4X3pqQZOuPD3kM/PbWM=", 145 | "path": "github.com/TheThingsNetwork/ttn/api/router", 146 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 147 | "revisionTime": "2017-03-02T15:44:46Z" 148 | }, 149 | { 150 | "checksumSHA1": "izMlffGmnnnwmYCDS8i+6+U8gjc=", 151 | "path": "github.com/TheThingsNetwork/ttn/api/trace", 152 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 153 | "revisionTime": "2017-03-02T15:44:46Z" 154 | }, 155 | { 156 | "checksumSHA1": "3zyUPqj0fOTxm/38QwrOvE6jX/c=", 157 | "path": "github.com/TheThingsNetwork/ttn/core/types", 158 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 159 | "revisionTime": "2017-03-02T15:44:46Z" 160 | }, 161 | { 162 | "checksumSHA1": "zxokGYK2jJVdnA+Hn6+QJxZ0cTQ=", 163 | "path": "github.com/TheThingsNetwork/ttn/utils/backoff", 164 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 165 | "revisionTime": "2017-03-02T15:44:46Z" 166 | }, 167 | { 168 | "checksumSHA1": "0KY2SIIp2CMW5bBk1HWmkOcw70Q=", 169 | "path": "github.com/TheThingsNetwork/ttn/utils/errors", 170 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 171 | "revisionTime": "2017-03-02T15:44:46Z" 172 | }, 173 | { 174 | "checksumSHA1": "VG6n8lqridYPsN6sT4XZbBhPQsg=", 175 | "path": "github.com/TheThingsNetwork/ttn/utils/random", 176 | "revision": "c5dba333c69b411bf8208639f229a2a3f414eae6", 177 | "revisionTime": "2017-03-02T15:44:46Z" 178 | }, 179 | { 180 | "checksumSHA1": "EZ0pNaUAiIbJuT5c0Sew85egLgw=", 181 | "path": "github.com/apex/log", 182 | "revision": "a97903d8456938a31ce3cb7d58ad3f378f951f24", 183 | "revisionTime": "2017-02-22T07:03:41Z" 184 | }, 185 | { 186 | "checksumSHA1": "AHCiF3VnEqmXyZDeH+z/IGsAtnI=", 187 | "path": "github.com/apex/log/handlers/level", 188 | "revision": "a97903d8456938a31ce3cb7d58ad3f378f951f24", 189 | "revisionTime": "2017-02-22T07:03:41Z" 190 | }, 191 | { 192 | "checksumSHA1": "dPI35hOM4TMrLugm6M1js14jHVQ=", 193 | "path": "github.com/asaskevich/govalidator", 194 | "revision": "fdf19785fd3558d619ef81212f5edf1d6c2a5911", 195 | "revisionTime": "2017-01-04T21:11:26Z" 196 | }, 197 | { 198 | "checksumSHA1": "3A9KyolRUkMn+hgddTvzy788/t4=", 199 | "path": "github.com/bluele/gcache", 200 | "revision": "f8e0098c5b2fe36eb2c00773c2670987076b356f", 201 | "revisionTime": "2017-03-06T01:05:55Z" 202 | }, 203 | { 204 | "checksumSHA1": "ZHpBCsUv5lUxdt1gF9HWRZ4IffI=", 205 | "path": "github.com/brocaar/lorawan", 206 | "revision": "c61721fa96c85c25ea7ba635fc477224344ddbe3", 207 | "revisionTime": "2017-03-20T08:06:39Z" 208 | }, 209 | { 210 | "checksumSHA1": "MCRyJiR3hs/bt++6w5SCcyIYaZE=", 211 | "path": "github.com/brocaar/lorawan/band", 212 | "revision": "f4277a4ddb8f1adc1fead0f96e2e8a7e7bae0456", 213 | "revisionTime": "2016-09-30T18:12:03Z" 214 | }, 215 | { 216 | "checksumSHA1": "2Fy1Y6Z3lRRX1891WF/+HT4XS2I=", 217 | "path": "github.com/dgrijalva/jwt-go", 218 | "revision": "2268707a8f0843315e2004ee4f1d021dc08baedf", 219 | "revisionTime": "2017-02-01T22:58:49Z" 220 | }, 221 | { 222 | "checksumSHA1": "JhI3dzfib2NMGL11NiUswioZP8U=", 223 | "path": "github.com/fsnotify/fsnotify", 224 | "revision": "a904159b9206978bb6d53fcc7a769e5cd726c737", 225 | "revisionTime": "2016-11-02T19:13:10Z" 226 | }, 227 | { 228 | "checksumSHA1": "wDZdTaY9JiqqqnF4c3pHP71nWmk=", 229 | "path": "github.com/go-ole/go-ole", 230 | "revision": "de8695c8edbf8236f30d6e1376e20b198a028d42", 231 | "revisionTime": "2017-02-09T15:13:32Z" 232 | }, 233 | { 234 | "checksumSHA1": "Q0ZOcJW0fqOefDzEdn+PJHOeSgI=", 235 | "path": "github.com/go-ole/go-ole/oleutil", 236 | "revision": "de8695c8edbf8236f30d6e1376e20b198a028d42", 237 | "revisionTime": "2017-02-09T15:13:32Z" 238 | }, 239 | { 240 | "checksumSHA1": "KNyoFOwJJ2A4Jdsf/5E80sfPfqw=", 241 | "path": "github.com/gogo/protobuf/gogoproto", 242 | "revision": "3ea128ab69017b2bb9720f38a32e2dcfc22c0546", 243 | "revisionTime": "2017-02-20T12:55:46Z" 244 | }, 245 | { 246 | "checksumSHA1": "6ZxSmrIx3Jd15aou16oG0HPylP4=", 247 | "path": "github.com/gogo/protobuf/proto", 248 | "revision": "3ea128ab69017b2bb9720f38a32e2dcfc22c0546", 249 | "revisionTime": "2017-02-20T12:55:46Z" 250 | }, 251 | { 252 | "checksumSHA1": "wg7MHG+Fuc0ZGWFIlAZJulPxR2s=", 253 | "path": "github.com/gogo/protobuf/protoc-gen-gogo/descriptor", 254 | "revision": "3ea128ab69017b2bb9720f38a32e2dcfc22c0546", 255 | "revisionTime": "2017-02-20T12:55:46Z" 256 | }, 257 | { 258 | "checksumSHA1": "JSHl8b3nI8EWvzm+uyrIqj2Hiu4=", 259 | "path": "github.com/golang/mock/gomock", 260 | "revision": "bd3c8e81be01eef76d4b503f5e687d2d1354d2d9", 261 | "revisionTime": "2016-01-21T18:51:14Z" 262 | }, 263 | { 264 | "checksumSHA1": "APDDi2ohrU7OkChQCekD9tSVUhs=", 265 | "path": "github.com/golang/protobuf/jsonpb", 266 | "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", 267 | "revisionTime": "2016-11-17T03:31:26Z" 268 | }, 269 | { 270 | "checksumSHA1": "kBeNcaKk56FguvPSUCEaH6AxpRc=", 271 | "path": "github.com/golang/protobuf/proto", 272 | "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", 273 | "revisionTime": "2016-11-17T03:31:26Z" 274 | }, 275 | { 276 | "checksumSHA1": "AjyXQ5eohrCPS/jSWZFPn5E8wnQ=", 277 | "path": "github.com/golang/protobuf/protoc-gen-go/descriptor", 278 | "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", 279 | "revisionTime": "2016-11-17T03:31:26Z" 280 | }, 281 | { 282 | "checksumSHA1": "9wOTz0iWfOSTSTmUkoq0WYkiMdY=", 283 | "path": "github.com/golang/protobuf/ptypes/empty", 284 | "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", 285 | "revisionTime": "2016-11-17T03:31:26Z" 286 | }, 287 | { 288 | "checksumSHA1": "LoEQ+t5UoMm4InaYVPVn0XqHPwA=", 289 | "path": "github.com/grpc-ecosystem/grpc-gateway/runtime", 290 | "revision": "199c40a060d1e55508b3b85182ce6f3895ae6302", 291 | "revisionTime": "2016-11-28T00:20:07Z" 292 | }, 293 | { 294 | "checksumSHA1": "x396LPNfci/5x8aVJbliQHH11HQ=", 295 | "path": "github.com/grpc-ecosystem/grpc-gateway/runtime/internal", 296 | "revision": "199c40a060d1e55508b3b85182ce6f3895ae6302", 297 | "revisionTime": "2016-11-28T00:20:07Z" 298 | }, 299 | { 300 | "checksumSHA1": "NCyVGekDqPMTHHK4ZbEDPZeiN2s=", 301 | "path": "github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api", 302 | "revision": "199c40a060d1e55508b3b85182ce6f3895ae6302", 303 | "revisionTime": "2016-11-28T00:20:07Z" 304 | }, 305 | { 306 | "checksumSHA1": "vqiK5r5dntV7JNZ+ZsGlD0Samos=", 307 | "path": "github.com/grpc-ecosystem/grpc-gateway/utilities", 308 | "revision": "199c40a060d1e55508b3b85182ce6f3895ae6302", 309 | "revisionTime": "2016-11-28T00:20:07Z" 310 | }, 311 | { 312 | "checksumSHA1": "Ok3Csn6Voou7pQT6Dv2mkwpqFtw=", 313 | "path": "github.com/hashicorp/hcl", 314 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 315 | "revisionTime": "2017-02-02T00:05:34Z" 316 | }, 317 | { 318 | "checksumSHA1": "XQmjDva9JCGGkIecOgwtBEMCJhU=", 319 | "path": "github.com/hashicorp/hcl/hcl/ast", 320 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 321 | "revisionTime": "2017-02-02T00:05:34Z" 322 | }, 323 | { 324 | "checksumSHA1": "MGYzZActhzSs9AnCx3wrEYVbKFg=", 325 | "path": "github.com/hashicorp/hcl/hcl/parser", 326 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 327 | "revisionTime": "2017-02-02T00:05:34Z" 328 | }, 329 | { 330 | "checksumSHA1": "z6wdP4mRw4GVjShkNHDaOWkbxS0=", 331 | "path": "github.com/hashicorp/hcl/hcl/scanner", 332 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 333 | "revisionTime": "2017-02-02T00:05:34Z" 334 | }, 335 | { 336 | "checksumSHA1": "oS3SCN9Wd6D8/LG0Yx1fu84a7gI=", 337 | "path": "github.com/hashicorp/hcl/hcl/strconv", 338 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 339 | "revisionTime": "2017-02-02T00:05:34Z" 340 | }, 341 | { 342 | "checksumSHA1": "c6yprzj06ASwCo18TtbbNNBHljA=", 343 | "path": "github.com/hashicorp/hcl/hcl/token", 344 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 345 | "revisionTime": "2017-02-02T00:05:34Z" 346 | }, 347 | { 348 | "checksumSHA1": "138aCV5n8n7tkGYMsMVQQnnLq+0=", 349 | "path": "github.com/hashicorp/hcl/json/parser", 350 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 351 | "revisionTime": "2017-02-02T00:05:34Z" 352 | }, 353 | { 354 | "checksumSHA1": "YdvFsNOMSWMLnY6fcliWQa0O5Fw=", 355 | "path": "github.com/hashicorp/hcl/json/scanner", 356 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 357 | "revisionTime": "2017-02-02T00:05:34Z" 358 | }, 359 | { 360 | "checksumSHA1": "fNlXQCQEnb+B3k5UDL/r15xtSJY=", 361 | "path": "github.com/hashicorp/hcl/json/token", 362 | "revision": "372e8ddaa16fd67e371e9323807d056b799360af", 363 | "revisionTime": "2017-02-02T00:05:34Z" 364 | }, 365 | { 366 | "checksumSHA1": "K6exl2ouL7d8cR2i378EzZOdRVI=", 367 | "path": "github.com/howeyc/gopass", 368 | "revision": "bf9dde6d0d2c004a008c27aaee91170c786f6db8", 369 | "revisionTime": "2017-01-09T16:22:49Z" 370 | }, 371 | { 372 | "checksumSHA1": "7PLlrIaGI1TKWB96RkizgkTtOtQ=", 373 | "path": "github.com/jacobsa/crypto/cmac", 374 | "revision": "293ce0c192fb4f59cd879b46544922b9ed09a13a", 375 | "revisionTime": "2016-11-11T03:08:13Z" 376 | }, 377 | { 378 | "checksumSHA1": "NBvtX91AEKxFLmj8mwwhXEKl6d0=", 379 | "path": "github.com/jacobsa/crypto/common", 380 | "revision": "293ce0c192fb4f59cd879b46544922b9ed09a13a", 381 | "revisionTime": "2016-11-11T03:08:13Z" 382 | }, 383 | { 384 | "checksumSHA1": "KR72MpmwQRMYJuK0BHi2RWgGU2o=", 385 | "path": "github.com/magiconair/properties", 386 | "revision": "b3b15ef068fd0b17ddf408a23669f20811d194d2", 387 | "revisionTime": "2017-01-13T09:48:12Z" 388 | }, 389 | { 390 | "checksumSHA1": "wTMmmuol+KHkz9EwVKaOjd2O4cs=", 391 | "path": "github.com/mitchellh/mapstructure", 392 | "revision": "db1efb556f84b25a0a13a04aad883943538ad2e0", 393 | "revisionTime": "2017-01-25T05:19:37Z" 394 | }, 395 | { 396 | "checksumSHA1": "8Y05Pz7onrQPcVWW6JStSsYRh6E=", 397 | "path": "github.com/pelletier/go-buffruneio", 398 | "revision": "df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d", 399 | "revisionTime": "2016-01-24T19:35:03Z" 400 | }, 401 | { 402 | "checksumSHA1": "bM0KEShwScin9ADEYWUrkkF27Zk=", 403 | "path": "github.com/pelletier/go-toml", 404 | "revision": "d1fa2118c12c44e4f5004da216d1efad10cb4924", 405 | "revisionTime": "2017-02-03T21:36:21Z" 406 | }, 407 | { 408 | "checksumSHA1": "ynJSWoF6v+3zMnh9R0QmmG6iGV8=", 409 | "path": "github.com/pkg/errors", 410 | "revision": "ff09b135c25aae272398c51a07235b90a75aa4f0", 411 | "revisionTime": "2017-03-16T20:15:38Z" 412 | }, 413 | { 414 | "checksumSHA1": "llmzhtIUy63V3Pl65RuEn18ck5g=", 415 | "path": "github.com/segmentio/go-prompt", 416 | "revision": "f0d19b6901ade831d5a3204edc0d6a7d6457fbb2", 417 | "revisionTime": "2016-10-17T23:32:05Z" 418 | }, 419 | { 420 | "checksumSHA1": "fZClM/Bc4qNmhSph4pH4oImQ2XM=", 421 | "path": "github.com/shirou/gopsutil", 422 | "revision": "70693b6a3da51a8a686d31f1b346077bbc066062", 423 | "revisionTime": "2017-04-14T03:53:08Z" 424 | }, 425 | { 426 | "checksumSHA1": "P5gY+n8agEZEyTygmZjgRQK7WtE=", 427 | "path": "github.com/shirou/gopsutil/cpu", 428 | "revision": "70693b6a3da51a8a686d31f1b346077bbc066062", 429 | "revisionTime": "2017-04-14T03:53:08Z" 430 | }, 431 | { 432 | "checksumSHA1": "hKDsT0KAOtA7UqiXYdO0RahnQZ8=", 433 | "path": "github.com/shirou/gopsutil/internal/common", 434 | "revision": "70693b6a3da51a8a686d31f1b346077bbc066062", 435 | "revisionTime": "2017-04-14T03:53:08Z" 436 | }, 437 | { 438 | "checksumSHA1": "jB8En6qWQ7G2yPJey4uY1FvOjWM=", 439 | "path": "github.com/shirou/gopsutil/load", 440 | "revision": "70693b6a3da51a8a686d31f1b346077bbc066062", 441 | "revisionTime": "2017-04-14T03:53:08Z" 442 | }, 443 | { 444 | "checksumSHA1": "zkNNauN735M7Jghr+MsARbb+/xQ=", 445 | "path": "github.com/shirou/gopsutil/mem", 446 | "revision": "70693b6a3da51a8a686d31f1b346077bbc066062", 447 | "revisionTime": "2017-04-14T03:53:08Z" 448 | }, 449 | { 450 | "checksumSHA1": "lBehULzb2/kIK3wZ0gz2yNmHq9s=", 451 | "path": "github.com/spf13/afero", 452 | "revision": "72b31426848c6ef12a7a8e216708cb0d1530f074", 453 | "revisionTime": "2017-01-09T22:53:20Z" 454 | }, 455 | { 456 | "checksumSHA1": "5KRbEQ28dDaQmKwAYTD0if/aEvg=", 457 | "path": "github.com/spf13/afero/mem", 458 | "revision": "72b31426848c6ef12a7a8e216708cb0d1530f074", 459 | "revisionTime": "2017-01-09T22:53:20Z" 460 | }, 461 | { 462 | "checksumSHA1": "6O3g4Dgt5zkgPcccYK73YvzKfWI=", 463 | "path": "github.com/spf13/cast", 464 | "revision": "d1139bab1c07d5ad390a65e7305876b3c1a8370b", 465 | "revisionTime": "2017-01-28T06:04:53Z" 466 | }, 467 | { 468 | "checksumSHA1": "hgI9+1CnDX7GUG7WuHqjYH2nyA0=", 469 | "path": "github.com/spf13/cobra", 470 | "revision": "fcd0c5a1df88f5d6784cb4feead962c3f3d0b66c", 471 | "revisionTime": "2017-02-28T19:17:48Z" 472 | }, 473 | { 474 | "checksumSHA1": "9pkkhgKp3mwSreiML3plQlQYdLQ=", 475 | "path": "github.com/spf13/jwalterweatherman", 476 | "revision": "fa7ca7e836cf3a8bb4ebf799f472c12d7e903d66", 477 | "revisionTime": "2017-01-09T13:33:55Z" 478 | }, 479 | { 480 | "checksumSHA1": "5KvHyB1CImtwZT3fwNkNUlc8R0k=", 481 | "path": "github.com/spf13/pflag", 482 | "revision": "9ff6c6923cfffbcd502984b8e0c80539a94968b7", 483 | "revisionTime": "2017-01-30T21:42:45Z" 484 | }, 485 | { 486 | "checksumSHA1": "cNe3MKwsFLDRzRjKEtOduUiG344=", 487 | "path": "github.com/spf13/viper", 488 | "revision": "7538d73b4eb9511d85a9f1dfef202eeb8ac260f4", 489 | "revisionTime": "2017-02-17T16:38:17Z" 490 | }, 491 | { 492 | "checksumSHA1": "dyI8tS2bXHlHpPIp6VGZ/HXKyJQ=", 493 | "path": "github.com/stianeikeland/go-rpio", 494 | "revision": "896db2ee1c7f95240dd6a09e56edf9cece107a34", 495 | "revisionTime": "2015-12-08T00:50:14Z" 496 | }, 497 | { 498 | "checksumSHA1": "ZaU56svwLgiJD0y8JOB3+/mpYBA=", 499 | "path": "golang.org/x/crypto/ssh/terminal", 500 | "revision": "c7af5bf2638a1164f2eb5467c39c6cffbd13a02e", 501 | "revisionTime": "2017-04-25T18:31:00Z" 502 | }, 503 | { 504 | "checksumSHA1": "Y+HGqEkYM15ir+J93MEaHdyFy0c=", 505 | "path": "golang.org/x/net/context", 506 | "revision": "236b8f043b920452504e263bc21d354427127473", 507 | "revisionTime": "2017-02-06T03:21:01Z" 508 | }, 509 | { 510 | "checksumSHA1": "LC+mzxnIrUS9Kr4s7shpY+A9J2o=", 511 | "path": "golang.org/x/net/http2", 512 | "revision": "236b8f043b920452504e263bc21d354427127473", 513 | "revisionTime": "2017-02-06T03:21:01Z" 514 | }, 515 | { 516 | "checksumSHA1": "G3e2/HrQjRy2Ml9lfX0e3AGRWWE=", 517 | "path": "golang.org/x/net/http2/hpack", 518 | "revision": "236b8f043b920452504e263bc21d354427127473", 519 | "revisionTime": "2017-02-06T03:21:01Z" 520 | }, 521 | { 522 | "checksumSHA1": "GIGmSrYACByf5JDIP9ByBZksY80=", 523 | "path": "golang.org/x/net/idna", 524 | "revision": "236b8f043b920452504e263bc21d354427127473", 525 | "revisionTime": "2017-02-06T03:21:01Z" 526 | }, 527 | { 528 | "checksumSHA1": "UxahDzW2v4mf/+aFxruuupaoIwo=", 529 | "path": "golang.org/x/net/internal/timeseries", 530 | "revision": "236b8f043b920452504e263bc21d354427127473", 531 | "revisionTime": "2017-02-06T03:21:01Z" 532 | }, 533 | { 534 | "checksumSHA1": "3xyuaSNmClqG4YWC7g0isQIbUTc=", 535 | "path": "golang.org/x/net/lex/httplex", 536 | "revision": "236b8f043b920452504e263bc21d354427127473", 537 | "revisionTime": "2017-02-06T03:21:01Z" 538 | }, 539 | { 540 | "checksumSHA1": "GQHKESPeCcAsnerZPtHadvKUIzs=", 541 | "path": "golang.org/x/net/trace", 542 | "revision": "236b8f043b920452504e263bc21d354427127473", 543 | "revisionTime": "2017-02-06T03:21:01Z" 544 | }, 545 | { 546 | "checksumSHA1": "Zt7DIRCaUg5qfhfxyR1wCA+EjCE=", 547 | "path": "golang.org/x/oauth2", 548 | "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", 549 | "revisionTime": "2017-02-14T22:24:16Z" 550 | }, 551 | { 552 | "checksumSHA1": "gChvVZYdb6Bw/vjIpfYJfNvXPoU=", 553 | "path": "golang.org/x/oauth2/internal", 554 | "revision": "b9780ec78894ab900c062d58ee3076cd9b2a4501", 555 | "revisionTime": "2017-02-14T22:24:16Z" 556 | }, 557 | { 558 | "checksumSHA1": "uTQtOqR0ePMMcvuvAIksiIZxhqU=", 559 | "path": "golang.org/x/sys/unix", 560 | "revision": "7a6e5648d140666db5d920909e082ca00a87ba2c", 561 | "revisionTime": "2017-02-01T04:15:14Z" 562 | }, 563 | { 564 | "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=", 565 | "path": "golang.org/x/text/transform", 566 | "revision": "06d6eba81293389cafdff7fca90d75592194b2d9", 567 | "revisionTime": "2017-02-07T17:42:20Z" 568 | }, 569 | { 570 | "checksumSHA1": "7Hjtgu1Yu+Ks0cKf2ldRhXAu/LE=", 571 | "path": "golang.org/x/text/unicode/norm", 572 | "revision": "06d6eba81293389cafdff7fca90d75592194b2d9", 573 | "revisionTime": "2017-02-07T17:42:20Z" 574 | }, 575 | { 576 | "checksumSHA1": "mEyChIkG797MtkrJQXW8X/qZ0l0=", 577 | "path": "google.golang.org/grpc", 578 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 579 | "revisionTime": "2017-02-08T00:26:47Z" 580 | }, 581 | { 582 | "checksumSHA1": "08icuA15HRkdYCt6H+Cs90RPQsY=", 583 | "path": "google.golang.org/grpc/codes", 584 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 585 | "revisionTime": "2017-02-08T00:26:47Z" 586 | }, 587 | { 588 | "checksumSHA1": "AGkvu7gY1jWK7v5s9a8qLlH2gcQ=", 589 | "path": "google.golang.org/grpc/credentials", 590 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 591 | "revisionTime": "2017-02-08T00:26:47Z" 592 | }, 593 | { 594 | "checksumSHA1": "3Lt5hNAG8qJAYSsNghR5uA1zQns=", 595 | "path": "google.golang.org/grpc/grpclog", 596 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 597 | "revisionTime": "2017-02-08T00:26:47Z" 598 | }, 599 | { 600 | "checksumSHA1": "pSFXzfvPlaDBK2RsMcTiIeks4ok=", 601 | "path": "google.golang.org/grpc/health/grpc_health_v1", 602 | "revision": "4eaacfed9779ee7568c9e72e9f763dbd3af8e0b4", 603 | "revisionTime": "2017-03-07T00:54:00Z" 604 | }, 605 | { 606 | "checksumSHA1": "T3Q0p8kzvXFnRkMaK/G8mCv6mc0=", 607 | "path": "google.golang.org/grpc/internal", 608 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 609 | "revisionTime": "2017-02-08T00:26:47Z" 610 | }, 611 | { 612 | "checksumSHA1": "XXpD8+S3gLrfmCLOf+RbxblOQkU=", 613 | "path": "google.golang.org/grpc/metadata", 614 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 615 | "revisionTime": "2017-02-08T00:26:47Z" 616 | }, 617 | { 618 | "checksumSHA1": "4GSUFhOQ0kdFlBH4D5OTeKy78z0=", 619 | "path": "google.golang.org/grpc/naming", 620 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 621 | "revisionTime": "2017-02-08T00:26:47Z" 622 | }, 623 | { 624 | "checksumSHA1": "3RRoLeH6X2//7tVClOVzxW2bY+E=", 625 | "path": "google.golang.org/grpc/peer", 626 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 627 | "revisionTime": "2017-02-08T00:26:47Z" 628 | }, 629 | { 630 | "checksumSHA1": "wzkOAxlah+y75EpH0QVgzb8hdfc=", 631 | "path": "google.golang.org/grpc/stats", 632 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 633 | "revisionTime": "2017-02-08T00:26:47Z" 634 | }, 635 | { 636 | "checksumSHA1": "N0TftT6/CyWqp6VRi2DqDx60+Fo=", 637 | "path": "google.golang.org/grpc/tap", 638 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 639 | "revisionTime": "2017-02-08T00:26:47Z" 640 | }, 641 | { 642 | "checksumSHA1": "yHpUeGwKoqqwd3cbEp3lkcnvft0=", 643 | "path": "google.golang.org/grpc/transport", 644 | "revision": "2a6bf6142e96942e4fb9c0dfb157ee5d3cecafaa", 645 | "revisionTime": "2017-02-08T00:26:47Z" 646 | }, 647 | { 648 | "checksumSHA1": "njiX7ss0SJl8AxnwDDT6wN8huEc=", 649 | "path": "gopkg.in/redis.v5", 650 | "revision": "135cb12c7689ffe92338baf677a1e0d2086144dc", 651 | "revisionTime": "2017-02-19T07:48:52Z" 652 | }, 653 | { 654 | "checksumSHA1": "efyYmNqK7vcPhXW4KXfwbdA1wr4=", 655 | "path": "gopkg.in/redis.v5/internal", 656 | "revision": "135cb12c7689ffe92338baf677a1e0d2086144dc", 657 | "revisionTime": "2017-02-19T07:48:52Z" 658 | }, 659 | { 660 | "checksumSHA1": "2Ek4SixeRSKOX3mUiBMs3Aw+Guc=", 661 | "path": "gopkg.in/redis.v5/internal/consistenthash", 662 | "revision": "135cb12c7689ffe92338baf677a1e0d2086144dc", 663 | "revisionTime": "2017-02-19T07:48:52Z" 664 | }, 665 | { 666 | "checksumSHA1": "rJYVKcBrwYUGl7nuuusmZGrt8mY=", 667 | "path": "gopkg.in/redis.v5/internal/hashtag", 668 | "revision": "135cb12c7689ffe92338baf677a1e0d2086144dc", 669 | "revisionTime": "2017-02-19T07:48:52Z" 670 | }, 671 | { 672 | "checksumSHA1": "KQHbi6qd6MlmEYYCdeH6NypL2wY=", 673 | "path": "gopkg.in/redis.v5/internal/pool", 674 | "revision": "135cb12c7689ffe92338baf677a1e0d2086144dc", 675 | "revisionTime": "2017-02-19T07:48:52Z" 676 | }, 677 | { 678 | "checksumSHA1": "EqPdu5g8NhzxQOMCvzbreTQlzVE=", 679 | "path": "gopkg.in/redis.v5/internal/proto", 680 | "revision": "135cb12c7689ffe92338baf677a1e0d2086144dc", 681 | "revisionTime": "2017-02-19T07:48:52Z" 682 | }, 683 | { 684 | "checksumSHA1": "fALlQNY1fM99NesfLJ50KguWsio=", 685 | "path": "gopkg.in/yaml.v2", 686 | "revision": "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b", 687 | "revisionTime": "2017-04-07T17:21:22Z" 688 | } 689 | ], 690 | "rootPath": "github.com/TheThingsNetwork/packet_forwarder" 691 | } 692 | -------------------------------------------------------------------------------- /wrapper/common.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network. Use of this source code is governed by the MIT license that can be found in the LICENSE file. 2 | 3 | package wrapper 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/TheThingsNetwork/ttn/api/gateway" 9 | ) 10 | 11 | const LengthPayload = 256 // length of the payload in bytes 12 | 13 | // Packet describes the packets manipulated by the gateway 14 | type Packet struct { 15 | Freq uint32 // central frequency of the IF chain (in Hz) 16 | IFChain uint8 // by which IF chain was packet received 17 | Status uint8 // status of the received Packet 18 | CountUS uint32 // internal concentrator counter for timestamping, 1 microsecond resolution 19 | Time int64 // GPS-determined time 20 | Gps *gateway.GPSMetadata // GPS Metadata 21 | RFChain uint8 // by which RF chain was packet received 22 | Modulation uint8 // modulation used by the packet 23 | Bandwidth uint8 // modulation bandwidth (LoRa only) 24 | Datarate uint32 // RX datarate of the packet (SF for LoRa) 25 | Coderate uint8 // error-correcting code of the packet (LoRa only) 26 | RSSI float32 // average packet RSSI in dB 27 | SNR float32 // average packet SNR, in dB (LoRa only) 28 | MinSNR float32 // minimum packet SNR, in dB (LoRa only) 29 | MaxSNR float32 // maximum packet SNR, in dB (LoRa only) 30 | CRC uint16 // CRC that was received in the payload 31 | Size uint32 // Payload size in bytes 32 | Payload []byte // Buffer containing the payload, not yet base64-encoded 33 | } 34 | 35 | type GPSCoordinates struct { 36 | Altitude float64 37 | Latitude float64 38 | Longitude float64 39 | } 40 | 41 | func (p Packet) DatarateString() (string, error) { 42 | if val, ok := datarateString[p.Datarate]; ok { 43 | return val, nil 44 | } 45 | return "", fmt.Errorf("LoRa packet with unknown datarate code %v", p.Datarate) 46 | } 47 | 48 | func (p Packet) BandwidthString() (string, error) { 49 | if val, ok := bandwidthString[p.Bandwidth]; ok { 50 | return val, nil 51 | } 52 | return "", fmt.Errorf("LoRa packet with unknown bandwidth code %v", p.Bandwidth) 53 | } 54 | 55 | func (p Packet) CoderateString() (string, error) { 56 | if val, ok := coderateString[p.Coderate]; ok { 57 | return val, nil 58 | } 59 | return "", fmt.Errorf("LoRa packet with unknown coderate code %v", p.Coderate) 60 | } 61 | -------------------------------------------------------------------------------- /wrapper/concentrator_dummy.go: -------------------------------------------------------------------------------- 1 | // +build dummy 2 | 3 | package wrapper 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/go-utils/log" 7 | "github.com/TheThingsNetwork/packet_forwarder/util" 8 | ) 9 | 10 | func LoRaGatewayVersionInfo() string { 11 | return "Dummy HAL" 12 | } 13 | 14 | func StartLoRaGateway() error { 15 | return nil 16 | } 17 | 18 | func StopLoRaGateway() error { 19 | return nil 20 | } 21 | 22 | func SetBoardConf(ctx log.Interface, conf util.Config) error { 23 | return nil 24 | } 25 | 26 | func SetTXGainConf(ctx log.Interface, conc util.SX1301Conf) error { 27 | return nil 28 | } 29 | 30 | func SetRFChannels(ctx log.Interface, conf util.Config) error { 31 | return nil 32 | } 33 | 34 | func SetSFChannels(ctx log.Interface, conf util.Config) error { 35 | return nil 36 | } 37 | 38 | func SetStandardChannel(ctx log.Interface, stdChan util.ChannelConf) error { 39 | return nil 40 | } 41 | 42 | func SetFSKChannel(ctx log.Interface, fskChan util.ChannelConf) error { 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /wrapper/concentrator_halV1.go: -------------------------------------------------------------------------------- 1 | // +build halv1 2 | 3 | package wrapper 4 | 5 | // #cgo CFLAGS: -I${SRCDIR}/../lora_gateway/libloragw/inc 6 | // #cgo LDFLAGS: -lm ${SRCDIR}/../lora_gateway/libloragw/libloragw.a 7 | // #include "config.h" 8 | // #include "loragw_hal.h" 9 | // #include "loragw_gps.h" 10 | // void setType(struct lgw_conf_rxrf_s *rxrfConf, enum lgw_radio_type_e val) { 11 | // rxrfConf->type = val; 12 | // } 13 | import "C" 14 | import ( 15 | "errors" 16 | "fmt" 17 | "sync" 18 | 19 | "github.com/TheThingsNetwork/go-utils/log" 20 | "github.com/TheThingsNetwork/packet_forwarder/util" 21 | ) 22 | 23 | var concentratorMutex = &sync.Mutex{} 24 | 25 | var loraChannelBandwidths = map[uint32]C.uint8_t{ 26 | 7800: C.BW_7K8HZ, 27 | 15600: C.BW_15K6HZ, 28 | 31200: C.BW_31K2HZ, 29 | 62500: C.BW_62K5HZ, 30 | 125000: C.BW_125KHZ, 31 | 250000: C.BW_250KHZ, 32 | 500000: C.BW_500KHZ, 33 | } 34 | 35 | var loraChannelSpreadingFactors = map[uint32]C.uint32_t{ 36 | 7: C.DR_LORA_SF7, 37 | 8: C.DR_LORA_SF8, 38 | 9: C.DR_LORA_SF9, 39 | 10: C.DR_LORA_SF10, 40 | 11: C.DR_LORA_SF11, 41 | 12: C.DR_LORA_SF12, 42 | } 43 | 44 | // LoRaGatewayVersionInfo returns a string with information on the HAL 45 | func LoRaGatewayVersionInfo() string { 46 | var versionInfo = C.GoString(C.lgw_version_info()) 47 | return versionInfo 48 | } 49 | 50 | // StartLoRaGateway wraps the HAL function to start the concentrator once configured 51 | func StartLoRaGateway() error { 52 | state := C.lgw_start() 53 | 54 | if state != C.LGW_HAL_SUCCESS { 55 | return errors.New("Failed to start concentrator") 56 | } 57 | return nil 58 | } 59 | 60 | // StopLoRaGateway wraps the HAL function to stop the concentrator once started 61 | func StopLoRaGateway() error { 62 | state := C.lgw_stop() 63 | 64 | if state != C.LGW_HAL_SUCCESS { 65 | return errors.New("Failed to stop concentrator gracefully") 66 | } 67 | return nil 68 | } 69 | 70 | // SetBoardConf wraps the HAL function to configure the concentrator's board 71 | func SetBoardConf(ctx log.Interface, conf util.Config) error { 72 | var boardConf = C.struct_lgw_conf_board_s{ 73 | clksrc: C.uint8_t(conf.Concentrator.Clksrc), 74 | lorawan_public: C.bool(conf.Concentrator.LorawanPublic), 75 | } 76 | 77 | if C.lgw_board_setconf(boardConf) != C.LGW_HAL_SUCCESS { 78 | return errors.New("Failed board configuration") 79 | } 80 | ctx.WithFields(log.Fields{ 81 | "ClockSource": conf.Concentrator.Clksrc, 82 | "LorawanPublic": conf.Concentrator.LorawanPublic, 83 | }).Info("SX1301 board configured") 84 | return nil 85 | } 86 | 87 | /* prepareTXLut takes the pointer to an empty C.struct_lgw_tx_gain_s, its configuration wrapped in Go, and transposes 88 | the configuration in the C.struct_lgw_tx_gain_s. It also increments the size of the TX Gain Lut table. */ 89 | func prepareTXLut(txLut *C.struct_lgw_tx_gain_s, txConf util.GainTableConf) { 90 | if txConf.DacGain != nil { 91 | txLut.dac_gain = C.uint8_t(*txConf.DacGain) 92 | } else { 93 | txLut.dac_gain = 3 94 | } 95 | txLut.dig_gain = C.uint8_t(txConf.DigGain) 96 | txLut.mix_gain = C.uint8_t(txConf.MixGain) 97 | txLut.rf_power = C.int8_t(txConf.RfPower) 98 | txLut.pa_gain = C.uint8_t(txConf.PaGain) 99 | } 100 | 101 | // SetTXGainConf prepares, and then sends the configuration of the TX Gain LUT to the concentrator 102 | func SetTXGainConf(ctx log.Interface, conc util.SX1301Conf) error { 103 | var gainLut = C.struct_lgw_tx_gain_lut_s{ 104 | size: 0, 105 | lut: [C.TX_GAIN_LUT_SIZE_MAX]C.struct_lgw_tx_gain_s{}, 106 | } 107 | txLuts := conc.GetTXLuts() 108 | for i, txLut := range txLuts { 109 | prepareTXLut(&gainLut.lut[i], txLut) 110 | } 111 | gainLut.size = C.uint8_t(len(txLuts)) 112 | 113 | if C.lgw_txgain_setconf(&gainLut) != C.LGW_HAL_SUCCESS { 114 | return errors.New("Failed to configure concentrator TX Gain LUT") 115 | } 116 | ctx.WithField("Indexes", gainLut.size).Info("Configured TX Lut") 117 | return nil 118 | } 119 | 120 | // initRadio initiates a radio configuration in the C.struct_lgw_conf_rxrf_s format, given 121 | // the configuration for that radio. 122 | func initRadio(radio util.RadioConf) (C.struct_lgw_conf_rxrf_s, error) { 123 | var cRadio = C.struct_lgw_conf_rxrf_s{ 124 | enable: C.bool(radio.Enabled), 125 | freq_hz: C.uint32_t(radio.Freq), 126 | rssi_offset: C.float(radio.RssiOffset), 127 | tx_enable: C.bool(radio.TxEnabled), 128 | } 129 | 130 | // Checking the radio is of a pre-defined type 131 | switch radio.RadioType { 132 | case "SX1257": 133 | C.setType(&cRadio, C.LGW_RADIO_TYPE_SX1257) 134 | case "SX1255": 135 | C.setType(&cRadio, C.LGW_RADIO_TYPE_SX1255) 136 | default: 137 | return cRadio, errors.New("Invalid radio type (should be SX1255 or SX1257)") 138 | } 139 | return cRadio, nil 140 | } 141 | 142 | // enableRadio is enabling the radio 143 | func enableRadio(ctx log.Interface, radio util.RadioConf, nb uint8) error { 144 | // Checking if radio is enabled and thus needs to be activated 145 | if !radio.Enabled { 146 | ctx.WithField("Radio", nb).Info("Radio disabled") 147 | return nil 148 | } 149 | 150 | cRadio, err := initRadio(radio) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | if C.lgw_rxrf_setconf(C.uint8_t(nb), cRadio) != C.LGW_HAL_SUCCESS { 156 | ctx.WithField("Radio", nb).Warn("Invalid configuration") 157 | return errors.New("Radio configuration failed") 158 | } 159 | 160 | ctx.WithFields(log.Fields{ 161 | "Radio": nb, 162 | "Type": radio.RadioType, 163 | "EnabledTX": radio.TxEnabled, 164 | "Frequency": radio.Freq, 165 | "RSSIOffset": radio.RssiOffset, 166 | }).Info("Radio configured") 167 | return nil 168 | } 169 | 170 | // SetRFChannels send the configuration of the radios to the concentrator 171 | func SetRFChannels(ctx log.Interface, conf util.Config) error { 172 | for i, radio := range conf.Concentrator.GetRadios() { 173 | err := enableRadio(ctx, radio, uint8(i)) 174 | if err != nil { 175 | return err 176 | } 177 | } 178 | 179 | return nil 180 | } 181 | 182 | func enableSFChannel(ctx log.Interface, channelConf util.ChannelConf, nb uint8) error { 183 | if !channelConf.Enabled { 184 | ctx.WithField("Channel", nb).Info("Lora multi-SF channel disabled") 185 | return nil 186 | } 187 | 188 | var cChannel = C.struct_lgw_conf_rxif_s{ 189 | enable: C.bool(channelConf.Enabled), 190 | rf_chain: C.uint8_t(channelConf.Radio), 191 | freq_hz: C.int32_t(channelConf.IfValue), 192 | } 193 | 194 | channelLog := ctx.WithField("Lora multi-SF channel", nb) 195 | if C.lgw_rxif_setconf(C.uint8_t(nb), cChannel) != C.LGW_HAL_SUCCESS { 196 | return errors.New(fmt.Sprintf("Missing configuration for SF channel %d", nb)) 197 | } 198 | channelLog.WithFields(log.Fields{ 199 | "RFChain": channelConf.Radio, 200 | "Freq": channelConf.IfValue, 201 | }).Info("LoRa multi-SF channel configured") 202 | return nil 203 | } 204 | 205 | // SetSFChannels enables the different SF channels 206 | func SetSFChannels(ctx log.Interface, conf util.Config) error { 207 | for i, sfChannel := range conf.Concentrator.GetMultiSFChannels() { 208 | err := enableSFChannel(ctx, sfChannel, uint8(i)) 209 | if err != nil { 210 | return err 211 | } 212 | } 213 | return nil 214 | } 215 | 216 | // initLoRaStdChannel initiates a C.struct_lgw_conf_rxif_s from a LoRaChannelConf 217 | func initLoRaStdChannel(stdChan util.ChannelConf) C.struct_lgw_conf_rxif_s { 218 | var cChannel = C.struct_lgw_conf_rxif_s{ 219 | enable: C.bool(stdChan.Enabled), 220 | rf_chain: C.uint8_t(stdChan.Radio), 221 | freq_hz: C.int32_t(stdChan.IfValue), 222 | } 223 | 224 | switch *stdChan.Bandwidth { 225 | case 125000, 250000, 500000: 226 | cChannel.bandwidth = loraChannelBandwidths[*stdChan.Bandwidth] 227 | default: 228 | cChannel.bandwidth = C.BW_UNDEFINED 229 | } 230 | 231 | if stdChan.Datarate != nil && *stdChan.Datarate >= 7 && *stdChan.Datarate <= 12 { 232 | cChannel.datarate = loraChannelSpreadingFactors[*stdChan.Datarate] 233 | } else { 234 | cChannel.datarate = C.DR_UNDEFINED 235 | } 236 | 237 | return cChannel 238 | } 239 | 240 | // SetStandardChannel enables the LoRa standard channel from the configuration 241 | func SetStandardChannel(ctx log.Interface, stdChan util.ChannelConf) error { 242 | if !stdChan.Enabled { 243 | ctx.Info("LoRa standard channel disabled") 244 | return nil 245 | } 246 | 247 | var cChannel = initLoRaStdChannel(stdChan) 248 | 249 | if C.lgw_rxif_setconf(8, cChannel) != C.LGW_HAL_SUCCESS { 250 | return errors.New("Configuration for LoRa standard channel failed") 251 | } 252 | return nil 253 | } 254 | 255 | // SetFSKChannel sets the FSK Channel configuration on the concentrator 256 | func SetFSKChannel(ctx log.Interface, fskChan util.ChannelConf) error { 257 | if !fskChan.Enabled { 258 | ctx.Info("FSK channel disabled") 259 | return nil 260 | } 261 | 262 | var cFSKChan = C.struct_lgw_conf_rxif_s{ 263 | enable: C.bool(fskChan.Enabled), 264 | rf_chain: C.uint8_t(fskChan.Radio), 265 | freq_hz: C.int32_t(fskChan.IfValue), 266 | bandwidth: C.BW_UNDEFINED, 267 | } 268 | if fskChan.Datarate != nil { 269 | cFSKChan.datarate = C.uint32_t(*fskChan.Datarate) 270 | } 271 | if fskChan.Bandwidth == nil { 272 | return errors.New("No bandwidth information in the configuration for the FSK channel - cannot retransmit the FSK packet") 273 | } 274 | 275 | val := *fskChan.Bandwidth 276 | switch { 277 | case val > 0 && val <= 7800: 278 | cFSKChan.bandwidth = loraChannelBandwidths[7800] 279 | case val > 7800 && val <= 15600: 280 | cFSKChan.bandwidth = loraChannelBandwidths[15600] 281 | case val > 15600 && val <= 31200: 282 | cFSKChan.bandwidth = loraChannelBandwidths[31200] 283 | case val > 31200 && val <= 62500: 284 | cFSKChan.bandwidth = loraChannelBandwidths[62500] 285 | case val > 62500 && val <= 125000: 286 | cFSKChan.bandwidth = loraChannelBandwidths[125000] 287 | case val > 125000 && val <= 250000: 288 | cFSKChan.bandwidth = loraChannelBandwidths[250000] 289 | case val > 250000 && val <= 500000: 290 | cFSKChan.bandwidth = loraChannelBandwidths[500000] 291 | } 292 | 293 | if C.lgw_rxif_setconf(9, cFSKChan) != C.LGW_HAL_SUCCESS { 294 | return errors.New("Configuration for FSK channel failed") 295 | } 296 | return nil 297 | } 298 | -------------------------------------------------------------------------------- /wrapper/downlinks_dummy.go: -------------------------------------------------------------------------------- 1 | // +build dummy 2 | 3 | package wrapper 4 | 5 | import ( 6 | "github.com/TheThingsNetwork/go-utils/log" 7 | "github.com/TheThingsNetwork/packet_forwarder/util" 8 | "github.com/TheThingsNetwork/ttn/api/router" 9 | ) 10 | 11 | func SendDownlink(downlink *router.DownlinkMessage, conf util.Config, ctx log.Interface) error { 12 | ctx.Info("Dummy HAL - Downlink accepted") 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /wrapper/downlinks_halV1.go: -------------------------------------------------------------------------------- 1 | // +build halv1 2 | 3 | package wrapper 4 | 5 | // #cgo CFLAGS: -I${SRCDIR}/../lora_gateway/libloragw/inc 6 | // #cgo LDFLAGS: -lm ${SRCDIR}/../lora_gateway/libloragw/libloragw.a 7 | // #include "config.h" 8 | // #include "loragw_hal.h" 9 | // #include "loragw_gps.h" 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | 16 | "github.com/TheThingsNetwork/go-utils/log" 17 | "github.com/TheThingsNetwork/packet_forwarder/util" 18 | "github.com/TheThingsNetwork/ttn/api/protocol/lorawan" 19 | "github.com/TheThingsNetwork/ttn/api/router" 20 | ) 21 | 22 | const ( 23 | stdFSKPreamble = 4 24 | stdLoRaPreamble = 8 25 | fieldInfo = 0 26 | crcPoly16 = uint16(0x1021) 27 | crcInitVal16 = uint16(0xFFFF) 28 | ) 29 | 30 | var coderateValueMap = map[string]C.uint8_t{ 31 | "4/5": C.CR_LORA_4_5, 32 | "4/6": C.CR_LORA_4_6, 33 | "2/3": C.CR_LORA_4_6, 34 | "4/7": C.CR_LORA_4_7, 35 | "4/8": C.CR_LORA_4_8, 36 | "1/2": C.CR_LORA_4_8, 37 | } 38 | 39 | func coderateValue(i string) (C.uint8_t, error) { 40 | if val, ok := coderateValueMap[i]; ok { 41 | return val, nil 42 | } 43 | return 0, errors.New("TX packet with unknown coderate") 44 | } 45 | 46 | func bandwidthValue(i uint32) (C.uint8_t, error) { 47 | if val, ok := loraChannelBandwidths[i]; ok { 48 | return val, nil 49 | } 50 | return 0, errors.New("TX packet with unknown bandwidth") 51 | } 52 | 53 | func sfValue(i uint32) (C.uint32_t, error) { 54 | if val, ok := loraChannelSpreadingFactors[i]; ok { 55 | return val, nil 56 | } 57 | return 0, errors.New("TX packet with unknown spreading factor") 58 | } 59 | 60 | func getLoRaDatarate(datarateStr string) (C.uint32_t, C.uint8_t, error) { 61 | var sf, bw uint32 62 | var err error 63 | var bandwidth C.uint8_t 64 | var spreadingFactor C.uint32_t 65 | nb, err := fmt.Sscanf(datarateStr, "SF%dBW%d", &sf, &bw) 66 | if err != nil { 67 | return 0, 0, err 68 | } 69 | 70 | if nb != 2 { 71 | return 0, 0, errors.New("Couldn't parse LoRa datarate for the downlink message - aborting this TX packet") 72 | } 73 | 74 | spreadingFactor, err = sfValue(sf) 75 | if err != nil { 76 | return 0, 0, errors.New("Couldn't read LoRa datarate for the downlink message (unknown Spreading Factor value)") 77 | } 78 | 79 | bandwidth, err = bandwidthValue(bw * 1000) 80 | if err != nil { 81 | return 0, 0, errors.New("Couldn't read LoRa datarate for the downlink message (unknown Bandwidth value)") 82 | } 83 | 84 | return spreadingFactor, bandwidth, nil 85 | } 86 | 87 | func setupLoRaDownlink(txPacket *C.struct_lgw_pkt_tx_s, downlink router.DownlinkMessage) error { 88 | txPacket.modulation = C.MOD_LORA 89 | var err error 90 | txPacket.datarate, txPacket.bandwidth, err = getLoRaDatarate(downlink.GetProtocolConfiguration().GetLorawan().GetDataRate()) 91 | if err != nil { 92 | return err 93 | } 94 | txPacket.coderate, err = coderateValue(downlink.GetProtocolConfiguration().GetLorawan().GetCodingRate()) 95 | if err != nil { 96 | return err 97 | } 98 | txPacket.invert_pol = C.bool(downlink.GetGatewayConfiguration().GetPolarizationInversion()) 99 | txPacket.preamble = C.uint16_t(stdLoRaPreamble) 100 | return nil 101 | } 102 | 103 | func setupFSKDownlink(txPacket *C.struct_lgw_pkt_tx_s, downlink router.DownlinkMessage) { 104 | txPacket.modulation = C.MOD_FSK 105 | txPacket.preamble = C.uint16_t(stdFSKPreamble) 106 | txPacket.datarate = C.uint32_t(downlink.GetProtocolConfiguration().GetLorawan().GetBitRate()) 107 | txPacket.f_dev = C.uint8_t(downlink.GetGatewayConfiguration().GetFrequencyDeviation() / 1000) /* gRPC value in Hz, txpkt.f_dev in kHz */ 108 | } 109 | 110 | func checkRFPower(cconf util.SX1301Conf, downlink router.DownlinkMessage) error { 111 | for _, val := range cconf.GetTXLuts() { 112 | if val.RfPower == int8(downlink.GetGatewayConfiguration().GetPower()) { 113 | return nil 114 | } 115 | } 116 | return errors.New("Unsupported RF Power for TX") 117 | } 118 | 119 | func setupDownlinkModulation(downlink router.DownlinkMessage, txPacket *C.struct_lgw_pkt_tx_s) error { 120 | if downlink.GetProtocolConfiguration().GetLorawan().GetModulation() == lorawan.Modulation_LORA { 121 | return setupLoRaDownlink(txPacket, downlink) 122 | } else if downlink.GetProtocolConfiguration().GetLorawan().GetModulation() == lorawan.Modulation_FSK { 123 | setupFSKDownlink(txPacket, downlink) 124 | return nil 125 | } 126 | return errors.New("Modulation neither LoRa nor FSK") 127 | } 128 | 129 | func insertPayload(downlink router.DownlinkMessage, txPacket *C.struct_lgw_pkt_tx_s) error { 130 | payload := downlink.GetPayload() 131 | if len(payload) > 256 { 132 | return errors.New("Payload too big to transmit") 133 | } 134 | txPacket.size = C.uint16_t(len(payload)) 135 | for i := 0; i < len(payload); i++ { 136 | txPacket.payload[i] = C.uint8_t(payload[i]) 137 | } 138 | return nil 139 | } 140 | 141 | func SendDownlink(downlink *router.DownlinkMessage, conf util.Config, ctx log.Interface) error { 142 | var txPacket = C.struct_lgw_pkt_tx_s{ 143 | freq_hz: C.uint32_t(downlink.GetGatewayConfiguration().GetFrequency()), 144 | rf_chain: C.uint8_t(downlink.GetGatewayConfiguration().GetRfChain()), 145 | no_crc: C.bool(false), 146 | no_header: C.bool(false), 147 | payload: [256]C.uint8_t{}, 148 | tx_mode: C.TIMESTAMPED, 149 | count_us: C.uint32_t(downlink.GetGatewayConfiguration().GetTimestamp()), 150 | } 151 | 152 | // Inserting payload 153 | if err := insertPayload(*downlink, &txPacket); err != nil { 154 | ctx.WithError(err).Warn("Failure parsing and wrapping the current TX packet - aborting transmission") 155 | return err 156 | } 157 | 158 | // Antenna gain 159 | if antennaGain := conf.Concentrator.AntennaGain; antennaGain != nil { 160 | txPacket.rf_power = C.int8_t(downlink.GetGatewayConfiguration().GetPower() - int32(*antennaGain)) 161 | } else { 162 | txPacket.rf_power = C.int8_t(downlink.GetGatewayConfiguration().GetPower()) 163 | } 164 | 165 | // LoRa/FSK parameters 166 | if err := setupDownlinkModulation(*downlink, &txPacket); err != nil { 167 | ctx.WithError(err).Warn("Failure parsing and wrapping the current TX packet during the parameter verification - aborting transmission") 168 | return err 169 | } 170 | 171 | // Checking RFPower 172 | if err := checkRFPower(conf.Concentrator, *downlink); err != nil { 173 | ctx.WithError(err).Warn("Failure parsing and wrapping the current TX packet during the RFPower check - aborting transmission") 174 | return err 175 | } 176 | 177 | return sendDownlinkConcentrator(txPacket, ctx) 178 | } 179 | 180 | func sendDownlinkConcentrator(txPacket C.struct_lgw_pkt_tx_s, ctx log.Interface) error { 181 | for { 182 | var txStatus C.uint8_t 183 | concentratorMutex.Lock() 184 | var result = C.lgw_status(C.TX_STATUS, &txStatus) 185 | concentratorMutex.Unlock() 186 | if result == C.LGW_HAL_ERROR { 187 | ctx.Warn("Couldn't get concentrator status") 188 | } else if txStatus == C.TX_EMITTING { 189 | // XX: Should we stop emission (like in the legacy packet forwarder) or retry? 190 | // If we retry, we might overwrite a normally scheduled downlink, that might 191 | // then not be relayed by the concentrator... 192 | ctx.Error("Concentrator is currently emitting") 193 | return errors.New("Concentrator is already emitting") 194 | } else if txStatus == C.TX_SCHEDULED { 195 | ctx.Warn("A downlink was already scheduled, overwriting it") 196 | } 197 | break 198 | } 199 | 200 | concentratorMutex.Lock() 201 | result := C.lgw_send(txPacket) 202 | concentratorMutex.Unlock() 203 | 204 | if result == C.LGW_HAL_ERROR { 205 | ctx.Warn("Downlink transmission to the concentrator failed") 206 | return errors.New("Downlink transmission to the concentrator failed") 207 | } 208 | 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /wrapper/gps_HALV1.go: -------------------------------------------------------------------------------- 1 | // +build halv1 2 | 3 | package wrapper 4 | 5 | // #cgo CFLAGS: -I${SRCDIR}/../lora_gateway/libloragw/inc 6 | // #cgo LDFLAGS: -lm ${SRCDIR}/../lora_gateway/libloragw/libloragw.a 7 | // #include "config.h" 8 | // #include "loragw_hal.h" 9 | // #include "loragw_gps.h" 10 | import "C" 11 | import ( 12 | "io" 13 | "os" 14 | "sync" 15 | "time" 16 | 17 | "github.com/TheThingsNetwork/go-utils/log" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | var gps *os.File 22 | 23 | var gpsTimeReference = C.struct_tref{} 24 | var gpsTimeReferenceMutex = &sync.Mutex{} 25 | 26 | var validCoordinates bool 27 | var coordinates GPSCoordinates 28 | var coordinatesMutex = &sync.Mutex{} 29 | 30 | var gpsMaxAge = time.Second * 30 31 | 32 | const bufferSize = 128 33 | 34 | func GetGPSCoordinates() (GPSCoordinates, error) { 35 | coordinatesMutex.Lock() 36 | defer coordinatesMutex.Unlock() 37 | tmpCoordinates := coordinates 38 | 39 | if !validCoordinates { 40 | return tmpCoordinates, errors.New("No valid coordinates obtained from GPS yet") 41 | } 42 | 43 | if !gpsActive() { 44 | return tmpCoordinates, errors.New("GPS not active") 45 | } 46 | 47 | return tmpCoordinates, nil 48 | } 49 | 50 | // timeReference returns the GPS time reference in a time.Time format 51 | func timeReference() time.Time { 52 | gpsTimeReferenceMutex.Lock() 53 | currentTimeReference := gpsTimeReference 54 | gpsTimeReferenceMutex.Unlock() 55 | return time.Unix(int64(currentTimeReference.systime), 0) 56 | } 57 | 58 | // LoRaGPSEnable acts as a wrapper for lgw_gps_enable 59 | func LoRaGPSEnable(TTYPath string) error { 60 | fd := C.int(0) 61 | 62 | // HAL only supports u-blox7 for now, so gps_family must be "ubx7" 63 | ok := (C.lgw_gps_enable(C.CString(TTYPath), C.CString("ubx7"), C.speed_t(0), &fd) == C.LGW_GPS_SUCCESS) 64 | if !ok { 65 | return errors.New("Failed GPS configuration - impossible to open port for GPS sync (check permissions?)") 66 | } 67 | 68 | gps = os.NewFile(uintptr(fd), "GPS") 69 | 70 | return nil 71 | } 72 | 73 | func gpsActive() bool { 74 | return gps != nil 75 | } 76 | 77 | func checkGPSTimeReference() bool { 78 | if !gpsActive() { 79 | return false 80 | } 81 | 82 | if timeReference().Add(gpsMaxAge).Before(time.Now()) { 83 | // GPS Time Reference considered obsolete 84 | return false 85 | } 86 | 87 | return true 88 | } 89 | 90 | func UpdateGPSData(ctx log.Interface) error { 91 | var ( 92 | coord C.struct_coord_s 93 | coordErr C.struct_coord_s 94 | ts C.uint32_t 95 | utcTime C.struct_timespec 96 | ) 97 | buffer := make([]byte, bufferSize) 98 | _, err := gps.Read(buffer) 99 | if err != nil && err != io.EOF { 100 | return errors.Wrap(err, "GPS interface read error") 101 | } 102 | 103 | gpsRawData := string(buffer[:]) 104 | 105 | nmea := C.lgw_parse_nmea(C.CString(gpsRawData), C.int(cap(buffer))) 106 | if nmea != C.NMEA_RMC { 107 | // No sync to do 108 | ctx.Debug("Unknown GPS status") 109 | return nil 110 | } 111 | 112 | ctx.Debug("Recommended Minimum sentence C received, triggering GPS sync") 113 | if C.lgw_gps_get(&utcTime, nil, nil) != C.LGW_GPS_SUCCESS { 114 | ctx.Debug("Couldn't get UTC time from GPS") 115 | return nil 116 | } 117 | 118 | ctx.Debug("Fetching GPS timestamp") 119 | concentratorMutex.Lock() 120 | ok := C.lgw_get_trigcnt(&ts) == C.LGW_GPS_SUCCESS 121 | concentratorMutex.Unlock() 122 | 123 | if !ok { 124 | ctx.Warn("Failed to read concentrator timestamp") 125 | return nil 126 | } 127 | 128 | ctx.Debug("Fetching GPS time reference") 129 | gpsTimeReferenceMutex.Lock() 130 | ok = C.lgw_gps_sync(&gpsTimeReference, ts, utcTime) == C.LGW_GPS_SUCCESS 131 | gpsTimeReferenceMutex.Unlock() 132 | 133 | if !ok { 134 | ctx.Warn("GPS out of sync, keeping previous time reference") 135 | return nil 136 | } 137 | ctx.WithField("GPSDateComputation", timeReference()).Debug("Date sync with GPS complete") 138 | 139 | ctx.Debug("Fetching GPS coordinates") 140 | coordinatesMutex.Lock() 141 | ok = C.lgw_gps_get(nil, &coord, &coordErr) != C.LGW_GPS_SUCCESS 142 | // For the moment, coordErr is unused, because the back-end doesn't handle the GPS's margin of error. 143 | // One possible improvement, if it is handled upstream, would be handling this. 144 | if !ok { 145 | ctx.Warn("Couldn't retrieve GPS coordinates") 146 | return nil 147 | } 148 | 149 | coordinates = GPSCoordinates{ 150 | Altitude: float64(coord.alt), 151 | Latitude: float64(coord.lat), 152 | Longitude: float64(coord.lon), 153 | } 154 | validCoordinates = true 155 | ctx.WithFields(log.Fields{"Altitude": coordinates.Altitude, "Latitude": coordinates.Latitude, "Longitude": coordinates.Longitude}).Info("GPS coordinates updated") 156 | coordinatesMutex.Unlock() 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /wrapper/gps_dummy.go: -------------------------------------------------------------------------------- 1 | // +build dummy 2 | 3 | package wrapper 4 | 5 | import "github.com/TheThingsNetwork/go-utils/log" 6 | 7 | func LoRaGPSEnable(TTYPath string) error { 8 | return nil 9 | } 10 | 11 | func GetGPSCoordinates() (GPSCoordinates, error) { 12 | return GPSCoordinates{}, nil 13 | } 14 | 15 | func UpdateGPSData(ctx log.Interface) error { 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /wrapper/uplinks_HALV1.go: -------------------------------------------------------------------------------- 1 | // +build halv1 2 | 3 | package wrapper 4 | 5 | // #cgo CFLAGS: -I${SRCDIR}/../lora_gateway/libloragw/inc 6 | // #cgo LDFLAGS: -lm ${SRCDIR}/../lora_gateway/libloragw/libloragw.a 7 | // #include "config.h" 8 | // #include "loragw_hal.h" 9 | // #include "loragw_gps.h" 10 | import "C" 11 | import "errors" 12 | import "time" 13 | 14 | import "github.com/TheThingsNetwork/ttn/api/gateway" 15 | 16 | const NbMaxPackets = 8 17 | const nbRadios = C.LGW_RF_CHAIN_NB 18 | 19 | const StatusCRCOK = uint8(C.STAT_CRC_OK) 20 | const StatusCRCBAD = uint8(C.STAT_CRC_BAD) 21 | const StatusNOCRC = uint8(C.STAT_NO_CRC) 22 | 23 | const ModulationLoRa = uint8(C.MOD_LORA) 24 | const ModulationFSK = uint8(C.MOD_FSK) 25 | 26 | var datarateString = map[uint32]string{ 27 | uint32(C.DR_LORA_SF7): "SF7", 28 | uint32(C.DR_LORA_SF8): "SF8", 29 | uint32(C.DR_LORA_SF9): "SF9", 30 | uint32(C.DR_LORA_SF10): "SF10", 31 | uint32(C.DR_LORA_SF11): "SF11", 32 | uint32(C.DR_LORA_SF12): "SF12", 33 | } 34 | 35 | var bandwidthString = map[uint8]string{ 36 | uint8(C.BW_125KHZ): "BW125", 37 | uint8(C.BW_250KHZ): "BW250", 38 | uint8(C.BW_500KHZ): "BW500", 39 | } 40 | 41 | var coderateString = map[uint8]string{ 42 | uint8(C.CR_LORA_4_5): "4/5", 43 | uint8(C.CR_LORA_4_6): "4/6", 44 | uint8(C.CR_LORA_4_7): "4/7", 45 | uint8(C.CR_LORA_4_8): "4/8", 46 | 0: "OFF", 47 | } 48 | 49 | // gpsReference is used to pass the GPS reference when building packets 50 | type gpsReference struct { 51 | valid bool 52 | validTimeReference bool 53 | timeReference C.struct_tref 54 | locationReference GPSCoordinates 55 | } 56 | 57 | func packetsFromCPackets(cPackets [8]C.struct_lgw_pkt_rx_s, nbPackets int) []Packet { 58 | var packetReference gpsReference 59 | if gpsActive() { 60 | // Using one global gpsReference avoids having one mutex lock per packet 61 | packetReference.valid = true 62 | packetReference.validTimeReference = checkGPSTimeReference() 63 | gpsTimeReferenceMutex.Lock() 64 | packetReference.timeReference = gpsTimeReference 65 | gpsTimeReferenceMutex.Unlock() 66 | coordinatesMutex.Lock() 67 | packetReference.locationReference = coordinates 68 | coordinatesMutex.Unlock() 69 | } 70 | 71 | var packets = make([]Packet, nbPackets) 72 | for i := 0; i < nbPackets && i < 8; i++ { 73 | packets[i] = packetFromCPacket(cPackets[i], packetReference) 74 | } 75 | return packets 76 | } 77 | 78 | func packetFromCPacket(cPacket C.struct_lgw_pkt_rx_s, currentReference gpsReference) Packet { 79 | // When using packetFromCPacket, it is assumed that accessing gpsTimeReferenceMutex 80 | // is safe => Use gpsTimeReferenceMutex before calling packetFromCPacket /before/ 81 | // using this function 82 | var p = Packet{ 83 | Freq: uint32(cPacket.freq_hz), 84 | IFChain: uint8(cPacket.if_chain), 85 | Status: uint8(cPacket.status), 86 | CountUS: uint32(cPacket.count_us), 87 | RFChain: uint8(cPacket.rf_chain), 88 | Modulation: uint8(cPacket.modulation), 89 | Bandwidth: uint8(cPacket.bandwidth), 90 | Datarate: uint32(cPacket.datarate), 91 | Coderate: uint8(cPacket.coderate), 92 | RSSI: float32(cPacket.rssi), 93 | SNR: float32(cPacket.snr), 94 | MinSNR: float32(cPacket.snr_min), 95 | MaxSNR: float32(cPacket.snr_max), 96 | CRC: uint16(cPacket.crc), 97 | Size: uint32(cPacket.size), 98 | } 99 | 100 | p.Payload = make([]byte, p.Size) 101 | var i uint32 102 | for i = 0; i < p.Size; i++ { 103 | p.Payload[i] = byte(cPacket.payload[i]) 104 | } 105 | 106 | if currentReference.valid { 107 | p.Gps = &gateway.GPSMetadata{ 108 | Latitude: float32(currentReference.locationReference.Latitude), 109 | Longitude: float32(currentReference.locationReference.Longitude), 110 | Altitude: int32(currentReference.locationReference.Altitude), 111 | } 112 | 113 | var pktUtcTime C.struct_timespec 114 | if currentReference.validTimeReference && C.lgw_cnt2utc(currentReference.timeReference, cPacket.count_us, &pktUtcTime) == C.LGW_GPS_SUCCESS { 115 | // conversion successful 116 | p.Time = time.Unix(int64(pktUtcTime.tv_sec), int64(pktUtcTime.tv_nsec)).UnixNano() 117 | p.Gps.Time = p.Time 118 | } 119 | } 120 | return p 121 | } 122 | 123 | func Receive() ([]Packet, error) { 124 | var packets [NbMaxPackets]C.struct_lgw_pkt_rx_s 125 | concentratorMutex.Lock() 126 | nbPackets := C.lgw_receive(NbMaxPackets, &packets[0]) 127 | concentratorMutex.Unlock() 128 | if nbPackets == C.LGW_HAL_ERROR { 129 | return nil, errors.New("Failed packet fetch from the concentrator") 130 | } 131 | return packetsFromCPackets(packets, int(nbPackets)), nil 132 | } 133 | -------------------------------------------------------------------------------- /wrapper/uplinks_dummy.go: -------------------------------------------------------------------------------- 1 | // +build dummy 2 | 3 | package wrapper 4 | 5 | import "math/rand" 6 | 7 | const ( 8 | StatusCRCOK = uint8(0) 9 | StatusCRCBAD = uint8(1) 10 | StatusNOCRC = uint8(2) 11 | 12 | ModulationLoRa = uint8(0) 13 | ModulationFSK = uint8(1) 14 | 15 | NbMaxPackets = 8 16 | ) 17 | 18 | // Randomly return 1 empty packet, once every 5000 times (since there's one query per 5 milliseconds) 19 | 20 | func Receive() ([]Packet, error) { 21 | packets := make([]Packet, 0) 22 | if rand.Float64() <= 0.0002 { 23 | dummyPacket := Packet{ 24 | Payload: make([]byte, 0), 25 | } 26 | packets = append(packets, dummyPacket) 27 | } 28 | return packets, nil 29 | } 30 | 31 | var datarateString = map[uint32]string{ 32 | uint32(0): "SF7", 33 | uint32(1): "SF8", 34 | uint32(2): "SF9", 35 | uint32(3): "SF10", 36 | uint32(4): "SF11", 37 | uint32(5): "SF12", 38 | } 39 | 40 | var bandwidthString = map[uint8]string{ 41 | uint8(0): "BW125", 42 | uint8(1): "BW250", 43 | uint8(2): "BW500", 44 | } 45 | 46 | var coderateString = map[uint8]string{ 47 | uint8(4): "4/5", 48 | uint8(1): "4/6", 49 | uint8(2): "4/7", 50 | uint8(3): "4/8", 51 | 0: "OFF", 52 | } 53 | --------------------------------------------------------------------------------