Skip to content

Commit 1105255

Browse files
authored
Merge pull request #13 from PyBLE-dev/feature/github-example-browser
Import SHA-pinned public GitHub examples
2 parents fa535dd + 64228c7 commit 1105255

54 files changed

Lines changed: 8928 additions & 79 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ its annotated tag rather than from the newer branch tip.
1111

1212
### App source
1313

14+
- Added a connected Files action for browsing a canonical public GitHub
15+
repository without an account or token, resolving the requested ref once to
16+
a displayed immutable commit SHA, and lazily browsing bounded non-recursive
17+
Git trees.
18+
- Added exact review and separate overwrite consent for importing selected
19+
ordinary lowercase `.py` files into the captured board folder. The complete
20+
size-bounded, strict-UTF-8 batch is fetched before sequential PBLE/1 writes;
21+
partial results identify succeeded, failed, and unattempted paths, refresh
22+
Files honestly, and never auto-open or auto-run downloaded code.
23+
- Hardened GitHub import against redirects, unexpected hosts and object
24+
identities, incomplete board listings, reconnect/session races, response
25+
resource exhaustion, slow-trickle requests, and public API retry deadlines,
26+
with adaptive, keyboard-accessible, localized, and golden-tested states.
27+
- Advanced the independently versioned app source build from `0.1.0+2` to
28+
`0.1.0+3`, distinguishing this development build from TestFlight build 2.
1429
- Added the connected board's PBLE/1 board ID and PyBLE agent firmware version
1530
to Board info and retained both in the ready/running status pill, with
1631
localized missing-value handling, full accessibility context, and
@@ -57,8 +72,9 @@ its annotated tag rather than from the newer branch tip.
5772

5873
## Firmware 0.6.0 — 2026-08-21
5974

60-
_Firmware release only. The app is versioned independently and remains
61-
`0.1.0+2` in this source tree._
75+
_Firmware release only. At release qualification, the independently versioned
76+
app source was `0.1.0+2`; later app-source work does not alter firmware
77+
v0.6.0._
6278

6379
- Published and deployed the qualified five-profile release at
6480
[pyble.dev/flash](https://pyble.dev/flash): `esp32-4mb`,

‎README.md‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,26 @@ The Flutter app currently provides:
4343
version retained in the always-visible connection status;
4444
- a MicroPython editor with save, run, stop, soft reboot, and live console;
4545
- wireless file browsing and transfer with integrity checks;
46+
- an optional public GitHub example browser that resolves a repository/ref to
47+
an immutable commit, previews exact `.py` source-to-board paths, and imports
48+
selected files into the connected Files folder only after explicit review;
4649
- an offline Blockly workspace with GPIO and standard MicroPython NeoPixel
4750
blocks, including explicit numeric GPIOs and bounded named `machine.Pin`
4851
identities;
4952
- editable beginner examples;
5053
- exact Blockly sidecar reopening and a bounded Python-to-blocks importer;
5154
- an adaptive tablet interface for portrait and landscape use; and
52-
- local operation without an account, analytics, or cloud dependency.
55+
- core editing, BLE, Files, Blocks, and Run operation without an account,
56+
analytics, or cloud dependency. GitHub is the sole optional network surface;
57+
its public, unauthenticated requests start only when the user opens the
58+
importer.
59+
60+
GitHub import is deliberately bounded and non-executing. It accepts a canonical
61+
public repository URL, lazily browses one SHA-pinned snapshot, and selects only
62+
ordinary lowercase `.py` files from one folder. Before any board write, PyBLE
63+
fetches and validates the complete batch, shows the exact destination paths,
64+
and asks separately before overwriting existing files. Import never creates a
65+
remote folder hierarchy, opens an editor document, or runs downloaded code.
5366

5467
The iPad build is available through public TestFlight. An invited Android
5568
internal test is available through Google Play; this is not a public Play
@@ -113,6 +126,8 @@ This repository is intentionally a monorepo:
113126
- [`app/`](app/) — the Flutter tablet application;
114127
- [`firmware/`](firmware/) — the portable agent, board overlays, and release
115128
tooling;
129+
- [`examples/`](examples/) — clean-room, board-neutral MicroPython examples,
130+
including the public GitHub-import fixtures;
116131
- [`docs/specifications/protocol.md`](docs/specifications/protocol.md) — the
117132
open PBLE/1 wire contract;
118133
- [`tests/`](tests/) — host, conformance, release, and HIL test runners;
@@ -136,11 +151,19 @@ shared conformance corpus, documentation, and CI atomically.
136151
4. For the four ESP profiles, connect the board and install with Web Serial.
137152
For Pico 2 W, download the verified UF2 and copy it manually to the BOOTSEL
138153
mass-storage volume. iPadOS cannot perform either wired provisioning step.
139-
5. Open PyBLE, scan for the provisioned board, connect, and run an example over
140-
BLE.
154+
5. Open PyBLE, scan for the provisioned board, and connect over BLE.
155+
6. To try the optional public example importer, open the destination folder in
156+
**Files**, choose **Import examples from GitHub**, enter
157+
`https://github.com/PyBLE-dev/PyBLE` with ref `main`, then browse to
158+
`examples/github-import`.
159+
7. Select `hello.py` or `count.py`, review the displayed commit SHA and exact
160+
board target, and confirm any overwrite separately. Import does not open or
161+
run the file; open it from Files and choose Run only when you are ready.
141162

142163
See [support and troubleshooting](https://pyble.dev/support) for browser,
143-
Bluetooth, and recovery requirements.
164+
Bluetooth, network, and recovery requirements. A GitHub account or token is not
165+
needed for the optional import; every other workflow remains available without
166+
it.
144167

145168
## Build and test
146169

‎app/DEPENDENCIES.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Toolchain pin: **Flutter 3.44.1 (stable)** / Dart SDK `^3.12.1` (see
3636
| `flutter_blue_plus` | runtime | `^1.35.0` | 1.36.8 | BSD-3-Clause | The BLE transport (A-02; FR-BLE-1/2/3/5). The **sole** importer lives under `lib/ble/`; no other layer may import it (enforced by `tools/ci/import_boundary.sh`, CON-8). Min 1.35.0 is the floor carrying the GATT service-filtered scan + `requestMtu(247)` + write-without-response API the `lib/ble` adapter binds to. It is a **federated** plugin: it hard-depends on all platform backends (`_android`/`_darwin`/`_linux`/`_web`); only the Android + iOS (`_darwin`) backends compile into PyBLE's shipped targets — see the **copyleft carve-out** under the transitive table. |
3737
| `intl` | runtime | `^0.20.2` | 0.20.2 | BSD-3-Clause | The ARB message runtime consumed by the generated `AppLocalizations` (X-12). The constraint is reconciled to the exact `intl` the pinned `flutter_localizations` (Flutter 3.44.1) admits — 0.20.2. |
3838
| `package_info_plus` | runtime | `^10.2.1` | 10.2.1 | BSD-3-Clause | The sole `lib/app/app_build_info.dart` adapter reads the actual installed iOS/Android version and build number for the X-11 About page (FR-ABOUT-3). It is never imported by `lib/pble`; static ASCII `kAppVersion` remains the PBLE/1 HELLO identity. Min 10.2.1 supports PyBLE's pinned Flutter 3.44.1, Dart 3.12.1, iOS 13, Java 17, AGP 9.0.1, and Gradle 9.1 toolchain. |
39+
| `http` | runtime | `^1.6.0` | 1.6.0 | BSD-3-Clause | The bounded public GitHub REST client for the connected A-33 subset ([ADR-0040](../docs/decisions/0040-sha-pinned-connected-github-import.md)). The direct dependency is confined to `lib/github_import/`'s production composition point and `GithubRepositoryClient`; all feature consumers bind to the neutral `GithubApi` seam. The adapter accepts only canonical public repository input, sends no credential or user source, permits requests only to exact HTTPS host `api.github.com`, disables redirects, pins every browse to a full commit SHA, and bounds time and response bodies. Min 1.6.0 supplies the abortable request surface used by the cancellation contract on the pinned Dart/Flutter toolchain. |
3940
| `webview_flutter` | runtime | `^4.14.1` | 4.14.1 | BSD-3-Clause | The offline platform-WebView host for the pinned Blockly runtime (A-31, ADR-0013). Min 4.14.1 matches PyBLE's Android 24 / iOS 13 platform floors and supplies `loadFlutterAsset`, JavaScript channels, and navigation delegates. Its sole importer is `lib/blocks/`; generated source and board actions stay in Dart behind the neutral `Connection` seam. |
4041
| `flutter_test` | dev (SDK) | Flutter SDK | 3.44.1 | BSD-3-Clause | Widget/unit/golden test harness; the substrate for the G1 suites (NFR-MAINT-4). |
4142
| `integration_test` | dev (SDK) | Flutter SDK | 3.44.1 | BSD-3-Clause | On-device Android/iPadOS parity coverage for the real Blockly platform WebView, local asset load, JavaScript bridge, generated Python, and file-backed actions (A-31). Ships with the Flutter SDK and is never part of a release build. |
@@ -70,7 +71,6 @@ directly. Every one is permissive and MIT-compatible (verified against the
7071
| `flutter_driver` | (Flutter SDK) | BSD-3-Clause |
7172
| `flutter_web_plugins` | (Flutter SDK) | BSD-3-Clause |
7273
| `fuchsia_remote_debug_protocol` | (Flutter SDK) | BSD-3-Clause |
73-
| `http` | 1.6.0 | BSD-3-Clause |
7474
| `http_parser` | 4.1.2 | BSD-3-Clause |
7575
| `leak_tracker` | 11.0.2 | BSD-3-Clause |
7676
| `leak_tracker_flutter_testing` | 3.0.10 | BSD-3-Clause |
@@ -151,5 +151,4 @@ rationale when added:
151151
|---|---|---|
152152
| `drift`, `sqlite3` | S5 / A-24 | Local persistence. |
153153
| `fl_chart` | later | Plotting surface. |
154-
| direct `http` / `dio` client | later | Network fetch for the import flow. (`http` is currently transitive through `package_info_plus`, but PyBLE imports no network client for About and performs no metadata network request.) |
155154
| `go_router` | — | Not planned for S1; navigation is index-based via a Riverpod `StateProvider` + `IndexedStack`. |

‎app/README.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Low Energy.
99
- BLE discovery, connection, reconnect, and capability negotiation
1010
- Python editing, save, run, stop, soft reboot, and live console
1111
- Wireless filesystem browsing and integrity-checked transfer
12+
- Optional public, unauthenticated GitHub browsing with immutable commit
13+
pinning, exact board-target review, and bounded `.py` import into the current
14+
connected Files folder
1215
- Offline Blockly workspace with beginner examples, GPIO, and NeoPixel blocks
1316
- Exact Blockly sidecars and bounded Python-to-block conversion
1417
- Adaptive portrait and landscape layouts
@@ -17,6 +20,11 @@ Low Energy.
1720
The public iPad beta is available through
1821
[TestFlight](https://testflight.apple.com/join/yU4e8s6d).
1922

23+
GitHub import is the app's sole optional internet workflow. It sends no account
24+
credential, board fact, or source file; it never auto-opens or runs imported
25+
code. Editing, BLE, Files, Blocks, and Run remain account-free and independent
26+
of GitHub availability.
27+
2028
## Requirements
2129

2230
- Flutter stable matching the version pinned in root CI
@@ -40,10 +48,11 @@ Pixel goldens use reviewed macOS baselines. Run them separately on macOS:
4048
flutter test --tags golden
4149
```
4250

43-
Run on a connected device:
51+
Install a standalone development build on a connected device (the current
52+
source build is `0.1.0+3`):
4453

4554
```sh
46-
flutter run
55+
flutter run --release -d <device-id>
4756
```
4857

4958
Platform WebView integration tests live under `integration_test/`. CI runs

‎app/android/app/src/main/AndroidManifest.xml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
2020
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
2121
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
22+
<!-- User-started, public GitHub example import (A-33). This must be in the
23+
main manifest so release builds can reach the exact HTTPS API host. -->
24+
<uses-permission android:name="android.permission.INTERNET" />
2225
<application
2326
android:label="PyBLE"
2427
android:name="${applicationName}"

‎app/lib/app/app_info.dart‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ library;
1717
const String kAppName = 'PyBLE';
1818

1919
/// The client version sent in HELLO (ASCII semver). Kept in step with the
20-
/// `version:` field in `app/pubspec.yaml` (currently `0.1.0+1`); the build-
21-
/// metadata suffix (`+1`) is dropped so the wire value is a clean semver.
20+
/// `version:` field in `app/pubspec.yaml` (currently `0.1.0+3`); the build-
21+
/// metadata suffix (`+3`) is dropped so the wire value is a clean semver.
2222
const String kAppVersion = '0.1.0';

‎app/lib/files/files_view.dart‎

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
/// * **Disconnected** — a quiet `folder_off` [EmptyState] with guidance and NO
99
/// destructive actions (nothing to act on).
1010
/// * **Connected** — a breadcrumb + action bar (Up / Refresh / New file / New
11-
/// folder / Upload) over the live listing. Tapping a folder descends; tapping
11+
/// folder / Upload / GitHub import) over the live listing. Tapping a folder
12+
/// descends; tapping
1213
/// a file opens it in the editor. Each row carries rename/delete affordances.
1314
/// A determinate transfer bar shows during a transfer; a typed failure is
1415
/// surfaced as an ARB-localized message (FR-FILES-3), never a raw code.
@@ -29,6 +30,8 @@ import 'package:pyble/app/providers.dart';
2930
import 'package:pyble/blocks/blocks.dart'
3031
show pythonBlocksFlowBusyProvider, showBoardFileAsBlocksConversion;
3132
import 'package:pyble/editor/editor.dart' show SmartPunctuationFormatter;
33+
import 'package:pyble/github_import/github_import.dart'
34+
show showGithubImportBrowser;
3235
import 'package:pyble/localization/localization.dart';
3336
import 'package:pyble/pble/pble.dart';
3437
import 'package:pyble/theme/theme.dart';
@@ -56,19 +59,36 @@ class FilesView extends ConsumerWidget {
5659
detail: l10n.filesDisconnectedDetail,
5760
);
5861
}
59-
return const _Explorer();
62+
return _Explorer(connState: connState);
6063
},
6164
);
6265
}
6366
}
6467

6568
/// The connected explorer body: action bar + transfer progress + error banner +
6669
/// the live listing. Reads (and thereby builds) the [fileExplorerProvider].
67-
class _Explorer extends ConsumerWidget {
68-
const _Explorer();
70+
class _Explorer extends ConsumerStatefulWidget {
71+
const _Explorer({required this.connState});
72+
73+
final ConnState connState;
6974

7075
@override
71-
Widget build(BuildContext context, WidgetRef ref) {
76+
ConsumerState<_Explorer> createState() => _ExplorerState();
77+
}
78+
79+
class _ExplorerState extends ConsumerState<_Explorer> {
80+
final FocusNode _githubImportFocus = FocusNode(
81+
debugLabel: 'Files GitHub import action',
82+
);
83+
84+
@override
85+
void dispose() {
86+
_githubImportFocus.dispose();
87+
super.dispose();
88+
}
89+
90+
@override
91+
Widget build(BuildContext context) {
7292
final FileExplorerState state = ref.watch(fileExplorerProvider);
7393
final FileExplorerController ctrl = ref.read(fileExplorerProvider.notifier);
7494
final bool pythonBlocksBusy = ref.watch(pythonBlocksFlowBusyProvider);
@@ -77,7 +97,22 @@ class _Explorer extends ConsumerWidget {
7797
return Column(
7898
crossAxisAlignment: CrossAxisAlignment.stretch,
7999
children: <Widget>[
80-
_ActionBar(state: state, ctrl: ctrl, l10n: l10n),
100+
_ActionBar(
101+
state: state,
102+
ctrl: ctrl,
103+
l10n: l10n,
104+
githubImportFocus: _githubImportFocus,
105+
githubImportEnabled: widget.connState == ConnState.ready,
106+
openGithubImport: () async {
107+
await showGithubImportBrowser(
108+
context,
109+
ref,
110+
cwd: state.cwd,
111+
refreshFiles: ctrl.refresh,
112+
);
113+
if (mounted) _githubImportFocus.requestFocus();
114+
},
115+
),
81116
if (state.progress != null)
82117
_TransferBar(
83118
progress: state.progress!,
@@ -107,11 +142,17 @@ class _ActionBar extends StatelessWidget {
107142
required this.state,
108143
required this.ctrl,
109144
required this.l10n,
145+
required this.githubImportFocus,
146+
required this.githubImportEnabled,
147+
required this.openGithubImport,
110148
});
111149

112150
final FileExplorerState state;
113151
final FileExplorerController ctrl;
114152
final AppLocalizations l10n;
153+
final FocusNode githubImportFocus;
154+
final bool githubImportEnabled;
155+
final Future<void> Function() openGithubImport;
115156

116157
@override
117158
Widget build(BuildContext context) {
@@ -173,6 +214,14 @@ class _ActionBar extends StatelessWidget {
173214
icon: const Icon(Icons.upload_file_outlined),
174215
onPressed: () => ctrl.uploadCurrentBuffer(),
175216
),
217+
IconButton(
218+
focusNode: githubImportFocus,
219+
tooltip: githubImportEnabled
220+
? l10n.githubImportAction
221+
: l10n.githubImportRequiresReady,
222+
icon: const Icon(Icons.cloud_download_outlined),
223+
onPressed: githubImportEnabled ? openGithubImport : null,
224+
),
176225
],
177226
),
178227
),

0 commit comments

Comments
 (0)