mirror of
https://git.kernel.org/pub/scm/bluetooth/bluez.git
synced 2024-11-16 00:34:39 +08:00
301c717357
This ensures that the value of Device UUID when invoking Join method is compliant with RFC 4122.
1065 lines
26 KiB
Python
Executable File
1065 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
###################################################################
|
|
#
|
|
# This is a unified test sample for BT Mesh
|
|
#
|
|
# To run the test:
|
|
# test-mesh [token]
|
|
#
|
|
# 'token' is an optional argument. It must be a 16-digit
|
|
# hexadecimal number. The token must be associated with
|
|
# an existing node. The token is generated and assigned
|
|
# to a node as a result of successful provisioning (see
|
|
# explanation of "join" option).
|
|
# When the token is set, the menu operations "attach"
|
|
# and "remove" may be performed on a node specified
|
|
# by this token.
|
|
#
|
|
# The test imitates a device with 2 elements:
|
|
# element 0: OnOff Server model
|
|
# Sample Vendor model
|
|
# element 1: OnOff Client model
|
|
#
|
|
# The main menu:
|
|
# token
|
|
# join
|
|
# attach
|
|
# remove
|
|
# dest
|
|
# uuid
|
|
# app-index
|
|
# client-menu
|
|
# exit
|
|
#
|
|
# The main menu options explained:
|
|
# token
|
|
# Set the unique node token.
|
|
# The token can be set from command line arguments as
|
|
# well.
|
|
#
|
|
# 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
|
|
# an outside entity that acts as a Provisioner. Also,
|
|
# during the provisioning process, an agent that is
|
|
# part of the test, will request (or will be requested)
|
|
# to perform a specified operation, e.g., a number will
|
|
# be displayed and this number will need to be entered
|
|
# on the Provisioner's side.
|
|
# In case of successful provisioning, the application
|
|
# automatically attaches as a node to the daemon. A node
|
|
# 'token' is returned to the application and is used
|
|
# for the runtime of the test.
|
|
#
|
|
# 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
|
|
# executing "set token" operation or automatically after
|
|
# successfully executing "join" operation in the same
|
|
# test run.
|
|
#
|
|
# 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.
|
|
#
|
|
# 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
|
|
#
|
|
# 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.
|
|
#
|
|
# quit
|
|
# Exits the test.
|
|
#
|
|
###################################################################
|
|
import sys
|
|
import struct
|
|
import fcntl
|
|
import os
|
|
import numpy
|
|
import random
|
|
import dbus
|
|
import dbus.service
|
|
import dbus.exceptions
|
|
|
|
from threading import Timer
|
|
import time
|
|
import uuid
|
|
|
|
try:
|
|
from gi.repository import GLib
|
|
except ImportError:
|
|
import glib as GLib
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
|
|
try:
|
|
from termcolor import colored, cprint
|
|
set_error = lambda x: colored('!' + x, 'red', attrs=['bold'])
|
|
set_cyan = lambda x: colored(x, 'cyan', attrs=['bold'])
|
|
set_green = lambda x: colored(x, 'green', attrs=['bold'])
|
|
set_yellow = lambda x: colored(x, 'yellow', attrs=['bold'])
|
|
except ImportError:
|
|
print('!!! Install termcolor module for better experience !!!')
|
|
set_error = lambda x: x
|
|
set_cyan = lambda x: x
|
|
set_green = lambda x: x
|
|
set_yellow = lambda x: x
|
|
|
|
# Provisioning agent
|
|
try:
|
|
import agent
|
|
except ImportError:
|
|
agent = None
|
|
|
|
MESH_SERVICE_NAME = 'org.bluez.mesh'
|
|
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
|
|
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
|
|
|
|
MESH_MGR_IFACE = 'org.bluez.mesh.Management1'
|
|
MESH_NETWORK_IFACE = 'org.bluez.mesh.Network1'
|
|
MESH_NODE_IFACE = 'org.bluez.mesh.Node1'
|
|
MESH_APPLICATION_IFACE = 'org.bluez.mesh.Application1'
|
|
MESH_ELEMENT_IFACE = 'org.bluez.mesh.Element1'
|
|
|
|
APP_COMPANY_ID = 0x05f1
|
|
APP_PRODUCT_ID = 0x0001
|
|
APP_VERSION_ID = 0x0001
|
|
|
|
VENDOR_ID_NONE = 0xffff
|
|
|
|
TRANSACTION_TIMEOUT = 6
|
|
|
|
app = None
|
|
bus = None
|
|
mainloop = None
|
|
node = None
|
|
node_mgr = None
|
|
mesh_net = None
|
|
|
|
dst_addr = 0x0000
|
|
app_idx = 0
|
|
|
|
# Node token housekeeping
|
|
token = None
|
|
have_token = False
|
|
attached = False
|
|
|
|
# Remote device UUID
|
|
have_uuid = False
|
|
remote_uuid = None
|
|
|
|
# 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
|
|
INPUT_MESSAGE_PAYLOAD = 4
|
|
INPUT_UUID = 5
|
|
|
|
menus = []
|
|
current_menu = None
|
|
|
|
user_input = 0
|
|
input_error = False
|
|
|
|
send_opts = dbus.Dictionary(signature='sv')
|
|
send_opts = {'ForceSegmented' : dbus.Boolean(True)}
|
|
|
|
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
|
|
global app
|
|
|
|
for el in app.elements:
|
|
for model in el.models:
|
|
if model.timer != None:
|
|
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 set_uuid(str_value):
|
|
global remote_uuid
|
|
global have_uuid
|
|
|
|
if len(str_value) != 32:
|
|
raise_error('Expected 32 digits')
|
|
return
|
|
|
|
remote_uuid = bytearray.fromhex(str_value)
|
|
have_uuid = True
|
|
|
|
def array_to_string(b_array):
|
|
str_value = ""
|
|
for b in b_array:
|
|
str_value += "%02x" % b
|
|
return str_value
|
|
|
|
def generic_error_cb(error):
|
|
print(set_error('D-Bus call failed: ') + str(error))
|
|
|
|
def generic_reply_cb():
|
|
return
|
|
|
|
def attach_app_error_cb(error):
|
|
print(set_error('Failed to register application: ') + str(error))
|
|
|
|
def attach(token):
|
|
print('Attach mesh node to bluetooth-meshd daemon')
|
|
|
|
mesh_net.Attach(app.get_path(), token,
|
|
reply_handler=attach_app_cb,
|
|
error_handler=attach_app_error_cb)
|
|
|
|
def join_cb():
|
|
print('Join procedure started')
|
|
|
|
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)
|
|
if isinstance(item, (dbus.UInt16, dbus.Int16, dbus.UInt32, dbus.Int32,
|
|
dbus.UInt64, dbus.Int64)):
|
|
return int(item)
|
|
if isinstance(item, dbus.Byte):
|
|
return bytes([int(item)])
|
|
if isinstance(item, dbus.String):
|
|
return item
|
|
if isinstance(item, (dbus.Array, list, tuple)):
|
|
return [unwrap(x) for x in item]
|
|
if isinstance(item, (dbus.Dictionary, dict)):
|
|
return dict([(unwrap(x), unwrap(y)) for x, y in item.items()])
|
|
|
|
print(set_error('Dictionary item not handled: ') + type(item))
|
|
|
|
return item
|
|
|
|
def attach_app_cb(node_path, dict_array):
|
|
global attached
|
|
|
|
attached = True
|
|
|
|
print(set_yellow('Mesh app registered: ') + set_green(node_path))
|
|
|
|
obj = bus.get_object(MESH_SERVICE_NAME, node_path)
|
|
|
|
global node_mgr
|
|
node_mgr = dbus.Interface(obj, MESH_MGR_IFACE)
|
|
|
|
global node
|
|
node = dbus.Interface(obj, MESH_NODE_IFACE)
|
|
|
|
els = unwrap(dict_array)
|
|
|
|
for el in els:
|
|
idx = struct.unpack('b', el[0])[0]
|
|
|
|
models = el[1]
|
|
element = app.get_element(idx)
|
|
element.set_model_config(models)
|
|
|
|
def interfaces_removed_cb(object_path, interfaces):
|
|
print('Removed')
|
|
if not mesh_net:
|
|
return
|
|
|
|
print(object_path)
|
|
if object_path == mesh_net[2]:
|
|
print('Service was removed')
|
|
app_exit()
|
|
|
|
def print_state(state):
|
|
print('State is ', end='')
|
|
if state == 0:
|
|
print('OFF')
|
|
elif state == 1:
|
|
print('ON')
|
|
else:
|
|
print('UNKNOWN')
|
|
class ModTimer():
|
|
def __init__(self):
|
|
self.seconds = None
|
|
self.func = None
|
|
self.thread = None
|
|
self.busy = False
|
|
|
|
def _timeout_cb(self):
|
|
self.func()
|
|
self.busy = True
|
|
self._schedule_timer()
|
|
self.busy =False
|
|
|
|
def _schedule_timer(self):
|
|
self.thread = Timer(self.seconds, self._timeout_cb)
|
|
self.thread.start()
|
|
|
|
def start(self, seconds, func):
|
|
self.func = func
|
|
self.seconds = seconds
|
|
if not self.busy:
|
|
self._schedule_timer()
|
|
|
|
def cancel(self):
|
|
if self.thread is not None:
|
|
self.thread.cancel()
|
|
self.thread = None
|
|
|
|
class Application(dbus.service.Object):
|
|
|
|
def __init__(self, bus):
|
|
self.path = '/example'
|
|
self.agent = None
|
|
self.elements = []
|
|
dbus.service.Object.__init__(self, bus, self.path)
|
|
|
|
def set_agent(self, agent):
|
|
self.agent = agent
|
|
|
|
def get_path(self):
|
|
return dbus.ObjectPath(self.path)
|
|
|
|
def add_element(self, element):
|
|
self.elements.append(element)
|
|
|
|
def get_element(self, idx):
|
|
for ele in self.elements:
|
|
if ele.get_index() == idx:
|
|
return ele
|
|
|
|
def get_properties(self):
|
|
return {
|
|
MESH_APPLICATION_IFACE: {
|
|
'CompanyID': dbus.UInt16(APP_COMPANY_ID),
|
|
'ProductID': dbus.UInt16(APP_PRODUCT_ID),
|
|
'VersionID': dbus.UInt16(APP_VERSION_ID)
|
|
}
|
|
}
|
|
|
|
@dbus.service.method(DBUS_OM_IFACE, out_signature='a{oa{sa{sv}}}')
|
|
def GetManagedObjects(self):
|
|
response = {}
|
|
response[self.path] = self.get_properties()
|
|
response[self.agent.get_path()] = self.agent.get_properties()
|
|
for element in self.elements:
|
|
response[element.get_path()] = element.get_properties()
|
|
return response
|
|
|
|
@dbus.service.method(MESH_APPLICATION_IFACE,
|
|
in_signature="t", out_signature="")
|
|
def JoinComplete(self, value):
|
|
global token
|
|
global have_token
|
|
global attach
|
|
|
|
print(set_yellow('Joined mesh network with token ') +
|
|
set_green(format(value, '016x')))
|
|
|
|
token = value
|
|
have_token = True
|
|
|
|
@dbus.service.method(MESH_APPLICATION_IFACE,
|
|
in_signature="s", out_signature="")
|
|
def JoinFailed(self, value):
|
|
print(set_error('JoinFailed '), value)
|
|
|
|
|
|
class Element(dbus.service.Object):
|
|
PATH_BASE = '/example/ele'
|
|
|
|
def __init__(self, bus, index):
|
|
self.path = self.PATH_BASE + format(index, '02x')
|
|
self.models = []
|
|
self.bus = bus
|
|
self.index = index
|
|
dbus.service.Object.__init__(self, bus, self.path)
|
|
|
|
def _get_sig_models(self):
|
|
mods = []
|
|
for model in self.models:
|
|
opts = []
|
|
id = model.get_id()
|
|
vendor = model.get_vendor()
|
|
if vendor == VENDOR_ID_NONE:
|
|
mod = (id, opts)
|
|
mods.append(mod)
|
|
return mods
|
|
|
|
def _get_v_models(self):
|
|
mods = []
|
|
for model in self.models:
|
|
opts = []
|
|
id = model.get_id()
|
|
v = model.get_vendor()
|
|
if v != VENDOR_ID_NONE:
|
|
mod = (v, id, opts)
|
|
mods.append(mod)
|
|
return mods
|
|
|
|
def get_properties(self):
|
|
vendor_models = self._get_v_models()
|
|
sig_models = self._get_sig_models()
|
|
|
|
props = {'Index' : dbus.Byte(self.index)}
|
|
props['Models'] = dbus.Array(sig_models, signature='(qa{sv})')
|
|
props['VendorModels'] = dbus.Array(vendor_models,
|
|
signature='(qqa{sv})')
|
|
#print(props)
|
|
return { MESH_ELEMENT_IFACE: props }
|
|
|
|
def add_model(self, model):
|
|
model.set_path(self.path)
|
|
self.models.append(model)
|
|
|
|
def get_index(self):
|
|
return self.index
|
|
|
|
def set_model_config(self, configs):
|
|
for config in configs:
|
|
mod_id = config[0]
|
|
self.update_model_config(mod_id, config[1])
|
|
|
|
@dbus.service.method(MESH_ELEMENT_IFACE,
|
|
in_signature="qqvay", out_signature="")
|
|
def MessageReceived(self, source, key, dest, data):
|
|
print(('Message Received on Element %02x') % self.index, end='')
|
|
print(', src=', format(source, '04x'), end='')
|
|
|
|
if isinstance(dest, int):
|
|
print(', dst=%04x' % dest)
|
|
elif isinstance(dest, dbus.Array):
|
|
dst_str = array_to_string(dest)
|
|
print(', dst=' + dst_str)
|
|
|
|
for model in self.models:
|
|
model.process_message(source, dest, key, data)
|
|
|
|
@dbus.service.method(MESH_ELEMENT_IFACE,
|
|
in_signature="qa{sv}", out_signature="")
|
|
|
|
def UpdateModelConfiguration(self, model_id, config):
|
|
cfg = unwrap(config)
|
|
print(cfg)
|
|
self.update_model_config(model_id, cfg)
|
|
|
|
def update_model_config(self, model_id, config):
|
|
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)
|
|
return
|
|
|
|
@dbus.service.method(MESH_ELEMENT_IFACE,
|
|
in_signature="", out_signature="")
|
|
|
|
def get_path(self):
|
|
return dbus.ObjectPath(self.path)
|
|
|
|
class Model():
|
|
def __init__(self, model_id):
|
|
self.cmd_ops = []
|
|
self.model_id = model_id
|
|
self.vendor = VENDOR_ID_NONE
|
|
self.bindings = []
|
|
self.pub_period = 0
|
|
self.pub_id = 0
|
|
self.path = None
|
|
self.timer = None
|
|
|
|
def set_path(self, path):
|
|
self.path = path
|
|
|
|
def get_id(self):
|
|
return self.model_id
|
|
|
|
def get_vendor(self):
|
|
return self.vendor
|
|
|
|
def process_message(self, source, dest, key, data):
|
|
return
|
|
|
|
def set_publication(self, period):
|
|
self.pub_period = period
|
|
|
|
def send_publication(self, data):
|
|
pub_opts = dbus.Dictionary(signature='sv')
|
|
|
|
print('Send publication ', end='')
|
|
print(data)
|
|
node.Publish(self.path, self.model_id, pub_opts, data,
|
|
reply_handler=generic_reply_cb,
|
|
error_handler=generic_error_cb)
|
|
|
|
def send_message(self, dest, key, data):
|
|
global send_opts
|
|
|
|
node.Send(self.path, dest, key, send_opts, 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')
|
|
print('Bindings: ', end='')
|
|
print(self.bindings)
|
|
if 'PublicationPeriod' in config:
|
|
self.set_publication(config.get('PublicationPeriod'))
|
|
print('Model publication period ', end='')
|
|
print(self.pub_period, end='')
|
|
print(' ms')
|
|
if 'Subscriptions' in config:
|
|
print('Model subscriptions ', end='')
|
|
self.print_subscriptions(config.get('Subscriptions'))
|
|
print()
|
|
|
|
def print_subscriptions(self, subscriptions):
|
|
for sub in subscriptions:
|
|
if isinstance(sub, int):
|
|
print('%04x,' % sub, end=' ')
|
|
|
|
if isinstance(sub, list):
|
|
label = uuid.UUID(bytes=b''.join(sub))
|
|
print(label, ',', end=' ')
|
|
|
|
########################
|
|
# On Off Server Model
|
|
########################
|
|
class OnOffServer(Model):
|
|
def __init__(self, model_id):
|
|
Model.__init__(self, model_id)
|
|
self.tid = None
|
|
self.last_src = 0x0000
|
|
self.last_dst = 0x0000
|
|
self.cmd_ops = { 0x8201, # get
|
|
0x8202, # set
|
|
0x8203, # set unacknowledged
|
|
0x8204 } # status
|
|
|
|
print("OnOff Server ")
|
|
self.state = 0
|
|
print_state(self.state)
|
|
self.pub_timer = ModTimer()
|
|
self.t_timer = ModTimer()
|
|
|
|
def process_message(self, source, dest, key, data):
|
|
datalen = len(data)
|
|
|
|
if datalen != 2 and datalen != 4:
|
|
# The opcode is not recognized by this model
|
|
return
|
|
|
|
if datalen == 2:
|
|
op_tuple=struct.unpack('>H',bytes(data))
|
|
opcode = op_tuple[0]
|
|
|
|
if opcode != 0x8201:
|
|
# The opcode is not recognized by this model
|
|
return
|
|
print('Get state')
|
|
elif datalen == 4:
|
|
opcode,self.state, tid = struct.unpack('>HBB',
|
|
bytes(data))
|
|
|
|
if opcode != 0x8202 and opcode != 0x8203:
|
|
# The opcode is not recognized by this model
|
|
return
|
|
print_state(self.state)
|
|
|
|
if (self.tid != None and self.tid == tid and
|
|
self.last_src == source and
|
|
self.last_dst == dest):
|
|
# Ignore duplicate transaction
|
|
return
|
|
|
|
self.t_timer.cancel()
|
|
self.tid = tid
|
|
self.last_src = source
|
|
self.last_dst = dest
|
|
self.t_timer.start(TRANSACTION_TIMEOUT, self.t_track)
|
|
|
|
# Unacknowledged "set"
|
|
if opcode == 0x8203:
|
|
return
|
|
|
|
rsp_data = struct.pack('>HB', 0x8204, self.state)
|
|
self.send_message(source, key, rsp_data)
|
|
|
|
def t_track(self):
|
|
self.t_timer.cancel()
|
|
self.tid = None
|
|
self.last_src = 0x0000
|
|
self.last_dst = 0x0000
|
|
|
|
def set_publication(self, period):
|
|
|
|
self.pub_period = period
|
|
if period == 0:
|
|
self.pub_timer.cancel()
|
|
return
|
|
|
|
# We do not handle ms in this example
|
|
if period < 1000:
|
|
return
|
|
|
|
self.pub_timer.start(period/1000, self.publish)
|
|
|
|
def publish(self):
|
|
print('Publish')
|
|
data = struct.pack('>HB', 0x8204, self.state)
|
|
self.send_publication(data)
|
|
|
|
########################
|
|
# On Off Client Model
|
|
########################
|
|
class OnOffClient(Model):
|
|
def __init__(self, model_id):
|
|
Model.__init__(self, model_id)
|
|
self.tid = 0
|
|
self.data = None
|
|
self.cmd_ops = { 0x8201, # get
|
|
0x8202, # set
|
|
0x8203, # set unacknowledged
|
|
0x8204 } # status
|
|
print('OnOff Client')
|
|
|
|
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
|
|
self.data = struct.pack('>H', opcode)
|
|
self._send_message(dest, key, self.data)
|
|
|
|
def set_state(self, dest, key, state):
|
|
opcode = 0x8202
|
|
print('Set state:', state)
|
|
self.data = struct.pack('>HBB', opcode, state, self.tid)
|
|
self.tid = (self.tid + 1) % 255
|
|
self._send_message(dest, key, self.data)
|
|
|
|
def repeat(self, dest, key):
|
|
if self.data != None:
|
|
self._send_message(dest, key, self.data)
|
|
else:
|
|
print('No previous command stored')
|
|
|
|
def process_message(self, source, dest, key, data):
|
|
print('OnOffClient process message len = ', end = '')
|
|
datalen = len(data)
|
|
print(datalen)
|
|
|
|
if datalen != 3:
|
|
# The opcode is not recognized by this model
|
|
return
|
|
|
|
opcode, state = struct.unpack('>HB',bytes(data))
|
|
|
|
if opcode != 0x8204 :
|
|
# The opcode is not recognized by this model
|
|
return
|
|
|
|
print(set_yellow('Got state '), end = '')
|
|
|
|
state_str = "ON"
|
|
if state == 0:
|
|
state_str = "OFF"
|
|
|
|
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
|
|
flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
|
|
flags |= os.O_NONBLOCK
|
|
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags)
|
|
sys.stdin.flush()
|
|
GLib.io_add_watch(sys.stdin, GLib.IO_IN, self.input_callback)
|
|
|
|
def input_callback(self, fd, condition):
|
|
chunk = fd.read()
|
|
buffer = ''
|
|
for char in chunk:
|
|
buffer += char
|
|
if char == '\n':
|
|
self.cb(buffer)
|
|
|
|
return True
|
|
|
|
def process_input(input_str):
|
|
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 current_menu
|
|
|
|
if level >= len(menus):
|
|
return
|
|
|
|
current_menu = menus[level]
|
|
current_menu.show()
|
|
|
|
########################
|
|
# Main menu class
|
|
########################
|
|
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),
|
|
'uuid': MenuItem(' - set remote uuid',
|
|
self.__cmd_set_uuid),
|
|
'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)
|
|
}
|
|
|
|
Menu.__init__(self, 'Main Menu', menu_items)
|
|
|
|
def __cmd_client_menu(self):
|
|
if attached != True:
|
|
print(set_error('Disallowed: node is not attached'))
|
|
return
|
|
switch_menu(ON_OFF_CLIENT_MENU)
|
|
|
|
def __cmd_set_token(self):
|
|
global user_input
|
|
|
|
if have_token == True:
|
|
print('Token already set')
|
|
return
|
|
|
|
user_input = INPUT_TOKEN
|
|
print(set_cyan('Enter 16-digit hex node ID:'))
|
|
|
|
def __cmd_set_dest(self):
|
|
global user_input
|
|
|
|
user_input = INPUT_DEST_ADDRESS
|
|
print(set_cyan('Enter 4-digit hex destination address:'))
|
|
|
|
def __cmd_set_uuid(self):
|
|
global user_input
|
|
|
|
user_input = INPUT_UUID
|
|
print(set_cyan('Enter 32-digit hex remote UUID:'))
|
|
|
|
def __cmd_set_app_idx(self):
|
|
global user_input
|
|
|
|
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'))
|
|
return
|
|
|
|
uuid_bytes = uuid.uuid4().bytes
|
|
uuid_str = array_to_string(uuid_bytes)
|
|
|
|
print(set_yellow('Joining with UUID ') + set_green(uuid_str))
|
|
mesh_net.Join(app.get_path(), uuid_bytes,
|
|
reply_handler=join_cb,
|
|
error_handler=join_error_cb)
|
|
|
|
def __cmd_attach(self):
|
|
if have_token == False:
|
|
print(set_error('Token is not set'))
|
|
self.show()
|
|
return
|
|
|
|
attach(token)
|
|
|
|
def __cmd_remove(self):
|
|
if have_token == False:
|
|
print(set_error('Token is not set'))
|
|
self.show()
|
|
return
|
|
|
|
print('Removing mesh node')
|
|
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
|
|
global app_idx
|
|
|
|
if user_input == INPUT_TOKEN:
|
|
set_token(str_value)
|
|
elif user_input == INPUT_UUID:
|
|
set_uuid(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')))
|
|
elif user_input == INPUT_MESSAGE_PAYLOAD:
|
|
self.__send_vendor_msg(str_value)
|
|
|
|
if user_input != INPUT_NONE:
|
|
user_input = INPUT_NONE
|
|
if is_error() != True:
|
|
return
|
|
|
|
Menu.process_cmd(self, str_value)
|
|
|
|
##############################
|
|
# On/Off Client menu class
|
|
##############################
|
|
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),
|
|
'repeat': MenuItem(' - repeat last command',
|
|
self.__cmd_repeat_transaction),
|
|
'back': MenuItem(' - back to main menu',
|
|
self.__cmd_main_menu),
|
|
'quit': MenuItem(' - exit the test', app_exit)
|
|
}
|
|
|
|
Menu.__init__(self, 'On/Off Client Menu', menu_items)
|
|
|
|
def __cmd_main_menu(self):
|
|
switch_menu(MAIN_MENU)
|
|
|
|
def __cmd_get_state(self):
|
|
app.elements[1].models[0].get_state(dst_addr, app_idx)
|
|
|
|
def __cmd_set_state_off(self):
|
|
app.elements[1].models[0].set_state(dst_addr, app_idx, 0)
|
|
|
|
def __cmd_set_state_on(self):
|
|
app.elements[1].models[0].set_state(dst_addr, app_idx, 1)
|
|
|
|
def __cmd_repeat_transaction(self):
|
|
app.elements[1].models[0].repeat(dst_addr, app_idx)
|
|
|
|
def set_value(str_value, min, max):
|
|
|
|
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_value, 16)
|
|
except ValueError:
|
|
raise_error('Not a valid hexadecimal number')
|
|
return -1
|
|
|
|
return value
|
|
|
|
|
|
########################
|
|
# Main entry
|
|
########################
|
|
def main():
|
|
|
|
DBusGMainLoop(set_as_default=True)
|
|
|
|
global bus
|
|
bus = dbus.SystemBus()
|
|
global mainloop
|
|
global app
|
|
global mesh_net
|
|
global menu
|
|
global current_menu
|
|
|
|
if len(sys.argv) > 1 :
|
|
set_token(sys.argv[1])
|
|
|
|
mesh_net = dbus.Interface(bus.get_object(MESH_SERVICE_NAME,
|
|
"/org/bluez/mesh"),
|
|
MESH_NETWORK_IFACE)
|
|
|
|
mesh_net.connect_to_signal('InterfacesRemoved', interfaces_removed_cb)
|
|
|
|
app = Application(bus)
|
|
|
|
# Provisioning agent
|
|
if agent != None:
|
|
app.set_agent(agent.Agent(bus))
|
|
|
|
first_ele = Element(bus, 0x00)
|
|
second_ele = Element(bus, 0x01)
|
|
|
|
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()
|
|
|
|
menus.append(MainMenu())
|
|
menus.append(ClientMenu())
|
|
switch_menu(MAIN_MENU)
|
|
|
|
event_catcher = MenuHandler(process_input);
|
|
mainloop.run()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|