Skip to content

Commit bf1844b

Browse files
authored
Merge pull request #82 from arduino/sync_updates
misc: Sync mode and workflow updates.
2 parents a8bf94c + 14a531b commit bf1844b

File tree

3 files changed

+77
-34
lines changed

3 files changed

+77
-34
lines changed

.github/workflows/client-test.yml

+20-4
Original file line numberDiff line numberDiff line change
@@ -71,34 +71,50 @@ jobs:
7171
run: |
7272
source tests/ci.sh && ci_configure_softhsm
7373
74-
- name: '☁️ Connect to IoT cloud (CPython / Basic Auth)'
74+
- name: '☁️ Connect to IoT cloud (CPython / Basic Auth / Async)'
7575
env:
7676
DEVICE_ID: ${{ secrets.DEVICE_ID1 }}
7777
SECRET_KEY: ${{ secrets.SECRET_KEY }}
7878
run: |
7979
python tests/ci.py --basic-auth
8080
81-
- name: '☁️ Connect to IoT cloud (CPython / Key/Cert Auth)'
81+
- name: '☁️ Connect to IoT cloud (CPython / Basic Auth / Sync)'
82+
env:
83+
DEVICE_ID: ${{ secrets.DEVICE_ID1 }}
84+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
85+
run: |
86+
python tests/ci.py --basic-auth --sync
87+
88+
- name: '☁️ Connect to IoT cloud (CPython / Key-Cert Auth / Async)'
8289
env:
8390
DEVICE_ID: ${{ secrets.DEVICE_ID2 }}
8491
SECRET_KEY: ${{ secrets.SECRET_KEY }}
8592
run: |
8693
python tests/ci.py --file-auth
8794
8895
89-
- name: '☁️ Connect to IoT cloud (CPython / Crypto Auth)'
96+
- name: '☁️ Connect to IoT cloud (CPython / Crypto Auth / Async)'
9097
env:
9198
DEVICE_ID: ${{ secrets.DEVICE_ID2 }}
9299
SECRET_KEY: ${{ secrets.SECRET_KEY }}
93100
run: |
94101
export SOFTHSM2_CONF="${HOME}/softhsm/tokens/softhsm2.conf"
95102
python tests/ci.py --crypto-device
96103
97-
- name: '☁️ Connect to IoT cloud (MicroPython / Basic Auth)'
104+
- name: '☁️ Connect to IoT cloud (MicroPython / Basic Auth / Async)'
98105
env:
99106
DEVICE_ID: ${{ secrets.DEVICE_ID1 }}
100107
SECRET_KEY: ${{ secrets.SECRET_KEY }}
101108
run: |
102109
export PATH="${HOME}/cache/bin:${PATH}"
103110
micropython -c "import sys; print(sys.path)"
104111
micropython tests/ci.py --basic-auth
112+
113+
- name: '☁️ Connect to IoT cloud (MicroPython / Basic Auth / Sync)'
114+
env:
115+
DEVICE_ID: ${{ secrets.DEVICE_ID1 }}
116+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
117+
run: |
118+
export PATH="${HOME}/cache/bin:${PATH}"
119+
micropython -c "import sys; print(sys.path)"
120+
micropython tests/ci.py --basic-auth --sync

src/arduino_iot_cloud/ucloud.py

+36-27
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def __init__(self, name, **kwargs):
6363
self._updated = False
6464
self.on_write_scheduled = False
6565
self.timestamp = timestamp()
66-
self.last_run = timestamp_ms()
66+
self.last_poll = timestamp_ms()
6767
self.runnable = any((self.on_run, self.on_read, self.on_write))
6868
callback = kwargs.pop("callback", self.senml_callback)
6969
for key in kwargs: # kwargs should be empty by now, unless a wrong attr was used.
@@ -193,7 +193,6 @@ def __init__(
193193
self.thing_id = None
194194
self.keepalive = keepalive
195195
self.last_ping = timestamp()
196-
self.last_run = timestamp()
197196
self.senmlpack = SenmlPack("", self.senml_generic_callback)
198197
self.ntp_server = ntp_server
199198
self.ntp_timeout = ntp_timeout
@@ -322,8 +321,20 @@ def mqtt_callback(self, topic, message):
322321
self.senmlpack.from_cbor(message)
323322
self.senmlpack.clear()
324323

325-
def ts_expired(self, record, ts):
326-
return (ts - record.last_run) > int(record.interval * 1000)
324+
def ts_expired(self, ts, last_ts_ms, interval_s):
325+
return last_ts_ms == 0 or (ts - last_ts_ms) > int(interval_s * 1000)
326+
327+
def poll_records(self):
328+
ts = timestamp_ms()
329+
try:
330+
for record in self.records.values():
331+
if record.runnable and self.ts_expired(ts, record.last_poll, record.interval):
332+
record.run_sync(self)
333+
record.last_poll = ts
334+
except Exception as e:
335+
self.records.pop(record.name)
336+
if log_level_enabled(logging.ERROR):
337+
logging.error(f"task: {record.name} raised exception: {str(e)}.")
327338

328339
def poll_connect(self, aiot=None):
329340
logging.info("Connecting to Arduino IoT cloud...")
@@ -332,7 +343,7 @@ def poll_connect(self, aiot=None):
332343
except Exception as e:
333344
if log_level_enabled(logging.WARNING):
334345
logging.warning(f"Connection failed {e}, retrying...")
335-
return False
346+
return
336347

337348
if self.thing_id is None:
338349
self.mqtt.subscribe(self.device_topic, qos=1)
@@ -341,10 +352,10 @@ def poll_connect(self, aiot=None):
341352

342353
if self.async_mode:
343354
if self.thing_id is None:
344-
self.register("discovery", on_run=self.poll_discovery, interval=0.100)
355+
self.register("discovery", on_run=self.poll_discovery, interval=0.200)
345356
self.register("mqtt_task", on_run=self.poll_mqtt, interval=0.100)
346357
raise DoneException()
347-
return True
358+
self.connected = True
348359

349360
def poll_discovery(self, aiot=None):
350361
self.mqtt.check_msg()
@@ -429,17 +440,26 @@ async def run(self, interval, backoff):
429440
def start(self, interval=1.0, backoff=1.2):
430441
if self.async_mode:
431442
asyncio.run(self.run(interval, backoff))
432-
else:
433-
# Synchronous mode.
434-
while not self.poll_connect():
435-
time.sleep(interval)
436-
interval = min(interval * backoff, 5.0)
443+
return
437444

438-
while self.thing_id is None:
445+
last_conn_ms = 0
446+
last_disc_ms = 0
447+
448+
while True:
449+
ts = timestamp_ms()
450+
if not self.connected and self.ts_expired(ts, last_conn_ms, interval):
451+
self.poll_connect()
452+
if last_conn_ms != 0:
453+
interval = min(interval * backoff, 5.0)
454+
last_conn_ms = ts
455+
456+
if self.connected and self.thing_id is None and self.ts_expired(ts, last_disc_ms, 0.250):
439457
self.poll_discovery()
440-
time.sleep(0.100)
458+
last_disc_ms = ts
441459

442-
self.connected = True
460+
if self.connected and self.thing_id is not None:
461+
break
462+
self.poll_records()
443463

444464
def update(self):
445465
if self.async_mode:
@@ -448,25 +468,14 @@ def update(self):
448468
if not self.connected:
449469
try:
450470
self.start()
451-
self.connected = True
452471
except Exception as e:
453472
raise e
454473

455-
try:
456-
ts = timestamp_ms()
457-
for record in self.records.values():
458-
if record.runnable and self.ts_expired(record, ts):
459-
record.run_sync(self)
460-
record.last_run = ts
461-
except Exception as e:
462-
self.records.pop(record.name)
463-
if log_level_enabled(logging.ERROR):
464-
logging.error(f"task: {record.name} raised exception: {str(e)}.")
474+
self.poll_records()
465475

466476
try:
467477
self.poll_mqtt()
468478
except Exception as e:
469479
self.connected = False
470480
if log_level_enabled(logging.WARNING):
471481
logging.warning(f"Connection lost {e}")
472-
raise e

tests/ci.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
# https://creativecommons.org/publicdomain/zero/1.0/
44
import logging
55
import os
6+
import time
67
import sys
78
import asyncio
89
from arduino_iot_cloud import ArduinoCloudClient
10+
from arduino_iot_cloud import Task
911
import argparse
1012

1113

@@ -20,6 +22,16 @@ def on_value_changed(client, value):
2022
sys.exit(0)
2123

2224

25+
def wdt_task(client, ts=[None]):
26+
if ts[0] is None:
27+
ts[0] = time.time()
28+
if time.time() - ts[0] > 5:
29+
loop = asyncio.get_event_loop()
30+
loop.set_exception_handler(exception_handler)
31+
logging.error("Timeout waiting for variable")
32+
sys.exit(1)
33+
34+
2335
if __name__ == "__main__":
2436
# Parse command line args.
2537
parser = argparse.ArgumentParser(description="arduino_iot_cloud.py")
@@ -35,6 +47,9 @@ def on_value_changed(client, value):
3547
parser.add_argument(
3648
"-f", "--file-auth", action="store_true", help="Use key/cert files"
3749
)
50+
parser.add_argument(
51+
"-s", "--sync", action="store_true", help="Run in synchronous mode"
52+
)
3853
args = parser.parse_args()
3954

4055
# Configure the logger.
@@ -56,6 +71,7 @@ def on_value_changed(client, value):
5671
device_id=os.getenv("DEVICE_ID"),
5772
username=os.getenv("DEVICE_ID"),
5873
password=os.getenv("SECRET_KEY"),
74+
sync_mode=args.sync,
5975
)
6076
elif args.file_auth:
6177
import ssl
@@ -67,6 +83,7 @@ def on_value_changed(client, value):
6783
"ca_certs": "ca-root.pem",
6884
"cert_reqs": ssl.CERT_REQUIRED,
6985
},
86+
sync_mode=args.sync,
7087
)
7188
elif args.crypto_device:
7289
import ssl
@@ -82,16 +99,17 @@ def on_value_changed(client, value):
8299
"engine_path": "/lib/x86_64-linux-gnu/engines-3/libpkcs11.so",
83100
"module_path": "/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so",
84101
},
102+
sync_mode=args.sync,
85103
)
86104
else:
87105
parser.print_help()
88106
sys.exit(1)
89107

90108
# Register cloud objects.
91-
# Note: The following objects must be created first in the dashboard and linked to the device.
92-
# This cloud object is initialized with its last known value from the cloud. When this object is updated
93-
# from the dashboard, the on_switch_changed function is called with the client object and the new value.
109+
# When this object gets initialized from the cloud the test is complete.
94110
client.register("answer", value=None, on_write=on_value_changed)
111+
# This task will exist with failure after a timeout.
112+
client.register(Task("wdt_task", on_run=wdt_task, interval=1.0))
95113

96114
# Start the Arduino IoT cloud client.
97115
client.start()

0 commit comments

Comments
 (0)