An MQTT action-game

MQTT (Re-)Action Game
###################################################################################
import network                         # mqtt needs network connection            #
import framebuf                        # we also connect a display                #
from umqtt.simple import MQTTClient    # MQTT library                             #
from SH1107_OLED import OLED           # our driver for the OLED display          #
from utils   import Config             # to read a configurattion file            #
from machine import Pin, I2C, TouchPad # API for the I2C interfaces               #
import time                            # standard package for timing              #
import math                            # for normalising the pressure to sea level#
import json                            # we publish our data in json format       #
import random                                                                     #
import sys                                                                        #
###################################################################################

######## Globals accessed in the subroutines ############
players = {}
startt = 0
nplayer = 0
state = "idle"

##################################################################################
def mqtt_connect(client_id, mqtt_server, oled):
##################################################################################

    # Now set up the connection to the MQTT Broker

    fb = oled.getFramebuffer()
    client = MQTTClient(client_id, mqtt_server, keepalive=3600)
    if oled:
        fb.fill(0)
        fb.text( "mqtt connect...", 0,10 )
        oled.copyFramebuf()
    try:
        client.connect()
    except:
        if oled:
            fb.text("...failed...", 0,20 )
            fb.text("check broker!", 0,30 )
            fb.text("cont. wo MQTT!", 0,45 )
            oled.copyFramebuf()
            time.sleep(5)
            fb.fill(0)
            return False
        else:
            return False
    if oled:
        fb.text("mqtt broker :", 0,30 )
        fb.text(mqtt_server, 0,40 )
        oled.copyFramebuf()
        time.sleep( 4 )

    return client
##################################################################################


##################################################################################
def wifi_connect(oled):
##################################################################################
    fb = oled.getFramebuffer()
    fb.fill(0)

    # get a "station interface" (opposed to access point interface) from the
    # netwrok library. This object has the magic methods to connect to the
    # wireless network and then to the LAN on the IP level.
    sta_if = network.WLAN( network.STA_IF )
    # If it is already active de-activate it first so that we always start
    # from the same base state.
    if  sta_if.active():
        sta_if.active(False)

    # Now try to connect to the WIFI

    sta_if.active( True )
    fb.text( "Connecting...", 0, 10 )
    oled.copyFramebuf()

    sta_if.connect( config.get("ssid"), config.get("password") )

    # Poll to know when the connection succeeds

    connected = sta_if.isconnected()

    # create some dotted lines on the display to
    # indicate the process which takes time.

    x = 0
    col = 1
    while not connected:
        fb.pixel( x, 0, col)
        fb.pixel( x, 1, col)
        fb.pixel( x+1, 0, col)
        fb.pixel( x+1, 1, col)
        oled.copyFramebuf()
        x += 4
        if x > 122:
            x=0
            col = (col+1)%2
        connected = sta_if.isconnected()
        time.sleep(0.1) # this is 100ms

    # If we arrive here we should be connected

    fb.text( "Success !", 0, 25 )
    oled.copyFramebuf()
    mac = sta_if.config('mac')
    # Show the IP address we got from the DHCP server

    ifparm = sta_if.ifconfig()
    iptxt = "IP:%s" % ifparm[0]
    fb.text( iptxt, 0,40 )
    oled.copyFramebuf()

    # short break to read the display before we move on

    time.sleep(2)
##################################################################################



# This is the callback method when mqtt messages arrive. Our program can be in 2 different
# states : "waitevt" and "waitresult".
# In waitevt the program waits for an "event" (event we call the moment when we should make
# the screen white and start measuring the reaction time of the player). The event comes in
# via a mqtt message. The first incoming event triggers the start of the measurement of the
# reaction time. We do this measurement here in the callback and also publish the value on
# MQTT. We then change the state to waitresult where we read all incoming results (also our
# own result which we have published comes back to us)
#
# If the callback is executed in the waitresult state we just look for result messages and
# put results in a list for displaying later. We also count the number of incoming results
# to know the number of players. We need this number to calculate a reasonable probability
# distribution for the delay. 
#
def sub_cb( topic, msg ):
    global players, client_id, startt, nplayer, state
    topic = topic.decode()

    if state == "waitevt":
        if topic == "game/event":
            # Now the user has to react and we measure the reaction time
            # change display and initialise variable to measure reaction time
            oled.invert( True )
            startt = time.ticks_ms()        
            nplayer = 0 # this will be counted up by 1 each time a player publishes
            # his result. Like this we know the number of players participating.

            # wait for the user to react
            try:
                while touchpin.read() > 450 :
                    pass
            except Exception as e:
                print(str(e))


            # the user reacted: calculate the measured reaction time
            dt = time.ticks_ms() - startt

            # reset the display
            oled.invert(False)

            # display the result locally
            rtxt = "%d ms" % dt 
            fb.text(rtxt, 10, 30 )
            oled.copyFramebuf()

            # publish our result on mqtt:
            res = { 'name' : client_id,
                    'time' : dt }
            mqtt.publish( b'game/result', json.dumps( res ) )

            # now we prepare for waiting on incoming results
            nplayer = 0
            players = {}
            state = "waitresult"

        else:
            print( "state %s topic %s" % (state, topic) )

    elif state == "waitresult":
        if topic == "game/result":
            nplayer += 1
            result = json.loads( msg.decode() )
            #print(result)
            players[result['name']] = result['time']
            #print(players)
        else:
            print( "state %s topic %s" % (state, topic) )

    else:
        # if we come here there is a bug
        print( "state %s topic %s" % (state, topic) )

    if topic == "game/info":
        print("list of players")
        for p in players.keys():
            print("    %s" % p )

####################### Here the main programme starts ###########################
print("starting")
# read the configuration file
config = Config( "config_game.json" )

touchpin = TouchPad( Pin( 2, mode=Pin.IN ))

# Initialise and configure the first I2C port of the ESP32
# We put both sensors and the display on the same I2C bus.
# The frequency is the I2C default frequency. You can try
# and go higher. At some point things will stop working...
i2c = I2C(0, sda=Pin(33), scl=Pin(32), freq=400000)

# Setup our super I2C OLED display

oled = OLED( i2c, 0x3c )
oled.init()
oled.setLandscape()
fb = oled.getFramebuffer()

# Setup the network connection via the built in WIFI

wifi_connect( oled )

# Connect to MQTT broker
client_id = config.get("client_id")
mqtt = mqtt_connect( client_id, config.get("mqtt_server"), oled )

# We now need to subscribe to the relevant topics on the mqtt network
mqtt.set_callback( sub_cb )

# first we subscribe:
mqtt.subscribe( b'game/#' )


# Now we start out endless loop.

fb.fill(0)
oled.copyFramebuf()
while True:
    # We go in the state "wait event". 
    # We calculate a random delay after which we intend to fire the event for the
    # starting the reaction time measurements of all players. Of course when many
    # players generate a reaction time with a flat probability distribution and we
    # use the first incoming event (i.e. the event with the smallest delay) we
    # always will have very small delays when we have a large number of players.
    # Hence we weight the probability distribution of the delays according to the
    # number of players so that in the end we get a flat probabitly distribution.
    # We generate a delay between 2000 and 5000 (arbitrary unit: they will be used
    # in a loop below)

    state = "waitevt"
    wait = 1.0 - random.random()**nplayer
    wait =  int(2000 + 5000 * wait)
    cnt = wait

    # Now we start waiting with our generated delay. The delay is simply generated
    # by going through the loop below as often as the "delay number" we generated
    # tells us. In the loop we check if a MQTT message arrived, because an earlier
    # event might have been created by another player. In that case the callback
    # will handle the measurement of the reaction time and change our state to
    # "waitresult". If no earlier event occurs we will come out of this loop in the
    # original "waitevt" state.
    while cnt > 0 :
        mqtt.check_msg()
        if state == "waitresult":
            break
        cnt-=1

    # Ff we are still in waitevt at this point it is us who have
    # to fire the event (otherwise we are in waitresult and
    # someone else has fired the event before us):
    if state == "waitevt":
        mqtt.publish( "game/event", " " )
        # we now simply wait until we get to the waitresult state which should
        # be entered after the measurement of the reaction time in the callback
        while state != "waitresult":
            mqtt.check_msg()

    # Now we wait for 5 seconds: We assume that the results
    # of the other players will arrive in this time. (Noboady
    # should have a reaction time larger than 5 seconds...)
    while time.ticks_ms() - startt < 5000:
        mqtt.check_msg()

    # Now all results should have arrived: display the ranking
    result = sorted( players.items(), key=lambda x : x[1] )
    # display the players with the best reaction time (5 fit on
    # our display)
    fb.fill(0)
    y = 0
    print(result)
    for i in range(0, min(6,len(result))):
        dstr = "%4.2f %s" % (result[i][1]/1000.,result[i][0])
        fb.text(dstr,0,y)
        y+=10
    oled.copyFramebuf()

    # Show our nice result for 5 seconds:
    time.sleep(5)
    fb.fill(0)
    oled.copyFramebuf()

    # and we start from the beginning