├── .gitignore ├── VERSION ├── doc ├── resources │ ├── clip.jpg │ ├── logic.png │ ├── banner_alpha.png │ ├── bed_of_nails.jpg │ ├── calibration.jpg │ ├── ble_ota_dfu_app.jpg │ ├── disassembly_swd.jpg │ ├── construction_banner.jpg │ ├── ble_cfg_device_id_app.jpg │ ├── airspy_schematic_partial.png │ ├── ppk-20250206T233534_ON_2.png │ ├── proto_analysis_clustering.png │ └── ppk-20250206T233615_IDLE_2.png ├── 3d │ └── pogo_programmer_clip_01.stl ├── stlink_bmdb_firmware │ ├── README.md │ └── blackmagic_stlink_bootloader_bmdb_boot.hex ├── ANT_TPMS_profile.md ├── INSTALL.md └── HARDWARE_PROTO.md ├── boards └── sks_airspy │ ├── Kconfig │ ├── sks_airspy-pinctrl.dtsi │ ├── board.yml │ ├── Kconfig.sks_airspy │ ├── Kconfig.defconfig │ ├── sks_airspy.yaml │ ├── pre_dt_board.cmake │ ├── board.cmake │ ├── sks_airspy_defconfig │ ├── fstab.dtsi │ └── sks_airspy.dts ├── include ├── spi.h ├── common.h ├── zbus_com.h ├── ant.h ├── settings.h ├── bluetooth.h ├── ant_profiles │ ├── tpms │ │ ├── ant_tpms_local.h │ │ ├── pages │ │ │ ├── ant_tpms_pages.h │ │ │ └── ant_tpms_page_1.h │ │ ├── simulator │ │ │ ├── ant_tpms_simulator_local.h │ │ │ └── ant_tpms_simulator.h │ │ ├── ant_tpms_utils.h │ │ └── ant_tpms.h │ └── common │ │ └── pages │ │ └── ant_common_page_82.h ├── sensor.h └── retained.h ├── dts └── bindings │ └── wake-signal-pin.yaml ├── NOTICE ├── sks_airspy_nrf52832.overlay ├── ant_sdk_nrf52832.patch ├── child_image └── mcuboot.conf ├── src ├── zbus_com.c ├── common.c ├── ant_profiles │ ├── ant_tpms │ │ ├── pages │ │ │ └── ant_tpms_page_1.c │ │ ├── simulator │ │ │ └── ant_tpms_simulator.c │ │ └── ant_tpms.c │ └── ant_common │ │ └── pages │ │ └── ant_common_page_82.c ├── sensor.c ├── settings.c ├── spi.c ├── main.c ├── ant.c ├── retained.c └── bluetooth.c ├── CMakeLists.txt ├── pm_static_sks_airspy_nrf52832.yml ├── prj.conf ├── .github └── workflows │ └── ci.yaml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *build*/ 2 | .vscode/ 3 | *.pem -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | VERSION_MAJOR = 1 2 | VERSION_MINOR = 2 3 | PATCHLEVEL = 3 4 | VERSION_TWEAK = 0 5 | EXTRAVERSION = -------------------------------------------------------------------------------- /doc/resources/clip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/clip.jpg -------------------------------------------------------------------------------- /doc/resources/logic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/logic.png -------------------------------------------------------------------------------- /doc/resources/banner_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/banner_alpha.png -------------------------------------------------------------------------------- /doc/resources/bed_of_nails.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/bed_of_nails.jpg -------------------------------------------------------------------------------- /doc/resources/calibration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/calibration.jpg -------------------------------------------------------------------------------- /doc/resources/ble_ota_dfu_app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/ble_ota_dfu_app.jpg -------------------------------------------------------------------------------- /doc/resources/disassembly_swd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/disassembly_swd.jpg -------------------------------------------------------------------------------- /doc/3d/pogo_programmer_clip_01.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/3d/pogo_programmer_clip_01.stl -------------------------------------------------------------------------------- /doc/resources/construction_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/construction_banner.jpg -------------------------------------------------------------------------------- /boards/sks_airspy/Kconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # SKS AIRSPY board configuration 5 | -------------------------------------------------------------------------------- /doc/resources/ble_cfg_device_id_app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/ble_cfg_device_id_app.jpg -------------------------------------------------------------------------------- /doc/resources/airspy_schematic_partial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/airspy_schematic_partial.png -------------------------------------------------------------------------------- /doc/resources/ppk-20250206T233534_ON_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/ppk-20250206T233534_ON_2.png -------------------------------------------------------------------------------- /doc/resources/proto_analysis_clustering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/proto_analysis_clustering.png -------------------------------------------------------------------------------- /doc/resources/ppk-20250206T233615_IDLE_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitmeal/sks_airspy_ant_community_fw/HEAD/doc/resources/ppk-20250206T233615_IDLE_2.png -------------------------------------------------------------------------------- /boards/sks_airspy/sks_airspy-pinctrl.dtsi: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | &pinctrl { 7 | }; 8 | -------------------------------------------------------------------------------- /boards/sks_airspy/board.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | board: 5 | name: sks_airspy 6 | vendor: sks 7 | socs: 8 | - name: nrf52832 9 | -------------------------------------------------------------------------------- /boards/sks_airspy/Kconfig.sks_airspy: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_SKS_AIRSPY 5 | select SOC_NRF52832_QFAA 6 | select SOC_DCDC_NRF52X 7 | -------------------------------------------------------------------------------- /include/spi.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_SPI_H__ 7 | #define INCLUDE_SPI_H__ 8 | 9 | int spim_init(void); 10 | 11 | #endif // INCLUDE_SPI_H__ 12 | -------------------------------------------------------------------------------- /boards/sks_airspy/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # SKS AIRSPY configuration 5 | 6 | if BOARD_SKS_AIRSPY 7 | 8 | config BT_CTLR 9 | default BT 10 | 11 | endif # BOARD_SKS_AIRSPY 12 | -------------------------------------------------------------------------------- /include/common.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_COMMON_H__ 7 | #define INCLUDE_COMMON_H__ 8 | 9 | #include 10 | 11 | uint16_t get_hwid_16bit(); 12 | 13 | #endif // INCLUDE_COMMON_H__ -------------------------------------------------------------------------------- /include/zbus_com.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_ZBUS_COM_H__ 7 | #define INCLUDE_ZBUS_COM_H__ 8 | 9 | #include 10 | 11 | ZBUS_CHAN_DECLARE(sensor_data_chan); 12 | 13 | #endif // INCLUDE_ZBUS_COM_H__ -------------------------------------------------------------------------------- /boards/sks_airspy/sks_airspy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | identifier: sks_airspy 5 | name: SKS-AIRSPY 6 | type: mcu 7 | arch: arm 8 | toolchain: 9 | - zephyr 10 | ram: 64 11 | flash: 512 12 | supported: 13 | - ble 14 | - gpio 15 | - spi 16 | - spi_bitbang 17 | vendor: sks 18 | -------------------------------------------------------------------------------- /boards/sks_airspy/pre_dt_board.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Nordic Semiconductor 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Suppress "unique_unit_address_if_enabled" to handle the following overlaps: 5 | # - power@40000000 & clock@40000000 & bprot@40000000 6 | # - acl@4001e000 & flash-controller@4001e000 7 | list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled") 8 | -------------------------------------------------------------------------------- /doc/stlink_bmdb_firmware/README.md: -------------------------------------------------------------------------------- 1 | | File | Type | Comment | 2 | |---|---|---| 3 | | `blackmagic_stlink_firmware_st_boot.hex` | Firmware | use with ST bootloader; no extras | 4 | | `blackmagic_stlink_firmware_uart_rtt_bmdb_boot.hex` | Firmware | use with BMDB bootloader; RTT support, UART on SWIM pins | 5 | | `blackmagic_stlink_bootloader_bmdb_boot.hex` | Bootloader | BMDB bootloader | -------------------------------------------------------------------------------- /include/ant.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_APP_ANT_H__ 7 | #define INCLUDE_APP_ANT_H__ 8 | 9 | #include 10 | 11 | int start_ant_device(void); 12 | 13 | void ant_sensor_data_handler_cb(const struct zbus_channel *chan); 14 | 15 | #endif // INCLUDE_APP_ANT_H__ -------------------------------------------------------------------------------- /dts/bindings/wake-signal-pin.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | description: GPIO pin to wake up controller from low power mode 5 | 6 | compatible: "wake-signal-pin" 7 | 8 | properties: 9 | gpios: 10 | type: phandle-array 11 | required: true 12 | description: | 13 | The GPIO connected to the wake up signal source. 14 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Arne Wendt (@bitmeal) 2 | 3 | The Initial Developer of some parts of this software is Nordic Semiconductor ASA (https://www.nordicsemi.com/). 4 | Copyright (c) 2015 - 2021 Nordic Semiconductor ASA. All Rights Reserved. 5 | 6 | The Initial Developer or Contributor of some parts of this software is Garmin Ltd. (https://www.garmin.com/). 7 | Copyright (c) 2023 Garmin Ltd. or its subsidiaries. All Rights Reserved. 8 | -------------------------------------------------------------------------------- /sks_airspy_nrf52832.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | / { 7 | spi_master: spibb0 { 8 | compatible = "zephyr,spi-bitbang"; 9 | status = "okay"; 10 | 11 | #address-cells = <1>; 12 | #size-cells = <0>; 13 | 14 | clk-gpios = <&gpio0 19 GPIO_ACTIVE_HIGH>; 15 | miso-gpios = <&gpio0 15 0>; 16 | 17 | // interrupt pin 18 | cs-gpios = <&gpio0 22 GPIO_ACTIVE_LOW>; 19 | }; 20 | }; -------------------------------------------------------------------------------- /ant_sdk_nrf52832.patch: -------------------------------------------------------------------------------- 1 | diff --git a/Kconfig b/Kconfig 2 | index 3d8204b..bfe9111 100644 3 | --- a/Kconfig 4 | +++ b/Kconfig 5 | @@ -11,7 +11,7 @@ menu "ANT Wireless" 6 | 7 | config ANT 8 | bool "ANT Wireless" 9 | - depends on (SOC_NRF5340_CPUNET || SOC_NRF5340_CPUAPP || SOC_NRF52840) 10 | + depends on (SOC_NRF5340_CPUNET || SOC_NRF5340_CPUAPP || SOC_NRF52840 || SOC_NRF52832) 11 | help 12 | Enable ANT ultra low power wireless protocol by Garmin Canada Inc. 13 | 14 | -------------------------------------------------------------------------------- /boards/sks_airspy/board.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | board_runner_args(jlink "--device=nRF52832_xxAA" "--speed=4000") 5 | board_runner_args(pyocd "--target=nrf52832" "--frequency=4000000") 6 | include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) 7 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 8 | include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) 9 | include(${ZEPHYR_BASE}/boards/common/blackmagicprobe.board.cmake) 10 | -------------------------------------------------------------------------------- /boards/sks_airspy/sks_airspy_defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Bootloader 5 | CONFIG_BOOTLOADER_MCUBOOT=y 6 | # # 512kB flash; two images (each < 256kB); sector size 0x1000 / 4096B 7 | # CONFIG_BOOT_MAX_IMG_SECTORS=64 8 | 9 | # Enable MPU 10 | CONFIG_ARM_MPU=y 11 | 12 | # Enable RTT 13 | CONFIG_USE_SEGGER_RTT=y 14 | 15 | # Enable GPIO 16 | CONFIG_GPIO=y 17 | 18 | # Enable SPI 19 | CONFIG_SPI=y 20 | 21 | # Pinctrl 22 | CONFIG_PINCTRL=y 23 | -------------------------------------------------------------------------------- /child_image/mcuboot.conf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | ## signing 5 | # injected by CMake 6 | # CONFIG_BOOT_SIGNATURE_KEY_FILE="../mcuboot.pem" 7 | 8 | # mcuboot 9 | CONFIG_MCUBOOT_SERIAL=n 10 | 11 | # peripherals/IO 12 | CONFIG_GPIO=n 13 | CONFIG_SERIAL=n 14 | CONFIG_USE_SEGGER_RTT=n 15 | CONFIG_CONSOLE=n 16 | CONFIG_UART_CONSOLE=n 17 | CONFIG_RTT_CONSOLE=n 18 | 19 | # logging 20 | CONFIG_LOG=n 21 | CONFIG_BOOT_BANNER=n 22 | CONFIG_MCUBOOT_BOOT_BANNER=n 23 | CONFIG_NCS_BOOT_BANNER=n 24 | 25 | -------------------------------------------------------------------------------- /src/zbus_com.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include "zbus_com.h" 7 | #include "sensor.h" 8 | 9 | ZBUS_CHAN_DEFINE(sensor_data_chan, 10 | struct sensor_readings_t, 11 | 12 | NULL, // no validator 13 | NULL, // no user data 14 | ZBUS_OBSERVERS(ant_sensor_data_handler), 15 | ZBUS_MSG_INIT( .pressure_hpa = 0, .temperature_c = 0, .voltage_mv = 0, .flags = 0, .checksum = 0) 16 | ); 17 | 18 | #include "ant.h" 19 | ZBUS_LISTENER_DEFINE(ant_sensor_data_handler, ant_sensor_data_handler_cb); -------------------------------------------------------------------------------- /include/settings.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_SETTINGS_H__ 7 | #define INCLUDE_SETTINGS_H__ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // implement first-boot initialization of all shared and required settings! 15 | #define DEVICE_ID_SETTINGS_KEY "id" 16 | 17 | int start_settings_subsys(); 18 | 19 | int load_immediate_value(const char *name, void *dest, size_t len); 20 | 21 | 22 | #endif // INCLUDE_SETTINGS_H__ -------------------------------------------------------------------------------- /boards/sks_airspy/fstab.dtsi: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | 7 | /* Flash partition table for use with MCUBoot */ 8 | &flash0 { 9 | partitions { 10 | compatible = "fixed-partitions"; 11 | #address-cells = <1>; 12 | #size-cells = <1>; 13 | 14 | boot_partition: partition@0 { 15 | label = "mcuboot"; 16 | reg = <0x00000000 0x6000>; 17 | }; 18 | slot0_partition: partition@6000 { 19 | label = "image-0"; 20 | reg = <0x00006000 0x3D000>; 21 | }; 22 | slot1_partition: partition@43000 { 23 | label = "image-1"; 24 | reg = <0x00043000 0x3D000>; 25 | }; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/common.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include "common.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | LOG_MODULE_REGISTER(common, LOG_LEVEL_INF); 13 | 14 | uint16_t get_hwid_16bit() 15 | { 16 | char hw_id_buf[HW_ID_LEN]; 17 | uint16_t hwid_16bit = 0; 18 | int ret = hw_id_get(hw_id_buf, ARRAY_SIZE(hw_id_buf)); 19 | if (ret) { 20 | LOG_ERR("hw_id_get failed (err %d)", ret); 21 | } 22 | else 23 | { 24 | hwid_16bit = (uint16_t)strtoul(hw_id_buf + (HW_ID_LEN - 1 - 2*sizeof(uint16_t)), NULL, 16); 25 | LOG_DBG("hwid: %s, hwid_16bit: %d", hw_id_buf, hwid_16bit); 26 | } 27 | 28 | return hwid_16bit; 29 | } 30 | -------------------------------------------------------------------------------- /include/bluetooth.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_BLUETOOTH_H__ 7 | #define INCLUDE_BLUETOOTH_H__ 8 | 9 | #include 10 | 11 | // config GATT service 12 | #define BT_CFG_SRV_UUID_STR "2079cd72-8955-487c-bfbf-0bf85b255f3c" 13 | #define BT_CFG_SRV_UUID_ENC BT_UUID_128_ENCODE(0x2079cd72, 0x8955, 0x487c, 0xbfbf, 0x0bf85b255f3c) 14 | #define BT_CFG_SRV_UUID BT_UUID_DECLARE_128(BT_CFG_SRV_UUID_ENC) 15 | // config service characteristics 16 | #define BT_CFG_SRV_DEVID_CHRX_UUID_STR "f819d540-6c73-44e5-94bb-dfeb32926c2b" 17 | #define BT_CFG_SRV_DEVID_CHRX_UUID_ENC BT_UUID_128_ENCODE(0xf819d540, 0x6c73, 0x44e5, 0x94bb, 0xdfeb32926c2b) 18 | #define BT_CFG_SRV_DEVID_CHRX_UUID BT_UUID_DECLARE_128(BT_CFG_SRV_DEVID_CHRX_UUID_ENC) 19 | 20 | 21 | void start_bluetooth_services(void); 22 | 23 | #endif // INCLUDE_BLUETOOTH_H__ -------------------------------------------------------------------------------- /boards/sks_airspy/sks_airspy.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | /dts-v1/; 7 | #include 8 | #include "sks_airspy-pinctrl.dtsi" 9 | #include 10 | 11 | / { 12 | model = "SKS AIRSPY"; 13 | compatible = "sks_airspy"; 14 | 15 | chosen { 16 | zephyr,console = &rtt0; 17 | zephyr,sram = &sram0; 18 | zephyr,flash = &flash0; 19 | zephyr,code-partition = &slot0_partition; 20 | }; 21 | 22 | rtt0: rtt_chan0 { 23 | compatible = "segger,rtt-uart"; 24 | status = "okay"; 25 | }; 26 | 27 | wake_pin: wake_pin { 28 | compatible = "wake-signal-pin"; 29 | gpios = <&gpio0 25 GPIO_ACTIVE_HIGH>; 30 | }; 31 | 32 | aliases { 33 | wake-pin = &wake_pin; 34 | }; 35 | }; 36 | 37 | &gpiote { 38 | status = "okay"; 39 | }; 40 | 41 | &uicr { 42 | nfct-pins-as-gpios; 43 | }; 44 | 45 | &gpio0 { 46 | status = "okay"; 47 | }; 48 | 49 | // Include flash partition table. 50 | #include "fstab.dtsi" 51 | -------------------------------------------------------------------------------- /include/ant_profiles/tpms/ant_tpms_local.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | 16 | #ifndef ANT_TPMS_LOCAL_H__ 17 | #define ANT_TPMS_LOCAL_H__ 18 | 19 | #include 20 | #include 21 | #include "ant_profiles/tpms/ant_tpms.h" 22 | 23 | #ifdef __cplusplus 24 | extern "C" { 25 | #endif 26 | 27 | /** 28 | * @addtogroup ant_tpms 29 | * @{ 30 | */ 31 | 32 | /** @brief Tire Pressure Sensor control block. */ 33 | typedef struct 34 | { 35 | uint8_t message_counter; 36 | } ant_tpms_sens_cb_t; 37 | 38 | /**@brief Tire Pressure Sensor RX control block. */ 39 | typedef struct 40 | { 41 | } ant_tpms_disp_cb_t; 42 | 43 | /** 44 | * @} 45 | */ 46 | 47 | #ifdef __cplusplus 48 | } 49 | #endif 50 | 51 | #endif // ANT_TPMS_LOCAL_H__ 52 | -------------------------------------------------------------------------------- /include/sensor.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #ifndef INCLUDE_DECODER_H__ 7 | #define INCLUDE_DECODER_H__ 8 | 9 | #include 10 | #include 11 | 12 | #define SENSOR_BUFFER_SIZE 6 13 | 14 | /* 15 | layout: 16 | [0, 17): pressure --> b[9:17].int * 16.75 - 248 17 | [17, 25) - 55 ^= temperature 18 | [25, 33) + 122 ^= voltage 19 | 35: FLAG: under voltage 20 | [40,48): XOR checksum; ignore lowest bit after shifting whole buffer 21 | */ 22 | 23 | struct __attribute__((__packed__)) sensor_readings_t { 24 | int16_t pressure_hpa; 25 | int8_t temperature_c; 26 | int16_t voltage_mv; 27 | unsigned char flags; 28 | unsigned char checksum; 29 | }; 30 | 31 | #define SENSOR_CLAMP_PRESS_LOW_HPA 75 32 | #define SENSOR_COMP_CONST_PRESS_SLOPE 16.75 33 | #define SENSOR_COMP_CONST_PRESS_OFFSET -248 34 | #define SENSOR_COMP_CONST_TEMP -55 35 | #define SENSOR_COMP_CONST_VOLT 122 36 | 37 | #define SENSOR_FLAG_UNDERVOLTAGE = 0x20 38 | 39 | #define SENSOR_ERROR_CHK 1 40 | 41 | int decode_sensor_buffer(uint8_t* buffer, struct sensor_readings_t* sensor_readings); 42 | uint8_t battery_level_percent(const int16_t voltage_mv); 43 | 44 | #endif // INCLUDE_DECODER_H__ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | cmake_minimum_required(VERSION 3.20.0) 5 | 6 | ### Zephyr 7 | ## early stage configuration 8 | # MCUBoot signing key 9 | set(mcuboot_CONFIG_BOOT_SIGNATURE_KEY_FILE \"${CMAKE_CURRENT_LIST_DIR}/mcuboot.pem\") 10 | # include board definition 11 | list(APPEND BOARD_ROOT ${CMAKE_CURRENT_LIST_DIR}) 12 | 13 | ## zephyr 14 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 15 | zephyr_include_directories(include) 16 | 17 | ## configure project 18 | project(sks_airspy_community) 19 | 20 | ### APP 21 | # app sources 22 | target_sources(app PRIVATE src/main.c) 23 | target_sources(app PRIVATE src/common.c) 24 | target_sources(app PRIVATE src/settings.c) 25 | target_sources(app PRIVATE src/bluetooth.c) 26 | target_sources(app PRIVATE src/ant.c) 27 | target_sources(app PRIVATE src/retained.c) 28 | target_sources(app PRIVATE src/spi.c) 29 | target_sources(app PRIVATE src/zbus_com.c) 30 | target_sources(app PRIVATE src/sensor.c) 31 | 32 | # ant tpms profile sources 33 | target_sources(app PRIVATE src/ant_profiles/ant_tpms/ant_tpms.c) 34 | target_sources(app PRIVATE src/ant_profiles/ant_tpms/pages/ant_tpms_page_1.c) 35 | target_sources(app PRIVATE src/ant_profiles/ant_common/pages/ant_common_page_82.c) 36 | -------------------------------------------------------------------------------- /include/ant_profiles/tpms/pages/ant_tpms_pages.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | #ifndef ANT_TPMS_PAGES_H__ 16 | #define ANT_TPMS_PAGES_H__ 17 | 18 | /** @file 19 | * 20 | * @defgroup ant_sdk_profiles_tpms_pages Tire Pressure profile pages 21 | * @{ 22 | * @ingroup ant_tpms 23 | * @brief This module implements functions for the TPMS data pages. 24 | */ 25 | 26 | #include "ant_tpms_page_1.h" // Tire pressure main data page. 27 | #include "ant_profiles/common/pages/ant_common_page_80.h" // Manufacturer's information data page. 28 | #include "ant_profiles/common/pages/ant_common_page_81.h" // Product information data page. 29 | #include "ant_profiles/common/pages/ant_common_page_82.h" // Battery status data page. 30 | 31 | #ifdef __cplusplus 32 | extern "C" { 33 | #endif 34 | 35 | 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | 40 | #endif // ANT_TPMS_PAGES_H__ 41 | /** @} */ 42 | -------------------------------------------------------------------------------- /include/ant_profiles/tpms/simulator/ant_tpms_simulator_local.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | 16 | #ifndef ANT_TPMS_SIMULATOR_LOCAL_H__ 17 | #define ANT_TPMS_SIMULATOR_LOCAL_H__ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #ifdef __cplusplus 25 | extern "C" { 26 | #endif 27 | 28 | /** 29 | * @ingroup ant_sdk_tpms_simulator 30 | * @brief TPMS simulator control block structure. */ 31 | typedef struct 32 | { 33 | bool auto_change; ///< Pressure will change automatically (if auto_change is set) or manually. 34 | // uint32_t tick_incr; ///< Fractional part of tick increment. 35 | sensorsim_state_t pressure_sensorsim_state; ///< Pressure state of the simulated sensor. 36 | sensorsim_cfg_t pressure_sensorsim_cfg; ///< Pressure configuration of the simulated sensor. 37 | }ant_tpms_simulator_cb_t; 38 | 39 | 40 | 41 | #ifdef __cplusplus 42 | } 43 | #endif 44 | 45 | #endif // ANT_TPMS_SIMULATOR_LOCAL_H__ 46 | -------------------------------------------------------------------------------- /include/retained.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Nordic Semiconductor ASA 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef RETAINED_H_ 8 | #define RETAINED_H_ 9 | 10 | #include 11 | #include 12 | 13 | /* Example of validatable retained data. */ 14 | struct retained_data { 15 | /* The uptime from the current session the last time the 16 | * retained data was updated. 17 | */ 18 | uint64_t uptime_latest; 19 | 20 | /* Cumulative uptime from all previous sessions up through 21 | * uptime_latest of this session. 22 | */ 23 | uint64_t uptime_sum; 24 | 25 | /* Number of times the application has started. */ 26 | uint32_t boots; 27 | 28 | /* Number of times the application has gone into system off. */ 29 | uint32_t off_count; 30 | 31 | /* CRC used to validate the retained data. This must be 32 | * stored little-endian, and covers everything up to but not 33 | * including this field. 34 | */ 35 | uint32_t crc; 36 | }; 37 | 38 | /* For simplicity in the sample just allow anybody to see and 39 | * manipulate the retained state. 40 | */ 41 | extern struct retained_data retained; 42 | 43 | /* Check whether the retained data is valid, and if not reset it. 44 | * 45 | * @return true if and only if the data was valid and reflects state 46 | * from previous sessions. 47 | */ 48 | bool retained_validate(void); 49 | 50 | /* Update any generic retained state and recalculate its checksum so 51 | * subsequent boots can verify the retained state. 52 | */ 53 | void retained_update(void); 54 | 55 | #endif /* RETAINED_H_ */ 56 | -------------------------------------------------------------------------------- /pm_static_sks_airspy_nrf52832.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # partition manager configuration for minimum MCUBoot size on NRF52832 5 | 6 | app: 7 | region: flash_primary 8 | address: 0x6400 9 | size: 0x3BC00 10 | 11 | 12 | mcuboot: 13 | region: flash_primary 14 | placement: 15 | after: 16 | - start 17 | address: 0x0 18 | size: 0x6000 19 | 20 | mcuboot_primary: 21 | region: flash_primary 22 | orig_span: &id001 23 | - mcuboot_pad 24 | - mcuboot_primary_app 25 | span: *id001 26 | address: 0x6000 27 | size: 0x3C000 28 | mcuboot_pad: 29 | region: flash_primary 30 | placement: 31 | align: 32 | start: 0x1000 33 | after: 34 | - mcuboot 35 | address: 0x6000 36 | size: 0x400 37 | mcuboot_primary_app: 38 | region: flash_primary 39 | placement: 40 | after: 41 | - mcuboot_pad 42 | orig_span: &id002 43 | - app 44 | span: *id002 45 | address: 0x6400 46 | size: 0x3BC00 47 | 48 | mcuboot_secondary: 49 | region: flash_primary 50 | placement: 51 | after: 52 | - mcuboot_primary 53 | before: 54 | - storage 55 | align: 56 | start: 0x1000 57 | address: 0x42000 58 | size: 0x3C000 59 | share_size: 60 | - mcuboot_primary 61 | 62 | 63 | storage: 64 | region: flash_primary 65 | placement: 66 | after: 67 | - mcuboot_secondary 68 | before: 69 | - end 70 | align: 71 | start: 0x1000 72 | address: 0x7E000 73 | size: 0x2000 74 | share_size: 75 | - mcuboot_primary 76 | 77 | 78 | sram_primary: 79 | region: sram_primary 80 | address: 0x20000000 81 | size: 0x10000 82 | -------------------------------------------------------------------------------- /include/ant_profiles/tpms/pages/ant_tpms_page_1.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2015 - 2021, Nordic Semiconductor ASA 5 | * 6 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 7 | */ 8 | 9 | #ifndef ANT_TPMS_PAGE_1_H__ 10 | #define ANT_TPMS_PAGE_1_H__ 11 | 12 | /** @file 13 | * 14 | * @defgroup ant_sdk_profiles_tpms_page1 Tire Pressure profile page 1 15 | * @{ 16 | * @ingroup ant_sdk_profiles_tpms_pages 17 | */ 18 | 19 | #include 20 | 21 | #ifdef __cplusplus 22 | extern "C" { 23 | #endif 24 | 25 | 26 | /**@brief Data structure for Tire Pressure data page 1. 27 | */ 28 | typedef struct 29 | { 30 | uint16_t pressure; ///< Pressure type; in 0.1 bar or hPa. 31 | } ant_tpms_page1_data_t; 32 | 33 | /**@brief Initialize page 1. 34 | */ 35 | #define DEFAULT_ANT_TPMS_page1() \ 36 | (ant_tpms_page1_data_t) \ 37 | { \ 38 | .pressure = 0, \ 39 | } 40 | 41 | /**@brief Function for encoding page 1. 42 | * 43 | * @param[in] p_page_data Pointer to the page data. 44 | * @param[out] p_page_buffer Pointer to the data buffer. 45 | */ 46 | void ant_tpms_page_1_encode(uint8_t * p_page_buffer, 47 | ant_tpms_page1_data_t const * p_page_data); 48 | 49 | /**@brief Function for decoding page 1. 50 | * 51 | * @param[in] p_page_buffer Pointer to the data buffer. 52 | * @param[out] p_page_data Pointer to the page data. 53 | */ 54 | void ant_tpms_page_1_decode(uint8_t const * p_page_buffer, 55 | ant_tpms_page1_data_t * p_page_data); 56 | 57 | 58 | 59 | #ifdef __cplusplus 60 | } 61 | #endif 62 | 63 | #endif // ANT_TPMS_PAGE_1_H__ 64 | /** @} */ 65 | -------------------------------------------------------------------------------- /src/ant_profiles/ant_tpms/pages/ant_tpms_page_1.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | #include 16 | #include 17 | #include 18 | #include 19 | // LOG_MODULE_REGISTER(ant_tpms_page_1, CONFIG_TPMS_PAGES_LOG_LEVEL); 20 | LOG_MODULE_REGISTER(ant_tpms_page_1, LOG_LEVEL_WRN); 21 | 22 | /**@brief tire pressure page 1 data layout structure. */ 23 | typedef struct 24 | { 25 | uint8_t _reserved[5]; 26 | uint8_t pressure[2]; 27 | // uint8_t tpms_pressure_LSB; 28 | // uint8_t tpms_pressure_MSB; 29 | } ant_tpms_page1_data_layout_t; 30 | 31 | 32 | static void page1_data_log(ant_tpms_page1_data_t const * p_page_data) 33 | { 34 | LOG_INF("Pressure [hPa]: %u", p_page_data->pressure); 35 | } 36 | 37 | 38 | void ant_tpms_page_1_encode(uint8_t * p_page_buffer, 39 | ant_tpms_page1_data_t const * p_page_data) 40 | { 41 | ant_tpms_page1_data_layout_t * p_outcoming_data = (ant_tpms_page1_data_layout_t *)p_page_buffer; 42 | 43 | page1_data_log(p_page_data); 44 | 45 | uint16_encode(p_page_data->pressure, p_outcoming_data->pressure); 46 | // uint16_encode(p_page_data->pressure, &p_outcoming_data->tpms_pressure_LSB); 47 | } 48 | 49 | 50 | void ant_tpms_page_1_decode(uint8_t const * p_page_buffer, 51 | ant_tpms_page1_data_t * p_page_data) 52 | { 53 | ant_tpms_page1_data_layout_t const * p_incoming_data = 54 | (ant_tpms_page1_data_layout_t *)p_page_buffer; 55 | 56 | p_page_data->pressure = uint16_decode(p_incoming_data->pressure); 57 | // p_page_data->pressure = uint16_decode(&p_incoming_data->tpms_pressure_LSB); 58 | 59 | page1_data_log(p_page_data); 60 | } 61 | -------------------------------------------------------------------------------- /doc/ANT_TPMS_profile.md: -------------------------------------------------------------------------------- 1 | # ANT+ TPMS Profile 2 | This TPMS device profile is no officially released profile. The information provided here is modeled after what 3 | is found in the wild, as implemented by other manufacturers. Presented in a format common to ANT+ device profile specifications. 4 | 5 | ## Channel Configuration 6 | | Parameter | Value | Comment | 7 | |---|---|---| 8 | | Channel Type | Slave (0x00) | Within the ANT protocol the master channel (0x10) allows for bi-directional communication channels and utilizes the interference avoidance techniques and other features inherent to the ANT protocol. | 9 | | Network Key | ANT+ Managed Network Key| The ANT+ Managed Network Key is governed by the ANT+ Managed Network licensing agreement. | 10 | | RF Channel Frequency | 57 (0x39) | RF Channel 57 (2457MHz) is used for ANT+ | 11 | | Transmission Type | 5 (0x05) | 5 (0x05) indicates use of common pages | 12 | | Device Type | 48 (0x30) | An ANT+ TPMS shall [MD_0001] transmit its device type as 0x30. Please see the ANT Message Protocol and Usage document for more details. | 13 | | Device Number | 1-65535 | This is a two-byte field that allows for unique identification of a given ANT+ TPMS. It is imperative that the implementation allow for a unique device number to be assigned to a given device. NOTE: The device number for the transmitting sensor shall [self-verify] not be 0x0000. | 14 | | Channel Period | 8192 counts | Data is transmitted every 8192/32768 seconds (4 Hz). | 15 | 16 | ### Channel Period (Master / display device) 17 | The channel period is set such that the display device shall [SD_0003] receive data at the full 18 | message rate (4 Hz) or at one half or one quarter of this rate; data can be received four times per 19 | second, twice per second, or once per second. The developer sets the channel period count to 20 | receive data at one of the allowable receive rates: 21 | • 8192 counts (4 Hz, 4 messages/second) 22 | • 16384 counts (2 Hz, 2 messages/second) 23 | • 32768 counts (1 Hz, 1 message/second) 24 | The minimum receive rate allowed is 32768 counts (1 Hz). 25 | The longer the count (i.e. lower receive rate) the more power is conserved by the receiver, but a 26 | tradeoff is made for the latency of the data as it is being updated at a slower rate. The 27 | implementation of the receiving message rate by the display device is chosen by the developer. 28 | -------------------------------------------------------------------------------- /src/ant_profiles/ant_tpms/simulator/ant_tpms_simulator.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | 16 | #include 17 | #include 18 | 19 | #define PRESSURE_MIN 1000 20 | #define PRESSURE_MAX 5000 21 | #define PRESSURE_INCR 100 22 | 23 | #define SIMULATOR_TIME_INCREMENT TPMS_MSG_PERIOD 24 | 25 | 26 | void ant_tpms_simulator_init(ant_tpms_simulator_t * p_simulator, 27 | ant_tpms_simulator_cfg_t const * p_config, 28 | bool auto_change) 29 | { 30 | p_simulator->p_profile = p_config->p_profile; 31 | p_simulator->_cb.auto_change = auto_change; 32 | // p_simulator->_cb.tick_incr = 0; 33 | 34 | p_simulator->_cb.pressure_sensorsim_cfg.min = PRESSURE_MIN; 35 | p_simulator->_cb.pressure_sensorsim_cfg.max = PRESSURE_MAX; 36 | p_simulator->_cb.pressure_sensorsim_cfg.incr = PRESSURE_INCR; 37 | p_simulator->_cb.pressure_sensorsim_cfg.start_at_max = false; 38 | 39 | sensorsim_init(&(p_simulator->_cb.pressure_sensorsim_state), 40 | &(p_simulator->_cb.pressure_sensorsim_cfg)); 41 | } 42 | 43 | 44 | void ant_tpms_simulator_one_iteration(ant_tpms_simulator_t * p_simulator, ant_tpms_evt_t event) 45 | { 46 | switch (event) 47 | { 48 | case ANT_TPMS_PAGE_1_UPDATED: 49 | 50 | if (p_simulator->_cb.auto_change) 51 | { 52 | sensorsim_measure(&(p_simulator->_cb.pressure_sensorsim_state), 53 | &(p_simulator->_cb.pressure_sensorsim_cfg)); 54 | } 55 | 56 | p_simulator->p_profile->TPMS_PROFILE_pressure = 57 | p_simulator->_cb.pressure_sensorsim_state.current_val; 58 | break; 59 | 60 | default: 61 | break; 62 | } 63 | } 64 | 65 | 66 | void ant_tpms_simulator_increment(ant_tpms_simulator_t * p_simulator) 67 | { 68 | if (!p_simulator->_cb.auto_change) 69 | { 70 | sensorsim_increment(&(p_simulator->_cb.pressure_sensorsim_state), 71 | &(p_simulator->_cb.pressure_sensorsim_cfg)); 72 | } 73 | } 74 | 75 | 76 | void ant_tpms_simulator_decrement(ant_tpms_simulator_t * p_simulator) 77 | { 78 | if (!p_simulator->_cb.auto_change) 79 | { 80 | sensorsim_decrement(&(p_simulator->_cb.pressure_sensorsim_state), 81 | &(p_simulator->_cb.pressure_sensorsim_cfg)); 82 | } 83 | } 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/sensor.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include "sensor.h" 7 | 8 | /* 9 | layout: 10 | [0, 17): pressure --> b[9:17].int * 16.75 - 248 11 | [17, 25) - 55 ^= temperature 12 | [25, 33) + 122 ^= voltage 13 | 35: FLAG: under voltage 14 | [40,48): XOR checksum; ignore lowest bit after shifting whole buffer 15 | */ 16 | 17 | int decode_sensor_buffer(uint8_t* buffer, struct sensor_readings_t* sensor_readings) 18 | { 19 | // shift whole buffer left by 1 20 | for(int i = 0; i < SENSOR_BUFFER_SIZE; i++) 21 | { 22 | buffer[i] <<= 1; 23 | if ( i < SENSOR_BUFFER_SIZE - 1) 24 | { 25 | buffer[i] |= buffer[i + 1] >> 7; 26 | } 27 | } 28 | 29 | // compensate and assign data 30 | sensor_readings->pressure_hpa = (uint16_t)( (float)SENSOR_COMP_CONST_PRESS_SLOPE * (int16_t)( ( (*(uint16_t*)buffer) >> 8 ) | ( (*(uint16_t*)buffer) << 8 ) ) ) + SENSOR_COMP_CONST_PRESS_OFFSET; 31 | 32 | // clamp to avoid unstable readings at zero overpressure 33 | sensor_readings->pressure_hpa = sensor_readings->pressure_hpa >= SENSOR_CLAMP_PRESS_LOW_HPA ? sensor_readings->pressure_hpa : 0; // clamp at 0 34 | 35 | sensor_readings->temperature_c = buffer[2] + SENSOR_COMP_CONST_TEMP; 36 | 37 | sensor_readings->voltage_mv = ( buffer[3] + SENSOR_COMP_CONST_VOLT ) * 10; 38 | 39 | sensor_readings->flags = buffer[4]; 40 | 41 | // build xor checksum of data, but ignore lowest bit 42 | sensor_readings->checksum = 0xFE & (buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3] ^ buffer[4]); 43 | 44 | if (sensor_readings->checksum != buffer[5]) 45 | { 46 | return SENSOR_ERROR_CHK; 47 | } 48 | 49 | return EXIT_SUCCESS; 50 | } 51 | 52 | /* based on NRF5 SDK components/libraries/util/app_util.h 53 | * 54 | * The calculation is based on a linearized version of the battery's discharge 55 | * curve. 3.0V returns 100% battery level. The limit for power failure is 2.1V and 56 | * is considered to be the lower boundary. 57 | * 58 | * The discharge curve for CR2032 is non-linear. In this model it is split into 59 | * 4 linear sections: 60 | * - Section 1: 3.0V - 2.9V = 100% - 42% (58% drop on 100 mV) 61 | * - Section 2: 2.9V - 2.74V = 42% - 18% (24% drop on 160 mV) 62 | * - Section 3: 2.74V - 2.44V = 18% - 6% (12% drop on 300 mV) 63 | * - Section 4: 2.44V - 2.1V = 6% - 0% (6% drop on 340 mV) 64 | */ 65 | uint8_t battery_level_percent(const int16_t voltage_mv) 66 | { 67 | if (voltage_mv >= 3000) 68 | { 69 | return 100; 70 | } 71 | else if (voltage_mv > 2900) 72 | { 73 | return 100 - ((3000 - voltage_mv) * 58) / 100; 74 | } 75 | else if (voltage_mv > 2740) 76 | { 77 | return 42 - ((2900 - voltage_mv) * 24) / 160; 78 | } 79 | else if (voltage_mv > 2440) 80 | { 81 | return 18 - ((2740 - voltage_mv) * 12) / 300; 82 | } 83 | else if (voltage_mv > 2100) 84 | { 85 | return 6 - ((2440 - voltage_mv) * 6) / 340; 86 | } 87 | else 88 | { 89 | return 0; 90 | } 91 | } -------------------------------------------------------------------------------- /include/ant_profiles/tpms/simulator/ant_tpms_simulator.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | 16 | #ifndef ANT_TPMS_SIMULATOR_H__ 17 | #define ANT_TPMS_SIMULATOR_H__ 18 | 19 | /** @file 20 | * 21 | * @defgroup ant_sdk_simulators ANT simulators 22 | * @ingroup ant_sdk_utils 23 | * @brief Modules that simulate sensors. 24 | * 25 | * @defgroup ant_sdk_tpms_simulator ANT TPMS simulator 26 | * @{ 27 | * @ingroup ant_sdk_simulators 28 | * @brief ANT TPMS simulator module. 29 | * 30 | * @details This module simulates pressure for the ANT TPMS profile. The module calculates 31 | * abstract values, which are handled by the TPMS pages data model to ensure that they are 32 | * compatible. It provides a handler for changing the pressure value manually and functionality 33 | * for changing the pressure automatically. 34 | * 35 | */ 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | 43 | #ifdef __cplusplus 44 | extern "C" { 45 | #endif 46 | 47 | 48 | /**@brief TPMS simulator configuration structure. */ 49 | typedef struct 50 | { 51 | ant_tpms_profile_t * p_profile; ///< Related profile. 52 | } ant_tpms_simulator_cfg_t; 53 | 54 | /**@brief TPMS simulator structure. */ 55 | typedef struct 56 | { 57 | ant_tpms_profile_t * p_profile; ///< Related profile. 58 | ant_tpms_simulator_cb_t _cb; ///< Internal control block. 59 | } ant_tpms_simulator_t; 60 | 61 | 62 | /**@brief Function for initializing the ANT TPMS simulator instance. 63 | * 64 | * @param[in] p_simulator Pointer to the simulator instance. 65 | * @param[in] p_config Pointer to the simulator configuration structure. 66 | * @param[in] auto_change Enable or disable automatic changes of the pressure. 67 | */ 68 | void ant_tpms_simulator_init(ant_tpms_simulator_t * p_simulator, 69 | ant_tpms_simulator_cfg_t const * p_config, 70 | bool auto_change); 71 | 72 | /**@brief Function for simulating a device event. 73 | * 74 | * @details Based on this event, the transmitter data is simulated. 75 | * 76 | * This function should be called in the TPMS TX event handler. 77 | */ 78 | void ant_tpms_simulator_one_iteration(ant_tpms_simulator_t * p_simulator, ant_tpms_evt_t event); 79 | 80 | /**@brief Function for incrementing the pressure value. 81 | * 82 | * @param[in] p_simulator Pointer to the simulator instance. 83 | */ 84 | void ant_tpms_simulator_increment(ant_tpms_simulator_t * p_simulator); 85 | 86 | /**@brief Function for decrementing the pressure value. 87 | * 88 | * @param[in] p_simulator Pointer to the simulator instance. 89 | */ 90 | void ant_tpms_simulator_decrement(ant_tpms_simulator_t * p_simulator); 91 | 92 | 93 | #ifdef __cplusplus 94 | } 95 | #endif 96 | 97 | #endif // ANT_TPMS_SIMULATOR_H__ 98 | /** @} */ 99 | -------------------------------------------------------------------------------- /prj.conf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Arne Wendt (@bitmeal) 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | 5 | ## image signing 6 | # injected by CMake 7 | # CONFIG_MCUBOOT_SIGNATURE_KEY_FILE="mcuboot.pem" 8 | 9 | ## hardware info 10 | CONFIG_HW_ID_LIBRARY=y 11 | CONFIG_HW_ID_LIBRARY_SOURCE_DEVICE_ID=y 12 | 13 | 14 | ## power & device management 15 | CONFIG_PM_DEVICE=y 16 | CONFIG_POWEROFF=y 17 | 18 | CONFIG_RESET_ON_FATAL_ERROR=n 19 | 20 | 21 | ## logging 22 | CONFIG_LOG=y 23 | CONFIG_LOG_MODE_DEFERRED=y 24 | CONFIG_LOG_BACKEND_RTT=y 25 | CONFIG_LOG_BACKEND_BLE=y 26 | CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048 27 | CONFIG_NCS_APPLICATION_BOOT_BANNER_STRING="SKS AIRSPY Community" 28 | 29 | ## ZBUS 30 | CONFIG_ZBUS=y 31 | 32 | ## SPI 33 | CONFIG_SPI_BITBANG=y 34 | CONFIG_SPI_NRFX=n 35 | # CONFIG_SOC_NRF52832_ALLOW_SPIM_DESPITE_PAN_58=y 36 | 37 | ## BT 38 | CONFIG_BT=y 39 | CONFIG_BT_DEVICE_NAME="SKS AIRSPY Community" 40 | CONFIG_BT_DEVICE_NAME_DYNAMIC=y 41 | CONFIG_BT_DEVICE_NAME_MAX=28 42 | # https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Assigned_Numbers/out/en/Assigned_Numbers.pdf?v=1729112093517 43 | # 0x0559 / 1369 Vehicle Tire Pressure Sensor 44 | # CONFIG_BT_DEVICE_APPEARANCE=1369 45 | # 0x0480 / 1152 Generic Cycling 46 | CONFIG_BT_DEVICE_APPEARANCE=1152 47 | CONFIG_BT_PERIPHERAL=y 48 | CONFIG_BT_SMP=y 49 | CONFIG_BT_MAX_CONN=1 50 | 51 | # Disable Bluetooth ping support 52 | CONFIG_BT_CTLR_LE_PING=n 53 | 54 | # Allow for large Bluetooth data packets. 55 | # --> DFU 56 | CONFIG_BT_L2CAP_TX_MTU=498 57 | CONFIG_BT_BUF_ACL_RX_SIZE=502 58 | CONFIG_BT_BUF_ACL_TX_SIZE=502 59 | CONFIG_BT_CTLR_DATA_LENGTH_MAX=251 60 | 61 | 62 | ## ANT 63 | CONFIG_ANT=y 64 | CONFIG_ANT_TOTAL_CHANNELS_ALLOCATED=1 65 | # Evaluation key for ANT 66 | # Set this Kconfig to use the evaluation stack for SINGLE CORE builds (for example, nRF52 Series). 67 | # You MUST obtain a valid commercial license key BEFORE releasing a product to market that uses ANT. 68 | # For more information about licensing please see CONFIG_ANT_LICENSE_KEY and visit the website below: 69 | # https://www.thisisant.com/developer/ant/licensing 70 | CONFIG_ANT_EVALUATION_KEY=y 71 | 72 | CONFIG_ANT_CHANNEL_CONFIG=y 73 | CONFIG_ANT_KEY_MANAGER=y 74 | 75 | CONFIG_HEAP_MEM_POOL_SIZE=4096 76 | 77 | # TPMS 78 | CONFIG_ANT_COMMON=y 79 | 80 | 81 | ## Settings storage 82 | CONFIG_NVS=y 83 | CONFIG_SETTINGS=y 84 | CONFIG_SETTINGS_NVS=y 85 | 86 | # allow flash writes 87 | CONFIG_MPU_ALLOW_FLASH_WRITE=y 88 | 89 | ## DFU 90 | # Enable MCUmgr and dependencies. 91 | CONFIG_MCUMGR=y 92 | 93 | CONFIG_FLASH=y 94 | CONFIG_STREAM_FLASH=y 95 | CONFIG_FLASH_MAP=y 96 | CONFIG_NET_BUF=y 97 | CONFIG_ZCBOR=y 98 | CONFIG_CRC=y 99 | 100 | # add 256 bytes to accommodate upload command 101 | CONFIG_MAIN_STACK_SIZE=2048 102 | CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2304 103 | 104 | # Enable most core commands. 105 | CONFIG_IMG_MANAGER=y 106 | CONFIG_MCUMGR_GRP_IMG=y 107 | CONFIG_MCUMGR_GRP_OS=y 108 | CONFIG_MCUMGR_GRP_OS_MCUMGR_PARAMS=y 109 | 110 | # disable storage erase to keep persistent settings 111 | CONFIG_MCUMGR_GRP_ZBASIC_STORAGE_ERASE=n 112 | 113 | # Enable the Bluetooth mcumgr transport (unauthenticated). 114 | CONFIG_MCUMGR_TRANSPORT_BT=y 115 | CONFIG_MCUMGR_TRANSPORT_BT_AUTHEN=n 116 | CONFIG_MCUMGR_TRANSPORT_BT_CONN_PARAM_CONTROL=y 117 | 118 | # Enable the mcumgr Packet Reassembly feature over Bluetooth and its configuration dependencies. 119 | # MCUmgr buffer size is optimized to fit one SMP packet divided into five Bluetooth Write Commands, 120 | # transmitted with the maximum possible MTU value: 498 bytes. 121 | CONFIG_MCUMGR_TRANSPORT_BT_REASSEMBLY=y 122 | CONFIG_MCUMGR_TRANSPORT_NETBUF_SIZE=2475 123 | CONFIG_MCUMGR_TRANSPORT_WORKQUEUE_STACK_SIZE=4608 124 | 125 | -------------------------------------------------------------------------------- /src/settings.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include "common.h" 7 | #include "settings.h" 8 | 9 | #include 10 | LOG_MODULE_REGISTER(app_settings, LOG_LEVEL_INF); 11 | 12 | 13 | /* 14 | * START: based on settings sample: zephyr/samples/subsys/settings 15 | * 16 | */ 17 | 18 | struct direct_immediate_value { 19 | size_t len; 20 | void *dest; 21 | uint8_t fetched; 22 | }; 23 | 24 | static int direct_loader_immediate_value(const char *name, size_t len, 25 | settings_read_cb read_cb, void *cb_arg, 26 | void *param) 27 | { 28 | const char *next; 29 | size_t name_len; 30 | int rc; 31 | struct direct_immediate_value *one_value = 32 | (struct direct_immediate_value *)param; 33 | 34 | name_len = settings_name_next(name, &next); 35 | 36 | if (name_len == 0) { 37 | if (len == one_value->len) { 38 | rc = read_cb(cb_arg, one_value->dest, len); 39 | if (rc >= 0) { 40 | one_value->fetched = 1; 41 | LOG_DBG("immediate load: OK"); 42 | return 0; 43 | } 44 | 45 | LOG_ERR("immediate load failed; (rc %d)", rc); 46 | return rc; 47 | } 48 | return -EINVAL; 49 | } 50 | 51 | /* other keys aren't served by the callback 52 | * Return success in order to skip them 53 | * and keep storage processing. 54 | */ 55 | return 0; 56 | } 57 | 58 | int load_immediate_value(const char *name, void *dest, size_t len) 59 | { 60 | int rc; 61 | struct direct_immediate_value dov; 62 | 63 | dov.fetched = 0; 64 | dov.len = len; 65 | dov.dest = dest; 66 | 67 | rc = settings_load_subtree_direct(name, direct_loader_immediate_value, 68 | (void *)&dov); 69 | if (rc == 0) { 70 | if (!dov.fetched) { 71 | rc = -ENOENT; 72 | } 73 | } 74 | 75 | return rc; 76 | } 77 | 78 | /* 79 | * END: based on settings sample: zephyr/samples/subsys/settings 80 | * 81 | */ 82 | 83 | // TODO(bitmeal): implement universal helper method for default inti 84 | 85 | static int initialize_settings_defaults_DEVICE_ID() 86 | { 87 | int rc; 88 | 89 | uint16_t device_id; 90 | 91 | while(load_immediate_value(DEVICE_ID_SETTINGS_KEY, &device_id, sizeof(device_id)) == -ENOENT) 92 | { 93 | LOG_WRN("setting %s not existent; initializing to default", DEVICE_ID_SETTINGS_KEY); 94 | 95 | device_id = get_hwid_16bit(); 96 | 97 | rc = settings_save_one(DEVICE_ID_SETTINGS_KEY, (const void *)&device_id, sizeof(device_id)); 98 | if (rc) 99 | { 100 | LOG_WRN("failed writing default value for %s; (rc %d)", DEVICE_ID_SETTINGS_KEY, rc); 101 | } 102 | else 103 | { 104 | LOG_INF("initialized %s: %d", DEVICE_ID_SETTINGS_KEY, device_id); 105 | } 106 | }; 107 | 108 | // final load (again) 109 | rc = load_immediate_value(DEVICE_ID_SETTINGS_KEY, &device_id, sizeof(device_id)); 110 | 111 | if (rc == 0) 112 | { 113 | LOG_DBG("loaded { %s: %d }", DEVICE_ID_SETTINGS_KEY, device_id); 114 | } 115 | else 116 | { 117 | LOG_ERR("failed loading %s; (rc %d)", DEVICE_ID_SETTINGS_KEY, rc); 118 | return rc; 119 | } 120 | 121 | return EXIT_SUCCESS; 122 | } 123 | 124 | static int initialize_settings_defaults() 125 | { 126 | int rc; 127 | 128 | rc = initialize_settings_defaults_DEVICE_ID(); 129 | if( rc ){ return rc; } 130 | 131 | return EXIT_SUCCESS; 132 | } 133 | 134 | int start_settings_subsys() 135 | { 136 | int rc; 137 | 138 | rc = settings_subsys_init(); 139 | if (rc) { 140 | LOG_ERR("settings subsystem initialization failed; (rc %d)", rc); 141 | return rc; 142 | } 143 | 144 | // first-boot (after upgrade/changes) initialization of all shared and required settings! 145 | rc = initialize_settings_defaults(); 146 | 147 | return rc; 148 | } 149 | -------------------------------------------------------------------------------- /src/spi.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | LOG_MODULE_REGISTER(spim, LOG_LEVEL_INF); 14 | 15 | #include "spi.h" 16 | #include "zbus_com.h" 17 | #include "sensor.h" 18 | 19 | // SPI: 20 | // /INT: active low 21 | // CLK: idle low 22 | // sample on CLK rise 23 | // freq: 47 / 4.5317e-3 ^= 10.37 kHz 24 | 25 | #define SPI_MASTER_DT_LABEL spi_master 26 | #define SPI_MASTER_NODE DT_NODELABEL(SPI_MASTER_DT_LABEL) 27 | 28 | // SPI slave 29 | const struct device *spim_dev; 30 | static const struct gpio_dt_spec int_gpio = GPIO_DT_SPEC_GET(SPI_MASTER_NODE, cs_gpios); 31 | 32 | static const struct spi_config spim_cfg = { 33 | // .frequency = 125000, 34 | .frequency = 10000, 35 | .operation = SPI_WORD_SET(8) | 36 | // SPI_TRANSFER_MSB | 37 | SPI_OP_MODE_MASTER, 38 | .slave = 0, 39 | .cs = NULL 40 | }; 41 | #define SPIM_INT_TRANSFER_DELAY_MS 0 42 | 43 | 44 | static void spim_receive(struct k_work *work); 45 | K_WORK_DELAYABLE_DEFINE(spim_receive_work, spim_receive); 46 | 47 | static struct gpio_callback int_cb_data; 48 | void int_cb_handler(const struct device *dev, struct gpio_callback *cb, 49 | uint32_t pins) 50 | { 51 | LOG_DBG("interrupt signal received from slave; scheduling SPI receive"); 52 | 53 | k_work_schedule(&spim_receive_work, K_MSEC(SPIM_INT_TRANSFER_DELAY_MS)); 54 | } 55 | 56 | int spim_init(void) 57 | { 58 | int ret; 59 | 60 | spim_dev = DEVICE_DT_GET(SPI_MASTER_NODE); 61 | 62 | if (spim_dev == NULL) 63 | { 64 | LOG_ERR("Could not get %s device", spim_dev->name); 65 | return EXIT_FAILURE; 66 | } 67 | 68 | if (!device_is_ready(spim_dev)) 69 | { 70 | LOG_ERR("SPI slave device not ready!"); 71 | return EXIT_FAILURE; 72 | } 73 | 74 | if (!gpio_is_ready_dt(&int_gpio)) 75 | { 76 | LOG_ERR("Error: SPIM interrupt GPIO device %s is not ready", 77 | int_gpio.port->name); 78 | return EXIT_FAILURE; 79 | } 80 | 81 | ret = gpio_pin_configure_dt(&int_gpio, GPIO_INPUT); 82 | if (ret != 0) 83 | { 84 | LOG_ERR("Error %d: failed to configure %s pin %d", 85 | ret, int_gpio.port->name, int_gpio.pin); 86 | return EXIT_FAILURE; 87 | } 88 | 89 | ret = gpio_pin_interrupt_configure_dt(&int_gpio, GPIO_INT_EDGE_TO_ACTIVE); 90 | if (ret != 0) 91 | { 92 | LOG_ERR("Error %d: failed to configure interrupt on %s pin %d\n", 93 | ret, int_gpio.port->name, int_gpio.pin); 94 | return EXIT_FAILURE; 95 | } 96 | 97 | gpio_init_callback(&int_cb_data, int_cb_handler, BIT(int_gpio.pin)); 98 | 99 | ret = gpio_add_callback(int_gpio.port, &int_cb_data); 100 | if (ret != 0) 101 | { 102 | LOG_ERR("Error %d: failed to configure callback for interrupt on %s pin %d\n", 103 | ret, int_gpio.port->name, int_gpio.pin); 104 | return EXIT_FAILURE; 105 | } 106 | 107 | LOG_DBG("SPI device %s OK", spim_dev->name); 108 | 109 | return EXIT_SUCCESS; 110 | } 111 | 112 | static void spim_receive(struct k_work *work) 113 | { 114 | int err; 115 | 116 | struct sensor_readings_t sensor_data; 117 | 118 | static uint8_t rx_buffer[SENSOR_BUFFER_SIZE]; 119 | struct spi_buf rx_buf = {.buf = rx_buffer, .len = sizeof(rx_buffer),}; 120 | const struct spi_buf_set rx = {.buffers = &rx_buf, .count = 1}; 121 | 122 | err = spi_read(spim_dev, &spim_cfg, &rx); 123 | if (err < 0) 124 | { 125 | LOG_ERR("SPI error: %d", err); 126 | } 127 | else 128 | { 129 | err = decode_sensor_buffer(rx_buffer, &sensor_data); 130 | 131 | if (err == SENSOR_ERROR_CHK) 132 | { 133 | LOG_WRN("SPI sensor data checksum error! buff: %x; decoder: %x", rx_buffer[5], sensor_data.checksum); 134 | return; 135 | } 136 | 137 | LOG_HEXDUMP_INF(rx_buffer, sizeof(rx_buffer), "SPI rx:"); 138 | LOG_INF("P[hPa]: %u; T[C]: %d; V[mV]: %u", sensor_data.pressure_hpa, sensor_data.temperature_c, sensor_data.voltage_mv); 139 | 140 | zbus_chan_pub(&sensor_data_chan, &sensor_data, K_MSEC(250)); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /include/ant_profiles/common/pages/ant_common_page_82.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 - 2021, Nordic Semiconductor ASA 3 | * 4 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 5 | */ 6 | #ifndef ANT_COMMON_PAGE_82_H__ 7 | #define ANT_COMMON_PAGE_82_H__ 8 | 9 | /** @file 10 | * 11 | * @defgroup ant_sdk_common_pages ANT+ common pages 12 | * @{ 13 | * @ingroup ant_sdk_profiles 14 | * @brief This module implements functions for the ANT+ common pages. 15 | * @details ANT+ common data pages define common data formats that can be used by any device on any ANT network. The ability to send and receive these common pages is defined by the transmission type of the ANT channel parameter. 16 | * 17 | * Note that all unused pages in this section are not defined and therefore cannot be used. 18 | * @} 19 | * 20 | * @defgroup ant_common_page_82 ANT+ common page 82 21 | * @{ 22 | * @ingroup ant_sdk_common_pages 23 | */ 24 | 25 | #include 26 | 27 | #ifdef __cplusplus 28 | extern "C" { 29 | #endif 30 | 31 | #define ANT_COMMON_PAGE_82 (82) ///< @brief ID value of common page 82. 32 | 33 | typedef enum { 34 | ANT_COMMON_page82_BATTERY_STATE_RESERVED_0 = 0, 35 | ANT_COMMON_page82_BATTERY_STATE_NEW = 1, 36 | ANT_COMMON_page82_BATTERY_STATE_GOOD = 2, 37 | ANT_COMMON_page82_BATTERY_STATE_OK = 3, 38 | ANT_COMMON_page82_BATTERY_STATE_LOW = 4, 39 | ANT_COMMON_page82_BATTERY_STATE_CRITICAL = 5, 40 | ANT_COMMON_page82_BATTERY_STATE_RESERVED_6 = 6, 41 | ANT_COMMON_page82_BATTERY_STATE_INVALID = 7 42 | } ANT_COMMON_page82_BATTERY_STATE; 43 | 44 | const char* get_ant_common_page_82_battery_status_string(const ANT_COMMON_page82_BATTERY_STATE state); 45 | 46 | /**@brief Data structure for ANT+ common data page 82. 47 | * 48 | * @note This structure implements only page 82 specific data. 49 | */ 50 | typedef struct 51 | { 52 | uint8_t battery_count; ///< Number of batteries. 4 bits 53 | uint8_t battery_id; ///< Battery identifier. 4 bits 54 | uint32_t operating_time; ///< Cumulative Operating Time in seconds [s]. 55 | uint16_t battery_voltage_mv; ///< Battery voltage in milli Volts [mV]. 56 | ANT_COMMON_page82_BATTERY_STATE battery_status; ///< Descriptive battery status: NEW, GOOD, OK, LOW, CRITICAL 57 | 58 | } ant_common_page82_data_t; 59 | 60 | /**@brief Initialize page 82. 61 | */ 62 | #define DEFAULT_ANT_COMMON_page82() \ 63 | (ant_common_page82_data_t) \ 64 | { \ 65 | .battery_count = 0xF, \ 66 | .battery_id = 0xF, \ 67 | .operating_time = 0, \ 68 | .battery_voltage_mv = 0, \ 69 | .battery_status = ANT_COMMON_page82_BATTERY_STATE_INVALID \ 70 | } 71 | 72 | /**@brief Initialize page 82. 73 | */ 74 | #define ANT_COMMON_page82(bat_cnt, bat_id, op_time, bat_vlt, bat_sta) \ 75 | (ant_common_page82_data_t) \ 76 | { \ 77 | .battery_count = (bat_cnt), \ 78 | .battery_id = (bat_id), \ 79 | .operating_time = (op_time), \ 80 | .battery_voltage_mv = (bat_vlt), \ 81 | .battery_status = (bat_sta) \ 82 | } 83 | 84 | /**@brief Function for encoding page 82. 85 | * 86 | * @param[in] p_page_data Pointer to the page data. 87 | * @param[out] p_page_buffer Pointer to the data buffer. 88 | */ 89 | void ant_common_page_82_encode(uint8_t * p_page_buffer, 90 | volatile ant_common_page82_data_t const * p_page_data); 91 | 92 | /**@brief Function for decoding page 82. 93 | * 94 | * @param[in] p_page_buffer Pointer to the data buffer. 95 | * @param[out] p_page_data Pointer to the page data. 96 | */ 97 | void ant_common_page_82_decode(uint8_t const * p_page_buffer, 98 | volatile ant_common_page82_data_t * p_page_data); 99 | 100 | 101 | #ifdef __cplusplus 102 | } 103 | #endif 104 | 105 | #endif // ANT_COMMON_PAGE_82_H__ 106 | /** @} */ 107 | -------------------------------------------------------------------------------- /include/ant_profiles/tpms/ant_tpms_utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021 Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | #ifndef ANT_TPMS_UTILS_H__ 16 | #define ANT_TPMS_UTILS_H__ 17 | 18 | #include 19 | #include 20 | 21 | #ifdef __cplusplus 22 | extern "C" { 23 | #endif 24 | 25 | /** @file 26 | * 27 | * @defgroup ant_sdk_profiles_tpms_utils Tire Pressure profile utilities 28 | * @{ 29 | * @ingroup ant_tpms 30 | * @brief This module implements utilities for the Tire Pressure profile. 31 | * 32 | */ 33 | 34 | /**@brief Macro for performing rounded integer division (as opposed to truncating the result). 35 | * 36 | * @param[in] A Numerator. 37 | * @param[in] B Denominator. 38 | * 39 | * @return Rounded (integer) result of dividing A by B. 40 | */ 41 | #define ROUNDED_DIV(A, B) (((A) + ((B) / 2)) / (B)) 42 | 43 | /**@brief Function for encoding a uint16 value. 44 | * 45 | * @param[in] value Value to be encoded. 46 | * @param[out] p_encoded_data Buffer where the encoded data is to be written. 47 | * 48 | * @return Number of bytes written. 49 | */ 50 | static inline uint8_t uint16_encode(uint16_t value, uint8_t * p_encoded_data) 51 | { 52 | p_encoded_data[0] = (uint8_t) ((value & 0x00FF) >> 0); 53 | p_encoded_data[1] = (uint8_t) ((value & 0xFF00) >> 8); 54 | return sizeof(uint16_t); 55 | } 56 | 57 | /**@brief Function for decoding a uint16 value. 58 | * 59 | * @param[in] p_encoded_data Buffer where the encoded data is stored. 60 | * 61 | * @return Decoded value. 62 | */ 63 | static inline uint16_t uint16_decode(const uint8_t * p_encoded_data) 64 | { 65 | return ( (((uint16_t)((uint8_t *)p_encoded_data)[0])) | 66 | (((uint16_t)((uint8_t *)p_encoded_data)[1]) << 8 )); 67 | } 68 | 69 | /**@brief Function for encoding a uint24 value. 70 | * 71 | * @param[in] value Value to be encoded. 72 | * @param[out] p_encoded_data Buffer where the encoded data is to be written. 73 | * 74 | * @return Number of bytes written. 75 | */ 76 | static inline uint8_t uint24_encode(uint32_t value, uint8_t * p_encoded_data) 77 | { 78 | p_encoded_data[0] = (uint8_t) ((value & 0x000000FF) >> 0); 79 | p_encoded_data[1] = (uint8_t) ((value & 0x0000FF00) >> 8); 80 | p_encoded_data[2] = (uint8_t) ((value & 0x00FF0000) >> 16); 81 | return 3; 82 | } 83 | 84 | /**@brief Function for decoding a uint24 value. 85 | * 86 | * @param[in] p_encoded_data Buffer where the encoded data is stored. 87 | * 88 | * @return Decoded value. 89 | */ 90 | static inline uint32_t uint24_decode(const uint8_t * p_encoded_data) 91 | { 92 | return ( (((uint32_t)((uint8_t *)p_encoded_data)[0])) | 93 | (((uint32_t)((uint8_t *)p_encoded_data)[1]) << 8 ) | 94 | (((uint32_t)((uint8_t *)p_encoded_data)[2]) << 16 )); 95 | } 96 | 97 | /**@brief Function for encoding a uint32 value. 98 | * 99 | * @param[in] value Value to be encoded. 100 | * @param[out] p_encoded_data Buffer where the encoded data is to be written. 101 | * 102 | * @return Number of bytes written. 103 | */ 104 | static inline uint8_t uint32_encode(uint32_t value, uint8_t * p_encoded_data) 105 | { 106 | p_encoded_data[0] = (uint8_t) ((value & 0x000000FF) >> 0); 107 | p_encoded_data[1] = (uint8_t) ((value & 0x0000FF00) >> 8); 108 | p_encoded_data[2] = (uint8_t) ((value & 0x00FF0000) >> 16); 109 | p_encoded_data[3] = (uint8_t) ((value & 0xFF000000) >> 24); 110 | return sizeof(uint32_t); 111 | } 112 | 113 | /**@brief Function for decoding a uint32 value. 114 | * 115 | * @param[in] p_encoded_data Buffer where the encoded data is stored. 116 | * 117 | * @return Decoded value. 118 | */ 119 | static inline uint32_t uint32_decode(const uint8_t * p_encoded_data) 120 | { 121 | return ( (((uint32_t)((uint8_t *)p_encoded_data)[0]) << 0) | 122 | (((uint32_t)((uint8_t *)p_encoded_data)[1]) << 8) | 123 | (((uint32_t)((uint8_t *)p_encoded_data)[2]) << 16) | 124 | (((uint32_t)((uint8_t *)p_encoded_data)[3]) << 24 )); 125 | } 126 | 127 | 128 | /** @} */ 129 | 130 | #ifdef __cplusplus 131 | } 132 | #endif 133 | 134 | #endif // ANT_TPMS_UTILS_H__ 135 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | LOG_MODULE_REGISTER(main, LOG_LEVEL_WRN); 18 | 19 | #include "retained.h" 20 | #include "settings.h" 21 | 22 | #include "bluetooth.h" 23 | #include "ant.h" 24 | #include "spi.h" 25 | 26 | 27 | 28 | // TODO: check DT nodes on compile time 29 | // #if !DT_NODE_EXISTS(DT_NODELABEL(wake_pin)) 30 | // #error "DT nodes not properly configured." 31 | // #endif 32 | // #define SW0_NODE DT_ALIAS(sw0) 33 | // #if !DT_NODE_HAS_STATUS(SW0_NODE, okay) 34 | // #error "Unsupported board: sw0 devicetree alias is not defined" 35 | // #endif 36 | // static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET_OR(SW0_NODE, gpios, 37 | // {0}); 38 | 39 | static const struct gpio_dt_spec wake_signal = GPIO_DT_SPEC_GET(DT_ALIAS(wake_pin), gpios); 40 | 41 | #define SUPERVISION_CYCLE_TIME_MS 1000 42 | #define SYS_POWEROFF_DELAY 500 43 | 44 | static void poweroff(struct k_work *work); 45 | K_WORK_DELAYABLE_DEFINE(poweroff_work, poweroff); 46 | 47 | static void supervise(struct k_work *work); 48 | K_WORK_DELAYABLE_DEFINE(supervision_work, supervise); 49 | 50 | 51 | static void poweroff(struct k_work *work) 52 | { 53 | // spis_suspend(); 54 | 55 | int ret = gpio_pin_interrupt_configure_dt(&wake_signal, GPIO_INT_LEVEL_ACTIVE); 56 | 57 | if (ret != 0) 58 | { 59 | LOG_ERR("Error %d: failed to configure interrupt on %s pin %d\n", 60 | ret, wake_signal.port->name, wake_signal.pin); 61 | 62 | k_work_schedule(&supervision_work, K_MSEC(SUPERVISION_CYCLE_TIME_MS)); 63 | } 64 | else 65 | { 66 | LOG_INF("Set up wake signal at %s pin %d\n", wake_signal.port->name, wake_signal.pin); 67 | 68 | /* Update the retained state */ 69 | retained.off_count += 1; 70 | retained_update(); 71 | 72 | LOG_INF("Powering OFF NOW"); 73 | sys_poweroff(); 74 | 75 | // infinite loop for emulated power off 76 | while(true){ continue; } 77 | } 78 | } 79 | 80 | static void supervise(struct k_work *work) 81 | { 82 | int val = gpio_pin_get_dt(&wake_signal); 83 | 84 | if (val == 0) 85 | { 86 | LOG_INF("Will power off in %dms", SYS_POWEROFF_DELAY); 87 | k_work_schedule(&poweroff_work, K_MSEC(SYS_POWEROFF_DELAY)); 88 | } 89 | else 90 | { 91 | k_work_schedule(&supervision_work, K_MSEC(SUPERVISION_CYCLE_TIME_MS)); 92 | } 93 | } 94 | 95 | int main(void) 96 | { 97 | int ret; 98 | 99 | // /* using __TIME__ ensure that a new binary will be built on every 100 | // * compile which is convenient when testing firmware upgrade. 101 | // */ 102 | // LOG_INF("build time: " __DATE__ " " __TIME__); 103 | 104 | /////////////////////////////////////////// 105 | LOG_INF("reading BOOT state..."); 106 | 107 | bool retained_ok = retained_validate(); 108 | if( !retained_ok ) 109 | { 110 | LOG_WRN("Retained data is INVALID; initializing"); 111 | } 112 | 113 | /* Increment for this boot attempt and update. */ 114 | retained.boots += 1; 115 | retained_update(); 116 | 117 | LOG_INF("Boot: %u; Uptime: %llus", retained.boots, retained.uptime_sum); 118 | 119 | /////////////////////////////////////////// 120 | LOG_INF("initializing settings storage..."); 121 | start_settings_subsys(); 122 | 123 | /////////////////////////////////////////// 124 | if( retained.boots <= 1) 125 | { 126 | LOG_INF("starting bluetooth services..."); 127 | 128 | start_bluetooth_services(); 129 | LOG_INF("OK bluetooth advertising"); 130 | } 131 | else 132 | { 133 | LOG_INF("will not start bluetooth"); 134 | } 135 | /////////////////////////////////////////// 136 | LOG_INF("starting ANT+ device..."); 137 | 138 | start_ant_device(); 139 | LOG_INF("OK ANT+ device"); 140 | 141 | /////////////////////////////////////////// 142 | LOG_INF("starting GPIO and power management..."); 143 | 144 | if (!gpio_is_ready_dt(&wake_signal)) 145 | { 146 | LOG_ERR("Error: wake signal device %s is not ready\n", 147 | wake_signal.port->name); 148 | return EXIT_FAILURE; 149 | } 150 | 151 | ret = gpio_pin_configure_dt(&wake_signal, GPIO_INPUT); 152 | if (ret != 0) 153 | { 154 | LOG_ERR("Error %d: failed to configure %s pin %d\n", 155 | ret, wake_signal.port->name, wake_signal.pin); 156 | return EXIT_FAILURE; 157 | } 158 | 159 | /////////////////////////////////////////// 160 | LOG_INF("starting SPI sensor interface..."); 161 | spim_init(); 162 | 163 | /////////////////////////////////////////// 164 | LOG_INF("Scheduling application supervision"); 165 | k_work_schedule(&supervision_work, K_MSEC(SUPERVISION_CYCLE_TIME_MS)); 166 | 167 | 168 | return EXIT_SUCCESS; 169 | } 170 | -------------------------------------------------------------------------------- /src/ant_profiles/ant_common/pages/ant_common_page_82.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021, Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | LOG_MODULE_REGISTER(ant_common_page_82, LOG_LEVEL_WRN); 21 | 22 | const static char* ant_common_page_82_battery_status_strings[] = { 23 | "RESERVED_0", 24 | "NEW", 25 | "GOOD", 26 | "OK", 27 | "LOW", 28 | "CRITICAL", 29 | "RESERVED_6", 30 | "INVALID" 31 | }; 32 | const char* get_ant_common_page_82_battery_status_string(const ANT_COMMON_page82_BATTERY_STATE state) 33 | { 34 | return ant_common_page_82_battery_status_strings[(uint8_t)state]; 35 | } 36 | 37 | /**@brief ant+ common page 82 data layout structure. */ 38 | typedef struct 39 | { 40 | uint8_t _reserved[1]; ///< unused, fill by 0xFF 41 | uint8_t battery_identifier; 42 | uint8_t operating_time[3]; 43 | uint8_t fractional_voltage; 44 | uint8_t descriptive_field; 45 | }ant_common_page82_data_layout_t; 46 | 47 | 48 | /**@brief Function for tracing page 82 data. 49 | * 50 | * @param[in] p_page_data Pointer to the page 82 data. 51 | */ 52 | static void page82_data_log(volatile ant_common_page82_data_t const * p_page_data) 53 | { 54 | 55 | if((p_page_data->battery_count & 0xF & p_page_data->battery_id) == 0xF) 56 | { 57 | LOG_INF("Battery count: %s", "unused"); 58 | LOG_INF("Battery ID: %s", "unused"); 59 | } 60 | else 61 | { 62 | LOG_INF("Battery count: %u", p_page_data->battery_count); 63 | LOG_INF("Battery ID: %u", p_page_data->battery_id); 64 | } 65 | 66 | LOG_INF("Operating time: %u", p_page_data->operating_time); 67 | 68 | if(p_page_data->battery_status != ANT_COMMON_page82_BATTERY_STATE_INVALID) 69 | { 70 | LOG_INF("Battery Voltage [mV]: %u", p_page_data->battery_voltage_mv); 71 | } 72 | LOG_INF("Battery Status: %u (%s)", (uint8_t)(p_page_data->battery_status), get_ant_common_page_82_battery_status_string(p_page_data->battery_status)); 73 | } 74 | 75 | // encodes with 2 second time resolution only 76 | void ant_common_page_82_encode(uint8_t * p_page_buffer, 77 | volatile ant_common_page82_data_t const * p_page_data) 78 | { 79 | ant_common_page82_data_layout_t * p_outcoming_data = 80 | (ant_common_page82_data_layout_t *)p_page_buffer; 81 | 82 | memset(p_page_buffer, UINT8_MAX, sizeof (p_outcoming_data->_reserved)); 83 | 84 | p_outcoming_data->battery_identifier = ( 85 | (p_page_data->battery_count & 0xF) | 86 | ((p_page_data->battery_id & 0xF) << 4) 87 | ); 88 | 89 | uint24_encode(p_page_data->operating_time / 2, 90 | p_outcoming_data->operating_time); 91 | 92 | uint8_t fractional_voltage = 0xFF; 93 | uint8_t coarse_voltage = 0xF; 94 | 95 | if(p_page_data->battery_status != ANT_COMMON_page82_BATTERY_STATE_INVALID) 96 | { 97 | fractional_voltage = (uint8_t)((uint32_t)(p_page_data->battery_voltage_mv % 1000) * 256 / 1000); 98 | coarse_voltage = (uint8_t)(p_page_data->battery_voltage_mv / 1000); 99 | } 100 | 101 | p_outcoming_data->fractional_voltage = fractional_voltage; 102 | 103 | p_outcoming_data->descriptive_field = ( 104 | (coarse_voltage & 0xF) | 105 | ((p_page_data->battery_status & 0x7) << 4) | 106 | (0x1 << 7) // 2 second cumulative operating time resolution 107 | ); 108 | 109 | page82_data_log(p_page_data); 110 | } 111 | 112 | 113 | void ant_common_page_82_decode(uint8_t const * p_page_buffer, 114 | volatile ant_common_page82_data_t * p_page_data) 115 | { 116 | ant_common_page82_data_layout_t const * p_incoming_data = 117 | (ant_common_page82_data_layout_t *)p_page_buffer; 118 | 119 | p_page_data->battery_count = (p_incoming_data->battery_identifier & 0xF); 120 | p_page_data->battery_id = ((p_incoming_data->battery_identifier >> 4) & 0xF); 121 | 122 | p_page_data->operating_time = uint24_decode(p_incoming_data->operating_time) * ( ((p_incoming_data->descriptive_field >> 7) & 0x1) == 0x1 ? 2 : 16 ); 123 | 124 | 125 | p_page_data->battery_voltage_mv = (uint16_t)(p_incoming_data->descriptive_field & 0xF) * 1000 + (uint16_t)(((uint32_t)p_incoming_data->fractional_voltage) * 1000 / 256); 126 | 127 | p_page_data->battery_status = (p_incoming_data->descriptive_field >> 4) & 0x7; 128 | 129 | page82_data_log(p_page_data); 130 | } 131 | 132 | -------------------------------------------------------------------------------- /src/ant.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include 7 | 8 | #include "app_version.h" 9 | 10 | #include "ant.h" 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | #include 18 | LOG_MODULE_REGISTER(ant, LOG_LEVEL_INF); 19 | 20 | #include "common.h" 21 | #include "retained.h" 22 | #include "settings.h" 23 | #include "zbus_com.h" 24 | #include "sensor.h" 25 | 26 | // typedef typeof(((struct sensor_readings_t){}).pressure_hpa) pressure_hpa_t; 27 | 28 | static void ant_tpms_evt_handler(ant_tpms_profile_t * p_profile, ant_tpms_evt_t event); 29 | 30 | TPMS_SENS_PROFILE_CONFIG_DEF(tpms, ant_tpms_evt_handler); 31 | ant_channel_config_t tpms_channel_tpms_sens_config; 32 | static ant_tpms_profile_t tpms; 33 | 34 | static void ant_tpms_evt_handler(ant_tpms_profile_t * p_profile, ant_tpms_evt_t event) 35 | { 36 | switch (event) { 37 | case ANT_TPMS_PAGE_1_UPDATED: 38 | break; 39 | case ANT_TPMS_PAGE_80_UPDATED: 40 | break; 41 | case ANT_TPMS_PAGE_81_UPDATED: 42 | break; 43 | case ANT_TPMS_PAGE_82_UPDATED: 44 | break; 45 | default: 46 | break; 47 | } 48 | } 49 | 50 | static void ant_evt_handler(ant_evt_t *p_ant_evt) 51 | { 52 | ant_tpms_sens_evt_handler(p_ant_evt, &tpms); 53 | } 54 | 55 | void ant_sensor_data_handler_cb(const struct zbus_channel *chan) 56 | { 57 | const struct sensor_readings_t *msg = zbus_chan_const_msg(chan); 58 | 59 | LOG_DBG("Updating ANT+ pages with sensor data"); 60 | 61 | // page 1: pressure 62 | tpms.page_1.pressure = msg->pressure_hpa; 63 | 64 | // page 82: battery state and uptime 65 | uint8_t battery_perc = battery_level_percent(msg->voltage_mv); 66 | ANT_COMMON_page82_BATTERY_STATE battery_state = ANT_COMMON_page82_BATTERY_STATE_INVALID; 67 | if(battery_perc >= 100) 68 | { 69 | battery_state = ANT_COMMON_page82_BATTERY_STATE_NEW; 70 | } 71 | else if(battery_perc >= 50) // < 100 72 | { 73 | battery_state = ANT_COMMON_page82_BATTERY_STATE_GOOD; 74 | } 75 | else if(battery_perc >= 15) // < 50 76 | { 77 | battery_state = ANT_COMMON_page82_BATTERY_STATE_OK; 78 | } 79 | else if(battery_perc >= 5) // < 15 80 | { 81 | battery_state = ANT_COMMON_page82_BATTERY_STATE_LOW; 82 | } 83 | else // < 5 84 | { 85 | battery_state = ANT_COMMON_page82_BATTERY_STATE_CRITICAL; 86 | } 87 | 88 | tpms.page_82.operating_time = retained.uptime_sum + (k_uptime_seconds() - retained.uptime_latest); 89 | tpms.page_82.battery_voltage_mv = msg->voltage_mv; 90 | tpms.page_82.battery_status = battery_state; 91 | } 92 | 93 | static int profile_setup(void) 94 | { 95 | int rc; 96 | 97 | uint16_t device_id; 98 | rc = load_immediate_value(DEVICE_ID_SETTINGS_KEY, &device_id, sizeof(device_id)); 99 | if(rc) 100 | { 101 | LOG_ERR("failed reading %s to set BT name", DEVICE_ID_SETTINGS_KEY); 102 | return rc; 103 | } 104 | 105 | tpms_channel_tpms_sens_config = (ant_channel_config_t) { 106 | .channel_number = 0, // hardware ANT channel 0 107 | .channel_type = TPMS_SENS_CHANNEL_TYPE, 108 | .ext_assign = TPMS_EXT_ASSIGN, 109 | .rf_freq = TPMS_ANTPLUS_RF_FREQ, 110 | .transmission_type = 5, // transmission type: use common pages 111 | .device_type = TPMS_DEVICE_TYPE, 112 | .device_number = device_id, // device number id 113 | .channel_period = TPMS_MSG_PERIOD, 114 | .network_number = 0, // network number 115 | }; 116 | 117 | int err = ant_tpms_sens_init(&tpms, 118 | TPMS_SENS_CHANNEL_CONFIG(tpms), 119 | TPMS_SENS_PROFILE_CONFIG(tpms)); 120 | if (err) { 121 | LOG_ERR("ant_tpms_sens_init failed: %d", err); 122 | return err; 123 | } 124 | 125 | tpms.page_81.sw_revision_minor = APP_VERSION_MINOR; 126 | tpms.page_81.sw_revision_major = APP_VERSION_MAJOR; 127 | tpms.page_81.serial_number = get_hwid_16bit(); 128 | 129 | // tpms.page_82.battery_count = 1; 130 | // tpms.page_82.battery_id = 0; 131 | 132 | err = ant_tpms_sens_open(&tpms); 133 | if (err) { 134 | LOG_ERR("ant_tpms_sens_open failed: %d", err); 135 | return err; 136 | } 137 | 138 | LOG_DBG("OK ant_tpms_sens_open"); 139 | return err; 140 | } 141 | 142 | static int ant_stack_setup(void) 143 | { 144 | int err = ant_init(); 145 | if (err) { 146 | LOG_ERR("ant_init failed: %d", err); 147 | return err; 148 | } 149 | LOG_DBG("OK ant_init"); 150 | LOG_INF("ANT Version %s", ANT_VERSION_STRING); 151 | 152 | err = ant_cb_register(&ant_evt_handler); 153 | if (err) { 154 | LOG_ERR("ant_cb_register failed: %d", err); 155 | return err; 156 | } 157 | 158 | err = ant_plus_key_set(0); // default network number 159 | if (err) { 160 | LOG_ERR("ant_plus_key_set failed: %d", err); 161 | } 162 | return err; 163 | } 164 | 165 | int start_ant_device(void) 166 | { 167 | LOG_INF("ANT+ TPMS device starting..."); 168 | 169 | int err = ant_stack_setup(); 170 | if (err) { 171 | LOG_ERR("ANT stack setup failed (rc %d)", err); 172 | return err; 173 | } 174 | 175 | err = profile_setup(); 176 | if (err) { 177 | LOG_ERR("ANT+ profile setup failed (rc %d)", err); 178 | return err; 179 | } 180 | 181 | return 0; 182 | } 183 | -------------------------------------------------------------------------------- /doc/INSTALL.md: -------------------------------------------------------------------------------- 1 | # Firmware Installation 2 | - [Firmware Installation](#firmware-installation) 3 | - [SWD programmer recommendation](#swd-programmer-recommendation) 4 | - [Flashing example using GDB](#flashing-example-using-gdb) 5 | - [Reverting to stock firmware](#reverting-to-stock-firmware) 6 | 7 | ## SWD programmer recommendation 8 | If you do not already own a compatible SWD programmer, a personal recommendation is to head over to [Black Magic](https://black-magic.org/hardware.html) and get hold of some compatible hardware, build the corresponding firmware with nRF5 series support from their [GitHub Repo](https://github.com/blackmagic-debug/blackmagic/tree/main) and install according to their instructions. The easiest and likely cheapest variant is to get hold of one or two ST-Link programmers; firmwares are provided in [`./doc/stlink_bmdb_firmware`](/doc/stlink_bmdb_firmware/), installation per [documentation](https://github.com/blackmagic-debug/blackmagic/tree/main/src/platforms/stlink#upload-bmp-firmware). 9 | 10 | 11 | ## Flashing example using GDB 12 | When using anything providing a GDB server ([OpenOCD](https://openocd.org/pages/getting-openocd.html), [pyOCD](https://pyocd.io/), [Black Magic probe](https://black-magic.org/), etc.), get a copy of GDB for AArch32 bare-metal targets (`arm-none-eabi-gdb`) for your plattform. You can obtain it from [arm here](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads). 13 | 14 | The example uses a Black Magic probe providing a GDB server we connect to on COM1. If using some OCD variant, GDB should pick up a local server in default configuration automatically. `(gdb)` is your prompt. 15 | 16 | 17 | 1. Start GDB 18 | ```console 19 | ## OCD flavor default config 20 | arm-none-eabi-gdb 21 | 22 | ## bmdb COM1 23 | arm-none-eabi-gdb -iex "target extended-remote \\.\COM1" 24 | 25 | 26 | #### expected GDB response (similar) and prompt 27 | GNU gdb (Arm GNU Toolchain 13.3.Rel1 (Build arm-13.24)) 14.2.90.20240526-git 28 | Copyright (C) 2023 Free Software Foundation, Inc. 29 | License GPLv3+: GNU GPL version 3 or later 30 | This is free software: you are free to change and redistribute it. 31 | There is NO WARRANTY, to the extent permitted by law. 32 | Type "show copying" and "show warranty" for details. 33 | This GDB was configured as "--host=i686-w64-mingw32 --target=arm-none-eabi". 34 | Type "show configuration" for configuration details. 35 | For bug reporting instructions, please see: 36 | . 37 | Find the GDB manual and other documentation resources online at: 38 | . 39 | 40 | For help, type "help". 41 | Type "apropos word" to search for commands related to "word". 42 | (gdb) 43 | ``` 44 | 45 | 2. Scan for Devices, attach to the locked access port, and unlock and erase chip 46 | ```console 47 | ## ... 48 | ## scan devices 49 | (gdb) monitor swd_scan 50 | Target voltage: 2.99V 51 | Available Targets: 52 | No. Att Driver 53 | 1 Nordic nRF52 Access Port (protected) 54 | 55 | ## attach 56 | (gdb) attach 1 57 | Attaching to Remote target 58 | 59 | ## unlock and erase 60 | (gdb) monitor erase_mass 61 | Erasing device Flash: done 62 | ``` 63 | 64 | 3. **Power cycle the device!** It may work without for you, but it proved to be the most failsafe procedure. 65 | 66 | 4. Scan for Devices, attach to the unlocked device and flash new firmware. 67 | ```console 68 | ## ... 69 | ## scan devices 70 | (gdb) monitor swd_scan 71 | Target voltage: 2.99V 72 | Available Targets: 73 | No. Att Driver 74 | 1 Nordic nRF52 M4 75 | 2 Nordic nRF52 Access Port 76 | 77 | ## attach 78 | (gdb) attach 1 79 | Attaching to Remote target 80 | warning: No executable has been specified and target does not support 81 | determining executable automatically. Try using the "file" command. 82 | 83 | 84 | ## flash 85 | (gdb) load ./firmware_file_name.hex 86 | Loading section .sec1, size 0x5ea8 lma 0x0 87 | Loading section .sec2, size 0xa000 lma 0x6000 88 | Loading section .sec3, size 0x10000 lma 0x10000 89 | Loading section .sec4, size 0x10000 lma 0x20000 90 | Loading section .sec5, size 0xa60d lma 0x30000 91 | Start address 0x00000000, load size 238773 92 | Transfer rate: 42 KB/sec, 966 bytes/write. 93 | ``` 94 | 95 | ## Reverting to stock firmware 96 | > ⚠ To revert to stock firmware, you have to have access to a dump of the original firmware. 97 | 98 | SKS embeds firmware update packages in their mobile apps. From these you can obtain the application firmware, and the information about the used Softdevice. To date, for all tested update packages, the Softdevice is S332, but none does include a bootloader. Thus, no assembly of a full firmware is possible from public sources. Dumping the firmware is disabled by access port protection on the nRF52832. 99 | 100 | On a side note, while talking about APPROTECT: Parts of the nRF52 series are vulnerable to a glitching attack to circumvent this protection. As [demonstrated by limitedresults](https://limitedresults.com/2020/06/nrf52-debug-resurrection-approtect-bypass/), and [implemented by atc1441](https://github.com/atc1441/ESP32_nRF52_SWD) on an ESP32. Apart from the suggestions in both links, DEC4 with a delay of 29000 - 31000 µs is supposed to be a rewarding target as well. 101 | 102 | To convert a binary dump to a loadable hex file use: 103 | ```bash 104 | arm-none-eabi-objcopy -I binary -O elf32-littlearm --change-section-address=.data=0x0 -B arm -S flash_0x0.bin flash_0x0.elf 105 | arm-none-eabi-objcopy -I binary -O elf32-littlearm --change-section-address=.data=0x10001000 -B arm -S uicr_0x10001000.bin uicr_0x10001000.elf 106 | ``` 107 | 108 | Load using GDB: 109 | ```bash 110 | # gdb 111 | load flash_0x0.elf 0x0 112 | load uicr_0x10001000.elf 0x10001000 113 | ``` 114 | -------------------------------------------------------------------------------- /src/retained.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2021 Nordic Semiconductor ASA 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | 8 | #include "retained.h" 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | /* nRF52 RAM (really, RAM AHB slaves) are partitioned as: 21 | * * Up to 8 blocks of two 4 KiBy byte "small" sections 22 | * * A 9th block of with 32 KiBy "large" sections 23 | * 24 | * At time of writing the maximum number of large sections is 6, all 25 | * within the first large block. Theoretically there could be more 26 | * sections in the 9th block, and possibly more blocks. 27 | */ 28 | 29 | /* Inclusive address of RAM start */ 30 | #define SRAM_BEGIN (uintptr_t)DT_REG_ADDR(DT_NODELABEL(sram0)) 31 | 32 | /* Exclusive address of RAM end */ 33 | #define SRAM_END (SRAM_BEGIN + (uintptr_t)DT_REG_SIZE(DT_NODELABEL(sram0))) 34 | 35 | /* Size of a controllable RAM section in the small blocks */ 36 | #define SMALL_SECTION_SIZE 4096 37 | 38 | /* Number of controllable RAM sections in each of the lower blocks */ 39 | #define SMALL_SECTIONS_PER_BLOCK 2 40 | 41 | /* Span of a small block */ 42 | #define SMALL_BLOCK_SIZE (SMALL_SECTIONS_PER_BLOCK * SMALL_SECTION_SIZE) 43 | 44 | /* Number of small blocks */ 45 | #define SMALL_BLOCK_COUNT 8 46 | 47 | /* Span of the SRAM area covered by small sections */ 48 | #define SMALL_SECTION_SPAN (SMALL_BLOCK_COUNT * SMALL_BLOCK_SIZE) 49 | 50 | /* Inclusive address of the RAM range covered by large sections */ 51 | #define LARGE_SECTION_BEGIN (SRAM_BEGIN + SMALL_SECTION_SPAN) 52 | 53 | /* Size of a controllable RAM section in large blocks */ 54 | #define LARGE_SECTION_SIZE 32768 55 | 56 | /* Set or clear RAM retention in SYSTEM_OFF for the provided object. 57 | * 58 | * @note This only works for nRF52 with the POWER module. The other 59 | * Nordic chips use a different low-level API, which is not currently 60 | * used by this function. 61 | * 62 | * @param ptr pointer to the start of the retainable object 63 | * 64 | * @param len length of the retainable object 65 | * 66 | * @param enable true to enable retention, false to clear retention 67 | */ 68 | static int ram_range_retain(const void *ptr, 69 | size_t len, 70 | bool enable) 71 | { 72 | uintptr_t addr = (uintptr_t)ptr; 73 | uintptr_t addr_end = addr + len; 74 | 75 | /* Error if the provided range is empty or doesn't lie 76 | * entirely within the SRAM address space. 77 | */ 78 | if ((len == 0U) 79 | || (addr < SRAM_BEGIN) 80 | || (addr > (SRAM_END - len))) { 81 | return -EINVAL; 82 | } 83 | 84 | /* Iterate over each section covered by the range, setting the 85 | * corresponding RAM OFF retention bit in the parent block. 86 | */ 87 | do { 88 | uintptr_t block_base = SRAM_BEGIN; 89 | uint32_t section_size = SMALL_SECTION_SIZE; 90 | uint32_t sections_per_block = SMALL_SECTIONS_PER_BLOCK; 91 | bool is_large = (addr >= LARGE_SECTION_BEGIN); 92 | uint8_t block = 0; 93 | 94 | if (is_large) { 95 | block = 8; 96 | block_base = LARGE_SECTION_BEGIN; 97 | section_size = LARGE_SECTION_SIZE; 98 | 99 | /* RAM[x] supports only 16 sections, each its own bit 100 | * for POWER (0..15) and RETENTION (16..31). We don't 101 | * know directly how many sections are present, so 102 | * assume they all are; the true limit will be 103 | * determined by the SRAM size. 104 | */ 105 | sections_per_block = 16; 106 | } 107 | 108 | uint32_t section = (addr - block_base) / section_size; 109 | 110 | if (section >= sections_per_block) { 111 | block += section / sections_per_block; 112 | section %= sections_per_block; 113 | } 114 | 115 | uint32_t section_mask = 116 | (POWER_RAM_POWERSET_S0RETENTION_On 117 | << (section + POWER_RAM_POWERSET_S0RETENTION_Pos)); 118 | 119 | if (enable) { 120 | nrf_power_rampower_mask_on(NRF_POWER, block, section_mask); 121 | } else { 122 | nrf_power_rampower_mask_off(NRF_POWER, block, section_mask); 123 | } 124 | 125 | /* Move to the first address in the next section. */ 126 | addr += section_size - (addr % section_size); 127 | } while (addr < addr_end); 128 | 129 | return 0; 130 | } 131 | 132 | /* Retained data must be defined in a no-init section to prevent the C 133 | * runtime initialization from zeroing it before anybody can see it. 134 | */ 135 | __noinit struct retained_data retained; 136 | 137 | #define RETAINED_CRC_OFFSET offsetof(struct retained_data, crc) 138 | #define RETAINED_CHECKED_SIZE (RETAINED_CRC_OFFSET + sizeof(retained.crc)) 139 | 140 | bool retained_validate(void) 141 | { 142 | /* The residue of a CRC is what you get from the CRC over the 143 | * message catenated with its CRC. This is the post-final-xor 144 | * residue for CRC-32 (CRC-32/ISO-HDLC) which Zephyr calls 145 | * crc32_ieee. 146 | */ 147 | const uint32_t residue = 0x2144df1c; 148 | uint32_t crc = crc32_ieee((const uint8_t *)&retained, 149 | RETAINED_CHECKED_SIZE); 150 | bool valid = (crc == residue); 151 | 152 | /* If the CRC isn't valid, reset the retained data. */ 153 | if (!valid) { 154 | memset(&retained, 0, sizeof(retained)); 155 | } 156 | 157 | /* Reset to accrue runtime from this session. */ 158 | retained.uptime_latest = 0; 159 | 160 | /* Reconfigure to retain the state during system off, regardless of 161 | * whether validation succeeded. Although these values can sometimes 162 | * be observed to be preserved across System OFF, the product 163 | * specification states they are not retained in that situation, and 164 | * that can also be observed. 165 | */ 166 | (void)ram_range_retain(&retained, RETAINED_CHECKED_SIZE, true); 167 | 168 | return valid; 169 | } 170 | 171 | void retained_update(void) 172 | { 173 | uint64_t now = k_uptime_seconds(); 174 | 175 | retained.uptime_sum += (now - retained.uptime_latest); 176 | retained.uptime_latest = now; 177 | 178 | uint32_t crc = crc32_ieee((const uint8_t *)&retained, 179 | RETAINED_CRC_OFFSET); 180 | 181 | retained.crc = sys_cpu_to_le32(crc); 182 | } 183 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Builder 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request_target: 10 | # prevent secret leaks by enabling: "Require approval for all external contributors" in repository settings "Actions">"General" 11 | types: 12 | - opened 13 | - synchronize 14 | branches: 15 | - master 16 | workflow_dispatch: 17 | 18 | env: 19 | NRF_TOOLCHAIN_VERSION: v2.7.0 20 | NRF_SDK_VERSION: v2.7.0 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-24.04 25 | steps: 26 | - name: Install Dependencies + West + NRF Util 27 | run: | 28 | sudo apt update 29 | sudo apt install --no-install-recommends \ 30 | curl \ 31 | git \ 32 | python3 python3-pip \ 33 | 34 | pip3 install --break-system-packages west 35 | 36 | curl https://files.nordicsemi.com/artifactory/swtools/external/nrfutil/executables/x86_64-unknown-linux-gnu/nrfutil -o /usr/local/bin/nrfutil 37 | chmod +x /usr/local/bin/nrfutil 38 | nrfutil install toolchain-manager 39 | 40 | - name: Prepare Restore NCS 41 | run: | 42 | mkdir -p /home/runner/ncs/ 43 | 44 | - name: Restore NCS 45 | id: cache-ncs-restore 46 | uses: actions/cache/restore@v4 47 | with: 48 | path: /home/runner/ncs/ 49 | key: ncs-${{ runner.os }}-${{ env.NRF_TOOLCHAIN_VERSION }}-${{ env.NRF_SDK_VERSION }} 50 | 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | 54 | - name: Fetch Toolchain + NRF & ANT SDK 55 | if: steps.cache-ncs-restore.outputs.cache-hit != 'true' 56 | run: | 57 | # install toolchain 58 | nrfutil toolchain-manager install --ncs-version ${NRF_TOOLCHAIN_VERSION} 59 | 60 | # install nrf sdk 61 | mkdir -p /home/runner/ncs/sdks/ 62 | cd /home/runner/ncs/sdks/ 63 | west init -m https://github.com/nrfconnect/sdk-nrf --mr ${NRF_SDK_VERSION} ${NRF_SDK_VERSION} 64 | cd /home/runner/ncs/sdks/${NRF_SDK_VERSION}/ 65 | west update 66 | 67 | ## install ant SDK 68 | # allow access 69 | git config --global credential.helper cache 70 | git ls-remote https://SKS_AIRSPY_ANT_TOKEN:${{ secrets.SKS_AIRSPY_ANT_TOKEN }}@github.com/ant-nrfconnect/sdk-ant > /dev/null 71 | # fetch ant SDK 72 | west config manifest.group-filter +ant 73 | west update 74 | pushd /home/runner/ncs/sdks/${NRF_SDK_VERSION}/ant && git apply --whitespace=fix ${{ github.workspace }}/ant_sdk_nrf52832.patch && popd 75 | west list ant 76 | 77 | # finalize sdk install 78 | nrfutil toolchain-manager launch -- west zephyr-export 79 | 80 | - name: Save NCS 81 | if: steps.cache-ncs-restore.outputs.cache-hit != 'true' 82 | id: cache-ncs-save 83 | uses: actions/cache/save@v4 84 | with: 85 | path: /home/runner/ncs/ 86 | key: ncs-${{ runner.os }}-${{ env.NRF_TOOLCHAIN_VERSION }}-${{ env.NRF_SDK_VERSION }} 87 | 88 | - name: Build Application 89 | id: app-build 90 | working-directory: /home/runner/ncs/sdks/${{ env.NRF_SDK_VERSION }}/ 91 | run: | 92 | echo "${{ secrets.MCUBOOT_SIGNING_KEY }}" > ${{ github.workspace }}/mcuboot.pem 93 | nrfutil toolchain-manager launch -- west build --board sks_airspy --build-dir ${{ github.workspace }}/build/ --no-sysbuild ${{ github.workspace }} 94 | 95 | - name: Save Artifacts 96 | uses: actions/upload-artifact@v4 97 | with: 98 | name: sks-airspy-community-firmware 99 | path: | 100 | ${{ github.workspace }}/build/zephyr/merged.hex 101 | ${{ github.workspace }}/build/zephyr/dfu_application.zip 102 | 103 | release: 104 | runs-on: ubuntu-24.04 105 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 106 | env: 107 | VERSION_TAG_NAME: ${{ github.ref_name }} 108 | FILENAME_PREFIX: sascf 109 | needs: build 110 | steps: 111 | - uses: actions/checkout@v4 112 | 113 | # TODO: do not rely on cache restoration, but use outputs for ANT SDK version 114 | - name: Restore NCS 115 | id: cache-ncs-restore 116 | uses: actions/cache/restore@v4 117 | with: 118 | path: /home/runner/ncs/ 119 | key: ncs-${{ runner.os }}-${{ env.NRF_TOOLCHAIN_VERSION }}-${{ env.NRF_SDK_VERSION }} 120 | 121 | - uses: actions/download-artifact@v4 122 | with: 123 | name: sks-airspy-community-firmware 124 | path: release_artifacts 125 | 126 | - name: Prepare Release Notes 127 | run: | 128 | touch release_artifacts/NOTES 129 | 130 | echo "|Release | ${VERSION_TAG_NAME}|" >> release_artifacts/NOTES 131 | echo "|---|---|" >> release_artifacts/NOTES 132 | 133 | # read and write VERSION file version info 134 | set -o allexport 135 | source <(cat VERSION | tr -d ' ') 136 | set +o allexport 137 | echo "| APP | ${VERSION_MAJOR}.${VERSION_MINOR}.${PATCHLEVEL} |" >> release_artifacts/NOTES 138 | 139 | # get ANT SDK version 140 | ANT_SDK_VERSION=$(sed -n -E 's/.*ANT_VERSION_STRING\s+\"([[:digit:]\.]*)\".*$/\1/p;' /home/runner/ncs/sdks/${NRF_SDK_VERSION}/ant/include/ant_parameters.h) 141 | 142 | # write SDKs and toolchain versions 143 | echo "| nRF SDK | ${NRF_SDK_VERSION} |" >> release_artifacts/NOTES 144 | echo "| ANT SDK | ${ANT_SDK_VERSION} |" >> release_artifacts/NOTES 145 | echo "| Toolchain | ${NRF_TOOLCHAIN_VERSION} |" >> release_artifacts/NOTES 146 | 147 | - name: Prepare Release Files 148 | run: | 149 | mv release_artifacts/merged.hex release_artifacts/${FILENAME_PREFIX}-${VERSION_TAG_NAME}-$(git describe --abbrev=7 --always).hex 150 | mv release_artifacts/dfu_application.zip release_artifacts/${FILENAME_PREFIX}_ota_dfu-${VERSION_TAG_NAME}-$(git describe --abbrev=7 --always).zip 151 | 152 | - name: Release 153 | env: 154 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 155 | run: | 156 | gh release create \ 157 | ${VERSION_TAG_NAME} \ 158 | release_artifacts/${FILENAME_PREFIX}*.hex \ 159 | release_artifacts/${FILENAME_PREFIX}_ota_dfu*.zip \ 160 | --title ${VERSION_TAG_NAME} \ 161 | --notes-file release_artifacts/NOTES \ 162 | --verify-tag \ 163 | --latest \ 164 | --target ${{ github.sha }} \ 165 | --repo ${{ github.repository }} 166 | 167 | -------------------------------------------------------------------------------- /src/bluetooth.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include "bluetooth.h" 15 | #include "settings.h" 16 | 17 | #include 18 | 19 | 20 | #include 21 | LOG_MODULE_REGISTER(bt, LOG_LEVEL_INF); 22 | 23 | #define BT_DISABLE_DELAY 30000 24 | #define BT_DISABLE_RESCHEDULE 500 25 | 26 | static int shutdown_bluetooth(void); 27 | 28 | static void advertise(struct k_work *work); 29 | K_WORK_DEFINE(advertise_work, advertise); 30 | 31 | static void disable_bt(struct k_work *work); 32 | K_WORK_DELAYABLE_DEFINE(disable_bt_work, disable_bt); 33 | 34 | static void connected(struct bt_conn *conn, uint8_t err); 35 | static void disconnected(struct bt_conn *conn, uint8_t reason); 36 | BT_CONN_CB_DEFINE(conn_callbacks) = { 37 | .connected = connected, 38 | .disconnected = disconnected, 39 | }; 40 | 41 | static const struct bt_data advertising_data_smp[] = { 42 | /* Appearance */ 43 | BT_DATA_BYTES(BT_DATA_GAP_APPEARANCE, 44 | (CONFIG_BT_DEVICE_APPEARANCE >> 0) & 0xff, 45 | (CONFIG_BT_DEVICE_APPEARANCE >> 8) & 0xff), 46 | 47 | /* Flags */ 48 | BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), 49 | 50 | /* SVC */ 51 | BT_DATA_BYTES(BT_DATA_UUID128_ALL, 52 | 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 53 | 0xd3, 0x4c, 0xb7, 0x1d, 0x1d, 0xdc, 0x53, 0x8d), 54 | }; 55 | 56 | #if CONFIG_LOG_BACKEND_BLE 57 | static const struct bt_data advertising_data_nus[] = { 58 | /* Appearance */ 59 | BT_DATA_BYTES(BT_DATA_GAP_APPEARANCE, 60 | (CONFIG_BT_DEVICE_APPEARANCE >> 0) & 0xff, 61 | (CONFIG_BT_DEVICE_APPEARANCE >> 8) & 0xff), 62 | 63 | /* Flags */ 64 | BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), 65 | 66 | /* SVC */ 67 | BT_DATA_BYTES(BT_DATA_UUID128_ALL, LOGGER_BACKEND_BLE_ADV_UUID_DATA), 68 | }; 69 | #endif 70 | 71 | // BEGIN config service 72 | 73 | static void reboot_work_wrapper(struct k_work *work) 74 | { 75 | sys_reboot(SYS_REBOOT_COLD); 76 | } 77 | 78 | #define REBOOT_DELAY_MS 250 79 | K_WORK_DELAYABLE_DEFINE(reboot_work, reboot_work_wrapper); 80 | 81 | 82 | #define DEVICE_ID_SIZE 2 83 | 84 | static ssize_t cfg_srv_devid_chrx_on_write_cb(struct bt_conn *conn, 85 | const struct bt_gatt_attr *attr, 86 | const void *buf, 87 | uint16_t len, 88 | uint16_t offset, 89 | uint8_t flags) 90 | { 91 | LOG_DBG("Received BT data, handle %d, conn %p", attr->handle, (void *)conn); 92 | 93 | if ( len && len <= DEVICE_ID_SIZE) 94 | { 95 | uint16_t device_id = 0x0000; 96 | memcpy(&device_id, buf, len); 97 | 98 | int rc; 99 | rc = settings_save_one(DEVICE_ID_SETTINGS_KEY, (const void *)&device_id, sizeof(device_id)); 100 | if (rc) 101 | { 102 | LOG_WRN("failed writing setting for %s; (rc %d)", DEVICE_ID_SETTINGS_KEY, rc); 103 | } 104 | else 105 | { 106 | LOG_INF("set %s: %d", DEVICE_ID_SETTINGS_KEY, device_id); 107 | LOG_INF("scheduling reboot"); 108 | 109 | // schedule reboot 110 | k_work_schedule(&reboot_work, K_MSEC(REBOOT_DELAY_MS)); 111 | } 112 | 113 | } 114 | 115 | // we processed the whole message; signal to stack 116 | return len; 117 | } 118 | 119 | static const struct bt_data advertising_data_cfg[] = { 120 | /* Appearance */ 121 | BT_DATA_BYTES(BT_DATA_GAP_APPEARANCE, 122 | (CONFIG_BT_DEVICE_APPEARANCE >> 0) & 0xff, 123 | (CONFIG_BT_DEVICE_APPEARANCE >> 8) & 0xff), 124 | 125 | /* Flags */ 126 | BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), 127 | 128 | /* SVC */ 129 | BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_CFG_SRV_UUID_ENC), 130 | }; 131 | 132 | BT_GATT_SERVICE_DEFINE( 133 | cfg_srv, 134 | BT_GATT_PRIMARY_SERVICE(BT_CFG_SRV_UUID), 135 | BT_GATT_CHARACTERISTIC(BT_CFG_SRV_DEVID_CHRX_UUID, 136 | BT_GATT_CHRC_WRITE_WITHOUT_RESP, 137 | BT_GATT_PERM_WRITE, 138 | NULL, cfg_srv_devid_chrx_on_write_cb, NULL), 139 | ); 140 | 141 | // END config service 142 | 143 | 144 | struct bt_data scan_response_data[1]; 145 | char bt_name[CONFIG_BT_DEVICE_NAME_MAX + 1]; 146 | 147 | static void auth_cancel(struct bt_conn *conn) 148 | { 149 | char addr[BT_ADDR_LE_STR_LEN]; 150 | 151 | bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); 152 | 153 | LOG_DBG("Pairing cancelled: %s", addr); 154 | } 155 | 156 | static struct bt_conn_auth_cb auth_cb_display = { 157 | .cancel = auth_cancel, 158 | }; 159 | 160 | static void advertise(struct k_work *work) 161 | { 162 | int rc; 163 | 164 | bt_le_adv_stop(); 165 | 166 | bt_conn_auth_cb_register(&auth_cb_display); 167 | 168 | // construct dynamic device name 169 | uint16_t device_id; 170 | rc = load_immediate_value(DEVICE_ID_SETTINGS_KEY, &device_id, sizeof(device_id)); 171 | if(rc) 172 | { 173 | LOG_ERR("failed reading %s to set BT name", DEVICE_ID_SETTINGS_KEY); 174 | return; 175 | } 176 | 177 | snprintf(bt_name, CONFIG_BT_DEVICE_NAME_MAX, "%s %05d", CONFIG_BT_DEVICE_NAME, device_id); 178 | LOG_INF("BT name: %s", bt_name); 179 | bt_set_name(bt_name); 180 | 181 | 182 | scan_response_data[0] = (struct bt_data) BT_DATA(BT_DATA_NAME_COMPLETE, bt_name, strlen(bt_name)); 183 | 184 | rc = bt_le_adv_start(BT_LE_ADV_CONN, advertising_data_smp, ARRAY_SIZE(advertising_data_smp), scan_response_data, ARRAY_SIZE(scan_response_data)); 185 | if (rc) { 186 | LOG_ERR("Advertising %s failed to start (rc %d)", "advertising_data_smp", rc); 187 | return; 188 | } 189 | else 190 | { 191 | LOG_INF("Advertising %s successfully started", "advertising_data_smp"); 192 | } 193 | 194 | #if CONFIG_LOG_BACKEND_BLE 195 | rc = bt_le_adv_start(BT_LE_ADV_CONN, advertising_data_nus, ARRAY_SIZE(advertising_data_nus), scan_response_data, ARRAY_SIZE(scan_response_data)); 196 | if (rc) { 197 | LOG_ERR("Advertising %s failed to start (rc %d)", "advertising_data_nus", rc); 198 | return; 199 | } 200 | else 201 | { 202 | LOG_INF("Advertising %s successfully started", "advertising_data_nus"); 203 | } 204 | #endif 205 | 206 | rc = bt_le_adv_start(BT_LE_ADV_CONN, advertising_data_cfg, ARRAY_SIZE(advertising_data_cfg), scan_response_data, ARRAY_SIZE(scan_response_data)); 207 | if (rc) { 208 | LOG_ERR("Advertising %s failed to start (rc %d)", "advertising_data_cfg", rc); 209 | return; 210 | } 211 | else 212 | { 213 | LOG_INF("Advertising %s successfully started", "advertising_data_cfg"); 214 | } 215 | } 216 | 217 | static void connected(struct bt_conn *conn, uint8_t err) 218 | { 219 | if (err) { 220 | LOG_ERR("Bluetooth Connection failed (err 0x%02x)", err); 221 | } else { 222 | LOG_INF("Bluetooth Connected"); 223 | LOG_INF("Canceling Bluetooth disable task; staying alive"); 224 | k_work_cancel_delayable(&disable_bt_work); 225 | } 226 | } 227 | 228 | static void disconnected(struct bt_conn *conn, uint8_t reason) 229 | { 230 | LOG_INF("Bluetooth Disconnected (reason 0x%02x)", reason); 231 | k_work_submit(&advertise_work); 232 | 233 | LOG_INF("Scheduling Bluetooth shutdown in %dms", BT_DISABLE_DELAY); 234 | k_work_schedule(&disable_bt_work, K_MSEC(BT_DISABLE_DELAY)); 235 | } 236 | 237 | static void disable_bt(struct k_work *work) 238 | { 239 | int rc; 240 | rc = shutdown_bluetooth(); 241 | if(rc != 0) 242 | { 243 | LOG_ERR("Failed disabling Bluetooth after %dms. Rescheduling for %dms", BT_DISABLE_DELAY, BT_DISABLE_RESCHEDULE); 244 | k_work_schedule(&disable_bt_work, K_MSEC(BT_DISABLE_RESCHEDULE)); 245 | } 246 | else 247 | { 248 | LOG_INF("Bluetooth shutdown OK"); 249 | } 250 | } 251 | 252 | static void bt_ready(int err) 253 | { 254 | if (err != 0) { 255 | LOG_ERR("Bluetooth failed to initialise: %d", err); 256 | } else { 257 | k_work_submit(&advertise_work); 258 | } 259 | 260 | LOG_INF("Bluetooth enabled"); 261 | LOG_INF("Scheduling Bluetooth shutdown in %dms", BT_DISABLE_DELAY); 262 | k_work_schedule(&disable_bt_work, K_MSEC(BT_DISABLE_DELAY)); 263 | } 264 | 265 | #if CONFIG_LOG_BACKEND_BLE 266 | void logging_backend_ble_hook(bool status, void *ctx) 267 | { 268 | ARG_UNUSED(ctx); 269 | 270 | if (status) { 271 | LOG_DBG("BLE Logger Backend enabled."); 272 | } else { 273 | LOG_DBG("BLE Logger Backend disabled."); 274 | } 275 | } 276 | #endif 277 | 278 | void start_bluetooth_services(void) 279 | { 280 | int rc; 281 | 282 | #if CONFIG_LOG_BACKEND_BLE 283 | logger_backend_ble_set_hook(logging_backend_ble_hook, NULL); 284 | #endif 285 | 286 | rc = bt_enable(bt_ready); 287 | 288 | if (rc != 0) { 289 | LOG_ERR("Bluetooth enable failed: %d", rc); 290 | } 291 | } 292 | 293 | static int shutdown_bluetooth(void) 294 | { 295 | // TODO: possibly check for updater connection? 296 | int rc; 297 | 298 | rc = bt_le_adv_stop(); 299 | if (rc != 0) { 300 | LOG_ERR("Failed to stop bluetooth advertising: %d", rc); 301 | return rc; 302 | } 303 | 304 | rc = smp_bt_unregister(); 305 | if (rc != 0) { 306 | LOG_ERR("Failed to unregister McuMgr SMP service: %d", rc); 307 | return rc; 308 | } 309 | 310 | rc = bt_disable(); 311 | if (rc != 0) { 312 | LOG_ERR("Failed to disable Bluetooth: %d", rc); 313 | return rc; 314 | } 315 | 316 | return rc; 317 | } 318 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SKS*[1](#disclaimer)* AIRSPY*[1](#disclaimer)* ANT+*[2](#disclaimer)* Community Firmware 2 | An alternative firmware for the SKS AIRSPY tire pressure sensor. Implementing a device profile compatible with the - not yet officially released - ANT+ TPMS (Tyre/Tire Pressure Monitoring System) device profile, as implemented by major cycling computer manufacturers. 3 | 4 | ![banner](./doc/resources/banner_alpha.png) 5 | 6 | 7 | ## What is it and why do I want it? 8 | Stock SKS AIRSPY sensors only talk to a proprietary companion app on Android, iOS or Garmin devices. They do not implement any industry standard to provide the tire pressure information. Let's change this! With this firmware, your AIRSPY sensors seamlessly integrate with a range of cycling computers supporting the ANT+ TPMS profile, providing real-time tire pressure monitoring without need for a proprietary app. 9 | 10 | You may want this, if you like some more gadgets on you bike, own a bike computer, like some tinkering, and because these can be had second hand for as low as 30€ a pair. As the sensor uses a nRF52832 for communication, implementing and flashing new firmware is pretty straightforward, as development tools are openly accessible. 11 | 12 | 13 | ### Features 14 | * ANT+ TPMS compatible device profile (non certified) 15 | * Battery state and cummulated operating time (ANT common page 82) 16 | * Software Version (ANT common page 81) 17 | * Manufacturer Info (default values) (ANT common page 80) 18 | * OTA DFU: firmware updates over BLE; using DFU in [nRF Connect](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-mobile) mobile app 19 | * Debug Logging over BLE; using Nordic UART Service in [nRF Toolbox](https://www.nordicsemi.com/Products/Development-tools/nRF-Toolbox) mobile app 20 | * Power consumption: [276µA while operating](./doc/resources/ppk-20250206T233534_ON_2.png), [19µA idle](./doc/resources/ppk-20250206T233615_IDLE_2.png); max. 33 days continuous operation, 482 days standby @ 220mAh battery 21 | 22 | *OTA DFU and logging over BLE services are only available to connect to **within 30 seconds** after a cold-boot (removing and re-installing battery).* 23 | 24 | 25 | ### Hardware compatibility 26 | Please report other hardware configurations, board revisions, etc. you find out in the wild. 27 | 28 | | Product | Board (rev.) | SoC | | 29 | |---|---|---|---| 30 | | AIRSPY | TPMS-10-2 | nRF52832 | ✅ | 31 | 32 | 33 | ### Tested ANT+ Displays (Devices): 34 | | Device | Tire Pressure | Battery | Software Info | Manufacturer Info | Comments | 35 | |---|---|---|---|---|---| 36 | | Wahoo ELEMNT Bolt | ✅ | ❌ | ✅ | ❌ | | 37 | | [IpSensorMan](http://www.iforpowell.com/cms/index.php?page=ipantman)| ✅ | ✅ | ❌ | ✅ | [Garmin USB ANT Stick](https://www.garmin.com/en-US/p/10997) on Android 13 | 38 | 39 | 40 | ## Usage 41 | After installation, you just pair the sensor as any other ANT+ device with your bike computer. Done. Only ANT, no app, no Bluetooth. 42 | 43 | ## Installation 44 | The stock firmware provides wireless update capabilities. Sadly, we cannot use it to load our own firmware, as the update payload has to be signed, and only SKS knows the private key to do so. To flash this firmware we need physical access to the programming interface (SWD port) and a programmer. 45 | 46 | *If you are more into biking than into tech, now is a good time to call that one friend.* 47 | 48 | 49 | ### Warnings 50 | > ⚠ The installation requires you to open up the hardware. All warranty will most likely be lost in this process. You may brick the device in the process. Proceed at own risk! 51 | 52 | > ⚠ To revert to the original firmware, you have to jump some hoops; [read here first](./doc/INSTALL.md)! Consider reverting to original firmware as impossible! Proceed at own risk! 53 | 54 | > ℹ The ID of the sensor not match the laser engraving on the bottom after flashing. You can change the ID after flashing using the nRF Connect application. 55 | 56 | ### SWD Access 57 | Tools you need: 58 | * Cutter / X-Acto 59 | * Sturdy tweezers or small pliers 60 | * Spudger 61 | * SWD programmer, compatible with the nRF5 series 62 | * Something to attach to the SWD port: 4-Pin 1.27mm spacing pogo adapter, pin header, probe clips, etc. 63 | 64 | ![Disassembly & SWD access](./doc/resources/disassembly_swd.jpg) 65 | 1. Open up the battery compartment and remove the battery 66 | 2. Cut, or break by prying on the cover, the three glue blobs 67 | 3. Twist the cover clockwise, best while pulling it slightly upward, as it is held down by an adhesive foam 68 | 4. Take out cover 69 | 5. Connect your programmer according to its documentation/pin-out 70 | 71 | The SWD port is the top-right unpopulated four pin connector. Pin-out: 72 | 1. `GND` 73 | 2. `DIO`/`SWDIO` Serial Wire Data I/O 74 | 3. `CLK`/`SWCLK` Serial Wire Clock 75 | 4. `Vcc` up to 3.3V 76 | 77 | To connect the programmer, one of the best options is using the battery tabs to connect power supply with some clips. This should allow to connect two jumper (DuPont) wires to `DIO` and `CLK`, just pushing them to the pads with your fingers. 78 | 79 | 80 | ### Flashing 81 | > ℹ If you cannot detect any device on the SWD bus, cut power to the sensor for some seconds and turn it on again. The sensor will turn off after ~10 minutes. 82 | 83 | 1. Find a release on the release page and download the `.hex` file. 84 | 2. Use the tooling of your choice to connect to the SWD port 85 | 3. Unlock and erase the chip 86 | 4. Flash the `.hex` file you obtained earlier 87 | 5. 🎉 88 | 89 | > ℹ More information and examples in [INSTALL.md](./doc/INSTALL.md) 90 | 91 | ### Setting Device ID 92 | After flashing the firmware, the ANT device ID will differ from the laser engraving on the sensor. You can reset it to the engraved ID using the [nRF Connect app for mobile](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-mobile) or [desktop](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-Desktop). The changed ID will persist across firmware updates. 93 | 94 | 1. Connect to the sensor 95 | 2. Select service with UUID `2079cd72-8955-487c-bfbf-0bf85b255f3c` 96 | 3. Select attribute with UUID `f819d540-6c73-44e5-94bb-dfeb32926c2b` 97 | 4. Tap *Write*/*Upload* button 98 | 5. Enter Device ID (as engraved) - max. 65535 (16bit unsigned) 99 | 6. Select *Type* as `UINT 16 (Little Endian)` 100 | 7. Write 101 | 8. Device will reboot und use new ID for BLE and ANT 102 | 103 | ![set device ID](./doc/resources/ble_cfg_device_id_app.jpg) 104 | 105 | 106 | ## Updating 107 | If you already have a version of this firmware on your sensors, you can update wirelessly (OTA DFU) using your mobile phone: 108 | 109 | 1. Find a firmware release on the release page and download an update package `.zip` file to your phone 110 | 2. Install and open [nRF Connect](https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-mobile) mobile app 111 | 3. Remove battery from sensor for some seconds (up to a minute) 112 | 4. Re-install battery 113 | 5. Using nRF Connect app, find and connect to the sensor **within 30 seconds** of inserting the battery 114 | 6. Start firmware update by tapping the **DFU** button and selecting the update package 115 | 116 | ![BLE OTA DFU using Nordic nRF Connect app](./doc/resources/ble_ota_dfu_app.jpg) 117 | 118 | ## Inside the Hardware 119 | More info about the internal workings of the sensor and another part of the story of this project can be found in [`./doc/HARDWARE_PROTO.md`](./doc/HARDWARE_PROTO.md). 120 | 121 | ## Developing & Debugging 122 | For development you need the nRF SDK, matching toolchain, the [ANT SDK](https://www.thisisant.com/APIassets/ANTnRFConnectDoc/) (need to register and [become an ANT adopter](https://www.thisisant.com/my-ant/join-adopter) for this). Additionally, for the current version of the ANT SDK (*1.3.0*) you need to [patch the SDKs](./ant_sdk_nrf52832.patch) `Kconfig` to allow builds for the nRF52832 SoC. You will need to generate your own signing key as well. As the images are signed, you will not be able to use the OTA DFU functionality to flash your own build. 123 | 124 | For development on your bench, a RTT console is provided over SWD. Memory location is unknown, as the Black Magic probe did pick it up automatically. 125 | 126 | For debugging in operation, logging over BLE - using Nordic UART Service in [nRF Toolbox](https://www.nordicsemi.com/Products/Development-tools/nRF-Toolbox) mobile app - is provided. As with updating, connect to the console **within 30 seconds** after a cold-boot (remove and re-install battery). 127 | 128 | 129 | ## TODO 130 | - [x] Release from CI 131 | - [x] Write development documentation / article 132 | - [x] Add storage partition; will break OTA DFU! 133 | - [x] Allow ANT ID to be set using BLE service and store in storage partition 134 | - [ ] [Migrate to sysbuild](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/releases_and_maturity/migration/migration_sysbuild.html) 135 | - [ ] [Testing using BabbleSim](https://docs.zephyrproject.org/latest/boards/native/nrf_bsim/doc/nrf52_bsim.html) 136 | - [ ] Clean up code 137 | - [ ] Thank everybody I pestered with this project for too long 138 | 139 | 140 | ## License 141 | Most of the sources are licensed under [MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/), parts under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) or [Nordic-5-Clause](https://devzone.nordicsemi.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-00-04-DZ-1122/0333.5_5F00_Clause_5F00_Nordic_5F00_License_5F00_text.txt) license. 142 | 143 | 144 | ## Disclaimer 145 | This is a non-commercial project; the provided binary variant of the software uses the development ANT license key. 146 | 147 | 1\. The names SKS (SKS Germany) and AIRSPY are registered trademarks of SKS metaplast Scheffer-Klute GmbH. The names are solely used to describe compatibility of this software. 148 | 149 | 2\. ANT+ is a registered trademark of Garmin Canada Inc. This software/product is not ANT+ certified and does not claim compliance. The name ANT+ is solely used to describe parts of the functionality of this software. 150 | -------------------------------------------------------------------------------- /src/ant_profiles/ant_tpms/ant_tpms.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2023 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021, Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | #include 16 | #include 17 | #include 18 | #include 19 | // TODO: use kconfig option 20 | LOG_MODULE_REGISTER(ant_tpms, LOG_LEVEL_WRN); 21 | // LOG_MODULE_REGISTER(ant_tpms, CONFIG_ANT_TPMS_LOG_LEVEL); 22 | 23 | 24 | #define COMMON_PAGE_80_INTERVAL 119 // Minimum: Interleave every 121 messages 25 | #define COMMON_PAGE_81_INTERVAL 120 // Minimum: Interleave every 121 messages 26 | #define COMMON_PAGE_82_INTERVAL 121 // Minimum: Interleave every 121 messages 27 | 28 | /**@brief Tire Pressure message data layout structure. */ 29 | typedef struct 30 | { 31 | uint8_t page_number; 32 | uint8_t page_payload[7]; 33 | } ant_tpms_message_layout_t; 34 | 35 | 36 | /**@brief Function for initializing the ANT Tire Pressure Profile instance. 37 | * 38 | * @param[in] p_profile Pointer to the profile instance. 39 | * @param[in] p_channel_config Pointer to the ANT channel configuration structure. 40 | * 41 | * @retval 0 If initialization was successful. Otherwise, an error code is returned. 42 | */ 43 | static int ant_tpms_init(ant_tpms_profile_t * p_profile, 44 | ant_channel_config_t const * p_channel_config) 45 | { 46 | p_profile->channel_number = p_channel_config->channel_number; 47 | 48 | p_profile->page_1 = DEFAULT_ANT_TPMS_page1(); 49 | p_profile->page_80 = DEFAULT_ANT_COMMON_page80(); 50 | p_profile->page_81 = DEFAULT_ANT_COMMON_page81(); 51 | p_profile->page_82 = DEFAULT_ANT_COMMON_page82(); 52 | 53 | LOG_INF("ANT TPMS channel %u init", p_profile->channel_number); 54 | return ant_channel_init(p_channel_config); 55 | } 56 | 57 | 58 | int ant_tpms_disp_init(ant_tpms_profile_t * p_profile, 59 | ant_channel_config_t const * p_channel_config, 60 | ant_tpms_disp_config_t const * p_disp_config) 61 | { 62 | __ASSERT_NO_MSG(p_profile != NULL); 63 | __ASSERT_NO_MSG(p_channel_config != NULL); 64 | __ASSERT_NO_MSG(p_disp_config != NULL); 65 | __ASSERT_NO_MSG(p_disp_config->evt_handler != NULL); 66 | __ASSERT_NO_MSG(p_disp_config->p_cb != NULL); 67 | 68 | p_profile->evt_handler = p_disp_config->evt_handler; 69 | p_profile->_cb.p_disp_cb = p_disp_config->p_cb; 70 | 71 | return ant_tpms_init(p_profile, p_channel_config); 72 | } 73 | 74 | 75 | int ant_tpms_sens_init(ant_tpms_profile_t * p_profile, 76 | ant_channel_config_t const * p_channel_config, 77 | ant_tpms_sens_config_t const * p_sens_config) 78 | { 79 | __ASSERT_NO_MSG(p_profile != NULL); 80 | __ASSERT_NO_MSG(p_channel_config != NULL); 81 | __ASSERT_NO_MSG(p_sens_config != NULL); 82 | __ASSERT_NO_MSG(p_sens_config->p_cb != NULL); 83 | __ASSERT_NO_MSG(p_sens_config->evt_handler != NULL); 84 | 85 | p_profile->evt_handler = p_sens_config->evt_handler; 86 | p_profile->_cb.p_sens_cb = p_sens_config->p_cb; 87 | 88 | p_profile->_cb.p_sens_cb->message_counter = 0; 89 | 90 | return ant_tpms_init(p_profile, p_channel_config); 91 | } 92 | 93 | 94 | 95 | /**@brief Function for getting next page number to send. 96 | * 97 | * @param[in] p_profile Pointer to the profile instance. 98 | * 99 | * @return Next page number. 100 | */ 101 | static ant_tpms_page_t next_page_number_get(ant_tpms_profile_t * p_profile) 102 | { 103 | ant_tpms_sens_cb_t * p_tpms_cb = p_profile->_cb.p_sens_cb; 104 | ant_tpms_page_t page_number; 105 | 106 | p_tpms_cb->message_counter++; 107 | 108 | switch(p_tpms_cb->message_counter - 1){ 109 | case COMMON_PAGE_80_INTERVAL: 110 | page_number = ANT_TPMS_PAGE_80; 111 | break; 112 | case COMMON_PAGE_81_INTERVAL: 113 | page_number = ANT_TPMS_PAGE_81; 114 | break; 115 | case COMMON_PAGE_82_INTERVAL: 116 | page_number = ANT_TPMS_PAGE_82; 117 | p_tpms_cb->message_counter = 0; 118 | break; 119 | default: 120 | page_number = ANT_TPMS_PAGE_1; 121 | } 122 | 123 | return page_number; 124 | } 125 | 126 | 127 | /**@brief Function for encoding Tire Pressure Sensor message. 128 | * 129 | * @note Assume to be call each time when Tx window will occur. 130 | */ 131 | static void sens_message_encode(ant_tpms_profile_t * p_profile, uint8_t * p_message_payload) 132 | { 133 | ant_tpms_message_layout_t * p_tpms_message_payload = 134 | (ant_tpms_message_layout_t *)p_message_payload; 135 | 136 | p_tpms_message_payload->page_number = next_page_number_get(p_profile); 137 | 138 | LOG_INF("TPMS tx page: %u", p_tpms_message_payload->page_number); 139 | 140 | switch (p_tpms_message_payload->page_number) 141 | { 142 | case ANT_TPMS_PAGE_1: 143 | ant_tpms_page_1_encode(p_tpms_message_payload->page_payload, &(p_profile->page_1)); 144 | break; 145 | 146 | case ANT_COMMON_PAGE_80: 147 | ant_common_page_80_encode(p_tpms_message_payload->page_payload, &(p_profile->page_80)); 148 | break; 149 | 150 | case ANT_COMMON_PAGE_81: 151 | ant_common_page_81_encode(p_tpms_message_payload->page_payload, &(p_profile->page_81)); 152 | break; 153 | 154 | case ANT_COMMON_PAGE_82: 155 | ant_common_page_82_encode(p_tpms_message_payload->page_payload, &(p_profile->page_82)); 156 | break; 157 | 158 | default: 159 | return; 160 | } 161 | 162 | p_profile->evt_handler(p_profile, (ant_tpms_evt_t)p_tpms_message_payload->page_number); 163 | 164 | } 165 | 166 | 167 | /**@brief Function for decoding messages received by Tire Pressure sensor message. 168 | * 169 | * @note Assume to be call each time when Rx window will occur. 170 | */ 171 | static void sens_message_decode(ant_tpms_profile_t * p_profile, uint8_t * p_message_payload) 172 | { 173 | const ant_tpms_message_layout_t * p_tpms_message_payload = 174 | (ant_tpms_message_layout_t *)p_message_payload; 175 | ant_tpms_page1_data_t page1; 176 | 177 | switch (p_tpms_message_payload->page_number) 178 | { 179 | case ANT_TPMS_PAGE_1: 180 | ant_tpms_page_1_decode(p_tpms_message_payload->page_payload, &page1); 181 | break; 182 | 183 | default: 184 | break; 185 | } 186 | } 187 | 188 | 189 | /**@brief Function for decoding messages received by Tire Pressure display message. 190 | * 191 | * @note Assume to be call each time when Rx window will occur. 192 | */ 193 | static void disp_message_decode(ant_tpms_profile_t * p_profile, uint8_t * p_message_payload) 194 | { 195 | const ant_tpms_message_layout_t * p_tpms_message_payload = 196 | (ant_tpms_message_layout_t *)p_message_payload; 197 | 198 | LOG_INF("TPMS rx page: %u", p_tpms_message_payload->page_number); 199 | 200 | switch (p_tpms_message_payload->page_number) 201 | { 202 | case ANT_TPMS_PAGE_1: 203 | ant_tpms_page_1_decode(p_tpms_message_payload->page_payload, &(p_profile->page_1)); 204 | break; 205 | 206 | case ANT_COMMON_PAGE_80: 207 | ant_common_page_80_decode(p_tpms_message_payload->page_payload, &(p_profile->page_80)); 208 | break; 209 | 210 | case ANT_COMMON_PAGE_81: 211 | ant_common_page_81_decode(p_tpms_message_payload->page_payload, &(p_profile->page_81)); 212 | break; 213 | 214 | case ANT_COMMON_PAGE_82: 215 | ant_common_page_82_decode(p_tpms_message_payload->page_payload, &(p_profile->page_82)); 216 | break; 217 | 218 | default: 219 | return; 220 | } 221 | 222 | p_profile->evt_handler(p_profile, (ant_tpms_evt_t)p_tpms_message_payload->page_number); 223 | } 224 | 225 | 226 | static void ant_message_send(ant_tpms_profile_t * p_profile) 227 | { 228 | uint8_t p_message_payload[ANT_STANDARD_DATA_PAYLOAD_SIZE]; 229 | 230 | sens_message_encode(p_profile, p_message_payload); 231 | 232 | int err_code = ant_broadcast_message_tx(p_profile->channel_number, 233 | sizeof (p_message_payload), 234 | p_message_payload); 235 | 236 | __ASSERT_NO_MSG(err_code == 0); 237 | } 238 | 239 | 240 | int ant_tpms_disp_open(ant_tpms_profile_t * p_profile) 241 | { 242 | __ASSERT_NO_MSG(p_profile != NULL); 243 | 244 | LOG_INF("ANT TPMS %u open", p_profile->channel_number); 245 | return ant_channel_open(p_profile->channel_number); 246 | } 247 | 248 | 249 | int ant_tpms_sens_open(ant_tpms_profile_t * p_profile) 250 | { 251 | __ASSERT_NO_MSG(p_profile != NULL); 252 | 253 | // Fill tx buffer for the first frame 254 | ant_message_send(p_profile); 255 | 256 | LOG_INF("ANT TPMS %u open", p_profile->channel_number); 257 | return ant_channel_open(p_profile->channel_number); 258 | } 259 | 260 | 261 | void ant_tpms_sens_evt_handler(ant_evt_t * p_ant_event, void * p_context) 262 | { 263 | ant_tpms_profile_t * p_profile = ( ant_tpms_profile_t *)p_context; 264 | 265 | if (p_ant_event->channel == p_profile->channel_number) 266 | { 267 | switch (p_ant_event->event) 268 | { 269 | case EVENT_TX: 270 | ant_message_send(p_profile); 271 | break; 272 | 273 | case EVENT_RX: 274 | if (p_ant_event->message.ANT_MESSAGE_ucMesgID == MESG_ACKNOWLEDGED_DATA_ID) 275 | { 276 | sens_message_decode(p_profile, p_ant_event->message.ANT_MESSAGE_aucPayload); 277 | } 278 | break; 279 | 280 | default: 281 | // No implementation needed 282 | break; 283 | } 284 | } 285 | } 286 | 287 | 288 | void ant_tpms_disp_evt_handler(ant_evt_t * p_ant_event, void * p_context) 289 | { 290 | ant_tpms_profile_t * p_profile = ( ant_tpms_profile_t *)p_context; 291 | 292 | if (p_ant_event->channel == p_profile->channel_number) 293 | { 294 | switch (p_ant_event->event) 295 | { 296 | case EVENT_RX: 297 | 298 | if (p_ant_event->message.ANT_MESSAGE_ucMesgID == MESG_BROADCAST_DATA_ID 299 | || p_ant_event->message.ANT_MESSAGE_ucMesgID == MESG_ACKNOWLEDGED_DATA_ID 300 | || p_ant_event->message.ANT_MESSAGE_ucMesgID == MESG_BURST_DATA_ID) 301 | { 302 | disp_message_decode(p_profile, p_ant_event->message.ANT_MESSAGE_aucPayload); 303 | } 304 | break; 305 | 306 | default: 307 | break; 308 | } 309 | } 310 | } 311 | 312 | -------------------------------------------------------------------------------- /include/ant_profiles/tpms/ant_tpms.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Arne Wendt (@bitmeal) 3 | * 4 | * Copyright (c) 2024 by Garmin Ltd. or its subsidiaries. 5 | * All rights reserved. 6 | * 7 | * Use of this Software is limited and subject to the License Agreement for ANT SoftDevice 8 | * and Associated Software. The Agreement accompanies the Software in the root directory of 9 | * the repository. 10 | * 11 | * Copyright (c) 2015 - 2021, Nordic Semiconductor ASA 12 | * 13 | * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 14 | */ 15 | 16 | #ifndef ANT_TPMS_H__ 17 | #define ANT_TPMS_H__ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #define TPMS_DEVICE_TYPE 0x30u ///< Device type reserved for ANT+ Tire Pressure. 27 | #define TPMS_ANTPLUS_RF_FREQ 0x39u ///< Frequency, decimal 57 (2457 MHz). 28 | #define TPMS_MSG_PERIOD 8192u ///< Message period, decimal 8192 (4 Hz). 29 | 30 | #define TPMS_EXT_ASSIGN 0x00 ///< ANT ext assign (see Ext. Assign Channel Parameters in ant_parameters.h: @ref ant_parameters). 31 | #define TPMS_DISP_CHANNEL_TYPE CHANNEL_TYPE_SLAVE ///< Display Tire Pressure channel type. 32 | #define TPMS_SENS_CHANNEL_TYPE CHANNEL_TYPE_MASTER ///< Sensor Tire Pressure channel type. 33 | 34 | /**@brief Helper macro with two level expansion for concatenation of two parameters.*/ 35 | #define _ANT_TPMS_CONCAT_2(p1, p2) _ANT_TPMS_CONCAT_2_(p1, p2) 36 | #define _ANT_TPMS_CONCAT_2_(p1, p2) p1##p2 37 | 38 | /**@brief Initialize an ANT channel configuration structure for the Tire Pressure profile (Display). 39 | * 40 | * @param[in] NAME Name of related instance. 41 | * @param[in] CHANNEL_NUMBER Number of the channel assigned to the profile instance. 42 | * @param[in] TRANSMISSION_TYPE Type of transmission assigned to the profile instance. 43 | * @param[in] DEVICE_NUMBER Number of the device assigned to the profile instance. 44 | * @param[in] NETWORK_NUMBER Number of the network assigned to the profile instance. 45 | */ 46 | #define TPMS_DISP_CHANNEL_CONFIG_DEF(NAME, \ 47 | CHANNEL_NUMBER, \ 48 | TRANSMISSION_TYPE, \ 49 | DEVICE_NUMBER, \ 50 | NETWORK_NUMBER) \ 51 | static const ant_channel_config_t _ANT_TPMS_CONCAT_2(NAME, _channel_tpms_disp_config) = \ 52 | { \ 53 | .channel_number = (CHANNEL_NUMBER), \ 54 | .channel_type = TPMS_DISP_CHANNEL_TYPE, \ 55 | .ext_assign = TPMS_EXT_ASSIGN, \ 56 | .rf_freq = TPMS_ANTPLUS_RF_FREQ, \ 57 | .transmission_type = (TRANSMISSION_TYPE), \ 58 | .device_type = TPMS_DEVICE_TYPE, \ 59 | .device_number = (DEVICE_NUMBER), \ 60 | .channel_period = TPMS_MSG_PERIOD, \ 61 | .network_number = (NETWORK_NUMBER), \ 62 | } 63 | #define TPMS_DISP_CHANNEL_CONFIG(NAME) &_ANT_TPMS_CONCAT_2(NAME, _channel_tpms_disp_config) 64 | 65 | /**@brief Initialize an ANT channel configuration structure for the Tire Pressure profile (Sensor). 66 | * 67 | * @param[in] NAME Name of related instance. 68 | * @param[in] CHANNEL_NUMBER Number of the channel assigned to the profile instance. 69 | * @param[in] TRANSMISSION_TYPE Type of transmission assigned to the profile instance. 70 | * @param[in] DEVICE_NUMBER Number of the device assigned to the profile instance. 71 | * @param[in] NETWORK_NUMBER Number of the network assigned to the profile instance. 72 | */ 73 | #define TPMS_SENS_CHANNEL_CONFIG_DEF(NAME, \ 74 | CHANNEL_NUMBER, \ 75 | TRANSMISSION_TYPE, \ 76 | DEVICE_NUMBER, \ 77 | NETWORK_NUMBER) \ 78 | static const ant_channel_config_t _ANT_TPMS_CONCAT_2(NAME, _channel_tpms_sens_config) = \ 79 | { \ 80 | .channel_number = (CHANNEL_NUMBER), \ 81 | .channel_type = TPMS_SENS_CHANNEL_TYPE, \ 82 | .ext_assign = TPMS_EXT_ASSIGN, \ 83 | .rf_freq = TPMS_ANTPLUS_RF_FREQ, \ 84 | .transmission_type = (TRANSMISSION_TYPE), \ 85 | .device_type = TPMS_DEVICE_TYPE, \ 86 | .device_number = (DEVICE_NUMBER), \ 87 | .channel_period = TPMS_MSG_PERIOD, \ 88 | .network_number = (NETWORK_NUMBER), \ 89 | } 90 | #define TPMS_SENS_CHANNEL_CONFIG(NAME) &_ANT_TPMS_CONCAT_2(NAME, _channel_tpms_sens_config) 91 | 92 | /**@brief Initialize an ANT profile configuration structure for the TPMS profile (Display). 93 | * 94 | * @param[in] NAME Name of related instance. 95 | * @param[in] EVT_HANDLER Event handler to be called for handling events in the TPMS profile. 96 | */ 97 | #define TPMS_DISP_PROFILE_CONFIG_DEF(NAME, \ 98 | EVT_HANDLER) \ 99 | static ant_tpms_disp_cb_t _ANT_TPMS_CONCAT_2(NAME, _tpms_disp_cb); \ 100 | static const ant_tpms_disp_config_t _ANT_TPMS_CONCAT_2(NAME, _profile_tpms_disp_config) = \ 101 | { \ 102 | .p_cb = &_ANT_TPMS_CONCAT_2(NAME, _tpms_disp_cb), \ 103 | .evt_handler = (EVT_HANDLER), \ 104 | } 105 | #define TPMS_DISP_PROFILE_CONFIG(NAME) &_ANT_TPMS_CONCAT_2(NAME, _profile_tpms_disp_config) 106 | 107 | 108 | /**@brief Initialize an ANT profile configuration structure for the TPMS profile (Sensor). 109 | * 110 | * @param[in] NAME Name of related instance. 111 | * @param[in] EVT_HANDLER Event handler to be called for handling events in the TPMS profile. 112 | */ 113 | #define TPMS_SENS_PROFILE_CONFIG_DEF(NAME, \ 114 | EVT_HANDLER) \ 115 | static ant_tpms_sens_cb_t _ANT_TPMS_CONCAT_2(NAME, _tpms_sens_cb); \ 116 | static const ant_tpms_sens_config_t _ANT_TPMS_CONCAT_2(NAME, _profile_tpms_sens_config) = \ 117 | { \ 118 | .p_cb = &_ANT_TPMS_CONCAT_2(NAME, _tpms_sens_cb), \ 119 | .evt_handler = (EVT_HANDLER), \ 120 | } 121 | #define TPMS_SENS_PROFILE_CONFIG(NAME) &NAME##_profile_tpms_sens_config 122 | 123 | 124 | /**@brief Tire Pressure page number type. */ 125 | typedef enum 126 | { 127 | ANT_TPMS_PAGE_1 = 1, ///< Tire pressure main data page. 128 | ANT_TPMS_PAGE_80 = ANT_COMMON_PAGE_80, 129 | ANT_TPMS_PAGE_81 = ANT_COMMON_PAGE_81, 130 | ANT_TPMS_PAGE_82 = ANT_COMMON_PAGE_82 131 | } ant_tpms_page_t; 132 | 133 | /**@brief TPMS profile event type. */ 134 | typedef enum 135 | { 136 | ANT_TPMS_PAGE_1_UPDATED = ANT_TPMS_PAGE_1, ///< Data page 1 and speed have been updated (Display) or sent (Sensor). 137 | ANT_TPMS_PAGE_80_UPDATED = ANT_TPMS_PAGE_80, ///< Data page 80 has been updated (Display) or sent (Sensor). 138 | ANT_TPMS_PAGE_81_UPDATED = ANT_TPMS_PAGE_81, ///< Data page 81 has been updated (Display) or sent (Sensor). 139 | ANT_TPMS_PAGE_82_UPDATED = ANT_TPMS_PAGE_82 ///< Data page 82 has been updated (Display) or sent (Sensor). 140 | } ant_tpms_evt_t; 141 | 142 | // Forward declaration of the ant_tpms_profile_t type. 143 | typedef struct ant_tpms_profile_s ant_tpms_profile_t; 144 | 145 | /**@brief TPMS event handler type. */ 146 | typedef void (* ant_tpms_evt_handler_t) (ant_tpms_profile_t *, ant_tpms_evt_t); 147 | 148 | 149 | #include "ant_profiles/tpms/ant_tpms_local.h" 150 | 151 | #ifdef __cplusplus 152 | extern "C" { 153 | #endif 154 | 155 | /**@brief Tire Pressure Sensor configuration structure. */ 156 | typedef struct 157 | { 158 | ant_tpms_sens_cb_t * p_cb; ///< Pointer to the data buffer for internal use. 159 | ant_tpms_evt_handler_t evt_handler; ///< Event handler to be called for handling events in the TPMS profile. 160 | } ant_tpms_sens_config_t; 161 | 162 | /**@brief Tire Pressure Display configuration structure. */ 163 | typedef struct 164 | { 165 | ant_tpms_disp_cb_t * p_cb; ///< Pointer to the data buffer for internal use. 166 | ant_tpms_evt_handler_t evt_handler; ///< Event handler to be called for handling events in the TPMS profile. 167 | } ant_tpms_disp_config_t; 168 | 169 | /**@brief Tire Pressure profile structure. */ 170 | struct ant_tpms_profile_s 171 | { 172 | uint8_t channel_number; ///< Channel number assigned to the profile. 173 | union { 174 | ant_tpms_disp_cb_t * p_disp_cb; 175 | ant_tpms_sens_cb_t * p_sens_cb; 176 | } _cb; ///< Pointer to internal control block. 177 | ant_tpms_evt_handler_t evt_handler; ///< Event handler to be called for handling events in the TPMS profile. 178 | ant_tpms_page1_data_t page_1; ///< Page 1. 179 | ant_common_page80_data_t page_80; ///< Page 80. 180 | ant_common_page81_data_t page_81; ///< Page 81. 181 | ant_common_page82_data_t page_82; ///< Page 82. 182 | }; 183 | 184 | /** @name Defines for accessing ant_tpms_profile_t member variables 185 | @{ */ 186 | #define TPMS_PROFILE_pressure page_1.pressure 187 | 188 | #define TPMS_PROFILE_manuf_id page_80.manuf_id 189 | #define TPMS_PROFILE_hw_revision page_80.hw_revision 190 | #define TPMS_PROFILE_manufacturer_id page_80.manufacturer_id 191 | #define TPMS_PROFILE_model_number page_80.model_number 192 | 193 | #define TPMS_PROFILE_sw_revision_minor page_81.sw_revision_minor 194 | #define TPMS_PROFILE_sw_revision_major page_81.sw_revision_major 195 | #define TPMS_PROFILE_serial_number page_81.serial_number 196 | 197 | #define TPMS_PROFILE_battery_count page_82.battery_count 198 | #define TPMS_PROFILE_battery_id page_82.battery_id 199 | #define TPMS_PROFILE_operating_time page_82.operating_time 200 | #define TPMS_PROFILE_battery_voltage_mv page_82.battery_voltage_mv 201 | #define TPMS_PROFILE_battery_status page_82.battery_status 202 | 203 | 204 | /** @} */ 205 | 206 | /**@brief Function for initializing the ANT Tire Pressure Display profile instance. 207 | * 208 | * @param[in] p_profile Pointer to the profile instance. 209 | * @param[in] p_channel_config Pointer to the ANT channel configuration structure. 210 | * @param[in] p_disp_config Pointer to the Tire Pressure Display configuration structure. 211 | * 212 | * @retval 0 If initialization was successful. Otherwise, an error code is returned. 213 | */ 214 | int ant_tpms_disp_init(ant_tpms_profile_t * p_profile, 215 | ant_channel_config_t const * p_channel_config, 216 | ant_tpms_disp_config_t const * p_disp_config); 217 | 218 | /**@brief Function for initializing the ANT Tire Pressure Sensor profile instance. 219 | * 220 | * @param[in] p_profile Pointer to the profile instance. 221 | * @param[in] p_channel_config Pointer to the ANT channel configuration structure. 222 | * @param[in] p_sens_config Pointer to the Tire Pressure Sensor configuration structure. 223 | * 224 | * @retval 0 If initialization was successful. Otherwise, an error code is returned. 225 | */ 226 | int ant_tpms_sens_init(ant_tpms_profile_t * p_profile, 227 | ant_channel_config_t const * p_channel_config, 228 | ant_tpms_sens_config_t const * p_sens_config); 229 | 230 | /**@brief Function for opening the profile instance channel for ANT TPMS Display. 231 | * 232 | * Before calling this function, pages should be configured. 233 | * 234 | * @param[in] p_profile Pointer to the profile instance. 235 | * 236 | * @retval 0 If the channel was successfully opened. Otherwise, an error code is returned. 237 | */ 238 | int ant_tpms_disp_open(ant_tpms_profile_t * p_profile); 239 | 240 | /**@brief Function for opening the profile instance channel for ANT TPMS Sensor. 241 | * 242 | * Before calling this function, pages should be configured. 243 | * 244 | * @param[in] p_profile Pointer to the profile instance. 245 | * 246 | * @retval 0 If the channel was successfully opened. Otherwise, an error code is returned. 247 | */ 248 | int ant_tpms_sens_open(ant_tpms_profile_t * p_profile); 249 | 250 | 251 | /**@brief Function for handling the Sensor ANT events. 252 | * 253 | * @details This function handles all events from the ANT stack that are of interest to the Tire Pressure Display profile. 254 | * 255 | * @param[in] p_ant_evt Event received from the ANT stack. 256 | * @param[in] p_context Pointer to the profile instance. 257 | */ 258 | void ant_tpms_sens_evt_handler(ant_evt_t * p_ant_evt, void * p_context); 259 | 260 | /**@brief Function for handling the Display ANT events. 261 | * 262 | * @details This function handles all events from the ANT stack that are of interest to the Tire Pressure Display profile. 263 | * 264 | * @param[in] p_ant_evt Event received from the ANT stack. 265 | * @param[in] p_context Pointer to the profile instance. 266 | */ 267 | void ant_tpms_disp_evt_handler(ant_evt_t * p_ant_evt, void * p_context); 268 | 269 | /** 270 | * @} 271 | */ 272 | 273 | #ifdef __cplusplus 274 | } 275 | #endif 276 | 277 | #endif // ANT_TPMS_H__ 278 | 279 | -------------------------------------------------------------------------------- /doc/HARDWARE_PROTO.md: -------------------------------------------------------------------------------- 1 | # SKS AIRSPY Hardware and on-Board Communications 2 | ![banner image](./resources/construction_banner.jpg) 3 | 4 | This document describes the important hardware parts of the sensor, their function and their communication. And the story of how and why we know. 5 | 6 | ## Hardware Components 7 | The board hosts a *Nordic nRF52832* SoC for 2,4GHz BLE and ANT communications and a *Freescale/NXP FXTH87* series tire pressure monitoring (TPMS) SoC, as pressure, voltage and temperature sensor (but we don't really know this by now). Both are easily identified after disassembly by their markings on the package, or - without disassembly - by finding the [internal photos of FCC report 2ASEG-1161A](https://fcc.report/FCC-ID/2ASEG-1161A/4672637). This is the way I went before buying a pair of the sensors and decided to tackle this project at all. 8 | 9 | ### FXTH87 (bottom side) 10 | FXTH87 series SoCs are fully integrated TPMS solutions. They include own RF communication systems and a programmable MCU. They are intended to be used stand-alone with their low frequency, but longer range radios, compared to 2,4GHz radios. And this is where the problem begins: Without flashing any software, these SoCs won't do anything. But why is this a problem? We cannot just take a look at the data-sheet and understand what the device does. We can look at data-sheets and documentation and get an idea of what the device is theoretically capable of, but not what the currently flashed firmware makes it do. Documentation used to determine the workings are [1, 2]. For now we know from the documentation, that this device can sense pressure, temperature, supply voltage, has at least one accelerometer and a number of GPIOs. The goal is to not touch this device more than necessary and to not write any software for an arcane NXP 8-bit MCU with proprietary programming/debugging interface. 11 | 12 | #### Documentation 13 | 1. [**FXTH87xx02 Embedded Firmware User Guide; Rev. 2.2 — 5 April 2017**](https://www.nxp.com/docs/en/user-guide/FXTH87xx02FWUG.pdf) 14 | 2. [**FXTH87E, Family of Tire Pressure Monitor Sensors; Reference manual; Rev. 6.1 — 21 June 2021**](https://www.nxp.com/docs/en/reference-manual/FXTH87ERM.pdf) 15 | 3. **Correspondence between compensated sensor data and data in common units Rev. 4**; from *TPMS Demo BLE Beacons by GenFSK*; available via [NXP here](https://www.nxp.com/products/FXTH87) 16 | 17 | ### nRF52832 (top side) 18 | The nRF5 series controllers are well known and documented ARM Cortex M4 SoCs with 2,4GHz radios. The development tools are openly available and the SoC packs most functions and peripherals you would expect. The nRF provides BLE and ANT communications for the sensor. It is the target for new firmware to implement ANT communications compatible with a new device profile. The nRF allows most peripherals to be used on any pin (except e.g. power supply, oscillator or radio of course), thus the wiring does not allow drawing any conclusions about the performed function of the pins. 19 | 20 | 21 | ## Communications (Bus) 22 | ![partial schematic](./resources/airspy_schematic_partial.png) 23 | ![bed of nails fixture](./resources/bed_of_nails.jpg) 24 | 25 | Both SoCs do have to communicate with each other - at least uni-directionally - to make use of each others functions. Figure above shows the connection between them, as probed with a multimeter. The nets are already named by their function, which in reality, we have to find in the next steps. To capture all interaction and possibly missed connections, a bed-of-nails fixture was built, to connect to all logic pins, as well as reset and Vcc, of the FXTH87. The figures below show the - already labeled - captured data for all probed pins for: 26 | 1. startup 27 | 2. single data transmission event 28 | 3. shutdown 29 | 30 | ![logic analyzer traces](./resources/logic.png) 31 | 32 | ### Analysis: The awakening 33 | Looking at the first and last presented trace, we can easily identify the signal used to enable and disable the system. Looking at the first and third trace, we find two signals (`WAKE0` and `WAKE1`) going high when powering on the sensor, and going low after the documented timeout of ~10 minutes. `WAKE0` periodically goes high for ~3s every ~3m when the sensor is off. In operation and with stock firmware, the sensor wakes up when being moved. As the FXTH87 includes an accelerometer, and the nRF52 does not, the former has to be responsible for providing data about when to wake up and go to sleep. As no data is exchanged when sleeping, even when `WAKE0` is periodically high, the FXTH87 has to have full authority to control both `WAKE0` and `WAKE1` to wake up the nRF52. *This has been confirmed with experiments not presented in the traces above.* As `WAKE0` is not connected to the nRF52 on the board, we obtain following result: 34 | 35 | `WAKE1`, connected to FXTH87 `PTA3`(18) and nRF52 `P0.25`(37) signals to the nRF52 when the sensor shall wake and sleep. 36 | 37 | ### Analysis: The great deception 38 | The second trace shows a seemingly standard SPI communication and allows to identify and label the signals for clock (`CLK`) data (`DATA`) and a chip select (`/CS, /INT`). Now, we have to find who is master and who is slave. The nRF52 has peripherals for SPI master and slave, so both is possible in theory. Let's take a look at the FXTH87 documentation [1], section 2.4: 39 | > #### 2.4 Simulated SPI interface signal format 40 | > The FXTH87xx02 includes three routines (`TPMS_MSG_INIT`, `TPMS_MSG_READ` and `TPMS_MSG_WRITE`) that, when used together, allow the user to perform serial communication with the device through a simulated SPI interface. The following assumptions are made: 41 | > * Only two pins are used: `PTA0` for data (both incoming and outgoing) and `PTA1` for clock. No slave select is included by default, but the user may use any other pin if required. 42 | > * The data pin has a pullup resistor enabled. 43 | > * The FXTH87xx02 will be a master device (the FXTH87xx02 will provide the clock). 44 | > * Data can be read/written eight bits at a time. 45 | > * Speed of the interface is dependant on bus clock settings. 46 | > * Data is transferred MSB first. 47 | > * A single line will be used for both sending and receiving data (`BIDIROE = SET` according to NXP nomenclature). 48 | > * At the clock's rising edge, the master will place data on the pin. It will be valid until the clock's falling edge. The slave must not drive the line during this period. 49 | > * At the clock's falling edge, the master will make the data pin an input and will "listen" for data. The slave must then place data on the data line until the clock's rising edge. 50 | > * Clock Polarity = 0 (Normally low). 51 | > * Clock Phase = 1 (First half is high). 52 | 53 | We learn: A (simulated/software) SPI interface is present, `PTA0` is data line, `PTA1` is clock, no slave/chip select by default and FXTH87 is the SPI master. The described pin-out does match the hardware, so this should be a first success - right? 54 | 55 | When implementing a SPI slave on the nRF52, the communications look like in the startup sequence in the first trace above: The clock is missing and the data is written to the line about 10x slower than expected (trace two). With a missing clock, the nRF52 as a SPI slave does obviously never receive any data. What's wrong? Didn't the documentation tell us the FXTH87 will be the SPI master? 56 | 57 | ### Analysis: (Not so) Grand Finale 58 | > In reality, the following took way longer than it should have taken. 59 | 60 | The answer is really quite simple, and the startup sequence in trace one does give us a hint: The nRF52 is not fully booted when `/CS,/INT` is driven low by the FXTH87, and cannot react to the event. The event is not a chip select, but an interrupt signal, as a request to send data. The FXTH87 is actually a SPI slave and the nRF52 is the master, reacting to an interrupt event! But, if no clock is provided, the data will be written to the line none the less. As shown by experiments, the frequency of the clock does matter as well: The nRF52832 SPI Master minimum clock frequency of 125 kHz is ~10x higher than the frequency used by the stock sensor, and too damn high! The FXTH87 is not capable of clocking out the data at this frequency. Thus, the later implementation will use a bit-bang SPI master. 61 | 62 | Following table summarizes the findings: 63 | 64 | | Signal | Description | nRF Pin (#) | FXTH Pin (#) | 65 | |---|---|---|---| 66 | | SPI CLK | clock @ 10.37 kHz; provided by nRF | `P0.19`(22) | `PTA1`(3) | 67 | | SPI MISO | unidirectional data; sent by FXTH | `P0.15`(18) | `PTA0`(4) | 68 | | SPI /INT | interrupt to request data transfer; driven by FXTH | `P0.22`(27) | `PTB0`(24) | 69 | | WAKE | operational state of the device; controlled by FXTH | `P0.25`(37) | `PTA3`(18) | 70 | 71 | 72 | 73 | ## Data Decoding 74 | Now, that we have identified how data is exchanged between both systems, we have to make sense of the exchanged data. Interpreting the data is easier if we can make some - hopefully correct - educated guesses and formulate expectations or even a hypothesis. First, the second trace above shows us, that we get 6 Bytes of data per transmission. To build a hypothesis, let's look at [1], where we find some information in the documented methods relating to the TPMS functions: 75 | 76 | | Name | Description | 77 | |---|---| 78 | | `TPMS_COMP_VOLTAGE` | 8-bit compensation of 10-bit voltage reading | 79 | | `TPMS_COMP_TEMPERATURE` | 8-bit compensation of 10-bit temperature reading | 80 | | `TPMS_COMP_PRESSURE` | 9-bit compensation of 10-bit pressure reading | 81 | | `TPMS_COMP_ACCELERATION`| 9-bit compensation of 10-bit acceleration reading | 82 | | `TPMS_CHECKSUM_XOR` | Calculates a checksum for given buffer in XOR | 83 | | `TPMS_CRC8` | Calculates CRC8 on portion of memory | 84 | | `TPMS_CRC16` | Calculates CRC16 on portion of memory | 85 | 86 | All these methods operate exclusively on integral types! 87 | 88 | ### Building a hypothesis 89 | 1. No particular protocol, just packed data 90 | 2. **Pressure** data occupies at least 9 bits; expected 16 bits for 8 bit alignment 91 | 3. **Temperature** data occupies 8 bits; if present 92 | 4. Supply **Voltage** data occupies 8 bits; if present 93 | 5. **Acceleration** data occupies at least 9 bits; expected 16 bits for 8 bit alignment; if present 94 | 6. If present, a **checksum** is calculated as either of: CRC8, CRC16, XOR 95 | 96 | Not all of this will fit in our 6 Byte transaction. So, on to some experiments to find what is present! 97 | 98 | ### Performing experiments 99 | To find out, which information is present in the data, we can systematically vary all measurable parameters one by one. For each experiment, an unspecified number of transactions is recorded. The figure below shows the normalized percentage of changes per bit per experiment and the normalized overall percentage of each bit being set. 100 | 101 | ![experiments analysis bits statistics](./resources/proto_analysis_clustering.png) 102 | > ⚠ The temperature (Temp.) experiment was designed wrong and did not yield any meaningful data! 103 | 104 | For all experiments, clusters are built from the bit change statistics, using [Mean Shift clustering](https://scikit-learn.org/stable/auto_examples/cluster/plot_mean_shift.html). The resulting clusters are highlighted in the figure above. 105 | 106 | #### Finding 1: Checksum 107 | All traces show a cluster at the end of each transaction - a common place for checksuma. Interestingly, the (geometric) shape of the cluster resembles a combination of the other clusters in the respective experiment. From the available checksum calculation methods, this matches application of `XOR` most closely. 108 | 109 | Applying `XOR` over the first 5 Bytes of all transactions, yields a result matching the potential checksum in most cases. But only most! At times, the first bit of the checksum in the transaction and the calculated checksum do not match. Looking at the statistics, we find the cluster to be of an unusual width of only 7 bits. What's wrong here? 110 | 111 | #### Finding 2: (Shape-) Shifting 112 | Finding the pattern, as present in the second trace above, took way too long... But let's take a look: **After** the last clock pulse, we find another short pulse on the data line. Beginning just after the clock goes low and ending nearly in sync with the interrupt line going inactive (high) again. May this be the eighth bit of the checksum? 113 | 114 | Let's take a look at the bit statistics of the experiments again, as well: The bits with the highest percentage of changes are likely the least significant ones. Under the - quite reasonable - assumption of 8 bit aligned data values, the least significant bits should be found on positions {7,15,23,31,39,47}. From the experiments we find more likely candidates in positions {16,24,32}. The conclusion? 115 | 116 | Yes, **all data is shifted by one bit**. The used implementation on the FXTH87 seems to count the correct number of clock cycles to end the data transmission, but clocks out its data too slow. 117 | 118 | 119 | ### Data interpretation 120 | Now, that we know all data is shifted by one bit and can check data integrity - except for the least significant bits, we can correlate the remaining blocks of aligned 8 bit wide data with some info from [3], and most things fall into place. All further bit/Byte indices assume a single bit left shift `<< 1` has been performed 121 | 122 | #### Pressure 123 | The **first two Bytes** is the pressure value, as evident from the experiment statistics above. The conversion to kPa does not align with the information in [3] though: 124 | 125 | > The pressure in kPa can be calculated [...]: P = ΔP * P_CODE + (P_min - ΔP ) [3] 126 | 127 | Where ΔP depends on calibration range of the FXTH. P_min = 100 kPa to normalize overpressure readings for atmospheric pressure. No source for the possible value of ΔP could be found, and **the given function did not match the data**. With experiments, as in the figure below, and with a least squares linear regression, the best transfer function was found to be: 128 | 129 | ``` 130 | P_hPa = P_CODE * 16.75 - 248 131 | ``` 132 | 133 | ![calibration setup](./resources/calibration.jpg) 134 | 135 | #### Voltage 136 | > For most part numbers, converting the value in Volt is done by adding 122 and then dividing by 100. [...] 137 | > *Example: Volt = 173 => V = (173 + 122)/100 = 2.95V* 138 | > [3] 139 | 140 | Position of the voltage information can be determined from the experiments as Byte 3 (0-based index). The transfer function matches what is given in [3]. 141 | 142 | #### Temperature 143 | > For most part numbers, converting the value in °C is done by subtracting the value 55. [...] 144 | > *Example: Temp = 83 => T = 83 - 55 = 28°C* 145 | > [3] 146 | 147 | Even if the temperature experiment was flawed, Byte 2 showed some small changes in the recorded data. The values did fit - and do after further experiment - perfectly the transfer function for the temperature. 148 | 149 | #### Flags 150 | Byte 4 seems to include additional status flags. By now, only a low-voltage flag has been identified. Under/over-temperature and negative pressure are likely candidates as well, as these kind of information is present in the documentation. 151 | 152 | #### Summary 153 | ```C 154 | /* 155 | layout: 156 | [0, 17): pressure --> b[9:17].int * 16.75 - 248 157 | [17, 25) - 55 ^= temperature 158 | [25, 33) + 122 ^= voltage 159 | 35: FLAG: under voltage 160 | [40,48): XOR checksum; ignore lowest bit after shifting whole buffer 161 | */ 162 | 163 | struct __attribute__((__packed__)) sensor_readings_t { 164 | int16_t pressure_hpa; 165 | int8_t temperature_c; 166 | int16_t voltage_mv; 167 | unsigned char flags; 168 | unsigned char checksum; 169 | }; 170 | 171 | #define SENSOR_CLAMP_PRESS_LOW_HPA 75 172 | #define SENSOR_COMP_CONST_PRESS_SLOPE 16.75 173 | #define SENSOR_COMP_CONST_PRESS_OFFSET -248 174 | #define SENSOR_COMP_CONST_TEMP -55 175 | #define SENSOR_COMP_CONST_VOLT 122 176 | 177 | #define SENSOR_FLAG_UNDERVOLTAGE = 0x20 178 | 179 | ``` 180 | 🎉 181 | 182 | ## Device IDs 183 | The IDs lasered on the devices do not not seem to relate to any hardware ID, BT-MAC, etc. of the nRF. The IDs can be found in a storage partition of the flash though. As there is no official method to change them, these might be just random hardcoded IDs. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /doc/stlink_bmdb_firmware/blackmagic_stlink_bootloader_bmdb_boot.hex: -------------------------------------------------------------------------------- 1 | :020000040800F2 2 | :1000000000100020250C0008230C0008210C00081B 3 | :10001000210C0008210C0008210C00080000000041 4 | :10002000000000000000000000000000230C000899 5 | :10003000230C000800000000230C0008D90200086F 6 | :10004000210C0008210C0008210C0008210C0008DC 7 | :10005000210C0008210C0008210C0008210C0008CC 8 | :10006000210C0008210C0008210C0008210C0008BC 9 | :10007000210C0008210C0008210C0008210C0008AC 10 | :10008000210C0008210C0008210C0008210C00089C 11 | :10009000210C0008210C0008210C0008210C00088C 12 | :1000A000210C0008210C0008210C0008210C00087C 13 | :1000B000210C0008210C0008210C0008210C00086C 14 | :1000C000210C0008210C0008210C0008210C00085C 15 | :1000D000210C0008210C0008210C0008210C00084C 16 | :1000E000210C0008210C0008210C0008210C00083C 17 | :1000F000210C0008210C0008210C0008210C00082C 18 | :10010000210C0008210C0008210C0008210C00081B 19 | :10011000210C0008210C0008210C0008210C00080B 20 | :10012000210C0008210C0008210C0008210C0008FB 21 | :10013000210C0008210C0008210C0008210C0008EB 22 | :10014000210C0008210C0008210C0008210C0008DB 23 | :10015000F7B5524E746814F00F044ED140F20230DD 24 | :1001600000F019FD40F2033000F015FD4FF441702E 25 | :1001700000F011FD40F2973000F00DFD40F2172025 26 | :1001800000F016FD464F4FF4407000F004FD40F2C1 27 | :10019000862000F000FD4FF4C0430222214638467D 28 | :1001A00000F044FB4FF4C0433E4D3B6128464FF402 29 | :1001B00000630222214600F039FB4FF40063AB825A 30 | :1001C00025460094009B632B1CD9374F0DB3D7F8FD 31 | :1001D000083C13F4006F40F2011318BF1C4614F0E2 32 | :1001E000FE0F07D14FF48053304801220221838251 33 | :1001F00000F01CFB204603B0F0BD736823F003033E 34 | :100200007360A7E7BD68009BC5F3403501330093D9 35 | :10021000D8E74FF4407000F0BEFC7B68234C43F0FD 36 | :1002200000737B6002224FF400432946204600F011 37 | :10023000FDFA4FF40043A3820195019B632B23D960 38 | :1002400055B34FF40045174F4FF4004301220221EC 39 | :10025000384600F0EBFA2B46012202211348BD82FA 40 | :1002600000F0E4FAA58202247368022223F070638E 41 | :1002700073607368032143F0C06373600B484FF4ED 42 | :10028000807300F0D3FAAAE7D7F80858019BC5F3AA 43 | :10029000C03501330193D0E70124E5E700100240A7 44 | :1002A00000100140000C0140000001400008014026 45 | :1002B00008B5022300211A46044800F0B7FA4FF0AF 46 | :1002C000E0230022C3F8082D08BD00BF000801404C 47 | :1002D00008B500F07FFC704738B50F4C2268A38842 48 | :1002E0002AB91946BDE838400C4800F04BBCA2685A 49 | :1002F00012F001020BD0094D00220221284600F025 50 | :1003000095FAA388AB82A3680133A36038BD114678 51 | :10031000024800F08BFAF6E7481000200008014080 52 | :1003200038B5FFF715FF00280BBF4FF480724FF46C 53 | :100330000072022301231D4C1D4D2060A280D5F8C0 54 | :100340000028E380520611D502220021194800F04E 55 | :100350006DFA42F21073E288C5F8102C013BD5F813 56 | :10036000081CFBD10A4201D000F055F8002000F033 57 | :1003700051F8114800F07EFB002000F039FC0F48D6 58 | :1003800000F030FC00F03EFC00F043FC23680C4819 59 | :10039000012B82BF4FF40042054BC3F8102800F038 60 | :1003A00041F900F095F9002038BD00BF4810002049 61 | :1003B00000000140000C0140C41D0008A0BB0D005E 62 | :1003C000F01D000838B5064D20F47F742B6824F02A 63 | :1003D0000304A34203D0204600F0EEF92C6038BDA0 64 | :1003E00004100020F8B506460F4615460024AC421E 65 | :1003F00003D3BDE8F840FFF76EBF6308A01937F8D4 66 | :10040000131000F0A7F90234F1E741280CBF1420C3 67 | :100410001B20704770470A4B0A4A1B6819680A403C 68 | :10042000B2F1005F0AD14FF0E022C3F31400C2F82A 69 | :10043000080D81F30888D3F804F0FEE7704700BF89 70 | :10044000001000200000FE2F014BD3F80100704780 71 | :10045000541000202DE9F0411D460B780E4603F0A4 72 | :100460007F03212B63D14B781468013B052B5ED8A9 73 | :10047000DFE803F0033D5F348E3BDFF820810DB1F0 74 | :100480002B882BB9062388F800300120BDE8F081C5 75 | :10049000CA788B78424F43EA0223A7F808342A88A7 76 | :1004A00021463846A7F8002401F0E4FB73889BB985 77 | :1004B0003B78212B10D1FFF7C7FF3A4B1B688342D3 78 | :1004C00003D8D7F80C34834204D8012387F81034BA 79 | :1004D0000A23D8E7C7F804040323D4E72F4B1A787C 80 | :1004E0000A2AD2D102221A70CFE72C4BFAE72B4A04 81 | :1004F0001178092917D84FF40973CB40DB0712D5BF 82 | :100500007088274B81B2A3F80804092001291070D4 83 | :10051000BBD9D3F80434023920462A8803EB812161 84 | :1005200001F0A8FBB1E70122002100F021FC00202E 85 | :10053000ACE71A4E3378062B22D00A2B23D0032B9C 86 | :1005400011D0002000232370030AA3703378607059 87 | :100550002371002363710623000CE070124A2B8084 88 | :10056000069B1A6091E704233370FFF76DFF0C4A76 89 | :1005700001461778B2F808243846FFF746FFE1E74E 90 | :1005800007233370DDE7064B002093F81034DAE7D9 91 | :10059000024B1B78237001232B8076E70810002084 92 | :1005A0005410002000100020AD05000810B51A4CB2 93 | :1005B0002378042B02D0072B2AD010BD00F0C8FAF4 94 | :1005C0001649B1F80804C8B9FFF73EFF0A78412A76 95 | :1005D0000FD1134A1268904203D3D1F80C249042F1 96 | :1005E00005D30A232370BDE8104000F0BDBAFFF721 97 | :1005F000E9FE00F0B9FA05232370DEE7D1F80434F0 98 | :100600000238B1F8002403EB8020FFF7EBFEF0E79F 99 | :10061000BDE81040FFF75CBE081000205410002019 100 | :100620000010002030B585B0044600F081FA402863 101 | :10063000034614BF0346802340F2E632A3F10801CB 102 | :1006400091421F4828D9392280F8272080F8282095 103 | :1006500080F829201B4D9B0203F10063C5F80C3480 104 | :1006600000F040F84FF48063029305F211430193C8 105 | :1006700004230093144B204603F1100203F11D01E3 106 | :1006800000F01AFB7F222121104BC5F8140805B099 107 | :10069000BDE8304000F016BC0A25B1FBF5F205FBC1 108 | :1006A0001211303180F82910B2FBF5F105FB11224F 109 | :1006B0003031303280F8282080F82710CAE700BF98 110 | :1006C0000810002054100020F41C000855040008F5 111 | :1006D00008B5034CD4F8140800F018FBFAE700BF83 112 | :1006E00054100020104B10B5D3F8E827D3F8EC17BE 113 | :1006F000D3F8F0370A4400210C481A4422FA01F3D7 114 | :1007000003F00F0303F13004392C88BF373301F1B4 115 | :10071000040194BF04700370202900F1FF30EDD173 116 | :100720000022034B1A7210BD00F0FF1F7318002047 117 | :100730006C18002038B5064DEC6800F0F9F9B0F5FA 118 | :10074000007F84BFEB6C1C4304F0350038BD00BF54 119 | :100750000020024038B504460D4600F00DFA00F0C6 120 | :10076000E7F9B0F5007F124B17D9124A944214D919 121 | :100770001A6D42F001021A65258000F0FDF900F0C3 122 | :10078000D7F9B0F5007F0A4B0CD90A4A944209D92F 123 | :100790001A6D22F001021A6538BD1A6942F0010291 124 | :1007A0001A61E9E71A6922F001021A61F4E700BF51 125 | :1007B00000200240FFFF070810B5044600F0DCF9F6 126 | :1007C00000F0B6F9B0F5007F164B1BD9164A9442DB 127 | :1007D00018D91A6D42F002021A655C651A6D42F072 128 | :1007E00040021A6500F0C8F900F0A2F9B0F5007FE8 129 | :1007F0000C4B11D90C4A94420ED91A6D22F0020208 130 | :100800001A6510BD1A6942F002021A615C611A6928 131 | :1008100042F040021A61E5E71A6922F002021A6109 132 | :10082000EFE700BF00200240FFFF07082DE9F0417D 133 | :100830004FF00F0C00250768466843FA05F4E407FB 134 | :100840001CD5072D94BFBE46B646ACB24FEA840411 135 | :100850008ABF203CA4B2A4B20CFA04F82EEA08081D 136 | :1008600004F1020E02FA0EFE01FA04F44EEA040448 137 | :1008700044EA0804072D94BF274626460135102D6B 138 | :10088000DBD107604660BDE8F0810000062824D86F 139 | :10089000DFE800F004090E13181D2000104B186843 140 | :1008A000C0F3406070470E4B1868C0F3C0607047DB 141 | :1008B0000B4B1868C0F340707047094B1868C0F3C1 142 | :1008C00040407047064B1868C0F340007047044B27 143 | :1008D000186AF9E7024B586AF6E70020704700BF34 144 | :1008E00000100240024608B51046FFF7CFFF00286F 145 | :1008F000FAD008BD06282AD8DFE800F0040A0F1451 146 | :10090000191E2400124A136843F080731360704765 147 | :100910000F4A136843F08063F8E70D4A136843F009 148 | :100920008053F3E70A4A136843F48033EEE7084A3A 149 | :10093000136843F00103E9E7054A136A43F0010332 150 | :1009400013627047024A536A43F0010353627047CF 151 | :1009500000100240034A536823F00303034353602B 152 | :10096000704700BF00100240034A536823F470131D 153 | :1009700043EA80435360704700100240034AD36A41 154 | :1009800023F4706343EA0023D362704700100240EF 155 | :10099000034AD36A23F4704343EA0033D3627047B7 156 | :1009A00000100240034A536823F4803343EA0043B3 157 | :1009B0005360704700100240034A536823F4003329 158 | :1009C00043EA40435360704700100240034A5368B3 159 | :1009D00023F4404343EA80335360704700100240E1 160 | :1009E000034A536823F4605343EAC02353607047BB 161 | :1009F00000100240034A536823F4E06343EA0023F3 162 | :100A00005360704700100240034A536823F0F0031C 163 | :100A100043EA00135360704700100240044B5A68C9 164 | :100A200018B142F480025A60704722F48002FAE75B 165 | :100A300000100240034AD36A23F00F030343D3623A 166 | :100A4000704700BF00100240034AD36A23F0F0034E 167 | :100A500043EA0013D362704700100240044BDA6A85 168 | :100A600018B142F48032DA62704722F48032FAE739 169 | :100A70000010024010B504464378012B0CBF032040 170 | :100A80000420FFF737FFFFF72DFFA078FFF7BCFF2B 171 | :100A9000E078FFF7AFFF2079FFF7A2FF6079FFF75B 172 | :100AA00095FF207BFFF7BAFFA07900F047F8207888 173 | :100AB000FFF75AFF6078FFF775FFE07901282DD91D 174 | :100AC000FFF7B8FF207A08B1FFF7C8FF607A08B1D6 175 | :100AD000FFF7BAFFA07A30B1FFF750FF0120FFF710 176 | :100AE00009FFFFF7FFFEE07A30B1FFF751FF022068 177 | :100AF000FFF700FFFFF7F6FE0020FFF7FBFEFFF712 178 | :100B0000F1FE0220FFF726FF2269064B1A60626998 179 | :100B1000054B1A60A269054B1A6010BDFFF74CFF28 180 | :100B2000D0E700BF3C1000204410002040100020FF 181 | :100B3000014BB3F8E007704700F0FF1F034A13684A 182 | :100B400023F0070303431360704700BF00200240F7 183 | :100B5000034B044A5A6002F188325A60704700BF62 184 | :100B60000020024023016745024A136943F08003D5 185 | :100B7000136170470020024008B5FFF7DBFDC30793 186 | :100B8000FBD408BDC36801EA030221EA030141EA7C 187 | :100B90000241016170470122430903F1804303F5DB 188 | :100BA0000433196800F01F0082400A431A6070473E 189 | :100BB0000122430903F1804303F50433196800F06F 190 | :100BC0001F00824011431960196821EA02021A606D 191 | :100BD000704700004FF0E023014AC3F80C2DFEE7F8 192 | :100BE0000400FA054FF0E02320F07F405861704781 193 | :100BF0004FF0E022136900F0040023F004030343E4 194 | :100C0000136170474FF0E022136943F00203136150 195 | :100C100070474FF0E022136943F0010313617047FE 196 | :100C2000FEE7704738B51A4A1A4B1B498B421AD354 197 | :100C300000211A4A93421BD34FF0E022D2F8143D10 198 | :100C4000174C43F40073174DC2F8143DAC4212D355 199 | :100C5000154C164DAC4212D3FFF762FB144C154DE8 200 | :100C6000AC4210D338BD52F8040B43F8040BDDE757 201 | :100C700043F8041BDEE754F8043B9847E6E754F8D2 202 | :100C8000043B9847E6E754F8043B9847E8E700BF81 203 | :100C90002C1E00080010002048100020941900208D 204 | :100CA000241E0008241E0008241E0008241E00081C 205 | :100CB000241E0008241E00082DE9F0411E4603688A 206 | :100CC0001746054688469847069B079A0361002306 207 | :100CD0004261BDF82020C0E900870283084AC0E9CC 208 | :100CE0003C33C0F88020074AC0E93633C267064A61 209 | :100CF000C0E93833C0F8EC50C6608267BDE8F081C7 210 | :100D0000F10E00089D0F000821100008D0F8EC300B 211 | :100D10005B6A184710B4D0F8EC40A468A44610BC35 212 | :100D2000604737B50025044603684583DB792A46CA 213 | :100D300029460095FFF7EEFFD4F8EC3029465B68B2 214 | :100D400020469847236A1BB103B0BDE830401847DE 215 | :100D500003B030BD10B4D0F8EC40E469A44610BC38 216 | :100D6000604710B4D0F8EC40246AA44610BC604739 217 | :100D7000D0F8EC301B691847D0F8EC309B69184765 218 | :100D8000D0F8EC305B69184710B50268B0F8403015 219 | :100D9000D17904468B42C26B11D90B460021FFF773 220 | :100DA000D9FF022384F830302368DA79E36B1344E7 221 | :100DB000E363B4F840309B1AA4F8403010BD002122 222 | :100DC000FFF7C8FF94F84830C3F1030384F83030CC 223 | :100DD000002384F84830A4F84030E363EEE72DE9BF 224 | :100DE000F74F04460D46064600F1200900F1400A7F 225 | :100DF00000F13C0700F14408D6F858B0BBF1000FF1 226 | :100E00000AD02B7896F85D1096F85C200B409A4239 227 | :100E100011D008364E45EFD1A36804F1400804F123 228 | :100E20003C07A3B943463A462946204603B0BDE8ED 229 | :100E3000F04F00F031BC53463A4629462046CDF8E3 230 | :100E40000080D8470128E4D803B0BDE8F08FE66CF5 231 | :100E5000002EE7D0297801F07F014029E2D14346F6 232 | :100E60003A4629462046B0470128DBD8ECE710B5C2 233 | :100E7000012200210446FFF77BFF002384F8303075 234 | :100E800010BD70B50368B0F84020448FDB79A41A18 235 | :100E90009C42A8BF1C46C16BA6B20A44334600213F 236 | :100EA0000546FFF75EFF864205D02846FFF7DFFFC5 237 | :100EB0004FF0FF3070BDB5F84030A0B21E44A5F829 238 | :100EC0004060F7E770B50446002500F1580656F873 239 | :100ED000350028B10135042DF9D14FF0FF3070BD38 240 | :100EE00004EBC50484F85C1084F85D20A365F6E784 241 | :100EF00038B500250446012229464564FFF740FF26 242 | :100F0000638F9AB21AB194F93410A94230DA626947 243 | :100F10002046E263A4F8403004F13401FFF75FFF9C 244 | :100F200008B3638FB3B1B4F8402021689342C97904 245 | :100F30000ED932B1B2FBF1F301FB1322B2FA82F205 246 | :100F40005209204684F84820BDE83840FFF71CBF0E 247 | :100F50000022F6E71A4619462046FFF7FBFE042357 248 | :100F600084F8303038BD2046BDE83840FFF77FBFF9 249 | :100F7000238B9342F7D36369A4F84050E36323685B 250 | :100F80002946DB79204693422CBF0623052384F8AB 251 | :100F900030302A46BDE83840FFF7F2BE10B590F871 252 | :100FA00030300446062B13D0072B28D0052B22D136 253 | :100FB000FFF767FF00280ADBB4F84020638F9B1A15 254 | :100FC0002268D279934202DC062384F8303010BDC7 255 | :100FD000FFF757FF0028FADB204604F13401FFF742 256 | :100FE000FEFE38B1002320461A461946FFF7B2FE2E 257 | :100FF0000423EAE72046BDE81040FFF738BF00238E 258 | :101000001A461946FFF7ADFE002384F83030636CB2 259 | :101010001BB1204604F13401984700236364D6E7EE 260 | :1010200010B590F830300446032B07D0042B0ED0B7 261 | :10103000022B23D1BDE81040FFF7A6BE07230022F4 262 | :1010400080F830301146BDE81040FFF799BE436C80 263 | :1010500013B100F13401984794F8343053B994F83F 264 | :101060003530052B06D1E18ED4F8EC3020465B6894 265 | :10107000C9B29847002384F8303010BDBDE8104055 266 | :10108000FFF7F5BE08B50B7873B949887F2909D8F1 267 | :10109000D0F8EC30C9B2817693F830200AB15B68A1 268 | :1010A00098470123184608BD0023FBE7F8B50D4615 269 | :1010B0004988044679B103680D26587C002398427C 270 | :1010C00001D80020F8BD626806FB032252798A42EB 271 | :1010D00003D00133F3E74FF0FF330133DBB2E376A4 272 | :1010E0004BB9D4F8EC302046DB689847D4F8D830B8 273 | :1010F0009BB90120E6E70D22534362680D3B1A4479 274 | :101100000023184611799942EBD9D2F809101E0133 275 | :10111000715801B108700133F4E7002304F1D406DB 276 | :10112000A3652366A366236704F1E40756F8043F2A 277 | :1011300013B1204669889847BE42F7D1D9E719888C 278 | :10114000012984BF01211980C37E11682BB1426837 279 | :101150000D2000FB032313F8083C01200B7070479F 280 | :1011600070B51C460D23C27E5A4343680D3A1344A2 281 | :101170001D798A88954219D9D3F80950160105EBD3 282 | :1011800002124B8812799A4210D9AA5952B113709F 283 | :10119000D0F8E83013B14A888988984700230120A5 284 | :1011A000238070BD002BF3D01046FAE70020F8E74B 285 | :1011B00030B50D25C47E40686C430D3C20448D88BD 286 | :1011C0000479A5420CD201241C808B88D0F8090038 287 | :1011D0001B011B5803B11B7801201268137030BD2E 288 | :1011E0000020FCE719880120022984BF0221198010 289 | :1011F000002311680B7012685370704710B51446C5 290 | :101200001A88022A84BF02221A808988C9B2FFF78D 291 | :10121000B3FD003818BF012000222368187023682E 292 | :1012200001205A7010BD08B589880122C9B2FFF7A4 293 | :101230009FFD012008BD08B589880022C9B2FFF7CB 294 | :1012400097FD012008BD00002DE9F04F9B464B881B 295 | :101250001746DEB21B0A032B354689B000F05581D4 296 | :1012600005D8012B2AD0022B36D0002031E00F2BDD 297 | :10127000FBD18468002CF8D0002EF6D1D0F8149061 298 | :10128000BBF80070C2F8009094F800802146B84581 299 | :1012900028BFB8464846424600F0ECFC2378A7EB4E 300 | :1012A0000807BFB201936B0122790293EBB29A4215 301 | :1012B00000F2E380019BA9F80230EAE003681360C2 302 | :1012C0001B78BBF80020934228BF1346ABF80030D0 303 | :1012D000012009B0BDE8F08F4369476813600393AC 304 | :1012E0000D235E43BBF80040BD5DBB19A54228BF7E 305 | :1012F000254619462A460398059300F0BBFC0022B8 306 | :10130000039B641BBE5D2B44A4B20292059A117923 307 | :10131000BDF80820914204D8039B5E80ABF80050D2 308 | :10132000D6E7029A029912010692059AD2F809208C 309 | :1013300002EB0112D2F80880B8F1000F13D098F830 310 | :1013400000701846A74228BF274641463A4600F09B 311 | :1013500091FC034698F80020E41B3B4432442F44A0 312 | :10136000A4B2BDB296B200220492059A0699D2F8B0 313 | :1013700009200A441079BDF81010884202D8029A58 314 | :101380000132C2E715200499D2F80C8000FB01F766 315 | :1013900018F8079008EB0702A14528BFA146184698 316 | :1013A000019208EB07014A4600F064FC034618F876 317 | :1013B0000720A4EB0904324496B2019A4B44D2F8B8 318 | :1013C0000D10A944A4B21FFA89F5A9B1D2F8118071 319 | :1013D0001846A045A8BFA0461FFA88F73A4600F075 320 | :1013E00049FC0346019AE41BD2F811203B443244E5 321 | :1013F0002F44A4B2BDB296B20027019A1179BAB2B5 322 | :10140000914202D8049A0132AEE7019AC7EB07195C 323 | :10141000D2F809A018461AF809200AEB0908A242D6 324 | :1014200028BF22464146079200F024FC0346079A53 325 | :10143000D8F80710A41A13442A4495B21AF80920C0 326 | :10144000A4B2324496B2B1B1D8F80BA01846A24566 327 | :10145000A8BFA2461FFA8AF94A4600F00BFC0346D1 328 | :10146000D8F80B20A4EB09044B443244A944A4B29D 329 | :101470001FFA89F596B20137BFE7D4F80530029A12 330 | :10148000039303EB020A9AF80230052B08D04FF0C1 331 | :101490000008B8F1000018BF0120ABF8008018E781 332 | :1014A000142F3E4628BF142609EB0803184632467F 333 | :1014B0005146049300F0DEFB102243490AF1040078 334 | :1014C00000F0C8FB049BF0B9BA1B92B2082A28BFEF 335 | :1014D000082204929819DAF8141000F0CBFB049A51 336 | :1014E0001644B6B2DDE902239B5C019A1A4493B21A 337 | :1014F00001930EB9002FCAD1BF1B4644BFB21FFAD9 338 | :1015000086F80135CFE60026ECE746697DB9092368 339 | :10151000B3700423F3703370BBF80030042B28BF82 340 | :101520000423ABF80030032373703E60D0E6D0F89C 341 | :10153000F030AB421CD1D0F8F440204600F0A8FBBC 342 | :1015400001304000C0B23070BBF80030834228BF89 343 | :101550000346ABF800300023BBF800205208013AE4 344 | :101560009A42E0DDE15C06EB430251800133F3E790 345 | :10157000C468013D002C3FF478AE0369AB427FF7AD 346 | :1015800074AE40F209438A889A427FF46EAE54F8F2 347 | :101590002540204600F07CFB01304000C0B2307096 348 | :1015A000BBF80030834228BF0346ABF8003000236D 349 | :1015B000BBF800205208013A9A42B4DDE15C06EB28 350 | :1015C000430251800133F3E7E01D00081988012030 351 | :1015D000022984BF02211980002311680B70126850 352 | :1015E0005370704730B44C78092C12D8DFE804F0FF 353 | :1015F00005111111110D0F11090B074CA44630BC38 354 | :101600006047064CFAE7064CF8E7064CF6E7064C4E 355 | :10161000F4E7002030BC7047E51100083F110008D6 356 | :10162000AD100008851000084912000810B44C786D 357 | :101630000A2C06D00B2C06D03CB9054CA44610BC95 358 | :101640006047044CFAE7044CF8E7002010BC7047F0 359 | :10165000CD150008B11100086111000810B44C78D4 360 | :10166000012C05D0032C08D074B1002010BC7047A9 361 | :101670004C88002CF9D1054C03E04C88002CF4D1A7 362 | :10168000034CA44610BC6047024CFAE7371200082E 363 | :1016900027120008FD11000810B40C7814F0600F38 364 | :1016A0000FD104F01F04012C05D0022C06D044B940 365 | :1016B00010BCFFF797BF10BCFFF7B8BF10BCFFF717 366 | :1016C000CDBF002010BC704708B540F29730FFF73F 367 | :1016D00062FA0022064B0748C3F8402CC3F8502C8E 368 | :1016E000C3F8442C4FF41C42C3F8402C08BD00BF83 369 | :1016F00000500040781800200132521001EB4202E5 370 | :10170000914200D1704731F8023B40F8043BF7E7C3 371 | :10171000034670B502F00104520801EB8205A942AC 372 | :1017200004D114B10B7800F8123070BD31F8046B9D 373 | :1017300023F8026BF3E70000024B41F08001C3F88D 374 | :101740004C1C7047005000403E2A14D9013AC2F3A5 375 | :10175000441302F4787202F1200043F02003094A96 376 | :101760009B02D2F8502C9BB292B202EBC101064A06 377 | :101770004900535070470132530822F0010290B2E1 378 | :10178000EDE700BF005000400C6000402DE9F34140 379 | :1017900006464FF0C060444C01F07F08CDE90040A0 380 | :1017A0004FEA880404F1804404F5B844206802AD8F 381 | :1017B00020F4E04020F07F0040EA080080B2206082 382 | :1017C000206805EB420232F8082C20F4EC4020F0AF 383 | :1017D0007000104349B280B20029089F20604FEA90 384 | :1017E000C8022ADBB8F1000F27D03049B58BD1F8F9 385 | :1017F000501C304689B20A442D4952008D501A4679 386 | :101800004146FFF7A1FF002F47D12368054423F489 387 | :10181000415323F030031B041B0C2360236823F483 388 | :10182000804323F070039BB283F4405343F400439E 389 | :1018300043F080032360B5832CE01C48B6F81CC03D 390 | :10184000D0F8500C80B21044400000F1804000F508 391 | :10185000C040C0F800C01FB10C2000FB08608767C3 392 | :1018600020689C4420F4E04020F030000004000C8C 393 | :101870002060256825F4E04525F04005ADB285F0EF 394 | :10188000200545F4004545F080052560A6F81CC0FC 395 | :101890000029AAD002B0BDE8F0810C2303FB086840 396 | :1018A000C8F87C70B1E700BF00020004005000409F 397 | :1018B0000860004048F2800170B50B4A0B4E0C4D99 398 | :1018C0000C4C136833400B439BB2136013682B40DE 399 | :1018D0000B439BB242F8043BA242F2D10368DB798E 400 | :1018E00020335B00838370BD045C0040BF8FFFFF2B 401 | :1018F0008FBFFFFF205C004010B599BB002A0CBFD2 402 | :10190000202410242A48D0F8003C23F4E04323F09C 403 | :1019100040039BB2634043F4004343F08003C0F8AC 404 | :10192000003C890001F1804101F5B8413AB90B68EA 405 | :1019300023F4415323F030031B041B0C0B60002ADB 406 | :101940000CBF4FF440524FF480520B6823F4804395 407 | :1019500023F070039BB2534043F4004343F08003F1 408 | :101960000B6023E00B06DCD5002A0CBF20221022DE 409 | :1019700001F07F034FEA830303F1804303F5B8438B 410 | :10198000196821F4E04121F0400189B282EA0102A4 411 | :1019900042F4004242F080021A6007D11A6822F431 412 | :1019A000E04222F030021204120C1A6010BD00BF97 413 | :1019B000005000400B060ED501F07F01890001F1B7 414 | :1019C000804101F5B841086800F03000102814BFCC 415 | :1019D000002001207047890001F1804101F5B841E4 416 | :1019E000086800F44050B0F5805FF0E70B0612D4B1 417 | :1019F0000B4B0C485A54890001F1804101F5B84164 418 | :101A00000B6803409BB23AB183F4005343F40043A4 419 | :101A100043F080030B60704783F44053F6E700BF48 420 | :101A20008C1900208FBFFFFF08462DE9F0411E46AC 421 | :101A300000F07F039D0005F1804505F5B84511468E 422 | :101A40002A6802F03002302A2CD0DFF85C8032465F 423 | :101A5000D8F8504CDF00A4B204EBC304640004F1D6 424 | :101A6000804404F5C0442088400000F1804000F527 425 | :101A7000C040FFF741FED8F8503C9BB21F440B4BCF 426 | :101A80007F00DE51296821F4E04121F0400189B254 427 | :101A900081F0300141F4004141F08001296030467D 428 | :101AA000BDE8F0810026FAE70050004004600040E5 429 | :101AB000F8B58D0005F1804505F5B8451C462B6845 430 | :101AC0000E4603F44053B3F5405F10463FD0214F1C 431 | :101AD000CA00D7F8503C9BB203EBC1031E495B0020 432 | :101AE000CB58C3F30903A34207DAD7F8503C9BB2A3 433 | :101AF00013445B00CC58C4F30904164BD3F8503C94 434 | :101B00009BB21A44154B5200995A2246490001F1E2 435 | :101B1000804101F5C041FFF7FBFD2B6823F0F00386 436 | :101B20001B051B0D43F080032B600D4B9B5D63B9C0 437 | :101B3000296821F4804121F0700189B281F440517B 438 | :101B400041F4004141F0800129602046F8BD0024A5 439 | :101B5000FBE700BF005000400C6000400860004000 440 | :101B60008C190020F8B53A4B0446D3F8446C77053D 441 | :101B700009D54FF6FF32C3F8442C40238383BDE8D8 442 | :101B8000F840FFF7CEB835041ED506F00F07BD00AC 443 | :101B900005F1804505F5B84516F010022B683CD0DC 444 | :101BA000190542D500F1340208233946FFF780FFBA 445 | :101BB000022207EB470313441E3354F82330002B53 446 | :101BC00035D0394620469847320507D54FF2FF7287 447 | :101BD0001F4BC3F8442C636A03B19847F30407D53D 448 | :101BE0004EF6FF721A4BC3F8442CA36A03B1984710 449 | :101BF00016F4007FE36A164D05D04FF6FF52C5F884 450 | :101C0000442CEBB19847E36AD3B1114AD2F8403C77 451 | :101C100043F40073C2F8403CF8BD23F0F0031B0509 452 | :101C20001B0D43F400432B60C3E70122C1E72B687F 453 | :101C300023F0F0031B051B0D43F080032B60C3E76B 454 | :101C4000D5F8403C23F40073C5F8403CE4E700BFFE 455 | :101C50000050004010B501390244904201D10020EB 456 | :101C600005E0037811F8014FA34201D0181B10BD05 457 | :101C70000130F2E70A44914200F1FF3300D170478E 458 | :101C800010B511F8014B914203F8014FF9D110BD85 459 | :101C9000034613F8012B002AFBD1181A01387047AC 460 | :101CA000426C61636B204D616769632044656275B6 461 | :101CB0006700426C61636B204D61676963205072FD 462 | :101CC0006F626520444655202853542D4C696E6B35 463 | :101CD0002F7632292076312E31302E302D3132328E 464 | :101CE000342D6766313231326338382D646972744D 465 | :101CF00079000000A01C0008B21C00086C1800202D 466 | :101D00000910002009020000010100C032241D005A 467 | :101D1000081201000200000040501D176000010180 468 | :101D200002030100000000000100000000000000AC 469 | :101D3000341D00080904000000FE01020400000038 470 | :101D400000491D00080900000009210BFF000004E4 471 | :101D50001A0100000401000400020200000000005B 472 | :101D60000000000000A24A040051250200A24A041B 473 | :101D70000701000400020201000000000000000052 474 | :101D800000A24A040051250200A24A0407010004EF 475 | :101D900000020204010406000000000000A24A0440 476 | :101DA0000051250200A24A040101000000000000C9 477 | :101DB000000000000000000000366E0100366E01D9 478 | :101DC00000366E010701000400030200000000005D 479 | :101DD0000000000000A24A040051250200A24A04AB 480 | :101DE000DF60DDD88945C74C9CD2659D9E648A9F83 481 | :101DF000C9160008391700088D170008B518000823 482 | :101E0000F9180008ED190008B5190008291A00088A 483 | :101E1000B11A0008651B0008000000000000000067 484 | :041E200000000000BE 485 | :081E24006CFEFF7F01000000CD 486 | :101E2C0000200008FFFFFFFF0240496E7465726ED0 487 | :101E3C00616C20466C6173682020202F307830381C 488 | :101E4C003030303030302F382A3030314B612C303C 489 | :101E5C0030302A3030314B670000000000127A001D 490 | :081E6C0000127A0000127A0056 491 | :0400000508000C25BE 492 | :00000001FF 493 | --------------------------------------------------------------------------------