369 lines
12 KiB
Python
369 lines
12 KiB
Python
# ============================================================+
|
|
# Identity IoT — Boot Manager
|
|
# (c) Copyright : ae Aeonian Engineering Limited - Hong Kong
|
|
# (c) Copyright : WIDE di D. Papa - Naples - Italy
|
|
# ============================================================+
|
|
# State machine: BOOT → CONFIG_CHECK → PROVISIONING | WIFI_CONNECT
|
|
# → CHECKIN → OPERATIONAL
|
|
# ============================================================+
|
|
|
|
import ujson
|
|
import utime
|
|
import network
|
|
import machine
|
|
import ubinascii
|
|
from machine import Pin, I2C
|
|
|
|
# ---- Optional OLED ----
|
|
try:
|
|
from ssd1306 import SSD1306_I2C
|
|
_i2c = I2C(0, sda=Pin(4), scl=Pin(5))
|
|
_oled = SSD1306_I2C(128, 64, _i2c)
|
|
HAS_OLED = True
|
|
except Exception:
|
|
_oled = None
|
|
HAS_OLED = False
|
|
|
|
# ---- Optional NFC (PN532 I2C) ----
|
|
try:
|
|
from pn532 import PN532_I2C
|
|
HAS_NFC = True
|
|
except Exception:
|
|
HAS_NFC = False
|
|
|
|
# ---- Optional sensor (DHT22 on GP15) ----
|
|
try:
|
|
import dht
|
|
_dht = dht.DHT22(Pin(15))
|
|
HAS_SENSOR = True
|
|
except Exception:
|
|
HAS_SENSOR = False
|
|
|
|
# ---- Optional relay (GP16) ----
|
|
try:
|
|
_relay = Pin(16, Pin.OUT)
|
|
HAS_RELAY = True
|
|
except Exception:
|
|
HAS_RELAY = False
|
|
|
|
CONFIG_PATH = 'config.json'
|
|
|
|
STATES = [
|
|
'BOOT',
|
|
'CAPABILITY_DETECT',
|
|
'CONFIG_CHECK',
|
|
'PROVISIONING',
|
|
'WIFI_CONNECT',
|
|
'CHECKIN',
|
|
'OPERATIONAL',
|
|
'ERROR',
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# Display helpers
|
|
# =============================================================================
|
|
|
|
def _show(lines):
|
|
if not HAS_OLED or _oled is None:
|
|
print(' | '.join(str(l) for l in lines))
|
|
return
|
|
_oled.fill(0)
|
|
for i, line in enumerate(lines[:5]):
|
|
_oled.text(str(line)[:16], 0, i * 12)
|
|
_oled.show()
|
|
|
|
|
|
def _show_state(state, detail=''):
|
|
_show(['Identity IoT', 'WIDE / AEL', '', state, detail])
|
|
|
|
|
|
# =============================================================================
|
|
# Capability detection
|
|
# =============================================================================
|
|
|
|
def detect_capabilities():
|
|
caps = {
|
|
'has_oled': HAS_OLED,
|
|
'has_nfc': HAS_NFC,
|
|
'has_sensor': HAS_SENSOR,
|
|
'has_relay': HAS_RELAY,
|
|
}
|
|
print('[caps]', caps)
|
|
return caps
|
|
|
|
|
|
# =============================================================================
|
|
# Config
|
|
# =============================================================================
|
|
|
|
def load_config():
|
|
try:
|
|
with open(CONFIG_PATH) as f:
|
|
return ujson.load(f)
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def save_config(cfg):
|
|
with open(CONFIG_PATH, 'w') as f:
|
|
ujson.dump(cfg, f)
|
|
|
|
|
|
def validate_config(cfg):
|
|
required = ['wifi_ssid', 'wifi_pass', 'comm_key', 'comm_iv',
|
|
'tenant_id', 'node_type', 'node_id',
|
|
'identity_iot_url', 'identity_tag_url']
|
|
for k in required:
|
|
if k not in cfg:
|
|
print('[config] missing key:', k)
|
|
return False
|
|
return True
|
|
|
|
|
|
# =============================================================================
|
|
# WiFi
|
|
# =============================================================================
|
|
|
|
def connect_wifi(cfg, timeout=30):
|
|
_show_state('WIFI_CONNECT', cfg['wifi_ssid'])
|
|
wlan = network.WLAN(network.STA_IF)
|
|
wlan.active(True)
|
|
utime.sleep(2)
|
|
|
|
if not wlan.isconnected():
|
|
wlan.connect(cfg['wifi_ssid'], cfg['wifi_pass'])
|
|
t = timeout
|
|
while t > 0:
|
|
if wlan.isconnected() and wlan.ifconfig()[0] != '0.0.0.0':
|
|
break
|
|
utime.sleep(1)
|
|
t -= 1
|
|
|
|
if not wlan.isconnected() or wlan.ifconfig()[0] == '0.0.0.0':
|
|
_show_state('ERROR', 'WiFi failed')
|
|
raise Exception('WiFi connect failed')
|
|
|
|
if cfg.get('wifi_static'):
|
|
wlan.ifconfig((
|
|
cfg['wifi_ip'],
|
|
cfg['wifi_mask'],
|
|
cfg['wifi_gw'],
|
|
cfg['wifi_dns'],
|
|
))
|
|
|
|
ip = wlan.ifconfig()[0]
|
|
print('[wifi] connected:', ip)
|
|
_show_state('WIFI_OK', ip)
|
|
utime.sleep(3)
|
|
return wlan
|
|
|
|
|
|
# =============================================================================
|
|
# Checkin
|
|
# =============================================================================
|
|
|
|
def do_checkin(cfg):
|
|
_show_state('CHECKIN', '')
|
|
from identity_iot import IdentityIoT
|
|
|
|
key = cfg['comm_key'].encode() if isinstance(cfg['comm_key'], str) else cfg['comm_key']
|
|
iv = cfg['comm_iv'].encode() if isinstance(cfg['comm_iv'], str) else cfg['comm_iv']
|
|
|
|
identity = IdentityIoT(
|
|
base_url = cfg['identity_iot_url'],
|
|
node_type = cfg.get('node_type', 'access_control'),
|
|
node_label = cfg.get('node_label', ''),
|
|
tenant_id = cfg['tenant_id'],
|
|
comm_key = key,
|
|
comm_iv = iv,
|
|
wifi_ssid = cfg['wifi_ssid'],
|
|
wifi_pass = cfg['wifi_pass'],
|
|
)
|
|
|
|
# Register — retry loop
|
|
for attempt in range(3):
|
|
try:
|
|
_show(['CHECKIN', 'Registering...', 'attempt ' + str(attempt+1) + '/3', '', ''])
|
|
identity.ensure_registered()
|
|
break
|
|
except Exception as e:
|
|
print('[checkin] register attempt', attempt + 1, ':', e)
|
|
if attempt == 2:
|
|
_show_state('ERROR', 'Check network')
|
|
raise
|
|
utime.sleep(3)
|
|
|
|
# Seconda chiamata idempotente
|
|
reg = identity.ensure_registered()
|
|
uid_short = reg.get('node_uid', '')[:12] + '...'
|
|
_show(['CHECKIN', 'Node ' + reg.get('result', ''), uid_short, '', ''])
|
|
utime.sleep(1)
|
|
|
|
# Authenticate → JWT
|
|
_show(['CHECKIN', 'Challenge...', '', '', ''])
|
|
utime.sleep(0.5)
|
|
_show(['CHECKIN', 'Signing...', '', 'Please wait', ''])
|
|
token = identity.authenticate()
|
|
|
|
if not token:
|
|
_show_state('ERROR', 'Auth failed')
|
|
raise Exception('authenticate returned no token')
|
|
|
|
print('[checkin] JWT issued')
|
|
|
|
# it_node_checkin su identity_tag con JWT
|
|
_show(['CHECKIN', 'Node checkin...', '', '', ''])
|
|
import urequests
|
|
from identity_iot_aes import aes_cbc_encrypt, aes_cbc_decrypt
|
|
|
|
payload = ujson.dumps({
|
|
'request': 'it_node_checkin',
|
|
'node_id': str(cfg.get('node_id', '')),
|
|
})
|
|
cipher = aes_cbc_encrypt(payload, key, iv)
|
|
body = ujson.dumps({'data': cipher})
|
|
|
|
for attempt in range(3):
|
|
try:
|
|
resp = urequests.post(
|
|
cfg['identity_tag_url'],
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + token,
|
|
},
|
|
data=body,
|
|
timeout=20,
|
|
)
|
|
raw = resp.text
|
|
resp.close()
|
|
outer = ujson.loads(raw)
|
|
clear = aes_cbc_decrypt(outer['data'], key, iv)
|
|
result = ujson.loads(clear)
|
|
if result.get('status') == 'ok':
|
|
print('[checkin] node checkin ok')
|
|
_show_state('CHECKIN OK', cfg.get('node_label', ''))
|
|
utime.sleep(1)
|
|
return {'status': 'ok', 'token': token, 'identity': identity}
|
|
raise Exception('it_node_checkin: ' + str(result.get('code', 'error')))
|
|
except Exception as e:
|
|
print('[checkin] it_node_checkin attempt', attempt + 1, ':', e)
|
|
if attempt == 2:
|
|
raise
|
|
utime.sleep(3)
|
|
|
|
raise Exception('it_node_checkin failed')
|
|
|
|
|
|
# =============================================================================
|
|
# Boot Manager — main entry point
|
|
# =============================================================================
|
|
|
|
def run():
|
|
state = 'BOOT'
|
|
|
|
while True:
|
|
|
|
# ------------------------------------------------------------------
|
|
if state == 'BOOT':
|
|
_show(['Identity IoT', 'WIDE / AEL', '', 'Booting...', ''])
|
|
utime.sleep(1)
|
|
state = 'CAPABILITY_DETECT'
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'CAPABILITY_DETECT':
|
|
caps = detect_capabilities()
|
|
_show([
|
|
'Identity IoT',
|
|
'NFC:' + ('Y' if caps['has_nfc'] else 'N') +
|
|
' SNS:' + ('Y' if caps['has_sensor'] else 'N') +
|
|
' RLY:' + ('Y' if caps['has_relay'] else 'N'),
|
|
'',
|
|
'Checking config...',
|
|
'',
|
|
])
|
|
utime.sleep(1)
|
|
state = 'CONFIG_CHECK'
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'CONFIG_CHECK':
|
|
cfg = load_config()
|
|
if cfg and validate_config(cfg):
|
|
print('[config] found and valid')
|
|
state = 'WIFI_CONNECT'
|
|
else:
|
|
print('[config] missing or invalid — provisioning mode')
|
|
state = 'PROVISIONING'
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'PROVISIONING':
|
|
if caps.get('has_nfc'):
|
|
from provisioning_nfc import run_provisioning_nfc
|
|
_show(['PROVISIONING', 'NFC mode', '', 'Tap writer badge', ''])
|
|
cfg = run_provisioning_nfc()
|
|
else:
|
|
from provisioning_server import run_provisioning
|
|
import network as _net
|
|
_wlan = _net.WLAN(_net.STA_IF)
|
|
_ip = _wlan.ifconfig()[0] if _wlan.isconnected() else '0.0.0.0'
|
|
_show(['PROVISIONING', 'IP: ' + _ip, 'HTTP:8080', 'UDP :5000', 'Waiting app...'])
|
|
cfg = run_provisioning()
|
|
save_config(cfg)
|
|
print('[provisioning] config saved — rebooting')
|
|
_show_state('CONFIG SAVED', 'Rebooting...')
|
|
utime.sleep(2)
|
|
machine.reset()
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'WIFI_CONNECT':
|
|
try:
|
|
connect_wifi(cfg)
|
|
state = 'CHECKIN'
|
|
except Exception as e:
|
|
print('[wifi] error:', e)
|
|
_show_state('ERROR', 'WiFi')
|
|
utime.sleep(10)
|
|
# retry
|
|
state = 'WIFI_CONNECT'
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'CHECKIN':
|
|
try:
|
|
import gc
|
|
gc.collect()
|
|
checkin_result = do_checkin(cfg)
|
|
identity = checkin_result.get('identity')
|
|
state = 'OPERATIONAL'
|
|
except Exception as e:
|
|
print('[checkin] error:', e)
|
|
_show_state('ERROR', 'Checkin')
|
|
utime.sleep(15)
|
|
state = 'CHECKIN'
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'OPERATIONAL':
|
|
node_type = cfg.get('node_type', 'sensor')
|
|
_show_state('OPERATIONAL', node_type)
|
|
|
|
if node_type == 'access_control':
|
|
from node_access_control import run_access_control
|
|
run_access_control(cfg, caps)
|
|
|
|
elif node_type == 'sensor':
|
|
from node_sensor import run_sensor
|
|
run_sensor(cfg, caps)
|
|
|
|
elif node_type == 'relay':
|
|
from node_relay import run_relay
|
|
run_relay(cfg, caps)
|
|
|
|
else:
|
|
print('[operational] unknown node_type:', node_type)
|
|
_show_state('ERROR', 'node_type?')
|
|
utime.sleep(30)
|
|
|
|
# ------------------------------------------------------------------
|
|
elif state == 'ERROR':
|
|
_show_state('ERROR', 'halted')
|
|
utime.sleep(60)
|
|
machine.reset() |