esp_prov.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2018 Espressif Systems (Shanghai) PTE LTD
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. from __future__ import print_function
  18. from builtins import input as binput
  19. import argparse
  20. import textwrap
  21. import time
  22. import os
  23. import sys
  24. import json
  25. from getpass import getpass
  26. try:
  27. import security
  28. import transport
  29. import prov
  30. except ImportError:
  31. idf_path = os.environ['IDF_PATH']
  32. sys.path.insert(0, idf_path + "/components/protocomm/python")
  33. sys.path.insert(1, idf_path + "/tools/esp_prov")
  34. import security
  35. import transport
  36. import prov
  37. # Set this to true to allow exceptions to be thrown
  38. config_throw_except = False
  39. def on_except(err):
  40. if config_throw_except:
  41. raise RuntimeError(err)
  42. else:
  43. print(err)
  44. def get_security(secver, pop=None, verbose=False):
  45. if secver == 1:
  46. return security.Security1(pop, verbose)
  47. elif secver == 0:
  48. return security.Security0(verbose)
  49. return None
  50. def get_transport(sel_transport, service_name):
  51. try:
  52. tp = None
  53. if (sel_transport == 'softap'):
  54. if service_name is None:
  55. service_name = '192.168.4.1:80'
  56. tp = transport.Transport_HTTP(service_name)
  57. elif (sel_transport == 'ble'):
  58. if service_name is None:
  59. raise RuntimeError('"--service_name" must be specified for ble transport')
  60. # BLE client is now capable of automatically figuring out
  61. # the primary service from the advertisement data and the
  62. # characteristics corresponding to each endpoint.
  63. # Below, the service_uuid field and 16bit UUIDs in the nu_lookup
  64. # table are provided only to support devices running older firmware,
  65. # in which case, the automated discovery will fail and the client
  66. # will fallback to using the provided UUIDs instead
  67. nu_lookup = {'prov-session': 'ff51', 'prov-config': 'ff52', 'proto-ver': 'ff53'}
  68. tp = transport.Transport_BLE(devname=service_name,
  69. service_uuid='021a9004-0382-4aea-bff4-6b3f1c5adfb4',
  70. nu_lookup=nu_lookup)
  71. elif (sel_transport == 'console'):
  72. tp = transport.Transport_Console()
  73. return tp
  74. except RuntimeError as e:
  75. on_except(e)
  76. return None
  77. def version_match(tp, protover, verbose=False):
  78. try:
  79. response = tp.send_data('proto-ver', protover)
  80. if verbose:
  81. print("proto-ver response : ", response)
  82. # First assume this to be a simple version string
  83. if response.lower() == protover.lower():
  84. return True
  85. try:
  86. # Else interpret this as JSON structure containing
  87. # information with versions and capabilities of both
  88. # provisioning service and application
  89. info = json.loads(response)
  90. if info['prov']['ver'].lower() == protover.lower():
  91. return True
  92. except ValueError:
  93. # If decoding as JSON fails, it means that capabilities
  94. # are not supported
  95. return False
  96. except Exception as e:
  97. on_except(e)
  98. return None
  99. def has_capability(tp, capability='none', verbose=False):
  100. # Note : default value of `capability` argument cannot be empty string
  101. # because protocomm_httpd expects non zero content lengths
  102. try:
  103. response = tp.send_data('proto-ver', capability)
  104. if verbose:
  105. print("proto-ver response : ", response)
  106. try:
  107. # Interpret this as JSON structure containing
  108. # information with versions and capabilities of both
  109. # provisioning service and application
  110. info = json.loads(response)
  111. supported_capabilities = info['prov']['cap']
  112. if capability.lower() == 'none':
  113. # No specific capability to check, but capabilities
  114. # feature is present so return True
  115. return True
  116. elif capability in supported_capabilities:
  117. return True
  118. return False
  119. except ValueError:
  120. # If decoding as JSON fails, it means that capabilities
  121. # are not supported
  122. return False
  123. except RuntimeError as e:
  124. on_except(e)
  125. return False
  126. def get_version(tp):
  127. response = None
  128. try:
  129. response = tp.send_data('proto-ver', '---')
  130. except RuntimeError as e:
  131. on_except(e)
  132. response = ''
  133. return response
  134. def establish_session(tp, sec):
  135. try:
  136. response = None
  137. while True:
  138. request = sec.security_session(response)
  139. if request is None:
  140. break
  141. response = tp.send_data('prov-session', request)
  142. if (response is None):
  143. return False
  144. return True
  145. except RuntimeError as e:
  146. on_except(e)
  147. return None
  148. def custom_config(tp, sec, custom_info, custom_ver):
  149. try:
  150. message = prov.custom_config_request(sec, custom_info, custom_ver)
  151. response = tp.send_data('custom-config', message)
  152. return (prov.custom_config_response(sec, response) == 0)
  153. except RuntimeError as e:
  154. on_except(e)
  155. return None
  156. def custom_data(tp, sec, custom_data):
  157. try:
  158. message = prov.custom_data_request(sec, custom_data)
  159. response = tp.send_data('custom-data', message)
  160. return (prov.custom_data_response(sec, response) == 0)
  161. except RuntimeError as e:
  162. on_except(e)
  163. return None
  164. def scan_wifi_APs(sel_transport, tp, sec):
  165. APs = []
  166. group_channels = 0
  167. readlen = 100
  168. if sel_transport == 'softap':
  169. # In case of softAP we must perform the scan on individual channels, one by one,
  170. # so that the Wi-Fi controller gets ample time to send out beacons (necessary to
  171. # maintain connectivity with authenticated stations. As scanning one channel at a
  172. # time will be slow, we can group more than one channels to be scanned in quick
  173. # succession, hence speeding up the scan process. Though if too many channels are
  174. # present in a group, the controller may again miss out on sending beacons. Hence,
  175. # the application must should use an optimum value. The following value usually
  176. # works out in most cases
  177. group_channels = 5
  178. elif sel_transport == 'ble':
  179. # Read at most 4 entries at a time. This is because if we are using BLE transport
  180. # then the response packet size should not exceed the present limit of 256 bytes of
  181. # characteristic value imposed by protocomm_ble. This limit may be removed in the
  182. # future
  183. readlen = 4
  184. try:
  185. message = prov.scan_start_request(sec, blocking=True, group_channels=group_channels)
  186. start_time = time.time()
  187. response = tp.send_data('prov-scan', message)
  188. stop_time = time.time()
  189. print("++++ Scan process executed in " + str(stop_time - start_time) + " sec")
  190. prov.scan_start_response(sec, response)
  191. message = prov.scan_status_request(sec)
  192. response = tp.send_data('prov-scan', message)
  193. result = prov.scan_status_response(sec, response)
  194. print("++++ Scan results : " + str(result["count"]))
  195. if result["count"] != 0:
  196. index = 0
  197. remaining = result["count"]
  198. while remaining:
  199. count = [remaining, readlen][remaining > readlen]
  200. message = prov.scan_result_request(sec, index, count)
  201. response = tp.send_data('prov-scan', message)
  202. APs += prov.scan_result_response(sec, response)
  203. remaining -= count
  204. index += count
  205. except RuntimeError as e:
  206. on_except(e)
  207. return None
  208. return APs
  209. def send_wifi_config(tp, sec, ssid, passphrase):
  210. try:
  211. message = prov.config_set_config_request(sec, ssid, passphrase)
  212. response = tp.send_data('prov-config', message)
  213. return (prov.config_set_config_response(sec, response) == 0)
  214. except RuntimeError as e:
  215. on_except(e)
  216. return None
  217. def apply_wifi_config(tp, sec):
  218. try:
  219. message = prov.config_apply_config_request(sec)
  220. response = tp.send_data('prov-config', message)
  221. return (prov.config_apply_config_response(sec, response) == 0)
  222. except RuntimeError as e:
  223. on_except(e)
  224. return None
  225. def get_wifi_config(tp, sec):
  226. try:
  227. message = prov.config_get_status_request(sec)
  228. response = tp.send_data('prov-config', message)
  229. return prov.config_get_status_response(sec, response)
  230. except RuntimeError as e:
  231. on_except(e)
  232. return None
  233. def wait_wifi_connected(tp, sec):
  234. """
  235. Wait for provisioning to report Wi-Fi is connected
  236. Returns True if Wi-Fi connection succeeded, False if connection consistently failed
  237. """
  238. TIME_PER_POLL = 5
  239. retry = 3
  240. while True:
  241. time.sleep(TIME_PER_POLL)
  242. print("\n==== Wi-Fi connection state ====")
  243. ret = get_wifi_config(tp, sec)
  244. if ret == "connecting":
  245. continue
  246. elif ret == "connected":
  247. print("==== Provisioning was successful ====")
  248. return True
  249. elif retry > 0:
  250. retry -= 1
  251. print("Waiting to poll status again (status %s, %d tries left)..." % (ret, retry))
  252. else:
  253. print("---- Provisioning failed ----")
  254. return False
  255. def desc_format(*args):
  256. desc = ''
  257. for arg in args:
  258. desc += textwrap.fill(replace_whitespace=False, text=arg) + "\n"
  259. return desc
  260. if __name__ == '__main__':
  261. parser = argparse.ArgumentParser(description=desc_format(
  262. 'ESP Provisioning tool for configuring devices '
  263. 'running protocomm based provisioning service.',
  264. 'See esp-idf/examples/provisioning for sample applications'),
  265. formatter_class=argparse.RawTextHelpFormatter)
  266. parser.add_argument("--transport", required=True, dest='mode', type=str,
  267. help=desc_format(
  268. 'Mode of transport over which provisioning is to be performed.',
  269. 'This should be one of "softap", "ble" or "console"'))
  270. parser.add_argument("--service_name", dest='name', type=str,
  271. help=desc_format(
  272. 'This specifies the name of the provisioning service to connect to, '
  273. 'depending upon the mode of transport :',
  274. '\t- transport "ble" : The BLE Device Name',
  275. '\t- transport "softap" : HTTP Server hostname or IP',
  276. '\t (default "192.168.4.1:80")'))
  277. parser.add_argument("--proto_ver", dest='version', type=str, default='',
  278. help=desc_format(
  279. 'This checks the protocol version of the provisioning service running '
  280. 'on the device before initiating Wi-Fi configuration'))
  281. parser.add_argument("--sec_ver", dest='secver', type=int, default=None,
  282. help=desc_format(
  283. 'Protocomm security scheme used by the provisioning service for secure '
  284. 'session establishment. Accepted values are :',
  285. '\t- 0 : No security',
  286. '\t- 1 : X25519 key exchange + AES-CTR encryption',
  287. '\t + Authentication using Proof of Possession (PoP)',
  288. 'In case device side application uses IDF\'s provisioning manager, '
  289. 'the compatible security version is automatically determined from '
  290. 'capabilities retrieved via the version endpoint'))
  291. parser.add_argument("--pop", dest='pop', type=str, default='',
  292. help=desc_format(
  293. 'This specifies the Proof of possession (PoP) when security scheme 1 '
  294. 'is used'))
  295. parser.add_argument("--ssid", dest='ssid', type=str, default='',
  296. help=desc_format(
  297. 'This configures the device to use SSID of the Wi-Fi network to which '
  298. 'we would like it to connect to permanently, once provisioning is complete. '
  299. 'If Wi-Fi scanning is supported by the provisioning service, this need not '
  300. 'be specified'))
  301. parser.add_argument("--passphrase", dest='passphrase', type=str, default='',
  302. help=desc_format(
  303. 'This configures the device to use Passphrase for the Wi-Fi network to which '
  304. 'we would like it to connect to permanently, once provisioning is complete. '
  305. 'If Wi-Fi scanning is supported by the provisioning service, this need not '
  306. 'be specified'))
  307. parser.add_argument("--custom_data", dest='custom_data', type=str, default='',
  308. help=desc_format(
  309. 'This is an optional parameter, only intended for use with '
  310. '"examples/provisioning/wifi_prov_mgr_custom_data"'))
  311. parser.add_argument("--custom_config", action="store_true",
  312. help=desc_format(
  313. 'This is an optional parameter, only intended for use with '
  314. '"examples/provisioning/custom_config"'))
  315. parser.add_argument("--custom_info", dest='custom_info', type=str, default='<some custom info string>',
  316. help=desc_format(
  317. 'Custom Config Info String. "--custom_config" must be specified for using this'))
  318. parser.add_argument("--custom_ver", dest='custom_ver', type=int, default=2,
  319. help=desc_format(
  320. 'Custom Config Version Number. "--custom_config" must be specified for using this'))
  321. parser.add_argument("-v","--verbose", help="Increase output verbosity", action="store_true")
  322. args = parser.parse_args()
  323. obj_transport = get_transport(args.mode.lower(), args.name)
  324. if obj_transport is None:
  325. print("---- Failed to establish connection ----")
  326. exit(1)
  327. # If security version not specified check in capabilities
  328. if args.secver is None:
  329. # First check if capabilities are supported or not
  330. if not has_capability(obj_transport):
  331. print('Security capabilities could not be determined. Please specify "--sec_ver" explicitly')
  332. print("---- Invalid Security Version ----")
  333. exit(2)
  334. # When no_sec is present, use security 0, else security 1
  335. args.secver = int(not has_capability(obj_transport, 'no_sec'))
  336. print("Security scheme determined to be :", args.secver)
  337. if (args.secver != 0) and not has_capability(obj_transport, 'no_pop'):
  338. if len(args.pop) == 0:
  339. print("---- Proof of Possession argument not provided ----")
  340. exit(2)
  341. elif len(args.pop) != 0:
  342. print("---- Proof of Possession will be ignored ----")
  343. args.pop = ''
  344. obj_security = get_security(args.secver, args.pop, args.verbose)
  345. if obj_security is None:
  346. print("---- Invalid Security Version ----")
  347. exit(2)
  348. if args.version != '':
  349. print("\n==== Verifying protocol version ====")
  350. if not version_match(obj_transport, args.version, args.verbose):
  351. print("---- Error in protocol version matching ----")
  352. exit(3)
  353. print("==== Verified protocol version successfully ====")
  354. print("\n==== Starting Session ====")
  355. if not establish_session(obj_transport, obj_security):
  356. print("Failed to establish session. Ensure that security scheme and proof of possession are correct")
  357. print("---- Error in establishing session ----")
  358. exit(4)
  359. print("==== Session Established ====")
  360. if args.custom_config:
  361. print("\n==== Sending Custom config to esp32 ====")
  362. if not custom_config(obj_transport, obj_security, args.custom_info, args.custom_ver):
  363. print("---- Error in custom config ----")
  364. exit(5)
  365. print("==== Custom config sent successfully ====")
  366. if args.custom_data != '':
  367. print("\n==== Sending Custom data to esp32 ====")
  368. if not custom_data(obj_transport, obj_security, args.custom_data):
  369. print("---- Error in custom data ----")
  370. exit(5)
  371. print("==== Custom data sent successfully ====")
  372. if args.ssid == '':
  373. if not has_capability(obj_transport, 'wifi_scan'):
  374. print("---- Wi-Fi Scan List is not supported by provisioning service ----")
  375. print("---- Rerun esp_prov with SSID and Passphrase as argument ----")
  376. exit(3)
  377. while True:
  378. print("\n==== Scanning Wi-Fi APs ====")
  379. start_time = time.time()
  380. APs = scan_wifi_APs(args.mode.lower(), obj_transport, obj_security)
  381. end_time = time.time()
  382. print("\n++++ Scan finished in " + str(end_time - start_time) + " sec")
  383. if APs is None:
  384. print("---- Error in scanning Wi-Fi APs ----")
  385. exit(8)
  386. if len(APs) == 0:
  387. print("No APs found!")
  388. exit(9)
  389. print("==== Wi-Fi Scan results ====")
  390. print("{0: >4} {1: <33} {2: <12} {3: >4} {4: <4} {5: <16}".format(
  391. "S.N.", "SSID", "BSSID", "CHN", "RSSI", "AUTH"))
  392. for i in range(len(APs)):
  393. print("[{0: >2}] {1: <33} {2: <12} {3: >4} {4: <4} {5: <16}".format(
  394. i + 1, APs[i]["ssid"], APs[i]["bssid"], APs[i]["channel"], APs[i]["rssi"], APs[i]["auth"]))
  395. while True:
  396. try:
  397. select = int(binput("Select AP by number (0 to rescan) : "))
  398. if select < 0 or select > len(APs):
  399. raise ValueError
  400. break
  401. except ValueError:
  402. print("Invalid input! Retry")
  403. if select != 0:
  404. break
  405. args.ssid = APs[select - 1]["ssid"]
  406. prompt_str = "Enter passphrase for {0} : ".format(args.ssid)
  407. args.passphrase = getpass(prompt_str)
  408. print("\n==== Sending Wi-Fi credential to esp32 ====")
  409. if not send_wifi_config(obj_transport, obj_security, args.ssid, args.passphrase):
  410. print("---- Error in send Wi-Fi config ----")
  411. exit(6)
  412. print("==== Wi-Fi Credentials sent successfully ====")
  413. print("\n==== Applying config to esp32 ====")
  414. if not apply_wifi_config(obj_transport, obj_security):
  415. print("---- Error in apply Wi-Fi config ----")
  416. exit(7)
  417. print("==== Apply config sent successfully ====")
  418. wait_wifi_connected(obj_transport, obj_security)