Skip to content

Commit

Permalink
Merge pull request #36 from ow-breaker/security
Browse files Browse the repository at this point in the history
Added ability to lock boards
  • Loading branch information
lolwheel authored May 31, 2022
2 parents db830cf + 15e5867 commit fd56b2b
Show file tree
Hide file tree
Showing 17 changed files with 287 additions and 82 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build_and_test_all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ jobs:
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Set up Python
uses: actions/setup-python@v2
- name: Install PlatformIO
- name: Install PlatformIO and html-minifier-terser
run: |
python -m pip install --upgrade pip
pip install --upgrade platformio
npm install html-minifier-terser -g
- name: Install library dependencies
run: pio lib -g install 1
- name: Run tests Owie
Expand Down
2 changes: 1 addition & 1 deletion .gitpod.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ FROM gitpod/workspace-full

USER gitpod

RUN pip3 install -U platformio
RUN pip3 install -U platformio && npm install html-minifier-terser -g
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.formatOnType": false,
"C_Cpp.clang_format_fallbackStyle": "Google",
"files.associations": {
Expand Down
45 changes: 38 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ This is a hobby projet for its contributors and comes with absolutely no guarant
- Defeats BMS <-> Controller pairing and allows you to use any Pint or XR BMS in your board.
- Shows various stats about your battery on a web page through WiFi - Voltage, current, individual cell voltages and more.
- Supports future firmware updates via WiFi - no need to reopen your board.
- Adds password protection to your board

# Installing Owie into your board

## Prerequisites:

- Have essential soldering skills and tools: Soldering iron, some 22 gauge or otherwise thin wires, fish tape or isolating tape.
- Be comfortable with opening your board's battery enclosure.
- For the PINT you require a somewhat exotic Torx 5 point security bit, size TS20. [Amazon link](https://www.amazon.com/gp/product/B07TC79LVH)
- For the XR+ you will need a 3/32" Allen key. [Amazon link](https://www.amazon.com/dp/B0000CBJE1)
- For the PINT you require a somewhat exotic Torx 5 point security bit, size TS20. [Amazon link](https://www.amazon.com/gp/product/B07TC79LVH)
- For the XR+ you will need a 3/32" Allen key. [Amazon link](https://www.amazon.com/dp/B0000CBJE1)
- Wemos D1 Mini Lite - the cheapest and most compact ESP8266 board that I'm aware of. You can find those on Aliexpress and Amazon. Buy version without the metal shield or ceramic WiFi antenna on it as they're too bulky to fit inside of the battery enclosure. [5 pack Amazon Link](https://www.amazon.com/dp/B081PX9YFV).

## Build and download firmware
Expand All @@ -39,8 +40,8 @@ This is a hobby projet for its contributors and comes with absolutely no guarant
1. Use the ESP WebTools page provided [here](https://ow-breaker.github.io/).
1. Follow the instructions on that page to flash the firmware.
1. Verify the flash success: When the chip is on, you should see
a WiFi network called `Owie-XXXX`. Connecting to it should send you
straight to the status page of the Owie board. Don't worry about the data because the board isn't hooked up yet.
a WiFi network called `Owie-XXXX`. Connecting to it should send you
straight to the status page of the Owie board. Don't worry about the data because the board isn't hooked up yet.

## Installation:

Expand Down Expand Up @@ -78,7 +79,6 @@ If after installing OWIE into your board it reports that your battery is at 1% e
This problem occurs because the BMS goes through a state reset and doesn't know the status of the battery, and plugging the board
into a charger corrects this issue by forcing the BMS (and controller potentially) to do a state check.


Pictures demonstrating soldering points on the board:

<img src= "docs/img/wemos_d1_top.png?raw=true" height="180px">
Expand All @@ -89,6 +89,36 @@ How it looks like in my setup:

<img src="docs/img/wemos_d1_installed.jpg" height="180px">

# Board Locking functionality

TL;DR: You can immobilize your board by quickly power cycling the board. Once immobilized, you unlock the board by logging into Owie WiFi, tapping a button and power cycling the board. Keep reading for details.

**WARNING:** Arming your board for parking **will** disable the emergency recovery mode (2 restarts), so if you forget your network password, the only way to recover is to reflash via USB.
The normal OTA update mode will still be functional as normal (see below for OTA instructions).
Disarming the board will restore the emergency recovery mode.

Use these instructions if you want to be able to 'park' your onewheel using the power button sequence.
The park functionality comes by interrupting all communication between the BMS and the controller, thus causing an error 16.
This functionality can be removed quite easily by someone motivated enough and with enough knowledge; all that's required is to open up the board, remove Owie and solder the wires back together, or to reflash it via USB.

## Setup

1. Set an Owie network password in `Settings`.
1. Tap the `Arm` button in `Settings`. This arms your board so you can put it into 'park'.

## Parking your board

When you need to park your board, turn it on and then off in less than 5 seconds.

## Un-parking your board

Use these instructions to un-park your board so you can go ride.

1. Power on your board normally. Ignore the error 16 (that's how the board gets parked).
1. Connect to your password protected Owie network.
1. On the status screen, click the `Unlock` button.
1. Then as the button will remind you, restart your board to get rid of the error 16.

# Updating Owie

Use these instructions to update your Owie installation over WiFi (OTA).
Expand Down Expand Up @@ -120,7 +150,7 @@ They are the last step you can reasonably take before having to remove the chip

1. Once you have your hands on a firmware.bin file, copy that binary onto your flashing device of choice (desktop, laptop, phone). Some phones might not let you select the binary, thus you will need to use a computer.
1. Bring that device close to your board, and ensure that your Onewheel has at least a few percent of battery left in the tank (don't have it plugged in though).
1. Power cycle the Onewheel 3 times (reboot it) in 10 seconds. Keeping your app connected can be useful here as once Owie makes it into recovery mode, your board will report an error 16 (don't worry, this is supposed to happen).
1. Power cycle the Onewheel 2 times (reboot it) in less than 5 seconds. Keeping your app connected can be useful here as once Owie makes it into recovery mode, your board will report an error 16 (don't worry, this is supposed to happen).
1. On XR's your headlights will come on as normal, but after a few seconds they will dim and then totally turn off, followed by your power button light flashing rapidly to indicate that error 16.
1. Connect to the wifi network named _Owie-Recovery_, and navigate to your normal owie IP (192.168.4.1).
1. You should see the ElegantOTA selection screen (as shown below) come up.
Expand Down Expand Up @@ -167,4 +197,5 @@ The data frames sent by BMS are of the following general format:
1. Checksum - last two bytes of the frame - simply sum of all of the bytes in the frame, including the preamble.

## Message types:
I've isolated all message parsing code in `src/lib/bms/packet_parses.cpp`, the code should be self-explanatory.

I've isolated all message parsing code in `src/lib/bms/packet_parses.cpp`, the code should be self-explanatory.
28 changes: 28 additions & 0 deletions data/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ <h2>Owie Status</h2>
<form action="/settings">
<button>Settings</button>
</form>
<p></p>
<button style="display:none" id="unlockButton" data-locked="%IS_LOCKED%">
Unlock Board
</button>
</div>
<div id="homeButton" style="text-align:right;font-size:11px;">
<form action="/">
Expand All @@ -290,6 +294,30 @@ <h2>Owie Status</h2>
<p style="color:#aaa;">Owie <span id="OWIE_version">%OWIE_version%</span></p>
</div>
</div>
<script>
(() => {
const unlockBtn = document.getElementById("unlockButton");
if (!unlockBtn.dataset.locked) {
return;
}
unlockBtn.onclick = () => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/lock?unlock");
xhr.responseType = "text";
xhr.onload = () => {
if (xhr.status === 200) {
unlockBtn.innerHTML = "Unlocked! Restart your board to ride it.";
} else {
unlockBtn.innerHTML = "Error: " + xhr.status;
}
};
unlockBtn.setAttribute("disabled", "1");
xhr.send();
};
unlockBtn.removeAttribute("style");
})();
//# sourceURL=index.js
</script>
</body>

</html>
88 changes: 68 additions & 20 deletions data/settings.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,69 @@
<form method="post" action="/settings">
<fieldset>
<legend>BMS pairing:</legend>
<p>Serial of the currently hooked up BMS:</p>
<p><b>%BMS_CURRENT_SERIAL%</b></p>
<hr>
<p>Override the BMS serial:<input placeholder="Last 6 digits of the BMS your controller is paried with"
value="%BMS_SERIAL_OVERRIDE%" name="bs"></p>
</fieldset>
<fieldset>
<legend>WiFi options:</legend>
<p>Owie WiFi network name:<br><input placeholder="WiFi network name" value="%AP_SELF_NAME%"
name="apselfname"></p>
<p>Password:<br><input placeholder="Between 8 and 32 characters" value="%AP_PASSWORD%" name="pw"></p>
<p>Power (dBm):<br>
<select value="%WIFI_POWER%" name="wifipower">
%WIFI_POWER_OPTIONS%
</select>
</fieldset>
<button>Save</button>
</form>
<fieldset>
<legend>BMS pairing:</legend>
<p>Serial of the currently hooked up BMS:</p>
<p><b>%BMS_CURRENT_SERIAL%</b></p>
<hr>
<p>Override the BMS serial:<input placeholder="Last 6 digits of the BMS serial"
value="%BMS_SERIAL_OVERRIDE%" name="bs"></p>
</fieldset>
<fieldset>
<legend>WiFi options:</legend>
<p>Owie WiFi network name override:<br><input placeholder="WiFi network name" value="%AP_SELF_NAME%" name="apselfname"></p>
<p>Password:<br><input placeholder="Between 8 and 31 characters" value="%AP_PASSWORD%" name="pw"></p>
<p>Power (dBm):<br>
<select value="%WIFI_POWER%" name="wifipower">
%WIFI_POWER_OPTIONS%
</select>
</fieldset>
<fieldset>
<legend>Board locking:</legend>
<p id="lockingMsg"></p>
<button onclick="toggleArming(this)" id="armBtn" style="display:none" data-canenable="%CAN_ENABLE_LOCKING%"
data-enabled="%LOCKING_ENABLED%">
Disabled
</button>
</fieldset>
<hr>
<button>Save</button>
</form>
<script>
(() => {
const cannotLockMsg = "First set WiFi password above and save settings to enable board locking.";
const lockingEnabledMsg = "Locking is enabled! To lock the board, turn it on and off in less than 5 seconds.";
const lockingDisabledMsg = "Locking is disabled."
const btn = document.getElementById("armBtn");
const lockingMsg = document.getElementById("lockingMsg");
if (!btn.dataset.canenable) {
lockingMsg.innerText = cannotLockMsg;
return;
}
btn.removeAttribute('style');

function updateButton() {
const isEnabled = !!btn.dataset.enabled;
btn.removeAttribute("disabled");
lockingMsg.innerText = isEnabled ? lockingEnabledMsg : lockingDisabledMsg;
btn.innerText = isEnabled ? "Disable locking" : "Enable locking";
}
updateButton();

btn.onclick = () => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/lock?toggleArm");
xhr.resposeType = "text";

xhr.onload = () => {
if (xhr.readyState !== xhr.DONE || xhr.status !== 200) {
btn.innerText = "Error: " + xhr.status;
} else {
btn.dataset.enabled = xhr.responseText;
updateButton();
}
};
btn.setAttribute("disabled", "1");
xhr.send();
}
})();
//# sourceURL=settings.js
</script>
10 changes: 10 additions & 0 deletions include/global_instances.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#ifndef GLOBAL_INSTANCES_H
#define GLOBAL_INSTANCES_H

#include "ArduinoOTA.h"
#include "HardwareSerial.h"

extern HardwareSerial Serial;
extern ArduinoOTAClass ArduinoOTA;

#endif // GLOBAL_INSTANCES_H
83 changes: 55 additions & 28 deletions pio_tools/gen_data.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
Import("env")
import os
import subprocess

from SCons.Script import COMMAND_LINE_TARGETS

if "idedata" in COMMAND_LINE_TARGETS:
env.Exit(0)


def ReadAndMaybeMinifyFiles(fullPath):
if not fullPath.endswith('.html'):
with open(fullPath, "rb") as f:
return f.read()
originalSize = os.stat(fullPath).st_size
result = subprocess.run(['html-minifier-terser',
'--collapse-whitespace',
'--remove-comments',
'--minify-js',
'true',
'--minify-css',
'true',
fullPath], stdout=subprocess.PIPE)
minifiedContent = result.stdout
print("Minified '%s' with from %d to %d bytes" % (fullPath, originalSize, len(minifiedContent)))
return minifiedContent


def GenData():
dataDir = os.path.join(env["PROJECT_DIR"], "data")
print("dataDir = %s\n" % dataDir)
Expand All @@ -16,31 +36,38 @@ def GenData():
if not os.path.exists(genDir):
os.mkdir(genDir)
env.Append(CPPPATH=[genDir])

files = sorted(file for file in os.listdir(dataDir)
if os.path.isfile(os.path.join(dataDir, file)))

with open(os.path.join(genDir, "data.h"), 'w') as out:
out.write("// WARNING: Autogenerated by pio_tools/gen_data.py, don't edit manually.\n")
out.write("#ifndef DATA_H\n")
out.write("#define DATA_H\n\n")
for name in files:
varName = name.upper().replace(".", "_")
sizeName = varName + "_SIZE"
storageArrayName = varName + "_PROGMEM_ARRAY"
out.write("static const unsigned char %s[] PROGMEM = {\n" % storageArrayName)
with open(os.path.join(dataDir, name), "rb") as f:
contents = f.read()
firstByte = True
for b in contents:
if not firstByte:
out.write(",")
else:
firstByte = False
out.write(str(b))
out.write("};\n")
out.write("#define %s FPSTR(%s)\n" % (varName, storageArrayName))
out.write("#define %s sizeof(%s)\n\n" % (sizeName, storageArrayName))
out.write("\n#endif // DATA_H\n")

GenData()

files = sorted(file for file in os.listdir(dataDir)
if os.path.isfile(os.path.join(dataDir, file)))

out = "// WARNING: Autogenerated by pio_tools/gen_data.py, don't edit manually.\n"
out += "#ifndef OWIE_GENERATED_DATA_H\n"
out +="#define OWIE_GENERATED_DATA_H\n\n"
for name in files:
varName = name.upper().replace(".", "_")
sizeName = varName + "_SIZE"
storageArrayName = varName + "_PROGMEM_ARRAY"
out += (
"static const unsigned char %s[] PROGMEM = {\n " % storageArrayName)
firstByte = True
fileContent = ReadAndMaybeMinifyFiles(os.path.join(dataDir, name))
column = 0
for b in fileContent:
if not firstByte:
out += ","
else:
firstByte = False
column = column + 1
if column > 20:
column = 0
out += "\n "
out += str(b)
out += "};\n"
out += "#define %s FPSTR(%s)\n" % (varName, storageArrayName)
out += "#define %s sizeof(%s)\n\n" % (sizeName, storageArrayName)
out += "\n#endif // OWIE_GENERATED_DATA_H\n"
with open(os.path.join(genDir, "data.h"), 'w') as f:
f.write(out)
print("Wrote data.h\n")

GenData()
9 changes: 5 additions & 4 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ custom_nanopb_options =

extra_scripts =
pre:pio_tools/gen_data.py

build_flags =
; Disable global eeprom as we use a custom library
-DNO_GLOBAL_EEPROM
; Disable global instances to save space
-DNO_GLOBAL_INSTANCES
;-DDEBUG_ESP_PORT=Serial
;-DDEBUG_EEPROM_ROTATE_PORT=Serial
;-DDEBUG_ESP_CORE
Expand All @@ -36,15 +37,15 @@ build_flags =
lib_deps =
ayushsharma82/AsyncElegantOTA@^2.2.7
xoseperez/EEPROM_Rotate@^0.9.2
me-no-dev/ESP Async WebServer@^1.2.3
ottowinter/ESPAsyncWebServer-esphome@^2.1.0
nanopb/Nanopb@^0.4.6
bblanchon/ArduinoJson@^6.19.4

[env:ota]
extends = env:d1_mini_lite_clone
upload_protocol = espota
upload_port = owie.local
;board_build.gzip_fw = true
upload_port = 192.168.1.161

[env:native]
platform = native
Expand Down
Loading

0 comments on commit fd56b2b

Please sign in to comment.