First commit

This commit is contained in:
MultiMote 2021-06-28 21:07:46 +03:00
commit 4354a3ace5
63 changed files with 5530 additions and 0 deletions

3
.gitignore vendored Normal file

@ -0,0 +1,3 @@
build
.vscode
font/font.c

3
.gitmodules vendored Normal file

@ -0,0 +1,3 @@
[submodule "libopencm3"]
path = libopencm3
url = https://github.com/libopencm3/libopencm3.git

120
CMakeLists.txt Normal file

@ -0,0 +1,120 @@
cmake_minimum_required(VERSION 3.13)
option(SWAP_BEEP_AND_SPEED "Swap speed and beep buttons" OFF)
option(WARNINGS_AS_ERRORS "Threeat warnings as errors" OFF)
option(USE_DEFAULT_TOOLCHAIN "Use ./extra/arm-gcc-toolchain.cmake toolcahin" ON)
if(USE_DEFAULT_TOOLCHAIN)
set(CMAKE_TOOLCHAIN_FILE ./extra/arm-gcc-toolchain.cmake)
endif()
project(kugoo-s3-bluepill VERSION 0.1)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "MinSizeRel" CACHE STRING "Build type" FORCE)
endif()
set(TARGET_BASENAME ${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_BUILD_TYPE})
set(TARGET ${TARGET_BASENAME}.elf)
find_library(OPENCM3_LIBRARY opencm3_stm32f1
HINTS
ENV OPENCM3_ROOT
./libopencm3/lib
PATH_SUFFIXES lib
REQUIRED)
find_path(OPENCM3_INCLUDE_DIR libopencm3/stm32/gpio.h
HINTS
ENV OPENCM3_ROOT
./libopencm3
PATH_SUFFIXES include
REQUIRED)
find_program(OPENOCD openocd)
add_definitions(-DSTM32F1)
if(SWAP_BEEP_AND_SPEED)
add_definitions(-DSWAP_BEEP_AND_SPEED)
endif()
add_definitions(-DFIRMWARE_VERSION="${PROJECT_VERSION}")
set(CMAKE_EXE_LINKER_FLAGS
"-T${PROJECT_SOURCE_DIR}/extra/stm32f103c8t6-opencm3.ld \
-mthumb \
-mcpu=cortex-m3 \
-nostartfiles \
-Wl,--gc-sections \
-Wl,-Map=${TARGET_BASENAME}.map")
# set(CMAKE_EXE_LINKER_FLAGS_DEBUG "-u _printf_float")
set(CMAKE_C_FLAGS
"-Wall \
-Wextra \
-std=gnu99 \
-mthumb \
-mcpu=cortex-m3 \
-mfloat-abi=soft \
-ffunction-sections \
-finline-functions \
-fdata-sections \
-specs=nano.specs \
-specs=nosys.specs")
if(WARNINGS_AS_ERRORS)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror")
endif()
include_directories(src)
include_directories(${OPENCM3_INCLUDE_DIR})
add_executable(${TARGET}
src/globals.c
src/utils.c
src/persistence.c
src/hardware.c
src/kugoo_s3.c
src/ssd1306.c
src/keyboard.c
src/gui.c
src/views/main_view.c
src/views/settings_view.c
src/views/detailed_view.c
src/views/trigger_calibration_view.c
src/views/last_trips_view.c
src/main.c)
target_link_libraries(${TARGET} ${OPENCM3_LIBRARY})
add_custom_command(
TARGET ${TARGET}
POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O ihex ${TARGET} ${TARGET_BASENAME}.hex
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)
if(CMAKE_SIZE)
add_custom_command(
TARGET ${TARGET}
POST_BUILD
COMMAND ${CMAKE_SIZE} --format=berkeley ${TARGET}
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)
else()
message(WARNING "arm-none-eabi-size not found, please set the CMAKE_SIZE variable")
endif()
if (OPENOCD)
add_custom_target(flash-openocd
COMMAND ${OPENOCD} -d0 -f interface/stlink-v2.cfg -f target/stm32f1x.cfg -c "program ${TARGET} verify reset exit"
DEPENDS ${TARGET}
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)
endif()

16
Dockerfile Normal file

@ -0,0 +1,16 @@
FROM debian:10-slim
RUN apt-get update && apt-get install cmake make python3 gcc-arm-none-eabi dos2unix -y
COPY . /code/
WORKDIR /code
# Fix CRLF line endings
RUN mkdir /build && mkdir /dist \
&& find libopencm3 \( -name '*.py' -o -iname 'Makefile' -o -iname 'irq2nvic_h' \) -exec dos2unix {} \;
CMD cd /code/libopencm3 && make TARGETS=stm32/f1 \
&& cd /build \
&& cmake -G "Unix Makefiles" -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=/dist /code \
&& cmake --build .

21
LICENSE Normal file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 MultiMote
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 without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

213
README.md Normal file

@ -0,0 +1,213 @@
# ПРЕДУПРЕЖДЕНИЕ
Это любительское устройство, созданное автором для личного электросамоката. В
случае ВНЕЗАПНОГО отказа устройства во время пути или другого странного поведения,
автор не несёт никакой ответственности за причиненный ущерб.
Вы делаете всё на свой страх и риск.
Ко всему прочему, проект не находится в стадии завершённого.
# Описание
[📖 Руководство пользователя](docs/reference.ru.md) [(pdf)](docs/reference.ru.pdf)
## Особенности
> В связи с тем, что штатная функция круиз-контроля таковым не является, далее
> буду называть эту функцию автоматической фиксации ускорения.
> Иначе не знаю как это назвать.
* Подавление неотключаемой автоматической фиксации ускорения. Осуществляется за счёт
подачи нулевого ускорения, затем плавного возврата в режим.
* Поддержание заданной скорости (П-регулятор, почти).
* Настоящий круиз-контроль (с поддержанием скорости, а не ускорения) с
включением по кнопке. Это значит что устройство будет поддерживать заданную
скорость независимо от уклона дороги.
* Переключение максимальной скорости в км/ч.
* Отображение ошибок: неподключенная ручка тормоза, неподключенная ручка газа,
перегрузка по току, проблема с двигателем, нет ответа от контроллера мотор-колеса.
* Программное ограничение тока.
* Плавный старт.
* История поездок (последние 8).
* Экранная заставка для предотвращения выгорания пикселей.
* Защита от зависаний при помощи сторожевого таймера.
* Внешняя EEPROM память. Настройки не сбрасываются при перепрошивке.
* UTF-8 для исходников и шрифта. Даже Emoji можно выводить. Зачем? Да захотелось.
* Односторонняя печатная плата.
## Прочее
* Штатная функция выбора "скорости" в контроллере мотор-колеса не используется и
установлена на уровне 3, так как после замеров оказалось, что это просто
ограничение хода ручки газа.
* В устройстве используется ШИМ на выходе для пищалки. То, что пищалка уже с генератором,
я понял слишком поздно. Но, в принципе, работает. Пищалку рекомендуется заменить на
пьезоизлучатель с примерно такой обвязкой:
```
R1
>----[ ]----
220R |
-----
L1 |3 C| BZ1
150mH |3 |
(154) -----
|
>--------------
```
* ⚠ Есть нерешённая проблема с резким ускорением на долю секунды во время пути.
Я не знаю, проблема в помехах во время передачи или же в моём контроллере мотор-колеса.
* При отсутствии конденсаторов параллельно высоковольтной линии, возникают
помехи, которые искажают пакет данных и самокат может ВНЕЗАПНО затормозить
или разогнаться.
* По каким-то причинам контроллер мотор-колеса может вернуть время оборота колеса
около 5мс, что равняется скорости примерно 400 км/ч.
Поэтому слишком низкие значения игнорируются.
* При слишком частом подаче пакетов на контроллер мотор-колеса,
последний начинает игнорировать некоторые из них.
## TODO
* ⚠ Довести до ума механизм подавления штатного круиз-контроля.
* ⚠ Разобраться с редким зависанием c последующей перезагрузкой.
Кажется, это связано с дисплеем. Серьёзно?
* Реализовать "расстояние с последней зарядки". То, что самокат
был на зарядке, определять по разнице напряжения между включениями.
* Редактирование конфигурации по USART/Bluetooth.
* Последовательности звуков.
* Моргание фары, шаблоны мигания.
* Как-нибудь применить стоп-сигнал.
* Немного зарефакторить код, не всё находится в логичных местах.
* Провести тесты на другом контроллере мотор-колеса. Возможно,
мой контроллер - источник проблем.
* Изменение шаблоноа значений ограничения скорости.
* Поддержка трёхпозиционного переключателя скоростей.
## Фото
![assembly order](docs/images/pcb_top.jpg)
![assembly order](docs/images/installed.jpg)
![assembly order](docs/images/installed_menu.jpg)
# Сборка прошивки
## Сборка через Docker
Самый простой способ. Необходим [Docker](https://docker.com).
Параметры сборки можно изменить в Dockerfile.
```sh
docker build -t kugoo-s3-bluepill .
```
```sh
docker run -it --rm -v путь/куда/сохранить/результат:/dist kugoo-s3-bluepill
```
## Сборка вручную
Для сборки необходимы:
* Python3
* [GNU Arm Embedded Toolchain](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads)
* [CMake](https://cmake.org/download/)
* make
* [libopencm3 0d72e67](https://github.com/libopencm3/libopencm3/tree/0d72e6739c5f7c90f28350a8bb228722ff094806)
Дополнительные зависимости:
* openocd (прошивка и отладка)
Если вы используете Windows, то все эти пакеты можно поставить через [msys2](https://www.msys2.org/).
Сборка на примере msys2:
В корне msys2 запускаем `mingw32.exe`
Установка необходимых пакетов
```bash
pacman -S mingw-w64-i686-arm-none-eabi-gcc mingw-w64-i686-arm-none-eabi-gdb \
mingw-w64-i686-cmake mingw-w64-i686-make mingw-w64-i686-openocd
```
Собираем libopencm3.
```shell
cd libopencm3
mingw32-make TARGETS=stm32/f1
```
Конфигурируем проект. Переходим в его корневой каталог, затем выполняем
```
mkdir build
cd build
cmake -G "MinGW Makefiles" ..
```
Файл extra/arm-gcc-toolchain.cmake при этом можно скопировать
в любое расположение и настроить под себя
(использовать абсолютные пути, например):
```
cmake -G "MinGW Makefiles" -DUSE_DEFAULT_TOOLCHAIN=OFF -DCMAKE_TOOLCHAIN_FILE=/path/to/custom/arm-gcc-toolchain.cmake ..
```
Собираем
```
cmake --build .
```
или
```
mingw32-make
```
# Точки интереса в файлах проекта
* `globals.h` - различные константы
* `settings_view.c` - объявление пунктов меню настроек
* `gui.c` - объявление экранов (например, главный экран, экран настроек,
экран калибровки ручек)
* `views/*` - обработчики экранов
# Порядок сборки устройства
[Печатная плата, схема, список компонентов](pcb)
![assembly order](docs/images/assembly_order.jpg)
![assembly pins](docs/images/assembly_pins.jpg)
![assembled](docs/images/assembled.jpg)
1. Монтаж XL7005 и обвязки. Проверка напряжений.
2. Монтаж линейного стабилизатора. Проверка напряжений.
3. Монтаж цепи включения питания и временной перемычки для проверки.
При нажатой кнопке должны появится напряжения на стабилизаторах.
4. Монтаж остальных мелких компонентов.
5. Монтаж гребёнки для дисплея.
6. Монтаж штырьков для bluepill (**не** на плате bluepill).
Для удобства штырьки можно припаивать в пластиковой оправе, потом
её снять (см. выше)
7. Установка bluepill.
8. Установка дисплея.

BIN
docs/gui.kra Normal file

Binary file not shown.

BIN
docs/images/assembled.jpg Normal file

Binary file not shown.

After

(image error) Size: 201 KiB

Binary file not shown.

After

(image error) Size: 174 KiB

Binary file not shown.

Binary file not shown.

After

(image error) Size: 122 KiB

BIN
docs/images/installed.jpg Normal file

Binary file not shown.

After

(image error) Size: 150 KiB

Binary file not shown.

After

(image error) Size: 184 KiB

BIN
docs/images/main_screen.png Normal file

Binary file not shown.

After

(image error) Size: 15 KiB

BIN
docs/images/pcb_top.jpg Normal file

Binary file not shown.

After

(image error) Size: 166 KiB

214
docs/reference.ru.md Normal file

@ -0,0 +1,214 @@
- [1. Краткие имена кнопок](#1-краткие-имена-кнопок)
- [2. Неочевидные комбинации клавиш](#2-неочевидные-комбинации-клавиш)
- [2.1 Выключение](#21-выключение)
- [2.2 Активация круиз-контроля](#22-активация-круиз-контроля)
- [2.3 Сброс настроек](#23-сброс-настроек)
- [2.4 Ограничение тока](#24-ограничение-тока)
- [3. Редактирование значений в меню настроек](#3-редактирование-значений-в-меню-настроек)
- [3.1 Логические значения (1|0)](#31-логические-значения-10)
- [3.2 Числовые значения](#32-числовые-значения)
- [4. Пункты меню настроек](#4-пункты-меню-настроек)
- [ОТКЛЮЧИТЬ ЗВУКИ (1|0)](#отключить-звуки-10)
- [ГРОМКОСТЬ КНОПОК](#громкость-кнопок)
- [ГРОМКОСТЬ СИГНАЛОВ](#громкость-сигналов)
- [АНТИ-ФИКСАЦИЯ УСКОР. (1|0)](#анти-фиксация-ускор-10)
- [ZERO-START (1|0)](#zero-start-10)
- [ПЛАВНОЕ УСКОРЕНИЕ (1|0)](#плавное-ускорение-10)
- [ИНКРЕМ. ПЛАВН. УСКОР](#инкрем-плавн-ускор)
- [ОГРАНИЧЕНИЕ ТОКА (1|0)](#ограничение-тока-10)
- [ЗНАЧ. ОГРАНИЧЕН. ТОКА](#знач-ограничен-тока)
- [КОЭФФ. СТАБ. ТОКА](#коэфф-стаб-тока)
- [СТАБИЛИЗАЦИЯ СКОР. (1|0)](#стабилизация-скор-10)
- [КОЭФФ. СТАБ. СКОРОСТИ](#коэфф-стаб-скорости)
- [КАЛИБРОВКА РУЧЕК](#калибровка-ручек)
- [АВТОКАЛИБРОВКА РУЧЕК (1|0)](#автокалибровка-ручек-10)
- [СОХРАНЯТЬ СКОРОСТЬ](#сохранять-скорость)
- [ДЛИНА ОКР. КОЛЕСА, ММ](#длина-окр-колеса-мм)
- [КОЛИЧЕСТВО МАГНИТОВ](#количество-магнитов)
- [ПОСЛЕДНИЕ ПОЕЗДКИ](#последние-поездки)
# 1. Краткие имена кнопок
`BEEP` - звуковой сигнал / возврат
`SET` - меню настроек / уменьшить значение
`POWER` - питание / подтверждения
`LIGHT` - фонарь / увеличить значение
`SPEED` - ограничение скорости
# 2. Неочевидные комбинации клавиш
## 2.1 Выключение
Выключение производится из главного экрана долгим нажатием кнопки `POWER`.
## 2.2 Активация круиз-контроля
Вывести ручку акселератора в ненулевое положение и нажать кнопку `SET`. Для
поддержания скорости используется значение ручки (не текущая скорость самоката).
Автоматической активации пока нет. И нужно ли?
## 2.3 Сброс настроек
При включении зажать `BEEP`. Далее нажать `LIGHT` чтобы сбросить все сохранённые
данные, или `SET`, чтобы сохранить при этом показания одометра.
Для отмены нажать `POWER`.
## 2.4 Ограничение тока
Вывести ручку тормоза в ненулевое положение и нажать кнопку `SPEED`.
# 3. Редактирование значений в меню настроек
## 3.1 Логические значения (1|0)
Выбрать пункт меню и нажать кнопку питания.
`✔` - включено
`▪` - выключено
## 3.2 Числовые значения
Выбрать пункт меню и нажать кнопку питания. Навигация между разрядами
происходит с помощью кнопки питания. Увеличить значение разряда - кнопка
`LIGHT`, уменьшить - кнопка `SET`. Для установки значения по-умолчанию
используется кнопка `SPEED`. Выход из редактирования осуществляется
кнопкой `BEEP`.
При редактирования значений с плавающей точкой, возможны странные явления
в связи с своеобразностью этого типа данных.
# 4. Пункты меню настроек
## ОТКЛЮЧИТЬ ЗВУКИ (1|0)
Отключить все звуки кроме звука на кнопке сигнала.
## ГРОМКОСТЬ КНОПОК
Громкость сигнала нажатия на кнопки.
## ГРОМКОСТЬ СИГНАЛОВ
Громкость различных оповещений (например, вход-выход из круиз-контроля).
Громкость "гудка" не регулируется этим параметром.
## АНТИ-ФИКСАЦИЯ УСКОР. (1|0)
Подавление фиксации ускорения.
Головная боль штатного контроллера мотор-колеса. Он же "круиз-контроль", коим
на самом деле не является. Не отключается. При долгом удержании (около 5 секунд)
ручки акселератора в одном положении её значение фиксируется, после чего ручку можно
отпускать. При отключении данной функции в меню данное устройство совершает
попытки подавить данный функционал методом периодической подачи нулевого
ускорения на контроллер мотор-колеса.
> ⚠️ При выключении данной функции становится невозможной какая-либо
> продолжительная регулировка ускорения (стабилизация скорости, ограничение тока).
## ZERO-START (1|0)
Двигатель начинает работать только после того, как начать его вращать.
> ⚠️ При использовании стабилизации скорости и функции zero-start
> гарантированы неожиданные результаты.
## ПЛАВНОЕ УСКОРЕНИЕ (1|0)
Включение функции плавного старта. При повышении значения ручки акселератора,
фактическое значение линейно нарастает. Хорошо работает вместе
со стабилизацией скорости.
## ИНКРЕМ. ПЛАВН. УСКОР
Инкремент плавного ускорения (км/ч).
При включенной функции плавного ускорения к текущему значению скорости каждые
50 мс добавляется данное значение до достижения требуемого.
## ОГРАНИЧЕНИЕ ТОКА (1|0)
Включить или выключить ограничение тока.
## ЗНАЧ. ОГРАНИЧЕН. ТОКА
Значение ограничения тока.
При превышении заданного значения устройство совершает попытки снизить скорость
движения, пока не понизится ток до доступного предела.
## КОЭФФ. СТАБ. ТОКА
Коэффициент стабилизации тока.
При превышении заданного значения разница тока умножается
на этот коэффициент и отнимается от значения скорости (км/ч).
Происходит это после приёма пакета от контроллера мотор-колеса.
При установке значения в 0 стабилизация тока работать не может.
## СТАБИЛИЗАЦИЯ СКОР. (1|0)
Включить или выключить стабилизацию скорости.
Как это работает? При нажатии на ручку акселератора на контроллер мотор-колеса
подаётся приблизительное значение ускорения для достижения данной скорости на
холостых оборотах. Затем реальная скорость сравнивается с ожидаемой, разница
значений умножается на коэффициент стабилизации скорости и прибавляется
к передаваемому значению. Происходит это после приёма
пакета от контроллера мотор-колеса.
## КОЭФФ. СТАБ. СКОРОСТИ
Коэффициент стабилизации скорости.
При установке значения в 0 стабилизация скорости работать не может.
## КАЛИБРОВКА РУЧЕК
Произвести установку минимальных и максимальных значений ручек тормоза/акселератора.
Калибровка происходит в следующем порядке:
1. Нулевое положение ручки акселератора
2. Максимальное положение ручки акселератора
3. Нулевое положение ручки тормоза
4. Максимальное положение ручки тормоза
## АВТОКАЛИБРОВКА РУЧЕК (1|0)
При запуске устройства установить текущие положения ручек как минимальные.
Минимальные значения ручной калибровки при этом игнорируются.
## СОХРАНЯТЬ СКОРОСТЬ
Сохранять ли выбранное значение ограничения скорости после перезапуска.
## ДЛИНА ОКР. КОЛЕСА, ММ
Длина окружности колеса в миллиметрах.
Формула: `2 × π × (диаметр_в_мм / 2)`
## КОЛИЧЕСТВО МАГНИТОВ
Количество магнитов в мотор-колесе. По умолчанию 30.
## ПОСЛЕДНИЕ ПОЕЗДКИ
Открывает список последних восьми поездок. Данные отображаются в формате
```
Время - Расстояние
```
При долгом нажатии кнопки `SPEED` список очищается.
Список обновляется при выключении устройства кнопкой `POWER`.

1037
docs/reference.ru.pdf Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/throttle_to_speed.ods Normal file

Binary file not shown.

@ -0,0 +1,493 @@
speed_lvl;data;battery_val;current;ms_per_rev
1;0;387;0;3000
1;0;387;0;3000
1;0;388;0;3000
1;0;387;0;3000
1;25;388;0;3000
1;25;388;0;3000
1;25;387;0;3000
1;25;387;0;3000
1;50;387;0;3000
1;50;388;0;3000
1;50;387;0;3000
1;50;387;0;3000
1;75;387;0;3000
1;75;387;0;3000
1;75;387;0;3000
1;75;388;0;3000
1;100;386;0;3000
1;100;387;0;3000
1;100;387;0;3000
1;100;387;0;3000
1;125;388;0;3000
1;125;387;0;3000
1;125;387;0;3000
1;125;388;0;3000
1;150;387;0;3000
1;150;387;0;3000
1;150;387;0;3000
1;150;387;0;3000
1;175;387;0;3000
1;175;387;0;3000
1;175;388;0;3000
1;175;388;0;3000
1;200;386;0;3000
1;200;387;0;3000
1;200;388;0;3000
1;200;386;0;3000
1;225;386;0;3000
1;225;386;0;929
1;225;387;0;929
1;225;386;0;926
1;250;386;0;618
1;250;386;1;618
1;250;386;0;619
1;250;386;1;618
1;275;386;1;471
1;275;386;1;470
1;275;386;1;470
1;275;385;1;471
1;300;386;1;371
1;300;386;1;370
1;300;386;1;371
1;300;386;1;370
1;325;386;2;321
1;325;385;1;326
1;325;386;1;321
1;325;386;2;321
1;350;386;1;281
1;350;385;1;285
1;350;385;1;285
1;350;386;1;285
1;375;384;2;251
1;375;385;2;251
1;375;385;2;250
1;375;385;2;251
1;400;385;2;220
1;400;385;2;220
1;400;385;2;220
1;400;385;2;220
1;425;385;2;200
1;425;385;2;201
1;425;385;2;201
1;425;385;2;200
1;450;385;2;182
1;450;385;3;182
1;450;385;3;182
1;450;384;2;182
1;475;385;3;166
1;475;385;3;167
1;475;385;3;167
1;475;384;3;167
1;500;385;4;153
1;500;384;3;153
1;500;384;4;153
1;500;385;3;153
1;525;385;4;143
1;525;384;4;143
1;525;385;4;142
1;525;385;4;143
1;550;384;5;133
1;550;383;3;133
1;550;384;4;133
1;550;384;3;133
1;575;383;5;124
1;575;384;6;125
1;575;384;5;125
1;575;384;4;125
1;600;383;5;117
1;600;383;5;118
1;600;384;4;117
1;600;384;4;117
1;625;384;4;110
1;625;383;5;111
1;625;383;5;110
1;625;384;4;111
1;650;383;6;111
1;650;383;5;111
1;650;384;5;110
1;650;383;5;111
1;675;384;6;110
1;675;383;5;110
1;675;383;6;111
1;675;384;7;111
1;700;383;6;111
1;700;384;6;111
1;700;383;6;111
1;700;383;6;110
1;725;383;6;111
1;725;383;5;110
1;725;383;4;111
1;725;383;5;110
1;750;383;5;111
1;750;383;6;110
1;750;383;6;110
1;750;383;6;111
1;775;383;6;111
1;775;383;5;111
1;775;383;5;110
1;775;383;6;111
1;800;383;6;111
1;800;383;5;111
1;800;383;5;111
1;800;383;6;110
1;825;383;5;111
1;825;383;6;111
1;825;383;5;111
1;825;383;6;110
1;850;383;5;110
1;850;382;5;110
1;850;382;5;111
1;850;382;6;111
1;875;383;5;111
1;875;383;6;111
1;875;383;6;110
1;875;382;6;111
1;900;382;6;111
1;900;383;6;111
1;900;383;5;111
1;900;383;5;111
1;925;383;6;111
1;925;383;6;111
1;925;382;5;111
1;925;382;6;110
1;950;383;6;111
1;950;382;5;110
1;950;382;5;111
1;950;382;6;111
1;975;382;3;111
1;975;382;5;111
1;975;382;6;111
1;975;382;6;111
1;1000;382;5;111
1;1000;383;5;111
1;1000;382;5;111
1;1000;383;5;110
2;0;385;0;256
2;0;385;0;3000
2;0;385;0;3000
2;0;385;0;3000
2;25;385;0;3000
2;25;386;0;3000
2;25;385;0;3000
2;25;385;0;3000
2;50;385;0;3000
2;50;385;0;3000
2;50;386;0;3000
2;50;385;0;3000
2;75;386;0;3000
2;75;385;0;3000
2;75;385;0;3000
2;75;384;0;3000
2;100;386;0;3000
2;100;385;0;3000
2;100;385;0;3000
2;100;385;0;3000
2;125;385;0;3000
2;125;385;0;3000
2;125;385;0;3000
2;125;385;0;3000
2;150;385;0;3000
2;150;385;0;3000
2;150;386;0;3000
2;150;385;0;3000
2;175;385;0;3000
2;175;385;0;3000
2;175;385;0;3000
2;175;386;0;3000
2;200;385;0;3000
2;200;385;0;3000
2;200;385;0;3000
2;200;385;0;3000
2;225;384;0;3000
2;225;384;0;3000
2;225;384;0;925
2;225;384;0;923
2;250;384;0;617
2;250;384;0;618
2;250;384;0;617
2;250;384;0;617
2;275;384;1;470
2;275;384;1;470
2;275;383;1;469
2;275;384;1;470
2;300;384;0;371
2;300;385;1;371
2;300;385;1;370
2;300;384;1;371
2;325;384;1;321
2;325;384;1;326
2;325;384;1;321
2;325;384;1;321
2;350;384;2;284
2;350;384;1;285
2;350;384;1;285
2;350;384;1;285
2;375;384;2;251
2;375;384;2;251
2;375;384;2;251
2;375;384;2;251
2;400;384;2;220
2;400;383;2;220
2;400;383;2;221
2;400;384;2;221
2;425;384;2;201
2;425;384;2;201
2;425;384;2;201
2;425;383;2;201
2;450;382;2;182
2;450;384;3;182
2;450;383;3;181
2;450;383;3;182
2;475;383;3;167
2;475;383;3;167
2;475;383;3;167
2;475;383;3;167
2;500;383;3;153
2;500;382;3;153
2;500;383;3;154
2;500;384;3;154
2;525;383;4;143
2;525;382;4;143
2;525;383;4;143
2;525;383;4;143
2;550;383;4;133
2;550;383;5;133
2;550;383;3;133
2;550;383;4;133
2;575;383;5;125
2;575;383;5;124
2;575;383;4;125
2;575;382;5;125
2;600;382;4;118
2;600;382;4;117
2;600;382;5;117
2;600;381;4;117
2;625;383;6;111
2;625;382;4;111
2;625;381;5;111
2;625;382;5;110
2;650;382;6;105
2;650;382;7;106
2;650;382;6;105
2;650;382;6;105
2;675;382;6;100
2;675;381;6;100
2;675;382;6;101
2;675;381;6;100
2;700;381;6;95
2;700;381;8;95
2;700;382;7;95
2;700;382;7;95
2;725;381;8;91
2;725;381;8;90
2;725;382;6;90
2;725;382;8;91
2;750;382;7;87
2;750;380;7;87
2;750;381;8;87
2;750;382;9;86
2;775;381;8;84
2;775;381;7;83
2;775;381;7;83
2;775;381;8;84
2;800;381;8;83
2;800;380;7;83
2;800;381;8;83
2;800;382;8;83
2;825;381;8;83
2;825;381;7;83
2;825;381;7;83
2;825;381;8;84
2;850;382;7;84
2;850;381;8;84
2;850;381;8;83
2;850;379;7;84
2;875;381;7;84
2;875;381;8;84
2;875;381;7;83
2;875;381;7;83
2;900;380;7;84
2;900;379;8;84
2;900;380;8;83
2;900;380;8;84
2;925;381;8;84
2;925;380;7;83
2;925;380;8;84
2;925;380;7;84
2;950;381;8;83
2;950;381;7;84
2;950;380;7;83
2;950;381;8;84
2;975;380;9;85
2;975;379;7;84
2;975;380;8;83
2;975;380;7;84
2;1000;381;8;83
2;1000;381;9;84
2;1000;380;9;84
2;1000;380;8;84
3;0;384;0;230
3;0;384;0;3000
3;0;384;0;3000
3;0;383;0;3000
3;25;384;0;3000
3;25;384;0;3000
3;25;384;0;3000
3;25;384;0;3000
3;50;384;0;3000
3;50;383;0;3000
3;50;384;0;3000
3;50;384;0;3000
3;75;383;0;3000
3;75;384;0;3000
3;75;384;0;3000
3;75;384;0;3000
3;100;384;0;3000
3;100;384;0;3000
3;100;384;0;3000
3;100;384;0;3000
3;125;385;0;3000
3;125;384;0;3000
3;125;384;0;3000
3;125;384;0;3000
3;150;383;0;3000
3;150;384;0;3000
3;150;384;0;3000
3;150;384;0;3000
3;175;384;0;3000
3;175;384;0;3000
3;175;385;0;3000
3;175;384;0;3000
3;200;384;0;3000
3;200;384;0;3000
3;200;384;0;3000
3;200;384;0;3000
3;225;383;0;3000
3;225;383;0;3000
3;225;383;0;924
3;225;384;0;922
3;250;383;0;618
3;250;383;0;617
3;250;383;0;617
3;250;383;0;617
3;275;384;1;470
3;275;383;0;471
3;275;383;0;469
3;275;382;1;470
3;300;384;1;371
3;300;383;1;371
3;300;383;1;370
3;300;384;0;371
3;325;383;1;321
3;325;383;1;327
3;325;383;1;322
3;325;384;1;321
3;350;383;1;279
3;350;383;1;285
3;350;383;1;286
3;350;383;1;286
3;375;383;1;251
3;375;383;2;251
3;375;382;2;251
3;375;383;2;251
3;400;383;2;220
3;400;383;2;221
3;400;382;2;221
3;400;382;2;220
3;425;382;2;201
3;425;383;2;202
3;425;383;2;201
3;425;383;2;201
3;450;383;2;182
3;450;382;3;182
3;450;384;3;182
3;450;383;3;182
3;475;383;3;167
3;475;383;3;167
3;475;382;3;167
3;475;382;3;168
3;500;383;3;153
3;500;382;3;153
3;500;382;3;153
3;500;382;3;154
3;525;383;3;143
3;525;382;4;143
3;525;382;4;143
3;525;382;4;143
3;550;382;4;133
3;550;382;4;133
3;550;382;5;133
3;550;382;3;133
3;575;382;4;126
3;575;381;4;125
3;575;382;4;125
3;575;382;4;125
3;600;382;4;117
3;600;382;5;118
3;600;382;4;118
3;600;381;4;118
3;625;382;5;111
3;625;382;4;111
3;625;381;5;111
3;625;381;6;111
3;650;381;6;105
3;650;381;7;105
3;650;382;6;106
3;650;381;6;106
3;675;381;5;100
3;675;381;6;100
3;675;381;6;100
3;675;381;7;100
3;700;381;7;95
3;700;381;6;95
3;700;381;8;95
3;700;381;6;95
3;725;381;6;91
3;725;380;7;91
3;725;381;8;91
3;725;380;6;90
3;750;380;7;87
3;750;380;7;87
3;750;380;7;87
3;750;379;7;87
3;775;380;7;84
3;775;380;7;84
3;775;380;7;83
3;775;380;8;84
3;800;380;9;80
3;800;380;9;80
3;800;380;11;80
3;800;379;9;80
3;825;379;7;77
3;825;379;9;78
3;825;380;7;78
3;825;379;9;78
3;850;381;10;75
3;850;379;11;74
3;850;379;8;75
3;850;380;9;75
3;875;379;8;71
3;875;379;9;72
3;875;380;8;72
3;875;379;10;72
3;900;378;9;69
3;900;379;11;69
3;900;379;11;69
3;900;379;14;70
3;925;379;11;64
3;925;379;11;63
3;925;379;13;64
3;925;379;9;64
3;950;379;11;64
3;950;378;10;64
3;950;378;14;63
3;950;378;11;64
3;975;379;0;64
3;975;379;11;64
3;975;378;14;64
3;975;377;12;64
3;1000;379;11;64
3;1000;378;8;63
3;1000;378;12;63
3;1000;378;11;63
1 speed_lvl data battery_val current ms_per_rev
2 1 0 387 0 3000
3 1 0 387 0 3000
4 1 0 388 0 3000
5 1 0 387 0 3000
6 1 25 388 0 3000
7 1 25 388 0 3000
8 1 25 387 0 3000
9 1 25 387 0 3000
10 1 50 387 0 3000
11 1 50 388 0 3000
12 1 50 387 0 3000
13 1 50 387 0 3000
14 1 75 387 0 3000
15 1 75 387 0 3000
16 1 75 387 0 3000
17 1 75 388 0 3000
18 1 100 386 0 3000
19 1 100 387 0 3000
20 1 100 387 0 3000
21 1 100 387 0 3000
22 1 125 388 0 3000
23 1 125 387 0 3000
24 1 125 387 0 3000
25 1 125 388 0 3000
26 1 150 387 0 3000
27 1 150 387 0 3000
28 1 150 387 0 3000
29 1 150 387 0 3000
30 1 175 387 0 3000
31 1 175 387 0 3000
32 1 175 388 0 3000
33 1 175 388 0 3000
34 1 200 386 0 3000
35 1 200 387 0 3000
36 1 200 388 0 3000
37 1 200 386 0 3000
38 1 225 386 0 3000
39 1 225 386 0 929
40 1 225 387 0 929
41 1 225 386 0 926
42 1 250 386 0 618
43 1 250 386 1 618
44 1 250 386 0 619
45 1 250 386 1 618
46 1 275 386 1 471
47 1 275 386 1 470
48 1 275 386 1 470
49 1 275 385 1 471
50 1 300 386 1 371
51 1 300 386 1 370
52 1 300 386 1 371
53 1 300 386 1 370
54 1 325 386 2 321
55 1 325 385 1 326
56 1 325 386 1 321
57 1 325 386 2 321
58 1 350 386 1 281
59 1 350 385 1 285
60 1 350 385 1 285
61 1 350 386 1 285
62 1 375 384 2 251
63 1 375 385 2 251
64 1 375 385 2 250
65 1 375 385 2 251
66 1 400 385 2 220
67 1 400 385 2 220
68 1 400 385 2 220
69 1 400 385 2 220
70 1 425 385 2 200
71 1 425 385 2 201
72 1 425 385 2 201
73 1 425 385 2 200
74 1 450 385 2 182
75 1 450 385 3 182
76 1 450 385 3 182
77 1 450 384 2 182
78 1 475 385 3 166
79 1 475 385 3 167
80 1 475 385 3 167
81 1 475 384 3 167
82 1 500 385 4 153
83 1 500 384 3 153
84 1 500 384 4 153
85 1 500 385 3 153
86 1 525 385 4 143
87 1 525 384 4 143
88 1 525 385 4 142
89 1 525 385 4 143
90 1 550 384 5 133
91 1 550 383 3 133
92 1 550 384 4 133
93 1 550 384 3 133
94 1 575 383 5 124
95 1 575 384 6 125
96 1 575 384 5 125
97 1 575 384 4 125
98 1 600 383 5 117
99 1 600 383 5 118
100 1 600 384 4 117
101 1 600 384 4 117
102 1 625 384 4 110
103 1 625 383 5 111
104 1 625 383 5 110
105 1 625 384 4 111
106 1 650 383 6 111
107 1 650 383 5 111
108 1 650 384 5 110
109 1 650 383 5 111
110 1 675 384 6 110
111 1 675 383 5 110
112 1 675 383 6 111
113 1 675 384 7 111
114 1 700 383 6 111
115 1 700 384 6 111
116 1 700 383 6 111
117 1 700 383 6 110
118 1 725 383 6 111
119 1 725 383 5 110
120 1 725 383 4 111
121 1 725 383 5 110
122 1 750 383 5 111
123 1 750 383 6 110
124 1 750 383 6 110
125 1 750 383 6 111
126 1 775 383 6 111
127 1 775 383 5 111
128 1 775 383 5 110
129 1 775 383 6 111
130 1 800 383 6 111
131 1 800 383 5 111
132 1 800 383 5 111
133 1 800 383 6 110
134 1 825 383 5 111
135 1 825 383 6 111
136 1 825 383 5 111
137 1 825 383 6 110
138 1 850 383 5 110
139 1 850 382 5 110
140 1 850 382 5 111
141 1 850 382 6 111
142 1 875 383 5 111
143 1 875 383 6 111
144 1 875 383 6 110
145 1 875 382 6 111
146 1 900 382 6 111
147 1 900 383 6 111
148 1 900 383 5 111
149 1 900 383 5 111
150 1 925 383 6 111
151 1 925 383 6 111
152 1 925 382 5 111
153 1 925 382 6 110
154 1 950 383 6 111
155 1 950 382 5 110
156 1 950 382 5 111
157 1 950 382 6 111
158 1 975 382 3 111
159 1 975 382 5 111
160 1 975 382 6 111
161 1 975 382 6 111
162 1 1000 382 5 111
163 1 1000 383 5 111
164 1 1000 382 5 111
165 1 1000 383 5 110
166 2 0 385 0 256
167 2 0 385 0 3000
168 2 0 385 0 3000
169 2 0 385 0 3000
170 2 25 385 0 3000
171 2 25 386 0 3000
172 2 25 385 0 3000
173 2 25 385 0 3000
174 2 50 385 0 3000
175 2 50 385 0 3000
176 2 50 386 0 3000
177 2 50 385 0 3000
178 2 75 386 0 3000
179 2 75 385 0 3000
180 2 75 385 0 3000
181 2 75 384 0 3000
182 2 100 386 0 3000
183 2 100 385 0 3000
184 2 100 385 0 3000
185 2 100 385 0 3000
186 2 125 385 0 3000
187 2 125 385 0 3000
188 2 125 385 0 3000
189 2 125 385 0 3000
190 2 150 385 0 3000
191 2 150 385 0 3000
192 2 150 386 0 3000
193 2 150 385 0 3000
194 2 175 385 0 3000
195 2 175 385 0 3000
196 2 175 385 0 3000
197 2 175 386 0 3000
198 2 200 385 0 3000
199 2 200 385 0 3000
200 2 200 385 0 3000
201 2 200 385 0 3000
202 2 225 384 0 3000
203 2 225 384 0 3000
204 2 225 384 0 925
205 2 225 384 0 923
206 2 250 384 0 617
207 2 250 384 0 618
208 2 250 384 0 617
209 2 250 384 0 617
210 2 275 384 1 470
211 2 275 384 1 470
212 2 275 383 1 469
213 2 275 384 1 470
214 2 300 384 0 371
215 2 300 385 1 371
216 2 300 385 1 370
217 2 300 384 1 371
218 2 325 384 1 321
219 2 325 384 1 326
220 2 325 384 1 321
221 2 325 384 1 321
222 2 350 384 2 284
223 2 350 384 1 285
224 2 350 384 1 285
225 2 350 384 1 285
226 2 375 384 2 251
227 2 375 384 2 251
228 2 375 384 2 251
229 2 375 384 2 251
230 2 400 384 2 220
231 2 400 383 2 220
232 2 400 383 2 221
233 2 400 384 2 221
234 2 425 384 2 201
235 2 425 384 2 201
236 2 425 384 2 201
237 2 425 383 2 201
238 2 450 382 2 182
239 2 450 384 3 182
240 2 450 383 3 181
241 2 450 383 3 182
242 2 475 383 3 167
243 2 475 383 3 167
244 2 475 383 3 167
245 2 475 383 3 167
246 2 500 383 3 153
247 2 500 382 3 153
248 2 500 383 3 154
249 2 500 384 3 154
250 2 525 383 4 143
251 2 525 382 4 143
252 2 525 383 4 143
253 2 525 383 4 143
254 2 550 383 4 133
255 2 550 383 5 133
256 2 550 383 3 133
257 2 550 383 4 133
258 2 575 383 5 125
259 2 575 383 5 124
260 2 575 383 4 125
261 2 575 382 5 125
262 2 600 382 4 118
263 2 600 382 4 117
264 2 600 382 5 117
265 2 600 381 4 117
266 2 625 383 6 111
267 2 625 382 4 111
268 2 625 381 5 111
269 2 625 382 5 110
270 2 650 382 6 105
271 2 650 382 7 106
272 2 650 382 6 105
273 2 650 382 6 105
274 2 675 382 6 100
275 2 675 381 6 100
276 2 675 382 6 101
277 2 675 381 6 100
278 2 700 381 6 95
279 2 700 381 8 95
280 2 700 382 7 95
281 2 700 382 7 95
282 2 725 381 8 91
283 2 725 381 8 90
284 2 725 382 6 90
285 2 725 382 8 91
286 2 750 382 7 87
287 2 750 380 7 87
288 2 750 381 8 87
289 2 750 382 9 86
290 2 775 381 8 84
291 2 775 381 7 83
292 2 775 381 7 83
293 2 775 381 8 84
294 2 800 381 8 83
295 2 800 380 7 83
296 2 800 381 8 83
297 2 800 382 8 83
298 2 825 381 8 83
299 2 825 381 7 83
300 2 825 381 7 83
301 2 825 381 8 84
302 2 850 382 7 84
303 2 850 381 8 84
304 2 850 381 8 83
305 2 850 379 7 84
306 2 875 381 7 84
307 2 875 381 8 84
308 2 875 381 7 83
309 2 875 381 7 83
310 2 900 380 7 84
311 2 900 379 8 84
312 2 900 380 8 83
313 2 900 380 8 84
314 2 925 381 8 84
315 2 925 380 7 83
316 2 925 380 8 84
317 2 925 380 7 84
318 2 950 381 8 83
319 2 950 381 7 84
320 2 950 380 7 83
321 2 950 381 8 84
322 2 975 380 9 85
323 2 975 379 7 84
324 2 975 380 8 83
325 2 975 380 7 84
326 2 1000 381 8 83
327 2 1000 381 9 84
328 2 1000 380 9 84
329 2 1000 380 8 84
330 3 0 384 0 230
331 3 0 384 0 3000
332 3 0 384 0 3000
333 3 0 383 0 3000
334 3 25 384 0 3000
335 3 25 384 0 3000
336 3 25 384 0 3000
337 3 25 384 0 3000
338 3 50 384 0 3000
339 3 50 383 0 3000
340 3 50 384 0 3000
341 3 50 384 0 3000
342 3 75 383 0 3000
343 3 75 384 0 3000
344 3 75 384 0 3000
345 3 75 384 0 3000
346 3 100 384 0 3000
347 3 100 384 0 3000
348 3 100 384 0 3000
349 3 100 384 0 3000
350 3 125 385 0 3000
351 3 125 384 0 3000
352 3 125 384 0 3000
353 3 125 384 0 3000
354 3 150 383 0 3000
355 3 150 384 0 3000
356 3 150 384 0 3000
357 3 150 384 0 3000
358 3 175 384 0 3000
359 3 175 384 0 3000
360 3 175 385 0 3000
361 3 175 384 0 3000
362 3 200 384 0 3000
363 3 200 384 0 3000
364 3 200 384 0 3000
365 3 200 384 0 3000
366 3 225 383 0 3000
367 3 225 383 0 3000
368 3 225 383 0 924
369 3 225 384 0 922
370 3 250 383 0 618
371 3 250 383 0 617
372 3 250 383 0 617
373 3 250 383 0 617
374 3 275 384 1 470
375 3 275 383 0 471
376 3 275 383 0 469
377 3 275 382 1 470
378 3 300 384 1 371
379 3 300 383 1 371
380 3 300 383 1 370
381 3 300 384 0 371
382 3 325 383 1 321
383 3 325 383 1 327
384 3 325 383 1 322
385 3 325 384 1 321
386 3 350 383 1 279
387 3 350 383 1 285
388 3 350 383 1 286
389 3 350 383 1 286
390 3 375 383 1 251
391 3 375 383 2 251
392 3 375 382 2 251
393 3 375 383 2 251
394 3 400 383 2 220
395 3 400 383 2 221
396 3 400 382 2 221
397 3 400 382 2 220
398 3 425 382 2 201
399 3 425 383 2 202
400 3 425 383 2 201
401 3 425 383 2 201
402 3 450 383 2 182
403 3 450 382 3 182
404 3 450 384 3 182
405 3 450 383 3 182
406 3 475 383 3 167
407 3 475 383 3 167
408 3 475 382 3 167
409 3 475 382 3 168
410 3 500 383 3 153
411 3 500 382 3 153
412 3 500 382 3 153
413 3 500 382 3 154
414 3 525 383 3 143
415 3 525 382 4 143
416 3 525 382 4 143
417 3 525 382 4 143
418 3 550 382 4 133
419 3 550 382 4 133
420 3 550 382 5 133
421 3 550 382 3 133
422 3 575 382 4 126
423 3 575 381 4 125
424 3 575 382 4 125
425 3 575 382 4 125
426 3 600 382 4 117
427 3 600 382 5 118
428 3 600 382 4 118
429 3 600 381 4 118
430 3 625 382 5 111
431 3 625 382 4 111
432 3 625 381 5 111
433 3 625 381 6 111
434 3 650 381 6 105
435 3 650 381 7 105
436 3 650 382 6 106
437 3 650 381 6 106
438 3 675 381 5 100
439 3 675 381 6 100
440 3 675 381 6 100
441 3 675 381 7 100
442 3 700 381 7 95
443 3 700 381 6 95
444 3 700 381 8 95
445 3 700 381 6 95
446 3 725 381 6 91
447 3 725 380 7 91
448 3 725 381 8 91
449 3 725 380 6 90
450 3 750 380 7 87
451 3 750 380 7 87
452 3 750 380 7 87
453 3 750 379 7 87
454 3 775 380 7 84
455 3 775 380 7 84
456 3 775 380 7 83
457 3 775 380 8 84
458 3 800 380 9 80
459 3 800 380 9 80
460 3 800 380 11 80
461 3 800 379 9 80
462 3 825 379 7 77
463 3 825 379 9 78
464 3 825 380 7 78
465 3 825 379 9 78
466 3 850 381 10 75
467 3 850 379 11 74
468 3 850 379 8 75
469 3 850 380 9 75
470 3 875 379 8 71
471 3 875 379 9 72
472 3 875 380 8 72
473 3 875 379 10 72
474 3 900 378 9 69
475 3 900 379 11 69
476 3 900 379 11 69
477 3 900 379 14 70
478 3 925 379 11 64
479 3 925 379 11 63
480 3 925 379 13 64
481 3 925 379 9 64
482 3 950 379 11 64
483 3 950 378 10 64
484 3 950 378 14 63
485 3 950 378 11 64
486 3 975 379 0 64
487 3 975 379 11 64
488 3 975 378 14 64
489 3 975 377 12 64
490 3 1000 379 11 64
491 3 1000 378 8 63
492 3 1000 378 12 63
493 3 1000 378 11 63

@ -0,0 +1,13 @@
set(CMAKE_SYSTEM_NAME Generic)
# undefined reference to `_exit' fix on compiler test
set(CMAKE_EXE_LINKER_FLAGS_INIT "--specs=nosys.specs")
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_DEBUGGER arm-none-eabi-gdb)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)
set(CMAKE_SIZE arm-none-eabi-size)

@ -0,0 +1,132 @@
MEMORY
{
rom (rx) : ORIGIN = 0x08000000, LENGTH = 64K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
/*
* This file is part of the libopencm3 project.
*
* Copyright (C) 2009 Uwe Hermann <uwe@hermann-uwe.de>
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* This is a generic linker script for Cortex-M targets using libopencm3.
*
* Memory regions MUST be defined in the ld script which includes this one!
* Example:
MEMORY
{
rom (rx) : ORIGIN = 0x08000000, LENGTH = 256K
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 16K
}
INCLUDE cortex-m-generic.ld
*/
/* Enforce emmition of the vector table. */
EXTERN (vector_table)
/* Define the entry point of the output file. */
ENTRY(reset_handler)
/* Define sections. */
SECTIONS
{
.text : {
*(.vectors) /* Vector table */
*(.text*) /* Program code */
. = ALIGN(4);
*(.rodata*) /* Read-only data */
. = ALIGN(4);
} >rom
/* C++ Static constructors/destructors, also used for __attribute__
* ((constructor)) and the likes */
.preinit_array : {
. = ALIGN(4);
__preinit_array_start = .;
KEEP (*(.preinit_array))
__preinit_array_end = .;
} >rom
.init_array : {
. = ALIGN(4);
__init_array_start = .;
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array))
__init_array_end = .;
} >rom
.fini_array : {
. = ALIGN(4);
__fini_array_start = .;
KEEP (*(.fini_array))
KEEP (*(SORT(.fini_array.*)))
__fini_array_end = .;
} >rom
/*
* Another section used by C++ stuff, appears when using newlib with
* 64bit (long long) printf support
*/
.ARM.extab : {
*(.ARM.extab*)
} >rom
.ARM.exidx : {
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} >rom
. = ALIGN(4);
_etext = .;
/* ram, but not cleared on reset, eg boot/app comms */
.noinit (NOLOAD) : {
*(.noinit*)
} >ram
. = ALIGN(4);
.data : {
_data = .;
*(.data*) /* Read-write initialized data */
*(.ramtext*) /* "text" functions to run in ram */
. = ALIGN(4);
_edata = .;
} >ram AT >rom
_data_loadaddr = LOADADDR(.data);
.bss : {
*(.bss*) /* Read-write zero initialized data */
*(COMMON)
. = ALIGN(4);
_ebss = .;
} >ram
/*
* The .eh_frame section appears to be used for C++ exception handling.
* You may need to fix this if you're using C++.
*/
/DISCARD/ : { *(.eh_frame) }
. = ALIGN(4);
end = .;
}
PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram));

145
font/font_convert.py Normal file

@ -0,0 +1,145 @@
import glob
import os
import re
from PIL import Image
header_body = "#include <stdint.h>\n\n"
header_body += "#define FONT_WIDTH 5\n\n"
header_body += "static const uint8_t font_error_symbol[FONT_WIDTH] = {0xfe, 0xaa, 0x92, 0xaa, 0xfe};"
image_list = glob.glob("*.png")
char_table = []
def utf8_code(ch):
return int.from_bytes(ch.encode("utf-8"), "big")
for filename in image_list:
basename = os.path.splitext(filename)[0]
match_multi = re.search(r"sprites_(.)-(.).png", filename)
match_single = re.search(r"sprite_(.).png", filename)
if match_multi is not None:
start_character = match_multi.group(1)
end_character = match_multi.group(2)
elif match_single is not None:
start_character = match_single.group(1)
end_character = match_single.group(1)
else: continue
start_code = utf8_code(start_character)
end_code = utf8_code(end_character)
print("Processing %s (0x%08x - 0x%08x)" % (basename, start_code, end_code))
image = Image.open(filename)
image_rgb = image.convert("RGB")
table_size = ord(end_character) - ord(start_character) + 1
print("Table size: %d" % table_size)
if table_size < 1 or table_size > image.width / 5:
print("invalid table size")
continue
for i in range(0, table_size):
char_bytes = [];
for col in range(0, 5):
col_byte = 0
for row in range(0, 8):
r, g, b = image_rgb.getpixel((i*5 + col, row))
if (r + g + b) == 0:
col_byte |= (1 << row)
char_bytes.append(col_byte)
char_char = chr(ord(start_character) + i)
char_table.append({
"char": char_char,
"code": utf8_code(char_char),
"bytes": char_bytes
})
image.close()
if not char_table:
print("Nothing found")
exit(0)
char_table.sort(key=lambda x: x["code"])
table_blocks = []
current_block = {}
prev_code = 0
# split to blocks if character in different ranges
for idx, item in enumerate(char_table):
code = item["code"]
if code - prev_code > 1:
if len(current_block) > 0:
current_block["end_code"] = prev_code
table_blocks.append(current_block)
current_block = {"start_code": code, "data": []}
current_block["data"].append(item)
prev_code = code
current_block["end_code"] = prev_code
table_blocks.append(current_block)
array_body = "static const uint8_t font_data[FONT_WIDTH * %d] = {\n" % len(char_table)
function_body = "void ssd1306_mbchar(uint32_t ch) {\n";
function_body += " uint16_t mapped_idx;\n\n";
idx = 0
for block in table_blocks:
start_code = block["start_code"]
end_code = block["end_code"]
array_body += " // 0x%08x - 0x%08x \n" % (start_code, end_code)
first_case = function_body.endswith("idx;\n\n")
function_body += " "
if not first_case:
function_body += "} else "
if start_code == end_code:
function_body += "if (ch == 0x%08x) {\n" % start_code
function_body += " mapped_idx = %d;\n" % idx
else:
function_body += "if (ch >= 0x%08x && ch <= 0x%08x) {\n" % (start_code, end_code)
function_body += " mapped_idx = %d + (ch - 0x%08x);\n" % (idx, start_code)
for char_data in block["data"]:
b = char_data["bytes"]
array_body += " 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, // %s (0x%08x) #%d\n" % (
b[0], b[1], b[2], b[3], b[4], char_data["char"], char_data["code"], idx
)
idx += 1
function_body += " } else {\n"
function_body += " draw_font_bytes(font_error_symbol);\n"
function_body += " return;\n"
function_body += " }\n\n"
function_body += " draw_font_bytes(font_data + mapped_idx * FONT_WIDTH);\n"
function_body += "}"
array_body += "};"
# out = open("font.json", "w", encoding="utf-8")
# out.write(json.dumps(table_blocks))
# out.close()
out = open("font.c", "w", encoding="utf-8")
out.write(header_body)
out.write("\n\n")
out.write(array_body)
out.write("\n\n")
out.write(function_body)
out.write("\n\n")
out.close()

BIN
font/sprite_Ё.png Normal file

Binary file not shown.

After

(image error) Size: 507 B

BIN
font/sprite_▪.png Normal file

Binary file not shown.

After

(image error) Size: 496 B

BIN
font/sprite_✔.png Normal file

Binary file not shown.

After

(image error) Size: 513 B

BIN
font/sprites.kra Normal file

Binary file not shown.

BIN
font/sprites_ -_.png Normal file

Binary file not shown.

After

(image error) Size: 1.3 KiB

BIN
font/sprites_А-Я.png Normal file

Binary file not shown.

After

(image error) Size: 1.0 KiB

1
libopencm3 Submodule

@ -0,0 +1 @@
Subproject commit 44928416eacb4c8d7774b1385c7f38fb226d080b

1
pcb/README.md Normal file

@ -0,0 +1 @@
Проект DipTrace.

Binary file not shown.

After

(image error) Size: 125 KiB

BIN
pcb/kugoo-s3-bluepill.dch Normal file

Binary file not shown.

BIN
pcb/kugoo-s3-bluepill.dip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

122
src/font.h Normal file

@ -0,0 +1,122 @@
#ifndef FONT_H
#define FONT_H
#include <stdint.h>
#define FONT_WIDTH 5
static const uint8_t font_error_symbol[FONT_WIDTH] = {0xfe, 0xaa, 0x92, 0xaa, 0xfe};
//
// Generated values below (use font_convert.py)
//
static const uint8_t font_data[FONT_WIDTH * 99] = {
// 0x00000020 - 0x0000005f
0x00, 0x00, 0x00, 0x00, 0x00, // (0x00000020) #0
0x00, 0x00, 0xbe, 0x00, 0x00, // ! (0x00000021) #1
0x00, 0x0c, 0x00, 0x0c, 0x00, // " (0x00000022) #2
0x28, 0xfe, 0x28, 0xfe, 0x28, // # (0x00000023) #3
0x4c, 0x92, 0xfe, 0x92, 0x64, // $ (0x00000024) #4
0x06, 0xe6, 0x10, 0xce, 0xc0, // % (0x00000025) #5
0x40, 0xac, 0x92, 0xac, 0x60, // & (0x00000026) #6
0x00, 0x00, 0x0c, 0x00, 0x00, // ' (0x00000027) #7
0x00, 0x7c, 0x82, 0x00, 0x00, // ( (0x00000028) #8
0x00, 0x00, 0x82, 0x7c, 0x00, // ) (0x00000029) #9
0x00, 0x08, 0x14, 0x08, 0x00, // * (0x0000002a) #10
0x00, 0x20, 0x70, 0x20, 0x00, // + (0x0000002b) #11
0x00, 0x00, 0x80, 0x40, 0x00, // , (0x0000002c) #12
0x00, 0x20, 0x20, 0x20, 0x00, // - (0x0000002d) #13
0x00, 0x00, 0x80, 0x00, 0x00, // . (0x0000002e) #14
0x00, 0xc0, 0x38, 0x06, 0x00, // / (0x0000002f) #15
0x7c, 0xc2, 0xba, 0x86, 0x7c, // 0 (0x00000030) #16
0x00, 0x8c, 0xfe, 0x80, 0x00, // 1 (0x00000031) #17
0x84, 0xc2, 0xa2, 0x92, 0x8c, // 2 (0x00000032) #18
0x44, 0x82, 0x92, 0xaa, 0x44, // 3 (0x00000033) #19
0x1e, 0x10, 0x10, 0x10, 0xfe, // 4 (0x00000034) #20
0x4e, 0x92, 0x92, 0x92, 0x62, // 5 (0x00000035) #21
0x7c, 0x92, 0x92, 0x92, 0x64, // 6 (0x00000036) #22
0x02, 0x02, 0xe2, 0x12, 0x0e, // 7 (0x00000037) #23
0x6c, 0x92, 0x92, 0x92, 0x6c, // 8 (0x00000038) #24
0x4c, 0x92, 0x92, 0x92, 0x7c, // 9 (0x00000039) #25
0x00, 0x00, 0x48, 0x00, 0x00, // : (0x0000003a) #26
0x00, 0x80, 0x48, 0x00, 0x00, // ; (0x0000003b) #27
0x00, 0x20, 0x50, 0x88, 0x00, // < (0x0000003c) #28
0x00, 0x50, 0x50, 0x50, 0x00, // = (0x0000003d) #29
0x00, 0x88, 0x50, 0x20, 0x00, // > (0x0000003e) #30
0x0c, 0x02, 0xa2, 0x12, 0x0c, // ? (0x0000003f) #31
0x7c, 0x82, 0x9a, 0xa2, 0xbc, // @ (0x00000040) #32
0xfc, 0x12, 0x12, 0x12, 0xfc, // A (0x00000041) #33
0xfe, 0x92, 0x92, 0x92, 0x7c, // B (0x00000042) #34
0x7c, 0x82, 0x82, 0x82, 0x44, // C (0x00000043) #35
0xfe, 0x82, 0x82, 0x82, 0x7c, // D (0x00000044) #36
0xfe, 0x92, 0x92, 0x82, 0x82, // E (0x00000045) #37
0xfe, 0x12, 0x12, 0x12, 0x02, // F (0x00000046) #38
0x7c, 0x82, 0x92, 0x92, 0x74, // G (0x00000047) #39
0xfe, 0x10, 0x10, 0x10, 0xfe, // H (0x00000048) #40
0x82, 0x82, 0xfe, 0x82, 0x82, // I (0x00000049) #41
0x40, 0x80, 0x82, 0x7e, 0x02, // J (0x0000004a) #42
0xfe, 0x10, 0x10, 0x28, 0xc6, // K (0x0000004b) #43
0xfe, 0x80, 0x80, 0x80, 0x80, // L (0x0000004c) #44
0xfe, 0x08, 0x30, 0x08, 0xfe, // M (0x0000004d) #45
0xfe, 0x08, 0x10, 0x20, 0xfe, // N (0x0000004e) #46
0x7c, 0x82, 0x82, 0x82, 0x7c, // O (0x0000004f) #47
0xfe, 0x12, 0x12, 0x12, 0x0c, // P (0x00000050) #48
0x7c, 0x82, 0x82, 0x42, 0xbc, // Q (0x00000051) #49
0xfe, 0x12, 0x12, 0x12, 0xec, // R (0x00000052) #50
0x4c, 0x92, 0x92, 0x92, 0x64, // S (0x00000053) #51
0x02, 0x02, 0xfe, 0x02, 0x02, // T (0x00000054) #52
0x7e, 0x80, 0x80, 0x80, 0x7e, // U (0x00000055) #53
0x1e, 0x60, 0x80, 0x60, 0x1e, // V (0x00000056) #54
0x7e, 0x80, 0x7e, 0x80, 0x7e, // W (0x00000057) #55
0xc6, 0x28, 0x10, 0x28, 0xc6, // X (0x00000058) #56
0x06, 0x08, 0xf0, 0x08, 0x06, // Y (0x00000059) #57
0xc2, 0xa2, 0x92, 0x8a, 0x86, // Z (0x0000005a) #58
0x00, 0xfe, 0x82, 0x00, 0x00, // [ (0x0000005b) #59
0x00, 0x06, 0x38, 0xc0, 0x00, // \ (0x0000005c) #60
0x00, 0x00, 0x82, 0xfe, 0x00, // ] (0x0000005d) #61
0x00, 0x08, 0x04, 0x08, 0x00, // ^ (0x0000005e) #62
0x00, 0x80, 0x80, 0x80, 0x00, // _ (0x0000005f) #63
// 0x0000d081 - 0x0000d081
0xfc, 0x95, 0x94, 0x85, 0x84, // Ё (0x0000d081) #64
// 0x0000d090 - 0x0000d0af
0xfc, 0x12, 0x12, 0x12, 0xfc, // А (0x0000d090) #65
0xfe, 0x92, 0x92, 0x92, 0xe2, // Б (0x0000d091) #66
0xfe, 0x92, 0x92, 0x92, 0x6c, // В (0x0000d092) #67
0xfe, 0x02, 0x02, 0x02, 0x02, // Г (0x0000d093) #68
0xc0, 0x7c, 0x42, 0x42, 0xfe, // Д (0x0000d094) #69
0xfe, 0x92, 0x92, 0x92, 0x82, // Е (0x0000d095) #70
0xc6, 0x28, 0xfe, 0x28, 0xc6, // Ж (0x0000d096) #71
0x44, 0x82, 0x82, 0x92, 0x6c, // З (0x0000d097) #72
0xfe, 0x40, 0x20, 0x10, 0xfe, // И (0x0000d098) #73
0xfe, 0x40, 0x23, 0x10, 0xfe, // Й (0x0000d099) #74
0xfe, 0x10, 0x10, 0x28, 0xc6, // К (0x0000d09a) #75
0xf8, 0x04, 0x02, 0x02, 0xfe, // Л (0x0000d09b) #76
0xfe, 0x08, 0x10, 0x08, 0xfe, // М (0x0000d09c) #77
0xfe, 0x10, 0x10, 0x10, 0xfe, // Н (0x0000d09d) #78
0x7c, 0x82, 0x82, 0x82, 0x7c, // О (0x0000d09e) #79
0xfe, 0x02, 0x02, 0x02, 0xfe, // П (0x0000d09f) #80
0xfe, 0x22, 0x22, 0x22, 0x1c, // Р (0x0000d0a0) #81
0x7c, 0x82, 0x82, 0x82, 0x82, // С (0x0000d0a1) #82
0x02, 0x02, 0xfe, 0x02, 0x02, // Т (0x0000d0a2) #83
0x4e, 0x90, 0x90, 0x90, 0x7e, // У (0x0000d0a3) #84
0x1c, 0x22, 0xfe, 0x22, 0x1c, // Ф (0x0000d0a4) #85
0xc6, 0x28, 0x10, 0x28, 0xc6, // Х (0x0000d0a5) #86
0xfe, 0x80, 0x80, 0xfe, 0x80, // Ц (0x0000d0a6) #87
0x0e, 0x10, 0x10, 0x10, 0xfe, // Ч (0x0000d0a7) #88
0xfe, 0x80, 0xfc, 0x80, 0xfe, // Ш (0x0000d0a8) #89
0x7e, 0x40, 0x7c, 0x40, 0xfe, // Щ (0x0000d0a9) #90
0x02, 0xfe, 0x90, 0x90, 0x60, // Ъ (0x0000d0aa) #91
0xfe, 0x90, 0x60, 0x00, 0xfe, // Ы (0x0000d0ab) #92
0xfe, 0x90, 0x90, 0x90, 0x60, // Ь (0x0000d0ac) #93
0x44, 0x82, 0x92, 0x92, 0x7c, // Э (0x0000d0ad) #94
0xfe, 0x10, 0x7c, 0x82, 0x7c, // Ю (0x0000d0ae) #95
0xec, 0x12, 0x12, 0x12, 0xfe, // Я (0x0000d0af) #96
// 0x00e296aa - 0x00e296aa
0x00, 0x00, 0x10, 0x00, 0x00, // ▪ (0x00e296aa) #97
// 0x00e29c94 - 0x00e29c94
0x10, 0x20, 0x10, 0x08, 0x04, // ✔ (0x00e29c94) #98
};
#endif /* FONT_H */

10
src/globals.c Normal file

@ -0,0 +1,10 @@
#include "globals.h"
#include <string.h>
char sprintf_buf[64] = { '\0' };
// float desired_speed_kmh = 0;
float cruise_ctl_speed_kmh = 0;
// float control_output_kmh = 0;
uint32_t reset_reason = 0;
enum cruise_control_status_t cruise_ctl_status = CRUISE_CTL_DISABLED;

73
src/globals.h Normal file

@ -0,0 +1,73 @@
#ifndef GLOBALS_H
#define GLOBALS_H
#include <stdint.h>
#define TRIGGER_MAX 1000U
#define CONTROLLER_TRIGGER_MAX 1000U
#define PERSIST_DISTANCE_EVERY_METERS 100
#define DEFAULT_SPEED_LIMIT_KMH 15.0f
#define BATTERY_VOLTS_0_PERCENTS 310
#define BATTERY_VOLTS_100_PERCENTS 420
/// Calibration values saved +- this value
#define TRIGGER_ANTI_JITTER 30
/// GUI repain interval in milliseconds
#define DISPLAY_REFRESH_INTERVAL_MS 500
#define LOGIC_REFRESH_INTERVAL_MS 50
#define PACKET_SEND_INTERVAL_MS 50
#define ANTI_THROTTLE_LOCK_PERIOD_MS 1000
#define ANTI_THROTTLE_LOCK_ZERO_PACKETS 2
#define STATIC_BEEP_VOLUME 16
#define KEY_BEEP_FREQ 4000
// Show separators between menu categories
#define MENU_SEPARATORS
/// throttle value after which wheel starts to rotate
#define CONTROLLER_STOPPED_VAL 175
#define SCREENSAVER_INACTIVITY_PERIOD_MS (1000U * 60U)
/**
* speed to throttle value ratio, approximately
* used in kugoo_s3_set_speed_approx
*
* see speed.ods
*
* Example:
* CONTROLLER_STOPPED_VAL + 10 km/H * 23.07046 = 175 + 230.7 = 405
*/
#define THROTTLE_TO_SPEED_COEFF 23.07046f
/// Used in anti throttle lock logic.
/// After throttle sets to 0, then it slowly rises to normal value.
#define THROTTLE_RECOVER_INCREMENT 3
// Print reset reason in detailed view instead of some indicators
// #define DEBUG_RESET_REASON
enum control_state_t {
CS_NORMAL,
CS_THROTTLE_RECOVER,
CS_FORCE_ZERO_THROTTLE,
};
enum cruise_control_status_t {
CRUISE_CTL_DISABLED,
CRUISE_CTL_WAITING_RELEASE, ///< Waiting for throttle trigger value becomes 0
CRUISE_CTL_ENABLED,
};
extern char sprintf_buf[64];
// extern float desired_speed_kmh;
extern float cruise_ctl_speed_kmh;
// extern float control_output_kmh;
extern uint32_t reset_reason;
extern enum cruise_control_status_t cruise_ctl_status;
#endif /* GLOBALS_H */

162
src/gui.c Normal file

@ -0,0 +1,162 @@
#include "gui.h"
#include "globals.h"
#include "hardware.h"
#include "keyboard.h"
#include "kugoo_s3.h"
#include "persistence.h"
#include "ssd1306.h"
#include "utils.h"
#include <stdio.h>
#include "views/detailed_view.h"
#include "views/main_view.h"
#include "views/settings_view.h"
#include "views/trigger_calibration_view.h"
#include "views/last_trips_view.h"
static uint32_t next_display_refrash_time = 0;
static uint32_t last_activity_time = 0;
static const struct view_t views[] = {
{
.id = GUI_VIEW_MAIN,
.on_draw = main_view_redraw,
.on_key = main_view_keyhandler,
.on_open = NULL
},
{
.id = GUI_VIEW_MAIN_DETAILED,
.on_draw = detailed_view_redraw,
.on_key = main_view_keyhandler,
.on_open = NULL
},
{
.id = GUI_VIEW_SETTINGS,
.on_draw = settings_view_redraw,
.on_key = settings_view_keyhandler,
.on_open = NULL
},
{
.id = GUI_VIEW_TRIGGER_CALIBRATION,
.on_draw = trigger_calibration_view_redraw,
.on_key = trigger_calibration_view_keyhandler,
.on_open = trigger_calibration_view_reset
},
{
.id = GUI_VIEW_LAST_TRIPS,
.on_draw = last_trips_view_redraw,
.on_key = last_trips_view_keyhandler,
.on_open = NULL
}
};
static const uint8_t views_count = sizeof(views) / sizeof(struct view_t);
const struct view_t *current_view = views;
void process_events(void) {
keyboard_poll();
uint16_t key_evt = keyboard_pop_event();
if(key_evt ||
kugoo_s3_get_speed() > 0 ||
brake_value_normalized() > 0 ||
throttle_value_normalized() > 0) {
last_activity_time = millis();
}
if ((gui_is_view(GUI_VIEW_MAIN) ||
gui_is_view(GUI_VIEW_MAIN_DETAILED)) &&
key_evt & KB_EVT_TYPE_REPEAT &&
key_evt & KB_EVT_KEY_POWER) {
force_persist_distance();
persist_last_trip();
eeprom_persist(&storage.speed_limit_last);
beep_blocking(500, 10, storage.keys_volume);
ssd1306_clear();
ssd1306_redraw();
power_disable();
}
if (current_view->on_key) {
current_view->on_key(key_evt);
}
}
static void error_message(const char *msg) {
ssd1306_framebuffer_setpos(1, 2);
ssd1306_font_scale(2);
ssd1306_string("ТОВАРИЩ!");
ssd1306_framebuffer_setpos(4, 0);
ssd1306_font_scale(1);
ssd1306_string(msg);
ssd1306_redraw();
}
void gui_redraw(void) {
if (millis() < next_display_refrash_time) {
return;
}
next_display_refrash_time = millis() + DISPLAY_REFRESH_INTERVAL_MS;
ssd1306_clear();
if((millis() - last_activity_time) > SCREENSAVER_INACTIVITY_PERIOD_MS) {
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(millis() / 5 % 7, (millis() / 3) % (SSD1306_WIDTH - 24));
ssd1306_string(":)");
ssd1306_redraw();
return;
}
ssd1306_font_scale(1);
if (gui_is_view(GUI_VIEW_MAIN) || gui_is_view(GUI_VIEW_MAIN_DETAILED)) {
// throttle lock is not an error
if (kugoo_s3_rx.state & ~KUGOO_S3_STATE_THROTTLE_LOCKED) {
if (kugoo_s3_rx.state & KUGOO_S3_STATE_MOTOR_ERROR) {
error_message("ОТКАЗ\nМОТОР-КОЛЕСА!");
} else if (kugoo_s3_rx.state & KUGOO_S3_STATE_OVERCURRENT) {
error_message("ПЕРЕТОК!");
} else {
error_message("КОНТРОЛЛЕР\nМОТОР-КОЛЕСА\nИЗВЕЩАЕТ ОБ ОШИБКЕ!");
}
return;
} else if (millis() - kugoo_s3_last_packet_time() > 2000) {
error_message("КОНТРОЛЛЕР\nМОТОР-КОЛЕСА\nНЕ ОТВЕЧАЕТ!");
return;
} else if (brake_adc_value() < 500) {
error_message("ПОДКЛЮЧИ РУЧКУ\nТОРМОЗА!");
return;
} else if (throttle_adc_value() < 500) {
error_message("ПОДКЛЮЧИ РУЧКУ\nАКСЕЛЕРАТОРА!");
return;
}
}
if (current_view->on_draw) {
current_view->on_draw();
}
ssd1306_redraw();
}
void gui_redraw_force(void) {
next_display_refrash_time = 0;
gui_redraw();
}
void gui_set_view(enum view_id_t id) {
for (uint8_t i = 0; i < views_count; i++) {
if (views[i].id == id) {
current_view = &views[i];
if (current_view->on_open) {
current_view->on_open();
}
}
}
}
uint8_t gui_is_view(enum view_id_t id) { return current_view->id == id; }

27
src/gui.h Normal file

@ -0,0 +1,27 @@
#ifndef GUI_H
#define GUI_H
#include <stdint.h>
enum view_id_t {
GUI_VIEW_MAIN,
GUI_VIEW_MAIN_DETAILED,
GUI_VIEW_SETTINGS,
GUI_VIEW_TRIGGER_CALIBRATION,
GUI_VIEW_LAST_TRIPS,
};
struct view_t {
enum view_id_t id;
void (*on_key)(uint8_t);
void (*on_draw)();
void (*on_open)();
};
void process_events(void);
void gui_redraw(void);
void gui_redraw_force(void);
void gui_set_view(enum view_id_t id);
uint8_t gui_is_view(enum view_id_t id);
#endif /* GUI_H */

319
src/hardware.c Normal file

@ -0,0 +1,319 @@
#include "hardware.h"
#include "persistence.h"
#include "utils.h"
#include "globals.h"
#include <errno.h>
#include <string.h>
static volatile uint32_t system_millis = 0;
static volatile uint16_t beep_time_left = 0;
#define BUZZER_PRESCALER 10U
struct adc_buf_t {
uint16_t vbat;
uint16_t throttle;
uint16_t brake;
};
static volatile struct adc_buf_t adc_buf;
void nops(uint32_t n) {
volatile uint32_t i;
for (i = 0; i < n; i++) {
__asm__("nop");
}
}
void sys_tick_handler(void) {
system_millis++;
if (beep_time_left > 0) {
if (beep_time_left == 1) {
buzzer_freq_vol(0, 0);
}
--beep_time_left;
}
}
inline uint32_t millis(void) { return system_millis; }
void msleep(uint32_t ms) {
uint32_t wake = system_millis + ms;
while (wake > system_millis) {
iwdg_reset();
}
}
void buzzer_freq_vol(uint16_t freq, uint8_t volume) {
if (freq == 0) {
timer_disable_oc_output(TIM1, TIM_OC4);
gpio_clear(GPIOA, GPIO_TIM1_CH4);
} else {
uint16_t period = (rcc_ahb_frequency / BUZZER_PRESCALER) / freq;
timer_enable_oc_output(TIM1, TIM_OC4);
timer_set_period(TIM1, period);
timer_set_oc_value(TIM1, TIM_OC4, convert_range_u16(volume, 100U, period / 2));
}
}
void beep_blocking(uint16_t freq, uint16_t ms, uint8_t volume) {
if(!storage.mute) {
beep_time_left = 0;
buzzer_freq_vol(freq, volume);
}
msleep(ms);
buzzer_freq_vol(0, volume);
}
void beep(uint16_t freq, uint16_t ms, uint8_t volume) {
if(storage.mute) {
buzzer_freq_vol(0, 0);
return;
}
buzzer_freq_vol(freq, volume);
beep_time_left = ms;
}
uint16_t throttle_adc_value(void) { return adc_buf.throttle; }
uint16_t brake_adc_value(void) { return adc_buf.brake; }
uint16_t throttle_value_normalized(void) {
uint16_t clamped = clamp_u16(throttle_adc_value(),
storage.throttle_trigger_min_value,
storage.throttle_trigger_max_value);
return ((clamped - storage.throttle_trigger_min_value) * TRIGGER_MAX) /
(storage.throttle_trigger_max_value - storage.throttle_trigger_min_value);
}
float throttle_value_to_kmh(void){
return convert_range_float(throttle_value_normalized(), TRIGGER_MAX, storage.speed_limit_last);
}
uint16_t brake_value_normalized(void) {
uint16_t clamped = clamp_u16(brake_adc_value(),
storage.brake_trigger_min_value,
storage.brake_trigger_max_value);
return ((clamped - storage.brake_trigger_min_value) * TRIGGER_MAX) /
(storage.brake_trigger_max_value - storage.brake_trigger_min_value);
}
uint16_t battery_value(void) {
// volts per unit = 3.3v / 4096 = 0.0008056640625
// res divider = r2 / (r1 + r2) = 3.3k / (47k + 3.3k) = 0.06560636182902585
// coeff = (res divider / volts per unit)*100 = ~814
// voltage_int = (adc_val * 1000) / coeff
// volts = voltage_int / 10
// .1 volts = voltage_int % 10
return ((uint32_t)adc_buf.vbat * 100) / 814;
}
static void enable_peripherals(void) {
rcc_periph_clock_enable(RCC_AFIO);
rcc_periph_clock_enable(RCC_GPIOA);
rcc_periph_clock_enable(RCC_GPIOB);
rcc_periph_clock_enable(RCC_GPIOC);
rcc_periph_clock_enable(RCC_ADC1);
rcc_periph_clock_enable(RCC_USART1);
rcc_periph_clock_enable(RCC_USART3);
rcc_periph_clock_enable(RCC_SPI2);
rcc_periph_clock_enable(RCC_I2C1);
rcc_periph_clock_enable(RCC_TIM1);
rcc_periph_clock_enable(RCC_DMA1);
}
static void systick_setup(void) {
rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
systick_set_frequency(1000, rcc_ahb_frequency); // 1 ms
systick_counter_enable();
systick_interrupt_enable();
}
void gpio_setup(void) {
// builtin led
gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL,
GPIO13);
gpio_set(GPIOC, GPIO13);
// power en
gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL,
GPIO8);
// power, light, speed buttons
gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN,
GPIO5 | GPIO6 | GPIO7);
gpio_set(GPIOA, GPIO5 | GPIO6 | GPIO7);
// set, beep buttons
gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN,
GPIO0 | GPIO1);
gpio_set(GPIOB, GPIO0 | GPIO1);
// adc
gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_ANALOG,
GPIO0 | GPIO1 | GPIO2);
// usart1
gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
GPIO_USART1_TX);
gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN,
GPIO_USART1_RX);
gpio_set(GPIOA, GPIO_USART1_RX);
// usart3
gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
GPIO_USART3_TX);
gpio_set_mode(GPIOB, GPIO_MODE_INPUT, GPIO_CNF_INPUT_PULL_UPDOWN,
GPIO_USART3_RX);
gpio_set(GPIOB, GPIO_USART3_RX);
// i2c1
gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN,
GPIO_I2C1_SCL | GPIO_I2C1_SDA);
// buzzer pwm
gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
GPIO_TIM1_CH4);
// light
gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL,
GPIO8);
}
static void i2c_setup(void) {
i2c_reset(I2C1);
i2c_peripheral_disable(I2C1);
i2c_enable_ack(I2C1);
i2c_set_fast_mode(I2C1);
i2c_set_speed(I2C1, i2c_speed_fm_400k, rcc_apb1_frequency / 1000000);
i2c_peripheral_enable(I2C1);
}
static void pwm_setup(void) {
rcc_periph_reset_pulse(RST_TIM1);
// timer mode: no divider (72MHz), edge aligned, upcounting
timer_set_mode(TIM1, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP);
timer_set_prescaler(TIM1, BUZZER_PRESCALER-1);
// PWM mode 1 (output high if CNT > CCR1)
timer_set_oc_mode(TIM1, TIM_OC4, TIM_OCM_PWM1);
timer_enable_oc_preload(TIM1, TIM_OC4);
timer_enable_preload(TIM1);
timer_enable_break_main_output(TIM1);
timer_disable_oc_output(TIM1, TIM_OC4);
// timer_enable_oc_output(TIM1, TIM_OC4);
timer_enable_counter(TIM1);
}
void dma1_channel1_isr(void) {
dma_clear_interrupt_flags(DMA1, DMA_CHANNEL1, DMA_IFCR_CGIF1);
}
static void adc_setup_dma(void) {
uint8_t channel_seq[16];
dma_disable_channel(DMA1, DMA_CHANNEL1);
dma_enable_circular_mode(DMA1, DMA_CHANNEL1);
dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL1);
dma_set_peripheral_size(DMA1, DMA_CHANNEL1, DMA_CCR_PSIZE_16BIT);
dma_set_memory_size(DMA1, DMA_CHANNEL1, DMA_CCR_MSIZE_16BIT);
dma_set_read_from_peripheral(DMA1, DMA_CHANNEL1);
dma_set_peripheral_address(DMA1, DMA_CHANNEL1, (uint32_t)&ADC_DR(ADC1));
dma_set_memory_address(DMA1, DMA_CHANNEL1, (uint32_t)&adc_buf);
dma_set_number_of_data(DMA1, DMA_CHANNEL1, 3);
dma_enable_transfer_complete_interrupt(DMA1, DMA_CHANNEL1);
dma_enable_channel(DMA1, DMA_CHANNEL1);
adc_power_off(ADC1);
adc_enable_scan_mode(ADC1);
adc_set_continuous_conversion_mode(ADC1);
adc_disable_discontinuous_mode_regular(ADC1);
adc_enable_external_trigger_regular(ADC1, ADC_CR2_EXTSEL_SWSTART);
adc_set_right_aligned(ADC1);
adc_set_sample_time_on_all_channels(ADC1, ADC_SMPR_SMP_7DOT5CYC);
adc_power_on(ADC1);
nops(800000);
adc_reset_calibration(ADC1);
adc_calibrate(ADC1);
channel_seq[0] = ADC_CHANNEL0;
channel_seq[1] = ADC_CHANNEL1;
channel_seq[2] = ADC_CHANNEL2;
adc_set_regular_sequence(ADC1, 3, channel_seq);
adc_enable_dma(ADC1);
nops(100);
adc_start_conversion_regular(ADC1);
}
static void usart_setup(void) {
nvic_enable_irq(NVIC_USART1_IRQ);
usart_set_baudrate(USART1, 9600);
usart_set_databits(USART1, 8);
usart_set_stopbits(USART1, USART_STOPBITS_1);
usart_set_mode(USART1, USART_MODE_TX_RX);
usart_set_parity(USART1, USART_PARITY_NONE);
usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
usart_enable_rx_interrupt(USART1);
usart_enable(USART1);
usart_set_baudrate(USART3, 9600);
usart_set_databits(USART3, 8);
usart_set_stopbits(USART3, USART_STOPBITS_1);
usart_set_mode(USART3, USART_MODE_TX_RX);
usart_set_parity(USART3, USART_PARITY_NONE);
usart_set_flow_control(USART3, USART_FLOWCONTROL_NONE);
usart_enable(USART3);
}
/* printf redirect */
int _write(int file, char *ptr, int len) {
int i;
if (file == 1) {
for (i = 0; i < len; i++) {
usart_send_blocking(USART3, ptr[i]);
}
return i;
}
errno = EIO;
return -1;
}
// extern void initialise_monitor_handles(void);
void hardware_setup(void) {
enable_peripherals();
systick_setup();
gpio_setup();
power_enable();
adc_setup_dma();
usart_setup();
i2c_setup();
pwm_setup();
iwdg_set_period_ms(2000); // 2s watchdog
iwdg_start();
}

66
src/hardware.h Normal file

@ -0,0 +1,66 @@
#ifndef HARDWARE_H
#define HARDWARE_H
#include <libopencm3/cm3/systick.h>
#include <libopencm3/stm32/adc.h>
#include <libopencm3/stm32/dma.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/usart.h>
#include <libopencm3/stm32/spi.h>
#include <libopencm3/stm32/i2c.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/stm32/iwdg.h>
#include <libopencm3/cm3/nvic.h>
void hardware_setup(void);
uint32_t millis(void);
void msleep(uint32_t ms);
/**
* @brief Outputs frequency to the buzzer
*
* Do not set frequency lower than 150 Hz
*
* @param freq Frequency in hertz
* @param volume 0-100
*/
void buzzer_freq_vol(uint16_t freq, uint8_t volume);
void beep_blocking(uint16_t freq, uint16_t ms, uint8_t volume);
void beep(uint16_t freq, uint16_t ms, uint8_t volume);
uint16_t throttle_adc_value(void);
uint16_t brake_adc_value(void);
/// 0-1000, based on min and max value
uint16_t throttle_value_normalized(void);
/// Get throttle trigger value in range from 0 to speed limit
float throttle_value_to_kmh(void);
/// 0-1000, based on min and max value
uint16_t brake_value_normalized(void);
// No floating point, 39.2v is 392
uint16_t battery_value(void);
#define power_enable() gpio_set(GPIOB, GPIO8)
#define power_disable() do { gpio_clear(GPIOB, GPIO8); while(1){} } while(0)
#define light_on() gpio_set(GPIOA, GPIO8)
#define light_off() gpio_clear(GPIOA, GPIO8)
#define light_toggle() gpio_toggle(GPIOA, GPIO8)
#define set_button_pressed() (!gpio_get(GPIOB, GPIO0))
#define power_button_pressed() (!gpio_get(GPIOA, GPIO7))
#define light_button_pressed() (!gpio_get(GPIOA, GPIO6))
#ifdef SWAP_BEEP_AND_SPEED
#define beep_button_pressed() (!gpio_get(GPIOA, GPIO5))
#define speed_button_pressed() (!gpio_get(GPIOB, GPIO1))
#else
#define beep_button_pressed() (!gpio_get(GPIOB, GPIO1))
#define speed_button_pressed() (!gpio_get(GPIOA, GPIO5))
#endif
#endif /* HARDWARE_H */

69
src/keyboard.c Normal file

@ -0,0 +1,69 @@
#include "keyboard.h"
#include "hardware.h"
static uint16_t last_key_event = KB_EVT_NONE;
static uint32_t beep_btn_pressed_at = 0;
static uint32_t set_btn_pressed_at = 0;
static uint32_t power_btn_pressed_at = 0;
static uint32_t light_btn_pressed_at = 0;
static uint32_t speed_btn_pressed_at = 0;
static uint8_t check_key_event(uint8_t key_state, uint8_t key_id,
uint32_t *pressed_at) {
uint32_t ms_presssed = millis() - *pressed_at;
if (key_state) {
if (*pressed_at == 0) {
*pressed_at = millis();
last_key_event = key_id | KB_EVT_TYPE_KEYDOWN;
} else if ((ms_presssed > KEY_REPEAT_DELAY) &&
(ms_presssed > (KEY_REPEAT_INTERVAL + KEY_REPEAT_DELAY))) {
*pressed_at += KEY_REPEAT_INTERVAL;
last_key_event = key_id | KB_EVT_TYPE_REPEAT;
}
return 1;
} else if (!key_state && *pressed_at > 0) {
last_key_event = key_id | KB_EVT_TYPE_KEYUP;
*pressed_at = 0;
return 1;
}
return 0;
}
void keyboard_poll(void) {
if (last_key_event != KB_EVT_NONE) { // no poll if last event unprocessed
return;
}
if (check_key_event(beep_button_pressed(), KB_EVT_KEY_BEEP,
&beep_btn_pressed_at)) {
return;
}
if (check_key_event(set_button_pressed(), KB_EVT_KEY_SET,
&set_btn_pressed_at)) {
return;
}
if (check_key_event(power_button_pressed(), KB_EVT_KEY_POWER,
&power_btn_pressed_at)) {
return;
}
if (check_key_event(light_button_pressed(), KB_EVT_KEY_LIGHT,
&light_btn_pressed_at)) {
return;
}
if (check_key_event(speed_button_pressed(), KB_EVT_KEY_SPEED,
&speed_btn_pressed_at)) {
return;
}
}
uint8_t keyboard_pop_event(void)
{
uint8_t val = last_key_event;
last_key_event = KB_EVT_NONE;
return val;
}

26
src/keyboard.h Normal file

@ -0,0 +1,26 @@
#ifndef KEYBOARD_H
#define KEYBOARD_H
#include <stdint.h>
#define KEY_REPEAT_DELAY 750
#define KEY_REPEAT_INTERVAL 100
#define KB_EVT_MASK_KEYS 0x1f
#define KB_EVT_NONE 0x00
#define KB_EVT_KEY_BEEP 0x01
#define KB_EVT_KEY_SET 0x02
#define KB_EVT_KEY_POWER 0x04
#define KB_EVT_KEY_LIGHT 0x08
#define KB_EVT_KEY_SPEED 0x10
#define KB_EVT_TYPE_KEYDOWN 0x20
#define KB_EVT_TYPE_REPEAT 0x40
#define KB_EVT_TYPE_KEYUP 0x80
void keyboard_poll(void);
uint8_t keyboard_pop_event(void);
#endif /* KEYBOARD_H */

149
src/kugoo_s3.c Normal file

@ -0,0 +1,149 @@
#include "kugoo_s3.h"
#include "hardware.h"
#include "persistence.h"
#include "utils.h"
#include <stdio.h>
volatile struct kugoo_s3_packet_in kugoo_s3_rx;
struct kugoo_s3_packet_out kugoo_s3_tx;
volatile uint8_t kugoo_s3_tx_ready = false;
static volatile uint8_t packet_in_buf[8];
static volatile uint8_t packet_in_buf_pos = 0;
static volatile uint32_t last_packet_time = 0;
void kugoo_s3_packet_init(struct kugoo_s3_packet_out *packet) {
packet->header = KUGOO_S3_PACKET_OUT_HEADER;
packet->features = KUGOO_S3_FEATURE_UNLOCK;
packet->n_a_1 = 0U;
packet->magnets_count = storage.magnets_count;
packet->config_batt_voltage1 = KUGOO_S3_BATTERY_VOLTAGE;
packet->brake = 0U;
packet->throttle = 0U;
packet->config_batt_voltage2 = KUGOO_S3_BATTERY_VOLTAGE;
packet->n_a_2 = 0U;
packet->speed_limit = 0U;
packet->speed_level = 3U;
}
static void kugoo_s3_packet_update_checksum(struct kugoo_s3_packet_out *p) {
uint8_t *ptr = (uint8_t*) p;
p->checksum = 0;
for(uint8_t i = 0; i < sizeof(struct kugoo_s3_packet_out) - 1; ++i) {
p->checksum ^= *ptr;
++ptr;
}
}
void kugoo_s3_packet_send(struct kugoo_s3_packet_out *p) {
kugoo_s3_packet_update_checksum(p);
usart_send_blocking(KUGOO_S3_USART, p->header);
usart_send_blocking(KUGOO_S3_USART, p->features);
usart_send_blocking(KUGOO_S3_USART, p->n_a_1);
usart_send_blocking(KUGOO_S3_USART, p->magnets_count);
usart_send_blocking(KUGOO_S3_USART, (p->config_batt_voltage1 >> 8) & 0xFF);
usart_send_blocking(KUGOO_S3_USART, p->config_batt_voltage1 & 0xFF);
usart_send_blocking(KUGOO_S3_USART, (p->brake >> 8) & 0xFF);
usart_send_blocking(KUGOO_S3_USART, p->brake & 0xFF);
usart_send_blocking(KUGOO_S3_USART, (p->throttle >> 8) & 0xFF);
usart_send_blocking(KUGOO_S3_USART, p->throttle & 0xFF);
usart_send_blocking(KUGOO_S3_USART, (p->config_batt_voltage2 >> 8) & 0xFF);
usart_send_blocking(KUGOO_S3_USART, p->config_batt_voltage2 & 0xFF);
usart_send_blocking(KUGOO_S3_USART, p->n_a_2);
usart_send_blocking(KUGOO_S3_USART, p->speed_limit);
usart_send_blocking(KUGOO_S3_USART, p->speed_level);
usart_send_blocking(KUGOO_S3_USART, p->checksum);
//printf("%X %X\r\n", (p->throttle >> 8) & 0xFF, p->throttle & 0xFF);
}
float kugoo_s3_get_speed(void) {
// last_packet.ms_per_rev - время в миллисекундах, за которое колесо делает полный
// оборот диаметр колеса - 19 см 1 оборот - 2*PI*r = 2 * 3.141592653589793 *
// (19/2)
// = 59.690260418206066 см = 0.5969026041820606м
// допустим, время оборота = 500мс, 0.5969м за 500мс
// важно умножить сначала на 1000, а не на (1000/500), чтобы не потерять
// точность сантиметры в секунду = (59.69 * 1000) / 500 = 119.38 см/с
// Как перевести метры в секунду в километры в час:
// Нужно 1 м/с разделить на 1000 (количество метров в километре)
// и умножить на 3600 (количество секунд в часе) получаем 3.6 километра в час;
// ((59.69 * 1000 / (double)last_packet.ms_per_rev) / 100.0) * 3.6;
// упрощаем
// (((596.9mm * 10) / last_packet.ms_per_rev * 36) / 100)
// или для float
// (596.9mm / (float) last_packet.ms_per_rev) * 3.6f
// ух.
if (kugoo_s3_rx.ms_per_rev == 0) { // is this possible?
return 0;
} else if (kugoo_s3_rx.ms_per_rev == KUGOO_S3_SPEED_VAL_STOPPED) {
return 0;
}
return ((float) storage.wheel_length_mm / (float) kugoo_s3_rx.ms_per_rev) * 3.6f;
}
void kugoo_s3_set_speed_approx(float kmh) {
float new_throttle = 0;
if(kmh <= 0) {
kugoo_s3_tx.throttle = 0;
return;
}
new_throttle = CONTROLLER_STOPPED_VAL + kmh * THROTTLE_TO_SPEED_COEFF;
kugoo_s3_tx.throttle = clamp_u16((uint16_t)new_throttle, 0, CONTROLLER_TRIGGER_MAX);
}
static inline void update_distance(void) {
if (kugoo_s3_rx.ms_per_rev == 0) {
return;
} else if (kugoo_s3_rx.ms_per_rev == KUGOO_S3_SPEED_VAL_STOPPED) {
return;
}
// получаем сантиметры в секунду как описано выше,
// но так как мы получаем пакеты чаще, чем 1 раз в секунду,
// то делим на количество пакетов в секунду
uint32_t cm =
((storage.wheel_length_mm * 100) / kugoo_s3_rx.ms_per_rev) /
KUGOO_S3_PACKETS_IN_PER_SECOND;
session_add_distance(cm);
}
inline uint32_t kugoo_s3_last_packet_time(void) { return last_packet_time; }
void kugoo_s3_byte_received(uint8_t b) {
static uint16_t ms_per_rev = 0;
if (b == KUGOO_S3_PACKET_IN_HEADER) {
packet_in_buf_pos = 0;
} else if (++packet_in_buf_pos > 7) {
packet_in_buf_pos = 7;
}
packet_in_buf[packet_in_buf_pos] = b;
// @todo ?
if (packet_in_buf_pos == 7) {
uint8_t crc = 0;
for (uint8_t i = 0; i < 7; i++) {
crc ^= packet_in_buf[i];
}
if (crc == packet_in_buf[7]) {
kugoo_s3_rx.header = packet_in_buf[0];
kugoo_s3_rx.state = packet_in_buf[1];
kugoo_s3_rx.n_a_1 = packet_in_buf[2];
kugoo_s3_rx.current =
(((uint16_t)packet_in_buf[3]) << 8) | packet_in_buf[4];
ms_per_rev = (((uint16_t)packet_in_buf[5]) << 8) | packet_in_buf[6];
if (ms_per_rev > 20) { // 20ms is ~100km/h (trash measured or you are an idiot)
kugoo_s3_rx.ms_per_rev = ms_per_rev;
}
kugoo_s3_rx.checksum = crc;
last_packet_time = millis();
update_distance();
kugoo_s3_tx_ready = true;
}
}
}

92
src/kugoo_s3.h Normal file

@ -0,0 +1,92 @@
#ifndef KUGOO_S3_H
#define KUGOO_S3_H
#include <stdint.h>
#define KUGOO_S3_USART USART1
#define KUGOO_S3_PACKET_OUT_HEADER 0x2F
#define KUGOO_S3_PACKET_IN_HEADER 0x28
#define KUGOO_S3_SPEED_VAL_STOPPED 0xbb8
/// 1000ms in second / 200ms period, for speed calculation
#define KUGOO_S3_PACKETS_IN_PER_SECOND 5
enum kugoo_s3_battery_voltage_t {
KUGOO_S3_BATTERY_VOLTAGE_24V = 0x00D2,
KUGOO_S3_BATTERY_VOLTAGE_36V = 0x0136,
KUGOO_S3_BATTERY_VOLTAGE_48V = 0x019A
};
#define KUGOO_S3_BATTERY_VOLTAGE KUGOO_S3_BATTERY_VOLTAGE_36V
enum kugoo_s3_state_t {
KUGOO_S3_STATE_MOTOR_ERROR = 0x01,
KUGOO_S3_STATE_THROTTLE_LOCKED = 0x02, ///< Not an error
KUGOO_S3_STATE_OVERCURRENT = 0x08
};
enum kugoo_s3_feature_t {
KUGOO_S3_FEATURE_UNLOCK = 0x01,
KUGOO_S3_FEATURE_ZERO_START = 0x02,
KUGOO_S3_FEATURE_REAR_LIGHT = 0x04,
// KUGOO_S3_FEATURE_4 = 0x08,
// KUGOO_S3_FEATURE_5 = 0x10,
// KUGOO_S3_FEATURE_6 = 0x20,
// KUGOO_S3_FEATURE_7 = 0x40,
// KUGOO_S3_FEATURE_8 = 0x80,
};
#pragma pack(push, 1)
/// Packet from me to the wheel controller
struct kugoo_s3_packet_out {
uint8_t header; ///< @see KUGOO_S3_PACKET_OUT_HEADER
uint8_t features; ///< @see kugoo_s3_feature_t
uint8_t n_a_1;
uint8_t magnets_count;
uint16_t config_batt_voltage1; ///< @see kugoo_s3_battery_voltage_t
uint16_t brake; ///< 0-1000
uint16_t throttle; ///< 0-1000
uint16_t config_batt_voltage2; ///< @see kugoo_s3_battery_voltage_t
uint8_t n_a_2;
uint8_t speed_limit; ///< Unused
uint8_t speed_level; ///< 1-3
uint8_t checksum; ///< All bytes XOR
};
/// Packet from wheel controller to me
struct kugoo_s3_packet_in {
uint8_t header; ///< @see KUGOO_S3_PACKET_IN _HEADER
uint8_t state; ///< @see kugoo_s3_state_t
uint8_t n_a_1;
uint16_t current; ///< Current * 10 (95 = 9.5A)
uint16_t ms_per_rev; ///< Milliseconds per full wheel revolution
uint8_t checksum; ///< All bytes XOR
};
#pragma pack(pop)
/// Last received packet
extern volatile struct kugoo_s3_packet_in kugoo_s3_rx;
/// Last sent packet
extern struct kugoo_s3_packet_out kugoo_s3_tx;
/// Sets to true when packet successfully processed
extern volatile uint8_t kugoo_s3_tx_ready;
void kugoo_s3_packet_init(struct kugoo_s3_packet_out *packet);
void kugoo_s3_packet_send(struct kugoo_s3_packet_out *packet);
void kugoo_s3_try_persist_distance(void);
void kugoo_s3_force_persist_distance(void);
void kugoo_s3_byte_received(uint8_t b);
/// fuck everything, I want to float
float kugoo_s3_get_speed(void);
/// @see KUGOO_S3_SPEED_COEFF
void kugoo_s3_set_speed_approx(float kmh);
uint32_t kugoo_s3_last_packet_time(void);
#endif /* KUGOO_S3_H */

231
src/main.c Normal file

@ -0,0 +1,231 @@
#include <stdio.h>
#include <string.h>
#include "globals.h"
#include "gui.h"
#include "hardware.h"
#include "kugoo_s3.h"
#include "persistence.h"
#include "ssd1306.h"
#include "utils.h"
void usart1_isr(void) {
// while data is not empty
while (USART_SR(USART1) & USART_SR_RXNE) {
kugoo_s3_byte_received(usart_recv(USART1));
}
}
int main(void) {
reset_reason = RCC_CSR;
RCC_CSR |= RCC_CSR_RMVF;
memset((void *)&kugoo_s3_rx, 0, sizeof(struct kugoo_s3_packet_in));
uint32_t next_packet_send_time;
uint32_t next_logic_refresh;
uint32_t next_throttle_lock_cancellation = 0;
uint16_t throttle_trigger = 0;
uint8_t prev_throttle_lock_state = 0;
uint8_t throttle_locked = 0;
uint8_t zero_packets_left = 0; // anti throttle lock zero packets
enum control_state_t control_state = CS_NORMAL;
float desired_speed_kmh = 0;
float control_output_kmh = 0;
float speed_error = 0;
float current_error = 0;
float speed = 0;
hardware_setup();
eeprom_read_and_validate();
beep_blocking(1000, 10, storage.keys_volume);
ssd1306_init();
ssd1306_contrast(0xf0);
ssd1306_blend_mode(SSD1306_BLEND_MODE_LIGHTEN);
ssd1306_fill(0xff); // screen test
ssd1306_redraw();
msleep(200);
if (beep_button_pressed()) {
ssd1306_clear();
ssd1306_string("СБРОС НАСТРОЕК\n\n"
"СОХРАНИТЬ ОДОМЕТР:\n"
"SET\n\n"
"СТЕРЕТЬ ВСЁ:\n"
"LIGHT");
ssd1306_redraw();
while (1) {
if (set_button_pressed()) {
uint32_t distance = storage.total_distance_meters;
eeprom_reset_all(false);
storage.total_distance_meters = distance;
eeprom_write_all();
beep_blocking(2000, 300, STATIC_BEEP_VOLUME);
power_disable();
} else if (light_button_pressed()) {
eeprom_reset_all(true);
beep_blocking(2000, 300, STATIC_BEEP_VOLUME);
power_disable();
} else if (power_button_pressed()) {
break;
}
msleep(20);
}
}
while (power_button_pressed()) {
msleep(20);
}
kugoo_s3_packet_init(&kugoo_s3_tx);
next_packet_send_time = millis() + PACKET_SEND_INTERVAL_MS;
next_logic_refresh = millis() + LOGIC_REFRESH_INTERVAL_MS;
beep_blocking(2000, 10, storage.keys_volume);
while (1) {
// update only when new speed and current received
if (kugoo_s3_tx_ready) {
kugoo_s3_tx_ready = false;
// control loop
speed = kugoo_s3_get_speed();
speed_error = desired_speed_kmh - speed;
current_error = storage.current_limit - ((float) kugoo_s3_rx.current / 10.0f);
// when current overflows maximum current, switch to current limiting
// instead of speed stabilization
if(control_state == CS_NORMAL) {
if (storage.current_limit_enabled &&
storage.current_limit > 0 &&
current_error < 0) {
control_output_kmh += current_error * storage.current_stabilization_Kp;
} else if(storage.speed_stabilization_enabled) {
control_output_kmh += speed_error * storage.speed_stabilization_Kp;
}
}
try_persist_distance();
}
if (millis() >= next_logic_refresh) {
next_logic_refresh = millis() + LOGIC_REFRESH_INTERVAL_MS;
iwdg_reset();
process_events();
speed = kugoo_s3_get_speed();
if(gui_is_view(GUI_VIEW_TRIGGER_CALIBRATION)){
throttle_trigger = 0;
kugoo_s3_tx.brake = 0;
} else {
throttle_trigger = throttle_value_normalized();
kugoo_s3_tx.brake = brake_value_normalized();
}
throttle_locked = kugoo_s3_rx.state & KUGOO_S3_STATE_THROTTLE_LOCKED;
if (prev_throttle_lock_state != throttle_locked) {
prev_throttle_lock_state = throttle_locked;
if (throttle_locked && !storage.anti_throttle_lock) {
beep(2000, 10, storage.signals_volume);
}
}
// factory cruise control cancellation
// sometimes doesn't work
// fuck this
if (throttle_locked && storage.anti_throttle_lock && millis() > next_throttle_lock_cancellation) {
if(control_state == CS_NORMAL) {
next_throttle_lock_cancellation = millis() + ANTI_THROTTLE_LOCK_PERIOD_MS;
control_state = CS_FORCE_ZERO_THROTTLE;
zero_packets_left = ANTI_THROTTLE_LOCK_ZERO_PACKETS;
}
}
// todo: reduce complexity
if (control_state == CS_THROTTLE_RECOVER) {
desired_speed_kmh += THROTTLE_RECOVER_INCREMENT;
if (cruise_ctl_status == CRUISE_CTL_DISABLED) {
if (desired_speed_kmh >= throttle_value_to_kmh()) {
desired_speed_kmh = throttle_value_to_kmh();
control_state = CS_NORMAL;
}
} else {
if (desired_speed_kmh >= cruise_ctl_speed_kmh) {
desired_speed_kmh = cruise_ctl_speed_kmh;
control_state = CS_NORMAL;
}
}
} else if (cruise_ctl_status == CRUISE_CTL_DISABLED) {
if (throttle_trigger > 10) { // throttle trigger pressed
if (control_state == CS_FORCE_ZERO_THROTTLE) {
// do nothing
} else if (storage.soft_start_enabled) {
if (desired_speed_kmh < speed / 2) {
desired_speed_kmh =
speed / 2; // do not start with zero when speed > 0
} else if (desired_speed_kmh + storage.soft_start_increment <=
throttle_value_to_kmh()) {
desired_speed_kmh += storage.soft_start_increment;
} else {
desired_speed_kmh = throttle_value_to_kmh();
}
} else {
desired_speed_kmh = throttle_value_to_kmh();
}
} else { // throttle trigger not pressed
desired_speed_kmh = 0;
control_output_kmh = 0;
}
} else if (cruise_ctl_status == CRUISE_CTL_WAITING_RELEASE && throttle_trigger < 50) {
desired_speed_kmh = cruise_ctl_speed_kmh;
cruise_ctl_status = CRUISE_CTL_ENABLED;
} else if (cruise_ctl_status == CRUISE_CTL_ENABLED && throttle_trigger > 50) {
desired_speed_kmh = cruise_ctl_speed_kmh;
cruise_ctl_status = CRUISE_CTL_DISABLED;
beep(2000, 10, storage.signals_volume);
}
if (kugoo_s3_tx.brake > 50) {
cruise_ctl_status = CRUISE_CTL_DISABLED;
control_output_kmh = 0;
}
// Do not run throttle lock cancellation while idle
if(desired_speed_kmh == 0) {
next_throttle_lock_cancellation = millis() + ANTI_THROTTLE_LOCK_PERIOD_MS;
}
if(control_state == CS_FORCE_ZERO_THROTTLE && zero_packets_left > 0){
kugoo_s3_tx.throttle = 0;
if(--zero_packets_left == 0) {
control_state = CS_NORMAL;
}
} else {
kugoo_s3_set_speed_approx(desired_speed_kmh + control_output_kmh);
}
kugoo_s3_tx.features = KUGOO_S3_FEATURE_UNLOCK;
if (storage.zero_start_enabled) {
kugoo_s3_tx.features |= KUGOO_S3_FEATURE_ZERO_START;
}
}
if (millis() >= next_packet_send_time) {
next_packet_send_time = millis() + PACKET_SEND_INTERVAL_MS;
kugoo_s3_packet_send(&kugoo_s3_tx);
// printf("%d;%d;%d;%d;%d\r\n", throttle, (int)desired_speed_kmh, kugoo_s3_tx.throttle, (int)kugoo_s3_get_speed(), (int)(kugoo_s3_rx.current/10));
}
gui_redraw();
}
return 0;
}

317
src/persistence.c Normal file

@ -0,0 +1,317 @@
#include "persistence.h"
#include "hardware.h"
#include "globals.h"
#include "utils.h"
#include <libopencm3/stm32/i2c.h>
#include <string.h>
#define EEPROM_HEADER_SIZE 6
static const uint8_t eeprom_valid_header[EEPROM_HEADER_SIZE] = {
0x4C, 0x4F, 0x56, 0x45, 0x4C, 0x59};
struct eeprom_data_t storage;
static volatile uint32_t current_session_distance_cm = 0;
static volatile uint32_t distance_cm_accumulator = 0;
struct float_config_entry_t cfg_speed_stabilization_Kp = {
.ptr = &storage.speed_stabilization_Kp,
._min = 0.0f, ._max = 1.0f, ._default = 0.14f,
};
struct float_config_entry_t cfg_current_stabilization_Kp = {
.ptr = &storage.current_stabilization_Kp,
._min = 0.0f, ._max = 5.0f, ._default = 1.0f
};
struct float_config_entry_t cfg_current_limit = {
.ptr = &storage.current_limit,
._min = 0.0f, ._max = 100.0f, ._default = 6.0f
};
struct float_config_entry_t cfg_soft_start_increment = {
.ptr = &storage.soft_start_increment,
._min = 0.1f, ._max = 1.0f, ._default = 0.5f // 0.5 km/h every 50ms
};
struct u16_config_entry_t cfg_throttle_trigger_min_value = {
.ptr = &storage.throttle_trigger_min_value,
._min = 500, ._max = 4096, ._default = 1200
};
struct u16_config_entry_t cfg_brake_trigger_min_value = {
.ptr = &storage.brake_trigger_min_value,
._min = 500, ._max = 4096, ._default = 1200
};
struct u16_config_entry_t cfg_throttle_trigger_max_value = {
.ptr = &storage.throttle_trigger_max_value,
._min = 500, ._max = 4096, ._default = 3050
};
struct u16_config_entry_t cfg_brake_trigger_max_value = {
.ptr = &storage.brake_trigger_max_value,
._min = 500, ._max = 4096, ._default = 3050
};
struct u16_config_entry_t cfg_wheel_length_mm = {
.ptr = &storage.wheel_length_mm,
._min = 300, ._max = 4470, ._default = 597
};
struct u16_config_entry_t cfg_magnets_count = {
.ptr = &storage.magnets_count,
._min = 2, ._max = 1000, ._default = 30
};
struct u16_config_entry_t cfg_keys_volume = {
.ptr = &storage.keys_volume,
._min = 0, ._max = 100, ._default = 16
};
struct u16_config_entry_t cfg_signals_volume = {
.ptr = &storage.signals_volume,
._min = 0, ._max = 100, ._default = 255
};
static inline void validate_cfg_float(struct float_config_entry_t *entry) {
*entry->ptr = clamp_float(*entry->ptr, entry->_min, entry->_max);
}
static inline void validate_cfg_u16(struct u16_config_entry_t *entry) {
*entry->ptr = clamp_float(*entry->ptr, entry->_min, entry->_max);
}
static inline void reset_cfg_float(struct float_config_entry_t *entry) {
*entry->ptr = entry->_default;
}
static inline void reset_cfg_u16(struct u16_config_entry_t *entry) {
*entry->ptr = entry->_default;
}
static void eeprom_write(uint8_t addr, const void *ptr, uint8_t count) {
// Mostly copy-pasted from i2c_write7_v1,
// but data now leading with address byte
while ((I2C_SR2(EEPROM_I2C_BUS) & I2C_SR2_BUSY)) {
}
i2c_send_start(EEPROM_I2C_BUS);
// Wait for the end of the start condition, master mode selected, and BUSY
// bit set
while (!((I2C_SR1(EEPROM_I2C_BUS) & I2C_SR1_SB) &&
(I2C_SR2(EEPROM_I2C_BUS) & I2C_SR2_MSL) &&
(I2C_SR2(EEPROM_I2C_BUS) & I2C_SR2_BUSY))) {
}
i2c_send_7bit_address(EEPROM_I2C_BUS, EEPROM_I2C_ADDRESS, I2C_WRITE);
// Waiting for address is transferred.
while (!(I2C_SR1(EEPROM_I2C_BUS) & I2C_SR1_ADDR)) {
}
// Clearing ADDR condition sequence.
(void)I2C_SR2(EEPROM_I2C_BUS);
i2c_send_data(EEPROM_I2C_BUS, addr);
while (!(I2C_SR1(EEPROM_I2C_BUS) & (I2C_SR1_BTF))) {
}
const uint8_t *byte_ptr = ptr;
for (uint8_t i = 0; i < count; i++) {
i2c_send_data(EEPROM_I2C_BUS, *byte_ptr);
while (!(I2C_SR1(EEPROM_I2C_BUS) & (I2C_SR1_BTF))) {
}
++byte_ptr;
}
i2c_send_stop(EEPROM_I2C_BUS);
msleep(15); // see maximum write time in datasheet
}
void eeprom_set_bytes(uint8_t addr, const void *ptr, uint8_t count) {
const uint8_t *bytes = (uint8_t *)ptr;
uint8_t page_offset = addr % EEPROM_PAGE_SIZE;
uint8_t bytes_to_write;
while (count > 0) {
if (count < EEPROM_PAGE_SIZE - page_offset) {
bytes_to_write = count;
} else {
bytes_to_write = EEPROM_PAGE_SIZE - page_offset;
}
eeprom_write(addr, bytes, bytes_to_write);
page_offset = 0;
addr += bytes_to_write;
bytes += bytes_to_write;
count -= bytes_to_write;
}
}
void eeprom_set_byte(uint8_t addr, uint8_t data) {
uint8_t tmp[] = {addr, data};
i2c_transfer7(EEPROM_I2C_BUS, EEPROM_I2C_ADDRESS, tmp, 2, NULL, 0);
}
void eeprom_read_bytes(uint8_t addr, void *ptr, uint8_t count) {
uint8_t mem_addr[1] = {addr};
i2c_transfer7(EEPROM_I2C_BUS, EEPROM_I2C_ADDRESS, mem_addr, 1, (uint8_t *)ptr,
count);
}
void eeprom_read_and_validate(void) {
uint8_t eeprom_read_header[EEPROM_HEADER_SIZE];
eeprom_read_bytes(0, eeprom_read_header, EEPROM_HEADER_SIZE);
if(memcmp(eeprom_read_header, eeprom_valid_header, EEPROM_HEADER_SIZE) != 0) {
eeprom_reset_all(true);
beep(200, 1000, 255);
}
eeprom_read_bytes(EEPROM_DATA_START_ADDRESS, &storage, STORAGE_SIZE);
if (storage.calibrate_triggers_on_startup){
storage.brake_trigger_min_value = brake_adc_value() + TRIGGER_ANTI_JITTER;
storage.throttle_trigger_min_value = throttle_adc_value() + TRIGGER_ANTI_JITTER;
}
validate_cfg_float(&cfg_current_limit);
validate_cfg_float(&cfg_speed_stabilization_Kp);
validate_cfg_u16(&cfg_brake_trigger_min_value);
validate_cfg_u16(&cfg_throttle_trigger_min_value);
validate_cfg_u16(&cfg_brake_trigger_max_value);
validate_cfg_u16(&cfg_throttle_trigger_max_value);
validate_cfg_u16(&cfg_wheel_length_mm);
validate_cfg_u16(&cfg_magnets_count);
validate_cfg_u16(&cfg_keys_volume);
validate_cfg_u16(&cfg_signals_volume);
storage.speed_limit_last = clamp_u8(storage.speed_limit_last, 5, 100);
// this is bad
if (storage.brake_trigger_max_value <= storage.brake_trigger_min_value) {
storage.brake_trigger_max_value = storage.brake_trigger_min_value + 100;
}
if (storage.throttle_trigger_max_value <= storage.throttle_trigger_min_value) {
storage.throttle_trigger_max_value = storage.throttle_trigger_min_value + 100;
}
if(!storage.persist_speed_limit) {
storage.speed_limit_last = DEFAULT_SPEED_LIMIT_KMH;
}
}
void eeprom_reset_all(uint8_t commit) {
storage.contrast = 0xff;
storage.soft_start_enabled = 0;
storage.zero_start_enabled = 0;
storage.current_limit_enabled = 0;
storage.speed_stabilization_enabled = 0;
storage.total_distance_meters = 0;
storage.anti_throttle_lock = 1;
storage.mute = 0;
storage.calibrate_triggers_on_startup = 1;
storage.speed_limit_last = DEFAULT_SPEED_LIMIT_KMH;
storage.persist_speed_limit = 0;
reset_cfg_u16(&cfg_brake_trigger_min_value);
reset_cfg_u16(&cfg_throttle_trigger_min_value);
reset_cfg_u16(&cfg_brake_trigger_max_value);
reset_cfg_u16(&cfg_throttle_trigger_max_value);
reset_cfg_u16(&cfg_wheel_length_mm);
reset_cfg_u16(&cfg_magnets_count);
reset_cfg_u16(&cfg_keys_volume);
reset_cfg_u16(&cfg_signals_volume);
reset_cfg_float(&cfg_speed_stabilization_Kp);
reset_cfg_float(&cfg_current_limit);
reset_cfg_float(&cfg_current_stabilization_Kp);
reset_cfg_float(&cfg_soft_start_increment);
memset(&storage.last_trips, 0U, sizeof(storage.last_trips));
if (commit) {
eeprom_write_all();
}
}
void eeprom_write_all(void) {
eeprom_set_bytes(0, eeprom_valid_header, EEPROM_HEADER_SIZE);
eeprom_set_bytes(EEPROM_DATA_START_ADDRESS, &storage, STORAGE_SIZE);
}
void eeprom_write_entry(void *variable_addr, uint8_t variable_size) {
void *storage_addr = &storage;
// entry is outside struct
if (variable_addr < storage_addr ||
variable_addr > storage_addr + STORAGE_SIZE) {
return;
}
uint8_t eeprom_addr =
EEPROM_DATA_START_ADDRESS + (variable_addr - storage_addr);
eeprom_set_bytes(eeprom_addr, variable_addr, variable_size);
}
void try_persist_distance(void) {
if(distance_cm_accumulator > (PERSIST_DISTANCE_EVERY_METERS * 100)) {
distance_cm_accumulator -= (PERSIST_DISTANCE_EVERY_METERS * 100);
storage.total_distance_meters += PERSIST_DISTANCE_EVERY_METERS;
eeprom_persist(&storage.total_distance_meters);
}
}
void force_persist_distance(void) {
storage.total_distance_meters += distance_cm_accumulator / 100;
distance_cm_accumulator = 0;
eeprom_persist(&storage.total_distance_meters);
}
void session_add_distance(uint16_t centimeters) {
current_session_distance_cm += centimeters;
distance_cm_accumulator += centimeters;
}
uint32_t session_distance_cm() {
return current_session_distance_cm;
}
void persist_last_trip() {
struct last_trip_info_t new_info = {
.minutes = millis() / 1000 / 60,
.meters = session_distance_cm() / 100,
};
struct last_trip_info_t *info = storage.last_trips;
uint8_t free_index = 0;
if(new_info.meters == 0) {
return;
}
for (; free_index < 8; free_index++) {
if(info->minutes == 0 && info->meters == 0){
break;
}
info++;
}
if(free_index > 7) { // shift array and insert to last position
info = storage.last_trips;
for (uint8_t i = 0; i < 7; i++) {
*info = *(info + 1);
++info;
}
storage.last_trips[7] = new_info;
} else {
storage.last_trips[free_index] = new_info;
}
eeprom_persist(&storage.last_trips);
}

88
src/persistence.h Normal file

@ -0,0 +1,88 @@
#ifndef PERSISTENCE_H
#define PERSISTENCE_H
#include <stdint.h>
#include "globals.h"
#define EEPROM_I2C_ADDRESS 0x50
#define EEPROM_I2C_BUS I2C1
#define EEPROM_DATA_START_ADDRESS 0x10
#define EEPROM_PAGE_SIZE 16U
#pragma pack(push, 1)
struct last_trip_info_t {
uint16_t minutes;
uint16_t meters;
};
/// Struct is automatically mapped to the EEPROM.
/// Do not change order of variables without settings reset.
struct eeprom_data_t {
uint8_t contrast;
uint8_t mute;
uint32_t total_distance_meters;
uint8_t anti_throttle_lock; ///< Factory cruise control cancellation
uint16_t keys_volume;
uint16_t signals_volume;
uint16_t wheel_length_mm;
uint16_t magnets_count;
uint8_t zero_start_enabled;
uint8_t soft_start_enabled;
float soft_start_increment;
uint8_t current_limit_enabled;
float current_limit;
float current_stabilization_Kp;
uint8_t speed_stabilization_enabled;
float speed_stabilization_Kp;
uint8_t calibrate_triggers_on_startup;
uint16_t brake_trigger_min_value;
uint16_t brake_trigger_max_value;
uint16_t throttle_trigger_min_value;
uint16_t throttle_trigger_max_value;
uint8_t speed_limit_last;
uint8_t persist_speed_limit;
struct last_trip_info_t last_trips[8];
};
#pragma pack(pop)
struct float_config_entry_t {
float* ptr;
float _min;
float _max;
float _default;
};
struct u16_config_entry_t {
uint16_t* ptr;
uint16_t _min;
uint16_t _max;
uint16_t _default;
};
extern struct eeprom_data_t storage;
#define STORAGE_SIZE sizeof(storage)
// split bytes to pages and write
void eeprom_set_bytes(uint8_t addr, const void *ptr, uint8_t count);
void eeprom_read_bytes(uint8_t addr, void *ptr, uint8_t count);
void eeprom_read_and_validate(void);
void eeprom_reset_all(uint8_t commit);
void eeprom_write_all(void);
/// Persist variable inside @ref storage to the EEPROM
void eeprom_write_entry(void* variable_addr, uint8_t variable_size);
/// Helper for eeprom_write_entry
#define eeprom_persist(variable_addr) (eeprom_write_entry(variable_addr, sizeof(*variable_addr)))
void try_persist_distance(void);
void force_persist_distance(void);
void session_add_distance(uint16_t centimeters);
/// Current session distance in centimeters
uint32_t session_distance_cm();
void persist_last_trip();
#endif /* PERSISTENCE_H */

334
src/ssd1306.c Normal file

@ -0,0 +1,334 @@
#include "ssd1306.h"
#include "font.h"
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/i2c.h>
#include <libopencm3/stm32/spi.h>
#include <string.h>
#define SSD1306_FRAMEBUFFER_SIZE (SSD1306_LINES * SSD1306_WIDTH)
static uint8_t framebuffer[SSD1306_FRAMEBUFFER_SIZE];
static uint8_t *framebuffer_pointer = framebuffer;
static const uint8_t *framebuffer_end = framebuffer + SSD1306_FRAMEBUFFER_SIZE;
static enum ssd1306_blend_mode_t blend_mode = SSD1306_BLEND_MODE_LIGHTEN;
static enum ssd1306_blend_mode_t blend_mode_prev = SSD1306_BLEND_MODE_LIGHTEN;
static uint8_t font_scale = 1;
// If the Co bit is set as logic “0”, the transmission of the following
// information will contain data bytes only.
#define SSD1306_CONTROL_BYTE_CONTINUATION_OFF (1 << 7)
// The D/C# bit determines the next data byte is acted as a command or a data.
// If the D/C# bit is set to logic “0”, it defines the following data byte as a
// command. If the D/C# bit is set to logic “1”, it defines the following data
// byte as a data which will be stored at the GDDRAM.
#define SSD1306_CONTROL_BYTE_DC (1 << 6)
static inline void wait_for_data_transfer_finished(void) {
while (!(I2C_SR1(SSD1306_I2C_REGISTER) & (I2C_SR1_BTF))) {
}
}
static inline void wait_for_address_transfer_finished(void) {
while (!(I2C_SR1(SSD1306_I2C_REGISTER) & I2C_SR1_ADDR)) {
}
}
void ssd1306_transfer_start(void) {
i2c_send_start(SSD1306_I2C_REGISTER);
// wait for i2c ready
while (!((I2C_SR1(SSD1306_I2C_REGISTER) & I2C_SR1_SB) &
(I2C_SR2(SSD1306_I2C_REGISTER) & (I2C_SR2_MSL | I2C_SR2_BUSY)))) {
}
i2c_send_7bit_address(SSD1306_I2C_REGISTER, SSD1306_ADDRESS, I2C_WRITE);
wait_for_address_transfer_finished();
(void)I2C_SR2(SSD1306_I2C_REGISTER);
}
void ssd1306_transfer_end(void) { i2c_send_stop(SSD1306_I2C_REGISTER); }
void ssd1306_byte(uint8_t b) {
i2c_send_data(SSD1306_I2C_REGISTER, b);
wait_for_data_transfer_finished();
}
void ssd1306_cmd(uint8_t cmd) {
ssd1306_byte(0x00 | SSD1306_CONTROL_BYTE_CONTINUATION_OFF); // control byte
ssd1306_byte(cmd);
}
void ssd1306_cmd_with_value(uint8_t cmd, uint8_t value) {
ssd1306_cmd(cmd);
ssd1306_cmd(value);
}
void ssd1306_init() {
ssd1306_transfer_start();
ssd1306_cmd(0xAE); // Set display OFF
ssd1306_cmd(0xD5); // Set display clock divide ratio or oscillator frequency
ssd1306_cmd(0x80); // Display Clock Divide Ratio / OSC Frequency
ssd1306_cmd_with_value(0xA8, 0x3F); // Multiplex Ratio for 128x64 (64-1)
ssd1306_cmd_with_value(0xD3, 0x00); // Display Offset
ssd1306_cmd(0x40); // Set Display Start Line
ssd1306_cmd_with_value(
0x8D, 0x14); // Charge Pump (0x10 External, 0x14 Internal DC/DC)
// ssd1306_cmd(0x20); // Set Memory Addressing Mode
// ssd1306_cmd(0x00); // Horizontal addressing mode
ssd1306_cmd(0xA1); // Set Segment Re-Map
ssd1306_cmd(0xC8); // Set Com Output Scan Direction
ssd1306_cmd(0xDA); // Set COM Hardware Configuration
ssd1306_cmd(0x12); // COM Hardware Configuration
ssd1306_cmd_with_value(0x81, 0x7f); // contrast control
ssd1306_cmd_with_value(0xD9, 0xF1); // Set Pre-Charge Period (0x22 External, 0xF1 Internal)
ssd1306_cmd_with_value(0xDB, 0x40); // VCOMH Deselect Level
ssd1306_cmd(0xA4); // Disable Entire Display On
ssd1306_cmd(0xA6); // Normal display, 0xA7 - inverse
ssd1306_transfer_end();
ssd1306_clear();
ssd1306_redraw();
ssd1306_transfer_start();
ssd1306_cmd(0xAF); // Set display On
ssd1306_transfer_end();
}
void ssd1306_contrast(uint8_t val) {
ssd1306_transfer_start();
ssd1306_cmd_with_value(0x81, val); // contrast control
ssd1306_transfer_end();
}
void ssd1306_clear(void) { ssd1306_fill(0x00); }
void ssd1306_fill(uint8_t pattern) {
memset(framebuffer, pattern, SSD1306_FRAMEBUFFER_SIZE);
}
void ssd1306_redraw(void) {
for (uint8_t line = 0; line < SSD1306_LINES; line++) {
ssd1306_transfer_start();
ssd1306_cmd(0xB0 + line); // Set page start address for page addressing mode
ssd1306_cmd(0x10); // Set higher column start address for page addressing mode
ssd1306_cmd(0x00); // Set lower column start address for page addressing mode
ssd1306_byte(SSD1306_CONTROL_BYTE_DC); // Control byte
uint8_t *framebuffer_pos = framebuffer + (line * SSD1306_WIDTH);
for (uint8_t col = 0; col < SSD1306_WIDTH; col++) {
ssd1306_byte(*(framebuffer_pos++));
}
ssd1306_transfer_end();
}
}
void ssd1306_blend_mode(enum ssd1306_blend_mode_t mode) { blend_mode = mode; }
void ssd1306_blend_mode_push() { blend_mode_prev = blend_mode; }
void ssd1306_blend_mode_pop() { blend_mode = blend_mode_prev; }
void ssd1306_framebuffer_setpos(uint8_t line, uint8_t column) {
uint8_t *new_pos = framebuffer + (line * SSD1306_WIDTH) + column;
if (new_pos >= framebuffer && new_pos < framebuffer_end) {
framebuffer_pointer = new_pos;
}
}
void ssd1306_framebuffer_byte(uint8_t data) {
if (framebuffer_pointer >= framebuffer &&
framebuffer_pointer < framebuffer_end) { // is pointer within framebuffer
switch (blend_mode) {
case SSD1306_BLEND_MODE_REPLACE:
*framebuffer_pointer = data;
break;
case SSD1306_BLEND_MODE_LIGHTEN:
*framebuffer_pointer |= data;
break;
case SSD1306_BLEND_MODE_EXCLUSION:
*framebuffer_pointer &= ~data;
break;
case SSD1306_BLEND_MODE_XOR:
*framebuffer_pointer ^= data;
break;
}
++framebuffer_pointer;
}
}
void ssd1306_framebuffer_byte_fixedline(uint8_t data) {
uint8_t current_line = (framebuffer_pointer - framebuffer) / SSD1306_WIDTH;
uint8_t next_line = (framebuffer_pointer + 1U - framebuffer) / SSD1306_WIDTH;
if (current_line != next_line) {
return;
}
ssd1306_framebuffer_byte(data);
}
void ssd1306_framebuffer_nextline(uint8_t start_from_zero) {
uint8_t line = (framebuffer_pointer - framebuffer) / SSD1306_WIDTH;
if (start_from_zero) {
ssd1306_framebuffer_setpos(line + 1, 0);
} else if (framebuffer_pointer + SSD1306_WIDTH < framebuffer_end) {
framebuffer_pointer += SSD1306_WIDTH;
}
}
static void draw_font_bytes(const uint8_t *bytes) {
uint8_t column = (framebuffer_pointer - framebuffer) % SSD1306_WIDTH;
uint8_t *starting_framebuffer_pos = framebuffer_pointer;
// todo: character wrap
if (font_scale == 1) { // optimization :D
for (uint8_t col = 0; col < FONT_WIDTH; col++) {
ssd1306_framebuffer_byte(*bytes);
++bytes;
}
} else {
const uint8_t *ptr;
for (uint8_t pass = 0; pass < font_scale; pass++) {
ptr = bytes;
for (uint8_t col = 0; col < FONT_WIDTH; col++) {
uint8_t b = 0;
uint8_t source_pos = 0;
// multiply column pixel heights by blocks
for (uint8_t i = 0; i < 8; i++) {
// What I have made?
source_pos = (pass * (8 / font_scale) + i / font_scale);
b |= !!(*ptr & (1 << source_pos)) << i;
}
// multiply column width
for (uint8_t i = 0; i < font_scale; i++) {
ssd1306_framebuffer_byte(b);
}
++ptr;
}
ssd1306_framebuffer_nextline(true);
framebuffer_pointer += column;
}
}
framebuffer_pointer =
starting_framebuffer_pos + (FONT_WIDTH + 1) * font_scale;
}
void ssd1306_font_scale(uint8_t scale) { font_scale = scale; }
void ssd1306_char(char ch) {
if (ch == '\n') {
ssd1306_framebuffer_nextline(true);
} else {
ssd1306_mbchar(0x000000ff & ch);
}
}
/// auto-generated by font_convert.py
void ssd1306_mbchar(uint32_t ch) {
uint16_t mapped_idx;
if (ch >= 0x00000020 && ch <= 0x0000005f) {
mapped_idx = 0 + (ch - 0x00000020);
} else if (ch == 0x0000d081) {
mapped_idx = 64;
} else if (ch >= 0x0000d090 && ch <= 0x0000d0af) {
mapped_idx = 65 + (ch - 0x0000d090);
} else if (ch == 0x00e296aa) {
mapped_idx = 97;
} else if (ch == 0x00e29c94) {
mapped_idx = 98;
} else {
draw_font_bytes(font_error_symbol);
return;
}
draw_font_bytes(font_data + mapped_idx * FONT_WIDTH);
}
void ssd1306_string(const char *str) {
uint8_t byte_count;
uint32_t char_code;
while (*str != '\0') {
// determine size of sequence
if ((*str & 0x80) == 0x00) {
byte_count = 1;
} else if ((*str & 0xE0) == 0xC0) {
byte_count = 2;
} else if ((*str & 0xF0) == 0xE0) {
byte_count = 3;
} else if ((*str & 0xF8) == 0xF0) {
byte_count = 4;
} else {
ssd1306_mbchar(0); // error
break;
}
char_code = 0;
// read multibyte sequence to the 32-bit int
for (uint8_t i = 0; i < byte_count; i++) {
if (*str == '\0') {
break;
}
char_code |= (0x000000FF & *str) << ((byte_count - 1 - i) * 8);
str++;
}
if(byte_count == 1 && char_code == '\n') {
ssd1306_framebuffer_nextline(true);
continue;
}
ssd1306_mbchar(char_code);
}
}
void ssd1306_horizontal_line(uint8_t x1, uint8_t x2, uint8_t y) {
ssd1306_framebuffer_setpos(y / SSD1306_LINES, x1);
uint8_t bits = (1 << (y % 8));
for (uint16_t x = x1; x < x2 + 1; ++x) {
ssd1306_framebuffer_byte(bits);
}
}
void ssd1306_vertical_line(uint8_t y1, uint8_t y2, uint8_t x) {
uint8_t pixels_left = y2 - y1;
uint8_t line = y1 / SSD1306_LINES;
uint8_t tmp_y = y1 % 8;
uint8_t bits;
uint8_t line_pixels;
if (y2 < y1) {
return;
}
while (pixels_left > 0) {
line_pixels = (pixels_left < 8 ? pixels_left : 8) - tmp_y;
bits = 0xff >> tmp_y;
bits &= 0xff << (8 - line_pixels - tmp_y);
pixels_left -= line_pixels;
tmp_y = 0;
ssd1306_framebuffer_setpos(line++, x);
ssd1306_framebuffer_byte(bits);
}
}
void ssd1306_pixel(uint8_t x, uint8_t y) {
ssd1306_framebuffer_setpos(y / SSD1306_LINES, x);
ssd1306_framebuffer_byte((1 << (y % 8)));
}

46
src/ssd1306.h Normal file

@ -0,0 +1,46 @@
#ifndef SSD1306_H
#define SSD1306_H
#include <stdint.h>
enum ssd1306_blend_mode_t {
SSD1306_BLEND_MODE_REPLACE, // Replace background and pixels
SSD1306_BLEND_MODE_LIGHTEN, // Keep background, switch pixels on
SSD1306_BLEND_MODE_EXCLUSION, // Keep background, switch pixels off
SSD1306_BLEND_MODE_XOR, // Keep background, toggle pixels
};
#define SSD1306_ADDRESS 0x3c
#define SSD1306_WIDTH 128
#define SSD1306_LINES 8 // 64 pixels = 8 lines of 8 pixels
#define SSD1306_I2C_REGISTER I2C1
void ssd1306_init(void);
void ssd1306_contrast(uint8_t val);
void ssd1306_clear(void);
void ssd1306_fill(uint8_t pattern);
void ssd1306_redraw(void);
void ssd1306_blend_mode(enum ssd1306_blend_mode_t mode);
void ssd1306_blend_mode_push();
void ssd1306_blend_mode_pop();
/// Origin is top-left corner
void ssd1306_framebuffer_setpos(uint8_t line, uint8_t column);
void ssd1306_framebuffer_byte(uint8_t data);
void ssd1306_framebuffer_byte_fixedline(uint8_t data);
void ssd1306_framebuffer_nextline(uint8_t start_from_zero);
// Only powers of 2 accepted (1, 2, 4, 8)
void ssd1306_font_scale(uint8_t scale);
void ssd1306_string(const char* str);
/// ASCII char
void ssd1306_char(char ch);
void ssd1306_mbchar(uint32_t ch);
void ssd1306_horizontal_line(uint8_t x1, uint8_t x2, uint8_t y);
void ssd1306_vertical_line(uint8_t y1, uint8_t y2, uint8_t x);
/// The most innefficient method of drawing
void ssd1306_pixel(uint8_t x, uint8_t y);
#endif /* SSD1306_H */

125
src/utils.c Normal file

@ -0,0 +1,125 @@
#include "utils.h"
uint8_t clamp_u8(uint8_t val, uint8_t min_val, uint8_t max_val) {
if (val < min_val)
return min_val;
if (val > max_val)
return max_val;
return val;
}
inline uint16_t clamp_u16(uint16_t val, uint16_t min_val, uint16_t max_val) {
if (val < min_val)
return min_val;
if (val > max_val)
return max_val;
return val;
}
float clamp_float(float val, float min_val, float max_val) {
if (val < min_val)
return min_val;
if (val > max_val)
return max_val;
return val;
}
float convert_range_float(float val, float old_max, float new_max) {
if (val < 0) {
return 0;
} else if (val > old_max) {
return new_max;
}
return (val * new_max) / old_max;
}
uint16_t convert_range_u16(uint16_t val, uint16_t old_max, uint16_t new_max) {
if (val > old_max) {
return new_max;
}
return (val * new_max) / old_max;
}
inline void incr_u8_loop(uint8_t *val, uint8_t min_val, uint8_t max_val,
uint8_t step) {
if ((*val + step) <= max_val) {
*val += step;
} else {
*val = min_val;
}
}
inline void decr_u8_loop(uint8_t *val, uint8_t min_val, uint8_t max_val,
uint8_t step) {
if ((*val - step) >= min_val) {
*val -= step;
} else {
*val = max_val;
}
}
void incr_u16_loop(uint16_t *val, uint16_t min_val, uint16_t max_val,
uint16_t step) {
if ((*val + step) <= max_val) {
*val += step;
} else {
*val = min_val;
}
}
void decr_u16_loop(uint16_t *val, uint16_t min_val, uint16_t max_val,
uint16_t step) {
if ((*val - step) >= min_val) {
*val -= step;
} else {
*val = max_val;
}
}
void incr_float_loop(float *val, float min_val, float max_val, float step) {
if ((*val + step) <= max_val) {
*val += step;
} else {
*val = min_val;
}
}
void decr_float_loop(float *val, float min_val, float max_val, float step) {
if ((*val - step) >= min_val) {
*val -= step;
} else {
*val = max_val;
}
}
uint8_t digit_count(uint16_t val) {
uint8_t count = 0;
while (val > 0) {
val /= 10;
++count;
}
return count;
}
uint16_t pow_u16(uint16_t base, uint16_t exponent) {
uint16_t result = 1;
for (; exponent > 0; exponent--) {
result = result * base;
}
return result;
}
float pid_update(struct pid_t *pid, float error, float dt) {
pid->P = error;
pid->I += error * dt;
pid->D = (error - pid->last_error) / dt;
pid->last_error = error;
return pid->P * pid->Kp + pid->I * pid->Ki + pid->D * pid->Kd;
}
void pid_reset(struct pid_t *pid) {
pid->P = 0;
pid->I = 0;
pid->D = 0;
pid->last_error = 0;
}

41
src/utils.h Normal file

@ -0,0 +1,41 @@
#ifndef UTILS_H
#define UTILS_H
#include <stdint.h>
struct pid_t {
float Kp;
float Ki;
float Kd;
float P;
float I;
float D;
float last_error;
};
struct pid_coeff_t {
float Kp;
float Ki;
float Kd;
};
float pid_update(struct pid_t *pid, float error, float dt);
void pid_reset(struct pid_t *pid);
uint8_t clamp_u8(uint8_t val, uint8_t min_val, uint8_t max_val);
uint16_t clamp_u16(uint16_t val, uint16_t min_val, uint16_t max_val);
float clamp_float(float val, float min_val, float max_val);
float convert_range_float(float val, float old_max, float new_max);
uint16_t convert_range_u16(uint16_t val, uint16_t old_max, uint16_t new_max);
void incr_u8_loop(uint8_t *val, uint8_t min_val, uint8_t max_val, uint8_t step);
void decr_u8_loop(uint8_t *val, uint8_t min_val, uint8_t max_val, uint8_t step);
void incr_u16_loop(uint16_t *val, uint16_t min_val, uint16_t max_val, uint16_t step);
void decr_u16_loop(uint16_t *val, uint16_t min_val, uint16_t max_val, uint16_t step);
void incr_float_loop(float *val, float min_val, float max_val, float step);
void decr_float_loop(float *val, float min_val, float max_val, float step);
uint8_t digit_count(uint16_t val);
uint16_t pow_u16(uint16_t base, uint16_t exponent);
#endif /* UTILS_H */

79
src/views/detailed_view.c Normal file

@ -0,0 +1,79 @@
#include "detailed_view.h"
#include "globals.h"
#include "gui.h"
#include "persistence.h"
#include "hardware.h"
#include "keyboard.h"
#include "kugoo_s3.h"
#include "ssd1306.h"
#include "utils.h"
#include <stdio.h>
void detailed_view_redraw() {
uint16_t current = kugoo_s3_rx.current;
uint16_t batt = battery_value();
uint32_t distance_m = session_distance_cm() / 100;
ssd1306_framebuffer_setpos(0, 0);
sprintf(sprintf_buf, "0X%lX", reset_reason);
ssd1306_string(sprintf_buf);
ssd1306_framebuffer_setpos(1, 0);
#ifdef DEBUG_RESET_REASON
if(reset_reason & RCC_CSR_LPWRRSTF) {
ssd1306_string("LOW-POWER\n");
}
if(reset_reason & RCC_CSR_WWDGRSTF) {
ssd1306_string("WINDOW WATCHDOG\n");
}
if(reset_reason & RCC_CSR_IWDGRSTF) {
ssd1306_string("INDEPENDENT WATCHDOG\n");
}
if(reset_reason & RCC_CSR_SFTRSTF) {
ssd1306_string("SOFTWARE\n");
}
if(reset_reason & RCC_CSR_PORRSTF) {
ssd1306_string("POWERON/POWERDOWN\n");
}
if(reset_reason & RCC_CSR_PINRSTF) {
ssd1306_string("RESET BTN\n");
}
#else
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(0, 0);
sprintf(sprintf_buf, "%d.%d", batt / 10, batt % 10);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
ssd1306_string(" В");
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(2, 0);
sprintf(sprintf_buf, "%d.%d", current / 10, current % 10);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
ssd1306_string(" А");
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(4, 0);
sprintf(sprintf_buf, "%lu", distance_m);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
ssd1306_string(" М");
#endif
ssd1306_framebuffer_setpos(6, 0);
ssd1306_font_scale(2);
sprintf(sprintf_buf, "%lu.%lu", storage.total_distance_meters / 1000,
(storage.total_distance_meters / 100) % 10);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
ssd1306_string(" КМ");
}

@ -0,0 +1,8 @@
#ifndef DETAILED_VIEW_H
#define DETAILED_VIEW_H
#include <stdint.h>
void detailed_view_redraw();
#endif /* DETAILED_VIEW_H */

@ -0,0 +1,58 @@
#include "last_trips_view.h"
#include <stdio.h>
#include <string.h>
#include "persistence.h"
#include "keyboard.h"
#include "gui.h"
#include "ssd1306.h"
#include "hardware.h"
void last_trips_view_keyhandler(uint8_t e) {
if (e & KB_EVT_TYPE_KEYDOWN) {
beep(KEY_BEEP_FREQ, 10, storage.keys_volume);
}
if (e & KB_EVT_TYPE_KEYDOWN && e & KB_EVT_KEY_BEEP) {
gui_set_view(GUI_VIEW_SETTINGS);
} else if (e & KB_EVT_KEY_SPEED && e & KB_EVT_TYPE_REPEAT) {
memset(&storage.last_trips, 0U, sizeof(storage.last_trips));
eeprom_persist(&storage.last_trips);
}
}
void last_trips_view_redraw() {
struct last_trip_info_t *info = storage.last_trips;
uint8_t i = 0;
for (; i < 8; i++) {
if(info->minutes == 0 && info->meters == 0){
break;
}
ssd1306_framebuffer_setpos(i, 0);
if(info->minutes < 60) {
sprintf(sprintf_buf, "%d МИН - ", info->minutes);
} else {
sprintf(sprintf_buf, "%d.%d Ч", info->minutes / 60, info->meters % 60 / 6);
}
ssd1306_string(sprintf_buf);
if(info->meters < 1000) {
sprintf(sprintf_buf, "%d М", info->meters);
} else {
sprintf(sprintf_buf, "%d.%d КМ", info->meters / 1000, info->meters % 1000 / 100);
}
ssd1306_string(sprintf_buf);
++info;
}
if(i == 0) {
// 6 = font width (5) + interval (1)
// 5 - symbols count
ssd1306_framebuffer_setpos(3, (millis() / 50) % (SSD1306_WIDTH - 6 * 5));
ssd1306_string("ПУСТО");
}
}

@ -0,0 +1,9 @@
#ifndef LAST_TRIPS_VIEW_H
#define LAST_TRIPS_VIEW_H
#include <stdint.h>
void last_trips_view_keyhandler(uint8_t e);
void last_trips_view_redraw();
#endif /* LAST_TRIPS_VIEW_H */

194
src/views/main_view.c Normal file

@ -0,0 +1,194 @@
#include "main_view.h"
#include "globals.h"
#include "gui.h"
#include "hardware.h"
#include "persistence.h"
#include "keyboard.h"
#include "kugoo_s3.h"
#include "ssd1306.h"
#include "utils.h"
#include <stdio.h>
float speed_limit_prev = 0;
uint32_t speed_limit_changed_at;
void main_view_keyhandler(uint8_t e) {
if (e & KB_EVT_TYPE_KEYDOWN) {
if (e & KB_EVT_KEY_BEEP) {
buzzer_freq_vol(1000, 255);
} else if (e & KB_EVT_KEY_SET) {
// enable cruise control when throttle trigger pressed
if(throttle_value_normalized() > 10) {
// cruise_control_speed_kmh = kugoo_s3_get_speed();
// todo: maybe use current speed, not throttle trigger
cruise_ctl_speed_kmh = throttle_value_to_kmh();
cruise_ctl_status = CRUISE_CTL_WAITING_RELEASE;
} else {
gui_set_view(GUI_VIEW_SETTINGS);
gui_redraw_force();
}
} else if (e & KB_EVT_KEY_LIGHT) {
beep_blocking(2000, 10, storage.keys_volume);
light_toggle();
} else if (e & KB_EVT_KEY_SPEED) {
beep_blocking(3000, 10, storage.keys_volume);
if(brake_value_normalized() > 10) {
storage.current_limit_enabled = !storage.current_limit_enabled;
eeprom_persist(&storage.current_limit_enabled);
} else {
incr_u8_loop(&storage.speed_limit_last, 10, 30, 5);
gui_redraw_force();
}
}
} else if (e & KB_EVT_TYPE_KEYUP) {
if (e & KB_EVT_KEY_BEEP) {
buzzer_freq_vol(0, 255);
} else if (e & KB_EVT_KEY_POWER) {
if (gui_is_view(GUI_VIEW_MAIN)) {
gui_set_view(GUI_VIEW_MAIN_DETAILED);
} else if (gui_is_view(GUI_VIEW_MAIN_DETAILED)) {
gui_set_view(GUI_VIEW_MAIN);
}
gui_redraw_force();
}
}
}
void main_view_redraw() {
uint16_t current = kugoo_s3_rx.current;
uint32_t ms = millis();
uint16_t speed = kugoo_s3_get_speed();
uint16_t batt = battery_value();
uint16_t batt_clamp =
clamp_u16(batt, BATTERY_VOLTS_0_PERCENTS, BATTERY_VOLTS_100_PERCENTS);
uint16_t batt_pixels = ((batt_clamp - BATTERY_VOLTS_0_PERCENTS) * 83) /
(BATTERY_VOLTS_100_PERCENTS - BATTERY_VOLTS_0_PERCENTS);
uint32_t distance_m = session_distance_cm() / 100;
if(speed_limit_prev != storage.speed_limit_last) {
speed_limit_prev = storage.speed_limit_last;
speed_limit_changed_at = ms;
}
if(speed_limit_changed_at - ms < 2000) {
ssd1306_framebuffer_setpos(0, 20);
ssd1306_font_scale(8);
sprintf(sprintf_buf, "%u", storage.speed_limit_last);
ssd1306_string(sprintf_buf);
return;
}
if (batt > BATTERY_VOLTS_0_PERCENTS || (ms / 500) % 2 == 0) {
// left battery cap
ssd1306_framebuffer_setpos(0, 39);
ssd1306_framebuffer_byte(0x7F);
ssd1306_framebuffer_byte(0x41);
// right battery cap
ssd1306_framebuffer_setpos(0, SSD1306_WIDTH - 4);
ssd1306_framebuffer_byte(0x41);
ssd1306_framebuffer_byte(0x7F);
ssd1306_framebuffer_byte(0x1C);
// battery progressbar
ssd1306_framebuffer_setpos(0, 41);
for (uint8_t i = 0; i < 83; i++) {
if (i < batt_pixels) {
ssd1306_framebuffer_byte(0x5D);
} else {
ssd1306_framebuffer_byte(0x41);
}
}
}
// vertical line
for (uint8_t i = 0; i < 8; i++) {
ssd1306_framebuffer_setpos(i, 37);
ssd1306_framebuffer_byte(0x55);
}
// bottom horizontal line
ssd1306_framebuffer_setpos(6, 0);
for (uint8_t i = 0; i < 19; i++) {
ssd1306_framebuffer_byte(0x80);
ssd1306_framebuffer_byte(0x00);
}
if (current > 0 && millis() / 1000 % 7 > 3) {
// current
ssd1306_framebuffer_setpos(3, 41);
ssd1306_font_scale(4);
sprintf(sprintf_buf, "%d", current / 10);
ssd1306_string(sprintf_buf);
ssd1306_framebuffer_nextline(false);
ssd1306_framebuffer_nextline(false);
ssd1306_font_scale(2);
sprintf(sprintf_buf, ".%d", current % 10);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
ssd1306_string(" A");
} else {
// speeed
ssd1306_framebuffer_setpos(0, 39);
ssd1306_font_scale(8);
if (speed > 99) {
ssd1306_string("ЖП");
} else {
sprintf(sprintf_buf, "%02d", speed);
ssd1306_string(sprintf_buf);
}
}
ssd1306_font_scale(1);
ssd1306_framebuffer_setpos(7, 3);
sprintf(sprintf_buf, "%02lu%c%02lu", (ms / 1000) / 60,
(ms / 500) % 2 == 0 ? ':' : ' ', (ms / 1000) % 60);
ssd1306_string(sprintf_buf);
// current limit indicator
if(storage.current_limit_enabled) {
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(2, 0);
ssd1306_string("LIM");
}
// curent session distance
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(4, 0);
if(distance_m < 99) {
sprintf(sprintf_buf, "%lu", distance_m);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
ssd1306_string("М");
} else {
uint32_t km = distance_m / 1000;
sprintf(sprintf_buf, "%lu", km);
ssd1306_string(sprintf_buf);
ssd1306_font_scale(1);
ssd1306_framebuffer_nextline(false);
if(km < 10) {
sprintf(sprintf_buf, ".%luКМ", distance_m % 1000 / 100);
} else {
sprintf(sprintf_buf, "КМ");
}
ssd1306_string(sprintf_buf);
}
// todo: trottle lock indicator, remove
if(kugoo_s3_rx.state & KUGOO_S3_STATE_THROTTLE_LOCKED) {
ssd1306_framebuffer_setpos(0, 0);
for (uint8_t i = 0; i < 24; i++) {
ssd1306_framebuffer_byte(0xFF);
}
}
}

9
src/views/main_view.h Normal file

@ -0,0 +1,9 @@
#ifndef MAIN_VIEW_H
#define MAIN_VIEW_H
#include <stdint.h>
void main_view_keyhandler(uint8_t e);
void main_view_redraw();
#endif /* MAIN_VIEW_H */

308
src/views/settings_view.c Normal file

@ -0,0 +1,308 @@
#include "settings_view.h"
#include "globals.h"
#include "gui.h"
#include "hardware.h"
#include "keyboard.h"
#include "kugoo_s3.h"
#include "persistence.h"
#include "ssd1306.h"
#include "utils.h"
#include <stdio.h>
/// Selected menu item index
static uint8_t menu_index = 0;
static uint8_t is_editing = 0;
static uint8_t editor_digit_index = 0;
// @see persistence.c
extern struct float_config_entry_t cfg_speed_stabilization_Kp;
extern struct float_config_entry_t cfg_current_stabilization_Kp;
extern struct float_config_entry_t cfg_current_limit;
extern struct float_config_entry_t cfg_soft_start_increment;
extern struct u16_config_entry_t cfg_wheel_length_mm;
extern struct u16_config_entry_t cfg_magnets_count;
extern struct u16_config_entry_t cfg_keys_volume;
extern struct u16_config_entry_t cfg_signals_volume;
#ifdef MENU_SEPARATORS
#define MENU_SEPARATOR(text) { \
.title = (text), \
.type = SETTING_TYPE_NONE, \
.data = NULL \
},
#else
#define MENU_SEPARATOR(text)
#endif
static const struct settings_item_t menu_items[] = {
{
.title = " == ВЕРСИЯ " FIRMWARE_VERSION " ==",
.type = SETTING_TYPE_NONE,
.data = NULL
},
{
.title = "ОТКЛЮЧИТЬ ЗВУКИ",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.mute
},
{
.title = "ГРОМКОСТЬ КНОПОК",
.type = SETTING_TYPE_EDIT_U16,
.data = &cfg_keys_volume
},
{
.title = "ГРОМКОСТЬ СИГНАЛОВ",
.type = SETTING_TYPE_EDIT_U16,
.data = &cfg_signals_volume
},
MENU_SEPARATOR("")
{
.title = "АНТИ-ФИКСАЦИЯ УСКОР.",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.anti_throttle_lock
},
{
.title = "ZERO-START",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.zero_start_enabled
},
MENU_SEPARATOR("")
{
.title = "ПЛАВНОЕ УСКОРЕНИЕ",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.soft_start_enabled
},
{
.title = "ИНКРЕМ. ПЛАВН. УСКОР.",
.type = SETTING_TYPE_EDIT_FLOAT,
.data = &cfg_soft_start_increment
},
MENU_SEPARATOR("")
{
.title = "ОГРАНИЧЕНИЕ ТОКА",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.current_limit_enabled
},
{
.title = "ЗНАЧ. ОГРАНИЧЕН. ТОКА",
.type = SETTING_TYPE_EDIT_FLOAT,
.data = &cfg_current_limit
},
{
.title = "КОЭФФ. СТАБИЛИЗ. ТОКА",
.type = SETTING_TYPE_EDIT_FLOAT,
.data = &cfg_current_stabilization_Kp
},
MENU_SEPARATOR("")
{
.title = "СТАБИЛИЗАЦИЯ СКОР.",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.speed_stabilization_enabled
},
{
.title = "КОЭФФ. СТАБ. СКОРОСТИ",
.type = SETTING_TYPE_EDIT_FLOAT,
.data = &cfg_speed_stabilization_Kp
},
MENU_SEPARATOR("")
{
.title = "КАЛИБРОВКА РУЧЕК",
.type = SETTING_TYPE_VIEW_CHANGE,
.data = (void*)GUI_VIEW_TRIGGER_CALIBRATION
},
{
.title = "АВТОКАЛИБРОВКА РУЧЕК",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.calibrate_triggers_on_startup
},
{
.title = "СОХРАНЯТЬ СКОРОСТЬ",
.type = SETTING_TYPE_TOGGLE,
.data = &storage.persist_speed_limit
},
{
.title = "ДЛИНА ОКР. КОЛЕСА, ММ",
.type = SETTING_TYPE_EDIT_U16,
.data = &cfg_wheel_length_mm
},
{
.title = "КОЛИЧЕСТВО МАГНИТОВ",
.type = SETTING_TYPE_EDIT_U16,
.data = &cfg_magnets_count
},
MENU_SEPARATOR("")
{
.title = "ПОСЛЕДНИЕ ПОЕЗДКИ",
.type = SETTING_TYPE_VIEW_CHANGE,
.data = (void*)GUI_VIEW_LAST_TRIPS
}
};
static const uint8_t menu_items_count = sizeof(menu_items) / sizeof(struct settings_item_t);
void settings_view_keyhandler(uint8_t e) {
const struct settings_item_t *item = menu_items + menu_index;
if (!(e & KB_EVT_TYPE_KEYDOWN || e & KB_EVT_TYPE_REPEAT)) {
return;
}
beep(KEY_BEEP_FREQ, 10, storage.keys_volume);
if (is_editing) {
if (e & KB_EVT_KEY_BEEP) { // persist data after closing editor
is_editing = false;
if(item->type == SETTING_TYPE_EDIT_FLOAT) {
struct float_config_entry_t *entry = (struct float_config_entry_t *) item->data;
eeprom_write_entry(entry->ptr, sizeof(float));
} else if(item->type == SETTING_TYPE_EDIT_U16) {
struct u16_config_entry_t *entry = (struct u16_config_entry_t *) item->data;
eeprom_write_entry(entry->ptr, sizeof(uint16_t));
}
} else if (item->type == SETTING_TYPE_EDIT_U16) {
struct u16_config_entry_t *entry = (struct u16_config_entry_t *) item->data;
uint8_t digits = digit_count(entry->_max);
uint16_t step = pow_u16(10, editor_digit_index);
if (e & KB_EVT_KEY_POWER) {
incr_u8_loop(&editor_digit_index, 0, digits - 1, 1);
} if (e & KB_EVT_KEY_SET) {
decr_u16_loop(entry->ptr, entry->_min, entry->_max, step);
} else if (e & KB_EVT_KEY_LIGHT) {
incr_u16_loop(entry->ptr, entry->_min, entry->_max, step);
} else if (e & KB_EVT_KEY_SPEED) {
*entry->ptr = entry->_default;
}
} else if (item->type == SETTING_TYPE_EDIT_FLOAT) {
struct float_config_entry_t *entry = (struct float_config_entry_t *) item->data;
float step = 0.1f;
if (editor_digit_index > 3) { // integer part
step = pow_u16(10, editor_digit_index - 4);
} else if (editor_digit_index == 0) { // 0.1 * 10 ^ -y is too hard
step = 0.0001f;
} else if (editor_digit_index == 1) {
step = 0.001f;
} else if (editor_digit_index == 2) {
step = 0.01f;
}
if (e & KB_EVT_KEY_POWER) {
incr_u8_loop(&editor_digit_index, 0, 7, 1);
} if (e & KB_EVT_KEY_SET) {
decr_float_loop(entry->ptr, entry->_min, entry->_max, step);
} else if (e & KB_EVT_KEY_LIGHT) {
incr_float_loop(entry->ptr, entry->_min, entry->_max, step);
} else if (e & KB_EVT_KEY_SPEED) {
*entry->ptr = entry->_default;
}
}
} else {
if (e & KB_EVT_KEY_SET) {
decr_u8_loop(&menu_index, 0, menu_items_count - 1, 1);
} else if (e & KB_EVT_KEY_LIGHT) {
incr_u8_loop(&menu_index, 0, menu_items_count - 1, 1);
} else if (e & KB_EVT_KEY_BEEP) {
gui_set_view(GUI_VIEW_MAIN);
} else if (e & KB_EVT_KEY_POWER) {
if (item->type == SETTING_TYPE_TOGGLE) {
uint8_t *val = (uint8_t *)item->data;
*val = !*val;
eeprom_persist(val);
} else if (item->type == SETTING_TYPE_VIEW_CHANGE) {
gui_set_view((enum view_id_t)item->data);
} else if (item->type == SETTING_TYPE_EDIT_U16 || item->type == SETTING_TYPE_EDIT_FLOAT) {
is_editing = true;
editor_digit_index = 0;
}
}
}
gui_redraw_force();
}
void settings_view_redraw() {
const struct settings_item_t *item;
uint8_t menu_shift;
uint8_t new_index;
if(is_editing) {
item = menu_items + menu_index;
ssd1306_framebuffer_setpos(0, 0);
ssd1306_string(item->title);
ssd1306_font_scale(2);
ssd1306_framebuffer_setpos(3, 0);
if(item->type == SETTING_TYPE_EDIT_U16) {
struct u16_config_entry_t *params = (struct u16_config_entry_t *) item->data;
uint8_t digits = digit_count(params->_max);
if(digits < 6) { // suppress compiler warning
sprintf(sprintf_buf, "%0*u", digits, *params->ptr);
ssd1306_string(sprintf_buf);
}
ssd1306_blend_mode_push();
ssd1306_blend_mode(SSD1306_BLEND_MODE_XOR);
ssd1306_framebuffer_setpos(5, 0);
for (uint8_t i = 1; i <= digits; i++){
if(i == (digits - editor_digit_index)) {
ssd1306_char('^');
} else {
ssd1306_char(' ');
}
}
ssd1306_blend_mode_pop();
} else if(item->type == SETTING_TYPE_EDIT_FLOAT) {
struct float_config_entry_t *params = (struct float_config_entry_t *) item->data;
float f = *params->ptr;
uint16_t integer_part = f;
uint16_t fractional_part = (f - integer_part) * 10000;
sprintf(sprintf_buf, "%04d.%04d", integer_part, fractional_part);
ssd1306_string(sprintf_buf);
ssd1306_blend_mode_push();
ssd1306_blend_mode(SSD1306_BLEND_MODE_XOR);
ssd1306_framebuffer_setpos(5, 0);
for (uint8_t i = 1; i <= 8; i++) {
if(i == (8 - editor_digit_index)) {
if(i > 4) { // do not point to point
ssd1306_char(' ');
}
ssd1306_char('^');
} else {
ssd1306_char(' ');
}
}
ssd1306_blend_mode_pop();
}
return;
}
for (uint8_t i = 0; i < SSD1306_LINES; i++) {
menu_shift = menu_index / SSD1306_LINES;
new_index = (menu_shift * SSD1306_LINES) + i;
if (new_index >= menu_items_count) {
break;
}
item = menu_items + new_index;
ssd1306_framebuffer_setpos(i, 0);
ssd1306_string(item->title);
if(new_index == menu_index) {
ssd1306_framebuffer_setpos(i, 0);
ssd1306_blend_mode_push();
ssd1306_blend_mode(SSD1306_BLEND_MODE_XOR);
for (uint8_t i = 0; i < SSD1306_WIDTH; i++){
ssd1306_framebuffer_byte(0xff);
}
}
if (item->type == SETTING_TYPE_TOGGLE) {
ssd1306_framebuffer_setpos(i, 122);
ssd1306_string(*((uint8_t *)item->data) ? "" : "");
}
}
}

24
src/views/settings_view.h Normal file

@ -0,0 +1,24 @@
#ifndef SETTINGS_VIEW_H
#define SETTINGS_VIEW_H
#include <stdint.h>
enum settings_item_type_t {
SETTING_TYPE_NONE,
SETTING_TYPE_VIEW_CHANGE,
SETTING_TYPE_TOGGLE,
SETTING_TYPE_EDIT_FLOAT,
SETTING_TYPE_EDIT_U16,
};
struct settings_item_t {
const char *title;
enum settings_item_type_t type;
void *data;
};
void settings_view_keyhandler(uint8_t e);
void settings_view_redraw();
#endif /* SETTINGS_VIEW_H */

@ -0,0 +1,113 @@
#include "trigger_calibration_view.h"
#include "keyboard.h"
#include "gui.h"
#include "ssd1306.h"
#include "globals.h"
#include "hardware.h"
#include "persistence.h"
#include <stdio.h>
static enum trigger_calibration_phase_t current_phase = TRIGGER_CAL_PHASE_NONE;
void trigger_calibration_view_keyhandler(uint8_t e) {
if (!(e & KB_EVT_TYPE_KEYDOWN)) {
return;
}
beep(KEY_BEEP_FREQ, 10, storage.keys_volume);
if (e & KB_EVT_KEY_BEEP) {
gui_set_view(GUI_VIEW_SETTINGS);
} else if (e & KB_EVT_KEY_POWER) {
if(current_phase == TRIGGER_CAL_PHASE_NONE) {
current_phase = TRIGGER_CAL_PHASE_THROTTLE_MIN;
} else if(current_phase == TRIGGER_CAL_PHASE_THROTTLE_MIN) {
storage.throttle_trigger_min_value = throttle_adc_value() + TRIGGER_ANTI_JITTER;
current_phase = TRIGGER_CAL_PHASE_THROTTLE_MAX;
} else if(current_phase == TRIGGER_CAL_PHASE_THROTTLE_MAX) {
storage.throttle_trigger_max_value = throttle_adc_value() - TRIGGER_ANTI_JITTER;
current_phase = TRIGGER_CAL_PHASE_BRAKE_MIN;
} else if(current_phase == TRIGGER_CAL_PHASE_BRAKE_MIN) {
storage.brake_trigger_min_value = brake_adc_value() + TRIGGER_ANTI_JITTER;
current_phase = TRIGGER_CAL_PHASE_BRAKE_MAX;
} else if(current_phase == TRIGGER_CAL_PHASE_BRAKE_MAX) {
storage.brake_trigger_max_value = brake_adc_value() - TRIGGER_ANTI_JITTER;
eeprom_persist(&storage.throttle_trigger_min_value);
eeprom_persist(&storage.throttle_trigger_max_value);
eeprom_persist(&storage.brake_trigger_min_value);
eeprom_persist(&storage.brake_trigger_max_value);
current_phase = TRIGGER_CAL_PHASE_FINISHED;
}
gui_redraw_force();
}
}
void trigger_calibration_view_redraw() {
ssd1306_framebuffer_setpos(0, 0);
ssd1306_string("КАЛИБРОВКА РУЧЕК");
ssd1306_framebuffer_setpos(2, 0);
if(current_phase == TRIGGER_CAL_PHASE_NONE) {
ssd1306_string("ДЛЯ НАЧАЛА\n"
"НАЖМИТЕ [POWER]");
} else if(current_phase == TRIGGER_CAL_PHASE_THROTTLE_MIN) {
ssd1306_string("ВЫВЕДИТЕ РУЧКУ\n"
"[АКСЕЛЕРАТОРА]\n"
"В НУЛЕВОЕ ПОЛОЖЕНИЕ\n"
"И НАЖМИТЕ [POWER]");
} else if(current_phase == TRIGGER_CAL_PHASE_THROTTLE_MAX) {
ssd1306_string("ВЫВЕДИТЕ РУЧКУ\n"
"[АКСЕЛЕРАТОРА]\n"
"В МАКС. ПОЛОЖЕНИЕ\n"
"И НАЖМИТЕ [POWER]");
} else if(current_phase == TRIGGER_CAL_PHASE_BRAKE_MIN) {
ssd1306_string("ВЫВЕДИТЕ РУЧКУ\n"
"[ТОРМОЗА]\n"
"В НУЛЕВОЕ ПОЛОЖЕНИЕ\n"
"И НАЖМИТЕ [POWER]");
} else if(current_phase == TRIGGER_CAL_PHASE_BRAKE_MAX) {
ssd1306_string("ВЫВЕДИТЕ РУЧКУ\n"
"[ТОРМОЗА]\n"
"В МАКС. ПОЛОЖЕНИЕ\n"
"И НАЖМИТЕ [POWER]");
} else if(current_phase == TRIGGER_CAL_PHASE_FINISHED) {
ssd1306_string("ПОЗДРАВЛЯЕМ\n"
"ВЫ ПОБЕДИЛИ\n"
"ЗНАЧЕНИЯ СОХРАНЕНЫ");
}
if(current_phase == TRIGGER_CAL_PHASE_THROTTLE_MIN ||
current_phase == TRIGGER_CAL_PHASE_THROTTLE_MAX) {
ssd1306_framebuffer_setpos(7, 0);
sprintf(sprintf_buf, "ЗНАЧЕНИЕ АЦП: %d", throttle_adc_value());
ssd1306_string(sprintf_buf);
} else if(current_phase == TRIGGER_CAL_PHASE_BRAKE_MIN ||
current_phase == TRIGGER_CAL_PHASE_BRAKE_MAX) {
ssd1306_framebuffer_setpos(7, 0);
sprintf(sprintf_buf, "ЗНАЧЕНИЕ АЦП: %d", brake_adc_value());
ssd1306_string(sprintf_buf);
} else if(current_phase == TRIGGER_CAL_PHASE_NONE ||
current_phase == TRIGGER_CAL_PHASE_FINISHED) {
ssd1306_framebuffer_setpos(6, 0);
sprintf(sprintf_buf, "УСКОРЕНИЕ %d = %d%%\n"
" ТОРМОЗ %d = %d%%",
throttle_adc_value(),
throttle_value_normalized() / 10,
brake_adc_value(),
brake_value_normalized() / 10);
ssd1306_string(sprintf_buf);
}
}
void trigger_calibration_view_reset() {
current_phase = TRIGGER_CAL_PHASE_NONE;
}

@ -0,0 +1,19 @@
#ifndef TRIGGER_CALIBRATION_VIEW_H
#define TRIGGER_CALIBRATION_VIEW_H
#include <stdint.h>
enum trigger_calibration_phase_t {
TRIGGER_CAL_PHASE_NONE,
TRIGGER_CAL_PHASE_THROTTLE_MIN,
TRIGGER_CAL_PHASE_THROTTLE_MAX,
TRIGGER_CAL_PHASE_BRAKE_MIN,
TRIGGER_CAL_PHASE_BRAKE_MAX,
TRIGGER_CAL_PHASE_FINISHED
};
void trigger_calibration_view_keyhandler(uint8_t e);
void trigger_calibration_view_redraw();
void trigger_calibration_view_reset();
#endif /* TRIGGER_CALIBRATION_VIEW_H */