├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── config.json ├── images ├── linger_front_small.jpg └── linger_moos_small.jpg ├── linger.py ├── linger_button.py ├── linger_button.sh ├── linger_counter.py ├── linger_counter.sh ├── linger_rx.py ├── linger_rx.sh ├── linger_tx.py ├── linger_tx.sh └── tm1637.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: javl 2 | buy_me_a_coffee: javl 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | *.sqlite 3 | log 4 | lingerSettings.py* 5 | *.sqlite-journal 6 | *.pyc 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linger # 2 | 3 | The project page can be found [here](https://jaspervanloenen.com/linger/). 4 | 5 | Did you find this script useful? Feel free to support my open source software: 6 | 7 | [![GitHub Sponsor](https://img.shields.io/github/sponsors/javl?label=Sponsor&logo=GitHub)](https://github.com/sponsors/javl) 8 | 9 | [![BMC](https://www.buymeacoffee.com/assets/img/custom_images/white_img.png)](https://www.buymeacoffee.com/javl) 10 | 11 | ![Linger and Moos](images/linger_moos_small.jpg) 12 | ![Linger front](images/linger_front_small.jpg) 13 | 14 | ## Short description ## 15 | 16 | Most mobile devices — such as smartphones — are always searching 17 | for wifi networks they have been connected to in the past. Your 18 | phone is basically yelling every name of every network it has ever 19 | been connected to — at home, at the office or at that hotel with 20 | the dodgy connection — to see if it will get a response from the router. 21 | These messages contain enough unique information\* to be used to 22 | fingerprint and track individuals, something that is already being 23 | done by many different parties, and for various reasons. Shops for 24 | instance, use this data to track how many people walk by, how many 25 | actually come into the store, and how much time you’ve spent in the 26 | candy aisle before making your choice. 27 | 28 | Linger is a small, portable device that allows you to create and 29 | blend into a virtual crowd by storing the specific wifi signals 30 | from everyone that comes near you, and rebroadcasting their signals 31 | infinitely when they leave, making it seem as if they are still there. 32 | As you pass people in the street and their signals are stored in the 33 | database, a small display on the device automatically updates to show 34 | the number of unique individuals in your group. 35 | 36 | Physically they may have left, but their virtual presence will stay with you forever. 37 | 38 | \* This information can be faked (like Linger is doing) and some 39 | software even allows you to turn these signals off completely, but 40 | most devices will send these signals by default (including iPhones 41 | and most Android devices). 42 | 43 | ## Short description (tech version) ## 44 | Linger listens for and stores probe requests coming from WIFI enabled 45 | devices within range into an sqlite database. When these devices 46 | leave the area (determined by the time since their last probe 47 | request) it will start resending the saved probe requests 48 | (with updated sequence numbers), tricking other listeners 49 | into thinking the device is still there. 50 | 51 | ## Long description ## 52 | Most WIFI enabled devices remember the names of all wireless 53 | networks they have been connected to in the past. Whenever 54 | your device is on, but not connected to a network (or sometimes 55 | even when connected), it will broadcast the names of these known 56 | networks (so called probe requests). In theory, when a router 57 | notices a device calling out its name, it will respond by saying 58 | it's there and a connection will be set up. 59 | 60 | In practice, this broadcasting of networks names is not needed. 61 | Routers often broadcast their own presence anyway (you know, that 62 | list of network names you can select from when you turn on your 63 | wifi). So instead of asking for known networks, your device can 64 | also wait and listen to find out if a known router is nearby. 65 | 66 | The problem with actively sending out probe request is that those 67 | messages contain a 'unique' device number and the name of the network 68 | your device wants to connect to. This can act as a fingerprint to 69 | your device, and by using one of many geolocation databases with 70 | network names, it is trivial to find out where a device (and so, 71 | its user) has been before (think of names like "The Hague Airport", 72 | or "some company name") - a tactic used by shops and other 73 | commercial parties to track people. 74 | 75 | Linger listens for, and saves, probe requests coming from all WIFI 76 | enabled devices that are within reach. When these devices leave 77 | the area (determined by the time since their last probe request) 78 | it will start resending the saved probe requests, tricking others 79 | that might be listening into thinking the other device is still there. 80 | 81 | The more devices linger sees, the larger its collection of saved probe 82 | requests will become. This way, a virtual crowd of people will linger 83 | and grow around the device. 84 | 85 | ## Hardware Setup 86 | 87 | The device uses a 7-segment LED display, a Raspberry Pi Zero, two TL-WN722N Wifi dongles and a powered USB hub. The powered hub is needed because the Pi can't provide enough power to run both Wifi dongles. 88 | 89 | A simple explaination of how to connect the 7-segment display and some example code can be found [here](https://raspberrytips.nl/tm1637-4-digit-led-display-raspberry-pi/). 90 | After I checked eveything was working, I stripped the cases off of the Wifi dongles and USB hub, removed the USB sockets from the PCBs and connected all of the different parts by small wires to save space. I've put some cardboard inbetween the different components to prevent any shorts. 91 | 92 | I've also soldered in a single full-size female USB A port; I used this to connect a USB-to-ethernet adapter for easy access to the Pi. A small momentary switch was also added to power off the device. I used the first scematic on [this page](http://www.raspberry-pi-geek.com/Archive/2013/01/Adding-an-On-Off-switch-to-your-Raspberry-Pi). 93 | 94 | ## Software Setup 95 | 96 | There are three parts to this script: 97 | * `linger_rx`: receives probe requests and saves them to `probes.sqlite` by default 98 | * `linger_tx`: transmits probe requests found in the database 99 | * `linger_counter`: gets the amount of unique MAC addresses in the database 100 | and shows this on a 7-segment display. 101 | 102 | Copy the three `.sh` files to `/etc/init.d/`. Make sure they are executable 103 | (`chmod +x linger_*`). Then register them so they are started after booting 104 | by running `sudo update-rc.d defaults` for each of the three files. 105 | 106 | To power down the device I used the circuit and code [found here](http://www.raspberry-pi-geek.com/Archive/2013/01/Adding-an-On-Off-switch-to-your-Raspberry-Pi/(offset)/5)). After pressing my power button the display turns off to show the shutdown script is running, and after about 10 seconds it is safe to remove the power cable. 107 | 108 | ## Links: 109 | * To create the startup scripts I used [a tutorial by Stephen C Phillips](http://blog.scphillips.com/posts/2013/07/getting-a-python-script-to-run-in-the-background-as-a-service-on-boot/). 110 | * The script to control a tm1637 7-segment display from Python is a modified version of a script by [Richard IJzermans](https://raspberrytips.nl/tm1637-4-digit-led-display-raspberry-pi/). 111 | * The schematic and code for the power button come from [Raspberry Pi Geek](http://www.raspberry-pi-geek.com/Archive/2013/01/Adding-an-On-Off-switch-to-your-Raspberry-Pi). 112 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "/home/pi/linger/", 3 | "log_level" : "WARNING", 4 | "run_tx" : true, 5 | "run_rx" : true, 6 | "run_counter" : true 7 | } 8 | -------------------------------------------------------------------------------- /images/linger_front_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javl/linger/cd109166cc1a917f9143b683f7148db1051e547e/images/linger_front_small.jpg -------------------------------------------------------------------------------- /images/linger_moos_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javl/linger/cd109166cc1a917f9143b683f7148db1051e547e/images/linger_moos_small.jpg -------------------------------------------------------------------------------- /linger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse, threading, time, os, sys 4 | from argparse import RawTextHelpFormatter 5 | import sqlite3 as lite 6 | from scapy.all import * 7 | from random import random 8 | 9 | #=========================================================== 10 | # Handle arguments 11 | #=========================================================== 12 | PARSER = argparse.ArgumentParser(prog='linger', description= 13 | '''Linger listens for, and saves, probe requests coming from other 14 | WIFI enabled devices. When these devices leave the area (determined 15 | by the time since their last probe request) it will start resending 16 | the saved probe request, tricking other listeners into thinking the 17 | device is still around. 18 | 19 | For more info on what Linger does see README.md''', 20 | formatter_class=RawTextHelpFormatter) 21 | 22 | PARSER.add_argument('-db', default='probes.db', dest='db_name', metavar='filename',\ 23 | help='Database name. Defaults to probes.', action='store') 24 | 25 | PARSER.add_argument('-f', default='', dest='local_file', metavar='filename',\ 26 | help='Use a .pcap file as input instead of a live capture.', action='store') 27 | 28 | PARSER.add_argument('-d', dest='drop_database', action='store_true',\ 29 | help='Overwrite the database.') 30 | 31 | PARSER.add_argument('-t', default='mon0', dest='iface_transmit', metavar='interface',\ 32 | help='Transmitter interface. Defaults to mon0.', action='store') 33 | 34 | PARSER.add_argument('-r', default='mon1', dest='iface_receive', metavar='interface',\ 35 | help='Receiver interface. Defaults to mon1.', action='store') 36 | 37 | PARSER.add_argument('-mode', default='3', dest='mode', metavar='number', type=int, \ 38 | choices=[1, 2, 3], help='Mode: 1 = scan, 2 = transmit, 3 = both. Defaults to 3.', action='store') 39 | 40 | PARSER.add_argument('-v', dest='verbose', action='count',\ 41 | help='Verbose; can be used up to 3 times to set the verbose level.') 42 | 43 | PARSER.add_argument('--version', action='version', version='%(prog)s version 0.1',\ 44 | help='Show program\'s version number and exit.') 45 | 46 | 47 | ARGS = PARSER.parse_args() 48 | if ARGS.db_name[-3:] != ".db": ARGS.db_name += ".db" 49 | 50 | # Stop script if not running as root. Doing this after the argparse so you can still 51 | # read the help info without sudo (using -h / --help flag) 52 | if not os.geteuid() == 0: 53 | sys.exit('Script must be run as root') 54 | 55 | MAX_SN = 4096 # Max value for the 802.11 sequence number field 56 | MAX_FGNUM = 16 # Max value for the 802.11 fragment number field 57 | 58 | #======================================================= 59 | # Get the sequence number 60 | def extractSN(sc): 61 | hexSC = '0' * (4 - len(hex(sc)[2:])) + hex(sc)[2:] # "normalize" to four digit hexadecimal number 62 | sn = int(hexSC[:-1], 16) 63 | return sn 64 | 65 | #======================================================= 66 | # Generate a sequence number 67 | def calculateSC(sn, fgnum=0): 68 | if (sn > MAX_SN): sn = sn - MAX_SN 69 | if fgnum > MAX_FGNUM: fgnum = 0 70 | hexSN = hex(sn)[2:] + hex(fgnum)[2:] 71 | sc = int(hexSN, 16) 72 | if ARGS.verbose > 2: print "use sn/sc: %i/%i" % (sn, sc) 73 | return sc 74 | 75 | def randomSN(): 76 | return int(random()*MAX_SN) 77 | 78 | #=========================================================== 79 | # The packet transmitter class 80 | #=========================================================== 81 | class PacketTransmitter(threading.Thread): 82 | """ Class to transmit probe requests """ 83 | 84 | def __init__(self): 85 | super(PacketTransmitter, self).__init__() 86 | self.stoprequest = threading.Event() 87 | #self.local_list = [] 88 | 89 | """ 90 | threading.Thread.__init__(self) 91 | self.event = threading.Event() 92 | self.daemon = True 93 | self.stoprequest = threading.Event() 94 | self.con = None 95 | """ 96 | 97 | #======================================================= 98 | # Get a user 99 | def send_existing_packets(self): 100 | con = lite.connect('%s' % ARGS.db_name) 101 | with con: 102 | cur = con.cursor() 103 | cur.execute("SELECT id, command FROM entries \ 104 | WHERE mac = (SELECT mac \ 105 | FROM entries \ 106 | WHERE strftime('%s', last_used) - strftime('%s','now') < -10 \ 107 | ORDER BY last_used ASC LIMIT 1)") 108 | 109 | rows = cur.fetchall() 110 | if len(rows) != 0: 111 | SN = randomSN() 112 | packets = [] 113 | for row in rows: 114 | id = int(row[0]) 115 | command = row[1] 116 | p = eval(command) 117 | p.SC = calculateSC(SN) 118 | packets.append(p) 119 | SN+=1 120 | cur.execute("UPDATE entries SET last_used=CURRENT_TIMESTAMP WHERE id = ?", (id,)) 121 | con.commit() 122 | 123 | sendp(packets, iface=ARGS.iface_transmit, verbose=ARGS.verbose>2) 124 | 125 | #========================================================= 126 | # Keep checking the packages for probe requests 127 | def run(self): 128 | if ARGS.verbose > 0: print "Starting PacketTransmitter" 129 | while not self.stoprequest.isSet(): 130 | self.send_existing_packets() 131 | 132 | #========================================================= 133 | # Kill the thread when requested 134 | def die(self, timeout=None): 135 | self.stoprequest.set() 136 | #super(WorkerThread, self).join(timeout) 137 | 138 | #=========================================================== 139 | # The packet retrieval and analyzer class 140 | #=========================================================== 141 | class PacketAnalyzer(threading.Thread): 142 | """ Class to look for probe requests """ 143 | 144 | def __init__(self): 145 | super(PacketAnalyzer, self).__init__() 146 | self.stoprequest = threading.Event() 147 | #self.local_list = [] 148 | 149 | """ 150 | threading.Thread.__init__(self) 151 | self.event = threading.Event() 152 | self.daemon = True 153 | self.stoprequest = threading.Event() 154 | self.con = None 155 | """ 156 | 157 | #======================================================= 158 | # Handle the incoming packet 159 | def pkt_callback(self, pkt): 160 | if ARGS.verbose > 2: print "Packet coming in" 161 | if not self.stoprequest.isSet(): 162 | mac = pkt.addr2 163 | essid=pkt[Dot11Elt].info.decode('utf-8', 'ignore') 164 | SN = extractSN(pkt.SC) 165 | if essid != '': 166 | con = lite.connect('%s' % ARGS.db_name) 167 | with con: 168 | cur = con.cursor() 169 | 170 | # TODO: combine these two statements into a single one: 171 | # check if combination of mac and essid exists, if so, only update 'last_used' 172 | cur.execute("SELECT id from entries WHERE mac=? and essid=?", (mac, essid)) 173 | if not cur.fetchone(): 174 | if ARGS.verbose > 0: print "New entry -> %s, %s" % (mac, essid) 175 | cur.execute("INSERT INTO entries ('mac', 'essid', 'command', 'sequencenumber', 'added', last_used) \ 176 | VALUES(?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", (mac, essid, pkt.command(), SN)) 177 | con.commit() 178 | else: 179 | if ARGS.verbose > 1: print "Entry already exists -> %s, %s" % (mac, essid) 180 | cur.execute("UPDATE entries SET last_used=CURRENT_TIMESTAMP WHERE mac=? and essid=?", (mac, essid)) 181 | 182 | 183 | #========================================================= 184 | # Keep checking the packages for probe requests 185 | def run(self): 186 | if ARGS.verbose > 0: print "Starting PacketAnalyzer" 187 | if ARGS.local_file != '': 188 | sniff(offline=ARGS.local_file, prn=pkt_callback, store=0, lfilter = lambda x: x.haslayer(Dot11ProbeReq)) 189 | else: 190 | while not self.stoprequest.isSet(): 191 | if ARGS.verbose > 2: print "(re)starting sniff()" 192 | sniff(iface=ARGS.iface_receive, timeout=10, prn=self.pkt_callback, store=0, lfilter = lambda x: x.haslayer(Dot11ProbeReq)) 193 | 194 | #========================================================= 195 | # Kill the thread when requested 196 | def die(self, timeout=None): 197 | self.stoprequest.set() 198 | #super(WorkerThread, self).join(timeout) 199 | 200 | #=========================================================== 201 | # Main program 202 | #=========================================================== 203 | def main(): 204 | """ Main function that starts the packet analyzer and 205 | the transmitter threads """ 206 | 207 | #========================================================= 208 | # Create a database connection 209 | if ARGS.verbose > 1: print "Using database %s" % ARGS.db_name 210 | con = lite.connect('%s' % ARGS.db_name) 211 | with con: 212 | cur = con.cursor() 213 | #======================================================= 214 | # Delete database if requested 215 | if ARGS.drop_database: 216 | if raw_input("Drop the database? (y/n) [n] ") == 'y': 217 | cur.execute('DROP TABLE IF EXISTS entries') 218 | 219 | #======================================================= 220 | # Create the database if it doesn't exist yet 221 | cur.execute('CREATE TABLE IF NOT EXISTS "main"."entries" \ 222 | ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , \ 223 | "mac" TEXT, \ 224 | "essid" TEXT, \ 225 | "command" TEXT, \ 226 | "sequencenumber" INT, \ 227 | "added" DATETIME DEFAULT CURRENT_TIMESTAMP, \ 228 | "last_used" DATETIME DEFAULT CURRENT_TIMESTAMP)') 229 | 230 | #========================================================= 231 | # Start the packet analyzer thread 232 | try: 233 | if ARGS.mode == 1 or ARGS.mode == 3: 234 | packet_analyzer_thread = PacketAnalyzer() 235 | packet_analyzer_thread.start() 236 | 237 | if ARGS.mode == 2 or ARGS.mode == 3: 238 | packet_transmitter_thread = PacketTransmitter() 239 | packet_transmitter_thread.start() 240 | 241 | while True:#not packet_analyzer.event.isSet(): 242 | time.sleep(100) 243 | 244 | #========================================================= 245 | # Kill the threads when exiting with ctrl+c 246 | except (KeyboardInterrupt, SystemExit): 247 | print "" 248 | print "Received keyboard interrupt." 249 | if ARGS.mode == 1 or ARGS.mode == 3: 250 | print "Please wait for the sniffing thread to end." 251 | print "This can take up to 10 seconds..." 252 | packet_analyzer_thread.die() 253 | 254 | if ARGS.mode == 2 or ARGS.mode == 3: packet_transmitter_thread.die() 255 | exit() 256 | 257 | if __name__ == "__main__": 258 | main() -------------------------------------------------------------------------------- /linger_button.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Quit this script if we're not running on the pi zero 4 | # (otherwise sticking the sd card into my regular pi 5 | # without the button will turn it off immediately 6 | import platform 7 | if platform.machine() != "armv6l": 8 | exit(); 9 | 10 | import RPi.GPIO as GPIO 11 | import subprocess 12 | 13 | GPIO.setmode(GPIO.BOARD) 14 | GPIO.setup(7, GPIO.IN) 15 | GPIO.wait_for_edge(7, GPIO.FALLING) 16 | 17 | subprocess.call(['service', 'linger_counter.sh', 'stop'], shell=False) 18 | subprocess.call(['shutdown', '-h', 'now'], shell=False) 19 | -------------------------------------------------------------------------------- /linger_button.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: linger_button 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Display script for Linger 10 | # Description: Gets the current amount of unique MAC addresses from the database and displays them on a 7-segment display 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=/home/pi/linger 15 | DAEMON=$DIR/linger_button.py 16 | DAEMON_NAME=linger_button 17 | 18 | # Add any command line options for your daemon here 19 | DAEMON_OPTS="" 20 | 21 | # This next line determines what user the script runs as. 22 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. 23 | DAEMON_USER=root 24 | 25 | # The process ID of the script when it runs is stored here: 26 | PIDFILE=/var/run/$DAEMON_NAME.pid 27 | 28 | . /lib/lsb/init-functions 29 | 30 | do_start () { 31 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 32 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS 33 | log_end_msg $? 34 | } 35 | do_stop () { 36 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 37 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 38 | log_end_msg $? 39 | } 40 | 41 | case "$1" in 42 | 43 | start|stop) 44 | do_${1} 45 | ;; 46 | 47 | restart|reload|force-reload) 48 | do_stop 49 | do_start 50 | ;; 51 | 52 | status) 53 | status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? 54 | ;; 55 | 56 | *) 57 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 58 | exit 1 59 | ;; 60 | 61 | esac 62 | exit 0 63 | -------------------------------------------------------------------------------- /linger_counter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse, os, platform, sys, time, logging, json, logging.config, signal 3 | from argparse import RawTextHelpFormatter 4 | import sqlite3 as lite 5 | 6 | LOGLEVEL = logging.WARNING 7 | # Load our config file 8 | try: 9 | with open('config.json') as f: 10 | print "open" 11 | config = json.load(f) 12 | print config 13 | if config['run_rx'] == False: 14 | sys.exit(); 15 | # if config['loglevel'] == 'debug': 16 | # LOGLEVEL = logging.DEBUG 17 | # elif config['loglevel'] == 'info': 18 | # LOGLEVEL = logging.INFO 19 | # elif config['loglevel'] == 'warning': 20 | # LOGLEVEL = logging.WARNING 21 | # elif config['loglevel'] == 'error': 22 | # LOGLEVEL = logging.ERROR 23 | # elif config['loglevel'] == 'critical': 24 | # LOGLEVEL = logging.CRITICAL 25 | print "loaded" 26 | try: 27 | print "use level: ", config['log_level'] 28 | logging_config = { 29 | 'filename': '/var/log/linger_counter.log', 30 | 'format': '%(asctime)s [%(levelname)s] %(message)s', 31 | 'level': config['log_level'] 32 | } 33 | except Exception, e: 34 | print "exc: ", e 35 | logging.config.dictConfig(**logging_config) 36 | # logging.basicConfig(**logging_config) 37 | print "do log" 38 | logging.debug("debugggg") 39 | logging.error("errrrrror") 40 | logging.warning("warningg") 41 | logging.critical("crit") 42 | print "end log" 43 | 44 | except Exception, e: 45 | print "non" 46 | print e 47 | pass 48 | 49 | #============================================================================== 50 | logging_config = { 51 | 'filename': '/var/log/linger_counter.log', 52 | 'format': '%(asctime)s [%(levelname)s] %(message)s', 53 | 'level': LOGLEVEL 54 | } 55 | logging.basicConfig(**logging_config) 56 | logging.debug("debugggg") 57 | logging.error("errrrrror") 58 | logging.warning("warningg") 59 | 60 | #============================================================================== 61 | try: 62 | from lingerSettings import * 63 | except: 64 | # if no alternative path has been set, use the RPi path 65 | lingerPath = "/home/pi/linger/" 66 | 67 | 68 | #============================================================================== 69 | # Handle arguments 70 | #============================================================================== 71 | PARSER = argparse.ArgumentParser(prog='linger', description= 72 | '''This script checks the amount of devives saved in the 73 | database and displays the number on a 7 segment display. 74 | For more info on what Linger 75 | does see README.md''', 76 | formatter_class=RawTextHelpFormatter) 77 | 78 | PARSER.add_argument('-db', default='probes', dest='db_name', metavar='filename',\ 79 | help='Database name. Defaults to probes.', action='store') 80 | 81 | PARSER.add_argument('-v', dest='verbose', action='count',\ 82 | help='Verbose; can be used up to 3 times to set the verbose level.') 83 | 84 | PARSER.add_argument('--version', action='version', version='%(prog)s version 0.1.0',\ 85 | help='Show program\'s version number and exit.') 86 | 87 | ARGS = PARSER.parse_args() 88 | 89 | #============================================================================== 90 | # To be able to run this script for testing on a non-RPi device, 91 | # we check if we're on RPi hardware and skip some code if not 92 | onPi = True 93 | if platform.machine() != "armv7l" and platform.machine() != "armv6l": 94 | onPi = False 95 | if ARGS.verbose > 2: print "onPi: False" 96 | logging.info('Not a RPi, so running in limited mode') 97 | else: 98 | if ARGS.verbose > 2: print "onPi: True" 99 | import tm1637 100 | 101 | #============================================================================== 102 | # Stop script if not running as root. Doing this after the argparse so you can still 103 | # read the help info without sudo (using -h / --help flag) 104 | if onPi and not os.geteuid() == 0: 105 | sys.exit('Script must be run as root because of GPIO access') 106 | 107 | 108 | #============================================================================== 109 | # Add .sqlite to our database name if needed 110 | if ARGS.db_name[-7:] != ".sqlite": ARGS.db_name += ".sqlite" 111 | db_path = ''.join([lingerPath, ARGS.db_name]) 112 | print "dbpath: ", db_path 113 | 114 | if onPi: 115 | # Load our display so we can use it globally 116 | Display = tm1637.TM1637(23,24,tm1637.BRIGHT_TYPICAL) 117 | 118 | # Functions used to catch a kill signal so we can cleanly 119 | # exit (like storing a database) 120 | def set_exit_handler(func): 121 | signal.signal(signal.SIGTERM, func) 122 | 123 | def on_exit(sig, func=None): 124 | if ARGS.verbose > 0: print "Received kill signal. Stop" 125 | Display.Clear() 126 | Display.SetBrightness(0) 127 | sys.exit(1) 128 | 129 | #======================================================= 130 | # Get a user 131 | def get_device_amount(con): 132 | with con: 133 | cur = con.cursor() 134 | try: 135 | cur.execute("SELECT COUNT(DISTINCT mac) AS amount FROM entries") 136 | return cur.fetchone()[0] 137 | except Exception, e: # simply return 0 if there was a problem 138 | if ARGS.verbose > 0: print "Encountered a problem getting the device amount" 139 | print e 140 | logging.warning('Problem getting amount of devices from database') 141 | return 0 142 | 143 | #=========================================================== 144 | # Main program 145 | #=========================================================== 146 | def main(): 147 | set_exit_handler(on_exit) 148 | 149 | global Display 150 | if onPi: 151 | if ARGS.verbose > 2: print "On Pi: initiate display" 152 | #Display = tm1637.TM1637(23,24,tm1637.BRIGHT_TYPICAL) 153 | Display.Clear() 154 | Display.SetBrightness(3) 155 | Display.ShowInt(0) 156 | 157 | #========================================================= 158 | if ARGS.verbose > 1: print "Using database {}".format(db_path) 159 | logging.info('Using database: {}'.format(db_path)) 160 | # Check if the database file exists, if not, retry every 10 161 | # seconds, as the other scripts might need some time to 162 | # create the file 163 | firstError = True 164 | while not os.path.isfile(db_path): 165 | if firstError: 166 | firstError = False; 167 | logging.warning('Database file does not exist: {}'.format(db_path)) 168 | if ARGS.verbose > 0: print "Database {} does not exist".format(db_path) 169 | logging.info('Retry in 10 seconds...') 170 | if ARGS.verbose > 0: print "Retry in 10 seconds...".format(db_path) 171 | time.sleep(10) 172 | 173 | # Create a database connection, catch any trouble while connecting 174 | try: 175 | con = lite.connect("{}{}".format(lingerPath, ARGS.db_name)) 176 | cur = con.cursor() 177 | except: 178 | logging.error('Error connecting to database') 179 | if ARGS.verbose > 0: print "Error connecting to database..." 180 | 181 | while True: 182 | amount = get_device_amount(con) 183 | if amount > 9999: 184 | amount = 9999 185 | if onPi: 186 | Display.ShowInt(amount) 187 | time.sleep(5) 188 | 189 | if __name__ == "__main__": 190 | main() 191 | -------------------------------------------------------------------------------- /linger_counter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: linger_counter 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Display script for Linger 10 | # Description: Gets the current amount of unique MAC addresses from the database and displays them on a 7-segment display 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=/home/pi/linger 15 | DAEMON=$DIR/linger_counter.py 16 | DAEMON_NAME=linger_counter 17 | 18 | # Add any command line options for your daemon here 19 | DAEMON_OPTS="" 20 | 21 | # This next line determines what user the script runs as. 22 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. 23 | DAEMON_USER=root 24 | 25 | # The process ID of the script when it runs is stored here: 26 | PIDFILE=/var/run/$DAEMON_NAME.pid 27 | 28 | . /lib/lsb/init-functions 29 | 30 | do_start () { 31 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 32 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS 33 | log_end_msg $? 34 | } 35 | do_stop () { 36 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 37 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 38 | log_end_msg $? 39 | } 40 | 41 | case "$1" in 42 | 43 | start|stop) 44 | do_${1} 45 | ;; 46 | 47 | restart|reload|force-reload) 48 | do_stop 49 | do_start 50 | ;; 51 | 52 | status) 53 | status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? 54 | ;; 55 | 56 | *) 57 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 58 | exit 1 59 | ;; 60 | 61 | esac 62 | exit 0 63 | -------------------------------------------------------------------------------- /linger_rx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Stop script if not running as root. Doing this after the argparse so you can still 4 | # read the help info without sudo (using -h / --help flag) 5 | import os, sys 6 | if not os.geteuid() == 0: 7 | sys.exit('Script must be run as root') 8 | 9 | try: 10 | from lingerSettings import * 11 | except: 12 | lingerPath = "/home/pi/linger/" 13 | 14 | import signal, argparse, subprocess, re, json 15 | from argparse import RawTextHelpFormatter 16 | import sqlite3 as lite 17 | from scapy.all import * 18 | from random import random 19 | 20 | LOGLEVEL = logging.WARNING 21 | # Load our config file 22 | try: 23 | with open('config.json'.format(lingerPath)) as f: 24 | config = json.load(f) 25 | if config['run_rx'] == False: 26 | sys.exit(); 27 | if config['loglevel'] == 'debug': 28 | LOGLEVEL = logging.DEBUG 29 | elif config['loglevel'] == 'info': 30 | LOGLEVEL = logging.INFO 31 | elif config['loglevel'] == 'warning': 32 | LOGLEVEL = logging.WARNING 33 | elif config['loglevel'] == 'error': 34 | LOGLEVEL = logging.ERROR 35 | elif config['loglevel'] == 'critical': 36 | LOGLEVEL = logging.CRITICAL 37 | except Exception, e: 38 | pass 39 | 40 | #============================================================================== 41 | import logging 42 | logging_config = { 43 | 'filename': '/var/log/linger_rx.log', 44 | 'format': '%(asctime)s [%(levelname)s] %(message)s', 45 | 'level': LOGLEVEL 46 | } 47 | logging.basicConfig(**logging_config) 48 | #path = "/home/pi/linger" 49 | #path = "/home/javl/Projects/linger" 50 | #=========================================================== 51 | # Handle arguments 52 | #=========================================================== 53 | PARSER = argparse.ArgumentParser(prog='linger', description= 54 | '''This is the receiver part of Linger, which listens for, 55 | and saves, probe requests coming from other WIFI enabled devices, 56 | and will replay them after the original device has left the area. 57 | For more info see README.md''', 58 | formatter_class=RawTextHelpFormatter) 59 | 60 | PARSER.add_argument('-db', default='probes.sqlite', dest='db_name', metavar='filename',\ 61 | help='Name of database to use. Defaults to "probes".', action='store') 62 | 63 | PARSER.add_argument('-d', dest='drop_database', action='store_true',\ 64 | help='Drop the database before starting. Will ask for confirmation.') 65 | 66 | PARSER.add_argument('-i', default='wlan1', dest='iface_receive', metavar='interface',\ 67 | help='Interface to use for receiving packets. Defaults to wlan1.', action='store') 68 | 69 | PARSER.add_argument('-v', dest='verbose', action='count',\ 70 | help='Verbose; can be used up to 3 times to set the verbose level.') 71 | 72 | PARSER.add_argument('--version', action='version', version='%(prog)s version 0.1.0',\ 73 | help='Show program\'s version number and exit.') 74 | 75 | ARGS = PARSER.parse_args() 76 | # Stop script if not running as root. Doing this after the argparse so you can still 77 | # read the help info without sudo (using -h / --help flag) 78 | if not os.geteuid() == 0: 79 | sys.exit('Script must be run as root') 80 | 81 | # Add .sqlite to our database name if needed 82 | if ARGS.db_name[-7:] != ".sqlite": ARGS.db_name += ".sqlite" 83 | 84 | # Creating this here so we can use it globally later 85 | monitorIface = None 86 | 87 | # Functions used to catch a kill signal so we can cleanly 88 | # exit (like storing the database) 89 | def set_exit_handler(func): 90 | signal.signal(signal.SIGTERM, func) 91 | 92 | def on_exit(sig, func=None): 93 | global monitorIface 94 | if ARGS.verbose > 0: print "Received kill signal. Stop monitor mode and exit" 95 | result = subprocess.check_output("sudo airmon-ng stop {}".format(monitorIface), shell=True) 96 | sys.exit(1) 97 | 98 | #======================================================= 99 | # Extract a sequence number 100 | #======================================================= 101 | def extractSN(sc): 102 | hexSC = '0' * (4 - len(hex(sc)[2:])) + hex(sc)[2:] # "normalize" to four digit hexadecimal number 103 | sn = int(hexSC[:-1], 16) 104 | return sn 105 | 106 | #======================================================= 107 | # Handle incoming packets 108 | #======================================================= 109 | def pkt_callback(pkt): 110 | if ARGS.verbose > 2: print "Packet coming in" 111 | logging.debug('Packet coming in'); 112 | mac = pkt.addr2 113 | essid = pkt[Dot11Elt].info.decode('utf-8', 'ignore') 114 | SN = extractSN(pkt.SC) 115 | if essid != '': 116 | con = lite.connect('{}{}'.format(lingerPath, ARGS.db_name)) 117 | with con: 118 | cur = con.cursor() 119 | 120 | # TODO: combine these two statements into a single one: 121 | # check if combination of mac and essid exists, if so, only update 'last_used' 122 | cur.execute("SELECT id from entries WHERE mac=? and essid=?", (mac, essid)) 123 | if not cur.fetchone(): 124 | if ARGS.verbose > 0: print "New entry -> {}, {}".format(mac, essid) 125 | logging.info("New entry -> {}, {}".format(mac, essid)) 126 | cur.execute("INSERT INTO entries ('mac', 'essid', 'command', 'sequencenumber', 'added', last_used)\ 127 | VALUES(?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", (mac, essid, pkt.command(), SN)) 128 | con.commit() 129 | else: 130 | if ARGS.verbose > 1: print "Entry already exists -> {}, {}".format(mac, essid) 131 | logging.info("Entry already exists -> {}, {}".format(mac, essid)) 132 | cur.execute("UPDATE entries SET last_used=CURRENT_TIMESTAMP WHERE mac=? and essid=?", (mac, essid)) 133 | 134 | #=========================================================== 135 | # Main loop 136 | #=========================================================== 137 | def main(): 138 | global monitorIface 139 | # Start monitor mode 140 | if ARGS.verbose > 1: print "start monitor mode on: ", ARGS.iface_receive 141 | result = subprocess.check_output("sudo airmon-ng start {}".format(ARGS.iface_receive), shell=True) 142 | if ARGS.verbose > 2: print "Result: ", result 143 | m = re.search("\(monitor mode enabled on (.+?)\)", result) 144 | if m: 145 | monitorIface = m.groups()[0] 146 | else: 147 | logging.critical("Something went wrong enabling monitor mode.") 148 | print "Something went wrong enabling monitor mode." 149 | sys.exit(0) 150 | #========================================================= 151 | # Create a database connection 152 | if ARGS.verbose > 1: print "Using database {}".format(ARGS.db_name) 153 | logging.info("Using database {}".format(ARGS.db_name)) 154 | 155 | con = lite.connect('{}{}'.format(lingerPath, ARGS.db_name)) 156 | with con: 157 | cur = con.cursor() 158 | #======================================================= 159 | # Delete database if requested 160 | if ARGS.drop_database: 161 | if raw_input("Drop the database? (y/n) [n] ") == 'y': 162 | cur.execute('DROP TABLE IF EXISTS entries') 163 | 164 | #======================================================= 165 | # Create the database if it doesn't exist yet 166 | cur.execute('CREATE TABLE IF NOT EXISTS "main"."entries" \ 167 | ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , \ 168 | "mac" TEXT, \ 169 | "essid" TEXT, \ 170 | "command" TEXT, \ 171 | "sequencenumber" INT, \ 172 | "added" DATETIME DEFAULT CURRENT_TIMESTAMP, \ 173 | "last_used" DATETIME DEFAULT CURRENT_TIMESTAMP)') 174 | 175 | # Start looking for packets 176 | if ARGS.verbose > 0: print "Starting linger_rx on {} with database {}".format(monitorIface, ARGS.db_name) 177 | logging.info("Starting linger_rx on {} with database {}".format(monitorIface, ARGS.db_name)) 178 | 179 | sniff(iface=monitorIface, prn=pkt_callback, store=0, lfilter = lambda x: x.haslayer(Dot11ProbeReq)) 180 | 181 | 182 | if __name__ == "__main__": 183 | # Set our handler for kill signals 184 | set_exit_handler(on_exit) 185 | main() 186 | -------------------------------------------------------------------------------- /linger_rx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: linger_rx 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Display script for Linger 10 | # Description: Gets the current amount of unique MAC addresses from the database and displays them on a 7-segment display 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=/home/pi/linger 15 | DAEMON=$DIR/linger_rx.py 16 | DAEMON_NAME=linger_rx 17 | 18 | # Add any command line options for your daemon here 19 | DAEMON_OPTS="" 20 | 21 | # This next line determines what user the script runs as. 22 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. 23 | DAEMON_USER=root 24 | 25 | # The process ID of the script when it runs is stored here: 26 | PIDFILE=/var/run/$DAEMON_NAME.pid 27 | 28 | . /lib/lsb/init-functions 29 | 30 | do_start () { 31 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 32 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS 33 | log_end_msg $? 34 | } 35 | do_stop () { 36 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 37 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 38 | log_end_msg $? 39 | } 40 | 41 | case "$1" in 42 | 43 | start|stop) 44 | do_${1} 45 | ;; 46 | 47 | restart|reload|force-reload) 48 | do_stop 49 | do_start 50 | ;; 51 | 52 | status) 53 | status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? 54 | ;; 55 | 56 | *) 57 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 58 | exit 1 59 | ;; 60 | 61 | esac 62 | exit 0 63 | -------------------------------------------------------------------------------- /linger_tx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Stop script if not running as root. Doing this after the argparse so you can still 4 | # read the help info without sudo (using -h / --help flag) 5 | import os, sys 6 | if not os.geteuid() == 0: 7 | sys.exit('Script must be run as root') 8 | 9 | try: 10 | from lingerSettings import * 11 | except: 12 | lingerPath = "/home/pi/linger/" 13 | 14 | import argparse, threading, time, subprocess, re, signal 15 | from argparse import RawTextHelpFormatter 16 | import sqlite3 as lite 17 | from scapy.all import * 18 | from random import random 19 | 20 | LOGLEVEL = logging.WARNING 21 | # Load our config file 22 | try: 23 | with open('config.json'.format(lingerPath)) as f: 24 | config = json.load(f) 25 | if config['run_tx'] == False: 26 | sys.exit(); 27 | if config['loglevel'] == 'debug': 28 | LOGLEVEL = logging.DEBUG 29 | elif config['loglevel'] == 'info': 30 | LOGLEVEL = logging.INFO 31 | elif config['loglevel'] == 'warning': 32 | LOGLEVEL = logging.WARNING 33 | elif config['loglevel'] == 'error': 34 | LOGLEVEL = logging.ERROR 35 | elif config['loglevel'] == 'critical': 36 | LOGLEVEL = logging.CRITICAL 37 | except Exception, e: 38 | pass 39 | 40 | #============================================================================== 41 | import logging 42 | logging_config = { 43 | 'filename': '/var/log/linger_tx.log', 44 | 'format': '%(asctime)s [%(levelname)s] %(message)s', 45 | 'level': config['loglevel'] 46 | } 47 | logging.basicConfig(**logging_config) 48 | #=========================================================== 49 | # Handle arguments 50 | #=========================================================== 51 | PARSER = argparse.ArgumentParser(prog='linger', description= 52 | '''This is the sender part of Linger, which listens for, and saves, 53 | probe requests coming from other WIFI enabled devices, and will 54 | replay them after the original device has left the area. 55 | For more info on what Linger 56 | does see README.md''', 57 | formatter_class=RawTextHelpFormatter) 58 | 59 | PARSER.add_argument('-db', default='probes', dest='db_name', metavar='filename',\ 60 | help='Database name. Defaults to probes.', action='store') 61 | 62 | PARSER.add_argument('-i', default='wlan2', dest='iface_transmit', metavar='interface',\ 63 | help='Transmitter interface. Defaults to wlan2.', action='store') 64 | 65 | PARSER.add_argument('-v', dest='verbose', action='count',\ 66 | help='Verbose; can be used up to 3 times to set the verbose level.') 67 | 68 | PARSER.add_argument('--version', action='version', version='%(prog)s version 0.1.0',\ 69 | help='Show program\'s version number and exit.') 70 | 71 | 72 | ARGS = PARSER.parse_args() 73 | 74 | # Add .sqlite to our database name if needed 75 | if ARGS.db_name[-7:] != ".sqlite": ARGS.db_name += ".sqlite" 76 | 77 | # Creating this here so we can use it globally later 78 | monitorIface = None 79 | 80 | MAX_SN = 4096 # Max value for the 802.11 sequence number field 81 | MAX_FGNUM = 16 # Max value for the 802.11 fragment number field 82 | 83 | # Functions used to catch a kill signal so we can cleanly 84 | # exit (like storing the database) 85 | def set_exit_handler(func): 86 | signal.signal(signal.SIGTERM, func) 87 | 88 | def on_exit(sig, func=None): 89 | global monitorIface 90 | if ARGS.verbose > 0: print "Received kill signal. Stop monitor mode and exit" 91 | result = subprocess.check_output("sudo airmon-ng stop {}".format(monitorIface), shell=True) 92 | sys.exit(1) 93 | 94 | #======================================================= 95 | # Get the sequence number 96 | def extractSN(sc): 97 | hexSC = '0' * (4 - len(hex(sc)[2:])) + hex(sc)[2:] # "normalize" to four digit hexadecimal number 98 | sn = int(hexSC[:-1], 16) 99 | return sn 100 | 101 | #======================================================= 102 | # Generate a sequence number 103 | def calculateSC(sn, fgnum=0): 104 | if (sn > MAX_SN): sn = sn - MAX_SN 105 | if fgnum > MAX_FGNUM: fgnum = 0 106 | hexSN = hex(sn)[2:] + hex(fgnum)[2:] 107 | sc = int(hexSN, 16) 108 | if ARGS.verbose > 2: print "use sn/sc: {}/{}".format(sn, sc) 109 | return sc 110 | 111 | def randomSN(): 112 | return int(random()*MAX_SN) 113 | 114 | #======================================================= 115 | # Get a user 116 | def send_existing_packets(con): 117 | global monitorIface 118 | with con: 119 | cur = con.cursor() 120 | cur.execute("SELECT id, mac, essid, command FROM entries \ 121 | WHERE mac = (SELECT mac \ 122 | FROM entries \ 123 | WHERE strftime('%s', last_used) - strftime('%s','now') < -10 \ 124 | ORDER BY last_used ASC LIMIT 1)") 125 | 126 | rows = cur.fetchall() 127 | if len(rows) != 0: 128 | SN = randomSN() 129 | packets = [] 130 | if ARGS.verbose > 1: print "Mac address: ", rows[0][1]; 131 | logging.debug('Mac address: {}'.format(rows[0][1])) 132 | for row in rows: 133 | if ARGS.verbose > 1: print "--> ", row[2]; 134 | logging.debug('--> {}'.format(row[2])) 135 | id = int(row[0]) 136 | command = row[3] 137 | p = eval(command) 138 | p.SC = calculateSC(SN) 139 | packets.append(p) 140 | SN+=1 141 | cur.execute("UPDATE entries SET last_used=CURRENT_TIMESTAMP WHERE id = ?", (id,)) 142 | con.commit() 143 | sendp(packets, iface=monitorIface, verbose=ARGS.verbose>2) 144 | 145 | #=========================================================== 146 | # Main program 147 | #=========================================================== 148 | def main(): 149 | # Start monitor mode 150 | if ARGS.verbose > 0: print "start monitor mode on: ", ARGS.iface_transmit 151 | result = subprocess.check_output("sudo airmon-ng start {}".format(ARGS.iface_transmit), shell=True) 152 | if ARGS.verbose > 2: print "result: ", result 153 | m = re.search("\(monitor mode enabled on (.+?)\)", result) 154 | if m: 155 | monitorIface = m.groups()[0] 156 | else: 157 | print "Something went wrong enabling monitor mode." 158 | sys.exit(0) 159 | 160 | #========================================================= 161 | # Create a database connection 162 | if ARGS.verbose > 1: print "Using database {}".format(ARGS.db_name) 163 | logging.info("Using database {}".format(ARGS.db_name)); 164 | con = lite.connect("{}{}".format(lingerPath, ARGS.db_name)) 165 | cur = con.cursor() 166 | 167 | while True: 168 | send_existing_packets(con) 169 | 170 | if __name__ == "__main__": 171 | set_exit_handler(on_exit) 172 | main() 173 | -------------------------------------------------------------------------------- /linger_tx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: linger_tx 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Display script for Linger 10 | # Description: Gets the current amount of unique MAC addresses from the database and displays them on a 7-segment display 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=/home/pi/linger 15 | DAEMON=$DIR/linger_tx.py 16 | DAEMON_NAME=linger_tx 17 | 18 | # Add any command line options for your daemon here 19 | DAEMON_OPTS="" 20 | 21 | # This next line determines what user the script runs as. 22 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. 23 | DAEMON_USER=root 24 | 25 | # The process ID of the script when it runs is stored here: 26 | PIDFILE=/var/run/$DAEMON_NAME.pid 27 | 28 | . /lib/lsb/init-functions 29 | 30 | do_start () { 31 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 32 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS 33 | log_end_msg $? 34 | } 35 | do_stop () { 36 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 37 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 38 | log_end_msg $? 39 | } 40 | 41 | case "$1" in 42 | 43 | start|stop) 44 | do_${1} 45 | ;; 46 | 47 | restart|reload|force-reload) 48 | do_stop 49 | do_start 50 | ;; 51 | 52 | status) 53 | status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? 54 | ;; 55 | 56 | *) 57 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 58 | exit 1 59 | ;; 60 | 61 | esac 62 | exit 0 63 | -------------------------------------------------------------------------------- /tm1637.py: -------------------------------------------------------------------------------- 1 | # Original script by Richard IJzermans, with edits by Jasper van Loenen 2 | # https://raspberrytips.nl/tm1637-4-digit-led-display-raspberry-pi/ 3 | 4 | import sys 5 | import os 6 | import time 7 | import RPi.GPIO as IO 8 | 9 | IO.setwarnings(False) 10 | IO.setmode(IO.BCM) 11 | 12 | HexDigits = [0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71] 13 | 14 | ADDR_AUTO = 0x40 15 | ADDR_FIXED = 0x44 16 | STARTADDR = 0xC0 17 | BRIGHT_DARKEST = 0 18 | BRIGHT_TYPICAL = 2 19 | BRIGHT_HIGHEST = 7 20 | OUTPUT = IO.OUT 21 | INPUT = IO.IN 22 | LOW = IO.LOW 23 | HIGH = IO.HIGH 24 | 25 | class TM1637: 26 | __doublePoint = False 27 | __Clkpin = 0 28 | __Datapin = 0 29 | __brightness = BRIGHT_TYPICAL; 30 | __currentData = [0,0,0,0]; 31 | 32 | def __init__( self, pinClock, pinData, brightness ): 33 | self.__Clkpin = pinClock 34 | self.__Datapin = pinData 35 | self.__brightness = brightness; 36 | IO.setup(self.__Clkpin,OUTPUT) 37 | IO.setup(self.__Datapin,OUTPUT) 38 | 39 | def Clear(self): 40 | b = self.__brightness; 41 | point = self.__doublePoint; 42 | self.__brightness = 0; 43 | self.__doublePoint = False; 44 | data = [0x7F,0x7F,0x7F,0x7F]; 45 | self.Show(data); 46 | self.__brightness = b; # restore saved brightness 47 | self.__doublePoint = point; 48 | 49 | def ShowInt(self, i): 50 | d = map(int, ','.join(str(i)).split(',')) 51 | # add padding whitespace when using less than 4 digits 52 | for x in xrange(0, 4-len(d)): 53 | d.insert(0, 0x7F) 54 | self.Clear() 55 | self.Show(d) 56 | 57 | def Show( self, data ): 58 | for i in range(0,4): 59 | self.__currentData[i] = data[i]; 60 | 61 | self.start(); 62 | self.writeByte(ADDR_AUTO); 63 | self.stop(); 64 | self.start(); 65 | self.writeByte(STARTADDR); 66 | for i in range(0,4): 67 | self.writeByte(self.coding(data[i])); 68 | self.stop(); 69 | self.start(); 70 | self.writeByte(0x88 + self.__brightness); 71 | self.stop(); 72 | 73 | def SetBrightness(self, brightness): # brightness 0...7 74 | if( brightness > 7 ): 75 | brightness = 7; 76 | elif( brightness < 0 ): 77 | brightness = 0; 78 | 79 | if( self.__brightness != brightness): 80 | self.__brightness = brightness; 81 | self.Show(self.__currentData); 82 | 83 | def ShowDoublepoint(self, on): # shows or hides the doublepoint 84 | if( self.__doublePoint != on): 85 | self.__doublePoint = on; 86 | self.Show(self.__currentData); 87 | 88 | def writeByte( self, data ): 89 | for i in range(0,8): 90 | IO.output( self.__Clkpin, LOW) 91 | if(data & 0x01): 92 | IO.output( self.__Datapin, HIGH) 93 | else: 94 | IO.output( self.__Datapin, LOW) 95 | data = data >> 1 96 | IO.output( self.__Clkpin, HIGH) 97 | 98 | # wait for ACK 99 | IO.output( self.__Clkpin, LOW) 100 | IO.output( self.__Datapin, HIGH) 101 | IO.output( self.__Clkpin, HIGH) 102 | IO.setup(self.__Datapin, INPUT) 103 | 104 | while(IO.input(self.__Datapin)): 105 | time.sleep(0.001) 106 | if( IO.input(self.__Datapin)): 107 | IO.setup(self.__Datapin, OUTPUT) 108 | IO.output( self.__Datapin, LOW) 109 | IO.setup(self.__Datapin, INPUT) 110 | IO.setup(self.__Datapin, OUTPUT) 111 | 112 | def start(self): 113 | IO.output( self.__Clkpin, HIGH) # send start signal to TM1637 114 | IO.output( self.__Datapin, HIGH) 115 | IO.output( self.__Datapin, LOW) 116 | IO.output( self.__Clkpin, LOW) 117 | 118 | def stop(self): 119 | IO.output( self.__Clkpin, LOW) 120 | IO.output( self.__Datapin, LOW) 121 | IO.output( self.__Clkpin, HIGH) 122 | IO.output( self.__Datapin, HIGH) 123 | 124 | def coding(self, data): 125 | if( self.__doublePoint ): 126 | pointData = 0x80 127 | else: 128 | pointData = 0; 129 | 130 | if(data == 0x7F): 131 | data = 0 132 | else: 133 | data = HexDigits[data] + pointData; 134 | return data 135 | --------------------------------------------------------------------------------