55__author__ = "Jordan Yates"
66__copyright__ = "Copyright 2024, Embeint Holdings Pty Ltd"
77
8+ import glob
89import sys
910from typing import Any
11+ from uuid import UUID
1012
1113from tabulate import tabulate
1214
1315import infuse_iot .api_client .models as models
1416from infuse_iot .api_client import Client
17+ from infuse_iot .api_client .api .application import (
18+ create_application ,
19+ create_release ,
20+ get_application_by_organisation_id_and_application_id ,
21+ get_applications_by_organisation_id ,
22+ get_releases_by_organisation_id_and_application_id ,
23+ )
1524from infuse_iot .api_client .api .board import (
1625 create_board ,
1726 get_board_by_id ,
3039 get_all_organisations ,
3140 get_organisation_by_id ,
3241)
33- from infuse_iot .api_client .models import COAPFilesList , Error , NewBoard , NewOrganisation
34- from infuse_iot .api_client .types import Unset
42+ from infuse_iot .api_client .types import File , Unset
3543from infuse_iot .commands import InfuseCommand
3644from infuse_iot .credentials import get_api_key
45+ from infuse_iot .util .argparse import ValidRelease
46+ from infuse_iot .util .console import choose_one , user_confirm , user_response
47+ from infuse_iot .util .version import Version
3748
3849
3950class CloudSubCommand :
@@ -322,6 +333,188 @@ def list(self, client: Client):
322333 print ("\t " + "\n \t " .join (sorted_list ))
323334
324335
336+ class Applications (CloudSubCommand ):
337+ @classmethod
338+ def add_parser (cls , parser ):
339+ parser_coap = parser .add_parser ("apps" , help = "Application release management" )
340+ parser_coap .set_defaults (command_class = cls )
341+
342+ tool_parser = parser_coap .add_subparsers (title = "commands" , metavar = "<command>" , required = True )
343+
344+ list_parser = tool_parser .add_parser ("list" , help = "List all application releases" )
345+ list_parser .add_argument ("--org" , "-o" , type = str , required = True , help = "Organisation ID" )
346+ list_parser .set_defaults (command_fn = cls .list )
347+
348+ info_parser = tool_parser .add_parser ("info" , help = "Display summary of application releases" )
349+ info_parser .add_argument ("--org" , "-o" , type = str , required = True , help = "Organisation ID" )
350+ info_parser .add_argument ("--app" , "-a" , type = lambda x : int (x , 16 ), required = True , help = "Application ID (hex)" )
351+ info_parser .set_defaults (command_fn = cls .info )
352+
353+ upload_parser = tool_parser .add_parser ("upload" , help = "Upload application release" )
354+ upload_parser .add_argument ("--org" , "-o" , type = str , help = "Organisation ID" )
355+ upload_parser .add_argument ("--board" , "-b" , type = str , help = "Board ID" )
356+ upload_parser .add_argument ("--release" , "-r" , type = ValidRelease , required = True , help = "Release to upload" )
357+ upload_parser .set_defaults (command_fn = cls .upload )
358+
359+ def run (self ):
360+ with self .client () as client :
361+ self .args .command_fn (self , client )
362+
363+ def list (self , client : Client ):
364+ applications = get_applications_by_organisation_id .sync (client = client , id = UUID (self .args .org ))
365+
366+ if not isinstance (applications , list ):
367+ print (f"Failed to retrieve application list { applications } " )
368+ return
369+
370+ app_list = []
371+ for app in applications :
372+ app_list .append (
373+ [
374+ f"0x{ app .id :08X} " ,
375+ app .name ,
376+ app .description ,
377+ ]
378+ )
379+ print (
380+ tabulate (
381+ app_list ,
382+ headers = ["ID" , "Name" , "Description" ],
383+ )
384+ )
385+
386+ def info (self , client : Client ):
387+ releases = get_releases_by_organisation_id_and_application_id .sync (
388+ client = client , id = UUID (self .args .org ), application_id = self .args .app
389+ )
390+
391+ if releases is None :
392+ sys .exit ("Failed to retrieve release list (No response)" )
393+ elif isinstance (releases , models .Error ):
394+ sys .exit (f"<{ releases .code } >: { releases .message } " )
395+
396+ release_list = []
397+ for release in releases :
398+ version = release .version
399+ version_str = f"{ version .major } .{ version .minor } .{ version .revision } +{ version .build_num :08x} "
400+ release_list .append (
401+ [
402+ f"{ release .board_target } " ,
403+ version_str ,
404+ f"{ release .id } " ,
405+ f"{ release .file .len_ / 1024 :.2f} kB" ,
406+ str (release .created_at ),
407+ ]
408+ )
409+ print (
410+ tabulate (
411+ release_list ,
412+ headers = ["Board Target" , "Version" , "ID" , "Full OTA" , "Created" ],
413+ )
414+ )
415+
416+ def upload (self , client : Client ):
417+ try :
418+ self ._board = UUID (self .args .board ) if self .args .board else None
419+ except ValueError :
420+ sys .exit (f"Board ID: '{ self .args .board } ' is not a valid UUID" )
421+ try :
422+ self ._org = UUID (self .args .org ) if self .args .org else None
423+ except ValueError :
424+ sys .exit (f"Organisation ID: '{ self .args .org } ' is not a valid UUID" )
425+
426+ release : ValidRelease = self .args .release
427+ release_app_meta = release .metadata ["application" ]
428+ name = release_app_meta ["primary" ]
429+ app_id = release_app_meta ["id" ]
430+ board_target = release_app_meta ["board" ]
431+ version = Version .from_string (release_app_meta ["version" ])
432+
433+ if self ._org is None :
434+ orgs = get_all_organisations .sync (client = client )
435+ if isinstance (orgs , models .Error ) or orgs is None :
436+ sys .exit (f"Organisation query failed { orgs } " )
437+ options = [f"{ o .name :20s} ({ o .id } )" for o in orgs ]
438+
439+ idx , _val = choose_one ("Organisation" , options )
440+ self ._org = orgs [idx ].id
441+ self ._org_name = orgs [idx ].name
442+ else :
443+ org = get_organisation_by_id .sync (client = client , id = self ._org )
444+ if not isinstance (org , models .Organisation ):
445+ sys .exit (f"Failed to query org for ID { self ._org } " )
446+ self ._org_name = org .name
447+
448+ if self ._board is None :
449+ boards = get_boards .sync (client = client , organisation_id = self ._org )
450+ if isinstance (boards , models .Error ) or boards is None :
451+ sys .exit (f"Board query failed { boards } " )
452+ options = [f"{ b .name :20s} ({ b .id } )" for b in boards ]
453+
454+ idx , _val = choose_one ("Board" , options )
455+ self ._board = boards [idx ].id
456+ self ._board_name = boards [idx ].name
457+ else :
458+ board = get_board_by_id .sync (client = client , id = self ._board )
459+ if not isinstance (org , models .Board ):
460+ sys .exit (f"Failed to query board for ID { self ._board } " )
461+ self ._board_name = board .name
462+
463+ application = get_application_by_organisation_id_and_application_id .sync (
464+ client = client , id = self ._org , application_id = app_id
465+ )
466+
467+ if application is None :
468+ dialog = f"Application 0x{ app_id :08x} does not exist in organisation { self .args .org } , create?"
469+ if not user_confirm (dialog ):
470+ return
471+ print (f"Creating application 0x{ app_id :08x} in organisation { self .args .org } " )
472+ description = user_response ("Application description:" )
473+ body = models .NewApplication (id = app_id , name = name , description = description )
474+ application = create_application .sync (client = client , id = self ._org , body = body )
475+
476+ if not isinstance (application , models .Application ):
477+ sys .exit (f"Unexpected internal type { type (application )} " )
478+
479+ ota_files = glob .glob (str (release .dir / "ota-*.bin" ))
480+ if len (ota_files ) != 1 :
481+ sys .exit (f"Unexpected OTA file search result { ota_files } " )
482+
483+ dialog = (
484+ f"Create release for application '0x{ app_id :08x} { str (version )} '"
485+ + f" in organisation '{ self ._org_name } ' for board '{ self ._board_name } '?"
486+ )
487+ if not user_confirm (dialog ):
488+ return
489+
490+ with open (ota_files [0 ], "rb" ) as f :
491+ ota_file = File (f , ota_files [0 ], None )
492+
493+ release_obj = models .CreateReleaseBody (
494+ file = ota_file ,
495+ file_diff_len = str (0 ),
496+ version_major = str (version .major ),
497+ version_minor = str (version .minor ),
498+ version_revision = str (version .revision ),
499+ version_build_num = str (version .build_num ),
500+ board_id = self ._board ,
501+ board_target = board_target ,
502+ )
503+
504+ rsp = create_release .sync (
505+ client = client ,
506+ id = self ._org ,
507+ application_id = app_id ,
508+ body = release_obj ,
509+ )
510+ if rsp is None :
511+ sys .exit ("Create release: No response" )
512+ elif isinstance (rsp , models .Error ):
513+ sys .exit (f"<{ rsp .code } >: { rsp .message } " )
514+ else :
515+ print (f"Release created with ID '{ rsp .id } '" )
516+
517+
325518class SubCommand (InfuseCommand ):
326519 NAME = "cloud"
327520 HELP = "Infuse-IoT cloud interaction"
@@ -335,6 +528,7 @@ def add_parser(cls, parser):
335528 Boards .add_parser (subparser )
336529 Device .add_parser (subparser )
337530 Coap .add_parser (subparser )
531+ Applications .add_parser (subparser )
338532
339533 def __init__ (self , args ):
340534 self .tool = args .command_class (args )
0 commit comments