mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-09-11 18:30:14 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5778cc010 | ||
|
|
ccf60753f9 | ||
|
|
66a36881cf | ||
|
|
87db9e043f | ||
|
|
cf4595e953 | ||
|
|
aa881ee596 | ||
|
|
52ff1d0fb6 | ||
|
|
38a745fdf4 | ||
|
|
0c1d5791b4 | ||
|
|
ba353eadbd | ||
|
|
e509bc5311 | ||
|
|
cf2b268a7d | ||
|
|
9f90c3f8b9 | ||
|
|
bed137069b | ||
|
|
3e4876c20d | ||
|
|
1f99ddd4db | ||
|
|
3d1ffe9b47 | ||
|
|
75485b9a19 | ||
|
|
ac63dbdac8 | ||
|
|
1ddf4f01bc | ||
|
|
d0b2df564b | ||
|
|
d562549b5d | ||
|
|
ab59e7af00 | ||
|
|
81ce26a15b | ||
|
|
4b2c29fa0b | ||
|
|
909a7e7eda | ||
|
|
f7350ce11e | ||
|
|
976ee266b7 | ||
|
|
c0477cd961 | ||
|
|
197232c148 | ||
|
|
f39de047be | ||
|
|
d13001bab8 | ||
|
|
7b1faccc52 |
@@ -0,0 +1,309 @@
|
||||
const FEATURE_FREEZE_LABEL = "feature freeze";
|
||||
const CLOSE_SOON_LABEL = "close soon";
|
||||
const INACTIVITY_MARKER = "<!-- chameleon-ultra-draft-inactivity:";
|
||||
|
||||
const LABELS = {
|
||||
[FEATURE_FREEZE_LABEL]: {
|
||||
color: "1d76db",
|
||||
description: "Included in the current review and release batch",
|
||||
},
|
||||
[CLOSE_SOON_LABEL]: {
|
||||
color: "d73a4a",
|
||||
description: "Will be closed unless development resumes",
|
||||
},
|
||||
};
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
module.exports = async ({ github, context, core, mode }) => {
|
||||
const { owner, repo } = context.repo;
|
||||
let knownLabels;
|
||||
|
||||
const daysSince = (timestamp) =>
|
||||
(Date.now() - new Date(timestamp).getTime()) / DAY;
|
||||
|
||||
const hasLabel = (pull, name) =>
|
||||
pull.labels.some((label) => label.name.toLowerCase() === name.toLowerCase());
|
||||
|
||||
async function ensureLabel(name) {
|
||||
if (!knownLabels) {
|
||||
const labels = await github.paginate(github.rest.issues.listLabelsForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
knownLabels = new Set(labels.map((label) => label.name.toLowerCase()));
|
||||
}
|
||||
|
||||
if (knownLabels.has(name.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.createLabel({ owner, repo, name, ...LABELS[name] });
|
||||
knownLabels.add(name.toLowerCase());
|
||||
core.info(`Created the "${name}" label`);
|
||||
} catch (error) {
|
||||
// Two simultaneous runs may both notice that a label is absent.
|
||||
if (error.status !== 422) throw error;
|
||||
knownLabels.add(name.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
async function addLabel(pullNumber, name) {
|
||||
await ensureLabel(name);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
labels: [name],
|
||||
});
|
||||
}
|
||||
|
||||
async function removeLabel(pull, name) {
|
||||
if (!hasLabel(pull, name)) return;
|
||||
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull.number,
|
||||
name,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function inactivityComments(pullNumber) {
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
return comments.filter(
|
||||
(comment) =>
|
||||
comment.user?.login === "github-actions[bot]" &&
|
||||
comment.body?.includes(INACTIVITY_MARKER),
|
||||
);
|
||||
}
|
||||
|
||||
async function clearDraftCountdown(pull) {
|
||||
const hadCloseSoon = hasLabel(pull, CLOSE_SOON_LABEL);
|
||||
await removeLabel(pull, CLOSE_SOON_LABEL);
|
||||
const comments = await inactivityComments(pull.number);
|
||||
|
||||
for (const comment of comments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (comments.length || hadCloseSoon) {
|
||||
core.info(`Reset the inactivity countdown for #${pull.number}`);
|
||||
}
|
||||
}
|
||||
|
||||
function warningBody(warningDays, responseDays) {
|
||||
return [
|
||||
`${INACTIVITY_MARKER}warning -->`,
|
||||
"This draft pull request has not had activity for a while. Are you still interested in finishing it?",
|
||||
"",
|
||||
`Any new commit, PR edit, or conversation comment will reset this timer. If there is no activity for another ${responseDays} days, the \`${CLOSE_SOON_LABEL}\` label will be added.`,
|
||||
"",
|
||||
"If you are no longer interested, reply with `no`, `no longer interested`, `please close`, or `/close-soon`.",
|
||||
"",
|
||||
`<sub>This reminder was posted after ${warningDays} days of inactivity.</sub>`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function closeSoonBody(closeDays) {
|
||||
return [
|
||||
`${INACTIVITY_MARKER}close-soon -->`,
|
||||
`This draft pull request is now marked \`${CLOSE_SOON_LABEL}\` because no continuing interest was confirmed.`,
|
||||
"",
|
||||
`It will be closed after ${closeDays} more days without activity. A new commit, PR edit, or conversation comment will cancel the countdown.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function markCloseSoon(pull, comment, closeDays) {
|
||||
await addLabel(pull.number, CLOSE_SOON_LABEL);
|
||||
const body = closeSoonBody(closeDays);
|
||||
|
||||
if (comment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
core.info(`Marked draft #${pull.number} as "${CLOSE_SOON_LABEL}"`);
|
||||
}
|
||||
|
||||
if (mode === "start-feature-freeze") {
|
||||
await ensureLabel(FEATURE_FREEZE_LABEL);
|
||||
await ensureLabel(CLOSE_SOON_LABEL);
|
||||
|
||||
const pulls = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
});
|
||||
const readyPulls = pulls.filter((pull) => !pull.draft);
|
||||
|
||||
for (const pull of readyPulls) {
|
||||
await addLabel(pull.number, FEATURE_FREEZE_LABEL);
|
||||
await removeLabel(pull, CLOSE_SOON_LABEL);
|
||||
}
|
||||
|
||||
core.summary
|
||||
.addHeading("Feature-freeze batch started")
|
||||
.addRaw(
|
||||
readyPulls.length
|
||||
? `Labeled ${readyPulls.length} open, non-draft pull request(s): ${readyPulls.map((pull) => `#${pull.number}`).join(", ")}.`
|
||||
: "There were no open, non-draft pull requests to label.",
|
||||
);
|
||||
await core.summary.write();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "pull-activity") {
|
||||
const pull = context.payload.pull_request;
|
||||
const action = context.payload.action;
|
||||
|
||||
// A converted draft has left the current batch. A reopened PR waits for the next batch rather than silently rejoining its old one.
|
||||
if (action === "converted_to_draft" || action === "reopened") {
|
||||
await removeLabel(pull, FEATURE_FREEZE_LABEL);
|
||||
}
|
||||
|
||||
if (pull.draft || action === "ready_for_review" || action === "reopened") {
|
||||
await clearDraftCountdown(pull);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "discussion-activity") {
|
||||
if (context.payload.issue && !context.payload.issue.pull_request) return;
|
||||
|
||||
const pullNumber = context.payload.issue?.number ?? context.payload.pull_request?.number;
|
||||
if (!pullNumber || context.actor.endsWith("[bot]")) return;
|
||||
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
if (!pull.draft) return;
|
||||
|
||||
const comments = await inactivityComments(pull.number);
|
||||
const activeComment = comments.at(-1);
|
||||
if (!activeComment) return;
|
||||
|
||||
const reply = context.payload.comment?.body?.trim() ?? "";
|
||||
const isAuthorReply = context.actor === pull.user.login;
|
||||
const isNegativeReply = /^(?:no|no thanks|not anymore|no longer interested|please close(?: this)?|close (?:it|this|this pr)|\/close-soon)[\s.!]*$/i.test(reply);
|
||||
|
||||
if (isAuthorReply && isNegativeReply) {
|
||||
const closeDays = Number(process.env.DRAFT_CLOSE_DAYS);
|
||||
await markCloseSoon(pull, activeComment, closeDays);
|
||||
return;
|
||||
}
|
||||
|
||||
await clearDraftCountdown(pull);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === "scan-drafts") {
|
||||
const warningDays = Number(process.env.DRAFT_WARNING_DAYS);
|
||||
const responseDays = Number(process.env.DRAFT_RESPONSE_DAYS);
|
||||
const closeDays = Number(process.env.DRAFT_CLOSE_DAYS);
|
||||
|
||||
if (![warningDays, responseDays, closeDays].every(Number.isFinite)) {
|
||||
core.setFailed("Draft inactivity periods must all be numbers");
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureLabel(CLOSE_SOON_LABEL);
|
||||
const pulls = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
for (const pull of pulls.filter((candidate) => candidate.draft)) {
|
||||
const comments = await inactivityComments(pull.number);
|
||||
const activeComment = comments.at(-1);
|
||||
const phase = activeComment?.body?.includes(`${INACTIVITY_MARKER}close-soon`)
|
||||
? "close-soon"
|
||||
: activeComment?.body?.includes(`${INACTIVITY_MARKER}warning`)
|
||||
? "warning"
|
||||
: undefined;
|
||||
|
||||
// Honor a close-soon label applied manually by starting the final timer.
|
||||
if (hasLabel(pull, CLOSE_SOON_LABEL) && phase !== "close-soon") {
|
||||
await markCloseSoon(pull, activeComment, closeDays);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Removing the label manually cancels the final countdown.
|
||||
if (phase === "close-soon" && !hasLabel(pull, CLOSE_SOON_LABEL)) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: activeComment.id,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!activeComment && daysSince(pull.updated_at) >= warningDays) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull.number,
|
||||
body: warningBody(warningDays, responseDays),
|
||||
});
|
||||
core.info(`Warned inactive draft #${pull.number}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "warning" && daysSince(activeComment.created_at) >= responseDays) {
|
||||
await markCloseSoon(pull, activeComment, closeDays);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "close-soon" && daysSince(activeComment.updated_at) >= closeDays) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: activeComment.id,
|
||||
body: [
|
||||
`${INACTIVITY_MARKER}closed -->`,
|
||||
`Closing this draft pull request after the \`${CLOSE_SOON_LABEL}\` waiting period expired. It can be reopened if development resumes.`,
|
||||
].join("\n"),
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pull.number,
|
||||
state: "closed",
|
||||
});
|
||||
core.info(`Closed inactive draft #${pull.number}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,8 @@ on:
|
||||
jobs:
|
||||
build_client:
|
||||
name: Build client
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -35,11 +37,12 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.checkout-sha == null && github.sha || inputs.checkout-sha }}
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install PyInstaller and client dependencies
|
||||
@@ -61,14 +64,14 @@ jobs:
|
||||
cd software
|
||||
pyinstaller pyinstaller.spec
|
||||
- name: Upload built client
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: client-${{ matrix.name }}
|
||||
path: software/dist/*
|
||||
- name: Zip up client for release
|
||||
run: ${{ matrix.bundle_command }}
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-artifacts-${{ matrix.name }}
|
||||
path: client-${{ matrix.name }}.zip
|
||||
|
||||
@@ -6,38 +6,44 @@ on:
|
||||
checkout-sha:
|
||||
required: false
|
||||
type: string
|
||||
publish-builder:
|
||||
description: Build and publish the firmware builder image
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build_fw_builder:
|
||||
name: Build and push fw-builder Docker image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
name: Build fw-builder Docker image
|
||||
runs-on: ubuntu-latest # Inherits permissions from caller
|
||||
outputs:
|
||||
image_hash: ${{ steps.push.outputs.digest }}
|
||||
image_ref: ${{ steps.published-image.outputs.image_ref || steps.local-image.outputs.image_ref }}
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.checkout-sha == null && github.sha || inputs.checkout-sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: ghcr.io login
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ inputs.publish-builder }}
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
if: ${{ inputs.publish-builder }}
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-fw-builder
|
||||
- name: Build and push Docker images
|
||||
if: ${{ inputs.publish-builder }}
|
||||
id: push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: firmware
|
||||
push: true
|
||||
@@ -45,26 +51,63 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Build Docker image for this workflow
|
||||
if: ${{ !inputs.publish-builder }}
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: firmware
|
||||
tags: chameleonultra-fw-builder:pr
|
||||
outputs: type=docker,dest=${{ runner.temp }}/fw-builder.tar
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Upload Docker image
|
||||
if: ${{ !inputs.publish-builder }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: firmware-builder-image
|
||||
path: ${{ runner.temp }}/fw-builder.tar
|
||||
retention-days: 1
|
||||
- name: Use local builder image
|
||||
if: ${{ !inputs.publish-builder }}
|
||||
id: local-image
|
||||
run: echo "image_ref=chameleonultra-fw-builder:pr" >> "$GITHUB_OUTPUT"
|
||||
- name: Use published builder image
|
||||
if: ${{ inputs.publish-builder }}
|
||||
id: published-image
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}-fw-builder
|
||||
DIGEST: ${{ steps.push.outputs.digest }}
|
||||
run: echo "image_ref=${IMAGE,,}@${DIGEST}" >> "$GITHUB_OUTPUT"
|
||||
build_fw:
|
||||
name: Build firmware
|
||||
runs-on: ubuntu-latest
|
||||
needs: build_fw_builder
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
device_type: [ultra, lite]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.checkout-sha == null && github.sha || inputs.checkout-sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Download Docker image
|
||||
if: ${{ !inputs.publish-builder }}
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: firmware-builder-image
|
||||
path: ${{ runner.temp }}
|
||||
- name: Load Docker image
|
||||
if: ${{ !inputs.publish-builder }}
|
||||
run: docker load --input "${{ runner.temp }}/fw-builder.tar"
|
||||
- name: Build firmware
|
||||
env:
|
||||
repo: ${{ github.repository }}
|
||||
run: |
|
||||
docker run --rm -v ${PWD}:/workdir -e CURRENT_DEVICE_TYPE=${{ matrix.device_type }} ghcr.io/${repo,,}-fw-builder@${{ needs.build_fw_builder.outputs.image_hash }} firmware/build.sh
|
||||
docker run --rm -v ${PWD}:/workdir -e CURRENT_DEVICE_TYPE=${{ matrix.device_type }} "${{ needs.build_fw_builder.outputs.image_ref }}" firmware/build.sh
|
||||
- name: Upload built binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.device_type }}-firmware
|
||||
path: firmware/objects/*.hex
|
||||
@@ -76,17 +119,17 @@ jobs:
|
||||
unzip firmware/objects/${{ matrix.device_type }}-dfu-app.zip -d firmware/objects/${{ matrix.device_type }}-dfu-app
|
||||
unzip firmware/objects/${{ matrix.device_type }}-dfu-full.zip -d firmware/objects/${{ matrix.device_type }}-dfu-full
|
||||
- name: Upload dfu app image
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.device_type }}-dfu-app
|
||||
path: firmware/objects/${{ matrix.device_type }}-dfu-app/*
|
||||
- name: Upload dfu full image
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.device_type }}-dfu-full
|
||||
path: firmware/objects/${{ matrix.device_type }}-dfu-full/*
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-artifacts-${{ matrix.device_type }}
|
||||
path: firmware/objects/*.zip
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
on: pull_request_target
|
||||
name: Changelog Reminder
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
remind:
|
||||
name: Changelog Reminder
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Changelog Reminder
|
||||
uses: peterjgrainger/action-changelog-reminder@v1.2.0
|
||||
with:
|
||||
changelog_regex: 'CHANGELOG.md'
|
||||
customPrMessage: 'You are welcome to add an entry to the CHANGELOG.md as well'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Changelog Reminder
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const message = 'You are welcome to add an entry to the CHANGELOG.md as well';
|
||||
const pullNumber = context.payload.pull_request.number;
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
if (files.some((file) => file.filename === 'CHANGELOG.md')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const alreadyReminded = comments.some((comment) =>
|
||||
comment.user?.login === 'github-actions[bot]' && comment.body === message
|
||||
);
|
||||
|
||||
if (!alreadyReminded) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
body: message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ on:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
permissions: {}
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
@@ -21,20 +17,23 @@ jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
persist-credentials: false
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
- name: Build with Jekyll
|
||||
uses: actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: ./
|
||||
destination: ./_site
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
@@ -43,7 +42,10 @@ jobs:
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
@@ -1,44 +1,24 @@
|
||||
name: PR handler
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: pr-build-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
firmware_pipeline:
|
||||
name: Build Firmware
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
with:
|
||||
checkout-sha: "${{ github.event.pull_request.head.sha }}"
|
||||
publish-builder: false
|
||||
client_pipeline:
|
||||
name: Build Firmware
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build_client.yml
|
||||
with:
|
||||
checkout-sha: "${{ github.event.pull_request.head.sha }}"
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
name: Comment on PR
|
||||
needs:
|
||||
- firmware_pipeline
|
||||
- client_pipeline
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
message: |
|
||||
# Built artifacts for commit ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
## Firmware
|
||||
|
||||
- [Ultra APP DFU Package](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/ultra-dfu-app.zip)
|
||||
- [Ultra binaries](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/ultra-firmware.zip)
|
||||
- [Lite APP DFU Package](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/lite-dfu-app.zip)
|
||||
- [Lite binaries](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/lite-firmware.zip)
|
||||
|
||||
## Client
|
||||
|
||||
- [Linux](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/client-linux.zip)
|
||||
- [macOS](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/client-macos.zip)
|
||||
- [Windows](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/client-windows.zip)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
name: PR build artifact comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR handler"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment on PR build result
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add or update artifact links
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const run = context.payload.workflow_run;
|
||||
let pullNumber = run.pull_requests[0]?.number;
|
||||
|
||||
if (!pullNumber) {
|
||||
const head = `${run.head_repository.owner.login}:${run.head_branch}`;
|
||||
const candidates = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head,
|
||||
per_page: 100,
|
||||
});
|
||||
pullNumber = candidates.find((pull) => pull.head.sha === run.head_sha)?.number;
|
||||
}
|
||||
|
||||
if (!pullNumber) {
|
||||
core.setFailed(`Could not find the pull request for workflow run ${run.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
|
||||
if (pull.head.sha !== run.head_sha) {
|
||||
core.notice(`Skipping superseded workflow run ${run.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = '<!-- chameleon-ultra-pr-build-artifacts -->';
|
||||
const lines = [marker, `# Build result for commit ${run.head_sha}`, ''];
|
||||
|
||||
if (run.conclusion === 'success') {
|
||||
const baseUrl = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${run.id}`;
|
||||
lines.push(
|
||||
'## Firmware',
|
||||
'',
|
||||
`- [Ultra APP DFU Package](${baseUrl}/ultra-dfu-app.zip)`,
|
||||
`- [Ultra binaries](${baseUrl}/ultra-firmware.zip)`,
|
||||
`- [Lite APP DFU Package](${baseUrl}/lite-dfu-app.zip)`,
|
||||
`- [Lite binaries](${baseUrl}/lite-firmware.zip)`,
|
||||
'',
|
||||
'## Client',
|
||||
'',
|
||||
`- [Linux](${baseUrl}/client-linux.zip)`,
|
||||
`- [macOS](${baseUrl}/client-macos.zip)`,
|
||||
`- [Windows](${baseUrl}/client-windows.zip)`,
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`**Status:** ${run.conclusion ?? 'unknown'}`,
|
||||
'',
|
||||
`[View workflow run](${run.html_url})`,
|
||||
);
|
||||
}
|
||||
|
||||
const body = lines.join('\n');
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find((comment) =>
|
||||
comment.user?.login === 'github-actions[bot]' &&
|
||||
(comment.body?.includes(marker) || comment.body?.includes('# Built artifacts for commit '))
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,10 @@ name: Push handler
|
||||
on:
|
||||
push:
|
||||
|
||||
concurrency:
|
||||
group: push-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
firmware_pipeline:
|
||||
name: Build Firmware
|
||||
@@ -12,6 +16,8 @@ jobs:
|
||||
uses: ./.github/workflows/build_firmware.yml
|
||||
client_pipeline:
|
||||
name: Build Firmware
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/build_client.yml
|
||||
create_dev_release:
|
||||
permissions:
|
||||
@@ -24,15 +30,15 @@ jobs:
|
||||
- client_pipeline
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: release-artifacts-*
|
||||
merge-multiple: true
|
||||
path: release-artifacts
|
||||
- name: Upload to dev release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body: |
|
||||
Auto-Generated DFU packages from latest `main` commit.
|
||||
@@ -49,7 +55,7 @@ jobs:
|
||||
- name: Fix up release tag
|
||||
run: |
|
||||
git tag -f dev
|
||||
git push --tags -f
|
||||
git push origin refs/tags/dev --force
|
||||
create_release:
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -61,15 +67,17 @@ jobs:
|
||||
- client_pipeline
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: release-artifacts-*
|
||||
merge-multiple: true
|
||||
path: release-artifacts
|
||||
- name: Upload to tagged release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
body: |
|
||||
Auto-Generated DFU packages for Release ${{ github.ref_name }}
|
||||
@@ -80,3 +88,15 @@ jobs:
|
||||
generate_release_notes: true
|
||||
append_body: true
|
||||
files: release-artifacts/*
|
||||
|
||||
start_feature_freeze:
|
||||
name: Start the next PR review cycle
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: create_release
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/pr_review_cycle.yml
|
||||
with:
|
||||
start_feature_freeze: true
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
name: PR review cycle
|
||||
|
||||
on:
|
||||
# The tagged-release workflow calls this explicitly because events created by GITHUB_TOKEN do not normally start another workflow.
|
||||
workflow_call:
|
||||
inputs:
|
||||
start_feature_freeze:
|
||||
description: Label the next review batch
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
pull_request_target:
|
||||
types: [converted_to_draft, edited, ready_for_review, reopened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
schedule:
|
||||
- cron: "23 4 * * *"
|
||||
|
||||
env:
|
||||
DRAFT_WARNING_DAYS: 30
|
||||
DRAFT_RESPONSE_DAYS: 14
|
||||
DRAFT_CLOSE_DAYS: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-review-cycle-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
start-feature-freeze:
|
||||
name: Start feature-freeze batch
|
||||
if: >-
|
||||
inputs.start_feature_freeze == true ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'release' && github.event.release.prerelease == false)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out trusted automation
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
- name: Label all currently ready PRs
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const run = require('./.github/scripts/pr-review-cycle.js');
|
||||
await run({ github, context, core, mode: 'start-feature-freeze' });
|
||||
|
||||
handle-pull-activity:
|
||||
name: Update labels after PR activity
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out trusted automation
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
- name: Update the PR lifecycle
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const run = require('./.github/scripts/pr-review-cycle.js');
|
||||
await run({ github, context, core, mode: 'pull-activity' });
|
||||
|
||||
handle-discussion-activity:
|
||||
name: Reset inactive draft after discussion
|
||||
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out trusted automation
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
- name: Update the draft lifecycle
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const run = require('./.github/scripts/pr-review-cycle.js');
|
||||
await run({ github, context, core, mode: 'discussion-activity' });
|
||||
|
||||
scan-inactive-drafts:
|
||||
name: Scan inactive drafts
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out trusted automation
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
- name: Warn, label, or close inactive drafts
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const run = require('./.github/scripts/pr-review-cycle.js');
|
||||
await run({ github, context, core, mode: 'scan-drafts' });
|
||||
@@ -7,8 +7,11 @@ on:
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
lint-formatting:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -17,10 +20,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
@@ -32,25 +37,42 @@ jobs:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Install dependencies with uv (if lockfile present)
|
||||
if: ${{ hashFiles('software/uv.lock') != '' }}
|
||||
- name: Install dependencies with uv
|
||||
run: uv sync --dev
|
||||
|
||||
- name: Install tools with pip (fallback)
|
||||
if: ${{ hashFiles('software/uv.lock') == '' }}
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
# Try project requirements if present
|
||||
if [ -f script/requirements.txt ]; then pip install -r script/requirements.txt || true; fi
|
||||
# Ensure ruff and pyrefly are available
|
||||
pip install ruff pyrefly
|
||||
|
||||
- name: Ruff check
|
||||
run: |
|
||||
set -e
|
||||
(uv run ruff --version && uv run ruff check .) || ruff check .
|
||||
run: uv run ruff check .
|
||||
|
||||
lint-types:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: software
|
||||
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
software/pyproject.toml
|
||||
software/uv.lock
|
||||
software/script/requirements.txt
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
|
||||
- name: Install dependencies with uv
|
||||
run: uv sync --dev
|
||||
|
||||
- name: Pyrefly check
|
||||
run: |
|
||||
set -e
|
||||
(uv run pyrefly --help >/dev/null 2>&1 && uv run pyrefly check) || pyrefly check
|
||||
run: uv run pyrefly check
|
||||
|
||||
@@ -27,6 +27,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac
|
||||
- Added `hf 14a config` to deal with badly configured cards (@azuwis)
|
||||
- New Symmetrical LED Animation Mode and Improved Minimal Mode (@WillyJL)
|
||||
- Fix MF1 state reset logic and access control conditions (@unkernet)
|
||||
- Added support for SEOS credentials (@aaronjamt)
|
||||
|
||||
## [v2.1.0][2025-09-02]
|
||||
- Added UV, formatter and linter. Contribution guidelines. (@GameTec-live)
|
||||
|
||||
@@ -19,3 +19,26 @@ Heres a bit of info and a few guidelines to get you started:
|
||||
- Type safety is important. The CLI should be typesafe. Python 3.9+ offer a wide variety of type declarations. Metas [pyrefly](https://pyrefly.org/) is used to do type validation. It is recommended to install the appropriate vscode extension and check your types before opening a PR.
|
||||
- Formatting matters. Mostly. While pixelpeeping and exact rules are annoying and unnescesary, format your code in a readable and logical way. [Ruff](https://docs.astral.sh/ruff/) is used to enforce various formatting rules. You may install the Ruff vscode extension or use the CLI to format before opening a PR.
|
||||
- Avoid extra packages. Almost everyone knows the "meme" of the javascript ["is-even"](https://www.npmjs.com/package/is-even) package. While it is encouraged and makes sense to use packages where appropriate, just installing packages for the hell of it even if its a 2 liner is not sensible.
|
||||
|
||||
## Feature freeze and review cycle
|
||||
|
||||
Pull requests are reviewed in batches so maintainers and testers can focus on a manageable set of changes. The `feature freeze` label marks the PRs in the current batch. This does not mean that you can not make new Pull requests or contributions.
|
||||
|
||||
The cycle works as follows:
|
||||
|
||||
1. At the start of a cycle, every open PR that is ready for review is given the `feature freeze` label. Draft PRs are not included.
|
||||
2. Maintainers and contributors focus their review and testing on the labeled PRs. These PRs are then merged, closed, or converted back to drafts as appropriate.
|
||||
3. PRs opened or marked ready after the batch starts normally wait for the next cycle. Maintainers may make exceptions when necessary.
|
||||
4. Once no open, ready PRs remain in the current batch, a release is made and the next cycle begins. All PRs that are ready at that point enter the new batch.
|
||||
|
||||
If a PR in the current batch is converted to a draft, it leaves that batch. Marking it ready again does not automatically add it back to the current batch. It may be included in the next one.
|
||||
|
||||
### Inactive drafts
|
||||
|
||||
Draft PRs are welcome and the ideal solution while work is in progress and you may want to gather early feedback. To prevent stale PRs from cluttering up the list, inactive draft PRs will follow the following process:
|
||||
|
||||
- After 30 days without activity, a bot asks whether there is still interest in completing the PR.
|
||||
- After another 14 days without activity, the PR receives the `close soon` label.
|
||||
- After 30 more days without activity, the draft is closed. It can be reopened if development resumes.
|
||||
|
||||
A new commit, an edit to the PR, or a new comment resets the inactivity countdown. An author can also reply to the bot with `no`, `no longer interested`, `please close`, or `/close-soon` to move the PR directly to the `close soon` stage.
|
||||
@@ -47,4 +47,9 @@ Where do you find the community?
|
||||
* Devices/chameleon-ultra for usage discussions
|
||||
* [GameTec_live discord server](https://discord.gg/DJ2A4wxncK)
|
||||
|
||||
Other notable projects:
|
||||
* [Fantasi](https://fantasi.cloud)
|
||||
* [Discord](https://fantasi.cloud/discord)
|
||||
* [chameleon-ultra.js](https://github.com/taichunmin/chameleon-ultra.js/)
|
||||
|
||||
###### Searching for the docs repo? Find it [here](https://github.com/RfidResearchGroup/ChameleonUltraDocs)
|
||||
|
||||
@@ -31,6 +31,7 @@ SRC_FILES += \
|
||||
$(PROJ_DIR)/rfid/nfctag/hf/nfc_14a_4.c \
|
||||
$(PROJ_DIR)/rfid/nfctag/hf/nfc_mf1.c \
|
||||
$(PROJ_DIR)/rfid/nfctag/hf/nfc_mf0_ntag.c \
|
||||
$(PROJ_DIR)/rfid/nfctag/hf/nfc_seos.c \
|
||||
$(PROJ_DIR)/rfid/nfctag/lf/lf_tag_em.c \
|
||||
$(PROJ_DIR)/rfid/nfctag/lf/utils/fskdemod.c \
|
||||
$(PROJ_DIR)/rfid/nfctag/lf/utils/circular_buffer.c \
|
||||
|
||||
@@ -1163,6 +1163,88 @@ static data_frame_tx_t *cmd_processor_idteck_get_emu_id(uint16_t cmd, uint16_t s
|
||||
return data_frame_make(cmd, STATUS_SUCCESS, LF_IDTECK_TAG_ID_SIZE, buffer->buffer);
|
||||
}
|
||||
|
||||
static data_frame_tx_t *cmd_processor_seos_read_emu_data(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
|
||||
tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_SEOS);
|
||||
nfc_tag_seos_information_t *info = (nfc_tag_seos_information_t *)buffer->buffer;
|
||||
|
||||
uint8_t output[1+info->diversifier_len + 1+info->oid_len + 1+info->data_tag_len + 1+info->data_len + 2];
|
||||
uint16_t offset = 0;
|
||||
|
||||
output[offset++] = info->data_len;
|
||||
memcpy(output+offset, info->data, info->data_len);
|
||||
offset += info->data_len;
|
||||
|
||||
output[offset++] = info->oid_len;
|
||||
memcpy(output+offset, info->oid, info->oid_len);
|
||||
offset += info->oid_len;
|
||||
|
||||
output[offset++] = info->data_tag_len;
|
||||
memcpy(output+offset, info->data_tag, info->data_tag_len);
|
||||
offset += info->data_tag_len;
|
||||
|
||||
output[offset++] = info->diversifier_len;
|
||||
memcpy(output+offset, info->diversifier, info->diversifier_len);
|
||||
offset += info->diversifier_len;
|
||||
|
||||
output[offset++] = info->hash_alg;
|
||||
output[offset++] = info->encr_alg;
|
||||
|
||||
return data_frame_make(cmd, STATUS_SUCCESS, sizeof(output), output);
|
||||
}
|
||||
|
||||
static data_frame_tx_t *cmd_processor_seos_write_emu_data(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
|
||||
if (length < 6) {
|
||||
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
|
||||
}
|
||||
tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_SEOS);
|
||||
nfc_tag_seos_information_t *info = (nfc_tag_seos_information_t *)buffer->buffer;
|
||||
|
||||
uint16_t offset = 0;
|
||||
|
||||
uint8_t len = data[offset++];
|
||||
if (len > NFC_TAG_SEOS_DATA_MAX) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
|
||||
info->data_len = len;
|
||||
memcpy(info->data, data+offset, len);
|
||||
offset += len;
|
||||
|
||||
len = data[offset++];
|
||||
if (len > NFC_TAG_SEOS_OID_MAX) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
|
||||
info->oid_len = len;
|
||||
memcpy(info->oid, data+offset, len);
|
||||
offset += len;
|
||||
|
||||
len = data[offset++];
|
||||
if (len > NFC_TAG_SEOS_DATA_TAG_MAX) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
|
||||
info->data_tag_len = len;
|
||||
memcpy(info->data_tag, data+offset, len);
|
||||
offset += len;
|
||||
|
||||
len = data[offset++];
|
||||
if (len > NFC_TAG_SEOS_DIVERSIFIER_MAX) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
|
||||
info->diversifier_len = len;
|
||||
memcpy(info->diversifier, data+offset, len);
|
||||
offset += len;
|
||||
|
||||
info->hash_alg = data[offset++];
|
||||
info->encr_alg = data[offset++];
|
||||
|
||||
return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL);
|
||||
}
|
||||
|
||||
static data_frame_tx_t *cmd_processor_seos_write_emu_keys(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
|
||||
if (length != 16 * 3) {
|
||||
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
|
||||
}
|
||||
tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_SEOS);
|
||||
nfc_tag_seos_information_t *info = (nfc_tag_seos_information_t *)buffer->buffer;
|
||||
|
||||
memcpy(info->authkey, data+ 0, 16);
|
||||
memcpy(info->privenc, data+16, 16);
|
||||
memcpy(info->privmac, data+32, 16);
|
||||
|
||||
return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL);
|
||||
}
|
||||
|
||||
#if defined(PROJECT_CHAMELEON_ULTRA)
|
||||
// T55xx clone is only available on Chameleon Ultra; the Lite firmware
|
||||
// has no LF reader hardware and does not compile the write_*_to_t55xx
|
||||
@@ -1284,6 +1366,9 @@ static nfc_tag_14a_coll_res_reference_t *get_coll_res_data(bool write) {
|
||||
case TAG_TYPE_HF14A_4:
|
||||
info = nfc_tag_14a_4_get_coll_res();
|
||||
break;
|
||||
case TAG_TYPE_SEOS:
|
||||
info = nfc_tag_seos_get_coll_res();
|
||||
break;
|
||||
default:
|
||||
// no collision resolution data for slot
|
||||
info = NULL;
|
||||
@@ -3121,6 +3206,11 @@ static cmd_data_map_t m_data_cmd_map[] = {
|
||||
{ DATA_CMD_JABLOTRON_GET_EMU_ID, NULL, cmd_processor_jablotron_get_emu_id, NULL },
|
||||
{ DATA_CMD_IDTECK_SET_EMU_ID, NULL, cmd_processor_idteck_set_emu_id, NULL },
|
||||
{ DATA_CMD_IDTECK_GET_EMU_ID, NULL, cmd_processor_idteck_get_emu_id, NULL },
|
||||
|
||||
{ DATA_CMD_SEOS_READ_EMU_DATA, NULL, cmd_processor_seos_read_emu_data, NULL },
|
||||
{ DATA_CMD_SEOS_WRITE_EMU_DATA, NULL, cmd_processor_seos_write_emu_data, NULL },
|
||||
{ DATA_CMD_SEOS_WRITE_EMU_KEYS, NULL, cmd_processor_seos_write_emu_keys, NULL },
|
||||
|
||||
/* ISO14443-4 T=CL emulation */
|
||||
#if defined(PROJECT_CHAMELEON_ULTRA)
|
||||
/* ISO14443-4 T=CL emulation */
|
||||
|
||||
@@ -165,6 +165,9 @@
|
||||
#define DATA_CMD_MF1_GET_FIELD_OFF_DO_RESET (4039)
|
||||
#define DATA_CMD_MF1_GET_PRNG_TYPE (4040) // 0=static 1=weak(LFSR) 2=hard(rand)
|
||||
#define DATA_CMD_MF1_SET_PRNG_TYPE (4041)
|
||||
#define DATA_CMD_SEOS_READ_EMU_DATA (4042)
|
||||
#define DATA_CMD_SEOS_WRITE_EMU_DATA (4043)
|
||||
#define DATA_CMD_SEOS_WRITE_EMU_KEYS (4044)
|
||||
//
|
||||
// ******************************************************************
|
||||
|
||||
|
||||
@@ -23,24 +23,24 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
#define PCB_IBLOCK_MASK 0xC0
|
||||
#define PCB_IBLOCK_VAL 0x00
|
||||
#define PCB_RBLOCK_MASK 0xE0
|
||||
#define PCB_RBLOCK_VAL 0x80 /* R(ACK) = 0xA2/0xA3, R(NAK) = 0xB2/0xB3 */
|
||||
#define PCB_RBLOCK_MASK 0xE6 /* R-block: bit8=1, bit7=0, bit6=1, bit3=0, bit2=1 */
|
||||
#define PCB_RBLOCK_VAL 0xA2 /* R(ACK) = 0xA2/0xA3, R(NAK) = 0xB2/0xB3 */
|
||||
#define PCB_SBLOCK_MASK 0xC0
|
||||
#define PCB_SBLOCK_VAL 0xC0
|
||||
#define PCB_BLOCK_NUM 0x01
|
||||
#define PCB_CID_FOLLOWING 0x10 /* bit4: CID follows */
|
||||
#define PCB_NAD_FOLLOWING 0x08 /* bit3: NAD follows */
|
||||
#define PCB_CHAIN 0x20 /* bit5: chaining flag per ISO14443-4 Table 3 */
|
||||
#define PCB_SBLOCK_WTX 0x30
|
||||
#define PCB_CID_FOLLOWING 0x08 /* bit4: CID follows */
|
||||
#define PCB_NAD_FOLLOWING 0x04 /* bit3: NAD follows */
|
||||
#define PCB_CHAIN 0x10 /* bit5: chaining flag per ISO14443-4 Table 3 */
|
||||
#define PCB_SBLOCK_WTX 0xF2
|
||||
#define PCB_SBLOCK_DESELECT 0xC2
|
||||
#define WTX_VALUE 0x3B /* WTXM=59 (~3s extra wait) */
|
||||
#define PCB_PPS 0xD0
|
||||
#define WTX_VALUE 0x3B /* WTXM=59 (~3s extra wait) */
|
||||
|
||||
static inline bool is_iblock(uint8_t pcb) {
|
||||
return (pcb & PCB_IBLOCK_MASK) == PCB_IBLOCK_VAL;
|
||||
}
|
||||
static inline bool is_rblock(uint8_t pcb) {
|
||||
/* R-block: bit7=1, bit6=0, bit2=1, bit1=0 (mask 0xC6, value 0x82) */
|
||||
return (pcb & 0xC6) == 0x82;
|
||||
return (pcb & PCB_RBLOCK_MASK) == PCB_RBLOCK_VAL;
|
||||
}
|
||||
static inline bool is_sblock(uint8_t pcb) {
|
||||
return (pcb & PCB_SBLOCK_MASK) == PCB_SBLOCK_VAL;
|
||||
@@ -55,15 +55,7 @@ static nfc_tag_14a_4_information_t *m_tag_information = NULL;
|
||||
static nfc_tag_14a_coll_res_reference_t m_shadow_coll_res;
|
||||
|
||||
/* T=CL session state */
|
||||
static uint8_t m_block_num = 0;
|
||||
static bool m_cid_supported = false;
|
||||
static uint8_t m_cid = 0;
|
||||
static uint8_t m_apdu_buf[NFC_14A_4_MAX_APDU];
|
||||
static uint16_t m_apdu_len = 0;
|
||||
static bool m_apdu_pending = false;
|
||||
static uint8_t m_resp_buf[NFC_14A_4_MAX_APDU];
|
||||
static uint16_t m_resp_len = 0;
|
||||
static bool m_response_ready = false;
|
||||
static nfc_tag_14a_4_tcl_state_t m_tcl_session_state;
|
||||
|
||||
/* TX scratch buffer */
|
||||
static uint8_t m_tx_buf[NFC_14A_4_MAX_APDU + 4];
|
||||
@@ -160,34 +152,35 @@ static bool find_static_response(const uint8_t *apdu, uint16_t apdu_len,
|
||||
/* TX helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void send_iblock(const uint8_t *data, uint16_t len) {
|
||||
uint8_t pcb = 0x02 | (m_block_num & 0x01);
|
||||
if (m_cid_supported) pcb |= PCB_CID_FOLLOWING;
|
||||
static void send_iblock(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state) {
|
||||
uint16_t len = m_tcl_session_state->m_resp_len;
|
||||
uint8_t pcb = 0x02 | (m_tcl_session_state->m_block_num & 0x01);
|
||||
if (m_tcl_session_state->m_cid_supported) pcb |= PCB_CID_FOLLOWING;
|
||||
uint8_t off = 0;
|
||||
m_tx_buf[off++] = pcb;
|
||||
if (m_cid_supported) m_tx_buf[off++] = m_cid & 0x0F;
|
||||
if (m_tcl_session_state->m_cid_supported) m_tx_buf[off++] = m_tcl_session_state->m_cid & 0x0F;
|
||||
if (len > NFC_14A_4_MAX_APDU) len = NFC_14A_4_MAX_APDU;
|
||||
memcpy(&m_tx_buf[off], data, len);
|
||||
memcpy(&m_tx_buf[off], m_tcl_session_state->m_resp_buf, len);
|
||||
nfc_tag_14a_tx_bytes(m_tx_buf, off + len, true);
|
||||
m_block_num ^= 1;
|
||||
m_tcl_session_state->m_block_num ^= 1;
|
||||
}
|
||||
|
||||
static void send_rack(void) {
|
||||
uint8_t pcb = 0xA2 | (m_block_num & 0x01);
|
||||
if (m_cid_supported) {
|
||||
static void send_rack(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state) {
|
||||
uint8_t pcb = PCB_RBLOCK_VAL | (m_tcl_session_state->m_block_num & 0x01);
|
||||
if (m_tcl_session_state->m_cid_supported) {
|
||||
pcb |= PCB_CID_FOLLOWING;
|
||||
uint8_t buf[2] = { pcb, m_cid & 0x0F };
|
||||
uint8_t buf[2] = { pcb, m_tcl_session_state->m_cid & 0x0F };
|
||||
nfc_tag_14a_tx_bytes(buf, 2, true);
|
||||
} else {
|
||||
nfc_tag_14a_tx_bytes(&pcb, 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
static void send_wtx(void) {
|
||||
static void send_wtx(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state) {
|
||||
uint8_t buf[3];
|
||||
uint8_t off = 0;
|
||||
buf[off++] = PCB_SBLOCK_WTX | (m_cid_supported ? PCB_CID_FOLLOWING : 0);
|
||||
if (m_cid_supported) buf[off++] = m_cid & 0x0F;
|
||||
buf[off++] = PCB_SBLOCK_WTX | (m_tcl_session_state->m_cid_supported ? PCB_CID_FOLLOWING : 0);
|
||||
if (m_tcl_session_state->m_cid_supported) buf[off++] = m_tcl_session_state->m_cid & 0x0F;
|
||||
buf[off++] = WTX_VALUE;
|
||||
nfc_tag_14a_tx_bytes(buf, off, true);
|
||||
}
|
||||
@@ -196,8 +189,18 @@ static void send_wtx(void) {
|
||||
/* State handler (called from NFCT ISR on each received frame) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) {
|
||||
if (szBytes == 0) return;
|
||||
void nfc_tag_14a_4_base_respond(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state) {
|
||||
if (m_tcl_session_state->m_response_ready) {
|
||||
m_tcl_session_state->m_response_ready = false;
|
||||
send_iblock(m_tcl_session_state);
|
||||
} else {
|
||||
/* No response ready — keep reader alive with WTX */
|
||||
send_wtx(m_tcl_session_state);
|
||||
}
|
||||
}
|
||||
|
||||
bool nfc_tag_14a_4_base_handler(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state, uint8_t *data, uint16_t szBytes) {
|
||||
if (szBytes == 0) return false;
|
||||
uint8_t pcb = data[0];
|
||||
|
||||
/* ---- S-block ---- */
|
||||
@@ -206,31 +209,37 @@ static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) {
|
||||
/* Echo DESELECT */
|
||||
nfc_tag_14a_tx_bytes(data, szBytes, true);
|
||||
nfc_tag_14a_4_reset_handler();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if ((pcb & 0x3F) == (PCB_SBLOCK_WTX & 0x3F)) {
|
||||
/* Reader sending WTX — echo back with our WTXM */
|
||||
uint8_t wtxm = (szBytes > 1) ? data[szBytes - 1] & 0x3F : WTX_VALUE;
|
||||
uint8_t resp[3];
|
||||
uint8_t off = 0;
|
||||
resp[off++] = PCB_SBLOCK_WTX | (m_cid_supported ? PCB_CID_FOLLOWING : 0);
|
||||
if (m_cid_supported) resp[off++] = m_cid & 0x0F;
|
||||
resp[off++] = PCB_SBLOCK_WTX | (m_tcl_session_state->m_cid_supported ? PCB_CID_FOLLOWING : 0);
|
||||
if (m_tcl_session_state->m_cid_supported) resp[off++] = m_tcl_session_state->m_cid & 0x0F;
|
||||
resp[off++] = wtxm;
|
||||
nfc_tag_14a_tx_bytes(resp, off, true);
|
||||
/* If we now have a response ready, send it next I-block */
|
||||
if (m_response_ready) {
|
||||
m_response_ready = false;
|
||||
send_iblock(m_resp_buf, m_resp_len);
|
||||
if (m_tcl_session_state->m_response_ready) {
|
||||
m_tcl_session_state->m_response_ready = false;
|
||||
send_iblock(m_tcl_session_state);
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
if ((pcb & PCB_PPS) == PCB_PPS) {
|
||||
/* Echo back with our own PPS */
|
||||
uint8_t resp[1] = { PCB_PPS };
|
||||
nfc_tag_14a_tx_bytes(resp, 1, true);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---- R-block ---- */
|
||||
if (is_rblock(pcb)) {
|
||||
send_rack();
|
||||
return;
|
||||
send_rack(m_tcl_session_state);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---- I-block ---- */
|
||||
@@ -242,15 +251,14 @@ static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) {
|
||||
|
||||
uint8_t offset = 1;
|
||||
if (has_cid) {
|
||||
/* CID acknowledged but not used in responses (keeps protocol simpler) */
|
||||
m_cid_supported = false;
|
||||
m_tcl_session_state->m_cid_supported = true;
|
||||
offset++; /* skip CID byte */
|
||||
}
|
||||
if (has_nad) offset++;
|
||||
|
||||
if (offset >= szBytes) {
|
||||
send_rack();
|
||||
return;
|
||||
send_rack(m_tcl_session_state);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t apdu_len = szBytes - offset;
|
||||
@@ -258,95 +266,102 @@ static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) {
|
||||
|
||||
m_dbg_iblocks_rx++;
|
||||
m_dbg_last_rx_pcb = pcb;
|
||||
NRF_LOG_INFO("14A4 I-block #%d: reader_blk=%d m_block_num=%d apdu_len=%d",
|
||||
m_dbg_iblocks_rx, reader_blknum, m_block_num, apdu_len);
|
||||
NRF_LOG_INFO("14A4 I-block #%d: reader_blk=%d m_tcl_session_state->m_block_num=%d apdu_len=%d",
|
||||
m_dbg_iblocks_rx, reader_blknum, m_tcl_session_state->m_block_num, apdu_len);
|
||||
|
||||
/* Block number check per ISO14443-4 §7.5.3.3:
|
||||
* If block number matches expected, process new APDU.
|
||||
* If block number does NOT match, it is a retransmit —
|
||||
* resend the last response without re-processing. */
|
||||
if (reader_blknum != (m_block_num & 0x01)) {
|
||||
if (reader_blknum != (m_tcl_session_state->m_block_num & 0x01)) {
|
||||
/* Retransmit: resend last response */
|
||||
if (m_resp_len > 0) {
|
||||
if (m_tcl_session_state->m_resp_len > 0) {
|
||||
/* Restore block num to what we sent last time and resend */
|
||||
m_block_num ^= 1; /* undo the increment from last send */
|
||||
send_iblock(m_resp_buf, m_resp_len);
|
||||
m_tcl_session_state->m_block_num ^= 1; /* undo the increment from last send */
|
||||
send_iblock(m_tcl_session_state);
|
||||
} else {
|
||||
send_rack();
|
||||
send_rack(m_tcl_session_state);
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(m_apdu_buf, &data[offset], apdu_len);
|
||||
m_apdu_len = apdu_len;
|
||||
m_apdu_pending = true;
|
||||
m_response_ready = false;
|
||||
memcpy(m_tcl_session_state->m_apdu_buf, &data[offset], apdu_len);
|
||||
m_tcl_session_state->m_apdu_len = apdu_len;
|
||||
m_tcl_session_state->m_apdu_pending = true;
|
||||
m_tcl_session_state->m_response_ready = false;
|
||||
|
||||
if (more_chain) {
|
||||
send_rack();
|
||||
return;
|
||||
send_rack(m_tcl_session_state);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* APDU complete — check static table first, then WTX */
|
||||
{
|
||||
uint8_t *static_resp = NULL;
|
||||
uint16_t static_len = 0;
|
||||
bool _found = find_static_response(m_apdu_buf, apdu_len,
|
||||
&static_resp, &static_len);
|
||||
m_dbg_last_match = _found ? 1 : 0;
|
||||
NRF_LOG_INFO("14A4 find_static: found=%d static_len=%d resp_count=%d",
|
||||
_found, static_len, m_static_resp_count);
|
||||
if (_found) {
|
||||
m_dbg_iblocks_tx++;
|
||||
memcpy(m_resp_buf, static_resp, static_len);
|
||||
m_resp_len = static_len;
|
||||
send_iblock(m_resp_buf, m_resp_len);
|
||||
} else if (m_response_ready) {
|
||||
m_response_ready = false;
|
||||
send_iblock(m_resp_buf, m_resp_len);
|
||||
} else {
|
||||
/* No response ready — keep reader alive with WTX */
|
||||
send_wtx();
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NRF_LOG_INFO("14A-4: unknown PCB 0x%02x", pcb);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* State handler (called from NFCT ISR on each received frame) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void nfc_tag_14a_4_state_handler(uint8_t *data, uint16_t szBytes) {
|
||||
if (!nfc_tag_14a_4_base_handler(&m_tcl_session_state, data, szBytes)) return;
|
||||
|
||||
/* APDU complete — check static table first, then WTX */
|
||||
uint8_t *static_resp = NULL;
|
||||
uint16_t static_len = 0;
|
||||
bool _found = find_static_response(m_tcl_session_state.m_apdu_buf, m_tcl_session_state.m_apdu_len,
|
||||
&static_resp, &static_len);
|
||||
m_dbg_last_match = _found ? 1 : 0;
|
||||
NRF_LOG_INFO("14A4 find_static: found=%d static_len=%d resp_count=%d",
|
||||
_found, static_len, m_static_resp_count);
|
||||
if (_found) {
|
||||
m_dbg_iblocks_tx++;
|
||||
memcpy(m_tcl_session_state.m_resp_buf, static_resp, static_len);
|
||||
m_tcl_session_state.m_resp_len = static_len;
|
||||
m_tcl_session_state.m_response_ready = true;
|
||||
}
|
||||
|
||||
nfc_tag_14a_4_base_respond(&m_tcl_session_state);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* APDU relay API (for host-driven responses) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length) {
|
||||
if (!m_apdu_pending) return false;
|
||||
m_apdu_pending = false;
|
||||
*length = m_apdu_len;
|
||||
memcpy(buf, m_apdu_buf, m_apdu_len);
|
||||
if (!m_tcl_session_state.m_apdu_pending) return false;
|
||||
m_tcl_session_state.m_apdu_pending = false;
|
||||
*length = m_tcl_session_state.m_apdu_len;
|
||||
memcpy(buf, m_tcl_session_state.m_apdu_buf, m_tcl_session_state.m_apdu_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length) {
|
||||
if (length > NFC_14A_4_MAX_APDU) length = NFC_14A_4_MAX_APDU;
|
||||
memcpy(m_resp_buf, data, length);
|
||||
m_resp_len = length;
|
||||
m_response_ready = true;
|
||||
memcpy(m_tcl_session_state.m_resp_buf, data, length);
|
||||
m_tcl_session_state.m_resp_len = length;
|
||||
m_tcl_session_state.m_response_ready = true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Reset handler */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void nfc_tag_14a_4_reset_state(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state) {
|
||||
m_tcl_session_state->m_block_num = 0;
|
||||
m_tcl_session_state->m_cid_supported = false;
|
||||
m_tcl_session_state->m_cid = 0;
|
||||
m_tcl_session_state->m_apdu_pending = false;
|
||||
m_tcl_session_state->m_response_ready = false;
|
||||
m_tcl_session_state->m_apdu_len = 0;
|
||||
m_tcl_session_state->m_resp_len = 0;
|
||||
}
|
||||
|
||||
void nfc_tag_14a_4_reset_handler(void) {
|
||||
m_block_num = 0;
|
||||
m_cid_supported = false;
|
||||
m_cid = 0;
|
||||
m_apdu_pending = false;
|
||||
m_response_ready = false;
|
||||
m_apdu_len = 0;
|
||||
m_resp_len = 0;
|
||||
nfc_tag_14a_4_reset_state(&m_tcl_session_state);
|
||||
}
|
||||
|
||||
void nfc_tag_14a_4_get_debug_counters(uint8_t *rx, uint8_t *tx,
|
||||
|
||||
@@ -48,9 +48,27 @@ typedef struct __attribute__((packed)) {
|
||||
}
|
||||
nfc_tag_14a_4_information_t;
|
||||
|
||||
/* T=CL session state */
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t m_block_num;
|
||||
bool m_cid_supported;
|
||||
uint8_t m_cid;
|
||||
uint8_t m_apdu_buf[NFC_14A_4_MAX_APDU];
|
||||
uint16_t m_apdu_len;
|
||||
bool m_apdu_pending;
|
||||
uint8_t m_resp_buf[NFC_14A_4_MAX_APDU];
|
||||
uint16_t m_resp_len;
|
||||
bool m_response_ready;
|
||||
}
|
||||
nfc_tag_14a_4_tcl_state_t;
|
||||
|
||||
/* Anti-collision resource — used by get_coll_res_data in app_cmd.c */
|
||||
nfc_tag_14a_coll_res_reference_t *nfc_tag_14a_4_get_coll_res(void);
|
||||
|
||||
/* Handles the low-level ISO14443-4 communication. Returns true when a complete APDU has been read and is ready for response. */
|
||||
bool nfc_tag_14a_4_base_handler(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state, uint8_t *data, uint16_t szBytes);
|
||||
void nfc_tag_14a_4_base_respond(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state);
|
||||
|
||||
/* tag_base_map callbacks */
|
||||
int nfc_tag_14a_4_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer);
|
||||
int nfc_tag_14a_4_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
|
||||
@@ -66,6 +84,7 @@ bool nfc_tag_14a_4_get_pending_apdu(uint8_t *buf, uint16_t *length);
|
||||
void nfc_tag_14a_4_set_response(const uint8_t *data, uint16_t length);
|
||||
|
||||
/* Reset handler */
|
||||
void nfc_tag_14a_4_reset_state(nfc_tag_14a_4_tcl_state_t *m_tcl_session_state);
|
||||
void nfc_tag_14a_4_reset_handler(void);
|
||||
|
||||
#endif /* NFC_14A_4_H */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
#ifndef NFC_SEOS_H
|
||||
#define NFC_SEOS_H
|
||||
|
||||
#include "nfc_14a.h"
|
||||
#include "tag_emulation.h"
|
||||
|
||||
#define NFC_TAG_SEOS_DATA_MAX 255
|
||||
#define NFC_TAG_SEOS_OID_MAX 32
|
||||
#define NFC_TAG_SEOS_DATA_TAG_MAX 2
|
||||
#define NFC_TAG_SEOS_DIVERSIFIER_MAX 16
|
||||
|
||||
/**
|
||||
* Per-slot persistent data layout stored in FDS flash.
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
nfc_tag_14a_coll_res_entity_t res_coll;
|
||||
|
||||
uint8_t data[NFC_TAG_SEOS_DATA_MAX];
|
||||
uint8_t data_len;
|
||||
|
||||
uint8_t oid[NFC_TAG_SEOS_OID_MAX];
|
||||
uint8_t oid_len;
|
||||
|
||||
uint8_t data_tag[NFC_TAG_SEOS_DATA_TAG_MAX];
|
||||
uint8_t data_tag_len;
|
||||
|
||||
uint8_t diversifier[NFC_TAG_SEOS_DIVERSIFIER_MAX];
|
||||
uint8_t diversifier_len;
|
||||
|
||||
uint8_t hash_alg;
|
||||
uint8_t encr_alg;
|
||||
|
||||
// Keys
|
||||
uint8_t authkey[16];
|
||||
uint8_t privenc[16];
|
||||
uint8_t privmac[16];
|
||||
}
|
||||
nfc_tag_seos_information_t;
|
||||
|
||||
/* Anti-collision resource — used by get_coll_res_data in app_cmd.c */
|
||||
nfc_tag_14a_coll_res_reference_t *nfc_tag_seos_get_coll_res(void);
|
||||
|
||||
/* tag_base_map callbacks */
|
||||
int nfc_tag_seos_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer);
|
||||
int nfc_tag_seos_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer);
|
||||
bool nfc_tag_seos_data_factory(uint8_t slot, tag_specific_type_t tag_type);
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user