James Yu
Published © GPL3+

Universal Low-Cost Contact Tracing Solution

This is a low-cost location-based method of managing contact tracing in places where other methods may be unusable.

AdvancedFull instructions provided5 hours500
Universal Low-Cost Contact Tracing Solution

Things used in this project

Hardware components

balenaFin
balenaFin
*NOTE: You may also need a USB keyboard and mouse, as well as a suitable monitor screen with HDMI output (plus the relevant HDMI cable) if you do not use SSH. This is the particular board I used to manage the server/central component of the contact tracing system. Alternatives to the balenaFin include any Raspberry Pi or Pi-compatible board that has the ability to simultaneously access WiFi and Bluetooth. Ones on the lower end of the pricing scale include the Pi Zero W.
×1
Nordic Semiconductor nRF52840 DK
This is the board I used for the client/peripheral component. Alternatives include any breakout board with USB programming and Bluetooth advertising capabilities, such as smaller, cheaper nRF52840 breakout boards.
×1
USB-A to Micro-USB Cable
USB-A to Micro-USB Cable
Used for programming of both the nRF and BalenaFin modules.
×1

Software apps and online services

Nordic Semiconductor nRF Connect
Arduino IDE
Arduino IDE
Only used if you are using an alternative peripheral board to the nRF52840-DK with your own broadcasting code.
balenaEtcher
Used if you use a Pi Compute Module based device for the servers.

Story

Read more

Schematics

Setup Diagram

Example setup for the ULCTS. Made with MS Paint.

Code

main.c

C/C++
main.c for nRF peripherals using the nRF SDK. Make sure to place this in an src folder inside the project folder, and make sure the project folder is called Peripheral
/*
 * Code based on Zephyr libraries and an example 
 * that were Copyright (c) 2015-2016 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <bluetooth/bluetooth.h> // bluetooth library

static const struct bt_data ad[] = {
	BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR))
}; // bluetooth data struct containing default data about the board

void main(void)
{
        // turn bluetooth on
	bt_enable(NULL); 
        // advertise the local name and equip the above data struct
	bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
        // do nothing else forever, nothing else needs to be done
	while (1);
}

prj.conf

C/C++
NOTE THIS IS NOT A C file, this is a CONF file, but this option isn't available for the language field. As it is used to configure the C file, I have labeled it so.
Conf file for Peripheral. Place in Peripheral folder.
CONFIG_BT=y
CONFIG_BT_BROADCASTER=y
CONFIG_BT_DEVICE_NAME="detect0"

CMakeLists.txt

C/C++
AGAIN, THIS IS NOT A C FILE.
This is a CMake file for building the Peripheral project. Place it in the Peripheral folder.
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.13.1)
include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE)
project(Peripheral)

target_sources(app PRIVATE
  src/main.c
)

central.py

Python
Server code to be run on terminal. Handles bluetooth analysis. Place it in the root directory of whatever Pi-based device you are using, or any directory if you are using a desktop/laptop.
# libraries to access bluetooth
from bluepy.btle import Scanner, DefaultDelegate
   
# flask server
from flask import Flask, jsonify  

# for defaultdict                 
from collections import defaultdict 
 
# for timestamps              
import time    

# for command-line args                                    
import sys                                         


# create a dict where keys that don't exist are defined as empty lists
entrydict = defaultdict(list)     
  
# set the name of this server to the given name from cmd line               
servername = sys.argv[1]                           

# class for bluetooth scanner object
class ScanDelegate(DefaultDelegate):               
    def __init__(self):
        DefaultDelegate.__init__(self)

    def handleDiscovery(self, dev, isNewDev, isNewData):
        global entrydict
        for entry in entrydict:
            for log in entrydict[entry].copy():
              # if log is 14+ days old (time it takes for symptoms to arise), delete
              if time.time() - log[0] > 1209600:               
                entrydict[entry].remove(log)

        # 9 corresponds to the local name of the device
        name = dev.getValueText(9)            

        # if the name is the type we need...                 
        if name and name.startswith("detect"):       
            # log it with time, device ID and server ID          
            entrydict[name].append([time.time(), servername])    

# create bt scanner
scanner = Scanner().withDelegate(ScanDelegate())   

# create flask app with "main"           
app = Flask(__name__)                                          

# homepage
@app.route('/')                                                
def main():
    # scan for nearby devices
    scanner.scan(1)    
    # send json data containing device logs                                        
    return jsonify(entrydict)                                  

if __name__ == '__main__':
    IP = "0.0.0.0" # change this to the LOCAL IP of your device (the one that starts with 192.168.1. when you call "ip address" in terminal)
    PORT = 80      # change this to the port you forwarded on your router

    #run the server on IP and PORT
    app.run(host=IP, port=PORT) 

prime.py

Python
This is the command line version of the master device program. Used for user-friendly printing of contact traces.
import requests # for looking up websites
import time     # for timestamps
# replace all entries here with URLs or IPs of all your servers (no limit to how many)
urls = ["192.168.1.00", "192.168.1.01", "192.168.1.02", "192.168.1.03"]
utcsub = -7 # your local UTC offset assuming your servers use UTC

while True:
    res = input("Enter a name to lookup\n")
    primedict = {}
    for url in urls: # get all urls and merge content
        content = requests.get(url)
        toappend = content.json()
        primedict.update(toappend)
    primelist = []
    for entry in primedict:
        if entry == res: # filter list by ID to search
            primelist += primedict[entry]
    primelist.sort(key = lambda x: x[0]) # sort by time
    outstring = ""
    for entry in primelist: # format
        outstring+=f"{time.ctime(entry[0]+3600*utcsub)}: {entry[1]}\n"
    print(outstring)

prime.py

Python
This is the Flask server version of the master device program. Used for user-friendly retrieval of contact traces.
from flask import Flask, request # for the server
import requests # for urls
import time     # for timestamps

# replace all entries here with URLs or IPs of all your servers (no limit to how many)
urls = ["192.168.1.00", "192.168.1.01", "192.168.1.02", "192.168.1.03"]
utcsub = -7 # your local UTC offset assuming your servers use UTC

app = Flask(__name__)                                          

@app.route('/')                                                
def main():
    res = request.args.get('tag') # look for the tag querystring
    if not res:
        return "Please use the format your_url/?tag=tag_name"   
    primedict = {}
    for url in urls: # get all urls and merge content
        content = requests.get(url)
        toappend = content.json()
        primedict.update(toappend)
    primelist = []
    for entry in primedict:
        if entry == res: # filter list by ID to search
            primelist += primedict[entry]
    primelist.sort(key = lambda x: x[0]) # sort by time
    outstring = ""
    for entry in primelist: # format
        outstring+=f"{time.ctime(entry[0]+3600*utcsub)}: {entry[1]}<br>"     
    return outstring

if __name__ == '__main__':
    IP = "0.0.0.0" # change this to the LOCAL IP of your device (call "ip address" or check your router homepage)
    PORT = 80      # change this to the port you forwarded if you did so
    app.run(host=IP, port=PORT) 

Credits

James Yu

James Yu

2 projects • 9 followers
Hobbyist fascinated with technology and Arduino, currently studying at UBC.

Comments