@@ -58,7 +58,7 @@ def __init__(self, name, **kwargs):
58
58
self .timestamp = timestamp ()
59
59
self .dtype = type (value ) # NOTE: must be set before calling super
60
60
callback = kwargs .pop ("callback" , self .senml_callback )
61
- for key in kwargs : # kwargs should be empty unless a wrong attr was used.
61
+ for key in kwargs : # kwargs should be empty by now, unless a wrong attr was used.
62
62
raise TypeError (f"'{ self .__class__ .__name__ } ' got an unexpected keyword argument '{ key } '" )
63
63
super ().__init__ (name , value = value , callback = callback )
64
64
@@ -97,11 +97,11 @@ def value(self, value):
97
97
if self .dtype is type (None ):
98
98
self .dtype = type (value )
99
99
elif not isinstance (value , self .dtype ):
100
- raise TypeError (f"task : { self .name } invalid data type. Expected { self .dtype } not { type (value )} " )
100
+ raise TypeError (f"record : { self .name } invalid data type. Expected { self .dtype } not { type (value )} " )
101
101
else :
102
102
self ._updated = True
103
103
self .timestamp = timestamp ()
104
- logging .debug (f"task : { self .name } %s: { value } ts: { self .timestamp } "
104
+ logging .debug (f"record : { self .name } %s: { value } ts: { self .timestamp } "
105
105
% ("initialized" if self .value is None else "updated" ))
106
106
self ._value = value
107
107
@@ -135,9 +135,8 @@ def add_to_pack(self, pack):
135
135
136
136
def senml_callback (self , record , ** kwargs ):
137
137
"""
138
- This is called after the record is updated from the senml pack.
139
- Sets updated flag to False, to avoid sending the same value back,
140
- and schedules on_write callback on the next run.
138
+ This is called after the record is updated from the cloud. Clear the updated flag to
139
+ avoid sending the same value back to the cloud, and schedule the on_write callback.
141
140
"""
142
141
self .updated = False
143
142
self .on_write_scheduled = True
@@ -153,18 +152,18 @@ async def run(self, aiot):
153
152
154
153
class AIOTClient ():
155
154
def __init__ (self , device_id , ssl_params = None , server = "mqtts-sa.iot.oniudra.cc" , port = 8883 , keepalive = 10 ):
156
- self .tasks = []
155
+ self .tasks = {}
157
156
self .records = {}
158
157
self .thing_id = None
159
158
self .keepalive = keepalive
160
159
self .update_systime ()
161
160
self .last_ping = timestamp ()
162
161
self .device_topic = b"/a/d/" + device_id + b"/e/i"
163
162
self .senmlpack = SenmlPack ("urn:uuid:" + device_id .decode ("utf-8" ), self .senml_generic_callback )
164
- self .mqtt_client = MQTTClient (device_id , server , port , ssl_params , keepalive = keepalive , callback = self .mqtt_callback )
165
- # Note: the following objects are initialized by the cloud.
166
- self . register ( "thing_id" , value = None , on_write = self . discovery_callback )
167
- self .register ("tz_offset" , value = None )
163
+ self .mqtt = MQTTClient (device_id , server , port , ssl_params , keepalive = keepalive , callback = self .mqtt_callback )
164
+ # Note: the following internal objects are initialized by the cloud.
165
+ for name in [ "thing_id" , "tz_offset" , "tz_dst_until" ]:
166
+ self .register (name , value = None )
168
167
169
168
def __getitem__ (self , key ):
170
169
if isinstance (self .records [key ].value , dict ):
@@ -192,13 +191,13 @@ def update_systime(self):
192
191
except Exception as e :
193
192
logging .error (f"Failed to set RTC time from NTP: { e } ." )
194
193
195
- def create_new_task (self , coro , * args , name = None ):
196
- if hasattr (asyncio .Task , "get_name" ):
197
- self .tasks .append (asyncio .create_task (coro (* args ), name = name ))
198
- else :
199
- self .tasks .append (asyncio .create_task (coro (* args )))
194
+ def create_task (self , name , coro , * args , ** kwargs ):
195
+ self .tasks [name ] = asyncio .create_task (coro (* args ))
200
196
logging .debug (f"task: { name } created." )
201
197
198
+ def create_topic (self , topic , inout ):
199
+ return bytes (f"/a/t/{ self .thing_id } /{ topic } /{ inout } " , "utf-8" )
200
+
202
201
def register (self , aiotobj , ** kwargs ):
203
202
if isinstance (aiotobj , str ):
204
203
if kwargs .get ("value" , None ) is None and kwargs .get ("on_read" , None ) is not None :
@@ -210,39 +209,21 @@ def register(self, aiotobj, **kwargs):
210
209
211
210
# Create a task for this object if it has any callbacks.
212
211
if aiotobj .runnable :
213
- self .create_new_task (aiotobj .run , self , name = aiotobj .name )
212
+ self .create_task (aiotobj .name , aiotobj .run , self )
214
213
215
214
# Check if object needs to be initialized from the cloud.
216
215
if not aiotobj .initialized and "r:m" not in self .records :
217
216
self .register ("r:m" , value = "getLastValues" )
218
217
219
218
def senml_generic_callback (self , record , ** kwargs ):
220
219
"""
221
- This callback catches all unknown/umatched records.
220
+ This callback catches all unknown/umatched records that were not part the pack .
222
221
"""
223
- rname , sname = record .name .split (":" ) if ":" in record .name else [record .name , "" ]
222
+ rname , sname = record .name .split (":" ) if ":" in record .name else [record .name , None ]
224
223
if rname in self .records :
225
224
logging .debug (f"Ignoring cloud initialization for record: { record .name } " )
226
225
else :
227
- logging .debug (f"Unkown record: { record .name } value: { record .value } " )
228
-
229
- def discovery_callback (self , aiot , thing_id ):
230
- logging .info (f"Device configured via discovery protocol." )
231
- if not thing_id :
232
- raise (Exception ("Device is not linked to a Thing ID." ))
233
- self .thing_id = bytes (thing_id , "utf-8" )
234
- self .topic_in = b"/a/t/" + self .thing_id + b"/e/i"
235
- self .topic_out = b"/a/t/" + self .thing_id + b"/e/o"
236
- logging .info (f"Subscribing to thing topic { self .topic_in } ." )
237
- self .mqtt_client .subscribe (self .topic_in )
238
-
239
- if lastval_record := self .records .pop ("r:m" , None ):
240
- shadow_in = b"/a/t/" + self .thing_id + b"/shadow/i"
241
- shadow_out = b"/a/t/" + self .thing_id + b"/shadow/o"
242
- lastval_record .add_to_pack (self .senmlpack )
243
- logging .info (f"Subscribing to shadow topic { shadow_in } ." )
244
- self .mqtt_client .subscribe (shadow_in )
245
- self .mqtt_client .publish (shadow_out , self .senmlpack .to_cbor (), qos = True )
226
+ logging .info (f"Unkown record found: { record .name } value: { record .value } " )
246
227
247
228
def mqtt_callback (self , topic , message ):
248
229
logging .debug (f"mqtt topic: { topic [- 8 :]} ... message: { message [:8 ]} ..." )
@@ -253,9 +234,27 @@ def mqtt_callback(self, topic, message):
253
234
self .senmlpack .from_cbor (message )
254
235
self .senmlpack .clear ()
255
236
237
+ async def discovery_task (self , interval = 0.100 ):
238
+ while self .thing_id is None :
239
+ self .mqtt .check_msg ()
240
+ if self .records .get ("thing_id" ).value is not None :
241
+ self .thing_id = self .records .pop ("thing_id" ).value
242
+ if not self .thing_id : # Empty thing ID should not happen.
243
+ raise (Exception ("Device is not linked to a Thing ID." ))
244
+
245
+ self .topic_out = self .create_topic ("e" , "o" )
246
+ self .mqtt .subscribe (self .create_topic ("e" , "i" ))
247
+
248
+ if lastval_record := self .records .pop ("r:m" , None ):
249
+ lastval_record .add_to_pack (self .senmlpack )
250
+ self .mqtt .subscribe (self .create_topic ("shadow" , "i" ))
251
+ self .mqtt .publish (self .create_topic ("shadow" , "o" ), self .senmlpack .to_cbor (), qos = True )
252
+ logging .info (f"Device configured via discovery protocol." )
253
+ await asyncio .sleep (interval )
254
+
256
255
async def mqtt_task (self , interval = 0.100 ):
257
256
while True :
258
- self .mqtt_client .check_msg ()
257
+ self .mqtt .check_msg ()
259
258
if self .thing_id is not None :
260
259
self .senmlpack .clear ()
261
260
for record in self .records .values ():
@@ -266,43 +265,40 @@ async def mqtt_task(self, interval=0.100):
266
265
if (self .debug ):
267
266
for record in self .senmlpack :
268
267
logging .debug (f" ==> record: { record .name } value: { str (record .value )[:48 ]} ..." )
269
- self .mqtt_client .publish (self .topic_out , self .senmlpack .to_cbor (), qos = True )
268
+ self .mqtt .publish (self .topic_out , self .senmlpack .to_cbor (), qos = True )
270
269
self .last_ping = timestamp ()
271
270
elif (self .keepalive and (timestamp () - self .last_ping ) > self .keepalive ):
272
- self .mqtt_client .ping ()
271
+ self .mqtt .ping ()
273
272
self .last_ping = timestamp ()
274
273
logging .debug ("No records to push, sent a ping request." )
275
274
await asyncio .sleep (interval )
276
275
277
276
async def run (self , user_main = None , debug = False ):
278
277
self .debug = debug
279
- if (user_main is not None ):
280
- # If user code is provided, append to tasks list.
281
- self .create_new_task (user_main , self , name = "user code" )
282
-
283
278
logging .info ("Connecting to AIoT cloud..." )
284
- if not self .mqtt_client .connect ():
279
+ if not self .mqtt .connect ():
285
280
logging .error ("Failed to connect AIoT cloud." )
286
281
return
287
282
288
- logging .info ("Subscribing to device topic." )
289
- self .mqtt_client .subscribe (self .device_topic )
290
- self .create_new_task (self .mqtt_task , name = "mqtt" )
283
+ self .mqtt .subscribe (self .device_topic )
284
+ if (user_main is not None ):
285
+ self .create_task ("user_main" , user_main , self )
286
+ self .create_task ("mqtt_task" , self .mqtt_task )
287
+ self .create_task ("discovery" , self .discovery_task )
291
288
292
289
while True :
293
290
try :
294
- await asyncio .gather (* self .tasks , return_exceptions = False )
291
+ await asyncio .gather (* self .tasks . values () , return_exceptions = False )
295
292
logging .info ("All tasks finished!" )
296
293
break
297
294
except Exception as e :
298
295
pass #import traceback; traceback.print_exc()
299
296
300
- for task in self .tasks :
297
+ for name in list (self .tasks ):
298
+ task = self .tasks [name ]
301
299
try :
302
300
if task .done ():
303
- self .tasks .remove (task )
304
- if hasattr (asyncio .Task , "get_name" ):
305
- logging .error (f"Removed task: { task .get_name ()} . Raised exception: { task .exception ()} ." )
306
- else :
307
- logging .error (f"Removed task." )
301
+ self .tasks .pop (name )
302
+ self .records .pop (name , None )
303
+ logging .error (f"Removed task: { name } . Raised exception: { task .exception ()} ." )
308
304
except (CancelledError , InvalidStateError ) as e : pass
0 commit comments