Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ jobs:
"ANDROID_NDK_PATH=$ANDROID_NDK_PATH" \
"ANDROID_NDK_LATEST_HOME=$ANDROID_NDK_LATEST_HOME" \
>> "$GITHUB_ENV"
- name: Production release APK build (no integration-test plugin)
shell: bash
run: |
set -euo pipefail
flutter build apk --release --flavor production --target lib/main.dart
PYBLE_AAPT="$PYBLE_ANDROID_SDK_ROOT/build-tools/36.0.0/aapt"
PYBLE_APK="build/app/outputs/flutter-apk/app-production-release.apk"
"$PYBLE_AAPT" dump badging "$PYBLE_APK" \
| grep -F "package: name='dev.pyble.pyble'"
- name: Create API 34 AOSP x86_64 tablet AVD
shell: bash
run: |
Expand Down Expand Up @@ -387,6 +396,7 @@ jobs:
timeout --signal=TERM --kill-after=30s 18m \
flutter build apk \
--debug \
--flavor integration \
--no-pub \
--target-platform android-x64 \
--target integration_test/android_smoke_test.dart \
Expand All @@ -400,7 +410,7 @@ jobs:
if [ "$PREBUILD_STATUS" -ne 0 ]; then
exit "$PREBUILD_STATUS"
fi
test -s build/app/outputs/flutter-apk/app-debug.apk
test -s build/app/outputs/flutter-apk/app-integration-debug.apk
- name: Boot headless emulator (KVM + software GPU)
shell: bash
run: |
Expand Down Expand Up @@ -564,7 +574,8 @@ jobs:
--no-pub \
--driver test_driver/integration_test.dart \
--target integration_test/android_smoke_test.dart \
--use-application-binary build/app/outputs/flutter-apk/app-debug.apk \
--flavor integration \
--use-application-binary build/app/outputs/flutter-apk/app-integration-debug.apk \
--timeout 660 \
-d "$PYBLE_ANDROID_SERIAL" \
>"$PYBLE_ANDROID_LOG_DIR/flutter-drive.log" 2>&1 \
Expand Down
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ flutter test --tags golden

Use a recent stable Flutter SDK matching the version pinned by CI.

Use the normal package directly on iOS:

```sh
flutter run -d <ios-device>
```

Always name an Android flavor. `production` is the normal launcher, while
`integration` uses the disposable `dev.pyble.pyble.integrationtest` package:

```sh
flutter run --flavor production --target lib/main.dart -d <android-device>
flutter build apk --release --flavor production --target lib/main.dart
flutter drive --flavor integration \
--driver test_driver/integration_test.dart \
--target integration_test/android_smoke_test.dart \
-d <android-device>
```

The current local production-flavor release build uses Gradle's debug signing
configuration for device testing. It is not a distributable beta artifact.

### Firmware

Host-side protocol and release tests do not require hardware:
Expand Down
14 changes: 13 additions & 1 deletion app/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,22 @@ android {
versionName = flutter.versionName
}

flavorDimensions += "purpose"
productFlavors {
create("production") {
dimension = "purpose"
}
create("integration") {
dimension = "purpose"
applicationIdSuffix = ".integrationtest"
}
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
// Signing with debug keys for now, so
// `flutter run --release --flavor production` works.
signingConfig = signingConfigs.getByName("debug")
}
}
Expand Down
6 changes: 6 additions & 0 deletions app/android/app/src/integration/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:label="PyBLE Integration Test"
tools:replace="android:label" />
</manifest>
21 changes: 19 additions & 2 deletions app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,32 @@
The flutter_blue_plus_android plugin manifest is intentionally blank, so
the app declares these itself. neverForLocation: the scan is filtered to
the PyBLE service UUID and derives no location (startScan is called with
androidUsesFineLocation: false). ACCESS_FINE_LOCATION is legacy-only,
capped at Android 11 (API 30). -->
androidUsesFineLocation: false). The app is BLE-first, so BLE hardware
is required. Classic Bluetooth and location hardware remain optional:
those permissions are only older-Android BLE scan prerequisites.
Bluetooth/Fine-Location declarations are legacy-only and capped at
Android 11 (API 30); Coarse Location is capped at Android 9 (API 28),
matching the pinned adapter contract. -->
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.location" android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
<application
android:label="PyBLE"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- Flutter 3.44 Impeller/Vulkan renders a blank frame on the validated
Lenovo TB-J616X (MediaTek, Android 12). Keep the supported legacy
OpenGL renderer until the physical Android gate passes on a later
Flutter engine. See NFR-COMPAT-4. -->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
16 changes: 14 additions & 2 deletions app/assets/blockly/pyble_blockly.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@

let workspace;
let revision = 0;
let hostEpoch;
let snapshotScheduled = false;
let pendingRestore;
let resizeFrame = 0;
Expand All @@ -678,7 +679,9 @@
function postMessage(message) {
const channel = window.PybleBlocks;
if (channel && typeof channel.postMessage === "function") {
channel.postMessage(JSON.stringify(message));
const envelope =
message.type === "hostReady" ? message : { ...message, hostEpoch };
channel.postMessage(JSON.stringify(envelope));
}
}

Expand Down Expand Up @@ -892,10 +895,17 @@
return value;
}

function configureHost(messages, workspaceJson, priorRevision) {
function configureHost(messages, dartHostEpoch, workspaceJson, priorRevision) {
if (workspace) {
throw new Error("Blockly host is already configured.");
}
if (
!Number.isSafeInteger(dartHostEpoch) ||
dartHostEpoch < 1 ||
dartHostEpoch > maxRevision
) {
throw new TypeError("Dart host epoch must be a positive safe integer.");
}
if (!messages || typeof messages !== "object" || Array.isArray(messages)) {
throw new TypeError("Blockly host messages must be an object.");
}
Expand Down Expand Up @@ -1049,6 +1059,7 @@
registerGpioBlocks();
registerNeopixelBlocks();
registerTimeBlocks();
hostEpoch = dartHostEpoch;
pendingRestore = initialRestore;
initialise();
return true;
Expand All @@ -1060,6 +1071,7 @@
restore,
snapshot: publishSnapshot,
});
postMessage({ version: bridgeVersion, type: "hostReady" });

function resizeWorkspace() {
if (!workspace) {
Expand Down
Loading
Loading