Skip to content

Commit de8b6d1

Browse files
committed
Move bootloader code to Modulino
1 parent 91fa5f8 commit de8b6d1

File tree

3 files changed

+81
-75
lines changed

3 files changed

+81
-75
lines changed

examples/change_address.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from modulino import Modulino
1313

1414
print()
15-
devices = Modulino.available_devices()
15+
bus = None # Change this to the I2C bus you are using on 3rd party host boards
16+
devices = Modulino.available_devices(bus)
1617

1718
if len(devices) == 0:
1819
print("No devices found on the bus. Try resetting the board.")

src/modulino/firmware_flasher.py

Lines changed: 55 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
import os
1515
import sys
16-
from machine import I2C, Pin
1716
import time
1817
from micropython import const
18+
from machine import I2C
19+
from modulino import Modulino
1920

2021
BOOTLOADER_I2C_ADDRESS = const(0x64)
2122
ACK = const(0x79)
@@ -31,55 +32,29 @@
3132

3233
CHUNK_SIZE = const(128) # Size of the memory chunk to write
3334

34-
# Define I2C pins and initialize I2C
35-
i2c = I2C(0, freq=100000)
35+
bus = None # Change this to the I2C bus you are using on 3rd party host boards
3636

37-
def send_reset(address):
38-
"""
39-
Send a reset command to the I2C device at the given address.
40-
41-
:param address: I2C address of the device.
42-
:return: 0 if the reset command was sent successfully, otherwise -1.
43-
"""
44-
buffer = b'DIE'
45-
buffer += b'\x00' * (8 - len(buffer)) # Pad buffer to 8 bytes
46-
47-
try:
48-
print(f"🔄 Resetting device at address {hex(address)}")
49-
i2c.writeto(address, buffer, True)
50-
print("📤 Reset command sent")
51-
time.sleep(0.25) # Wait for the device to reset
52-
return True
53-
except OSError as e:
54-
# ENODEV can be thrown if either the device reset while writing out the buffer or if the device
55-
# was already in bootloader mode in which case there is no device at the original address
56-
if e.errno == 19:
57-
time.sleep(0.25) # Wait for the device to reset
58-
return True
59-
else:
60-
print(f"Error sending reset command: {e}")
61-
return False
62-
63-
def wait_for_ack():
37+
def wait_for_ack(bus):
6438
"""
6539
Wait for an acknowledgment from the I2C device.
6640
6741
:return: True if an acknowledgment was received, otherwise False.
6842
"""
69-
res = i2c.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
43+
res = bus.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
7044
if res != ACK:
7145
while res == BUSY:
7246
time.sleep(0.1)
73-
res = i2c.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
47+
res = bus.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
7448
if res != ACK:
7549
print(f"❌ Error processing command. Result code: {hex(res)}")
7650
return False
7751
return True
7852

79-
def execute_command(opcode, command_params, response_length = 0, verbose=False):
53+
def execute_command(bus, opcode, command_params, response_length = 0, verbose=False):
8054
"""
8155
Execute an I2C command on the device.
8256
57+
:param bus: The I2C bus to use.
8358
:param opcode: The command opcode.
8459
:param command_params: The buffer containing the command parameters.
8560
:param response_length: The expected length of the response data frame.
@@ -90,44 +65,45 @@ def execute_command(opcode, command_params, response_length = 0, verbose=False):
9065
print(f"🕵️ Executing command {hex(opcode)}")
9166

9267
cmd = bytes([opcode, 0xFF ^ opcode]) # Send command code and complement (XOR = 0x00)
93-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, cmd, True)
94-
if not wait_for_ack():
68+
bus.writeto(BOOTLOADER_I2C_ADDRESS, cmd, True)
69+
if not wait_for_ack(bus):
9570
print(f"❌ Command not acknowledged: {hex(opcode)}")
9671
return None
9772

9873
if command_params is not None:
99-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, command_params, True)
100-
if not wait_for_ack():
74+
bus.writeto(BOOTLOADER_I2C_ADDRESS, command_params, True)
75+
if not wait_for_ack(bus):
10176
print("❌ Command failed")
10277
return None
10378

10479
if response_length == 0:
10580
return None
10681

107-
data = i2c.readfrom(BOOTLOADER_I2C_ADDRESS, response_length)
82+
data = bus.readfrom(BOOTLOADER_I2C_ADDRESS, response_length)
10883

109-
if not wait_for_ack():
84+
if not wait_for_ack(bus):
11085
print("❌ Failed completing command")
11186
return None
11287

11388
return data
11489

115-
def flash_firmware(firmware_path, verbose=False):
90+
def flash_firmware(device : Modulino, firmware_path, verbose=False):
11691
"""
11792
Flash the firmware to the I2C device.
11893
119-
:param firmware: The binary firmware data.
120-
:param length: The length of the firmware data.
94+
:param device: The Modulino device to flash.
95+
:param firmware_path: The binary firmware path.
12196
:param verbose: Whether to print debug information.
12297
:return: True if the flashing was successful, otherwise False.
12398
"""
124-
data = execute_command(CMD_GET_VERSION, None, 1, verbose)
99+
bus = device.i2c_bus
100+
data = execute_command(bus, CMD_GET_VERSION, None, 1, verbose)
125101
if data is None:
126102
print("❌ Failed to get protocol version")
127103
return False
128104
print(f"ℹ️ Protocol version: {data[0] & 0xF}.{data[0] >> 4}")
129105

130-
data = execute_command(CMD_GET, None, CMD_GET_LENGTH_V12, verbose)
106+
data = execute_command(bus, CMD_GET, None, CMD_GET_LENGTH_V12, verbose)
131107
if data is None:
132108
print("❌ Failed to get command list")
133109
return False
@@ -136,7 +112,7 @@ def flash_firmware(firmware_path, verbose=False):
136112
print("👀 Supported commands:")
137113
print(", ".join([hex(byte) for byte in data[2:]]))
138114

139-
data = execute_command(CMD_GET_ID, None, 3, verbose)
115+
data = execute_command(bus, CMD_GET_ID, None, 3, verbose)
140116
if data is None:
141117
print("❌ Failed to get device ID")
142118
return False
@@ -146,7 +122,7 @@ def flash_firmware(firmware_path, verbose=False):
146122

147123
print("🗑️ Erasing memory...")
148124
erase_params = bytearray([0xFF, 0xFF, 0x0]) # Mass erase flash
149-
execute_command(CMD_ERASE_NO_STRETCH, erase_params, 0, verbose)
125+
execute_command(bus, CMD_ERASE_NO_STRETCH, erase_params, 0, verbose)
150126

151127
with open(firmware_path, 'rb') as file:
152128
firmware_data = file.read()
@@ -161,7 +137,7 @@ def flash_firmware(firmware_path, verbose=False):
161137
checksum ^= b
162138
start_address.append(checksum)
163139
data_slice = firmware_data[i:i + CHUNK_SIZE]
164-
if not write_firmware_page(start_address, data_slice):
140+
if not write_firmware_page(bus, start_address, data_slice):
165141
print(f"❌ Failed to write page {hex(i)}")
166142
return False
167143
time.sleep(0.01) # Give the device some time to process the data
@@ -170,39 +146,40 @@ def flash_firmware(firmware_path, verbose=False):
170146

171147
print("🏃 Starting firmware")
172148
go_params = bytearray([0x8, 0x00, 0x00, 0x00, 0x8])
173-
execute_command(CMD_GO, go_params, 0, verbose) # Jump to the application
149+
execute_command(bus, CMD_GO, go_params, 0, verbose) # Jump to the application
174150

175151
return True
176152

177-
def write_firmware_page(command_params, firmware_data):
153+
def write_firmware_page(bus, command_params, firmware_data):
178154
"""
179155
Write a page of the firmware to the I2C device.
180156
157+
:param bus: The I2C bus to use.
181158
:param command_params: The buffer containing the command parameters.
182159
:param firmware_data: The buffer containing the firmware data.
183160
:return: True if the page was written successfully, otherwise False.
184161
"""
185162
cmd = bytes([CMD_WRITE_NO_STRETCH, 0xFF ^ CMD_WRITE_NO_STRETCH])
186-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, cmd)
187-
if not wait_for_ack():
163+
bus.writeto(BOOTLOADER_I2C_ADDRESS, cmd)
164+
if not wait_for_ack(bus):
188165
print("❌ Write command not acknowledged")
189166
return False
190167

191-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, command_params)
192-
if not wait_for_ack():
168+
bus.writeto(BOOTLOADER_I2C_ADDRESS, command_params)
169+
if not wait_for_ack(bus):
193170
print("❌ Failed to write command parameters")
194171
return False
195172

196173
data_size = len(firmware_data)
197174
tmp_buffer = bytearray(data_size + 2) # Data plus size and checksum
198-
tmp_buffer[0] = data_size - 1 # Size of the data # TODO Arduino code uses data_size - 1
175+
tmp_buffer[0] = data_size - 1 # Size of the data
199176
tmp_buffer[1:data_size + 1] = firmware_data
200177
tmp_buffer[-1] = 0 # Checksum placeholder
201178
for i in range(data_size + 1): # Calculate checksum over size byte + data bytes
202179
tmp_buffer[-1] ^= tmp_buffer[i]
203180

204-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, tmp_buffer)
205-
if not wait_for_ack():
181+
bus.writeto(BOOTLOADER_I2C_ADDRESS, tmp_buffer)
182+
if not wait_for_ack(bus):
206183
print("❌ Failed to write firmware")
207184
return False
208185

@@ -243,7 +220,7 @@ def select_file(bin_files):
243220
return None
244221

245222
if len(bin_files) == 1:
246-
confirm = input(f"📄 Found one biary file: {bin_files[0]}. Do you want to flash it? (yes/no) ")
223+
confirm = input(f"📄 Found one binary file: {bin_files[0]}. Do you want to flash it? (yes/no) ")
247224
if confirm.lower() == 'yes':
248225
return bin_files[0]
249226
else:
@@ -257,37 +234,41 @@ def select_file(bin_files):
257234
return None
258235
return bin_files[choice - 1]
259236

260-
def select_i2c_device():
237+
def select_device(bus : I2C) -> Modulino:
261238
"""
262239
Scan the I2C bus for devices and prompt the user to select one.
263240
264-
:return: The selected I2C device address.
241+
:param bus: The I2C bus to scan.
242+
:return: The selected Modulino device.
265243
"""
266-
devices = i2c.scan()
244+
devices = Modulino.available_devices(bus)
267245

268246
if len(devices) == 0:
269-
print("❌ No I2C devices found")
247+
print("❌ No devices found")
270248
return None
271249

272250
if len(devices) == 1:
273-
confirm = input(f"🔌 Found one I2C device at address {hex(devices[0])}. Do you want to flash it? (yes/no) ")
251+
device = devices[0]
252+
confirm = input(f"🔌 Found {device.device_type} at address {hex(device.address)}. Do you want to update this device? (yes/no) ")
274253
if confirm.lower() == 'yes':
275254
return devices[0]
276255
else:
277256
return None
278257

279-
print("🔌 I2C devices found:")
258+
print("🔌 Devices found:")
280259
for index, device in enumerate(devices):
281-
print(f"{index + 1}. Address: {hex(device)}")
282-
choice = int(input("Select the I2C device to flash (number): "))
260+
print(f"{index + 1}) {device.device_type} at {hex(device.address)}")
261+
choice = int(input("Select the device to flash (number): "))
283262
if choice < 1 or choice > len(devices):
284263
return None
285264
return devices[choice - 1]
286265

287-
def run():
266+
def run(bus: I2C):
288267
"""
289268
Initialize the flashing process.
290269
Finds .bin files, scans for I2C devices, and flashes the selected firmware.
270+
271+
:param bus: The I2C bus to use. If None, the default I2C bus will be used.
291272
"""
292273

293274
bin_files = find_bin_files()
@@ -300,23 +281,25 @@ def run():
300281
print("❌ No file selected")
301282
return
302283

303-
device_address = select_i2c_device()
304-
if device_address is None:
284+
device = select_device(bus)
285+
if device is None:
305286
print("❌ No device selected")
306287
return
307-
308-
if send_reset(device_address):
288+
289+
print(f"🔄 Resetting device at address {hex(device.address)}")
290+
if device.enter_bootloader():
309291
print("✅ Device reset successfully")
310292
else:
311293
print("❌ Failed to reset device")
312294
return
313295

314296
print(f"🕵️ Flashing {bin_file} to device at address {hex(BOOTLOADER_I2C_ADDRESS)}")
315297

316-
if flash_firmware(bin_file):
298+
if flash_firmware(device, bin_file):
317299
print("✅ Firmware flashed successfully")
318300
else:
319301
print("❌ Failed to flash firmware")
320302

321303
if __name__ == "__main__":
322-
run()
304+
print()
305+
run(bus)

src/modulino/modulino.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,24 @@ def change_address(self, new_address: int):
250250

251251
self.address = new_address
252252

253+
def enter_bootloader(self):
254+
"""
255+
Enters the I2C bootloader of the device.
256+
This is only supported on Modulinos that have a microcontroller.
257+
258+
Returns:
259+
bool: True if the device entered bootloader mode, False otherwise.
260+
"""
261+
buffer = b'DIE'
262+
buffer += b'\x00' * (8 - len(buffer)) # Pad buffer to 8 bytes
263+
try:
264+
self.i2c_bus.writeto(self.address, buffer, True)
265+
sleep(0.25) # Wait for the device to reset
266+
return True
267+
except OSError as e:
268+
# ENODEV (e.errno == 19) can be thrown if either the device reset while writing out the buffer
269+
return False
270+
253271
def read(self, amount_of_bytes: int) -> bytes | None:
254272
"""
255273
Reads the given amount of bytes from the i2c device and returns the data.
@@ -293,14 +311,18 @@ def has_default_address(self) -> bool:
293311
return self.address in self.default_addresses
294312

295313
@staticmethod
296-
def available_devices() -> list[Modulino]:
314+
def available_devices(bus: I2C = None) -> list[Modulino]:
297315
"""
298316
Finds all devices on the i2c bus and returns them as a list of Modulino objects.
299317
318+
Parameters:
319+
bus (I2C): The I2C bus to use. If not provided, the default I2C bus will be used.
320+
300321
Returns:
301322
list: A list of Modulino objects.
302323
"""
303-
bus = _I2CHelper.get_interface()
324+
if bus is None:
325+
bus = _I2CHelper.get_interface()
304326
device_addresses = bus.scan()
305327
devices = []
306328
for address in device_addresses:

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy