Merge branch 'Next-Flip:dev' into dev

This commit is contained in:
Spooks
2024-03-10 11:46:15 -06:00
committed by GitHub
54 changed files with 1241 additions and 676 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @Willy-JL @Sil333033 * @Willy-JL @Sil333033 @HaxSam @MatthewKuKanich

View File

@@ -1,4 +0,0 @@
#!/bin/bash
export VERSION_TAG="$(python -c 'import fbt_options; print(fbt_options.DIST_SUFFIX, end="")')"
echo "VERSION_TAG=${VERSION_TAG}" >> $GITHUB_ENV

View File

@@ -1,31 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
import nextcloud_client
import datetime as dt import datetime as dt
import requests import requests
import json import json
import os import os
dev_share_id = "" artifact_tgz = f"{os.environ['INDEXER_URL']}/firmware/dev/{os.environ['ARTIFACT_TAG']}.tgz"
dev_share = os.environ["NC_HOST"] + f"s/{dev_share_id}/download?path=/&files={{files}}" artifact_sdk = f"{os.environ['INDEXER_URL']}/firmware/dev/{os.environ['ARTIFACT_TAG'].replace('update', 'sdk')}.zip"
if __name__ == "__main__": if __name__ == "__main__":
with open(os.environ["GITHUB_EVENT_PATH"], "r") as f: with open(os.environ["GITHUB_EVENT_PATH"], "r") as f:
event = json.load(f) event = json.load(f)
client = nextcloud_client.Client(os.environ["NC_HOST"])
client.login(os.environ["NC_USER"], os.environ["NC_PASS"])
for file in (
os.environ["ARTIFACT_TGZ"],
os.environ["ARTIFACT_SDK"],
):
path = f"MNTM-Dev/{file}"
# try:
# client.delete(path)
# except Exception:
# pass
client.put_file(path, file)
requests.post( requests.post(
os.environ["BUILD_WEBHOOK"], os.environ["BUILD_WEBHOOK"],
headers={"Accept": "application/json", "Content-Type": "application/json"}, headers={"Accept": "application/json", "Content-Type": "application/json"},
@@ -33,10 +19,10 @@ if __name__ == "__main__":
"content": None, "content": None,
"embeds": [ "embeds": [
{ {
"title": "New Devbuild:", "title": "New Devbuild!",
"description": "", "description": "",
"url": "", "url": "",
"color": 16734443, "color": 16751147,
"fields": [ "fields": [
{ {
"name": "Changes since last commit:", "name": "Changes since last commit:",
@@ -44,21 +30,13 @@ if __name__ == "__main__":
}, },
{ {
"name": "Changes since last release:", "name": "Changes since last release:",
"value": f"[Compare release to {event['after'][:7]}]({event['compare'].rsplit('/', 1)[0] + '/main...' + event['after']})" "value": f"[Compare release to {event['after'][:7]}]({event['compare'].rsplit('/', 1)[0] + '/release...' + event['after']})"
}, },
{ {
"name": "Firmware download:", "name": "Download artifacts:",
"value": f"- [Download SDK for development]({dev_share.format(files=os.environ['ARTIFACT_SDK'])})\n- [Download Firmware TGZ]({dev_share.format(files=os.environ['ARTIFACT_TGZ'])})" "value": f"- [Download Firmware TGZ]({artifact_tgz})\n- [SDK (for development)]({artifact_sdk})"
} }
], ],
"author": {
"name": "Build Succeeded!",
# "icon_url": ""
},
# "footer": {
# "text": "Build go brrrr",
# "icon_url": ""
# },
"timestamp": dt.datetime.utcnow().isoformat() "timestamp": dt.datetime.utcnow().isoformat()
} }
], ],

View File

@@ -1,18 +0,0 @@
#!/bin/bash
export ARTIFACT_DIR="${VERSION_TAG}"
export ARTIFACT_TGZ="${VERSION_TAG}.tgz"
export ARTIFACT_ZIP="${VERSION_TAG}.zip"
export ARTIFACT_SDK="${VERSION_TAG}-sdk.zip"
cd dist/${DEFAULT_TARGET}-*
mv ${DEFAULT_TARGET}-update-* ${ARTIFACT_DIR}
tar --format=ustar -czvf ../../${ARTIFACT_TGZ} ${ARTIFACT_DIR}
cd ${ARTIFACT_DIR}
7z a ../../../${ARTIFACT_ZIP} .
cd ..
mv flipper-z-${DEFAULT_TARGET}-sdk-*.zip ../../${ARTIFACT_SDK}
cd ../..
echo "ARTIFACT_TGZ=${ARTIFACT_TGZ}" >> $GITHUB_ENV
echo "ARTIFACT_ZIP=${ARTIFACT_ZIP}" >> $GITHUB_ENV
echo "ARTIFACT_SDK=${ARTIFACT_SDK}" >> $GITHUB_ENV

View File

@@ -1,9 +1,9 @@
## ⬇️ Download ## ⬇️ Download
>### [🖥️ Web Updater (chrome)](https://momentum-fw.dev/update) [recommended] >### [🖥️ Web Updater (chrome)](https://momentum-fw.dev/update) [recommended]
>### [🐬 qFlipper Package (.tgz)](https://github.com/Next-Flip/Momentum-Firmware/releases/download/{VERSION_TAG}/{ARTIFACT_TGZ}) >### [🐬 qFlipper Package (.tgz)](https://github.com/Next-Flip/Momentum-Firmware/releases/download/{VERSION_TAG}/flipper-z-f7-update-{VERSION_TAG}.tgz)
>### [📦 Zipped Archive (.zip)](https://github.com/Next-Flip/Momentum-Firmware/releases/download/{VERSION_TAG}/{ARTIFACT_ZIP}) >### [📦 Zipped Archive (.zip)](https://github.com/Next-Flip/Momentum-Firmware/releases/download/{VERSION_TAG}/flipper-z-f7-update-{VERSION_TAG}.zip)
**Check the [install guide](https://github.com/Next-Flip/Momentum-Firmware#install) if you're not sure, or [join our Discord](https://discord.gg/momentum) if you have questions or encounter issues!** **Check the [install guide](https://github.com/Next-Flip/Momentum-Firmware#install) if you're not sure, or [join our Discord](https://discord.gg/momentum) if you have questions or encounter issues!**

View File

@@ -1,20 +1,27 @@
#!/usr/bin/env python #!/usr/bin/env python
import requests
import json import json
import os import os
if __name__ == "__main__": if __name__ == "__main__":
with open(os.environ["GITHUB_EVENT_PATH"], "r") as f:
event = json.load(f)
release = requests.get(
event["release"]["url"],
headers={
"Accept": "application/vnd.github.v3+json",
"Authorization": f"token {os.environ['GITHUB_TOKEN']}"
}
).json()
version_tag = release["tag_name"]
changelog = release["body"]
notes_path = '.github/workflow_data/release.md' notes_path = '.github/workflow_data/release.md'
with open(os.environ['GITHUB_EVENT_PATH'], "r") as f:
changelog = json.load(f)['pull_request']['body']
with open(notes_path, "r") as f: with open(notes_path, "r") as f:
template = f.read() template = f.read()
notes = template.format( notes = template.format(
ARTIFACT_TGZ=os.environ['ARTIFACT_TGZ'], VERSION_TAG=version_tag,
ARTIFACT_ZIP=os.environ['ARTIFACT_ZIP'],
VERSION_TAG=os.environ['VERSION_TAG'],
CHANGELOG=changelog CHANGELOG=changelog
) )
with open(notes_path, "w") as f: with open(notes_path, "w") as f:
f.write(notes) f.write(notes)
with open(os.environ["ARTIFACT_TGZ"].removesuffix(".tgz") + ".md", "w") as f:
f.write(changelog.strip() + "\n\n")

View File

@@ -1,13 +0,0 @@
#!/bin/bash
export VERSION_TAG="$(python -c '''
import datetime as dt
import json
import os
with open(os.environ["GITHUB_EVENT_PATH"], "r") as f:
event = json.load(f)
version = int(event["pull_request"]["title"].removeprefix("V").removesuffix(" Release")
date = dt.datetime.now().strftime("%d%m%Y")
print(f"MNTM-{version:03}_{date}", end="")
''')"
echo "VERSION_TAG=${VERSION_TAG}" >> $GITHUB_ENV

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import datetime as dt
import requests import requests
import json import json
import sys import sys
@@ -41,97 +42,16 @@ if __name__ == "__main__":
desc = desc.rsplit("\n", 1)[0] + f"\n+ {count - i} more commits" desc = desc.rsplit("\n", 1)[0] + f"\n+ {count - i} more commits"
break break
url = event["compare"] url = event["compare"]
color = 16723712 if event["forced"] else 3669797 color = 16723712 if event["forced"] else 11761899
# case "pull_request":
# pr = event["pull_request"]
# url = pr["html_url"]
# branch = pr["base"]["ref"] + (
# ""
# if pr["base"]["repo"]["full_name"] != pr["head"]["repo"]["full_name"]
# else f" <- {pr['head']['ref']}"
# )
# name = pr["title"][:50] + ("..." if len(pr["title"]) > 50 else "")
# title = f"Pull Request {event['action'].title()} ({branch}): {name}"
# match event["action"]:
# case "opened":
# desc = (pr["body"][:2045] + "...") if len(pr["body"]) > 2048 else pr["body"]
# color = 3669797
# fields.append(
# {
# "name": "Changed Files:",
# "value": str(pr["changed_files"]),
# "inline": True,
# }
# )
# fields.append(
# {
# "name": "Added:",
# "value": "+" + str(pr["additions"]),
# "inline": True,
# }
# )
# fields.append(
# {
# "name": "Removed:",
# "value": "-" + str(pr["deletions"]),
# "inline": True,
# }
# )
# case "closed":
# color = 16723712
# case "reopened":
# color = 16751872
# case _:
# sys.exit(1)
case "release": case "release":
match event["action"]: webhook = "RELEASE_WEBHOOK"
case "published": color = 9471191
webhook = "RELEASE_WEBHOOK" version_tag = event['release']['tag_name']
color = 13845998 title = f"New Release: `{version_tag}`"
title = f"New Release published: {event['name']}" desc += f"> 💻 [**Web Installer**](https://momentum-fw.dev/update)\n\n"
desc += f"Changelog:" desc += f"> 🐬 [**Changelog & Download**](https://github.com/Next-Flip/Momentum-Firmware/releases/tag/{version_tag})\n\n"
desc += f"> 🛞 [**Project Page**](https://github.com/Next-Flip/Momentum-Firmware)"
changelog = "".join(
event["body"]
.split("Changelog")[1]
.split("<!---")[0]
.split("###")
)
downloads = [
option
for option in [
Type.replace("\n\n>", "")
for Type in event["body"]
.split("Download\n>")[1]
.split("### ")[:3]
]
if option
]
for category in changelog:
group = category.split(":")[0].replace(" ", "")
data = category.split(":")[1:].join(":")
fields.append(
{
"name": {group},
"value": {
(data[:2045] + "...") if len(data) > 2048 else data
},
}
)
fields.append(
{
"name": "Downloads:",
"value": "\n".join(downloads),
"inline": True,
}
)
case _:
sys.exit(1)
case "workflow_run": case "workflow_run":
run = event["workflow_run"] run = event["workflow_run"]
@@ -192,6 +112,7 @@ if __name__ == "__main__":
"url": event["sender"]["html_url"], "url": event["sender"]["html_url"],
"icon_url": event["sender"]["avatar_url"], "icon_url": event["sender"]["avatar_url"],
}, },
"timestamp": dt.datetime.utcnow().isoformat()
} }
], ],
"attachments": [], "attachments": [],

View File

@@ -1,36 +0,0 @@
import nextcloud_client
import requests
import json
import os
if __name__ == "__main__":
client = nextcloud_client.Client(os.environ["NC_HOST"])
client.login(os.environ["NC_USER"], os.environ["NC_PASS"])
file = os.environ["ARTIFACT_TGZ"]
path = f"MNTM-Release/{file}"
try:
client.delete(path)
except Exception:
pass
client.put_file(path, file)
file = file.removesuffix(".tgz") + ".md"
path = path.removesuffix(".tgz") + ".md"
try:
client.delete(path)
except Exception:
pass
client.put_file(path, file)
version = os.environ['VERSION_TAG'].split("_")[0]
files = (
os.environ['ARTIFACT_TGZ'],
os.environ['ARTIFACT_TGZ'].removesuffix(".tgz") + ".md"
)
for file in client.list("MNTM-Release"):
if file.name.startswith(version) and file.name not in files:
try:
client.delete(file.path)
except Exception:
pass

View File

@@ -1,12 +1,11 @@
name: 'Build' name: "Build"
on: on:
push: push:
branches: branches:
- dev - dev
- main
tags: tags:
- '*' - "*"
pull_request: pull_request:
concurrency: concurrency:
@@ -14,54 +13,93 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
TARGETS: f7
DEFAULT_TARGET: f7 DEFAULT_TARGET: f7
FBT_GIT_SUBMODULE_SHALLOW: 1 FBT_GIT_SUBMODULE_SHALLOW: 1
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [f7]
steps: steps:
- name: "Checkout code"
- name: 'Checkout code'
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: "Read version tag" - name: "Get commit details"
run: bash .github/workflow_data/commit.sh id: names
run: |
BUILD_TYPE='DEBUG=0 COMPACT=1'
if [[ ${{ github.event_name }} == 'pull_request' ]]; then
TYPE="pull"
elif [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
TYPE="tag"
else
TYPE="other"
fi
python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE" || cat "${{ github.event_path }}"
echo "event_type=$TYPE" >> $GITHUB_OUTPUT
echo "FBT_BUILD_TYPE=$BUILD_TYPE" >> $GITHUB_ENV
echo "TARGET=${{ matrix.target }}" >> $GITHUB_ENV
echo "TARGET_HW=$(echo "${{ matrix.target }}" | sed 's/f//')" >> $GITHUB_ENV
- name: 'Build the firmware' - name: "Check API versions for consistency between targets"
run: | run: |
set -e set -e
for TARGET in ${TARGETS}; do N_API_HEADER_SIGNATURES=`ls -1 targets/f*/api_symbols.csv | xargs -I {} sh -c "head -n2 {} | md5sum" | sort -u | wc -l`
TARGET_HW="$(echo "${TARGET}" | sed 's/f//')"; \ if [ $N_API_HEADER_SIGNATURES != 1 ] ; then
./fbt TARGET_HW=$TARGET_HW DIST_SUFFIX=$VERSION_TAG updater_package echo API versions aren\'t matching for available targets. Please update!
done echo API versions are:
head -n2 targets/f*/api_symbols.csv
exit 1
fi
- name: "Build the firmware and apps"
id: build-fw
run: |
./fbt TARGET_HW=$TARGET_HW $FBT_BUILD_TYPE updater_package
echo "firmware_api=$(./fbt TARGET_HW=$TARGET_HW get_apiversion)" >> $GITHUB_OUTPUT
- name: "Check for uncommitted changes" - name: "Check for uncommitted changes"
run: | run: |
git diff --exit-code git diff --exit-code
- name: 'Dist artifact' - name: "Upload artifacts to GitHub"
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: dist
path: | path: |
dist/${{ env.DEFAULT_TARGET }}-*/ dist/${TARGET}-*/flipper-z-${TARGET}-update-*
dist/${TARGET}-*/flipper-z-${TARGET}-sdk-*
- name: "Make tgz, zip and sdk" - name: "Copy build output"
run: bash .github/workflow_data/package.sh run: |
set -e
rm -rf artifacts || true
mkdir artifacts
cp dist/${TARGET}-*/flipper-z-${TARGET}-{update,sdk}-* artifacts/
cd dist/${TARGET}-*/${TARGET}-update-*/
ARTIFACT_TAG=flipper-z-"$(basename "$(realpath .)")"
7z a ../../../artifacts/${ARTIFACT_TAG}.zip .
echo "ARTIFACT_TAG=$ARTIFACT_TAG" >> $GITHUB_ENV
# - name: Send devbuild webhook - name: "Upload artifacts to update server"
# if: "github.event_name == 'push' && github.ref_name == 'dev' && !contains(github.event.head_commit.message, '--nobuild')" if: ${{ !github.event.pull_request.head.repo.fork }} && !contains(github.event.head_commit.message, '--nobuild')
# env: run: |
# NC_HOST: "https://cloud.cynthialabs.net/" FILES=$(for ARTIFACT in $(find artifacts -maxdepth 1 -not -type d); do echo "-F files=@${ARTIFACT}"; done)
# NC_USERAGENT: "${{ secrets.NC_USERAGENT }}" curl --fail -L -H "Token: ${{ secrets.INDEXER_TOKEN }}" \
# NC_USER: "${{ secrets.NC_USER }}" -F "branch=${BRANCH_NAME}" \
# NC_PASS: "${{ secrets.NC_PASS }}" -F "version_token=${COMMIT_SHA}" \
# BUILD_WEBHOOK: ${{ secrets.BUILD_WEBHOOK }} ${FILES[@]} \
# run: | "${{ secrets.INDEXER_URL }}"/firmware/uploadfiles
# python -m pip install pyncclient
# python .github/workflow_data/devbuild.py - name: Send devbuild webhook
if: "github.event_name == 'push' && github.ref_name == 'dev' && !contains(github.event.head_commit.message, '--nobuild')"
env:
INDEXER_URL: ${{ secrets.INDEXER_URL }}
BUILD_WEBHOOK: ${{ secrets.BUILD_WEBHOOK }}
run: |
python .github/workflow_data/devbuild.py

View File

@@ -1,4 +1,4 @@
name: 'Generate documentation with Doxygen' name: "Docs"
on: on:
push: push:
@@ -10,21 +10,21 @@ env:
DEFAULT_TARGET: f7 DEFAULT_TARGET: f7
jobs: jobs:
doxygen: docs:
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Wipe workspace' - name: "Wipe workspace"
run: find ./ -mount -maxdepth 1 -exec rm -rf {} \; run: find ./ -mount -maxdepth 1 -exec rm -rf {} \;
- name: 'Checkout code' - name: "Checkout code"
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
fetch-depth: 1 fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: 'Get commit details' - name: "Get commit details"
id: names id: names
run: | run: |
if [[ ${{ github.event_name }} == 'pull_request' ]]; then if [[ ${{ github.event_name }} == 'pull_request' ]]; then
@@ -36,13 +36,13 @@ jobs:
fi fi
python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE" python3 scripts/get_env.py "--event_file=${{ github.event_path }}" "--type=$TYPE"
- name: 'Generate documentation' - name: "Generate documentation"
uses: mattnotmitt/doxygen-action@v1.9.8 uses: mattnotmitt/doxygen-action@v1.9.8
with: with:
working-directory: 'documentation/' working-directory: "documentation/"
doxyfile-path: './doxygen/Doxyfile-awesome.cfg' doxyfile-path: "./doxygen/Doxyfile-awesome.cfg"
- name: 'Upload documentation' - name: "Upload documentation"
uses: jakejarvis/s3-sync-action@v0.5.1 uses: jakejarvis/s3-sync-action@v0.5.1
env: env:
AWS_S3_BUCKET: "${{ secrets.FW_DOCS_AWS_BUCKET }}" AWS_S3_BUCKET: "${{ secrets.FW_DOCS_AWS_BUCKET }}"
@@ -53,4 +53,3 @@ jobs:
DEST_DIR: "${{steps.names.outputs.branch_name}}" DEST_DIR: "${{steps.names.outputs.branch_name}}"
with: with:
args: "--delete" args: "--delete"

View File

@@ -1,12 +1,11 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
branches: branches:
- dev - dev
- main
tags: tags:
- '*' - "*"
pull_request: pull_request:
env: env:
@@ -16,12 +15,11 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "Checkout code"
- name: 'Checkout code'
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: 'Check code formatting' - name: "Check code formatting"
run: ./fbt lint lint_py run: ./fbt lint lint_py

View File

@@ -1,83 +1,49 @@
name: 'Release' name: "Release"
on: on:
pull_request_review: release:
types: [submitted] types:
- released
env:
TARGETS: f7
DEFAULT_TARGET: f7
jobs: jobs:
release: release:
if: |
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name &&
endsWith(github.event.pull_request.title, ' Release') &&
github.event.review.author_association == 'MEMBER' &&
startsWith(github.event.pull_request.title, 'V') &&
github.event.pull_request.base.ref == 'main' &&
github.event.pull_request.head.ref == 'dev' &&
github.event.pull_request.state == 'open' &&
github.event.pull_request.draft == false &&
github.event.review.state == 'APPROVED'
runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
runs-on: ubuntu-latest
steps: steps:
- name: "Checkout code"
- name: 'Checkout code' uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: "Read version tag"
run: bash .github/workflow_data/version.sh
- name: 'Build the firmware'
run: |
set -e
for TARGET in ${TARGETS}; do
TARGET_HW="$(echo "${TARGET}" | sed 's/f//')"; \
./fbt TARGET_HW=$TARGET_HW DIST_SUFFIX=$VERSION_TAG updater_package
done
- name: "Check for uncommitted changes"
run: |
git diff --exit-code
- name: "Make tgz, zip and sdk"
run: bash .github/workflow_data/package.sh
- name: "Update release notes" - name: "Update release notes"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: python .github/workflow_data/release.py run: python .github/workflow_data/release.py
- name: "Upload to webupdater" - name: "Download release assets from tag build"
env:
NC_HOST: "https://cloud.cynthialabs.net/"
NC_USERAGENT: "${{ secrets.NC_USERAGENT }}"
NC_USER: "${{ secrets.NC_USER }}"
NC_PASS: "${{ secrets.NC_PASS }}"
run: | run: |
python -m pip install pyncclient set -e
python .github/workflow_data/webupdater.py wget "${{ secrets.INDEXER_URL }}"/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-update-${{ github.event.release.tag_name }}.tgz
wget "${{ secrets.INDEXER_URL }}"/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-update-${{ github.event.release.tag_name }}.zip
wget "${{ secrets.INDEXER_URL }}"/firmware/${{ github.event.release.tag_name }}/flipper-z-f7-sdk-${{ github.event.release.tag_name }}.zip
- name: "Merge pull request" - name: "Update release with assets and notes"
uses: "pascalgn/automerge-action@v0.15.6" uses: "softprops/action-gh-release@v1"
env: env:
MERGE_LABELS: ""
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: "Make release"
uses: softprops/action-gh-release@v1
with: with:
body_path: ".github/workflow_data/release.md" body_path: .github/workflow_data/release.md
draft: false
prerelease: false
files: | files: |
${{ env.ARTIFACT_TGZ }} flipper-z-f7-update-${{ github.event.release.tag_name }}.tgz
${{ env.ARTIFACT_ZIP }} flipper-z-f7-update-${{ github.event.release.tag_name }}.zip
${{ env.ARTIFACT_SDK }} flipper-z-f7-sdk-${{ github.event.release.tag_name }}.zip
name: "${{ env.VERSION_TAG }}"
tag_name: "${{ env.VERSION_TAG }}" - name: "Trigger reindex"
target_commitish: ${{ github.event.pull_request.base.ref }} run: |
curl --fail -L -H "Token: ${{ secrets.INDEXER_TOKEN }}" \
"${{ secrets.INDEXER_URL }}"/firmware/reindex;
- name: "Send release notification"
env:
RELEASE_WEBHOOK: ${{ secrets.RELEASE_WEBHOOK }}
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: python .github/workflow_data/webhook.py

View File

@@ -1,65 +0,0 @@
name: 'SonarCloud'
on:
workflow_dispatch:
# pull_request:
# types: [opened, synchronize, reopened]
env:
TARGETS: f7
DEFAULT_TARGET: f7
jobs:
sonarcloud:
runs-on: ubuntu-latest
env:
SONAR_SCANNER_VERSION: 4.7.0.2747
SONAR_SERVER_URL: "https://sonarcloud.io"
BUILD_WRAPPER_OUT_DIR: "$HOME/.sonar/build_wrapper_output" # Directory where build-wrapper output will be placed
FBT_NO_SYNC: "true"
steps:
- name: 'Checkout code'
uses: actions/checkout@v3
with:
submodules: 'recursive' # FBT_NO_SYNC is on, get submodules now
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Download and set up sonar-scanner
env:
SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip
run: |
mkdir -p $HOME/.sonar
curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }}
unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH
- name: Download and set up build-wrapper
env:
BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip
run: |
curl -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }}
unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/
echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH
- name: Run build-wrapper
run: |
mkdir ${{ env.BUILD_WRAPPER_OUT_DIR }}
set -e
for TARGET in ${TARGETS}; do
TARGET_HW="$(echo "${TARGET}" | sed 's/f//')"; \
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} ./sonar-build "./fbt TARGET_HW=$TARGET_HW updater_package"
done
- name: Run sonar-scanner
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"

View File

@@ -1,42 +0,0 @@
name: 'Submodules'
on:
push:
branches:
- dev
- main
tags:
- '*'
pull_request:
jobs:
submodules:
runs-on: ubuntu-latest
steps:
- name: 'Checkout code'
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: 'Check protobuf branch'
run: |
git submodule update --init
SUB_PATH="assets/protobuf";
SUB_BRANCH="dev";
SUB_COMMITS_MIN=40;
cd "$SUB_PATH";
SUBMODULE_HASH="$(git rev-parse HEAD)";
BRANCHES=$(git branch -r --contains "$SUBMODULE_HASH");
COMMITS_IN_BRANCH="$(git rev-list --count dev)";
if [ $COMMITS_IN_BRANCH -lt $SUB_COMMITS_MIN ]; then
echo "name=fails::error" >> $GITHUB_OUTPUT
echo "::error::Error: Too low commits in $SUB_BRANCH of submodule $SUB_PATH: $COMMITS_IN_BRANCH(expected $SUB_COMMITS_MIN+)";
exit 1;
fi
if ! grep -q "/$SUB_BRANCH" <<< "$BRANCHES"; then
echo "name=fails::error" >> $GITHUB_OUTPUT
echo "::error::Error: Submodule $SUB_PATH is not on branch $SUB_BRANCH";
exit 1;
fi

View File

@@ -1,15 +1,7 @@
name: 'Webhook' name: "Webhook"
on: on:
push: push:
# pull_request:
# types:
# - "opened"
# - "closed"
# - "reopened"
release:
types:
- "published"
workflow_run: workflow_run:
workflows: workflows:
- "Build" - "Build"
@@ -32,8 +24,7 @@ jobs:
webhook: webhook:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "Checkout code"
- name: 'Checkout code'
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Send webhook - name: Send webhook

View File

@@ -93,12 +93,14 @@ Note that this repo is always updated with the great work from our friends at [U
- More UI customization, redesigns and optimizations - More UI customization, redesigns and optimizations
- Bad-Keyboard App - Bad-Keyboard App
- BLE Spam App - BLE Spam App
- FindMy Flipper App
- NFC Maker App - NFC Maker App
- Wardriver App - Wardriver App
- File Search across SD Card - File Search across SD Card
- Additional NFC parsers and protocols - Additional NFC parsers and protocols
- Subdriving (saving GPS coordinates for Sub-GHz) - Subdriving (saving GPS coordinates for Sub-GHz)
- Easy spoofing (Name, MAC address, Serial number) - Easy spoofing (Name, MAC address, Serial number)
- Video Game Module color configuration right from Flipper
- Enhanced RGB Backlight modes (Full customization & Rainbow mode) - Enhanced RGB Backlight modes (Full customization & Rainbow mode)
- File management on device (Cut, Copy, Paste, Show, New Dir, etc.) - File management on device (Cut, Copy, Paste, Show, New Dir, etc.)
- Remember Infrared GPIO settings and add IR Blaster support in apps - Remember Infrared GPIO settings and add IR Blaster support in apps

View File

@@ -19,9 +19,9 @@ const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) {
case ArchiveFileTypeNFC: case ArchiveFileTypeNFC:
return "NFC"; return "NFC";
case ArchiveFileTypeSubGhz: case ArchiveFileTypeSubGhz:
return "SubGHz"; return "Sub-GHz";
case ArchiveFileTypeLFRFID: case ArchiveFileTypeLFRFID:
return "RFID"; return "125 kHz RFID";
case ArchiveFileTypeInfrared: case ArchiveFileTypeInfrared:
return "Infrared"; return "Infrared";
case ArchiveFileTypeSubghzPlaylist: case ArchiveFileTypeSubghzPlaylist:

View File

@@ -13,7 +13,7 @@ static const char* ArchiveTabNames[] = {
[ArchiveTabIButton] = "iButton", [ArchiveTabIButton] = "iButton",
[ArchiveTabNFC] = "NFC", [ArchiveTabNFC] = "NFC",
[ArchiveTabSubGhz] = "Sub-GHz", [ArchiveTabSubGhz] = "Sub-GHz",
[ArchiveTabLFRFID] = "RFID", [ArchiveTabLFRFID] = "RFID LF",
[ArchiveTabInfrared] = "Infrared", [ArchiveTabInfrared] = "Infrared",
[ArchiveTabBadKb] = "Bad KB", [ArchiveTabBadKb] = "Bad KB",
[ArchiveTabU2f] = "U2F", [ArchiveTabU2f] = "U2F",

View File

@@ -1,6 +1,6 @@
App( App(
appid="lfrfid", appid="lfrfid",
name="RFID", name="125 kHz RFID",
apptype=FlipperAppType.MENUEXTERNAL, apptype=FlipperAppType.MENUEXTERNAL,
targets=["f7"], targets=["f7"],
entry_point="lfrfid_app", entry_point="lfrfid_app",

View File

@@ -17,7 +17,7 @@ bool momentum_app_apply(MomentumApp* app) {
if(app->save_mainmenu_apps) { if(app->save_mainmenu_apps) {
Stream* stream = file_stream_alloc(storage); Stream* stream = file_stream_alloc(storage);
if(file_stream_open(stream, MAINMENU_APPS_PATH, FSAM_READ_WRITE, FSOM_CREATE_ALWAYS)) { if(file_stream_open(stream, MAINMENU_APPS_PATH, FSAM_READ_WRITE, FSOM_CREATE_ALWAYS)) {
stream_write_format(stream, "MenuAppList Version %u\n", 0); stream_write_format(stream, "MenuAppList Version %u\n", 1);
CharList_it_t it; CharList_it_t it;
CharList_it(it, app->mainmenu_app_exes); CharList_it(it, app->mainmenu_app_exes);
for(size_t i = 0; i < CharList_size(app->mainmenu_app_exes); i++) { for(size_t i = 0; i < CharList_size(app->mainmenu_app_exes); i++) {
@@ -249,7 +249,13 @@ MomentumApp* momentum_app_alloc() {
furi_string_replace_all(line, "\n", ""); furi_string_replace_all(line, "\n", "");
CharList_push_back(app->mainmenu_app_exes, strdup(furi_string_get_cstr(line))); CharList_push_back(app->mainmenu_app_exes, strdup(furi_string_get_cstr(line)));
flipper_application_load_name_and_icon(line, storage, NULL, line); flipper_application_load_name_and_icon(line, storage, NULL, line);
if(furi_string_start_with_str(line, "[")) { if(!furi_string_cmp(line, "Momentum")) {
furi_string_set(line, "MNTM");
} else if(!furi_string_cmp(line, "125 kHz RFID")) {
furi_string_set(line, "RFID");
} else if(!furi_string_cmp(line, "Sub-GHz")) {
furi_string_set(line, "SubGHz");
} else if(furi_string_start_with_str(line, "[")) {
size_t trim = furi_string_search_str(line, "] ", 1); size_t trim = furi_string_search_str(line, "] ", 1);
if(trim != FURI_STRING_FAILURE) { if(trim != FURI_STRING_FAILURE) {
furi_string_right(line, trim + 2); furi_string_right(line, trim + 2);
@@ -303,15 +309,16 @@ MomentumApp* momentum_app_alloc() {
app->dolphin_angry = stats.butthurt; app->dolphin_angry = stats.butthurt;
furi_record_close(RECORD_DOLPHIN); furi_record_close(RECORD_DOLPHIN);
if(strcmp(version_get_version(NULL), "MNTM-DEV") == 0) { app->version_tag = furi_string_alloc_printf("%s ", version_get_version(NULL));
app->version_tag = furi_string_alloc_printf("%s ", version_get_version(NULL)); if(furi_string_start_with(app->version_tag, "mntm-dev")) {
furi_string_set(app->version_tag, "MNTM-DEV ");
const char* sha = version_get_githash(NULL); const char* sha = version_get_githash(NULL);
for(size_t i = 0; i < strlen(sha); ++i) { for(size_t i = 0; i < strlen(sha); ++i) {
furi_string_push_back(app->version_tag, toupper(sha[i])); furi_string_push_back(app->version_tag, toupper(sha[i]));
} }
} else { } else {
app->version_tag = furi_string_alloc_printf( furi_string_replace(app->version_tag, "mntm", "MNTM");
"%s %s", version_get_version(NULL), version_get_builddate(NULL)); furi_string_cat(app->version_tag, version_get_builddate(NULL));
} }
return app; return app;

View File

@@ -22,7 +22,7 @@ const char* const menu_style_names[MenuStyleCount] = {
"Vertical", "Vertical",
"C64", "C64",
"Compact", "Compact",
"Terminal", "MNTM",
}; };
static void momentum_app_scene_interface_mainmenu_menu_style_changed(VariableItem* item) { static void momentum_app_scene_interface_mainmenu_menu_style_changed(VariableItem* item) {
MomentumApp* app = variable_item_get_context(item); MomentumApp* app = variable_item_get_context(item);

View File

@@ -14,6 +14,7 @@ void momentum_app_scene_misc_vgm_var_item_list_callback(void* context, uint32_t
const char* const colors_names[VgmColorModeCount] = { const char* const colors_names[VgmColorModeCount] = {
"Default", "Default",
"Custom", "Custom",
"Rainbow",
"RGB Backlight", "RGB Backlight",
}; };
static void momentum_app_scene_misc_vgm_colors_changed(VariableItem* item) { static void momentum_app_scene_misc_vgm_colors_changed(VariableItem* item) {

View File

@@ -1,6 +1,6 @@
App( App(
appid="subghz", appid="subghz",
name="SubGHz", name="Sub-GHz",
apptype=FlipperAppType.APP, apptype=FlipperAppType.APP,
targets=["f7"], targets=["f7"],
entry_point="subghz_app", entry_point="subghz_app",
@@ -23,7 +23,7 @@ App(
App( App(
appid="subghz_fap", appid="subghz_fap",
name="SubGHz", name="Sub-GHz",
apptype=FlipperAppType.EXTERNAL, apptype=FlipperAppType.EXTERNAL,
entry_point="subghz_fap", entry_point="subghz_fap",
stack_size=3 * 1024, stack_size=3 * 1024,

View File

@@ -44,11 +44,23 @@ static void menu_process_left(Menu* menu);
static void menu_process_right(Menu* menu); static void menu_process_right(Menu* menu);
static void menu_process_ok(Menu* menu); static void menu_process_ok(Menu* menu);
static void menu_short_name(MenuItem* item, FuriString* name) { static void menu_get_name(MenuItem* item, FuriString* name, bool shorter) {
furi_string_set(name, item->label); furi_string_set(name, item->label);
if(shorter) {
if(!furi_string_cmp(name, "Momentum")) {
furi_string_set(name, "MNTM");
return;
} else if(!furi_string_cmp(name, "125 kHz RFID")) {
furi_string_set(name, "RFID");
return;
} else if(!furi_string_cmp(name, "Sub-GHz")) {
furi_string_set(name, "SubGHz");
return;
}
}
if(furi_string_start_with_str(name, "[")) { if(furi_string_start_with_str(name, "[")) {
size_t trim = furi_string_search_str(name, "] ", 1); size_t trim = furi_string_search_str(name, "] ", 1);
if(trim != STRING_FAILURE) { if(trim != FURI_STRING_FAILURE) {
furi_string_right(name, trim + 2); furi_string_right(name, trim + 2);
} }
} }
@@ -97,7 +109,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
shift_position = (position + items_count + i - 1) % items_count; shift_position = (position + items_count + i - 1) % items_count;
item = MenuItemArray_get(model->items, shift_position); item = MenuItemArray_get(model->items, shift_position);
menu_centered_icon(canvas, item, 4, 3 + 22 * i, 14, 14); menu_centered_icon(canvas, item, 4, 3 + 22 * i, 14, 14);
menu_short_name(item, name); menu_get_name(item, name, false);
size_t scroll_counter = menu_scroll_counter(model, i == 1); size_t scroll_counter = menu_scroll_counter(model, i == 1);
elements_scrollable_text_line( elements_scrollable_text_line(
canvas, 22, 14 + 22 * i, 98, name, scroll_counter, false); canvas, 22, 14 + 22 * i, 98, name, scroll_counter, false);
@@ -132,7 +144,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
} }
item = MenuItemArray_get(model->items, item_i); item = MenuItemArray_get(model->items, item_i);
menu_centered_icon(canvas, item, x_off, y_off, 40, 20); menu_centered_icon(canvas, item, x_off, y_off, 40, 20);
menu_short_name(item, name); menu_get_name(item, name, true);
size_t scroll_counter = menu_scroll_counter(model, selected); size_t scroll_counter = menu_scroll_counter(model, selected);
elements_scrollable_text_line_centered( elements_scrollable_text_line_centered(
canvas, 20 + x_off, 26 + y_off, 36, name, scroll_counter, false, true); canvas, 20 + x_off, 26 + y_off, 36, name, scroll_counter, false, true);
@@ -174,7 +186,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
canvas_set_color(canvas, ColorBlack); canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontPrimary); canvas_set_font(canvas, FontPrimary);
menu_short_name(item, name); menu_get_name(item, name, false);
size_t scroll_counter = menu_scroll_counter(model, true); size_t scroll_counter = menu_scroll_counter(model, true);
elements_scrollable_text_line_centered( elements_scrollable_text_line_centered(
canvas, canvas,
@@ -225,7 +237,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
canvas_set_color(canvas, ColorBlack); canvas_set_color(canvas, ColorBlack);
canvas_set_font(canvas, FontSecondary); canvas_set_font(canvas, FontSecondary);
menu_short_name(item, name); menu_get_name(item, name, true);
size_t scroll_counter = menu_scroll_counter(model, true); size_t scroll_counter = menu_scroll_counter(model, true);
elements_scrollable_text_line( elements_scrollable_text_line(
canvas, canvas,
@@ -247,6 +259,14 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
case MenuStyleVertical: { case MenuStyleVertical: {
canvas_set_orientation(canvas, CanvasOrientationVertical); canvas_set_orientation(canvas, CanvasOrientationVertical);
shift_position = model->vertical_offset; shift_position = model->vertical_offset;
if(shift_position >= position || shift_position + 7 <= position) {
// In case vertical_offset is out of sync due to changing menu styles
shift_position = CLAMP(
MAX((int32_t)position - 4, 0),
MAX((int32_t)MenuItemArray_size(model->items) - 8, 0),
0);
model->vertical_offset = shift_position;
}
canvas_set_font(canvas, FontSecondary); canvas_set_font(canvas, FontSecondary);
size_t item_i; size_t item_i;
size_t y_off; size_t y_off;
@@ -261,7 +281,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
} }
item = MenuItemArray_get(model->items, item_i); item = MenuItemArray_get(model->items, item_i);
menu_centered_icon(canvas, item, 0, y_off, 16, 16); menu_centered_icon(canvas, item, 0, y_off, 16, 16);
menu_short_name(item, name); menu_get_name(item, name, true);
size_t scroll_counter = menu_scroll_counter(model, selected); size_t scroll_counter = menu_scroll_counter(model, selected);
elements_scrollable_text_line( elements_scrollable_text_line(
canvas, 17, y_off + 12, 46, name, scroll_counter, false); canvas, 17, y_off + 12, 46, name, scroll_counter, false);
@@ -299,7 +319,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
canvas_set_color(canvas, ColorWhite); canvas_set_color(canvas, ColorWhite);
} }
item = MenuItemArray_get(model->items, index); item = MenuItemArray_get(model->items, index);
menu_short_name(item, name); menu_get_name(item, name, true);
char indexstr[5]; char indexstr[5];
snprintf(indexstr, sizeof(indexstr), "%d.", index); snprintf(indexstr, sizeof(indexstr), "%d.", index);
@@ -335,7 +355,7 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
canvas_set_color(canvas, ColorWhite); canvas_set_color(canvas, ColorWhite);
} }
item = MenuItemArray_get(model->items, index); item = MenuItemArray_get(model->items, index);
menu_short_name(item, name); menu_get_name(item, name, true);
elements_scrollable_text_line( elements_scrollable_text_line(
canvas, x_off + 1, y_off + 7, 62, name, scroll_counter, false); canvas, x_off + 1, y_off + 7, 62, name, scroll_counter, false);
@@ -348,47 +368,55 @@ static void menu_draw_callback(Canvas* canvas, void* _model) {
break; break;
} }
case MenuStyleTerminal: { case MenuStyleMNTM: {
// Draw a border around the screen canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 5, 13, "Momentum");
canvas_draw_icon(canvas, 62, 4, &I_Release_arrow_18x15);
canvas_draw_line(canvas, 5, 15, 59, 15);
canvas_draw_line(canvas, 7, 17, 61, 17);
canvas_draw_line(canvas, 10, 19, 63, 19);
char title[20];
snprintf(title, sizeof(title), "%s", furi_hal_version_get_name_ptr());
canvas_draw_str(canvas, 5, 34, title);
DateTime curr_dt;
furi_hal_rtc_get_datetime(&curr_dt);
uint8_t hour = curr_dt.hour;
uint8_t min = curr_dt.minute;
if(hour > 12) {
hour -= 12;
}
if(hour == 0) {
hour = 12;
}
canvas_set_font(canvas, FontSecondary);
char clk[20];
snprintf(clk, sizeof(clk), "%02u:%02u", hour, min);
canvas_draw_str(canvas, 5, 46, clk);
// Draw the selected menu item
MenuItem* item = MenuItemArray_get(model->items, position);
menu_get_name(item, name, true);
elements_bold_rounded_frame(canvas, 42, 23, 35, 33);
menu_centered_icon(canvas, item, 43, 24, 35, 32);
canvas_draw_frame(canvas, 0, 0, 128, 64); canvas_draw_frame(canvas, 0, 0, 128, 64);
// current dir on the title bar uint8_t startY = 15;
canvas_set_font(canvas, FontSecondary); uint8_t itemHeight = 10;
char title[20]; uint8_t itemMaxVisible = 5;
snprintf(title, sizeof(title), "%s@fz: ~/Home", furi_hal_version_get_name_ptr()); size_t endItem = position + itemMaxVisible;
canvas_draw_str(canvas, 20, 10, title); endItem = (endItem > MenuItemArray_size(model->items)) ?
MenuItemArray_size(model->items) :
endItem;
canvas_draw_str(canvas, 118, 9, "x"); // "X" button on the top-right corner for(size_t i = position; i < endItem; i++) {
canvas_draw_frame(canvas, 116, 2, 8, 9); MenuItem* item = MenuItemArray_get(model->items, i);
canvas_draw_frame(canvas, 0, 0, 128, 13); menu_get_name(item, name, true);
uint8_t yPos = startY + ((i - position) * itemHeight);
// Display the user's name line at the bottom size_t scroll_counter = menu_scroll_counter(model, i == position);
canvas_set_font(canvas, FontBatteryPercent); elements_scrollable_text_line(canvas, 83, yPos, 43, name, scroll_counter, false);
char prefix[15];
snprintf(prefix, sizeof(prefix), "%s@fz:~$", furi_hal_version_get_name_ptr());
canvas_draw_str(canvas, 2, 56, prefix);
size_t name_start_x = 2 + (strlen(prefix) - 1) * 6;
for(size_t i = 0; i < 4 && (position + i) < items_count; i++) {
item = MenuItemArray_get(model->items, position + i);
menu_short_name(item, name);
size_t scroll_counter = menu_scroll_counter(model, item);
if(i == 0) {
// Display selected item to the right of the $ symbol
// May want to reduce spacing
elements_scrollable_text_line(
canvas, name_start_x, 56, 60, name, scroll_counter, false);
} else {
// Display the previous items above the user's name line
canvas_draw_str(canvas, 2, 56 - i * 12, item->label);
}
} }
break; break;
} }
default: default:
break; break;
} }
@@ -412,7 +440,7 @@ static bool menu_input_callback(InputEvent* event, void* context) {
} }
} }
if(event->type == InputTypeShort) { if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
switch(event->key) { switch(event->key) {
case InputKeyUp: case InputKeyUp:
menu_process_up(menu); menu_process_up(menu);
@@ -427,25 +455,9 @@ static bool menu_input_callback(InputEvent* event, void* context) {
menu_process_right(menu); menu_process_right(menu);
break; break;
case InputKeyOk: case InputKeyOk:
menu_process_ok(menu); if(event->type != InputTypeRepeat) {
break; menu_process_ok(menu);
default: }
consumed = false;
break;
}
} else if(event->type == InputTypeRepeat) {
switch(event->key) {
case InputKeyUp:
menu_process_up(menu);
break;
case InputKeyDown:
menu_process_down(menu);
break;
case InputKeyLeft:
menu_process_left(menu);
break;
case InputKeyRight:
menu_process_right(menu);
break; break;
default: default:
consumed = false; consumed = false;
@@ -610,19 +622,14 @@ static void menu_process_up(Menu* menu) {
{ {
position = model->position; position = model->position;
size_t count = MenuItemArray_size(model->items); size_t count = MenuItemArray_size(model->items);
size_t vertical_offset = model->vertical_offset;
switch(momentum_settings.menu_style) { switch(momentum_settings.menu_style) {
case MenuStyleList: case MenuStyleList:
case MenuStyleTerminal: case MenuStyleMNTM:
if(position > 0) { if(position > 0) {
position--; position--;
if(vertical_offset && vertical_offset == position) {
vertical_offset--;
}
} else { } else {
position = count - 1; position = count - 1;
vertical_offset = count - 8;
} }
break; break;
case MenuStyleWii: case MenuStyleWii:
@@ -631,7 +638,6 @@ static void menu_process_up(Menu* menu) {
} else { } else {
position++; position++;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
case MenuStyleC64: case MenuStyleC64:
case MenuStyleCompact: case MenuStyleCompact:
@@ -640,14 +646,11 @@ static void menu_process_up(Menu* menu) {
} else { } else {
position = count - 1; position = count - 1;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
default: default:
break; break;
} }
model->vertical_offset = vertical_offset;
}, },
false); false);
menu_set_selected_item(menu, position); menu_set_selected_item(menu, position);
@@ -661,19 +664,14 @@ static void menu_process_down(Menu* menu) {
{ {
position = model->position; position = model->position;
size_t count = MenuItemArray_size(model->items); size_t count = MenuItemArray_size(model->items);
size_t vertical_offset = model->vertical_offset;
switch(momentum_settings.menu_style) { switch(momentum_settings.menu_style) {
case MenuStyleList: case MenuStyleList:
case MenuStyleTerminal: case MenuStyleMNTM:
if(position < count - 1) { if(position < count - 1) {
position++; position++;
if(vertical_offset < count - 8 && vertical_offset == position - 7) {
vertical_offset++;
}
} else { } else {
position = 0; position = 0;
vertical_offset = 0;
} }
break; break;
case MenuStyleWii: case MenuStyleWii:
@@ -682,7 +680,6 @@ static void menu_process_down(Menu* menu) {
} else { } else {
position++; position++;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
case MenuStyleC64: case MenuStyleC64:
case MenuStyleCompact: case MenuStyleCompact:
@@ -691,14 +688,11 @@ static void menu_process_down(Menu* menu) {
} else { } else {
position = 0; position = 0;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
default: default:
break; break;
} }
model->vertical_offset = vertical_offset;
}, },
false); false);
menu_set_selected_item(menu, position); menu_set_selected_item(menu, position);
@@ -712,7 +706,6 @@ static void menu_process_left(Menu* menu) {
{ {
position = model->position; position = model->position;
size_t count = MenuItemArray_size(model->items); size_t count = MenuItemArray_size(model->items);
size_t vertical_offset = model->vertical_offset;
switch(momentum_settings.menu_style) { switch(momentum_settings.menu_style) {
case MenuStyleWii: case MenuStyleWii:
@@ -725,11 +718,11 @@ static void menu_process_left(Menu* menu) {
} else { } else {
position -= 2; position -= 2;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
case MenuStyleDsi: case MenuStyleDsi:
case MenuStylePs4: case MenuStylePs4:
case MenuStyleVertical: case MenuStyleVertical:
size_t vertical_offset = model->vertical_offset;
if(position > 0) { if(position > 0) {
position--; position--;
if(vertical_offset && vertical_offset == position) { if(vertical_offset && vertical_offset == position) {
@@ -739,6 +732,7 @@ static void menu_process_left(Menu* menu) {
position = count - 1; position = count - 1;
vertical_offset = count - 8; vertical_offset = count - 8;
} }
model->vertical_offset = vertical_offset;
break; break;
case MenuStyleC64: case MenuStyleC64:
if((position % 10) < 5) { if((position % 10) < 5) {
@@ -746,7 +740,6 @@ static void menu_process_left(Menu* menu) {
} else { } else {
position = position - 5; position = position - 5;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
case MenuStyleCompact: case MenuStyleCompact:
if((position % 16) < 8) { if((position % 16) < 8) {
@@ -754,14 +747,11 @@ static void menu_process_left(Menu* menu) {
} else { } else {
position = position - 8; position = position - 8;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
default: default:
break; break;
} }
model->vertical_offset = vertical_offset;
}, },
false); false);
menu_set_selected_item(menu, position); menu_set_selected_item(menu, position);
@@ -775,7 +765,6 @@ static void menu_process_right(Menu* menu) {
{ {
position = model->position; position = model->position;
size_t count = MenuItemArray_size(model->items); size_t count = MenuItemArray_size(model->items);
size_t vertical_offset = model->vertical_offset;
switch(momentum_settings.menu_style) { switch(momentum_settings.menu_style) {
case MenuStyleWii: case MenuStyleWii:
@@ -793,11 +782,11 @@ static void menu_process_right(Menu* menu) {
position = position % 2; position = position % 2;
} }
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
case MenuStyleDsi: case MenuStyleDsi:
case MenuStylePs4: case MenuStylePs4:
case MenuStyleVertical: case MenuStyleVertical:
size_t vertical_offset = model->vertical_offset;
if(position < count - 1) { if(position < count - 1) {
position++; position++;
if(vertical_offset < count - 8 && vertical_offset == position - 7) { if(vertical_offset < count - 8 && vertical_offset == position - 7) {
@@ -807,6 +796,7 @@ static void menu_process_right(Menu* menu) {
position = 0; position = 0;
vertical_offset = 0; vertical_offset = 0;
} }
model->vertical_offset = vertical_offset;
break; break;
case MenuStyleC64: case MenuStyleC64:
if((position % 10) < 5) { if((position % 10) < 5) {
@@ -814,7 +804,6 @@ static void menu_process_right(Menu* menu) {
} else { } else {
position = position - 5; position = position - 5;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
case MenuStyleCompact: case MenuStyleCompact:
if((position % 16) < 8) { if((position % 16) < 8) {
@@ -822,14 +811,11 @@ static void menu_process_right(Menu* menu) {
} else { } else {
position = position - 8; position = position - 8;
} }
vertical_offset = CLAMP(MAX((int)position - 4, 0), MAX((int)count - 8, 0), 0);
break; break;
default: default:
break; break;
} }
model->vertical_offset = vertical_offset;
}, },
false); false);
menu_set_selected_item(menu, position); menu_set_selected_item(menu, position);

View File

@@ -208,7 +208,7 @@ static void loader_make_menu_file(Storage* storage) {
Stream* new = file_stream_alloc(storage); Stream* new = file_stream_alloc(storage);
if(!storage_file_exists(storage, MAINMENU_APPS_PATH)) { if(!storage_file_exists(storage, MAINMENU_APPS_PATH)) {
if(file_stream_open(new, MAINMENU_APPS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { if(file_stream_open(new, MAINMENU_APPS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
stream_write_format(new, "MenuAppList Version %u\n", 0); stream_write_format(new, "MenuAppList Version %u\n", 1);
for(size_t i = 0; i < FLIPPER_APPS_COUNT; i++) { for(size_t i = 0; i < FLIPPER_APPS_COUNT; i++) {
stream_write_format(new, "%s\n", FLIPPER_APPS[i].name); stream_write_format(new, "%s\n", FLIPPER_APPS[i].name);
} }
@@ -258,7 +258,7 @@ static Loader* loader_alloc() {
uint32_t version; uint32_t version;
if(!stream_read_line(stream, line) || if(!stream_read_line(stream, line) ||
sscanf(furi_string_get_cstr(line), "MenuAppList Version %lu", &version) != 1 || sscanf(furi_string_get_cstr(line), "MenuAppList Version %lu", &version) != 1 ||
version > 0) { version > 1) {
file_stream_close(stream); file_stream_close(stream);
storage_common_remove(storage, MAINMENU_APPS_PATH); storage_common_remove(storage, MAINMENU_APPS_PATH);
loader_make_menu_file(storage); loader_make_menu_file(storage);
@@ -266,13 +266,20 @@ static Loader* loader_alloc() {
break; break;
if(!stream_read_line(stream, line) || if(!stream_read_line(stream, line) ||
sscanf(furi_string_get_cstr(line), "MenuAppList Version %lu", &version) != 1 || sscanf(furi_string_get_cstr(line), "MenuAppList Version %lu", &version) != 1 ||
version > 0) version > 1)
break; break;
} }
while(stream_read_line(stream, line)) { while(stream_read_line(stream, line)) {
furi_string_replace_all(line, "\r", ""); furi_string_replace_all(line, "\r", "");
furi_string_replace_all(line, "\n", ""); furi_string_replace_all(line, "\n", "");
if(version == 0) {
if(!furi_string_cmp(line, "RFID")) {
furi_string_set(line, "125 kHz RFID");
} else if(!furi_string_cmp(line, "SubGHz")) {
furi_string_set(line, "Sub-GHz");
}
}
const char* label = NULL; const char* label = NULL;
const Icon* icon = NULL; const Icon* icon = NULL;
const char* exe = NULL; const char* exe = NULL;
@@ -574,15 +581,10 @@ static LoaderStatus loader_do_start_by_name(
break; break;
} }
// Translate app names (mainly for RPC, thanks OFW for not using a smart system like appid's :/) // Translate app names (mainly for RPC)
if(!strncmp(name, "Bad USB", strlen("Bad USB"))) if(!strncmp(name, "Bad USB", strlen("Bad USB"))) {
name = "Bad KB"; name = "Bad KB";
else if(!strncmp(name, "Applications", strlen("Applications"))) }
name = "Apps";
else if(!strncmp(name, "125 kHz RFID", strlen("125 kHz RFID")))
name = "RFID";
else if(!strncmp(name, "Sub-GHz", strlen("Sub-GHz")))
name = "SubGHz";
// check internal apps // check internal apps
{ {

View File

@@ -6,6 +6,7 @@ App(
"updater_app", "updater_app",
"storage_move_to_sd", "storage_move_to_sd",
"js_app", "js_app",
"findmy_startup",
# "archive", # "archive",
], ],
) )

View File

@@ -0,0 +1,74 @@
# FindMy Flipper - FindMy SmartTag Emulator
This app extends the functionality of the FlipperZero's bluetooth capabilities, enabling it to act as an Apple AirTag or Samsung SmartTag, or even both simultaneously. It utilizes the FlipperZero's BLE beacon to broadcast a SmartTag signal to be picked up by the FindMy Network. I made this to serve as a versatile tool for tracking purposes, offering the ability to clone existing tags, generate OpenHaystack key pairs for integration with Apple's FindMy network, and tune the device's beacon broadcast settings.
## Features
1. Tag Emulation: Clone your existing Apple AirTag or Samsung SmartTag to the FlipperZero, or generate a key pair for use with the FindMy network without owning an actual AirTag.
2. Customization: Users can adjust the interval between beacon broadcasts and modify the transmit power to suit their needs, optimizing for both visibility and battery life.
3. Efficient Background Operation: The app is optimized to run in the background, ensuring that your FlipperZero can still be tracked with minimal battery usage and without stopping normal use.
## Usage Guide
### Step 1: Installation
- **Option A:** Use the released/precompiled firmware appropriate (FAP) for your device.
- **Option B:** Build the firmware yourself using `fbt/ufbt`.
- Both Installation options require you to be running a dev build of firmware. When release gets access to the extra BLE beacon this will change, thank you!
### Step 2: Obtaining SmartTag Data
#### Option A: Open Haystack Method
1. **Generate a Tag:** Download the `generate_keys.py` file and execute it in your terminal. (You will need cryptography ```python3 -m pip install cryptography```)
2. **Follow Prompts:** During execution, you'll be prompted for inputs. By the end, you'll obtain a **Private Key**, **Public Key**, **Payload**, and **MAC Address**.
- **Private Key** is necessary to receive location reports from Apple.
- **MAC Address** should be registered in the FlipperZero app:
1. Open the app and navigate to the config menu.
2. Choose "register tag" and enter the MAC Address when prompted.
3. A payload dialog will appear next. Enter your **Payload** here.
4. Click save.
3. **Configuration Completion:** With this setup, your device is ready for Open Haystack. Proceed with the specific steps for Open Haystack or MaclessHaystack based on your setup.
- Don't Own a Mac: https://github.com/dchristl/macless-haystack
- Own a Mac: https://github.com/seemoo-lab/openhaystack
#### Option B: Cloning Existing Tag
1. **Pair a Tag:** First, pair an AirTag or Samsung SmartTag with your device.
2. **Enter 'Lost' Mode:** Keep the tag away from the device it's registered to for approximately 15 minutes.
3. **Download nrfConnect:** Install nrfConnect from the Apple App Store or Google Play Store.
4. **Filter and Scan:**
- Open the app, click on filters, and exclude all except for the brand of your tag (Apple/Samsung).
- Adjust the RSSI to the lowest setting (-40 dBm).
- Initiate a scan. Wait for your SmartTag to appear as a "FindMy" device.
5. **Capture Data:** Click **Raw** or **View Raw** to capture your **payload** and note your tag's **MAC Address**. Immediately remove the tag's battery to prevent key/MAC rotation.
6. **Enter Data in FlipperZero App:** Input the captured **payload** and **MAC Address** into the FlipperZero app.
### Step 3: Configuration
- Upon launching the app, choose whether to clone an AirTag or SmartTag, generate a new Open Haystack key pair, or adjust broadcast settings.
### Step 4: Tracking
- Once the app is configured, your FlipperZero can be tracked using the relevant platform's tracking service (FindMy app for Apple devices, SmartThings for Samsung devices, and respective web browsers).
Customization
- Beacon Interval: Adjust how frequently your FlipperZero broadcasts its presence.
- Transmit Power: Increase or decrease the signal strength to balance between tracking range and battery life.
Background Use
The app is designed to have a negligible impact on battery life, even when running in the background. This allows for continuous tracking without the need for frequent recharging.
Compatibility
- Apple devices for AirTag tracking via the FindMy network.
- Any device that supports Samsung SmartTag tracking, including web browsers (previously FindMyMobile).
Thanks
- Huge thanks to all the people that contributed to the OpenHaystack project, supporting projects, and guides on the subject. This wouldn't be a thing without any of you!
Legal and Privacy
This app is intended for personal and educational use. Users are responsible for complying with local privacy laws and regulations regarding tracking devices. The cloning and emulation of tracking tags should be done responsibly and with respect to the ownership of the original devices.
Disclaimer
This project is not affiliated with Apple Inc. or Samsung. All product names, logos, and brands are property of their respective owners. Use this app responsibly and ethically.

View File

@@ -4,11 +4,21 @@ App(
apptype=FlipperAppType.EXTERNAL, apptype=FlipperAppType.EXTERNAL,
entry_point="findmy_main", entry_point="findmy_main",
requires=["gui"], requires=["gui"],
stack_size=1 * 1024, stack_size=2 * 1024,
order=35,
fap_icon="location_icon.png", fap_icon="location_icon.png",
fap_icon_assets="icons",
fap_category="Bluetooth", fap_category="Bluetooth",
fap_author="@MatthewKuKanich", fap_author="@MatthewKuKanich",
fap_weburl="https://github.com/MatthewKuKanich/FindMyFlipper",
fap_version="1.0", fap_version="1.0",
fap_description="BLE FindMy Location Beacon", fap_description="BLE FindMy Location Beacon",
) )
App(
appid="findmy_startup",
targets=["f7"],
apptype=FlipperAppType.STARTUP,
entry_point="findmy_startup",
sources=["findmy_startup.c", "findmy_state.c"],
order=1000,
)

View File

@@ -16,6 +16,8 @@ static FindMy* findmy_app_alloc() {
FindMy* app = malloc(sizeof(FindMy)); FindMy* app = malloc(sizeof(FindMy));
app->gui = furi_record_open(RECORD_GUI); app->gui = furi_record_open(RECORD_GUI);
app->storage = furi_record_open(RECORD_STORAGE);
app->dialogs = furi_record_open(RECORD_DIALOGS);
app->view_dispatcher = view_dispatcher_alloc(); app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_enable_queue(app->view_dispatcher); view_dispatcher_enable_queue(app->view_dispatcher);
@@ -41,15 +43,17 @@ static FindMy* findmy_app_alloc() {
FindMyViewVarItemList, FindMyViewVarItemList,
variable_item_list_get_view(app->var_item_list)); variable_item_list_get_view(app->var_item_list));
app->popup = popup_alloc();
view_dispatcher_add_view(app->view_dispatcher, FindMyViewPopup, popup_get_view(app->popup));
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
app->beacon_active = false; findmy_state_load(&app->state);
findmy_main_update_active(app->findmy_main, app->beacon_active); findmy_state_apply(&app->state);
app->broadcast_interval = 5;
findmy_main_update_interval(app->findmy_main, app->broadcast_interval); findmy_main_update_active(app->findmy_main, furi_hal_bt_extra_beacon_is_active());
app->transmit_power = 6; findmy_main_update_interval(app->findmy_main, app->state.broadcast_interval);
app->apple = true; findmy_main_update_type(app->findmy_main, findmy_data_get_type(app->state.data));
findmy_main_update_apple(app->findmy_main, app->apple);
return app; return app;
} }
@@ -57,6 +61,9 @@ static FindMy* findmy_app_alloc() {
static void findmy_app_free(FindMy* app) { static void findmy_app_free(FindMy* app) {
furi_assert(app); furi_assert(app);
view_dispatcher_remove_view(app->view_dispatcher, FindMyViewPopup);
popup_free(app->popup);
view_dispatcher_remove_view(app->view_dispatcher, FindMyViewVarItemList); view_dispatcher_remove_view(app->view_dispatcher, FindMyViewVarItemList);
variable_item_list_free(app->var_item_list); variable_item_list_free(app->var_item_list);
@@ -69,48 +76,17 @@ static void findmy_app_free(FindMy* app) {
view_dispatcher_free(app->view_dispatcher); view_dispatcher_free(app->view_dispatcher);
scene_manager_free(app->scene_manager); scene_manager_free(app->scene_manager);
furi_record_close(RECORD_DIALOGS);
furi_record_close(RECORD_STORAGE);
furi_record_close(RECORD_GUI); furi_record_close(RECORD_GUI);
free(app); free(app);
} }
static void findmy_start(FindMy* app) {
furi_hal_bt_extra_beacon_stop(); // Stop any running beacon
app->config.min_adv_interval_ms = app->broadcast_interval * 1000; // Converting s to ms
app->config.max_adv_interval_ms = (app->broadcast_interval * 1000) + 150;
app->config.adv_channel_map = GapAdvChannelMapAll;
app->config.adv_power_level = GapAdvPowerLevel_0dBm + app->transmit_power;
app->config.address_type = GapAddressTypePublic;
uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE] = {0x4D, 0x61, 0x74, 0x4B, 0x75, 0x4B};
furi_hal_bt_reverse_mac_addr(mac);
memcpy(&app->config.address, mac, sizeof(app->config.address));
furi_check(furi_hal_bt_extra_beacon_set_config(&app->config));
uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE];
uint8_t* it = data;
// For Apple AirTags
*it++ = 0x1E; // Length
*it++ = 0xFF; // Manufacturer Specific Data
*it++ = 0x4C; // Company ID (Apple, Inc.)
*it++ = 0x00; // State
*it++ = 0x12; // Data - Public Key without the MAC address
*it++ = 0x81; // ...
*it++ = 0xB9; // ...
*it++ = 0x02; // First 2 bits are the version, the rest is the battery level
*it++ = 0x7E; // Hint (0x00)
furi_check(furi_hal_bt_extra_beacon_set_data(data, it - data));
}
int32_t findmy_main(void* p) { int32_t findmy_main(void* p) {
UNUSED(p); UNUSED(p);
FindMy* app = findmy_app_alloc(); FindMy* app = findmy_app_alloc();
findmy_start(app);
scene_manager_next_scene(app->scene_manager, FindMySceneMain); scene_manager_next_scene(app->scene_manager, FindMySceneMain);
view_dispatcher_run(app->view_dispatcher); view_dispatcher_run(app->view_dispatcher);
@@ -123,16 +99,16 @@ void findmy_change_broadcast_interval(FindMy* app, uint8_t value) {
if(value > 10 || value < 1) { if(value > 10 || value < 1) {
return; return;
} }
app->broadcast_interval = value; app->state.broadcast_interval = value;
findmy_main_update_interval(app->findmy_main, app->broadcast_interval); findmy_state_sync_config(&app->state);
if(app->beacon_active) { findmy_state_save(&app->state);
findmy_main_update_interval(app->findmy_main, app->state.broadcast_interval);
if(furi_hal_bt_extra_beacon_is_active()) {
// Always check if beacon is active before changing config // Always check if beacon is active before changing config
furi_check(furi_hal_bt_extra_beacon_stop()); furi_check(furi_hal_bt_extra_beacon_stop());
} }
app->config.min_adv_interval_ms = app->broadcast_interval * 1000; furi_check(furi_hal_bt_extra_beacon_set_config(&app->state.config));
app->config.max_adv_interval_ms = app->config.min_adv_interval_ms + 150; if(app->state.beacon_active) {
furi_check(furi_hal_bt_extra_beacon_set_config(&app->config));
if(app->beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start()); furi_check(furi_hal_bt_extra_beacon_start());
} }
} }
@@ -141,24 +117,40 @@ void findmy_change_transmit_power(FindMy* app, uint8_t value) {
if(value > 6) { if(value > 6) {
return; return;
} }
app->transmit_power = value; app->state.transmit_power = value;
if(app->beacon_active) { findmy_state_sync_config(&app->state);
findmy_state_save(&app->state);
if(furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_stop()); furi_check(furi_hal_bt_extra_beacon_stop());
} }
app->config.adv_power_level = GapAdvPowerLevel_0dBm + app->transmit_power; furi_check(furi_hal_bt_extra_beacon_set_config(&app->state.config));
furi_check(furi_hal_bt_extra_beacon_set_config(&app->config)); if(app->state.beacon_active) {
if(app->beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start()); furi_check(furi_hal_bt_extra_beacon_start());
} }
} }
void findmy_toggle_beacon(FindMy* app) { void findmy_toggle_beacon(FindMy* app) {
app->beacon_active = !app->beacon_active; app->state.beacon_active = !app->state.beacon_active;
findmy_main_update_active(app->findmy_main, app->beacon_active); findmy_state_save(&app->state);
findmy_main_update_apple(app->findmy_main, app->apple); if(furi_hal_bt_extra_beacon_is_active()) {
if(app->beacon_active) { furi_check(furi_hal_bt_extra_beacon_stop());
furi_hal_bt_extra_beacon_start(); }
if(app->state.beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
findmy_main_update_active(app->findmy_main, furi_hal_bt_extra_beacon_is_active());
}
FindMyType findmy_data_get_type(uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE]) {
if(data[0] == 0x1E && // Length
data[1] == 0xFF && // Manufacturer Specific Data
data[2] == 0x4C && // Company ID (Apple, Inc.)
data[3] == 0x00 && // ...
data[4] == 0x12 && // Type (FindMy)
data[5] == 0x19 // Length
) {
return FindMyTypeApple;
} else { } else {
furi_hal_bt_extra_beacon_stop(); return FindMyTypeSamsung;
} }
} }

View File

@@ -1,3 +1,5 @@
#pragma once #pragma once
typedef struct FindMy FindMy; typedef struct FindMy FindMy;
typedef enum FindMyType FindMyType;

View File

@@ -1,42 +1,57 @@
#pragma once #pragma once
#include "findmy.h" #include "findmy.h"
#include "findmy_state.h"
#include <furi_hal_bt.h> #include <furi_hal_bt.h>
#include <extra_beacon.h> #include <extra_beacon.h>
#include <assets_icons.h> #include <assets_icons.h>
#include "findmy_icons.h"
#include <toolbox/stream/file_stream.h>
#include <toolbox/hex.h>
#include <toolbox/path.h>
#include <gui/gui.h> #include <gui/gui.h>
#include <storage/storage.h>
#include <dialogs/dialogs.h>
#include <gui/scene_manager.h> #include <gui/scene_manager.h>
#include <gui/view_dispatcher.h> #include <gui/view_dispatcher.h>
#include "views/findmy_main.h" #include "views/findmy_main.h"
#include <gui/modules/byte_input.h> #include <gui/modules/byte_input.h>
#include <gui/modules/variable_item_list.h> #include <gui/modules/variable_item_list.h>
#include <gui/modules/popup.h>
#include "scenes/findmy_scene.h" #include "scenes/findmy_scene.h"
#include "helpers/base64.h"
struct FindMy { struct FindMy {
Gui* gui; Gui* gui;
Storage* storage;
DialogsApp* dialogs;
SceneManager* scene_manager; SceneManager* scene_manager;
ViewDispatcher* view_dispatcher; ViewDispatcher* view_dispatcher;
FindMyMain* findmy_main; FindMyMain* findmy_main;
ByteInput* byte_input; ByteInput* byte_input;
VariableItemList* var_item_list; VariableItemList* var_item_list;
Popup* popup;
uint8_t mac_buf[EXTRA_BEACON_MAC_ADDR_SIZE]; uint8_t mac_buf[EXTRA_BEACON_MAC_ADDR_SIZE];
uint8_t packet_buf[EXTRA_BEACON_MAX_DATA_SIZE]; uint8_t packet_buf[EXTRA_BEACON_MAX_DATA_SIZE];
GapExtraBeaconConfig config; FindMyState state;
bool apple;
bool beacon_active;
uint8_t broadcast_interval;
uint8_t transmit_power;
}; };
typedef enum { typedef enum {
FindMyViewMain, FindMyViewMain,
FindMyViewByteInput, FindMyViewByteInput,
FindMyViewVarItemList, FindMyViewVarItemList,
FindMyViewPopup,
} FindMyView; } FindMyView;
enum FindMyType {
FindMyTypeApple,
FindMyTypeSamsung,
};
void findmy_change_broadcast_interval(FindMy* app, uint8_t value); void findmy_change_broadcast_interval(FindMy* app, uint8_t value);
void findmy_change_transmit_power(FindMy* app, uint8_t value); void findmy_change_transmit_power(FindMy* app, uint8_t value);
void findmy_toggle_beacon(FindMy* app); void findmy_toggle_beacon(FindMy* app);
FindMyType findmy_data_get_type(uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE]);

View File

@@ -0,0 +1,11 @@
#include "findmy_state.h"
#include <furi_hal.h>
void findmy_startup() {
if(!furi_hal_is_normal_boot()) return;
FindMyState state;
if(findmy_state_load(&state)) {
findmy_state_apply(&state);
}
}

View File

@@ -0,0 +1,130 @@
#include "findmy_state.h"
#include <string.h>
#include <stddef.h>
#include <furi_hal_bt.h>
#include <flipper_format/flipper_format.h>
bool findmy_state_load(FindMyState* out_state) {
FindMyState state;
// Try to load from file
bool loaded_from_file = false;
Storage* storage = furi_record_open(RECORD_STORAGE);
if(storage_file_exists(storage, FINDMY_STATE_PATH)) {
FlipperFormat* file = flipper_format_file_alloc(storage);
do {
uint32_t tmp;
FuriString* str = furi_string_alloc();
if(!flipper_format_file_open_existing(file, FINDMY_STATE_PATH)) break;
if(!flipper_format_read_header(file, str, &tmp)) break;
if(furi_string_cmp_str(str, FINDMY_STATE_HEADER)) break;
if(tmp != FINDMY_STATE_VER) break;
if(!flipper_format_read_bool(file, "beacon_active", &state.beacon_active, 1)) break;
if(!flipper_format_read_uint32(file, "broadcast_interval", &tmp, 1)) break;
state.broadcast_interval = tmp;
if(!flipper_format_read_uint32(file, "transmit_power", &tmp, 1)) break;
state.transmit_power = tmp;
if(!flipper_format_read_hex(file, "mac", state.mac, sizeof(state.mac))) break;
if(!flipper_format_read_hex(file, "data", state.data, sizeof(state.data))) break;
loaded_from_file = true;
} while(0);
flipper_format_free(file);
}
furi_record_close(RECORD_STORAGE);
// Otherwise set default values
if(!loaded_from_file) {
state.beacon_active = false;
state.broadcast_interval = 5;
state.transmit_power = 6;
// Set default mac
uint8_t default_mac[EXTRA_BEACON_MAC_ADDR_SIZE] = {0x66, 0x55, 0x44, 0x33, 0x22, 0x11};
memcpy(state.mac, default_mac, sizeof(state.mac));
// Set default empty AirTag data
uint8_t* data = state.data;
*data++ = 0x1E; // Length
*data++ = 0xFF; // Manufacturer Specific Data
*data++ = 0x4C; // Company ID (Apple, Inc.)
*data++ = 0x00; // ...
*data++ = 0x12; // Type (FindMy)
*data++ = 0x19; // Length
*data++ = 0x00; // Status
// Placeholder Empty Public Key without the MAC address
for(size_t i = 0; i < 22; ++i) {
*data++ = 0x00;
}
*data++ = 0x00; // First 2 bits are the version, the rest is the battery level
*data++ = 0x00; // Hint (0x00)
}
// Sync values to config
findmy_state_sync_config(&state);
// Set constants
state.config.adv_channel_map = GapAdvChannelMapAll;
state.config.address_type = GapAddressTypePublic;
// Copy to caller state before popping stack
memcpy(out_state, &state, sizeof(state));
// Return if active, can be used to start after loading in an if statement
return state.beacon_active;
}
void findmy_state_apply(FindMyState* state) {
// Stop any running beacon
if(furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_stop());
}
furi_check(furi_hal_bt_extra_beacon_set_config(&state->config));
furi_check(furi_hal_bt_extra_beacon_set_data(state->data, sizeof(state->data)));
if(state->beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
}
void findmy_state_sync_config(FindMyState* state) {
state->config.min_adv_interval_ms = state->broadcast_interval * 1000; // Converting s to ms
state->config.max_adv_interval_ms = (state->broadcast_interval * 1000) + 150;
state->config.adv_power_level = GapAdvPowerLevel_0dBm + state->transmit_power;
memcpy(state->config.address, state->mac, sizeof(state->config.address));
}
void findmy_state_save(FindMyState* state) {
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, FINDMY_STATE_DIR);
FlipperFormat* file = flipper_format_file_alloc(storage);
do {
uint32_t tmp;
if(!flipper_format_file_open_always(file, FINDMY_STATE_PATH)) break;
if(!flipper_format_write_header_cstr(file, FINDMY_STATE_HEADER, FINDMY_STATE_VER)) break;
if(!flipper_format_write_bool(file, "beacon_active", &state->beacon_active, 1)) break;
tmp = state->broadcast_interval;
if(!flipper_format_write_uint32(file, "broadcast_interval", &tmp, 1)) break;
tmp = state->transmit_power;
if(!flipper_format_write_uint32(file, "transmit_power", &tmp, 1)) break;
if(!flipper_format_write_hex(file, "mac", state->mac, sizeof(state->mac))) break;
if(!flipper_format_write_hex(file, "data", state->data, sizeof(state->data))) break;
} while(0);
flipper_format_free(file);
furi_record_close(RECORD_STORAGE);
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include <extra_beacon.h>
#define FINDMY_STATE_HEADER "FindMy Flipper State"
#define FINDMY_STATE_VER 1
#define FINDMY_STATE_DIR EXT_PATH("apps_data/findmy")
#define FINDMY_STATE_PATH FINDMY_STATE_DIR "/findmy_state.txt"
typedef struct {
bool beacon_active;
uint8_t broadcast_interval;
uint8_t transmit_power;
uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE];
uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE];
// Generated from the other state values
GapExtraBeaconConfig config;
} FindMyState;
bool findmy_state_load(FindMyState* out_state);
void findmy_state_apply(FindMyState* state);
void findmy_state_sync_config(FindMyState* state);
void findmy_state_save(FindMyState* state);

View File

@@ -0,0 +1,112 @@
import base64
import os
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
def advertisement_template():
adv = ""
adv += "1e" # length (30)
adv += "ff" # manufacturer specific data
adv += "4c00" # company ID (Apple)
adv += "1219" # offline finding type and length
adv += "00" # state
for _ in range(22):
adv += "00"
adv += "00" # first two bits of key[0]
adv += "00" # hint
return bytearray.fromhex(adv)
def convert_key_to_hex(private_key, public_key):
private_key_hex = (
private_key.private_numbers().private_value.to_bytes(28, byteorder="big").hex()
)
public_key_hex = public_key.public_numbers().x.to_bytes(28, byteorder="big").hex()
return private_key_hex, public_key_hex
def generate_mac_and_payload(public_key):
key = public_key.public_numbers().x.to_bytes(28, byteorder="big")
addr = bytearray(key[:6])
addr[0] |= 0b11000000
adv = advertisement_template()
adv[7:29] = key[6:28]
adv[29] = key[0] >> 6
return addr.hex(), adv.hex()
def main():
nkeys = int(input("Enter the number of keys to generate: "))
prefix = input("Enter a name for the keyfiles (optional, press enter to skip): ")
print()
if not os.path.exists("keys"):
os.makedirs("keys")
for i in range(nkeys):
while True:
private_key = ec.generate_private_key(ec.SECP224R1(), default_backend())
public_key = private_key.public_key()
private_key_bytes = private_key.private_numbers().private_value.to_bytes(
28, byteorder="big"
)
public_key_bytes = public_key.public_numbers().x.to_bytes(
28, byteorder="big"
)
private_key_b64 = base64.b64encode(private_key_bytes).decode("ascii")
public_key_b64 = base64.b64encode(public_key_bytes).decode("ascii")
private_key_hex, public_key_hex = convert_key_to_hex(
private_key, public_key
)
mac, payload = generate_mac_and_payload(public_key)
public_key_hash = hashes.Hash(hashes.SHA256())
public_key_hash.update(public_key_bytes)
s256_b64 = base64.b64encode(public_key_hash.finalize()).decode("ascii")
if "/" not in s256_b64[:7]:
fname = (
f"{prefix}_{s256_b64[:7]}.keys"
if prefix
else f"{s256_b64[:7]}.keys"
)
print(f"{i + 1})")
print("Private key (Base64):", private_key_b64)
print("Public key (Base64):", public_key_b64)
print("Hashed adv key (Base64):", s256_b64)
print(
"---------------------------------------------------------------------------------"
)
print("Private key (Hex):", private_key_hex)
print("Public key (Hex):", public_key_hex)
print(
"---------------------------------------------------------------------------------"
)
print("MAC:", mac)
print("Payload:", payload)
print()
print(
"Place the .keys file onto your Flipper or input the MAC and Payload manually."
)
with open(f"keys/{fname}", "w") as f:
f.write(f"Private key: {private_key_b64}\n")
f.write(f"Public key: {public_key_b64}\n")
f.write(f"Hashed adv key: {s256_b64}\n")
f.write(f"Private key (Hex): {private_key_hex}\n")
f.write(f"Public key (Hex): {public_key_hex}\n")
f.write(f"MAC: {mac}\n")
f.write(f"Payload: {payload}\n")
break
main()

View File

@@ -0,0 +1,141 @@
/*
* Base64 encoding/decoding (RFC1341)
* Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
// https://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c
#include "base64.h"
#define os_malloc malloc
#define os_free free
#define os_memset memset
static const unsigned char base64_table[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/**
* base64_encode - Base64 encode
* @src: Data to be encoded
* @len: Length of the data to be encoded
* @out_len: Pointer to output length variable, or %NULL if not used
* Returns: Allocated buffer of out_len bytes of encoded data,
* or %NULL on failure
*
* Caller is responsible for freeing the returned buffer. Returned buffer is
* nul terminated to make it easier to use as a C string. The nul terminator is
* not included in out_len.
*/
unsigned char* base64_encode(const unsigned char* src, size_t len, size_t* out_len) {
unsigned char *out, *pos;
const unsigned char *end, *in;
size_t olen;
int line_len;
olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
olen += olen / 72; /* line feeds */
olen++; /* nul termination */
if(olen < len) return NULL; /* integer overflow */
out = os_malloc(olen);
if(out == NULL) return NULL;
end = src + len;
in = src;
pos = out;
line_len = 0;
while(end - in >= 3) {
*pos++ = base64_table[in[0] >> 2];
*pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
*pos++ = base64_table[in[2] & 0x3f];
in += 3;
line_len += 4;
if(line_len >= 72) {
*pos++ = '\n';
line_len = 0;
}
}
if(end - in) {
*pos++ = base64_table[in[0] >> 2];
if(end - in == 1) {
*pos++ = base64_table[(in[0] & 0x03) << 4];
*pos++ = '=';
} else {
*pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64_table[(in[1] & 0x0f) << 2];
}
*pos++ = '=';
line_len += 4;
}
if(line_len) *pos++ = '\n';
*pos = '\0';
if(out_len) *out_len = pos - out;
return out;
}
/**
* base64_decode - Base64 decode
* @src: Data to be decoded
* @len: Length of the data to be decoded
* @out_len: Pointer to output length variable
* Returns: Allocated buffer of out_len bytes of decoded data,
* or %NULL on failure
*
* Caller is responsible for freeing the returned buffer.
*/
unsigned char* base64_decode(const unsigned char* src, size_t len, size_t* out_len) {
unsigned char dtable[256], *out, *pos, block[4], tmp;
size_t i, count, olen;
int pad = 0;
os_memset(dtable, 0x80, 256);
for(i = 0; i < sizeof(base64_table) - 1; i++) dtable[base64_table[i]] = (unsigned char)i;
dtable['='] = 0;
count = 0;
for(i = 0; i < len; i++) {
if(dtable[src[i]] != 0x80) count++;
}
if(count == 0 || count % 4) return NULL;
olen = count / 4 * 3;
pos = out = os_malloc(olen);
if(out == NULL) return NULL;
count = 0;
for(i = 0; i < len; i++) {
tmp = dtable[src[i]];
if(tmp == 0x80) continue;
if(src[i] == '=') pad++;
block[count] = tmp;
count++;
if(count == 4) {
*pos++ = (block[0] << 2) | (block[1] >> 4);
*pos++ = (block[1] << 4) | (block[2] >> 2);
*pos++ = (block[2] << 6) | block[3];
count = 0;
if(pad) {
if(pad == 1)
pos--;
else if(pad == 2)
pos -= 2;
else {
/* Invalid padding */
os_free(out);
return NULL;
}
break;
}
}
}
*out_len = pos - out;
return out;
}

View File

@@ -0,0 +1,21 @@
/*
* Base64 encoding/decoding (RFC1341)
* Copyright (c) 2005, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
// https://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.h
#ifndef BASE64_H
#define BASE64_H
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
unsigned char* base64_encode(const unsigned char* src, size_t len, size_t* out_len);
unsigned char* base64_decode(const unsigned char* src, size_t len, size_t* out_len);
#endif /* BASE64_H */

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

View File

@@ -3,7 +3,8 @@
enum VarItemListIndex { enum VarItemListIndex {
VarItemListIndexBroadcastInterval, VarItemListIndexBroadcastInterval,
VarItemListIndexTransmitPower, VarItemListIndexTransmitPower,
VarItemListIndexRegisterTag, VarItemListIndexImportTagFromFile,
VarItemListIndexRegisterTagManually,
VarItemListIndexAbout, VarItemListIndexAbout,
}; };
@@ -12,9 +13,9 @@ void findmy_scene_config_broadcast_interval_changed(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item); uint8_t index = variable_item_get_current_value_index(item);
findmy_change_broadcast_interval(app, index + 1); findmy_change_broadcast_interval(app, index + 1);
char str[5]; char str[5];
snprintf(str, sizeof(str), "%ds", app->broadcast_interval); snprintf(str, sizeof(str), "%ds", app->state.broadcast_interval);
variable_item_set_current_value_text(item, str); variable_item_set_current_value_text(item, str);
variable_item_set_current_value_index(item, app->broadcast_interval - 1); variable_item_set_current_value_index(item, app->state.broadcast_interval - 1);
} }
void findmy_scene_config_transmit_power_changed(VariableItem* item) { void findmy_scene_config_transmit_power_changed(VariableItem* item) {
@@ -22,9 +23,9 @@ void findmy_scene_config_transmit_power_changed(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item); uint8_t index = variable_item_get_current_value_index(item);
findmy_change_transmit_power(app, index); findmy_change_transmit_power(app, index);
char str[7]; char str[7];
snprintf(str, sizeof(str), "%ddBm", app->transmit_power); snprintf(str, sizeof(str), "%ddBm", app->state.transmit_power);
variable_item_set_current_value_text(item, str); variable_item_set_current_value_text(item, str);
variable_item_set_current_value_index(item, app->transmit_power); variable_item_set_current_value_index(item, app->state.transmit_power);
} }
void findmy_scene_config_callback(void* context, uint32_t index) { void findmy_scene_config_callback(void* context, uint32_t index) {
@@ -45,20 +46,28 @@ void findmy_scene_config_on_enter(void* context) {
findmy_scene_config_broadcast_interval_changed, findmy_scene_config_broadcast_interval_changed,
app); app);
// Broadcast Interval is 1-10, so use 0-9 and offset indexes by 1 // Broadcast Interval is 1-10, so use 0-9 and offset indexes by 1
variable_item_set_current_value_index(item, app->broadcast_interval - 1); variable_item_set_current_value_index(item, app->state.broadcast_interval - 1);
char broadcast_interval_s[5]; char interval_str[5];
snprintf(broadcast_interval_s, sizeof(broadcast_interval_s), "%ds", app->broadcast_interval); snprintf(interval_str, sizeof(interval_str), "%ds", app->state.broadcast_interval);
variable_item_set_current_value_text(item, broadcast_interval_s); variable_item_set_current_value_text(item, interval_str);
item = variable_item_list_add( item = variable_item_list_add(
var_item_list, "Transmit Power", 7, findmy_scene_config_transmit_power_changed, app); var_item_list, "Transmit Power", 7, findmy_scene_config_transmit_power_changed, app);
variable_item_set_current_value_index(item, app->transmit_power); variable_item_set_current_value_index(item, app->state.transmit_power);
char transmit_power_s[7]; char power_str[7];
snprintf(transmit_power_s, sizeof(transmit_power_s), "%ddBm", app->transmit_power); snprintf(power_str, sizeof(power_str), "%ddBm", app->state.transmit_power);
variable_item_set_current_value_text(item, transmit_power_s); variable_item_set_current_value_text(item, power_str);
item = variable_item_list_add(var_item_list, "Register Tag", 0, NULL, NULL); item = variable_item_list_add(var_item_list, "Import Tag From File", 0, NULL, NULL);
item = variable_item_list_add(var_item_list, "Matthew KuKanich, Thanks to Chapoly1305, WillyJL, OpenHaystack, Testers", 1, NULL, NULL);
item = variable_item_list_add(var_item_list, "Register Tag Manually", 0, NULL, NULL);
item = variable_item_list_add(
var_item_list,
"Matthew KuKanich, Thanks to Chapoly1305, WillyJL, OpenHaystack, Testers",
1,
NULL,
NULL);
variable_item_set_current_value_text(item, "Credits"); variable_item_set_current_value_text(item, "Credits");
variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_callback, app); variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_callback, app);
@@ -77,7 +86,10 @@ bool findmy_scene_config_on_event(void* context, SceneManagerEvent event) {
scene_manager_set_scene_state(app->scene_manager, FindMySceneConfig, event.event); scene_manager_set_scene_state(app->scene_manager, FindMySceneConfig, event.event);
consumed = true; consumed = true;
switch(event.event) { switch(event.event) {
case VarItemListIndexRegisterTag: case VarItemListIndexImportTagFromFile:
scene_manager_next_scene(app->scene_manager, FindMySceneConfigImport);
break;
case VarItemListIndexRegisterTagManually:
scene_manager_next_scene(app->scene_manager, FindMySceneConfigMac); scene_manager_next_scene(app->scene_manager, FindMySceneConfigMac);
break; break;
case VarItemListIndexAbout: case VarItemListIndexAbout:

View File

@@ -0,0 +1,222 @@
#include "../findmy_i.h"
enum VarItemListIndex {
VarItemListIndexNrfConnect,
VarItemListIndexOpenHaystack,
};
static const char* parse_nrf_connect(FindMy* app, const char* path) {
const char* error = NULL;
Stream* stream = file_stream_alloc(app->storage);
FuriString* line = furi_string_alloc();
do {
// XX-XX-XX-XX-XX-XX_YYYY-MM-DD HH_MM_SS.txt
error = "Filename must\nhave MAC\naddress";
uint8_t mac[EXTRA_BEACON_MAC_ADDR_SIZE];
path_extract_filename_no_ext(path, line);
if(furi_string_size(line) < sizeof(mac) * 3 - 1) break;
error = NULL;
for(size_t i = 0; i < sizeof(mac); i++) {
char a = furi_string_get_char(line, i * 3);
char b = furi_string_get_char(line, i * 3 + 1);
if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') ||
(b < '0' && b > '9') || !hex_char_to_uint8(a, b, &mac[i])) {
error = "Filename must\nhave MAC\naddress";
break;
}
}
if(error) break;
furi_hal_bt_reverse_mac_addr(mac);
error = "Can't open file";
if(!file_stream_open(stream, path, FSAM_READ, FSOM_OPEN_EXISTING)) break;
// YYYY-MM-DD HH:MM:SS.ms, XX dBm, 0xXXXXX
error = "Wrong file format";
if(!stream_read_line(stream, line)) break;
const char* marker = " dBm, 0x";
size_t pos = furi_string_search(line, marker);
if(pos == FURI_STRING_FAILURE) break;
furi_string_right(line, pos + strlen(marker));
furi_string_trim(line);
error = "Wrong payload size";
uint8_t data[EXTRA_BEACON_MAX_DATA_SIZE];
if(furi_string_size(line) != sizeof(data) * 2) break;
error = NULL;
for(size_t i = 0; i < sizeof(data); i++) {
char a = furi_string_get_char(line, i * 2);
char b = furi_string_get_char(line, i * 2 + 1);
if((a < 'A' && a > 'F') || (a < '0' && a > '9') || (b < 'A' && b > 'F') ||
(b < '0' && b > '9') || !hex_char_to_uint8(a, b, &data[i])) {
error = "Invalid payload";
break;
}
}
if(error) break;
memcpy(app->state.mac, mac, sizeof(app->state.mac));
memcpy(app->state.data, data, sizeof(app->state.data));
findmy_state_sync_config(&app->state);
findmy_state_save(&app->state);
error = NULL;
} while(false);
furi_string_free(line);
file_stream_close(stream);
stream_free(stream);
return error;
}
static const char* parse_open_haystack(FindMy* app, const char* path) {
const char* error = NULL;
Stream* stream = file_stream_alloc(app->storage);
FuriString* line = furi_string_alloc();
do {
error = "Can't open file";
if(!file_stream_open(stream, path, FSAM_READ, FSOM_OPEN_EXISTING)) break;
error = "Wrong file format";
while(stream_read_line(stream, line)) {
if(furi_string_start_with(line, "Public key: ") ||
furi_string_start_with(line, "Advertisement key: ")) {
error = NULL;
break;
}
}
if(error) break;
furi_string_right(line, furi_string_search_char(line, ':') + 2);
furi_string_trim(line);
error = "Base64 failed";
size_t decoded_len;
uint8_t* public_key = base64_decode(
(uint8_t*)furi_string_get_cstr(line), furi_string_size(line), &decoded_len);
if(decoded_len != 28) {
free(public_key);
break;
}
memcpy(app->state.mac, public_key, sizeof(app->state.mac));
app->state.mac[0] |= 0b11000000;
furi_hal_bt_reverse_mac_addr(app->state.mac);
uint8_t advertisement_template[EXTRA_BEACON_MAX_DATA_SIZE] = {
0x1e, // length (30)
0xff, // manufacturer specific data
0x4c, 0x00, // company ID (Apple)
0x12, 0x19, // offline finding type and length
0x00, //state
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, // first two bits of key[0]
0x00, // hint
};
memcpy(app->state.data, advertisement_template, sizeof(app->state.data));
memcpy(&app->state.data[7], &public_key[6], decoded_len - 6);
app->state.data[29] = public_key[0] >> 6;
findmy_state_sync_config(&app->state);
findmy_state_save(&app->state);
free(public_key);
error = NULL;
} while(false);
furi_string_free(line);
file_stream_close(stream);
stream_free(stream);
return error;
}
void findmy_scene_config_import_callback(void* context, uint32_t index) {
furi_assert(context);
FindMy* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, index);
}
void findmy_scene_config_import_on_enter(void* context) {
FindMy* app = context;
VariableItemList* var_item_list = app->var_item_list;
VariableItem* item;
variable_item_list_set_header(var_item_list, "Choose file type");
item = variable_item_list_add(var_item_list, "nRF Connect (.txt)", 0, NULL, NULL);
item = variable_item_list_add(var_item_list, "OpenHaystack (.keys)", 0, NULL, NULL);
// This scene acts more like a submenu than a var item list tbh
UNUSED(item);
variable_item_list_set_enter_callback(var_item_list, findmy_scene_config_import_callback, app);
variable_item_list_set_selected_item(
var_item_list, scene_manager_get_scene_state(app->scene_manager, FindMySceneConfigImport));
view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewVarItemList);
}
bool findmy_scene_config_import_on_event(void* context, SceneManagerEvent event) {
FindMy* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
scene_manager_set_scene_state(app->scene_manager, FindMySceneConfigImport, event.event);
consumed = true;
const char* extension = NULL;
switch(event.event) {
case VarItemListIndexNrfConnect:
extension = ".txt";
break;
case VarItemListIndexOpenHaystack:
extension = ".keys";
break;
default:
break;
}
if(!extension) {
return consumed;
}
const DialogsFileBrowserOptions browser_options = {
.extension = extension,
.icon = &I_text_10px,
.base_path = FINDMY_STATE_DIR,
};
storage_simply_mkdir(app->storage, browser_options.base_path);
FuriString* path = furi_string_alloc_set_str(browser_options.base_path);
if(dialog_file_browser_show(app->dialogs, path, path, &browser_options)) {
// The parse functions return the error text, or NULL for success
// Used in result to show success or error message
const char* error = NULL;
switch(event.event) {
case VarItemListIndexNrfConnect:
error = parse_nrf_connect(app, furi_string_get_cstr(path));
break;
case VarItemListIndexOpenHaystack:
error = parse_open_haystack(app, furi_string_get_cstr(path));
break;
}
scene_manager_set_scene_state(
app->scene_manager, FindMySceneConfigImportResult, (uint32_t)error);
scene_manager_next_scene(app->scene_manager, FindMySceneConfigImportResult);
}
furi_string_free(path);
}
return consumed;
}
void findmy_scene_config_import_on_exit(void* context) {
FindMy* app = context;
VariableItemList* var_item_list = app->var_item_list;
variable_item_list_reset(var_item_list);
}

View File

@@ -0,0 +1,58 @@
#include "../findmy_i.h"
enum PopupEvent {
PopupEventExit,
};
static void findmy_scene_config_import_result_callback(void* context) {
FindMy* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, PopupEventExit);
}
void findmy_scene_config_import_result_on_enter(void* context) {
FindMy* app = context;
Popup* popup = app->popup;
const char* error = (const char*)scene_manager_get_scene_state(
app->scene_manager, FindMySceneConfigImportResult);
if(error) {
popup_set_icon(popup, 83, 22, &I_WarningDolphinFlip_45x42);
popup_set_header(popup, "Error!", 13, 22, AlignLeft, AlignBottom);
popup_set_text(popup, error, 6, 26, AlignLeft, AlignTop);
popup_disable_timeout(popup);
} else {
popup_set_icon(popup, 36, 5, &I_DolphinDone_80x58);
popup_set_header(popup, "Imported!", 7, 14, AlignLeft, AlignBottom);
popup_enable_timeout(popup);
}
popup_set_timeout(popup, 1500);
popup_set_context(popup, app);
popup_set_callback(popup, findmy_scene_config_import_result_callback);
view_dispatcher_switch_to_view(app->view_dispatcher, FindMyViewPopup);
}
bool findmy_scene_config_import_result_on_event(void* context, SceneManagerEvent event) {
FindMy* app = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
consumed = true;
switch(event.event) {
case PopupEventExit:
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, FindMySceneConfig);
break;
default:
break;
}
}
return consumed;
}
void findmy_scene_config_import_result_on_exit(void* context) {
FindMy* app = context;
popup_reset(app->popup);
}

View File

@@ -16,7 +16,7 @@ void findmy_scene_config_mac_on_enter(void* context) {
byte_input_set_header_text(byte_input, "Enter Bluetooth MAC:"); byte_input_set_header_text(byte_input, "Enter Bluetooth MAC:");
memcpy(app->mac_buf, &app->config.address, sizeof(app->mac_buf)); memcpy(app->mac_buf, app->state.mac, sizeof(app->mac_buf));
furi_hal_bt_reverse_mac_addr(app->mac_buf); furi_hal_bt_reverse_mac_addr(app->mac_buf);
byte_input_set_result_callback( byte_input_set_result_callback(
@@ -39,8 +39,16 @@ bool findmy_scene_config_mac_on_event(void* context, SceneManagerEvent event) {
switch(event.event) { switch(event.event) {
case ByteInputResultOk: case ByteInputResultOk:
furi_hal_bt_reverse_mac_addr(app->mac_buf); furi_hal_bt_reverse_mac_addr(app->mac_buf);
memcpy(&app->config.address, app->mac_buf, sizeof(app->config.address)); memcpy(&app->state.mac, app->mac_buf, sizeof(app->state.mac));
furi_hal_bt_extra_beacon_set_config(&app->config); findmy_state_sync_config(&app->state);
findmy_state_save(&app->state);
if(furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_stop());
}
furi_check(furi_hal_bt_extra_beacon_set_config(&app->state.config));
if(app->state.beacon_active) {
furi_check(furi_hal_bt_extra_beacon_start());
}
scene_manager_next_scene(app->scene_manager, FindMySceneConfigPacket); scene_manager_next_scene(app->scene_manager, FindMySceneConfigPacket);
break; break;
default: default:
@@ -56,5 +64,4 @@ void findmy_scene_config_mac_on_exit(void* context) {
byte_input_set_result_callback(app->byte_input, NULL, NULL, NULL, NULL, 0); byte_input_set_result_callback(app->byte_input, NULL, NULL, NULL, NULL, 0);
byte_input_set_header_text(app->byte_input, ""); byte_input_set_header_text(app->byte_input, "");
} }

View File

@@ -16,8 +16,7 @@ void findmy_scene_config_packet_on_enter(void* context) {
byte_input_set_header_text(byte_input, "Enter Bluetooth Payload:"); byte_input_set_header_text(byte_input, "Enter Bluetooth Payload:");
memset(app->packet_buf, 0, sizeof(app->packet_buf)); memcpy(app->packet_buf, app->state.data, sizeof(app->packet_buf));
furi_hal_bt_extra_beacon_get_data(app->packet_buf);
byte_input_set_result_callback( byte_input_set_result_callback(
byte_input, byte_input,
@@ -40,13 +39,11 @@ bool findmy_scene_config_packet_on_event(void* context, SceneManagerEvent event)
case ByteInputResultOk: case ByteInputResultOk:
scene_manager_search_and_switch_to_previous_scene( scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, FindMySceneConfig); app->scene_manager, FindMySceneConfig);
furi_check(furi_hal_bt_extra_beacon_set_data(app->packet_buf, sizeof(app->packet_buf))); memcpy(app->state.data, app->packet_buf, sizeof(app->state.data));
if (app->packet_buf[0] == 0x1E && app->packet_buf[3] == 0x00) { findmy_state_save(&app->state);
app->apple = true; // Checks payload data for Apple identifier furi_check(
} else { furi_hal_bt_extra_beacon_set_data(app->state.data, sizeof(app->state.data)));
app->apple = false; findmy_main_update_type(app->findmy_main, findmy_data_get_type(app->state.data));
}
findmy_main_update_apple(app->findmy_main, app->apple);
break; break;
default: default:
break; break;

View File

@@ -25,19 +25,28 @@ bool findmy_scene_main_on_event(void* context, SceneManagerEvent event) {
findmy_toggle_beacon(app); findmy_toggle_beacon(app);
break; break;
case FindMyMainEventBackground: case FindMyMainEventBackground:
furi_hal_bt_extra_beacon_start(); app->state.beacon_active = true;
findmy_state_save(&app->state);
if(!furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_start());
}
view_dispatcher_stop(app->view_dispatcher); view_dispatcher_stop(app->view_dispatcher);
break; break;
case FindMyMainEventConfig: case FindMyMainEventConfig:
scene_manager_next_scene(app->scene_manager, FindMySceneConfig); scene_manager_next_scene(app->scene_manager, FindMySceneConfig);
break; break;
case FindMyMainEventIntervalUp: case FindMyMainEventIntervalUp:
findmy_change_broadcast_interval(app, app->broadcast_interval + 1); findmy_change_broadcast_interval(app, app->state.broadcast_interval + 1);
break; break;
case FindMyMainEventIntervalDown: case FindMyMainEventIntervalDown:
findmy_change_broadcast_interval(app, app->broadcast_interval - 1); findmy_change_broadcast_interval(app, app->state.broadcast_interval - 1);
break; break;
case FindMyMainEventQuit: case FindMyMainEventQuit:
app->state.beacon_active = false;
findmy_state_save(&app->state);
if(furi_hal_bt_extra_beacon_is_active()) {
furi_check(furi_hal_bt_extra_beacon_stop());
}
break; break;
default: default:
consumed = false; consumed = false;

View File

@@ -1,4 +1,6 @@
ADD_SCENE(findmy, main, Main) ADD_SCENE(findmy, main, Main)
ADD_SCENE(findmy, config, Config) ADD_SCENE(findmy, config, Config)
ADD_SCENE(findmy, config_import, ConfigImport)
ADD_SCENE(findmy, config_import_result, ConfigImportResult)
ADD_SCENE(findmy, config_mac, ConfigMac) ADD_SCENE(findmy, config_mac, ConfigMac)
ADD_SCENE(findmy, config_packet, ConfigPacket) ADD_SCENE(findmy, config_packet, ConfigPacket)

View File

@@ -9,8 +9,8 @@ struct FindMyMain {
typedef struct { typedef struct {
bool active; bool active;
bool apple;
uint8_t interval; uint8_t interval;
FindMyType type;
} FindMyMainModel; } FindMyMainModel;
static void findmy_main_draw_callback(Canvas* canvas, void* _model) { static void findmy_main_draw_callback(Canvas* canvas, void* _model) {
@@ -34,12 +34,17 @@ static void findmy_main_draw_callback(Canvas* canvas, void* _model) {
snprintf(interval_str, sizeof(interval_str), "Ping Interval: %ds", model->interval); snprintf(interval_str, sizeof(interval_str), "Ping Interval: %ds", model->interval);
canvas_draw_str(canvas, 4, 62, interval_str); canvas_draw_str(canvas, 4, 62, interval_str);
canvas_set_font(canvas, FontPrimary); canvas_set_font(canvas, FontPrimary);
if(model->apple){ switch(model->type) {
case FindMyTypeApple:
canvas_draw_str(canvas, 4, 32, "Apple Network"); canvas_draw_str(canvas, 4, 32, "Apple Network");
canvas_draw_icon(canvas, 80, 24, &I_Lock_7x8); canvas_draw_icon(canvas, 80, 24, &I_Lock_7x8);
} else { break;
case FindMyTypeSamsung:
canvas_draw_str(canvas, 4, 32, "Samsung Network"); canvas_draw_str(canvas, 4, 32, "Samsung Network");
canvas_draw_icon(canvas, 97, 24, &I_Lock_7x8); canvas_draw_icon(canvas, 97, 24, &I_Lock_7x8);
break;
default:
break;
} }
canvas_set_font(canvas, FontSecondary); canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 100, 61, "Config"); canvas_draw_str(canvas, 100, 61, "Config");
@@ -56,30 +61,32 @@ static bool findmy_main_input_callback(InputEvent* event, void* context) {
if(event->type == InputTypePress) { if(event->type == InputTypePress) {
consumed = true; consumed = true;
// FIXME: finish implementing handlers in scene side FindMyMainEvent cb_event;
switch(event->key) { switch(event->key) {
case InputKeyBack: case InputKeyBack:
findmy_main->callback(FindMyMainEventQuit, findmy_main->context); cb_event = FindMyMainEventQuit;
// furi_hal_bt_extra_beacon_stop();
break; break;
case InputKeyOk: case InputKeyOk:
findmy_main->callback(FindMyMainEventToggle, findmy_main->context); cb_event = FindMyMainEventToggle;
break; break;
case InputKeyLeft: case InputKeyLeft:
findmy_main->callback(FindMyMainEventBackground, findmy_main->context); cb_event = FindMyMainEventBackground;
break; break;
case InputKeyRight: case InputKeyRight:
findmy_main->callback(FindMyMainEventConfig, findmy_main->context); cb_event = FindMyMainEventConfig;
break; break;
case InputKeyUp: case InputKeyUp:
findmy_main->callback(FindMyMainEventIntervalUp, findmy_main->context); cb_event = FindMyMainEventIntervalUp;
break; break;
case InputKeyDown: case InputKeyDown:
findmy_main->callback(FindMyMainEventIntervalDown, findmy_main->context); cb_event = FindMyMainEventIntervalDown;
break; break;
default: default:
break; return consumed;
} }
findmy_main->callback(cb_event, findmy_main->context);
} }
return consumed; return consumed;
@@ -94,9 +101,9 @@ FindMyMain* findmy_main_alloc(FindMy* app) {
findmy_main->view, findmy_main->view,
FindMyMainModel * model, FindMyMainModel * model,
{ {
model->active = app->beacon_active; model->active = app->state.beacon_active;
model->apple = app->apple; model->interval = app->state.broadcast_interval;
model->interval = app->broadcast_interval; model->type = findmy_data_get_type(app->state.data);
}, },
false); false);
view_set_context(findmy_main->view, findmy_main); view_set_context(findmy_main->view, findmy_main);
@@ -136,8 +143,8 @@ void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval) {
findmy_main->view, FindMyMainModel * model, { model->interval = interval; }, true); findmy_main->view, FindMyMainModel * model, { model->interval = interval; }, true);
} }
void findmy_main_update_apple(FindMyMain* findmy_main, bool apple) { void findmy_main_update_type(FindMyMain* findmy_main, FindMyType type) {
furi_assert(findmy_main); furi_assert(findmy_main);
with_view_model( with_view_model(
findmy_main->view, FindMyMainModel * model, { model->apple = apple; }, true); findmy_main->view, FindMyMainModel * model, { model->type = type; }, true);
} }

View File

@@ -26,4 +26,4 @@ void findmy_main_set_callback(FindMyMain* findmy_main, FindMyMainCallback callba
// To redraw when info changes // To redraw when info changes
void findmy_main_update_active(FindMyMain* findmy_main, bool active); void findmy_main_update_active(FindMyMain* findmy_main, bool active);
void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval); void findmy_main_update_interval(FindMyMain* findmy_main, uint8_t interval);
void findmy_main_update_apple(FindMyMain* findmy_main, bool apple); void findmy_main_update_type(FindMyMain* findmy_main, FindMyType type);

View File

@@ -6,7 +6,7 @@ Asset Packs are an exclusive feature of Momentum Firmware that allows you to loa
## How to install Asset Packs? ## How to install Asset Packs?
Installing Asset Packs is quite easy and straightforward. First, make sure you're on an updated version of XFW before you begin, Asset Packs were added in v40! Then, find some packs to install (we have a channel in our discord where you can find some) or make your own (see below). Once you have some packs to install: Installing Asset Packs is quite easy and straightforward. First, make sure you're on an updated version of Momentum before you begin, Asset Packs were added in v40! Then, find some packs to install (we have a channel in our discord where you can find some) or make your own (see below). Once you have some packs to install:
- Open qFlipper and navigate to `SD Card` and into `asset_packs`; if you do not see this folder, try reinstalling the firmware, or create it yourself. - Open qFlipper and navigate to `SD Card` and into `asset_packs`; if you do not see this folder, try reinstalling the firmware, or create it yourself.
@@ -56,7 +56,7 @@ SD/
Again, this is all fairly standard Flipper animation stuff, there are plenty of tutorials on YouTube. The key differences with the Asset Pack animation system are: Again, this is all fairly standard Flipper animation stuff, there are plenty of tutorials on YouTube. The key differences with the Asset Pack animation system are:
- They go in `SD/asset_packs/PackName/Anims` instead of `SD/dolphin`. - They go in `SD/asset_packs/PackName/Anims` instead of `SD/dolphin`.
- XFW has up to level 30, so make sure to update your manifest.txt accordingly! - Momentum has up to level 30, so make sure to update your manifest.txt accordingly!
<br> <br>

View File

@@ -18,7 +18,7 @@ DEBUG = 0
# Suffix to add to files when building distribution # Suffix to add to files when building distribution
# If OS environment has DIST_SUFFIX set, it will be used instead # If OS environment has DIST_SUFFIX set, it will be used instead
DIST_SUFFIX = f"MNTM-DEV_@{subprocess.check_output(['git', 'rev-parse', '--short=7', 'HEAD']).decode().strip().upper()}" DIST_SUFFIX = f"mntm-dev-{subprocess.check_output(['git', 'rev-parse', '--short=7', 'HEAD']).decode().strip()}"
# Coprocessor firmware # Coprocessor firmware
COPRO_OB_DATA = "scripts/ob.data" COPRO_OB_DATA = "scripts/ob.data"

View File

@@ -34,7 +34,7 @@ typedef enum {
MenuStyleVertical, MenuStyleVertical,
MenuStyleC64, MenuStyleC64,
MenuStyleCompact, MenuStyleCompact,
MenuStyleTerminal, MenuStyleMNTM,
MenuStyleCount, MenuStyleCount,
} MenuStyle; } MenuStyle;
@@ -47,6 +47,7 @@ typedef enum {
typedef enum { typedef enum {
VgmColorModeDefault, VgmColorModeDefault,
VgmColorModeCustom, VgmColorModeCustom,
VgmColorModeRainbow,
VgmColorModeRgbBacklight, VgmColorModeRgbBacklight,
VgmColorModeCount, VgmColorModeCount,
} VgmColorMode; } VgmColorMode;

View File

@@ -60,10 +60,9 @@ def get_details(event, args):
data["commit_sha"] = data["commit_hash"][:8] data["commit_sha"] = data["commit_hash"][:8]
data["branch_name"] = re.sub("refs/\w+/", "", ref) data["branch_name"] = re.sub("refs/\w+/", "", ref)
data["suffix"] = ( data["suffix"] = (
"mntm-" +
data["branch_name"].replace("/", "_") data["branch_name"].replace("/", "_")
+ "-" + "-"
+ current_time.strftime("%d%m%Y")
+ "-"
+ data["commit_sha"] + data["commit_sha"]
) )
if ref.startswith("refs/tags/"): if ref.startswith("refs/tags/"):

View File

@@ -35,11 +35,7 @@ class GitVersion:
or "unknown" or "unknown"
) )
version = ( version = self.suffix or os.environ.get("DIST_SUFFIX", None) or "unknown"
self.suffix.split("_")[0]
or os.environ.get("DIST_SUFFIX", None)
or "unknown"
)
if "SOURCE_DATE_EPOCH" in os.environ: if "SOURCE_DATE_EPOCH" in os.environ:
commit_date = datetime.utcfromtimestamp( commit_date = datetime.utcfromtimestamp(