Identity IoT: initial release — boot manager, provisioning, sensor/relay
This commit is contained in:
353
boot_manager.py
Normal file
353
boot_manager.py
Normal file
@@ -0,0 +1,353 @@
|
||||
# ============================================================+
|
||||
# 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
|
||||
|
||||
# Hardware instances — initialised in detect_capabilities() after config load
|
||||
_oled = None
|
||||
_nfc = None
|
||||
_dht = None
|
||||
_relay = None
|
||||
HAS_OLED = False
|
||||
HAS_NFC = False
|
||||
HAS_SENSOR = False
|
||||
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(cfg=None):
|
||||
global _oled, _nfc, _dht, _relay
|
||||
global HAS_OLED, HAS_NFC, HAS_SENSOR, HAS_RELAY
|
||||
|
||||
pinout = cfg.get('pinout', {}) if cfg else {}
|
||||
i2c0_sda = pinout.get('i2c0_sda', 4)
|
||||
i2c0_scl = pinout.get('i2c0_scl', 5)
|
||||
i2c1_sda = pinout.get('i2c1_sda', 6)
|
||||
i2c1_scl = pinout.get('i2c1_scl', 7)
|
||||
dht_pin = pinout.get('dht_pin', 15)
|
||||
relay_pin = pinout.get('relay_pin', 16)
|
||||
|
||||
# OLED
|
||||
try:
|
||||
from ssd1306 import SSD1306_I2C
|
||||
_i2c0 = I2C(0, sda=Pin(i2c0_sda), scl=Pin(i2c0_scl))
|
||||
_oled = SSD1306_I2C(128, 64, _i2c0)
|
||||
HAS_OLED = True
|
||||
except Exception:
|
||||
_oled = None
|
||||
HAS_OLED = False
|
||||
|
||||
# NFC (PN532 as provisioning dongle only — not present in normal operation)
|
||||
try:
|
||||
from pn532 import PN532_I2C
|
||||
_i2c1 = I2C(1, sda=Pin(i2c1_sda), scl=Pin(i2c1_scl))
|
||||
_nfc = PN532_I2C(_i2c1)
|
||||
HAS_NFC = True
|
||||
except Exception as e:
|
||||
print('[nfc] not detected:', e)
|
||||
_nfc = None
|
||||
HAS_NFC = False
|
||||
|
||||
# Sensor (DHT22)
|
||||
try:
|
||||
import dht
|
||||
_dht = dht.DHT22(Pin(dht_pin))
|
||||
HAS_SENSOR = True
|
||||
except Exception:
|
||||
_dht = None
|
||||
HAS_SENSOR = False
|
||||
|
||||
# Relay
|
||||
try:
|
||||
_relay = Pin(relay_pin, Pin.OUT)
|
||||
HAS_RELAY = True
|
||||
except Exception:
|
||||
_relay = None
|
||||
HAS_RELAY = False
|
||||
|
||||
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']
|
||||
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', 'sensor'),
|
||||
node_label = cfg.get('node_label', ''),
|
||||
tenant_id = cfg['tenant_id'],
|
||||
node_id = cfg['node_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', '', ''])
|
||||
reg = 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)
|
||||
|
||||
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')
|
||||
_show_state('CHECKIN OK', cfg.get('node_label', ''))
|
||||
utime.sleep(1)
|
||||
|
||||
return {'status': 'ok', 'token': token, 'identity': identity}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Boot Manager — main entry point
|
||||
# =============================================================================
|
||||
|
||||
def run():
|
||||
state = 'BOOT'
|
||||
cfg = None
|
||||
caps = {'has_oled': False, 'has_nfc': False, 'has_sensor': False, 'has_relay': False}
|
||||
|
||||
while True:
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
if state == 'BOOT':
|
||||
_show(['Identity IoT', 'WIDE / AEL', '', 'Booting...', ''])
|
||||
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 = 'CAPABILITY_DETECT'
|
||||
else:
|
||||
print('[config] missing or invalid — provisioning mode')
|
||||
caps = detect_capabilities()
|
||||
state = 'PROVISIONING'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
elif state == 'CAPABILITY_DETECT':
|
||||
caps = detect_capabilities(cfg)
|
||||
_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'),
|
||||
'',
|
||||
'READY',
|
||||
'',
|
||||
])
|
||||
utime.sleep(1)
|
||||
state = 'WIFI_CONNECT'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
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(_nfc)
|
||||
else:
|
||||
from provisioning_server import run_provisioning
|
||||
wlan_sta = network.WLAN(network.STA_IF)
|
||||
_ip = wlan_sta.ifconfig()[0] if wlan_sta.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)
|
||||
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 == '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()
|
||||
Reference in New Issue
Block a user