Compare commits
3 Commits
ef86085343
...
main
Author | SHA256 | Date | |
---|---|---|---|
f40f533f02 | |||
16913b5c7f | |||
10609e169f |
4
.gitignore
vendored
4
.gitignore
vendored
@ -256,4 +256,6 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
pyupdi-env/
|
pyupdi-env/
|
||||||
|
|
||||||
|
sbom.spdx.json
|
@ -1,37 +1,79 @@
|
|||||||
cmake_minimum_required(VERSION 3.13)
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
|
||||||
# Project
|
|
||||||
project(lezyne-rear-light-firmware C)
|
project(lezyne-rear-light-firmware C)
|
||||||
|
|
||||||
# MCU and clock
|
# MCU and clock
|
||||||
set(MCU attiny202)
|
set(MCU attiny202)
|
||||||
set(F_CPU 5000000UL) # 5 MHz
|
set(F_CPU 5000000UL)
|
||||||
|
|
||||||
# Toolchain executables
|
# Toolchain
|
||||||
set(CMAKE_SYSTEM_NAME Generic)
|
set(CMAKE_SYSTEM_NAME Generic)
|
||||||
set(CMAKE_C_COMPILER avr-gcc)
|
set(CMAKE_C_COMPILER avr-gcc)
|
||||||
set(OBJCOPY avr-objcopy)
|
set(OBJCOPY avr-objcopy)
|
||||||
|
|
||||||
# Compiler flags: optimize, warnings, treat warnings as errors
|
|
||||||
set(CMAKE_C_FLAGS "-mmcu=${MCU} -DF_CPU=${F_CPU} -Os -Wall -Werror")
|
|
||||||
|
|
||||||
# Sources
|
# Sources
|
||||||
add_executable(main.elf main.c)
|
add_executable(main.elf main.c)
|
||||||
|
|
||||||
# HEX file
|
# Compiler and linker flags
|
||||||
add_custom_command(
|
target_compile_options(main.elf PRIVATE -mmcu=${MCU} -DF_CPU=${F_CPU} -Os -Wall -Werror)
|
||||||
OUTPUT main.hex
|
set_target_properties(main.elf PROPERTIES LINK_FLAGS "-mmcu=${MCU}")
|
||||||
|
|
||||||
|
# Create HEX and BIN after build
|
||||||
|
add_custom_command(TARGET main.elf POST_BUILD
|
||||||
COMMAND ${OBJCOPY} -O ihex -R .eeprom main.elf main.hex
|
COMMAND ${OBJCOPY} -O ihex -R .eeprom main.elf main.hex
|
||||||
DEPENDS main.elf
|
|
||||||
)
|
|
||||||
|
|
||||||
# BIN file
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT main.bin
|
|
||||||
COMMAND ${OBJCOPY} -O binary -R .eeprom main.elf main.bin
|
COMMAND ${OBJCOPY} -O binary -R .eeprom main.elf main.bin
|
||||||
DEPENDS main.elf
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Targets
|
# Optional: show size
|
||||||
add_custom_target(hex ALL DEPENDS main.hex)
|
find_program(SIZE_TOOL avr-size)
|
||||||
add_custom_target(bin ALL DEPENDS main.bin)
|
if(SIZE_TOOL)
|
||||||
|
add_custom_command(TARGET main.elf POST_BUILD
|
||||||
|
COMMAND ${SIZE_TOOL} --mcu=${MCU} --format=avr main.elf
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Flash target using pymcuprog
|
||||||
|
find_program(PYMCUPROG pymcuprog)
|
||||||
|
set(UPDI_PORT "/dev/ttyUSB0" CACHE STRING "Serial port for UPDI programming")
|
||||||
|
|
||||||
|
if(PYMCUPROG)
|
||||||
|
add_custom_target(flash
|
||||||
|
COMMAND ${PYMCUPROG} -t uart -u ${UPDI_PORT} -d ${MCU} write -f main.hex
|
||||||
|
DEPENDS main.hex
|
||||||
|
COMMENT "Flashing ${MCU} with pymcuprog..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(WARNING "pymcuprog not found in PATH. 'make flash' will not be available.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- SBOM Generation (SPDX JSON) ---
|
||||||
|
find_package(Git REQUIRED)
|
||||||
|
|
||||||
|
# Generate current timestamp in ISO 8601 format (UTC)
|
||||||
|
string(TIMESTAMP CMAKE_TIMESTAMP "%Y-%m-%dT%H:%M:%SZ" UTC)
|
||||||
|
|
||||||
|
# Get current git hash
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get avr-gcc version
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${CMAKE_C_COMPILER} --version
|
||||||
|
OUTPUT_VARIABLE AVR_GCC_VERSION
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Where to write SBOM
|
||||||
|
set(SBOM_FILE ${CMAKE_SOURCE_DIR}/sbom.spdx.json)
|
||||||
|
|
||||||
|
# Generate from template
|
||||||
|
configure_file(${CMAKE_SOURCE_DIR}/sbom.template.json ${SBOM_FILE} @ONLY)
|
||||||
|
|
||||||
|
# Always regenerate on build
|
||||||
|
add_custom_target(sbom ALL
|
||||||
|
DEPENDS ${SBOM_FILE}
|
||||||
|
COMMENT "Generating SPDX SBOM..."
|
||||||
|
)
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 localhorst
|
Copyright (c) 2025 Hendrik Schutter
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
12
README.md
12
README.md
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
**🚧 Work in progress. No complete firmware yet 🚧**
|
**🚧 Work in progress. No complete firmware yet 🚧**
|
||||||
|
|
||||||
|
🚀 For pre-build binaries go to [Releases](https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware/releases).
|
||||||
|
|
||||||
Open firmware for Lezyne bike rear lights based on ATTINY202
|
Open firmware for Lezyne bike rear lights based on ATTINY202
|
||||||
|
|
||||||
This repository contains a minimal firmware as a **C project** for the ATtiny202 microcontroller using **GCC**, **CMake**, and **VS Code**. It also includes instructions for programming the chip using an **FT232 USB-UART adapter** via the UPDI interface with `pymcuprog`.
|
This repository contains a minimal firmware as a **C project** for the ATtiny202 microcontroller using **GCC**, **CMake**, and **VS Code**. It also includes instructions for programming the chip using an **FT232 USB-UART adapter** via the UPDI interface with `pymcuprog`.
|
||||||
@ -105,11 +107,11 @@ Hardware: FT232 USB-UART adapter connected to UPDI with a 4.7 kΩ resistor.
|
|||||||
|
|
||||||
| Signal | PCB Usage | LOW | HIGH |
|
| Signal | PCB Usage | LOW | HIGH |
|
||||||
|--------|-------------------|--------------------------|---------------------------|
|
|--------|-------------------|--------------------------|---------------------------|
|
||||||
| PA6 | Green LED | LED **ON** | LED OFF |
|
| PA6 | Green LED | LED **ON** | LED **OFF** |
|
||||||
| PA7 | Red LED | LED **ON** | LED OFF |
|
| PA7 | Red LED | LED **ON** | LED **OFF** |
|
||||||
| PA1 | LED 1–2 | LED(s) **ON** | LED(s) OFF |
|
| PA1 | LED 1–2 | LED(s) **OFF** | LED(s) **ON** |
|
||||||
| PA2 | LED 3–6 | LED(s) **ON** | LED(s) OFF |
|
| PA2 | LED 3–6 | LED(s) **OFF** | LED(s) **ON** |
|
||||||
| PA3 | LED 7–8 | LED(s) **ON** | LED(s) OFF |
|
| PA3 | LED 7–8 | LED(s) **OFF** | LED(s) **ON** |
|
||||||
| PA0 | Activation Button | Button **pressed** (GND) | Button released (pull-up) |
|
| PA0 | Activation Button | Button **pressed** (GND) | Button released (pull-up) |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
100
main.c
100
main.c
@ -9,62 +9,54 @@
|
|||||||
#include <avr/io.h>
|
#include <avr/io.h>
|
||||||
#include <util/delay.h>
|
#include <util/delay.h>
|
||||||
|
|
||||||
/** @defgroup LED_Masks LED bitmasks for PORTA
|
#define BUTTON_PIN_MASK 0x01 // PA0 TODO: using RESET/UPDI pin
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
#define BUTTON_PIN_MASK 0x01 // PA0
|
|
||||||
#define PA1_SET_MASK 0x02 ///< LED 1–2
|
#define PA1_SET_MASK 0x02 ///< LED 1–2
|
||||||
#define PA2_SET_MASK 0x04 ///< LED 3–6
|
#define PA2_SET_MASK 0x04 ///< LED 3–6
|
||||||
#define PA3_SET_MASK 0x08 ///< LED 7–8
|
#define PA3_SET_MASK 0x08 ///< LED 7–8
|
||||||
#define PA6_SET_MASK 0x40 ///< Green LED pin
|
#define PA6_SET_MASK 0x40 ///< Green LED pin
|
||||||
#define PA7_SET_MASK 0x80 ///< Red LED
|
#define PA7_SET_MASK 0x80 ///< Red LED
|
||||||
/** @} */
|
|
||||||
|
|
||||||
#define MAIN_LOOP_SLEEP 10U // Main loop delay in ms (system tick)
|
#define MAIN_LOOP_SLEEP 10U // Main loop delay in ms
|
||||||
#define BUTTON_LONG_PRESS_DURATION_MS 1000U // Long press detection threshold
|
#define BUTTON_LONG_PRESS_DURATION_MS 1000U // Long press threshold
|
||||||
#define BUTTON_IGNORE_DURATION_MS 1000U // Time that the button is ignored after a long press
|
#define BUTTON_IGNORE_DURATION_MS 1000U // Time button ignored after long press
|
||||||
|
|
||||||
/** @brief Convert milliseconds to system ticks (integer division). */
|
/** Convert milliseconds to system ticks */
|
||||||
#define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP)
|
#define MS_TO_TICKS(ms) ((ms) / MAIN_LOOP_SLEEP)
|
||||||
|
|
||||||
/** @brief Global flags */
|
/** Global flags */
|
||||||
volatile bool bLedEnabled = true;
|
volatile bool bLedEnabled = true;
|
||||||
volatile bool bBtnPressed = false;
|
volatile bool bBtnPressed = false;
|
||||||
|
|
||||||
// Function forward declarations
|
// Forward declarations
|
||||||
void blinkLed(bool resetCounters);
|
void blinkLed(bool resetCounters);
|
||||||
static inline void leds_off(void);
|
static inline void leds_off(void);
|
||||||
static inline void leds_on(void);
|
static inline void leds_on(void);
|
||||||
|
static void battery_level_indicator(void);
|
||||||
static void handleSwitch(void);
|
static void handleSwitch(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Main entry point.
|
* @brief Main entry point
|
||||||
*
|
|
||||||
* Initializes I/O ports and runs the main loop, periodically calling @ref blinkLed().
|
|
||||||
*
|
|
||||||
* @return never returns
|
|
||||||
*/
|
*/
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
// --- configure LED pins as outputs ---
|
// Configure LED pins as outputs
|
||||||
VPORTA.DIR = (PA1_SET_MASK | PA2_SET_MASK | PA3_SET_MASK |
|
VPORTA.DIR = (PA1_SET_MASK | PA2_SET_MASK | PA3_SET_MASK | PA6_SET_MASK | PA7_SET_MASK);
|
||||||
/*PA6_SET_MASK |*/ // PA6 now input (switch)
|
|
||||||
PA7_SET_MASK);
|
|
||||||
|
|
||||||
// Configure PA0 as input with pull-up
|
// Configure PA0 as input with pull-up
|
||||||
VPORTA.DIR &= ~BUTTON_PIN_MASK; // Input
|
VPORTA.DIR &= ~BUTTON_PIN_MASK; // Input
|
||||||
PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // Pull-up enabled
|
PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // Pull-up enabled
|
||||||
|
|
||||||
// --- ensure all LEDs off at startup ---
|
// Ensure all LEDs off at startup
|
||||||
leds_off();
|
leds_off();
|
||||||
VPORTA.OUT &= (uint8_t) ~(PA7_SET_MASK);
|
battery_level_indicator(); // TODO: Implement
|
||||||
|
|
||||||
bool bLedEnabledOld = bLedEnabled;
|
bool bLedEnabledOld = bLedEnabled;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
handleSwitch(); // check switch state
|
handleSwitch(); // Check switch state
|
||||||
|
|
||||||
|
// Light LEDs while button is pressed
|
||||||
if (bBtnPressed)
|
if (bBtnPressed)
|
||||||
{
|
{
|
||||||
leds_on();
|
leds_on();
|
||||||
@ -74,23 +66,25 @@ int main(void)
|
|||||||
leds_off();
|
leds_off();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Long press detected → show confirmation blink
|
||||||
if (bLedEnabledOld != bLedEnabled)
|
if (bLedEnabledOld != bLedEnabled)
|
||||||
{
|
{
|
||||||
// A long press detected --> confirm with a blink
|
|
||||||
bLedEnabledOld = bLedEnabled;
|
bLedEnabledOld = bLedEnabled;
|
||||||
|
|
||||||
leds_off();
|
leds_off();
|
||||||
_delay_ms(BUTTON_IGNORE_DURATION_MS / 10);
|
_delay_ms(BUTTON_IGNORE_DURATION_MS / 10);
|
||||||
leds_on();
|
leds_on();
|
||||||
_delay_ms(BUTTON_IGNORE_DURATION_MS / 10);
|
_delay_ms(BUTTON_IGNORE_DURATION_MS / 10);
|
||||||
leds_off();
|
leds_off();
|
||||||
_delay_ms(BUTTON_IGNORE_DURATION_MS);
|
_delay_ms(BUTTON_IGNORE_DURATION_MS);
|
||||||
blinkLed(true); // reset the persistent counters
|
|
||||||
|
blinkLed(true); // reset blink state machine
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if ((bLedEnabled) && (!bBtnPressed))
|
if (bLedEnabled && !bBtnPressed)
|
||||||
{
|
{
|
||||||
blinkLed(false);
|
blinkLed(false); // run normal blink
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,9 +93,7 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Switch off all controlled LEDs (PA1, PA2, PA3).
|
* @brief Turn off all controlled LEDs (PA1, PA2, PA3)
|
||||||
*
|
|
||||||
* @note Declared inline for speed (single instruction sequence).
|
|
||||||
*/
|
*/
|
||||||
static inline void leds_off(void)
|
static inline void leds_off(void)
|
||||||
{
|
{
|
||||||
@ -109,9 +101,7 @@ static inline void leds_off(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Switch on all controlled LEDs (PA1, PA2, PA3).
|
* @brief Turn on all controlled LEDs (PA1, PA2, PA3)
|
||||||
*
|
|
||||||
* @note Declared inline for speed (single instruction sequence).
|
|
||||||
*/
|
*/
|
||||||
static inline void leds_on(void)
|
static inline void leds_on(void)
|
||||||
{
|
{
|
||||||
@ -119,26 +109,31 @@ static inline void leds_on(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Handle momentary switch input on PA6. TODO: Switch to PA0
|
* @brief Battery monitoring
|
||||||
|
*/
|
||||||
|
static void battery_level_indicator(void)
|
||||||
|
{
|
||||||
|
// TODO: Implement
|
||||||
|
VPORTA.OUT |= (PA6_SET_MASK | PA7_SET_MASK); // green + red OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handle momentary switch input on PA0
|
||||||
*
|
*
|
||||||
* A press longer than 2 seconds toggles ::bLedEnabled.
|
* A long press toggles ::bLedEnabled.
|
||||||
* Uses simple state and counters for debouncing and long-press detection.
|
|
||||||
*/
|
*/
|
||||||
static void handleSwitch(void)
|
static void handleSwitch(void)
|
||||||
{
|
{
|
||||||
static uint16_t pressTicks = 0; ///< press duration counter
|
static uint16_t pressTicks = 0; ///< Press duration counter
|
||||||
static bool prevPressed = false; ///< previous switch state
|
static bool prevPressed = false; ///< Previous button state
|
||||||
|
|
||||||
bool pressed = !(VPORTA.IN & BUTTON_PIN_MASK); // active-low
|
bool pressed = !(VPORTA.IN & BUTTON_PIN_MASK); // Active-low
|
||||||
|
|
||||||
if (pressed)
|
if (pressed)
|
||||||
{
|
{
|
||||||
bBtnPressed = true;
|
bBtnPressed = true;
|
||||||
if (pressTicks < 0xFFFF)
|
if (pressTicks < 0xFFFF)
|
||||||
{
|
pressTicks++; // Prevent overflow
|
||||||
// prevent overflow
|
|
||||||
pressTicks++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -148,15 +143,14 @@ static void handleSwitch(void)
|
|||||||
|
|
||||||
if (prevPressed && pressTicks >= MS_TO_TICKS(BUTTON_LONG_PRESS_DURATION_MS))
|
if (prevPressed && pressTicks >= MS_TO_TICKS(BUTTON_LONG_PRESS_DURATION_MS))
|
||||||
{
|
{
|
||||||
// long press detected → toggle blinking
|
bLedEnabled = !bLedEnabled; // Toggle LED blinking
|
||||||
bLedEnabled = !bLedEnabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPressed = pressed;
|
prevPressed = pressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief LED blink state machine (bike rear light style).
|
* @brief LED blink state machine (bike rear light style)
|
||||||
*
|
*
|
||||||
* Normal cycle:
|
* Normal cycle:
|
||||||
* 0: all off, wait 250 ms
|
* 0: all off, wait 250 ms
|
||||||
@ -172,15 +166,13 @@ static void handleSwitch(void)
|
|||||||
*/
|
*/
|
||||||
void blinkLed(bool resetCounters)
|
void blinkLed(bool resetCounters)
|
||||||
{
|
{
|
||||||
// --- precomputed constants ---
|
const uint8_t T50 = MS_TO_TICKS(50);
|
||||||
const uint8_t T50 = MS_TO_TICKS(50); ///< 50 ms in ticks
|
const uint8_t T100 = MS_TO_TICKS(100);
|
||||||
const uint8_t T100 = MS_TO_TICKS(100); ///< 100 ms in ticks
|
const uint8_t T250 = MS_TO_TICKS(250);
|
||||||
const uint8_t T250 = MS_TO_TICKS(250); ///< 250 ms in ticks
|
|
||||||
|
|
||||||
// --- persistent state ---
|
static uint16_t counter = 0;
|
||||||
static uint16_t counter = 0; ///< ticks for current state
|
static uint8_t state = 2; // start with LEDs-on state
|
||||||
static uint8_t state = 2; ///< start with first LEDs-on state
|
static uint8_t cycle = 0;
|
||||||
static uint8_t cycle = 0; ///< cycle counter
|
|
||||||
|
|
||||||
if (resetCounters)
|
if (resetCounters)
|
||||||
{
|
{
|
||||||
|
44
sbom.template.json
Normal file
44
sbom.template.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"spdxVersion": "SPDX-2.3",
|
||||||
|
"dataLicense": "CC0-1.0",
|
||||||
|
"SPDXID": "SPDXRef-DOCUMENT",
|
||||||
|
"name": "lezyne-rear-light-firmware",
|
||||||
|
"documentNamespace": "https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware/@GIT_HASH@",
|
||||||
|
"creationInfo": {
|
||||||
|
"created": "@CMAKE_TIMESTAMP@",
|
||||||
|
"creators": [
|
||||||
|
"Tool: CMake+SPDX"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "main.c",
|
||||||
|
"SPDXID": "SPDXRef-mainc",
|
||||||
|
"downloadLocation": "https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware/src/branch/main/main.c",
|
||||||
|
"filesAnalyzed": true,
|
||||||
|
"versionInfo": "@GIT_HASH@",
|
||||||
|
"licenseDeclared": "MIT License",
|
||||||
|
"homepage": "https://git.mosad.xyz/localhorst/lezyne-rear-light-firmware"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "avr-gcc",
|
||||||
|
"SPDXID": "SPDXRef-avrgcc",
|
||||||
|
"downloadLocation": "NOASSERTION",
|
||||||
|
"filesAnalyzed": false,
|
||||||
|
"versionInfo": "@AVR_GCC_VERSION@",
|
||||||
|
"licenseDeclared": "GPL-3.0-or-later",
|
||||||
|
"supplier": "Organization: The GNU Project",
|
||||||
|
"homepage": "https://gcc.gnu.org/git/gitweb.cgi?p=gcc.git"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "avr-libc",
|
||||||
|
"SPDXID": "SPDXRef-avrlibc",
|
||||||
|
"downloadLocation": "NOASSERTION",
|
||||||
|
"filesAnalyzed": false,
|
||||||
|
"versionInfo": "2.2.1-1.2",
|
||||||
|
"licenseDeclared": "Modified BSD License",
|
||||||
|
"supplier": "Organization: AVRDUDES Authors",
|
||||||
|
"homepage": "https://github.com/avrdudes/avr-libc/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user