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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/app-build.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: App Build

on:
pull_request:
workflow_dispatch:
workflow_call:
inputs:
Expand All @@ -17,9 +16,9 @@ on:
type: string
secrets:
MACOS_SIGNING_CERTIFICATE:
required: true
required: false
MACOS_SIGNING_CERTIFICATE_PASSWORD:
required: true
required: false
outputs:
artifact-name:
description: Uploaded app artifact name
Expand All @@ -41,8 +40,6 @@ jobs:
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Test app
run: swift test
- name: Import release signing certificate
if: inputs.version != ''
env:
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/app-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: App CI

on:
push:
branches: [master]
paths:
- app/**
- .github/workflows/app-build.yml
- .github/workflows/app-ci.yml
pull_request:
paths:
- app/**
- .github/workflows/app-build.yml
- .github/workflows/app-ci.yml

permissions:
contents: read

jobs:
test:
runs-on: macos-latest
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- run: swift test

build:
uses: ./.github/workflows/app-build.yml
1 change: 0 additions & 1 deletion .github/workflows/firmware-build.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Firmware Build

on:
pull_request:
workflow_dispatch:
workflow_call:
inputs:
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/firmware-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Firmware CI

on:
push:
branches: [master]
paths:
- firmware/**
- VERSION
- channels/app-firmware.json
- packaging/assemble-release.py
- packaging/release_integrity.py
- tests/test_protocol6_firmware.py
- .github/workflows/firmware-build.yml
- .github/workflows/firmware-ci.yml
pull_request:
paths:
- firmware/**
- VERSION
- channels/app-firmware.json
- packaging/assemble-release.py
- packaging/release_integrity.py
- tests/test_protocol6_firmware.py
- .github/workflows/firmware-build.yml
- .github/workflows/firmware-ci.yml

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- run: python3 -m unittest tests/test_protocol6_firmware.py

build:
uses: ./.github/workflows/firmware-build.yml
23 changes: 20 additions & 3 deletions app/TinyTouch/DeviceServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,8 @@ final class DeviceManager {
private let heartbeatInterval: TimeInterval, heartbeatTimeout: TimeInterval
private let sessionUsesKeychain: Bool
private var failures: [String: Int] = [:], activeLeaseNonce: String?
private var enabled = true, advancedDiscovery = false, timer: Timer?, wakeObserver: NSObjectProtocol?
private var enabled = true, advancedDiscovery = false, sleeping = false, timer: Timer?
private var wakeObserver: NSObjectProtocol?, sleepObserver: NSObjectProtocol?

init(discover: (@MainActor () -> [DeviceIdentity])? = nil,
backoff: BackoffPolicy = BackoffPolicy(), random: @escaping () -> Double = { Double.random(in: 0...1) },
Expand All @@ -835,6 +836,7 @@ final class DeviceManager {

deinit {
if let wakeObserver { NSWorkspace.shared.notificationCenter.removeObserver(wakeObserver) }
if let sleepObserver { NSWorkspace.shared.notificationCenter.removeObserver(sleepObserver) }
}

func start(enabled: Bool) {
Expand Down Expand Up @@ -864,15 +866,29 @@ final class DeviceManager {

private func observeWake() {
guard wakeObserver == nil else { return }
wakeObserver = NSWorkspace.shared.notificationCenter.addObserver(
let notifications = NSWorkspace.shared.notificationCenter
sleepObserver = notifications.addObserver(
forName: NSWorkspace.willSleepNotification, object: nil, queue: .main
) { [weak self] _ in
Task { @MainActor in self?.prepareForSleep() }
}
wakeObserver = notifications.addObserver(
forName: NSWorkspace.didWakeNotification, object: nil, queue: .main
) { [weak self] _ in
Task { @MainActor in self?.reconnectAfterWake() }
}
}

func prepareForSleep() {
sleeping = true
sessions.values.forEach { $0.stopSynchronously() }
sessions.removeAll(); opened.removeAll(); ready.removeAll()
publish()
}

func reconnectAfterWake() {
sessions.values.forEach { $0.stop() }
sleeping = false
sessions.values.forEach { $0.stopSynchronously() }
sessions.removeAll(); opened.removeAll(); ready.removeAll(); retryDeadlines.removeAll(); failures.removeAll()
scan()
}
Expand All @@ -887,6 +903,7 @@ final class DeviceManager {
nonisolated static func canReconnect(deadline: Date?, now: Date = Date()) -> Bool { deadline.map { now >= $0 } ?? true }

func scan() {
guard !sleeping else { publish(); return }
if let lease = leaseObserver.active() {
if activeLeaseNonce != lease.nonce {
sessions.values.forEach { $0.stopSynchronously() }
Expand Down
8 changes: 8 additions & 0 deletions app/TinyTouchTests/HIDProtocolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,14 @@ final class HIDProtocolTests: XCTestCase {
try oldPTY.writeLine("OK STATUS firmware=before protocol=2 mode=hid sensor=ok fingerprints=1")
_ = try await first.value

await MainActor.run {
manager.prepareForSleep()
manager.scan()
}
do {
_ = try await manager.command(deviceID: "WAKE", "STATUS")
XCTFail("Sleep should close the serial session")
} catch {}
await MainActor.run {
state.identities = [.init(id: "WAKE", port: newPTY.path)]
manager.reconnectAfterWake()
Expand Down
15 changes: 3 additions & 12 deletions firmware/tiny_touch_unified/main/fingerprint.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ static const uint8_t FP_LED_RED = 0x04;
static const uint8_t FP_LED_FUNC_STEADY = 3;
static const uint8_t FP_LED_FUNC_OFF = 4;

static uint8_t current_led = 0xff;
static SemaphoreHandle_t fp_mutex;
static bool sensor_ready;
static portMUX_TYPE sensor_state_lock = portMUX_INITIALIZER_UNLOCKED;
Expand Down Expand Up @@ -192,28 +191,19 @@ static void fp_give(void) {
}

static void set_aura(uint8_t color) {
if (color == current_led) return;
uint8_t params[] = {FP_LED_FUNC_STEADY, color, color, 0};
uint8_t confirm = 0xff;
if (fp_command(0x3c, params, sizeof(params), &confirm, NULL, NULL, 1000) &&
confirm == 0x00) {
current_led = color;
} else {
current_led = 0xff;
}
fp_command(0x3c, params, sizeof(params), &confirm, NULL, NULL, 1000);
}

static void set_idle_aura(void) {
if (device_config_idle_led_on()) {
set_aura(FP_LED_BLUE);
return;
}
if (current_led == 0) return;
uint8_t params[] = {FP_LED_FUNC_OFF, 0, 0, 0};
uint8_t confirm = 0xff;
bool off = fp_command(0x3c, params, sizeof(params), &confirm, NULL, NULL, 1000) &&
confirm == 0x00;
current_led = off ? 0 : 0xff;
fp_command(0x3c, params, sizeof(params), &confirm, NULL, NULL, 1000);
}

static void show_result(bool ok) {
Expand Down Expand Up @@ -316,6 +306,7 @@ fingerprint_match_t fingerprint_authorize_poll_match(void) {
}
fingerprint_match_t match = fingerprint_match_captured(true);
if (match.slot) set_aura(FP_LED_GREEN);
else set_idle_aura();
fp_give();
return match;
}
Expand Down
31 changes: 26 additions & 5 deletions firmware/tiny_touch_unified/main/touch_pin_hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ static void secure_wipe(void *data, size_t length) {
while (length--) *cursor++ = 0;
}

static bool usb_hid_ready(void) {
return tud_mounted() && !tud_suspended() && tud_hid_ready() &&
(device_config_mode() != DEVICE_MODE_HID || tud_cdc_connected());
}

static bool wait_hid_ready(void) {
TickType_t started = xTaskGetTickCount();
while (!tud_hid_ready()) {
while (!usb_hid_ready()) {
if (!tud_mounted() || tud_suspended()) return false;
if ((TickType_t)(xTaskGetTickCount() - started) >= pdMS_TO_TICKS(2000)) {
return false;
}
Expand All @@ -38,6 +44,16 @@ static bool wait_hid_ready(void) {
return true;
}

static bool receive_password_response(char response[640], uint32_t timeout_ms) {
TickType_t started = xTaskGetTickCount();
TickType_t timeout = pdMS_TO_TICKS(timeout_ms);
while ((TickType_t)(xTaskGetTickCount() - started) < timeout) {
if (!tud_mounted() || tud_suspended() || !tud_cdc_connected()) return false;
if (xQueueReceive(password_responses, response, pdMS_TO_TICKS(50)) == pdTRUE) return true;
}
return false;
}

static bool send_key(uint8_t modifier, uint8_t key) {
uint8_t report[6] = {key, 0, 0, 0, 0, 0};
if (!wait_hid_ready()) return false;
Expand Down Expand Up @@ -265,7 +281,7 @@ static bool request_and_type_password(fingerprint_match_t match) {
snprintf(event, sizeof(event), "EV %s %lu %u %u %s", nonce,
(unsigned long)event_counter, match.slot, match.score, mac_hex);
config_console_send_line(event);
if (xQueueReceive(password_responses, response, pdMS_TO_TICKS(6000)) != pdTRUE ||
if (!receive_password_response(response, 6000) ||
!decrypt_password(pairing_key, nonce, response, password, &password_length)) goto done;
} else {
int used = snprintf(event, sizeof(event), "EV2 %s %lu %u %u", nonce,
Expand All @@ -282,7 +298,7 @@ static bool request_and_type_password(fingerprint_match_t match) {
}
if (used <= 0 || used >= sizeof(event)) goto done;
config_console_send_line(event);
if (xQueueReceive(password_responses, response, pdMS_TO_TICKS(1500)) == pdTRUE &&
if (receive_password_response(response, 1500) &&
decrypt_password_v2(nonce, response, hosts, host_count, password,
&password_length)) {
result = type_ascii(password, password_length);
Expand All @@ -296,7 +312,7 @@ static bool request_and_type_password(fingerprint_match_t match) {
snprintf(event, sizeof(event), "EV %s %lu %u %u %s", nonce,
(unsigned long)event_counter, match.slot, match.score, mac_hex);
config_console_send_line(event);
if (xQueueReceive(password_responses, response, pdMS_TO_TICKS(4500)) != pdTRUE ||
if (!receive_password_response(response, 4500) ||
!decrypt_password(pairing_key, nonce, response, password, &password_length)) goto done;
}
result = type_ascii(password, password_length);
Expand Down Expand Up @@ -364,7 +380,7 @@ static void touch_hid_task(void *arg) {

// Presence is the sole trigger for a capture. Idle operation never sends
// sensor commands and therefore never flashes a failure indication.
if (!present || !tud_hid_ready()) {
if (!present || !usb_hid_ready()) {
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
Expand All @@ -375,6 +391,11 @@ static void touch_hid_task(void *arg) {
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
if (!usb_hid_ready()) {
fingerprint_led_idle();
auth_wait_for_lift(&runtime, now);
continue;
}

handle_fingerprint_match(match);
auth_wait_for_lift(&runtime, xTaskGetTickCount());
Expand Down
18 changes: 18 additions & 0 deletions firmware/tiny_touch_unified/main/usb_ccid.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include "tinyusb_default_config.h"
#include "tusb.h"
#include "device/usbd_pvt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "touch_pin_hid.h"
#include "usb_descriptors.h"

Expand All @@ -23,10 +25,24 @@ static uint8_t tx_buf[CCID_BUF_SIZE];
static uint8_t rhport_active;
static ccid_apdu_handler_t apdu_handler;
static bool in_busy;
static TaskHandle_t recovery_task;

static void recover_usb_after_resume(void *arg) {
(void)arg;
while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// ponytail: macOS can wedge composite endpoints on resume; remove when
// TinyUSB/macOS reliably restores CDC, HID, and CCID without re-enumeration.
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(100));
tud_connect();
}
}

static void usb_event_cb(tinyusb_event_t *event, void *arg) {
(void)arg;
if (event->id == TINYUSB_EVENT_ATTACHED) touch_pin_hid_usb_attached();
else if (event->id == TINYUSB_EVENT_RESUMED && recovery_task) xTaskNotifyGive(recovery_task);
}

static uint32_t le32(const uint8_t *p) {
Expand Down Expand Up @@ -199,4 +215,6 @@ void usb_ccid_start(ccid_apdu_handler_t handler) {
tusb_cfg.descriptor.full_speed_config = tiny_touch_configuration_descriptor;
tusb_cfg.event_cb = usb_event_cb;
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
configASSERT(xTaskCreate(recover_usb_after_resume, "usb_recover", 2048, NULL, 4,
&recovery_task) == pdPASS);
}
2 changes: 1 addition & 1 deletion firmware/tiny_touch_unified/sdkconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2044,7 +2044,7 @@ CONFIG_TINYUSB_MODE_DMA=y
# TinyUSB callbacks
#
# CONFIG_TINYUSB_SUSPEND_CALLBACK is not set
# CONFIG_TINYUSB_RESUME_CALLBACK is not set
CONFIG_TINYUSB_RESUME_CALLBACK=y
# end of TinyUSB callbacks

#
Expand Down
1 change: 1 addition & 0 deletions firmware/tiny_touch_unified/sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_TINYUSB_CDC_RX_BUFSIZE=64
CONFIG_TINYUSB_CDC_TX_BUFSIZE=512
CONFIG_TINYUSB_CDC_EP_BUFSIZE=64
CONFIG_TINYUSB_RESUME_CALLBACK=y
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_TASK_WDT_EN=y
CONFIG_ESP_TASK_WDT_INIT=y
Expand Down
Loading