├── .gitattributes ├── .gitignore ├── 1_Pi_Config.sh ├── 2_Setup.sh ├── 3_WiFi_AP.sh ├── 4_WiFi_Docker.sh ├── 5_GPS.sh ├── README.md ├── build_legacy.sh ├── config ├── 90-wireless.rules ├── dnsmasq.conf ├── hostapd.conf ├── interfaces └── rc.local ├── doc ├── AccessPoint.md ├── DisplayOled.md └── Lorawan-Server.md ├── loragw.service ├── monitor-gpio-helium.py ├── monitor-gpio.py ├── monitor-ws2812.py ├── monitor.service ├── oled.py ├── oled.service ├── sensors_js ├── install.sh ├── sensors-js.service └── sensors.js ├── set_config.py ├── start.sh ├── testled.js ├── testled.py └── wificfg.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | www/ 2 | 3 | # Object files 4 | *.o 5 | *.ko 6 | *.obj 7 | *.elf 8 | 9 | # Precompiled Headers 10 | *.gch 11 | *.pch 12 | 13 | # Libraries 14 | *.lib 15 | *.a 16 | *.la 17 | *.lo 18 | 19 | # Shared objects (inc. Windows DLLs) 20 | *.dll 21 | *.so 22 | *.so.* 23 | *.dylib 24 | 25 | # Executables 26 | *.exe 27 | *.out 28 | *.app 29 | *.i*86 30 | *.x86_64 31 | *.hex 32 | 33 | # Debug files 34 | *.dSYM/ 35 | -------------------------------------------------------------------------------- /1_Pi_Config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $(id -u) -ne 0 ]; then 4 | echo "Installer must be run as root." 5 | echo "Try 'sudo bash $0'" 6 | exit 1 7 | fi 8 | 9 | MODEL=`cat /proc/device-tree/model` 10 | 11 | echo "This script configure a Raspberry Pi" 12 | echo "Raspian for being a LoRaWAN Gateway" 13 | echo 14 | echo "Device is $MODEL" 15 | echo 16 | echo "Run time ~5 minutes. Reboot required." 17 | echo 18 | echo -n "CONTINUE? [Y/n] " 19 | read 20 | if [[ "$REPLY" =~ ^(no|n|N)$ ]]; then 21 | echo "Canceled." 22 | exit 0 23 | fi 24 | 25 | # These functions have been copied from excellent Adafruit Read only tutorial 26 | # https://github.com/adafruit/Raspberry-Pi-Installer-Scripts/blob/master/read-only-fs.sh 27 | # the one inspired by my original article http://hallard.me/raspberry-pi-read-only/ 28 | # That's an excellent demonstration of collaboration and open source sharing 29 | # 30 | # Given a filename, a regex pattern to match and a replacement string: 31 | # Replace string if found, else no change. 32 | # (# $1 = filename, $2 = pattern to match, $3 = replacement) 33 | replace() { 34 | grep $2 $1 >/dev/null 35 | if [ $? -eq 0 ]; then 36 | # Pattern found; replace in file 37 | sed -i "s/$2/$3/g" $1 >/dev/null 38 | fi 39 | } 40 | 41 | # Given a filename, a regex pattern to match and a replacement string: 42 | # If found, perform replacement, else append file w/replacement on new line. 43 | replaceAppend() { 44 | grep $2 $1 >/dev/null 45 | if [ $? -eq 0 ]; then 46 | # Pattern found; replace in file 47 | sed -i "s/$2/$3/g" $1 >/dev/null 48 | else 49 | # Not found; append on new line (silently) 50 | echo $3 | sudo tee -a $1 >/dev/null 51 | fi 52 | } 53 | 54 | # Given a filename, a regex pattern to match and a string: 55 | # If found, no change, else append file with string on new line. 56 | append1() { 57 | grep $2 $1 >/dev/null 58 | if [ $? -ne 0 ]; then 59 | # Not found; append on new line (silently) 60 | echo $3 | sudo tee -a $1 >/dev/null 61 | fi 62 | } 63 | 64 | # Given a list of strings representing options, display each option 65 | # preceded by a number (1 to N), display a prompt, check input until 66 | # a valid number within the selection range is entered. 67 | selectN() { 68 | for ((i=1; i<=$#; i++)); do 69 | echo $i. ${!i} 70 | done 71 | echo 72 | REPLY="" 73 | while : 74 | do 75 | echo -n "SELECT 1-$#: " 76 | read 77 | if [[ $REPLY -ge 1 ]] && [[ $REPLY -le $# ]]; then 78 | return $REPLY 79 | fi 80 | done 81 | } 82 | 83 | echo "Updating dependencies" 84 | apt-get update && apt-get upgrade -y --force-yes && apt-get update 85 | apt-get install -y --force-yes git-core build-essential ntp scons i2c-tools 86 | 87 | echo "Updating python dependencies" 88 | apt-get install -y --force-yes python-dev swig python-psutil python-rpi.gpio python-pip 89 | python -m pip install --upgrade pip setuptools wheel 90 | 91 | if [[ ! -d /home/loragw ]]; then 92 | echo "Adding new user loragw, enter it password" 93 | useradd -m loragw -s /bin/bash 94 | passwd loragw 95 | usermod -a -G sudo loragw 96 | cp /etc/sudoers.d/010_pi-nopasswd /etc/sudoers.d/010_loragw-nopasswd 97 | sed -i -- 's/pi/loragw/g' /etc/sudoers.d/010_loragw-nopasswd 98 | cp /home/pi/.profile /home/loragw/ 99 | cp /home/pi/.bashrc /home/loragw/ 100 | chown loragw:loragw /home/loragw/.* 101 | usermod -a -G i2c,spi,gpio loragw 102 | fi 103 | 104 | echo "Enabling Uart, I2C, SPI, Video Memory to 16MB" 105 | replaceAppend /boot/config.txt "^.*enable_uart.*$" "enable_uart=1" 106 | replaceAppend /boot/config.txt "^.*dtparam=i2c_arm=.*$" "dtparam=i2c_arm=on" 107 | replaceAppend /boot/config.txt "^.*dtparam=spi=.*$" "dtparam=spi=on" 108 | replaceAppend /boot/config.txt "^.*gpu_mem=.*$" "gpu_mem=16" 109 | replaceAppend /etc/modules "^.*i2c-dev.*$" "i2c-dev" 110 | 111 | echo -n "Do you want to configure timezone [y/N] " 112 | read 113 | if [[ "$REPLY" =~ ^(yes|y|Y)$ ]]; then 114 | echo "Reconfiguring Time Zone." 115 | dpkg-reconfigure tzdata 116 | fi 117 | 118 | if [[ ! -f /usr/local/bin/log2ram ]]; then 119 | echo -n "Do you want to enable log2ram [y/N] " 120 | read 121 | if [[ "$REPLY" =~ ^(yes|y|Y)$ ]]; then 122 | echo "Setting up log2ram." 123 | git clone https://github.com/azlux/log2ram.git 124 | cd log2ram 125 | chmod +x install.sh uninstall.sh 126 | ./install.sh 127 | ln -s /usr/local/bin/log2ram /etc/cron.hourly/ 128 | echo "cleaning up log rotation" 129 | replace /etc/logrotage.d/rsyslog "^.*daily.*$" " hourly" 130 | replace /etc/logrotage.d/rsyslog "^.*monthly.*$" " daily" 131 | replace /etc/logrotage.d/rsyslog "^.*delaycompress.*$" " " 132 | 133 | echo "forcing one log rotation" 134 | logrotate /etc/logrotate.conf 135 | echo "Please don't forget to adjust the logrotate" 136 | echo "paratemeters in /etc/logrotage.d/* to avoid" 137 | echo "filling up the ramdisk, see README in" 138 | echo "https://github.com/ch2i/LoraGW-Setup/" 139 | echo "" 140 | fi 141 | fi 142 | 143 | # set hostname to loragw-xxyy with xxyy last MAC Address digits 144 | set -- `cat /sys/class/net/wlan0/address` 145 | IFS=":"; declare -a Array=($*) 146 | NEWHOST=loragw-${Array[4]}${Array[5]} 147 | 148 | echo "" 149 | echo "Please select new device name (hostname)" 150 | selectN "Leave as $HOSTNAME" "loragw" "$NEWHOST" 151 | SEL=$? 152 | if [[ $SEL -gt 1 ]]; then 153 | if [[ $SEL == 2 ]]; then 154 | NEWHOST=loragw 155 | fi 156 | sudo bash -c "echo $NEWHOST" > /etc/hostname 157 | replace /etc/hosts "^127.0.1.1.*$HOSTNAME.*$" "127.0.1.1\t$NEWHOST" 158 | echo "New hostname set to $NEWHOST" 159 | else 160 | echo "hostname unchanged" 161 | fi 162 | 163 | echo "Done." 164 | echo 165 | echo "Settings take effect on next boot." 166 | echo "after reboot, login back here with" 167 | echo "ssh loragw@$NEWHOST.local" 168 | echo 169 | echo -n "REBOOT NOW? [y/N] " 170 | read 171 | if [[ ! "$REPLY" =~ ^(yes|y|Y)$ ]]; then 172 | echo "Exiting without reboot." 173 | exit 0 174 | fi 175 | echo "Reboot started..." 176 | reboot 177 | exit 0 178 | 179 | -------------------------------------------------------------------------------- /2_Setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INSTALL_DIR="/opt/loragw" 4 | MODEL=`cat /proc/device-tree/model` 5 | 6 | if [ $(id -u) -ne 0 ]; then 7 | echo "Installer must be run as root." 8 | echo "Try 'sudo bash $0'" 9 | exit 1 10 | fi 11 | 12 | echo "This script configures a Raspberry Pi" 13 | echo "as a LoRaWAN Gateway connected to TTN," 14 | echo 15 | echo "It will install the following dependencies" 16 | echo "nodejs, git, pyhton, ntp, scons, ws2812" 17 | echo "compile packet forwarder and install the" 18 | echo "needed services (Monitor, Oled, GW) at boot" 19 | echo 20 | echo "Device is $MODEL" 21 | echo 22 | echo "Run time ~5 to 15 minutes depending on features." 23 | echo 24 | echo -n "CONTINUE? [Y/n] " 25 | read 26 | if [[ "$REPLY" =~ ^(no|n|N)$ ]]; then 27 | echo "Canceled." 28 | exit 0 29 | fi 30 | 31 | # Given a filename, a regex pattern to match and a replacement string: 32 | # Replace string if found, else no change. 33 | # (# $1 = filename, $2 = pattern to match, $3 = replacement) 34 | replace() { 35 | grep $2 $1 >/dev/null 36 | if [ $? -eq 0 ]; then 37 | # Pattern found; replace in file 38 | sed -i "s/$2/$3/g" $1 >/dev/null 39 | fi 40 | } 41 | 42 | # Given a filename, a regex pattern to match and a replacement string: 43 | # If found, perform replacement, else append file w/replacement on new line. 44 | replaceAppend() { 45 | grep $2 $1 >/dev/null 46 | if [ $? -eq 0 ]; then 47 | # Pattern found; replace in file 48 | sed -i "s/$2/$3/g" $1 >/dev/null 49 | else 50 | # Not found; append on new line (silently) 51 | echo $3 | sudo tee -a $1 >/dev/null 52 | fi 53 | } 54 | 55 | # Given a filename, a regex pattern to match and a string: 56 | # If found, no change, else append file with string on new line. 57 | append1() { 58 | grep $2 $1 >/dev/null 59 | if [ $? -ne 0 ]; then 60 | # Not found; append on new line (silently) 61 | echo $3 | sudo tee -a $1 >/dev/null 62 | fi 63 | } 64 | 65 | # These functions have been copied from excellent Adafruit Read only tutorial 66 | # https://github.com/adafruit/Raspberry-Pi-Installer-Scripts/blob/master/read-only-fs.sh 67 | # the one inspired by my original article http://hallard.me/raspberry-pi-read-only/ 68 | # That's an excellent demonstration of collaboration and open source sharing 69 | # 70 | # Given a list of strings representing options, display each option 71 | # preceded by a number (1 to N), display a prompt, check input until 72 | # a valid number within the selection range is entered. 73 | selectN() { 74 | for ((i=1; i<=$#; i++)); do 75 | echo $i. ${!i} 76 | done 77 | echo 78 | REPLY="" 79 | while : 80 | do 81 | echo -n "SELECT 1-$#: " 82 | read 83 | if [[ $REPLY -ge 1 ]] && [[ $REPLY -le $# ]]; then 84 | return $REPLY 85 | fi 86 | done 87 | } 88 | 89 | echo "" 90 | echo "Target board/shield for this $MODEL:" 91 | selectN "CH2i RAK831 Minimal" "CH2i RAK831 with WS2812B Led" "CH2i ic880a" "IMST Lora Lite (ic880a)" "RAK831 official shield" "All other models" 92 | BOARD_TARGET=$? 93 | if [[ $BOARD_TARGET == 1 ]]; then 94 | GW_RESET_PIN=25 95 | MONITOR_SCRIPT=monitor-ws2812.py 96 | export GW_RESET_PIN 97 | fi 98 | if [[ $BOARD_TARGET == 2 ]]; then 99 | GW_RESET_PIN=25 100 | MONITOR_SCRIPT=monitor-ws2812.py 101 | export GW_RESET_PIN 102 | fi 103 | if [[ $BOARD_TARGET == 4 ]]; then 104 | GW_RESET_PIN=5 105 | export GW_RESET_PIN 106 | fi 107 | if [[ $BOARD_TARGET == 3 ]]; then 108 | GW_RESET_PIN=17 109 | MONITOR_SCRIPT=monitor-gpio.py 110 | export GW_RESET_PIN 111 | fi 112 | if [[ $BOARD_TARGET == 5 ]]; then 113 | GW_RESET_PIN=17 114 | export GW_RESET_PIN 115 | fi 116 | 117 | if [[ $MONITOR_SCRIPT == "" ]]; then 118 | echo "No monitoring script" 119 | else 120 | echo "Monitor script is $MONITOR_SCRIPT" 121 | fi 122 | echo "GW Reset pin is GPIO$GW_RESET_PIN" 123 | 124 | echo "" 125 | echo -n "Do you want to build Kersing packet forwarder [Y/n] " 126 | read BUILD_GW 127 | 128 | echo "" 129 | echo -n "Do you want to build legacy packet forwarder [y/N] " 130 | read BUILD_LEGACY 131 | 132 | echo "" 133 | echo "You can enable monitor service that manage blinking led to" 134 | echo "display status and also add button management to shutdown PI" 135 | echo -n "Would you like to enable this [Y/n] " 136 | read EN_MONITOR 137 | 138 | echo "" 139 | echo "If you a OLED display, You can enable OLED service that" 140 | echo "display some system information and LoRaWAN packet info" 141 | echo -n "Do you want to install I2C OLED [y/N] " 142 | read EN_OLED 143 | 144 | echo "" 145 | echo -n "Do you want to setup TTN [Y/n] " 146 | read EN_TTN 147 | if [[ ! "$EN_TTN" =~ ^(no|n|N)$ ]]; then 148 | 149 | echo "It's now time to create and configure your gateway on TTN" 150 | echo "See https://www.thethingsnetwork.org/docs/gateways/registration.html#via-gateway-connector" 151 | echo "once done, grab your gateway id (GW_ID) and key (GW_KEY) and paste them here" 152 | 153 | if [[ $GW_ID == "" ]]; then 154 | echo "No environement for GW_ID" 155 | echo "" 156 | echo -n "Please enter GW_ID: " 157 | read GW_ID 158 | export GW_ID 159 | fi 160 | 161 | if [[ $GW_KEY == "" ]]; then 162 | echo "No environement for GW_KEY" 163 | echo "See https://www.thethingsnetwork.org/docs/gateways/registration.html#via-gateway-connector" 164 | echo "" 165 | echo -n "Please enter GW_KEY: " 166 | read GW_KEY 167 | export GW_KEY 168 | fi 169 | 170 | if [[ $GW_RESET_PIN == "" ]]; then 171 | GW_RESET_PIN=25 172 | echo "No environement for GW_RESET_PIN" 173 | echo "Please select your reset pin" 174 | echo "see https://github.com/jpmeijers/ttn-resin-gateway-rpi/blob/master/README.md" 175 | echo "under section Reset Pin Values" 176 | echo "enter 25 (GPIO25) for this RAK831 shield or classic Gonzalo Casas" 177 | echo "enter 17 (GPIO17) for classic ic880A by CH2i" 178 | echo "" 179 | echo -n "Please enter GW_RESET_PIN [$GW_RESET_PIN]:" 180 | read GW_RESET_PIN 181 | export GW_RESET_PIN 182 | fi 183 | fi 184 | 185 | # Set the reset în in startup shell 186 | replace ./start.sh "^.*RESET_BCM_PIN=.*$" "SX1301_RESET_BCM_PIN=$GW_RESET_PIN" 187 | 188 | grep "Pi\ 3" /proc/device-tree/model >/dev/null 189 | if [ $? -eq 0 ]; then 190 | echo "Installing nodejs v10 for Raspberry PI 3" 191 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 192 | apt-get install nodejs 193 | 194 | # iC880a and RPI 3 Setup Activity LED and Power OFF Led 195 | if [[ $BOARD_TARGET == 3 ]]; then 196 | replaceAppend /boot/config.txt "^dtoverlay=gpio-poweroff.*$" "dtoverlay=gpio-poweroff,gpiopin=24" 197 | replaceAppend /boot/config.txt "^dtoverlay=pi3-act-led.*$" "dtoverlay=pi3-act-led,gpio=23" 198 | fi 199 | 200 | fi 201 | 202 | grep "Pi\ Zero" /proc/device-tree/model >/dev/null 203 | if [ $? -eq 0 ]; then 204 | echo "Installing nodejs lts for Raspberry PI Zero" 205 | wget -O - https://raw.githubusercontent.com/sdesalas/node-pi-zero/master/install-node-v.lts.sh | bash 206 | append1 /home/loragw/.profile "^.*PATH:/opt/nodejs/bin.*$" "export PATH=$PATH:/opt/nodejs/bin" 207 | append1 /home/loragw/.profile "^.*NODE_PATH=.*$" "NODE_PATH=/opt/nodejs/lib/node_modules" 208 | fi 209 | 210 | apt-get -y install protobuf-compiler libprotobuf-dev libprotoc-dev automake libtool autoconf 211 | 212 | # Board has WS1812B LED 213 | if [[ $BOARD_TARGET == 2 ]]; then 214 | echo "Installing WS2812B LED driver" 215 | cd /home/loragw/ 216 | 217 | echo "Blacklisting snd_bcm2835 module due to WS2812b LED PWM" 218 | touch /etc/modprobe.d/snd-blacklist.conf 219 | append1 /etc/modprobe.d/snd-blacklist.conf "^.*snd_bcm2835.*$" "blacklist snd_bcm2835" 220 | 221 | echo "Installing WS2812B drivers and libraries" 222 | git clone https://github.com/jgarff/rpi_ws281x 223 | cd rpi_ws281x/ 224 | scons 225 | scons deb 226 | dpkg -i libws2811*.deb 227 | cp ws2811.h /usr/local/include/ 228 | cp rpihw.h /usr/local/include/ 229 | cp pwm.h /usr/local/include/ 230 | cd python 231 | python ./setup.py build 232 | python setup.py install 233 | cd /home/loragw/ 234 | npm install -g --unsafe-perm rpi-ws281x-native 235 | npm link rpi-ws281x-native 236 | # We're sudo reset owner 237 | chown -R loragw:loragw /home/loragw/rpi_ws281x 238 | chown -R loragw:loragw /home/loragw/node_modules 239 | fi 240 | 241 | if [[ "$EN_OLED" =~ ^(yes|y|Y)$ ]]; then 242 | echo "Configuring and installing OLED driver" 243 | cd /home/loragw/ 244 | replaceAppend /boot/config.txt "^.*dtparam=i2c_arm=.*$" "dtparam=i2c_arm=on,i2c_baudrate=400000" 245 | apt-get install -y --force-yes libfreetype6-dev libjpeg-dev 246 | 247 | echo "Install luma OLED core" 248 | sudo -H pip install --upgrade luma.oled 249 | 250 | echo "Get examples files (and font)" 251 | mkdir -p /usr/share/fonts/truetype/luma 252 | git clone https://github.com/rm-hull/luma.examples.git 253 | # We're sudo reset owner 254 | chown -R loragw:loragw /home/loragw/luma.examples 255 | cp luma.examples/examples/fonts/*.ttf /usr/share/fonts/truetype/luma/ 256 | 257 | #echo "Build examples files" 258 | #sudo apt install libsdl-dev libportmidi-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev libsdl-image1.2-dev 259 | #sudo pip install --upgrade setuptools 260 | #cd luma.examples 261 | #udo -H pip install -e . 262 | fi 263 | 264 | if [[ ! "$BUILD_GW" =~ ^(no|n|N)$ ]]; then 265 | echo "Building LoraGW and kersing packet Forwarder" 266 | mkdir -p $INSTALL_DIR/dev 267 | cd $INSTALL_DIR/dev 268 | 269 | if [ ! -d lora_gateway ]; then 270 | git clone https://github.com/kersing/lora_gateway.git || { echo 'Cloning lora_gateway failed.' ; exit 1; } 271 | else 272 | cd lora_gateway 273 | git reset --hard 274 | git pull 275 | cd .. 276 | fi 277 | 278 | if [ ! -d paho.mqtt.embedded-c ]; then 279 | git clone https://github.com/kersing/paho.mqtt.embedded-c.git || { echo 'Cloning paho mqtt failed.' ; exit 1; } 280 | else 281 | cd paho.mqtt.embedded-c 282 | git reset --hard 283 | git pull 284 | cd .. 285 | fi 286 | 287 | if [ ! -d ttn-gateway-connector ]; then 288 | git clone https://github.com/kersing/ttn-gateway-connector.git || { echo 'Cloning gateway connector failed.' ; exit 1; } 289 | else 290 | cd ttn-gateway-connector 291 | git reset --hard 292 | git pull 293 | cd .. 294 | fi 295 | 296 | if [ ! -d protobuf-c ]; then 297 | git clone https://github.com/kersing/protobuf-c.git || { echo 'Cloning protobuf-c failed.' ; exit 1; } 298 | else 299 | cd protobuf-c 300 | git reset --hard 301 | git pull 302 | cd .. 303 | fi 304 | 305 | if [ ! -d packet_forwarder ]; then 306 | git clone https://github.com/kersing/packet_forwarder.git || { echo 'Cloning packet forwarder failed.' ; exit 1; } 307 | else 308 | cd packet_forwarder 309 | git reset --hard 310 | git pull 311 | cd .. 312 | fi 313 | 314 | if [ ! -d protobuf ]; then 315 | git clone https://github.com/google/protobuf.git || { echo 'Cloning protobuf failed.' ; exit 1; } 316 | else 317 | cd protobuf 318 | git reset --hard 319 | git pull 320 | cd .. 321 | fi 322 | 323 | cd $INSTALL_DIR/dev/lora_gateway/libloragw 324 | sed -i -e 's/PLATFORM= .*$/PLATFORM= imst_rpi/g' library.cfg 325 | sed -i -e 's/CFG_SPI= .*$/CFG_SPI= native/g' library.cfg 326 | make 327 | 328 | cd $INSTALL_DIR/dev/protobuf-c 329 | ./autogen.sh 330 | ./configure 331 | make protobuf-c/libprotobuf-c.la 332 | mkdir bin 333 | ./libtool install /usr/bin/install -c protobuf-c/libprotobuf-c.la `pwd`/bin 334 | rm -f `pwd`/bin/*so* 335 | 336 | cd $INSTALL_DIR/dev/paho.mqtt.embedded-c/ 337 | make 338 | make install 339 | 340 | cd $INSTALL_DIR/dev/ttn-gateway-connector 341 | cp config.mk.in config.mk 342 | make 343 | cp bin/libttn-gateway-connector.so /usr/lib/ 344 | 345 | cd $INSTALL_DIR/dev/packet_forwarder/mp_pkt_fwd/ 346 | make 347 | 348 | # Copy things needed at runtime to where they'll be expected 349 | cp $INSTALL_DIR/dev/packet_forwarder/mp_pkt_fwd/mp_pkt_fwd $INSTALL_DIR/mp_pkt_fwd 350 | 351 | if [ ! -f $INSTALL_DIR/mp_pkt_fwd ]; then 352 | echo "Oup's, something went wrong, kersing forwarder not found" 353 | echo "please check for any build error" 354 | else 355 | echo "Build & Installation Completed." 356 | echo "kersing forwarder is located at $INSTALL_DIR/mp_pkt_fwd" 357 | echo "" 358 | fi 359 | fi 360 | 361 | 362 | if [[ "$BUILD_LEGACY" =~ ^(yes|y|Y)$ ]]; then 363 | echo "Building legacy packet Forwarder" 364 | 365 | mkdir -p $INSTALL_DIR/dev 366 | mkdir -p $INSTALL_DIR/dev/legacy 367 | cd $INSTALL_DIR/dev/legacy 368 | 369 | # Build legacy loragw library 370 | if [ ! -d lora_gateway ]; then 371 | git clone https://github.com/TheThingsNetwork/lora_gateway.git 372 | pushd lora_gateway 373 | else 374 | pushd lora_gateway 375 | git reset --hard 376 | git pull 377 | fi 378 | sed -i -e 's/PLATFORM= .*$/PLATFORM= imst_rpi/g' ./libloragw/library.cfg 379 | sed -i -e 's/CFG_SPI= .*$/CFG_SPI= native/g' ./libloragw/library.cfg 380 | make 381 | 382 | popd 383 | # Build legacy packet forwarder 384 | if [ ! -d packet_forwarder ]; then 385 | git clone https://github.com/ch2i/packet_forwarder 386 | pushd packet_forwarder 387 | else 388 | pushd packet_forwarder 389 | git pull 390 | git reset --hard 391 | fi 392 | make 393 | 394 | # Copy legacy forwarder where it'll be expected 395 | cp ./poly_pkt_fwd/poly_pkt_fwd $INSTALL_DIR/poly_pkt_fwd 396 | 397 | popd 398 | 399 | if [ ! -f $INSTALL_DIR/poly_pkt_fwd ]; then 400 | echo "Oup's, something went wrong, legacy forwarder not found" 401 | echo "please check for any build error" 402 | else 403 | # Copy things needed at runtime to where they'll be expected 404 | cp $INSTALL_DIR/dev/legacy/packet_forwarder/poly_pkt_fwd/poly_pkt_fwd $INSTALL_DIR/poly_pkt_fwd 405 | echo "Build & Installation Completed." 406 | echo "forwarder is located at $INSTALL_DIR/poly_pkt_fwd" 407 | echo "" 408 | echo "Do you want this forwarder to be run instead" 409 | echo -n "kersing mp_pkt_fwd? [y/N] " 410 | read 411 | if [[ "$REPLY" =~ ^(yes|y|Y)$ ]]; then 412 | replace ./start.sh "^.*mp_pkt_fwd.*$" "./poly_pkt_fwd" 413 | fi 414 | 415 | fi 416 | 417 | fi 418 | 419 | # Copying all needed script and system 420 | cd /home/loragw/LoraGW-Setup 421 | cp ./oled.py $INSTALL_DIR/ 422 | cp ./monitor-ws2812.py $INSTALL_DIR/ 423 | cp ./monitor-gpio.py $INSTALL_DIR/ 424 | if [[ $MONITOR_SCRIPT != "" ]]; then 425 | ln -s $INSTALL_DIR/$MONITOR_SCRIPT $INSTALL_DIR/monitor.py 426 | fi 427 | cp ./monitor.service /lib/systemd/system/ 428 | cp ./oled.service /lib/systemd/system/ 429 | cp start.sh $INSTALL_DIR/ 430 | 431 | if [[ ! "$EN_TTN" =~ ^(no|n|N)$ ]]; then 432 | # script to get config from TTN server 433 | python set_config.py 434 | 435 | # Copy config to running folder 436 | sudo mv global_conf.json $INSTALL_DIR/ 437 | 438 | # Prepare start forwarder as systemd script 439 | sudo cp ./loragw.service /lib/systemd/system/ 440 | sudo systemctl enable loragw.service 441 | sudo systemctl start loragw.service 442 | 443 | echo "" 444 | echo "all done, please check service running log with" 445 | echo "sudo journalctl -f -u loragw.service" 446 | 447 | fi 448 | 449 | if [[ ! "$EN_MONITOR" =~ ^(no|n|N)$ ]]; then 450 | echo "monitor service enabled!" 451 | sudo systemctl enable monitor.service 452 | sudo systemctl start monitor.service 453 | echo "" 454 | fi 455 | 456 | if [[ "$EN_OLED" =~ ^(yes|y|Y)$ ]]; then 457 | echo "Oled service enabled!" 458 | sudo systemctl enable oled.service 459 | sudo systemctl start oled.service 460 | echo "Please follow the procedure located here to finish setup" 461 | echo "https://github.com/ch2i/LoraGW-Setup/doc/DisplayOled.md" 462 | echo "Also don't forget to set the OLED wiring and type by editing file" 463 | echo "/opt/loragw/oled.py (as described into procedure)" 464 | echo "" 465 | fi 466 | -------------------------------------------------------------------------------- /3_WiFi_AP.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INSTALL_DIR="/opt/loragw" 4 | MODEL=`cat /proc/device-tree/model` 5 | 6 | if [ $(id -u) -ne 0 ]; then 7 | echo "Installer must be run as root." 8 | echo "Try 'sudo bash $0'" 9 | exit 1 10 | fi 11 | 12 | echo "This script configures a Raspberry Pi" 13 | echo "as a wifi access point and client" 14 | echo 15 | echo "It will install the following dependencies" 16 | echo "hostapd, dnsmasq and configure SSID / Password" 17 | echo 18 | echo "Device is $MODEL" 19 | echo 20 | 21 | if echo "$MODEL" | grep -q "Zero"; then 22 | echo "Device Match configuration OK"; 23 | else 24 | echo "This model is not supported, this" 25 | echo "script runs only on Pi Zero else" 26 | echo "it will break your network config" 27 | echo "You'll loose network on reboot and" 28 | echo "potentially access to your device" 29 | echo "" 30 | echo "!!! you've been warned !!!" 31 | echo "exiting immediatly to avoid this" 32 | echo "for this PI model use docker mode" 33 | echo "and follow procedure located here" 34 | echo "https://github.com/cjimti/iotwifi" 35 | echo "to install docker then run script" 36 | echo "4_WiFi_Docker.sh of this folder" 37 | 38 | 39 | exit 0 40 | fi 41 | 42 | 43 | echo "Run time ~1 minute. Reboot required." 44 | echo 45 | echo -n "CONTINUE? [Y/n] " 46 | read 47 | if [[ "$REPLY" =~ ^(no|n|N)$ ]]; then 48 | echo "Canceled." 49 | exit 0 50 | fi 51 | 52 | # These functions have been copied from excellent Adafruit Read only tutorial 53 | # https://github.com/adafruit/Raspberry-Pi-Installer-Scripts/blob/master/read-only-fs.sh 54 | # the one inspired by my original article http://hallard.me/raspberry-pi-read-only/ 55 | # That's an excellent demonstration of collaboration and open source sharing 56 | # 57 | # Given a filename, a regex pattern to match and a replacement string: 58 | # Replace string if found, else no change. 59 | # (# $1 = filename, $2 = pattern to match, $3 = replacement) 60 | replace() { 61 | grep $2 $1 >/dev/null 62 | if [ $? -eq 0 ]; then 63 | # Pattern found; replace in file 64 | sed -i "s/$2/$3/g" $1 >/dev/null 65 | fi 66 | } 67 | 68 | # Given a filename, a regex pattern to match and a replacement string: 69 | # If found, perform replacement, else append file w/replacement on new line. 70 | replaceAppend() { 71 | grep $2 $1 >/dev/null 72 | if [ $? -eq 0 ]; then 73 | # Pattern found; replace in file 74 | sed -i "s/$2/$3/g" $1 >/dev/null 75 | else 76 | # Not found; append on new line (silently) 77 | echo $3 | sudo tee -a $1 >/dev/null 78 | fi 79 | } 80 | 81 | # Given a filename, a regex pattern to match and a string: 82 | # If found, no change, else append file with string on new line. 83 | append1() { 84 | grep $2 $1 >/dev/null 85 | if [ $? -ne 0 ]; then 86 | # Not found; append on new line (silently) 87 | echo $3 | sudo tee -a $1 >/dev/null 88 | fi 89 | } 90 | 91 | 92 | echo "" 93 | echo "Enter SSID you want to see for your WiFi AP" 94 | echo -n "default is set to $HOSTNAME : " 95 | read SSID 96 | if [[ $SSID == "" ]]; then 97 | SSID=$HOSTNAME 98 | fi 99 | 100 | echo "" 101 | echo "Enter password you want for you access point" 102 | echo -n "default is set $SSID : " 103 | read PSK 104 | if [[ $PSK == "" ]]; then 105 | PSK=$SSID 106 | fi 107 | 108 | echo "" 109 | echo "Please enter WiFi country code" 110 | echo -n "default is set to FR : " 111 | read CCODE 112 | 113 | echo "" 114 | echo "Setting up WiFi access point with" 115 | echo "SSID : $SSID" 116 | echo "PSK : $PSK" 117 | 118 | sudo apt-get install -y hostapd dnsmasq 119 | 120 | # Set the SSID/PSK 121 | echo "Replacing SSID / PASK in hostapd.conf" 122 | replace ./config/hostapd.conf "^.*_AP_SSID_.*$" "ssid=$SSID" 123 | replace ./config/hostapd.conf "^.*_AP_PASSWORD_.*$" "wpa_passphrase=$PSK" 124 | if [[ $CCODE != "" ]]; then 125 | echo "Setting default country code to $CCCODE in hostapd.conf" 126 | replace ./config/hostapd.conf "^.*country_code=.*$" "country_code=$CCODE" 127 | fi 128 | 129 | # Copy configuration files (save orgininal files) 130 | cp ./config/90-wireless.rules /etc/udev/rules.d/ 131 | cp ./config/hostapd.conf /etc/hostapd/ 132 | cp ./config/dnsmasq.conf /etc/ 133 | cp /etc/network/interfaces /etc/network/interfaces.old 134 | cp ./config/interfaces /etc/network/ 135 | 136 | echo "Setting default hostapd config file" 137 | append1 /etc/default/hostapd "^.*DAEMON_CONF=.*$" "DAEMON_CONF=\"/etc/hostapd/hostapd.conf\"" 138 | 139 | # disable dhcpcd service 140 | update-rc.d dhcpcd disable 141 | 142 | # Fix bootup (save orgininal file) 143 | cp /etc/rc.local /etc/rc.local.old 144 | cp ./config/rc.local /etc/ 145 | 146 | echo "Done." 147 | echo 148 | echo "Settings take effect on next boot." 149 | echo "after reboot, login back here with" 150 | echo "ssh loragw@$HOSTNAME.local" 151 | echo 152 | echo -n "REBOOT NOW? [y/N] " 153 | read 154 | if [[ ! "$REPLY" =~ ^(yes|y|Y)$ ]]; then 155 | echo "Exiting without reboot." 156 | exit 0 157 | fi 158 | echo "Reboot started..." 159 | reboot 160 | exit 0 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /4_WiFi_Docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INSTALL_DIR="/opt/loragw" 4 | MODEL=`cat /proc/device-tree/model` 5 | 6 | if [ $(id -u) -ne 0 ]; then 7 | echo "Installer must be run as root." 8 | echo "Try 'sudo bash $0'" 9 | exit 1 10 | fi 11 | 12 | echo "This script configures a Raspberry Pi" 13 | echo "as a wifi access point and client" 14 | echo 15 | echo "It will install a docker container doing" 16 | echo "all the necessary staff, please see" 17 | echo "https://github.com/cjimti/iotwifi" 18 | echo 19 | echo "Device is $MODEL" 20 | echo 21 | 22 | echo "Run time ~3 minutes. Reboot required." 23 | echo 24 | echo -n "CONTINUE? [Y/n] " 25 | read 26 | if [[ "$REPLY" =~ ^(no|n|N)$ ]]; then 27 | echo "Canceled." 28 | exit 0 29 | fi 30 | 31 | # These functions have been copied from excellent Adafruit Read only tutorial 32 | # https://github.com/adafruit/Raspberry-Pi-Installer-Scripts/blob/master/read-only-fs.sh 33 | # the one inspired by my original article http://hallard.me/raspberry-pi-read-only/ 34 | # That's an excellent demonstration of collaboration and open source sharing 35 | # 36 | # Given a filename, a regex pattern to match and a replacement string: 37 | # Replace string if found, else no change. 38 | # (# $1 = filename, $2 = pattern to match, $3 = replacement) 39 | replace() { 40 | grep $2 $1 >/dev/null 41 | if [ $? -eq 0 ]; then 42 | # Pattern found; replace in file 43 | sed -i "s/$2/$3/g" $1 >/dev/null 44 | fi 45 | } 46 | 47 | echo "" 48 | echo "Enter SSID you want to see for your WiFi AP" 49 | echo -n "default is set to $HOSTNAME : " 50 | read SSID 51 | if [[ $SSID == "" ]]; then 52 | SSID=$HOSTNAME 53 | fi 54 | 55 | echo "" 56 | echo "Enter password you want for you access point" 57 | echo -n "default is set $SSID : " 58 | read PSK 59 | if [[ $PSK == "" ]]; then 60 | PSK=$SSID 61 | fi 62 | 63 | echo "" 64 | echo "Please enter WiFi country code" 65 | echo -n "default is set to FR : " 66 | read CCODE 67 | 68 | echo "" 69 | echo "Setting up WiFi access point with" 70 | echo "SSID : $SSID" 71 | echo "PSK : $PSK" 72 | 73 | echo "installing dpendencies" 74 | echo "Docker install script" 75 | curl -sSL https://get.docker.com | sh 76 | sudo usermod -aG docker loragw 77 | 78 | echo "ull the IOT Wifi Docker Image" 79 | docker pull cjimti/iotwifi 80 | 81 | # Set the SSID/PSK 82 | echo "Replacing SSID / PASK in hostapd.conf" 83 | replace ./wificfg.json "^.*_AP_SSID_.*$" "$SSID" 84 | replace ./wificfg.json "^.*_AP_PASSWORD_.*$" "$PSK" 85 | 86 | # Copy config file to install folder 87 | sudo mv ./wificfg.json $INSTALL_DIR/ 88 | sudo ln -s $INSTALL_DIR/wificfg.json ./ 89 | 90 | if [[ $CCODE != "" ]]; then 91 | echo "Setting default country code to $CCCODE in hostapd.conf" 92 | replace /etc/wpa_supplicant/wpa_supplicant.conf "^.*country=.*$" "country=$CCODE" 93 | fi 94 | 95 | 96 | # prevent wpa_supplicant from starting on boot it will be used by docker 97 | sudo systemctl mask wpa_supplicant.service 98 | # rename wpa_supplicant on the host to ensure that it is not used. 99 | sudo mv /sbin/wpa_supplicant /sbin/wpa_supplicant.old 100 | # kill any running processes named wpa_supplicant 101 | sudo pkill wpa_supplicant 102 | 103 | echo "starting docker as a service" 104 | docker run --restart unless-stopped -d --privileged --net host \ 105 | -v /opt/loragw//wificfg.json:/cfg/wificfg.json \ 106 | -v /etc/wpa_supplicant/wpa_supplicant.conf:/etc/wpa_supplicant/wpa_supplicant.conf \ 107 | cjimti/iotwifi 108 | 109 | echo "Done." 110 | echo 111 | echo "Settings take effect on next boot." 112 | echo "after reboot, login back here with" 113 | echo "ssh loragw@$HOSTNAME.local" 114 | echo 115 | echo -n "REBOOT NOW? [y/N] " 116 | read 117 | if [[ ! "$REPLY" =~ ^(yes|y|Y)$ ]]; then 118 | echo "Exiting without reboot." 119 | exit 0 120 | fi 121 | echo "Reboot started..." 122 | reboot 123 | exit 0 124 | 125 | -------------------------------------------------------------------------------- /5_GPS.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INSTALL_DIR="/opt/loragw" 4 | MODEL=`cat /proc/device-tree/model` 5 | 6 | if [ $(id -u) -ne 0 ]; then 7 | echo "Installer must be run as root." 8 | echo "Try 'sudo bash $0'" 9 | exit 1 10 | fi 11 | 12 | echo "This script configures a Raspberry Pi" 13 | echo "GPS for TTN gateway" 14 | echo 15 | echo "Device is $MODEL" 16 | echo 17 | 18 | echo "Run time ~1 minute. Reboot required." 19 | echo 20 | echo -n "CONTINUE? [Y/n] " 21 | read 22 | if [[ "$REPLY" =~ ^(no|n|N)$ ]]; then 23 | echo "Canceled." 24 | exit 0 25 | fi 26 | 27 | echo "" 28 | echo "Enter GPS device to use" 29 | echo -n "default is set to /dev/ttyS0 : " 30 | read DEVGPS 31 | if [[ $DEVGPS == "" ]]; then 32 | DEVGPS="/dev/ttyS0" 33 | fi 34 | 35 | sudo systemctl stop serial-getty@ttyS0.service 36 | sudo systemctl disable serial-getty@ttyS0.service 37 | 38 | echo 39 | echo "now change the file $INSTALL_DIR/local_conf.json" 40 | echo "and check that the following lines exist" 41 | echo 42 | echo '"fake_gps": false,' 43 | echo '"gps": true,' 44 | echo '"gps_tty_path": "'$DEVGPS'"' 45 | echo 46 | echo "you may also need to add theese lines" 47 | echo "to /boot/config.txt (example for PI 3)" 48 | echo 49 | echo "dtoverlay = pi3-miniuart-bt" 50 | echo "enable_uart=1" 51 | echo 52 | echo "then reboot the Raspberry Pi" 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lora Gateway base setup for SX1301 based concentrators 2 | 3 | This setup is used for some LoraWAN concentrators based on small computers such as Raspberry PI or others. 4 | For example it works fine with the RAK831 PI Zero [shield](https://github.com/hallard/RAK831-Zero) 5 | 6 | RAK831 Shield 7 | 8 | And for the [iC880a](https://github.com/ch2i/iC880A-Raspberry-PI) sield for Raspberry PI V2 or V3. 9 | 10 | iC880a Shield 11 | 12 | # Installation 13 | 14 | Download [Raspbian lite image](https://downloads.raspberrypi.org/raspbian_lite_latest) and [flash](https://www.raspberrypi.org/documentation/installation/installing-images/README.md) it to your SD card using [etcher](http://etcher.io/). 15 | 16 | ## Prepare SD to your environement 17 | 18 | Once flashed, you need to do some changes on boot partition (windows users, remove and then replug SD card) 19 | 20 | ### Enable SSH 21 | 22 | Create a dummy `ssh` file on this partition. By default SSH is now disabled so this is required to enable it. Windows users, make sure your file doesn't have an extension like .txt etc. 23 | 24 | ### Enable USB OTG (Pi Zero Only) 25 | 26 | If you need to be able to use OTG (Console access for any computer by conecting the PI to computer USB port) 27 | Open up the file `cmdline.txt`. Be careful with this file, it is very picky with its formatting! Each parameter is seperated by a single space (it does not use newlines). Insert `modules-load=dwc2,g_ether` after `rootwait quiet`. 28 | 29 | The new file `cmdline.txt` should looks like this 30 | ``` 31 | dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=37665771-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet modules-load=dwc2,g_ether init=/usr/lib/raspi-config/init_resize.sh 32 | ``` 33 | 34 | For OTG, add also the bottom of the `config.txt` file, on a new line 35 | ``` 36 | dtoverlay=dwc2 37 | ``` 38 | 39 | ### Optionnal, disable Auto Resize of SD Card 40 | 41 | And since I don't like the Auto Resize SD function (I prefer do do it manually from `raspi-config`), remove also from the file `cmdline.txt` auto resize by deleting the following 42 | ``` 43 | init=/usr/lib/raspi-config/init_resize.sh 44 | ``` 45 | 46 | The new file `cmdline.txt` should looks like this 47 | ``` 48 | dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=37665771-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet modules-load=dwc2,g_ether 49 | ``` 50 | 51 | ### Pre Connect to your WiFi AP 52 | 53 | Finally, on same partition (boot), to allow your PI to connect to your WiFi after first boot, create a file named `wpa_supplicant.conf` to allow the PI to be connected on your WiFi network. 54 | 55 | ``` 56 | country=FR 57 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 58 | update_config=1 59 | network={ 60 | ssid="YOUR-WIFI-SSID" 61 | psk="YOUR-WIFI-PASSWORD" 62 | } 63 | ``` 64 | Of course change country, ssid and psk with your own WiFi settings. 65 | 66 | 67 | That's it, eject the SD card from your computer, put it in your Raspberry Pi Zero . It will take up to 90s to boot up (shorter on subsequent boots). You then can SSH into it using `raspberrypi.local` as the address. 68 | If WiFi does not work, connect it via USB to your computer It should then appear as a USB Ethernet device. 69 | 70 | ```shell 71 | ssh pi@raspberrypi.local 72 | ``` 73 | 74 | ## Now connect to raspberry PI with ssh or via USB otg 75 | 76 | Remember default login/paswword (ssh or serial console) is pi/raspberry. 77 | 78 | So please **for security reasons, you should change this default password** 79 | ```shell 80 | passwd 81 | ``` 82 | 83 | ### Launch PI configuration script 84 | 85 | This [1_Pi_Config.sh](https://github.com/ch2i/LoraGW-Setup/blob/master/1_Pi_Config.sh) script will prepare your Pi environnment, create and configure a loragw user, add access to SPI, I2C, Uart. It will reduce video memory to 16MB to allow max memory for the stuff need to be done. 86 | It also enable excellent [log2ram](https://github.com/azlux/log2ram) SD card preservation. 87 | 88 | ```shell 89 | wget https://raw.githubusercontent.com/ch2i/LoraGW-Setup/master/1_Pi_Config.sh 90 | chmod ug+x 1_Pi_Config.sh 91 | sudo ./1_Pi_Config.sh 92 | ``` 93 | 94 | 95 | ## Reconnect after reboot 96 | 97 | Log back with `loragw` user and if you changed hostname to loragw-xxyy, use this command 98 | ```shell 99 | ssh loragw@loragw-xxyy.local 100 | ``` 101 | 102 | 103 | ### Get CH2i Gateway Install repository 104 | ``` 105 | git clone https://github.com/ch2i/LoraGW-Setup 106 | cd LoraGW-Setup 107 | ``` 108 | 109 | ## Configure Gateway on TTN console 110 | 111 | Now you need to register your new GW on TTN before next step, see [gateway registration](https://www.thethingsnetwork.org/docs/gateways/registration.html#via-gateway-connector), the GW_ID and GW_KEY will be asked by the script 112 | 113 | ### Then launch script to install all stuff 114 | 115 | ```shell 116 | sudo ./2_Setup.sh 117 | ``` 118 | 119 | 120 | That's it, If you are using PI Zero [shield](https://github.com/hallard/RAK831-Zero), the 2 LED should be blinking green and you should be able to see your brand new gateway on TTN 121 | 122 | # Usefull information 123 | 124 | ## Startup 125 | Check all is fine also at startup, reboot your gateway. 126 | ``` 127 | sudo reboot 128 | ``` 129 | 130 | ## LED Blinking colors (RAK831 Shied with 2 WS2812B Leds) 131 | 132 | WS2812B driver use DMA channel, and with new Raspbian version, using DMA 5 will corrupt your SD card. see this [issue](https://github.com/jgarff/rpi_ws281x/issues/224). It's now solved but if you have old GW with old scripts, be sure to update the line of script `/opt/loragw/monitor_ws2812.py` from 133 | ```python 134 | strip = Adafruit_NeoPixel(2, gpio_led, 800000, 5, False, 64, 0, ws.WS2811_STRIP_GRB) 135 | ``` 136 | 137 | to (using DMA channel 10 instead of 5) 138 | ```python 139 | strip = Adafruit_NeoPixel(2, gpio_led, 800000, 10, False, 64, 0, ws.WS2811_STRIP_GRB) 140 | ``` 141 | 142 | ### LED 1 143 | 144 | - green => connected to Internet 145 | - blue => No Internet connexion but gateway [WiFi AP](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/AccessPoint.md) is up 146 | - red => No Internet, no WiFi Access Point 147 | 148 | ### LED 2 149 | 150 | - green => packet forwarder is started and running 151 | - blue => no packed forwarder but local [LoRaWAN server](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/Lorawan-Server.md) is started 152 | - red => No packet forwarder nor LoRaWAN server 153 | 154 | ## LED Blinking colors (iC880a with 4 GPIO Leds) 155 | 156 | - GPIO 4 (Blue) Blink => Internet access OK 157 | - GPIO 18 (Yellow) Blink => local web server up & running 158 | - GPIO 24 (Green) 159 | - Blink => packet forwarder is running 160 | - Fixed => Shutdown OK, can remove power 161 | - GPIO 23 (Red) 162 | - Blink every second, one of the previous service down (local web, internet, ) 163 | - Middle bink on every bad LoRaWAN packet received 164 | - Lot of short blink => Activity on SD Card (seen a boot for example) 165 | 166 | 167 | ### Change behaviour 168 | 169 | You can change LED code behaviour at the end of script `/opt/loragw/monitor.py` 170 | 171 | 172 | ## Shutdown 173 | You can press (and let it pressed) the switch push button, leds well become RED and after 2s start blinking in blue. If you release button when they blink blue, the Pi will initiate a shutdown. So let it 30s before removing power. 174 | 175 | ### Shutdown LED display (for RPI 3 and iC880a only) 176 | 177 | If you have a raspberry PI 3 with this [iC880A shield](https://github.com/ch2i/iC880A-Raspberry-PI), then the `/boot/config.txt` file has been enhanced with the following lines: 178 | 179 | ``` 180 | # When system if Halted/OFF Light Green LED 181 | dtoverlay=gpio-poweroff,gpiopin=24 182 | ``` 183 | The Green LED (gpio24) will stay on when you can remove the power of the gateway. It's really a great indicator. 184 | 185 | You can also select which GPIO LED is used to replace activity LED if you need it. 186 | ``` 187 | # Activity LED 188 | dtoverlay=pi3-act-led,gpio=23 189 | ``` 190 | The Red LED (gpio23) will blink on activity. 191 | 192 | ### Shutdown or Activity LED (for RPI Zero Shield V1.5+) 193 | 194 | If you have a raspberry PI Zero with this RAK831, then you can change the `/boot/config.txt` file to choose one of the two following features: 195 | 196 | ``` 197 | # When system is Halted/OFF Light Green LED 198 | dtoverlay=gpio-poweroff,gpiopin=26 199 | ``` 200 | The Green LED (gpio26) will stay on when you can remove the power of the gateway. It's really a great indicator. 201 | 202 | You can also choose select this LED to replace activity LED if you need it. 203 | ``` 204 | # Activity LED 205 | dtparam=act_led_gpio=26 206 | ``` 207 | The Greend LED (gpio26) will blink on activity. 208 | 209 | You can select only one of the 2 options, not both at the same time. 210 | 211 | ## Detailled information 212 | 213 | The installed sofware is located on `/opt/loragw`, I changed this name (original was ttn-gateway) just because not all my gateways are connected to TTN so I wanted to have a more generic setup. 214 | 215 | ```shell 216 | ls -al /opt/loragw/ 217 | total 344 218 | drwxr-xr-x 3 root root 4096 Jan 21 03:15 . 219 | drwxr-xr-x 5 root root 4096 Jan 21 01:01 .. 220 | drwxr-xr-x 9 root root 4096 Jan 21 01:03 dev 221 | -rw-r--r-- 1 root root 6568 Jan 21 01:15 global_conf.json 222 | -rwxr-xr-- 1 root root 3974 Jan 21 01:15 monitor-gpio.py 223 | -rwxr-xr-- 1 root root 3508 Jan 21 03:15 monitor.py 224 | -rwxr-xr-- 1 root root 4327 Jan 21 01:15 monitor-ws2812.py 225 | -rwxr-xr-x 1 root root 307680 Jan 21 01:14 mp_pkt_fwd 226 | -rwxr-xr-- 1 root root 642 Jan 21 01:36 start.sh 227 | ``` 228 | 229 | LED blinking and push button functions are done with the monitor.py service (launched by systemd at startup). 230 | There are 2 versions of this service (with symlink), one with WS2812B led and another for classic GPIO LED such as the one on this [IC880A shield](https://github.com/ch2i/iC880A-Raspberry-PI). So if you want to change you can do it like that 231 | 232 | ### stop the service 233 | ```shell 234 | sudo systemctl stop monitor 235 | ``` 236 | 237 | ### If you have ic880a shield, change monitor service 238 | 239 | In this case you do not have WS2812B RGB LED on the shield, but GPIO classic one. The push button GPIO to power off the PI is also not on the same GPIO, so you need to setup the correct monitor service. 240 | 241 | ```shell 242 | sudo rm /opt/loragw/monitor.py 243 | sudo ln -s /opt/loragw/monitor-gpio.py /opt/loragw/monitor.py 244 | ``` 245 | 246 | ### start the service 247 | ```shell 248 | sudo systemctl start monitor 249 | ``` 250 | 251 | ### Check packed forwarder log 252 | ```shell 253 | sudo journalctl -f -u loragw 254 | ``` 255 | 256 | 257 | ``` 258 | -- Logs begin at Sun 2018-01-21 14:57:08 CET. -- 259 | Jan 22 01:00:41 loragw loragw[240]: ### GPS IS DISABLED! 260 | Jan 22 01:00:41 loragw loragw[240]: ### [PERFORMANCE] ### 261 | Jan 22 01:00:41 loragw loragw[240]: # Upstream radio packet quality: 100.00%. 262 | Jan 22 01:00:41 loragw loragw[240]: # Semtech status report send. 263 | Jan 22 01:00:41 loragw loragw[240]: ##### END ##### 264 | Jan 22 01:00:41 loragw loragw[240]: 01:00:41 INFO: [TTN] bridge.eu.thethings.network RTT 52 265 | Jan 22 01:00:41 loragw loragw[240]: 01:00:41 INFO: [TTN] send status success for bridge.eu.thethings.network 266 | Jan 22 01:00:53 loragw loragw[240]: 01:00:53 INFO: Disabling GPS mode for concentrator's counter... 267 | Jan 22 01:00:53 loragw loragw[240]: 01:00:53 INFO: host/sx1301 time offset=(1516578208s:159048µs) - drift=-55µs 268 | Jan 22 01:00:53 loragw loragw[240]: 01:00:53 INFO: Enabling GPS mode for concentrator's counter. 269 | Jan 22 01:01:11 loragw loragw[240]: ##### 2018-01-22 00:01:11 GMT ##### 270 | Jan 22 01:01:11 loragw loragw[240]: ### [UPSTREAM] ### 271 | Jan 22 01:01:11 loragw loragw[240]: # RF packets received by concentrator: 0 272 | Jan 22 01:01:11 loragw loragw[240]: # CRC_OK: 0.00%, CRC_FAIL: 0.00%, NO_CRC: 0.00% 273 | Jan 22 01:01:11 loragw loragw[240]: # RF packets forwarded: 0 (0 bytes) 274 | Jan 22 01:01:11 loragw loragw[240]: # PUSH_DATA datagrams sent: 0 (0 bytes) 275 | Jan 22 01:01:11 loragw loragw[240]: # PUSH_DATA acknowledged: 0.00% 276 | Jan 22 01:01:11 loragw loragw[240]: ### [DOWNSTREAM] ### 277 | Jan 22 01:01:11 loragw loragw[240]: # PULL_DATA sent: 0 (0.00% acknowledged) 278 | Jan 22 01:01:11 loragw loragw[240]: # PULL_RESP(onse) datagrams received: 0 (0 bytes) 279 | Jan 22 01:01:11 loragw loragw[240]: # RF packets sent to concentrator: 0 (0 bytes) 280 | Jan 22 01:01:11 loragw loragw[240]: # TX errors: 0 281 | Jan 22 01:01:11 loragw loragw[240]: ### BEACON IS DISABLED! 282 | Jan 22 01:01:11 loragw loragw[240]: ### [JIT] ### 283 | Jan 22 01:01:11 loragw loragw[240]: # INFO: JIT queue contains 0 packets. 284 | Jan 22 01:01:11 loragw loragw[240]: # INFO: JIT queue contains 0 beacons. 285 | Jan 22 01:01:11 loragw loragw[240]: ### GPS IS DISABLED! 286 | Jan 22 01:01:11 loragw loragw[240]: ### [PERFORMANCE] ### 287 | Jan 22 01:01:11 loragw loragw[240]: # Upstream radio packet quality: 0.00%. 288 | Jan 22 01:01:11 loragw loragw[240]: # Semtech status report send. 289 | Jan 22 01:01:11 loragw loragw[240]: ##### END ##### 290 | Jan 22 01:01:11 loragw loragw[240]: 01:01:11 INFO: [TTN] bridge.eu.thethings.network RTT 53 291 | Jan 22 01:01:11 loragw loragw[240]: 01:01:11 INFO: [TTN] send status success for bridge.eu.thethings.network 292 | ``` 293 | 294 | 295 | ### Use legacy Packet Forwarder (not needed) 296 | 297 | First build it 298 | ``` 299 | ./build_legacy.sh 300 | ``` 301 | 302 | If you want to use the legacy packet forwarder, you'll need to change file `/opt/loragw/start.sh` to replace the last line 303 | 304 | ``` 305 | ./mp_pkt_fwd.sh 306 | ``` 307 | by 308 | ``` 309 | ./poly_pkt_fwd.sh 310 | ``` 311 | 312 | ```shell 313 | sudo systemctl stop loragw 314 | sudo systemctl start loragw 315 | ``` 316 | 317 | ## Adjust log2ram 318 | 319 | if you chose log2ram to reduce SD card write, you need to change some log file rotation to avoid RAM Disk to be full. 320 | 321 | For this you need to edit each file in `/etc/logrotate.d/`, and on each file: 322 | 323 | - remove line(s) containing `delaycompress` (this avoid uncompressed old log) 324 | - change line(s) containing `rotate n` by rotate 12 (this this the max log file history) 325 | - change line(s) containing `daily` by `hourly` (rotate log each hour) 326 | - change line(s) containing `monthly` by `daily` (rotate log each day) 327 | 328 | In this case we got last 12H with 1 file per hour. Of course, you can adjust these paramaters to feet you need, it's just an example, 329 | 330 | file `/etc/logrotate.d/rsyslog` 331 | 332 | ``` 333 | /var/log/syslog 334 | { 335 | rotate 12 336 | hourly 337 | missingok 338 | notifempty 339 | compress 340 | postrotate 341 | invoke-rc.d rsyslog rotate > /dev/null 342 | endscript 343 | } 344 | 345 | /var/log/mail.info 346 | /var/log/mail.warn 347 | /var/log/mail.err 348 | /var/log/mail.log 349 | /var/log/daemon.log 350 | /var/log/kern.log 351 | /var/log/auth.log 352 | /var/log/user.log 353 | /var/log/lpr.log 354 | /var/log/cron.log 355 | /var/log/debug 356 | /var/log/messages 357 | { 358 | rotate 12 359 | hourly 360 | missingok 361 | notifempty 362 | compress 363 | sharedscripts 364 | postrotate 365 | invoke-rc.d rsyslog rotate > /dev/null 366 | endscript 367 | } 368 | 369 | ``` 370 | 371 | # And here is the final result 372 | 373 | Click on image to see the video 374 | 375 | [![CH2i RAK831 GW](http://img.youtube.com/vi/AZTomPGSOBY/0.jpg)](https://www.youtube.com/watch?v=AZTomPGSOBY "CH2i RAK831 GW") 376 | 377 | # Add some other features 378 | 379 | Here are other feature I use sometime on my gateways: 380 | 381 | - Put the whole filesystem in [ReadOnly](https://hallard.me/raspberry-pi-read-only/) 382 | - Setup PI as a WiFi [access point](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/AccessPoint.md) 383 | - Install a nice local [LoraWAN Server](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/Lorawan-Server.md) 384 | - Use a OLED [display](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/DisplayOled.md) 385 | -------------------------------------------------------------------------------- /build_legacy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | INSTALL_DIR="/opt/loragw" 4 | 5 | mkdir -p $INSTALL_DIR/dev 6 | mkdir -p $INSTALL_DIR/dev/legacy 7 | cd $INSTALL_DIR/dev/legacy 8 | 9 | # Build legacy loragw library 10 | if [ ! -d lora_gateway ]; then 11 | git clone https://github.com/TheThingsNetwork/lora_gateway.git || { echo 'Cloning lora_gateway failed.' ; exit 1; } 12 | pushd lora_gateway 13 | else 14 | pushd lora_gateway 15 | git reset --hard 16 | git pull 17 | fi 18 | sed -i -e 's/PLATFORM= .*$/PLATFORM= imst_rpi/g' ./libloragw/library.cfg 19 | sed -i -e 's/CFG_SPI= .*$/CFG_SPI= native/g' ./libloragw/library.cfg 20 | make 21 | 22 | popd 23 | # Build legacy packet forwarder 24 | if [ ! -d packet_forwarder ]; then 25 | git clone https://github.com/ch2i/packet_forwarder 26 | pushd packet_forwarder 27 | else 28 | pushd packet_forwarder 29 | git pull 30 | git reset --hard 31 | fi 32 | make 33 | popd 34 | 35 | 36 | # Copy things needed at runtime to where they'll be expected 37 | cp $INSTALL_DIR/dev/legacy/packet_forwarder/poly_pkt_fwd/poly_pkt_fwd $INSTALL_DIR/poly_pkt_fwd 38 | 39 | if [ ! -f $INSTALL_DIR/poly_pkt_fwd ]; then 40 | echo "Oup's, something went wrong, forwarder not found" 41 | echo "please check for any build error" 42 | else 43 | echo "Build & Installation Completed." 44 | echo "forwarder is located at $INSTALL_DIR/poly_pkt_fwd" 45 | echo "" 46 | echo "you can now run the setup script with sudo ./setup.sh" 47 | fi 48 | -------------------------------------------------------------------------------- /config/90-wireless.rules: -------------------------------------------------------------------------------- 1 | ACTION=="add|change", SUBSYSTEM=="ieee80211", KERNEL=="phy0", RUN+="/sbin/iw phy %k interface add ap0 type __ap" 2 | -------------------------------------------------------------------------------- /config/dnsmasq.conf: -------------------------------------------------------------------------------- 1 | interface=lo,ap0 2 | no-dhcp-interface=lo,wlan0 3 | bind-interfaces 4 | server=8.8.8.8 5 | domain-needed 6 | bogus-priv 7 | 8 | # You can change this to another network 9 | dhcp-range=192.168.50.50,192.168.50.100,12h 10 | 11 | # if you have read only FS best to put in tmp area 12 | #dhcp-leasefile=/tmp/dnsmasq.leases 13 | 14 | # if you want to to some rediection 15 | #address=/*.apple.com/192.168.50.1 16 | #address=/*.icloud.com/192.168.50.1 17 | #address=/#/192.168.50.1 18 | 19 | ###### logging ############ 20 | # own logfile 21 | #log-facility=/var/log/dnsmasq.log 22 | # log-async 23 | # log dhcp infos 24 | #log-dhcp 25 | # debugging dns -------------------------------------------------------------------------------- /config/hostapd.conf: -------------------------------------------------------------------------------- 1 | ctrl_interface=/var/run/hostapd 2 | ctrl_interface_group=0 3 | interface=ap0 4 | driver=nl80211 5 | ssid=_AP_SSID_ 6 | hw_mode=g 7 | channel=9 8 | wmm_enabled=0 9 | macaddr_acl=0 10 | auth_algs=1 11 | wpa=2 12 | wpa_passphrase=_AP_PASSWORD_ 13 | wpa_key_mgmt=WPA-PSK 14 | wpa_pairwise=TKIP CCMP 15 | rsn_pairwise=CCMP 16 | country_code=FR 17 | -------------------------------------------------------------------------------- /config/interfaces: -------------------------------------------------------------------------------- 1 | # Include files from /etc/network/interfaces.d: 2 | source-directory /etc/network/interfaces.d 3 | 4 | auto lo 5 | auto ap0 6 | auto wlan0 7 | iface lo inet loopback 8 | 9 | allow-hotplug wlan0 10 | iface wlan0 inet manual 11 | wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf 12 | iface wlan0 inet dhcp 13 | 14 | allow-hotplug ap0 15 | iface ap0 inet static 16 | address 192.168.50.1 17 | netmask 255.255.255.0 18 | hostapd /etc/hostapd/hostapd.conf 19 | -------------------------------------------------------------------------------- /config/rc.local: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # rc.local 4 | # 5 | # This script is executed at the end of each multiuser runlevel. 6 | # Make sure that the script will "exit 0" on success or any other 7 | # value on error. 8 | # 9 | # In order to enable or disable this script just change the execution 10 | # bits. 11 | # 12 | # By default this script does nothing. 13 | 14 | ifdown --force wlan0 && ifdown --force ap0 && ifup ap0 && ifup wlan0 15 | sysctl -w net.ipv4.ip_forward=1 16 | iptables -t nat -A POSTROUTING -s 192.168.50.0/24 ! -d 192.168.50.0/24 -j MASQUERADE 17 | systemctl restart dnsmasq 18 | iwconfig wlan0 power off 19 | 20 | # Print the IP address 21 | _IP=$(hostname -I) || true 22 | if [ "$_IP" ]; then 23 | printf "My IP address is %s\n" "$_IP" 24 | fi 25 | 26 | exit 0 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /doc/AccessPoint.md: -------------------------------------------------------------------------------- 1 | # Promote Raspberry PI based gateway as a WiFi access point 2 | 3 | I found several method to got this working but despite my best efforts, I was unable to get any of those tutorials to work reliably on their own until I found this article. The procedure here works with Raspberry **PI Zero W and Debian Stretch**. 4 | 5 | It may work (or not) with other PI model or OS version. 6 | 7 | I copied the procedure from [original article](https://albeec13.github.io/2017/09/26/raspberry-pi-zero-w-simultaneous-ap-and-managed-mode-wifi/) to have a backup in case and also to adapt to my needs. 8 | 9 | Other solutions I tried with mitigated success on PI Zero is excellent [RaspAP-webgui](https://github.com/billz/raspap-webgui) to make access point. It may works perfecttly for RPI 3 but still issues with PI Zero. 10 | 11 | # Pi Zero W with Raspbian Stretch Procedure 12 | 13 | ## Create new network interface for Access Point (ap0) 14 | 15 | Create/edit the file `/etc/udev/rules.d/90-wireless.rules`, and add the following line. 16 | 17 | ``` 18 | ACTION=="add|change", SUBSYSTEM=="ieee80211", KERNEL=="phy0", RUN+="/sbin/iw phy %k interface add ap0 type __ap" 19 | ``` 20 | 21 | 22 | ## Install the packages you need for DNS, Access Point and Firewall rules. 23 | ``` 24 | sudo apt-get install -y hostapd dnsmasq iptables-persistent 25 | ``` 26 | 27 | ## Configure the DNS server 28 | 29 | Edit the file `/etc/dnsmasq.conf` that should be like that, I kept the comments has a reminder to activate some features 30 | 31 | ``` 32 | interface=lo,ap0 33 | no-dhcp-interface=lo,wlan0 34 | bind-interfaces 35 | server=8.8.8.8 36 | domain-needed 37 | bogus-priv 38 | dhcp-range=192.168.50.50,192.168.50.100,12h 39 | 40 | #dhcp-leasefile=/tmp/dnsmasq.leases 41 | #address=/*.apple.com/192.168.50.1 42 | #address=/*.icloud.com/192.168.50.1 43 | #address=/#/192.168.50.1 44 | 45 | ###### logging ############ 46 | # own logfile 47 | #log-facility=/var/log/dnsmasq.log 48 | # log-async 49 | # log dhcp infos 50 | #log-dhcp 51 | # debugging dns 52 | ``` 53 | 54 | After that my AP is distributing address from `192.168.50.50` to `192.168.50.100` 55 | 56 | ## Setup the Access Point configuration 57 | 58 | ### Edit the file `/etc/hostapd/hostapd.conf` that should be like that 59 | 60 | ``` 61 | ctrl_interface=/var/run/hostapd 62 | ctrl_interface_group=0 63 | interface=ap0 64 | driver=nl80211 65 | ssid=_AP_SSID_ 66 | hw_mode=g 67 | channel=9 68 | wmm_enabled=0 69 | macaddr_acl=0 70 | auth_algs=1 71 | wpa=2 72 | wpa_passphrase=_AP_PASSWORD_ 73 | wpa_key_mgmt=WPA-PSK 74 | wpa_pairwise=TKIP CCMP 75 | rsn_pairwise=CCMP 76 | country_code=FR 77 | ``` 78 | 79 | Replace `_AP_SSID_` with the SSID you want for your access point. Replace `_AP_PASSWORD_` with the password for your access point. Make sure it has enough characters to be a legal password! (8 characters minimum) else hostapd won't start. 80 | 81 | Change also `country_code` to your own country 82 | 83 | ### Edit the file `/etc/default/hostapd` to enable service to start correctly 84 | 85 | ``` 86 | #DAEMON_OPTS=" -f /tmp/hostapd.log" 87 | DAEMON_CONF="/etc/hostapd/hostapd.conf" 88 | ``` 89 | 90 | ## Set up the client wifi (station) on wlan0. 91 | 92 | Create or edit `/etc/wpa_supplicant/wpa_supplicant.conf`. The contents depend on whether your home network is open, WEP or WPA/2. It is 93 | probably WPA2, and so should look like: 94 | 95 | ``` 96 | country=FR 97 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 98 | update_config=1 99 | 100 | network={ 101 | ssid="_ST_SSID_" 102 | psk="_ST_PASSWORD_" 103 | id_str="_MYAP_" 104 | } 105 | 106 | ``` 107 | 108 | Replace `_ST_SSID_` with your router SSID and `_ST_PASSWORD_` with your wifi password (in clear text). 109 | id_str is a name you'll need later, replace `_MYAP_` by the logical name you want. 110 | 111 | Change also `country` to your own country 112 | 113 | 114 | ## Set up the network inferfaces 115 | 116 | Edit the file `/etc/network/interfaces`. Mine looks like that: 117 | 118 | ``` 119 | # Include files from /etc/network/interfaces.d: 120 | source-directory /etc/network/interfaces.d 121 | 122 | auto lo 123 | auto ap0 124 | auto wlan0 125 | iface lo inet loopback 126 | 127 | allow-hotplug wlan0 128 | iface wlan0 inet manual 129 | wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf 130 | iface _MYAP_ inet dhcp 131 | 132 | allow-hotplug ap0 133 | iface ap0 inet static 134 | address 192.168.50.1 135 | netmask 255.255.255.0 136 | hostapd /etc/hostapd/hostapd.conf 137 | ``` 138 | 139 | Replace `_MYAP_` with the logical name you put in the file `wpa_supplicant.conf` 140 | 141 | ## Set up boot order and/or workaround 142 | 143 | Make sure do disable dhcpcd with 144 | ```shell 145 | sudo update-rc.d dhcpcd disable 146 | ``` 147 | 148 | Edit `/etc/rc.local` and add the following lines just before `# Print the IP Address` 149 | 150 | ```shell 151 | ifdown --force wlan0 && ifdown --force ap0 && ifup ap0 && ifup wlan0 152 | sysctl -w net.ipv4.ip_forward=1 153 | iptables -t nat -A POSTROUTING -s 192.168.50.0/24 ! -d 192.168.50.0/24 -j MASQUERADE 154 | systemctl restart dnsmasq 155 | iwconfig wlan0 power off 156 | ``` 157 | 158 | ## Optional, add AP display on OLED software 159 | 160 | If you have enabled OLED, you may need to change the following line in `/opt/loragw/oled.py` according to the connected network interface you have to add the ap0 interface 161 | 162 | Mine is wlan0 for WiFi and ap0 for WiFi [access point](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/AccessPoint.md) 163 | ```python 164 | draw.text((col1, line1),"Host :%s" % socket.gethostname(), font=font10, fill=255) 165 | draw.text((col1, line2), lan_ip("wlan0"), font=font10, fill=255) 166 | draw.text((col1, line3), network("wlan0"), font=font10, fill=255) 167 | draw.text((col1, line4), lan_ip("uap0"), font=font10, fill=255) 168 | draw.text((col1, line5), network("uap0"), font=font10, fill=255) 169 | ``` 170 | 171 | if for example it's running from RPI 3 with eth0 code could be 172 | ```python 173 | draw.text((col1, line1),"Host :%s" % socket.gethostname(), font=font10, fill=255) 174 | draw.text((col1, line2), lan_ip("eth0"), font=font10, fill=255) 175 | draw.text((col1, line3), network("eth0"), font=font10, fill=255) 176 | draw.text((col1, line4), lan_ip("ap0"), font=font10, fill=255) 177 | draw.text((col1, line5), network("ap0"), font=font10, fill=255) 178 | ``` 179 | -------------------------------------------------------------------------------- /doc/DisplayOled.md: -------------------------------------------------------------------------------- 1 | # Use nice I2C OLED display 2 | 3 | ## Install the packages you need 4 | ``` 5 | sudo apt-get update && sudo apt-get upgrade 6 | sudo apt-get install i2c-tools 7 | ``` 8 | 9 | ## Increase the I2C baudrate from the default of 100KHz to 400KHz 10 | 11 | ``` 12 | sudo nano /boot/config.txt 13 | ``` 14 | 15 | add this line at the end of the file 16 | ``` 17 | dtparam=i2c_arm=on,i2c_baudrate=400000 18 | ``` 19 | 20 | 21 | 22 | ## Add your user to I2C group 23 | 24 | ``` 25 | sudo usermod -a -G i2c loragw 26 | ``` 27 | 28 | then *logout (CTRL-d) and log back so that the group membership permissions take effect* 29 | ```shell 30 | sudo reboot 31 | ``` 32 | 33 | ## Check you can see I2C OLED 34 | 35 | ```shell 36 | i2cdetect -y 1 37 | ``` 38 | 39 | you should see something in your OLED I2C address, here it's 3C (Hex) 40 | ``` 41 | 0 1 2 3 4 5 6 7 8 9 a b c d e f 42 | 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 43 | 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 44 | 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 45 | 30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 46 | 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 47 | 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 48 | 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 49 | 70: -- -- -- -- -- -- -- -- 50 | ``` 51 | 52 | ## Install OLED Python library 53 | 54 | ### Install dependencies 55 | ``` 56 | sudo apt install python-dev python-pip libfreetype6-dev libjpeg-dev build-essential 57 | ``` 58 | 59 | ### Install luma OLED core 60 | You can go for a coffe after this one, takes some time 61 | ``` 62 | sudo -H pip install --upgrade luma.oled 63 | ``` 64 | 65 | ### Get examples files (and font) 66 | ``` 67 | sudo mkdir -p /usr/share/fonts/truetype/luma 68 | git clone https://github.com/rm-hull/luma.examples.git 69 | sudo cp luma.examples/examples/fonts/*.ttf /usr/share/fonts/truetype/luma/ 70 | ``` 71 | 72 | ### Build examples (recommend to skip this section) 73 | But if you want to run all examples, which I do not recommand because it takes SD space and very long time to install, type the following 74 | ``` 75 | sudo apt install libsdl-dev libportmidi-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev libsdl-image1.2-dev 76 | sudo pip install --upgrade setuptools 77 | cd luma.examples 78 | sudo -H pip install -e . 79 | ``` 80 | 81 | ## Configure OLED 82 | 83 | ### Configure OLED Hardware 84 | 85 | You may need to change the following line in `oled.py` according to the connected OLED type and it I2C address. 86 | 87 | The i2c adress can be seen with `i2cdetect -y 1` 88 | 89 | ``` 90 | 0 1 2 3 4 5 6 7 8 9 a b c d e f 91 | 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 92 | 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 93 | 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 94 | 30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 95 | 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 96 | 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 97 | 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 98 | 70: -- -- -- -- -- -- -- -- 99 | ``` 100 | 101 | If you already installed all the Gateway software it's located in `/opt/loragw/oled.py` else in `~/LoraGW-Setup/oled.py` 102 | 103 | Mine is 1.3" SH1106 with 0x3c address so my config is 104 | ```python 105 | serial = i2c(port=1, address=0x3c) 106 | device = sh1106(serial) 107 | ``` 108 | 109 | But as an example, for SSD1306 oled with address 0x3d 110 | ```python 111 | serial = i2c(port=1, address=0x3d) 112 | device = ssd1306(serial) 113 | ``` 114 | 115 | Then you can check OLED display with, shold display something 116 | ``` 117 | ./oled.py 118 | ``` 119 | 120 | 121 | ### Configure OLED software 122 | 123 | You may need to change the following line in `/opt/loragw/oled.py` according to the connected network interface you have 124 | 125 | Mine is wlan0 for WiFi and uap0 for WiFi [access point](https://github.com/ch2i/LoraGW-Setup/blob/master/doc/AccessPoint.md) 126 | ```python 127 | draw.text((col1, line1),"Host :%s" % socket.gethostname(), font=font10, fill=255) 128 | draw.text((col1, line2), lan_ip("wlan0"), font=font10, fill=255) 129 | draw.text((col1, line3), network("wlan0"), font=font10, fill=255) 130 | draw.text((col1, line4), lan_ip("uap0"), font=font10, fill=255) 131 | draw.text((col1, line5), network("uap0"), font=font10, fill=255) 132 | ``` 133 | 134 | if for example it's running from RPI 3 with eth0 code could be 135 | ```python 136 | draw.text((col1, line1),"Host :%s" % socket.gethostname(), font=font10, fill=255) 137 | draw.text((col1, line2), lan_ip("eth0"), font=font10, fill=255) 138 | draw.text((col1, line3), network("eth0"), font=font10, fill=255) 139 | draw.text((col1, line4), lan_ip("wlan0"), font=font10, fill=255) 140 | draw.text((col1, line5), network("wlan0"), font=font10, fill=255) 141 | ``` 142 | 143 | Once all is setup and running you can enable disbale OLED service with 144 | ``` 145 | sudo systemctl enable oled 146 | sudo systemctl disable oled 147 | ``` 148 | 149 | And start or stop the OLED service with 150 | ``` 151 | sudo systemctl start oled 152 | sudo systemctl stop oled 153 | ``` 154 | -------------------------------------------------------------------------------- /doc/Lorawan-Server.md: -------------------------------------------------------------------------------- 1 | # Use LoRaWAN Server 2 | 3 | ## Description 4 | For this we will use a freee open source lorawan-server written by Peter Gotthard. Thanks to him for sharing. The server is very light and efficient and always safisfied my need. 5 | 6 | ## Installation 7 | You can follow the installation procedure [here](https://github.com/gotthardp/lorawan-server/blob/master/doc/Installation.md) 8 | 9 | I generally use to download the Debian package install it with `dpkg -i` 10 | 11 | ``` 12 | wget https://github.com/gotthardp/lorawan-server/releases/download/v0.5.3/lorawan-server_0.5.3_all.deb 13 | dpkg -i lorawan-server_0.5.3_all.deb 14 | ``` 15 | 16 | ## Post Installation 17 | Then to improove log rotation, you can add this lines to 18 | `/etc/logrotate.d/rsyslog` 19 | ``` 20 | /var/log/lorawan-server/crash.log 21 | /var/log/lorawan-server/error.log 22 | /var/log/lorawan-server/debug.log 23 | { 24 | rotate 1 25 | daily 26 | missingok 27 | notifempty 28 | compress 29 | postrotate 30 | invoke-rc.d rsyslog rotate > /dev/null 31 | endscript 32 | } 33 | ``` 34 | 35 | ### Forward packets to local Server 36 | 37 | If you installed all your gateway stuff from this current [repository](https://github.com/ch2i/LoraGW-Setup), you just need to enable the forwarder to send packed to the Local LoRaWAN server. 38 | For this set the line `serv_enabled` to `true` for the server `127.0.0.1` 39 | Take care, the one that has both port to 1680, not the one with 1688/1689 (this one is for OLED) 40 | 41 | `sudo nano /opt/loragw/global_conf.json` 42 | 43 | ```json 44 | { 45 | "server_address": "127.0.0.1", 46 | "serv_enabled": true, // <== set this one to true 47 | "serv_port_up": 1680, 48 | "serv_port_down": 1680 49 | } 50 | ``` 51 | 52 | And restart loragw service to take into account the new local server with 53 | ``` 54 | sudo systemctl stop loragw 55 | sudo systemctl start loragw 56 | ``` 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /loragw.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Lora Gateway 3 | Requires=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | WorkingDirectory=/opt/loragw/ 8 | ExecStart=/opt/loragw/start.sh 9 | SyslogIdentifier=loragw 10 | Restart=on-failure 11 | RestartSec=5 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /monitor-gpio-helium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # ********************************************************************************** 3 | # monitor-gpio-helium.py 4 | # ********************************************************************************** 5 | # Script for monitoring LoraWAN Gateways based on small Linux computers 6 | # it's lighing some LEDs depending on status and process 7 | # it's also monitoring a push button to do a clean shutdown 8 | # this one monitor also status of helium gateway service 9 | # 10 | # Written by Charles-Henri Hallard http://ch2i.eu 11 | # 12 | # History : V1.00 2017-12-22 - Creation 13 | # 14 | # All text above must be included in any redistribution. 15 | # 16 | # ********************************************************************************** 17 | 18 | import RPi.GPIO as GPIO 19 | import thread 20 | import time 21 | import os 22 | import urllib 23 | import sys 24 | import signal 25 | import subprocess 26 | import select 27 | from systemd import journal 28 | 29 | gpio_blu = 4 30 | gpio_yel = 18 31 | gpio_red = 23 32 | gpio_grn = 24 33 | 34 | internet = False # True if internet connected 35 | pktfwd = False # True if packet forwarder is started 36 | helium = False # True if helium_gateway is running 37 | 38 | def signal_handler(signal, frame): 39 | GPIO.output(gpio_blu, GPIO.LOW) 40 | GPIO.output(gpio_yel, GPIO.LOW) 41 | GPIO.output(gpio_red, GPIO.LOW) 42 | GPIO.output(gpio_grn, GPIO.LOW) 43 | sys.exit(0) 44 | 45 | 46 | def check_process(process): 47 | proc = subprocess.Popen(["if pgrep " + process + " >/dev/null 2>&1; then echo '1'; else echo '0'; fi"], stdout=subprocess.PIPE, shell=True) 48 | (ret, err) = proc.communicate() 49 | ret = int(ret) 50 | # print ret 51 | if ret==1: 52 | return True 53 | else: 54 | return False 55 | 56 | def check_inet(delay): 57 | global internet 58 | global pktfwd 59 | global helium 60 | 61 | while True: 62 | #print "check Internet" 63 | try: 64 | url = "https://www.google.com" 65 | urllib.urlopen(url) 66 | internet = True 67 | except: 68 | internet = False 69 | 70 | # Check packet forwarder 71 | pktfwd = check_process("mp_pkt_fwd") or check_process("poly_pkt_fwd") or check_process("lora_pkt_fwd") 72 | # Check helium gateway 73 | helium = check_process("helium_gateway") 74 | 75 | time.sleep(delay) 76 | 77 | def check_packet(pool_delay): 78 | 79 | # Create a systemd.journal.Reader instance 80 | j = journal.Reader() 81 | # Set the reader's default log level 82 | j.log_level(journal.LOG_INFO) 83 | # Filter log entries 84 | j.add_match(_SYSTEMD_UNIT="helium_gateway.service", SYSLOG_IDENTIFIER="helium_gateway") 85 | j.seek_tail() 86 | j.get_previous() 87 | p = select.poll() 88 | # Register the journal's file descriptor with the polling object. 89 | journal_fd = j.fileno() 90 | poll_event_mask = j.get_events() 91 | p.register(journal_fd, poll_event_mask) 92 | 93 | # Poll for new journal entries every 250ms 94 | while True: 95 | if p.poll(pool_delay): 96 | if j.process() == journal.APPEND: 97 | for entry in j: 98 | try: 99 | # message starts with what we call packet 100 | msg = str(entry['MESSAGE']) 101 | if msg.startswith(('uplink','downlink','join')): 102 | GPIO.output(gpio_grn, GPIO.HIGH) 103 | time.sleep(.3) 104 | except: 105 | pass 106 | GPIO.output(gpio_grn, GPIO.LOW) 107 | 108 | # Use the Broadcom SOC Pin numbers 109 | # Setup the Pin with Internal pullups enabled and PIN in reading mode. 110 | GPIO.setwarnings(False) 111 | GPIO.setmode(GPIO.BCM) 112 | GPIO.setup(gpio_blu, GPIO.OUT) 113 | GPIO.setup(gpio_yel, GPIO.OUT) 114 | GPIO.setup(gpio_red, GPIO.OUT) 115 | GPIO.setup(gpio_grn, GPIO.OUT) 116 | 117 | signal.signal(signal.SIGINT, signal_handler) 118 | 119 | try: 120 | thread.start_new_thread( check_inet, (5, ) ) 121 | except: 122 | print "Error: unable to start check_inet thread" 123 | 124 | try: 125 | thread.start_new_thread( check_packet, (250, ) ) 126 | except: 127 | print "Error: unable to start check_packet thread" 128 | 129 | # Now wait! 130 | while True: 131 | # Light off all LEDS 132 | led_blu = GPIO.LOW 133 | led_yel = GPIO.LOW 134 | led_red = GPIO.LOW 135 | 136 | # Blue Internet OK else light RED 137 | if internet == True: 138 | led_blu = GPIO.HIGH 139 | else: 140 | led_red = GPIO.HIGH 141 | 142 | # Yellow forwarders OK else light RED 143 | if pktfwd == True and helium == True: 144 | led_yel = GPIO.HIGH 145 | else: 146 | led_red = GPIO.HIGH 147 | 148 | # Blink Blue (Internet) 149 | GPIO.output(gpio_blu, led_blu) 150 | time.sleep(.2) 151 | GPIO.output(gpio_blu, GPIO.LOW) 152 | time.sleep(.8) 153 | 154 | # Blink Yellow (Forwarders) 155 | GPIO.output(gpio_yel, led_yel) 156 | time.sleep(.2) 157 | GPIO.output(gpio_yel, GPIO.LOW) 158 | time.sleep(.8) 159 | 160 | 161 | -------------------------------------------------------------------------------- /monitor-gpio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # ********************************************************************************** 3 | # monitor-gpio.py 4 | # ********************************************************************************** 5 | # Script for monitoring LoraWAN Gateways based on small Linux computers 6 | # it's lighing some LEDs depending on status and process 7 | # it's also monitoring a push button to do a clean shutdown 8 | # 9 | # Written by Charles-Henri Hallard http://ch2i.eu 10 | # 11 | # History : V1.00 2017-12-22 - Creation 12 | # 13 | # All text above must be included in any redistribution. 14 | # 15 | # ********************************************************************************** 16 | 17 | import RPi.GPIO as GPIO 18 | import thread 19 | import time 20 | import os 21 | import urllib 22 | import sys 23 | import signal 24 | import subprocess 25 | 26 | # Switch push button 27 | gpio_pin = 19 28 | 29 | gpio_blu = 4 30 | gpio_yel = 18 31 | gpio_red = 23 32 | gpio_grn = 24 33 | 34 | internet = False # True if internet connected 35 | lorawan = False # True if local LoraWan server is running 36 | web = False # True if local Web Server is running 37 | hostapd = False # True if wifi access point is started 38 | pktfwd = False # True if packet forwarder is started 39 | 40 | 41 | def signal_handler(signal, frame): 42 | GPIO.output(gpio_blu, GPIO.LOW) 43 | GPIO.output(gpio_yel, GPIO.LOW) 44 | GPIO.output(gpio_red, GPIO.LOW) 45 | GPIO.output(gpio_grn, GPIO.LOW) 46 | sys.exit(0) 47 | 48 | 49 | def check_process(process): 50 | proc = subprocess.Popen(["if pgrep " + process + " >/dev/null 2>&1; then echo '1'; else echo '0'; fi"], stdout=subprocess.PIPE, shell=True) 51 | (ret, err) = proc.communicate() 52 | ret = int(ret) 53 | # print ret 54 | if ret==1: 55 | return True 56 | else: 57 | return False 58 | 59 | def check_inet(delay): 60 | global internet 61 | global lorawan 62 | global web 63 | global hostapd 64 | global pktfwd 65 | 66 | while True: 67 | #print "check Internet" 68 | try: 69 | url = "https://www.google.com" 70 | urllib.urlopen(url) 71 | internet = True 72 | except: 73 | internet = False 74 | 75 | try: 76 | url = "http://127.0.0.1" 77 | urllib.urlopen(url) 78 | web = True 79 | except: 80 | web = False 81 | 82 | try: 83 | url = "http://127.0.0.1:8080" 84 | urllib.urlopen(url) 85 | lorawan = True 86 | except: 87 | lorawan = False 88 | 89 | # Check WiFi AP mode and packet forwarder 90 | #hostapd = check_process("hostapd") 91 | pktfwd = check_process("mp_pkt_fwd") or check_process("poly_pkt_fwd") 92 | 93 | time.sleep(delay) 94 | 95 | # Use the Broadcom SOC Pin numbers 96 | # Setup the Pin with Internal pullups enabled and PIN in reading mode. 97 | GPIO.setwarnings(False) 98 | GPIO.setmode(GPIO.BCM) 99 | GPIO.setup(gpio_pin, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) 100 | GPIO.setup(gpio_blu, GPIO.OUT) 101 | GPIO.setup(gpio_yel, GPIO.OUT) 102 | GPIO.setup(gpio_red, GPIO.OUT) 103 | GPIO.setup(gpio_grn, GPIO.OUT) 104 | 105 | 106 | # Our function on what to do when the button is pressed 107 | def checkShutdown(): 108 | if GPIO.input(gpio_pin) == 1: 109 | 110 | GPIO.output(gpio_blu, GPIO.LOW) 111 | GPIO.output(gpio_yel, GPIO.LOW) 112 | GPIO.output(gpio_red, GPIO.LOW) 113 | GPIO.output(gpio_grn, GPIO.LOW) 114 | 115 | time.sleep(.9) 116 | if GPIO.input(gpio_pin) == 1: 117 | GPIO.output(gpio_blu, GPIO.HIGH) 118 | time.sleep(.9) 119 | if GPIO.input(gpio_pin) == 1: 120 | GPIO.output(gpio_yel, GPIO.HIGH) 121 | time.sleep(.9) 122 | if GPIO.input(gpio_pin) == 1: 123 | GPIO.output(gpio_red, GPIO.HIGH) 124 | time.sleep(.9) 125 | if GPIO.input(gpio_pin) == 1: 126 | for x in range(0, 10): 127 | GPIO.output(gpio_blu, GPIO.HIGH) 128 | GPIO.output(gpio_yel, GPIO.HIGH) 129 | GPIO.output(gpio_red, GPIO.HIGH) 130 | GPIO.output(gpio_grn, GPIO.HIGH) 131 | time.sleep(.2) 132 | GPIO.output(gpio_blu, GPIO.LOW) 133 | GPIO.output(gpio_yel, GPIO.LOW) 134 | GPIO.output(gpio_red, GPIO.LOW) 135 | GPIO.output(gpio_grn, GPIO.LOW) 136 | time.sleep(.4) 137 | print "shutdown" 138 | os.system("sudo halt &") 139 | time.sleep(30) 140 | 141 | signal.signal(signal.SIGINT, signal_handler) 142 | 143 | try: 144 | thread.start_new_thread( check_inet, (5, ) ) 145 | except: 146 | print "Error: unable to start thread" 147 | 148 | # Now wait! 149 | while 1: 150 | led_blu = GPIO.LOW 151 | led_yel = GPIO.LOW 152 | led_red = GPIO.LOW 153 | led_grn = GPIO.LOW 154 | 155 | if internet == True: 156 | led_blu = GPIO.HIGH 157 | else: 158 | led_red = GPIO.HIGH 159 | 160 | if web == True: 161 | led_yel = GPIO.HIGH 162 | else: 163 | led_red = GPIO.HIGH 164 | 165 | if pktfwd == True: 166 | led_grn = GPIO.HIGH 167 | else: 168 | led_red = GPIO.HIGH 169 | 170 | GPIO.output(gpio_blu, led_blu) 171 | time.sleep(.2) 172 | checkShutdown(); 173 | GPIO.output(gpio_blu, GPIO.LOW) 174 | time.sleep(.8) 175 | checkShutdown(); 176 | 177 | GPIO.output(gpio_yel, led_yel) 178 | time.sleep(.2) 179 | checkShutdown(); 180 | GPIO.output(gpio_yel, GPIO.LOW) 181 | time.sleep(.8) 182 | checkShutdown(); 183 | 184 | GPIO.output(gpio_grn, led_grn) 185 | time.sleep(.2) 186 | checkShutdown(); 187 | GPIO.output(gpio_grn, GPIO.LOW) 188 | time.sleep(.8) 189 | checkShutdown(); 190 | 191 | 192 | -------------------------------------------------------------------------------- /monitor-ws2812.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # ********************************************************************************** 4 | # monitor-ws2812.py 5 | # ********************************************************************************** 6 | # Script for monitoring LoraWAN Gateways based on small Linux computers 7 | # it's lighing some WS2812 LEDs depending on status and process 8 | # it's also monitoring a push button to do a clean shutdown 9 | # 10 | # Written by Charles-Henri Hallard http://ch2i.eu 11 | # 12 | # History : V1.00 2017-12-22 - Creation 13 | # 14 | # All text above must be included in any redistribution. 15 | # 16 | # Poor code, written in a hurry, need optimization and rewrite for sure 17 | # 18 | # ********************************************************************************** 19 | 20 | import RPi.GPIO as GPIO 21 | import thread 22 | import time 23 | import os 24 | import urllib 25 | from neopixel import * 26 | import sys 27 | import signal 28 | import subprocess 29 | 30 | gpio_pin = 17 # Switch push button pin 31 | gpio_led = 18 # WS2812 led pin 32 | 33 | internet = False # True if internet connected 34 | lorawan = False # True if local LoraWan server is running 35 | web = False # True if local Web Server is running 36 | 37 | hostapd = False # True if wifi access point is started 38 | pktfwd = False # True if packet forwarder is started 39 | 40 | # LED color definition 41 | color_off = Color(0,0,0) 42 | color_red = Color(128,0,0) 43 | color_grn = Color(0,128,0) 44 | color_blu = Color(0,0,128) 45 | color_yel = Color(128,128,0) 46 | 47 | def signal_handler(signal, frame): 48 | colorSet(strip, 0, color_off ) 49 | colorSet(strip, 1, color_off ) 50 | sys.exit(0) 51 | 52 | 53 | # Check if a process is running 54 | def check_process(process): 55 | proc = subprocess.Popen(["if pgrep " + process + " >/dev/null 2>&1; then echo '1'; else echo '0'; fi"], stdout=subprocess.PIPE, shell=True) 56 | (ret, err) = proc.communicate() 57 | ret = int(ret) 58 | #print ret 59 | if ret==1: 60 | return True 61 | else: 62 | return False 63 | 64 | # Check internet connected 65 | def check_inet(delay): 66 | global internet 67 | global lorawan 68 | global web 69 | global hostapd 70 | global pktfwd 71 | 72 | while True: 73 | #print "check Internet" 74 | try: 75 | url = "https://www.google.com" 76 | urllib.urlopen(url) 77 | internet = True 78 | except: 79 | internet = False 80 | 81 | # Check local Web Server (if any) 82 | try: 83 | url = "http://127.0.0.1" 84 | urllib.urlopen(url) 85 | web = True 86 | except: 87 | web = False 88 | 89 | # Check local LoRaWAN server (if any) 90 | try: 91 | url = "http://127.0.0.1:8080" 92 | urllib.urlopen(url) 93 | lorawan = True 94 | except: 95 | lorawan = False 96 | 97 | # Check WiFi AP mode and packet forwarder 98 | hostapd = check_process("hostapd") 99 | pktfwd = check_process("mp_pkt_fwd") or check_process("poly_pkt_fwd") 100 | 101 | time.sleep(delay) 102 | 103 | 104 | # Use the Broadcom SOC Pin numbers 105 | # Setup the Switch pin with pulldown enabled and PIN in input 106 | GPIO.setwarnings(False) 107 | GPIO.setmode(GPIO.BCM) 108 | GPIO.setup(gpio_pin, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) 109 | 110 | 111 | # Define functions which animate LEDs in various ways. 112 | def colorSet(strip, led, color): 113 | strip.setPixelColor(led, color) 114 | strip.show() 115 | 116 | # Our function on what to do when the button is pressed 117 | def checkShutdown(): 118 | # Button pressed light LED1 in RED 119 | if GPIO.input(gpio_pin) == 1: 120 | colorSet(strip, 0, color_red) 121 | colorSet(strip, 1, color_off) 122 | time.sleep(.9) 123 | # Still pressed after 900ms light LED2 in RED 124 | if GPIO.input(gpio_pin) == 1: 125 | colorSet(strip, 1, color_red) 126 | time.sleep(.9) 127 | # Still pressed after 900ms more, blink blue 10 times 128 | if GPIO.input(gpio_pin) == 1: 129 | for x in range(0, 10): 130 | colorSet(strip, 0, color_blu) 131 | colorSet(strip, 1, color_blu) 132 | time.sleep(.2) 133 | colorSet(strip, 0, color_off) 134 | colorSet(strip, 1, color_off) 135 | time.sleep(.4) 136 | print "shutdown" 137 | # start the poweroff process in background 138 | os.system("sudo poweroff &") 139 | # prevent this script to continue 140 | time.sleep(30) 141 | 142 | signal.signal(signal.SIGINT, signal_handler) 143 | 144 | # Create NeoPixel object 2 LEDs, 64 Brighness GRB leds 145 | strip = Adafruit_NeoPixel(2, gpio_led, 800000, 10, False, 64, 0, ws.WS2811_STRIP_GRB) 146 | 147 | # Intialize the library (must be called once before other functions). 148 | strip.begin() 149 | 150 | # Check connection/process every 5 seconds 151 | try: 152 | thread.start_new_thread( check_inet, (5, ) ) 153 | except: 154 | print "Error: unable to start thread" 155 | 156 | # Now wait in infinite loop 157 | while 1: 158 | led0 = color_red 159 | led1 = color_red 160 | 161 | if internet == True: 162 | led0 = color_grn 163 | else: 164 | if hostapd == True: 165 | led0 = color_blu 166 | 167 | # if pktfwd == True and web == True and lorawan == True: 168 | if pktfwd == True : 169 | led1 = color_grn 170 | else: 171 | if lorawan == True: 172 | led0 = color_blu 173 | 174 | colorSet(strip, 0, led0) 175 | time.sleep(.2) 176 | checkShutdown(); 177 | colorSet(strip, 0, color_off) 178 | time.sleep(.8) 179 | checkShutdown(); 180 | 181 | colorSet(strip, 1, led1) 182 | time.sleep(.2) 183 | checkShutdown(); 184 | colorSet(strip, 1, color_off) 185 | time.sleep(.8) 186 | checkShutdown(); 187 | -------------------------------------------------------------------------------- /monitor.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=LoraGW monitoring service 3 | 4 | [Service] 5 | WorkingDirectory=/opt/loragw/ 6 | ExecStart=/opt/loragw/monitor.py 7 | SyslogIdentifier=monitor 8 | Restart=on-failure 9 | RestartSec=5 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /oled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import psutil 5 | import platform 6 | import thread 7 | import socket 8 | import SocketServer 9 | import json 10 | from luma.core.interface.serial import i2c 11 | from luma.core.render import canvas 12 | from luma.oled.device import ssd1306 13 | from luma.oled.device import sh1106 14 | from PIL import ImageFont 15 | import time 16 | from datetime import datetime 17 | 18 | width=128 19 | height=64 20 | 21 | font_pixel=12 22 | line1=0 23 | line2=line1+12 24 | line3=line2+12 25 | line4=line3+12 26 | line5=line4+12 27 | col1=0 28 | lora_rssi = None 29 | lora_chan = None 30 | lora_freq = None 31 | lora_datr = None 32 | lora_mote = None 33 | 34 | class LeaseEntry: 35 | def __init__(self, leasetime, macAddress, ipAddress, name): 36 | if (leasetime == '0'): 37 | self.staticIP = True 38 | else: 39 | self.staticIP = False 40 | self.leasetime = datetime.fromtimestamp(int(leasetime)).strftime('%Y-%m-%d %H:%M:%S') 41 | self.macAddress = macAddress.upper() 42 | self.ipAddress = ipAddress 43 | self.name = name 44 | 45 | def serialize(self): 46 | return { 47 | 'staticIP': self.staticIP, 48 | 'leasetime': self.leasetime, 49 | 'macAddress': self.macAddress, 50 | 'ipAddress': self.ipAddress, 51 | 'name': self.name 52 | } 53 | 54 | 55 | class MyUDPHandler(SocketServer.BaseRequestHandler): 56 | 57 | def is_json(self, myjson): 58 | try: 59 | json_object = json.loads(myjson) 60 | except ValueError, e: 61 | return False 62 | return True 63 | 64 | def handle(self): 65 | json_data 66 | global lora_rssi 67 | global lora_chan 68 | global lora_freq 69 | global lora_datr 70 | global lora_mote 71 | 72 | data = self.request[0] 73 | # for poly_pkt_fwd remove 12 first bytes 74 | # if mp_pkt_fwd then packet are already fine 75 | if data.find("{\"rxpk\":[") > 0: 76 | data = data[12::] 77 | 78 | if self.is_json(data): 79 | #print("\r\n") 80 | #print(data) 81 | js_data = json.loads(data) 82 | 83 | # for mp_pkt_fwd 84 | if js_data.get('type')=='uplink': 85 | lora_mote = js_data["mote"] 86 | lora_rssi = js_data["rssi"] 87 | lora_chan = js_data["chan"] 88 | lora_freq = js_data["freq"] 89 | lora_datr = js_data["datr"] 90 | # for poly_pkt_fwd 91 | elif js_data.get('rxpk'): 92 | lora_mote = "legacy_fwd" 93 | lora_rssi = js_data["rxpk"][0]["rssi"] 94 | lora_chan = js_data["rxpk"][0]["chan"] 95 | lora_freq = js_data["rxpk"][0]["freq"] 96 | lora_datr = js_data["rxpk"][0]["datr"] 97 | 98 | def leaseSort(arg): 99 | # Fixed IPs first 100 | if arg.staticIP == True: 101 | return 0 102 | else: 103 | return arg.name.lower() 104 | 105 | def getLeases(): 106 | leases = list() 107 | try: 108 | with open("/var/lib/misc/dnsmasq.leases") as f: 109 | for line in f: 110 | elements = line.split() 111 | if len(elements) == 5: 112 | entry = LeaseEntry(elements[0],elements[1],elements[2],elements[3]) 113 | leases.append(entry) 114 | 115 | leases.sort(key = leaseSort) 116 | return [lease.serialize() for lease in leases] 117 | 118 | except Exception as e: 119 | print "error in getLeases" 120 | print(e) 121 | print(type(e)) 122 | return None 123 | 124 | 125 | def do_nothing(obj): 126 | pass 127 | 128 | def make_font(name, size): 129 | font_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fonts', name)) 130 | return ImageFont.truetype(font_path, size) 131 | 132 | # rev.1 users set port=0 133 | # substitute spi(device=0, port=0) below if using that interface 134 | serial = i2c(port=1, address=0x3c) 135 | device = sh1106(serial) 136 | #device = ssd1306(serial) 137 | device.cleanup = do_nothing 138 | 139 | font10 = make_font("/usr/share/fonts/truetype/luma/ProggyTiny.ttf", 16) 140 | byteunits = ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB') 141 | 142 | def udp_receive(delay): 143 | server = SocketServer.UDPServer(("127.0.0.1", 1688), MyUDPHandler) 144 | server.serve_forever() 145 | #server.handle_request() 146 | 147 | def filesizeformat(value): 148 | exponent = int(log(value, 1024)) 149 | return "%.1f %s" % (float(value) / pow(1024, exponent), byteunits[exponent]) 150 | 151 | def bytes2human(n): 152 | symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') 153 | prefix = {} 154 | for i, s in enumerate(symbols): 155 | prefix[s] = 1 << (i + 1) * 10 156 | for s in reversed(symbols): 157 | if n >= prefix[s]: 158 | value = int(float(n) / prefix[s]) 159 | return '%s%s' % (value, s) 160 | return "%sB" % n 161 | 162 | def if_stat(iface): 163 | try: 164 | stat = psutil.net_io_counters(pernic=True)[iface] 165 | return "Tx %s Rx %s" % (bytes2human(stat.bytes_sent), bytes2human(stat.bytes_recv)) 166 | 167 | except KeyError as e: 168 | return "Tx %s Rx %s" % (bytes2human(0), bytes2human(0)) 169 | 170 | def lan_info(iface): 171 | ip = "No Interface" 172 | stat = "" 173 | try: 174 | for snic in psutil.net_if_addrs()[iface]: 175 | if snic.family == socket.AF_INET: 176 | ip = snic.address 177 | stat = if_stat(iface) 178 | break 179 | else: 180 | ip ="No IP" 181 | 182 | return "%-5s: %s" % (iface, ip), stat 183 | 184 | except KeyError as e: 185 | return ip, stat 186 | 187 | 188 | def uptime(): 189 | try: 190 | f = open( "/proc/uptime" ) 191 | contents = f.read().split() 192 | f.close() 193 | except: 194 | return "no /proc/uptime" 195 | 196 | total_seconds = float(contents[0]) 197 | 198 | # Helper vars: 199 | MINUTE = 60 200 | HOUR = MINUTE * 60 201 | DAY = HOUR * 24 202 | 203 | # Get the days, hours, etc: 204 | days = int( total_seconds / DAY ) 205 | hours = int( ( total_seconds % DAY ) / HOUR ) 206 | minutes = int( ( total_seconds % HOUR ) / MINUTE ) 207 | seconds = int( total_seconds % MINUTE ) 208 | 209 | # Build up the pretty string (like this: "N days, N hours, N minutes, N seconds") 210 | string = "" 211 | if days > 0: 212 | string += str(days) + "d " 213 | if len(string) > 0 or hours > 0: 214 | string += str(hours) + "h " 215 | if len(string) > 0 or minutes > 0: 216 | string += str(minutes) + "m " 217 | if hours == 0 and seconds >0: 218 | string += str(seconds) + "s" 219 | 220 | return string; 221 | 222 | def ip_client(thislease): 223 | ips = thislease["ipAddress"].split('.') 224 | return "%s.%s: %s" % (ips[2],ips[3], thislease["name"]) 225 | 226 | def stats(): 227 | global looper 228 | lease = None 229 | with canvas(device) as draw: 230 | #draw.rectangle((0,0,127,63), outline="white", fill="black") 231 | if looper==0: 232 | looper=1 233 | if lora_mote != None: 234 | draw.text((col1, line1),"Mote %s" % lora_mote, font=font10, fill=255) 235 | draw.text((col1, line2),"RSSI %sdBm" % lora_rssi, font=font10, fill=255) 236 | draw.text((col1, line3),"Chan %s" % lora_chan, font=font10, fill=255) 237 | draw.text((col1, line4),"Freq %.2f MHz" % lora_freq, font=font10, fill=255) 238 | draw.text((col1, line5),"Rate %s" % lora_datr, font=font10, fill=255) 239 | else: 240 | draw.text((col1, line1),"No LoRaWAN Data yet", font=font10, fill=255) 241 | 242 | elif looper==1: 243 | looper = 2 244 | draw.text((col1, line1),"Host :%s" % socket.gethostname(), font=font10, fill=255) 245 | # Try to get wlan0 if not then eth0 246 | ip, stats = lan_info("eth0") 247 | if ip == "No Interface": 248 | ip, stats = lan_info("wlan0") 249 | 250 | draw.text((col1, line2), ip, font=font10, fill=255) 251 | draw.text((col1, line3), stats, font=font10, fill=255) 252 | 253 | ip, stats = lan_info("ap0") 254 | if ip != "No IP" and ip != "No Interface": 255 | draw.text((col1, line4), ip, font=font10, fill=255) 256 | draw.text((col1, line5), stats, font=font10, fill=255) 257 | lease = getLeases() 258 | else: 259 | draw.text((col1, line4), "ap0", font=font10, fill=255) 260 | draw.text((col1, line5), "No Access Point", font=font10, fill=255) 261 | 262 | if lease == None: 263 | looper = 3 264 | elif looper==2: 265 | looper = 3 266 | lease = getLeases() 267 | lg = len(lease) 268 | 269 | draw.text((col1, line1), "Wifi Clients => %d" % lg, font=font10, fill=255) 270 | if lg>=1: draw.text((col1, line2), ip_client(lease[0]), font=font10, fill=255) 271 | if lg>=2: draw.text((col1, line3), ip_client(lease[1]), font=font10, fill=255) 272 | if lg>=3: draw.text((col1, line4), ip_client(lease[2]), font=font10, fill=255) 273 | if lg>=4: draw.text((col1, line5), ip_client(lease[3]), font=font10, fill=255) 274 | 275 | elif looper==3: 276 | looper = 0 277 | tempC = int(open('/sys/class/thermal/thermal_zone0/temp').read()) 278 | av1, av2, av3 = os.getloadavg() 279 | mem = psutil.virtual_memory() 280 | dsk = psutil.disk_usage("/") 281 | 282 | draw.text((col1, line1), "CPU LOAD: %.1f %.1f" % (av1, av3), font=font10, fill=255) 283 | draw.text((col1, line2), "MEM FREE: %s/%s" % (bytes2human(mem.available), bytes2human(mem.total)), font=font10, fill=255) 284 | draw.text((col1, line3), "DSK FREE: %s/%s" % (bytes2human(dsk.total-dsk.used), bytes2human(dsk.total)),font=font10, fill=255) 285 | draw.text((col1, line4), "CPU TEMP: %sc" % (str(tempC/1000)), font=font10, fill=255) 286 | draw.text((col1, line5), "UP : %s" % uptime(), font=font10, fill=255) 287 | 288 | else: 289 | #draw.text((col1, line1),"%s %s" % (platform.system(),platform.release()), font=font10, fill=255) 290 | #uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time()) 291 | #draw.text((col1, line2),str(datetime.now().strftime('%a %b %d %H:%M:%S')), font=font10, fill=255) 292 | #draw.text((col1, line3),"Uptime %s" % str(uptime).split('.')[0], font=font10, fill=255) 293 | looper=0 294 | 295 | def main(): 296 | global looper 297 | global json_data 298 | looper = 1 299 | json_data = None 300 | 301 | try: 302 | thread.start_new_thread( udp_receive, (5, ) ) 303 | except: 304 | print "Error: unable to start thread" 305 | 306 | while True: 307 | stats() 308 | if looper==0: 309 | time.sleep(2) 310 | else: 311 | time.sleep(5) 312 | 313 | if __name__ == "__main__": 314 | try: 315 | main() 316 | except KeyboardInterrupt: 317 | pass 318 | -------------------------------------------------------------------------------- /oled.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=LoraGW OLED service 3 | 4 | [Service] 5 | WorkingDirectory=/opt/loragw/ 6 | ExecStart=/opt/loragw/oled.py 7 | SyslogIdentifier=monitor 8 | Restart=on-failure 9 | RestartSec=5 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /sensors_js/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ****************************************************************************** 4 | # installer script to send sensors data to cayenne IoT dashboard 5 | # you can have a BMP280/BME280 and SI7021/HTU21D conencted to I2C bus 6 | # This sample has been written by Charles-Henri Hallard (ch2i.eu) 7 | # ******************************************************************************* 8 | 9 | function prog_installed { 10 | local ret=1 11 | type $1 >/dev/null 2>&1 || { local ret=0; } 12 | echo "$ret" 13 | } 14 | 15 | # Stop on the first sign of trouble 16 | set -e 17 | 18 | if [ $UID != 0 ]; then 19 | echo "ERROR: Operation not permitted. Forgot sudo?" 20 | exit 1 21 | fi 22 | 23 | # Install repositories 24 | INSTALL_DIR="/opt/loragw" 25 | if [ ! -d "$INSTALL_DIR" ]; then mkdir $INSTALL_DIR; fi 26 | 27 | 28 | # Check if nodejs is installed 29 | if [ $(prog_installed node) == 0 ]; then 30 | echo "nodejs not found, please install following lastest procedure" 31 | exit 1; 32 | else 33 | echo "nodejs found, "`node -v` 34 | fi 35 | 36 | # nodejs module dependencies 37 | pushd $INSTALL_DIR 38 | if [ ! -d node_modules ]; then mkdir node_modules; fi 39 | modules=( bme280-sensor si7021-sensor cayennejs ) 40 | for mod in "${modules[@]}" 41 | do 42 | echo "Installing NodeJS $mod" 43 | npm install -g --unsafe-perm $mod 44 | npm link $mod 45 | done 46 | popd 47 | 48 | printf "Please enter your cayenne credentials and your\n" 49 | printf "device client ID (see https://cayenne.mydevices.com/)\n" 50 | printf " Cayenne username : " 51 | 52 | read CAYENNE_USER 53 | if [[ $CAYENNE_USER != "" ]]; then sed -i -- 's/__CAYENNE_USER__/'$CAYENNE_USER'/g' sensors.js; fi 54 | printf " Cayenne password : " 55 | read CAYENNE_PASS 56 | if [[ $CAYENNE_PASS != "" ]]; then sed -i -- 's/__CAYENNE_PASS__/'$CAYENNE_PASS'/g' sensors.js; fi 57 | printf " Cayenne clientID : " 58 | read CAYENNE_CLID 59 | if [[ $CAYENNE_CLID != "" ]]; then sed -i -- 's/__CAYENNE_CLID__/'$CAYENNE_CLID'/g' sensors.js; fi 60 | 61 | # cp nodejs sensor script 62 | cp -f ./sensors.js $INSTALL_DIR/ 63 | 64 | # cp startup script as a service 65 | cp ./sensors-js.service /lib/systemd/system/ 66 | 67 | # Enable the service 68 | systemctl enable sensors-js.service 69 | systemctl start sensors-js.service 70 | 71 | # wait service to read values 72 | echo "Waiting service to start and connect..." 73 | sleep 3 74 | 75 | echo 76 | echo "Installation completed.\n" 77 | echo "use sudo systemctl status sensors-js to see service status" 78 | echo "use sudo journalctl -f -u sensors-js to see service log" 79 | echo -n 80 | systemctl status sensors-js.service 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /sensors_js/sensors-js.service: -------------------------------------------------------------------------------- 1 | # Enable the service 2 | # systemctl enable sensors-js.service 3 | # 4 | # Start the service 5 | # systemctl start sensors-js.service 6 | # 7 | # Verify it is running 8 | # systemctl status sensors-js.service 9 | 10 | 11 | [Unit] 12 | Description=CH2i Lora Gateway Sensors service 13 | 14 | [Service] 15 | ExecStart=/usr/bin/node /opt/loragw/sensors.js 16 | Restart=always 17 | RestartSec=30 18 | StandardOutput=syslog 19 | StandardError=syslog 20 | SyslogIdentifier=sensors-js 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /sensors_js/sensors.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * NodeJS script to send sensors data to cayenne IoT dashboard 3 | * you can have a BMP280/BME280 and SI7021/HTU21D conencted to I2C bus 4 | * This sample has been written by Charles-Henri Hallard (ch2i.eu) 5 | * 6 | * Requires nodejs to be already installed 7 | * https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions 8 | * 9 | * Requires CayenneJS bme280 and si7021 libraries 10 | * npm install bme280-sensor si7021-sensor cayennejs 11 | * 12 | * Don't forget to put your Cayenne credential in this script, all of this 13 | * stuff can be done with the script installer install.sh of this folder 14 | * installer will also make the as daemon to start/stop with the system 15 | * 16 | * sudo node sensors.js 17 | * 18 | *******************************************************************************/ 19 | 20 | const BME280 = require('bme280-sensor'); 21 | const Si7021 = require('si7021-sensor'); 22 | var Cayenne = require('cayennejs'); 23 | 24 | const bme280 = new BME280({ i2cBusNo : 1, i2cAddress : 0x76 }); 25 | const si7021 = new Si7021({ i2cBusNo : 1, i2cAddress : 0x40 }); 26 | 27 | // Update every x seconds 28 | const updateInterval = 60; 29 | 30 | // Initiate Cayenne MQTT API 31 | const cayenneClient = new Cayenne.MQTT({ 32 | username: "a1ced9e0-b24e-11e6-bb76-1157597ded22", 33 | password: "0858f39268653283bf68bb08b165c07cd6cb1959", 34 | clientId: "63bf8990-bd27-11e6-ae5a-dfc2c3108b24" 35 | }); 36 | 37 | // Read BME280 sensor data, repeat 38 | const readBME280SensorData = () => { 39 | bme280.readSensorData() 40 | .then((data) => { 41 | 42 | console.log(`BME280 data = ${JSON.stringify(data, null, 2)}`); 43 | 44 | // dashboard widget automatically detects datatype & unit 45 | cayenneClient.celsiusWrite (0, data.temperature_C.toFixed(1)); 46 | cayenneClient.rawWrite(1, data.humidity.toFixed(0), "rel_hum" , "p" ); 47 | cayenneClient.hectoPascalWrite (2, data.pressure_hPa.toFixed(0)); 48 | 49 | setTimeout(readBME280SensorData, updateInterval*1000); 50 | }) 51 | .catch((err) => { 52 | console.log(`BME280 read error: ${err}`); 53 | setTimeout(readBME280SensorData, updateInterval*1000); 54 | }); 55 | }; 56 | 57 | const readSI7021SensorData = () => { 58 | si7021.readSensorData() 59 | .then((data) => { 60 | 61 | console.log(`SI7021 data = ${JSON.stringify(data, null, 2)}`); 62 | 63 | cayenneClient.celsiusWrite (4, data.temperature_C.toFixed(1)); 64 | cayenneClient.rawWrite(5, data.humidity.toFixed(0), "rel_hum" , "p" ); 65 | 66 | setTimeout(readSI7021SensorData, updateInterval*1000); 67 | }) 68 | .catch((err) => { 69 | console.log(`Si7021 read error: ${err}`); 70 | setTimeout(readSI7021SensorData, updateInterval*1000); 71 | }); 72 | }; 73 | 74 | cayenneClient.connect((err , mqttClient) => { 75 | console.log('Cayenne connected') 76 | 77 | // Initialize the BME280 sensor 78 | bme280.init() 79 | .then(() => { 80 | console.log('BME280 initialization succeeded'); 81 | readBME280SensorData(); 82 | }) 83 | .catch((err) => console.error(`BME280 initialization failed: ${err} `)); 84 | 85 | // Initialize the si7021 sensor 86 | si7021.reset() 87 | .then(() => { 88 | console.log('SI7021 initialization succeeded'); 89 | readSI7021SensorData(); 90 | }) 91 | .catch((err) => console.error(`Si7021 reset failed: ${err} `)); 92 | }) 93 | 94 | -------------------------------------------------------------------------------- /set_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Author: JP Meijers 4 | Date: 2017-02-26 5 | Based on: https://github.com/rayozzie/ttn-resin-gateway-rpi/blob/master/run.sh 6 | """ 7 | import os 8 | import os.path 9 | import sys 10 | import urllib2 11 | import time 12 | import uuid 13 | import json 14 | import subprocess 15 | try: 16 | import urlparse 17 | except: 18 | import urllib.parse as urlparse 19 | try: 20 | import RPi.GPIO as GPIO 21 | except RuntimeError: 22 | print("Error importing RPi.GPIO! This is probably because you need superuser privileges. You can achieve this by using 'sudo' to run your script") 23 | 24 | GWID_PREFIX="FFFE" 25 | 26 | if os.environ.get('HALT') != None: 27 | print ("*** HALT asserted - exiting ***") 28 | sys.exit(0) 29 | 30 | # Check if the correct environment variables are set 31 | 32 | print ("*******************") 33 | print ("*** Configuration:") 34 | print ("*******************") 35 | 36 | if os.environ.get("GW_EUI")==None: 37 | # The FFFE should be inserted in the middle (so xxxxxxFFFExxxxxx) 38 | my_eui = format(uuid.getnode(), '012x') 39 | my_eui = my_eui[:6]+GWID_PREFIX+my_eui[6:] 40 | my_eui = my_eui.upper() 41 | else: 42 | my_eui = os.environ.get("GW_EUI") 43 | 44 | 45 | # Define default configs 46 | description = os.getenv('GW_DESCRIPTION', "") 47 | placement = "" 48 | latitude = os.getenv('GW_REF_LATITUDE', 0) 49 | longitude = os.getenv('GW_REF_LONGITUDE', 0) 50 | altitude = os.getenv('GW_REF_ALTITUDE', 0) 51 | frequency_plan_url = os.getenv('FREQ_PLAN_URL', "https://account.thethingsnetwork.org/api/v2/frequency-plans/EU_863_870") 52 | 53 | # Fetch config from TTN if TTN is enabled 54 | if(os.getenv('SERVER_TTN', "true")=="true"): 55 | 56 | if os.environ.get("GW_ID")==None: 57 | print ("ERROR: GW_ID required") 58 | print ("See https://www.thethingsnetwork.org/docs/gateways/registration.html#via-gateway-connector") 59 | sys.exit(0) 60 | 61 | if os.environ.get("GW_KEY")==None: 62 | print ("ERROR: GW_KEY required") 63 | print ("See https://www.thethingsnetwork.org/docs/gateways/registration.html#via-gateway-connector") 64 | sys.exit(0) 65 | 66 | print ("*******************") 67 | print ("*** Fetching config from TTN account server") 68 | print ("*******************") 69 | 70 | # Fetch the URL, if it fails try 30 seconds later again. 71 | config_response = "" 72 | try: 73 | req = urllib2.Request('https://account.thethingsnetwork.org/gateways/'+os.environ.get("GW_ID")) 74 | req.add_header('Authorization', 'Key '+os.environ.get("GW_KEY")) 75 | response = urllib2.urlopen(req, timeout=30) 76 | config_response = response.read() 77 | except urllib2.URLError as err: 78 | print ("Unable to fetch configuration from TTN. Are your GW_ID and GW_KEY correct?") 79 | sys.exit(0) 80 | 81 | # Parse config 82 | ttn_config = {} 83 | try: 84 | ttn_config = json.loads(config_response) 85 | except: 86 | print ("Unable to parse configuration from TTN") 87 | sys.exit(0) 88 | 89 | frequency_plan = ttn_config.get('frequency_plan', "EU_863_870") 90 | frequency_plan_url = ttn_config.get('frequency_plan_url', "https://account.thethingsnetwork.org/api/v2/frequency-plans/EU_863_870") 91 | 92 | if "router" in ttn_config: 93 | router = ttn_config['router'].get('mqtt_address', "mqtt://router.dev.thethings.network:1883") 94 | router = urlparse.urlparse(router) 95 | #router = router.hostname # mp_pkt_fwd only wants the hostname, not the protocol and port 96 | router = router.path # mp_pkt_fwd only wants the hostname, not the protocol and port 97 | else: 98 | router = "router.dev.thethings.network" 99 | 100 | if "attributes" in ttn_config: 101 | description = ttn_config['attributes'].get('description', "") 102 | placement = ttn_config['attributes'].get('placement', "unknown") 103 | 104 | if "antenna_location" in ttn_config: 105 | latitude = ttn_config['antenna_location'].get('latitude', 0) 106 | longitude = ttn_config['antenna_location'].get('longitude', 0) 107 | altitude = ttn_config['antenna_location'].get('altitude', 0) 108 | 109 | fallback_routers = [] 110 | if "fallback_routers" in ttn_config: 111 | for fb_router in ttn_config["fallback_routers"]: 112 | if "mqtt_address" in fb_router: 113 | fallback_routers.append(fb_router["mqtt_address"]) 114 | 115 | 116 | print ("Gateway ID:\t"+os.environ.get("GW_ID")) 117 | print ("Gateway Key:\t"+os.environ.get("GW_KEY")) 118 | print ("Frequency plan:\t\t"+frequency_plan) 119 | print ("Frequency plan url:\t"+frequency_plan_url) 120 | print ("Gateway description:\t"+description) 121 | print ("Gateway placement:\t"+placement) 122 | print ("Router:\t\t\t"+router) 123 | print ("") 124 | print ("Fallback routers:") 125 | for fb_router in fallback_routers: 126 | print ("\t"+fb_router) 127 | # Done fetching config from TTN 128 | else: 129 | print ("TTN gateway connector disabled. Not fetching config from account server.") 130 | 131 | print ("Latitude:\t\t"+str(latitude)) 132 | print ("Longitude:\t\t"+str(longitude)) 133 | print ("Altitude:\t\t"+str(altitude)) 134 | print ("Gateway EUI:\t"+my_eui) 135 | print ("Has hardware GPS:\t"+str(os.getenv('GW_GPS', False))) 136 | print ("Hardware GPS port:\t"+os.getenv('GW_GPS_PORT', "/dev/ttyAMA0")) 137 | 138 | 139 | 140 | # Retrieve global_conf 141 | sx1301_conf = {} 142 | try: 143 | response = urllib2.urlopen(frequency_plan_url, timeout=30) 144 | global_conf = response.read() 145 | global_conf_object = json.loads(global_conf) 146 | if('SX1301_conf' in global_conf_object): 147 | sx1301_conf = global_conf_object['SX1301_conf'] 148 | except urllib2.URLError as err: 149 | print ("Unable to fetch global conf from Github") 150 | sys.exit(0) 151 | 152 | sx1301_conf['antenna_gain'] = float(os.getenv('GW_ANTENNA_GAIN', 0)) 153 | 154 | 155 | # Build local_conf 156 | gateway_conf = {} 157 | gateway_conf['gateway_ID'] = my_eui 158 | gateway_conf['contact_email'] = os.getenv('GW_CONTACT_EMAIL', "") 159 | gateway_conf['description'] = description 160 | 161 | if(os.getenv('GW_FWD_CRC_ERR', "false")=="true"): 162 | #default is False 163 | gateway_conf['forward_crc_error'] = True 164 | 165 | if(os.getenv('GW_FWD_CRC_VAL', "true")=="false"): 166 | #default is True 167 | gateway_conf['forward_crc_valid'] = False 168 | 169 | # Parse GW_GPS env var. It is a string, we need a boolean. 170 | if(os.getenv('GW_GPS', "false")=="true"): 171 | gw_gps = True 172 | else: 173 | gw_gps = False 174 | 175 | # Use hardware GPS 176 | if(gw_gps): 177 | print ("Using real GPS") 178 | gateway_conf['gps'] = True 179 | gateway_conf['fake_gps'] = False 180 | gateway_conf['gps_tty_path'] = os.getenv('GW_GPS_PORT', "/dev/ttyAMA0") 181 | # Use fake GPS with coordinates from TTN 182 | elif(gw_gps==False and latitude!=0 and longitude!=0): 183 | print ("Using fake GPS") 184 | gateway_conf['gps'] = True 185 | gateway_conf['fake_gps'] = True 186 | gateway_conf['ref_latitude'] = float(latitude) 187 | gateway_conf['ref_longitude'] = float(longitude) 188 | gateway_conf['ref_altitude'] = float(altitude) 189 | # No GPS coordinates 190 | else: 191 | print ("Not sending coordinates") 192 | gateway_conf['gps'] = False 193 | gateway_conf['fake_gps'] = False 194 | 195 | 196 | # Add server configuration 197 | gateway_conf['servers'] = [] 198 | 199 | # Add TTN server 200 | if(os.getenv('SERVER_TTN', "true")=="true"): 201 | server = {} 202 | server['serv_type'] = "ttn" 203 | server['server_address'] = router 204 | server['server_fallbacks'] = fallback_routers 205 | server['serv_gw_id'] = os.environ.get("GW_ID") 206 | server['serv_gw_key'] = os.environ.get("GW_KEY") 207 | server['serv_enabled'] = True 208 | gateway_conf['servers'].append(server) 209 | else: 210 | if(os.getenv('SERVER_0_ENABLED', "false")=="true"): 211 | server = {} 212 | if(os.getenv('SERVER_0_TYPE', "semtech")=="ttn"): 213 | server['serv_type'] = "ttn" 214 | server['serv_gw_id'] = os.environ.get("SERVER_0_GWID") 215 | server['serv_gw_key'] = os.environ.get("SERVER_0_GWKEY") 216 | server['server_address'] = os.environ.get("SERVER_0_ADDRESS") 217 | server['serv_port_up'] = int(os.getenv("SERVER_0_PORTUP", 1700)) 218 | server['serv_port_down'] = int(os.getenv("SERVER_0_PORTDOWN", 1700)) 219 | server['serv_enabled'] = True 220 | if(os.getenv('SERVER_0_DOWNLINK', "false")=="true"): 221 | server['serv_down_enabled'] = True 222 | else: 223 | server['serv_down_enabled'] = False 224 | gateway_conf['servers'].append(server) 225 | 226 | # Add up to 3 additional servers 227 | if(os.getenv('SERVER_1_ENABLED', "false")=="true"): 228 | server = {} 229 | if(os.getenv('SERVER_1_TYPE', "semtech")=="ttn"): 230 | server['serv_type'] = "ttn" 231 | server['serv_gw_id'] = os.environ.get("SERVER_1_GWID") 232 | server['serv_gw_key'] = os.environ.get("SERVER_1_GWKEY") 233 | server['server_address'] = os.environ.get("SERVER_1_ADDRESS") 234 | server['serv_port_up'] = int(os.getenv("SERVER_1_PORTUP", 1700)) 235 | server['serv_port_down'] = int(os.getenv("SERVER_1_PORTDOWN", 1700)) 236 | server['serv_enabled'] = True 237 | if(os.getenv('SERVER_1_DOWNLINK', "false")=="true"): 238 | server['serv_down_enabled'] = True 239 | else: 240 | server['serv_down_enabled'] = False 241 | gateway_conf['servers'].append(server) 242 | 243 | if(os.getenv('SERVER_2_ENABLED', "false")=="true"): 244 | server = {} 245 | if(os.getenv('SERVER_2_TYPE', "semtech")=="ttn"): 246 | server['serv_type'] = "ttn" 247 | server['serv_gw_id'] = os.environ.get("SERVER_2_GWID") 248 | server['serv_gw_key'] = os.environ.get("SERVER_2_GWKEY") 249 | server['server_address'] = os.environ.get("SERVER_2_ADDRESS") 250 | server['serv_port_up'] = int(os.getenv("SERVER_2_PORTUP", 1700)) 251 | server['serv_port_down'] = int(os.getenv("SERVER_2_PORTDOWN", 1700)) 252 | server['serv_enabled'] = True 253 | if(os.getenv('SERVER_2_DOWNLINK', "false")=="true"): 254 | server['serv_down_enabled'] = True 255 | else: 256 | server['serv_down_enabled'] = False 257 | gateway_conf['servers'].append(server) 258 | 259 | if(os.getenv('SERVER_3_ENABLED', "false")=="true"): 260 | server = {} 261 | if(os.getenv('SERVER_3_TYPE', "semtech")=="ttn"): 262 | server['serv_type'] = "ttn" 263 | server['serv_gw_id'] = os.environ.get("SERVER_3_GWID") 264 | server['serv_gw_key'] = os.environ.get("SERVER_3_GWKEY") 265 | server['server_address'] = os.environ.get("SERVER_3_ADDRESS") 266 | server['serv_port_up'] = int(os.getenv("SERVER_3_PORTUP", 1700)) 267 | server['serv_port_down'] = int(os.getenv("SERVER_3_PORTDOWN", 1700)) 268 | server['serv_enabled'] = True 269 | if(os.getenv('SERVER_3_DOWNLINK', "false")=="true"): 270 | server['serv_down_enabled'] = True 271 | else: 272 | server['serv_down_enabled'] = False 273 | gateway_conf['servers'].append(server) 274 | 275 | 276 | # Add GW Traff server 277 | server = {} 278 | server['serv_type'] = "gwtraf" 279 | server['server_address'] = "127.0.0.1" 280 | server['serv_port_up'] = 1688 281 | server['serv_port_down'] = 1689 282 | server['serv_enabled'] = True 283 | gateway_conf['servers'].append(server) 284 | 285 | # Add Local LoRaWAN server 286 | server = {} 287 | server['server_address'] = "127.0.0.1" 288 | server['serv_port_up'] = 1680 289 | server['serv_port_down'] = 1680 290 | server['serv_enabled'] = False 291 | gateway_conf['servers'].append(server) 292 | 293 | 294 | # We merge the json objects from the global_conf and local_conf and save it to the global_conf. 295 | # Therefore there will not be a local_conf.json file. 296 | local_conf = {'SX1301_conf': sx1301_conf, 'gateway_conf': gateway_conf} 297 | with open('global_conf.json', 'w') as the_file: 298 | the_file.write(json.dumps(local_conf, indent=4)) 299 | 300 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Test the connection, wait if needed. 4 | #while [[ $(ping -c1 google.com 2>&1 | grep " 0% packet loss") == "" ]]; do 5 | # echo "[Lora Gateway]: Waiting for internet connection..." 6 | # sleep 2 7 | # done 8 | 9 | # Reset RAK831 PIN 10 | SX1301_RESET_BCM_PIN=25 11 | 12 | WAIT_GPIO() { 13 | sleep 0.1 14 | } 15 | 16 | # cleanup GPIO 17 | if [ -d /sys/class/gpio/gpio$SX1301_RESET_BCM_PIN ] 18 | then 19 | echo "GPIO$SX1301_RESET_BCM_PIN Already available" 20 | else 21 | echo "$SX1301_RESET_BCM_PIN" > /sys/class/gpio/export; WAIT_GPIO 22 | fi 23 | 24 | # setup GPIO 25 | echo "out" > /sys/class/gpio/gpio$SX1301_RESET_BCM_PIN/direction 26 | WAIT_GPIO 27 | echo "1" > /sys/class/gpio/gpio$SX1301_RESET_BCM_PIN/value 28 | WAIT_GPIO 29 | echo "0" > /sys/class/gpio/gpio$SX1301_RESET_BCM_PIN/value 30 | WAIT_GPIO 31 | 32 | # removed, prevent start on IMST Gateway Lite 33 | #echo "in" > /sys/class/gpio/gpio$SX1301_RESET_BCM_PIN/direction 34 | 35 | # Fire up the forwarder. 36 | ./mp_pkt_fwd 37 | -------------------------------------------------------------------------------- /testled.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var ws281x = require('rpi-ws281x-native'); 4 | var NUM_LEDS = 2 5 | var r = 255 6 | var g = 0 7 | var b = 0 8 | pixelData = new Uint32Array(NUM_LEDS); 9 | ws281x.init(NUM_LEDS); 10 | 11 | // trap the SIGINT and reset before exit 12 | process.on('SIGINT', function () { 13 | ws281x.reset(); 14 | process.nextTick(function () { process.exit(0); }); 15 | }); 16 | 17 | 18 | // animation-loop 19 | var offset = 0; 20 | 21 | setInterval(function () { 22 | if (r==255) console.log('Red'); 23 | if (g==255) console.log('Green'); 24 | if (b==255) console.log('Blue'); 25 | 26 | for (var i = 0; i < NUM_LEDS; i++) { 27 | pixelData[i] = rgb2Int(r,g,b); 28 | } 29 | ws281x.render(pixelData); 30 | 31 | if (r==255) { 32 | g=255; r=0; 33 | } else if (g==255){ 34 | b=255; g=0; 35 | } else { 36 | r=255; b=0; 37 | } 38 | }, 2000 ); 39 | 40 | function rgb2Int(r, g, b) { 41 | return ((r & 0xff) << 16) + ((g & 0xff) << 8) + (b & 0xff); 42 | } 43 | 44 | console.log('Press +C to exit.'); 45 | 46 | 47 | -------------------------------------------------------------------------------- /testled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # NeoPixel library strandtest example 4 | # Author: Tony DiCola (tony@tonydicola.com) 5 | # 6 | # Direct port of the Arduino NeoPixel library strandtest example. Showcases 7 | # various animations on a strip of NeoPixels. 8 | import time 9 | 10 | from neopixel import * 11 | 12 | import argparse 13 | import signal 14 | import sys 15 | 16 | def signal_handler(signal, frame): 17 | colorWipe(strip, Color(0,0,0)) 18 | sys.exit(0) 19 | 20 | # LED strip configuration: 21 | LED_COUNT = 2 # Number of LED pixels. 22 | LED_PIN = 18 # GPIO pin connected to the pixels (18 uses PWM!). 23 | #LED_PIN = 10 # GPIO pin connected to the pixels (10 uses SPI /dev/spidev0.0). 24 | LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz) 25 | LED_DMA = 5 # DMA channel to use for generating signal (try 5) 26 | LED_BRIGHTNESS = 255 # Set to 0 for darkest and 255 for brightest 27 | LED_INVERT = False # True to invert the signal (when using NPN transistor level shift) 28 | LED_CHANNEL = 0 # set to '1' for GPIOs 13, 19, 41, 45 or 53 29 | LED_STRIP = ws.WS2811_STRIP_GRB # Strip type and colour ordering 30 | 31 | 32 | 33 | # Define functions which animate LEDs in various ways. 34 | def colorWipe(strip, color, wait_ms=50): 35 | """Wipe color across display a pixel at a time.""" 36 | for i in range(strip.numPixels()): 37 | strip.setPixelColor(i, color) 38 | strip.show() 39 | time.sleep(wait_ms/1000.0) 40 | 41 | # Main program logic follows: 42 | if __name__ == '__main__': 43 | signal.signal(signal.SIGINT, signal_handler) 44 | 45 | # Create NeoPixel object with appropriate configuration. 46 | strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL, LED_STRIP) 47 | 48 | # Intialize the library (must be called once before other functions). 49 | strip.begin() 50 | 51 | print ('Press Ctrl-C to quit.') 52 | while True: 53 | print ('Color Red') 54 | colorWipe(strip, Color(255, 0, 0)) # Red wipe 55 | time.sleep(2) 56 | print ('Color Green') 57 | colorWipe(strip, Color(0, 255, 0)) # Green wipe 58 | time.sleep(2) 59 | print ('Color Blue') 60 | colorWipe(strip, Color(0, 0, 255)) # Blue wipe 61 | time.sleep(2) 62 | -------------------------------------------------------------------------------- /wificfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dnsmasq_cfg": { 3 | "address": "/#/192.168.50.1", 4 | "dhcp_range": "192.168.50.50,192.168.50.100,1h", 5 | "vendor_class": "set:device,IoT" 6 | }, 7 | "host_apd_cfg": { 8 | "ip": "192.168.50.1", 9 | "ssid": "_AP_SSID_", 10 | "wpa_passphrase":"_AP_PASSWORD_", 11 | "channel": "6" 12 | }, 13 | "wpa_supplicant_cfg": { 14 | "cfg_file": "/etc/wpa_supplicant/wpa_supplicant.conf" 15 | } 16 | } 17 | --------------------------------------------------------------------------------