test-mesh 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: LGPL-2.1-or-later
  3. ###################################################################
  4. #
  5. # This is a unified test sample for BT Mesh
  6. #
  7. # To run the test:
  8. # test-mesh [token]
  9. #
  10. # 'token' is an optional argument. It must be a 16-digit
  11. # hexadecimal number. The token must be associated with
  12. # an existing node. The token is generated and assigned
  13. # to a node as a result of successful provisioning (see
  14. # explanation of "join" option).
  15. # When the token is set, the menu operations "attach"
  16. # and "remove" may be performed on a node specified
  17. # by this token.
  18. #
  19. # The test imitates a device with 2 elements:
  20. # element 0: OnOff Server model
  21. # Sample Vendor model
  22. # element 1: OnOff Client model
  23. #
  24. # The main menu:
  25. # token
  26. # join
  27. # attach
  28. # remove
  29. # dest
  30. # uuid
  31. # app-index
  32. # client-menu
  33. # exit
  34. #
  35. # The main menu options explained:
  36. # token
  37. # Set the unique node token.
  38. # The token can be set from command line arguments as
  39. # well.
  40. #
  41. # join
  42. # Request provisioning of a device to become a node
  43. # on a mesh network. The test generates device UUID
  44. # which is displayed and will need to be provided to
  45. # an outside entity that acts as a Provisioner. Also,
  46. # during the provisioning process, an agent that is
  47. # part of the test, will request (or will be requested)
  48. # to perform a specified operation, e.g., a number will
  49. # be displayed and this number will need to be entered
  50. # on the Provisioner's side.
  51. # In case of successful provisioning, the application
  52. # automatically attaches as a node to the daemon. A node
  53. # 'token' is returned to the application and is used
  54. # for the runtime of the test.
  55. #
  56. # attach
  57. # Attach the application to bluetoothd-daemon as a node.
  58. # For the call to be successful, the valid node token must
  59. # be already set, either from command arguments or by
  60. # executing "set token" operation or automatically after
  61. # successfully executing "join" operation in the same
  62. # test run.
  63. #
  64. # remove
  65. # Permanently removes any node configuration from daemon
  66. # and persistent storage. After this operation, the node
  67. # is permanently forgotten by the daemon and the associated
  68. # node token is no longer valid.
  69. #
  70. # dest
  71. # Set destination address to send messages: 4 hex digits
  72. #
  73. # app-index
  74. # Set AppKey index to indicate which application key to use
  75. # to encode outgoing messages: up to 3 hex digits
  76. #
  77. # vendor-send
  78. # Allows to send an arbitrary endor message.
  79. # The destination is set based on previously executed "dest"
  80. # command (if not set, the outbound message will fail).
  81. # User is prompted to enter hex bytearray payload.
  82. # The message is originated from the vendor model registered
  83. # on element 0. For the command to succeed, the AppKey index
  84. # that is set by executing "app-key" must correspond to the
  85. # application key to which the Sample Vendor model is bound.
  86. #
  87. # client-menu
  88. # Enter On/Off client submenu.
  89. #
  90. # quit
  91. # Exits the test.
  92. #
  93. ###################################################################
  94. import sys
  95. import struct
  96. import fcntl
  97. import os
  98. import numpy
  99. import random
  100. import dbus
  101. import dbus.service
  102. import dbus.exceptions
  103. from threading import Timer
  104. import time
  105. import uuid
  106. try:
  107. from gi.repository import GLib
  108. except ImportError:
  109. import glib as GLib
  110. from dbus.mainloop.glib import DBusGMainLoop
  111. try:
  112. from termcolor import colored, cprint
  113. set_error = lambda x: colored('!' + x, 'red', attrs=['bold'])
  114. set_cyan = lambda x: colored(x, 'cyan', attrs=['bold'])
  115. set_green = lambda x: colored(x, 'green', attrs=['bold'])
  116. set_yellow = lambda x: colored(x, 'yellow', attrs=['bold'])
  117. except ImportError:
  118. print('!!! Install termcolor module for better experience !!!')
  119. set_error = lambda x: x
  120. set_cyan = lambda x: x
  121. set_green = lambda x: x
  122. set_yellow = lambda x: x
  123. # Provisioning agent
  124. try:
  125. import agent
  126. except ImportError:
  127. agent = None
  128. MESH_SERVICE_NAME = 'org.bluez.mesh'
  129. DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
  130. DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
  131. MESH_MGR_IFACE = 'org.bluez.mesh.Management1'
  132. MESH_NETWORK_IFACE = 'org.bluez.mesh.Network1'
  133. MESH_NODE_IFACE = 'org.bluez.mesh.Node1'
  134. MESH_APPLICATION_IFACE = 'org.bluez.mesh.Application1'
  135. MESH_ELEMENT_IFACE = 'org.bluez.mesh.Element1'
  136. APP_COMPANY_ID = 0x05f1
  137. APP_PRODUCT_ID = 0x0001
  138. APP_VERSION_ID = 0x0001
  139. VENDOR_ID_NONE = 0xffff
  140. TRANSACTION_TIMEOUT = 6
  141. app = None
  142. bus = None
  143. mainloop = None
  144. node = None
  145. node_mgr = None
  146. mesh_net = None
  147. dst_addr = 0x0000
  148. app_idx = 0
  149. # Node token housekeeping
  150. token = None
  151. have_token = False
  152. attached = False
  153. # Remote device UUID
  154. have_uuid = False
  155. remote_uuid = None
  156. # Menu housekeeping
  157. MAIN_MENU = 0
  158. ON_OFF_CLIENT_MENU = 1
  159. INPUT_NONE = 0
  160. INPUT_TOKEN = 1
  161. INPUT_DEST_ADDRESS = 2
  162. INPUT_APP_KEY_INDEX = 3
  163. INPUT_MESSAGE_PAYLOAD = 4
  164. INPUT_UUID = 5
  165. menus = []
  166. current_menu = None
  167. user_input = 0
  168. input_error = False
  169. send_opts = dbus.Dictionary(signature='sv')
  170. send_opts = {'ForceSegmented' : dbus.Boolean(True)}
  171. def raise_error(str_value):
  172. global input_error
  173. input_error = True
  174. print(set_error(str_value))
  175. def clear_error():
  176. global input_error
  177. input_error = False
  178. def is_error():
  179. return input_error
  180. def app_exit():
  181. global mainloop
  182. global app
  183. for el in app.elements:
  184. for model in el.models:
  185. if model.timer != None:
  186. model.timer.cancel()
  187. mainloop.quit()
  188. def set_token(str_value):
  189. global token
  190. global have_token
  191. if len(str_value) != 16:
  192. raise_error('Expected 16 digits')
  193. return
  194. try:
  195. input_number = int(str_value, 16)
  196. except ValueError:
  197. raise_error('Not a valid hexadecimal number')
  198. return
  199. token = numpy.uint64(input_number)
  200. have_token = True
  201. def set_uuid(str_value):
  202. global remote_uuid
  203. global have_uuid
  204. if len(str_value) != 32:
  205. raise_error('Expected 32 digits')
  206. return
  207. remote_uuid = bytearray.fromhex(str_value)
  208. have_uuid = True
  209. def array_to_string(b_array):
  210. str_value = ""
  211. for b in b_array:
  212. str_value += "%02x" % b
  213. return str_value
  214. def generic_error_cb(error):
  215. print(set_error('D-Bus call failed: ') + str(error))
  216. def generic_reply_cb():
  217. return
  218. def attach_app_error_cb(error):
  219. print(set_error('Failed to register application: ') + str(error))
  220. def attach(token):
  221. print('Attach mesh node to bluetooth-meshd daemon')
  222. mesh_net.Attach(app.get_path(), token,
  223. reply_handler=attach_app_cb,
  224. error_handler=attach_app_error_cb)
  225. def join_cb():
  226. print('Join procedure started')
  227. def join_error_cb(reason):
  228. print('Join procedure failed: ', reason)
  229. def remove_node_cb():
  230. global attached
  231. global have_token
  232. print(set_yellow('Node removed'))
  233. attached = False
  234. have_token = False
  235. def unwrap(item):
  236. if isinstance(item, dbus.Boolean):
  237. return bool(item)
  238. if isinstance(item, (dbus.UInt16, dbus.Int16, dbus.UInt32, dbus.Int32,
  239. dbus.UInt64, dbus.Int64)):
  240. return int(item)
  241. if isinstance(item, dbus.Byte):
  242. return bytes([int(item)])
  243. if isinstance(item, dbus.String):
  244. return item
  245. if isinstance(item, (dbus.Array, list, tuple)):
  246. return [unwrap(x) for x in item]
  247. if isinstance(item, (dbus.Dictionary, dict)):
  248. return dict([(unwrap(x), unwrap(y)) for x, y in item.items()])
  249. print(set_error('Dictionary item not handled: ') + type(item))
  250. return item
  251. def attach_app_cb(node_path, dict_array):
  252. global attached
  253. attached = True
  254. print(set_yellow('Mesh app registered: ') + set_green(node_path))
  255. obj = bus.get_object(MESH_SERVICE_NAME, node_path)
  256. global node_mgr
  257. node_mgr = dbus.Interface(obj, MESH_MGR_IFACE)
  258. global node
  259. node = dbus.Interface(obj, MESH_NODE_IFACE)
  260. els = unwrap(dict_array)
  261. for el in els:
  262. idx = struct.unpack('b', el[0])[0]
  263. models = el[1]
  264. element = app.get_element(idx)
  265. element.set_model_config(models)
  266. def interfaces_removed_cb(object_path, interfaces):
  267. print('Removed')
  268. if not mesh_net:
  269. return
  270. print(object_path)
  271. if object_path == mesh_net[2]:
  272. print('Service was removed')
  273. app_exit()
  274. def print_state(state):
  275. print('State is ', end='')
  276. if state == 0:
  277. print('OFF')
  278. elif state == 1:
  279. print('ON')
  280. else:
  281. print('UNKNOWN')
  282. class ModTimer():
  283. def __init__(self):
  284. self.seconds = None
  285. self.func = None
  286. self.thread = None
  287. self.busy = False
  288. def _timeout_cb(self):
  289. self.func()
  290. self.busy = True
  291. self._schedule_timer()
  292. self.busy =False
  293. def _schedule_timer(self):
  294. self.thread = Timer(self.seconds, self._timeout_cb)
  295. self.thread.start()
  296. def start(self, seconds, func):
  297. self.func = func
  298. self.seconds = seconds
  299. if not self.busy:
  300. self._schedule_timer()
  301. def cancel(self):
  302. if self.thread is not None:
  303. self.thread.cancel()
  304. self.thread = None
  305. class Application(dbus.service.Object):
  306. def __init__(self, bus):
  307. self.path = '/example'
  308. self.agent = None
  309. self.elements = []
  310. dbus.service.Object.__init__(self, bus, self.path)
  311. def set_agent(self, agent):
  312. self.agent = agent
  313. def get_path(self):
  314. return dbus.ObjectPath(self.path)
  315. def add_element(self, element):
  316. self.elements.append(element)
  317. def get_element(self, idx):
  318. for ele in self.elements:
  319. if ele.get_index() == idx:
  320. return ele
  321. def get_properties(self):
  322. return {
  323. MESH_APPLICATION_IFACE: {
  324. 'CompanyID': dbus.UInt16(APP_COMPANY_ID),
  325. 'ProductID': dbus.UInt16(APP_PRODUCT_ID),
  326. 'VersionID': dbus.UInt16(APP_VERSION_ID)
  327. }
  328. }
  329. @dbus.service.method(DBUS_OM_IFACE, out_signature='a{oa{sa{sv}}}')
  330. def GetManagedObjects(self):
  331. response = {}
  332. response[self.path] = self.get_properties()
  333. response[self.agent.get_path()] = self.agent.get_properties()
  334. for element in self.elements:
  335. response[element.get_path()] = element.get_properties()
  336. return response
  337. @dbus.service.method(MESH_APPLICATION_IFACE,
  338. in_signature="t", out_signature="")
  339. def JoinComplete(self, value):
  340. global token
  341. global have_token
  342. global attach
  343. print(set_yellow('Joined mesh network with token ') +
  344. set_green(format(value, '016x')))
  345. token = value
  346. have_token = True
  347. @dbus.service.method(MESH_APPLICATION_IFACE,
  348. in_signature="s", out_signature="")
  349. def JoinFailed(self, value):
  350. print(set_error('JoinFailed '), value)
  351. class Element(dbus.service.Object):
  352. PATH_BASE = '/example/ele'
  353. def __init__(self, bus, index):
  354. self.path = self.PATH_BASE + format(index, '02x')
  355. self.models = []
  356. self.bus = bus
  357. self.index = index
  358. dbus.service.Object.__init__(self, bus, self.path)
  359. def _get_sig_models(self):
  360. mods = []
  361. for model in self.models:
  362. opts = []
  363. id = model.get_id()
  364. vendor = model.get_vendor()
  365. if vendor == VENDOR_ID_NONE:
  366. mod = (id, opts)
  367. mods.append(mod)
  368. return mods
  369. def _get_v_models(self):
  370. mods = []
  371. for model in self.models:
  372. opts = []
  373. id = model.get_id()
  374. v = model.get_vendor()
  375. if v != VENDOR_ID_NONE:
  376. mod = (v, id, opts)
  377. mods.append(mod)
  378. return mods
  379. def get_properties(self):
  380. vendor_models = self._get_v_models()
  381. sig_models = self._get_sig_models()
  382. props = {'Index' : dbus.Byte(self.index)}
  383. props['Models'] = dbus.Array(sig_models, signature='(qa{sv})')
  384. props['VendorModels'] = dbus.Array(vendor_models,
  385. signature='(qqa{sv})')
  386. #print(props)
  387. return { MESH_ELEMENT_IFACE: props }
  388. def add_model(self, model):
  389. model.set_path(self.path)
  390. self.models.append(model)
  391. def get_index(self):
  392. return self.index
  393. def set_model_config(self, configs):
  394. for config in configs:
  395. mod_id = config[0]
  396. self.update_model_config(mod_id, config[1])
  397. @dbus.service.method(MESH_ELEMENT_IFACE,
  398. in_signature="qqvay", out_signature="")
  399. def MessageReceived(self, source, key, dest, data):
  400. print(('Message Received on Element %02x') % self.index, end='')
  401. print(', src=', format(source, '04x'), end='')
  402. if isinstance(dest, int):
  403. print(', dst=%04x' % dest)
  404. elif isinstance(dest, dbus.Array):
  405. dst_str = array_to_string(dest)
  406. print(', dst=' + dst_str)
  407. for model in self.models:
  408. model.process_message(source, dest, key, data)
  409. @dbus.service.method(MESH_ELEMENT_IFACE,
  410. in_signature="qa{sv}", out_signature="")
  411. def UpdateModelConfiguration(self, model_id, config):
  412. cfg = unwrap(config)
  413. print(cfg)
  414. self.update_model_config(model_id, cfg)
  415. def update_model_config(self, model_id, config):
  416. print(('Update Model Config '), end='')
  417. print(format(model_id, '04x'))
  418. for model in self.models:
  419. if model_id == model.get_id():
  420. model.set_config(config)
  421. return
  422. @dbus.service.method(MESH_ELEMENT_IFACE,
  423. in_signature="", out_signature="")
  424. def get_path(self):
  425. return dbus.ObjectPath(self.path)
  426. class Model():
  427. def __init__(self, model_id):
  428. self.cmd_ops = []
  429. self.model_id = model_id
  430. self.vendor = VENDOR_ID_NONE
  431. self.bindings = []
  432. self.pub_period = 0
  433. self.pub_id = 0
  434. self.path = None
  435. self.timer = None
  436. def set_path(self, path):
  437. self.path = path
  438. def get_id(self):
  439. return self.model_id
  440. def get_vendor(self):
  441. return self.vendor
  442. def process_message(self, source, dest, key, data):
  443. return
  444. def set_publication(self, period):
  445. self.pub_period = period
  446. def send_publication(self, data):
  447. pub_opts = dbus.Dictionary(signature='sv')
  448. print('Send publication ', end='')
  449. print(data)
  450. node.Publish(self.path, self.model_id, pub_opts, data,
  451. reply_handler=generic_reply_cb,
  452. error_handler=generic_error_cb)
  453. def send_message(self, dest, key, data):
  454. global send_opts
  455. node.Send(self.path, dest, key, send_opts, data,
  456. reply_handler=generic_reply_cb,
  457. error_handler=generic_error_cb)
  458. def set_config(self, config):
  459. if 'Bindings' in config:
  460. self.bindings = config.get('Bindings')
  461. print('Bindings: ', end='')
  462. print(self.bindings)
  463. if 'PublicationPeriod' in config:
  464. self.set_publication(config.get('PublicationPeriod'))
  465. print('Model publication period ', end='')
  466. print(self.pub_period, end='')
  467. print(' ms')
  468. if 'Subscriptions' in config:
  469. print('Model subscriptions ', end='')
  470. self.print_subscriptions(config.get('Subscriptions'))
  471. print()
  472. def print_subscriptions(self, subscriptions):
  473. for sub in subscriptions:
  474. if isinstance(sub, int):
  475. print('%04x,' % sub, end=' ')
  476. if isinstance(sub, list):
  477. label = uuid.UUID(bytes=b''.join(sub))
  478. print(label, ',', end=' ')
  479. ########################
  480. # On Off Server Model
  481. ########################
  482. class OnOffServer(Model):
  483. def __init__(self, model_id):
  484. Model.__init__(self, model_id)
  485. self.tid = None
  486. self.last_src = 0x0000
  487. self.last_dst = 0x0000
  488. self.cmd_ops = { 0x8201, # get
  489. 0x8202, # set
  490. 0x8203, # set unacknowledged
  491. 0x8204 } # status
  492. print("OnOff Server ")
  493. self.state = 0
  494. print_state(self.state)
  495. self.pub_timer = ModTimer()
  496. self.t_timer = ModTimer()
  497. def process_message(self, source, dest, key, data):
  498. datalen = len(data)
  499. if datalen != 2 and datalen != 4:
  500. # The opcode is not recognized by this model
  501. return
  502. if datalen == 2:
  503. op_tuple=struct.unpack('>H',bytes(data))
  504. opcode = op_tuple[0]
  505. if opcode != 0x8201:
  506. # The opcode is not recognized by this model
  507. return
  508. print('Get state')
  509. elif datalen == 4:
  510. opcode,self.state, tid = struct.unpack('>HBB',
  511. bytes(data))
  512. if opcode != 0x8202 and opcode != 0x8203:
  513. # The opcode is not recognized by this model
  514. return
  515. print_state(self.state)
  516. if (self.tid != None and self.tid == tid and
  517. self.last_src == source and
  518. self.last_dst == dest):
  519. # Ignore duplicate transaction
  520. return
  521. self.t_timer.cancel()
  522. self.tid = tid
  523. self.last_src = source
  524. self.last_dst = dest
  525. self.t_timer.start(TRANSACTION_TIMEOUT, self.t_track)
  526. # Unacknowledged "set"
  527. if opcode == 0x8203:
  528. return
  529. rsp_data = struct.pack('>HB', 0x8204, self.state)
  530. self.send_message(source, key, rsp_data)
  531. def t_track(self):
  532. self.t_timer.cancel()
  533. self.tid = None
  534. self.last_src = 0x0000
  535. self.last_dst = 0x0000
  536. def set_publication(self, period):
  537. self.pub_period = period
  538. if period == 0:
  539. self.pub_timer.cancel()
  540. return
  541. # We do not handle ms in this example
  542. if period < 1000:
  543. return
  544. self.pub_timer.start(period/1000, self.publish)
  545. def publish(self):
  546. print('Publish')
  547. data = struct.pack('>HB', 0x8204, self.state)
  548. self.send_publication(data)
  549. ########################
  550. # On Off Client Model
  551. ########################
  552. class OnOffClient(Model):
  553. def __init__(self, model_id):
  554. Model.__init__(self, model_id)
  555. self.tid = 0
  556. self.data = None
  557. self.cmd_ops = { 0x8201, # get
  558. 0x8202, # set
  559. 0x8203, # set unacknowledged
  560. 0x8204 } # status
  561. print('OnOff Client')
  562. def _send_message(self, dest, key, data):
  563. print('OnOffClient send command')
  564. self.send_message(dest, key, data)
  565. def get_state(self, dest, key):
  566. opcode = 0x8201
  567. self.data = struct.pack('>H', opcode)
  568. self._send_message(dest, key, self.data)
  569. def set_state(self, dest, key, state):
  570. opcode = 0x8202
  571. print('Set state:', state)
  572. self.data = struct.pack('>HBB', opcode, state, self.tid)
  573. self.tid = (self.tid + 1) % 255
  574. self._send_message(dest, key, self.data)
  575. def repeat(self, dest, key):
  576. if self.data != None:
  577. self._send_message(dest, key, self.data)
  578. else:
  579. print('No previous command stored')
  580. def process_message(self, source, dest, key, data):
  581. print('OnOffClient process message len = ', end = '')
  582. datalen = len(data)
  583. print(datalen)
  584. if datalen != 3:
  585. # The opcode is not recognized by this model
  586. return
  587. opcode, state = struct.unpack('>HB',bytes(data))
  588. if opcode != 0x8204 :
  589. # The opcode is not recognized by this model
  590. return
  591. print(set_yellow('Got state '), end = '')
  592. state_str = "ON"
  593. if state == 0:
  594. state_str = "OFF"
  595. print(set_green(state_str), set_yellow('from'),
  596. set_green('%04x' % source))
  597. ########################
  598. # Sample Vendor Model
  599. ########################
  600. class SampleVendor(Model):
  601. def __init__(self, model_id):
  602. Model.__init__(self, model_id)
  603. self.vendor = 0x05F1 # Linux Foundation Company ID
  604. ########################
  605. # Menu functions
  606. ########################
  607. class MenuItem():
  608. def __init__(self, desc, func):
  609. self.desc = desc
  610. self.func = func
  611. class Menu():
  612. def __init__(self, title, menu):
  613. self.title = title
  614. self.menu = menu
  615. def show(self):
  616. print(set_cyan('*** ' + self.title.upper() + ' ***'))
  617. for k, v in self.menu.items():
  618. print(set_green(k), set_cyan(v.desc))
  619. def process_cmd(self, str_value):
  620. if is_error():
  621. self.show()
  622. clear_error()
  623. return
  624. cmds = []
  625. for key in self.menu.keys():
  626. if key.startswith(str_value):
  627. cmds.append(key)
  628. if len(cmds) == 0:
  629. print(set_error('Unknown menu option: '), str_value)
  630. self.show()
  631. return
  632. if len(cmds) > 1:
  633. for cmd in cmds:
  634. print(set_cyan(cmd + '?'))
  635. return
  636. self.menu.get(cmds[0]).func()
  637. class MenuHandler(object):
  638. def __init__(self, callback):
  639. self.cb = callback
  640. flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
  641. flags |= os.O_NONBLOCK
  642. fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags)
  643. sys.stdin.flush()
  644. GLib.io_add_watch(sys.stdin, GLib.IO_IN, self.input_callback)
  645. def input_callback(self, fd, condition):
  646. chunk = fd.read()
  647. buffer = ''
  648. for char in chunk:
  649. buffer += char
  650. if char == '\n':
  651. self.cb(buffer)
  652. return True
  653. def process_input(input_str):
  654. str_value = input_str.strip()
  655. # Allow entering empty lines for better output visibility
  656. if len(str_value) == 0:
  657. return
  658. current_menu.process_cmd(str_value)
  659. def switch_menu(level):
  660. global current_menu
  661. if level >= len(menus):
  662. return
  663. current_menu = menus[level]
  664. current_menu.show()
  665. ########################
  666. # Main menu class
  667. ########################
  668. class MainMenu(Menu):
  669. def __init__(self):
  670. menu_items = {
  671. 'token': MenuItem(' - set node ID (token)',
  672. self.__cmd_set_token),
  673. 'join': MenuItem(' - join mesh network',
  674. self.__cmd_join),
  675. 'attach': MenuItem(' - attach mesh node',
  676. self.__cmd_attach),
  677. 'remove': MenuItem(' - delete node',
  678. self.__cmd_remove),
  679. 'dest': MenuItem(' - set destination address',
  680. self.__cmd_set_dest),
  681. 'uuid': MenuItem(' - set remote uuid',
  682. self.__cmd_set_uuid),
  683. 'app-index': MenuItem(' - set AppKey index',
  684. self.__cmd_set_app_idx),
  685. 'vendor-send': MenuItem(' - send raw vendor message',
  686. self.__cmd_vendor_msg),
  687. 'client-menu': MenuItem(' - On/Off client menu',
  688. self.__cmd_client_menu),
  689. 'quit': MenuItem(' - exit the test', app_exit)
  690. }
  691. Menu.__init__(self, 'Main Menu', menu_items)
  692. def __cmd_client_menu(self):
  693. if attached != True:
  694. print(set_error('Disallowed: node is not attached'))
  695. return
  696. switch_menu(ON_OFF_CLIENT_MENU)
  697. def __cmd_set_token(self):
  698. global user_input
  699. if have_token == True:
  700. print('Token already set')
  701. return
  702. user_input = INPUT_TOKEN
  703. print(set_cyan('Enter 16-digit hex node ID:'))
  704. def __cmd_set_dest(self):
  705. global user_input
  706. user_input = INPUT_DEST_ADDRESS
  707. print(set_cyan('Enter 4-digit hex destination address:'))
  708. def __cmd_set_uuid(self):
  709. global user_input
  710. user_input = INPUT_UUID
  711. print(set_cyan('Enter 32-digit hex remote UUID:'))
  712. def __cmd_set_app_idx(self):
  713. global user_input
  714. user_input = INPUT_APP_KEY_INDEX;
  715. print(set_cyan('Enter app key index (up to 3 digit hex):'))
  716. def __cmd_vendor_msg(self):
  717. global user_input
  718. user_input = INPUT_MESSAGE_PAYLOAD;
  719. print(set_cyan('Enter message payload (hex):'))
  720. def __cmd_join(self):
  721. if agent == None:
  722. print(set_error('Provisioning agent not found'))
  723. return
  724. uuid_bytes = uuid.uuid4().bytes
  725. uuid_str = array_to_string(uuid_bytes)
  726. print(set_yellow('Joining with UUID ') + set_green(uuid_str))
  727. mesh_net.Join(app.get_path(), uuid_bytes,
  728. reply_handler=join_cb,
  729. error_handler=join_error_cb)
  730. def __cmd_attach(self):
  731. if have_token == False:
  732. print(set_error('Token is not set'))
  733. self.show()
  734. return
  735. attach(token)
  736. def __cmd_remove(self):
  737. if have_token == False:
  738. print(set_error('Token is not set'))
  739. self.show()
  740. return
  741. print('Removing mesh node')
  742. mesh_net.Leave(token, reply_handler=remove_node_cb,
  743. error_handler=generic_error_cb)
  744. def __send_vendor_msg(self, str_value):
  745. try:
  746. msg_data = bytearray.fromhex(str_value)
  747. except ValueError:
  748. raise_error('Not a valid hexadecimal input')
  749. return
  750. print(set_yellow('Send data: ' + set_green(str_value)))
  751. app.elements[0].models[1].send_message(dst_addr, app_idx,
  752. msg_data)
  753. def process_cmd(self, str_value):
  754. global user_input
  755. global dst_addr
  756. global app_idx
  757. if user_input == INPUT_TOKEN:
  758. set_token(str_value)
  759. elif user_input == INPUT_UUID:
  760. set_uuid(str_value)
  761. elif user_input == INPUT_DEST_ADDRESS:
  762. res = set_value(str_value, 4, 4)
  763. if is_error() != True:
  764. dst_addr = res
  765. print(set_yellow("Destination address: ") +
  766. set_green(format(dst_addr, '04x')))
  767. elif user_input == INPUT_APP_KEY_INDEX:
  768. res = set_value(str_value, 1, 3)
  769. if is_error() != True:
  770. app_idx = res
  771. print(set_yellow("Application index: ") +
  772. set_green(format(app_idx, '03x')))
  773. elif user_input == INPUT_MESSAGE_PAYLOAD:
  774. self.__send_vendor_msg(str_value)
  775. if user_input != INPUT_NONE:
  776. user_input = INPUT_NONE
  777. if is_error() != True:
  778. return
  779. Menu.process_cmd(self, str_value)
  780. ##############################
  781. # On/Off Client menu class
  782. ##############################
  783. class ClientMenu(Menu):
  784. def __init__(self):
  785. menu_items = {
  786. 'get-state': MenuItem(' - get server state',
  787. self.__cmd_get_state),
  788. 'off': MenuItem(' - set state OFF',
  789. self.__cmd_set_state_off),
  790. 'on': MenuItem(' - set state ON',
  791. self.__cmd_set_state_on),
  792. 'repeat': MenuItem(' - repeat last command',
  793. self.__cmd_repeat_transaction),
  794. 'back': MenuItem(' - back to main menu',
  795. self.__cmd_main_menu),
  796. 'quit': MenuItem(' - exit the test', app_exit)
  797. }
  798. Menu.__init__(self, 'On/Off Client Menu', menu_items)
  799. def __cmd_main_menu(self):
  800. switch_menu(MAIN_MENU)
  801. def __cmd_get_state(self):
  802. app.elements[1].models[0].get_state(dst_addr, app_idx)
  803. def __cmd_set_state_off(self):
  804. app.elements[1].models[0].set_state(dst_addr, app_idx, 0)
  805. def __cmd_set_state_on(self):
  806. app.elements[1].models[0].set_state(dst_addr, app_idx, 1)
  807. def __cmd_repeat_transaction(self):
  808. app.elements[1].models[0].repeat(dst_addr, app_idx)
  809. def set_value(str_value, min, max):
  810. if len(str_value) > max or len(str_value) < min:
  811. raise_error('Bad input length %d' % len(str_value))
  812. return -1
  813. try:
  814. value = int(str_value, 16)
  815. except ValueError:
  816. raise_error('Not a valid hexadecimal number')
  817. return -1
  818. return value
  819. ########################
  820. # Main entry
  821. ########################
  822. def main():
  823. DBusGMainLoop(set_as_default=True)
  824. global bus
  825. bus = dbus.SystemBus()
  826. global mainloop
  827. global app
  828. global mesh_net
  829. global menu
  830. global current_menu
  831. if len(sys.argv) > 1 :
  832. set_token(sys.argv[1])
  833. mesh_net = dbus.Interface(bus.get_object(MESH_SERVICE_NAME,
  834. "/org/bluez/mesh"),
  835. MESH_NETWORK_IFACE)
  836. mesh_net.connect_to_signal('InterfacesRemoved', interfaces_removed_cb)
  837. app = Application(bus)
  838. # Provisioning agent
  839. if agent != None:
  840. app.set_agent(agent.Agent(bus))
  841. first_ele = Element(bus, 0x00)
  842. second_ele = Element(bus, 0x01)
  843. print(set_yellow('Register OnOff Server model on element 0'))
  844. first_ele.add_model(OnOffServer(0x1000))
  845. print(set_yellow('Register Vendor model on element 0'))
  846. first_ele.add_model(SampleVendor(0x0001))
  847. print(set_yellow('Register OnOff Client model on element 1'))
  848. second_ele.add_model(OnOffClient(0x1001))
  849. app.add_element(first_ele)
  850. app.add_element(second_ele)
  851. mainloop = GLib.MainLoop()
  852. menus.append(MainMenu())
  853. menus.append(ClientMenu())
  854. switch_menu(MAIN_MENU)
  855. event_catcher = MenuHandler(process_input);
  856. mainloop.run()
  857. if __name__ == '__main__':
  858. main()