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