2019-04-18 06:31:13

by Stotland, Inga

[permalink] [raw]
Subject: [PATCH BlueZ 1/2] test: Drive test-mesh with a string-based menu

Switch to string interactive commands to drive testing
of bluetooth-meshd. Re-work the menu to allow global setting of
destination address and AppKey index for outbound mesh messages.
---
test/test-mesh | 539 +++++++++++++++++++++++++++++--------------------
1 file changed, 317 insertions(+), 222 deletions(-)

diff --git a/test/test-mesh b/test/test-mesh
index fd02207bc..02f52a269 100755
--- a/test/test-mesh
+++ b/test/test-mesh
@@ -18,23 +18,26 @@
#
# The test imitates a device with 2 elements:
# element 0: OnOff Server model
+# Sample Vendor model
# element 1: OnOff Client model
#
# The main menu:
-# 1 - set node ID (token)
-# 2 - join mesh network
-# 3 - attach mesh node
-# 4 - remove node
-# 5 - client menu
-# 6 - exit
+# token
+# join
+# attach
+# remove
+# dest
+# app-index
+# client-menu
+# exit
#
# The main menu options explained:
-# 1 - set token
+# token
# Set the unique node token.
# The token can be set from command line arguments as
# well.
#
-# 2 - join
+# join
# Request provisioning of a device to become a node
# on a mesh network. The test generates device UUID
# which is displayed and will need to be provided to
@@ -49,7 +52,7 @@
# 'token' is returned to the application and is used
# for the runtime of the test.
#
-# 3 - attach
+# attach
# Attach the application to bluetoothd-daemon as a node.
# For the call to be successful, the valid node token must
# be already set, either from command arguments or by
@@ -57,16 +60,24 @@
# successfully executing "join" operation in the same test
# run.
#
-# 4 - remove
+# remove
# Permanently removes any node configuration from daemon
# and persistent storage. After this operation, the node
# is permanently forgotten by the daemon and the associated
# node token is no longer valid.
#
-# 5 - client menu
+# dest
+# Set destination address to send messages: 4 hex digits
+#
+# app-index
+# Set AppKey index to indicate which application key to use
+# to encode outgoing messages: up to 3 hex digits
+#
+# client-menu
# Enter On/Off client submenu.
#
-# 6 - exit
+# quit
+# Exits the test.
#
###################################################################
import sys
@@ -128,16 +139,41 @@ mainloop = None
node = None
mesh_net = None

-menu_level = 0
dst_addr = 0x0000
app_idx = 0

# Node token housekeeping
token = None
have_token = False
+attached = False
+
+# Menu housekeeping
+MAIN_MENU = 0
+ON_OFF_CLIENT_MENU = 1
+
+INPUT_NONE = 0
+INPUT_TOKEN = 1
+INPUT_DEST_ADDRESS = 2
+INPUT_APP_KEY_INDEX = 3
+
+menus = []
+current_menu = None

user_input = 0
+input_error = False

+def raise_error(str_value):
+ global input_error
+
+ input_error = True
+ print(set_error(str_value))
+
+def clear_error():
+ global input_error
+ input_error = False
+
+def is_error():
+ return input_error

def app_exit():
global mainloop
@@ -149,11 +185,28 @@ def app_exit():
model.timer.cancel()
mainloop.quit()

+def set_token(str_value):
+ global token
+ global have_token
+
+ if len(str_value) != 16:
+ raise_error('Expected 16 digits')
+ return
+
+ try:
+ input_number = int(str_value, 16)
+ except ValueError:
+ raise_error('Not a valid hexadecimal number')
+ return
+
+ token = numpy.uint64(input_number)
+ have_token = True
+
def array_to_string(b_array):
- str = ""
+ str_value = ""
for b in b_array:
- str += "%02x" % b
- return str
+ str_value += "%02x" % b
+ return str_value

def generic_error_cb(error):
print(set_error('D-Bus call failed: ') + str(error))
@@ -177,6 +230,14 @@ def join_cb():
def join_error_cb(reason):
print('Join procedure failed: ', reason)

+def remove_node_cb():
+ global attached
+ global have_token
+
+ print(set_yellow('Node removed'))
+ attached = False
+ have_token = False
+
def unwrap(item):
if isinstance(item, dbus.Boolean):
return bool(item)
@@ -197,7 +258,11 @@ def unwrap(item):
return item

def attach_app_cb(node_path, dict_array):
- print('Mesh application registered ', node_path)
+ global attached
+
+ attached = True
+
+ print(set_yellow('Mesh app registered: ') + set_green(node_path))

obj = bus.get_object(MESH_SERVICE_NAME, node_path)

@@ -223,17 +288,6 @@ def interfaces_removed_cb(object_path, interfaces):
print('Service was removed')
app_exit()

-def send_response(path, dest, key, data):
- node.Send(path, dest, key, data, reply_handler=generic_reply_cb,
- error_handler=generic_error_cb)
-
-def send_publication(path, model_id, data):
- print('Send publication ', end='')
- print(data)
- node.Publish(path, model_id, data,
- reply_handler=generic_reply_cb,
- error_handler=generic_error_cb)
-
def print_state(state):
print('State is ', end='')
if state == 0:
@@ -315,13 +369,15 @@ class Application(dbus.service.Object):
def JoinComplete(self, value):
global token
global have_token
+ global attach

- print('JoinComplete with token ' + set_green(hex(value)))
+ print(set_yellow('Joined mesh network with token ') +
+ set_green(format(value, '16x')))

token = value
have_token = True
-
- attach(token)
+ if attached == False:
+ attach(token)

@dbus.service.method(MESH_APPLICATION_IFACE,
in_signature="s", out_signature="")
@@ -348,14 +404,28 @@ class Element(dbus.service.Object):
ids.append(id)
return ids

+ def _get_v_models(self):
+ ids = []
+ for model in self.models:
+ id = model.get_id()
+ v = model.get_vendor()
+ if v != VENDOR_ID_NONE:
+ vendor_id = (v, id)
+ ids.append(vendor_id)
+ return ids
+
def get_properties(self):
- return {
- MESH_ELEMENT_IFACE: {
- 'Index': dbus.Byte(self.index),
- 'Models': dbus.Array(
- self._get_sig_models(), signature='q')
- }
- }
+ vendor_models = self._get_v_models()
+ sig_models = self._get_sig_models()
+
+ props = {'Index' : dbus.Byte(self.index)}
+ if len(sig_models) != 0:
+ props['Models'] = dbus.Array(sig_models, signature='q')
+ if len(vendor_models) != 0:
+ props['VendorModels'] = dbus.Array(vendor_models,
+ signature='(qq)')
+ #print(props)
+ return { MESH_ELEMENT_IFACE: props }

def add_model(self, model):
model.set_path(self.path)
@@ -381,8 +451,8 @@ class Element(dbus.service.Object):
in_signature="qa{sv}", out_signature="")

def UpdateModelConfiguration(self, model_id, config):
- print('UpdateModelConfig ', end='')
- print(hex(model_id))
+ print(('Update Model Config '), end='')
+ print(format(model_id, '04x'))
for model in self.models:
if model_id == model.get_id():
model.set_config(config)
@@ -420,6 +490,18 @@ class Model():
def set_publication(self, period):
self.pub_period = period

+ def send_publication(self, data):
+ print('Send publication ', end='')
+ print(data)
+ node.Publish(self.path, self.model_id, data,
+ reply_handler=generic_reply_cb,
+ error_handler=generic_error_cb)
+
+ def send_message(self, dest, key, data):
+ node.Send(self.path, dest, key, data,
+ reply_handler=generic_reply_cb,
+ error_handler=generic_error_cb)
+
def set_config(self, config):
if 'Bindings' in config:
self.bindings = config.get('Bindings')
@@ -432,13 +514,15 @@ class Model():
print(' ms')

def print_bindings(self):
- print(set_cyan('Model'), set_cyan('%04x' % self.model_id),
- set_cyan('is bound to application key(s): '), end = '')
+ print(set_cyan('Model'), set_cyan('%03x' % self.model_id),
+ set_cyan('is bound to: '))

if len(self.bindings) == 0:
print(set_cyan('** None **'))
+ return
+
for b in self.bindings:
- print(set_cyan('%04x' % b), set_cyan(', '))
+ print(set_green('%03x' % b) + ' ')

########################
# On Off Server Model
@@ -479,7 +563,7 @@ class OnOffServer(Model):
print_state(self.state)

rsp_data = struct.pack('<HB', 0x8204, self.state)
- send_response(self.path, source, key, rsp_data)
+ self.send_message(source, key, rsp_data)

def set_publication(self, period):

@@ -494,11 +578,10 @@ class OnOffServer(Model):

self.timer.start(period/1000, self.publish)

-
def publish(self):
print('Publish')
data = struct.pack('<HB', 0x8204, self.state)
- send_publication(self.path, self.model_id, data)
+ self.send_publication(data)

########################
# On Off Client Model
@@ -512,25 +595,20 @@ class OnOffClient(Model):
0x8204 } # status
print('OnOff Client')

- def _reply_cb(state):
- print('State ', end='');
- print(state)
-
- def _send_message(self, dest, key, data, reply_cb):
- print('OnOffClient send data')
- node.Send(self.path, dest, key, data, reply_handler=reply_cb,
- error_handler=generic_error_cb)
+ def _send_message(self, dest, key, data):
+ print('OnOffClient send command')
+ self.send_message(dest, key, data)

def get_state(self, dest, key):
opcode = 0x8201
data = struct.pack('<H', opcode)
- self._send_message(dest, key, data, self._reply_cb)
+ self._send_message(dest, key, data)

def set_state(self, dest, key, state):
opcode = 0x8202
- print('State:', state)
+ print('Set state:', state)
data = struct.pack('<HB', opcode, state)
- self._send_message(dest, key, data, self._reply_cb)
+ self._send_message(dest, key, data)

def process_message(self, source, key, data):
print('OnOffClient process message len = ', end = '')
@@ -541,7 +619,7 @@ class OnOffClient(Model):
# The opcode is not recognized by this model
return

- opcode, state=struct.unpack('<HB',bytes(data))
+ opcode, state = struct.unpack('<HB',bytes(data))

if opcode != 0x8204 :
# The opcode is not recognized by this model
@@ -556,9 +634,54 @@ class OnOffClient(Model):
print(set_green(state_str), set_yellow('from'),
set_green('%04x' % source))

+########################
+# Sample Vendor Model
+########################
+class SampleVendor(Model):
+ def __init__(self, model_id):
+ Model.__init__(self, model_id)
+ self.vendor = 0x05F1 # Linux Foundation Company ID
+
########################
# Menu functions
########################
+class MenuItem():
+ def __init__(self, desc, func):
+ self.desc = desc
+ self.func = func
+
+class Menu():
+ def __init__(self, title, menu):
+ self.title = title
+ self.menu = menu
+
+ def show(self):
+ print(set_cyan('*** ' + self.title.upper() + ' ***'))
+ for k, v in self.menu.items():
+ print(set_green(k), set_cyan(v.desc))
+
+ def process_cmd(self, str_value):
+ if is_error():
+ self.show()
+ clear_error()
+ return
+
+ cmds = []
+ for key in self.menu.keys():
+ if key.startswith(str_value):
+ cmds.append(key)
+
+ if len(cmds) == 0:
+ print(set_error('Unknown menu option: '), str_value)
+ self.show()
+ return
+ if len(cmds) > 1:
+ for cmd in cmds:
+ print(set_cyan(cmd + '?'))
+ return
+
+ self.menu.get(cmds[0]).func()
+
class MenuHandler(object):
def __init__(self, callback):
self.cb = callback
@@ -579,221 +702,184 @@ class MenuHandler(object):
return True

def process_input(input_str):
- if menu_level == 0:
- process_main_menu(input_str)
- elif menu_level == 1:
- process_client_menu(input_str)
- else:
- print(set_error('BUG: bad menu level'))
+ str_value = input_str.strip()
+
+ # Allow entering empty lines for better output visibility
+ if len(str_value) == 0:
+ return
+
+ current_menu.process_cmd(str_value)

def switch_menu(level):
- global menu_level
+ global current_menu

- if level > 1:
+ if level >= len(menus):
return

- if level == 0:
- main_menu()
- elif level == 1:
- client_menu()
-
- menu_level = level
+ current_menu = menus[level]
+ current_menu.show()

########################
-# Main menu functions
+# Main menu class
########################
-def process_main_menu(input_str):
- global token
- global user_input
- global have_token
+class MainMenu(Menu):
+ def __init__(self):
+ menu_items = {
+ 'token': MenuItem(' - set node ID (token)',
+ self.__cmd_set_token),
+ 'join': MenuItem(' - join mesh network',
+ self.__cmd_join),
+ 'attach': MenuItem(' - attach mesh node',
+ self.__cmd_attach),
+ 'remove': MenuItem(' - delete node',
+ self.__cmd_remove),
+ 'dest': MenuItem(' - set destination address',
+ self.__cmd_set_dest),
+ 'app-index': MenuItem(' - set AppKey index',
+ self.__cmd_set_app_idx),
+ 'client-menu': MenuItem(' - On/Off client menu',
+ self.__cmd_client_menu),
+ 'quit': MenuItem(' - exit the test', app_exit)
+ }

- str = input_str.strip()
+ Menu.__init__(self, 'Main Menu', menu_items)

- if user_input == 1:
- res = set_token(str)
- user_input = 0
+ def __cmd_client_menu(self):
+ if attached != True:
+ print(set_error('Disallowed: node is not attached'))
+ return
+ switch_menu(ON_OFF_CLIENT_MENU)

- if res == False:
- main_menu()
+ def __cmd_set_token(self):
+ global user_input

- return
+ if have_token == True:
+ print('Token already set')
+ return

- # Allow entering empty lines for better output visibility
- if len(str) == 0:
- return
+ user_input = INPUT_TOKEN
+ print(set_cyan('Enter 16-digit hex node ID:'))

- if str.isdigit() == False:
- main_menu()
- return
+ def __cmd_set_dest(self):
+ global user_input

- opt = int(str)
+ user_input = INPUT_DEST_ADDRESS
+ print(set_cyan('Enter 4-digit hex destination address:'))

- if opt > 6:
- print(set_error('Unknown menu option: '), opt)
- main_menu()
- elif opt == 1:
- if have_token:
- print('Token already set')
- return
+ def __cmd_set_app_idx(self):
+ global user_input

- user_input = 1;
- print(set_cyan('Enter 16-digit hex node ID:'))
- elif opt == 2:
+ user_input = INPUT_APP_KEY_INDEX;
+ print(set_cyan('Enter app key index (up to 3 digit hex):'))
+
+ def __cmd_join(self):
if agent == None:
print(set_error('Provisioning agent not found'))
return

- join_mesh()
- elif opt == 3:
+ uuid = bytearray.fromhex("0a0102030405060708090A0B0C0D0E0F")
+ random.shuffle(uuid)
+ uuid_str = array_to_string(uuid)
+ caps = ["out-numeric"]
+ oob = ["other"]
+
+ print(set_yellow('Joining with UUID ') + set_green(uuid_str))
+ mesh_net.Join(app.get_path(), uuid,
+ reply_handler=join_cb,
+ error_handler=join_error_cb)
+
+ def __cmd_attach(self):
if have_token == False:
print(set_error('Token is not set'))
- main_menu()
+ self.show()
return

attach(token)
- elif opt == 4:
+
+ def __cmd_remove(self):
if have_token == False:
print(set_error('Token is not set'))
- main_menu()
+ self.show()
return

- print('Remove mesh node')
- mesh_net.Leave(token, reply_handler=generic_reply_cb,
+ print('Removing mesh node')
+ mesh_net.Leave(token, reply_handler=remove_node_cb,
error_handler=generic_error_cb)
- have_token = False
- elif opt == 5:
- switch_menu(1)
- elif opt == 6:
- app_exit()
-
-
-def main_menu():
- print(set_cyan('*** MAIN MENU ***'))
- print(set_cyan('1 - set node ID (token)'))
- print(set_cyan('2 - join mesh network'))
- print(set_cyan('3 - attach mesh node'))
- print(set_cyan('4 - remove node'))
- print(set_cyan('5 - client menu'))
- print(set_cyan('6 - exit'))

-def set_token(str):
- global token
- global have_token
-
- if len(str) != 16:
- print(set_error('Expected 16 digits'))
- return False
-
- try:
- input_number = int(str, 16)
- except ValueError:
- print(set_error('Not a valid hexadecimal number'))
- return False
-
- token = numpy.uint64(input_number)
- have_token = True
-
- return True
-
-def join_mesh():
- uuid = bytearray.fromhex("0a0102030405060708090A0B0C0D0E0F")
-
- caps = ["out-numeric"]
- oob = ["other"]
-
- random.shuffle(uuid)
- uuid_str = array_to_string(uuid)
- print('Joining with UUID ' + set_green(uuid_str))
+ def process_cmd(self, str_value):
+ global user_input
+ global dst_addr
+ global app_idx
+
+ if user_input == INPUT_TOKEN:
+ set_token(str_value)
+ elif user_input == INPUT_DEST_ADDRESS:
+ res = set_value(str_value, 4, 4)
+ if is_error() != True:
+ dst_addr = res
+ print(set_yellow("Destination address: ") +
+ set_green(format(dst_addr, '04x')))
+ elif user_input == INPUT_APP_KEY_INDEX:
+ res = set_value(str_value, 1, 3)
+ if is_error() != True:
+ app_idx = res
+ print(set_yellow("Application index: ") +
+ set_green(format(app_idx, '03x')))
+
+ if user_input != INPUT_NONE:
+ user_input = INPUT_NONE
+ if is_error() != True:
+ return

- mesh_net.Join(app.get_path(), uuid,
- reply_handler=join_cb,
- error_handler=join_error_cb)
+ Menu.process_cmd(self, str_value)

##############################
-# On/Off Client menu functions
+# On/Off Client menu class
##############################
-def process_client_menu(input_str):
- global user_input
- global dst_addr
- global app_idx
-
- res = -1
- str = input_str.strip()
-
- if user_input == 1:
- res = set_value(str)
- if res != -1:
- dst_addr = res
- elif user_input == 2:
- res = set_value(str)
- if res != -1:
- app_idx = res
-
- if user_input != 0:
- user_input = 0
- if res == -1:
- client_menu()
- return
-
- # Allow entering empty lines for better output visibility
- if len(str) == 0:
- return
+class ClientMenu(Menu):
+ def __init__(self):
+ menu_items = {
+ 'get-state': MenuItem(' - get server state',
+ self.__cmd_get_state),
+ 'off': MenuItem(' - set state OFF',
+ self.__cmd_set_state_off),
+ 'on': MenuItem(' - set state ON',
+ self.__cmd_set_state_on),
+ 'back': MenuItem(' - back to main menu',
+ self.__cmd_main_menu),
+ 'quit': MenuItem(' - exit the test', app_exit)
+ }

- if str.isdigit() == False:
- client_menu()
- return
+ Menu.__init__(self, 'On/Off Clien Menu', menu_items)

- opt = int(str)
+ def __cmd_main_menu(self):
+ switch_menu(MAIN_MENU)

- if opt > 7:
- print(set_error('Unknown menu option: '), opt)
- client_menu()
- return
+ def __cmd_get_state(self):
+ app.elements[1].models[0].get_state(dst_addr, app_idx)

- if opt >= 3 and opt <= 5 and dst_addr == 0x0000:
- print(set_error('Destination address not set!'))
- return
+ def __cmd_set_state_off(self):
+ app.elements[1].models[0].set_state(dst_addr, app_idx, 0)

- if opt == 1:
- user_input = 1;
- print(set_cyan('Enter 4-digit hex destination address:'))
- elif opt == 2:
- user_input = 2;
- app.elements[1].models[0].print_bindings()
- print(set_cyan('Choose application key index:'))
- elif opt == 3:
- app.elements[1].models[0].get_state(dst_addr, app_idx)
- elif opt == 4 or opt == 5:
- app.elements[1].models[0].set_state(dst_addr, app_idx, opt - 4)
- elif opt == 6:
- switch_menu(0)
- elif opt == 7:
- app_exit()
+ def __cmd_set_state_on(self):
+ app.elements[1].models[0].set_state(dst_addr, app_idx, 1)

-def client_menu():
- print(set_cyan('*** ON/OFF CLIENT MENU ***'))
- print(set_cyan('1 - set destination address'))
- print(set_cyan('2 - set application key index'))
- print(set_cyan('3 - get state'))
- print(set_cyan('4 - set state OFF'))
- print(set_cyan('5 - set state ON'))
- print(set_cyan('6 - back to main menu'))
- print(set_cyan('7 - exit'))

-def set_value(str):
+def set_value(str_value, min, max):

- if len(str) != 4:
- print(set_error('Expected 4 digits'))
+ if len(str_value) > max or len(str_value) < min:
+ raise_error('Bad input length %d' % len(str_value))
return -1

try:
- value = int(str, 16)
+ value = int(str_value, 16)
except ValueError:
- print(set_error('Not a valid hexadecimal number'))
+ raise_error('Not a valid hexadecimal number')
return -1

return value

+
########################
# Main entry
########################
@@ -806,6 +892,8 @@ def main():
global mainloop
global app
global mesh_net
+ global menu
+ global current_menu

if len(sys.argv) > 1 :
set_token(sys.argv[1])
@@ -827,14 +915,21 @@ def main():
print(set_yellow('Register OnOff Server model on element 0'))
first_ele.add_model(OnOffServer(0x1000))

+ print(set_yellow('Register Vendor model on element 0'))
+ first_ele.add_model(SampleVendor(0x0001))
+
print(set_yellow('Register OnOff Client model on element 1'))
second_ele.add_model(OnOffClient(0x1001))
+
app.add_element(first_ele)
app.add_element(second_ele)

mainloop = GLib.MainLoop()

- main_menu()
+ menus.append(MainMenu())
+ menus.append(ClientMenu())
+ switch_menu(MAIN_MENU)
+
event_catcher = MenuHandler(process_input);
mainloop.run()

--
2.17.2



2019-04-18 06:31:14

by Stotland, Inga

[permalink] [raw]
Subject: [PATCH BlueZ 2/2] test: Enable test-mesh to send raw vendor commands

This adds a sample vendor model to the first element of the
mesh node. A new menu entry allows to generate and send a raw
vendor command.
---
test/test-mesh | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/test/test-mesh b/test/test-mesh
index 02f52a269..7201669a8 100755
--- a/test/test-mesh
+++ b/test/test-mesh
@@ -73,6 +73,16 @@
# Set AppKey index to indicate which application key to use
# to encode outgoing messages: up to 3 hex digits
#
+# vendor-send
+# Allows to send an arbitrary endor message.
+# The destination is set based on previously executed "dest"
+# command (if not set, the outbound message will fail).
+# User is prompted to enter hex bytearray payload.
+# The message is originated from the vendor model registered
+# on element 0. For the command to succeed, the AppKey index
+# that is set by executing "app-key" must correspond to the
+# application key to which the Sample Vendor model is bound.
+#
# client-menu
# Enter On/Off client submenu.
#
@@ -155,6 +165,7 @@ INPUT_NONE = 0
INPUT_TOKEN = 1
INPUT_DEST_ADDRESS = 2
INPUT_APP_KEY_INDEX = 3
+INPUT_MESSAGE_PAYLOAD = 4

menus = []
current_menu = None
@@ -542,7 +553,6 @@ class OnOffServer(Model):

def process_message(self, source, key, data):
datalen = len(data)
- print('OnOff Server process message len: ', datalen)

if datalen != 2 and datalen != 3:
# The opcode is not recognized by this model
@@ -737,6 +747,8 @@ class MainMenu(Menu):
self.__cmd_set_dest),
'app-index': MenuItem(' - set AppKey index',
self.__cmd_set_app_idx),
+ 'vendor-send': MenuItem(' - send raw vendor message',
+ self.__cmd_vendor_msg),
'client-menu': MenuItem(' - On/Off client menu',
self.__cmd_client_menu),
'quit': MenuItem(' - exit the test', app_exit)
@@ -772,6 +784,12 @@ class MainMenu(Menu):
user_input = INPUT_APP_KEY_INDEX;
print(set_cyan('Enter app key index (up to 3 digit hex):'))

+ def __cmd_vendor_msg(self):
+ global user_input
+
+ user_input = INPUT_MESSAGE_PAYLOAD;
+ print(set_cyan('Enter message payload (hex):'))
+
def __cmd_join(self):
if agent == None:
print(set_error('Provisioning agent not found'))
@@ -806,6 +824,17 @@ class MainMenu(Menu):
mesh_net.Leave(token, reply_handler=remove_node_cb,
error_handler=generic_error_cb)

+ def __send_vendor_msg(self, str_value):
+ try:
+ msg_data = bytearray.fromhex(str_value)
+ except ValueError:
+ raise_error('Not a valid hexadecimal input')
+ return
+
+ print(set_yellow('Send data: ' + set_green(str_value)))
+ app.elements[0].models[1].send_message(dst_addr, app_idx,
+ msg_data)
+
def process_cmd(self, str_value):
global user_input
global dst_addr
@@ -825,6 +854,8 @@ class MainMenu(Menu):
app_idx = res
print(set_yellow("Application index: ") +
set_green(format(app_idx, '03x')))
+ elif user_input == INPUT_MESSAGE_PAYLOAD:
+ self.__send_vendor_msg(str_value)

if user_input != INPUT_NONE:
user_input = INPUT_NONE
--
2.17.2


2019-04-19 18:32:23

by Gix, Brian

[permalink] [raw]
Subject: Re: [PATCH BlueZ 1/2] test: Drive test-mesh with a string-based menu

Patch set applied

On Wed, 2019-04-17 at 23:31 -0700, Inga Stotland wrote:
> Switch to string interactive commands to drive testing
> of bluetooth-meshd. Re-work the menu to allow global setting of
> destination address and AppKey index for outbound mesh messages.
> ---
> test/test-mesh | 539 +++++++++++++++++++++++++++++--------------------
> 1 file changed, 317 insertions(+), 222 deletions(-)
>
> diff --git a/test/test-mesh b/test/test-mesh
> index fd02207bc..02f52a269 100755
> --- a/test/test-mesh
> +++ b/test/test-mesh
> @@ -18,23 +18,26 @@
> #
> # The test imitates a device with 2 elements:
> # element 0: OnOff Server model
> +# Sample Vendor model
> # element 1: OnOff Client model
> #
> # The main menu:
> -# 1 - set node ID (token)
> -# 2 - join mesh network
> -# 3 - attach mesh node
> -# 4 - remove node
> -# 5 - client menu
> -# 6 - exit
> +# token
> +# join
> +# attach
> +# remove
> +# dest
> +# app-index
> +# client-menu
> +# exit
> #
> # The main menu options explained:
> -# 1 - set token
> +# token
> # Set the unique node token.
> # The token can be set from command line arguments as
> # well.
> #
> -# 2 - join
> +# join
> # Request provisioning of a device to become a node
> # on a mesh network. The test generates device UUID
> # which is displayed and will need to be provided to
> @@ -49,7 +52,7 @@
> # 'token' is returned to the application and is used
> # for the runtime of the test.
> #
> -# 3 - attach
> +# attach
> # Attach the application to bluetoothd-daemon as a node.
> # For the call to be successful, the valid node token must
> # be already set, either from command arguments or by
> @@ -57,16 +60,24 @@
> # successfully executing "join" operation in the same test
> # run.
> #
> -# 4 - remove
> +# remove
> # Permanently removes any node configuration from daemon
> # and persistent storage. After this operation, the node
> # is permanently forgotten by the daemon and the associated
> # node token is no longer valid.
> #
> -# 5 - client menu
> +# dest
> +# Set destination address to send messages: 4 hex digits
> +#
> +# app-index
> +# Set AppKey index to indicate which application key to use
> +# to encode outgoing messages: up to 3 hex digits
> +#
> +# client-menu
> # Enter On/Off client submenu.
> #
> -# 6 - exit
> +# quit
> +# Exits the test.
> #
> ###################################################################
> import sys
> @@ -128,16 +139,41 @@ mainloop = None
> node = None
> mesh_net = None
>
> -menu_level = 0
> dst_addr = 0x0000
> app_idx = 0
>
> # Node token housekeeping
> token = None
> have_token = False
> +attached = False
> +
> +# Menu housekeeping
> +MAIN_MENU = 0
> +ON_OFF_CLIENT_MENU = 1
> +
> +INPUT_NONE = 0
> +INPUT_TOKEN = 1
> +INPUT_DEST_ADDRESS = 2
> +INPUT_APP_KEY_INDEX = 3
> +
> +menus = []
> +current_menu = None
>
> user_input = 0
> +input_error = False
>
> +def raise_error(str_value):
> + global input_error
> +
> + input_error = True
> + print(set_error(str_value))
> +
> +def clear_error():
> + global input_error
> + input_error = False
> +
> +def is_error():
> + return input_error
>
> def app_exit():
> global mainloop
> @@ -149,11 +185,28 @@ def app_exit():
> model.timer.cancel()
> mainloop.quit()
>
> +def set_token(str_value):
> + global token
> + global have_token
> +
> + if len(str_value) != 16:
> + raise_error('Expected 16 digits')
> + return
> +
> + try:
> + input_number = int(str_value, 16)
> + except ValueError:
> + raise_error('Not a valid hexadecimal number')
> + return
> +
> + token = numpy.uint64(input_number)
> + have_token = True
> +
> def array_to_string(b_array):
> - str = ""
> + str_value = ""
> for b in b_array:
> - str += "%02x" % b
> - return str
> + str_value += "%02x" % b
> + return str_value
>
> def generic_error_cb(error):
> print(set_error('D-Bus call failed: ') + str(error))
> @@ -177,6 +230,14 @@ def join_cb():
> def join_error_cb(reason):
> print('Join procedure failed: ', reason)
>
> +def remove_node_cb():
> + global attached
> + global have_token
> +
> + print(set_yellow('Node removed'))
> + attached = False
> + have_token = False
> +
> def unwrap(item):
> if isinstance(item, dbus.Boolean):
> return bool(item)
> @@ -197,7 +258,11 @@ def unwrap(item):
> return item
>
> def attach_app_cb(node_path, dict_array):
> - print('Mesh application registered ', node_path)
> + global attached
> +
> + attached = True
> +
> + print(set_yellow('Mesh app registered: ') + set_green(node_path))
>
> obj = bus.get_object(MESH_SERVICE_NAME, node_path)
>
> @@ -223,17 +288,6 @@ def interfaces_removed_cb(object_path, interfaces):
> print('Service was removed')
> app_exit()
>
> -def send_response(path, dest, key, data):
> - node.Send(path, dest, key, data, reply_handler=generic_reply_cb,
> - error_handler=generic_error_cb)
> -
> -def send_publication(path, model_id, data):
> - print('Send publication ', end='')
> - print(data)
> - node.Publish(path, model_id, data,
> - reply_handler=generic_reply_cb,
> - error_handler=generic_error_cb)
> -
> def print_state(state):
> print('State is ', end='')
> if state == 0:
> @@ -315,13 +369,15 @@ class Application(dbus.service.Object):
> def JoinComplete(self, value):
> global token
> global have_token
> + global attach
>
> - print('JoinComplete with token ' + set_green(hex(value)))
> + print(set_yellow('Joined mesh network with token ') +
> + set_green(format(value, '16x')))
>
> token = value
> have_token = True
> -
> - attach(token)
> + if attached == False:
> + attach(token)
>
> @dbus.service.method(MESH_APPLICATION_IFACE,
> in_signature="s", out_signature="")
> @@ -348,14 +404,28 @@ class Element(dbus.service.Object):
> ids.append(id)
> return ids
>
> + def _get_v_models(self):
> + ids = []
> + for model in self.models:
> + id = model.get_id()
> + v = model.get_vendor()
> + if v != VENDOR_ID_NONE:
> + vendor_id = (v, id)
> + ids.append(vendor_id)
> + return ids
> +
> def get_properties(self):
> - return {
> - MESH_ELEMENT_IFACE: {
> - 'Index': dbus.Byte(self.index),
> - 'Models': dbus.Array(
> - self._get_sig_models(), signature='q')
> - }
> - }
> + vendor_models = self._get_v_models()
> + sig_models = self._get_sig_models()
> +
> + props = {'Index' : dbus.Byte(self.index)}
> + if len(sig_models) != 0:
> + props['Models'] = dbus.Array(sig_models, signature='q')
> + if len(vendor_models) != 0:
> + props['VendorModels'] = dbus.Array(vendor_models,
> + signature='(qq)')
> + #print(props)
> + return { MESH_ELEMENT_IFACE: props }
>
> def add_model(self, model):
> model.set_path(self.path)
> @@ -381,8 +451,8 @@ class Element(dbus.service.Object):
> in_signature="qa{sv}", out_signature="")
>
> def UpdateModelConfiguration(self, model_id, config):
> - print('UpdateModelConfig ', end='')
> - print(hex(model_id))
> + print(('Update Model Config '), end='')
> + print(format(model_id, '04x'))
> for model in self.models:
> if model_id == model.get_id():
> model.set_config(config)
> @@ -420,6 +490,18 @@ class Model():
> def set_publication(self, period):
> self.pub_period = period
>
> + def send_publication(self, data):
> + print('Send publication ', end='')
> + print(data)
> + node.Publish(self.path, self.model_id, data,
> + reply_handler=generic_reply_cb,
> + error_handler=generic_error_cb)
> +
> + def send_message(self, dest, key, data):
> + node.Send(self.path, dest, key, data,
> + reply_handler=generic_reply_cb,
> + error_handler=generic_error_cb)
> +
> def set_config(self, config):
> if 'Bindings' in config:
> self.bindings = config.get('Bindings')
> @@ -432,13 +514,15 @@ class Model():
> print(' ms')
>
> def print_bindings(self):
> - print(set_cyan('Model'), set_cyan('%04x' % self.model_id),
> - set_cyan('is bound to application key(s): '), end = '')
> + print(set_cyan('Model'), set_cyan('%03x' % self.model_id),
> + set_cyan('is bound to: '))
>
> if len(self.bindings) == 0:
> print(set_cyan('** None **'))
> + return
> +
> for b in self.bindings:
> - print(set_cyan('%04x' % b), set_cyan(', '))
> + print(set_green('%03x' % b) + ' ')
>
> ########################
> # On Off Server Model
> @@ -479,7 +563,7 @@ class OnOffServer(Model):
> print_state(self.state)
>
> rsp_data = struct.pack('<HB', 0x8204, self.state)
> - send_response(self.path, source, key, rsp_data)
> + self.send_message(source, key, rsp_data)
>
> def set_publication(self, period):
>
> @@ -494,11 +578,10 @@ class OnOffServer(Model):
>
> self.timer.start(period/1000, self.publish)
>
> -
> def publish(self):
> print('Publish')
> data = struct.pack('<HB', 0x8204, self.state)
> - send_publication(self.path, self.model_id, data)
> + self.send_publication(data)
>
> ########################
> # On Off Client Model
> @@ -512,25 +595,20 @@ class OnOffClient(Model):
> 0x8204 } # status
> print('OnOff Client')
>
> - def _reply_cb(state):
> - print('State ', end='');
> - print(state)
> -
> - def _send_message(self, dest, key, data, reply_cb):
> - print('OnOffClient send data')
> - node.Send(self.path, dest, key, data, reply_handler=reply_cb,
> - error_handler=generic_error_cb)
> + def _send_message(self, dest, key, data):
> + print('OnOffClient send command')
> + self.send_message(dest, key, data)
>
> def get_state(self, dest, key):
> opcode = 0x8201
> data = struct.pack('<H', opcode)
> - self._send_message(dest, key, data, self._reply_cb)
> + self._send_message(dest, key, data)
>
> def set_state(self, dest, key, state):
> opcode = 0x8202
> - print('State:', state)
> + print('Set state:', state)
> data = struct.pack('<HB', opcode, state)
> - self._send_message(dest, key, data, self._reply_cb)
> + self._send_message(dest, key, data)
>
> def process_message(self, source, key, data):
> print('OnOffClient process message len = ', end = '')
> @@ -541,7 +619,7 @@ class OnOffClient(Model):
> # The opcode is not recognized by this model
> return
>
> - opcode, state=struct.unpack('<HB',bytes(data))
> + opcode, state = struct.unpack('<HB',bytes(data))
>
> if opcode != 0x8204 :
> # The opcode is not recognized by this model
> @@ -556,9 +634,54 @@ class OnOffClient(Model):
> print(set_green(state_str), set_yellow('from'),
> set_green('%04x' % source))
>
> +########################
> +# Sample Vendor Model
> +########################
> +class SampleVendor(Model):
> + def __init__(self, model_id):
> + Model.__init__(self, model_id)
> + self.vendor = 0x05F1 # Linux Foundation Company ID
> +
> ########################
> # Menu functions
> ########################
> +class MenuItem():
> + def __init__(self, desc, func):
> + self.desc = desc
> + self.func = func
> +
> +class Menu():
> + def __init__(self, title, menu):
> + self.title = title
> + self.menu = menu
> +
> + def show(self):
> + print(set_cyan('*** ' + self.title.upper() + ' ***'))
> + for k, v in self.menu.items():
> + print(set_green(k), set_cyan(v.desc))
> +
> + def process_cmd(self, str_value):
> + if is_error():
> + self.show()
> + clear_error()
> + return
> +
> + cmds = []
> + for key in self.menu.keys():
> + if key.startswith(str_value):
> + cmds.append(key)
> +
> + if len(cmds) == 0:
> + print(set_error('Unknown menu option: '), str_value)
> + self.show()
> + return
> + if len(cmds) > 1:
> + for cmd in cmds:
> + print(set_cyan(cmd + '?'))
> + return
> +
> + self.menu.get(cmds[0]).func()
> +
> class MenuHandler(object):
> def __init__(self, callback):
> self.cb = callback
> @@ -579,221 +702,184 @@ class MenuHandler(object):
> return True
>
> def process_input(input_str):
> - if menu_level == 0:
> - process_main_menu(input_str)
> - elif menu_level == 1:
> - process_client_menu(input_str)
> - else:
> - print(set_error('BUG: bad menu level'))
> + str_value = input_str.strip()
> +
> + # Allow entering empty lines for better output visibility
> + if len(str_value) == 0:
> + return
> +
> + current_menu.process_cmd(str_value)
>
> def switch_menu(level):
> - global menu_level
> + global current_menu
>
> - if level > 1:
> + if level >= len(menus):
> return
>
> - if level == 0:
> - main_menu()
> - elif level == 1:
> - client_menu()
> -
> - menu_level = level
> + current_menu = menus[level]
> + current_menu.show()
>
> ########################
> -# Main menu functions
> +# Main menu class
> ########################
> -def process_main_menu(input_str):
> - global token
> - global user_input
> - global have_token
> +class MainMenu(Menu):
> + def __init__(self):
> + menu_items = {
> + 'token': MenuItem(' - set node ID (token)',
> + self.__cmd_set_token),
> + 'join': MenuItem(' - join mesh network',
> + self.__cmd_join),
> + 'attach': MenuItem(' - attach mesh node',
> + self.__cmd_attach),
> + 'remove': MenuItem(' - delete node',
> + self.__cmd_remove),
> + 'dest': MenuItem(' - set destination address',
> + self.__cmd_set_dest),
> + 'app-index': MenuItem(' - set AppKey index',
> + self.__cmd_set_app_idx),
> + 'client-menu': MenuItem(' - On/Off client menu',
> + self.__cmd_client_menu),
> + 'quit': MenuItem(' - exit the test', app_exit)
> + }
>
> - str = input_str.strip()
> + Menu.__init__(self, 'Main Menu', menu_items)
>
> - if user_input == 1:
> - res = set_token(str)
> - user_input = 0
> + def __cmd_client_menu(self):
> + if attached != True:
> + print(set_error('Disallowed: node is not attached'))
> + return
> + switch_menu(ON_OFF_CLIENT_MENU)
>
> - if res == False:
> - main_menu()
> + def __cmd_set_token(self):
> + global user_input
>
> - return
> + if have_token == True:
> + print('Token already set')
> + return
>
> - # Allow entering empty lines for better output visibility
> - if len(str) == 0:
> - return
> + user_input = INPUT_TOKEN
> + print(set_cyan('Enter 16-digit hex node ID:'))
>
> - if str.isdigit() == False:
> - main_menu()
> - return
> + def __cmd_set_dest(self):
> + global user_input
>
> - opt = int(str)
> + user_input = INPUT_DEST_ADDRESS
> + print(set_cyan('Enter 4-digit hex destination address:'))
>
> - if opt > 6:
> - print(set_error('Unknown menu option: '), opt)
> - main_menu()
> - elif opt == 1:
> - if have_token:
> - print('Token already set')
> - return
> + def __cmd_set_app_idx(self):
> + global user_input
>
> - user_input = 1;
> - print(set_cyan('Enter 16-digit hex node ID:'))
> - elif opt == 2:
> + user_input = INPUT_APP_KEY_INDEX;
> + print(set_cyan('Enter app key index (up to 3 digit hex):'))
> +
> + def __cmd_join(self):
> if agent == None:
> print(set_error('Provisioning agent not found'))
> return
>
> - join_mesh()
> - elif opt == 3:
> + uuid = bytearray.fromhex("0a0102030405060708090A0B0C0D0E0F")
> + random.shuffle(uuid)
> + uuid_str = array_to_string(uuid)
> + caps = ["out-numeric"]
> + oob = ["other"]
> +
> + print(set_yellow('Joining with UUID ') + set_green(uuid_str))
> + mesh_net.Join(app.get_path(), uuid,
> + reply_handler=join_cb,
> + error_handler=join_error_cb)
> +
> + def __cmd_attach(self):
> if have_token == False:
> print(set_error('Token is not set'))
> - main_menu()
> + self.show()
> return
>
> attach(token)
> - elif opt == 4:
> +
> + def __cmd_remove(self):
> if have_token == False:
> print(set_error('Token is not set'))
> - main_menu()
> + self.show()
> return
>
> - print('Remove mesh node')
> - mesh_net.Leave(token, reply_handler=generic_reply_cb,
> + print('Removing mesh node')
> + mesh_net.Leave(token, reply_handler=remove_node_cb,
> error_handler=generic_error_cb)
> - have_token = False
> - elif opt == 5:
> - switch_menu(1)
> - elif opt == 6:
> - app_exit()
> -
> -
> -def main_menu():
> - print(set_cyan('*** MAIN MENU ***'))
> - print(set_cyan('1 - set node ID (token)'))
> - print(set_cyan('2 - join mesh network'))
> - print(set_cyan('3 - attach mesh node'))
> - print(set_cyan('4 - remove node'))
> - print(set_cyan('5 - client menu'))
> - print(set_cyan('6 - exit'))
>
> -def set_token(str):
> - global token
> - global have_token
> -
> - if len(str) != 16:
> - print(set_error('Expected 16 digits'))
> - return False
> -
> - try:
> - input_number = int(str, 16)
> - except ValueError:
> - print(set_error('Not a valid hexadecimal number'))
> - return False
> -
> - token = numpy.uint64(input_number)
> - have_token = True
> -
> - return True
> -
> -def join_mesh():
> - uuid = bytearray.fromhex("0a0102030405060708090A0B0C0D0E0F")
> -
> - caps = ["out-numeric"]
> - oob = ["other"]
> -
> - random.shuffle(uuid)
> - uuid_str = array_to_string(uuid)
> - print('Joining with UUID ' + set_green(uuid_str))
> + def process_cmd(self, str_value):
> + global user_input
> + global dst_addr
> + global app_idx
> +
> + if user_input == INPUT_TOKEN:
> + set_token(str_value)
> + elif user_input == INPUT_DEST_ADDRESS:
> + res = set_value(str_value, 4, 4)
> + if is_error() != True:
> + dst_addr = res
> + print(set_yellow("Destination address: ") +
> + set_green(format(dst_addr, '04x')))
> + elif user_input == INPUT_APP_KEY_INDEX:
> + res = set_value(str_value, 1, 3)
> + if is_error() != True:
> + app_idx = res
> + print(set_yellow("Application index: ") +
> + set_green(format(app_idx, '03x')))
> +
> + if user_input != INPUT_NONE:
> + user_input = INPUT_NONE
> + if is_error() != True:
> + return
>
> - mesh_net.Join(app.get_path(), uuid,
> - reply_handler=join_cb,
> - error_handler=join_error_cb)
> + Menu.process_cmd(self, str_value)
>
> ##############################
> -# On/Off Client menu functions
> +# On/Off Client menu class
> ##############################
> -def process_client_menu(input_str):
> - global user_input
> - global dst_addr
> - global app_idx
> -
> - res = -1
> - str = input_str.strip()
> -
> - if user_input == 1:
> - res = set_value(str)
> - if res != -1:
> - dst_addr = res
> - elif user_input == 2:
> - res = set_value(str)
> - if res != -1:
> - app_idx = res
> -
> - if user_input != 0:
> - user_input = 0
> - if res == -1:
> - client_menu()
> - return
> -
> - # Allow entering empty lines for better output visibility
> - if len(str) == 0:
> - return
> +class ClientMenu(Menu):
> + def __init__(self):
> + menu_items = {
> + 'get-state': MenuItem(' - get server state',
> + self.__cmd_get_state),
> + 'off': MenuItem(' - set state OFF',
> + self.__cmd_set_state_off),
> + 'on': MenuItem(' - set state ON',
> + self.__cmd_set_state_on),
> + 'back': MenuItem(' - back to main menu',
> + self.__cmd_main_menu),
> + 'quit': MenuItem(' - exit the test', app_exit)
> + }
>
> - if str.isdigit() == False:
> - client_menu()
> - return
> + Menu.__init__(self, 'On/Off Clien Menu', menu_items)
>
> - opt = int(str)
> + def __cmd_main_menu(self):
> + switch_menu(MAIN_MENU)
>
> - if opt > 7:
> - print(set_error('Unknown menu option: '), opt)
> - client_menu()
> - return
> + def __cmd_get_state(self):
> + app.elements[1].models[0].get_state(dst_addr, app_idx)
>
> - if opt >= 3 and opt <= 5 and dst_addr == 0x0000:
> - print(set_error('Destination address not set!'))
> - return
> + def __cmd_set_state_off(self):
> + app.elements[1].models[0].set_state(dst_addr, app_idx, 0)
>
> - if opt == 1:
> - user_input = 1;
> - print(set_cyan('Enter 4-digit hex destination address:'))
> - elif opt == 2:
> - user_input = 2;
> - app.elements[1].models[0].print_bindings()
> - print(set_cyan('Choose application key index:'))
> - elif opt == 3:
> - app.elements[1].models[0].get_state(dst_addr, app_idx)
> - elif opt == 4 or opt == 5:
> - app.elements[1].models[0].set_state(dst_addr, app_idx, opt - 4)
> - elif opt == 6:
> - switch_menu(0)
> - elif opt == 7:
> - app_exit()
> + def __cmd_set_state_on(self):
> + app.elements[1].models[0].set_state(dst_addr, app_idx, 1)
>
> -def client_menu():
> - print(set_cyan('*** ON/OFF CLIENT MENU ***'))
> - print(set_cyan('1 - set destination address'))
> - print(set_cyan('2 - set application key index'))
> - print(set_cyan('3 - get state'))
> - print(set_cyan('4 - set state OFF'))
> - print(set_cyan('5 - set state ON'))
> - print(set_cyan('6 - back to main menu'))
> - print(set_cyan('7 - exit'))
>
> -def set_value(str):
> +def set_value(str_value, min, max):
>
> - if len(str) != 4:
> - print(set_error('Expected 4 digits'))
> + if len(str_value) > max or len(str_value) < min:
> + raise_error('Bad input length %d' % len(str_value))
> return -1
>
> try:
> - value = int(str, 16)
> + value = int(str_value, 16)
> except ValueError:
> - print(set_error('Not a valid hexadecimal number'))
> + raise_error('Not a valid hexadecimal number')
> return -1
>
> return value
>
> +
> ########################
> # Main entry
> ########################
> @@ -806,6 +892,8 @@ def main():
> global mainloop
> global app
> global mesh_net
> + global menu
> + global current_menu
>
> if len(sys.argv) > 1 :
> set_token(sys.argv[1])
> @@ -827,14 +915,21 @@ def main():
> print(set_yellow('Register OnOff Server model on element 0'))
> first_ele.add_model(OnOffServer(0x1000))
>
> + print(set_yellow('Register Vendor model on element 0'))
> + first_ele.add_model(SampleVendor(0x0001))
> +
> print(set_yellow('Register OnOff Client model on element 1'))
> second_ele.add_model(OnOffClient(0x1001))
> +
> app.add_element(first_ele)
> app.add_element(second_ele)
>
> mainloop = GLib.MainLoop()
>
> - main_menu()
> + menus.append(MainMenu())
> + menus.append(ClientMenu())
> + switch_menu(MAIN_MENU)
> +
> event_catcher = MenuHandler(process_input);
> mainloop.run()
>