-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhexpansion_mgr.py
More file actions
1452 lines (1289 loc) · 73.7 KB
/
Copy pathhexpansion_mgr.py
File metadata and controls
1452 lines (1289 loc) · 73.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
""" Hexpansion & EEPROM Management Module for BadgeBot """
#
# Handles detection, initialisation, programming, upgrading and erasure of
# HexDrive / HexSense hexpansion EEPROMs.
#
# Public interface (called by the main app):
# __init__(app) – wire up to the main BadgeBotApp instance
# register_events() – register hexpansion insertion/removal handlers on eventbus
# unregister_events() – unregister hexpansion event handlers
# update(delta) – per-tick hexpansion state-machine update
# draw(ctx) – render hexpansion-related UI states
import os
import sys
import time
import vfs
from app_components.notification import Notification
from app_components.tokens import label_font_size, button_labels
from events.input import BUTTON_TYPES
from machine import I2C
from system.eventbus import eventbus
from system.hexpansion import app as hexpansion_app
from system.hexpansion.events import HexpansionInsertionEvent, HexpansionRemovalEvent
from system.hexpansion.header import HexpansionHeader, write_header
from system.hexpansion.util import get_hexpansion_block_devices, detect_eeprom_addr
from system.scheduler import scheduler
_SLOTS = 6
# HexDrive Hexpansion constants
# EEPROM Constants
_DEFAULT_EEPROM_PAGE_SIZE = 32
_DEFAULT_EEPROM_TOTAL_SIZE = 64 * 1024 // 8
_IS_SIMULATOR = sys.platform != "esp32"
# Local sub-states (internal to Hexpansion Mgr)
_SUB_INIT = 0 # Initial state on app startup, before first port scan
_SUB_CHECK = 1 # Checks for EEPROMs and HexDrives
_SUB_DETECTED = 2 # Hexpansion detected & ready for EEPROM initialisation
_SUB_ERASE_CONFIRM = 3 # Confirmation prompt for EEPROM erase
_SUB_ERASE = 4 # Hexpansion EEPROM erasing in progress
_SUB_UPGRADE_CONFIRM = 5 # Hexpansion ready for App upgrade
_SUB_PROGRAMMING = 6 # Hexpansion EEPROM programming (Initialsation or Upgrade) in progress
_SUB_PORT_SELECT = 7 # User selecting which hexpansion to erase (if multiple) in order to free up a slot for initialisation or upgrade
_SUB_DONE = 8 # Final state after successful initialisation or upgrade, before returning to menu
_SUB_EXIT = 9 # State for exiting from interactive mode back to menu)
# Hexpansion states
# unknown, blank, unrecognised, recognised but no app, recognised with app
_HEXPANSION_STATE_UNKNOWN = 0
_HEXPANSION_STATE_EMPTY = 1
_HEXPANSION_STATE_FAULTY = 2
_HEXPANSION_STATE_BLANK = 3
_HEXPANSION_STATE_UNRECOGNISED = 4
_HEXPANSION_STATE_RECOGNISED = 5
_HEXPANSION_STATE_RECOGNISED_NO_APP = 6
_HEXPANSION_STATE_RECOGNISED_OLD_APP = 7
_HEXPANSION_STATE_RECOGNISED_APP_OK = 8
_HEXPANSION_STATE_NAMES = [
"Unknown",
"Empty",
"Faulty",
"Blank",
"Unrecognised",
"Recognised",
"No App",
"Old App",
"App OK"
]
# EEPROM app programming outcomes
_APP_EEPROM_RESULT_FAILURE = 0
_APP_EEPROM_RESULT_MISSING = -1
_APP_EEPROM_RESULT_SUCCESSFUL_UPGRADE = 1
_APP_EEPROM_RESULT_SUCCESSFUL_WRITE = 2
_HEXDRIVE_REQUIRED_MESSAGE = ["Requires:","RobotMad HexDrive","github.com","/TeamRobotmad","/BadgeBot"]
_HEXDRIVE_REQUIRED_MESSAGE_COLOURS = [(1,1,0),(1,1,0),(0,1,1),(0,1,1),(0,1,1)]
# Modes
_MODE_INIT = 0 # Initial mode on app startup to do first scan of all ports
_MODE_IDLE = 1 # Idle mode with no hexpansion-related activity, waiting for events
_MODE_UPDATE = 2 # Normal mode for responding to hexpansion-related events (insertion/removal)
_MODE_INTERACTIVE = 3 # Interactive mode for user interactions for initialisation/upgrade/erasure of hexpansions
# WHEN Badge hexpansion handling has stabillised - revisit this and make much much simpler...
_SINGLE_PORT_HEXPANSION_REFS = (
("hexsense_port", "HexSense", "HEXSENSE_HEXPANSION_INDEX"),
("hexdiag_port", "HexDiag", "HEXDIAG_HEXPANSION_INDEX"),
("hexaudio_port", "HexAudio", "HEXAUDIO_HEXPANSION_INDEX"),
)
# ---- Settings initialisation -----------------------------------------------
def init_settings(s, MySetting): # pylint: disable=unused-argument, invalid-name
"""Register hexpansion-management-specific settings in the shared settings dict."""
return
# ---- Hexpansion management -------------------------------------------------
class HexpansionMgr:
"""Manages hexpansion detection, EEPROM programming, and upgrades.
Parameters
----------
app : BadgeBotApp
Reference to the main application instance so that shared state
(settings, hexdrive_apps, current_state …) can be
read and written.
"""
# Sub-states are defined at module level (_SUB_*); app-level state
# routing is handled by the dispatch tables in app.py.
def __init__(self, app, logging: bool = False):
self._app = app
self._logging: bool = logging
self._mode: int = _MODE_INIT
self._sub_state: int = _SUB_INIT
self._prev_state: int = _SUB_INIT
self._port_selected: int = 0
self._port_selected_header: HexpansionHeader | None = None # HexpansionHeader for selected port
self._port_detail_page: int = 0 # 0=vid/pid, 1=eeprom, 2=details (conditional)
self._port_detail_page_count: int = 2 # 2 or 3 depending on whether details page is available
self._hexpansion_app_startup_timer: int = 0
self._hexpansion_type_by_slot: list[int | None] = [None]*_SLOTS
self._hexpansion_state_by_slot: list[int] = [_HEXPANSION_STATE_UNKNOWN]*_SLOTS
self._hexpansion_eeprom_addr_len: list[int | None] = [None]*_SLOTS
self._hexpansion_eeprom_addr: list[int | None] = [None]*_SLOTS
self._hexpansion_init_type: int = 0
self._detected_port: int | None = None
self._waiting_app_port: int | None = None
self._erase_port: int | None = None
self._upgrade_port: int | None = None
self._ports_to_initialise: set[int] = set() # ports with blank EEPROM which could be initialised
self._ports_to_check_app: set[int] = set() # ports with recognised hexpansion type which should be checked for the correct app before being used
self._reboop_required: bool = False
self._hexpansion_serial_number: int | None = None
self._message_being_shown: bool = False
if self._logging:
print("HexpansionMgr initialised")
# ------------------------------------------------------------------
def register_events(self):
"""Register hexpansion insertion/removal event handlers directly."""
eventbus.on_async(HexpansionInsertionEvent, self._handle_insertion, self._app)
eventbus.on_async(HexpansionRemovalEvent, self._handle_removal, self._app)
def unregister_events(self):
"""Unregister hexpansion event handlers."""
eventbus.remove(HexpansionInsertionEvent, self._handle_insertion, self._app)
eventbus.remove(HexpansionRemovalEvent, self._handle_removal, self._app)
# ------------------------------------------------------------------
@property
def logging(self) -> bool:
"""Get the current logging state."""
return self._logging
@logging.setter
def logging(self, value: bool):
"""Set the logging state."""
self._logging = value
def _clear_single_port_hexpansion_refs(self, port: int | None):
"""Clear app references for single-port hexpansions assigned to *port*."""
if port is None:
return
app = self._app
for attr_name, display_name, _ in _SINGLE_PORT_HEXPANSION_REFS:
if getattr(app, attr_name, None) == port:
setattr(app, attr_name, None)
if self._logging:
print(f"H:{display_name} on port {port} erased!")
def _has_single_port_hexpansion_on_port(self, port: int) -> bool:
"""Return True when a tracked single-port hexpansion is assigned to *port*."""
app = self._app
for attr_name, _, _ in _SINGLE_PORT_HEXPANSION_REFS:
if getattr(app, attr_name, None) == port:
print(f"H:Found hexpansion {attr_name} on port {port}")
return True
return False
def _refresh_single_port_hexpansion_assignments(self):
"""Update tracked single-port hexpansion ports and refresh dependent state."""
app = self._app
previous_ports = {}
print("H:Refreshing hexpansion assignments...")
for attr_name, _, type_index_attr in _SINGLE_PORT_HEXPANSION_REFS:
previous_port = getattr(app, attr_name, None)
previous_ports[attr_name] = previous_port
type_index = getattr(app, type_index_attr)
current_port, _ = self._check_hexpansion(previous_port, type_index)
setattr(app, attr_name, current_port)
if previous_ports["hexdiag_port"] != app.hexdiag_port:
app.hexdiag_setup()
def _should_claim_single_port_hexpansion(self, type_index: int) -> bool:
"""Return True if a detected single-port hexpansion type is not yet assigned."""
app = self._app
for attr_name, _, type_index_attr in _SINGLE_PORT_HEXPANSION_REFS:
if type_index == getattr(app, type_index_attr):
return getattr(app, attr_name, None) is None
return False
# ------------------------------------------------------------------
# Async event handlers (registered directly on eventbus)
# ------------------------------------------------------------------
async def _handle_removal(self, event: HexpansionRemovalEvent):
app = self._app
port = event.port
self._hexpansion_type_by_slot[port - 1] = None
self._hexpansion_state_by_slot[port - 1] = _HEXPANSION_STATE_EMPTY
if port in self._ports_to_initialise:
self._ports_to_initialise.remove(port)
self._ports_to_check_app.discard(port)
if (self._detected_port is not None and port == self._detected_port) or \
(self._upgrade_port is not None and port == self._upgrade_port) or \
(self._waiting_app_port is not None and port == self._waiting_app_port) or \
(self._erase_port is not None and port == self._erase_port) or \
(self._port_selected != 0 and port == self._port_selected) or \
(port in app.hexdrive_ports) or \
self._has_single_port_hexpansion_on_port(port):
# The port from which a hexpansion has been removed is significant
self._hexpansion_eeprom_addr_len[port - 1] = None
self._hexpansion_eeprom_addr[port - 1] = None
app.hexpansion_update_required = True
if self._logging:
print(f"H:Hexpansion removed from port {port}")
# Although the Badge S/W now provides HexpansionMountedEvent which is emitted after a hexpansion is inserted and successfully mounted,
# we still want to listen for the raw insertion event as we want to know about hexpansions with blank eeproms.
async def _handle_insertion(self, event: HexpansionInsertionEvent):
if self._check_port_for_known_hexpansions(event.port) or event.port == self._port_selected:
# A known hexpansion type has been detected on the inserted port, so trigger an update of
# the hexpansion management state machine to handle it. Or the inserted port is the one
# currently being selected by the user in interactive mode, so we should also trigger an
# update to check if it's a valid hexpansion and update the UI accordingly.
self._app.hexpansion_update_required = True
if self._logging:
print(f"H:Hexpansion inserted into port {event.port}")
# ------------------------------------------------------------------
# Entry point from menu
# ------------------------------------------------------------------
def start(self) -> bool:
"""Enter hexpansion management from the main menu."""
app = self._app
app.set_menu(None)
app.button_states.clear()
app.refresh = True
app.auto_repeat_clear()
self._mode = _MODE_INTERACTIVE
if self._logging:
print("Entered Hexpansion Management mode")
self._enter_port_select()
return True
def _enter_port_select(self):
if self._port_selected == 0:
self._port_selected = 1
if self._logging:
print(f"H:Entering port select mode, starting with port {self._port_selected}")
self._read_port_header(self._port_selected)
self._sub_state = _SUB_PORT_SELECT
self._app.refresh = True
def _read_port_header(self, port: int):
"""Read the EEPROM header for the given port and set the default detail page."""
try:
self._port_selected_header = self._read_header(port)
except (OSError, RuntimeError):
self._port_selected_header = None
self._update_detail_page_count()
# remember the unique ID so that we can program it back if the EEPROM is re-initialised.
self._hexpansion_serial_number = self._port_selected_header.unique_id if self._port_selected_header is not None else None
# Default to details page if available, otherwise vid/pid
self._port_detail_page = self._PAGE_DETAILS if self._port_detail_page_count > 2 else self._PAGE_VID_PID
def _update_detail_page_count(self):
"""Set page count to 3 if the selected port has a recognised type with sub_type or app_name, else 2, or 1 if blank EEPROM."""
state_idx = self._hexpansion_state_by_slot[self._port_selected - 1] if 1 <= self._port_selected <= _SLOTS else None
if state_idx is not None:
if state_idx == _HEXPANSION_STATE_UNRECOGNISED:
# Unrecognised type - show vid/pid page and EEPROM page but not details page
self._port_detail_page_count = 2
self._port_detail_page = self._PAGE_VID_PID
elif state_idx >= _HEXPANSION_STATE_RECOGNISED:
# Recognised type - show vid/pid page and details page
self._port_detail_page_count = 3
self._port_detail_page = self._PAGE_DETAILS
else:
# Empty, Faulty or Blank
self._port_detail_page_count = 0
else:
# No state information
self._port_detail_page_count = 0
# ------------------------------------------------------------------
# Per-tick update (state machine for hexpansion management)
# ------------------------------------------------------------------
def update(self, delta: int) -> bool:
"""Per-tick update for hexpansion management state machine."""
app = self._app
if self._sub_state == _SUB_INIT:
if self._logging and self._port_selected == 0:
print("H:Initial scan for hexpansions")
if self._scan_ports():
self._port_selected = 0
self._sub_state = _SUB_CHECK
elif app.hexpansion_update_required:
# This flag is set when a hexpansion-related event occurs that should trigger an update of the hexpansion management state machine (e.g. insertion/removal of a hexpansion).
if self._mode == _MODE_IDLE:
self._mode = _MODE_UPDATE
if self._logging:
print("H:Hexpansion update triggered by event")
self._sub_state = _SUB_CHECK
app.set_menu(None)
elif self._mode == _MODE_INTERACTIVE:
app.hexpansion_update_required = False
self._sub_state = _SUB_CHECK
if self._check_ports_to_upgrade(delta):
# moves to _SUB_UPGRADE_CONFIRM if there are any ports with recognised hexpansion which need upgrading
# then to _SUB_PROGRAMMING if the user confirms the upgrade
# finally gets to _SUB_CHECK
pass
elif self._check_ports_to_initialise(delta):
# moves to _SUB_DETECTED if there are any ports with blank EEPROM which could be initialised
# then to _SUB_PROGRAMMING if the user confirms the initialisation
# finally gets to _SUB_CHECK
pass
elif self._sub_state == _SUB_DETECTED:
self._update_state_detected(delta)
elif self._sub_state == _SUB_ERASE_CONFIRM:
self._update_state_erase_confirm(delta)
elif self._sub_state == _SUB_ERASE:
self._update_state_erase(delta)
elif self._sub_state == _SUB_UPGRADE_CONFIRM:
self._update_state_upgrade(delta)
elif self._sub_state == _SUB_PROGRAMMING:
self._update_state_programming(delta)
elif self._sub_state == _SUB_PORT_SELECT:
self._update_state_port_select(delta)
elif self._sub_state == _SUB_CHECK:
self._update_state_check(delta)
if self._sub_state != self._prev_state:
if self._logging:
print(f"H:HexpansionMgr State: {self._prev_state} -> {self._sub_state}")
self._prev_state = self._sub_state
if self._sub_state == _SUB_DONE:
print("H:DONE")
if self._mode == _MODE_INTERACTIVE:
self._enter_port_select()
# Exit from _SUB_DONE to allow user to select another port for management if they wish
else:
if self._mode == _MODE_UPDATE:
app.hexpansion_update_required = False #to avoid beign called immediately
self._sub_state = _SUB_EXIT # exit to menu on next call (when user accepts warning)
elif self._sub_state == _SUB_EXIT:
print("H:EXIT")
app.hexpansion_update_required = False
self._message_being_shown = False
app.initialise_settings()
app.return_to_menu()
self._mode = _MODE_IDLE
return True
# ------------------------------------------------------------------
# Individual state handlers
# ------------------------------------------------------------------
def _update_state_programming(self, delta: int): # pylint: disable=unused-argument
app = self._app
if self._upgrade_port is not None:
# There is a hexpansion ready for App (upgrade) programming
# remember if the EEPROM already contains an app, so we can show a more appropriate message after programming (e.g. "Upgraded" vs "Initialised")
if app.HEXPANSION_TYPES[self._hexpansion_init_type].app_mpy_name is None:
#app.notification = Notification("No App", port=self._upgrade_port)
#app.show_message(["No App", "for this", "Hexpansion"], [(1,0,0),(1,0,0),(1,0,0)], "hexpansion")
#self._message_being_shown = True
self._sub_state = _SUB_CHECK
else:
result = self._update_app_in_eeprom(self._upgrade_port)
if result == _APP_EEPROM_RESULT_FAILURE:
app.notification = Notification("Failed", port=self._upgrade_port)
app.show_message(["Hexpansion", "programming", "failed", "Protected?"], [(1,0,0),(1,0,0),(1,0,0),(1,0,0)], "warning")
self._message_being_shown = True
self._sub_state = _SUB_CHECK
elif result == _APP_EEPROM_RESULT_MISSING:
app.notification = Notification("App Missing", port=self._upgrade_port)
app.show_message(["App file", "missing for", "this Hexpansion"], [(1,0,0),(1,0,0),(1,0,0)], "warning")
self._message_being_shown = True
self._sub_state = _SUB_CHECK
else:
#Easisest way to cope with there being a new EEPROM image is to ask the user to reboop
#otherwise we actually need to do a lot to get the old module and mount removed first...
#upgrade_text = "Upgraded" if result == _APP_EEPROM_RESULT_SUCCESSFUL_UPGRADE else "Programmed"
#app.notification = Notification(upgrade_text, port=self._upgrade_port)
# No point showing "Programmed" vs "Upgraded" as the Hexpansion Insertion Notification will cover it up
#eventbus.emit(HexpansionInsertionEvent(self._upgrade_port))
#app.show_message([f"{upgrade_text}:", "Please", "reboop"], [(0,1,0),(1,1,1),(1,1,1)], "reboop")
self._reboop_required = True
self._hexpansion_state_by_slot[self._upgrade_port - 1] = _HEXPANSION_STATE_RECOGNISED_APP_OK
self._sub_state = _SUB_CHECK
self._upgrade_port = None
elif self._detected_port is not None:
# There is a hexpansion ready for EEPROM initialisation
if self._prepare_eeprom(self._detected_port):
self._hexpansion_type_by_slot[self._detected_port - 1] = self._hexpansion_init_type
if app.HEXPANSION_TYPES[self._hexpansion_init_type].app_mpy_name is not None:
app.notification = Notification("Initialised", port=self._detected_port)
# Only worth showing "Initialised" Notification is we are NOT going to trigger Hexpansion Insertion Notification
self._upgrade_port = self._detected_port
self._sub_state = _SUB_PROGRAMMING
self._hexpansion_state_by_slot[self._detected_port - 1] = _HEXPANSION_STATE_RECOGNISED
else:
eventbus.emit(HexpansionInsertionEvent(self._detected_port))
#app.show_message(["No App", "for this", "Hexpansion"], [(1,1,0),(1,1,1),(1,1,1)], "hexpansion")
#self._message_being_shown = True
self._sub_state = _SUB_CHECK
self._hexpansion_state_by_slot[self._detected_port - 1] = _HEXPANSION_STATE_RECOGNISED_NO_APP
else:
app.notification = Notification("Failed", port=self._detected_port)
self._hexpansion_type_by_slot[self._detected_port - 1] = None
self._hexpansion_state_by_slot[self._detected_port - 1] = _HEXPANSION_STATE_FAULTY
app.show_message(["EEPROM", "initialisation", "failed", "Protected?"], [(1,0,0),(1,0,0),(1,0,0),(1,0,0)], "warning")
self._message_being_shown = True
self._sub_state = _SUB_CHECK
self._detected_port = None
else:
print("H:Error - no port to program")
self._sub_state = _SUB_INIT if self._mode == _MODE_INIT else _SUB_CHECK
def _update_state_detected(self, delta: int): # pylint: disable=unused-argument
""" Allow User to select which sub-type they want to initialise the hexpansion as (if there are multiple sub-types with the same PID), and confirm or cancel the initialisation."""
app = self._app
if app.button_states.get(BUTTON_TYPES["CONFIRM"]):
app.button_states.clear()
self._sub_state = _SUB_PROGRAMMING
elif app.button_states.get(BUTTON_TYPES["CANCEL"]):
app.button_states.clear()
if self._logging:
print("H:Initialise Cancelled")
self._detected_port = None
self._sub_state = _SUB_INIT if self._mode == _MODE_INIT else _SUB_CHECK
elif app.button_states.get(BUTTON_TYPES["UP"]):
app.button_states.clear()
self._hexpansion_init_type = (self._hexpansion_init_type + 1) % len(app.HEXPANSION_TYPES)
app.refresh = True
elif app.button_states.get(BUTTON_TYPES["DOWN"]):
app.button_states.clear()
self._hexpansion_init_type = (self._hexpansion_init_type - 1) % len(app.HEXPANSION_TYPES)
app.refresh = True
def _update_state_erase_confirm(self, delta: int): # pylint: disable=unused-argument
""" Allow User to confirm or cancel EEPROM erasure."""
# not used in _MODE_INIT
app = self._app
if app.button_states.get(BUTTON_TYPES["CONFIRM"]):
if self._logging:
print(f"H:Erase Confirmed on port {self._erase_port}")
app.button_states.clear()
self._sub_state = _SUB_ERASE
elif app.button_states.get(BUTTON_TYPES["CANCEL"]):
if self._logging:
print("H:Erase Cancelled")
app.button_states.clear()
self._erase_port = None
self._sub_state = _SUB_CHECK
def _update_state_erase(self, delta: int): # pylint: disable=unused-argument
""" Perform EEPROM erasure, and update app state accordingly (e.g. if the erased hexpansion is currently in use or being initialised/upgraded, reset those states).
Unresponsive to buttons during the erasure process."""
# not used in _MODE_INIT
app = self._app
erase_port = self._erase_port
if erase_port is None:
self._sub_state = _SUB_CHECK
return
if self._logging:
print(f"H:Erasing EEPROM on port {erase_port}")
existing_type = self._hexpansion_type_by_slot[erase_port - 1]
if existing_type is not None:
self._hexpansion_init_type = existing_type
eeprom_page_size=app.HEXPANSION_TYPES[self._hexpansion_init_type].eeprom_page_size if self._hexpansion_init_type > 0 else _DEFAULT_EEPROM_PAGE_SIZE
eeprom_total_size=app.HEXPANSION_TYPES[self._hexpansion_init_type].eeprom_total_size if self._hexpansion_init_type > 0 else _DEFAULT_EEPROM_TOTAL_SIZE
erase_addr_len = self._hexpansion_eeprom_addr_len[erase_port - 1]
erase_addr = self._hexpansion_eeprom_addr[erase_port - 1]
if erase_addr_len is None or erase_addr is None:
app.notification = Notification("Failed", port=erase_port)
self._sub_state = _SUB_CHECK
return
if self._logging:
print(f"H:Erase {self._hexpansion_init_type} page size: {eeprom_page_size} bytes, total size: {eeprom_total_size} bytes, addr_len: {erase_addr_len}, addr: {hex(erase_addr)}")
if self._erase_eeprom(erase_port,
erase_addr,
erase_addr_len,
eeprom_total_size,
eeprom_page_size):
app.notification = Notification("Erased", port=erase_port)
self._hexpansion_state_by_slot[erase_port - 1] = _HEXPANSION_STATE_BLANK
hexpansion_type = self._type_name_for_port(erase_port)
app.show_message([hexpansion_type, f"in slot {erase_port}:", "Erased"], [(1,1,0), (1,1,1), (0,1,0)], "hexpansion")
self._sub_state = _SUB_DETECTED
self._detected_port = erase_port
else:
app.notification = Notification("Failed", port=erase_port)
app.show_message(["EEPROM", "erasure", "failed", "Protected?"], [(1,0,0),(1,0,0),(1,0,0),(1,0,0)], "warning")
self._message_being_shown = True
self._sub_state = _SUB_CHECK
#self._reboop_required = True
if erase_port in app.hexdrive_ports:
hexdrive_index = app.hexdrive_ports.index(erase_port)
del app.hexdrive_ports[hexdrive_index]
if hexdrive_index < len(app.hexdrive_apps):
del app.hexdrive_apps[hexdrive_index]
app.motor_controller = None
if self._logging:
print(f"H:HexDrive on port {erase_port} erased!")
self._clear_single_port_hexpansion_refs(erase_port)
self._erase_port = None
def _update_state_upgrade(self, delta: int): # pylint: disable=unused-argument
""" Allow User to confirm or cancel App upgrade."""
app = self._app
upgrade_port = self._upgrade_port
if upgrade_port is None:
if self.logging:
print("H:Error - no port to upgrade")
self._sub_state = _SUB_INIT if self._mode == _MODE_INIT else _SUB_CHECK
return
if app.button_states.get(BUTTON_TYPES["CONFIRM"]):
app.button_states.clear()
app.notification = Notification("Upgrading", port=upgrade_port)
self._sub_state = _SUB_PROGRAMMING
elif app.button_states.get(BUTTON_TYPES["CANCEL"]):
if self._logging:
print("H:Upgrade Cancelled")
app.button_states.clear()
#self._hexpansion_state_by_slot[upgrade_port - 1] = _HEXPANSION_STATE_RECOGNISED_OLD_APP
self._upgrade_port = None
self._sub_state = _SUB_INIT if self._mode == _MODE_INIT else _SUB_CHECK
def _get_hexpansion_by_type(self, hexpansion_type: int) -> int | None:
""" Return the port number of a hexpansion of the given type, or None if no such hexpansion is currently detected."""
for port in range(0, _SLOTS):
if self._hexpansion_type_by_slot[port] == hexpansion_type:
return port+1
return None
def _report_hexpansion_states(self):
""" Utility function to print the current detected hexpansion types and states for each port, for debugging purposes."""
if not self._logging:
return
app = self._app
print("H:Current Hexpansion States:")
for port in range(0, _SLOTS):
type_idx = self._hexpansion_type_by_slot[port]
type_name = app.HEXPANSION_TYPES[type_idx].name if type_idx is not None else "None"
state_name = _HEXPANSION_STATE_NAMES[self._hexpansion_state_by_slot[port]]
print(f"Port {port+1}: Type={type_name}, State={state_name}")
print(f"\tPorts to initialise: {self._ports_to_initialise}")
print(f"\tPorts to check app: {self._ports_to_check_app}")
print(f"\thexsense_port:{app.hexsense_port}")
print(f"\thexdiag_port:{app.hexdiag_port}")
print(f"\thexaudio_port:{app.hexaudio_port}")
print(f"\thexdrive_ports:{app.hexdrive_ports}")
print(f"\thexpansion_update_required = {app.hexpansion_update_required}")
print(f"\tmode = {self._mode}")
def _check_hexpansion(self, port: int | None, type_index: int) -> tuple[int | None, object | None]:
""" Check if the currently configured hexpansion of the given type is still present, and
if not, check if there is another hexpansion of the same type that can be switched to, or if the hexpansion has been removed entirely."""
app = self._app
check_hexpansion_app = None
hexpansion_was_present = port is not None
old_port = port
if hexpansion_was_present:
if self._hexpansion_type_by_slot[port - 1] != type_index:
old_port = port
port = None
if port is None:
name = app.HEXPANSION_TYPES[type_index].name
new_port = self._get_hexpansion_by_type(type_index)
if new_port is not None:
if hexpansion_was_present:
if self._logging:
print(f"H:{name} moved from port {old_port} to port {new_port}")
#app.show_message([f"{name} moved", f"to port {new_port}"], [(1,1,0),(1,1,1)], "hexpansion")
#self._message_being_shown = True
else:
if self._logging:
print(f"H:{name} found on port {new_port}")
port = new_port
if app.HEXPANSION_TYPES[type_index].app_name is not None:
self._hexpansion_state_by_slot[port - 1] = _HEXPANSION_STATE_RECOGNISED # may be updated to _RECOGNISED_APP_OK or _RECOGNISED_OLD_APP after checking for the correct app
check_hexpansion_app = self._check_hexpansion_app_on_port(port, type_index)
else:
self._hexpansion_state_by_slot[port - 1] = _HEXPANSION_STATE_RECOGNISED_NO_APP
elif hexpansion_was_present:
if self._logging:
print(f"H:{name} on port {old_port} lost")
if self._mode == _MODE_UPDATE:
app.show_message([f"{name}","removed.","Please reinsert"], [(1,1,0),(1,1,1),(1,1,1)], "error")
self._message_being_shown = True
assert old_port is not None
app.notification = Notification(f"{name} removed", port=old_port)
return port, check_hexpansion_app
def _update_state_check(self, delta): # pylint: disable=unused-argument
""" Check for HexDrive & HexSense presence and update app state accordingly."""
app = self._app
self._report_hexpansion_states()
# For hexpansions of which we only need to know where one is we track movements between ports and update the assigned port accordingly
self._refresh_single_port_hexpansion_assignments()
# Build a new list of ports with HexDrives - to allow for more than one being present and used:
new_hexdrive_ports = []
for port in range(1, _SLOTS + 1):
# check if there is a hexpansion of a type that can be a HexDrive on this port
type_idx = self._hexpansion_type_by_slot[port-1]
if type_idx is not None and type_idx in app.hexdrive_hexpansion_types:
if self._logging:
print(f"H:HexDrive on port {port} added to list")
new_hexdrive_ports.append(port)
if set(new_hexdrive_ports) != set(app.hexdrive_ports):
if self._logging:
print(f"H:HexDrive ports changed from {app.hexdrive_ports} to {new_hexdrive_ports}")
app.hexdrive_ports = new_hexdrive_ports
app.hexdrive_apps = []
app.num_motors = 0
app.num_servos = 0
app.num_sensors = 0
for port in app.hexdrive_ports:
hexdrive_type_idx = self._hexpansion_type_by_slot[port - 1]
if hexdrive_type_idx is not None and 0 <= hexdrive_type_idx < len(app.HEXPANSION_TYPES):
app.num_motors += app.HEXPANSION_TYPES[hexdrive_type_idx].motors
app.num_servos += app.HEXPANSION_TYPES[hexdrive_type_idx].servos
app.num_sensors += app.HEXPANSION_TYPES[hexdrive_type_idx].sensors
if len(app.hexdrive_ports) != len (app.hexdrive_apps):
hexdrive_apps = []
for port in app.hexdrive_ports:
if self._logging:
print(f"H:Checking HexDrive app on port {port}, current state: {_HEXPANSION_STATE_NAMES[self._hexpansion_state_by_slot[port - 1]]}")
if self._hexpansion_state_by_slot[port - 1] == _HEXPANSION_STATE_RECOGNISED_APP_OK:
# already checked and app is OK, so just add it to the list
hexdrive_app = self._find_hexpansion_app(port)
if hexdrive_app is not None and hexdrive_app not in app.hexdrive_apps:
hexdrive_apps.append(hexdrive_app)
elif self._hexpansion_state_by_slot[port - 1] == _HEXPANSION_STATE_RECOGNISED:
# should this port have an app
type_idx = self._hexpansion_type_by_slot[port - 1]
if app.HEXPANSION_TYPES[type_idx].app_name is not None:
# Yes this port should have an app, but we haven't checked it yet, so check if the correct app is running on this port
if port not in self._ports_to_check_app:
if self._logging:
print(f"H:Request Check for {app.HEXPANSION_TYPES[type_idx].app_name} app on port {port}")
self._ports_to_check_app.add(port)
if len(hexdrive_apps) > 0:
app.hexdrive_apps = hexdrive_apps
if self._logging:
print(f"H:Latest HexDrive apps: {app.hexdrive_apps}")
# Create the high-level MotorController for IMU-aided driving
# (only when the HexDrive has motors)
if app.num_motors > 1 and len(app.hexdrive_apps) > 0 and app.motor_controller is None:
# App may still be loading - we can't initialise the MotorController until the HexDrive app is loaded
try:
from .motor_controller import MotorController
app.motor_controller = MotorController(
app.hexdrive_apps[0], app.settings,
logging=self._logging,
front_face_setting=app.settings.get('front_face'),
apply_motor_directions_callback=app.apply_motor_directions,
)
except Exception as e: # pylint: disable=broad-except
print(f"H:MotorController init failed: {e}")
app.motor_controller = None
elif app.motor_controller is not None and app.num_motors == 0:
app.motor_controller = None
if len(self._ports_to_check_app) > 0:
# there are outstandind apps to check
if self._logging:
print(f"H:Waiting for app version check on port(s): {self._ports_to_check_app}")
else:
# Check Complete - decide next state
if self._reboop_required:
app.show_message(["Please", "reboop"], [(1,1,1),(1,1,1)], "reboop")
return # so that you can't get out of this without a reboop
elif len(app.hexdrive_apps) == 0 and self._mode == _MODE_INIT:
app.show_message(_HEXDRIVE_REQUIRED_MESSAGE, _HEXDRIVE_REQUIRED_MESSAGE_COLOURS, "warning")
self._message_being_shown = True
if self._message_being_shown or self._mode == _MODE_INTERACTIVE:
# we will be called again when the message is dismissed
self._sub_state = _SUB_DONE
else:
self._sub_state = _SUB_EXIT
def _get_header_for_port(self, port: int) -> HexpansionHeader | None:
header = None
if hexpansion_app is not None:
if hasattr(hexpansion_app, "_hexpansion_manager"):
manager = hexpansion_app._hexpansion_manager # pylint: disable=protected-access
if manager is not None:
header = manager.hexpansion_headers[port]
return header
def get_active_hexdrive_unique_id(self) -> int | None:
"""Return unique_id of the first active HexDrive port, if available."""
app = self._app
for port in app.hexdrive_ports:
header = self._get_header_for_port(port)
unique_id = header.unique_id if header else None
if unique_id is not None:
return unique_id
return None
def _update_state_port_select(self, delta: int): # pylint: disable=unused-argument
app = self._app
if app.button_states.get(BUTTON_TYPES["RIGHT"]):
app.button_states.clear()
self._port_selected = (self._port_selected % _SLOTS) + 1
self._read_port_header(self._port_selected)
app.refresh = True
elif app.button_states.get(BUTTON_TYPES["LEFT"]):
app.button_states.clear()
self._port_selected = ((self._port_selected - 2) % _SLOTS) + 1
self._read_port_header(self._port_selected)
app.refresh = True
elif app.button_states.get(BUTTON_TYPES["CONFIRM"]):
app.button_states.clear()
app.refresh = True
if self._hexpansion_state_by_slot[self._port_selected - 1] == _HEXPANSION_STATE_BLANK:
# The selected port has a blank EEPROM, so we can initialise it without erasing first.
self._detected_port = self._port_selected
app.notification = Notification("Init?", port=self._detected_port)
self._sub_state = _SUB_DETECTED
elif self._hexpansion_state_by_slot[self._port_selected - 1] >= _HEXPANSION_STATE_FAULTY:
# The selected port has a non-blank EEPROM with a detected hexpansion type, so we need to erase it before we can initialise or upgrade it.
self._erase_port = self._port_selected
app.notification = Notification("Erase?", port=self._erase_port)
self._sub_state = _SUB_ERASE_CONFIRM
elif app.button_states.get(BUTTON_TYPES["UP"]):
app.button_states.clear()
if self._port_detail_page_count > 0:
self._port_detail_page = (self._port_detail_page - 1) % self._port_detail_page_count
app.refresh = True
elif app.button_states.get(BUTTON_TYPES["DOWN"]):
app.button_states.clear()
if self._port_detail_page_count > 0:
self._port_detail_page = (self._port_detail_page + 1) % self._port_detail_page_count
app.refresh = True
elif app.button_states.get(BUTTON_TYPES["CANCEL"]):
app.button_states.clear()
self._sub_state = _SUB_EXIT
# ------------------------------------------------------------------
# Draw hexpansion-related states
# ------------------------------------------------------------------
def _type_name_for_port(self, port: int, fallback_type_idx: int | None = None) -> str:
"""Return detected type name for a port, falling back to a selected type index."""
ignore_blank_eeprom = 1 if fallback_type_idx is not None else 0
if port is not None and 1 <= port <= _SLOTS:
type_idx = self._hexpansion_type_by_slot[port - 1]
if type_idx is not None and 0 <= type_idx < len(self._app.HEXPANSION_TYPES)-ignore_blank_eeprom:
return self._app.HEXPANSION_TYPES[type_idx].name
if fallback_type_idx is not None:
# Blank EEPROM is reported as the fallback type - because that is what the user is selecting during initialisation
return self._app.HEXPANSION_TYPES[fallback_type_idx].name
return "Empty"
def draw(self, ctx) -> bool:
"""Render UI for hexpansion-related states. Returns True if handled."""
app = self._app
if self._sub_state == _SUB_DETECTED:
hexpansion_type = app.HEXPANSION_TYPES[self._hexpansion_init_type].name
hexpansion_sub_type = app.HEXPANSION_TYPES[self._hexpansion_init_type].sub_type
app.draw_message(ctx, ["Hexpansion", f"in slot {self._detected_port}:", "Init EEPROM as", hexpansion_type, f"{hexpansion_sub_type if hexpansion_sub_type else ''}?"], \
[(1, 1, 0), (1, 1, 0), (1, 1, 0), (1, 0, 1), (1, 0, 1)], label_font_size)
button_labels(ctx, confirm_label="Yes", up_label="\u25B2", down_label="\u25BC", cancel_label="No",
left_label="\u25C0", right_label="\u25B6")
return True
elif self._sub_state == _SUB_PORT_SELECT:
self._draw_port_select(ctx)
return True
elif self._sub_state == _SUB_ERASE_CONFIRM:
if self._erase_port is None:
return False
hexpansion_type_name = self._type_name_for_port(self._erase_port, self._hexpansion_init_type)
# If the EEPROM type is unknown, show the proposed type and later allow selecting from common options.
app.draw_message(ctx, [hexpansion_type_name, f"in slot {self._erase_port}:", "Erase EEPROM?"], \
[(1, 0, 1), (1, 1, 0), (1, 0, 0)], label_font_size)
button_labels(ctx, confirm_label="Yes", cancel_label="No")
return True
elif self._sub_state == _SUB_ERASE:
if self._erase_port is None:
return False
hexpansion_type_name = self._type_name_for_port(self._erase_port, self._hexpansion_init_type)
app.draw_message(ctx, [hexpansion_type_name, f"in slot {self._erase_port}:", "Erasing..."], \
[(1, 0, 1), (1, 1, 0), (1, 0, 0)], label_font_size)
return True
elif self._sub_state == _SUB_UPGRADE_CONFIRM:
if self._upgrade_port is None:
return False
hexpansion_type_name = self._type_name_for_port(self._upgrade_port, self._hexpansion_init_type)
upgrade_or_install = "Upgrade" if self._hexpansion_state_by_slot[self._upgrade_port-1] == _HEXPANSION_STATE_RECOGNISED_OLD_APP else "Install"
app.draw_message(ctx, [hexpansion_type_name, f"in slot {self._upgrade_port}:", upgrade_or_install, f"{hexpansion_type_name} app?"], \
[(1, 0, 1), (1, 1, 0), (1, 1, 0), (1, 1, 0)], label_font_size)
button_labels(ctx, confirm_label="Yes", cancel_label="No")
return True
elif self._sub_state == _SUB_PROGRAMMING:
# During upgrade, show the already-detected type for the selected port.
if self._upgrade_port is None:
return False
hexpansion_type_name = self._type_name_for_port(self._upgrade_port, self._hexpansion_init_type)
app.draw_message(ctx, [f"{hexpansion_type_name}:", f"in slot {self._upgrade_port}:", "Programming", "Please wait..."], \
[(1, 0, 1), (1, 1, 0), (1, 1, 0), (1, 1, 0)], label_font_size)
return True
return False
# Hexpansion/EEPROM information pages
_PAGE_NAMES = ("VID/PID", "EEPROM", "Details")
_PAGE_VID_PID = 0
_PAGE_EEPROM = 1
_PAGE_DETAILS = 2
def _draw_port_select(self, ctx):
"""Draw the port-select screen with paged details."""
app = self._app
hdr = self._port_selected_header
hexpansion_state = _HEXPANSION_STATE_NAMES[self._hexpansion_state_by_slot[self._port_selected - 1]]
if self._hexpansion_state_by_slot[self._port_selected - 1] > _HEXPANSION_STATE_BLANK:
hexpansion_name = self._type_name_for_port(self._port_selected, None)
else:
hexpansion_name = ""
up_label = down_label = ""
if self._port_detail_page_count == 0:
lines = [f"Slot {self._port_selected}", hexpansion_state, hexpansion_name]
colours = [(1, 1, 0), (1, 0, 1), (0, 1, 1)]
else:
# Common header lines for all pages
page = self._port_detail_page
lines = [f"Slot {self._port_selected}-{self._PAGE_NAMES[page]}", hdr.friendly_name if hdr is not None else hexpansion_name]
colours = [(1, 1, 0), (1, 0, 1)]
if page == self._PAGE_VID_PID:
# VID / PID page
if hdr is not None:
lines += [f"VID: {hdr.vid:04X}", f"PID: {hdr.pid:04X}", f"UID: {hdr.unique_id}"]
colours += [(0, 1, 1), (0, 1, 1), (0, 1, 1)]
#else:
# lines = header_lines + ["No header"]
# colours = header_colours + [(1, 0, 0)]
elif page == self._PAGE_EEPROM:
# EEPROM parameters page
if hdr is not None:
lines += [f"Size: {hdr.eeprom_total_size} Bytes", f"Page: {hdr.eeprom_page_size} Bytes"]
colours += [(0, 1, 1), (0, 1, 1)]
else: # page == self._PAGE_DETAILS:
# Details page (only when page_count == 3)
type_idx = self._hexpansion_type_by_slot[self._port_selected - 1]
ht = app.HEXPANSION_TYPES[type_idx] if type_idx is not None else None
if ht is not None:
if ht.sub_type:
lines.append(ht.sub_type)
colours.append((0, 1, 1))
if ht.app_name:
lines.append(ht.app_name)
colours.append((0, 1, 1))
# Try to get running app version
running_app = self._find_hexpansion_app(self._port_selected)
if running_app is None:
lines.append("App not found")
colours.append((1, 0, 0))
else:
ver = getattr(running_app, "VERSION", getattr(running_app, "version", None))
if ver is not None:
lines.append(f"v{ver}")
colours.append((0, 1, 1))
else:
lines.append(hexpansion_state)
colours.append((0, 1, 1))
# Button labels: up/down show destination page names
down_page = (page + 1) % self._port_detail_page_count
up_page = (page - 1) % self._port_detail_page_count
# Only show the DOWN label if there is more than 1 page, otherwise there is no other page to go to
down_label=self._PAGE_NAMES[down_page] if self._port_detail_page_count > 1 else ""
# only show the UP label if there are more than 2 pages, otherwise it would just show the same as the DOWN
up_label=self._PAGE_NAMES[up_page] if self._port_detail_page_count > 2 else ""
# Ensure there are always 5 lines to draw for consistent layout, even if some are blank
while len(lines) < 5:
lines.append("")
colours.append((1, 1, 1))
app.draw_message(ctx, lines, colours, label_font_size)
confirm_label = "Init" if self._hexpansion_state_by_slot[self._port_selected - 1] == _HEXPANSION_STATE_BLANK else \
"Erase" if self._hexpansion_state_by_slot[self._port_selected - 1] >= _HEXPANSION_STATE_FAULTY else ""
button_labels(ctx, confirm_label=confirm_label, left_label="<Slot", right_label="Slot>",
up_label=up_label, down_label=down_label, cancel_label="Back")
# ------------------------------------------------------------------
# Port scanning
# ------------------------------------------------------------------
def _scan_ports(self) -> bool:
"""Scan all ports one at a time for known hexpansions, and update app state accordingly.
Returns True when all have been scanned (even if no hexpansions are detected), False if the scan is still in progress."""
# use _port_selected as the iterator variable for which port we are currently scanning, starting at 1 and going up to _SLOTS
if self._port_selected is None or self._port_selected > _SLOTS or self._port_selected < 1:
self._port_selected = 1
self._check_port_for_known_hexpansions(self._port_selected)
self._port_selected += 1
return self._port_selected > _SLOTS
def _read_header(self, port: int, i2c: I2C | None=None) -> HexpansionHeader | None:
if i2c is None:
if port is None:
return None
i2c = I2C(port)
addr_len = self._hexpansion_eeprom_addr_len[port-1]
eeprom_addr = self._hexpansion_eeprom_addr[port-1]
if addr_len is None:
# Autodetect eeprom addr
eeprom_addr, addr_len = detect_eeprom_addr(i2c)
if eeprom_addr is None:
print(f"H:Failed to detect EEPROM address on port {port}")
return None
self._hexpansion_eeprom_addr_len[port-1] = addr_len