An MQTT action-game

MQTT Game with Asyncio
###################################################################################
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                                                                        #
import asyncio                                                                    #
##################################################################################

######## Globals accessed in the subroutines and coroutines ############
players = {}
startt = 0
nplayer = 0
# init, waitEvt, waitResult
state = "init"
initTask = None
playRoundTask = None
showResultsTask = None
measureTask = None
oled = ""
config = ""
mqtt = ""
client_id = ""
touchpin = ""
##################################################################################
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, touchpin, client_id, startt, nplayer, state, initTask, showResultsTask, playRoundTask, measureTask
    print("in callback")

    topic = topic.decode()
    print( "callback got topic %s"%topic)

    if topic == "game/newRound":
        if state == "init":
            initTask.cancel()
            initTask = None
        if showResultsTask:
            showResultsTask.cancel()
            showResultsTask = None
        if measureTask:
            measureTask.cancel()
        state = "waitevt"
        playRoundTask = asyncio.create_task( playRound() )

    elif topic == "game/event":
        if state == "waitevt":
            state = "measure"
            if playRoundTask:
                playRoundTask.cancel()
                playRoundTask = None   
            measureTask = asyncio.create_task( measure() )
        else:
            print("Ignoring game/event")

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

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

####################### Here the main programme starts ###########################

def init():
    global client_id, touchpin, oled,config,mqtt, initTask

    # initialization of components and network

    print("starting init")
    # 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 )

    mqtt.set_callback( sub_cb )
    # first we subscribe:
    mqtt.subscribe( 'game/#' )
    time.sleep(1)

    fb = oled.getFramebuffer()
    fb.fill(0)
    oled.copyFramebuf()

    state = "init"
    initTask = asyncio.create_task( initTimeout() )

    print("init done")

##################### we have set up everything : here now comes the business logic of the game #################
def showResults():
    fb = oled.getFramebuffer()
    fb.fill(0)
    y = 0
    results = []
    for p,t in players.items():
        results.append( (p,t) )
    results.sort(key=lambda entry: entry[1])
    #print (results)
    for i in range(0, min(6,len(results))):        
        dstr = "%4.2f %s" % (results[i][1]/1000.,results[i][0])
        fb.text(dstr,0,y)
        y+=10
    oled.copyFramebuf()


async def initTimeout():
    global mqtt
    print("initTimeout")
    try:
        await asyncio.sleep(30)
        print("fire new round")
        mqtt.publish( "game/newRound", " " )
    finally:
        print("Timeout caught exception and ends")


async def showResultsTimeout():
    print("showResultsTimeout start")
    fb = oled.getFramebuffer()
    try:
        # Show our nice result for 5 seconds if we are not cancelled earlier:
        await asyncio.sleep(10)
        fb.fill(0)
        oled.copyFramebuf()

        # finally start a new Round (if nobody else did
        mqtt.publish( "game/newRound", " ")
    finally:
        print("Show results was cancelled")
        fb.fill(0)
        oled.copyFramebuf()

async def measure():
    global mqtt, oled, nplayer, players, state
    try:
        oled.invert(True)
        startt = time.ticks_ms()
        nplayer = 0
        players = {}
        # wait for the user to react
        try:
            print(touchpin.read())
            while touchpin.read() > 450 :
                await asyncio.sleep_ms(1)
        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
        fb = oled.getFramebuffer()
        rtxt = "%d ms" % dt 
        fb.text(rtxt, 10, 30 )
        oled.copyFramebuf()

        # publish our result on mqtt:
        state = "waitresult"
        res = { 'name' : client_id,
                'time' : dt }
        mqtt.publish( b'game/result', json.dumps( res ) )
        print("published result ", res)
        # now we prepare for waiting on incoming results
        resultShowTask = asyncio.create_task(showResultsTimeout())
    finally:
        print("measure task cancelled")
        oled.invert(False)

async def playRound():
    print("playRound start")
    try:
        fb = oled.getFramebuffer()
        fb.fill(0)
        oled.copyFramebuf()
        wait = 1.0 - random.random()**nplayer
        wait =  int(2000 + 10000 * wait)
        await asyncio.sleep_ms(wait)

        # fire the event
        mqtt.publish( "game/event", " " )
    finally:
        print("playRound was cancelled")


async def main():

    init();
    while True:
        mqtt.check_msg()
        await asyncio.sleep_ms(10)

asyncio.run(main())
print("never come here")