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")