BT handshake function complete
This commit is contained in:
parent
b6928e3ee7
commit
839100c0e1
@ -1,6 +1,6 @@
|
||||
{
|
||||
"host_ip": "192.168.0.114",
|
||||
"kl630_ip": "192.168.0.201",
|
||||
"host_ip": "192.168.0.105",
|
||||
"kl630_ip": "192.168.0.123",
|
||||
"port": 8080,
|
||||
"docker_image": "kl630-dev"
|
||||
}
|
||||
576
README.md
576
README.md
@ -199,12 +199,580 @@ killall kp_firmware_host_stream
|
||||
|
||||
---
|
||||
|
||||
## CLI 版本(不需要瀏覽器)
|
||||
---
|
||||
|
||||
## Web Server 命令行操作指南
|
||||
|
||||
若不喜歡 GUI,可透過指令和 API 完全控制 `web_serve.py`,不需要開瀏覽器。
|
||||
|
||||
### 啟動 Web Server
|
||||
|
||||
```bash
|
||||
python build_and_serve.py # 編譯 + 檢查 + 啟動 HTTP server
|
||||
python build_and_serve.py --no-build # 跳過編譯,直接 serve
|
||||
python build_and_serve.py --port 9090
|
||||
# 預設 port 8080
|
||||
python web_serve.py
|
||||
|
||||
# 指定端口
|
||||
python web_serve.py --port 8080
|
||||
python web_serve.py --port 9090
|
||||
```
|
||||
|
||||
Server 啟動後可用 `curl` 調用各項功能。
|
||||
|
||||
### Web Server API 參考
|
||||
|
||||
#### 1. **查詢/保存設定**
|
||||
|
||||
```bash
|
||||
# 查看現在設定(Host IP、KL630 IP、HTTP Port、Docker Image)
|
||||
curl http://localhost:8080/api/config
|
||||
|
||||
# 修改設定
|
||||
curl -X POST http://localhost:8080/api/config \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"host_ip": "192.168.3.1",
|
||||
"kl630_ip": "192.168.3.10",
|
||||
"port": 8080,
|
||||
"docker_image": "kl630-dev"
|
||||
}'
|
||||
```
|
||||
|
||||
#### 2. **查看 build/ 目錄下載文件列表**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/files
|
||||
```
|
||||
|
||||
輸出示例:
|
||||
```json
|
||||
[
|
||||
{"name": "kp_firmware_host_stream", "size": 5242880},
|
||||
{"name": "host_stream.ini", "size": 2048},
|
||||
{"name": "demo_rtsp.sh", "size": 512},
|
||||
{"name": "deploy.sh", "size": 1024}
|
||||
]
|
||||
```
|
||||
|
||||
#### 3. **編譯(Server-Sent Events 串流輸出)**
|
||||
|
||||
```bash
|
||||
# 啟動編譯,即時輸出編譯 log
|
||||
curl http://localhost:8080/api/compile/run
|
||||
|
||||
# 搭配 jq 只看 error
|
||||
curl http://localhost:8080/api/compile/run | grep -o '"kind":"error"' | wc -l
|
||||
```
|
||||
|
||||
編譯 log 格式(SSE):
|
||||
```json
|
||||
{"kind": "log", "text": "Starting cross-compile..."}
|
||||
{"kind": "ok", "text": "Compile SUCCESS"}
|
||||
{"kind": "error", "text": "Compile FAILED (exit 1)"}
|
||||
```
|
||||
|
||||
#### 4. **部署到 KL630(Telnet)**
|
||||
|
||||
```bash
|
||||
# 執行完整部署流程(停止舊 firmware、下載 binary、啟動新 firmware)
|
||||
curl http://localhost:8080/api/deploy/run
|
||||
|
||||
# 查看部署 log(最後 100 行)可透過網頁 Terminal 或直接用 tail
|
||||
```
|
||||
|
||||
部署流程等同於 Web UI 按下 **Deploy to KL630**。
|
||||
|
||||
#### 5. **ISP / Flash 一次性設定**
|
||||
|
||||
```bash
|
||||
# 一次性 ISP setup(DOL-HDR 參數)
|
||||
curl http://localhost:8080/api/spi_setup/run
|
||||
|
||||
# 或藍牙相關設定
|
||||
curl http://localhost:8080/api/bt_setup/run
|
||||
```
|
||||
|
||||
#### 6. **INI 設定讀取/寫入**
|
||||
|
||||
```bash
|
||||
# 讀取目前 INI 設定
|
||||
curl http://localhost:8080/api/ini
|
||||
|
||||
# 修改 INI 並立即套用到裝置(等同 Web UI "Apply to Device + Restart")
|
||||
curl -X POST http://localhost:8080/api/ini/apply \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"fec_mode": 4,
|
||||
"DrawBoxEnable": 1,
|
||||
"voc_enable": 0
|
||||
}'
|
||||
```
|
||||
|
||||
常見 INI 參數:
|
||||
- `fec_mode`: 0=關閉, 1-5=魚眼校正模式
|
||||
- `DrawBoxEnable`: 0/1 — H.264 burn-in 偵測框
|
||||
- `voc_enable`: 0/1 — HDMI VOC 輸出
|
||||
- `ModelPath`, `ModelId`, `JobId`: 推論模型設定
|
||||
|
||||
#### 7. **RTSP 串流預覽 / 停止**
|
||||
|
||||
```bash
|
||||
# 啟動 RTSP 預覽(返回 MJPEG 影像串流)
|
||||
curl http://localhost:8080/api/stream/video > stream.mjpeg
|
||||
|
||||
# 停止 RTSP 預覽
|
||||
curl -X POST http://localhost:8080/api/stream/stop
|
||||
```
|
||||
|
||||
#### 8. **STDC 推論統計**
|
||||
|
||||
```bash
|
||||
# 查看即時推論統計(幀率、語義分割百分比)
|
||||
curl http://localhost:8080/api/stdc/stats
|
||||
|
||||
# 啟動推論
|
||||
curl -X POST http://localhost:8080/api/stdc/start
|
||||
|
||||
# 停止推論
|
||||
curl -X POST http://localhost:8080/api/stdc/stop
|
||||
```
|
||||
|
||||
輸出示例:
|
||||
```json
|
||||
{
|
||||
"frame": 42,
|
||||
"mov": 1,
|
||||
"diff": 4.2,
|
||||
"classes": {
|
||||
"bunker": 0.0,
|
||||
"car": 8.3,
|
||||
"grass": 0.0,
|
||||
"greenery": 12.1,
|
||||
"person": 0.0,
|
||||
"pond": 0.0,
|
||||
"road": 71.4,
|
||||
"tree": 8.2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 9. **遠端執行指令(Telnet 到裝置)**
|
||||
|
||||
```bash
|
||||
# 在裝置上執行指令並取得輸出
|
||||
curl -X POST http://localhost:8080/api/terminal/exec \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cmd": "ps | grep firmware"}'
|
||||
|
||||
# 查看 firmware log
|
||||
curl -X POST http://localhost:8080/api/terminal/exec \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cmd": "cat /tmp/fw.log"}'
|
||||
|
||||
# 查看可用模型
|
||||
curl -X POST http://localhost:8080/api/terminal/exec \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cmd": "ls -lh /mnt/flash/vienna/nef/"}'
|
||||
```
|
||||
|
||||
#### 10. **開機自動啟動設定**
|
||||
|
||||
```bash
|
||||
# 查看自動啟動腳本設定
|
||||
curl http://localhost:8080/api/autostart/read
|
||||
|
||||
# 寫入自動啟動腳本(`/etc/init.d/S99firmware`)
|
||||
curl -X POST http://localhost:8080/api/autostart/write \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"autostart_mode": "rtsp"}' # or "hdmi", "rtsp_hdmi"
|
||||
```
|
||||
|
||||
#### 11. **模型管理**
|
||||
|
||||
```bash
|
||||
# 查看可用模型列表
|
||||
curl http://localhost:8080/api/model/list
|
||||
|
||||
# 切換模型(需要指定 ModelId / JobId)
|
||||
curl -X POST http://localhost:8080/api/model \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model_id": "100001",
|
||||
"job_id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### 完整工作流示例(指令行完全不用 GUI)
|
||||
|
||||
```bash
|
||||
# 1. 設定網路(Host IP 和 KL630 IP)
|
||||
curl -X POST http://localhost:8080/api/config \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"host_ip": "192.168.3.1", "kl630_ip": "192.168.3.10"}'
|
||||
|
||||
# 2. 編譯
|
||||
echo "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Compiling..."
|
||||
curl http://localhost:8080/api/compile/run | grep '"kind"' | tail -1
|
||||
|
||||
# 3. 檢查生成的文件
|
||||
echo "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Build files:"
|
||||
curl http://localhost:8080/api/files | grep name
|
||||
|
||||
# 4. 部署到裝置
|
||||
echo "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Deploying to KL630..."
|
||||
curl http://localhost:8080/api/deploy/run
|
||||
|
||||
# 5. 查看推論統計
|
||||
echo "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> STDC Stats:"
|
||||
curl http://localhost:8080/api/stdc/stats | python -m json.tool
|
||||
|
||||
# 6. 設定魚眼校正(FEC Mode 4)
|
||||
curl -X POST http://localhost:8080/api/ini/apply \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"fec_mode": 4}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI 指令完整參考
|
||||
|
||||
完全不用瀏覽器,所有操作均可透過指令完成。
|
||||
|
||||
---
|
||||
|
||||
### 1. 編譯(交叉編譯 ARM binary)
|
||||
|
||||
**方法 A — `build_and_serve.py`(推薦)**
|
||||
|
||||
```bash
|
||||
# 完整流程:編譯 + 檢查 binary + 啟動 HTTP server
|
||||
python build_and_serve.py
|
||||
|
||||
# 常用旗標
|
||||
python build_and_serve.py --no-build # 跳過編譯,直接 serve(binary 已存在時)
|
||||
python build_and_serve.py --no-serve # 只編譯 + 檢查,不啟動 server
|
||||
python build_and_serve.py --port 9090 # 指定 HTTP server port(預設 8080)
|
||||
python build_and_serve.py --image my-kl630 # 指定 Docker image 名稱
|
||||
python build_and_serve.py --no-copy # 不複製到網路資料夾
|
||||
python build_and_serve.py --copy-dst /tmp/out # 指定複製目的地
|
||||
```
|
||||
|
||||
**方法 B — 直接 Docker**
|
||||
|
||||
```bash
|
||||
# 建立 Docker image(只需一次)
|
||||
docker build -t kl630-dev .
|
||||
|
||||
# 交叉編譯
|
||||
docker run --rm \
|
||||
-v "$(pwd):/workspace/kl630_build" \
|
||||
kl630-dev \
|
||||
bash /workspace/kl630_build/compile.sh
|
||||
|
||||
# 確認輸出
|
||||
ls -lh build/kp_firmware_host_stream
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 啟動 HTTP File Server(裝置 wget 用)
|
||||
|
||||
```bash
|
||||
# 用 build_and_serve.py(推薦,自動 copy INI + deploy.sh)
|
||||
python build_and_serve.py --no-build --no-check --port 8080
|
||||
|
||||
# 或直接 Python 內建 server
|
||||
cd build && python -m http.server 8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 部署到裝置(Telnet / 裝置端指令)
|
||||
|
||||
先確認 HTTP server 在 PC 端已啟動,再 Telnet 進裝置:
|
||||
|
||||
```bash
|
||||
telnet 192.168.3.10
|
||||
```
|
||||
|
||||
在裝置端執行:
|
||||
|
||||
```sh
|
||||
# 一鍵下載並部署(最常用)
|
||||
wget http://192.168.3.1:8080/deploy.sh -O /tmp/deploy.sh && sh /tmp/deploy.sh
|
||||
|
||||
# 或分步驟手動執行:
|
||||
|
||||
# 停止舊 firmware
|
||||
killall -9 kp_firmware_host_stream 2>/dev/null; killall -9 rtsps 2>/dev/null
|
||||
sleep 1; rm -f /dev/shm/*
|
||||
|
||||
# 下載 binary 和 INI
|
||||
wget http://192.168.3.1:8080/kp_firmware_host_stream -O /mnt/flash/vienna/kp_firmware_host_stream
|
||||
chmod +x /mnt/flash/vienna/kp_firmware_host_stream
|
||||
wget http://192.168.3.1:8080/host_stream.ini -O /mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/ini/host_stream.ini
|
||||
|
||||
# 下載並啟動 RTSP demo
|
||||
wget http://192.168.3.1:8080/demo_rtsp.sh -O /mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/ini/demo_rtsp.sh
|
||||
chmod +x /mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/ini/demo_rtsp.sh
|
||||
cd /mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin
|
||||
sh ./ini/demo_rtsp.sh
|
||||
```
|
||||
|
||||
**新機器一次性 ISP 設定**(只需跑一次):
|
||||
|
||||
```sh
|
||||
wget http://192.168.3.1:8080/deploy.sh -O /tmp/deploy.sh && sh /tmp/deploy.sh --setup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 裝置端常用指令
|
||||
|
||||
```sh
|
||||
# 確認 firmware 是否在跑
|
||||
ps | grep firmware
|
||||
|
||||
# 查看 firmware log
|
||||
cat /tmp/fw.log
|
||||
tail -f /tmp/fw.log # 持續監看(BusyBox 無 -f,用 watch 替代)
|
||||
|
||||
# 查看 RTSP demo log
|
||||
cat /tmp/rtsp_demo.log
|
||||
|
||||
# 手動停止 firmware
|
||||
killall -9 kp_firmware_host_stream
|
||||
killall -9 rtsps
|
||||
|
||||
# 直接啟動(指定模型)
|
||||
BIN_DIR=/mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin
|
||||
FW=/mnt/flash/vienna/kp_firmware_host_stream
|
||||
LD_LIBRARY_PATH=/mnt/flash/vienna/lib $FW \
|
||||
-m nef/stdc_scnn_fp.nef \
|
||||
-i 100001 -j 1 &
|
||||
|
||||
# 切換輸出模式(不用重新 deploy)
|
||||
sh $BIN_DIR/ini/demo_rtsp.sh # RTSP 串流
|
||||
sh $BIN_DIR/ini/demo_hdmi.sh # HDMI 顯示
|
||||
sh $BIN_DIR/ini/demo_rtsp_hdmi.sh # RTSP + HDMI 同時
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. INI 設定(指令修改)
|
||||
|
||||
直接用 `sed` 修改裝置上的 INI,不需要重新 deploy:
|
||||
|
||||
```sh
|
||||
INI=/mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/ini/host_stream.ini
|
||||
|
||||
# 開啟 / 關閉魚眼校正 FEC
|
||||
sed -i 's/^fec_mode.*/fec_mode = 4/' $INI # Mode 4(180° Two Direction)
|
||||
sed -i 's/^fec_mode.*/fec_mode = 0/' $INI # 關閉
|
||||
|
||||
# 開啟 / 關閉 DrawBox(H.264 burn-in 框)
|
||||
sed -i 's/^DrawBoxEnable.*/DrawBoxEnable = 1/' $INI
|
||||
sed -i 's/^DrawBoxEnable.*/DrawBoxEnable = 0/' $INI
|
||||
|
||||
# 開啟 HDMI / VOC 輸出
|
||||
sed -i 's/^voc_enable.*/voc_enable = 1/' $INI
|
||||
|
||||
# 查看目前設定
|
||||
grep -E "fec_mode|DrawBoxEnable|voc_enable|ModelPath|ModelId" $INI
|
||||
```
|
||||
|
||||
修改 INI 後需重啟 firmware 才生效:
|
||||
|
||||
```sh
|
||||
killall -9 kp_firmware_host_stream 2>/dev/null
|
||||
sh /mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/ini/demo_rtsp.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Mock Server(事件測試)
|
||||
|
||||
```bash
|
||||
# 啟動 mock server(port 8081)
|
||||
python tools/mock_server/server.py
|
||||
|
||||
# 開啟監控網頁
|
||||
# http://localhost:8081/
|
||||
```
|
||||
|
||||
用 `curl` 送測試事件(不用開網頁):
|
||||
|
||||
```bash
|
||||
# Channel A — 送即時事件(HTTP)
|
||||
curl -X POST http://localhost:8081/api/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"response_type":"violation","content":{"id":"1","date":"2025-01-01T00:00:00+08:00","type":"road","level":2}}'
|
||||
|
||||
# 常用事件類型:road / grass / hazard / person / bunker / pond / tree / car
|
||||
# level:0=解除, 1=警告, 2=嚴重, 3=緊急
|
||||
|
||||
# 查詢事件列表
|
||||
curl http://localhost:8081/api/events
|
||||
|
||||
# Channel C — 送 CAN 事件(測試 CAN bus TX)
|
||||
curl -X POST http://localhost:8081/api/can/send \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"hazard","level":1,"can_id":256}'
|
||||
|
||||
# 送油門控制指令(CAN ID 0x75)
|
||||
curl -X POST http://localhost:8081/api/can/send_cmd \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cmd":128,"can_id":117}' # cmd: 0=關閉, 32/64/128/255=油門檔位
|
||||
|
||||
# 查詢 CAN bus 狀態
|
||||
curl http://localhost:8081/api/can/status
|
||||
|
||||
# 啟動 CAN interface(等同 UI Bring Up 按鈕)
|
||||
curl -X POST http://localhost:8081/api/can/bringup \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"channel":"can0","bitrate":250000}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BLE 指令協議(藍牙通訊)
|
||||
|
||||
KL630 透過 DX-BT24 模組(UART `/dev/ttyS1` @ 115200)與 iPad / 手機 app 溝通。
|
||||
所有訊息均為 UTF-8 JSON,無需 CRLF。
|
||||
|
||||
---
|
||||
|
||||
### Command(App → KL630)
|
||||
|
||||
#### 狀態確認
|
||||
```json
|
||||
{"command":"status_check"}
|
||||
```
|
||||
回傳 Notify:`response_type=status_check`
|
||||
|
||||
#### 查詢租借狀態
|
||||
```json
|
||||
{"command":"get_rent_status"}
|
||||
```
|
||||
回傳 Notify:`response_type=rent_status`
|
||||
|
||||
#### 租借
|
||||
```json
|
||||
{"command":"rent"}
|
||||
```
|
||||
若目前 status=0 則轉為 1,其他狀態忽略。回傳 Notify:`response_type=rent_status`
|
||||
|
||||
#### 歸還
|
||||
```json
|
||||
{"command":"return"}
|
||||
```
|
||||
若目前 status=1 則轉為 0,其他狀態忽略。回傳 Notify:`response_type=rent_status`
|
||||
|
||||
---
|
||||
|
||||
### Notify(KL630 → App)
|
||||
|
||||
#### 系統狀態(回應 status_check)
|
||||
```json
|
||||
{"response_type":"status_check","content":{"ares_x_version":"1.0.0","bluetooth_peripheral_name":"BT-24","is_intervention_cart_control":false}}
|
||||
```
|
||||
|
||||
| 欄位 | 說明 |
|
||||
|------|------|
|
||||
| `ares_x_version` | 韌體版本,由 INI `event:ares_version` 設定 |
|
||||
| `bluetooth_peripheral_name` | BLE 掃描名稱,由 INI `event:bt_name` 設定 |
|
||||
| `is_intervention_cart_control` | 是否正在介入車輛控制(CAN bus 活動中)|
|
||||
|
||||
#### 租借狀態(回應 rent / return / get_rent_status)
|
||||
```json
|
||||
{"response_type":"rent_status","content":{"status":0}}
|
||||
```
|
||||
|
||||
| status | 說明 |
|
||||
|--------|------|
|
||||
| `0` | 未租借 |
|
||||
| `1` | 租借中 |
|
||||
| `2` | 管理模式 |
|
||||
|
||||
#### 違規事件(由推論結果觸發)
|
||||
```json
|
||||
{"response_type":"violation","content":{"id":"abcd-1234-0000-0000","date":"2025-12-08T06:08:49Z","type":"lane","level":2}}
|
||||
```
|
||||
|
||||
| level | 說明 |
|
||||
|-------|------|
|
||||
| `0` | 解除 |
|
||||
| `1` | 違規發生 |
|
||||
| `2` | 違規持續 6 秒 |
|
||||
| `3` | 違規持續 10 秒 |
|
||||
|
||||
#### 危險區域警告
|
||||
```json
|
||||
{"response_type":"alert","content":{"left_level":0,"right_level":1}}
|
||||
```
|
||||
左右各獨立,`0`=無危險,`1`=有危險。
|
||||
|
||||
---
|
||||
|
||||
### INI 設定(`ini/host_stream.ini` `[event]` 區段)
|
||||
|
||||
```ini
|
||||
bt_uart_dev = /dev/ttyS1 # UART 裝置路徑
|
||||
bt_at_probe = 0 # 0: 正常啟動;1: 首次設定(9600→115200 升速)
|
||||
ares_version = 1.0.0 # 韌體版本字串
|
||||
bt_name = BT-24 # BLE 掃描名稱
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 測試指令
|
||||
|
||||
#### 用 BLE app(nRF Connect 等)直接測試
|
||||
|
||||
複製以下 JSON 貼入 Write Characteristic 欄位送出:
|
||||
|
||||
```
|
||||
{"command":"status_check"}
|
||||
```
|
||||
```
|
||||
{"command":"get_rent_status"}
|
||||
```
|
||||
```
|
||||
{"command":"rent"}
|
||||
```
|
||||
```
|
||||
{"command":"return"}
|
||||
```
|
||||
|
||||
#### 用 Mock Server 模擬裝置回傳(curl)
|
||||
|
||||
```bash
|
||||
# 模擬違規事件通知
|
||||
curl -X POST http://localhost:8081/api/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"response_type":"violation","content":{"id":"test-001","date":"2026-04-22T00:00:00Z","type":"lane","level":1}}'
|
||||
|
||||
# 模擬違規持續 6 秒
|
||||
curl -X POST http://localhost:8081/api/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"response_type":"violation","content":{"id":"test-001","date":"2026-04-22T00:00:00Z","type":"lane","level":2}}'
|
||||
|
||||
# 模擬違規解除
|
||||
curl -X POST http://localhost:8081/api/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"response_type":"violation","content":{"id":"test-001","date":"2026-04-22T00:00:00Z","type":"lane","level":0}}'
|
||||
|
||||
# 模擬危險區域警告(右側有危險)
|
||||
curl -X POST http://localhost:8081/api/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"response_type":"alert","content":{"left_level":0,"right_level":1}}'
|
||||
|
||||
# 模擬租借狀態更新
|
||||
curl -X POST http://localhost:8081/api/event \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"response_type":"rent_status","content":{"status":1}}'
|
||||
|
||||
# 查看目前所有事件記錄
|
||||
curl http://localhost:8081/api/events
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -41,6 +41,25 @@ DEMO_RTSP_SRC = SCRIPT_DIR / "tools" / "device" / "demo_rtsp.sh"
|
||||
# Docker image name — change if yours is different
|
||||
DOCKER_IMAGE = "kl630-dev"
|
||||
|
||||
# ── Step clean: Remove build artefacts ───────────────────────────────────────
|
||||
def step_clean() -> None:
|
||||
print(head("\n=== [clean] Clean Build Folder ==="))
|
||||
removed = 0
|
||||
for pattern in ("*.o",):
|
||||
for f in BUILD_DIR.glob(pattern):
|
||||
f.unlink()
|
||||
print(info(f" rm {f.name}"))
|
||||
removed += 1
|
||||
binary = BUILD_DIR / BINARY_NAME
|
||||
if binary.exists():
|
||||
binary.unlink()
|
||||
print(info(f" rm {BINARY_NAME}"))
|
||||
removed += 1
|
||||
if removed == 0:
|
||||
print(ok(" Nothing to clean."))
|
||||
else:
|
||||
print(ok(f" Removed {removed} file(s)."))
|
||||
|
||||
# ── Step 0: Ensure Docker image exists ───────────────────────────────────────
|
||||
def step_ensure_image() -> bool:
|
||||
"""Build the Docker image from Dockerfile if it doesn't exist yet."""
|
||||
@ -252,6 +271,10 @@ def step_serve(port: int):
|
||||
def main():
|
||||
global DOCKER_IMAGE
|
||||
parser = argparse.ArgumentParser(description="KL630 build + check + serve")
|
||||
parser.add_argument("--clean", action="store_true",
|
||||
help="清除 build/ 下的 .o 和 binary,然後結束(不 build)")
|
||||
parser.add_argument("--clean-build", action="store_true",
|
||||
help="先 clean 再 build(等同 --clean 後重新執行)")
|
||||
parser.add_argument("--no-build", action="store_true",
|
||||
help="跳過 Docker build,只 check + serve")
|
||||
parser.add_argument("--no-check", action="store_true",
|
||||
@ -277,6 +300,15 @@ def main():
|
||||
print(f" Port : {args.port}")
|
||||
print(f" Copy : {args.copy_dst}")
|
||||
|
||||
# ===== Clean only =====
|
||||
if args.clean:
|
||||
step_clean()
|
||||
sys.exit(0)
|
||||
|
||||
# ===== Clean then build =====
|
||||
if args.clean_build:
|
||||
step_clean()
|
||||
|
||||
# ===== Step 1: Build =====
|
||||
if not args.no_build:
|
||||
if not step_ensure_image():
|
||||
|
||||
@ -18,7 +18,11 @@ mkdir -p $BUILD_DIR
|
||||
CFLAGS="-DVATICS_PLATFORM -DKL630 -D_GNU_SOURCE -U_FORTIFY_SOURCE"
|
||||
CFLAGS="$CFLAGS -march=armv7-a -mfpu=neon -mfloat-abi=hard -Os"
|
||||
CFLAGS="$CFLAGS -Wall -Wno-unused-variable -Wno-unused-function"
|
||||
#CFLAGS="$CFLAGS -DENABLE_DBG_LOG"
|
||||
|
||||
# CAN bus: Use hardware SPI /dev/spidev1.0 (not GPIO bit-bang)
|
||||
# Disabled SPI_BITBANG - using native kernel SPI driver instead
|
||||
# CFLAGS="$CFLAGS -DSPI_BITBANG=1"
|
||||
# CFLAGS="$CFLAGS -DSPI_GPIO_CS=1 -DSPI_GPIO_MOSI=2 -DSPI_GPIO_SCK=3 -DSPI_GPIO_MISO=4"
|
||||
|
||||
INCLUDES="-I$WORKSPACE/include/host_stream"
|
||||
INCLUDES="$INCLUDES -I$WORKSPACE/include/app_flow"
|
||||
|
||||
@ -12,6 +12,13 @@
|
||||
8. [關鍵參數速查表](#8-關鍵參數速查表)
|
||||
9. [STDC 語義分割邏輯詳解](#9-stdc-語義分割邏輯詳解)
|
||||
10. [下一階段:高爾夫球車警示與煞車控制](#10-下一階段高爾夫球車警示與煞車控制)
|
||||
11. [藍牙通訊:DX-BT24 BLE UART 模組](#11-藍牙通訊dx-bt24-ble-uart-模組)
|
||||
12. [CAN Bus 通訊:MCP2515 SPI 控制器](#12-can-bus-通訊mcp2515-spi-控制器)
|
||||
13. [事件錄製器(Event Recorder)](#13-事件錄製器event-recorder)
|
||||
14. [GPIO 直接存取:gpio_devmem](#14-gpio-直接存取gpio_devmem)
|
||||
15. [STDC 模型重訓練與後處理 Python→C 移植](#15-stdc-模型重訓練與後處理-pythonc-移植)
|
||||
16. [快照功能:實作 / 事件觸發 / 打包上傳](#16-快照功能實作--事件觸發--打包上傳)
|
||||
17. [本週進度與下週計畫(2026-04-16)](#17-本週進度與下週計畫2026-04-16)
|
||||
|
||||
---
|
||||
|
||||
@ -870,16 +877,16 @@ GPIO 煞車訊號 GPIO 警告燈 ← gpio_ctrl.c 新增
|
||||
馬達控制器 / 電磁煞車
|
||||
```
|
||||
|
||||
### 10.3 需新增/修改的檔案
|
||||
### 10.3 已完成 / 待完成項目
|
||||
|
||||
| 檔案 | 動作 | 說明 |
|
||||
| 檔案 | 狀態 | 說明 |
|
||||
|------|------|------|
|
||||
| `src/host_stream/gpio_ctrl.c` | **新增** | GPIO 初始化、`gpio_set_warning(level)` |
|
||||
| `src/host_stream/gpio_ctrl.h` | **新增** | 函式宣告、pin 定義 |
|
||||
| `src/host_stream/app_header_init.c` | **修改** | STDC 結果區塊加 warn_level 計算 + 呼叫 `gpio_set_warning()` |
|
||||
| `src/host_stream/kp_firmware.c` | **修改** | `main()` 加入 `gpio_ctrl_init()` |
|
||||
| `ini/host_stream.ini` | **修改** | 新增 `[golf_cart]` 區段,放置 GPIO pin 號與閥值 |
|
||||
| `src/host_stream/kp_firmware.c` | **修改** | `loadConfig()` 讀取 `[golf_cart]` 參數 |
|
||||
| `src/host_stream/event_recorder.c` | ✅ **已實作** | 草地狀態機 + 單次碰撞事件 + JPEG 快照 + tar.gz 上傳(見第 13 節) |
|
||||
| `src/host_stream/can_bus.c` | ✅ **已實作** | CAN 控制指令 `can_bus_send_control_cmd(level)`(見第 12 節) |
|
||||
| `src/host_stream/gpio_devmem.c` | ✅ **已實作** | /dev/mem GPIO(SPI bit-bang 用,見第 14 節) |
|
||||
| `src/host_stream/bt_uart.c` | ✅ **已實作** | BLE JSON 事件推送(見第 11 節) |
|
||||
| 警告燈 / 煞車 relay GPIO | ❌ **待實作** | 需確認板上 GPIO pin 與繼電器接線後實作 `gpio_set_warning(level)` |
|
||||
| `ini/host_stream.ini [golf_cart]` | ❌ **待實作** | 警告/煞車 GPIO pin 號設定 |
|
||||
|
||||
### 10.4 警告等級 → GPIO 輸出對應
|
||||
|
||||
@ -919,5 +926,691 @@ BrakePulseMs = 0 ; 0=持續拉高;>0=觸發後維持 N ms 再釋放
|
||||
|
||||
---
|
||||
|
||||
*報告產生日期:2026-03-24*
|
||||
*適用版本:VMF Vienna SDK 2.5.6,STDC_0520.nef,Firmware Ver. 1.2.0.1203*
|
||||
---
|
||||
|
||||
## 11. 藍牙通訊:DX-BT24 BLE UART 模組
|
||||
|
||||
### 11.1 模組規格
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| 模組型號 | DX-BT24 |
|
||||
| 通訊介面 | 透明 UART → BLE(BLE 4.2) |
|
||||
| BLE 服務 | UUID `0xFFE0`;Notify `FFE1`(裝置→手機);Write `FFE2`(手機→裝置) |
|
||||
| UART 預設鮑率 | 9600 bps(出廠);一次性升級至 115200 bps |
|
||||
| KL630 UART 腳位 | `/dev/ttyS1`(J15 擴充連接器) |
|
||||
|
||||
### 11.2 功能架構
|
||||
|
||||
```
|
||||
KL630 firmware
|
||||
event_recorder_update()
|
||||
│
|
||||
▼
|
||||
bt_uart_send_json() ← 非阻塞:放入 FIFO 佇列即返回
|
||||
│
|
||||
▼
|
||||
[bt_writer_thread] ← 專用執行緒,實際寫入 UART
|
||||
│
|
||||
▼
|
||||
/dev/ttyS1 → DX-BT24 → BLE → iPhone nRF Connect / 球場 iPad App
|
||||
```
|
||||
|
||||
**關鍵設計**:`bt_uart_send_json()` 只做 `malloc + enqueue + cond_signal`,永不阻塞推論主迴圈。寫入執行緒在有訊息時才喚醒並寫入 UART。
|
||||
|
||||
### 11.3 JSON 事件格式
|
||||
|
||||
每次偵測到違規即送出一個 JSON 字串(無 `\r\n`,BLE 以封包為單位):
|
||||
|
||||
```json
|
||||
{"class":"car","level":1}
|
||||
{"class":"person","level":2}
|
||||
{"class":"grass","level":1}
|
||||
{"class":"boot","level":0}
|
||||
```
|
||||
|
||||
| 欄位 | 說明 |
|
||||
|------|------|
|
||||
| `class` | 違規類型:`boot` / `road` / `grass` / `car` / `person` / `pond` / `bunker` / `tree` / `hazard` |
|
||||
| `level` | 嚴重等級:0=正常/開機,1=警告,2=緊急 |
|
||||
|
||||
> **注意**:不加 `\r\n`。加了 DX-BT24 會在換行後額外送出一個空的 BLE Notification,手機端看到非 UTF-8 內容。
|
||||
|
||||
### 11.4 一次性鮑率升級
|
||||
|
||||
出廠模組預設 9600 bps。升級步驟(僅需做一次):
|
||||
|
||||
1. 在 `ini/host_stream.ini` 設 `bt_at_probe = 1`
|
||||
2. 啟動韌體 — `bt_uart_init()` 偵測模組當前鮑率並送出 `AT+BAUD7`(→115200)後 `AT+RESET`
|
||||
3. 確認 `/tmp/fw.log` 出現 `[BT] upgrade complete`
|
||||
4. 將 `bt_at_probe` 改回 `0`(避免 AT 指令被 BLE 客戶端看到)
|
||||
|
||||
### 11.5 INI 參數
|
||||
|
||||
```ini
|
||||
[event]
|
||||
enable = 1
|
||||
bt_uart_dev = /dev/ttyS1 # UART 裝置節點
|
||||
bt_at_probe = 0 # 0: 直接以 115200 開啟(正常使用)
|
||||
# 1: 執行一次性 AT 鮑率升級(出廠/重置後)
|
||||
```
|
||||
|
||||
### 11.6 相關原始碼
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|------|------|
|
||||
| `src/host_stream/bt_uart.c` | UART 驅動:FIFO 佇列 + 專用寫入執行緒 + AT 探測/升級邏輯 |
|
||||
| `include/host_stream/bt_uart.h` | 公開 API:`bt_uart_init()` / `bt_uart_send_json()` / `bt_uart_close()` |
|
||||
| `src/host_stream/event_recorder.c` | `fire_json_async()` 呼叫 `bt_uart_send_json()` |
|
||||
| `src/host_stream/kp_firmware.c` | `loadConfig()` 中讀取 INI 並呼叫 `bt_uart_init()` |
|
||||
|
||||
### 11.7 驗證方法
|
||||
|
||||
裝置端(韌體啟動後):
|
||||
|
||||
```sh
|
||||
# 手動送出 JSON(stty 先設定鮑率)
|
||||
stty -F /dev/ttyS1 115200 raw cs8 -parenb -cstopb -echo
|
||||
printf '{"class":"test","level":0}' > /dev/ttyS1
|
||||
```
|
||||
|
||||
iPhone 端:開啟 nRF Connect → 掃描 DX-BT24 → 連線 → 訂閱 FFE1 Notify → 應看到 `{"class":"test","level":0}`(正確 UTF-8 字串)。
|
||||
|
||||
---
|
||||
|
||||
## 12. CAN Bus 通訊:MCP2515 SPI 控制器
|
||||
|
||||
### 12.1 硬體配置
|
||||
|
||||
| 項目 | 規格 |
|
||||
|------|------|
|
||||
| CAN 控制器 | MCP2515 |
|
||||
| 介面 | SPI via J16 擴充連接器 |
|
||||
| SPI 裝置節點 | `/dev/spidev1.0` |
|
||||
| SPI 時脈 | 1 MHz |
|
||||
| 晶振 | 8 MHz |
|
||||
| 預設 CAN 速率 | 250 kbps |
|
||||
| 輸出 CAN Frame ID | 0x100(11-bit Standard Frame,可 INI 設定) |
|
||||
|
||||
J16 接線(依板上標示順序):
|
||||
|
||||
| J16 腳位 | MCP2515 模組 |
|
||||
|---|---|
|
||||
| CS | CS |
|
||||
| DO | SI (MOSI) |
|
||||
| CLK | SCK |
|
||||
| RESERVED | SO (MISO) |
|
||||
|
||||
註:若板上 `RESERVED` 版本實際未接到 `SPI_1_DI_E`,MCP2515 將無法回應。
|
||||
|
||||
### 12.2 功能架構
|
||||
|
||||
本週重構為**控制訊號模式(control-frame mode)**,移除了 MsgBroker FIFO 依賴,直接使用 CAN 控制幀進行速度控制:
|
||||
|
||||
```
|
||||
KL630 firmware
|
||||
event_recorder.c: grass_enter_level(level) / collision detection
|
||||
│
|
||||
▼
|
||||
msg_send() → can_bus_send_control_cmd(level) ← 直接呼叫(同步,有 retry)
|
||||
│
|
||||
▼
|
||||
send_one_frame_with_retry() ← 最多 20 次重試 + TX stuck 恢復
|
||||
│
|
||||
▼
|
||||
mcp2515_sendMessage() ← SPI ioctl 到 /dev/spidev1.0
|
||||
│ (或 SPI_BITBANG 模式:gpio_devmem)
|
||||
▼
|
||||
MCP2515 (J16) → CAN bus → 車體馬達控制器
|
||||
```
|
||||
|
||||
> **重要改動**:`msg_send("setSpeed")` 現在直接呼叫 `can_bus_send_control_cmd()`,不再依賴 `/tmp/canbus/c0/command.fifo` MsgBroker FIFO。JSON 事件僅透過藍牙(Channel A)傳送給 iPad。
|
||||
|
||||
CAN 控制器包含 keep-alive 機制,每 200ms 自動重發最後的速度指令,確保馬達控制器不會因通訊中斷而失去控制。
|
||||
|
||||
### 12.3 CAN Frame 格式
|
||||
|
||||
#### 控制 Frame(主要輸出)
|
||||
|
||||
```
|
||||
CAN ID : 0x75 (11-bit, hardcoded in s_ctl_can_id)
|
||||
DLC : 8
|
||||
Data[0] : 速度控制指令(見下表)
|
||||
Data[1-7]: 0x00 (reserved)
|
||||
```
|
||||
|
||||
| Data[0] | 定義常數 | 觸發條件 | 說明 |
|
||||
|---------|---------|---------|------|
|
||||
| `10` | `SPEED_LEVEL_0` | 碰撞事件 | 緊急停車(單次觸發) |
|
||||
| `10` | `SPEED_LEVEL_1` | 草地 L3(T+10s) | 嚴重違規,強制停車 |
|
||||
| `10` | `SPEED_LEVEL_2` | 草地 L2(T+6s) | 持續違規,中度減速 |
|
||||
| `10` | `SPEED_LEVEL_3` | 草地 L1 持續 | 持續違規,輕度減速 |
|
||||
| `10` | `SPEED_LEVEL_4` | 草地 L1 進入 | 初次草地違規,警告 |
|
||||
| `240` | `SPEED_LEVEL_5` | 正常狀態 / 事件結束 | 恢復正常(全速) |
|
||||
|
||||
> **注意**:目前測試階段所有 SPEED_LEVEL 均設為 10(低速),正常速度為 240。生產環境可依需求調整各等級的速度值。
|
||||
|
||||
控制指令由 `event_recorder.c` 的草地狀態機和碰撞檢測觸發(詳見第 13 節)。CAN 控制器會每 200ms 自動重發最後的控制指令作為 keep-alive。
|
||||
|
||||
#### JSON 多幀(保留,目前停用)
|
||||
|
||||
```
|
||||
CAN ID : 0x100 (INI can_id,可設定)
|
||||
DLC : 最多 8 bytes / frame
|
||||
格式 : JSON 字串分段,每幀 8 bytes,最後一幀以 0x00 補齊
|
||||
接收端 : 逐幀拼接直到遇到 null byte
|
||||
```
|
||||
|
||||
此模式已保留程式碼但 `can_bus_send_json()` 目前不發送。若日後需要同時傳送 JSON 到 CAN,移除 `can_bus_send_json()` 中的 early return 即可。
|
||||
|
||||
### 12.4 初始化流程
|
||||
|
||||
```c
|
||||
/* can_bus_init 內部步驟 */
|
||||
s_dev = new_mcp2515_dev(spidev); // 配置 SPI: 1MHz, 8-bit
|
||||
// 或 "gpio-bitbang"(SPI_BITBANG 模式)
|
||||
mcp2515_initial(s_dev); // SPI reset → 進入 CONFIG mode
|
||||
mcp2515_can_speed(s_dev, CAN_250KBPS, MCP_8MHZ); // 8MHz 晶振 @ 250kbps
|
||||
mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL); // 進入 NORMAL mode
|
||||
|
||||
/* 啟動迴環自測 (loopback self-test) */
|
||||
mcp2515_setMode(LOOPBACK) → sendMessage(0x321, 0xA5) → readMessage()
|
||||
// 成功:log "[CAN] startup loopback self-test OK"
|
||||
// 失敗:log "[CAN] startup loopback self-test failed (-N)"
|
||||
|
||||
// 啟動 can_writer_thread(佇列消費用,目前主要由 can_bus_send_control_cmd 直接送)
|
||||
// 啟動 can_keepalive_thread:每 200ms 重發最後控制指令 (預設 SPEED_LEVEL_5 = 240)
|
||||
// 初始化完成後送出 boot 控制 frame: can_bus_send_control_cmd(SPEED_LEVEL_5)
|
||||
```
|
||||
|
||||
### 12.5 TX 錯誤恢復機制
|
||||
|
||||
當 `mcp2515_sendMessage()` 失敗時觸發分層恢復:
|
||||
|
||||
```
|
||||
sendMessage() 回傳 ERROR_ALLTXBUSY 或 ERROR_FAILTX
|
||||
│
|
||||
▼
|
||||
重試最多 20 次(每次 backoff 2ms)
|
||||
│
|
||||
├── 仍失敗 → 讀取 EFLG / TEC / REC 暫存器
|
||||
│ 印出 hint(TX bus-off / TX error-passive / 警告)
|
||||
│ can_recover_tx_stuck():
|
||||
│ CANCTRL_ABAT → 中止掛起的重傳
|
||||
│ 清除 TXB0/1/2 TXREQ bit
|
||||
│ 恢復 NORMAL mode
|
||||
│
|
||||
└── 若 EFLG_TXBO 或 TEC ≥ 128 (bus-off 狀態)
|
||||
can_reinit_controller_locked(): reset + 重設速率 + NORMAL
|
||||
can_loopback_selftest_locked(): 確認 SPI 路徑健康
|
||||
```
|
||||
|
||||
|
||||
### 12.6 INI 參數
|
||||
|
||||
```ini
|
||||
[can]
|
||||
enable = 1 # 1: 啟用 CAN 輸出;0: 停用
|
||||
spidev = /dev/spidev1.0 # SPI 裝置路徑(SPI_BITBANG 模式下忽略)
|
||||
speed_kbps = 250 # CAN 速率:125 / 250 / 500 / 1000 kbps
|
||||
can_id = 0x100 # JSON 多幀的 11-bit CAN ID(目前停用)
|
||||
# 控制 Frame 固定使用 ID 0x75
|
||||
```
|
||||
|
||||
> **本週變更**:`enable` 已從 `0` 改為 `1`(預設開啟)。
|
||||
|
||||
### 12.7 SPI Bit-bang 模式
|
||||
|
||||
當 compile.sh 加上 `-DSPI_BITBANG` 時,MCP2515 透過 gpio_devmem 以軟體模擬 SPI:
|
||||
|
||||
```
|
||||
/dev/spidev1.0 不可用(被其他 kernel module 佔用)時使用此模式
|
||||
|
||||
J15 腳位 (GPIOC_0):
|
||||
GPIO1 = CS (SPI_GPIO_CS, compile.sh 定義)
|
||||
GPIO2 = MOSI (SPI_GPIO_MOSI)
|
||||
GPIO3 = SCK (SPI_GPIO_SCK)
|
||||
GPIO4 = MISO (SPI_GPIO_MISO)
|
||||
|
||||
底層實作:gpio_devmem_set(pin, 0/1) → /dev/mem → KL630 GPIO_C 0x402E0000
|
||||
```
|
||||
|
||||
詳細暫存器配置見第 14 節。
|
||||
|
||||
### 12.8 相關原始碼
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|------|------|
|
||||
| `src/host_stream/mcp2515.c` | MCP2515 低層 SPI 驅動 |
|
||||
| `include/host_stream/mcp2515.h` | MCP2515 暫存器定義、API 宣告 |
|
||||
| `src/host_stream/can_bus.c` | 高層封裝:`can_bus_send_control_cmd()` + 錯誤恢復 |
|
||||
| `include/host_stream/can_bus.h` | 公開 API:`can_bus_init()` / `can_bus_send_control_cmd()` / `can_bus_close()` |
|
||||
| `src/host_stream/gpio_devmem.c` | SPI bit-bang 用的 GPIO /dev/mem 驅動(第 14 節) |
|
||||
| `src/host_stream/kp_firmware.c` | `loadConfig()` 讀取 `[can]` INI 並呼叫 `can_bus_init()` |
|
||||
|
||||
### 12.9 待確認事項
|
||||
|
||||
| 項目 | 說明 |
|
||||
|------|------|
|
||||
| J16 MISO 接腳 | `RESERVED` 需確認為 `SPI_1_DI_E`,否則 MCP2515 回應讀不回來 |
|
||||
| 控制 Frame ID 0x75 | 與車體 ECU 協調確認不與其他 node 衝突 |
|
||||
| 控制指令語意 | cmd=1/2/3 的具體油門百分比或減速邏輯由馬達控制器端定義 |
|
||||
| 接收端需求 | 目前只實作發送;若需接收回授,在 can_writer_thread 加 mcp2515_readMessage() 輪詢 |
|
||||
| CAN 速率匹配 | 車體 CAN bus 速率需與 `speed_kbps` 一致,否則全部 Frame 被丟棄 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 13. 事件錄製器(Event Recorder)
|
||||
|
||||
`event_recorder.c` 是本週新增的核心模組,負責整合 STDC 分析結果、觸發雙通道事件輸出,並將現場 JPEG 快照打包上傳。
|
||||
|
||||
### 13.1 雙通道架構
|
||||
|
||||
```
|
||||
STDC 分析結果 (stdc_analysis_t)
|
||||
│
|
||||
▼
|
||||
event_recorder_update() ← 在 recv callback(約 25fps)呼叫
|
||||
│
|
||||
┌────┴──────────────────────────────┐
|
||||
▼ ▼
|
||||
Channel A (BLE/iPad) Channel B (tar.gz 上傳)
|
||||
bt_uart_send_json(json) VMF_SNAP → JPEG → event.json → tar.gz
|
||||
{"class":"lane","level":1} → kCurl POST → /api/golf.cgi
|
||||
│ │
|
||||
DX-BT24 BLE → iPad SD 卡存檔 (最多 7GB)
|
||||
```
|
||||
|
||||
另外,草地事件同時觸發 CAN 控制指令(Channel C,第 12 節):
|
||||
|
||||
```
|
||||
grass_enter_level(level)
|
||||
├── fire_json_async() → BLE JSON
|
||||
└── can_bus_send_control_cmd(level) → CAN 馬達控制
|
||||
```
|
||||
|
||||
### 13.2 草地違規狀態機
|
||||
|
||||
觸發條件:`on_grass == 1 AND is_moving == 1`(靜止停車在草地不觸發)
|
||||
|
||||
```
|
||||
GRASS_IDLE ──── on_grass & is_moving ────► GRASS_L1 (T+0s)
|
||||
│ fire BT level=1, CAN cmd=1, snap level1.jpg
|
||||
│
|
||||
!on_grass 持續 2s ◄─────────────┤─── on_grass & 經過 6s ──► GRASS_L2 (T+6s)
|
||||
(exit hysteresis) │ │ fire BT level=2
|
||||
│ │ │ CAN cmd=2, snap level2.jpg
|
||||
▼ │ !on_grass 持續 2s ◄─────────┤─── 經過 10s ──► GRASS_L3 (T+10s)
|
||||
GRASS_DONE │ │ │ │ fire BT level=3
|
||||
fire BT level=0 │ ▼ │ │ CAN cmd=3
|
||||
CAN cmd=0 │ GRASS_DONE │ !on_grass 持續 2s
|
||||
launch_upload() └─────────────────────────────── │ ──────────────► GRASS_DONE
|
||||
│
|
||||
▼
|
||||
upload_thread (detached)
|
||||
delay → event.json → tar.gz → kCurl POST → SD 清理
|
||||
│
|
||||
▼
|
||||
GRASS_IDLE
|
||||
```
|
||||
|
||||
**退出遲滯(exit hysteresis)**:`GRASS_EXIT_HYSTERESIS_MS = 2000ms`。
|
||||
球場上 STDC 偶爾在路邊草地閃爍,2 秒遲滯避免誤以為事件結束。
|
||||
|
||||
### 13.3 單次碰撞事件(Single-shot)
|
||||
|
||||
以下事件僅在碰撞 ROI 內偵測到,且只觸發一次(Rising edge 0→1):
|
||||
|
||||
| 類型 | 閾值 | 觸發行為 |
|
||||
|------|------|---------|
|
||||
| `person` | `col_person_ratio ≥ THR_PERSON_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
|
||||
| `bunker` | `col_bunker_ratio ≥ THR_BUNKER_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
|
||||
| `pond` | `col_pond_ratio ≥ THR_POND_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
|
||||
| `tree` | `col_tree_ratio ≥ THR_TREE_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
|
||||
|
||||
碰撞事件不觸發 CAN 控制指令(目前設計;日後可加)。
|
||||
|
||||
### 13.4 JSON 事件格式(BLE 輸出)
|
||||
|
||||
```json
|
||||
{"class":"lane","level":1} ← 草地 L1
|
||||
{"class":"lane","level":2} ← 草地 L2
|
||||
{"class":"lane","level":3} ← 草地 L3
|
||||
{"class":"lane","level":0} ← 草地結束(恢復正常)
|
||||
{"class":"person","level":1} ← 行人碰撞 ROI
|
||||
{"class":"bunker","level":1} ← 沙坑碰撞 ROI
|
||||
```
|
||||
|
||||
> **type "lane"**:草地事件統一以 `lane`(超出車道邊界)回報,方便前端 app 分類。
|
||||
|
||||
### 13.5 JPEG 快照與 tar.gz 打包
|
||||
|
||||
快照由 `event_recorder_provide_frame()` 在 VMF 影格發送 callback 中執行:
|
||||
|
||||
```
|
||||
[主執行緒: send callback]
|
||||
g_snap_req.active == 1
|
||||
→ VMF_SNAP_ProcessOneFrame(1920×1080, QP=75)
|
||||
→ save_jpeg("/tmp/ev_<id>/level1.jpg")
|
||||
→ 草地事件:保留,等事件結束後統一打包
|
||||
→ 碰撞事件:立即 launch_upload()
|
||||
```
|
||||
|
||||
tar.gz 結構:
|
||||
|
||||
```
|
||||
event_<id>_<timestamp>.tar.gz
|
||||
├── event.json ← id, date(UTC+8), type, max_level, duration_sec, images[]
|
||||
├── level1.jpg ← 草地 L1 快照(若存在)
|
||||
├── level2.jpg ← 草地 L2 快照(若存在)
|
||||
├── level3.jpg ← 草地 L3 快照(若存在)
|
||||
└── snapshot.jpg ← 碰撞事件快照
|
||||
```
|
||||
|
||||
SD 卡清理:總大小超過 `sd_max_mb`(預設 7168 MB = 7GB)時,按 mtime 由舊到新刪除。
|
||||
|
||||
### 13.6 INI 參數
|
||||
|
||||
```ini
|
||||
[event]
|
||||
enable = 1
|
||||
bt_uart_dev = /dev/ttyS1 # Channel A: DX-BT24 UART 節點
|
||||
bt_at_probe = 0 # 0: 直接 115200; 1: 一次性 AT 升級
|
||||
upload_url = http://192.168.0.114:8081/api/golf.cgi # Channel B: tar.gz 上傳端點
|
||||
# Production: http://192.168.0.99/api/golf.cgi
|
||||
sd_path = /tmp/sdcard/events # SD 卡存檔路徑
|
||||
sd_max_mb = 7168 # SD 卡容量上限(MB)
|
||||
upload_delay_ms = 0 # 0: 事件結束後立即打包上傳
|
||||
```
|
||||
|
||||
### 13.7 相關原始碼
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|------|------|
|
||||
| `src/host_stream/event_recorder.c` | 狀態機、快照、打包、上傳全部邏輯 |
|
||||
| `include/host_stream/event_recorder.h` | 公開 API:`event_recorder_init()` / `event_recorder_update()` / `event_recorder_provide_frame()` |
|
||||
| `src/host_stream/kp_firmware.c` | `loadConfig()` 讀取 `[event]` INI 並呼叫 `event_recorder_init()` |
|
||||
| `src/host_stream/kp_firmware.c` | 在 recv callback 呼叫 `event_recorder_update(ana)` |
|
||||
|
||||
---
|
||||
|
||||
## 14. GPIO 直接存取:gpio_devmem
|
||||
|
||||
### 14.1 背景
|
||||
|
||||
KL630 J15 擴充連接器的 GPIO 腳位在部分韌體版本中已被 `dh2228fv` display driver 透過 pinmux 佔用,導致 `/sys/class/gpio` 介面無法使用。解法是透過 `/dev/mem` 直接存取實體暫存器,繞過 pinmux 衝突。
|
||||
|
||||
### 14.2 硬體對應
|
||||
|
||||
```
|
||||
KL630 GPIO_C 基底位址:0x402E0000
|
||||
|
||||
J15 連接器腳位對應(GPIOC_0 group):
|
||||
J15 Pin 1 (CS) → GPIO1 = GPIOC_0_IO_DATA_1
|
||||
J15 Pin 2 (MOSI) → GPIO2 = GPIOC_0_IO_DATA_2
|
||||
J15 Pin 3 (SCK) → GPIO3 = GPIOC_0_IO_DATA_3
|
||||
J15 Pin 4 (MISO) → GPIO4 = GPIOC_0_IO_DATA_4
|
||||
```
|
||||
|
||||
### 14.3 暫存器對應
|
||||
|
||||
| 偏移 | 暫存器 | 功能 |
|
||||
|------|--------|------|
|
||||
| `+0x0000` | `GPIOD_PSR` | Port Status(唯讀,反映實際 pin 狀態) |
|
||||
| `+0x0004` | `GPIOD_PDDR` | Port Data Direction(bit=1 輸出,bit=0 輸入) |
|
||||
| `+0x0008` | `GPIOD_PSOR` | Port Set(寫 1 → 該腳拉高,其他 pin 不影響) |
|
||||
| `+0x000C` | `GPIOD_PCOR` | Port Clear(寫 1 → 該腳拉低,其他 pin 不影響) |
|
||||
| `+0x0010` | `GPIOD_PTOR` | Port Toggle |
|
||||
| `+0x0014` | `GPIOD_PIDR` | Port Input Disable |
|
||||
|
||||
### 14.4 API
|
||||
|
||||
```c
|
||||
int gpio_devmem_init(void); // 開啟 /dev/mem + mmap GPIO_C
|
||||
void gpio_devmem_cleanup(void); // munmap + close
|
||||
int gpio_devmem_set_direction(int gpio, int out); // gpio: 1-4,out: 1=輸出
|
||||
int gpio_devmem_set(int gpio, int value); // set high/low via PSOR/PCOR
|
||||
int gpio_devmem_get(int gpio); // 讀 PSR bit(0 或 1)
|
||||
```
|
||||
|
||||
### 14.5 使用方式(SPI bit-bang)
|
||||
|
||||
在 `compile.sh` 加入 `-DSPI_BITBANG` 後,`can_bus.c` 改用 gpio_devmem 模擬 SPI 時序:
|
||||
|
||||
```bash
|
||||
# compile.sh 加入的 flags:
|
||||
-DSPI_BITBANG
|
||||
-DSPI_GPIO_CS=1 # J15 Pin1
|
||||
-DSPI_GPIO_MOSI=2 # J15 Pin2
|
||||
-DSPI_GPIO_SCK=3 # J15 Pin3
|
||||
-DSPI_GPIO_MISO=4 # J15 Pin4
|
||||
```
|
||||
|
||||
### 14.6 注意事項
|
||||
|
||||
- 需要 root 權限(`CAP_SYS_RAWIO`,firmware 本身已以 root 執行)
|
||||
- mmap 以 page(4096 bytes)為單位,base 已自動對齊,偏移在程式碼中修正
|
||||
- 若日後 pinmux 問題解決,可切回 `/dev/spidev1.0` 硬體 SPI(移除 `-DSPI_BITBANG`)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 15. STDC 模型重訓練與後處理 Python→C 移植
|
||||
|
||||
### 15.1 重訓練背景
|
||||
|
||||
原始 STDC 模型以公開道路資料集訓練,對高爾夫球場環境的辨識率不足:
|
||||
|
||||
- 球道(fairway)被誤判為草地(grass)或道路(road)
|
||||
- 沙坑(bunker)辨識率偏低
|
||||
- 球場邊緣樹木與一般道路樹木特徵差異大
|
||||
|
||||
因此收集球場現場影像,重新標注並 fine-tune 模型,提升球場環境下的分類準確度。
|
||||
|
||||
> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(訓練資料範例圖)*
|
||||
|
||||
### 15.2 重訓練流程
|
||||
|
||||
```
|
||||
現場採集影像
|
||||
│
|
||||
▼
|
||||
人工標注(8 類:道路、草地、車輛、行人、沙坑、水塘、植被、樹木)
|
||||
│
|
||||
▼
|
||||
Fine-tune STDC 模型(基於原始權重)
|
||||
│
|
||||
▼
|
||||
Python 推論驗證(stdc630inference.py)
|
||||
│
|
||||
▼
|
||||
Kneron Model Zoo 轉換 → .nef 格式(KL630 NPU 使用)
|
||||
│
|
||||
▼
|
||||
STDC_0520.nef → 部署到裝置
|
||||
```
|
||||
|
||||
> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(訓練前後準確度對比圖)*
|
||||
|
||||
### 15.3 後處理 Python → C 移植
|
||||
|
||||
原先推論結果的後處理(ROI 計算、類別佔比、警告旗標)全部在 Python(`stdc630inference.py`)中完成,無法在嵌入式端即時執行。
|
||||
|
||||
**移植目標**:完全在 KL630 上執行,零 Python 依賴。
|
||||
|
||||
| 項目 | Python 原版 | C 移植版(`stdc_post_process.c`) |
|
||||
|------|------------|----------------------------------|
|
||||
| 分割圖解析 | numpy 陣列切片 | 直接 pointer 操作 NPU output buffer |
|
||||
| 前進 ROI(梯形) | 向量化條件判斷 | `stdc_is_in_forward_roi()` 逐像素計算 |
|
||||
| 碰撞 ROI | slice 計算 | 固定比例邊界,整數乘法 |
|
||||
| 動態偵測(is_moving) | frame diff | `stdc_compute_motion_diff()` + `prev_roi_luma` 快取 |
|
||||
| 草地持續時間計時 | time.time() | `gettimeofday()` + `consecutive_grass_frames` |
|
||||
| 輸出 | JSON / 終端機 | `stdc_analysis_t` struct → event_recorder |
|
||||
|
||||
**正規化對齊**:C 版輸入正規化設為 `KP_NORMALIZE_KNERON`(= `/256 − 0.5`),與 Python 訓練時的前處理一致,確保推論結果數值相同。
|
||||
|
||||
> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(Python vs C 輸出比對畫面)*
|
||||
|
||||
### 15.4 相關原始碼
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|------|------|
|
||||
| `src/pre_post_proc/stdc_post_process.c` | 後處理主體:ROI 計算、類別佔比、警告旗標、動態偵測 |
|
||||
| `src/app_flow/stdc_inf_single_model.c` | NPU 推論配置:正規化、resize、post_proc callback 綁定 |
|
||||
| `include/stdc_post_process.h` | `stdc_analysis_t` 結構定義、閾值常數 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 16. 快照功能:實作 / 事件觸發 / 打包上傳
|
||||
|
||||
### 16.1 Snapshot function implement
|
||||
|
||||
#### 架構選擇
|
||||
|
||||
KL630 VMF 提供 `VMF_SNAP`(Video Snapshot Mechanism)模組,可從現有 SSM 影像管線直接擷取 JPEG,無需額外複製 YUV 緩衝區。
|
||||
|
||||
```
|
||||
SSM Ring Buffer (vsrc_ssm_0, 1920×1080)
|
||||
│
|
||||
▼
|
||||
VMF_SNAP_Init() ← Lazy init,首次觸發時才建立(避免閒置佔資源)
|
||||
opt.dwStreamIdx = 0
|
||||
opt.dwQp = 75 ← JPEG 品質
|
||||
│
|
||||
▼
|
||||
VMF_SNAP_ProcessOneFrame() ← 在 VMF 影格 send callback 呼叫
|
||||
│ (必須在 VMF 執行緒內,不可跨執行緒)
|
||||
▼
|
||||
JPEG buffer (MemBroker, 最大 2MB)
|
||||
│
|
||||
▼
|
||||
save_jpeg("/tmp/ev_<id>/level1.jpg")
|
||||
```
|
||||
|
||||
#### 跨執行緒協調(SnapReq 機制)
|
||||
|
||||
VMF_SNAP 只能在 VMF 影格 callback 執行緒中呼叫。事件判斷在 recv callback(不同執行緒)。解法:
|
||||
|
||||
```
|
||||
[recv callback 執行緒] [VMF send callback 執行緒]
|
||||
event 觸發
|
||||
→ request_snap() → event_recorder_provide_frame()
|
||||
寫入 g_snap_req 讀取 g_snap_req.active
|
||||
g_snap_req.active = 1 若 active → 執行 VMF_SNAP
|
||||
(mutex 保護) 清除 active
|
||||
存檔
|
||||
```
|
||||
|
||||
若上一次快照尚未完成,新請求會被丟棄並 log(避免 race condition)。
|
||||
|
||||
> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(快照存檔後的 JPEG 範例)*
|
||||
|
||||
---
|
||||
|
||||
### 16.2 Snapshot trigger by specific event
|
||||
|
||||
快照由兩類事件觸發,時機與邏輯不同:
|
||||
|
||||
#### 草地違規(連續事件)
|
||||
|
||||
每升一個 level 拍一張,事件結束後統一打包:
|
||||
|
||||
```
|
||||
GRASS_L1 進入 → snap "level1.jpg"(immediate_upload = 0)
|
||||
GRASS_L2 進入 → snap "level2.jpg"(immediate_upload = 0)
|
||||
GRASS_L3 進入 → snap "level3.jpg"(immediate_upload = 0)
|
||||
事件結束 → launch_upload(images=["level1.jpg", "level2.jpg", ...])
|
||||
```
|
||||
|
||||
#### 碰撞 ROI 單次事件(Rising edge)
|
||||
|
||||
偵測到的瞬間拍照並立即上傳:
|
||||
|
||||
```
|
||||
偵測到 person / bunker / pond / tree 進入碰撞 ROI(0→1 上升緣)
|
||||
→ snap "snapshot.jpg"(immediate_upload = 1)
|
||||
→ 快照完成後立即 launch_upload(),delay_ms = 0
|
||||
```
|
||||
|
||||
| 事件類型 | 觸發條件 | 快照檔名 | 上傳時機 |
|
||||
|---------|---------|---------|---------|
|
||||
| 草地 L1 | on_grass & is_moving | level1.jpg | 事件結束後 |
|
||||
| 草地 L2 | 草地持續 ≥ 6s | level2.jpg | 事件結束後 |
|
||||
| 草地 L3 | 草地持續 ≥ 10s | level3.jpg | 事件結束後 |
|
||||
| 行人碰撞 | col_person_ratio 0→1 | snapshot.jpg | 立即 |
|
||||
| 沙坑碰撞 | col_bunker_ratio 0→1 | snapshot.jpg | 立即 |
|
||||
| 水塘碰撞 | col_pond_ratio 0→1 | snapshot.jpg | 立即 |
|
||||
| 樹木碰撞 | col_tree_ratio 0→1 | snapshot.jpg | 立即 |
|
||||
|
||||
> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(各事件觸發快照的對比圖)*
|
||||
|
||||
---
|
||||
|
||||
### 16.3 Zip event snapshots and send to GF cloud
|
||||
|
||||
#### 打包流程
|
||||
|
||||
事件結束後由獨立 detached thread 執行,不阻塞推論主迴圈:
|
||||
|
||||
```
|
||||
upload_thread (detached)
|
||||
│
|
||||
├─ [可選] sleep upload_delay_ms(目前設 0,立即執行)
|
||||
│
|
||||
├─ write_event_json("/tmp/ev_<id>/event.json")
|
||||
│ {
|
||||
│ "id": "<unix timestamp>",
|
||||
│ "date": "2026-04-16T10:30:00+08:00", ← 台灣時間 UTC+8
|
||||
│ "type": "grass",
|
||||
│ "max_level": 2,
|
||||
│ "duration_sec": 8.3,
|
||||
│ "images": ["level1.jpg", "level2.jpg"]
|
||||
│ }
|
||||
│
|
||||
├─ build_targz()
|
||||
│ (cd /tmp/ev_<id> && tar cf - .) | gzip -c > /tmp/sdcard/events/event_<id>_<ts>.tar.gz
|
||||
│ sync() ← SD 卡寫入確保
|
||||
│
|
||||
├─ http_post_file()
|
||||
│ kCurl --data-binary @event_<id>_<ts>.tar.gz
|
||||
│ 'http://192.168.0.99/api/golf.cgi?filename=event_<id>_<ts>.tar.gz'
|
||||
│
|
||||
├─ sd_cleanup() ← 總大小超過 7GB 時刪除最舊的 .tar.gz
|
||||
│
|
||||
└─ rm -rf /tmp/ev_<id> ← 清除暫存工作目錄
|
||||
```
|
||||
|
||||
#### tar.gz 內容結構
|
||||
|
||||
```
|
||||
event_<id>_20260416_103000.tar.gz
|
||||
├── event.json ← 事件 metadata
|
||||
├── level1.jpg ← 草地 L1 現場快照
|
||||
├── level2.jpg ← 草地 L2 現場快照(若存在)
|
||||
└── level3.jpg ← 草地 L3 現場快照(若存在)
|
||||
```
|
||||
|
||||
#### 上傳端點
|
||||
|
||||
| 環境 | URL |
|
||||
|------|-----|
|
||||
| 開發測試 | `http://192.168.0.114:8081/api/golf.cgi` |
|
||||
| 生產(GF cloud) | `http://192.168.0.99/api/golf.cgi` |
|
||||
|
||||
透過 INI `upload_url` 切換,無需重新編譯。
|
||||
|
||||
#### SD 卡管理
|
||||
|
||||
- 每次上傳後執行 `sd_cleanup()`
|
||||
- 掃描 `/tmp/sdcard/events/` 下所有 `.tar.gz`,超過 `sd_max_mb`(預設 7168 MB)時按 mtime 由舊到新刪除
|
||||
- 即使網路不通,tar.gz 仍保留在 SD 卡
|
||||
|
||||
> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(GF cloud 後台收到事件的截圖)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -90,6 +90,27 @@ typedef struct {
|
||||
uint8_t collision_risk; /* 1 if collision risk in center ROI */
|
||||
uint8_t tree_approaching; /* 1 if tree ROI grows too fast */
|
||||
uint8_t tree_dense; /* 1 if tree dense area */
|
||||
|
||||
/* Left ROI (x < 25% of image width) */
|
||||
float left_person_ratio;
|
||||
float left_car_ratio;
|
||||
float left_tree_ratio;
|
||||
float left_pond_ratio;
|
||||
float left_bunker_ratio;
|
||||
|
||||
/* Right ROI (x > 75% of image width) */
|
||||
float right_person_ratio;
|
||||
float right_car_ratio;
|
||||
float right_tree_ratio;
|
||||
float right_pond_ratio;
|
||||
float right_bunker_ratio;
|
||||
|
||||
/* Alert summary */
|
||||
uint8_t left_alert; /* 1 if any left class exceeds threshold */
|
||||
uint8_t right_alert; /* 1 if any right class exceeds threshold */
|
||||
char left_type[16]; /* "person","vehicle","tree","water_hazard","bush","" */
|
||||
char right_type[16];
|
||||
|
||||
uint32_t frame_count; /* analyzed frame count */
|
||||
uint32_t seg_width; /* segmentation output width */
|
||||
uint32_t seg_height; /* segmentation output height */
|
||||
|
||||
@ -22,6 +22,13 @@ const char * iniparser_getstring(dictionary *d, const char *key, const char *def
|
||||
int iniparser_getboolean(dictionary *d, const char *key, int notfound);
|
||||
int iniparser_find_entry(dictionary *d, const char *entry);
|
||||
|
||||
/* Write/update one key in the dictionary (section:key format).
|
||||
* Returns 0 on success, -1 on failure. */
|
||||
int iniparser_set(dictionary *d, const char *key, const char *val);
|
||||
|
||||
/* Dump the dictionary back to a file in INI format. */
|
||||
void iniparser_dump_ini(const dictionary *d, FILE *f);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -7,26 +7,45 @@
|
||||
* The DX-BT24 is a UART-transparent BLE module. Whatever is written to the
|
||||
* serial port is forwarded verbatim over BLE to the connected iOS/Android app.
|
||||
*
|
||||
* Usage (normal operation — module already at 115200):
|
||||
* bt_uart_init("/dev/ttyS1", 0) — do_at_probe=0: skip AT commands
|
||||
* bt_uart_send_json(json_str) — every event, thread-safe
|
||||
* bt_uart_close() — shutdown (optional)
|
||||
* TX (Notify → app):
|
||||
* bt_uart_send_json(json_str) — thread-safe, non-blocking queue
|
||||
*
|
||||
* First-time / factory-reset setup (module at factory default 9600):
|
||||
* bt_uart_init("/dev/ttyS1", 1) — do_at_probe=1: negotiate baud via AT
|
||||
* After success, set bt_at_probe = 0 in INI — never probe again.
|
||||
* RX (Command ← app) — handled automatically by the reader thread:
|
||||
* {"command": "status_check"} → Notify: response_type=status_check
|
||||
* {"command": "get_rent_status"} → Notify: response_type=rent_status
|
||||
* {"command": "rent"} → status 0→1; Notify: rent_status
|
||||
* {"command": "return"} → status 1→0; Notify: rent_status
|
||||
*
|
||||
* Usage:
|
||||
* bt_uart_init("/dev/ttyS1", 0)
|
||||
* bt_uart_set_identity("1.0.0", "BT-24")
|
||||
* bt_uart_set_intervention_cb(my_fn) // optional
|
||||
* bt_uart_send_json(json_str)
|
||||
* bt_uart_close()
|
||||
*/
|
||||
|
||||
/* Open and configure the UART to the BT module.
|
||||
* dev: device path, e.g. "/dev/ttyS1" (NULL or "" = disabled).
|
||||
* do_at_probe: 0 = open directly at 115200 (normal operation, no AT sent).
|
||||
* 1 = run AT baud negotiation first (one-time setup only).
|
||||
* Returns 0 on success, -1 on failure / disabled. */
|
||||
/* Open UART, start TX writer + RX reader threads.
|
||||
* do_at_probe=1: one-time baud upgrade 9600→115200 (factory only). */
|
||||
int bt_uart_init(const char *dev, int do_at_probe);
|
||||
|
||||
/* Write JSON bytes to the BT module (no CRLF added). Thread-safe. No-op if not init'd. */
|
||||
/* Set firmware version and BT peripheral name broadcast in status_check replies. */
|
||||
void bt_uart_set_identity(const char *aresx_version, const char *bt_name);
|
||||
|
||||
/* Write JSON bytes to the BT module. Thread-safe. No-op if not init'd. */
|
||||
void bt_uart_send_json(const char *json);
|
||||
|
||||
/* Rent status: 0=未租借, 1=租借中, 2=管理模式. Thread-safe. */
|
||||
int bt_uart_get_rent_status(void);
|
||||
void bt_uart_set_rent_status(int status);
|
||||
|
||||
/* Optional callback: return 1 if cart control is currently active, else 0.
|
||||
* Called when building status_check responses. */
|
||||
typedef int (*bt_intervention_fn)(void);
|
||||
void bt_uart_set_intervention_cb(bt_intervention_fn fn);
|
||||
|
||||
/* Reset handshake and notify peer; DX-BT24 is passthrough — no HW disconnect API. */
|
||||
void bt_uart_disconnect(void);
|
||||
|
||||
/* Close the UART fd and release resources. */
|
||||
void bt_uart_close(void);
|
||||
|
||||
|
||||
51
include/host_stream/can_bus.h
Normal file
51
include/host_stream/can_bus.h
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* can_bus.h — MCP2515 CAN bus wrapper for KL630 (J15 SPI connector)
|
||||
*
|
||||
* API mirrors bt_uart.h: both channels carry the same JSON payload.
|
||||
* BLE channel: bt_uart_send_json(json)
|
||||
* CAN channel: can_bus_send_json(json)
|
||||
*
|
||||
* The same JSON string {"class":"car","level":1} is transmitted on both
|
||||
* channels simultaneously from fire_json_async() in event_recorder.c.
|
||||
*
|
||||
* MCP2515 is classic CAN (max 8 bytes per frame). Long JSON strings are
|
||||
* split into sequential 8-byte frames automatically:
|
||||
* Frame N: bytes [N*8 .. N*8+7] of the JSON string (padded with 0x00)
|
||||
* The receiver reassembles by concatenating frames until a null byte.
|
||||
* If a CAN FD controller is fitted later, switch to single-frame send.
|
||||
*/
|
||||
|
||||
#ifndef CAN_BUS_H
|
||||
#define CAN_BUS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* can_bus_init — open MCP2515 via SPI and start writer thread.
|
||||
* spidev : e.g. "/dev/spidev1.0"
|
||||
* can_speed_kbps: 250 (typical) or 125, 500, 1000
|
||||
* can_id : 11-bit standard frame ID for outbound frames
|
||||
* Returns 0 on success, -1 on error.
|
||||
*/
|
||||
int can_bus_init(const char *spidev, int can_speed_kbps, uint32_t can_id);
|
||||
|
||||
/*
|
||||
* can_bus_send_json — non-blocking: enqueue a JSON string for CAN transmission.
|
||||
* Same signature as bt_uart_send_json(); call both from fire_json_async().
|
||||
* Long strings are split into multiple 8-byte CAN frames automatically.
|
||||
*/
|
||||
void can_bus_send_json(const char *json);
|
||||
|
||||
/*
|
||||
* can_bus_send_control_cmd — send one classic-CAN control frame.
|
||||
* Frame format (compatible with bt_proc.c reference):
|
||||
* CAN ID : 0x75 (11-bit)
|
||||
* DLC : 8
|
||||
* DATA : [cmd, 0, 0, 0, 0, 0, 0, 0]
|
||||
*/
|
||||
void can_bus_send_control_cmd(uint8_t cmd);
|
||||
|
||||
/* can_bus_close — drain queue, join writer thread, close SPI device. */
|
||||
void can_bus_close(void);
|
||||
|
||||
#endif /* CAN_BUS_H */
|
||||
@ -35,4 +35,9 @@ void event_recorder_update(const stdc_analysis_t *ana);
|
||||
/* Take VMF_SNAP if one is pending — call from app_header_send_inference. */
|
||||
void event_recorder_provide_frame(void);
|
||||
|
||||
/* Send collision_warning BLE notify(2.2.5)
|
||||
* level=1 時 type 為觸發物件類型("person","vehicle","tree","water_hazard","bush")
|
||||
* level=0 時 type 傳 NULL */
|
||||
void fire_collision_warning(int level, const char *type);
|
||||
|
||||
#endif /* EVENT_RECORDER_H */
|
||||
|
||||
65
include/host_stream/gpio_devmem.h
Normal file
65
include/host_stream/gpio_devmem.h
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* gpio_devmem.h — Direct GPIO control via /dev/mem for KL630
|
||||
*
|
||||
* Purpose:
|
||||
* Bypass kernel driver pinmux conflicts by directly accessing GPIO registers
|
||||
* in physical memory. Used when J15 GPIO pins are already claimed by another
|
||||
* kernel module (e.g., dh2228fv display driver).
|
||||
*
|
||||
* Register Layout (KL630, GPIO_C base = 0x402E0000):
|
||||
* +0x0000: GPIOD_PSR — Port Status Register (read-only, shows pin states)
|
||||
* +0x0004: GPIOD_PDDR — Port Data Direction Register (0=input, 1=output)
|
||||
* +0x0008: GPIOD_PSOR — Port Set Output Register (write 1 to set pin=1)
|
||||
* +0x000C: GPIOD_PCOR — Port Clear Output Register (write 1 to set pin=0)
|
||||
* +0x0010: GPIOD_PTOR — Port Toggle Output Register
|
||||
* +0x0014: GPIOD_PIDR — Port Input Disable Register
|
||||
*
|
||||
* Usage:
|
||||
* gpio_devmem_init();
|
||||
* gpio_devmem_set(1, 1); // GPIO1 = high
|
||||
* gpio_devmem_get(4); // read GPIO4
|
||||
* gpio_devmem_cleanup();
|
||||
*
|
||||
* Note: must run as root (CAP_SYS_RAWIO for /dev/mem access).
|
||||
*/
|
||||
|
||||
#ifndef GPIO_DEVMEM_H
|
||||
#define GPIO_DEVMEM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Initialize GPIO via /dev/mem.
|
||||
* Returns: 0 on success, -1 on error.
|
||||
*/
|
||||
int gpio_devmem_init(void);
|
||||
|
||||
/*
|
||||
* Cleanup /dev/mem mapping.
|
||||
*/
|
||||
void gpio_devmem_cleanup(void);
|
||||
|
||||
/*
|
||||
* Set GPIO pin direction.
|
||||
* gpio: 1-4 (J15 pins)
|
||||
* output: 1 = output, 0 = input
|
||||
* Returns: 0 on success, -1 on error.
|
||||
*/
|
||||
int gpio_devmem_set_direction(int gpio, int output);
|
||||
|
||||
/*
|
||||
* Set GPIO pin: 1 = high, 0 = low.
|
||||
* gpio: 1-4 (J15 pins)
|
||||
* value: 0 or 1
|
||||
* Returns: 0 on success, -1 on error.
|
||||
*/
|
||||
int gpio_devmem_set(int gpio, int value);
|
||||
|
||||
/*
|
||||
* Get GPIO pin state: 0 = low, 1 = high.
|
||||
* gpio: 1-4 (J15 pins)
|
||||
* Returns: 0, 1, or -1 on error.
|
||||
*/
|
||||
int gpio_devmem_get(int gpio);
|
||||
|
||||
#endif /* GPIO_DEVMEM_H */
|
||||
522
include/host_stream/mcp2515.h
Normal file
522
include/host_stream/mcp2515.h
Normal file
@ -0,0 +1,522 @@
|
||||
#ifndef _mcp2515_h_
|
||||
#define _mcp2515_h_
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <getopt.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/spi/spidev.h>
|
||||
|
||||
/***** Register Map *******/
|
||||
typedef __u32 canid_t;
|
||||
|
||||
/* special address description flags for the CAN_ID */
|
||||
#define CAN_EFF_FLAG 0x80000000UL /* EFF/SFF is set in the MSB */
|
||||
#define CAN_RTR_FLAG 0x40000000UL /* remote transmission request */
|
||||
#define CAN_ERR_FLAG 0x20000000UL /* error message frame */
|
||||
|
||||
/* valid bits in CAN ID for frame formats */
|
||||
#define CAN_SFF_MASK 0x000007FFUL /* standard frame format (SFF) */
|
||||
#define CAN_EFF_MASK 0x1FFFFFFFUL /* extended frame format (EFF) */
|
||||
#define CAN_ERR_MASK 0x1FFFFFFFUL /* omit EFF, RTR, ERR flags */
|
||||
|
||||
#define CAN_MAX_DLEN 8
|
||||
/**************** mcp2515 frequency define ********************/
|
||||
/*
|
||||
* Speed 8M
|
||||
*/
|
||||
#define MCP_8MHz_1000kBPS_CFG1 (0x00)
|
||||
#define MCP_8MHz_1000kBPS_CFG2 (0x80)
|
||||
#define MCP_8MHz_1000kBPS_CFG3 (0x80)
|
||||
|
||||
#define MCP_8MHz_500kBPS_CFG1 (0x00)
|
||||
#define MCP_8MHz_500kBPS_CFG2 (0x90)
|
||||
#define MCP_8MHz_500kBPS_CFG3 (0x82)
|
||||
|
||||
#define MCP_8MHz_250kBPS_CFG1 (0x00)
|
||||
#define MCP_8MHz_250kBPS_CFG2 (0xB1)
|
||||
#define MCP_8MHz_250kBPS_CFG3 (0x85)
|
||||
|
||||
#define MCP_8MHz_200kBPS_CFG1 (0x00)
|
||||
#define MCP_8MHz_200kBPS_CFG2 (0xB4)
|
||||
#define MCP_8MHz_200kBPS_CFG3 (0x86)
|
||||
|
||||
#define MCP_8MHz_125kBPS_CFG1 (0x01)
|
||||
#define MCP_8MHz_125kBPS_CFG2 (0xB1)
|
||||
#define MCP_8MHz_125kBPS_CFG3 (0x85)
|
||||
|
||||
#define MCP_8MHz_100kBPS_CFG1 (0x01)
|
||||
#define MCP_8MHz_100kBPS_CFG2 (0xB4)
|
||||
#define MCP_8MHz_100kBPS_CFG3 (0x86)
|
||||
|
||||
#define MCP_8MHz_80kBPS_CFG1 (0x01)
|
||||
#define MCP_8MHz_80kBPS_CFG2 (0xBF)
|
||||
#define MCP_8MHz_80kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_8MHz_50kBPS_CFG1 (0x03)
|
||||
#define MCP_8MHz_50kBPS_CFG2 (0xB4)
|
||||
#define MCP_8MHz_50kBPS_CFG3 (0x86)
|
||||
|
||||
#define MCP_8MHz_40kBPS_CFG1 (0x03)
|
||||
#define MCP_8MHz_40kBPS_CFG2 (0xBF)
|
||||
#define MCP_8MHz_40kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_8MHz_33k3BPS_CFG1 (0x47)
|
||||
#define MCP_8MHz_33k3BPS_CFG2 (0xE2)
|
||||
#define MCP_8MHz_33k3BPS_CFG3 (0x85)
|
||||
|
||||
#define MCP_8MHz_31k25BPS_CFG1 (0x07)
|
||||
#define MCP_8MHz_31k25BPS_CFG2 (0xA4)
|
||||
#define MCP_8MHz_31k25BPS_CFG3 (0x84)
|
||||
|
||||
#define MCP_8MHz_20kBPS_CFG1 (0x07)
|
||||
#define MCP_8MHz_20kBPS_CFG2 (0xBF)
|
||||
#define MCP_8MHz_20kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_8MHz_10kBPS_CFG1 (0x0F)
|
||||
#define MCP_8MHz_10kBPS_CFG2 (0xBF)
|
||||
#define MCP_8MHz_10kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_8MHz_5kBPS_CFG1 (0x1F)
|
||||
#define MCP_8MHz_5kBPS_CFG2 (0xBF)
|
||||
#define MCP_8MHz_5kBPS_CFG3 (0x87)
|
||||
|
||||
/*
|
||||
* speed 16M
|
||||
*/
|
||||
#define MCP_16MHz_1000kBPS_CFG1 (0x00)
|
||||
#define MCP_16MHz_1000kBPS_CFG2 (0xD0)
|
||||
#define MCP_16MHz_1000kBPS_CFG3 (0x82)
|
||||
|
||||
#define MCP_16MHz_500kBPS_CFG1 (0x00)
|
||||
#define MCP_16MHz_500kBPS_CFG2 (0xF0)
|
||||
#define MCP_16MHz_500kBPS_CFG3 (0x86)
|
||||
|
||||
#define MCP_16MHz_250kBPS_CFG1 (0x41)
|
||||
#define MCP_16MHz_250kBPS_CFG2 (0xF1)
|
||||
#define MCP_16MHz_250kBPS_CFG3 (0x85)
|
||||
|
||||
#define MCP_16MHz_200kBPS_CFG1 (0x01)
|
||||
#define MCP_16MHz_200kBPS_CFG2 (0xFA)
|
||||
#define MCP_16MHz_200kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_125kBPS_CFG1 (0x03)
|
||||
#define MCP_16MHz_125kBPS_CFG2 (0xF0)
|
||||
#define MCP_16MHz_125kBPS_CFG3 (0x86)
|
||||
|
||||
#define MCP_16MHz_100kBPS_CFG1 (0x03)
|
||||
#define MCP_16MHz_100kBPS_CFG2 (0xFA)
|
||||
#define MCP_16MHz_100kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_95kBPS_CFG1 (0x03)
|
||||
#define MCP_16MHz_95kBPS_CFG2 (0xAD)
|
||||
#define MCP_16MHz_95kBPS_CFG3 (0x07)
|
||||
|
||||
#define MCP_16MHz_83k3BPS_CFG1 (0x03)
|
||||
#define MCP_16MHz_83k3BPS_CFG2 (0xBE)
|
||||
#define MCP_16MHz_83k3BPS_CFG3 (0x07)
|
||||
|
||||
#define MCP_16MHz_80kBPS_CFG1 (0x03)
|
||||
#define MCP_16MHz_80kBPS_CFG2 (0xFF)
|
||||
#define MCP_16MHz_80kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_50kBPS_CFG1 (0x07)
|
||||
#define MCP_16MHz_50kBPS_CFG2 (0xFA)
|
||||
#define MCP_16MHz_50kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_40kBPS_CFG1 (0x07)
|
||||
#define MCP_16MHz_40kBPS_CFG2 (0xFF)
|
||||
#define MCP_16MHz_40kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_33k3BPS_CFG1 (0x4E)
|
||||
#define MCP_16MHz_33k3BPS_CFG2 (0xF1)
|
||||
#define MCP_16MHz_33k3BPS_CFG3 (0x85)
|
||||
|
||||
#define MCP_16MHz_20kBPS_CFG1 (0x0F)
|
||||
#define MCP_16MHz_20kBPS_CFG2 (0xFF)
|
||||
#define MCP_16MHz_20kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_10kBPS_CFG1 (0x1F)
|
||||
#define MCP_16MHz_10kBPS_CFG2 (0xFF)
|
||||
#define MCP_16MHz_10kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_16MHz_5kBPS_CFG1 (0x3F)
|
||||
#define MCP_16MHz_5kBPS_CFG2 (0xFF)
|
||||
#define MCP_16MHz_5kBPS_CFG3 (0x87)
|
||||
|
||||
/*
|
||||
* speed 20M
|
||||
*/
|
||||
#define MCP_20MHz_1000kBPS_CFG1 (0x00)
|
||||
#define MCP_20MHz_1000kBPS_CFG2 (0xD9)
|
||||
#define MCP_20MHz_1000kBPS_CFG3 (0x82)
|
||||
|
||||
#define MCP_20MHz_500kBPS_CFG1 (0x00)
|
||||
#define MCP_20MHz_500kBPS_CFG2 (0xFA)
|
||||
#define MCP_20MHz_500kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_250kBPS_CFG1 (0x41)
|
||||
#define MCP_20MHz_250kBPS_CFG2 (0xFB)
|
||||
#define MCP_20MHz_250kBPS_CFG3 (0x86)
|
||||
|
||||
#define MCP_20MHz_200kBPS_CFG1 (0x01)
|
||||
#define MCP_20MHz_200kBPS_CFG2 (0xFF)
|
||||
#define MCP_20MHz_200kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_125kBPS_CFG1 (0x03)
|
||||
#define MCP_20MHz_125kBPS_CFG2 (0xFA)
|
||||
#define MCP_20MHz_125kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_100kBPS_CFG1 (0x04)
|
||||
#define MCP_20MHz_100kBPS_CFG2 (0xFA)
|
||||
#define MCP_20MHz_100kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_83k3BPS_CFG1 (0x04)
|
||||
#define MCP_20MHz_83k3BPS_CFG2 (0xFE)
|
||||
#define MCP_20MHz_83k3BPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_80kBPS_CFG1 (0x04)
|
||||
#define MCP_20MHz_80kBPS_CFG2 (0xFF)
|
||||
#define MCP_20MHz_80kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_50kBPS_CFG1 (0x09)
|
||||
#define MCP_20MHz_50kBPS_CFG2 (0xFA)
|
||||
#define MCP_20MHz_50kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_40kBPS_CFG1 (0x09)
|
||||
#define MCP_20MHz_40kBPS_CFG2 (0xFF)
|
||||
#define MCP_20MHz_40kBPS_CFG3 (0x87)
|
||||
|
||||
#define MCP_20MHz_33k3BPS_CFG1 (0x0B)
|
||||
#define MCP_20MHz_33k3BPS_CFG2 (0xFF)
|
||||
#define MCP_20MHz_33k3BPS_CFG3 (0x87)
|
||||
|
||||
|
||||
/**************** mcp2515 flag typedef ********************/
|
||||
|
||||
typedef enum{
|
||||
CANCTRL_REQOP = 0xE0,
|
||||
CANCTRL_ABAT = 0x10,
|
||||
CANCTRL_OSM = 0x08,
|
||||
CANCTRL_CLKEN = 0x04,
|
||||
CANCTRL_CLKPRE = 0x03
|
||||
}canctrl_flags;
|
||||
|
||||
typedef enum {
|
||||
MCP_20MHZ,
|
||||
MCP_16MHZ,
|
||||
MCP_8MHZ
|
||||
}CAN_CLOCK;
|
||||
|
||||
typedef enum {
|
||||
CAN_5KBPS,
|
||||
CAN_10KBPS,
|
||||
CAN_20KBPS,
|
||||
CAN_31K25BPS,
|
||||
CAN_33KBPS,
|
||||
CAN_40KBPS,
|
||||
CAN_50KBPS,
|
||||
CAN_80KBPS,
|
||||
CAN_83K3BPS,
|
||||
CAN_95KBPS,
|
||||
CAN_100KBPS,
|
||||
CAN_125KBPS,
|
||||
CAN_200KBPS,
|
||||
CAN_250KBPS,
|
||||
CAN_500KBPS,
|
||||
CAN_1000KBPS
|
||||
}CAN_SPEED;
|
||||
|
||||
typedef enum {
|
||||
ERROR_OK = 0,
|
||||
ERROR_FAIL = 1,
|
||||
ERROR_ALLTXBUSY = 2,
|
||||
ERROR_FAILINIT = 3,
|
||||
ERROR_FAILTX = 4,
|
||||
ERROR_NOMSG = 5
|
||||
}ERROR;
|
||||
|
||||
typedef enum {
|
||||
MASK0,
|
||||
MASK1
|
||||
}MASK;
|
||||
|
||||
typedef enum {
|
||||
RXF0 = 0,
|
||||
RXF1 = 1,
|
||||
RXF2 = 2,
|
||||
RXF3 = 3,
|
||||
RXF4 = 4,
|
||||
RXF5 = 5
|
||||
}RXF;
|
||||
|
||||
typedef enum {
|
||||
RXB0 = 0,
|
||||
RXB1 = 1
|
||||
}RXBn;
|
||||
|
||||
typedef enum {
|
||||
TXB0 = 0,
|
||||
TXB1 = 1,
|
||||
TXB2 = 2
|
||||
}TXBn;
|
||||
|
||||
typedef enum {
|
||||
CANINTF_RX0IF = 0x01,
|
||||
CANINTF_RX1IF = 0x02,
|
||||
CANINTF_TX0IF = 0x04,
|
||||
CANINTF_TX1IF = 0x08,
|
||||
CANINTF_TX2IF = 0x10,
|
||||
CANINTF_ERRIF = 0x20,
|
||||
CANINTF_WAKIF = 0x40,
|
||||
CANINTF_MERRF = 0x80
|
||||
}CANINTF;
|
||||
|
||||
typedef enum{
|
||||
EFLG_RX1OVR = (1<<7),
|
||||
EFLG_RX0OVR = (1<<6),
|
||||
EFLG_TXBO = (1<<5),
|
||||
EFLG_TXEP = (1<<4),
|
||||
EFLG_RXEP = (1<<3),
|
||||
EFLG_TXWAR = (1<<2),
|
||||
EFLG_RXWAR = (1<<1),
|
||||
EFLG_EWARN = (1<<0)
|
||||
}EFLG;
|
||||
|
||||
typedef enum{
|
||||
INSTRUCTION_WRITE = 0x02,
|
||||
INSTRUCTION_READ = 0x03,
|
||||
INSTRUCTION_BITMOD = 0x05,
|
||||
INSTRUCTION_LOAD_TX0 = 0x40,
|
||||
INSTRUCTION_LOAD_TX1 = 0x42,
|
||||
INSTRUCTION_LOAD_TX2 = 0x44,
|
||||
INSTRUCTION_RTS_TX0 = 0x81,
|
||||
INSTRUCTION_RTS_TX1 = 0x82,
|
||||
INSTRUCTION_RTS_TX2 = 0x84,
|
||||
INSTRUCTION_RTS_ALL = 0x87,
|
||||
INSTRUCTION_READ_RX0 = 0x90,
|
||||
INSTRUCTION_READ_RX1 = 0x94,
|
||||
INSTRUCTION_READ_STATUS = 0xA0,
|
||||
INSTRUCTION_RX_STATUS = 0xB0,
|
||||
INSTRUCTION_RESET = 0xC0
|
||||
}INSTRUCTION;
|
||||
|
||||
typedef enum {
|
||||
MCP_RXF0SIDH = 0x00,
|
||||
MCP_RXF0SIDL = 0x01,
|
||||
MCP_RXF0EID8 = 0x02,
|
||||
MCP_RXF0EID0 = 0x03,
|
||||
MCP_RXF1SIDH = 0x04,
|
||||
MCP_RXF1SIDL = 0x05,
|
||||
MCP_RXF1EID8 = 0x06,
|
||||
MCP_RXF1EID0 = 0x07,
|
||||
MCP_RXF2SIDH = 0x08,
|
||||
MCP_RXF2SIDL = 0x09,
|
||||
MCP_RXF2EID8 = 0x0A,
|
||||
MCP_RXF2EID0 = 0x0B,
|
||||
MCP_CANSTAT = 0x0E,
|
||||
MCP_CANCTRL = 0x0F,
|
||||
MCP_RXF3SIDH = 0x10,
|
||||
MCP_RXF3SIDL = 0x11,
|
||||
MCP_RXF3EID8 = 0x12,
|
||||
MCP_RXF3EID0 = 0x13,
|
||||
MCP_RXF4SIDH = 0x14,
|
||||
MCP_RXF4SIDL = 0x15,
|
||||
MCP_RXF4EID8 = 0x16,
|
||||
MCP_RXF4EID0 = 0x17,
|
||||
MCP_RXF5SIDH = 0x18,
|
||||
MCP_RXF5SIDL = 0x19,
|
||||
MCP_RXF5EID8 = 0x1A,
|
||||
MCP_RXF5EID0 = 0x1B,
|
||||
MCP_TEC = 0x1C,
|
||||
MCP_REC = 0x1D,
|
||||
MCP_RXM0SIDH = 0x20,
|
||||
MCP_RXM0SIDL = 0x21,
|
||||
MCP_RXM0EID8 = 0x22,
|
||||
MCP_RXM0EID0 = 0x23,
|
||||
MCP_RXM1SIDH = 0x24,
|
||||
MCP_RXM1SIDL = 0x25,
|
||||
MCP_RXM1EID8 = 0x26,
|
||||
MCP_RXM1EID0 = 0x27,
|
||||
MCP_CNF3 = 0x28,
|
||||
MCP_CNF2 = 0x29,
|
||||
MCP_CNF1 = 0x2A,
|
||||
MCP_CANINTE = 0x2B,
|
||||
MCP_CANINTF = 0x2C,
|
||||
MCP_EFLG = 0x2D,
|
||||
MCP_TXB0CTRL = 0x30,
|
||||
MCP_TXB0SIDH = 0x31,
|
||||
MCP_TXB0SIDL = 0x32,
|
||||
MCP_TXB0EID8 = 0x33,
|
||||
MCP_TXB0EID0 = 0x34,
|
||||
MCP_TXB0DLC = 0x35,
|
||||
MCP_TXB0DATA = 0x36,
|
||||
MCP_TXB1CTRL = 0x40,
|
||||
MCP_TXB1SIDH = 0x41,
|
||||
MCP_TXB1SIDL = 0x42,
|
||||
MCP_TXB1EID8 = 0x43,
|
||||
MCP_TXB1EID0 = 0x44,
|
||||
MCP_TXB1DLC = 0x45,
|
||||
MCP_TXB1DATA = 0x46,
|
||||
MCP_TXB2CTRL = 0x50,
|
||||
MCP_TXB2SIDH = 0x51,
|
||||
MCP_TXB2SIDL = 0x52,
|
||||
MCP_TXB2EID8 = 0x53,
|
||||
MCP_TXB2EID0 = 0x54,
|
||||
MCP_TXB2DLC = 0x55,
|
||||
MCP_TXB2DATA = 0x56,
|
||||
MCP_RXB0CTRL = 0x60,
|
||||
MCP_RXB0SIDH = 0x61,
|
||||
MCP_RXB0SIDL = 0x62,
|
||||
MCP_RXB0EID8 = 0x63,
|
||||
MCP_RXB0EID0 = 0x64,
|
||||
MCP_RXB0DLC = 0x65,
|
||||
MCP_RXB0DATA = 0x66,
|
||||
MCP_RXB1CTRL = 0x70,
|
||||
MCP_RXB1SIDH = 0x71,
|
||||
MCP_RXB1SIDL = 0x72,
|
||||
MCP_RXB1EID8 = 0x73,
|
||||
MCP_RXB1EID0 = 0x74,
|
||||
MCP_RXB1DLC = 0x75,
|
||||
MCP_RXB1DATA = 0x76
|
||||
}REGISTER;
|
||||
|
||||
typedef enum{
|
||||
N_TXBUFFERS=3,
|
||||
N_RXBUFFERS=2
|
||||
}buffers_flag;
|
||||
|
||||
typedef enum{
|
||||
RXBnCTRL_RXM_STD = 0x20,
|
||||
RXBnCTRL_RXM_EXT = 0x40,
|
||||
RXBnCTRL_RXM_STDEXT = 0x00,
|
||||
RXBnCTRL_RXM_MASK = 0x60,
|
||||
RXBnCTRL_RTR = 0x08,
|
||||
RXB0CTRL_BUKT = 0x04,
|
||||
RXB0CTRL_FILHIT_MASK = 0x03,
|
||||
RXB1CTRL_FILHIT_MASK = 0x07,
|
||||
RXB0CTRL_FILHIT = 0x00,
|
||||
RXB1CTRL_FILHIT = 0x01
|
||||
}RXBnCTRLFlag;
|
||||
|
||||
typedef enum {
|
||||
CANCTRL_REQOP_NORMAL = 0x00,
|
||||
CANCTRL_REQOP_SLEEP = 0x20,
|
||||
CANCTRL_REQOP_LOOPBACK = 0x40,
|
||||
CANCTRL_REQOP_LISTENONLY = 0x60,
|
||||
CANCTRL_REQOP_CONFIG = 0x80,
|
||||
CANCTRL_REQOP_POWERUP = 0xE0
|
||||
}CANCTRL_REQOP_MODE;
|
||||
|
||||
typedef enum {
|
||||
TXB_EXIDE_MASK = 0x08,
|
||||
DLC_MASK = 0x0F,
|
||||
RTR_MASK = 0x40
|
||||
}mcp_mask_flag;
|
||||
|
||||
|
||||
typedef enum {
|
||||
MCP_SIDL = 1,
|
||||
MCP_SIDH = 0,
|
||||
MCP_EID8 = 2,
|
||||
MCP_EID0 = 3,
|
||||
MCP_DLC = 4,
|
||||
MCP_DATA = 5,
|
||||
}mcp_flag;
|
||||
|
||||
typedef enum{
|
||||
CANSTAT_OPMOD = 0xE0,
|
||||
CANSTAT_ICOD = 0x0E
|
||||
}canstat_flag;
|
||||
|
||||
struct TXBn_REGS {
|
||||
REGISTER CTRL;
|
||||
REGISTER SIDH;
|
||||
REGISTER DATA;
|
||||
};
|
||||
|
||||
struct RXBn_REGS {
|
||||
REGISTER CTRL;
|
||||
REGISTER SIDH;
|
||||
REGISTER DATA;
|
||||
CANINTF CANINTF_RXnIF;
|
||||
};
|
||||
|
||||
extern const struct TXBn_REGS TXB[N_TXBUFFERS];
|
||||
extern const struct RXBn_REGS RXB[N_RXBUFFERS];
|
||||
|
||||
typedef enum {
|
||||
TXB_ABTF = 0x40,
|
||||
TXB_MLOA = 0x20,
|
||||
TXB_TXERR = 0x10,
|
||||
TXB_TXREQ = 0x08,
|
||||
TXB_TXIE = 0x04,
|
||||
TXB_TXP = 0x03
|
||||
}TXBnCTRL;
|
||||
|
||||
typedef enum {
|
||||
STAT_RX0IF = (1<<0),
|
||||
STAT_RX1IF = (1<<1)
|
||||
}STAT;
|
||||
|
||||
typedef struct mcp2515_dev mcp2515_dev;
|
||||
typedef struct spi_description spi_description;
|
||||
|
||||
// defined mcp2515 feature
|
||||
typedef struct mcp2515_dev
|
||||
{
|
||||
spi_description* spi_dev;
|
||||
}mcp2515_dev;
|
||||
|
||||
// defined spi description
|
||||
typedef struct spi_description
|
||||
{
|
||||
char *spidev_path;
|
||||
uint8_t fd;
|
||||
uint8_t mode;
|
||||
uint8_t bits;
|
||||
uint32_t speed;
|
||||
}spi_description;
|
||||
|
||||
// constructor
|
||||
mcp2515_dev* new_mcp2515_dev(char *);
|
||||
|
||||
// Open and initial mcp2515
|
||||
int mcp2515_initial(mcp2515_dev*);
|
||||
|
||||
uint8_t mcp2515_setMode(mcp2515_dev *mcp2515_device,const CANCTRL_REQOP_MODE mode);
|
||||
uint8_t mcp2515_setConfigMode(mcp2515_dev *mcp2515_device);
|
||||
void mcp2515_prepareId(uint8_t *buffer, const uint8_t ext, const uint32_t id);
|
||||
int mcp2515_setFillter(mcp2515_dev *mcp2515_device,const RXF num, const uint8_t ext, const uint32_t ulData);
|
||||
int mcp2515_setFilterMask(mcp2515_dev *mcp2515_device,const MASK mask, const uint8_t ext, const uint32_t ulData);
|
||||
int mcp2515_reset(mcp2515_dev *mcp2515_device);
|
||||
int mcp2515_can_speed(mcp2515_dev *mcp2515_device, const CAN_SPEED canSpeed, CAN_CLOCK canClock);
|
||||
|
||||
// Send Data to MCP2515
|
||||
int mcp2515_write_register(mcp2515_dev*,uint8_t,uint8_t*,int8_t);
|
||||
int mcp2515_write_register_once(mcp2515_dev *mcp2515_device, uint8_t register_address, uint8_t data);
|
||||
|
||||
// Receive Data from MCP2515
|
||||
void mcp2515_read_register(mcp2515_dev *mcp2515_device, uint8_t register_address, uint8_t values[], uint8_t data_length);
|
||||
uint8_t mcp2515_read_register_once(mcp2515_dev *mcp2515_device, uint8_t register_address);
|
||||
|
||||
// MCP2515 mode setting
|
||||
int mcp2515_set_mode(mcp2515_dev*);
|
||||
uint8_t mcp2515_read_status(mcp2515_dev*);
|
||||
|
||||
// mcp2515 bit modify
|
||||
void mcp2515_modify_bit(mcp2515_dev*, uint8_t, uint8_t, uint8_t);
|
||||
|
||||
// mcp2515 speed select
|
||||
|
||||
int mcp2515_sendMessage(mcp2515_dev *mcp2515_device, uint32_t can_id, uint8_t* data, uint8_t data_length);
|
||||
int mcp2515_sendMessageT(mcp2515_dev *mcp2515_device, const TXBn txbn, uint32_t can_id, uint8_t* data, uint8_t data_length);
|
||||
|
||||
int mcp2515_readMessage(mcp2515_dev *mcp2515_device,canid_t* can_id, uint8_t* data, uint8_t* data_length);
|
||||
int mcp2515_readMessageT(mcp2515_dev *mcp2515_device,const RXBn rxbn,canid_t* can_id, uint8_t* data, uint8_t* data_length);
|
||||
|
||||
#endif
|
||||
@ -9,7 +9,7 @@ initial_fec_app_type = 0 # 0: ceiling, 1: table, 2: wall
|
||||
eis_enable = 1
|
||||
|
||||
[nnm]
|
||||
ModelPath = "nef/STDC04012026_models_630.nef"
|
||||
ModelPath = "nef/STDC03302026_models_630.nef"
|
||||
ModelId = 32769 # KNERON_YOLOV5S_COCO80_640_640_3 (YOLO v5s)
|
||||
JobId = 200 # KDP2_INF_ID_APP_YOLO
|
||||
InferenceStream = 1 # Inference stream index (use stream1: 640x640)
|
||||
@ -19,7 +19,7 @@ GetImageBufMode = 0 # 0: block mode 1: non-block mode
|
||||
RoiEnable = 0 # Enable ROI for nnm detect
|
||||
RoiX = 0 # ROI start x
|
||||
RoiY = 0 # ROI start y
|
||||
DrawBoxEnable = 1 # draw object bounding box on stream0 (also enables STDC seg overlay)
|
||||
DrawBoxEnable = 0 # draw object bounding box on stream0 (also enables STDC seg overlay)
|
||||
OnlyPerson = 1 # only draw person bounding box when DrawBoxEnable
|
||||
#(so far for yolo only, JobId = 11)
|
||||
DrawOnResize = 0; # If InferenceStream is 0. This setting needs to be enabled. The box will be drawn on all resize streams.
|
||||
@ -88,12 +88,26 @@ http_url = http://192.168.0.114:8081/api/upload
|
||||
cooldown_ms = 1000
|
||||
|
||||
[event]
|
||||
# Violation event recorder — two-channel: JSON (BT/iPad) + tar.gz (Allxon OOB)
|
||||
# Violation event recorder ??two-channel: JSON (BT/iPad) + tar.gz (Allxon OOB)
|
||||
enable = 1
|
||||
bt_uart_dev = /dev/ttyS1 # Channel A: UART device for DX-BT24 BLE module → iPad
|
||||
bt_uart_dev = /dev/ttyS1 # Channel A: UART device for DX-BT24 BLE module ??iPad
|
||||
bt_at_probe = 0 # 0: skip AT commands (normal); 1: one-time baud upgrade (factory/reset only)
|
||||
upload_url = http://192.168.0.114:8081/api/golf.cgi # Channel B: tar.gz upload (OOB/cloud path)
|
||||
aresx_version = 1.0.0 # Reported in status_check notify (ares_x_version)
|
||||
bt_name = BT-24 # Reported in status_check notify (bluetooth_peripheral_name)
|
||||
; device_id: Raw KL630 Kn chip number (e.g., 0xCE7B562C).
|
||||
; Leave empty to auto-detect on first boot; value is written back automatically.
|
||||
; BLE broadcast name (event:bt_name) is derived as AresX-{lower 16-bit hex}.
|
||||
device_id =
|
||||
upload_url = http://192.168.0.99/api/golf.cgi # Channel B: tar.gz upload (OOB/cloud path)
|
||||
# Production: http://192.168.0.99/api/golf.cgi
|
||||
sd_path = /tmp/sdcard/events # SD card event archive path
|
||||
sd_max_mb = 7168 # 7 GB — delete oldest when exceeded
|
||||
sd_max_mb = 7168 # 7 GB ??delete oldest when exceeded
|
||||
upload_delay_ms = 0 # 0: upload immediately after tar.gz built (tar is built after event ends, no extra delay needed)
|
||||
|
||||
[can]
|
||||
# CAN bus via MCP2515 SPI controller on J16 connector
|
||||
# J16 pin order: CS -> DO -> CLK -> RESERVED(DI)
|
||||
enable = 1 # 1: enable CAN bus output; 0: disabled
|
||||
spidev = /dev/spidev1.0 # SPI device path for MCP2515
|
||||
speed_kbps = 250 # CAN bus speed: 125 / 250 / 500 / 1000 kbps
|
||||
can_id = 0x100 # 11-bit standard frame ID for outbound event frames
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
flask>=2.3
|
||||
opencv-python>=4.8
|
||||
python-can>=4.3
|
||||
|
||||
@ -26,6 +26,9 @@
|
||||
#define FDFR_MODEL_SIZE_COL 90
|
||||
#define MAX_RESULT_BOX 80
|
||||
|
||||
#define STDC_ROI_SEGMENTS 12
|
||||
#define STDC_ROI_EDGE_THICKNESS 2
|
||||
|
||||
static bool _init_config_yolo_params = false;
|
||||
extern unsigned int g_dwDrawBoxEnable;
|
||||
extern unsigned int g_dwDrawBoxType;
|
||||
@ -102,7 +105,7 @@ static void print_yolo_result(kp_app_yolo_result_t *yolo_data)
|
||||
|
||||
if (dwResultCounts > MAX_RESULT_BOX)
|
||||
dwResultCounts = MAX_RESULT_BOX;
|
||||
|
||||
#if 0
|
||||
if (g_dwDrawBoxEnable) {
|
||||
for (i = 0 ; i < dwResultCounts; i++) {
|
||||
if (g_atDrawInfo[i].bDrawFlag && !g_dwDrawBoxType)
|
||||
@ -114,7 +117,7 @@ static void print_yolo_result(kp_app_yolo_result_t *yolo_data)
|
||||
g_dwResultCounts = 0;
|
||||
for (i = 0; i < yolo_data->box_count; i++) {
|
||||
if (g_dwOnlyPerson) {
|
||||
if (yolo_data->boxes[i].class_num != 0)
|
||||
if (yolo_data->boxes[i].class_num != 0 && yolo_data->boxes[i].class_num != 2)
|
||||
continue;
|
||||
}
|
||||
calculate_bbox_postion(&g_atDrawInfo[i], yolo_data->boxes[i].x1, yolo_data->boxes[i].y1,
|
||||
@ -130,7 +133,7 @@ static void print_yolo_result(kp_app_yolo_result_t *yolo_data)
|
||||
|
||||
for (i = 0; i < yolo_data->box_count; i++) {
|
||||
if (g_dwOnlyPerson) {
|
||||
if (yolo_data->boxes[i].class_num != 0)
|
||||
if (yolo_data->boxes[i].class_num != 0 && yolo_data->boxes[i].class_num != 2)
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -138,6 +141,133 @@ static void print_yolo_result(kp_app_yolo_result_t *yolo_data)
|
||||
yolo_data->box_count, yolo_data->boxes[i].x1, yolo_data->boxes[i].y1,
|
||||
yolo_data->boxes[i].x2, yolo_data->boxes[i].y2, yolo_data->boxes[i].score, yolo_data->boxes[i].class_num);
|
||||
}
|
||||
#else
|
||||
/* ── [YOLO] collision_warning(2.2.5)─────────────────────────────
|
||||
* 偵測到 vehicle(class=2) 時送 BLE notify。
|
||||
* 狀態有變化(進入/離開)才送,debounce 2 秒,避免頻繁發送。
|
||||
* 將 #else 改為 #if 1 可啟用原有僅列印的邏輯。
|
||||
* ---------------------------------------------------------------- */
|
||||
{
|
||||
static int s_cw_level = 0;
|
||||
static char s_cw_type[16] = "";
|
||||
static struct timeval s_cw_last_sent = {0, 0};
|
||||
|
||||
int cur_level = 0;
|
||||
char cur_type[16] = "";
|
||||
|
||||
if (g_dwDrawBoxEnable) {
|
||||
for (i = 0 ; i < dwResultCounts; i++) {
|
||||
if (g_atDrawInfo[i].bDrawFlag && !g_dwDrawBoxType)
|
||||
setup_isp_privacy_mask(0, g_atDrawInfo[i].dwStartX, g_atDrawInfo[i].dwStartY, g_atDrawInfo[i].dwWidth, g_atDrawInfo[i].dwHeight);
|
||||
}
|
||||
memset(&g_atDrawInfo, 0, sizeof(g_atDrawInfo));
|
||||
dwResultCounts = yolo_data->box_count;
|
||||
|
||||
g_dwResultCounts = 0;
|
||||
for (i = 0; i < yolo_data->box_count; i++) {
|
||||
if (g_dwOnlyPerson) {
|
||||
if (yolo_data->boxes[i].class_num != 0 && yolo_data->boxes[i].class_num != 2)
|
||||
continue;
|
||||
}
|
||||
calculate_bbox_postion(&g_atDrawInfo[i], yolo_data->boxes[i].x1, yolo_data->boxes[i].y1,
|
||||
yolo_data->boxes[i].x2, yolo_data->boxes[i].y2, yolo_data->boxes[i].score, yolo_data->boxes[i].class_num);
|
||||
g_atDrawInfo[i].bDrawFlag = true;
|
||||
if(!g_dwDrawBoxType) {
|
||||
setup_isp_privacy_mask(1, g_atDrawInfo[i].dwStartX, g_atDrawInfo[i].dwStartY, g_atDrawInfo[i].dwWidth, g_atDrawInfo[i].dwHeight);
|
||||
}
|
||||
g_dwResultCounts++;
|
||||
}
|
||||
if(g_dwResultCounts > MAX_RESULT_BOX) { g_dwResultCounts = MAX_RESULT_BOX; }
|
||||
}
|
||||
|
||||
for (i = 0; i < yolo_data->box_count; i++) {
|
||||
int cls = (int)yolo_data->boxes[i].class_num;
|
||||
if (cls != 2) continue; /* 只看 vehicle(2) */
|
||||
|
||||
cur_level = 1;
|
||||
snprintf(cur_type, sizeof(cur_type), "vehicle");
|
||||
break; /* 找到一輛車就夠了 */
|
||||
}
|
||||
|
||||
/* 狀態有變化時,檢查 debounce */
|
||||
if (cur_level != s_cw_level ||
|
||||
strcmp(cur_type, s_cw_type) != 0)
|
||||
{
|
||||
struct timeval now;
|
||||
gettimeofday(&now, NULL);
|
||||
|
||||
long elapsed_ms = (now.tv_sec - s_cw_last_sent.tv_sec) * 1000
|
||||
+ (now.tv_usec - s_cw_last_sent.tv_usec) / 1000;
|
||||
|
||||
if (s_cw_last_sent.tv_sec == 0 || elapsed_ms >= 2000) {
|
||||
fire_collision_warning(cur_level, cur_level ? cur_type : NULL);
|
||||
s_cw_level = cur_level;
|
||||
snprintf(s_cw_type, sizeof(s_cw_type), "%s", cur_type);
|
||||
s_cw_last_sent = now;
|
||||
printf("[YOLO] collision_warning sent level=%d type=%s\n",
|
||||
cur_level, cur_level ? cur_type : "null");
|
||||
} else {
|
||||
printf("[YOLO] collision_warning debounce bypass (%ldms < 5000ms)\n",
|
||||
elapsed_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static unsigned int stdc_u32_lerp(unsigned int a, unsigned int b, unsigned int i, unsigned int n)
|
||||
{
|
||||
int64_t diff;
|
||||
int64_t v;
|
||||
|
||||
if (n == 0)
|
||||
return a;
|
||||
|
||||
diff = (int64_t)b - (int64_t)a;
|
||||
v = (int64_t)a + (diff * (int64_t)i) / (int64_t)n;
|
||||
if (v < 0)
|
||||
return 0;
|
||||
return (unsigned int)v;
|
||||
}
|
||||
|
||||
static void stdc_add_rect(DETECT_INFO *di, unsigned int *cnt,
|
||||
unsigned int x, unsigned int y,
|
||||
unsigned int w, unsigned int h)
|
||||
{
|
||||
if (*cnt >= MAX_RESULT_BOX || w == 0 || h == 0)
|
||||
return;
|
||||
|
||||
di[*cnt].dwStartX = x;
|
||||
di[*cnt].dwStartY = y;
|
||||
di[*cnt].dwWidth = w;
|
||||
di[*cnt].dwHeight = h;
|
||||
di[*cnt].fScore = 0;
|
||||
di[*cnt].dwClass = 0;
|
||||
di[*cnt].bDrawFlag = true;
|
||||
(*cnt)++;
|
||||
}
|
||||
|
||||
static void stdc_add_trapezoid_outline(DETECT_INFO *di, unsigned int *cnt,
|
||||
unsigned int x_tl, unsigned int x_tr,
|
||||
unsigned int x_bl, unsigned int x_br,
|
||||
unsigned int y_top, unsigned int y_bottom)
|
||||
{
|
||||
/* Top and bottom edges */
|
||||
if (x_tr > x_tl) {
|
||||
stdc_add_rect(di, cnt, x_tl, y_top, x_tr - x_tl, STDC_ROI_EDGE_THICKNESS);
|
||||
}
|
||||
if (x_br > x_bl) {
|
||||
stdc_add_rect(di, cnt, x_bl, y_bottom, x_br - x_bl, STDC_ROI_EDGE_THICKNESS);
|
||||
}
|
||||
|
||||
/* Left and right slanted edges as sampled points */
|
||||
for (unsigned int i = 0; i <= STDC_ROI_SEGMENTS; i++) {
|
||||
unsigned int y = stdc_u32_lerp(y_top, y_bottom, i, STDC_ROI_SEGMENTS);
|
||||
unsigned int xl = stdc_u32_lerp(x_tl, x_bl, i, STDC_ROI_SEGMENTS);
|
||||
unsigned int xr = stdc_u32_lerp(x_tr, x_br, i, STDC_ROI_SEGMENTS);
|
||||
stdc_add_rect(di, cnt, xl, y, STDC_ROI_EDGE_THICKNESS, STDC_ROI_EDGE_THICKNESS);
|
||||
stdc_add_rect(di, cnt, xr, y, STDC_ROI_EDGE_THICKNESS, STDC_ROI_EDGE_THICKNESS);
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
@ -214,7 +344,7 @@ int app_header_send_inference(uint32_t buf_addr, bool *bl_run_next_inference, vo
|
||||
|
||||
app_yolo_header->image_format = kp_image_format;
|
||||
app_yolo_header->model_normalize = KP_NORMALIZE_KNERON;//KP_NORMALIZE_DISABLE;
|
||||
printf("[SEND] inf_number = %d \n", inf_number);
|
||||
//printf("[SEND] inf_number = %d \n", inf_number);
|
||||
//memcpy((void *)(buf_addr + sizeof(kdp2_ipc_app_yolo_inf_header_t)), image_buffer, image_buffer_size);
|
||||
ret = dma2d_copy(pInitOpt->ptDmaInfo->ptDmaHandle, pInitOpt->ptDmaInfo->ptDmaDesc, (void *)(buf_addr + sizeof(kdp2_ipc_app_yolo_inf_header_t)), (void*)image_buffer, vsrc_ssm_info);
|
||||
if(ret){
|
||||
@ -303,7 +433,7 @@ int app_header_recv_inference(uint32_t buf_addr, bool *bl_run_next_inference)
|
||||
|
||||
kdp2_ipc_app_yolo_result_t *app_yolo_result = (kdp2_ipc_app_yolo_result_t *)header_stamp;
|
||||
kp_app_yolo_result_t *yolo_data = &app_yolo_result->yolo_data;
|
||||
printf("[RECV] inf_number = %d \n", app_yolo_result->inf_number);
|
||||
//printf("[RECV] inf_number = %d \n", app_yolo_result->inf_number);
|
||||
print_yolo_result(yolo_data);
|
||||
/****************************************************************
|
||||
* finish application - close receive thead
|
||||
@ -432,12 +562,9 @@ int app_header_recv_inference(uint32_t buf_addr, bool *bl_run_next_inference)
|
||||
* Requires DrawBoxEnable=1 in host_stream.ini / demo_rtsp.sh.
|
||||
* The draw_box thread calls draw_rect() for each entry 0..g_dwResultCounts-1.
|
||||
*
|
||||
* Layout on 1920×1080 (stream0):
|
||||
*
|
||||
* [center] Collision ROI outline x=480 y=270 w=960 h=486
|
||||
* (always present; turns into a thick double-border on collision)
|
||||
* [center] Forward ROI outline x=576 y=594 w=768 h=432
|
||||
* (outer bbox of trapezoid: bottom-left 30%, bottom-right 70%)
|
||||
* Shared ROI trapezoid (same as web overlay polygon):
|
||||
* points="45,55 55,55 70,95 30,95"
|
||||
* i.e. top 45%~55%, bottom 30%~70%, y 55%~95%.
|
||||
*
|
||||
* [top-left] Warning boxes 200×55px, stride 63px (when warning active):
|
||||
* y=10 collision_risk y=199 grass_warning
|
||||
@ -457,27 +584,28 @@ int app_header_recv_inference(uint32_t buf_addr, bool *bl_run_next_inference)
|
||||
if (g_dwDrawBoxEnable) {
|
||||
unsigned int cnt = 0;
|
||||
DETECT_INFO *di = g_atDrawInfo;
|
||||
const unsigned int draw_w = 1920u;
|
||||
const unsigned int draw_h = 1080u;
|
||||
|
||||
/* Collision ROI outline — always drawn */
|
||||
di[cnt].dwStartX = 480; di[cnt].dwStartY = 270;
|
||||
di[cnt].dwWidth = 960; di[cnt].dwHeight = 486;
|
||||
di[cnt].fScore = 0; di[cnt].dwClass = 0; di[cnt].bDrawFlag = true;
|
||||
cnt++;
|
||||
unsigned int x_tl = (draw_w * 45u) / 100u;
|
||||
unsigned int x_tr = (draw_w * 55u) / 100u;
|
||||
unsigned int x_bl = (draw_w * 30u) / 100u;
|
||||
unsigned int x_br = (draw_w * 70u) / 100u;
|
||||
unsigned int y_top = (draw_h * 55u) / 100u;
|
||||
unsigned int y_bottom = (draw_h * 95u) / 100u;
|
||||
|
||||
/* When collision active: draw a second inner outline for emphasis */
|
||||
if (ana->collision_risk) {
|
||||
di[cnt].dwStartX = 492; di[cnt].dwStartY = 282;
|
||||
di[cnt].dwWidth = 936; di[cnt].dwHeight = 462;
|
||||
di[cnt].fScore = 0; di[cnt].dwClass = 0; di[cnt].bDrawFlag = true;
|
||||
cnt++;
|
||||
stdc_add_trapezoid_outline(di, &cnt,
|
||||
x_tl, x_tr, x_bl, x_br,
|
||||
y_top, y_bottom);
|
||||
|
||||
/* Collision emphasis: draw a second inner trapezoid when active. */
|
||||
if (ana->collision_risk && x_tr > x_tl + 8u && x_br > x_bl + 16u && y_bottom > y_top + 8u) {
|
||||
stdc_add_trapezoid_outline(di, &cnt,
|
||||
x_tl + 4u, x_tr - 4u,
|
||||
x_bl + 8u, x_br - 8u,
|
||||
y_top + 4u, y_bottom - 4u);
|
||||
}
|
||||
|
||||
/* Forward ROI outline — always drawn */
|
||||
di[cnt].dwStartX = 576; di[cnt].dwStartY = 594;
|
||||
di[cnt].dwWidth = 768; di[cnt].dwHeight = 432;
|
||||
di[cnt].fScore = 0; di[cnt].dwClass = 0; di[cnt].bDrawFlag = true;
|
||||
cnt++;
|
||||
|
||||
/* Side warning/class panels are intentionally disabled.
|
||||
* Keep only central ROI overlays to avoid per-class side boxes. */
|
||||
|
||||
|
||||
96
src/host_stream/base64.h
Normal file
96
src/host_stream/base64.h
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* base64.h — Standalone RFC 4648 Base64 encode/decode, header-only (static inline).
|
||||
* No external dependencies.
|
||||
*/
|
||||
#ifndef BASE64_H
|
||||
#define BASE64_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char B64_ENC[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/*
|
||||
* base64_encode - Encode src_len bytes to a NUL-terminated Base64 string.
|
||||
* dst must be at least ((src_len + 2) / 3) * 4 + 1 bytes.
|
||||
*/
|
||||
static inline void base64_encode(const uint8_t *src, size_t src_len, char *dst)
|
||||
{
|
||||
size_t i = 0, j = 0;
|
||||
|
||||
while (i + 2 < src_len) {
|
||||
uint32_t v = ((uint32_t)src[i] << 16)
|
||||
| ((uint32_t)src[i+1] << 8)
|
||||
| (uint32_t)src[i+2];
|
||||
dst[j++] = B64_ENC[(v >> 18) & 0x3F];
|
||||
dst[j++] = B64_ENC[(v >> 12) & 0x3F];
|
||||
dst[j++] = B64_ENC[(v >> 6) & 0x3F];
|
||||
dst[j++] = B64_ENC[ v & 0x3F];
|
||||
i += 3;
|
||||
}
|
||||
|
||||
if (i < src_len) {
|
||||
uint32_t v = (uint32_t)src[i] << 16;
|
||||
if (i + 1 < src_len) v |= (uint32_t)src[i+1] << 8;
|
||||
dst[j++] = B64_ENC[(v >> 18) & 0x3F];
|
||||
dst[j++] = B64_ENC[(v >> 12) & 0x3F];
|
||||
dst[j++] = (i + 1 < src_len) ? B64_ENC[(v >> 6) & 0x3F] : '=';
|
||||
dst[j++] = '=';
|
||||
}
|
||||
|
||||
dst[j] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* base64_decode - Decode a Base64 string to bytes.
|
||||
* Returns number of decoded bytes, or -1 on invalid input.
|
||||
* dst must be at least (strlen(src) / 4) * 3 bytes.
|
||||
*/
|
||||
static inline int base64_decode(const char *src, uint8_t *dst)
|
||||
{
|
||||
/* Lookup table: 0-63=value, 64='=' padding, -1=invalid */
|
||||
static const int8_t LUT[256] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 64, -1, -1, /* 0x30 */
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xa0 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xb0 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xc0 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xd0 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xe0 */
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* 0xf0 */
|
||||
};
|
||||
|
||||
int len = (int)strlen(src);
|
||||
if (len % 4 != 0) return -1;
|
||||
|
||||
int out = 0;
|
||||
for (int i = 0; i < len; i += 4) {
|
||||
int8_t a = LUT[(uint8_t)src[i]];
|
||||
int8_t b = LUT[(uint8_t)src[i+1]];
|
||||
int8_t c = LUT[(uint8_t)src[i+2]];
|
||||
int8_t d = LUT[(uint8_t)src[i+3]];
|
||||
if (a < 0 || b < 0 || c < 0 || d < 0) return -1;
|
||||
|
||||
uint32_t v = ((uint32_t)a << 18)
|
||||
| ((uint32_t)b << 12)
|
||||
| ((c == 64) ? 0 : ((uint32_t)c << 6))
|
||||
| ((d == 64) ? 0 : (uint32_t)d);
|
||||
|
||||
dst[out++] = (uint8_t)(v >> 16);
|
||||
if (c != 64) dst[out++] = (uint8_t)(v >> 8);
|
||||
if (d != 64) dst[out++] = (uint8_t)v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif /* BASE64_H */
|
||||
@ -25,13 +25,15 @@
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "bt_uart.h"
|
||||
#include "handshake.h"
|
||||
|
||||
/* ── UART fd ─────────────────────────────────────────────────────────────── */
|
||||
static int s_bt_fd = -1;
|
||||
|
||||
/* ── Message queue ───────────────────────────────────────────────────────── */
|
||||
/* ── Message queue (TX) ──────────────────────────────────────────────────── */
|
||||
typedef struct BtMsg {
|
||||
char *json;
|
||||
struct BtMsg *next;
|
||||
@ -42,8 +44,171 @@ static BtMsg *s_q_tail = NULL;
|
||||
static pthread_mutex_t s_q_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t s_q_cond = PTHREAD_COND_INITIALIZER;
|
||||
static pthread_t s_writer_tid;
|
||||
static pthread_t s_reader_tid;
|
||||
static volatile int s_running = 0;
|
||||
|
||||
/* ── Identity ────────────────────────────────────────────────────────────── */
|
||||
static char s_ares_version[32] = "1.0.0";
|
||||
static char s_bt_name[32] = "BT-24";
|
||||
|
||||
/* ── Rent status: 0=未租借 1=租借中 2=管理模式 ────────────────────────────── */
|
||||
static int s_rent_status = 0;
|
||||
static pthread_mutex_t s_rent_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* ── Cart-control intervention callback ──────────────────────────────────── */
|
||||
static bt_intervention_fn s_intervention_cb = NULL;
|
||||
|
||||
/* ── BLE Handshake context ───────────────────────────────────────────────── */
|
||||
static handshake_ctx_t s_handshake_ctx;
|
||||
|
||||
/* ── Response builders ───────────────────────────────────────────────────── */
|
||||
static void reply_status_check(void)
|
||||
{
|
||||
int intervention = s_intervention_cb ? s_intervention_cb() : 0;
|
||||
char json[256];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"response_type\":\"status_check\",\"content\":{"
|
||||
"\"ares_x_version\":\"%s\","
|
||||
"\"bluetooth_peripheral_name\":\"%s\","
|
||||
"\"is_intervention_cart_control\":%s}}",
|
||||
s_ares_version, s_bt_name,
|
||||
intervention ? "true" : "false");
|
||||
bt_uart_send_json(json);
|
||||
printf("[BT CMD] status_check sent\n");
|
||||
}
|
||||
|
||||
static void reply_rent_status(void)
|
||||
{
|
||||
int st;
|
||||
pthread_mutex_lock(&s_rent_mtx);
|
||||
st = s_rent_status;
|
||||
pthread_mutex_unlock(&s_rent_mtx);
|
||||
char json[80];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"response_type\":\"rent_status\",\"content\":{\"status\":%d}}", st);
|
||||
bt_uart_send_json(json);
|
||||
printf("[BT CMD] rent_status=%d sent\n", st);
|
||||
}
|
||||
|
||||
/* ── Command handler ─────────────────────────────────────────────────────── */
|
||||
static void handle_command(const char *json)
|
||||
{
|
||||
printf("[BT RX] %.128s\n", json);
|
||||
|
||||
/* ── Handshake state machine ─────────────────────────────────────────── */
|
||||
handshake_state_t hs_state = handshake_get_state(&s_handshake_ctx);
|
||||
|
||||
if (hs_state == HS_STATE_IDLE) {
|
||||
/* First message received before init sent challenge — send it now */
|
||||
char challenge_json[256];
|
||||
if (handshake_build_challenge(&s_handshake_ctx,
|
||||
challenge_json, sizeof(challenge_json)) == 0)
|
||||
bt_uart_send_json(challenge_json);
|
||||
return; /* do not process this message until handshake completes */
|
||||
}
|
||||
|
||||
if (hs_state == HS_STATE_WAIT_RESPONSE) {
|
||||
char confirm_json[256];
|
||||
int result = handshake_process_response(&s_handshake_ctx, json);
|
||||
if (result == 1) {
|
||||
handshake_build_confirm(&s_handshake_ctx, 1, NULL,
|
||||
confirm_json, sizeof(confirm_json));
|
||||
bt_uart_send_json(confirm_json);
|
||||
printf("[BT] handshake SUCCESS\n");
|
||||
} else {
|
||||
const char *reason = (result == -2) ? "timeout"
|
||||
: (result == -1) ? "malformed"
|
||||
: "invalid_response";
|
||||
handshake_build_confirm(&s_handshake_ctx, 0, reason,
|
||||
confirm_json, sizeof(confirm_json));
|
||||
bt_uart_send_json(confirm_json);
|
||||
printf("[BT] handshake FAILED: %s\n", reason);
|
||||
bt_uart_disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hs_state == HS_STATE_FAILED) {
|
||||
printf("[BT] command rejected — handshake failed, waiting for reconnect\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── HS_STATE_SUCCESS: normal command processing ─────────────────────── */
|
||||
if (strstr(json, "\"status_check\"")) {
|
||||
reply_status_check();
|
||||
} else if (strstr(json, "\"get_rent_status\"")) {
|
||||
reply_rent_status();
|
||||
} else if (strstr(json, "\"return\"")) {
|
||||
pthread_mutex_lock(&s_rent_mtx);
|
||||
if (s_rent_status == 1) s_rent_status = 0;
|
||||
pthread_mutex_unlock(&s_rent_mtx);
|
||||
printf("[BT CMD] return processed\n");
|
||||
reply_rent_status();
|
||||
} else if (strstr(json, "\"rent\"")) {
|
||||
pthread_mutex_lock(&s_rent_mtx);
|
||||
if (s_rent_status == 0) s_rent_status = 1;
|
||||
pthread_mutex_unlock(&s_rent_mtx);
|
||||
printf("[BT CMD] rent processed\n");
|
||||
reply_rent_status();
|
||||
} else {
|
||||
printf("[BT RX] unrecognised command\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Reader thread: accumulates bytes into JSON objects, calls handler ────── */
|
||||
static void *bt_reader_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char buf[512];
|
||||
int pos = 0, depth = 0;
|
||||
int in_str = 0, escaped = 0;
|
||||
|
||||
while (s_running) {
|
||||
if (s_bt_fd < 0) { usleep(100000); continue; }
|
||||
|
||||
fd_set fds;
|
||||
struct timeval tv = {0, 500000}; /* 500 ms — lets us recheck s_running */
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(s_bt_fd, &fds);
|
||||
if (select(s_bt_fd + 1, &fds, NULL, NULL, &tv) <= 0)
|
||||
continue;
|
||||
|
||||
unsigned char c;
|
||||
if (read(s_bt_fd, &c, 1) != 1) continue;
|
||||
|
||||
/* JSON brace / string-literal state machine */
|
||||
if (!in_str) {
|
||||
if (c == '"') { in_str = 1; }
|
||||
else if (c == '{') { depth++; }
|
||||
else if (c == '}') { depth--; }
|
||||
} else {
|
||||
if (escaped) { escaped = 0; }
|
||||
else if (c == '\\') { escaped = 1; }
|
||||
else if (c == '"') { in_str = 0; }
|
||||
}
|
||||
|
||||
/* Buffer the character once we are inside a JSON object */
|
||||
if (depth > 0 || (depth == 0 && pos > 0)) {
|
||||
if (pos < (int)sizeof(buf) - 1)
|
||||
buf[pos++] = (char)c;
|
||||
}
|
||||
|
||||
/* Complete object received */
|
||||
if (depth == 0 && pos > 0) {
|
||||
buf[pos] = '\0';
|
||||
handle_command(buf);
|
||||
pos = 0; in_str = 0; escaped = 0;
|
||||
}
|
||||
|
||||
/* Discard overlong garbage */
|
||||
if (pos >= (int)sizeof(buf) - 1) {
|
||||
printf("[BT RX] buffer overflow — discarding\n");
|
||||
pos = 0; depth = 0; in_str = 0; escaped = 0;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ── Internal: write all bytes to fd ────────────────────────────────────── */
|
||||
static void uart_write_all(const char *data, size_t len)
|
||||
{
|
||||
@ -122,6 +287,32 @@ static int open_uart(const char *dev, speed_t baud, int vmin, int vtime)
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* ── One-time AT helpers ─────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* bt_set_module_name - Send AT+NAME<name> to the DX-BT24 module.
|
||||
* @fd: already-open UART fd at 115200
|
||||
* @name: BT broadcast name to set (must be non-empty)
|
||||
*
|
||||
* The DX-BT24 command format is: AT+NAME<name>\r\n (no space or '=')
|
||||
*/
|
||||
static void bt_set_module_name(int fd, const char *name)
|
||||
{
|
||||
if (!name || !*name) {
|
||||
printf("[BT] WARNING: bt_set_module_name skipped — s_bt_name is empty\n");
|
||||
return;
|
||||
}
|
||||
char cmd[64];
|
||||
char buf[64] = {0};
|
||||
int n;
|
||||
snprintf(cmd, sizeof(cmd), "AT+NAME%s\r\n", name);
|
||||
write(fd, cmd, strlen(cmd));
|
||||
usleep(100000); /* 100 ms — module needs time to process */
|
||||
n = read(fd, buf, sizeof(buf) - 1);
|
||||
buf[n > 0 ? n : 0] = '\0';
|
||||
printf("[BT] AT+NAME%s => %d byte(s): %s\n", name, n, n > 0 ? buf : "(none)");
|
||||
}
|
||||
|
||||
/* ── One-time AT baud setup (call only when bt_at_probe = 1 in INI) ─────── */
|
||||
static void bt_uart_probe_and_upgrade(const char *dev)
|
||||
{
|
||||
@ -166,17 +357,18 @@ static void bt_uart_probe_and_upgrade(const char *dev)
|
||||
usleep(100000); /* 100 ms: wait for ack + baud switch */
|
||||
n = read(fd, buf, sizeof(buf) - 1);
|
||||
printf("[BT] AT+BAUD7 => %d byte(s): %s\n", n, n > 0 ? buf : "(none)");
|
||||
bt_set_module_name(fd, s_bt_name); /* set BT broadcast name before reset */
|
||||
write(fd, "AT+RESET\r\n", 10);
|
||||
usleep(100000);
|
||||
read(fd, buf, sizeof(buf) - 1);
|
||||
close(fd);
|
||||
usleep(500000); /* 500 ms: wait for module to reboot */
|
||||
close(fd);
|
||||
usleep(300000); /* 300 ms: let module stabilise at 115200 */
|
||||
|
||||
/* Reopen at 115200 and reset so the new baud persists across power cycles */
|
||||
fd = open_uart(dev, B115200, 0, 10);
|
||||
if (fd < 0) return;
|
||||
write(fd, "AT+RESET\r\n", 10);
|
||||
usleep(100000);
|
||||
read(fd, buf, sizeof(buf) - 1);
|
||||
close(fd);
|
||||
usleep(500000); /* 500 ms: wait for module to reboot */
|
||||
printf("[BT] upgrade complete — set bt_at_probe = 0 in INI to skip AT on next boot\n");
|
||||
}
|
||||
|
||||
@ -206,7 +398,7 @@ int bt_uart_init(const char *dev, int do_at_probe)
|
||||
|
||||
s_bt_fd = fd;
|
||||
|
||||
/* Start dedicated writer thread */
|
||||
/* Start TX writer thread */
|
||||
s_running = 1;
|
||||
if (pthread_create(&s_writer_tid, NULL, bt_writer_thread, NULL) != 0) {
|
||||
perror("[BT] pthread_create writer");
|
||||
@ -217,6 +409,15 @@ int bt_uart_init(const char *dev, int do_at_probe)
|
||||
}
|
||||
pthread_setname_np(s_writer_tid, "bt_writer");
|
||||
|
||||
/* Start RX reader thread */
|
||||
if (pthread_create(&s_reader_tid, NULL, bt_reader_thread, NULL) != 0) {
|
||||
perror("[BT] pthread_create reader");
|
||||
/* Non-fatal: TX still works; log and continue */
|
||||
s_reader_tid = 0;
|
||||
} else {
|
||||
pthread_setname_np(s_reader_tid, "bt_reader");
|
||||
}
|
||||
|
||||
printf("[BT] bt_uart_init OK: %s @ 115200 (at_probe=%d)\n", dev, do_at_probe);
|
||||
|
||||
/* Diagnostic ping — confirms UART→BLE channel is alive.
|
||||
@ -225,6 +426,17 @@ int bt_uart_init(const char *dev, int do_at_probe)
|
||||
bt_uart_send_json("{\"class\":\"boot\",\"level\":0}");
|
||||
printf("[BT] boot ping queued\n");
|
||||
|
||||
/* Initialise handshake context and send initial challenge to peer */
|
||||
handshake_init(&s_handshake_ctx);
|
||||
{
|
||||
char challenge_json[256];
|
||||
if (handshake_build_challenge(&s_handshake_ctx,
|
||||
challenge_json, sizeof(challenge_json)) == 0) {
|
||||
bt_uart_send_json(challenge_json);
|
||||
printf("[BT] handshake challenge queued\n");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -249,17 +461,27 @@ void bt_uart_send_json(const char *json)
|
||||
pthread_mutex_unlock(&s_q_mtx);
|
||||
}
|
||||
|
||||
void bt_uart_disconnect(void)
|
||||
{
|
||||
/* DX-BT24 is a passthrough UART module — no hardware-level BLE disconnect API.
|
||||
* Reset handshake so the next peer must re-authenticate, and notify the app. */
|
||||
printf("[BT] bt_uart_disconnect: resetting handshake state\n");
|
||||
handshake_reset(&s_handshake_ctx);
|
||||
bt_uart_send_json("{\"response_type\":\"disconnect\",\"content\":{}}");
|
||||
}
|
||||
|
||||
void bt_uart_close(void)
|
||||
{
|
||||
if (!s_running) return;
|
||||
|
||||
/* Signal writer thread to exit after draining remaining messages */
|
||||
/* Signal both threads to stop (reader uses select timeout to check s_running) */
|
||||
pthread_mutex_lock(&s_q_mtx);
|
||||
s_running = 0;
|
||||
pthread_cond_signal(&s_q_cond);
|
||||
pthread_mutex_unlock(&s_q_mtx);
|
||||
|
||||
pthread_join(s_writer_tid, NULL);
|
||||
if (s_reader_tid) pthread_join(s_reader_tid, NULL);
|
||||
|
||||
if (s_bt_fd >= 0) {
|
||||
close(s_bt_fd);
|
||||
@ -277,3 +499,34 @@ void bt_uart_close(void)
|
||||
}
|
||||
s_q_head = s_q_tail = NULL;
|
||||
}
|
||||
|
||||
/* ── Public: identity + rent + intervention ──────────────────────────────── */
|
||||
|
||||
void bt_uart_set_identity(const char *aresx_version, const char *bt_name)
|
||||
{
|
||||
if (aresx_version && *aresx_version)
|
||||
snprintf(s_ares_version, sizeof(s_ares_version), "%s", aresx_version);
|
||||
if (bt_name && *bt_name)
|
||||
snprintf(s_bt_name, sizeof(s_bt_name), "%s", bt_name);
|
||||
printf("[BT] identity: version=%s name=%s\n", s_ares_version, s_bt_name);
|
||||
}
|
||||
|
||||
int bt_uart_get_rent_status(void)
|
||||
{
|
||||
pthread_mutex_lock(&s_rent_mtx);
|
||||
int st = s_rent_status;
|
||||
pthread_mutex_unlock(&s_rent_mtx);
|
||||
return st;
|
||||
}
|
||||
|
||||
void bt_uart_set_rent_status(int status)
|
||||
{
|
||||
pthread_mutex_lock(&s_rent_mtx);
|
||||
s_rent_status = status;
|
||||
pthread_mutex_unlock(&s_rent_mtx);
|
||||
}
|
||||
|
||||
void bt_uart_set_intervention_cb(bt_intervention_fn fn)
|
||||
{
|
||||
s_intervention_cb = fn;
|
||||
}
|
||||
|
||||
507
src/host_stream/can_bus.c
Normal file
507
src/host_stream/can_bus.c
Normal file
@ -0,0 +1,507 @@
|
||||
/*
|
||||
* can_bus.c -- MCP2515 SPI CAN bus driver wrapper for KL630
|
||||
*
|
||||
* API mirrors bt_uart.c: both channels carry the same JSON payload.
|
||||
* BLE channel : bt_uart_send_json(json)
|
||||
* CAN channel : can_bus_send_json(json)
|
||||
*
|
||||
* Both are called from fire_json_async() in event_recorder.c so the
|
||||
* same {"class":"car","level":1} string goes out on both transports.
|
||||
*
|
||||
* MCP2515 is classic CAN (8-byte DLC max). Long JSON strings are split
|
||||
* into sequential 8-byte frames: frame N carries bytes [N*8 .. N*8+7]
|
||||
* of the JSON, zero-padded on the last frame. The receiver reassembles
|
||||
* by concatenating until it sees a null byte (0x00 pad byte).
|
||||
* When a CAN FD controller replaces MCP2515, remove the split loop.
|
||||
*
|
||||
* Hardware: MCP2515 CAN controller on J16 SPI connector
|
||||
* J16 pins : CS / DO / CLK / RESERVED(DI)
|
||||
* MCP2515 : CS / SI / SCK / SO
|
||||
* SPI device : /dev/spidev1.0 (1 MHz SPI clock)
|
||||
* Crystal : 8 MHz
|
||||
* CAN speed : configurable via INI (default 250 kbps)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "mcp2515.h"
|
||||
#include "can_bus.h"
|
||||
|
||||
/* MCP2515 device handle */
|
||||
static mcp2515_dev *s_dev = NULL;
|
||||
static uint32_t s_can_id = 0x100;
|
||||
static int s_can_speed_kbps = 250;
|
||||
static uint32_t s_ctl_can_id = 0x75;
|
||||
|
||||
/* SPI mutex (mcp2515 is not thread-safe) */
|
||||
static pthread_mutex_t s_spi_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* Keep-alive: resend s_keepalive_cmd every 200 ms so the motor controller
|
||||
* never loses the current command (cmd=0 = normal speed, 1/2/3 = violation). */
|
||||
#define CAN_KEEPALIVE_INTERVAL_MS 200
|
||||
|
||||
static volatile uint8_t s_keepalive_cmd = 240; /* SPEED_LEVEL_5: send normal speed from startup */
|
||||
static pthread_mutex_t s_keepalive_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t s_keepalive_tid;
|
||||
static volatile int s_keepalive_running = 0;
|
||||
|
||||
/* Message queue -- same design as bt_uart.c */
|
||||
typedef struct CanMsg {
|
||||
char *json; /* heap-allocated JSON string */
|
||||
struct CanMsg *next;
|
||||
} CanMsg;
|
||||
|
||||
static CanMsg *s_q_head = NULL;
|
||||
static CanMsg *s_q_tail = NULL;
|
||||
static pthread_mutex_t s_q_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t s_q_cond = PTHREAD_COND_INITIALIZER;
|
||||
static pthread_t s_writer_tid;
|
||||
static volatile int s_running = 0;
|
||||
|
||||
/* Forward declaration */
|
||||
static CAN_SPEED kbps_to_enum(int kbps);
|
||||
|
||||
static void can_log_eflg_hints(uint8_t eflg)
|
||||
{
|
||||
if (eflg & EFLG_TXBO) printf("[CAN] hint: TX bus-off (check bitrate/termination/PCAN mode)\n");
|
||||
if (eflg & EFLG_TXEP) printf("[CAN] hint: TX error-passive (ACK likely missing or weak bus)\n");
|
||||
if (eflg & EFLG_TXWAR) printf("[CAN] hint: TX warning level reached\n");
|
||||
if (eflg & EFLG_EWARN) printf("[CAN] hint: error warning flag set\n");
|
||||
}
|
||||
|
||||
static void can_recover_tx_stuck(void)
|
||||
{
|
||||
/* Abort pending retransmissions, clear TXREQ on all TX buffers,
|
||||
* then return to NORMAL mode. */
|
||||
mcp2515_modify_bit(s_dev, MCP_CANCTRL, CANCTRL_ABAT, CANCTRL_ABAT);
|
||||
usleep(1000);
|
||||
mcp2515_modify_bit(s_dev, MCP_TXB0CTRL, TXB_TXREQ, 0);
|
||||
mcp2515_modify_bit(s_dev, MCP_TXB1CTRL, TXB_TXREQ, 0);
|
||||
mcp2515_modify_bit(s_dev, MCP_TXB2CTRL, TXB_TXREQ, 0);
|
||||
mcp2515_modify_bit(s_dev, MCP_CANCTRL, CANCTRL_ABAT, 0);
|
||||
(void)mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL);
|
||||
}
|
||||
|
||||
static int can_reinit_controller_locked(void)
|
||||
{
|
||||
if (mcp2515_initial(s_dev) != 0) return -1;
|
||||
if (mcp2515_can_speed(s_dev, kbps_to_enum(s_can_speed_kbps), MCP_8MHZ) != ERROR_OK) return -2;
|
||||
if (mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL) != ERROR_OK) return -3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int can_loopback_selftest_locked(void)
|
||||
{
|
||||
canid_t rid = 0;
|
||||
uint8_t rdata[8] = {0};
|
||||
uint8_t rlen = 0;
|
||||
uint8_t payload = 0xA5;
|
||||
int i;
|
||||
|
||||
if (mcp2515_setMode(s_dev, CANCTRL_REQOP_LOOPBACK) != ERROR_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_sendMessage(s_dev, 0x321, &payload, 1) != ERROR_OK) {
|
||||
(void)mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL);
|
||||
return -2;
|
||||
}
|
||||
|
||||
for (i = 0; i < 20; i++) {
|
||||
int rc = mcp2515_readMessage(s_dev, &rid, rdata, &rlen);
|
||||
if (rc == ERROR_OK) {
|
||||
(void)mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL);
|
||||
if ((rid & CAN_SFF_MASK) == 0x321 && rlen == 1 && rdata[0] == payload) {
|
||||
return 0;
|
||||
}
|
||||
return -3;
|
||||
}
|
||||
usleep(1000);
|
||||
}
|
||||
|
||||
(void)mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL);
|
||||
return -4;
|
||||
}
|
||||
|
||||
/* Release MCP2515 device and owned SPI path string. */
|
||||
static void free_mcp_device(void)
|
||||
{
|
||||
if (!s_dev) return;
|
||||
if (s_dev->spi_dev) {
|
||||
free(s_dev->spi_dev->spidev_path);
|
||||
free(s_dev->spi_dev);
|
||||
}
|
||||
free(s_dev);
|
||||
s_dev = NULL;
|
||||
}
|
||||
|
||||
/* Map kbps to MCP2515 CAN_SPEED enum */
|
||||
static CAN_SPEED kbps_to_enum(int kbps)
|
||||
{
|
||||
switch (kbps) {
|
||||
case 1000: return CAN_1000KBPS;
|
||||
case 500: return CAN_500KBPS;
|
||||
case 250: return CAN_250KBPS;
|
||||
case 125: return CAN_125KBPS;
|
||||
case 100: return CAN_100KBPS;
|
||||
case 50: return CAN_50KBPS;
|
||||
default:
|
||||
printf("[CAN] unknown speed %d kbps, using 250\n", kbps);
|
||||
return CAN_250KBPS;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Send a JSON string as one or more 8-byte CAN frames.
|
||||
* Frame layout: up to 8 bytes of JSON content, zero-padded on last frame.
|
||||
* Receiver: concatenate until null byte encountered.
|
||||
*/
|
||||
static void send_json_frames(const char *json)
|
||||
{
|
||||
size_t len = strlen(json);
|
||||
size_t off = 0;
|
||||
|
||||
while (off <= len) { /* include the null terminator */
|
||||
uint8_t frame[8] = {0};
|
||||
size_t chunk = (len - off + 1); /* bytes remaining incl. null */
|
||||
if (chunk > 8) chunk = 8;
|
||||
|
||||
memcpy(frame, json + off, chunk); /* zero-fills remainder */
|
||||
|
||||
int rc = ERROR_FAIL;
|
||||
int tries = 0;
|
||||
for (tries = 0; tries < 20; tries++) {
|
||||
pthread_mutex_lock(&s_spi_mtx);
|
||||
rc = mcp2515_sendMessage(s_dev, s_can_id, frame, (uint8_t)chunk);
|
||||
pthread_mutex_unlock(&s_spi_mtx);
|
||||
|
||||
if (rc != ERROR_ALLTXBUSY) break;
|
||||
usleep(2000); /* brief backoff while TX buffers drain */
|
||||
}
|
||||
|
||||
if (rc != ERROR_OK) {
|
||||
if (rc == ERROR_ALLTXBUSY || rc == ERROR_FAILTX) {
|
||||
uint8_t eflg, tec, rec;
|
||||
pthread_mutex_lock(&s_spi_mtx);
|
||||
eflg = mcp2515_read_register_once(s_dev, MCP_EFLG);
|
||||
tec = mcp2515_read_register_once(s_dev, MCP_TEC);
|
||||
rec = mcp2515_read_register_once(s_dev, MCP_REC);
|
||||
can_recover_tx_stuck();
|
||||
pthread_mutex_unlock(&s_spi_mtx);
|
||||
printf("[CAN] sendMessage error %d at offset %zu after %d tries; EFLG=0x%02X TEC=%u REC=%u\n",
|
||||
rc, off, tries + 1, eflg, tec, rec);
|
||||
can_log_eflg_hints(eflg);
|
||||
printf("[CAN] TX recovery: abort/clear TXREQ/re-enter NORMAL done\n");
|
||||
if ((eflg & EFLG_TXBO) || tec >= 128) {
|
||||
int rr;
|
||||
rr = can_reinit_controller_locked();
|
||||
if (rr == 0) {
|
||||
printf("[CAN] controller reinit OK (reset+speed+normal)\n");
|
||||
rr = can_loopback_selftest_locked();
|
||||
if (rr == 0) {
|
||||
printf("[CAN] loopback self-test OK (controller/SPI path healthy)\n");
|
||||
} else {
|
||||
printf("[CAN] loopback self-test failed (%d)\n", rr);
|
||||
}
|
||||
} else {
|
||||
printf("[CAN] controller reinit failed (%d)\n", rr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("[CAN] sendMessage error %d at offset %zu\n", rc, off);
|
||||
}
|
||||
break;
|
||||
}
|
||||
off += chunk;
|
||||
if (chunk < 8) break; /* last (or only) frame sent */
|
||||
}
|
||||
}
|
||||
|
||||
static int send_one_frame_with_retry(uint32_t can_id, const uint8_t *frame, uint8_t dlc)
|
||||
{
|
||||
int rc = ERROR_FAIL;
|
||||
int tries;
|
||||
|
||||
for (tries = 0; tries < 20; tries++) {
|
||||
pthread_mutex_lock(&s_spi_mtx);
|
||||
rc = mcp2515_sendMessage(s_dev, can_id, (uint8_t *)frame, dlc);
|
||||
pthread_mutex_unlock(&s_spi_mtx);
|
||||
|
||||
if (rc != ERROR_ALLTXBUSY) break;
|
||||
usleep(2000);
|
||||
}
|
||||
|
||||
if (rc == ERROR_OK) return ERROR_OK;
|
||||
|
||||
if (rc == ERROR_ALLTXBUSY || rc == ERROR_FAILTX) {
|
||||
uint8_t eflg, tec, rec;
|
||||
pthread_mutex_lock(&s_spi_mtx);
|
||||
eflg = mcp2515_read_register_once(s_dev, MCP_EFLG);
|
||||
tec = mcp2515_read_register_once(s_dev, MCP_TEC);
|
||||
rec = mcp2515_read_register_once(s_dev, MCP_REC);
|
||||
can_recover_tx_stuck();
|
||||
pthread_mutex_unlock(&s_spi_mtx);
|
||||
|
||||
printf("[CAN] sendMessage error %d id=0x%03X after %d tries; EFLG=0x%02X TEC=%u REC=%u\n",
|
||||
rc, can_id, tries + 1, eflg, tec, rec);
|
||||
can_log_eflg_hints(eflg);
|
||||
printf("[CAN] TX recovery: abort/clear TXREQ/re-enter NORMAL done\n");
|
||||
|
||||
if ((eflg & EFLG_TXBO) || tec >= 128) {
|
||||
int rr = can_reinit_controller_locked();
|
||||
if (rr == 0) {
|
||||
printf("[CAN] controller reinit OK (reset+speed+normal)\n");
|
||||
rr = can_loopback_selftest_locked();
|
||||
if (rr == 0) printf("[CAN] loopback self-test OK (controller/SPI path healthy)\n");
|
||||
else printf("[CAN] loopback self-test failed (%d)\n", rr);
|
||||
} else {
|
||||
printf("[CAN] controller reinit failed (%d)\n", rr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("[CAN] sendMessage error %d id=0x%03X\n", rc, can_id);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Writer thread: drains queue, sends CAN frames */
|
||||
static void *can_writer_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (1) {
|
||||
pthread_mutex_lock(&s_q_mtx);
|
||||
while (!s_q_head && s_running)
|
||||
pthread_cond_wait(&s_q_cond, &s_q_mtx);
|
||||
|
||||
if (!s_running && !s_q_head) {
|
||||
pthread_mutex_unlock(&s_q_mtx);
|
||||
break;
|
||||
}
|
||||
|
||||
CanMsg *msg = s_q_head;
|
||||
if (msg) {
|
||||
s_q_head = msg->next;
|
||||
if (!s_q_head) s_q_tail = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&s_q_mtx);
|
||||
|
||||
if (msg && s_dev) {
|
||||
send_json_frames(msg->json);
|
||||
free(msg->json);
|
||||
free(msg);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Keep-alive thread: sends s_keepalive_cmd every CAN_KEEPALIVE_INTERVAL_MS */
|
||||
static void *can_keepalive_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (s_keepalive_running) {
|
||||
usleep(CAN_KEEPALIVE_INTERVAL_MS * 1000);
|
||||
if (!s_keepalive_running) break;
|
||||
|
||||
uint8_t cmd;
|
||||
pthread_mutex_lock(&s_keepalive_mtx);
|
||||
cmd = s_keepalive_cmd;
|
||||
pthread_mutex_unlock(&s_keepalive_mtx);
|
||||
|
||||
if (!s_dev) continue;
|
||||
|
||||
uint8_t frame[8] = {0};
|
||||
frame[0] = cmd;
|
||||
int rc = send_one_frame_with_retry(s_ctl_can_id, frame, 8);
|
||||
if (rc != ERROR_OK)
|
||||
printf("[CAN-KA] keepalive tx failed (cmd=%u rc=%d)\n", cmd, rc);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Public API */
|
||||
|
||||
int can_bus_init(const char *spidev, int can_speed_kbps, uint32_t can_id)
|
||||
{
|
||||
#ifdef SPI_BITBANG
|
||||
/* In bitbang mode the spidev path is unused; GPIO pins come from
|
||||
* compile-time defines SPI_GPIO_CS/MOSI/SCK/MISO set in compile.sh. */
|
||||
const char *path_str = "gpio-bitbang";
|
||||
#else
|
||||
if (!spidev || !*spidev) {
|
||||
printf("[CAN] can_bus_init: no SPI device configured -- CAN disabled\n");
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SPI_BITBANG
|
||||
{
|
||||
const char *path_str = "gpio-bitbang";
|
||||
char *path = strdup(path_str);
|
||||
if (!path) return -1;
|
||||
|
||||
s_dev = new_mcp2515_dev(path);
|
||||
if (!s_dev) {
|
||||
printf("[CAN] new_mcp2515_dev failed\n");
|
||||
free(path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_initial(s_dev) != 0) {
|
||||
printf("[CAN] mcp2515_initial failed -- check SPI wiring on J15\n");
|
||||
free_mcp_device();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_can_speed(s_dev, kbps_to_enum(can_speed_kbps), MCP_8MHZ) != ERROR_OK) {
|
||||
printf("[CAN] mcp2515_can_speed failed\n");
|
||||
free_mcp_device();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL) != ERROR_OK) {
|
||||
printf("[CAN] mcp2515_setMode(NORMAL) failed\n");
|
||||
free_mcp_device();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#else
|
||||
{
|
||||
char *path = strdup(spidev);
|
||||
if (!path) return -1;
|
||||
|
||||
s_dev = new_mcp2515_dev(path);
|
||||
if (!s_dev) {
|
||||
printf("[CAN] new_mcp2515_dev failed\n");
|
||||
free(path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_initial(s_dev) != 0) {
|
||||
printf("[CAN] mcp2515_initial failed\n");
|
||||
free_mcp_device();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_can_speed(s_dev, kbps_to_enum(can_speed_kbps), MCP_8MHZ) != ERROR_OK) {
|
||||
printf("[CAN] mcp2515_can_speed failed\n");
|
||||
free_mcp_device();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL) != ERROR_OK) {
|
||||
printf("[CAN] mcp2515_setMode(NORMAL) failed\n");
|
||||
free_mcp_device();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
s_can_speed_kbps = can_speed_kbps;
|
||||
s_can_id = can_id;
|
||||
|
||||
pthread_mutex_lock(&s_spi_mtx);
|
||||
{
|
||||
int st = can_loopback_selftest_locked();
|
||||
if (st == 0) {
|
||||
printf("[CAN] startup loopback self-test OK\n");
|
||||
} else {
|
||||
printf("[CAN] startup loopback self-test failed (%d)\n", st);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&s_spi_mtx);
|
||||
|
||||
s_running = 1;
|
||||
if (pthread_create(&s_writer_tid, NULL, can_writer_thread, NULL) != 0) {
|
||||
perror("[CAN] pthread_create writer");
|
||||
free_mcp_device();
|
||||
s_running = 0;
|
||||
return -1;
|
||||
}
|
||||
pthread_setname_np(s_writer_tid, "can_writer");
|
||||
|
||||
s_keepalive_running = 1;
|
||||
if (pthread_create(&s_keepalive_tid, NULL, can_keepalive_thread, NULL) != 0) {
|
||||
perror("[CAN] pthread_create keepalive");
|
||||
s_keepalive_running = 0;
|
||||
/* non-fatal: keep-alive not running but CAN still works */
|
||||
} else {
|
||||
pthread_setname_np(s_keepalive_tid, "can_keepalive");
|
||||
}
|
||||
|
||||
printf("[CAN] can_bus_init OK: %s @ %d kbps, CAN ID=0x%03X\n",
|
||||
s_dev->spi_dev->spidev_path, can_speed_kbps, can_id);
|
||||
printf("[CAN] control-frame mode enabled (ID=0x%03X, DLC=8, DATA[0]=cmd)\n", s_ctl_can_id);
|
||||
printf("[CAN] keep-alive thread started: cmd=%d every %d ms\n", s_keepalive_cmd, CAN_KEEPALIVE_INTERVAL_MS);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void can_bus_send_json(const char *json)
|
||||
{
|
||||
static int s_warn_once = 0;
|
||||
(void)json;
|
||||
if (!s_warn_once) {
|
||||
printf("[CAN] can_bus_send_json ignored (control-frame mode active)\n");
|
||||
s_warn_once = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void can_bus_send_control_cmd(uint8_t cmd)
|
||||
{
|
||||
uint8_t frame[8] = {0};
|
||||
int rc;
|
||||
|
||||
/* Update keep-alive so it maintains this cmd until next change */
|
||||
pthread_mutex_lock(&s_keepalive_mtx);
|
||||
s_keepalive_cmd = cmd;
|
||||
pthread_mutex_unlock(&s_keepalive_mtx);
|
||||
|
||||
if (!s_dev || !s_running) {
|
||||
printf("[CAN-CTL] drop: bus not ready (cmd=%u)\n", cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
frame[0] = cmd;
|
||||
rc = send_one_frame_with_retry(s_ctl_can_id, frame, 8);
|
||||
if (rc == ERROR_OK) {
|
||||
printf("[CAN-CTL] tx id=0x%03X dlc=8 data=[0x%02X 00 00 00 00 00 00 00]\n",
|
||||
s_ctl_can_id, cmd);
|
||||
} else {
|
||||
printf("[CAN-CTL] tx failed (cmd=%u rc=%d)\n", cmd, rc);
|
||||
}
|
||||
}
|
||||
|
||||
void can_bus_close(void)
|
||||
{
|
||||
if (!s_running) return;
|
||||
|
||||
s_keepalive_running = 0;
|
||||
if (s_keepalive_tid)
|
||||
pthread_join(s_keepalive_tid, NULL);
|
||||
|
||||
pthread_mutex_lock(&s_q_mtx);
|
||||
s_running = 0;
|
||||
pthread_cond_signal(&s_q_cond);
|
||||
pthread_mutex_unlock(&s_q_mtx);
|
||||
|
||||
pthread_join(s_writer_tid, NULL);
|
||||
|
||||
CanMsg *m = s_q_head;
|
||||
while (m) {
|
||||
CanMsg *next = m->next;
|
||||
free(m->json);
|
||||
free(m);
|
||||
m = next;
|
||||
}
|
||||
s_q_head = s_q_tail = NULL;
|
||||
|
||||
if (s_dev) {
|
||||
free_mcp_device();
|
||||
}
|
||||
printf("[CAN] bus closed\n");
|
||||
}
|
||||
@ -25,6 +25,7 @@
|
||||
#include <pthread.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <netinet/in.h>
|
||||
@ -35,20 +36,58 @@
|
||||
#include <MemBroker/mem_broker.h>
|
||||
#include <vmf/video_snapshot_mechanism.h>
|
||||
#include <vmf/video_source.h>
|
||||
#include <MsgBroker/msg_broker.h>
|
||||
|
||||
#include "event_recorder.h"
|
||||
#include "bt_uart.h"
|
||||
#include "can_bus.h"
|
||||
#include "stdc_post_process.h" /* THR_*_COLLISION constants */
|
||||
|
||||
/* ── External (from kdp2_host_stream.c) ──────────────────────────────────── */
|
||||
extern VMF_VSRC_HANDLE_T *g_ptVsrcHandle;
|
||||
|
||||
/* ── Speed control via MsgBroker ─────────────────────────────────────────── */
|
||||
/* Must be defined before any caller (grass state machine, single-shot). */
|
||||
static int msg_send(const char *fifo,
|
||||
const char *host,
|
||||
const char *cmd,
|
||||
const void *data,
|
||||
unsigned int data_size,
|
||||
int has_response)
|
||||
{
|
||||
MsgContext tMsgCtx;
|
||||
|
||||
/* Speed control has moved to direct CAN control frames.
|
||||
* Avoid MsgBroker FIFO dependency (/tmp/canbus/c0/command.fifo). */
|
||||
if (cmd && strcmp(cmd, "setSpeed") == 0 && data && data_size >= 1) {
|
||||
can_bus_send_control_cmd(*(const uint8_t *)data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!fifo || !host || !cmd)
|
||||
return -1;
|
||||
|
||||
memset(&tMsgCtx, 0, sizeof(tMsgCtx));
|
||||
|
||||
tMsgCtx.bHasResponse = has_response ? 1 : 0;
|
||||
|
||||
tMsgCtx.pszHost = (char *)host;
|
||||
tMsgCtx.dwHostLen = (unsigned int)strlen(host) + 1;
|
||||
|
||||
tMsgCtx.pszCmd = (char *)cmd;
|
||||
tMsgCtx.dwCmdLen = (unsigned int)strlen(cmd) + 1;
|
||||
|
||||
tMsgCtx.pbyData = (unsigned char *)data;
|
||||
tMsgCtx.dwDataSize = data_size;
|
||||
|
||||
return MsgBroker_SendMsg(fifo, &tMsgCtx);
|
||||
}
|
||||
|
||||
/* ── Config ──────────────────────────────────────────────────────────────── */
|
||||
/* Channel A: JSON events → BT UART → iPad (via DX-BT24 BLE module)
|
||||
* Initialized by bt_uart_init() in kp_firmware.c. No local config needed. */
|
||||
|
||||
/* Channel B (tar.gz upload → OOB / cloud path)
|
||||
* Same endpoint as capture JPG upload (golf.cgi), just posting tar.gz.
|
||||
* Simulation: http://192.168.0.114:8081/api/upload
|
||||
* Production: http://192.168.0.99/api/golf.cgi */
|
||||
static char s_up_host[64] = "192.168.0.99";
|
||||
@ -56,10 +95,21 @@ static int s_up_port = 80;
|
||||
static char s_up_path[128] = "/api/golf.cgi";
|
||||
|
||||
static char s_sd_path[256] = "/tmp/sdcard/events";
|
||||
static long long s_sd_max_bytes = (long long)7 * 1024 * 1024 * 1024; /* 7 GB — must be long long on 32-bit ARM */
|
||||
static long long s_sd_max_bytes = (long long)7 * 1024 * 1024 * 1024;
|
||||
static int s_upload_delay_ms = 60000;
|
||||
static int s_enabled = 0;
|
||||
|
||||
/* Speed levels sent via MsgBroker → CAN bus process.
|
||||
* To tune: adjust the numbers here only — no logic changes needed.
|
||||
* SPEED_LEVEL_5 (240) is the normal/idle speed; CAN keepalive sends this every 200ms.
|
||||
* Levels 0~4 are all 10 during testing; restore graduated values for production. */
|
||||
#define SPEED_LEVEL_0 10 /* collision single-shot — testing: set to 10 */
|
||||
#define SPEED_LEVEL_1 10 /* grass L3 (max severity) — testing: set to 10 */
|
||||
#define SPEED_LEVEL_2 10 /* grass L2 — testing: set to 10 */
|
||||
#define SPEED_LEVEL_3 10 /* grass L1 — testing: set to 10 */
|
||||
#define SPEED_LEVEL_4 10 /* grass L1 entry — testing: set to 10 */
|
||||
#define SPEED_LEVEL_5 240 /* normal / grass cleared (full speed) */
|
||||
|
||||
/* ── VMF SNAP ────────────────────────────────────────────────────────────── */
|
||||
#define SNAP_BUF_SIZE (2 * 1024 * 1024)
|
||||
#define SNAP_QP 75
|
||||
@ -107,15 +157,13 @@ typedef struct {
|
||||
static SnapReq g_snap_req;
|
||||
static pthread_mutex_t g_snap_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* ── Single-shot debounce ────────────────────────────────────────────────── */
|
||||
static int g_last_person = 0;
|
||||
static int g_last_bunker = 0;
|
||||
static int g_last_pond = 0;
|
||||
static int g_last_tree = 0;
|
||||
/* ── Collision / alert debounce state ───────────────────────────────────── */
|
||||
static int g_any_col_last = 0; /* 1 if any collision was active last frame */
|
||||
static int g_last_left_alert = 0;
|
||||
static int g_last_right_alert = 0;
|
||||
static int g_last_collision_warning = 0;
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* URL / network helpers (Channel B only)
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
/* *URL / network helpers (Channel B only)**/
|
||||
|
||||
static void parse_url_into(const char *url,
|
||||
char *host, size_t host_n,
|
||||
@ -123,7 +171,6 @@ static void parse_url_into(const char *url,
|
||||
char *path, size_t path_n)
|
||||
{
|
||||
const char *p = url;
|
||||
/* Default to HTTP port 80 when URL does not include :port */
|
||||
if (port) *port = 80;
|
||||
if (strncmp(p, "http://", 7) == 0) p += 7;
|
||||
const char *slash = strchr(p, '/');
|
||||
@ -163,8 +210,7 @@ static int open_socket_to(const char *host, int port)
|
||||
return sock;
|
||||
}
|
||||
|
||||
/* ── Channel B: POST tar.gz via kCurl (same method as golf.cgi JPEG upload) ── */
|
||||
/* kCurl lives in the firmware bin directory — use absolute path so CWD doesn't matter */
|
||||
/* ── Channel B: POST tar.gz via kCurl ───────────────────────────────────── */
|
||||
#define KCURL_PATH "/mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/kCurl"
|
||||
|
||||
static void http_post_file(const char *filepath)
|
||||
@ -172,8 +218,6 @@ static void http_post_file(const char *filepath)
|
||||
const char *basename = strrchr(filepath, '/');
|
||||
basename = basename ? basename + 1 : filepath;
|
||||
|
||||
/* Pass filename as query param so the server can name the file correctly.
|
||||
* Format mirrors golf.cgi usage: kCurl --data-binary @file URL */
|
||||
char url[320];
|
||||
snprintf(url, sizeof(url), "http://%s:%d%s?filename=%s",
|
||||
s_up_host, s_up_port, s_up_path, basename);
|
||||
@ -195,10 +239,8 @@ static void http_post_file(const char *filepath)
|
||||
* Helpers
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* UTC+8 offset for Taiwan time (TZ env may not be set on embedded device) */
|
||||
#define TZ_OFFSET_SEC (8 * 3600)
|
||||
|
||||
/* UTC time with Z suffix — reserved for future use */
|
||||
__attribute__((unused))
|
||||
static void now_iso_utc(char *buf, size_t n)
|
||||
{
|
||||
@ -208,7 +250,6 @@ static void now_iso_utc(char *buf, size_t n)
|
||||
strftime(buf, n, "%Y-%m-%dT%H:%M:%SZ", t);
|
||||
}
|
||||
|
||||
/* Taiwan time (UTC+8) — used in tar.gz filenames and event.json for local readability */
|
||||
static void now_iso(char *buf, size_t n)
|
||||
{
|
||||
struct timeval tv;
|
||||
@ -232,7 +273,6 @@ static void make_work_dir(const char *event_id, char *out, size_t n)
|
||||
mkdir(out, 0755);
|
||||
}
|
||||
|
||||
/* ── Write event.json into work_dir ─────────────────────────────────────── */
|
||||
static void write_event_json(const char *work_dir, const char *event_id,
|
||||
const char *type, int max_level,
|
||||
float duration_sec,
|
||||
@ -260,7 +300,6 @@ static void write_event_json(const char *work_dir, const char *event_id,
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ── Build tar.gz on SD card ─────────────────────────────────────────────── */
|
||||
static void build_targz(const char *work_dir, const char *event_id,
|
||||
char *out_path, size_t out_n)
|
||||
{
|
||||
@ -272,33 +311,25 @@ static void build_targz(const char *work_dir, const char *event_id,
|
||||
strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", t);
|
||||
|
||||
snprintf(out_path, out_n, "%s/event_%s_%s.tar.gz", s_sd_path, event_id, ts);
|
||||
|
||||
/* Ensure SD events dir exists */
|
||||
mkdir(s_sd_path, 0755);
|
||||
|
||||
char cmd[768];
|
||||
/* BusyBox tar: pipe to gzip -c (explicit stdout mode required on some builds).
|
||||
* Run cd in subshell so && pipe chain works correctly.
|
||||
* stderr goes to fw.log so we can see the real error if it fails. */
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"(cd '%s' && tar cf - . 2>>/tmp/fw.log) | gzip -c > '%s'",
|
||||
work_dir, out_path);
|
||||
int sys_rc = system(cmd);
|
||||
|
||||
/* FAT/SD metadata may not be flushed yet — sync before stat */
|
||||
sync();
|
||||
|
||||
/* Verify the file was actually created */
|
||||
struct stat _st;
|
||||
if (stat(out_path, &_st) != 0) {
|
||||
printf("[EVT] tar/gzip failed (shell rc=%d) — file not created\n", sys_rc);
|
||||
out_path[0] = '\0'; /* signal failure to caller */
|
||||
out_path[0] = '\0';
|
||||
return;
|
||||
}
|
||||
printf("[EVT] created %s (%ld bytes)\n", out_path, (long)_st.st_size);
|
||||
}
|
||||
|
||||
/* ── SD card cleanup: delete oldest .tar.gz if total > limit ────────────── */
|
||||
typedef struct { char path[320]; time_t mtime; } FileEntry;
|
||||
|
||||
static int cmp_mtime(const void *a, const void *b)
|
||||
@ -330,7 +361,6 @@ static void sd_cleanup(void)
|
||||
|
||||
if (total <= s_sd_max_bytes) return;
|
||||
|
||||
/* Sort by mtime ascending (oldest first) */
|
||||
qsort(files, count, sizeof(FileEntry), cmp_mtime);
|
||||
for (int i = 0; i < count && total > s_sd_max_bytes; i++) {
|
||||
struct stat st;
|
||||
@ -342,7 +372,6 @@ static void sd_cleanup(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Remove work dir ─────────────────────────────────────────────────────── */
|
||||
static void rm_work_dir(const char *dir)
|
||||
{
|
||||
char cmd[320];
|
||||
@ -350,27 +379,72 @@ static void rm_work_dir(const char *dir)
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* JSON event fire — bt_uart_send_json() is non-blocking (internal queue +
|
||||
* dedicated writer thread), so we call it directly without a wrapper thread.
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
/* *
|
||||
JSON event fire — BLE only (Channel A)
|
||||
**/
|
||||
|
||||
static void fire_json_async(const char *event_id, const char *type, int level)
|
||||
{
|
||||
(void)event_id; /* not included in BT payload — kept for caller compatibility */
|
||||
|
||||
if (!event_id) event_id = "";
|
||||
if (!type) type = "unknown";
|
||||
|
||||
char json[64];
|
||||
/* Compact format fits in a single BLE packet:
|
||||
* e.g. {"class":"lane","level":1} = 26 bytes */
|
||||
snprintf(json, sizeof(json), "{\"class\":\"%s\",\"level\":%d}", type, level);
|
||||
bt_uart_send_json(json); /* enqueues and returns immediately */
|
||||
char date[32];
|
||||
now_iso(date, sizeof(date));
|
||||
|
||||
char json[192];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"response_type\":\"violation\","
|
||||
"\"content\":{\"id\":\"%s\",\"date\":\"%s\",\"type\":\"%s\",\"level\":%d}}",
|
||||
event_id, date, type, level);
|
||||
bt_uart_send_json(json);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Upload thread (build tar.gz → upload → SD cleanup)
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
/* collision_warning notify (§2.2.5) */
|
||||
void fire_collision_warning(int level, const char *type)
|
||||
{
|
||||
char type_str[24];
|
||||
if (level && type && type[0])
|
||||
snprintf(type_str, sizeof(type_str), "\"%s\"", type);
|
||||
else
|
||||
snprintf(type_str, sizeof(type_str), "null");
|
||||
char json[128];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"response_type\":\"collision_warning\","
|
||||
"\"content\":{\"level\":%d,\"type\":%s}}",
|
||||
level, type_str);
|
||||
bt_uart_send_json(json);
|
||||
printf("[EVT] collision_warning level=%d type=%s\n", level, type ? type : "null");
|
||||
}
|
||||
|
||||
/* alert notify (§2.2.4) */
|
||||
static void fire_alert(int left_level, const char *left_type,
|
||||
int right_level, const char *right_type)
|
||||
{
|
||||
char l_str[24], r_str[24];
|
||||
if (left_level && left_type && left_type[0])
|
||||
snprintf(l_str, sizeof(l_str), "\"%s\"", left_type);
|
||||
else
|
||||
snprintf(l_str, sizeof(l_str), "null");
|
||||
if (right_level && right_type && right_type[0])
|
||||
snprintf(r_str, sizeof(r_str), "\"%s\"", right_type);
|
||||
else
|
||||
snprintf(r_str, sizeof(r_str), "null");
|
||||
char json[256];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"response_type\":\"alert\","
|
||||
"\"content\":{"
|
||||
"\"left\":{\"level\":%d,\"type\":%s},"
|
||||
"\"right\":{\"level\":%d,\"type\":%s}}}",
|
||||
left_level, l_str, right_level, r_str);
|
||||
bt_uart_send_json(json);
|
||||
printf("[EVT] alert left=%d(%s) right=%d(%s)\n",
|
||||
left_level, left_type ? left_type : "null",
|
||||
right_level, right_type ? right_type : "null");
|
||||
}
|
||||
|
||||
/**
|
||||
Upload thread (build tar.gz → upload → SD cleanup)
|
||||
**/
|
||||
|
||||
typedef struct {
|
||||
char work_dir[256];
|
||||
@ -395,31 +469,24 @@ static void *upload_thread(void *arg)
|
||||
usleep((useconds_t)a->delay_ms * 1000);
|
||||
}
|
||||
|
||||
/* Write event.json */
|
||||
printf("[EVT] upload: writing event.json to %s\n", a->work_dir);
|
||||
write_event_json(a->work_dir, a->event_id, a->event_type,
|
||||
a->max_level, a->duration_sec,
|
||||
(const char (*)[64])a->images, a->image_count);
|
||||
|
||||
/* Build tar.gz on SD card */
|
||||
printf("[EVT] upload: building tar.gz\n");
|
||||
char tgz_path[384];
|
||||
build_targz(a->work_dir, a->event_id, tgz_path, sizeof(tgz_path));
|
||||
|
||||
/* Upload Channel B */
|
||||
if (tgz_path[0]) {
|
||||
printf("[EVT] upload: posting %s to %s:%d%s\n",
|
||||
tgz_path, s_up_host, s_up_port, s_up_path);
|
||||
http_post_file(tgz_path);
|
||||
}
|
||||
|
||||
/* SD card cleanup */
|
||||
sd_cleanup();
|
||||
|
||||
/* Cleanup temp work dir */
|
||||
rm_work_dir(a->work_dir);
|
||||
|
||||
/* Signal grass upload done */
|
||||
pthread_mutex_lock(&g_grass_mtx);
|
||||
if (g_grass.upload_busy) {
|
||||
g_grass.upload_busy = 0;
|
||||
@ -476,7 +543,7 @@ static int snap_lazy_init(void)
|
||||
VMF_SNAP_INITOPT_T opt;
|
||||
memset(&opt, 0, sizeof(opt));
|
||||
opt.pszOutPinPrefix = "vsrc_ssm";
|
||||
opt.dwStreamIdx = 0; /* stream0 = 1920×1080 */
|
||||
opt.dwStreamIdx = 0;
|
||||
opt.pVsrcHandle = g_ptVsrcHandle;
|
||||
opt.dwQp = SNAP_QP;
|
||||
s_snap = VMF_SNAP_Init(&opt);
|
||||
@ -488,7 +555,6 @@ static int snap_lazy_init(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Save JPEG from snap buffer to file ──────────────────────────────────── */
|
||||
static void save_jpeg(const char *path, const uint8_t *buf, int size)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
@ -532,7 +598,6 @@ static void grass_enter_level(int level)
|
||||
now_iso(ts, sizeof(ts));
|
||||
printf("[EVT] grass Level %d id=%s\n", level, g_grass.event_id);
|
||||
|
||||
/* spec: type = "lane" (超出車道邊界), not "grass" */
|
||||
fire_json_async(g_grass.event_id, "lane", level);
|
||||
|
||||
char filename[32];
|
||||
@ -541,10 +606,7 @@ static void grass_enter_level(int level)
|
||||
g_grass.event_id, "grass", level, 0 /* not immediate */);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Public API
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* * Public API**/
|
||||
void event_recorder_init(const char *upload_url,
|
||||
const char *sd_path,
|
||||
int sd_max_mb,
|
||||
@ -567,24 +629,17 @@ void event_recorder_init(const char *upload_url,
|
||||
s_sd_path, sd_max_mb, s_upload_delay_ms);
|
||||
}
|
||||
|
||||
/* ── Recv callback: drives state machine ─────────────────────────────────── */
|
||||
/* Recv callback: drives state machine */
|
||||
void event_recorder_update(const stdc_analysis_t *ana)
|
||||
{
|
||||
#if 1
|
||||
if (!s_enabled) return;
|
||||
|
||||
/*
|
||||
* Grass trigger rules (per spec: 超出車道邊界 = type "lane"):
|
||||
* - Initial entry into L1 requires on_grass AND is_moving
|
||||
* (prevents false trigger if cart was parked on grass at startup)
|
||||
* - Once in L1/L2/L3, only on_grass is checked for hysteresis exit
|
||||
* (is_moving may flicker while driving slowly → don't let it break the timer)
|
||||
* - Level timers count wall-clock from t_l1 regardless of brief gaps
|
||||
* - GRASS_EXIT_HYSTERESIS_MS of sustained !on_grass required to end event
|
||||
*/
|
||||
int on_grass = ana->on_grass;
|
||||
int grass_trigger = on_grass && ana->is_moving;
|
||||
uint8_t speed_val = 0;
|
||||
|
||||
/* ── Grass state machine ─────────────────────────────────────────────── */
|
||||
/* Grass state machine */
|
||||
pthread_mutex_lock(&g_grass_mtx);
|
||||
|
||||
switch (g_grass.state) {
|
||||
@ -601,6 +656,9 @@ void event_recorder_update(const stdc_analysis_t *ana)
|
||||
g_grass.state = GRASS_L1;
|
||||
pthread_mutex_unlock(&g_grass_mtx);
|
||||
grass_enter_level(1);
|
||||
speed_val = SPEED_LEVEL_4;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@ -613,10 +671,16 @@ void event_recorder_update(const stdc_analysis_t *ana)
|
||||
g_grass.state = GRASS_L2;
|
||||
pthread_mutex_unlock(&g_grass_mtx);
|
||||
grass_enter_level(2);
|
||||
speed_val = SPEED_LEVEL_3;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
return;
|
||||
}
|
||||
} else if (elapsed_ms_tv(&g_grass.t_last_active) >= GRASS_EXIT_HYSTERESIS_MS) {
|
||||
/* Sustained absence — event ends */
|
||||
} else {
|
||||
speed_val = SPEED_LEVEL_5;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
if (elapsed_ms_tv(&g_grass.t_last_active) >= GRASS_EXIT_HYSTERESIS_MS) {
|
||||
g_grass.state = GRASS_DONE;
|
||||
gettimeofday(&g_grass.t_done, NULL);
|
||||
g_grass.upload_busy = 1;
|
||||
@ -628,7 +692,7 @@ void event_recorder_update(const stdc_analysis_t *ana)
|
||||
g_grass.max_level, dur, imgs, 1, s_upload_delay_ms);
|
||||
return;
|
||||
}
|
||||
/* else: still within hysteresis window — keep L1, timer keeps running */
|
||||
}
|
||||
break;
|
||||
|
||||
case GRASS_L2:
|
||||
@ -639,9 +703,16 @@ void event_recorder_update(const stdc_analysis_t *ana)
|
||||
g_grass.state = GRASS_L3;
|
||||
pthread_mutex_unlock(&g_grass_mtx);
|
||||
grass_enter_level(3);
|
||||
speed_val = SPEED_LEVEL_2;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
return;
|
||||
}
|
||||
} else if (elapsed_ms_tv(&g_grass.t_last_active) >= GRASS_EXIT_HYSTERESIS_MS) {
|
||||
} else {
|
||||
speed_val = SPEED_LEVEL_5;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
if (elapsed_ms_tv(&g_grass.t_last_active) >= GRASS_EXIT_HYSTERESIS_MS) {
|
||||
g_grass.state = GRASS_DONE;
|
||||
gettimeofday(&g_grass.t_done, NULL);
|
||||
g_grass.upload_busy = 1;
|
||||
@ -653,12 +724,20 @@ void event_recorder_update(const stdc_analysis_t *ana)
|
||||
g_grass.max_level, dur, imgs, 2, s_upload_delay_ms);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case GRASS_L3:
|
||||
if (on_grass) {
|
||||
gettimeofday(&g_grass.t_last_active, NULL);
|
||||
} else if (elapsed_ms_tv(&g_grass.t_last_active) >= GRASS_EXIT_HYSTERESIS_MS) {
|
||||
speed_val = SPEED_LEVEL_1;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
} else {
|
||||
speed_val = SPEED_LEVEL_5;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
if (elapsed_ms_tv(&g_grass.t_last_active) >= GRASS_EXIT_HYSTERESIS_MS) {
|
||||
g_grass.state = GRASS_DONE;
|
||||
gettimeofday(&g_grass.t_done, NULL);
|
||||
g_grass.upload_busy = 1;
|
||||
@ -670,47 +749,110 @@ void event_recorder_update(const stdc_analysis_t *ana)
|
||||
g_grass.max_level, dur, imgs, 3, s_upload_delay_ms);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case GRASS_DONE:
|
||||
/* Upload thread will reset to IDLE when done */
|
||||
/* Upload thread will reset to IDLE when done.
|
||||
* Keep sending SPEED_LEVEL_5 every frame until upload finishes
|
||||
* (keepalive in can_bus.c also maintains this every 200ms). */
|
||||
speed_val = SPEED_LEVEL_5;
|
||||
msg_send("/tmp/canbus/c0/command.fifo", "host_stream", "setSpeed",
|
||||
&speed_val, sizeof(speed_val), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
int grass_was_idle = (g_grass.state == GRASS_IDLE);
|
||||
pthread_mutex_unlock(&g_grass_mtx);
|
||||
|
||||
/* ── Single-shot events (Collision ROI) ─────────────────────────────── */
|
||||
/*
|
||||
* Trigger only when the hazard appears inside the collision ROI
|
||||
* (centre 25%–75% × 25%–70% of the frame), not from global class ratios.
|
||||
* Only fire on rising edge (0→1 transition) to avoid repeated triggers.
|
||||
*/
|
||||
struct { int cur; int *last; const char *type; } singles[] = {
|
||||
{ ana->col_person_ratio >= THR_PERSON_COLLISION, &g_last_person, "person" },
|
||||
{ ana->col_bunker_ratio >= THR_BUNKER_COLLISION, &g_last_bunker, "bunker" },
|
||||
{ ana->col_pond_ratio >= THR_POND_COLLISION, &g_last_pond, "pond" },
|
||||
{ ana->col_tree_ratio >= THR_TREE_COLLISION, &g_last_tree, "tree" },
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (singles[i].cur && !*singles[i].last) {
|
||||
const char *type = singles[i].type;
|
||||
|
||||
char ev_id[32];
|
||||
struct timeval now;
|
||||
gettimeofday(&now, NULL);
|
||||
/* Include type in id to avoid work_dir collision with grass events at same second */
|
||||
snprintf(ev_id, sizeof(ev_id), "%ld_%s", now.tv_sec, type);
|
||||
|
||||
char work_dir[256];
|
||||
make_work_dir(ev_id, work_dir, sizeof(work_dir));
|
||||
|
||||
printf("[EVT] single-shot: %s id=%s\n", type, ev_id);
|
||||
fire_json_async(ev_id, type, 1);
|
||||
request_snap("snapshot.jpg", work_dir, ev_id, type, 1, 1 /* immediate */);
|
||||
/* collision_warning (2.2.5)*/
|
||||
int col_now = ana->collision_risk;
|
||||
if (col_now != g_last_collision_warning) {
|
||||
const char *col_type = NULL;
|
||||
if (ana->col_person_ratio >= THR_PERSON_COLLISION) col_type = "person";
|
||||
else if (ana->col_car_ratio >= THR_CAR_COLLISION) col_type = "vehicle";
|
||||
else if (ana->col_tree_ratio >= THR_TREE_COLLISION) col_type = "tree";
|
||||
else if (ana->col_pond_ratio >= THR_POND_COLLISION) col_type = "water_hazard";
|
||||
else if (ana->col_bunker_ratio >= THR_BUNKER_COLLISION) col_type = "bush";
|
||||
fire_collision_warning(col_now, col_now ? col_type : NULL);
|
||||
g_last_collision_warning = col_now;
|
||||
}
|
||||
*singles[i].last = singles[i].cur;
|
||||
|
||||
/*alert (2.2.4)*/
|
||||
int left_now = ana->left_alert;
|
||||
int right_now = ana->right_alert;
|
||||
if (left_now != g_last_left_alert || right_now != g_last_right_alert) {
|
||||
fire_alert(left_now, left_now ? ana->left_type : NULL,
|
||||
right_now, right_now ? ana->right_type : NULL);
|
||||
g_last_left_alert = left_now;
|
||||
g_last_right_alert = right_now;
|
||||
}
|
||||
|
||||
g_any_col_last = col_now;
|
||||
#endif
|
||||
#if 0
|
||||
/* ── [TEST] violation(2.2.2)靜態測試向量 ──────────────────────────
|
||||
* 每隔 3 秒依序送出 level 1 → 2 → 3 → 0,模擬一次完整違規事件週期:
|
||||
* level 1:違規發生
|
||||
* level 2:違規持續 6 秒
|
||||
* level 3:違規持續 10 秒
|
||||
* level 0:違規解除
|
||||
* 啟用:把 #if 0 改成 #if 1
|
||||
* ------------------------------------------------------------------ */
|
||||
{
|
||||
static int s_viol_tick = 0;
|
||||
static int s_viol_phase = 0;
|
||||
static int s_viol_last = -1;
|
||||
static char s_viol_id[24] = "";
|
||||
|
||||
s_viol_tick++;
|
||||
if (s_viol_tick >= 75) { /* 約 3 秒(25 fps × 3)*/
|
||||
s_viol_tick = 0;
|
||||
s_viol_phase = (s_viol_phase + 1) % 4;
|
||||
}
|
||||
|
||||
if (s_viol_phase != s_viol_last) {
|
||||
s_viol_last = s_viol_phase;
|
||||
if (s_viol_phase == 0)
|
||||
snprintf(s_viol_id, sizeof(s_viol_id), "%ld", (long)time(NULL));
|
||||
static const int levels[] = {1, 2, 3, 0};
|
||||
int lvl = levels[s_viol_phase];
|
||||
fire_json_async(s_viol_id, "lane", lvl);
|
||||
printf("[TEST] violation id=%s level=%d\n", s_viol_id, lvl);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if 0
|
||||
/* ── [TEST] alert(2.2.4)靜態測試向量 ──────────────────────────────
|
||||
* 每隔 5 秒輪流送出四種組合,驗證 iPad 端能正確顯示:
|
||||
* 0: 左右都無危險
|
||||
* 1: 左側 tree,右側無危險
|
||||
* 2: 左側無危險,右側 person
|
||||
* 3: 左右都有危險(tree / vehicle)
|
||||
* 啟用:把 #if 0 改成 #if 1
|
||||
* ------------------------------------------------------------------ */
|
||||
{
|
||||
static int s_test_tick = 0;
|
||||
static int s_test_phase = 0;
|
||||
static int s_last_phase = -1;
|
||||
|
||||
s_test_tick++;
|
||||
if (s_test_tick >= 125) { /* 約 5 秒(25 fps × 5)*/
|
||||
s_test_tick = 0;
|
||||
s_test_phase = (s_test_phase + 1) % 4;
|
||||
}
|
||||
|
||||
if (s_test_phase != s_last_phase) {
|
||||
s_last_phase = s_test_phase;
|
||||
switch (s_test_phase) {
|
||||
case 0: fire_alert(0, NULL, 0, NULL); printf("[TEST] alert: left=0 right=0\n"); break;
|
||||
case 1: fire_alert(1, "tree", 0, NULL); printf("[TEST] alert: left=tree right=0\n"); break;
|
||||
case 2: fire_alert(0, NULL, 1, "person"); printf("[TEST] alert: left=0 right=person\n"); break;
|
||||
case 3: fire_alert(1, "tree", 1, "vehicle");printf("[TEST] alert: left=tree right=vehicle\n"); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── Send callback: take VMF_SNAP if one is pending ─────────────────────── */
|
||||
@ -723,7 +865,6 @@ void event_recorder_provide_frame(void)
|
||||
pthread_mutex_unlock(&g_snap_mtx);
|
||||
return;
|
||||
}
|
||||
/* Copy request locally and clear the flag */
|
||||
SnapReq req = g_snap_req;
|
||||
g_snap_req.active = 0;
|
||||
pthread_mutex_unlock(&g_snap_mtx);
|
||||
@ -747,8 +888,6 @@ void event_recorder_provide_frame(void)
|
||||
}
|
||||
MemBroker_FreeMemory(jpeg);
|
||||
|
||||
/* Single-shot: launch upload immediately (delay_ms = 0).
|
||||
* Always upload even if snap failed — event.json still carries the record. */
|
||||
if (req.immediate_upload) {
|
||||
const char imgs[4][64] = { "snapshot.jpg", "", "", "" };
|
||||
int image_count = snap_ok ? 1 : 0;
|
||||
|
||||
136
src/host_stream/gpio_devmem.c
Normal file
136
src/host_stream/gpio_devmem.c
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* gpio_devmem.c — Direct GPIO control via /dev/mem for KL630
|
||||
*
|
||||
* KL630 GPIO_C register base: 0x402E0000
|
||||
* J15 pinout (GPIOC_0 pins 1-4):
|
||||
* Pin 1 (CS) = GPIO1 = GPIOC_0_IO_DATA_1
|
||||
* Pin 2 (MOSI) = GPIO2 = GPIOC_0_IO_DATA_2
|
||||
* Pin 3 (SCK) = GPIO3 = GPIOC_0_IO_DATA_3
|
||||
* Pin 4 (MISO) = GPIO4 = GPIOC_0_IO_DATA_4
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gpio_devmem.h"
|
||||
|
||||
/* KL630 GPIO_C base address */
|
||||
#define GPIO_C_BASE 0x402E0000
|
||||
#define PAGE_SIZE 4096
|
||||
|
||||
/* GPIO register offsets */
|
||||
#define GPIOD_PSR_OFFSET 0x0000 /* Port Status Register (read-only) */
|
||||
#define GPIOD_PDDR_OFFSET 0x0004 /* Port Data Direction Register */
|
||||
#define GPIOD_PSOR_OFFSET 0x0008 /* Port Set Output Register */
|
||||
#define GPIOD_PCOR_OFFSET 0x000C /* Port Clear Output Register */
|
||||
#define GPIOD_PTOR_OFFSET 0x0010 /* Port Toggle Output Register */
|
||||
#define GPIOD_PIDR_OFFSET 0x0014 /* Port Input Disable Register */
|
||||
|
||||
static int s_devmem_fd = -1;
|
||||
static uint32_t *s_gpio_base = NULL;
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────── */
|
||||
int gpio_devmem_init(void)
|
||||
{
|
||||
if (s_gpio_base != NULL) return 0; /* already initialized */
|
||||
|
||||
s_devmem_fd = open("/dev/mem", O_RDWR | O_SYNC);
|
||||
if (s_devmem_fd < 0) {
|
||||
perror("[GPIO] open /dev/mem");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Map GPIO_C registers into user space */
|
||||
s_gpio_base = (uint32_t *)mmap(
|
||||
NULL,
|
||||
PAGE_SIZE,
|
||||
PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED,
|
||||
s_devmem_fd,
|
||||
GPIO_C_BASE & ~(PAGE_SIZE - 1) /* align to page boundary */
|
||||
);
|
||||
|
||||
if (s_gpio_base == MAP_FAILED) {
|
||||
perror("[GPIO] mmap /dev/mem");
|
||||
close(s_devmem_fd);
|
||||
s_devmem_fd = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Adjust for offset within page */
|
||||
s_gpio_base = (uint32_t *)((uintptr_t)s_gpio_base + (GPIO_C_BASE & (PAGE_SIZE - 1)));
|
||||
|
||||
printf("[GPIO] devmem init OK: GPIO_C at 0x%X, mapped to %p\n", GPIO_C_BASE, s_gpio_base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────── */
|
||||
void gpio_devmem_cleanup(void)
|
||||
{
|
||||
if (s_gpio_base != NULL) {
|
||||
s_gpio_base = (uint32_t *)((uintptr_t)s_gpio_base - (GPIO_C_BASE & (PAGE_SIZE - 1)));
|
||||
munmap(s_gpio_base, PAGE_SIZE);
|
||||
s_gpio_base = NULL;
|
||||
}
|
||||
if (s_devmem_fd >= 0) {
|
||||
close(s_devmem_fd);
|
||||
s_devmem_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────── */
|
||||
int gpio_devmem_set_direction(int gpio, int output)
|
||||
{
|
||||
if (!s_gpio_base || gpio < 1 || gpio > 4) return -1;
|
||||
|
||||
volatile uint32_t *pddr = (volatile uint32_t *)
|
||||
((uintptr_t)s_gpio_base + GPIOD_PDDR_OFFSET);
|
||||
|
||||
if (output) {
|
||||
/* Set bit for output */
|
||||
*pddr |= (1U << gpio);
|
||||
} else {
|
||||
/* Clear bit for input */
|
||||
*pddr &= ~(1U << gpio);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────── */
|
||||
int gpio_devmem_set(int gpio, int value)
|
||||
{
|
||||
if (!s_gpio_base || gpio < 1 || gpio > 4) return -1;
|
||||
|
||||
volatile uint32_t *psor = (volatile uint32_t *)
|
||||
((uintptr_t)s_gpio_base + GPIOD_PSOR_OFFSET);
|
||||
volatile uint32_t *pcor = (volatile uint32_t *)
|
||||
((uintptr_t)s_gpio_base + GPIOD_PCOR_OFFSET);
|
||||
|
||||
if (value) {
|
||||
/* Write 1 to PSOR to set output high */
|
||||
*psor = (1U << gpio);
|
||||
} else {
|
||||
/* Write 1 to PCOR to set output low */
|
||||
*pcor = (1U << gpio);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────── */
|
||||
int gpio_devmem_get(int gpio)
|
||||
{
|
||||
if (!s_gpio_base || gpio < 1 || gpio > 4) return -1;
|
||||
|
||||
volatile uint32_t *psr = (volatile uint32_t *)
|
||||
((uintptr_t)s_gpio_base + GPIOD_PSR_OFFSET);
|
||||
|
||||
uint32_t val = *psr;
|
||||
return (val >> gpio) & 1;
|
||||
}
|
||||
204
src/host_stream/handshake.c
Normal file
204
src/host_stream/handshake.c
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* handshake.c — BLE Challenge-Response Authentication implementation
|
||||
*
|
||||
* Key: SHA256("29C310F5")[0..15] XOR 0x5A
|
||||
* Challenge: R = 4-byte timestamp (big-endian) + 12 random bytes
|
||||
* Verify: base64(SHA256(R || Key)) == received_response
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "sha256.h"
|
||||
#include "base64.h"
|
||||
#include "handshake.h"
|
||||
|
||||
/* Timestamp when challenge was sent (for timeout check) */
|
||||
static time_t s_challenge_send_time = 0;
|
||||
|
||||
/* ── Key derivation ─────────────────────────────────────────────────────── */
|
||||
|
||||
void handshake_derive_key(uint8_t out_key[HANDSHAKE_KEY_LEN])
|
||||
{
|
||||
uint8_t digest[32];
|
||||
sha256((const uint8_t *)HANDSHAKE_KN_STR, strlen(HANDSHAKE_KN_STR), digest);
|
||||
for (int i = 0; i < HANDSHAKE_KEY_LEN; i++)
|
||||
out_key[i] = digest[i] ^ 0x5A;
|
||||
}
|
||||
|
||||
void handshake_print_key(const uint8_t key[HANDSHAKE_KEY_LEN])
|
||||
{
|
||||
for (int i = 0; i < HANDSHAKE_KEY_LEN; i++)
|
||||
printf("%02x%s", key[i], (i < HANDSHAKE_KEY_LEN - 1) ? " " : "\n");
|
||||
}
|
||||
|
||||
/* ── Init / Reset ────────────────────────────────────────────────────────── */
|
||||
|
||||
void handshake_init(handshake_ctx_t *ctx)
|
||||
{
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
ctx->state = HS_STATE_IDLE;
|
||||
handshake_derive_key(ctx->key);
|
||||
}
|
||||
|
||||
void handshake_reset(handshake_ctx_t *ctx)
|
||||
{
|
||||
ctx->state = HS_STATE_IDLE;
|
||||
memset(ctx->R, 0, sizeof(ctx->R));
|
||||
s_challenge_send_time = 0;
|
||||
}
|
||||
|
||||
handshake_state_t handshake_get_state(const handshake_ctx_t *ctx)
|
||||
{
|
||||
return ctx->state;
|
||||
}
|
||||
|
||||
int handshake_check_timeout(handshake_ctx_t *ctx)
|
||||
{
|
||||
if (ctx->state != HS_STATE_WAIT_RESPONSE)
|
||||
return 0;
|
||||
return (time(NULL) - s_challenge_send_time >= HANDSHAKE_TIMEOUT_SEC) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ── Challenge generation ────────────────────────────────────────────────── */
|
||||
|
||||
int handshake_build_challenge(handshake_ctx_t *ctx,
|
||||
char *out_json_buf, size_t buf_size)
|
||||
{
|
||||
#if 0 /* ── TEST VECTOR (Protocol 5.3): change to #if 0 to restore random R ── */
|
||||
/* R = 6a4636a0a1b2c3d4e5f60718293a4b5c
|
||||
* Expected challenge: akY2oKGyw9Tl9gcYKTpLXA==
|
||||
* Expected response: NISSISwhM59eitBMS/stTnJMSLIU7YWZG7bd+NkQBAU= */
|
||||
static const uint8_t TEST_R[HANDSHAKE_R_LEN] = {
|
||||
0x6a, 0x46, 0x36, 0xa0, 0xa1, 0xb2, 0xc3, 0xd4,
|
||||
0xe5, 0xf6, 0x07, 0x18, 0x29, 0x3a, 0x4b, 0x5c
|
||||
};
|
||||
memcpy(ctx->R, TEST_R, HANDSHAKE_R_LEN);
|
||||
time_t now = time(NULL); /* still needed for s_challenge_send_time */
|
||||
#else /* ── Normal operation: random R ────────────────────────────────────── */
|
||||
/* R[0..3] = current time big-endian */
|
||||
time_t now = time(NULL);
|
||||
ctx->R[0] = (uint8_t)((now >> 24) & 0xFF);
|
||||
ctx->R[1] = (uint8_t)((now >> 16) & 0xFF);
|
||||
ctx->R[2] = (uint8_t)((now >> 8) & 0xFF);
|
||||
ctx->R[3] = (uint8_t)( now & 0xFF);
|
||||
|
||||
/* R[4..15] = 12 random bytes from /dev/urandom */
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
if (read(fd, ctx->R + 4, 12) != 12) {
|
||||
/* partial read fallback */
|
||||
for (int i = 4; i < 16; i++) ctx->R[i] ^= (uint8_t)(rand() ^ i);
|
||||
}
|
||||
close(fd);
|
||||
} else {
|
||||
/* /dev/urandom unavailable — simple fallback */
|
||||
srand((unsigned int)now);
|
||||
for (int i = 4; i < 16; i++)
|
||||
ctx->R[i] = (uint8_t)(rand() ^ i ^ (int)now);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Base64-encode R (16 bytes → 24 chars + NUL) */
|
||||
char r_b64[28]; /* ceil(16/3)*4+1 = 25 bytes, round up */
|
||||
base64_encode(ctx->R, HANDSHAKE_R_LEN, r_b64);
|
||||
|
||||
/* Build JSON */
|
||||
int n = snprintf(out_json_buf, buf_size,
|
||||
"{\"response_type\":\"handshake_challenge\","
|
||||
"\"content\":{\"challenge\":\"%s\"}}",
|
||||
r_b64);
|
||||
if (n < 0 || (size_t)n >= buf_size)
|
||||
return -1;
|
||||
|
||||
/* Record send time and advance state */
|
||||
s_challenge_send_time = now;
|
||||
ctx->state = HS_STATE_WAIT_RESPONSE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Response validation ─────────────────────────────────────────────────── */
|
||||
|
||||
int handshake_process_response(handshake_ctx_t *ctx, const char *json_str)
|
||||
{
|
||||
/* 1. Timeout check */
|
||||
if (handshake_check_timeout(ctx)) {
|
||||
ctx->state = HS_STATE_FAILED;
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* 2. Parse "response" field value from JSON
|
||||
* Looks for: "response":"<base64>" */
|
||||
const char *p = strstr(json_str, "\"response\"");
|
||||
if (!p) return -1;
|
||||
p = strchr(p, ':');
|
||||
if (!p) return -1;
|
||||
/* skip whitespace and colon */
|
||||
while (*p == ':' || *p == ' ' || *p == '\t') p++;
|
||||
if (*p != '"') return -1;
|
||||
p++; /* skip opening quote */
|
||||
const char *end = strchr(p, '"');
|
||||
if (!end) return -1;
|
||||
size_t val_len = (size_t)(end - p);
|
||||
if (val_len == 0 || val_len > 64) return -1;
|
||||
|
||||
char received_b64[68];
|
||||
memcpy(received_b64, p, val_len);
|
||||
received_b64[val_len] = '\0';
|
||||
|
||||
/* 3. Compute expected = base64(SHA256(R || Key)) */
|
||||
uint8_t input[HANDSHAKE_R_LEN + HANDSHAKE_KEY_LEN]; /* 32 bytes */
|
||||
memcpy(input, ctx->R, HANDSHAKE_R_LEN);
|
||||
memcpy(input + HANDSHAKE_R_LEN, ctx->key, HANDSHAKE_KEY_LEN);
|
||||
|
||||
uint8_t digest[32];
|
||||
sha256(input, HANDSHAKE_R_LEN + HANDSHAKE_KEY_LEN, digest);
|
||||
|
||||
char expected_b64[48]; /* 32 bytes → 44 chars + NUL */
|
||||
base64_encode(digest, 32, expected_b64);
|
||||
|
||||
printf("[HS DEBUG] R : ");
|
||||
for (int i = 0; i < HANDSHAKE_R_LEN; i++) printf("%02x", ctx->R[i]);
|
||||
printf("\n");
|
||||
|
||||
printf("[HS DEBUG] Key : ");
|
||||
for (int i = 0; i < HANDSHAKE_KEY_LEN; i++) printf("%02x", ctx->key[i]);
|
||||
printf("\n");
|
||||
|
||||
printf("[HS DEBUG] expected : %s\n", expected_b64);
|
||||
printf("[HS DEBUG] received : %s\n", received_b64);
|
||||
|
||||
/* 4. Constant-time compare (simple for embedded) */
|
||||
if (strcmp(received_b64, expected_b64) == 0) {
|
||||
ctx->state = HS_STATE_SUCCESS;
|
||||
return 1;
|
||||
}
|
||||
|
||||
ctx->state = HS_STATE_FAILED;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Confirm message ─────────────────────────────────────────────────────── */
|
||||
|
||||
int handshake_build_confirm(handshake_ctx_t *ctx, int success,
|
||||
const char *reason_str,
|
||||
char *out_json_buf, size_t buf_size)
|
||||
{
|
||||
(void)ctx;
|
||||
if (success) {
|
||||
snprintf(out_json_buf, buf_size,
|
||||
"{\"response_type\":\"handshake_confirm\","
|
||||
"\"content\":{\"status\":\"success\"}}");
|
||||
} else {
|
||||
const char *reason = reason_str ? reason_str : "invalid_response";
|
||||
snprintf(out_json_buf, buf_size,
|
||||
"{\"response_type\":\"handshake_confirm\","
|
||||
"\"content\":{\"status\":\"failed\",\"reason\":\"%s\"}}",
|
||||
reason);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
72
src/host_stream/handshake.h
Normal file
72
src/host_stream/handshake.h
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* handshake.h — BLE Challenge-Response Authentication Protocol
|
||||
*
|
||||
* Protocol flow:
|
||||
* 1. Device sends: {"response_type":"handshake_challenge","content":{"challenge":"<base64(R)>"}}
|
||||
* 2. iPad computes: SHA256(R || Key) and replies with base64 of the digest
|
||||
* 3. Device verifies and sends: {"response_type":"handshake_confirm","content":{"status":"success"|"failed"}}
|
||||
*
|
||||
* Key derivation: SHA256("29C310F5") → first 16 bytes XOR 0x5A
|
||||
*/
|
||||
#ifndef HANDSHAKE_H
|
||||
#define HANDSHAKE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define HANDSHAKE_KEY_LEN 16
|
||||
#define HANDSHAKE_KN_STR "29C310F5"//"48872dc21ede" /* test vector; restore to "29C310F5" after verification */
|
||||
#define HANDSHAKE_R_LEN 16
|
||||
#define HANDSHAKE_TIMEOUT_SEC 5
|
||||
|
||||
typedef enum {
|
||||
HS_STATE_IDLE = 0,
|
||||
HS_STATE_WAIT_RESPONSE,
|
||||
HS_STATE_SUCCESS,
|
||||
HS_STATE_FAILED
|
||||
} handshake_state_t;
|
||||
|
||||
typedef struct {
|
||||
handshake_state_t state;
|
||||
uint8_t key[HANDSHAKE_KEY_LEN];
|
||||
uint8_t R[HANDSHAKE_R_LEN];
|
||||
} handshake_ctx_t;
|
||||
|
||||
/* Derive the 16-byte handshake key: SHA256(HANDSHAKE_KN_STR)[0..15] XOR 0x5A */
|
||||
void handshake_derive_key(uint8_t out_key[HANDSHAKE_KEY_LEN]);
|
||||
|
||||
/* Print key bytes to stdout as space-separated hex (e.g., "58 48 c8 ff ...") */
|
||||
void handshake_print_key(const uint8_t key[HANDSHAKE_KEY_LEN]);
|
||||
|
||||
/* Initialise ctx: derive key, set state=IDLE, zero R */
|
||||
void handshake_init(handshake_ctx_t *ctx);
|
||||
|
||||
/* Generate random R, set state=WAIT_RESPONSE, write JSON to out_json_buf.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int handshake_build_challenge(handshake_ctx_t *ctx,
|
||||
char *out_json_buf, size_t buf_size);
|
||||
|
||||
/* Validate iPad response JSON (must contain "response" field with base64 value).
|
||||
* Returns 1: success (state→SUCCESS)
|
||||
* 0: wrong answer (state→FAILED)
|
||||
* -1: malformed JSON
|
||||
* -2: timeout */
|
||||
int handshake_process_response(handshake_ctx_t *ctx, const char *json_str);
|
||||
|
||||
/* Build confirm JSON. success=1 → {"status":"success"}; success=0 → {"status":"failed","reason":"..."}
|
||||
* reason_str may be NULL (defaults to "invalid_response").
|
||||
* Returns 0. */
|
||||
int handshake_build_confirm(handshake_ctx_t *ctx, int success,
|
||||
const char *reason_str,
|
||||
char *out_json_buf, size_t buf_size);
|
||||
|
||||
/* Reset state to IDLE, zero R (key is retained) */
|
||||
void handshake_reset(handshake_ctx_t *ctx);
|
||||
|
||||
/* Return current state */
|
||||
handshake_state_t handshake_get_state(const handshake_ctx_t *ctx);
|
||||
|
||||
/* Return 1 if WAIT_RESPONSE and timeout expired, else 0 */
|
||||
int handshake_check_timeout(handshake_ctx_t *ctx);
|
||||
|
||||
#endif /* HANDSHAKE_H */
|
||||
@ -440,8 +440,8 @@ static int init_video_source(HOST_STREAM_INIT_OPT_T* pHostStreamInit)
|
||||
}
|
||||
} else {
|
||||
/* FEC passthrough (no lens correction). Load FEC config for gFecDefValue defaults only.
|
||||
* Pass NULL for ptFrontConfig — skips lens curve node loading (not needed for passthrough).
|
||||
* Do NOT set ptFecConfig here — SDK sets only eFecMethod=GTR (below) for this case. */
|
||||
* Pass NULL for ptFrontConfig ??skips lens curve node loading (not needed for passthrough).
|
||||
* Do NOT set ptFecConfig here ??SDK sets only eFecMethod=GTR (below) for this case. */
|
||||
if(loadFECConfig(pHostStreamInit, NULL) == -1){
|
||||
printf("[%s] No fec config file, using defaults\n", __func__);
|
||||
}
|
||||
@ -534,15 +534,24 @@ void set_data_to_yuv(unsigned char* pucYBuff, unsigned int dwWidth, unsigned int
|
||||
dwOffsetU = dwWidth * dwHeight + (((tDrawPoint.dwY >> 1) * dwWidth) >> 1) + (tDrawPoint.dwX >> 1);
|
||||
dwOffsetV = dwOffsetU + dwPlaneSize ;
|
||||
|
||||
if(iColor){
|
||||
pucYBuff[dwOffsetY] = 0x00;
|
||||
pucYBuff[dwOffsetU] = 0x00;
|
||||
pucYBuff[dwOffsetV] = 0xFF;
|
||||
}else{
|
||||
pucYBuff[dwOffsetY] = 0xFF;
|
||||
pucYBuff[dwOffsetU] = 0x00;
|
||||
pucYBuff[dwOffsetV] = 0x00;
|
||||
}
|
||||
/* YCbCr colour table indexed by YOLO class number (BT.601 full-range).
|
||||
* class=0 person ??green (Y=149, Cb= 43, Cr= 21)
|
||||
* class=1 (other) ??white (Y=235, Cb=128, Cr=128)
|
||||
* class=2 vehicle ??red (Y= 76, Cb= 84, Cr=255)
|
||||
* fallback ??white */
|
||||
static const unsigned char CLASS_YUV[][3] = {
|
||||
{149, 43, 21}, /* 0: person ??green */
|
||||
{235, 128, 128}, /* 1: other ??white */
|
||||
{ 76, 84, 255}, /* 2: vehicle ??red */
|
||||
};
|
||||
const unsigned char *yuv;
|
||||
if (iColor >= 0 && iColor < (int)(sizeof(CLASS_YUV)/sizeof(CLASS_YUV[0])))
|
||||
yuv = CLASS_YUV[iColor];
|
||||
else
|
||||
yuv = CLASS_YUV[1]; /* default white */
|
||||
pucYBuff[dwOffsetY] = yuv[0];
|
||||
pucYBuff[dwOffsetU] = yuv[1];
|
||||
pucYBuff[dwOffsetV] = yuv[2];
|
||||
|
||||
}
|
||||
|
||||
@ -613,6 +622,10 @@ void draw_rect(YUV_BUFF_INFO_T* yuvBuffInfo, DETECT_INFO *ptDetInfo, int iColor)
|
||||
int iLineEndX = 0, iLineEndY = 0;
|
||||
int iStartX = 0, iStartY = 0, iWidth = 0, iHeight = 0;
|
||||
|
||||
/* Use class number as colour index so each class gets a distinct colour.
|
||||
* iColor parameter is kept for API compatibility but ignored. */
|
||||
iColor = (int)ptDetInfo->dwClass;
|
||||
|
||||
iStartX = (int)ptDetInfo->dwStartX;
|
||||
iStartY = (int)ptDetInfo->dwStartY;
|
||||
iWidth = (int)ptDetInfo->dwWidth;
|
||||
@ -656,7 +669,7 @@ void draw_rect(YUV_BUFF_INFO_T* yuvBuffInfo, DETECT_INFO *ptDetInfo, int iColor)
|
||||
* Per-class YCbCr colours for STDC segmentation overlay (BT.601 full-range).
|
||||
* Order matches STDC_CLASS_* indices:
|
||||
* 0=bunker 1=car 2=grass 3=greenery 4=person 5=pond 6=road 7=tree
|
||||
* road (class 6) is left transparent — it occupies most of the frame and adds
|
||||
* road (class 6) is left transparent ??it occupies most of the frame and adds
|
||||
* visual noise rather than information.
|
||||
*/
|
||||
static const uint8_t s_stdc_yuv[STDC_NUM_CLASSES][3] = {
|
||||
@ -666,7 +679,7 @@ static const uint8_t s_stdc_yuv[STDC_NUM_CLASSES][3] = {
|
||||
{137, 104, 55}, /* greenery #22c55e */
|
||||
{144, 59, 203}, /* person #f97316 */
|
||||
{154, 182, 87}, /* pond #60a5fa */
|
||||
{ 0, 0, 0}, /* road — transparent (unused) */
|
||||
{ 0, 0, 0}, /* road ??transparent (unused) */
|
||||
{ 88, 113, 80}, /* tree #15803d */
|
||||
};
|
||||
|
||||
@ -697,7 +710,7 @@ static void stdc_paint_seg_overlay(unsigned char *base_buf,
|
||||
uint32_t y_stride = info->dwYStride;
|
||||
uint32_t uv_stride = info->dwYStride >> 1;
|
||||
|
||||
/* Pre-compute column mapping: output x → seg col index */
|
||||
/* Pre-compute column mapping: output x ??seg col index */
|
||||
uint8_t col_lut[1920];
|
||||
uint32_t fx_max = (frame_w < 1920) ? frame_w : 1920;
|
||||
for (uint32_t fx = 0; fx < fx_max; fx++)
|
||||
@ -708,7 +721,7 @@ static void stdc_paint_seg_overlay(unsigned char *base_buf,
|
||||
const uint8_t *seg_row = local_map + seg_r * seg_w;
|
||||
uint8_t *y_row = y_buf + fy * y_stride;
|
||||
|
||||
/* Y plane — every pixel */
|
||||
/* Y plane ??every pixel */
|
||||
for (uint32_t fx = 0; fx < fx_max; fx++) {
|
||||
uint8_t cls = seg_row[col_lut[fx]];
|
||||
if (cls == STDC_CLASS_ROAD || cls >= STDC_NUM_CLASSES) continue;
|
||||
@ -716,7 +729,7 @@ static void stdc_paint_seg_overlay(unsigned char *base_buf,
|
||||
y_row[fx] = (uint8_t)(((uint16_t)y_row[fx] + col[0]) >> 1);
|
||||
}
|
||||
|
||||
/* UV plane — every other row, every other column */
|
||||
/* UV plane ??every other row, every other column */
|
||||
if ((fy & 1) == 0) {
|
||||
uint8_t *cb_row = cb_buf + (fy >> 1) * uv_stride;
|
||||
uint8_t *cr_row = cr_buf + (fy >> 1) * uv_stride;
|
||||
@ -1530,8 +1543,8 @@ void *kdp2_host_voc_thread(void *arg)
|
||||
if (vsrc_ssm_info.dwOffset[0] != 0 && vsrc_ssm_info.dwOffset[1] != 0) {
|
||||
abuf[q_idx].apdwData[0] = ssm_buf[q_idx].buffer + vsrc_ssm_info.dwOffset[0];
|
||||
abuf[q_idx].apdwData[1] = ssm_buf[q_idx].buffer + vsrc_ssm_info.dwOffset[1];
|
||||
/* NV12 (semi-planar): offset[2]==0 means no separate Cr plane → NULL.
|
||||
* YM12 (planar): offset[2]!=0 → Cr plane pointer. */
|
||||
/* NV12 (semi-planar): offset[2]==0 means no separate Cr plane ??NULL.
|
||||
* YM12 (planar): offset[2]!=0 ??Cr plane pointer. */
|
||||
abuf[q_idx].apdwData[2] = vsrc_ssm_info.dwOffset[2]
|
||||
? (ssm_buf[q_idx].buffer + vsrc_ssm_info.dwOffset[2]) : NULL;
|
||||
} else {
|
||||
@ -1659,7 +1672,7 @@ void *kdp2_host_stream_image_thread(void *arg)
|
||||
* VOC thread unblocks on g_dwInitBind and immediately checks g_dwDrawBoxType
|
||||
* to decide which SSM pin to read from. If draw box is enabled on stream 0,
|
||||
* it must see g_dwDrawBoxType=1 so it reads from VENC_VSRC_B_PIN (overlay
|
||||
* output) rather than the raw ISP SSM — fixes intermittent HDMI no-overlay. */
|
||||
* output) rather than the raw ISP SSM ??fixes intermittent HDMI no-overlay. */
|
||||
if (pHostStreamInit->bDrawBoxEnable && pHostStreamInit->dwEncodeStreamCount > 0)
|
||||
g_dwDrawBoxType = 1;
|
||||
g_dwInitBind = 1;
|
||||
|
||||
@ -42,6 +42,8 @@
|
||||
#include "fec_api.h"
|
||||
#include "event_recorder.h"
|
||||
#include "bt_uart.h"
|
||||
#include "can_bus.h"
|
||||
#include "handshake.h"
|
||||
|
||||
//fifo queue buffer setting
|
||||
#define IMAGE_BUFFER_COUNT 3
|
||||
@ -227,14 +229,75 @@ int loadConfig(HOST_STREAM_INIT_OPT_T* pHostStreamInit)
|
||||
int ev_enable = iniparser_getint(ini, "event:enable", 0);
|
||||
const char *bt_dev = iniparser_getstring(ini, "event:bt_uart_dev", "/dev/ttyS1");
|
||||
int bt_at_probe = iniparser_getint(ini, "event:bt_at_probe", 0);
|
||||
const char *aresx_ver = iniparser_getstring(ini, "event:aresx_version", "1.0.0");
|
||||
const char *ev_up = iniparser_getstring(ini, "event:upload_url", "http://192.168.0.114:8081/api/upload");
|
||||
const char *ev_sd = iniparser_getstring(ini, "event:sd_path", "/tmp/sdcard/events");
|
||||
int ev_max_mb = iniparser_getint(ini, "event:sd_max_mb", 7168);
|
||||
int ev_delay = iniparser_getint(ini, "event:upload_delay_ms", 60000);
|
||||
|
||||
/* device_id (event:device_id): raw Kn chip number string, e.g. "0xCE7B562C".
|
||||
* bt_name (event:bt_name): BLE broadcast name derived from kn, e.g. "AresX-562C".
|
||||
*
|
||||
* First-boot (device_id empty): get kn, build both strings, mark need_ini_save.
|
||||
* AT-probe flow (bt_at_probe==1): bt_set_module_name runs inside bt_uart_init();
|
||||
* after it returns, persist bt_name + device_id and set bt_at_probe=0. */
|
||||
const char *device_id_ini = iniparser_getstring(ini, "event:device_id", "");
|
||||
char device_id[32]; /* raw kn number, e.g. "0xCE7B562C" */
|
||||
char bt_name[32]; /* derived BLE name, e.g. "AresX-562C" */
|
||||
int need_ini_save = 0;
|
||||
|
||||
if (!device_id_ini || !*device_id_ini) {
|
||||
uint32_t kn = VMF_NNM_Get_Kn_Number();
|
||||
snprintf(device_id, sizeof(device_id), "0x%08X", kn);
|
||||
snprintf(bt_name, sizeof(bt_name), "AresX-%04X", (unsigned)(kn & 0xFFFF));
|
||||
printf("[BT] auto device_id=%s bt_name=%s\n", device_id, bt_name);
|
||||
iniparser_set(ini, "event:device_id", device_id);
|
||||
iniparser_set(ini, "event:bt_name", bt_name);
|
||||
need_ini_save = 1;
|
||||
} else {
|
||||
snprintf(device_id, sizeof(device_id), "%s", device_id_ini);
|
||||
const char *bt_name_ini = iniparser_getstring(ini, "event:bt_name", "BT-24");
|
||||
snprintf(bt_name, sizeof(bt_name), "%s", bt_name_ini);
|
||||
printf("[BT] device_id=%s bt_name=%s (from INI)\n", device_id, bt_name);
|
||||
}
|
||||
|
||||
/* Order matters: set_identity BEFORE init so s_bt_name is ready
|
||||
* when bt_uart_probe_and_upgrade() calls bt_set_module_name(). */
|
||||
bt_uart_set_identity(aresx_ver, bt_name);
|
||||
bt_uart_init(bt_dev, bt_at_probe);
|
||||
|
||||
/* After AT probe+upgrade: persist bt_name, disable at_probe for next boot. */
|
||||
if (bt_at_probe) {
|
||||
iniparser_set(ini, "event:bt_name", bt_name);
|
||||
iniparser_set(ini, "event:bt_at_probe", "0");
|
||||
need_ini_save = 1;
|
||||
}
|
||||
|
||||
if (need_ini_save) {
|
||||
FILE *fini = fopen(HOST_STREAM_CONFIG_PATH, "w");
|
||||
if (fini) {
|
||||
iniparser_dump_ini(ini, fini);
|
||||
fclose(fini);
|
||||
printf("[BT] INI persisted: bt_name=%s device_id=%s bt_at_probe=%d\n",
|
||||
bt_name, device_id, bt_at_probe ? 0 : bt_at_probe);
|
||||
} else {
|
||||
printf("[BT] WARNING: cannot write INI back (%s)\n", HOST_STREAM_CONFIG_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
event_recorder_init(ev_up, ev_sd, ev_max_mb, ev_delay, ev_enable);
|
||||
}
|
||||
|
||||
/* --- [can] section: MCP2515 CAN bus via J15 SPI connector --- */
|
||||
{
|
||||
int can_enable = iniparser_getint(ini, "can:enable", 0);
|
||||
const char *can_spidev = iniparser_getstring(ini, "can:spidev", "/dev/spidev1.0");
|
||||
int can_speed = iniparser_getint(ini, "can:speed_kbps", 250);
|
||||
int can_id_raw = iniparser_getint(ini, "can:can_id", 0x100);
|
||||
if (can_enable)
|
||||
can_bus_init(can_spidev, can_speed, (uint32_t)can_id_raw);
|
||||
}
|
||||
|
||||
iniparser_freedict(ini);
|
||||
return 0;
|
||||
}
|
||||
@ -379,14 +442,24 @@ int main (int argc, char* argv[])
|
||||
pthread_t thread_f = 0;
|
||||
|
||||
VMF_NNM_Get_Version(&major, &minor, &patch, &build);
|
||||
uint32_t kn_number = VMF_NNM_Get_Kn_Number();
|
||||
|
||||
/* Derive and print BLE handshake key for startup verification.
|
||||
* Expected: 58 48 c8 ff b2 eb b1 b8 69 92 b5 20 48 8d 1d 61 */
|
||||
{
|
||||
uint8_t hs_key[HANDSHAKE_KEY_LEN];
|
||||
handshake_derive_key(hs_key);
|
||||
printf("[INIT] handshake key: ");
|
||||
handshake_print_key(hs_key);
|
||||
}
|
||||
|
||||
printf("\n\n**********************************************************\n");
|
||||
printf("Kneron Firmware\n");
|
||||
printf("Ver. %d.%d.%d.%d\n", major, minor, patch, build);
|
||||
printf("Build Time: %s %s\n", __DATE__, __TIME__);
|
||||
printf("Kn Number = 0x%08x\n",kn_number);
|
||||
printf("**********************************************************\n");
|
||||
printf("HOST STREAM mode \n");
|
||||
|
||||
//pthread_t task_infcb_handle; //kmdw_inference_result_handler_callback_thread;
|
||||
pthread_t task_inf_data_handle; //VMF_NNM_Inference_Image_Dispatcher_Thread;
|
||||
pthread_t task_stream_image_handle; // <-> kdp2_host_stream_image_thread; //pthread_t task_usb_recv_handle; // <-> kdp2_usb_companion_image_thread;
|
||||
|
||||
1174
src/host_stream/mcp2515.c
Normal file
1174
src/host_stream/mcp2515.c
Normal file
File diff suppressed because it is too large
Load Diff
159
src/host_stream/sha256.h
Normal file
159
src/host_stream/sha256.h
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* sha256.h — Standalone RFC 6234 SHA-256, header-only (static inline).
|
||||
* No external dependencies beyond <stdint.h> and <string.h>.
|
||||
*
|
||||
* Test vector:
|
||||
* sha256("abc", 3, d) →
|
||||
* ba7816bf 8f01cfea 414140de 5dae2ec7 3b00361b bde327b6 0b82c10f 46850c58
|
||||
*/
|
||||
#ifndef SHA256_H
|
||||
#define SHA256_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t state[8];
|
||||
uint64_t count; /* total bits processed */
|
||||
uint8_t buf[64]; /* partial block */
|
||||
} sha256_ctx;
|
||||
|
||||
/* ── Round constants (first 32 bits of cube roots of first 64 primes) ── */
|
||||
static const uint32_t SHA256_K[64] = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
||||
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
||||
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
||||
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
||||
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
||||
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
};
|
||||
|
||||
/* ── Bit operations ── */
|
||||
#define SHA256_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||||
#define SHA256_CH(e, f, g) (((e) & (f)) ^ (~(e) & (g)))
|
||||
#define SHA256_MAJ(a, b, c) (((a) & (b)) ^ ((a) & (c)) ^ ((b) & (c)))
|
||||
#define SHA256_EP0(a) (SHA256_ROTR(a, 2) ^ SHA256_ROTR(a, 13) ^ SHA256_ROTR(a, 22))
|
||||
#define SHA256_EP1(e) (SHA256_ROTR(e, 6) ^ SHA256_ROTR(e, 11) ^ SHA256_ROTR(e, 25))
|
||||
#define SHA256_SIG0(w)(SHA256_ROTR(w, 7) ^ SHA256_ROTR(w, 18) ^ ((w) >> 3))
|
||||
#define SHA256_SIG1(w)(SHA256_ROTR(w, 17) ^ SHA256_ROTR(w, 19) ^ ((w) >> 10))
|
||||
|
||||
/* ── Process one 64-byte block ── */
|
||||
static inline void sha256_transform(sha256_ctx *ctx, const uint8_t blk[64])
|
||||
{
|
||||
uint32_t w[64];
|
||||
uint32_t a, b, c, d, e, f, g, h, t1, t2;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 16; i++)
|
||||
w[i] = ((uint32_t)blk[i*4 ] << 24) | ((uint32_t)blk[i*4+1] << 16)
|
||||
| ((uint32_t)blk[i*4+2] << 8) | (uint32_t)blk[i*4+3];
|
||||
for (i = 16; i < 64; i++)
|
||||
w[i] = SHA256_SIG1(w[i-2]) + w[i-7] + SHA256_SIG0(w[i-15]) + w[i-16];
|
||||
|
||||
a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3];
|
||||
e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7];
|
||||
|
||||
for (i = 0; i < 64; i++) {
|
||||
t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + SHA256_K[i] + w[i];
|
||||
t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c);
|
||||
h = g; g = f; f = e; e = d + t1;
|
||||
d = c; c = b; b = a; a = t1 + t2;
|
||||
}
|
||||
|
||||
ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d;
|
||||
ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h;
|
||||
}
|
||||
|
||||
/* ── Public API ── */
|
||||
static inline void sha256_init(sha256_ctx *ctx)
|
||||
{
|
||||
ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85;
|
||||
ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a;
|
||||
ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c;
|
||||
ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19;
|
||||
ctx->count = 0;
|
||||
memset(ctx->buf, 0, sizeof(ctx->buf));
|
||||
}
|
||||
|
||||
static inline void sha256_update(sha256_ctx *ctx, const uint8_t *data, size_t len)
|
||||
{
|
||||
/* bytes already in partial block */
|
||||
size_t used = (size_t)((ctx->count / 8) % 64);
|
||||
ctx->count += (uint64_t)len * 8;
|
||||
|
||||
if (used) {
|
||||
size_t fill = 64 - used;
|
||||
if (len < fill) {
|
||||
memcpy(ctx->buf + used, data, len);
|
||||
return;
|
||||
}
|
||||
memcpy(ctx->buf + used, data, fill);
|
||||
sha256_transform(ctx, ctx->buf);
|
||||
data += fill;
|
||||
len -= fill;
|
||||
}
|
||||
|
||||
while (len >= 64) {
|
||||
sha256_transform(ctx, data);
|
||||
data += 64;
|
||||
len -= 64;
|
||||
}
|
||||
|
||||
if (len)
|
||||
memcpy(ctx->buf, data, len);
|
||||
}
|
||||
|
||||
static inline void sha256_final(sha256_ctx *ctx, uint8_t digest[32])
|
||||
{
|
||||
/* Padding: append 0x80, zeros, then 64-bit big-endian bit count */
|
||||
uint64_t bits = ctx->count;
|
||||
size_t used = (size_t)((ctx->count / 8) % 64);
|
||||
uint8_t pad[64];
|
||||
int i;
|
||||
|
||||
memset(pad, 0, sizeof(pad));
|
||||
pad[0] = 0x80;
|
||||
|
||||
if (used < 56) {
|
||||
/* Enough room in current block: pad to byte 56 */
|
||||
sha256_update(ctx, pad, 56 - used);
|
||||
} else {
|
||||
/* Need an extra block: fill to end of current, then 56 zero bytes */
|
||||
sha256_update(ctx, pad, 64 - used + 56);
|
||||
}
|
||||
|
||||
/* Append 64-bit bit-length (big-endian) */
|
||||
for (i = 0; i < 8; i++)
|
||||
pad[i] = (uint8_t)(bits >> (56 - 8 * i));
|
||||
sha256_update(ctx, pad, 8);
|
||||
|
||||
/* Extract digest from state */
|
||||
for (i = 0; i < 8; i++) {
|
||||
digest[i*4 + 0] = (uint8_t)(ctx->state[i] >> 24);
|
||||
digest[i*4 + 1] = (uint8_t)(ctx->state[i] >> 16);
|
||||
digest[i*4 + 2] = (uint8_t)(ctx->state[i] >> 8);
|
||||
digest[i*4 + 3] = (uint8_t)(ctx->state[i] );
|
||||
}
|
||||
}
|
||||
|
||||
/* One-shot convenience: init + update + final */
|
||||
static inline void sha256(const uint8_t *data, size_t len, uint8_t digest[32])
|
||||
{
|
||||
sha256_ctx ctx;
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, data, len);
|
||||
sha256_final(&ctx, digest);
|
||||
}
|
||||
|
||||
#endif /* SHA256_H */
|
||||
@ -141,15 +141,12 @@ int stdc_post_process(int model_id, struct kdp_image_s *image_p)
|
||||
uint32_t fwd_total = 0, fwd_grass = 0, fwd_tree = 0;
|
||||
uint32_t col_total = 0;
|
||||
uint32_t col_count[STDC_NUM_CLASSES] = {0};
|
||||
uint32_t left_total = 0, right_total = 0;
|
||||
uint32_t left_count[STDC_NUM_CLASSES] = {0};
|
||||
uint32_t right_count[STDC_NUM_CLASSES] = {0};
|
||||
float motion_diff;
|
||||
uint8_t is_moving = 1;
|
||||
|
||||
/* Collision ROI boundaries in pixel coords */
|
||||
uint32_t col_r0 = (uint32_t)(COL_ROI_LEFT * num_col);
|
||||
uint32_t col_r1 = (uint32_t)(COL_ROI_RIGHT * num_col);
|
||||
uint32_t col_c0 = (uint32_t)(COL_ROI_TOP * num_row);
|
||||
uint32_t col_c1 = (uint32_t)(COL_ROI_BOTTOM * num_row);
|
||||
|
||||
motion_diff = stdc_compute_motion_diff(image_p, &is_moving);
|
||||
|
||||
for (uint32_t r = 0; r < num_row; r++) {
|
||||
@ -172,8 +169,10 @@ int stdc_post_process(int model_id, struct kdp_image_s *image_p)
|
||||
if (seg_idx < STDC_SEG_MAP_MAX)
|
||||
result->seg_map[seg_idx] = (uint8_t)best_cls;
|
||||
|
||||
/* Forward ROI */
|
||||
if (stdc_is_in_forward_roi(r, c, num_row, num_col)) {
|
||||
int in_trapezoid_roi = stdc_is_in_forward_roi(r, c, num_row, num_col);
|
||||
|
||||
/* Forward ROI (web trapezoid) */
|
||||
if (in_trapezoid_roi) {
|
||||
fwd_total++;
|
||||
if (best_cls == STDC_CLASS_GRASS)
|
||||
fwd_grass++;
|
||||
@ -181,11 +180,22 @@ int stdc_post_process(int model_id, struct kdp_image_s *image_p)
|
||||
fwd_tree++;
|
||||
}
|
||||
|
||||
/* Collision ROI */
|
||||
if (r >= col_c0 && r < col_c1 && c >= col_r0 && c < col_r1) {
|
||||
/* Collision ROI: align with web stream trapezoid ROI */
|
||||
if (in_trapezoid_roi) {
|
||||
col_total++;
|
||||
col_count[best_cls]++;
|
||||
}
|
||||
|
||||
/* Left ROI: x < 25% of image width */
|
||||
if (c < (uint32_t)((float)num_col * 0.25f)) {
|
||||
left_total++;
|
||||
left_count[best_cls]++;
|
||||
}
|
||||
/* Right ROI: x > 75% of image width */
|
||||
if (c > (uint32_t)((float)num_col * 0.75f)) {
|
||||
right_total++;
|
||||
right_count[best_cls]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,6 +229,67 @@ int stdc_post_process(int model_id, struct kdp_image_s *image_p)
|
||||
ana->col_bunker_ratio = (float)col_count[STDC_CLASS_BUNKER] * inv_col;
|
||||
ana->col_pond_ratio = (float)col_count[STDC_CLASS_POND] * inv_col;
|
||||
|
||||
/* Left / Right ROI ratios */
|
||||
float inv_left = (left_total > 0) ? 1.0f / (float)left_total : 0.0f;
|
||||
float inv_right = (right_total > 0) ? 1.0f / (float)right_total : 0.0f;
|
||||
ana->left_person_ratio = (float)left_count[STDC_CLASS_PERSON] * inv_left;
|
||||
ana->left_car_ratio = (float)left_count[STDC_CLASS_CAR] * inv_left;
|
||||
ana->left_tree_ratio = (float)left_count[STDC_CLASS_TREE] * inv_left;
|
||||
ana->left_pond_ratio = (float)left_count[STDC_CLASS_POND] * inv_left;
|
||||
ana->left_bunker_ratio = (float)left_count[STDC_CLASS_BUNKER] * inv_left;
|
||||
|
||||
ana->right_person_ratio = (float)right_count[STDC_CLASS_PERSON] * inv_right;
|
||||
ana->right_car_ratio = (float)right_count[STDC_CLASS_CAR] * inv_right;
|
||||
ana->right_tree_ratio = (float)right_count[STDC_CLASS_TREE] * inv_right;
|
||||
ana->right_pond_ratio = (float)right_count[STDC_CLASS_POND] * inv_right;
|
||||
ana->right_bunker_ratio = (float)right_count[STDC_CLASS_BUNKER] * inv_right;
|
||||
|
||||
/* Left alert: highest-ratio class that exceeds its collision threshold */
|
||||
ana->left_alert = 0;
|
||||
ana->left_type[0] = '\0';
|
||||
{
|
||||
struct { float ratio; float thr; const char *name; } lc[] = {
|
||||
{ ana->left_person_ratio, THR_PERSON_COLLISION, "person" },
|
||||
{ ana->left_car_ratio, THR_CAR_COLLISION, "vehicle" },
|
||||
{ ana->left_tree_ratio, THR_TREE_COLLISION, "tree" },
|
||||
{ ana->left_pond_ratio, THR_POND_COLLISION, "water_hazard" },
|
||||
{ ana->left_bunker_ratio, THR_BUNKER_COLLISION, "bush" },
|
||||
};
|
||||
float best = 0.0f;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (lc[i].ratio >= lc[i].thr) {
|
||||
ana->left_alert = 1;
|
||||
if (lc[i].ratio > best) {
|
||||
best = lc[i].ratio;
|
||||
snprintf(ana->left_type, sizeof(ana->left_type), "%s", lc[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Right alert: same logic */
|
||||
ana->right_alert = 0;
|
||||
ana->right_type[0] = '\0';
|
||||
{
|
||||
struct { float ratio; float thr; const char *name; } rc[] = {
|
||||
{ ana->right_person_ratio, THR_PERSON_COLLISION, "person" },
|
||||
{ ana->right_car_ratio, THR_CAR_COLLISION, "vehicle" },
|
||||
{ ana->right_tree_ratio, THR_TREE_COLLISION, "tree" },
|
||||
{ ana->right_pond_ratio, THR_POND_COLLISION, "water_hazard" },
|
||||
{ ana->right_bunker_ratio, THR_BUNKER_COLLISION, "bush" },
|
||||
};
|
||||
float best = 0.0f;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (rc[i].ratio >= rc[i].thr) {
|
||||
ana->right_alert = 1;
|
||||
if (rc[i].ratio > best) {
|
||||
best = rc[i].ratio;
|
||||
snprintf(ana->right_type, sizeof(ana->right_type), "%s", rc[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
* 3. Warning flags
|
||||
* ------------------------------------------------------- */
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
HOST_URL="http://192.168.0.114:8080"
|
||||
HOST_URL="http://192.168.0.105:8080"
|
||||
BIN_DIR=/mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin
|
||||
FW=/mnt/flash/vienna/kp_firmware_host_stream
|
||||
INI=$BIN_DIR/ini/host_stream.ini
|
||||
|
||||
@ -6,6 +6,10 @@ Two independent channels:
|
||||
Channel A (iPad / BLE path): POST /api/event → real-time violation JSON
|
||||
Channel B (OOB / Cloud path): POST /api/upload → tar.gz event archive
|
||||
|
||||
Channel C (CAN bus):
|
||||
POST /api/can/send → send a CAN frame via SocketCAN (PCAN-USB FD)
|
||||
GET /api/can/status → {available, channel, bitrate, last_error}
|
||||
|
||||
Plus:
|
||||
GET /api/time → provides UTC time to KL630 (no NTP needed)
|
||||
GET / → web dashboard
|
||||
@ -21,6 +25,299 @@ import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
# ── CAN bus support (python-can, optional) ───────────────────────────────────
|
||||
try:
|
||||
import can
|
||||
_CAN_AVAILABLE = True
|
||||
except ImportError:
|
||||
_CAN_AVAILABLE = False
|
||||
|
||||
# Event type → CAN Data[0] mapping (must match can_bus.h on KL630)
|
||||
_CAN_EVT = {
|
||||
'boot': 0x00,
|
||||
'road': 0x01,
|
||||
'grass': 0x02,
|
||||
'car': 0x03,
|
||||
'person': 0x04,
|
||||
'pond': 0x05,
|
||||
'bunker': 0x06,
|
||||
'tree': 0x07,
|
||||
'hazard': 0x08,
|
||||
}
|
||||
|
||||
class CanBus:
|
||||
"""Thread-safe SocketCAN wrapper around python-can Bus.
|
||||
|
||||
Sends CAN frames (test buttons) AND receives frames from the KL630
|
||||
device, reassembling multi-frame JSON strings and appending them to
|
||||
the shared events list (same as Channel A).
|
||||
"""
|
||||
DEFAULT_CHANNEL = 'can0'
|
||||
DEFAULT_BITRATE = 250000 # 250 kbps — must match device INI
|
||||
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._bus = None
|
||||
self._channel = self.DEFAULT_CHANNEL
|
||||
self._bitrate = self.DEFAULT_BITRATE
|
||||
self._last_error = ''
|
||||
self._available = False
|
||||
self._rx_thread = None
|
||||
self._rx_buf = {} # per-arbitration-id reassembly buffer
|
||||
self._rx_count = 0
|
||||
self._tx_count = 0
|
||||
self._tx_fail = 0
|
||||
self._last_rx_ts = 0.0
|
||||
self._last_rx_id = None
|
||||
self._last_rx_text = ''
|
||||
self._last_tx_error = ''
|
||||
self._open()
|
||||
|
||||
def _open(self):
|
||||
if not _CAN_AVAILABLE:
|
||||
self._last_error = 'python-can not installed (pip install python-can)'
|
||||
return
|
||||
# Try CAN FD first (PCAN-USB FD), fall back to classic CAN
|
||||
for fd_mode in (True, False):
|
||||
try:
|
||||
kwargs = dict(channel=self._channel, interface='socketcan',
|
||||
bitrate=self._bitrate)
|
||||
if fd_mode:
|
||||
kwargs['fd'] = True
|
||||
self._bus = can.interface.Bus(**kwargs)
|
||||
self._available = True
|
||||
self._last_error = ''
|
||||
mode_str = 'FD' if fd_mode else 'classic'
|
||||
print(f'[CAN] opened {self._channel} @ {self._bitrate//1000} kbps ({mode_str})')
|
||||
# Start receive thread
|
||||
self._rx_buf = {}
|
||||
t = threading.Thread(target=self._rx_loop, daemon=True,
|
||||
name='can_rx')
|
||||
t.start()
|
||||
self._rx_thread = t
|
||||
return
|
||||
except Exception as e:
|
||||
if fd_mode:
|
||||
continue # try classic CAN
|
||||
self._last_error = str(e)
|
||||
self._available = False
|
||||
print(f'[CAN] open failed: {e}')
|
||||
print(f'[CAN] hint: sudo ip link set {self._channel} up type can bitrate {self._bitrate}')
|
||||
|
||||
def _rx_loop(self):
|
||||
"""Background thread: receive CAN frames, reassemble JSON, log event."""
|
||||
buf = {} # arbitration_id → accumulated bytes
|
||||
while self._available and self._bus:
|
||||
try:
|
||||
msg = self._bus.recv(timeout=1.0)
|
||||
if msg is None:
|
||||
continue
|
||||
aid = msg.arbitration_id
|
||||
chunk = bytes(msg.data)
|
||||
|
||||
# Special handling for control frames (ID=0x75): process immediately as raw
|
||||
if aid == 0x75:
|
||||
self._on_raw_received(aid, chunk)
|
||||
continue
|
||||
|
||||
# Accumulate bytes for this CAN ID (for JSON reassembly)
|
||||
buf.setdefault(aid, b'')
|
||||
buf[aid] += chunk
|
||||
|
||||
# Check for null terminator (end of JSON string)
|
||||
if b'\x00' in buf[aid]:
|
||||
raw = buf[aid].split(b'\x00', 1)[0]
|
||||
buf[aid] = b''
|
||||
try:
|
||||
text = raw.decode('utf-8').strip()
|
||||
if text:
|
||||
self._on_json_received(aid, text)
|
||||
except Exception:
|
||||
self._on_raw_received(aid, raw)
|
||||
# Safety: discard if buffer grows too large (no null after 256 bytes)
|
||||
elif len(buf[aid]) > 256:
|
||||
self._on_raw_received(aid, buf[aid])
|
||||
buf[aid] = b''
|
||||
except Exception:
|
||||
break
|
||||
|
||||
def _on_json_received(self, can_id, text):
|
||||
"""Called when a complete JSON string arrives from CAN bus."""
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except Exception:
|
||||
# Not valid JSON — might be a single-frame message without null pad
|
||||
# Try treating raw text as-is if it looks like our format
|
||||
data = {'raw': text}
|
||||
|
||||
evt_class = data.get('class', data.get('type', '?'))
|
||||
evt_level = data.get('level', '?')
|
||||
ts = tw_str()
|
||||
print(f' [CHANNEL-C RX] CAN id=0x{can_id:03X} {text}')
|
||||
|
||||
entry = {
|
||||
'server_time': ts,
|
||||
'channel': 'CAN',
|
||||
'source': 'can',
|
||||
'can_id': f'0x{can_id:03X}',
|
||||
'response_type': 'violation',
|
||||
'content': {
|
||||
'id': str(int(time.time() * 1000)),
|
||||
'date': ts,
|
||||
'type': evt_class,
|
||||
'level': evt_level,
|
||||
}
|
||||
}
|
||||
self._rx_count += 1
|
||||
self._last_rx_ts = time.time()
|
||||
self._last_rx_id = can_id
|
||||
self._last_rx_text = text[:160]
|
||||
with events_lock:
|
||||
events.append(entry)
|
||||
if len(events) > 200:
|
||||
del events[:-200]
|
||||
|
||||
def _on_raw_received(self, can_id, raw: bytes):
|
||||
hx = ' '.join(f'{b:02X}' for b in raw[:32])
|
||||
ts = tw_str()
|
||||
|
||||
# Control frame detection (ID=0x75): throttle command — add to event list for display
|
||||
if can_id == 0x75 and len(raw) >= 1:
|
||||
level = raw[0]
|
||||
status = '关闭油门' if level == 0 else f'油门={level}'
|
||||
display = f'CAN id=0x{can_id:03X} {status}'
|
||||
print(f' [CHANNEL-C RX] {display}')
|
||||
self._rx_count += 1
|
||||
self._last_rx_ts = time.time()
|
||||
self._last_rx_id = can_id
|
||||
self._last_rx_text = display
|
||||
|
||||
# Add to event list for display
|
||||
entry = {
|
||||
'server_time': ts,
|
||||
'channel': 'CAN',
|
||||
'source': 'can',
|
||||
'can_id': f'0x{can_id:03X}',
|
||||
'response_type': 'throttle',
|
||||
'content': {
|
||||
'id': str(int(time.time() * 1000)),
|
||||
'date': ts,
|
||||
'type': 'throttle',
|
||||
'level': level,
|
||||
'status': status,
|
||||
}
|
||||
}
|
||||
with events_lock:
|
||||
events.append(entry)
|
||||
if len(events) > 200:
|
||||
del events[:-200]
|
||||
return
|
||||
|
||||
# Other CAN IDs: log as raw data, also add to event list
|
||||
display = f'RAW [{hx}]'
|
||||
print(f' [CHANNEL-C RX-RAW] CAN id=0x{can_id:03X} {display}')
|
||||
|
||||
self._rx_count += 1
|
||||
self._last_rx_ts = time.time()
|
||||
self._last_rx_id = can_id
|
||||
self._last_rx_text = display
|
||||
|
||||
entry = {
|
||||
'server_time': ts,
|
||||
'channel': 'CAN',
|
||||
'source': 'can',
|
||||
'can_id': f'0x{can_id:03X}',
|
||||
'response_type': 'can_raw',
|
||||
'content': {
|
||||
'id': str(int(time.time() * 1000)),
|
||||
'date': ts,
|
||||
'type': 'can_raw',
|
||||
'level': 0,
|
||||
'hex': hx,
|
||||
}
|
||||
}
|
||||
with events_lock:
|
||||
events.append(entry)
|
||||
if len(events) > 200:
|
||||
del events[:-200]
|
||||
|
||||
def reopen(self, channel=None, bitrate=None):
|
||||
with self._lock:
|
||||
if self._bus:
|
||||
try: self._bus.shutdown()
|
||||
except: pass
|
||||
self._bus = None
|
||||
if channel: self._channel = channel
|
||||
if bitrate: self._bitrate = int(bitrate)
|
||||
self._open()
|
||||
return self.status()
|
||||
|
||||
def send(self, can_id: int, data: bytes) -> dict:
|
||||
with self._lock:
|
||||
if not self._available or not self._bus:
|
||||
return {'ok': False, 'error': self._last_error or 'CAN not available'}
|
||||
try:
|
||||
# Keep TX compatible with classic CAN: split long payloads into
|
||||
# 8-byte frames (same framing idea as device-side JSON TX).
|
||||
frames = 0
|
||||
off = 0
|
||||
while off < len(data):
|
||||
chunk = data[off:off+8]
|
||||
msg = can.Message(
|
||||
arbitration_id=can_id,
|
||||
data=chunk,
|
||||
is_extended_id=False,
|
||||
is_fd=False,
|
||||
)
|
||||
self._bus.send(msg)
|
||||
frames += 1
|
||||
off += len(chunk)
|
||||
if len(data) > 8:
|
||||
time.sleep(0.001)
|
||||
|
||||
self._tx_count += frames
|
||||
self._last_tx_error = ''
|
||||
return {'ok': True, 'frames': frames}
|
||||
except Exception as e:
|
||||
self._last_error = str(e)
|
||||
self._last_tx_error = str(e)
|
||||
self._tx_fail += 1
|
||||
return {'ok': False, 'error': str(e)}
|
||||
|
||||
def check(self, can_id: int = 0x120) -> dict:
|
||||
# Short probe payload so status checks are valid on classic CAN too.
|
||||
payload = b'\xA5'
|
||||
tx = self.send(can_id, payload)
|
||||
st = self.status()
|
||||
st.update({
|
||||
'tx_probe_ok': bool(tx.get('ok')),
|
||||
'tx_probe_error': tx.get('error', ''),
|
||||
})
|
||||
return st
|
||||
|
||||
def status(self) -> dict:
|
||||
now = time.time()
|
||||
last_rx_age = None
|
||||
if self._last_rx_ts > 0:
|
||||
last_rx_age = round(now - self._last_rx_ts, 3)
|
||||
return {
|
||||
'available': self._available,
|
||||
'channel': self._channel,
|
||||
'bitrate': self._bitrate,
|
||||
'last_error': self._last_error,
|
||||
'lib': _CAN_AVAILABLE,
|
||||
'rx_count': self._rx_count,
|
||||
'tx_count': self._tx_count,
|
||||
'tx_fail': self._tx_fail,
|
||||
'last_rx_age_sec': last_rx_age,
|
||||
'last_rx_id': (f'0x{self._last_rx_id:03X}' if self._last_rx_id is not None else ''),
|
||||
'last_rx_text': self._last_rx_text,
|
||||
'last_tx_error': self._last_tx_error,
|
||||
}
|
||||
|
||||
|
||||
_can_bus = CanBus()
|
||||
|
||||
TZ_TW = datetime.timezone(datetime.timedelta(hours=8))
|
||||
def now_tw(): return datetime.datetime.now(TZ_TW)
|
||||
def tw_str(dt=None):
|
||||
@ -64,6 +361,14 @@ class Handler(BaseHTTPRequestHandler):
|
||||
with events_lock:
|
||||
self._json(list(events))
|
||||
|
||||
elif path == '/api/can/status':
|
||||
self._json(_can_bus.status())
|
||||
|
||||
elif path == '/api/can/check':
|
||||
qs = parse_qs(urlparse(self.path).query)
|
||||
can_id = int((qs.get('can_id') or ['0x120'])[0], 0)
|
||||
self._json(_can_bus.check(can_id=can_id))
|
||||
|
||||
elif path == '/api/files':
|
||||
files = []
|
||||
for name in sorted(os.listdir(UPLOAD_DIR), reverse=True):
|
||||
@ -169,8 +474,15 @@ class Handler(BaseHTTPRequestHandler):
|
||||
data = json.loads(body.decode('utf-8'))
|
||||
entry = {
|
||||
'server_time': tw_str(),
|
||||
'source': 'http',
|
||||
'channel': 'HTTP',
|
||||
}
|
||||
entry.update(data)
|
||||
# Normalize source/channel even if caller passes custom fields.
|
||||
if not entry.get('source'):
|
||||
entry['source'] = 'http'
|
||||
if not entry.get('channel'):
|
||||
entry['channel'] = 'HTTP'
|
||||
with events_lock:
|
||||
events.append(entry)
|
||||
if len(events) > 200:
|
||||
@ -183,6 +495,89 @@ class Handler(BaseHTTPRequestHandler):
|
||||
print(f" [CHANNEL-A] parse error: {e}")
|
||||
self.send_error(400, str(e))
|
||||
|
||||
# ── Channel C: CAN bus frame send ────────────────────────────
|
||||
elif path == '/api/can/send':
|
||||
try:
|
||||
req = json.loads(body.decode('utf-8'))
|
||||
evt_type = req.get('type', 'hazard')
|
||||
evt_level = int(req.get('level', 0))
|
||||
can_id = int(req.get('can_id', 0x100))
|
||||
# Same JSON payload as BLE/iPad channel
|
||||
payload = f'{{"class":"{evt_type}","level":{evt_level}}}'
|
||||
result = _can_bus.send(can_id, payload.encode('utf-8'))
|
||||
if result['ok']:
|
||||
print(f" [CHANNEL-C] CAN id=0x{can_id:03X} {payload}")
|
||||
else:
|
||||
print(f" [CHANNEL-C] CAN FAIL: {result['error']}")
|
||||
self._json(result)
|
||||
except Exception as e:
|
||||
self._json({'ok': False, 'error': str(e)})
|
||||
|
||||
elif path == '/api/can/send_cmd':
|
||||
try:
|
||||
req = json.loads(body.decode('utf-8'))
|
||||
cmd = int(req.get('cmd', 0)) & 0xFF
|
||||
can_id = int(req.get('can_id', 0x75))
|
||||
payload = bytes([cmd, 0, 0, 0, 0, 0, 0, 0])
|
||||
result = _can_bus.send(can_id, payload)
|
||||
if result.get('ok'):
|
||||
print(f' [CHANNEL-C TX-CMD] CAN id=0x{can_id:03X} cmd=0x{cmd:02X}')
|
||||
ts = tw_str()
|
||||
entry = {
|
||||
'server_time': ts,
|
||||
'channel': 'CAN',
|
||||
'source': 'can',
|
||||
'can_id': f'0x{can_id:03X}',
|
||||
'response_type': 'can_tx_cmd',
|
||||
'content': {
|
||||
'id': str(int(time.time() * 1000)),
|
||||
'date': ts,
|
||||
'type': 'can_tx_cmd',
|
||||
'level': cmd,
|
||||
'hex': f'{cmd:02X} 00 00 00 00 00 00 00',
|
||||
}
|
||||
}
|
||||
with events_lock:
|
||||
events.append(entry)
|
||||
if len(events) > 200:
|
||||
del events[:-200]
|
||||
self._json(result)
|
||||
except Exception as e:
|
||||
self._json({'ok': False, 'error': str(e)})
|
||||
|
||||
elif path == '/api/can/config':
|
||||
try:
|
||||
req = json.loads(body.decode('utf-8'))
|
||||
result = _can_bus.reopen(
|
||||
channel=req.get('channel'),
|
||||
bitrate=req.get('bitrate'),
|
||||
)
|
||||
self._json(result)
|
||||
except Exception as e:
|
||||
self._json({'ok': False, 'error': str(e)})
|
||||
|
||||
elif path == '/api/can/bringup':
|
||||
import subprocess
|
||||
try:
|
||||
req = json.loads(body.decode('utf-8'))
|
||||
ch = req.get('channel', 'can0')
|
||||
br = int(req.get('bitrate', 250000))
|
||||
# Bring the interface down first, then up with the requested bitrate
|
||||
subprocess.run(['sudo', 'ip', 'link', 'set', ch, 'down'], check=False)
|
||||
r = subprocess.run(
|
||||
['sudo', 'ip', 'link', 'set', ch, 'up', 'type', 'can', 'bitrate', str(br)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if r.returncode == 0:
|
||||
print(f'[CAN] brought up {ch} @ {br//1000} kbps')
|
||||
self._json({'ok': True})
|
||||
else:
|
||||
err = (r.stderr or r.stdout).strip()
|
||||
print(f'[CAN] bring-up failed: {err}')
|
||||
self._json({'ok': False, 'error': err})
|
||||
except Exception as e:
|
||||
self._json({'ok': False, 'error': str(e)})
|
||||
|
||||
# ── Channel B: tar.gz archive upload ─────────────────────────
|
||||
elif path in ('/api/upload', '/api/golf.cgi'):
|
||||
# 1) kCurl sends filename as query param: /api/upload?filename=xxx
|
||||
@ -235,6 +630,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', content_type + '; charset=utf-8')
|
||||
self.send_header('Content-Length', str(len(data)))
|
||||
# Prevent browser from caching HTML so edits are always picked up
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
self.send_header('Pragma', 'no-cache')
|
||||
self._cors()
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
@ -251,7 +650,16 @@ if __name__ == '__main__':
|
||||
print(f" Event API : POST http://localhost:{PORT}/api/event")
|
||||
print(f" Upload API : POST http://localhost:{PORT}/api/upload")
|
||||
print(f" Golf API : POST http://localhost:{PORT}/api/golf.cgi")
|
||||
print(f" CAN status : GET http://localhost:{PORT}/api/can/status")
|
||||
print(f" CAN send : POST http://localhost:{PORT}/api/can/send")
|
||||
print(f" Uploads : {UPLOAD_DIR}")
|
||||
st = _can_bus.status()
|
||||
if st['available']:
|
||||
print(f" CAN bus : {st['channel']} @ {st['bitrate']//1000} kbps [OK]")
|
||||
else:
|
||||
print(f" CAN bus : {st.get('last_error','unavailable')} [OFFLINE]")
|
||||
if st['lib']:
|
||||
print(f" fix: sudo ip link set can0 up type can bitrate 250000")
|
||||
print("=" * 55)
|
||||
try:
|
||||
server.serve_forever()
|
||||
|
||||
@ -17,6 +17,14 @@ header {
|
||||
.dot-live { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 8px #4ade80; flex-shrink: 0; }
|
||||
header h1 { font-size: 1rem; font-weight: 600; color: #fff; }
|
||||
#utc-clock { margin-left: auto; font-size: 0.78rem; color: #555d7a; font-family: monospace; }
|
||||
#can-status-badge {
|
||||
font-size: 0.68rem; font-weight: 700; padding: 2px 9px;
|
||||
border-radius: 10px; letter-spacing: 0.04em; cursor: pointer;
|
||||
border: none; background: #1e2130; color: #555d7a;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#can-status-badge.ok { background: #14312a; color: #4ade80; }
|
||||
#can-status-badge.err { background: #2a1a1a; color: #f87171; }
|
||||
|
||||
/* ── Layout ── */
|
||||
.layout { display: grid; grid-template-columns: 1fr 1fr; height: calc(100vh - 49px); }
|
||||
@ -104,6 +112,7 @@ header h1 { font-size: 1rem; font-weight: 600; color: #fff; }
|
||||
.log-row.lv2 { border-color: #fb923c; }
|
||||
.log-row.lv3 { border-color: #f87171; }
|
||||
.log-row.single { border-color: #a78bfa; }
|
||||
.log-row.throttle { border-color: #22d3ee; }
|
||||
.log-time { color: #555d7a; font-family: monospace; white-space: nowrap; flex-shrink: 0; }
|
||||
.log-type { font-weight: 600; color: #dde1f0; }
|
||||
.log-sub { color: #555d7a; }
|
||||
@ -112,6 +121,7 @@ header h1 { font-size: 1rem; font-weight: 600; color: #fff; }
|
||||
.test-bar {
|
||||
border-top: 1px solid #1e2130; padding: 8px 12px;
|
||||
background: #0e1018; flex-shrink: 0;
|
||||
max-height: 45vh; overflow-y: auto;
|
||||
}
|
||||
details summary {
|
||||
font-size: 0.68rem; color: #3a3f55; cursor: pointer; user-select: none;
|
||||
@ -133,6 +143,31 @@ details[open] summary::before { transform: rotate(90deg); }
|
||||
.b-hz { background:#60a5fa;color:#000; }
|
||||
.b-pe { background:#a78bfa;color:#fff; }
|
||||
.b-up { background:#1e2130;color:#60a5fa;border:1px solid #2a3050; }
|
||||
.b-can { background:#1a2b2f;color:#22d3ee;border:1px solid #164e63; }
|
||||
|
||||
/* ── CAN config row ── */
|
||||
.can-cfg {
|
||||
display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap;
|
||||
}
|
||||
.can-cfg label { font-size: 0.68rem; color: #555d7a; white-space: nowrap; }
|
||||
.can-cfg input {
|
||||
background: #1a1d29; border: 1px solid #252836; border-radius: 4px;
|
||||
color: #dde1f0; font-size: 0.72rem; padding: 3px 6px; width: 80px;
|
||||
}
|
||||
.can-toggle {
|
||||
display: flex; align-items: center; gap: 6px; margin-top: 8px;
|
||||
}
|
||||
.can-toggle input[type=checkbox] { accent-color: #22d3ee; width: 14px; height: 14px; cursor: pointer; }
|
||||
.can-toggle label { font-size: 0.72rem; color: #8891b0; cursor: pointer; }
|
||||
.can-diag {
|
||||
margin-top: 8px; padding: 8px;
|
||||
border: 1px solid #1e2130; border-radius: 6px;
|
||||
background: #10131d; font-size: 0.68rem; color: #8ea0bf;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.can-diag .k { color: #5f7093; }
|
||||
.can-diag .v { color: #c7d5ef; }
|
||||
|
||||
/* ── File list (Channel B) ── */
|
||||
.file-scroll { flex: 1; overflow-y: auto; padding: 12px; }
|
||||
@ -194,6 +229,7 @@ details[open] summary::before { transform: rotate(90deg); }
|
||||
<header>
|
||||
<div class="dot-live"></div>
|
||||
<h1>KL630 Golf Event Monitor</h1>
|
||||
<button id="can-status-badge" onclick="toggleCanConfig()" title="CAN bus 狀態 / 設定">CAN ···</button>
|
||||
<div id="utc-clock">—</div>
|
||||
</header>
|
||||
|
||||
@ -237,18 +273,67 @@ details[open] summary::before { transform: rotate(90deg); }
|
||||
</div>
|
||||
|
||||
<div class="test-bar">
|
||||
<details>
|
||||
<summary>測試送出</summary>
|
||||
<details open>
|
||||
<summary>指令</summary>
|
||||
<div class="btn-row">
|
||||
<button class="btn b-l1" onclick="sendEvt('grass',1)">草地 L1</button>
|
||||
<button class="btn b-l2" onclick="sendEvt('grass',2)">草地 L2</button>
|
||||
<button class="btn b-l3" onclick="sendEvt('grass',3)">草地 L3</button>
|
||||
<button class="btn b-l0" onclick="sendEvt('grass',0)">解除</button>
|
||||
<button class="btn b-hz" onclick="sendEvt('bunker',1)">沙坑</button>
|
||||
<button class="btn b-hz" onclick="sendEvt('pond',1)">水池</button>
|
||||
<button class="btn b-hz" onclick="sendEvt('tree',1)">樹木</button>
|
||||
<button class="btn b-pe" onclick="sendEvt('person',1)">行人</button>
|
||||
<button class="btn b-l1" onclick="sendCan('road',1)">道路 Lv1</button>
|
||||
<button class="btn b-l2" onclick="sendCan('road',2)">道路 Lv2</button>
|
||||
<button class="btn b-l3" onclick="sendCan('road',3)">道路 Lv3</button>
|
||||
<button class="btn b-l0" onclick="sendCan('road',0)">道路 解除</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn b-l1" onclick="sendCan('grass',1)">草地 Lv1</button>
|
||||
<button class="btn b-l2" onclick="sendCan('grass',2)">草地 Lv2</button>
|
||||
<button class="btn b-l3" onclick="sendCan('grass',3)">草地 Lv3</button>
|
||||
<button class="btn b-l0" onclick="sendCan('grass',0)">草地 解除</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn b-hz" onclick="sendCan('hazard',1)">危險 Lv1</button>
|
||||
<button class="btn b-hz" onclick="sendCan('hazard',2)">危險 Lv2</button>
|
||||
<button class="btn b-hz" onclick="sendCan('hazard',3)">危險 Lv3</button>
|
||||
<button class="btn b-l0" onclick="sendCan('hazard',0)">危險 解除</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn b-pe" onclick="sendCan('person',1)">行人偵測</button>
|
||||
<button class="btn b-l1" onclick="sendCan('bunker',1)">沙坑</button>
|
||||
<button class="btn b-l1" onclick="sendCan('pond',1)">水池</button>
|
||||
<button class="btn b-l1" onclick="sendCan('tree',1)">樹木</button>
|
||||
<button class="btn b-l1" onclick="sendCan('car',1)">車輛</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn b-can" onclick="sendCanCmd(0x00)">油門 關閉</button>
|
||||
<button class="btn b-can" onclick="sendCanCmd(0x20)">油門 32</button>
|
||||
<button class="btn b-can" onclick="sendCanCmd(0x40)">油門 64</button>
|
||||
<button class="btn b-can" onclick="sendCanCmd(0x80)">油門 128</button>
|
||||
<button class="btn b-can" onclick="sendCanCmd(0xFF)">油門 全開</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>CAN 設定</summary>
|
||||
|
||||
<!-- CAN config (always shown) -->
|
||||
<div id="can-cfg-row" class="can-cfg" style="display:block">
|
||||
<label>Channel</label>
|
||||
<input id="can-channel" value="can0" title="SocketCAN 介面名稱">
|
||||
<label>Bitrate</label>
|
||||
<input id="can-bitrate" value="250000" title="CAN 速率 bps">
|
||||
<label>CAN ID (hex)</label>
|
||||
<input id="can-id-input" value="100" title="11-bit CAN frame ID">
|
||||
<button class="btn b-can" onclick="applyCanCfg()">Apply</button>
|
||||
<button class="btn b-can" onclick="bringUpCan()" title="sudo ip link set canX up type can bitrate XXXXX">Bring Up</button>
|
||||
<button class="btn b-can" onclick="checkCanNow()">CAN 檢查</button>
|
||||
<button class="btn b-can" onclick="sendCanProbe()">測試送包</button>
|
||||
</div>
|
||||
|
||||
<div id="can-diag" class="can-diag" style="display:block">
|
||||
<div><span class="k">status</span>: <span class="v" id="diag-status">-</span></div>
|
||||
<div><span class="k">rx_count</span>: <span class="v" id="diag-rx">0</span> <span class="k">tx_count</span>: <span class="v" id="diag-tx">0</span> <span class="k">tx_fail</span>: <span class="v" id="diag-txf">0</span></div>
|
||||
<div><span class="k">last_rx</span>: <span class="v" id="diag-last-rx">-</span></div>
|
||||
<div><span class="k">last_tx_err</span>: <span class="v" id="diag-last-tx-err">-</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
@ -264,15 +349,6 @@ details[open] summary::before { transform: rotate(90deg); }
|
||||
<div class="file-count" id="file-count">尚未收到上傳</div>
|
||||
<div id="file-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-bar">
|
||||
<details>
|
||||
<summary>測試送出</summary>
|
||||
<div class="btn-row">
|
||||
<button class="btn b-up" onclick="sendFake()">產生假 tar.gz 並上傳</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -286,7 +362,7 @@ details[open] summary::before { transform: rotate(90deg); }
|
||||
|
||||
<script>
|
||||
// ── State ──────────────────────────────────────────────────────────────────
|
||||
const TYPE_LABEL = { grass:'草地違規', bunker:'沙坑', pond:'水池', tree:'樹木', person:'行人偵測' };
|
||||
const TYPE_LABEL = { grass:'草地違規', bunker:'沙坑', pond:'水池', tree:'樹木', person:'行人偵測', can_raw:'CAN 原始資料', can_tx_cmd:'CAN 控制輸出', throttle:'油門控制' };
|
||||
const SINGLE = ['bunker','pond','tree','person'];
|
||||
let knownCount = 0, knownFiles = [], eidCounter = Date.now();
|
||||
|
||||
@ -319,13 +395,23 @@ function render(evts) {
|
||||
log.innerHTML = [...evts].reverse().map(ev => {
|
||||
const c = ev.content || {};
|
||||
const ts = (ev.server_time||'').slice(11,19);
|
||||
const lbl= TYPE_LABEL[c.type] || c.type || '?';
|
||||
const lbl = TYPE_LABEL[c.type] || c.type || '?';
|
||||
const isSingle = SINGLE.includes(c.type);
|
||||
const cls = isSingle ? 'single' : `lv${c.level ?? ''}`;
|
||||
const sub = isSingle ? '單次紀錄' : `Level ${c.level}`;
|
||||
const isThrottle = c.type === 'throttle';
|
||||
const cls = isThrottle ? 'throttle' : (isSingle ? 'single' : `lv${c.level ?? ''}`);
|
||||
let sub = (c.type === 'can_raw' || c.type === 'can_tx_cmd')
|
||||
? (`RAW ${c.hex || ''}`)
|
||||
: isThrottle
|
||||
? (c.status || `throttle=${c.level}`)
|
||||
: (isSingle ? '單次紀錄' : `Level ${c.level}`);
|
||||
const src = (ev.source || '').toLowerCase();
|
||||
const isCan = src === 'can' || ev.channel === 'CAN';
|
||||
const sourceBadge = isCan
|
||||
? `<span style="font-size:0.65rem;background:#164e63;color:#22d3ee;border-radius:4px;padding:1px 5px;margin-left:4px">CAN</span>`
|
||||
: `<span style="font-size:0.65rem;background:#1a3254;color:#60a5fa;border-radius:4px;padding:1px 5px;margin-left:4px">HTTP</span>`;
|
||||
return `<div class="log-row ${cls}">
|
||||
<span class="log-time">${ts}</span>
|
||||
<span class="log-type">${lbl}</span>
|
||||
<span class="log-type">${lbl}</span>${sourceBadge}
|
||||
<span class="log-sub">— ${sub}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@ -335,6 +421,7 @@ function updateCard(ev) {
|
||||
const c = ev.content || {};
|
||||
const lbl = TYPE_LABEL[c.type] || c.type || '—';
|
||||
const isSingle = SINGLE.includes(c.type);
|
||||
const isThrottle = c.type === 'throttle';
|
||||
const el = document.getElementById('evt-type');
|
||||
el.textContent = lbl;
|
||||
el.className = '';
|
||||
@ -342,7 +429,10 @@ function updateCard(ev) {
|
||||
// reset all steps
|
||||
['s1','s2','s3','s0'].forEach(id => document.getElementById(id).className = 'lv-step');
|
||||
|
||||
if (isSingle) {
|
||||
if (isThrottle) {
|
||||
// Throttle just shows the label, no level steps
|
||||
// Could optionally show a simple indicator
|
||||
} else if (isSingle) {
|
||||
document.getElementById('s1').classList.add('single');
|
||||
} else {
|
||||
const lv = c.level ?? -1;
|
||||
@ -368,7 +458,7 @@ async function renderFiles(files) {
|
||||
document.getElementById('file-list').innerHTML = files.map(f => `
|
||||
<div class="file-card" style="flex-direction:column;align-items:stretch">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div class="file-icon">📦</div>
|
||||
<div class="file-icon"><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD></div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${f.name}</div>
|
||||
<div class="file-meta">${f.mtime} · ${(f.size/1024).toFixed(1)} KB</div>
|
||||
@ -432,37 +522,111 @@ function openLB(src, name) {
|
||||
}
|
||||
function closeLB() { document.getElementById('lightbox').classList.remove('open'); }
|
||||
|
||||
// ── CAN bus helpers ───────────────────────────────────────────────────
|
||||
function canId() { return parseInt(document.getElementById('can-id-input').value, 16) || 0x100; }
|
||||
|
||||
async function pollCanStatus() {
|
||||
try {
|
||||
const s = await fetch('/api/can/status').then(r=>r.json());
|
||||
const badge = document.getElementById('can-status-badge');
|
||||
if (s.available) {
|
||||
badge.textContent = `CAN ${s.channel} ${s.bitrate/1000}k`;
|
||||
badge.className = 'ok';
|
||||
} else {
|
||||
badge.textContent = 'CAN offline';
|
||||
badge.className = 'err';
|
||||
}
|
||||
renderCanDiag(s);
|
||||
return s;
|
||||
} catch(e) {
|
||||
document.getElementById('can-status-badge').className = 'err';
|
||||
document.getElementById('can-status-badge').textContent = 'CAN ?';
|
||||
}
|
||||
}
|
||||
|
||||
function renderCanDiag(s) {
|
||||
const diag = document.getElementById('can-diag');
|
||||
if (!diag) return;
|
||||
const status = s.available ? `online ${s.channel} ${s.bitrate/1000}k` : `offline (${s.last_error || 'unknown'})`;
|
||||
document.getElementById('diag-status').textContent = status;
|
||||
document.getElementById('diag-rx').textContent = String(s.rx_count ?? 0);
|
||||
document.getElementById('diag-tx').textContent = String(s.tx_count ?? 0);
|
||||
document.getElementById('diag-txf').textContent = String(s.tx_fail ?? 0);
|
||||
const age = (s.last_rx_age_sec == null) ? '-' : `${s.last_rx_age_sec}s ago`;
|
||||
const rxLine = s.last_rx_id ? `${s.last_rx_id} ${age} ${s.last_rx_text || ''}` : age;
|
||||
document.getElementById('diag-last-rx').textContent = rxLine;
|
||||
document.getElementById('diag-last-tx-err').textContent = s.last_tx_error || '-';
|
||||
}
|
||||
|
||||
|
||||
async function applyCanCfg() {
|
||||
const ch = document.getElementById('can-channel').value.trim();
|
||||
const br = parseInt(document.getElementById('can-bitrate').value) || 250000;
|
||||
const r = await fetch('/api/can/config', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({channel: ch, bitrate: br})
|
||||
}).then(r=>r.json());
|
||||
await pollCanStatus();
|
||||
if (!r.available) alert('CAN config: ' + (r.last_error || 'failed'));
|
||||
}
|
||||
|
||||
async function bringUpCan() {
|
||||
const ch = document.getElementById('can-channel').value.trim() || 'can0';
|
||||
const br = parseInt(document.getElementById('can-bitrate').value) || 250000;
|
||||
// Ask server to run the ip link command
|
||||
const r = await fetch('/api/can/bringup', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({channel: ch, bitrate: br})
|
||||
}).then(r=>r.json()).catch(e=>({ok:false,error:String(e)}));
|
||||
if (r.ok) { await new Promise(res=>setTimeout(res, 800)); await applyCanCfg(); }
|
||||
else alert('bring-up failed: ' + (r.error||'?') + '\nRun manually:\nsudo ip link set ' + ch + ' up type can bitrate ' + br);
|
||||
}
|
||||
|
||||
async function sendCan(type, level) {
|
||||
const r = await fetch('/api/can/send', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ type, level, can_id: canId() })
|
||||
}).then(r=>r.json()).catch(e=>({ok:false,error:String(e)}));
|
||||
const inline = document.getElementById('can-inline-status');
|
||||
if (r.ok) {
|
||||
if (inline) inline.textContent = `✓ ${type}:${level}`;
|
||||
} else {
|
||||
if (inline) inline.textContent = '✗ ' + (r.error||'error');
|
||||
await pollCanStatus(); // refresh badge immediately on error
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCanCmd(cmd) {
|
||||
const r = await fetch('/api/can/send_cmd', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ cmd, can_id: 0x75 })
|
||||
}).then(r=>r.json()).catch(e=>({ok:false,error:String(e)}));
|
||||
await pollCanStatus();
|
||||
}
|
||||
|
||||
async function sendCanProbe() {
|
||||
const can_id = canId();
|
||||
const r = await fetch('/api/can/send', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ type:'diag', level:9, can_id })
|
||||
}).then(r=>r.json()).catch(e=>({ok:false,error:String(e)}));
|
||||
await pollCanStatus();
|
||||
}
|
||||
|
||||
async function checkCanNow() {
|
||||
const can_id = canId();
|
||||
const r = await fetch(`/api/can/check?can_id=0x${can_id.toString(16)}`).then(r=>r.json()).catch(e=>({available:false,last_error:String(e)}));
|
||||
renderCanDiag(r);
|
||||
}
|
||||
|
||||
// ── Test helpers ──────────────────────────────────────────────────────
|
||||
function nowISO() { return new Date().toISOString().replace(/\.\d+Z$/,'Z'); }
|
||||
|
||||
async function sendEvt(type, level) {
|
||||
await fetch('/api/event', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ response_type:'violation',
|
||||
content:{ id: String(++eidCounter), date: nowISO(), type, level }})
|
||||
});
|
||||
await poll();
|
||||
}
|
||||
|
||||
async function sendFake() {
|
||||
const id = String(Date.now());
|
||||
const body = JSON.stringify({ id, date:nowISO(), type:'grass', max_level:3, duration_sec:15,
|
||||
images:['level1.jpg','level2.jpg','level3.jpg'] }, null, 2);
|
||||
const ts = nowISO().replace(/[:.TZ]/g,'').slice(0,15);
|
||||
await fetch('/api/upload', {
|
||||
method:'POST',
|
||||
headers:{ 'Content-Type':'application/gzip',
|
||||
'Content-Disposition':`attachment; filename="event_${id}_${ts}.tar.gz"` },
|
||||
body: new Blob([body])
|
||||
});
|
||||
await poll();
|
||||
}
|
||||
|
||||
// ── Boot ────────────────────────────────────────────────────────────
|
||||
setInterval(poll, 1000);
|
||||
setInterval(pollTime, 5000);
|
||||
poll(); pollTime();
|
||||
setInterval(pollCanStatus, 3000);
|
||||
poll(); pollTime(); pollCanStatus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
90
web_serve.py
90
web_serve.py
@ -413,6 +413,55 @@ def _verify_firmware_running(tn):
|
||||
ok = any("FW PID:" in ln for ln in lines)
|
||||
return ok, lines
|
||||
|
||||
def _spi_setup_on_device(tn, spi_node="spi1.0"):
|
||||
"""Try to switch SPI node from display driver to spidev and report status."""
|
||||
steps = [
|
||||
("SPI preflight: listing SPI devices...",
|
||||
"ls -l /sys/bus/spi/devices 2>/dev/null || true", 10),
|
||||
("SPI preflight: current modalias...",
|
||||
f"cat /sys/bus/spi/devices/{spi_node}/modalias 2>/dev/null || echo 'modalias missing'", 10),
|
||||
("SPI preflight: current driver link...",
|
||||
f"readlink /sys/bus/spi/devices/{spi_node}/driver 2>/dev/null || echo 'driver link missing'", 10),
|
||||
("SPI setup: unbind current driver...",
|
||||
f"DRV=$(basename $(readlink /sys/bus/spi/devices/{spi_node}/driver 2>/dev/null) 2>/dev/null || true); "
|
||||
f"echo DRIVER=$DRV; "
|
||||
f"[ -n \"$DRV\" ] && echo {spi_node} > /sys/bus/spi/drivers/$DRV/unbind 2>/dev/null || true; "
|
||||
"echo 'unbind done'", 12),
|
||||
("SPI setup: set driver_override=spidev (if supported)...",
|
||||
f"if [ -e /sys/bus/spi/devices/{spi_node}/driver_override ]; then "
|
||||
f"echo spidev > /sys/bus/spi/devices/{spi_node}/driver_override && echo 'override OK'; "
|
||||
"else echo 'driver_override missing'; fi", 10),
|
||||
("SPI setup: bind spidev...",
|
||||
f"echo {spi_node} > /sys/bus/spi/drivers/spidev/bind 2>/dev/null || true; echo 'bind done'", 10),
|
||||
("SPI verify: modalias + driver + /dev/spidev*",
|
||||
f"echo 'modalias:'; cat /sys/bus/spi/devices/{spi_node}/modalias 2>/dev/null || true; "
|
||||
f"echo 'driver:'; readlink /sys/bus/spi/devices/{spi_node}/driver 2>/dev/null || true; "
|
||||
"echo '/dev:'; ls -l /dev/spidev* 2>/dev/null || echo 'no /dev/spidev'", 12),
|
||||
]
|
||||
|
||||
out = []
|
||||
for label, cmd, timeout in steps:
|
||||
out.append(("log", label))
|
||||
out.append(("prompt", f"$ {cmd}"))
|
||||
for ln in _telnet_run(tn, cmd, timeout=timeout):
|
||||
kind = "ok"
|
||||
low = ln.lower()
|
||||
if "missing" in low or "no /dev/spidev" in low:
|
||||
kind = "warn"
|
||||
out.append((kind, ln))
|
||||
|
||||
# Final health hint
|
||||
final = _telnet_run(
|
||||
tn,
|
||||
f"M=$(cat /sys/bus/spi/devices/{spi_node}/modalias 2>/dev/null || true); "
|
||||
"echo FINAL_MODALIAS=$M; "
|
||||
"echo $M | grep -q dh2228fv && echo 'SPI_BINDING_BLOCKED' || echo 'SPI_BINDING_OK'",
|
||||
timeout=10,
|
||||
)
|
||||
for ln in final:
|
||||
out.append(("warn" if "BLOCKED" in ln else "ok", ln))
|
||||
return out
|
||||
|
||||
# ── API: deploy via Telnet (SSE) ──────────────────────────────────────────────
|
||||
@app.route("/api/deploy/run")
|
||||
def api_deploy_run():
|
||||
@ -422,6 +471,7 @@ def api_deploy_run():
|
||||
port = int(request.args.get("port", cfg["port"]))
|
||||
out_rtsp = request.args.get("out_rtsp", "1") == "1"
|
||||
out_hdmi = request.args.get("out_hdmi", "0") == "1"
|
||||
spi_fix = request.args.get("spi_fix", "1") == "1"
|
||||
base = f"http://{h_ip}:{port}"
|
||||
bd = BIN_DIR_DEVICE
|
||||
fw = FW_PATH_DEVICE
|
||||
@ -484,6 +534,12 @@ def api_deploy_run():
|
||||
tn = _telnet_connect(kl_ip)
|
||||
_drain_shell(tn)
|
||||
yield sse(f"Connected to {kl_ip}", "ok")
|
||||
|
||||
if spi_fix:
|
||||
yield sse("Running SPI setup preflight (spi1.0 -> spidev)...")
|
||||
for kind, text in _spi_setup_on_device(tn, "spi1.0"):
|
||||
yield sse(text, kind)
|
||||
|
||||
for label, cmd, bg, timeout in steps:
|
||||
yield sse(label)
|
||||
yield sse(f"$ {cmd}", "prompt")
|
||||
@ -506,6 +562,30 @@ def api_deploy_run():
|
||||
return Response(generate(), mimetype="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
|
||||
# ── API: SPI setup via Telnet (SSE) ─────────────────────────────────────────
|
||||
@app.route("/api/spi_setup/run")
|
||||
def api_spi_setup_run():
|
||||
cfg = load_config()
|
||||
kl_ip = request.args.get("ip", cfg["kl630_ip"])
|
||||
spi_node = request.args.get("node", "spi1.0")
|
||||
|
||||
def generate():
|
||||
yield sse(f"Connecting to {kl_ip}:23 via Telnet...")
|
||||
try:
|
||||
tn = _telnet_connect(kl_ip)
|
||||
_drain_shell(tn)
|
||||
yield sse("Connected.", "ok")
|
||||
for kind, text in _spi_setup_on_device(tn, spi_node):
|
||||
yield sse(text, kind)
|
||||
tn.close()
|
||||
yield sse("SPI setup flow complete.", "ok")
|
||||
except Exception as e:
|
||||
yield sse(f"Telnet error: {e}", "error")
|
||||
yield sse_done()
|
||||
|
||||
return Response(generate(), mimetype="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
|
||||
# ── API: BT UART one-time baud setup via Telnet (SSE) ────────────────────────
|
||||
@app.route("/api/bt_setup/run")
|
||||
def api_bt_setup_run():
|
||||
@ -1617,6 +1697,10 @@ input:checked+.slider:before{transform:translateX(16px);background:#fff}
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/></svg>
|
||||
BT 初始化
|
||||
</button>
|
||||
<button class="btn btn-ghost" id="btn-spi-setup" onclick="runAction('spi_setup')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M7 7h10v10H7z"/><path d="M3 10h4M17 10h4M10 3v4M10 17v4"/></svg>
|
||||
SPI 初始化
|
||||
</button>
|
||||
<button class="btn btn-ghost" onclick="clearLog()">Clear Log</button>
|
||||
<button class="btn btn-ghost" id="btn-autostart" onclick="runAction('autostart')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
|
||||
@ -1770,16 +1854,18 @@ function runAction(action) {
|
||||
deploy: '/api/deploy/run?ip=' + encodeURIComponent(cfg.kl630_ip) +
|
||||
'&host_ip=' + encodeURIComponent(cfg.host_ip) +
|
||||
'&port=' + encodeURIComponent(cfg.port) +
|
||||
'&spi_fix=1' +
|
||||
'&out_rtsp=' + (document.getElementById('out-rtsp').checked ? 1 : 0) +
|
||||
'&out_hdmi=' + (document.getElementById('out-hdmi').checked ? 1 : 0),
|
||||
bt_setup: '/api/bt_setup/run?ip=' + encodeURIComponent(cfg.kl630_ip),
|
||||
spi_setup: '/api/spi_setup/run?ip=' + encodeURIComponent(cfg.kl630_ip) + '&node=spi1.0',
|
||||
autostart: '/api/autostart/write?ip=' + encodeURIComponent(cfg.kl630_ip) +
|
||||
'&out_rtsp=' + (document.getElementById('out-rtsp').checked ? 1 : 0) +
|
||||
'&out_hdmi=' + (document.getElementById('out-hdmi').checked ? 1 : 0),
|
||||
autostart_read: '/api/autostart/read?ip=' + encodeURIComponent(cfg.kl630_ip),
|
||||
mount_sd: '/api/mount_sd?ip=' + encodeURIComponent(cfg.kl630_ip),
|
||||
};
|
||||
const labels = { compile:'Compile', deploy:'Deploy to KL630', bt_setup:'BT 初始化', autostart:'Write Autostart', autostart_read:'Read Autostart', mount_sd:'Mount SD' };
|
||||
const labels = { compile:'Compile', deploy:'Deploy to KL630', bt_setup:'BT 初始化', spi_setup:'SPI 初始化', autostart:'Write Autostart', autostart_read:'Read Autostart', mount_sd:'Mount SD' };
|
||||
|
||||
appendLog('\\n── ' + labels[action] + ' ' + '─'.repeat(40), 'prompt');
|
||||
setLogStatus('Running...');
|
||||
@ -1875,7 +1961,7 @@ function appendLog(text, kind) {
|
||||
}
|
||||
function clearLog() { document.getElementById('log').textContent = ''; setLogStatus(''); }
|
||||
function setLogStatus(s) { document.getElementById('log-status').textContent = s; }
|
||||
function setBtns(disabled){ ['btn-compile','btn-deploy','btn-bt-setup','btn-autostart','btn-autostart-read','btn-mount-sd','btn-model-apply'].forEach(id => document.getElementById(id).disabled = disabled); }
|
||||
function setBtns(disabled){ ['btn-compile','btn-deploy','btn-bt-setup','btn-spi-setup','btn-autostart','btn-autostart-read','btn-mount-sd','btn-model-apply'].forEach(id => document.getElementById(id).disabled = disabled); }
|
||||
|
||||
// ── Terminal ──────────────────────────────────────────────────────────────────
|
||||
// by mars
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user