mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-13 14:48:35 -07:00
Merge branch 'Next-Flip:dev' into dev
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @Willy-JL @Sil333033
|
* @Willy-JL @Sil333033 @HaxSam @MatthewKuKanich
|
||||||
|
|||||||
4
.github/workflow_data/commit.sh
vendored
4
.github/workflow_data/commit.sh
vendored
@@ -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
|
|
||||||
38
.github/workflow_data/devbuild.py
vendored
38
.github/workflow_data/devbuild.py
vendored
@@ -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()
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
18
.github/workflow_data/package.sh
vendored
18
.github/workflow_data/package.sh
vendored
@@ -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
|
|
||||||
4
.github/workflow_data/release.md
vendored
4
.github/workflow_data/release.md
vendored
@@ -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!**
|
||||||
|
|
||||||
|
|||||||
21
.github/workflow_data/release.py
vendored
21
.github/workflow_data/release.py
vendored
@@ -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")
|
|
||||||
|
|||||||
13
.github/workflow_data/version.sh
vendored
13
.github/workflow_data/version.sh
vendored
@@ -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
|
|
||||||
99
.github/workflow_data/webhook.py
vendored
99
.github/workflow_data/webhook.py
vendored
@@ -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": [],
|
||||||
|
|||||||
36
.github/workflow_data/webupdater.py
vendored
36
.github/workflow_data/webupdater.py
vendored
@@ -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
|
|
||||||
98
.github/workflows/build.yml
vendored
98
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
|||||||
19
.github/workflows/docs.yml
vendored
19
.github/workflows/docs.yml
vendored
@@ -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"
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@@ -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
|
||||||
|
|||||||
96
.github/workflows/release.yml
vendored
96
.github/workflows/release.yml
vendored
@@ -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
|
||||||
|
|||||||
65
.github/workflows/sonarcloud.yaml
vendored
65
.github/workflows/sonarcloud.yaml
vendored
@@ -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 }}"
|
|
||||||
42
.github/workflows/submodules.yml
vendored
42
.github/workflows/submodules.yml
vendored
@@ -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
|
|
||||||
13
.github/workflows/webhook.yml
vendored
13
.github/workflows/webhook.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Submodule applications/external updated: 3dbd298b07...a3b7d50469
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
74
applications/system/findmy/README.md
Normal file
74
applications/system/findmy/README.md
Normal 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.
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
typedef struct FindMy FindMy;
|
typedef struct FindMy FindMy;
|
||||||
|
|
||||||
|
typedef enum FindMyType FindMyType;
|
||||||
@@ -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]);
|
||||||
|
|||||||
11
applications/system/findmy/findmy_startup.c
Normal file
11
applications/system/findmy/findmy_startup.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
130
applications/system/findmy/findmy_state.c
Normal file
130
applications/system/findmy/findmy_state.c
Normal 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);
|
||||||
|
}
|
||||||
28
applications/system/findmy/findmy_state.h
Normal file
28
applications/system/findmy/findmy_state.h
Normal 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);
|
||||||
112
applications/system/findmy/generate_keys.py
Normal file
112
applications/system/findmy/generate_keys.py
Normal 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()
|
||||||
141
applications/system/findmy/helpers/base64.c
Normal file
141
applications/system/findmy/helpers/base64.c
Normal 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;
|
||||||
|
}
|
||||||
21
applications/system/findmy/helpers/base64.h
Normal file
21
applications/system/findmy/helpers/base64.h
Normal 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 */
|
||||||
BIN
applications/system/findmy/icons/text_10px.png
Normal file
BIN
applications/system/findmy/icons/text_10px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 B |
@@ -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:
|
||||||
|
|||||||
222
applications/system/findmy/scenes/findmy_scene_config_import.c
Normal file
222
applications/system/findmy/scenes/findmy_scene_config_import.c
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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, "");
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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/"):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user