wakemypc 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wakemypc/__init__.py ADDED
File without changes
wakemypc/flash.py ADDED
@@ -0,0 +1,230 @@
1
+ """
2
+ flash.py -- Flash MicroPython firmware (.uf2) onto a Raspberry Pi Pico
3
+ ======================================================================
4
+
5
+ WHAT IS FIRMWARE?
6
+ -----------------
7
+ "Firmware" is the base software that runs on a microcontroller. Think of it like
8
+ the operating system on your computer. Without firmware, the Pico is a blank chip
9
+ that cannot do anything.
10
+
11
+ We use MicroPython as our firmware. MicroPython is a tiny version of Python that
12
+ runs directly on microcontrollers. Once MicroPython is installed, you can write
13
+ Python scripts and the Pico will execute them.
14
+
15
+ WHAT IS A .uf2 FILE?
16
+ ---------------------
17
+ UF2 stands for "USB Flashing Format". It is a special file format designed by
18
+ Microsoft for flashing microcontrollers. The key insight is:
19
+
20
+ A .uf2 file can be flashed by simply copying it to a USB drive.
21
+
22
+ No special programmer hardware needed. No complex flashing software. Just drag
23
+ and drop (or cp on the command line).
24
+
25
+ HOW BOOTSEL MODE WORKS
26
+ -----------------------
27
+ BOOTSEL = "Boot Select". The Pico has a physical button labeled BOOTSEL on the board.
28
+
29
+ 1. Unplug the Pico from USB.
30
+ 2. Hold down the BOOTSEL button (it is a small white button on the board).
31
+ 3. While holding BOOTSEL, plug the USB cable back in.
32
+ 4. Release the BOOTSEL button.
33
+
34
+ Now the Pico appears as a USB mass storage device -- just like a flash drive!
35
+ It will show up as a drive called "RPI-RP2" (for original Pico) or "RP2350"
36
+ (for Pico 2 / Pico W 2).
37
+
38
+ The drive is tiny (only a few hundred KB) and contains two files:
39
+ - INFO_UF2.TXT (information about the bootloader)
40
+ - INDEX.HTM (a redirect to the Raspberry Pi documentation)
41
+
42
+ To flash new firmware, you just copy a .uf2 file onto this drive. The Pico
43
+ detects the new file, writes it to its internal flash memory, and automatically
44
+ reboots. After reboot, the Pico is no longer a USB drive -- it is now running
45
+ whatever firmware was in the .uf2 file.
46
+
47
+ AFTER FLASHING
48
+ --------------
49
+ Once MicroPython is flashed, the Pico reboots and appears as a USB serial device.
50
+ You can then connect to it via serial port (e.g. /dev/ttyACM0) and type Python
51
+ commands interactively, or upload .py files for it to run.
52
+ """
53
+
54
+ import platform
55
+ import shutil
56
+ import time
57
+ from pathlib import Path
58
+
59
+
60
+ # Known mount point names for Pico in BOOTSEL mode.
61
+ # These are the "drive names" that appear when the Pico is in BOOTSEL.
62
+ BOOTSEL_DRIVE_NAMES = {"RPI-RP2", "RP2350"}
63
+
64
+
65
+ def find_bootsel_drive():
66
+ """
67
+ Look for a Pico in BOOTSEL mode by searching for its USB mass storage mount point.
68
+
69
+ On Linux: typically mounted at /media/<username>/RPI-RP2 or /run/media/...
70
+ On macOS: typically mounted at /Volumes/RPI-RP2
71
+ On Windows: appears as a new drive letter like E:\\
72
+
73
+ Returns the path to the mounted drive, or None if not found.
74
+ """
75
+ system = platform.system()
76
+
77
+ if system == "Linux":
78
+ # On Linux, removable drives are usually auto-mounted under /media/<user>/
79
+ # or /run/media/<user>/ depending on the desktop environment.
80
+ search_dirs = []
81
+
82
+ # Check /media/<username>/
83
+ media_path = Path("/media")
84
+ if media_path.exists():
85
+ for user_dir in media_path.iterdir():
86
+ if user_dir.is_dir():
87
+ search_dirs.append(user_dir)
88
+
89
+ # Check /run/media/<username>/
90
+ run_media_path = Path("/run/media")
91
+ if run_media_path.exists():
92
+ for user_dir in run_media_path.iterdir():
93
+ if user_dir.is_dir():
94
+ search_dirs.append(user_dir)
95
+
96
+ for search_dir in search_dirs:
97
+ for drive_name in BOOTSEL_DRIVE_NAMES:
98
+ candidate = search_dir / drive_name
99
+ if candidate.is_dir() and (candidate / "INFO_UF2.TXT").exists():
100
+ return str(candidate)
101
+
102
+ elif system == "Darwin":
103
+ # macOS mounts volumes under /Volumes/
104
+ for drive_name in BOOTSEL_DRIVE_NAMES:
105
+ candidate = Path("/Volumes") / drive_name
106
+ if candidate.is_dir() and (candidate / "INFO_UF2.TXT").exists():
107
+ return str(candidate)
108
+
109
+ elif system == "Windows":
110
+ # On Windows, check all drive letters for the BOOTSEL drive.
111
+ # The drive will have INFO_UF2.TXT in its root.
112
+ import string
113
+
114
+ for letter in string.ascii_uppercase:
115
+ candidate = Path(f"{letter}:\\")
116
+ if candidate.exists() and (candidate / "INFO_UF2.TXT").exists():
117
+ return str(candidate)
118
+
119
+ return None
120
+
121
+
122
+ def read_bootsel_info(drive_path):
123
+ """
124
+ Read the INFO_UF2.TXT file on the BOOTSEL drive to get board information.
125
+
126
+ This file contains lines like:
127
+ UF2 Bootloader v1.0
128
+ Model: Raspberry Pi RP2350
129
+ Board-ID: RPI-RP2350
130
+ """
131
+ info_file = Path(drive_path) / "INFO_UF2.TXT"
132
+ if info_file.exists():
133
+ return info_file.read_text()
134
+ return "INFO_UF2.TXT not found"
135
+
136
+
137
+ def flash_uf2(uf2_path, drive_path=None):
138
+ """
139
+ Flash a .uf2 firmware file to a Pico in BOOTSEL mode.
140
+
141
+ This is literally just copying a file to a USB drive. That is all flashing is!
142
+ The Pico's bootloader detects the .uf2 file, writes it to internal flash,
143
+ and reboots automatically.
144
+
145
+ Parameters:
146
+ uf2_path: Path to the .uf2 firmware file (e.g. "micropython-pico-w2.uf2")
147
+ drive_path: Path to the BOOTSEL drive. If None, auto-detect.
148
+
149
+ Returns:
150
+ True on success, raises on failure.
151
+ """
152
+ uf2_path = Path(uf2_path)
153
+
154
+ # Validate the .uf2 file exists
155
+ if not uf2_path.exists():
156
+ raise FileNotFoundError(
157
+ f"UF2 file not found: {uf2_path}\n"
158
+ f"\n"
159
+ f"You need to download the MicroPython firmware for your Pico variant.\n"
160
+ f"Get it from: https://micropython.org/download/\n"
161
+ f" - For Pico W 2: look for 'RPI_PICO2_W' or 'PICO2-W'\n"
162
+ f" - The file will be named something like: RPI_PICO2_W-v1.xx.x.uf2"
163
+ )
164
+
165
+ if not uf2_path.suffix.lower() == ".uf2":
166
+ raise ValueError(
167
+ f"File does not have .uf2 extension: {uf2_path}\n"
168
+ f"Make sure you downloaded the correct firmware file."
169
+ )
170
+
171
+ # Find the BOOTSEL drive
172
+ if drive_path is None:
173
+ drive_path = find_bootsel_drive()
174
+ if drive_path is None:
175
+ raise RuntimeError(
176
+ "No Pico in BOOTSEL mode detected.\n"
177
+ "\n"
178
+ "To put the Pico in BOOTSEL mode:\n"
179
+ " 1. Unplug the Pico from USB.\n"
180
+ " 2. Hold down the BOOTSEL button (small white button on the board).\n"
181
+ " 3. While holding BOOTSEL, plug the USB cable back in.\n"
182
+ " 4. Release the BOOTSEL button.\n"
183
+ "\n"
184
+ "The Pico should appear as a USB drive named 'RPI-RP2' or 'RP2350'.\n"
185
+ "If it does not appear, try a different USB cable -- some cables are\n"
186
+ "charge-only and do not carry data."
187
+ )
188
+
189
+ # Read board info before flashing (for display purposes)
190
+ board_info = read_bootsel_info(drive_path)
191
+
192
+ # The actual flash: copy the .uf2 file to the BOOTSEL drive.
193
+ # shutil.copy2 preserves file metadata. The Pico's bootloader will
194
+ # detect the new file and start writing it to flash memory.
195
+ dest = Path(drive_path) / uf2_path.name
196
+ shutil.copy2(str(uf2_path), str(dest))
197
+
198
+ # After the copy completes, the Pico will automatically reboot.
199
+ # The BOOTSEL drive will disappear (unmount) as the Pico restarts.
200
+ # Give it a moment to start the reboot process.
201
+ return {
202
+ "uf2_file": str(uf2_path),
203
+ "drive_path": drive_path,
204
+ "board_info": board_info,
205
+ }
206
+
207
+
208
+ def wait_for_serial_after_flash(timeout=15):
209
+ """
210
+ After flashing, wait for the Pico to reboot and appear as a serial device.
211
+
212
+ The typical sequence after flashing MicroPython:
213
+ 1. .uf2 is copied to BOOTSEL drive (~2 seconds)
214
+ 2. Pico writes firmware to flash memory (~3 seconds)
215
+ 3. Pico reboots (~1 second)
216
+ 4. MicroPython starts and USB serial device appears (~2 seconds)
217
+
218
+ Total: about 5-10 seconds.
219
+ """
220
+ # Import here to avoid circular imports
221
+ from .serial_detect import list_pico_serial_ports
222
+
223
+ start = time.time()
224
+ while time.time() - start < timeout:
225
+ picos = list_pico_serial_ports()
226
+ if picos:
227
+ return picos[0]["port"]
228
+ time.sleep(1)
229
+
230
+ return None
wakemypc/identify.py ADDED
@@ -0,0 +1,212 @@
1
+ """
2
+ identify.py -- Blink the Pico's LED for physical identification
3
+ ================================================================
4
+
5
+ WHY YOU NEED THIS
6
+ -----------------
7
+ Imagine you have 5 Picos plugged into a USB hub, and your computer shows them as:
8
+ /dev/ttyACM0
9
+ /dev/ttyACM1
10
+ /dev/ttyACM2
11
+ /dev/ttyACM3
12
+ /dev/ttyACM4
13
+
14
+ Which physical Pico is /dev/ttyACM2? They all look the same! You cannot tell
15
+ just by looking at them.
16
+
17
+ The "identify" command solves this: it tells a specific Pico to blink its LED
18
+ rapidly. Now you can look at your pile of Picos and see which one is blinking.
19
+ Then you can label it with a sticker or note its position.
20
+
21
+ HOW IT WORKS
22
+ ------------
23
+ We send a small Python script to the Pico via the serial REPL (the interactive
24
+ Python prompt accessible over USB). The script:
25
+
26
+ 1. Imports the 'machine' module (MicroPython's hardware control library).
27
+ 2. Gets a reference to the onboard LED pin.
28
+ - On Pico W / Pico W 2, the LED is connected to the WiFi chip, so we
29
+ use the string "LED" instead of a pin number.
30
+ 3. Toggles the LED on and off rapidly in a loop.
31
+
32
+ The Pico W 2's onboard LED is a small green LED next to the USB connector.
33
+ It is not super bright, but it is visible enough to identify the device.
34
+
35
+ SERIAL REPL COMMUNICATION
36
+ --------------------------
37
+ The serial REPL is like having a Python terminal running on the Pico. When you
38
+ open a serial connection (like we do here), you can type Python code and the
39
+ Pico executes it immediately. This is the same thing that happens when you use
40
+ tools like Thonny, PuTTY, or 'screen' to talk to the Pico.
41
+
42
+ We use "raw REPL" mode (entered with Ctrl+A) for sending multi-line scripts.
43
+ In raw mode, there is no echo or prompt, making it easier for programs (vs humans)
44
+ to communicate with the Pico.
45
+ """
46
+
47
+ import time
48
+
49
+ import serial
50
+
51
+
52
+ # The MicroPython script that runs ON THE PICO (not on your computer!).
53
+ # This script is sent over USB serial and executed by the Pico's MicroPython
54
+ # interpreter.
55
+ #
56
+ # Note: This code uses MicroPython APIs (machine.Pin, time.sleep) which are
57
+ # different from regular Python. These only work on the Pico, not on your computer.
58
+ BLINK_SCRIPT = """\
59
+ import machine
60
+ import time
61
+
62
+ # On Pico W and Pico W 2, the onboard LED is controlled via the WiFi chip,
63
+ # so we reference it by the string "LED" rather than a GPIO pin number.
64
+ # On the original Pico (non-W), the LED is on GPIO 25: machine.Pin(25, machine.Pin.OUT)
65
+ led = machine.Pin("LED", machine.Pin.OUT)
66
+
67
+ # Blink rapidly 20 times (takes about 4 seconds total).
68
+ # Each cycle: 100ms on + 100ms off = 200ms per blink.
69
+ for i in range(20):
70
+ led.on()
71
+ time.sleep(0.1)
72
+ led.off()
73
+ time.sleep(0.1)
74
+
75
+ # Leave the LED off when done
76
+ led.off()
77
+ print("IDENTIFY_DONE")
78
+ """
79
+
80
+
81
+ def blink_led(port, duration_seconds=4, baudrate=115200):
82
+ """
83
+ Make a Pico's onboard LED blink rapidly for physical identification.
84
+
85
+ Parameters:
86
+ port: Serial port path, e.g. "/dev/ttyACM0"
87
+ duration_seconds: Approximate duration of blinking (not precise)
88
+ baudrate: Serial speed (115200 is standard for MicroPython)
89
+
90
+ How it works:
91
+ 1. Open a serial connection to the Pico.
92
+ 2. Enter raw REPL mode (Ctrl+A) for clean script execution.
93
+ 3. Send the blink script.
94
+ 4. Wait for the script to finish.
95
+ 5. Return to normal REPL mode (Ctrl+B).
96
+
97
+ The raw REPL protocol:
98
+ - Ctrl+A (0x01): Enter raw REPL mode. Pico responds with "raw REPL; CTRL-B to exit"
99
+ - Send Python code as plain text.
100
+ - Ctrl+D (0x04): Execute the code. Pico responds with "OK" then output then Ctrl+D.
101
+ - Ctrl+B (0x02): Exit raw REPL, return to normal interactive mode.
102
+ """
103
+ try:
104
+ ser = serial.Serial(port, baudrate, timeout=2)
105
+ except serial.SerialException as e:
106
+ raise RuntimeError(
107
+ f"Could not open serial port {port}: {e}\n"
108
+ f"\n"
109
+ f"Make sure:\n"
110
+ f" - The Pico is plugged in and has MicroPython installed\n"
111
+ f" - No other program (Thonny, screen, etc.) is using the port\n"
112
+ f" - You have permission to access the port (Linux: add yourself to 'dialout' group)"
113
+ )
114
+
115
+ time.sleep(0.5)
116
+
117
+ # Interrupt any currently running program on the Pico.
118
+ # Sending Ctrl+C (0x03) twice is the standard way to get back to the REPL
119
+ # prompt, even if a program is in the middle of a time.sleep() or a loop.
120
+ ser.write(b"\r\x03\x03")
121
+ time.sleep(0.5)
122
+ ser.read(ser.in_waiting) # Discard any buffered output
123
+
124
+ # Enter raw REPL mode.
125
+ # Normal REPL: echoes what you type, has ">>> " prompt, auto-indents.
126
+ # Raw REPL: no echo, no prompt, executes code blocks terminated by Ctrl+D.
127
+ # Raw mode is better for programmatic use because we do not have to parse prompts.
128
+ ser.write(b"\x01") # Ctrl+A = enter raw REPL
129
+ time.sleep(0.3)
130
+ ser.read(ser.in_waiting) # Read and discard the "raw REPL" banner
131
+
132
+ # Calculate blink count based on desired duration.
133
+ # Each blink cycle is ~200ms (100ms on + 100ms off).
134
+ blink_count = max(5, int(duration_seconds / 0.2))
135
+
136
+ # Build the script with the custom blink count
137
+ script = (
138
+ "import machine\n"
139
+ "import time\n"
140
+ 'led = machine.Pin("LED", machine.Pin.OUT)\n'
141
+ f"for i in range({blink_count}):\n"
142
+ " led.on()\n"
143
+ " time.sleep(0.1)\n"
144
+ " led.off()\n"
145
+ " time.sleep(0.1)\n"
146
+ "led.off()\n"
147
+ 'print("IDENTIFY_DONE")\n'
148
+ )
149
+
150
+ # Send the script and execute it
151
+ ser.write(script.encode())
152
+ ser.write(b"\x04") # Ctrl+D = execute the code
153
+
154
+ # Wait for the blinking to finish.
155
+ # The script takes approximately `duration_seconds` to complete.
156
+ timeout = duration_seconds + 5 # Extra buffer for slow serial
157
+ start = time.time()
158
+ response = b""
159
+
160
+ while time.time() - start < timeout:
161
+ if ser.in_waiting:
162
+ response += ser.read(ser.in_waiting)
163
+ if b"IDENTIFY_DONE" in response:
164
+ break
165
+ time.sleep(0.1)
166
+
167
+ # Exit raw REPL mode and return to normal mode
168
+ ser.write(b"\x02") # Ctrl+B = exit raw REPL
169
+ time.sleep(0.2)
170
+ ser.close()
171
+
172
+ success = b"IDENTIFY_DONE" in response
173
+ return {
174
+ "port": port,
175
+ "success": success,
176
+ "duration": duration_seconds,
177
+ "message": (
178
+ f"LED on {port} blinked for ~{duration_seconds} seconds."
179
+ if success
180
+ else f"Blink command was sent to {port} but completion was not confirmed."
181
+ ),
182
+ }
183
+
184
+
185
+ def read_device_id_and_blink(port, baudrate=115200):
186
+ """
187
+ Read the device ID AND blink the LED, so the user can match ID to physical device.
188
+
189
+ This is a convenience function that:
190
+ 1. Reads the Pico's unique hardware ID.
191
+ 2. Blinks the LED so you can see which physical Pico it is.
192
+ 3. Returns both the device ID and the port, so you can label the device.
193
+
194
+ Typical usage:
195
+ "I have 3 Picos. Let me identify each one."
196
+ For each port, call this function. It will tell you:
197
+ "Port /dev/ttyACM0 has device ID e660583883724a32 -- that's the one blinking now."
198
+ """
199
+ from .provision import read_device_id
200
+
201
+ device_id = read_device_id(port)
202
+ blink_result = blink_led(port)
203
+
204
+ return {
205
+ "port": port,
206
+ "device_id": device_id,
207
+ "blink_success": blink_result["success"],
208
+ "message": (
209
+ f"Device on {port} has ID: {device_id}\n"
210
+ f"The LED is blinking now -- look for the flashing green light!"
211
+ ),
212
+ }